├── favicon.ico ├── README.md ├── src ├── audio │ ├── song.mp3 │ ├── song.webm │ ├── test.mp3 │ ├── test.webm │ ├── doorOpen.mp3 │ ├── knocking.mp3 │ ├── lightHum.mp3 │ ├── outside.mp3 │ ├── outside.webm │ ├── doorOpen.webm │ ├── footstep1.mp3 │ ├── footstep1.webm │ ├── footstep2.mp3 │ ├── footstep2.webm │ ├── footstep3.mp3 │ ├── footstep3.webm │ ├── footstep4.mp3 │ ├── footstep4.webm │ ├── footstep5.mp3 │ ├── footstep5.webm │ ├── itemPickup.mp3 │ ├── itemPickup.webm │ ├── knocking.webm │ ├── lightHum.webm │ ├── slidingDoorClose.mp3 │ ├── slidingDoorOpen.mp3 │ ├── slidingDoorOpen.webm │ └── slidingDoorClose.webm ├── assets │ ├── items │ │ ├── note.png │ │ ├── apple.png │ │ ├── bread.png │ │ ├── stand.png │ │ ├── pickaxe.png │ │ ├── redbull.png │ │ └── barrelItem.png │ ├── objects │ │ ├── elmo.png │ │ ├── barrel.png │ │ ├── radio.png │ │ ├── redbull.png │ │ └── table.png │ ├── skyBoxes │ │ └── sky.jpg │ ├── walls │ │ ├── wall1.png │ │ ├── wall2.png │ │ ├── wall3.png │ │ ├── wall4.png │ │ ├── doubleDoorOpen.png │ │ ├── doubleDoor2Open.png │ │ ├── doubleDoorClosed.png │ │ └── doubleDoor2Closed.png │ ├── floors │ │ ├── floor1.png │ │ ├── floor2.png │ │ ├── floor3.png │ │ ├── floor4.png │ │ ├── floor5.png │ │ └── floor6.png │ ├── thinWalls │ │ ├── fence.png │ │ └── slidingDoor.png │ ├── ceilings │ │ ├── ceiling1.png │ │ └── ceiling2.png │ ├── paintings │ │ ├── shield.png │ │ ├── painting1.png │ │ ├── painting10.png │ │ ├── painting11.png │ │ ├── painting12.png │ │ ├── painting13.png │ │ ├── painting14.png │ │ ├── painting15.png │ │ ├── painting8.png │ │ ├── painting9.png │ │ ├── painting2one.png │ │ ├── painting2two.png │ │ ├── painting3one.png │ │ ├── painting3two.png │ │ ├── painting4one.png │ │ ├── painting4two.png │ │ ├── painting5one.png │ │ ├── painting5two.png │ │ ├── painting6one.png │ │ ├── painting6two.png │ │ ├── painting7one.png │ │ ├── painting7two.png │ │ └── bloodyHandprint.png │ └── lightSources │ │ ├── chandelier.png │ │ ├── hangingLight.png │ │ ├── hangingLight2.png │ │ └── standingLight.png ├── index.js ├── audio.js ├── hud.js ├── actions.js ├── maps.js └── engine.js ├── package.json ├── utils └── calc.js ├── index.html └── index.css /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtkyber/raycaster-game/HEAD/favicon.ico -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### Note 2 | If trying on Chrome, make sure "vulkan" is disabled in chrome://flags 3 | -------------------------------------------------------------------------------- /src/audio/song.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtkyber/raycaster-game/HEAD/src/audio/song.mp3 -------------------------------------------------------------------------------- /src/audio/song.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtkyber/raycaster-game/HEAD/src/audio/song.webm -------------------------------------------------------------------------------- /src/audio/test.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtkyber/raycaster-game/HEAD/src/audio/test.mp3 -------------------------------------------------------------------------------- /src/audio/test.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtkyber/raycaster-game/HEAD/src/audio/test.webm -------------------------------------------------------------------------------- /src/audio/doorOpen.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtkyber/raycaster-game/HEAD/src/audio/doorOpen.mp3 -------------------------------------------------------------------------------- /src/audio/knocking.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtkyber/raycaster-game/HEAD/src/audio/knocking.mp3 -------------------------------------------------------------------------------- /src/audio/lightHum.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtkyber/raycaster-game/HEAD/src/audio/lightHum.mp3 -------------------------------------------------------------------------------- /src/audio/outside.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtkyber/raycaster-game/HEAD/src/audio/outside.mp3 -------------------------------------------------------------------------------- /src/audio/outside.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtkyber/raycaster-game/HEAD/src/audio/outside.webm -------------------------------------------------------------------------------- /src/assets/items/note.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtkyber/raycaster-game/HEAD/src/assets/items/note.png -------------------------------------------------------------------------------- /src/audio/doorOpen.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtkyber/raycaster-game/HEAD/src/audio/doorOpen.webm -------------------------------------------------------------------------------- /src/audio/footstep1.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtkyber/raycaster-game/HEAD/src/audio/footstep1.mp3 -------------------------------------------------------------------------------- /src/audio/footstep1.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtkyber/raycaster-game/HEAD/src/audio/footstep1.webm -------------------------------------------------------------------------------- /src/audio/footstep2.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtkyber/raycaster-game/HEAD/src/audio/footstep2.mp3 -------------------------------------------------------------------------------- /src/audio/footstep2.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtkyber/raycaster-game/HEAD/src/audio/footstep2.webm -------------------------------------------------------------------------------- /src/audio/footstep3.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtkyber/raycaster-game/HEAD/src/audio/footstep3.mp3 -------------------------------------------------------------------------------- /src/audio/footstep3.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtkyber/raycaster-game/HEAD/src/audio/footstep3.webm -------------------------------------------------------------------------------- /src/audio/footstep4.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtkyber/raycaster-game/HEAD/src/audio/footstep4.mp3 -------------------------------------------------------------------------------- /src/audio/footstep4.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtkyber/raycaster-game/HEAD/src/audio/footstep4.webm -------------------------------------------------------------------------------- /src/audio/footstep5.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtkyber/raycaster-game/HEAD/src/audio/footstep5.mp3 -------------------------------------------------------------------------------- /src/audio/footstep5.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtkyber/raycaster-game/HEAD/src/audio/footstep5.webm -------------------------------------------------------------------------------- /src/audio/itemPickup.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtkyber/raycaster-game/HEAD/src/audio/itemPickup.mp3 -------------------------------------------------------------------------------- /src/audio/itemPickup.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtkyber/raycaster-game/HEAD/src/audio/itemPickup.webm -------------------------------------------------------------------------------- /src/audio/knocking.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtkyber/raycaster-game/HEAD/src/audio/knocking.webm -------------------------------------------------------------------------------- /src/audio/lightHum.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtkyber/raycaster-game/HEAD/src/audio/lightHum.webm -------------------------------------------------------------------------------- /src/assets/items/apple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtkyber/raycaster-game/HEAD/src/assets/items/apple.png -------------------------------------------------------------------------------- /src/assets/items/bread.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtkyber/raycaster-game/HEAD/src/assets/items/bread.png -------------------------------------------------------------------------------- /src/assets/items/stand.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtkyber/raycaster-game/HEAD/src/assets/items/stand.png -------------------------------------------------------------------------------- /src/assets/objects/elmo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtkyber/raycaster-game/HEAD/src/assets/objects/elmo.png -------------------------------------------------------------------------------- /src/assets/skyBoxes/sky.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtkyber/raycaster-game/HEAD/src/assets/skyBoxes/sky.jpg -------------------------------------------------------------------------------- /src/assets/walls/wall1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtkyber/raycaster-game/HEAD/src/assets/walls/wall1.png -------------------------------------------------------------------------------- /src/assets/walls/wall2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtkyber/raycaster-game/HEAD/src/assets/walls/wall2.png -------------------------------------------------------------------------------- /src/assets/walls/wall3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtkyber/raycaster-game/HEAD/src/assets/walls/wall3.png -------------------------------------------------------------------------------- /src/assets/walls/wall4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtkyber/raycaster-game/HEAD/src/assets/walls/wall4.png -------------------------------------------------------------------------------- /src/assets/floors/floor1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtkyber/raycaster-game/HEAD/src/assets/floors/floor1.png -------------------------------------------------------------------------------- /src/assets/floors/floor2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtkyber/raycaster-game/HEAD/src/assets/floors/floor2.png -------------------------------------------------------------------------------- /src/assets/floors/floor3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtkyber/raycaster-game/HEAD/src/assets/floors/floor3.png -------------------------------------------------------------------------------- /src/assets/floors/floor4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtkyber/raycaster-game/HEAD/src/assets/floors/floor4.png -------------------------------------------------------------------------------- /src/assets/floors/floor5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtkyber/raycaster-game/HEAD/src/assets/floors/floor5.png -------------------------------------------------------------------------------- /src/assets/floors/floor6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtkyber/raycaster-game/HEAD/src/assets/floors/floor6.png -------------------------------------------------------------------------------- /src/assets/items/pickaxe.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtkyber/raycaster-game/HEAD/src/assets/items/pickaxe.png -------------------------------------------------------------------------------- /src/assets/items/redbull.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtkyber/raycaster-game/HEAD/src/assets/items/redbull.png -------------------------------------------------------------------------------- /src/assets/objects/barrel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtkyber/raycaster-game/HEAD/src/assets/objects/barrel.png -------------------------------------------------------------------------------- /src/assets/objects/radio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtkyber/raycaster-game/HEAD/src/assets/objects/radio.png -------------------------------------------------------------------------------- /src/assets/objects/redbull.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtkyber/raycaster-game/HEAD/src/assets/objects/redbull.png -------------------------------------------------------------------------------- /src/assets/objects/table.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtkyber/raycaster-game/HEAD/src/assets/objects/table.png -------------------------------------------------------------------------------- /src/assets/thinWalls/fence.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtkyber/raycaster-game/HEAD/src/assets/thinWalls/fence.png -------------------------------------------------------------------------------- /src/audio/slidingDoorClose.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtkyber/raycaster-game/HEAD/src/audio/slidingDoorClose.mp3 -------------------------------------------------------------------------------- /src/audio/slidingDoorOpen.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtkyber/raycaster-game/HEAD/src/audio/slidingDoorOpen.mp3 -------------------------------------------------------------------------------- /src/audio/slidingDoorOpen.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtkyber/raycaster-game/HEAD/src/audio/slidingDoorOpen.webm -------------------------------------------------------------------------------- /src/assets/ceilings/ceiling1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtkyber/raycaster-game/HEAD/src/assets/ceilings/ceiling1.png -------------------------------------------------------------------------------- /src/assets/ceilings/ceiling2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtkyber/raycaster-game/HEAD/src/assets/ceilings/ceiling2.png -------------------------------------------------------------------------------- /src/assets/items/barrelItem.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtkyber/raycaster-game/HEAD/src/assets/items/barrelItem.png -------------------------------------------------------------------------------- /src/assets/paintings/shield.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtkyber/raycaster-game/HEAD/src/assets/paintings/shield.png -------------------------------------------------------------------------------- /src/audio/slidingDoorClose.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtkyber/raycaster-game/HEAD/src/audio/slidingDoorClose.webm -------------------------------------------------------------------------------- /src/assets/paintings/painting1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtkyber/raycaster-game/HEAD/src/assets/paintings/painting1.png -------------------------------------------------------------------------------- /src/assets/paintings/painting10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtkyber/raycaster-game/HEAD/src/assets/paintings/painting10.png -------------------------------------------------------------------------------- /src/assets/paintings/painting11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtkyber/raycaster-game/HEAD/src/assets/paintings/painting11.png -------------------------------------------------------------------------------- /src/assets/paintings/painting12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtkyber/raycaster-game/HEAD/src/assets/paintings/painting12.png -------------------------------------------------------------------------------- /src/assets/paintings/painting13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtkyber/raycaster-game/HEAD/src/assets/paintings/painting13.png -------------------------------------------------------------------------------- /src/assets/paintings/painting14.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtkyber/raycaster-game/HEAD/src/assets/paintings/painting14.png -------------------------------------------------------------------------------- /src/assets/paintings/painting15.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtkyber/raycaster-game/HEAD/src/assets/paintings/painting15.png -------------------------------------------------------------------------------- /src/assets/paintings/painting8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtkyber/raycaster-game/HEAD/src/assets/paintings/painting8.png -------------------------------------------------------------------------------- /src/assets/paintings/painting9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtkyber/raycaster-game/HEAD/src/assets/paintings/painting9.png -------------------------------------------------------------------------------- /src/assets/walls/doubleDoorOpen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtkyber/raycaster-game/HEAD/src/assets/walls/doubleDoorOpen.png -------------------------------------------------------------------------------- /src/assets/paintings/painting2one.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtkyber/raycaster-game/HEAD/src/assets/paintings/painting2one.png -------------------------------------------------------------------------------- /src/assets/paintings/painting2two.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtkyber/raycaster-game/HEAD/src/assets/paintings/painting2two.png -------------------------------------------------------------------------------- /src/assets/paintings/painting3one.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtkyber/raycaster-game/HEAD/src/assets/paintings/painting3one.png -------------------------------------------------------------------------------- /src/assets/paintings/painting3two.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtkyber/raycaster-game/HEAD/src/assets/paintings/painting3two.png -------------------------------------------------------------------------------- /src/assets/paintings/painting4one.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtkyber/raycaster-game/HEAD/src/assets/paintings/painting4one.png -------------------------------------------------------------------------------- /src/assets/paintings/painting4two.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtkyber/raycaster-game/HEAD/src/assets/paintings/painting4two.png -------------------------------------------------------------------------------- /src/assets/paintings/painting5one.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtkyber/raycaster-game/HEAD/src/assets/paintings/painting5one.png -------------------------------------------------------------------------------- /src/assets/paintings/painting5two.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtkyber/raycaster-game/HEAD/src/assets/paintings/painting5two.png -------------------------------------------------------------------------------- /src/assets/paintings/painting6one.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtkyber/raycaster-game/HEAD/src/assets/paintings/painting6one.png -------------------------------------------------------------------------------- /src/assets/paintings/painting6two.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtkyber/raycaster-game/HEAD/src/assets/paintings/painting6two.png -------------------------------------------------------------------------------- /src/assets/paintings/painting7one.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtkyber/raycaster-game/HEAD/src/assets/paintings/painting7one.png -------------------------------------------------------------------------------- /src/assets/paintings/painting7two.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtkyber/raycaster-game/HEAD/src/assets/paintings/painting7two.png -------------------------------------------------------------------------------- /src/assets/thinWalls/slidingDoor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtkyber/raycaster-game/HEAD/src/assets/thinWalls/slidingDoor.png -------------------------------------------------------------------------------- /src/assets/walls/doubleDoor2Open.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtkyber/raycaster-game/HEAD/src/assets/walls/doubleDoor2Open.png -------------------------------------------------------------------------------- /src/assets/walls/doubleDoorClosed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtkyber/raycaster-game/HEAD/src/assets/walls/doubleDoorClosed.png -------------------------------------------------------------------------------- /src/assets/lightSources/chandelier.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtkyber/raycaster-game/HEAD/src/assets/lightSources/chandelier.png -------------------------------------------------------------------------------- /src/assets/lightSources/hangingLight.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtkyber/raycaster-game/HEAD/src/assets/lightSources/hangingLight.png -------------------------------------------------------------------------------- /src/assets/paintings/bloodyHandprint.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtkyber/raycaster-game/HEAD/src/assets/paintings/bloodyHandprint.png -------------------------------------------------------------------------------- /src/assets/walls/doubleDoor2Closed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtkyber/raycaster-game/HEAD/src/assets/walls/doubleDoor2Closed.png -------------------------------------------------------------------------------- /src/assets/lightSources/hangingLight2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtkyber/raycaster-game/HEAD/src/assets/lightSources/hangingLight2.png -------------------------------------------------------------------------------- /src/assets/lightSources/standingLight.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtkyber/raycaster-game/HEAD/src/assets/lightSources/standingLight.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "raycaster-game", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "howler": "^2.2.4" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /utils/calc.js: -------------------------------------------------------------------------------- 1 | export function degToRad(deg) { 2 | return deg * (Math.PI / 180); 3 | } 4 | 5 | export function radToDeg(rad) { 6 | return rad * (180 / Math.PI); 7 | } 8 | 9 | export function getIntersection(x, y, r, theta, x1, y1, x2, y2, p4) { 10 | const x3 = x; 11 | const y3 = y; 12 | let x4; 13 | let y4; 14 | let uMax = Infinity; 15 | 16 | if (p4?.x && p4?.y) { 17 | x4 = p4.x; 18 | y4 = p4.y; 19 | uMax = 1; 20 | } else { 21 | x4 = x + r * Math.cos(theta); 22 | y4 = y + r * Math.sin(theta); 23 | } 24 | 25 | const denom = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4); 26 | 27 | if (denom == 0) { 28 | return; 29 | } 30 | 31 | const t = ((x1 - x3) * (y3 - y4) - (y1 - y3) * (x3 - x4)) / denom; 32 | const u = ((x1 - x3) * (y1 - y2) - (y1 - y3) * (x1 - x2)) / denom; 33 | 34 | if (t > 0 && t < 1 && u > 0 && u <= uMax) { 35 | const px = x3 + u * (x4 - x3); 36 | const py = y3 + u * (y4 - y3); 37 | return [px, py]; 38 | } else { 39 | return; 40 | } 41 | } 42 | 43 | export function convertDeg0To360(deg) { 44 | return ((deg % 360) + 360) % 360; 45 | } 46 | 47 | export function getPerpCoords(playerX, playerY, x, y, halfLen) { 48 | const slope = (y - playerY) / (x - playerX); 49 | const perpSlope = -(1 / slope); 50 | const angle = Math.atan(perpSlope); 51 | const x1 = x + halfLen * Math.cos(angle); 52 | const y1 = y + halfLen * Math.sin(angle); 53 | const x2 = x - halfLen * Math.cos(angle); 54 | const y2 = y - halfLen * Math.sin(angle); 55 | return [x1, y1, x2, y2]; 56 | } 57 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Ray Casting Engine 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 |
17 |

Processing Lighting Effects

18 |
This data will be saved until the next update
19 |
20 |
21 |

0%

22 |
23 |
24 |
25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /index.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | box-sizing: border-box; 5 | -webkit-user-select: none; 6 | -moz-user-select: none; 7 | -ms-user-select: none; 8 | user-select: none; 9 | font-family: 'Gill Sans', 'Gill Sans MT', Calibri, 'Trebuchet MS', sans-serif; 10 | } 11 | 12 | body { 13 | width: 100vw; 14 | height: 100vh; 15 | background-color: rgba(0, 0, 0, 1); 16 | overflow: hidden; 17 | } 18 | 19 | .container { 20 | width: 100%; 21 | height: 100%; 22 | display: flex; 23 | flex-flow: column nowrap; 24 | justify-content: center; 25 | align-items: center; 26 | gap: 1rem; 27 | } 28 | 29 | .loadingContainer { 30 | position: fixed; 31 | top: 0; 32 | left: 0; 33 | z-index: 1000; 34 | width: 100vw; 35 | height: 100vh; 36 | display: none; 37 | justify-content: center; 38 | align-items: center; 39 | flex-flow: column nowrap; 40 | } 41 | .loadingText { 42 | color: white; 43 | font-weight: 200; 44 | } 45 | .loadingTextSmall { 46 | color: white; 47 | font-weight: 800; 48 | } 49 | .loadingBar { 50 | position: relative; 51 | margin-top: 1rem; 52 | border: 1px solid white; 53 | background-color: rgba(255, 255, 255, 0.4); 54 | width: 10rem; 55 | height: 1.5rem; 56 | border-radius: 0.3rem; 57 | overflow: hidden; 58 | } 59 | .loadingFill { 60 | position: absolute; 61 | left: 0; 62 | top: 0; 63 | background-color: white; 64 | width: 0; 65 | height: 100%; 66 | z-index: -1; 67 | } 68 | .loadingValue { 69 | width: 100%; 70 | height: 100%; 71 | display: flex; 72 | justify-content: center; 73 | align-items: center; 74 | color: black; 75 | } 76 | 77 | #canvas { 78 | position: relative; 79 | /* border: 1px solid rgba(255, 255, 255, 0.4); */ 80 | height: 50%; 81 | width: auto; 82 | background-color: black; 83 | box-shadow: 0.5rem 0.5rem 1.5rem 0 rgba(0, 0, 0, 0.5); 84 | z-index: 2; 85 | margin: 0rem; 86 | aspect-ratio: 16/9; 87 | display: block; 88 | } 89 | #canvas.fullscreen { 90 | border: none; 91 | box-shadow: none; 92 | max-width: 1920px; 93 | max-height: 1080px; 94 | } 95 | 96 | #debugCanvas { 97 | position: relative; 98 | background-color: rgba(0, 0, 0, 1); 99 | box-shadow: 0.5rem 0.5rem 1.5rem 0 rgba(0, 0, 0, 0.5); 100 | z-index: 1; 101 | display: block; 102 | height: 50%; 103 | width: auto; 104 | } 105 | 106 | @media (min-aspect-ratio: 16/9) { 107 | #canvas.fullscreen { 108 | width: auto; 109 | height: 100%; 110 | } 111 | } 112 | @media (max-aspect-ratio: 16/9) { 113 | #canvas.fullscreen { 114 | width: 100%; 115 | height: auto; 116 | } 117 | } 118 | @media (min-aspect-ratio: 2/1) { 119 | .container { 120 | flex-flow: row nowrap; 121 | justify-content: center; 122 | margin-top: 0; 123 | } 124 | 125 | #canvas { 126 | width: 70%; 127 | height: auto; 128 | } 129 | 130 | #debugCanvas { 131 | width: 30%; 132 | height: auto; 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import Actions from './actions.js'; 2 | import Sound from './audio.js'; 3 | import Engine from './engine.js'; 4 | import Hud from './hud.js'; 5 | 6 | const request = window.indexedDB.open('RaycasterDB', 3); 7 | 8 | let audio; 9 | let engine; 10 | let hud; 11 | let actions; 12 | 13 | const fpsInterval = 1000 / 60; 14 | let animationFrameId; 15 | let deltaTime = 0; 16 | let timestamp = 0; 17 | 18 | const alterOffscreenCanvasPixels = () => { 19 | engine.update(); 20 | engine.ctx.putImageData(engine.offscreenCanvasPixels, 0, 0); 21 | }; 22 | 23 | const drawOntoCanvas = () => { 24 | hud.drawReticle(); 25 | engine.fade(); 26 | hud.drawFps(); 27 | if (engine.inventoryOpen) { 28 | hud.drawInventory(); 29 | if (hud.inventoryIndexSelected !== null) hud.drawSelectedInventoryItem(); 30 | } 31 | 32 | if (engine.consoleValues.length) hud.drawEngineConsole(engine.consoleValues); 33 | }; 34 | 35 | const gameLoop = () => { 36 | timestamp = Date.now(); 37 | 38 | if (engine.isCrouching) engine.fPlayerMoveSpeed *= 0.5; 39 | 40 | alterOffscreenCanvasPixels(); 41 | drawOntoCanvas(); 42 | actions.runNextFunction(); 43 | if (engine.inventoryOpen) hud.drawCursor(); 44 | else { 45 | hud.cursorX = engine.canvasWidth / 2; 46 | hud.cursorY = engine.canvasHeight / 2; 47 | hud.inventoryIndexSelected = null; 48 | hud.itemCanBePlaced = false; 49 | hud.itemOutsideOfInventory = false; 50 | } 51 | deltaTime = Date.now() - timestamp; 52 | 53 | engine.fGameSpeed = (deltaTime / fpsInterval) * 2.5; 54 | engine.fPlayerMoveSpeed = (deltaTime / fpsInterval) * 4; 55 | 56 | animationFrameId = requestAnimationFrame(gameLoop); 57 | }; 58 | 59 | const beginLoop = () => { 60 | gameLoop(); 61 | 62 | setInterval(() => { 63 | hud.framesCounted = ~~(1000 / deltaTime); 64 | }, 100); 65 | }; 66 | 67 | const setUp = async db => { 68 | audio = new Sound(db); 69 | engine = new Engine(audio, db); 70 | hud = new Hud(engine, audio, db); 71 | actions = new Actions(engine, audio, db); 72 | 73 | const promise = engine.init(); 74 | Promise.resolve(promise).then(() => { 75 | document.querySelector('.loadingContainer').remove(); 76 | actions.init(); 77 | // audio.init(); 78 | 79 | hud.frameRate = 0; 80 | 81 | beginLoop(); 82 | }); 83 | }; 84 | 85 | document.onmousemove = e => { 86 | if (engine?.inventoryOpen && engine?.userIsInTab) { 87 | const newX = hud.cursorX + e.movementX / 4; 88 | const newY = hud.cursorY + e.movementY / 4; 89 | 90 | if (newX >= 0 && newX <= engine.canvasWidth && newY >= 0 && newY <= engine.canvasHeight) { 91 | hud.cursorX += e.movementX / 3; 92 | hud.cursorY += e.movementY / 3; 93 | } 94 | } 95 | }; 96 | 97 | document.onmousedown = e => { 98 | if (engine?.inventoryOpen && engine?.userIsInTab) { 99 | if (hud.inventoryIndexSelected === null) hud.setItemSelected(); 100 | } 101 | }; 102 | 103 | document.onmouseup = e => { 104 | if (engine?.inventoryOpen && engine?.userIsInTab) { 105 | if (hud.inventoryIndexSelected !== null) hud.placeSelectedInventoryItem(); 106 | } 107 | }; 108 | 109 | request.onerror = e => { 110 | console.log(e.target.error); 111 | }; 112 | 113 | request.onupgradeneeded = e => { 114 | if (!e.target.result.objectStoreNames.contains('lighting')) { 115 | e.target.result.createObjectStore('lighting', { autoIncrement: true }); 116 | } 117 | if (!e.target.result.objectStoreNames.contains('lightingVersion')) { 118 | e.target.result.createObjectStore('lightingVersion', { autoIncrement: true }); 119 | } 120 | }; 121 | 122 | request.onsuccess = e => { 123 | setUp(e.target.result); 124 | }; 125 | -------------------------------------------------------------------------------- /src/audio.js: -------------------------------------------------------------------------------- 1 | import { convertDeg0To360, degToRad, radToDeg } from '../utils/calc.js'; 2 | import { maps } from './maps.js'; 3 | 4 | export default class Sound { 5 | constructor(db) { 6 | this.db = db; 7 | this.soundPaths = [ 8 | // './src/audio/song.mp3', 9 | './src/audio/song', 10 | './src/audio/outside', 11 | './src/audio/test', 12 | './src/audio/slidingDoorOpen', 13 | './src/audio/slidingDoorClose', 14 | './src/audio/footstep1', 15 | './src/audio/footstep2', 16 | './src/audio/footstep3', 17 | './src/audio/footstep4', 18 | './src/audio/footstep5', 19 | './src/audio/itemPickup', 20 | './src/audio/doorOpen', 21 | './src/audio/knocking', 22 | './src/audio/lightHum', 23 | ]; 24 | this.sounds = {}; 25 | this.soundsPlaying = []; 26 | } 27 | 28 | updateSoundPositions(pAng, px, py) { 29 | for (let i = 0; i < this.soundsPlaying.length; i++) { 30 | const name = this.soundsPlaying[i].name; 31 | if (this.soundsPlaying[i].hasPanning) { 32 | const x = this.soundsPlaying[i].x; 33 | const y = this.soundsPlaying[i].y; 34 | const dx = Math.abs(px - x); 35 | const dy = Math.abs(py - y); 36 | const d = Math.sqrt(dx * dx + dy * dy); 37 | 38 | let playerAng = convertDeg0To360(pAng); 39 | let sourceAngle = radToDeg(Math.atan2(py - y, px - x)) + 180; 40 | 41 | let angleDiff = ((sourceAngle - playerAng + 180 + 360) % 360) - 180; 42 | const angleDiffAbs = Math.abs(angleDiff); 43 | 44 | let newX = d * Math.cos(Math.PI / 2 - degToRad(angleDiffAbs)); 45 | if (angleDiff < 0) newX *= -1; 46 | newX; 47 | let newY = -30; 48 | let newZ = d; 49 | if (angleDiffAbs > 90) newZ = newZ + (newZ * angleDiffAbs) / 500; 50 | this.sounds[name].pos(newX, newY, newZ); 51 | } 52 | } 53 | } 54 | 55 | stopSound(name) { 56 | this.sounds[name].stop(); 57 | for (let i = 0; i < this.soundsPlaying.length; i++) { 58 | if (this.soundsPlaying[name] === name) this.soundsPlaying.splice(i, 1); 59 | } 60 | } 61 | 62 | playSound(name, x, y, hasPanning, i = '') { 63 | this.sounds[name + i].play(); 64 | this.soundsPlaying.push({ 65 | name: name + i, 66 | x, 67 | y, 68 | hasPanning, 69 | }); 70 | } 71 | 72 | init(i = 0) { 73 | Howler.unload(); 74 | this.sounds = {}; 75 | this.soundsPlaying = []; 76 | for (let j = 0; j < this.soundPaths.length; j++) { 77 | const name = this.soundPaths[j].split('/').pop()?.split('.')[0]; 78 | let rate = 1; 79 | let html5 = false; 80 | let autoplay = false; 81 | let volume = 1; 82 | let loop = false; 83 | 84 | if (name.includes('footstep')) { 85 | volume = 1.1; 86 | } else if (name.includes('itemPickup')) { 87 | volume = 0.4; 88 | } else if (name.includes('doorOpen')) { 89 | volume = 0.5; 90 | } else continue; 91 | 92 | this.sounds[name] = new Howl({ 93 | src: [`${this.soundPaths[j]}.webm`, `${this.soundPaths[j]}.mp3`], 94 | preload: true, 95 | html5: html5, 96 | autoplay: autoplay, 97 | loop: loop, 98 | volume: volume, 99 | rate: rate, 100 | maxDistance: 1000000, 101 | onend: () => { 102 | for (let k = 0; k < this.soundsPlaying.length; k++) { 103 | if (!loop && this.soundsPlaying[k].name === name) this.soundsPlaying.splice(k, 1); 104 | } 105 | }, 106 | }); 107 | this.sounds[name].pannerAttr({ 108 | ...this.sounds[name].pannerAttr(), 109 | distanceModel: 'exponential', 110 | rolloffFactor: 2, 111 | refDistance: 200, 112 | }); 113 | } 114 | 115 | for (let j = 0; j < maps[i].ambientAudio?.length; j++) { 116 | const name = maps[i].ambientAudio[j].name; 117 | let rate = 1; 118 | let html5 = false; 119 | let autoplay = true; 120 | let volume = 1; 121 | let loop = true; 122 | 123 | switch (name) { 124 | case 'outside': 125 | volume = 0.2; 126 | break; 127 | } 128 | 129 | this.sounds[name] = new Howl({ 130 | src: [`./src/audio/${name}.webm`, `./src/audio/${name}.mp3`], 131 | preload: true, 132 | html5: html5, 133 | autoplay: autoplay, 134 | loop: loop, 135 | volume: volume, 136 | rate: rate, 137 | maxDistance: 1000000, 138 | onend: () => { 139 | for (let k = 0; k < this.soundsPlaying.length; k++) { 140 | if (!loop && this.soundsPlaying[k].name === name) this.soundsPlaying.splice(k, 1); 141 | } 142 | }, 143 | }); 144 | this.sounds[name].pannerAttr({ 145 | ...this.sounds[name].pannerAttr(), 146 | distanceModel: 'exponential', 147 | rolloffFactor: 2, 148 | refDistance: 200, 149 | }); 150 | } 151 | 152 | for ( 153 | let j = 0; 154 | j < maps[i]?.objects.length + maps[i]?.thinWalls.length + maps[i]?.lightSources.length; 155 | j++ 156 | ) { 157 | let obj; 158 | let index; 159 | if (j < maps[i].objects.length) { 160 | index = j; 161 | obj = maps[i].objects[index]; 162 | } else if (j < maps[i].objects.length + maps[i].thinWalls.length) { 163 | index = j - maps[i]?.objects.length; 164 | obj = maps[i].thinWalls[index]; 165 | } else { 166 | index = j - maps[i]?.objects.length - maps[i]?.thinWalls.length; 167 | obj = maps[i].lightSources[index]; 168 | } 169 | 170 | if (obj?.sounds) { 171 | obj.sounds.forEach(fileName => { 172 | const name = fileName + index; 173 | let rate = 1; 174 | let html5 = false; 175 | let autoplay = false; 176 | let volume = 1; 177 | let loop = false; 178 | 179 | switch (fileName) { 180 | case 'slidingDoorOpen': 181 | rate = 1.7; 182 | break; 183 | case 'slidingDoorClose': 184 | rate = 1.7; 185 | break; 186 | case 'song': 187 | loop = true; 188 | volume = 0.5; 189 | break; 190 | case 'outside': 191 | loop = true; 192 | volume = 1; 193 | break; 194 | case 'knocking': 195 | loop = true; 196 | break; 197 | case 'lightHum': 198 | loop = true; 199 | volume = 0.1; 200 | break; 201 | default: 202 | break; 203 | } 204 | 205 | this.sounds[name] = new Howl({ 206 | src: [`./src/audio/${fileName}.webm`, `./src/audio/${fileName}.mp3`], 207 | preload: true, 208 | html5: html5, 209 | autoplay: autoplay, 210 | loop: loop, 211 | volume: volume, 212 | rate: rate, 213 | maxDistance: 1000000, 214 | onend: () => { 215 | for (let k = 0; k < this.soundsPlaying.length; k++) { 216 | if (!loop && this.soundsPlaying[k].name === name) this.soundsPlaying.splice(k, 1); 217 | } 218 | }, 219 | }); 220 | this.sounds[name].pannerAttr({ 221 | ...this.sounds[name].pannerAttr(), 222 | distanceModel: 'exponential', 223 | rolloffFactor: 2, 224 | refDistance: 200, 225 | }); 226 | }); 227 | } 228 | } 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /src/hud.js: -------------------------------------------------------------------------------- 1 | export default class Hud { 2 | constructor(engine, audio, db) { 3 | this.db = db; 4 | this.audio = audio; 5 | this.engine = engine; 6 | this.ctx = engine.ctx; 7 | this.canvasWidth = engine.canvasWidth; 8 | this.canvasHeight = engine.canvasHeight; 9 | this.canvasPixels = engine.ctx.getImageData(0, 0, engine.canvasWidth, engine.canvasWidth); 10 | this.showFps = true; 11 | this.framesCounted = 0; 12 | this.cursorX = this.canvasWidth / 2; 13 | this.cursorY = this.canvasHeight / 2; 14 | this.inventorySlotSize = ~~(this.canvasWidth / 20); 15 | this.inventoryIndexSelected = null; 16 | this.itemCanBePlaced = false; 17 | this.itemOutsideOfInventory = false; 18 | this.itemPlacementCol = 0; 19 | this.itemPlacementRow = 0; 20 | } 21 | 22 | drawCursor() { 23 | if (this.inventoryIndexSelected !== null) return; 24 | this.ctx.lineWidth = 2; 25 | this.ctx.strokeStyle = 'rgb(0, 0, 0)'; 26 | this.ctx.fillStyle = 'rgb(255, 255, 255)'; 27 | this.ctx.beginPath(); 28 | this.ctx.ellipse( 29 | this.cursorX, 30 | this.cursorY, 31 | this.canvasHeight / 200, 32 | this.canvasHeight / 200, 33 | 2 * Math.PI, 34 | 0, 35 | 2 * Math.PI 36 | ); 37 | this.ctx.stroke(); 38 | this.ctx.fill(); 39 | } 40 | 41 | drawFillRectangle(x, y, width, height, red, green, blue, alpha) { 42 | const bytesPerPixel = 4; 43 | let targetIndex = bytesPerPixel * this.engine.offscreenCanvasPixels.width * y + bytesPerPixel * x; 44 | for (let h = 0; h < height; h++) { 45 | for (let w = 0; w < width; w++) { 46 | this.engine.offscreenCanvasPixels.data[targetIndex] = red; 47 | this.engine.offscreenCanvasPixels.data[targetIndex + 1] = green; 48 | this.engine.offscreenCanvasPixels.data[targetIndex + 2] = blue; 49 | this.engine.offscreenCanvasPixels.data[targetIndex + 3] = alpha; 50 | targetIndex += bytesPerPixel; 51 | } 52 | targetIndex += bytesPerPixel * (this.engine.offscreenCanvasPixels.width - width); 53 | } 54 | } 55 | 56 | setItemSelected() { 57 | const inventory = this.engine.inventory; 58 | const inventoryW = this.engine.inventorySlotCols * this.inventorySlotSize; 59 | const inventoryH = this.engine.inventorySlotRows * this.inventorySlotSize; 60 | 61 | const inventoryStartX = this.canvasWidth / 2 - inventoryW / 2; 62 | const inventoryStartY = this.canvasHeight / 2 - inventoryH / 2; 63 | if ( 64 | this.cursorX < inventoryStartX || 65 | this.cursorX > inventoryStartX + inventoryW || 66 | this.cursorY < inventoryStartY || 67 | this.cursorY > inventoryStartY + inventoryH 68 | ) { 69 | return; 70 | } 71 | 72 | for (let i = 0; i < inventory.length; i++) { 73 | const xStart = inventoryStartX + inventory[i].slotIdStartCol * this.inventorySlotSize; 74 | const yStart = inventoryStartY + inventory[i].slotIdStartRow * this.inventorySlotSize; 75 | const xEnd = xStart + inventory[i].slotCols * this.inventorySlotSize; 76 | const yEnd = yStart + inventory[i].slotRows * this.inventorySlotSize; 77 | 78 | if (this.cursorX >= xStart && this.cursorX <= xEnd && this.cursorY >= yStart && this.cursorY <= yEnd) { 79 | this.inventoryIndexSelected = i; 80 | } 81 | } 82 | } 83 | 84 | drawSelectedInventoryItem() { 85 | const inventory = this.engine.inventory; 86 | const item = inventory[this.inventoryIndexSelected]; 87 | const img = this.engine.textures[item.name]; 88 | 89 | const inventoryW = this.engine.inventorySlotCols * this.inventorySlotSize; 90 | const inventoryH = this.engine.inventorySlotRows * this.inventorySlotSize; 91 | const inventoryStartX = this.canvasWidth / 2 - inventoryW / 2; 92 | const inventoryStartY = this.canvasHeight / 2 - inventoryH / 2; 93 | 94 | const slotCols = item.slotCols; 95 | const slotRows = item.slotRows; 96 | const scaleFactor = Math.min( 97 | (this.inventorySlotSize * slotCols) / img.width, 98 | (this.inventorySlotSize * slotRows) / img.height 99 | ); 100 | const newW = img.width * scaleFactor - 4; 101 | const newH = img.height * scaleFactor - 4; 102 | 103 | const colStartNew = Math.round((this.cursorX - inventoryStartX - newW / 2) / this.inventorySlotSize); 104 | const rowStartNew = Math.round((this.cursorY - inventoryStartY - newH / 2) / this.inventorySlotSize); 105 | const colEndNew = colStartNew + (item.slotCols - 1); 106 | const rowEndNew = rowStartNew + (item.slotRows - 1); 107 | 108 | let spaceFound = true; 109 | for (let i = 0; i < inventory.length; i++) { 110 | const endCol = inventory[i].slotIdStartCol + (inventory[i].slotCols - 1); 111 | const endRow = inventory[i].slotIdStartRow + (inventory[i].slotRows - 1); 112 | 113 | if ( 114 | i !== this.inventoryIndexSelected && 115 | colStartNew <= endCol && 116 | colEndNew >= inventory[i].slotIdStartCol && 117 | rowStartNew <= endRow && 118 | rowEndNew >= inventory[i].slotIdStartRow 119 | ) { 120 | spaceFound = false; 121 | } 122 | } 123 | 124 | const slotX = colStartNew * this.inventorySlotSize + inventoryStartX; 125 | const slotY = rowStartNew * this.inventorySlotSize + inventoryStartY; 126 | const slotXEnd = colEndNew * this.inventorySlotSize + inventoryStartX; 127 | const slotYEnd = rowEndNew * this.inventorySlotSize + inventoryStartY; 128 | 129 | this.itemCanBePlaced = false; 130 | if (spaceFound) { 131 | if ( 132 | slotX >= inventoryStartX && 133 | slotXEnd <= inventoryStartX + (inventoryW - this.inventorySlotSize) && 134 | slotY >= inventoryStartY && 135 | slotYEnd <= inventoryStartY + (inventoryH - this.inventorySlotSize) 136 | ) { 137 | this.itemPlacementCol = colStartNew; 138 | this.itemPlacementRow = rowStartNew; 139 | this.itemCanBePlaced = true; 140 | this.ctx.fillStyle = 'rgba(255, 255, 255, 0.3)'; 141 | this.ctx.beginPath(); 142 | this.ctx.fillRect(slotX, slotY, this.inventorySlotSize * slotCols, this.inventorySlotSize * slotRows); 143 | } else { 144 | this.itemOutsideOfInventory = true; 145 | 146 | this.ctx.font = `400 8px arial`; 147 | this.ctx.fillStyle = 'rgba(255, 255, 255, 0.5)'; 148 | this.ctx.textAlign = 'center'; 149 | this.ctx.textBaseline = 'center'; 150 | this.ctx.fillText('Drop', this.cursorX, this.cursorY - newH / 2 - 10); 151 | } 152 | } 153 | 154 | this.ctx.beginPath(); 155 | this.ctx.drawImage(img, this.cursorX - newW / 2, this.cursorY - newH / 2, newW, newH); 156 | } 157 | 158 | placeSelectedInventoryItem() { 159 | if (this.itemCanBePlaced) { 160 | this.engine.inventory[this.inventoryIndexSelected].slotIdStartCol = this.itemPlacementCol; 161 | this.engine.inventory[this.inventoryIndexSelected].slotIdStartRow = this.itemPlacementRow; 162 | this.itemCanBePlaced = false; 163 | } else if (this.itemOutsideOfInventory) { 164 | this.itemOutsideOfInventory = false; 165 | this.engine.items.push({ 166 | name: this.engine.inventory[this.inventoryIndexSelected].name, 167 | x: this.engine.fPlayerX, 168 | y: this.engine.fPlayerY, 169 | category: this.engine.inventory[this.inventoryIndexSelected].category, 170 | inReticle: this.engine.inventory[this.inventoryIndexSelected].inReticle, 171 | inventoryCols: this.engine.inventory[this.inventoryIndexSelected].slotCols, 172 | inventoryRows: this.engine.inventory[this.inventoryIndexSelected].slotRows, 173 | }); 174 | 175 | const img = this.engine.textures[this.engine.inventory[this.inventoryIndexSelected].name]; 176 | this.engine.fItemTextureBufferList.push(new OffscreenCanvas(img.width, img.height)); 177 | let lastIndex = this.engine.fItemTextureBufferList.length - 1; 178 | this.engine.fItemTextureBufferList[lastIndex].getContext('2d', { alpha: true }).drawImage(img, 0, 0); 179 | const imgData = this.engine.fItemTextureBufferList[lastIndex] 180 | .getContext('2d', { alpha: false }) 181 | .getImageData( 182 | 0, 183 | 0, 184 | this.engine.fItemTextureBufferList[lastIndex].width, 185 | this.engine.fItemTextureBufferList[lastIndex].height 186 | ); 187 | this.engine.fItemTexturePixelsList[lastIndex] = imgData.data; 188 | 189 | this.engine.inventory.splice(this.inventoryIndexSelected, 1); 190 | } 191 | this.inventoryIndexSelected = null; 192 | } 193 | 194 | drawInventory() { 195 | const inventory = this.engine.inventory; 196 | const ctx = this.ctx; 197 | const inventoryW = this.engine.inventorySlotCols * this.inventorySlotSize; 198 | const inventoryH = this.engine.inventorySlotRows * this.inventorySlotSize; 199 | const inventoryStartX = this.canvasWidth / 2 - inventoryW / 2; 200 | const inventoryStartY = this.canvasHeight / 2 - inventoryH / 2; 201 | 202 | ctx.strokeStyle = 'rgba(255, 255, 255, 0.5)'; 203 | ctx.lineWidth = 1; 204 | ctx.fillStyle = 'rgba(0, 0, 0, 0.4)'; 205 | ctx.beginPath(); 206 | ctx.fillRect(inventoryStartX, inventoryStartY, inventoryW, inventoryH); 207 | for (let i = 0; i < this.engine.inventorySlotCols; i++) { 208 | for (let j = 0; j < this.engine.inventorySlotRows; j++) { 209 | ctx.beginPath(); 210 | const slotX = i * this.inventorySlotSize + inventoryStartX; 211 | const slotY = j * this.inventorySlotSize + inventoryStartY; 212 | 213 | let slotFilled = false; 214 | for (let k = 0; k < inventory.length; k++) { 215 | if (k === this.inventoryIndexSelected) continue; 216 | if (inventory[k].slotIdStartCol === i && inventory[k].slotIdStartRow === j) { 217 | const img = this.engine.textures[inventory[k].name]; 218 | 219 | const slotCols = inventory[k].slotCols; 220 | const slotRows = inventory[k].slotRows; 221 | const scaleFactor = Math.min( 222 | (this.inventorySlotSize * slotCols) / img.width, 223 | (this.inventorySlotSize * slotRows) / img.height 224 | ); 225 | const newW = img.width * scaleFactor - 4; 226 | const newH = img.height * scaleFactor - 4; 227 | const x = slotX + (this.inventorySlotSize * slotCols) / 2 - newW / 2; 228 | const y = slotY + (this.inventorySlotSize * slotRows) / 2 - newH / 2; 229 | ctx.rect(slotX, slotY, this.inventorySlotSize * slotCols, this.inventorySlotSize * slotRows); 230 | ctx.stroke(); 231 | ctx.drawImage(img, x, y, newW, newH); 232 | if ( 233 | this.inventoryIndexSelected === null && 234 | this.cursorX >= slotX && 235 | this.cursorX <= slotX + this.inventorySlotSize * slotCols && 236 | this.cursorY >= slotY && 237 | this.cursorY <= slotY + this.inventorySlotSize * slotRows 238 | ) { 239 | ctx.fillStyle = 'rgba(255, 255, 255, 0.3)'; 240 | ctx.beginPath(); 241 | ctx.fillRect( 242 | slotX, 243 | slotY, 244 | this.inventorySlotSize * slotCols, 245 | this.inventorySlotSize * slotRows 246 | ); 247 | } 248 | } 249 | 250 | if ( 251 | i >= inventory[k].slotIdStartCol && 252 | i < inventory[k].slotIdStartCol + inventory[k].slotCols && 253 | j >= inventory[k].slotIdStartRow && 254 | j < inventory[k].slotIdStartRow + inventory[k].slotRows 255 | ) { 256 | slotFilled = true; 257 | } 258 | } 259 | 260 | if (!slotFilled) { 261 | ctx.beginPath(); 262 | ctx.rect(slotX, slotY, this.inventorySlotSize, this.inventorySlotSize); 263 | ctx.stroke(); 264 | } 265 | } 266 | } 267 | 268 | ctx.lineWidth = 1; 269 | ctx.beginPath(); 270 | ctx.rect(inventoryStartX, inventoryStartY, inventoryW, inventoryH); 271 | ctx.stroke(); 272 | } 273 | 274 | drawEngineConsole(values) { 275 | const fontSize = this.canvasHeight / 34; 276 | const xOffset = this.canvasWidth / 90; 277 | const h = 10 * values.length; 278 | 279 | this.ctx.fillStyle = 'rgba(0, 0, 0, 0.5)'; 280 | this.ctx.fillRect(0, this.canvasHeight - h - 2, this.canvasWidth, h + 2); 281 | 282 | for (let i = 0; i < values.length; i++) { 283 | const yOffset = this.canvasHeight - h + 10 * i; 284 | 285 | this.ctx.font = `500 ${fontSize}px arial`; 286 | this.ctx.fillStyle = 'white'; 287 | this.ctx.strokeStyle = 'black'; 288 | this.ctx.textAlign = 'left'; 289 | this.ctx.textBaseline = 'top'; 290 | this.ctx.fillText(`${i}) ${values[i]}`, xOffset, yOffset); 291 | } 292 | } 293 | 294 | drawReticle() { 295 | this.ctx.fillStyle = 'rgb(255, 255, 255)'; 296 | this.ctx.beginPath(); 297 | this.ctx.ellipse( 298 | ~~(this.canvasWidth / 2), 299 | ~~(this.canvasHeight / 2), 300 | this.canvasHeight / 300, 301 | this.canvasHeight / 300, 302 | 2 * Math.PI, 303 | 0, 304 | 2 * Math.PI 305 | ); 306 | this.ctx.fill(); 307 | } 308 | 309 | drawFps() { 310 | if (!this.showFps) return; 311 | const fontSize = this.canvasHeight / 28; 312 | const xOffset = this.canvasWidth / 90; 313 | const yOffset = this.canvasHeight / 90; 314 | 315 | this.ctx.font = `600 ${fontSize}px arial`; 316 | this.ctx.fillStyle = this.framesCounted < 60 ? 'red' : 'green'; 317 | this.ctx.strokeStyle = 'black'; 318 | this.ctx.textAlign = 'left'; 319 | this.ctx.textBaseline = 'top'; 320 | this.ctx.fillText(this.framesCounted, xOffset, yOffset); 321 | } 322 | } 323 | -------------------------------------------------------------------------------- /src/actions.js: -------------------------------------------------------------------------------- 1 | import { degToRad, getIntersection } from '../utils/calc.js'; 2 | import { maps } from './maps.js'; 3 | 4 | export default class Actions { 5 | constructor(engine, audio, db) { 6 | this.db = db; 7 | this.audio = audio; 8 | this.engine = engine; 9 | this.minUseDist = engine.minUseDist; 10 | this.keysPressed = []; 11 | this.functionToRun = null; 12 | } 13 | 14 | openDoor(rowFound, colFound, tileIndex) { 15 | const engine = this.engine; 16 | engine.map[rowFound * engine.mapCols + colFound] = 2; 17 | engine.levelTransition = true; 18 | 19 | const newTileIndex = engine.doorMap[tileIndex].indexTo; 20 | const newTileSide = engine.doorMap[tileIndex].side; 21 | const newMapNum = engine.doorMap[tileIndex].mapTo; 22 | const newMapCols = maps[newMapNum].map[0].length; 23 | let x = engine.TILE_SIZE * (newTileIndex % newMapCols); 24 | let y = engine.TILE_SIZE * Math.floor(newTileIndex / newMapCols); 25 | let newPlayerAngle = 0; 26 | const offset = 20; 27 | 28 | switch (newTileSide) { 29 | case 0: 30 | x = x + engine.TILE_SIZE / 2; 31 | y = y - offset; 32 | newPlayerAngle = 270; 33 | break; 34 | case 1: 35 | x = x + engine.TILE_SIZE + offset; 36 | y = y + engine.TILE_SIZE / 2; 37 | newPlayerAngle = 0; 38 | break; 39 | case 2: 40 | x = x + engine.TILE_SIZE / 2; 41 | y = y + engine.TILE_SIZE + offset; 42 | newPlayerAngle = 90; 43 | break; 44 | case 3: 45 | x = x - engine.TILE_SIZE - offset; 46 | y = y + engine.TILE_SIZE / 2; 47 | newPlayerAngle = 180; 48 | break; 49 | } 50 | 51 | const interval = setInterval(() => { 52 | if (!engine.levelTransition) { 53 | engine.fPlayerX = x; 54 | engine.fPlayerY = y; 55 | engine.fPlayerAngle = newPlayerAngle; 56 | engine.setNewMapData(newMapNum); 57 | clearInterval(interval); 58 | } 59 | }, 50); 60 | } 61 | 62 | checkThinWalls() { 63 | const engine = this.engine; 64 | if (!engine.reticleOnWall) return; 65 | 66 | let record = Infinity; 67 | let thinWallIndex = null; 68 | 69 | for (let i = 0; i < engine.thinWalls.length; i++) { 70 | if (engine.thinWalls[i].function !== 'door') continue; 71 | const intersection = getIntersection( 72 | engine.fPlayerX, 73 | engine.fPlayerY, 74 | 1, 75 | degToRad(engine.fPlayerAngle), 76 | engine.thinWalls[i].xStart, 77 | engine.thinWalls[i].yStart, 78 | engine.thinWalls[i].xEnd, 79 | engine.thinWalls[i].yEnd 80 | ); 81 | 82 | if (intersection?.[0]) { 83 | const dx = Math.abs(engine.fPlayerX - intersection[0]); 84 | const dy = Math.abs(engine.fPlayerY - intersection[1]); 85 | const d = Math.sqrt(dx * dx + dy * dy); 86 | record = Math.min(d, record); 87 | 88 | if (d <= record) { 89 | record = d; 90 | thinWallIndex = i; 91 | } 92 | } 93 | } 94 | 95 | if (record < this.minUseDist) { 96 | return { 97 | record: record, 98 | index: thinWallIndex, 99 | }; 100 | } 101 | } 102 | 103 | checkDoors() { 104 | const engine = this.engine; 105 | if (!engine.reticleOnWall) return; 106 | let record = Infinity; 107 | let rowFound = null; 108 | let colFound = null; 109 | let tileIndex = null; 110 | 111 | let adjustedAngle = engine.fPlayerAngle; 112 | if (adjustedAngle < 0) adjustedAngle += 360; 113 | const playerQuadrant = Math.floor(adjustedAngle / 90); 114 | const sidesToCheck = engine.getSidesToCheck(playerQuadrant); 115 | 116 | for (let row = 0; row < engine.mapRows; row++) { 117 | for (let col = 0; col < engine.mapCols; col++) { 118 | const tile = engine.map[row * engine.mapCols + col]; 119 | if (tile > 5) continue; 120 | 121 | const tileIntersection = engine.getIntersectionOfTile( 122 | engine.fPlayerX, 123 | engine.fPlayerY, 124 | row, 125 | col, 126 | degToRad(engine.fPlayerAngle), 127 | sidesToCheck 128 | ); 129 | 130 | if (tileIntersection.record < record) { 131 | tileIndex = row * engine.mapCols + col; 132 | record = tileIntersection.record; 133 | rowFound = row; 134 | colFound = col; 135 | } 136 | } 137 | } 138 | 139 | if (engine.doorMap[tileIndex] && record < this.minUseDist) { 140 | return { 141 | record: record, 142 | rowFound: rowFound, 143 | colFound: colFound, 144 | tileIndex: tileIndex, 145 | }; 146 | } 147 | return; 148 | } 149 | 150 | checkItems() { 151 | const engine = this.engine; 152 | let record = Infinity; 153 | let ItemIndex = null; 154 | 155 | for (let i = 0; i < engine.items.length; i++) { 156 | // Get perpendicular line coords 157 | const deltaY = engine.items[i].y - engine.fPlayerY; 158 | const deltaX = engine.items[i].x - engine.fPlayerX; 159 | const slope = deltaY / deltaX; 160 | const perpSlope = -(1 / slope); 161 | const angle = Math.atan(perpSlope); 162 | let x1; 163 | let y1; 164 | let x2; 165 | let y2; 166 | x1 = engine.items[i].x - (engine.fItemTextureBufferList[i].width / 2) * Math.cos(angle); 167 | y1 = engine.items[i].y - (engine.fItemTextureBufferList[i].width / 2) * Math.sin(angle); 168 | x2 = engine.items[i].x + (engine.fItemTextureBufferList[i].width / 2) * Math.cos(angle); 169 | y2 = engine.items[i].y + (engine.fItemTextureBufferList[i].width / 2) * Math.sin(angle); 170 | 171 | const intersection = getIntersection( 172 | engine.fPlayerX, 173 | engine.fPlayerY, 174 | 1, 175 | degToRad(engine.fPlayerAngle), 176 | x1, 177 | y1, 178 | x2, 179 | y2 180 | ); 181 | 182 | if (intersection?.[0]) { 183 | const dx = Math.abs(engine.fPlayerX - intersection[0]); 184 | const dy = Math.abs(engine.fPlayerY - intersection[1]); 185 | const d = Math.sqrt(dx * dx + dy * dy); 186 | 187 | if (d <= record && engine.items[i].inReticle) { 188 | record = Math.min(d, record); 189 | record = d; 190 | ItemIndex = i; 191 | } 192 | } 193 | } 194 | 195 | if (record < this.minUseDist) { 196 | return { 197 | record: record, 198 | index: ItemIndex, 199 | }; 200 | } 201 | } 202 | 203 | findSpotForItem(newItemIndex) { 204 | const inventory = this.engine.inventory; 205 | const newItemCols = this.engine.items[newItemIndex].inventoryCols; 206 | const newItemRows = this.engine.items[newItemIndex].inventoryRows; 207 | 208 | for (let j = 0; j < this.engine.inventorySlotRows; j++) { 209 | loop: for (let i = 0; i < this.engine.inventorySlotCols; i++) { 210 | let spaceFound = true; 211 | const newItemEndCol = i + (newItemCols - 1); 212 | const newItemEndRow = j + (newItemRows - 1); 213 | 214 | if ( 215 | newItemEndCol > this.engine.inventorySlotCols - 1 || 216 | newItemEndRow > this.engine.inventorySlotRows - 1 217 | ) { 218 | break loop; 219 | } 220 | 221 | for (let k = 0; k < inventory.length; k++) { 222 | const endCol = inventory[k].slotIdStartCol + (inventory[k].slotCols - 1); 223 | const endRow = inventory[k].slotIdStartRow + (inventory[k].slotRows - 1); 224 | 225 | if ( 226 | i <= endCol && 227 | newItemEndCol >= inventory[k].slotIdStartCol && 228 | j <= endRow && 229 | newItemEndRow >= inventory[k].slotIdStartRow 230 | ) { 231 | spaceFound = false; 232 | } 233 | } 234 | 235 | if (spaceFound) { 236 | this.engine.inventory.push({ 237 | name: this.engine.items[newItemIndex].name, 238 | slotIdStartCol: i, 239 | slotIdStartRow: j, 240 | slotCols: newItemCols, 241 | slotRows: newItemRows, 242 | category: this.engine.items[newItemIndex].category, 243 | }); 244 | this.engine.items.splice(newItemIndex, 1); 245 | this.engine.fItemTextureBufferList.splice(newItemIndex, 1); 246 | this.engine.fItemTexturePixelsList.splice(newItemIndex, 1); 247 | return; 248 | } 249 | } 250 | } 251 | } 252 | 253 | handleUseBtn() { 254 | let record = Infinity; 255 | let action = ''; 256 | 257 | const doorData = this.checkDoors(); 258 | if (doorData?.record < record) { 259 | record = doorData.record; 260 | action = 'openDoor'; 261 | } 262 | 263 | const thinWallData = this.checkThinWalls(); 264 | if (thinWallData?.record < record) { 265 | record = thinWallData.record; 266 | action = 'operateThinWall'; 267 | } 268 | 269 | const itemData = this.checkItems(); 270 | if (itemData?.record < record) { 271 | record = itemData.record; 272 | action = 'grabItem'; 273 | } 274 | 275 | switch (action) { 276 | case 'openDoor': 277 | this.openDoor(doorData.rowFound, doorData.colFound, doorData.tileIndex); 278 | this.audio.playSound('doorOpen', this.engine.fPlayerX, this.engine.fPlayerY, false); 279 | break; 280 | case 'operateThinWall': 281 | this.engine.activeThinWallId = thinWallData.index; 282 | const x = 283 | this.engine.thinWalls[thinWallData.index].xStartOriginal + 284 | Math.abs( 285 | this.engine.thinWalls[thinWallData.index].xEnd - this.engine.thinWalls[thinWallData.index].xStart 286 | ) / 287 | 2; 288 | const y = 289 | this.engine.thinWalls[thinWallData.index].yStartOriginal + 290 | Math.abs( 291 | this.engine.thinWalls[thinWallData.index].yEnd - this.engine.thinWalls[thinWallData.index].yStart 292 | ) / 293 | 2; 294 | 295 | if (this.engine.thinWalls[thinWallData.index].isOpen) { 296 | this.audio.playSound('slidingDoorClose', x, y, true, thinWallData.index); 297 | } else this.audio.playSound('slidingDoorOpen', x, y, true, thinWallData.index); 298 | break; 299 | case 'grabItem': 300 | this.findSpotForItem(itemData.index); 301 | this.audio.playSound('itemPickup', this.engine.fPlayerX, this.engine.fPlayerY, false); 302 | break; 303 | } 304 | } 305 | 306 | runNextFunction() { 307 | if (!this.functionToRun) return; 308 | this.functionToRun(); 309 | this.functionToRun = null; 310 | } 311 | 312 | init() { 313 | const engine = this.engine; 314 | document.addEventListener('mousedown', e => { 315 | if (!engine.userIsInTab || engine.DEBUG) return; 316 | }); 317 | 318 | document.addEventListener('mousemove', e => { 319 | if (!engine.userIsInTab) return; 320 | if (!engine.DEBUG) { 321 | if (!engine.inventoryOpen) { 322 | engine.fPlayerAngle += e.movementX / 20; 323 | engine.fProjectionPlaneYCenter -= e.movementY / 4; 324 | if (engine.fProjectionPlaneYCenter < -engine.PROJECTIONPLANEHEIGHT / 2) { 325 | engine.fProjectionPlaneYCenter = -engine.PROJECTIONPLANEHEIGHT / 2; 326 | } else if ( 327 | engine.fProjectionPlaneYCenter > 328 | engine.PROJECTIONPLANEHEIGHT + engine.PROJECTIONPLANEHEIGHT / 2 329 | ) { 330 | engine.fProjectionPlaneYCenter = engine.PROJECTIONPLANEHEIGHT + engine.PROJECTIONPLANEHEIGHT / 2; 331 | } 332 | } 333 | } 334 | }); 335 | 336 | document.addEventListener('keydown', e => { 337 | if (!this.keysPressed.includes(e.code)) { 338 | this.keysPressed.push(e.code); 339 | if ( 340 | !( 341 | this.keysPressed.includes('ControlLeft') && 342 | this.keysPressed.includes('ShiftLeft') && 343 | this.keysPressed.includes('KeyI') 344 | ) && 345 | !this.keysPressed.includes('F5') 346 | ) { 347 | e.preventDefault(); 348 | } 349 | } 350 | 351 | if (e.code === 'KeyW') { 352 | engine.fKeyForward = true; 353 | engine.fKeyBack = false; 354 | } else if (e.code === 'KeyS') { 355 | engine.fKeyBack = true; 356 | engine.fKeyForward = false; 357 | } 358 | 359 | if (e.code === 'KeyA') { 360 | if (engine.DEBUG) engine.fRotationDir = 'left'; 361 | else { 362 | engine.fKeyLeft = true; 363 | engine.fKeyRight = false; 364 | } 365 | } else if (e.code === 'KeyD') { 366 | if (engine.DEBUG) engine.fRotationDir = 'right'; 367 | else { 368 | engine.fKeyRight = true; 369 | engine.fKeyLeft = false; 370 | } 371 | } 372 | 373 | if (e.code === 'Space' && !engine.isJumping && !engine.isCrouching && !engine.isStanding) { 374 | engine.isJumping = true; 375 | } 376 | 377 | if (e.code === 'ShiftLeft' && !engine.isCrouching && !engine.isJumping && !engine.isStanding) { 378 | engine.isCrouching = true; 379 | } 380 | }); 381 | 382 | document.addEventListener('keyup', e => { 383 | e.preventDefault(); 384 | 385 | const index = this.keysPressed.indexOf(e.code); 386 | if (index > -1) this.keysPressed.splice(index, 1); 387 | 388 | if (e.code === 'Tab') { 389 | if (this.engine.inventoryOpen) this.engine.inventoryOpen = false; 390 | else this.engine.inventoryOpen = true; 391 | } 392 | 393 | if (!engine.userIsInTab && !engine.DEBUG) return; 394 | 395 | if (e.code === 'KeyW') { 396 | engine.fKeyForward = false; 397 | } else if (e.code === 'KeyS') { 398 | engine.fKeyBack = false; 399 | } 400 | 401 | if (e.code === 'KeyA') { 402 | if (engine.DEBUG) engine.fRotationDir = ''; 403 | else engine.fKeyLeft = false; 404 | } else if (e.code === 'KeyD') { 405 | if (engine.DEBUG) engine.fRotationDir = ''; 406 | else engine.fKeyRight = false; 407 | } 408 | 409 | if (e.code === 'ShiftLeft') { 410 | engine.isCrouching = false; 411 | engine.isStanding = true; 412 | } 413 | 414 | if (e.code === 'KeyE') { 415 | this.functionToRun = this.handleUseBtn; 416 | } 417 | }); 418 | } 419 | } 420 | -------------------------------------------------------------------------------- /src/maps.js: -------------------------------------------------------------------------------- 1 | // prettier-ignore 2 | 3 | // Walls: 0 - 5 4 | // Floors: 6 - 9 5 | // Closed Door --> 1 6 | // Open Door --> 2 7 | export const maps = [ 8 | { 9 | // 24 x 23 10 | map: [ 11 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 12 | [0, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 0, 7, 7, 7, 7, 7, 7, 7, 0], 13 | [0, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 7, 7, 7, 7, 7, 7, 0, 0], 14 | [0, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 0, 7, 7, 7, 7, 7, 7, 0, 0], 15 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 6, 0, 7, 7, 7, 7, 7, 7, 7, 0], 16 | [0, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 0, 0, 0, 7, 7, 0, 0, 0, 0], 17 | [0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 6, 0, 7, 7, 7, 7, 7, 7, 7, 0], 18 | [0, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 0, 6, 6, 0, 7, 7, 7, 7, 7, 0, 7, 0], 19 | [0, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 0, 6, 6, 0, 7, 7, 7, 7, 7, 0, 7, 0], 20 | [0, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 0, 6, 6, 0, 7, 7, 7, 7, 7, 7, 7, 0], 21 | [0, 0, 0, 0, 6, 6, 6, 6, 6, 6, 6, 0, 0, 6, 6, 0, 7, 7, 7, 7, 7, 7, 7, 0], 22 | [0, 6, 6, 6, 0, 6, 6, 6, 6, 6, 0, 6, 0, 6, 6, 6, 7, 7, 7, 7, 7, 0, 7, 0], 23 | [0, 6, 6, 6, 6, 0, 6, 6, 6, 0, 6, 6, 0, 6, 6, 0, 7, 7, 7, 7, 7, 0, 7, 0], 24 | [0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 7, 7, 7, 7, 7, 7, 7, 0], 25 | [4, 3, 3, 3, 3, 3, 3, 6, 3, 3, 3, 3, 3, 3, 3, 0, 7, 7, 7, 7, 7, 7, 7, 0], 26 | [3, 8, 9, 8, 9, 8, 9, 8, 9, 8, 9, 8, 9, 8, 3, 0, 7, 7, 7, 7, 7, 0, 7, 0], 27 | [3, 9, 8, 9, 8, 9, 8, 9, 8, 9, 8, 9, 8, 9, 3, 0, 7, 7, 7, 7, 7, 0, 7, 0], 28 | [3, 8, 9, 8, 9, 8, 9, 8, 9, 8, 9, 8, 9, 8, 3, 0, 7, 7, 7, 7, 7, 7, 7, 0], 29 | [1, 9, 8, 9, 8, 9, 8, 9, 8, 9, 8, 9, 8, 9, 3, 0, 7, 7, 7, 7, 7, 7, 7, 0], 30 | [3, 8, 9, 8, 9, 8, 9, 8, 9, 8, 9, 8, 9, 8, 3, 0, 7, 7, 7, 7, 7, 0, 7, 0], 31 | [3, 9, 8, 9, 8, 9, 8, 9, 8, 9, 8, 9, 8, 9, 3, 0, 7, 7, 7, 7, 7, 0, 7, 0], 32 | [3, 8, 9, 8, 9, 8, 9, 8, 9, 8, 9, 8, 9, 8, 3, 0, 7, 7, 7, 7, 7, 7, 7, 0], 33 | [3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0], 34 | ], 35 | noCeilingIndeces: [ 36 | 40, 41, 42, 43, 44, 45, 46, 37 | 64, 65, 66, 67, 68, 69, 70, 38 | 88, 89, 90, 91, 92, 93, 94, 39 | 112, 113, 114, 115, 116, 117, 118 40 | ], 41 | lightSources: [ 42 | { 43 | surface: 'floor', 44 | texture: 'standingLight', 45 | col: 14, 46 | row: 1, 47 | side: null, 48 | lightOffsetFromTexture: 10, 49 | strength: 0.5 50 | }, 51 | { 52 | surface: 'ceiling', 53 | texture: 'chandelier', 54 | col: 19, 55 | row: 11, 56 | side: null, 57 | lightOffsetFromTexture: 10, 58 | strength: 0.8 59 | }, 60 | { 61 | surface: 'ceiling', 62 | texture: 'hangingLight2', 63 | col: 7, 64 | row: 18, 65 | side: null, 66 | lightOffsetFromTexture: 10, 67 | strength: 1 68 | } 69 | ], 70 | thinWalls: [ 71 | { 72 | texture: 'slidingDoor', 73 | rowStart: 2, 74 | colStart: 15, 75 | rowEnd: 3, 76 | colEnd: 15, 77 | isOpen: false, 78 | function: 'door', 79 | sounds: ['slidingDoorOpen', 'slidingDoorClose'], 80 | transparent: true, 81 | vaultable: false 82 | }, 83 | { 84 | texture: 'slidingDoor', 85 | rowStart: 11, 86 | colStart: 15, 87 | rowEnd: 12, 88 | colEnd: 15, 89 | isOpen: false, 90 | function: 'door', 91 | sounds: ['slidingDoorOpen', 'slidingDoorClose'], 92 | transparent: true, 93 | vaultable: false 94 | } 95 | ], 96 | items: [ 97 | { 98 | name: 'apple', 99 | x: 500, 100 | y: 150, 101 | category: 'food', 102 | inReticle: false, 103 | inventoryCols: 1, 104 | inventoryRows: 1, 105 | }, 106 | { 107 | name: 'note', 108 | x: 940, 109 | y: 80, 110 | category: 'note', 111 | inReticle: false, 112 | inventoryCols: 1, 113 | inventoryRows: 1, 114 | }, 115 | { 116 | name: 'pickaxe', 117 | x: 1450, 118 | y: 80, 119 | category: 'tool', 120 | inReticle: false, 121 | inventoryCols: 2, 122 | inventoryRows: 3, 123 | }, 124 | { 125 | name: 'bread', 126 | x: 700, 127 | y: 600, 128 | category: 'food', 129 | inReticle: false, 130 | inventoryCols: 2, 131 | inventoryRows: 1, 132 | }, 133 | { 134 | name: 'pickaxe', 135 | x: 730, 136 | y: 530, 137 | category: 'tool', 138 | inReticle: false, 139 | inventoryCols: 2, 140 | inventoryRows: 3, 141 | }, 142 | { 143 | name: 'bread', 144 | x: 750, 145 | y: 480, 146 | category: 'food', 147 | inReticle: false, 148 | inventoryCols: 2, 149 | inventoryRows: 1, 150 | }, 151 | { 152 | name: 'bread', 153 | x: 710, 154 | y: 500, 155 | category: 'food', 156 | inReticle: false, 157 | inventoryCols: 2, 158 | inventoryRows: 1, 159 | }, 160 | { 161 | name: 'bread', 162 | x: 100, 163 | y: 1380, 164 | category: 'food', 165 | inReticle: false, 166 | inventoryCols: 2, 167 | inventoryRows: 1, 168 | }, 169 | ], 170 | objects: [ 171 | { 172 | name: 'barrel', 173 | x: 1420, 174 | y: 100, 175 | hFromGround: 0, 176 | sounds: [] 177 | }, 178 | { 179 | name: 'barrel', 180 | x: 1420, 181 | y: 280, 182 | hFromGround: 0, 183 | sounds: [] 184 | }, 185 | { 186 | name: 'table', 187 | x: 1370, 188 | y: 640, 189 | hFromGround: 0, 190 | sounds: [] 191 | }, 192 | { 193 | name: 'table', 194 | x: 1370, 195 | y: 900, 196 | hFromGround: 0, 197 | sounds: [] 198 | }, 199 | { 200 | name: 'table', 201 | x: 1370, 202 | y: 1160, 203 | hFromGround: 0, 204 | sounds: [] 205 | }, 206 | { 207 | name: 'radio', 208 | x: 800, 209 | y: 1350, 210 | hFromGround: 0, 211 | sounds: ['song'] 212 | }, 213 | ], 214 | wallTextures: ['wall1', 'doubleDoorClosed', 'doubleDoorOpen', 'wall3'], 215 | doorMap: { 216 | 432: { 217 | mapTo: 1, 218 | indexTo: 198, 219 | side: 1 220 | } 221 | }, 222 | floorTextures: ['floor1', 'floor3', 'floor4', 'floor5'], 223 | ceilingTexture: 'ceiling1', 224 | skyTexture: 'sky', 225 | paintings: ['painting3two', 'painting3one', 'painting1', 'painting4one', 'painting4two', 'painting2one', 'painting2two', 'painting5one', 'painting5two', 'painting6one', 'painting6two', 'painting7one', 'painting7two', 'shield', 'shield'], 226 | paintingDetails: [ 227 | { 228 | row: 4, 229 | col: 6, 230 | side: 0 231 | }, 232 | { 233 | row: 4, 234 | col: 7, 235 | side: 0 236 | }, 237 | { 238 | row: 5, 239 | col: 0, 240 | side: 1 241 | }, 242 | { 243 | row: 2, 244 | col: 22, 245 | side: 3 246 | }, 247 | { 248 | row: 3, 249 | col: 22, 250 | side: 3 251 | }, 252 | { 253 | row: 7, 254 | col: 21, 255 | side: 3 256 | }, 257 | { 258 | row: 8, 259 | col: 21, 260 | side: 3 261 | }, 262 | { 263 | row: 11, 264 | col: 21, 265 | side: 3 266 | }, 267 | { 268 | row: 12, 269 | col: 21, 270 | side: 3 271 | }, 272 | { 273 | row: 15, 274 | col: 21, 275 | side: 3 276 | }, 277 | { 278 | row: 16, 279 | col: 21, 280 | side: 3 281 | }, 282 | { 283 | row: 19, 284 | col: 21, 285 | side: 3 286 | }, 287 | { 288 | row: 20, 289 | col: 21, 290 | side: 3 291 | }, 292 | { 293 | row: 13, 294 | col: 6, 295 | side: 0 296 | }, 297 | { 298 | row: 13, 299 | col: 8, 300 | side: 0 301 | } 302 | ] 303 | }, 304 | { 305 | // 33 x 13 306 | map: [ 307 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 308 | [0, 6, 6, 6, 6, 0, 6, 6, 6, 6, 0, 6, 6, 6, 6, 0, 6, 6, 6, 6, 0, 6, 6, 6, 6, 0, 6, 6, 6, 6, 6, 6, 0], 309 | [0, 6, 6, 6, 6, 0, 6, 6, 6, 6, 0, 6, 6, 6, 6, 0, 6, 6, 6, 6, 0, 6, 6, 6, 6, 0, 6, 6, 6, 6, 6, 6, 0], 310 | [0, 6, 6, 6, 6, 0, 6, 6, 6, 6, 0, 6, 6, 6, 6, 0, 6, 6, 6, 6, 0, 6, 6, 6, 6, 0, 6, 6, 6, 6, 6, 6, 0], 311 | [0, 6, 6, 6, 6, 0, 6, 6, 6, 6, 0, 6, 6, 6, 6, 0, 6, 6, 6, 6, 0, 6, 6, 6, 6, 0, 6, 6, 6, 6, 6, 6, 0], 312 | [0, 0, 0, 6, 0, 0, 0, 0, 6, 0, 0, 0, 0, 6, 0, 0, 0, 0, 6, 0, 0, 0, 0, 6, 0, 0, 6, 6, 6, 6, 6, 6, 0], 313 | [1, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 0], 314 | [0, 0, 0, 6, 0, 0, 0, 0, 6, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 6, 6, 6, 6, 6, 6, 0], 315 | [0, 6, 6, 6, 6, 0, 6, 6, 6, 6, 0, 6, 6, 6, 6, 0, 6, 6, 6, 6, 0, 6, 6, 6, 6, 0, 6, 6, 6, 6, 6, 6, 0], 316 | [0, 6, 6, 6, 6, 0, 6, 6, 6, 6, 0, 6, 6, 6, 6, 0, 6, 6, 6, 6, 6, 6, 6, 6, 6, 0, 6, 6, 6, 6, 6, 6, 0], 317 | [0, 6, 6, 6, 6, 0, 6, 6, 6, 6, 0, 6, 6, 6, 6, 0, 6, 6, 6, 6, 0, 6, 6, 6, 6, 0, 6, 6, 6, 6, 6, 6, 0], 318 | [0, 6, 6, 6, 6, 0, 6, 6, 6, 6, 0, 6, 6, 6, 6, 0, 6, 6, 6, 6, 0, 6, 6, 6, 6, 0, 6, 6, 6, 6, 6, 6, 0], 319 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 320 | ], 321 | lightSources: [ 322 | { 323 | surface: 'ceiling', 324 | texture: 'hangingLight2', 325 | col: 8, 326 | row: 6, 327 | side: null, 328 | lightOffsetFromTexture: 10, 329 | strength: 0.5 330 | }, 331 | { 332 | surface: 'floor', 333 | texture: 'standingLight', 334 | col: 16, 335 | row: 11, 336 | side: null, 337 | lightOffsetFromTexture: 10, 338 | strength: 0.3 339 | } 340 | ], 341 | thinWalls: [ 342 | { 343 | texture: 'slidingDoor', 344 | rowStart: 9, 345 | colStart: 20, 346 | rowEnd: 10, 347 | colEnd: 20, 348 | isOpen: false, 349 | function: 'door', 350 | sounds: ['slidingDoorOpen', 'slidingDoorClose'], 351 | transparent: true, 352 | vaultable: false 353 | } 354 | ], 355 | objects: [ 356 | { 357 | name: 'elmo', 358 | x: 1042, 359 | y: 86, 360 | hFromGround: 0, 361 | sounds: ['knocking'] 362 | } 363 | ], 364 | wallTextures: ['wall1', 'doubleDoorClosed', 'doubleDoorOpen'], 365 | doorMap: { 366 | 198: { 367 | mapTo: 0, 368 | indexTo: 432, 369 | side: 1 370 | }, 371 | 413: { 372 | mapTo: 2, 373 | indexTo: 1, 374 | side: 2 375 | } 376 | }, 377 | noCeilingIndeces: [], 378 | floorTextures: ['floor1'], 379 | ceilingTexture: 'ceiling1', 380 | paintings: ['painting1', 'painting8', 'painting9', 'painting10', 'painting11', 'painting12', 'painting13', 'painting14', 'painting15', 'bloodyHandprint'], 381 | paintingDetails: [ 382 | { 383 | row: 5, 384 | col: 5, 385 | side: 2 386 | }, 387 | { 388 | row: 7, 389 | col: 10, 390 | side: 0 391 | }, 392 | { 393 | row: 5, 394 | col: 15, 395 | side: 2 396 | }, 397 | { 398 | row: 7, 399 | col: 20, 400 | side: 0 401 | }, 402 | { 403 | row: 12, 404 | col: 3, 405 | side: 0 406 | }, 407 | { 408 | row: 0, 409 | col: 8, 410 | side: 2 411 | }, 412 | { 413 | row: 12, 414 | col: 13, 415 | side: 0 416 | }, 417 | { 418 | row: 0, 419 | col: 18, 420 | side: 2 421 | }, 422 | { 423 | row: 12, 424 | col: 22, 425 | side: 0 426 | }, 427 | { 428 | row: 0, 429 | col: 16, 430 | side: 2 431 | }, 432 | ] 433 | }, 434 | { 435 | // 20 x 20 436 | map: [ 437 | [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 438 | [0, 6, 6, 0, 6, 6, 6, 6, 6, 6, 6, 6, 6, 0, 6, 6, 6, 6, 6, 0], 439 | [0, 6, 6, 0, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 0], 440 | [0, 6, 6, 0, 6, 6, 6, 6, 6, 6, 6, 6, 6, 0, 6, 6, 6, 6, 6, 0], 441 | [0, 6, 6, 0, 6, 6, 6, 6, 6, 6, 6, 6, 6, 0, 6, 6, 6, 6, 6, 0], 442 | [0, 6, 6, 0, 6, 6, 6, 6, 6, 6, 6, 6, 0, 6, 6, 6, 6, 6, 6, 0], 443 | [0, 6, 6, 0, 6, 6, 6, 6, 6, 6, 6, 0, 6, 6, 6, 6, 6, 6, 6, 0], 444 | [0, 6, 6, 0, 0, 0, 0, 0, 0, 0, 0, 6, 6, 6, 6, 6, 6, 6, 6, 0], 445 | [0, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 0], 446 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 6, 6, 6, 6, 6, 6, 6, 0], 447 | [0, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 0, 6, 6, 6, 6, 6, 6, 6, 0], 448 | [0, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 0, 6, 6, 6, 6, 6, 6, 0], 449 | [0, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 0, 6, 6, 6, 6, 6, 0], 450 | [0, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 0, 6, 6, 6, 6, 6, 0], 451 | [0, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 0, 6, 6, 6, 6, 6, 0], 452 | [0, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 0], 453 | [0, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 0, 6, 6, 6, 6, 6, 0], 454 | [1, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 0, 6, 6, 6, 6, 6, 0], 455 | [0, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 0, 6, 6, 6, 6, 6, 0], 456 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 457 | ], 458 | lightSources: [ 459 | { 460 | surface: 'ceiling', 461 | texture: 'hangingLight', 462 | col: 8, 463 | row: 8, 464 | side: null, 465 | lightOffsetFromTexture: 10, 466 | strength: 0.8, 467 | sounds: ['lightHum'] 468 | }, 469 | { 470 | surface: 'ceiling', 471 | texture: 'hangingLight', 472 | col: 6, 473 | row: 15, 474 | side: null, 475 | lightOffsetFromTexture: 10, 476 | strength: 0.8, 477 | sounds: ['lightHum'] 478 | } 479 | ], 480 | thinWalls: [ 481 | 482 | ], 483 | items: [ 484 | 485 | ], 486 | objects: [ 487 | 488 | ], 489 | noCeilingIndeces: [], 490 | wallTextures: ['wall2', 'doubleDoor2Closed', 'doubleDoor2Open'], 491 | doorMap: { 492 | 1: { 493 | mapTo: 1, 494 | indexTo: 413, 495 | side: 0 496 | }, 497 | 340: { 498 | mapTo: 3, 499 | indexTo: 79, 500 | side: 3 501 | } 502 | }, 503 | floorTextures: ['floor2'], 504 | skyTexture: 'sky', 505 | ceilingTexture: 'ceiling2', 506 | paintings: [], 507 | paintingDetails: [] 508 | }, 509 | { 510 | // 20 x 5 511 | map: [ 512 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 513 | [0, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 0], 514 | [0, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 0], 515 | [0, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 1], 516 | [0, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 0], 517 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 518 | ], 519 | lightSources: [ 520 | { 521 | surface: 'wall', 522 | texture: 'hangingLight', 523 | col: 0, 524 | row: 4, 525 | side: 1, 526 | lightOffsetFromTexture: 10, 527 | strength: 1 528 | }, 529 | ], 530 | thinWalls: [ 531 | { 532 | texture: 'fence', 533 | rowStart: 2, 534 | colStart: 1, 535 | rowEnd: 2, 536 | colEnd: 2, 537 | isOpen: false, 538 | function: null, 539 | sounds: [], 540 | transparent: true, 541 | vaultable: true 542 | }, 543 | { 544 | texture: 'fence', 545 | rowStart: 2, 546 | colStart: 2, 547 | rowEnd: 2, 548 | colEnd: 3, 549 | isOpen: false, 550 | function: null, 551 | sounds: [], 552 | transparent: true, 553 | vaultable: true 554 | }, 555 | { 556 | texture: 'fence', 557 | rowStart: 2, 558 | colStart: 3, 559 | rowEnd: 2, 560 | colEnd: 4, 561 | isOpen: false, 562 | function: null, 563 | sounds: [], 564 | transparent: true, 565 | vaultable: true 566 | }, 567 | { 568 | texture: 'fence', 569 | rowStart: 2, 570 | colStart: 5, 571 | rowEnd: 3, 572 | colEnd: 5, 573 | isOpen: false, 574 | function: null, 575 | sounds: [], 576 | transparent: true, 577 | vaultable: true 578 | }, 579 | { 580 | texture: 'fence', 581 | rowStart: 3, 582 | colStart: 5, 583 | rowEnd: 4, 584 | colEnd: 5, 585 | isOpen: false, 586 | function: null, 587 | sounds: [], 588 | transparent: true, 589 | vaultable: true 590 | }, 591 | { 592 | texture: 'fence', 593 | rowStart: 4, 594 | colStart: 5, 595 | rowEnd: 5, 596 | colEnd: 5, 597 | isOpen: false, 598 | function: null, 599 | sounds: [], 600 | transparent: true, 601 | vaultable: true 602 | }, 603 | ], 604 | items: [ 605 | 606 | ], 607 | objects: [ 608 | 609 | ], 610 | wallTextures: ['wall4', 'doubleDoorClosed', 'doubleDoorOpen'], 611 | doorMap: { 612 | 79: { 613 | mapTo: 2, 614 | indexTo: 340, 615 | side: 1 616 | } 617 | }, 618 | noCeilingIndeces: [], 619 | floorTextures: ['floor6'], 620 | skyTexture: 'sky', 621 | ceilingTexture: null, 622 | paintings: [], 623 | paintingDetails: [], 624 | ambientAudio: [ 625 | { 626 | name: 'outside', 627 | x: null, 628 | y: null, 629 | } 630 | ] 631 | }, 632 | ] 633 | 634 | export const texturePaths = [ 635 | // Walls 636 | 'src/assets/walls/wall1.png', 637 | 'src/assets/walls/wall2.png', 638 | 'src/assets/walls/wall3.png', 639 | 'src/assets/walls/wall4.png', 640 | 'src/assets/walls/doubleDoorClosed.png', 641 | 'src/assets/walls/doubleDoorOpen.png', 642 | 'src/assets/walls/doubleDoor2Closed.png', 643 | 'src/assets/walls/doubleDoor2Open.png', 644 | // Floors 645 | 'src/assets/floors/floor1.png', 646 | 'src/assets/floors/floor2.png', 647 | 'src/assets/floors/floor3.png', 648 | 'src/assets/floors/floor4.png', 649 | 'src/assets/floors/floor5.png', 650 | 'src/assets/floors/floor6.png', 651 | // Ceilings 652 | 'src/assets/ceilings/ceiling1.png', 653 | 'src/assets/ceilings/ceiling2.png', 654 | // Paintings 655 | 'src/assets/paintings/painting1.png', 656 | 'src/assets/paintings/painting2one.png', 657 | 'src/assets/paintings/painting2two.png', 658 | 'src/assets/paintings/painting3one.png', 659 | 'src/assets/paintings/painting3two.png', 660 | 'src/assets/paintings/painting4one.png', 661 | 'src/assets/paintings/painting4two.png', 662 | 'src/assets/paintings/painting5one.png', 663 | 'src/assets/paintings/painting5two.png', 664 | 'src/assets/paintings/painting6one.png', 665 | 'src/assets/paintings/painting6two.png', 666 | 'src/assets/paintings/painting7one.png', 667 | 'src/assets/paintings/painting7two.png', 668 | 'src/assets/paintings/painting8.png', 669 | 'src/assets/paintings/painting9.png', 670 | 'src/assets/paintings/painting10.png', 671 | 'src/assets/paintings/painting11.png', 672 | 'src/assets/paintings/painting12.png', 673 | 'src/assets/paintings/painting13.png', 674 | 'src/assets/paintings/painting14.png', 675 | 'src/assets/paintings/painting15.png', 676 | 'src/assets/paintings/shield.png', 677 | 'src/assets/paintings/bloodyHandprint.png', 678 | // Objects 679 | 'src/assets/objects/barrel.png', 680 | 'src/assets/objects/redbull.png', 681 | 'src/assets/objects/elmo.png', 682 | 'src/assets/objects/table.png', 683 | 'src/assets/objects/radio.png', 684 | // Thin Walls 685 | 'src/assets/thinWalls/slidingDoor.png', 686 | 'src/assets/thinWalls/fence.png', 687 | // Items 688 | 'src/assets/items/apple.png', 689 | 'src/assets/items/pickaxe.png', 690 | 'src/assets/items/note.png', 691 | 'src/assets/items/redbull.png', 692 | 'src/assets/items/bread.png', 693 | // Light Sources 694 | 'src/assets/lightSources/hangingLight.png', 695 | 'src/assets/lightSources/hangingLight2.png', 696 | 'src/assets/lightSources/standingLight.png', 697 | 'src/assets/lightSources/chandelier.png', 698 | // Skys 699 | 'src/assets/skyBoxes/sky.jpg', 700 | ]; 701 | -------------------------------------------------------------------------------- /src/engine.js: -------------------------------------------------------------------------------- 1 | import { convertDeg0To360, degToRad, getIntersection, radToDeg } from '../utils/calc.js'; 2 | import { maps, texturePaths } from './maps.js'; 3 | 4 | export default class Engine { 5 | constructor(audio, db) { 6 | this.db = db; 7 | this.audio = audio; 8 | this.canvas = document.getElementById('canvas'); 9 | this.canvasWidth = this.canvas.width; 10 | this.canvasHeight = this.canvas.height; 11 | this.ctx = this.canvas.getContext('2d', { alpha: true }); 12 | 13 | this.offscreenCanvas = new OffscreenCanvas(canvas.width, canvas.height); 14 | this.offscreenCanvasContext = this.offscreenCanvas.getContext('2d', { alpha: false }); 15 | this.offscreenCanvasPixels = this.offscreenCanvasContext.getImageData( 16 | 0, 17 | 0, 18 | this.canvasWidth, 19 | this.canvasHeight 20 | ); 21 | 22 | this.PROJECTIONPLANEWIDTH = this.canvasWidth; 23 | this.PROJECTIONPLANEHEIGHT = this.canvasHeight; 24 | 25 | this.mapLightValues = [null, null, null]; 26 | this.mapLightRefs = [null, null, null]; 27 | this.mapLightSides = [null, null, null]; 28 | this.currentLightValues = null; 29 | this.currentLightRefs = null; 30 | this.currentThinWallLightSides = null; 31 | 32 | this.minBrightness = 0.12; 33 | this.maxBrightness = 1.3; 34 | 35 | this.fWallTextureBufferList; 36 | this.fWallTexturePixelsList; 37 | 38 | this.fPaintingTextureBufferList; 39 | this.fPaintingTexturePixelsList; 40 | this.fPaintingDetails; 41 | 42 | this.fFloorTextureBufferList; 43 | this.fFloorTexturePixelsList; 44 | 45 | this.fCeilingTextureBuffer; 46 | this.fCeilingTexturePixels; 47 | 48 | this.fSkyTextureBuffer; 49 | this.fSkyTexturePixels; 50 | 51 | this.fObjectTextureBufferList; 52 | this.fObjectTexturePixelsList; 53 | 54 | this.fThinWallTextureBufferList; 55 | this.fThinWallTexturePixelsList; 56 | 57 | this.fItemTextureBufferList; 58 | this.fItemTexturePixelsList; 59 | 60 | this.fLightTextureBufferList; 61 | this.fLightTexturePixelList; 62 | 63 | this.noCeilingIndeces; 64 | 65 | this.items = []; 66 | this.objects = []; 67 | this.lightSources = []; 68 | this.objectRefs = new Array(this.PROJECTIONPLANEWIDTH); 69 | this.objectOffsets = new Array(this.PROJECTIONPLANEWIDTH); 70 | this.objectRayLengths = new Array(this.PROJECTIONPLANEWIDTH); 71 | this.objectCollisionsX = new Array(this.PROJECTIONPLANEWIDTH); 72 | this.objectCollisionsY = new Array(this.PROJECTIONPLANEWIDTH); 73 | this.isItemRay = new Array(this.PROJECTIONPLANEWIDTH); 74 | this.isLightsourceRay = new Array(this.PROJECTIONPLANEWIDTH); 75 | 76 | this.minUseDist = 140; 77 | 78 | this.objectTargetIndecesForStrip = []; 79 | this.objectDistsForStrip = []; 80 | 81 | this.thinWalls = []; 82 | this.thinWallRefs = new Array(this.PROJECTIONPLANEWIDTH); 83 | this.thinWallOffsets = new Float32Array(this.PROJECTIONPLANEWIDTH); 84 | this.thinWallRayLengths = new Uint16Array(this.PROJECTIONPLANEWIDTH); 85 | this.thinWallCollisionsX = new Float32Array(this.PROJECTIONPLANEWIDTH); 86 | this.thinWallCollisionsY = new Float32Array(this.PROJECTIONPLANEWIDTH); 87 | 88 | this.activeThinWallId = null; 89 | 90 | this.bytesPerPixel = 4; 91 | this.pi = Math.PI; 92 | 93 | this.texturePaths = texturePaths; 94 | this.textures = {}; 95 | 96 | this.TILE_SIZE = 64; 97 | this.WALL_HEIGHT = 64; 98 | 99 | this.mapCols = maps[0].map[0].length; 100 | this.mapRows = maps[0].map.length; 101 | this.mapWidth = this.TILE_SIZE * this.mapCols; 102 | this.mapHeight = this.TILE_SIZE * this.mapRows; 103 | this.TILE_SIZE - 1; 104 | 105 | this.mapNum = 0; 106 | this.map = maps[this.mapNum].map; 107 | 108 | this.debugCanvas; 109 | this.debugCanvasWidth; 110 | this.debugCanvasHeight; 111 | this.debugCtx; 112 | 113 | this.fProjectionPlaneYCenter = this.PROJECTIONPLANEHEIGHT / 2; 114 | 115 | this.fPlayerX = 100; 116 | this.fPlayerY = 100; 117 | this.fPlayerAngle = 10; 118 | this.fPlayerMoveDir = 0; 119 | this.fPlayerFov = 70; 120 | this.fPlayerHeight = this.TILE_SIZE / 2; 121 | this.fGameSpeed = 0; 122 | this.fPlayerMoveSpeed = 0; 123 | this.fPlayerDistanceToProjectionPlane = Math.floor( 124 | this.PROJECTIONPLANEWIDTH / 2 / Math.tan(degToRad(this.fPlayerFov) / 2) 125 | ); 126 | 127 | this.fKeyForward = false; 128 | this.fKeyBack = false; 129 | this.fKeyLeft = false; 130 | this.fKeyRight = false; 131 | 132 | this.fRotationDir = ''; 133 | 134 | this.fWallTextureCanvas; 135 | this.fWallTexturePixels; 136 | 137 | this.fFishTable = new Float32Array(this.PROJECTIONPLANEWIDTH); 138 | 139 | this.RAD0 = 0; 140 | this.RAD90 = (3 * this.pi) / 2; 141 | this.RAD180 = this.pi; 142 | this.RAD270 = this.pi / 2; 143 | this.RAD360 = 2 * this.pi; 144 | 145 | this.rayLengths = new Uint16Array(this.PROJECTIONPLANEWIDTH); 146 | this.rayAngles = new Float32Array(this.PROJECTIONPLANEWIDTH); 147 | this.rayAngleQuadrants = new Uint8Array(this.PROJECTIONPLANEWIDTH); 148 | this.tileCollisionsX = new Float32Array(this.PROJECTIONPLANEWIDTH); 149 | this.tileCollisionsY = new Float32Array(this.PROJECTIONPLANEWIDTH); 150 | this.tileTypes = new Uint8Array(this.PROJECTIONPLANEWIDTH); 151 | this.tileSides = new Uint8Array(this.PROJECTIONPLANEWIDTH); 152 | this.tileIndeces = new Uint16Array(this.PROJECTIONPLANEWIDTH); 153 | 154 | this.userIsInTab = false; 155 | this.reticleOnWall = false; 156 | this.isJumping = false; 157 | this.jumpSpeedStart = 1.2; 158 | this.jumpSpeed = this.jumpSpeedStart; 159 | this.gravityValue = 0.035; 160 | 161 | this.isCrouching = false; 162 | this.isStanding = false; 163 | this.crouchAmt = 10; 164 | this.crouchSpeedStart = 0.4; 165 | this.crouchSpeed = this.crouchSpeedStart; 166 | this.crouchGravity = 0.007; 167 | this.standSpeedStart = 0.4; 168 | this.standSpeed = this.standSpeedStart; 169 | this.standGravity = 0.007; 170 | 171 | this.redTint = 1.2; 172 | this.greenTint = 1.1; 173 | this.blueTint = 1; 174 | 175 | this.levelTransition = false; 176 | this.levelTransitionFadeAmt = 0; 177 | this.doorMap = {}; 178 | 179 | this.inventorySlotCols = 5; 180 | this.inventorySlotRows = 6; 181 | this.inventoryOpen = false; 182 | this.inventory = [ 183 | { 184 | name: 'apple', 185 | slotIdStartCol: 0, 186 | slotIdStartRow: 0, 187 | slotCols: 1, 188 | slotRows: 1, 189 | category: 'food', 190 | }, 191 | { 192 | name: 'bread', 193 | slotIdStartCol: 0, 194 | slotIdStartRow: 1, 195 | slotCols: 2, 196 | slotRows: 1, 197 | category: 'food', 198 | }, 199 | ]; 200 | 201 | this.itemInUseIndex = null; 202 | 203 | this.timeOfLastFootstep = 10000; 204 | 205 | this.DEBUG = false; 206 | this.preventPageReloadDialog = false; 207 | this.consoleValues = []; 208 | this.lightingVersionNum = 3; 209 | } 210 | 211 | getSidesToCheck(quadrant) { 212 | switch (quadrant) { 213 | case 0: 214 | return [0, 3]; 215 | case 1: 216 | return [0, 1]; 217 | case 2: 218 | return [1, 2]; 219 | case 3: 220 | return [2, 3]; 221 | } 222 | } 223 | 224 | drawFillRectangle(x, y, width, height, red, green, blue, alpha) { 225 | const bytesPerPixel = 4; 226 | let targetIndex = bytesPerPixel * this.canvasWidth * y + bytesPerPixel * x; 227 | for (let h = 0; h < height; h++) { 228 | for (let w = 0; w < width; w++) { 229 | this.offscreenCanvasPixels.data[targetIndex] = red; 230 | this.offscreenCanvasPixels.data[targetIndex + 1] = green; 231 | this.offscreenCanvasPixels.data[targetIndex + 2] = blue; 232 | this.offscreenCanvasPixels.data[targetIndex + 3] = alpha; 233 | targetIndex += bytesPerPixel; 234 | } 235 | targetIndex += bytesPerPixel * (this.canvasWidth - width); 236 | } 237 | } 238 | 239 | drawSky(x, angle) { 240 | const xRatio = convertDeg0To360(radToDeg(angle)) / 360; 241 | const sourceCol = ~~(this.fSkyTextureBuffer.width * xRatio); 242 | let sourceIndex = ~~( 243 | this.fSkyTextureBuffer.height * (this.fSkyTextureBuffer.width * this.bytesPerPixel) + 244 | sourceCol * this.bytesPerPixel 245 | ); 246 | for (let row = ~~this.fProjectionPlaneYCenter; row >= 0; row--) { 247 | if (row <= this.canvasHeight) { 248 | const targetIndex = row * (this.canvasWidth * this.bytesPerPixel) + this.bytesPerPixel * x; 249 | const red = this.fSkyTexturePixels[sourceIndex]; 250 | const green = this.fSkyTexturePixels[sourceIndex + 1]; 251 | const blue = this.fSkyTexturePixels[sourceIndex + 2]; 252 | this.offscreenCanvasPixels.data[targetIndex] = red; 253 | this.offscreenCanvasPixels.data[targetIndex + 1] = green; 254 | this.offscreenCanvasPixels.data[targetIndex + 2] = blue; 255 | this.offscreenCanvasPixels.data[targetIndex + 3] = 255; 256 | } 257 | sourceIndex -= this.bytesPerPixel * this.fSkyTextureBuffer.width; 258 | } 259 | } 260 | 261 | drawCeiling(wallTop, castColumn, rayAng) { 262 | let targetIndex = wallTop * (this.canvasWidth * this.bytesPerPixel) + this.bytesPerPixel * castColumn; 263 | 264 | for (let row = wallTop; row >= 0; row--) { 265 | const ratio = (this.WALL_HEIGHT - this.fPlayerHeight) / (this.fProjectionPlaneYCenter - row); 266 | const diagDist = ~~(this.fPlayerDistanceToProjectionPlane * ratio * this.fFishTable[castColumn]); 267 | 268 | const xEnd = ~~(diagDist * Math.cos(rayAng) + this.fPlayerX); 269 | const yEnd = ~~(diagDist * Math.sin(rayAng) + this.fPlayerY); 270 | 271 | let brightnessLevel = this.currentLightValues?.[yEnd * this.mapWidth + xEnd] || this.minBrightness; 272 | if (brightnessLevel > this.maxBrightness) brightnessLevel = this.maxBrightness; 273 | 274 | const cellX = ~~(xEnd / this.TILE_SIZE); 275 | const cellY = ~~(yEnd / this.TILE_SIZE); 276 | const cellIndex = cellY * this.mapCols + cellX; 277 | 278 | if ( 279 | this.fCeilingTextureBuffer && 280 | cellX < this.mapCols && 281 | cellY < this.mapRows && 282 | cellX >= 0 && 283 | cellY >= 0 284 | ) { 285 | if (!this.noCeilingIndeces.includes(cellIndex)) { 286 | const tileRow = xEnd & (this.TILE_SIZE - 1); 287 | const tileCol = yEnd & (this.TILE_SIZE - 1); 288 | 289 | const sourceIndex = 290 | tileRow * this.fCeilingTextureBuffer.width * this.bytesPerPixel + this.bytesPerPixel * tileCol; 291 | 292 | const red = this.fCeilingTexturePixels[sourceIndex] * (brightnessLevel * this.redTint); 293 | const green = this.fCeilingTexturePixels[sourceIndex + 1] * (brightnessLevel * this.greenTint); 294 | const blue = this.fCeilingTexturePixels[sourceIndex + 2] * (brightnessLevel * this.blueTint); 295 | 296 | this.offscreenCanvasPixels.data[targetIndex] = ~~red; 297 | this.offscreenCanvasPixels.data[targetIndex + 1] = ~~green; 298 | this.offscreenCanvasPixels.data[targetIndex + 2] = ~~blue; 299 | this.offscreenCanvasPixels.data[targetIndex + 3] = 255; 300 | } 301 | targetIndex -= this.bytesPerPixel * this.canvasWidth; 302 | } 303 | } 304 | } 305 | 306 | drawFloor(wallBottom, castColumn, rayAng) { 307 | let targetIndex = wallBottom * (this.canvasWidth * this.bytesPerPixel) + this.bytesPerPixel * castColumn; 308 | 309 | for (let row = wallBottom + 1; row <= this.PROJECTIONPLANEHEIGHT; row++) { 310 | const straightDistance = 311 | (this.fPlayerHeight / (row - this.fProjectionPlaneYCenter)) * this.fPlayerDistanceToProjectionPlane; 312 | 313 | const diagDist = straightDistance * this.fFishTable[castColumn]; 314 | 315 | const xEnd = ~~(diagDist * Math.cos(rayAng) + this.fPlayerX); 316 | const yEnd = ~~(diagDist * Math.sin(rayAng) + this.fPlayerY); 317 | 318 | let brightnessLevel = this.currentLightValues?.[yEnd * this.mapWidth + xEnd] || this.minBrightness; 319 | if (brightnessLevel > this.maxBrightness) brightnessLevel = this.maxBrightness; 320 | 321 | let cellX = ~~(xEnd / this.TILE_SIZE); 322 | let cellY = ~~(yEnd / this.TILE_SIZE); 323 | 324 | let fIndex = 0; 325 | 326 | const tileIndex = cellY * this.mapCols + cellX; 327 | let type = this.map[tileIndex]; 328 | if (type >= 6) fIndex = type - 6; 329 | 330 | if (cellX < this.mapCols && cellY < this.mapRows && cellX >= 0 && cellY >= 0) { 331 | const tileRow = xEnd & (this.TILE_SIZE - 1); 332 | const tileCol = yEnd & (this.TILE_SIZE - 1); 333 | 334 | const sourceIndex = 335 | tileRow * this.fFloorTextureBufferList[fIndex].width * this.bytesPerPixel + 336 | this.bytesPerPixel * tileCol; 337 | 338 | const red = this.fFloorTexturePixelsList[fIndex][sourceIndex] * (brightnessLevel * this.redTint); 339 | const green = 340 | this.fFloorTexturePixelsList[fIndex][sourceIndex + 1] * (brightnessLevel * this.greenTint); 341 | const blue = 342 | this.fFloorTexturePixelsList[fIndex][sourceIndex + 2] * (brightnessLevel * this.blueTint); 343 | 344 | this.offscreenCanvasPixels.data[targetIndex] = ~~red; 345 | this.offscreenCanvasPixels.data[targetIndex + 1] = ~~green; 346 | this.offscreenCanvasPixels.data[targetIndex + 2] = ~~blue; 347 | this.offscreenCanvasPixels.data[targetIndex + 3] = 255; 348 | 349 | targetIndex += this.bytesPerPixel * this.canvasWidth; 350 | } 351 | } 352 | } 353 | 354 | drawWallSliceRectangleTinted( 355 | x, 356 | rectTop, 357 | height, 358 | xOffset, 359 | brightnessLevel, 360 | textureBuffer, 361 | texturePixels, 362 | textureBufferPainting, 363 | texturePixelsPainting 364 | ) { 365 | rectTop = Math.floor(rectTop); 366 | 367 | let sourceIndex = this.bytesPerPixel * xOffset; 368 | const lastSourceIndex = sourceIndex + textureBuffer.width * textureBuffer.height * this.bytesPerPixel; 369 | 370 | let targetIndex = this.canvasWidth * this.bytesPerPixel * rectTop + this.bytesPerPixel * x; 371 | 372 | let heightToDraw = height; 373 | 374 | if (rectTop + heightToDraw > this.canvasHeight) heightToDraw = this.canvasHeight - rectTop; 375 | 376 | if (heightToDraw < 0) return; 377 | 378 | let red; 379 | let green; 380 | let blue; 381 | let alpha; 382 | 383 | let yError = 0; 384 | let sourceRow; 385 | 386 | sourceRow = ~~(sourceIndex / (this.bytesPerPixel * textureBuffer.width)); 387 | 388 | let paintingSourceTop = null; 389 | let paintingSourceBottom = null; 390 | let paintingSourceLeft = null; 391 | let paintingSourceRight = null; 392 | 393 | let sourceIndexPainting = null; 394 | 395 | if (textureBufferPainting) { 396 | // Painting is present on column 397 | paintingSourceTop = textureBuffer.height / 2 - textureBufferPainting.height / 2; 398 | paintingSourceBottom = textureBuffer.height / 2 + textureBufferPainting.height / 2; 399 | paintingSourceLeft = textureBuffer.width / 2 - textureBufferPainting.width / 2; 400 | paintingSourceRight = textureBuffer.width / 2 + textureBufferPainting.width / 2; 401 | 402 | sourceIndexPainting = this.bytesPerPixel * (xOffset - paintingSourceLeft); 403 | } 404 | 405 | while (true) { 406 | yError += height; 407 | if (texturePixelsPainting) alpha = texturePixelsPainting[sourceIndexPainting + 3]; 408 | 409 | if ( 410 | textureBufferPainting && 411 | sourceRow > paintingSourceTop - 1 && 412 | sourceRow < paintingSourceBottom && 413 | xOffset > paintingSourceLeft - 1 && 414 | xOffset < paintingSourceRight 415 | ) { 416 | // Painting on column and within size of painting source 417 | if (alpha > 0) { 418 | red = texturePixelsPainting[sourceIndexPainting] * (brightnessLevel * this.redTint); 419 | green = texturePixelsPainting[sourceIndexPainting + 1] * (brightnessLevel * this.greenTint); 420 | blue = texturePixelsPainting[sourceIndexPainting + 2] * (brightnessLevel * this.blueTint); 421 | } else { 422 | red = texturePixels[sourceIndex] * (brightnessLevel * this.redTint); 423 | green = texturePixels[sourceIndex + 1] * (brightnessLevel * this.greenTint); 424 | blue = texturePixels[sourceIndex + 2] * (brightnessLevel * this.blueTint); 425 | } 426 | 427 | sourceIndexPainting += this.bytesPerPixel * textureBufferPainting.width; 428 | } else { 429 | red = texturePixels[sourceIndex] * (brightnessLevel * this.redTint); 430 | green = texturePixels[sourceIndex + 1] * (brightnessLevel * this.greenTint); 431 | blue = texturePixels[sourceIndex + 2] * (brightnessLevel * this.blueTint); 432 | } 433 | 434 | while (yError >= textureBuffer.height) { 435 | yError -= textureBuffer.height; 436 | this.offscreenCanvasPixels.data[targetIndex] = ~~red; 437 | this.offscreenCanvasPixels.data[targetIndex + 1] = ~~green; 438 | this.offscreenCanvasPixels.data[targetIndex + 2] = ~~blue; 439 | this.offscreenCanvasPixels.data[targetIndex + 3] = 255; 440 | targetIndex += this.bytesPerPixel * this.canvasWidth; 441 | 442 | heightToDraw--; 443 | if (heightToDraw < 1) return; 444 | } 445 | 446 | sourceIndex += this.bytesPerPixel * textureBuffer.width; 447 | if (sourceIndex > lastSourceIndex) sourceIndex = lastSourceIndex; 448 | sourceRow = ~~(sourceIndex / (this.bytesPerPixel * textureBuffer.width)); 449 | } 450 | } 451 | 452 | drawObjectStrip(x, y, height, brightness, xOffset, textureBuffer, texturePixels, inReticle, dist) { 453 | if (textureBuffer == undefined) return; 454 | const bytesPerPixel = 4; 455 | 456 | let sourceIndex = bytesPerPixel * xOffset; 457 | const lastSourceIndex = sourceIndex + textureBuffer.width * textureBuffer.height * bytesPerPixel; 458 | 459 | let targetIndex = this.canvasWidth * bytesPerPixel * y + bytesPerPixel * x; 460 | 461 | let heightToDraw = height; 462 | 463 | if (y + heightToDraw > this.canvasHeight) heightToDraw = this.canvasHeight - y; 464 | 465 | let yError = 0; 466 | 467 | if (heightToDraw < 0) return; 468 | 469 | while (true) { 470 | yError += height; 471 | 472 | let red = texturePixels[sourceIndex] * brightness * this.redTint; 473 | let green = texturePixels[sourceIndex + 1] * brightness * this.greenTint; 474 | let blue = texturePixels[sourceIndex + 2] * brightness * this.blueTint; 475 | let alpha = texturePixels[sourceIndex + 3]; 476 | if (inReticle) { 477 | red += 70; 478 | green += 70; 479 | blue += 70; 480 | } 481 | 482 | while (yError >= textureBuffer.height) { 483 | if (alpha > 0) { 484 | this.offscreenCanvasPixels.data[targetIndex] = ~~red; 485 | this.offscreenCanvasPixels.data[targetIndex + 1] = ~~green; 486 | this.offscreenCanvasPixels.data[targetIndex + 2] = ~~blue; 487 | this.offscreenCanvasPixels.data[targetIndex + 3] = 255; 488 | this.objectTargetIndecesForStrip.push(targetIndex); 489 | this.objectDistsForStrip.push(dist); 490 | } 491 | yError -= textureBuffer.height; 492 | targetIndex += bytesPerPixel * this.canvasWidth; 493 | 494 | heightToDraw--; 495 | if (heightToDraw < 1) return; 496 | } 497 | sourceIndex += bytesPerPixel * textureBuffer.width; 498 | if (sourceIndex > lastSourceIndex) sourceIndex = lastSourceIndex; 499 | } 500 | } 501 | 502 | drawThinWallStrip(x, y, height, brightness, thinWallRef, xOffset, dist) { 503 | if (this.fThinWallTextureBufferList[thinWallRef] == undefined) return; 504 | const bytesPerPixel = 4; 505 | 506 | let sourceIndex = bytesPerPixel * xOffset; 507 | const lastSourceIndex = 508 | sourceIndex + 509 | this.fThinWallTextureBufferList[thinWallRef].width * 510 | this.fThinWallTextureBufferList[thinWallRef].height * 511 | bytesPerPixel; 512 | 513 | let targetIndex = this.canvasWidth * bytesPerPixel * y + bytesPerPixel * x; 514 | 515 | let heightToDraw = height; 516 | 517 | if (y + heightToDraw > this.canvasHeight) heightToDraw = this.canvasHeight - y; 518 | 519 | let yError = 0; 520 | 521 | if (heightToDraw < 0) return; 522 | 523 | while (true) { 524 | yError += height; 525 | 526 | let red = this.fThinWallTexturePixelsList[thinWallRef][sourceIndex] * brightness * this.redTint; 527 | let green = this.fThinWallTexturePixelsList[thinWallRef][sourceIndex + 1] * brightness * this.greenTint; 528 | let blue = this.fThinWallTexturePixelsList[thinWallRef][sourceIndex + 2] * brightness * this.blueTint; 529 | let alpha = this.fThinWallTexturePixelsList[thinWallRef][sourceIndex + 3]; 530 | 531 | while (yError >= this.fThinWallTextureBufferList[thinWallRef].height) { 532 | yError -= this.fThinWallTextureBufferList[thinWallRef].height; 533 | if ( 534 | !this.objectTargetIndecesForStrip.includes(targetIndex) || 535 | this.objectDistsForStrip[this.objectTargetIndecesForStrip.indexOf(targetIndex)] > dist 536 | ) { 537 | // Blend pixel color values with transparent thin wall color values 538 | let redBlend; 539 | let greenBlend; 540 | let blueBlend; 541 | if (alpha > 0) { 542 | redBlend = (alpha / 255) * red + (1 - alpha / 255) * this.offscreenCanvasPixels.data[targetIndex]; 543 | greenBlend = 544 | (alpha / 255) * green + (1 - alpha / 255) * this.offscreenCanvasPixels.data[targetIndex + 1]; 545 | blueBlend = 546 | (alpha / 255) * blue + (1 - alpha / 255) * this.offscreenCanvasPixels.data[targetIndex + 2]; 547 | 548 | this.offscreenCanvasPixels.data[targetIndex] = ~~redBlend; 549 | this.offscreenCanvasPixels.data[targetIndex + 1] = ~~greenBlend; 550 | this.offscreenCanvasPixels.data[targetIndex + 2] = ~~blueBlend; 551 | this.offscreenCanvasPixels.data[targetIndex + 3] = 255; 552 | } 553 | } 554 | 555 | targetIndex += bytesPerPixel * this.canvasWidth; 556 | 557 | heightToDraw--; 558 | if (heightToDraw < 1) return; 559 | } 560 | sourceIndex += bytesPerPixel * this.fThinWallTextureBufferList[thinWallRef].width; 561 | if (sourceIndex > lastSourceIndex) sourceIndex = lastSourceIndex; 562 | } 563 | } 564 | 565 | draw3d() { 566 | for (let i = 0; i < this.items.length; i++) { 567 | // Get perpendicular line coords 568 | const deltaY = this.items[i].y - this.fPlayerY; 569 | const deltaX = this.items[i].x - this.fPlayerX; 570 | const slope = deltaY / deltaX; 571 | const perpSlope = -(1 / slope); 572 | const angle = Math.atan(perpSlope); 573 | let x1; 574 | let y1; 575 | let x2; 576 | let y2; 577 | x1 = this.items[i].x - (this.fItemTextureBufferList[i].width / 2) * Math.cos(angle); 578 | y1 = this.items[i].y - (this.fItemTextureBufferList[i].width / 2) * Math.sin(angle); 579 | x2 = this.items[i].x + (this.fItemTextureBufferList[i].width / 2) * Math.cos(angle); 580 | y2 = this.items[i].y + (this.fItemTextureBufferList[i].width / 2) * Math.sin(angle); 581 | 582 | const intersection = getIntersection( 583 | this.fPlayerX, 584 | this.fPlayerY, 585 | 1, 586 | degToRad(this.fPlayerAngle), 587 | x1, 588 | y1, 589 | x2, 590 | y2 591 | ); 592 | 593 | if (intersection?.[0]) { 594 | const dx = Math.abs(this.fPlayerX - intersection[0]); 595 | const dy = Math.abs(this.fPlayerY - intersection[1]); 596 | const d = Math.sqrt(dx * dx + dy * dy); 597 | 598 | if (d <= this.minUseDist) this.items[i].inReticle = true; 599 | else this.items[i].inReticle = false; 600 | } else this.items[i].inReticle = false; 601 | } 602 | 603 | for (let i = 0; i < this.rayLengths.length; i++) { 604 | if (this.rayLengths[i] === 0) return; 605 | let dist = this.rayLengths[i] / this.fFishTable[i]; 606 | 607 | const ratio = this.fPlayerDistanceToProjectionPlane / dist; 608 | const scale = (this.fPlayerDistanceToProjectionPlane * this.WALL_HEIGHT) / dist; 609 | const wallBottom = ratio * this.fPlayerHeight + this.fProjectionPlaneYCenter; 610 | const wallTop = wallBottom - scale; 611 | const wallHeight = wallBottom - wallTop; 612 | 613 | let textureBufferPainting = null; 614 | let texturePixelsPainting = null; 615 | 616 | loop: for (let j = 0; j < this.fPaintingDetails.length; j++) { 617 | const tileIndexPainting = this.fPaintingDetails[j].row * this.mapCols + this.fPaintingDetails[j].col; 618 | if ( 619 | tileIndexPainting === this.tileIndeces[i] && 620 | this.fPaintingDetails[j].side === this.tileSides[i] 621 | ) { 622 | textureBufferPainting = this.fPaintingTextureBufferList[j]; 623 | texturePixelsPainting = this.fPaintingTexturePixelsList[j]; 624 | break loop; 625 | } 626 | } 627 | 628 | let adjustedAngle = this.rayAngles[i] + degToRad(this.fPlayerAngle); 629 | if (adjustedAngle < 0) adjustedAngle += 2 * this.pi; 630 | 631 | if ( 632 | i === this.PROJECTIONPLANEWIDTH / 2 && 633 | wallTop <= this.PROJECTIONPLANEHEIGHT / 2 && 634 | wallBottom >= this.PROJECTIONPLANEHEIGHT / 2 635 | ) { 636 | this.reticleOnWall = true; 637 | } else if (i === this.PROJECTIONPLANEWIDTH / 2) this.reticleOnWall = false; 638 | 639 | let offset = 640 | this.tileSides?.[i] === 0 || this.tileSides?.[i] === 2 641 | ? this.tileCollisionsX[i] & (this.TILE_SIZE - 1) 642 | : this.tileCollisionsY[i] & (this.TILE_SIZE - 1); 643 | 644 | if (this.tileSides?.[i] === 0 || this.tileSides?.[i] === 1) offset = this.TILE_SIZE - offset - 1; 645 | 646 | let textureBuffer = this.fWallTextureBufferList[this.tileTypes?.[i]]; 647 | let texturePixels = this.fWallTexturePixelsList[this.tileTypes?.[i]]; 648 | 649 | let brightnessLevel = 650 | this.currentLightValues?.[~~this.tileCollisionsY[i] * this.mapWidth + ~~this.tileCollisionsX[i]]; 651 | if (brightnessLevel > this.maxBrightness) brightnessLevel = this.maxBrightness; 652 | if (!brightnessLevel || brightnessLevel < this.minBrightness) brightnessLevel = this.minBrightness; 653 | if (this.tileSides?.[i] === 1 || this.tileSides?.[i] === 3) brightnessLevel *= 0.85; 654 | 655 | if (this?.fSkyTextureBuffer) this.drawSky(i, adjustedAngle); 656 | 657 | this.drawFloor(Math.floor(wallBottom), i, adjustedAngle); 658 | 659 | this.drawCeiling(Math.floor(wallTop), i, adjustedAngle); 660 | 661 | this.drawWallSliceRectangleTinted( 662 | i, 663 | wallTop, 664 | wallHeight + 1, 665 | offset, 666 | brightnessLevel, 667 | textureBuffer, 668 | texturePixels, 669 | textureBufferPainting, 670 | texturePixelsPainting 671 | ); 672 | 673 | this.objectTargetIndecesForStrip = []; 674 | this.objectDistsForStrip = []; 675 | // Objects 676 | for (let j = 0; j < this.objectRayLengths[i].length; j++) { 677 | let textureBuffer; 678 | let texturePixels; 679 | 680 | let objDist = 681 | this.objectRayLengths[i][j] > 0 ? this.objectRayLengths[i][j] / this.fFishTable[i] : null; 682 | 683 | if (objDist) { 684 | let objRatio; 685 | let objScale; 686 | let objBottom; 687 | let objTop; 688 | let objHeight; 689 | if (this.isItemRay[i][j]) { 690 | textureBuffer = this.fItemTextureBufferList[this.objectRefs[i][j]]; 691 | texturePixels = this.fItemTexturePixelsList[this.objectRefs[i][j]]; 692 | } else if (this.isLightsourceRay[i][j]) { 693 | textureBuffer = this.fLightTextureBufferList[this.objectRefs[i][j]]; 694 | texturePixels = this.fLightTexturePixelsList[this.objectRefs[i][j]]; 695 | } else { 696 | textureBuffer = this.fObjectTextureBufferList[this.objectRefs[i][j]]; 697 | texturePixels = this.fObjectTexturePixelsList[this.objectRefs[i][j]]; 698 | } 699 | 700 | objRatio = this.fPlayerDistanceToProjectionPlane / objDist; 701 | objScale = (this.fPlayerDistanceToProjectionPlane * textureBuffer.height) / objDist; 702 | objBottom = objRatio * this.fPlayerHeight + this.fProjectionPlaneYCenter; 703 | objTop = objBottom - objScale; 704 | objHeight = objBottom - objTop; 705 | 706 | if (this.isLightsourceRay[i][j]) { 707 | switch (this.lightSources[this.objectRefs[i][j]]?.surface) { 708 | case 'wall': 709 | objScale = 710 | (this.fPlayerDistanceToProjectionPlane * 711 | (this.WALL_HEIGHT / 2 + textureBuffer.height / 2)) / 712 | objDist; 713 | break; 714 | case 'ceiling': 715 | objScale = (this.fPlayerDistanceToProjectionPlane * this.WALL_HEIGHT) / objDist; 716 | break; 717 | } 718 | objTop = objBottom - objScale; 719 | } 720 | 721 | if ( 722 | this.isItemRay[i][j] && 723 | this.items[this.objectRefs[i][j]].inReticle && 724 | objTop <= this.canvasHeight / 2 && 725 | objBottom >= this.canvasHeight / 2 726 | ) { 727 | this.items[this.objectRefs[i][j]].inReticle = true; 728 | } else if (this.isItemRay[i][j]) this.items[this.objectRefs[i][j]].inReticle = false; 729 | 730 | let objBrightnessLevel = 731 | this.currentLightValues?.[ 732 | ~~this.objectCollisionsY[i][j] * this.mapWidth + ~~this.objectCollisionsX[i][j] 733 | ] || this.minBrightness; 734 | if (objBrightnessLevel > this.maxBrightness) objBrightnessLevel = this.maxBrightness; 735 | 736 | this.drawObjectStrip( 737 | i, 738 | Math.floor(objTop), 739 | objHeight, 740 | objBrightnessLevel, 741 | this.objectOffsets[i][j], 742 | textureBuffer, 743 | texturePixels, 744 | this.isItemRay[i][j] ? this.items[this.objectRefs[i][j]].inReticle : false, 745 | objDist 746 | ); 747 | } 748 | } 749 | 750 | let thinWallDist = 751 | this.thinWallRayLengths[i] > 0 ? this.thinWallRayLengths[i] / this.fFishTable[i] : null; 752 | 753 | if (thinWallDist) { 754 | let thinWallBrightnessLevel = 755 | this.currentLightValues?.[ 756 | ~~this.thinWallCollisionsY[i] * this.mapWidth + ~~this.thinWallCollisionsX[i] 757 | ] || this.minBrightness; 758 | if (thinWallBrightnessLevel > this.maxBrightness) thinWallBrightnessLevel = this.maxBrightness; 759 | 760 | const thinWallRatio = this.fPlayerDistanceToProjectionPlane / thinWallDist; 761 | const thinWallScale = (this.fPlayerDistanceToProjectionPlane * this.WALL_HEIGHT) / thinWallDist; 762 | const thinWallBottom = thinWallRatio * this.fPlayerHeight + this.fProjectionPlaneYCenter; 763 | const thinWallTop = thinWallBottom - thinWallScale; 764 | let thinWallHeight = thinWallBottom - thinWallTop; 765 | 766 | if ( 767 | i === this.PROJECTIONPLANEWIDTH / 2 && 768 | thinWallTop <= this.PROJECTIONPLANEHEIGHT / 2 && 769 | thinWallBottom >= this.PROJECTIONPLANEHEIGHT / 2 770 | ) { 771 | this.reticleOnWall = true; 772 | } else if (i === this.PROJECTIONPLANEWIDTH / 2) this.reticleOnWall = false; 773 | 774 | this.drawThinWallStrip( 775 | i, 776 | Math.floor(thinWallTop), 777 | thinWallHeight, 778 | thinWallBrightnessLevel, 779 | this.thinWallRefs[i], 780 | this.thinWallOffsets[i], 781 | thinWallDist 782 | ); 783 | } 784 | } 785 | } 786 | 787 | draw2d() { 788 | for (let i = 0; i < this.mapRows; i++) { 789 | for (let j = 0; j < this.mapCols; j++) { 790 | const tile = this.map[i * this.mapCols + j]; 791 | 792 | this.debugCtx.fillStyle = `rgb(${(tile + 1) / 0.1}, ${(tile + 1) / 0.1}, ${(tile + 1) / 0.1})`; 793 | this.debugCtx.beginPath(); 794 | this.debugCtx.fillRect(j * this.TILE_SIZE, i * this.TILE_SIZE, this.TILE_SIZE, this.TILE_SIZE); 795 | } 796 | } 797 | 798 | for (let i = 0; i < this.objects.length; i++) { 799 | this.debugCtx.fillStyle = `rgb(0, 100, 255)`; 800 | this.debugCtx.beginPath(); 801 | this.debugCtx.ellipse( 802 | this.objects[i].x, 803 | this.objects[i].y, 804 | this.fObjectTextureBufferList[i].width / 2, 805 | this.fObjectTextureBufferList[i].width / 2, 806 | 2 * this.pi, 807 | 0, 808 | 2 * this.pi 809 | ); 810 | this.debugCtx.fill(); 811 | } 812 | 813 | for (let i = 0; i < this.items.length; i++) { 814 | this.debugCtx.fillStyle = `rgb(0, 255, 0)`; 815 | this.debugCtx.beginPath(); 816 | this.debugCtx.ellipse( 817 | this.items[i].x, 818 | this.items[i].y, 819 | this.fItemTextureBufferList[i].width / 2, 820 | this.fItemTextureBufferList[i].width / 2, 821 | 2 * this.pi, 822 | 0, 823 | 2 * this.pi 824 | ); 825 | this.debugCtx.fill(); 826 | } 827 | 828 | for (let i = 0; i < this.thinWalls.length; i++) { 829 | const wall = this.thinWalls[i]; 830 | 831 | this.debugCtx.strokeStyle = `rgb(255, 0, 255)`; 832 | this.debugCtx.lineWidth = 10; 833 | this.debugCtx.beginPath(); 834 | this.debugCtx.moveTo(wall.xStart, wall.yStart); 835 | this.debugCtx.lineTo(wall.xEnd, wall.yEnd); 836 | this.debugCtx.stroke(); 837 | } 838 | } 839 | 840 | getIntersectionOfTile(x, y, row, col, theta, sides = [0, 1, 2, 3], p4 = null) { 841 | const x1 = col * this.TILE_SIZE; 842 | const y1 = row * this.TILE_SIZE; 843 | 844 | const x2 = x1 + this.TILE_SIZE; 845 | const y2 = y1; 846 | 847 | const x3 = x2; 848 | const y3 = y1 + this.TILE_SIZE; 849 | 850 | const x4 = x1; 851 | const y4 = y3; 852 | 853 | let record = Infinity; 854 | let closest = null; 855 | let dir = 0; 856 | 857 | let tX1 = 0; 858 | let tY1 = 0; 859 | let tX2 = 0; 860 | let tY2 = 0; 861 | 862 | for (let i = 0; i < sides.length; i++) { 863 | switch (sides[i]) { 864 | case 0: 865 | tX1 = x1; 866 | tY1 = y1; 867 | tX2 = x2; 868 | tY2 = y2; 869 | break; 870 | case 1: 871 | tX1 = x2; 872 | tY1 = y2; 873 | tX2 = x3; 874 | tY2 = y3; 875 | break; 876 | case 2: 877 | tX1 = x3; 878 | tY1 = y3; 879 | tX2 = x4; 880 | tY2 = y4; 881 | break; 882 | case 3: 883 | tX1 = x4; 884 | tY1 = y4; 885 | tX2 = x1; 886 | tY2 = y1; 887 | break; 888 | } 889 | 890 | const intersection = getIntersection(x, y, 1, theta, tX1, tY1, tX2, tY2, p4); 891 | if (intersection?.[0]) { 892 | const dx = Math.abs(x - intersection[0]); 893 | const dy = Math.abs(y - intersection[1]); 894 | const d = Math.sqrt(dx * dx + dy * dy); 895 | record = Math.min(d, record); 896 | if (d <= record) { 897 | record = d; 898 | closest = intersection; 899 | dir = sides[i]; 900 | } 901 | } 902 | } 903 | 904 | return { 905 | record, 906 | closest, 907 | dir, 908 | }; 909 | } 910 | 911 | raycaster() { 912 | let tileTypeTemp = 0; 913 | let tileSideDirTemp = 0; 914 | 915 | for (let i = 0; i < this.rayAngles.length; i++) { 916 | let adjustedAngle; 917 | adjustedAngle = this.rayAngles[i] + degToRad(this.fPlayerAngle); 918 | if (adjustedAngle < 0) adjustedAngle += 2 * this.pi; 919 | 920 | let closest = null; 921 | let record = Infinity; 922 | let thinWallRecord = Infinity; 923 | let thinWallClosest = null; 924 | 925 | // Filter through thin walls for each ray -------------------------------------------------------- 926 | this.thinWallRayLengths[i] = 0; 927 | this.thinWallCollisionsX[i] = 0; 928 | this.thinWallCollisionsY[i] = 0; 929 | this.thinWallRefs[i] = 0; 930 | this.thinWallOffsets[i] = 0; 931 | 932 | for (let j = 0; j < this.thinWalls.length; j++) { 933 | const intersection = getIntersection( 934 | this.fPlayerX, 935 | this.fPlayerY, 936 | 1, 937 | adjustedAngle, 938 | this.thinWalls[j].xStart, 939 | this.thinWalls[j].yStart, 940 | this.thinWalls[j].xEnd, 941 | this.thinWalls[j].yEnd 942 | ); 943 | 944 | if (intersection?.[0]) { 945 | const dx = Math.abs(this.fPlayerX - intersection[0]); 946 | const dy = Math.abs(this.fPlayerY - intersection[1]); 947 | const d = Math.sqrt(dx * dx + dy * dy); 948 | 949 | if (d < thinWallRecord) { 950 | thinWallRecord = d; 951 | thinWallClosest = intersection; 952 | this.thinWallRayLengths[i] = d; 953 | this.thinWallCollisionsX[i] = intersection[0]; 954 | this.thinWallCollisionsY[i] = intersection[1]; 955 | this.thinWallRefs[i] = j; 956 | this.thinWallOffsets[i] = ~~Math.sqrt( 957 | (intersection[0] - this.thinWalls[j].xStart) * (intersection[0] - this.thinWalls[j].xStart) + 958 | (intersection[1] - this.thinWalls[j].yStart) * (intersection[1] - this.thinWalls[j].yStart) 959 | ); 960 | } 961 | } 962 | } 963 | 964 | //------------------------------------------------------------------------------------------- 965 | 966 | this.rayAngleQuadrants[i] = Math.floor(adjustedAngle / (this.pi / 2)); 967 | 968 | let sidesToCheck = [0, 1, 2, 3]; 969 | if (this.rayAngleQuadrants[i] === 0) sidesToCheck = [0, 3]; 970 | else if (this.rayAngleQuadrants[i] === 1) sidesToCheck = [0, 1]; 971 | else if (this.rayAngleQuadrants[i] === 2) sidesToCheck = [1, 2]; 972 | else if (this.rayAngleQuadrants[i] === 3) sidesToCheck = [2, 3]; 973 | 974 | let tileIndex = 0; 975 | for (let row = 0; row < this.mapRows; row++) { 976 | for (let col = 0; col < this.mapCols; col++) { 977 | const tile = this.map[row * this.mapCols + col]; 978 | if (tile > 5) continue; 979 | 980 | const tileIntersection = this.getIntersectionOfTile( 981 | this.fPlayerX, 982 | this.fPlayerY, 983 | row, 984 | col, 985 | adjustedAngle, 986 | sidesToCheck 987 | ); 988 | 989 | if (tileIntersection.record < record) { 990 | record = tileIntersection.record; 991 | closest = tileIntersection.closest; 992 | tileIndex = row * this.mapCols + col; 993 | tileTypeTemp = tile; 994 | tileSideDirTemp = tileIntersection.dir; 995 | 996 | if (record < thinWallRecord) this.thinWallRayLengths[i] = 0; 997 | } 998 | } 999 | } 1000 | 1001 | if (closest) { 1002 | this.rayLengths[i] = ~~record; 1003 | this.tileCollisionsX[i] = closest[0]; 1004 | this.tileCollisionsY[i] = closest[1]; 1005 | this.tileTypes[i] = tileTypeTemp; 1006 | this.tileSides[i] = tileSideDirTemp; 1007 | this.tileIndeces[i] = tileIndex; 1008 | } else this.rayLengths[i] = 0; 1009 | 1010 | // Draw rays on debug canvas ---------------------------------------------------------------- 1011 | if (this.DEBUG) { 1012 | if (record < thinWallRecord) { 1013 | this.debugCtx.strokeStyle = 1014 | i === this.rayAngles.length ? `rgba(0,255,0,0.7)` : `rgba(255,255,255,0.3)`; 1015 | this.debugCtx.beginPath(); 1016 | this.debugCtx.moveTo(this.fPlayerX, this.fPlayerY); 1017 | this.debugCtx.lineTo(closest[0], closest[1]); 1018 | this.debugCtx.lineWidth = 1; 1019 | this.debugCtx.stroke(); 1020 | } else { 1021 | if (this.DEBUG) { 1022 | this.debugCtx.strokeStyle = `rgba(255,0,255,0.3)`; 1023 | this.debugCtx.beginPath(); 1024 | this.debugCtx.moveTo(this.fPlayerX, this.fPlayerY); 1025 | this.debugCtx.lineTo(thinWallClosest[0], thinWallClosest[1]); 1026 | this.debugCtx.lineWidth = 1; 1027 | this.debugCtx.stroke(); 1028 | } 1029 | } 1030 | } 1031 | 1032 | // Filter through objects and items for each ray ----------------------------------------------------------- 1033 | this.objectRayLengths[i] = []; 1034 | this.objectCollisionsX[i] = []; 1035 | this.objectCollisionsY[i] = []; 1036 | this.objectRefs[i] = []; 1037 | this.objectOffsets[i] = []; 1038 | this.isItemRay[i] = []; 1039 | this.isLightsourceRay[i] = []; 1040 | 1041 | const rayObjData = [ 1042 | { 1043 | rayLength: 0, 1044 | collisionX: 0, 1045 | collisionY: 0, 1046 | ref: 0, 1047 | offset: 0, 1048 | isItemRay: 0, 1049 | isLightsourceRay: 0, 1050 | }, 1051 | ]; 1052 | 1053 | for (let j = 0; j < this.objects.length + this.items.length + this.lightSources.length; j++) { 1054 | let object; 1055 | let textureBuffer; 1056 | let ref; 1057 | let isItem = false; 1058 | let isLightsource = false; 1059 | if (j < this.objects.length) { 1060 | object = this.objects[j]; 1061 | textureBuffer = this.fObjectTextureBufferList[j]; 1062 | ref = j; 1063 | } else if (j >= this.objects.length && j < this.objects.length + this.items.length) { 1064 | ref = j - this.objects.length; 1065 | object = this.items[ref]; 1066 | textureBuffer = this.fItemTextureBufferList[ref]; 1067 | isItem = true; 1068 | } else { 1069 | ref = j - this.objects.length - this.items.length; 1070 | object = this.lightSources[ref]; 1071 | textureBuffer = this.fLightTextureBufferList[ref]; 1072 | isLightsource = true; 1073 | } 1074 | // Get perpendicular line coords 1075 | const deltaY = object.y - this.fPlayerY; 1076 | const deltaX = object.x - this.fPlayerX; 1077 | const slope = deltaY / deltaX; 1078 | const perpSlope = -(1 / slope); 1079 | const angle = Math.atan(perpSlope); 1080 | const angle2 = Math.atan2(deltaY, deltaX); 1081 | let x1; 1082 | let y1; 1083 | let x2; 1084 | let y2; 1085 | if (angle2 < 0) { 1086 | x1 = object.x - (textureBuffer.width / 2) * Math.cos(angle); 1087 | y1 = object.y - (textureBuffer.width / 2) * Math.sin(angle); 1088 | x2 = object.x + (textureBuffer.width / 2) * Math.cos(angle); 1089 | y2 = object.y + (textureBuffer.width / 2) * Math.sin(angle); 1090 | } else { 1091 | x1 = object.x + (textureBuffer.width / 2) * Math.cos(angle); 1092 | y1 = object.y + (textureBuffer.width / 2) * Math.sin(angle); 1093 | x2 = object.x - (textureBuffer.width / 2) * Math.cos(angle); 1094 | y2 = object.y - (textureBuffer.width / 2) * Math.sin(angle); 1095 | } 1096 | 1097 | const intersection = getIntersection(this.fPlayerX, this.fPlayerY, 1, adjustedAngle, x1, y1, x2, y2); 1098 | 1099 | if (intersection?.[0]) { 1100 | const dx = Math.abs(this.fPlayerX - intersection[0]); 1101 | const dy = Math.abs(this.fPlayerY - intersection[1]); 1102 | const d = Math.sqrt(dx * dx + dy * dy); 1103 | 1104 | if (d < record) { 1105 | rayObjData.push({ 1106 | rayLength: ~~d, 1107 | collisionX: intersection[0], 1108 | collisionY: intersection[1], 1109 | ref: ref, 1110 | offset: ~~Math.sqrt( 1111 | (intersection[0] - x1) * (intersection[0] - x1) + 1112 | (intersection[1] - y1) * (intersection[1] - y1) 1113 | ), 1114 | isItemRay: isItem ? 1 : 0, 1115 | isLightsourceRay: isLightsource ? 1 : 0, 1116 | }); 1117 | 1118 | if (this.DEBUG && d < thinWallRecord) { 1119 | this.debugCtx.strokeStyle = isItem ? `rgba(0,255,0,0.3)` : `rgba(0,100,255,0.3)`; 1120 | this.debugCtx.beginPath(); 1121 | this.debugCtx.moveTo(this.fPlayerX, this.fPlayerY); 1122 | this.debugCtx.lineTo(intersection[0], intersection[1]); 1123 | this.debugCtx.lineWidth = 1; 1124 | this.debugCtx.stroke(); 1125 | } 1126 | } 1127 | } 1128 | } 1129 | 1130 | rayObjData.sort((a, b) => b.rayLength - a.rayLength); 1131 | 1132 | for (let j = 0; j < rayObjData.length; j++) { 1133 | this.objectRayLengths[i].push(rayObjData[j].rayLength); 1134 | this.objectCollisionsX[i].push(rayObjData[j].collisionX); 1135 | this.objectCollisionsY[i].push(rayObjData[j].collisionY); 1136 | this.objectRefs[i].push(rayObjData[j].ref); 1137 | this.objectOffsets[i].push(rayObjData[j].offset); 1138 | this.isItemRay[i].push(rayObjData[j].isItemRay); 1139 | this.isLightsourceRay[i].push(rayObjData[j].isLightsourceRay); 1140 | } 1141 | // if (this.isLightsourceRay[i]) console.log(this.lightSources[0]); 1142 | } 1143 | } 1144 | 1145 | rotate() { 1146 | if (this.fRotationDir === 'left') { 1147 | this.fPlayerAngle -= 4; 1148 | } else if (this.fRotationDir === 'right') { 1149 | this.fPlayerAngle += 4; 1150 | } 1151 | } 1152 | 1153 | setMoveDir() { 1154 | if (this.fKeyForward && !this.fKeyRight && !this.fKeyLeft) { 1155 | // forward 1156 | this.fPlayerMoveDir = this.fPlayerAngle; 1157 | } else if (this.fKeyBack && !this.fKeyRight && !this.fKeyLeft) { 1158 | // backwards 1159 | this.fPlayerMoveDir = this.fPlayerAngle + 180; 1160 | } else if (this.fKeyRight && !this.fKeyForward && !this.fKeyBack) { 1161 | // right 1162 | this.fPlayerMoveDir = this.fPlayerAngle + 90; 1163 | } else if (this.fKeyLeft && !this.fKeyForward && !this.fKeyBack) { 1164 | // left 1165 | this.fPlayerMoveDir = this.fPlayerAngle - 90; 1166 | } else if (this.fKeyForward && this.fKeyRight) { 1167 | // forward-right 1168 | this.fPlayerMoveDir = this.fPlayerAngle + 45; 1169 | } else if (this.fKeyForward && this.fKeyLeft) { 1170 | // forward-left 1171 | this.fPlayerMoveDir = this.fPlayerAngle - 45; 1172 | } else if (this.fKeyBack && this.fKeyRight) { 1173 | // backwards-right 1174 | this.fPlayerMoveDir = this.fPlayerAngle + 135; 1175 | } else if (this.fKeyBack && this.fKeyLeft) { 1176 | // backwards-left 1177 | this.fPlayerMoveDir = this.fPlayerAngle - 135; 1178 | } 1179 | } 1180 | 1181 | playerTooCloseToWall(row, col, minDist) { 1182 | const tileMidX = col * this.TILE_SIZE + this.TILE_SIZE / 2; 1183 | const tileMidY = row * this.TILE_SIZE + this.TILE_SIZE / 2; 1184 | 1185 | const dx = this.fPlayerX - tileMidX; 1186 | const dy = this.fPlayerY - tileMidY; 1187 | const d = Math.sqrt(dx * dx + dy * dy); 1188 | 1189 | if (d <= minDist) return [dx, dy]; 1190 | return; 1191 | } 1192 | 1193 | getXspeed = () => this.fPlayerMoveSpeed * Math.cos(degToRad(this.fPlayerMoveDir)); 1194 | 1195 | getYspeed = () => this.fPlayerMoveSpeed * Math.sin(degToRad(this.fPlayerMoveDir)); 1196 | 1197 | move() { 1198 | if (this.levelTransition) return; 1199 | this.rotate(); 1200 | 1201 | const playerTileCol = ~~(this.fPlayerX / this.TILE_SIZE); 1202 | const playerTileRow = ~~(this.fPlayerY / this.TILE_SIZE); 1203 | 1204 | this.setMoveDir(); 1205 | let moveDir = convertDeg0To360(this.fPlayerMoveDir); 1206 | let newPlayerX = null; 1207 | let newPlayerY = null; 1208 | 1209 | const minDist = (this.TILE_SIZE * Math.sqrt(2)) / 1.5; 1210 | if (this.fKeyForward || this.fKeyBack || this.fKeyLeft || this.fKeyRight) { 1211 | if (!this.isJumping && Date.now() - this.timeOfLastFootstep > (this.isCrouching ? 1000 : 500)) { 1212 | const footstepNum = ~~(Math.random() * (5 - 1) + 1); 1213 | this.audio.playSound(`footstep${footstepNum}`, this.fPlayerX, this.fPlayerY, false); 1214 | this.timeOfLastFootstep = Date.now(); 1215 | } 1216 | for (let i = 0; i < this.thinWalls.length; i++) { 1217 | const intersection = getIntersection( 1218 | this.fPlayerX, 1219 | this.fPlayerY, 1220 | 1, 1221 | degToRad(moveDir), 1222 | this.thinWalls[i].xStart, 1223 | this.thinWalls[i].yStart, 1224 | this.thinWalls[i].xEnd, 1225 | this.thinWalls[i].yEnd 1226 | ); 1227 | 1228 | if (intersection?.[0]) { 1229 | const dx = this.fPlayerX - intersection[0]; 1230 | const dy = this.fPlayerY - intersection[1]; 1231 | const d = Math.sqrt(dx * dx + dy * dy); 1232 | 1233 | if (d <= minDist && !(this.thinWalls[i].vaultable && this.isJumping)) return; 1234 | } 1235 | } 1236 | 1237 | for (let row = 0; row < this.mapRows; row++) { 1238 | loop1: for (let col = 0; col < this.mapCols; col++) { 1239 | const tileIndex = row * this.mapCols + col; 1240 | const tile = this.map[tileIndex]; 1241 | if (tile > 5 || (row === playerTileRow && col === playerTileCol)) continue; 1242 | 1243 | if (Math.abs(col - playerTileCol) <= 1 && Math.abs(row - playerTileRow) <= 1) { 1244 | const closeDistToTile = this.playerTooCloseToWall(row, col, minDist); 1245 | 1246 | if (closeDistToTile) { 1247 | const angleToWallCenter = radToDeg( 1248 | Math.atan2(Math.abs(closeDistToTile[1]), Math.abs(closeDistToTile[0])) 1249 | ); 1250 | 1251 | if (angleToWallCenter >= 0 && angleToWallCenter < 45) { 1252 | // On left or right of wall 1253 | const playerWallTileDiffCol = playerTileCol - col; 1254 | if (playerWallTileDiffCol > 0 && (moveDir > 270 || moveDir < 90)) break loop1; 1255 | else if (playerWallTileDiffCol < 0 && moveDir < 270 && moveDir > 90) break loop1; 1256 | 1257 | newPlayerY = this.fPlayerY + this.getYspeed(); 1258 | } else if (angleToWallCenter >= 45 && angleToWallCenter < 90) { 1259 | // On top or bottom of wall 1260 | const playerWallTileDiffRow = playerTileRow - row; 1261 | if (playerWallTileDiffRow > 0 && moveDir > 0 && moveDir < 180) break loop1; 1262 | else if (playerWallTileDiffRow < 0 && moveDir > 180 && moveDir < 360) break loop1; 1263 | 1264 | newPlayerX = this.fPlayerX + this.getXspeed(); 1265 | } 1266 | } 1267 | } 1268 | } 1269 | } 1270 | 1271 | if (newPlayerX && newPlayerY) return; 1272 | else if (newPlayerX) { 1273 | this.fPlayerX = newPlayerX; 1274 | return; 1275 | } else if (newPlayerY) { 1276 | this.fPlayerY = newPlayerY; 1277 | return; 1278 | } 1279 | } 1280 | 1281 | moveDir = degToRad(moveDir); 1282 | 1283 | if (this.fKeyForward || this.fKeyBack || this.fKeyLeft || this.fKeyRight) { 1284 | this.fPlayerX += this.fPlayerMoveSpeed * Math.cos(moveDir); 1285 | this.fPlayerY += this.fPlayerMoveSpeed * Math.sin(moveDir); 1286 | } 1287 | } 1288 | 1289 | onWallTextureLoaded(imgNames) { 1290 | this.fWallTextureBufferList = new Array(imgNames.length); 1291 | this.fWallTexturePixelsList = new Array(imgNames.length); 1292 | for (let i = 0; i < imgNames.length; i++) { 1293 | const img = this.textures[imgNames[i]]; 1294 | this.fWallTextureBufferList[i] = new OffscreenCanvas(img.width, img.height); 1295 | this.fWallTextureBufferList[i].getContext('2d', { alpha: false }).drawImage(img, 0, 0); 1296 | 1297 | const imgData = this.fWallTextureBufferList[i] 1298 | .getContext('2d', { alpha: false }) 1299 | .getImageData(0, 0, this.fWallTextureBufferList[i].width, this.fWallTextureBufferList[i].height); 1300 | this.fWallTexturePixelsList[i] = imgData.data; 1301 | } 1302 | } 1303 | 1304 | onCeilingTextureLoaded(imgName) { 1305 | const img = this.textures[imgName]; 1306 | this.fCeilingTextureBuffer = new OffscreenCanvas(img.width, img.height); 1307 | this.fCeilingTextureBuffer.getContext('2d', { alpha: false }).drawImage(img, 0, 0); 1308 | 1309 | const imgData = this.fCeilingTextureBuffer 1310 | .getContext('2d', { alpha: false }) 1311 | .getImageData(0, 0, this.fCeilingTextureBuffer.width, this.fCeilingTextureBuffer.height); 1312 | this.fCeilingTexturePixels = imgData.data; 1313 | } 1314 | 1315 | onSkyTextureLoaded(imgName) { 1316 | const img = this.textures[imgName]; 1317 | this.fSkyTextureBuffer = new OffscreenCanvas(img.width, img.height); 1318 | this.fSkyTextureBuffer.getContext('2d', { alpha: false }).drawImage(img, 0, 0); 1319 | 1320 | const imgData = this.fSkyTextureBuffer 1321 | .getContext('2d', { alpha: false }) 1322 | .getImageData(0, 0, this.fSkyTextureBuffer.width, this.fSkyTextureBuffer.height); 1323 | this.fSkyTexturePixels = imgData.data; 1324 | } 1325 | 1326 | onFloorTextureLoaded(imgNames) { 1327 | this.fFloorTextureBufferList = new Array(imgNames.length); 1328 | this.fFloorTexturePixelsList = new Array(imgNames.length); 1329 | for (let i = 0; i < imgNames.length; i++) { 1330 | const img = this.textures[imgNames[i]]; 1331 | this.fFloorTextureBufferList[i] = new OffscreenCanvas(img.width, img.height); 1332 | this.fFloorTextureBufferList[i].getContext('2d', { alpha: false }).drawImage(img, 0, 0); 1333 | 1334 | const imgData = this.fFloorTextureBufferList[i] 1335 | .getContext('2d', { alpha: false }) 1336 | .getImageData(0, 0, this.fFloorTextureBufferList[i].width, this.fFloorTextureBufferList[i].height); 1337 | this.fFloorTexturePixelsList[i] = imgData.data; 1338 | } 1339 | } 1340 | 1341 | onPaintingTexturesLoaded(imgNames) { 1342 | this.fPaintingTextureBufferList = new Array(imgNames.length); 1343 | this.fPaintingTexturePixelsList = new Array(imgNames.length); 1344 | 1345 | for (let i = 0; i < imgNames.length; i++) { 1346 | const img = this.textures[imgNames[i]]; 1347 | this.fPaintingTextureBufferList[i] = new OffscreenCanvas(img.width, img.height); 1348 | this.fPaintingTextureBufferList[i].getContext('2d', { alpha: true }).drawImage(img, 0, 0); 1349 | 1350 | const imgData = this.fPaintingTextureBufferList[i] 1351 | .getContext('2d', { alpha: false }) 1352 | .getImageData( 1353 | 0, 1354 | 0, 1355 | this.fPaintingTextureBufferList[i].width, 1356 | this.fPaintingTextureBufferList[i].height 1357 | ); 1358 | this.fPaintingTexturePixelsList[i] = imgData.data; 1359 | } 1360 | } 1361 | 1362 | onObjectTexturesLoaded(imgNames) { 1363 | this.fObjectTextureBufferList = new Array(imgNames.length); 1364 | this.fObjectTexturePixelsList = new Array(imgNames.length); 1365 | 1366 | for (let i = 0; i < imgNames.length; i++) { 1367 | const img = this.textures[imgNames[i]]; 1368 | this.fObjectTextureBufferList[i] = new OffscreenCanvas(img.width, img.height); 1369 | this.fObjectTextureBufferList[i].getContext('2d', { alpha: true }).drawImage(img, 0, 0); 1370 | 1371 | const imgData = this.fObjectTextureBufferList[i] 1372 | .getContext('2d', { alpha: false }) 1373 | .getImageData(0, 0, this.fObjectTextureBufferList[i].width, this.fObjectTextureBufferList[i].height); 1374 | this.fObjectTexturePixelsList[i] = imgData.data; 1375 | } 1376 | } 1377 | 1378 | onThinWallTexturesLoaded(imgNames) { 1379 | this.fThinWallTextureBufferList = new Array(imgNames.length); 1380 | this.fThinWallTexturePixelsList = new Array(imgNames.length); 1381 | 1382 | for (let i = 0; i < imgNames.length; i++) { 1383 | const img = this.textures[imgNames[i]]; 1384 | this.fThinWallTextureBufferList[i] = new OffscreenCanvas(img.width, img.height); 1385 | this.fThinWallTextureBufferList[i].getContext('2d', { alpha: true }).drawImage(img, 0, 0); 1386 | 1387 | const imgData = this.fThinWallTextureBufferList[i] 1388 | .getContext('2d', { alpha: false }) 1389 | .getImageData( 1390 | 0, 1391 | 0, 1392 | this.fThinWallTextureBufferList[i].width, 1393 | this.fThinWallTextureBufferList[i].height 1394 | ); 1395 | this.fThinWallTexturePixelsList[i] = imgData.data; 1396 | } 1397 | } 1398 | 1399 | onItemTexturesLoaded(imgNames) { 1400 | this.fItemTextureBufferList = new Array(imgNames.length); 1401 | this.fItemTexturePixelsList = new Array(imgNames.length); 1402 | 1403 | for (let i = 0; i < imgNames.length; i++) { 1404 | const img = this.textures[imgNames[i]]; 1405 | this.fItemTextureBufferList[i] = new OffscreenCanvas(img.width, img.height); 1406 | this.fItemTextureBufferList[i].getContext('2d', { alpha: true }).drawImage(img, 0, 0); 1407 | 1408 | const imgData = this.fItemTextureBufferList[i] 1409 | .getContext('2d', { alpha: false }) 1410 | .getImageData(0, 0, this.fItemTextureBufferList[i].width, this.fItemTextureBufferList[i].height); 1411 | this.fItemTexturePixelsList[i] = imgData.data; 1412 | } 1413 | } 1414 | 1415 | onLightTexturesLoaded(imgNames) { 1416 | this.fLightTextureBufferList = new Array(imgNames.length); 1417 | this.fLightTexturePixelsList = new Array(imgNames.length); 1418 | 1419 | for (let i = 0; i < imgNames.length; i++) { 1420 | const img = this.textures[imgNames[i]]; 1421 | this.fLightTextureBufferList[i] = new OffscreenCanvas(img.width, img.height); 1422 | this.fLightTextureBufferList[i].getContext('2d', { alpha: true }).drawImage(img, 0, 0); 1423 | 1424 | const imgData = this.fLightTextureBufferList[i] 1425 | .getContext('2d', { alpha: false }) 1426 | .getImageData(0, 0, this.fLightTextureBufferList[i].width, this.fLightTextureBufferList[i].height); 1427 | this.fLightTexturePixelsList[i] = imgData.data; 1428 | } 1429 | } 1430 | 1431 | jump() { 1432 | if (this.isCrouching || this.isStanding) return; 1433 | this.fPlayerHeight += this.jumpSpeed * this.fGameSpeed; 1434 | this.jumpSpeed -= this.gravityValue * this.fGameSpeed; 1435 | 1436 | if (this.fPlayerHeight <= this.TILE_SIZE / 2) { 1437 | this.jumpSpeed = this.jumpSpeedStart; 1438 | this.fPlayerHeight = this.TILE_SIZE / 2; 1439 | this.isJumping = false; 1440 | } 1441 | } 1442 | 1443 | crouch() { 1444 | if (this.isJumping || this.isStanding) return; 1445 | this.fPlayerHeight -= this.crouchSpeed * this.fGameSpeed; 1446 | this.crouchSpeed -= this.crouchGravity * this.fGameSpeed; 1447 | 1448 | if (this.fPlayerHeight <= this.TILE_SIZE / 2 - this.crouchAmt) { 1449 | this.crouchSpeed = this.crouchSpeedStart; 1450 | this.fPlayerHeight = this.TILE_SIZE / 2 - this.crouchAmt; 1451 | } 1452 | } 1453 | 1454 | stand() { 1455 | if (this.isJumping || this.isCrouching) return; 1456 | this.fPlayerHeight += this.crouchSpeed * this.fGameSpeed; 1457 | this.crouchSpeed -= this.crouchGravity * this.fGameSpeed; 1458 | 1459 | if (this.fPlayerHeight >= this.TILE_SIZE / 2 || this.crouchSpeed <= 0) { 1460 | this.crouchSpeed = this.crouchSpeedStart; 1461 | this.fPlayerHeight = this.TILE_SIZE / 2; 1462 | this.isStanding = false; 1463 | } 1464 | } 1465 | 1466 | fade() { 1467 | if (this.levelTransition) { 1468 | if (this.levelTransitionFadeAmt < 1) this.levelTransitionFadeAmt += this.fGameSpeed / 80; 1469 | else this.levelTransition = false; 1470 | this.ctx.fillStyle = `rgba(0, 0, 0, ${this.levelTransitionFadeAmt})`; 1471 | this.ctx.fillRect(0, 0, this.PROJECTIONPLANEWIDTH, this.PROJECTIONPLANEHEIGHT); 1472 | } else if (this.levelTransitionFadeAmt > 0) { 1473 | this.levelTransitionFadeAmt -= this.fGameSpeed / 80; 1474 | this.ctx.fillStyle = `rgba(0, 0, 0, ${this.levelTransitionFadeAmt})`; 1475 | this.ctx.fillRect(0, 0, this.PROJECTIONPLANEWIDTH, this.PROJECTIONPLANEHEIGHT); 1476 | } 1477 | } 1478 | 1479 | operateThinWall(i) { 1480 | const thinWall = this.thinWalls[i]; 1481 | const slideSpeed = this.fGameSpeed / 2.5; 1482 | const stopGap = 8; 1483 | 1484 | if (thinWall.xEnd - thinWall.xStart !== 0) { 1485 | const length = Math.abs(thinWall.xEnd - thinWall.xStart); 1486 | const moveDir = thinWall.xEnd > thinWall.xStart ? 1 : -1; 1487 | 1488 | if (!thinWall.isOpen) { 1489 | this.thinWalls[i].xStart -= slideSpeed * moveDir; 1490 | this.thinWalls[i].xEnd -= slideSpeed * moveDir; 1491 | if (Math.abs(this.thinWalls[i].xStart - this.thinWalls[i].xStartOriginal) >= length - stopGap) { 1492 | this.activeThinWallId = null; 1493 | this.thinWalls[i].isOpen = true; 1494 | this.thinWalls[i].xStart = this.thinWalls[i].xStartOriginal - (length - stopGap) * moveDir; 1495 | } 1496 | } else { 1497 | this.thinWalls[i].xStart += slideSpeed * moveDir; 1498 | this.thinWalls[i].xEnd += slideSpeed * moveDir; 1499 | if (Math.abs(this.thinWalls[i].xEnd - this.thinWalls[i].xStartOriginal) >= length) { 1500 | this.activeThinWallId = null; 1501 | this.thinWalls[i].isOpen = false; 1502 | this.thinWalls[i].xStart = this.thinWalls[i].xStartOriginal; 1503 | } 1504 | } 1505 | } else { 1506 | const length = Math.abs(thinWall.yEnd - thinWall.yStart); 1507 | const moveDir = thinWall.yEnd > thinWall.yStart ? 1 : -1; 1508 | 1509 | if (!thinWall.isOpen) { 1510 | this.thinWalls[i].yStart -= slideSpeed * moveDir; 1511 | this.thinWalls[i].yEnd -= slideSpeed * moveDir; 1512 | if (Math.abs(this.thinWalls[i].yStart - this.thinWalls[i].yStartOriginal) >= length - stopGap) { 1513 | this.activeThinWallId = null; 1514 | this.thinWalls[i].isOpen = true; 1515 | this.thinWalls[i].yStart = this.thinWalls[i].yStartOriginal - (length - stopGap) * moveDir; 1516 | } 1517 | } else { 1518 | this.thinWalls[i].yStart += slideSpeed * moveDir; 1519 | this.thinWalls[i].yEnd += slideSpeed * moveDir; 1520 | if (Math.abs(this.thinWalls[i].yEnd - this.thinWalls[i].yStartOriginal) >= length) { 1521 | this.activeThinWallId = null; 1522 | this.thinWalls[i].isOpen = false; 1523 | this.thinWalls[i].yStart = this.thinWalls[i].yStartOriginal; 1524 | } 1525 | } 1526 | } 1527 | } 1528 | 1529 | update() { 1530 | if (this.DEBUG && this.debugCtx) 1531 | this.debugCtx.clearRect(0, 0, this.debugCanvasWidth, this.debugCanvasHeight); 1532 | 1533 | if (this.isJumping) this.jump(); 1534 | if (this.isCrouching) this.crouch(); 1535 | if (this.isStanding) this.stand(); 1536 | if (this.activeThinWallId !== null) this.operateThinWall(this.activeThinWallId); 1537 | 1538 | this.move(); 1539 | this.audio.updateSoundPositions(this.fPlayerAngle, this.fPlayerX, this.fPlayerY); 1540 | 1541 | if (this.DEBUG) this.draw2d(); 1542 | this.raycaster(); 1543 | this.draw3d(); 1544 | 1545 | if (this.DEBUG && this.debugCtx) { 1546 | this.debugCtx.fillStyle = `rgba(255,255,255,1)`; 1547 | this.debugCtx.beginPath(); 1548 | this.debugCtx.ellipse(this.fPlayerX, this.fPlayerY, 4, 4, 0, 0, 2 * this.pi); 1549 | this.debugCtx.fill(); 1550 | } 1551 | } 1552 | 1553 | setAngles() { 1554 | const rayInc = this.fPlayerFov / this.PROJECTIONPLANEWIDTH; 1555 | let ang = 0; 1556 | 1557 | for (let i = 0; i < this.rayAngles.length; i++) { 1558 | this.rayAngles[i] = degToRad(ang - this.fPlayerFov / 2); 1559 | ang += rayInc; 1560 | } 1561 | 1562 | for (let i = -this.PROJECTIONPLANEWIDTH / 2; i < this.PROJECTIONPLANEWIDTH / 2; i++) { 1563 | const radian = (i * this.pi) / (this.PROJECTIONPLANEWIDTH * 3); 1564 | this.fFishTable[i + this.PROJECTIONPLANEWIDTH / 2] = 1 / Math.cos(radian); 1565 | } 1566 | } 1567 | 1568 | async preloadTextures() { 1569 | const promises = this.texturePaths.map(path => { 1570 | return new Promise((resolve, reject) => { 1571 | const name = path.split('/').pop()?.split('.')[0]; 1572 | const image = new Image(); 1573 | 1574 | image.src = path; 1575 | image.onload = () => resolve([name, image]); 1576 | image.onerror = () => reject(`Image failed to load: ${path}`); 1577 | }); 1578 | }); 1579 | 1580 | const imgArraytemp = await Promise.all(promises); 1581 | this.textures = Object.fromEntries(imgArraytemp); 1582 | } 1583 | 1584 | lockPointer() { 1585 | if (!this.userIsInTab && !this.DEBUG) { 1586 | this.canvas.requestPointerLock = 1587 | this.canvas.requestPointerLock || 1588 | this.canvas.mozRequestPointerLock || 1589 | this.canvas.webkitRequestPointerLock; 1590 | 1591 | const promise = this.canvas.requestPointerLock({ unadjustedMovement: true }); 1592 | 1593 | if (!promise) { 1594 | console.log('Disabling mouse acceleration is not supported'); 1595 | return this.canvas.requestPointerLock(); 1596 | } 1597 | 1598 | return promise 1599 | .then(() => console.log('Pointer is locked')) 1600 | .catch(err => { 1601 | if (err.name === 'NotSupportedError') { 1602 | return this.canvas.requestPointerLock(); 1603 | } 1604 | }); 1605 | } 1606 | } 1607 | 1608 | resetTextures() { 1609 | this.fCeilingTextureBuffer = undefined; 1610 | this.fCeilingTexturePixels = undefined; 1611 | this.fFloorTextureBufferList = undefined; 1612 | this.fFloorTexturePixelsList = undefined; 1613 | this.fWallTextureBufferList = undefined; 1614 | this.fWallTexturePixelsList = undefined; 1615 | this.fSkyTextureBuffer = undefined; 1616 | this.fSkyTexturePixels = undefined; 1617 | this.fPaintingTextureBufferList = undefined; 1618 | this.fPaintingTexturePixelsList = undefined; 1619 | this.fThinWallTextureBufferList = undefined; 1620 | this.fThinWallTexturePixelsList = undefined; 1621 | this.fObjectTextureBufferList = undefined; 1622 | this.fObjectTexturePixelsList = undefined; 1623 | this.fItemTextureBufferList = []; 1624 | this.fItemTexturePixelsList = []; 1625 | this.fLightTextureBufferList = undefined; 1626 | this.fLightTexturePixelsList = undefined; 1627 | } 1628 | 1629 | setNewMapData(i = this.mapNum) { 1630 | this.resetTextures(); 1631 | this.audio.init(i); 1632 | this.currentLightValues = this.mapLightValues[i]; 1633 | this.currentLightRefs = this.mapLightRefs[i]; 1634 | this.noCeilingIndeces = maps[i].noCeilingIndeces; 1635 | 1636 | this.onWallTextureLoaded(maps[i].wallTextures); 1637 | this.onPaintingTexturesLoaded(maps[i].paintings); 1638 | this.fPaintingDetails = maps[i].paintingDetails; 1639 | 1640 | if (maps[i].ceilingTexture) this.onCeilingTextureLoaded(maps[i].ceilingTexture); 1641 | if (maps[i].skyTexture) this.onSkyTextureLoaded(maps[i].skyTexture); 1642 | this.onFloorTextureLoaded(maps[i].floorTextures); 1643 | 1644 | if (maps[i]?.lightSources?.length) { 1645 | this.onLightTexturesLoaded(maps[i].lightSources.map(light => light.texture)); 1646 | this.lightSources = maps[i].lightSources; 1647 | for (let i = 0; i < this.lightSources.length; i++) { 1648 | const textureWidth = this.textures[this.lightSources[i].texture].width; 1649 | switch (this.lightSources[i].side) { 1650 | case 0: 1651 | this.lightSources[i].x = this.lightSources[i].col * this.TILE_SIZE + this.TILE_SIZE / 2; 1652 | this.lightSources[i].y = this.lightSources[i].row * this.TILE_SIZE - textureWidth / 2; 1653 | break; 1654 | case 1: 1655 | this.lightSources[i].x = 1656 | this.lightSources[i].col * this.TILE_SIZE + this.TILE_SIZE + textureWidth / 2; 1657 | this.lightSources[i].y = this.lightSources[i].row * this.TILE_SIZE + this.TILE_SIZE / 2; 1658 | break; 1659 | case 2: 1660 | this.lightSources[i].x = this.lightSources[i].col * this.TILE_SIZE + this.TILE_SIZE / 2; 1661 | this.lightSources[i].y = 1662 | this.lightSources[i].row * this.TILE_SIZE + this.TILE_SIZE + textureWidth / 2; 1663 | break; 1664 | case 3: 1665 | this.lightSources[i].x = this.lightSources[i].col * this.TILE_SIZE - textureWidth / 2; 1666 | this.lightSources[i].y = this.lightSources[i].row * this.TILE_SIZE + this.TILE_SIZE / 2; 1667 | break; 1668 | default: 1669 | this.lightSources[i].x = this.lightSources[i].col * this.TILE_SIZE + this.TILE_SIZE / 2; 1670 | this.lightSources[i].y = this.lightSources[i].row * this.TILE_SIZE + this.TILE_SIZE / 2; 1671 | } 1672 | } 1673 | } else this.lightSources = []; 1674 | 1675 | if (maps[i]?.items?.length) { 1676 | this.onItemTexturesLoaded(maps[i].items.map(item => item.name)); 1677 | this.items = maps[i].items; 1678 | } else this.items = []; 1679 | 1680 | if (maps[i]?.objects?.length) { 1681 | this.onObjectTexturesLoaded(maps[i].objects.map(obj => obj.name)); 1682 | this.objects = maps[i].objects; 1683 | } else this.objects = []; 1684 | 1685 | if (maps[i]?.thinWalls?.length) { 1686 | this.onThinWallTexturesLoaded(maps[i].thinWalls.map(wall => wall.texture)); 1687 | this.thinWalls = maps[i].thinWalls.map(wall => { 1688 | let xStartTemp = wall.colStart * this.TILE_SIZE; 1689 | let yStartTemp = wall.rowStart * this.TILE_SIZE; 1690 | let xEndTemp = wall.colEnd * this.TILE_SIZE; 1691 | let yEndTemp = wall.rowEnd * this.TILE_SIZE; 1692 | 1693 | if (yStartTemp < yEndTemp) { 1694 | xStartTemp += this.TILE_SIZE / 2; 1695 | xEndTemp += this.TILE_SIZE / 2; 1696 | } else if (yEndTemp < yStartTemp) { 1697 | xStartTemp += this.TILE_SIZE / 2; 1698 | xEndTemp += this.TILE_SIZE / 2; 1699 | } else if (xStartTemp < xEndTemp) { 1700 | yStartTemp += this.TILE_SIZE / 2; 1701 | yEndTemp += this.TILE_SIZE / 2; 1702 | } else if (xEndTemp < xStartTemp) { 1703 | yStartTemp += this.TILE_SIZE / 2; 1704 | yEndTemp += this.TILE_SIZE / 2; 1705 | } 1706 | 1707 | return { 1708 | texture: wall.texture, 1709 | xStartOriginal: xStartTemp, 1710 | yStartOriginal: yStartTemp, 1711 | xStart: xStartTemp, 1712 | yStart: yStartTemp, 1713 | xEnd: xEndTemp, 1714 | yEnd: yEndTemp, 1715 | isOpen: wall.isOpen, 1716 | sounds: wall.sounds, 1717 | function: wall.function, 1718 | transparent: wall.transparent, 1719 | vaultable: wall.vaultable, 1720 | }; 1721 | }); 1722 | } else this.thinWalls = []; 1723 | 1724 | this.map = new Uint8Array(maps[i].map.flat()); 1725 | this.mapNum = i; 1726 | this.doorMap = maps[i].doorMap; 1727 | this.mapCols = maps[i].map[0].length; 1728 | this.mapRows = maps[i].map.length; 1729 | this.mapWidth = this.TILE_SIZE * this.mapCols; 1730 | this.mapHeight = this.TILE_SIZE * this.mapRows; 1731 | this.fPlayerX = this.fPlayerX; 1732 | this.fPlayerY = this.fPlayerY; 1733 | 1734 | if (this.DEBUG && this.debugCanvas) { 1735 | this.debugCanvas.width = this.mapWidth; 1736 | this.debugCanvas.height = this.mapHeight; 1737 | this.debugCanvasWidth = this.debugCanvas.width; 1738 | this.debugCanvasHeight = this.debugCanvas.height; 1739 | this.debugCtx = this.debugCanvas.getContext('2d', { alpha: false }); 1740 | 1741 | this.debugCanvas.style.aspectRatio = this.debugCanvasWidth / this.debugCanvasHeight; 1742 | } 1743 | 1744 | for (let j = 0; j < this.objects.length; j++) { 1745 | const obj = this.objects[j]; 1746 | if (obj?.sounds?.length) { 1747 | obj.sounds.forEach(sound => { 1748 | if (this.audio.sounds[sound + j]?.loop()) { 1749 | this.audio.playSound(sound, obj.x, obj.y, true, j); 1750 | } 1751 | }); 1752 | } 1753 | } 1754 | 1755 | for (let j = 0; j < this.lightSources.length; j++) { 1756 | const light = this.lightSources[j]; 1757 | if (light?.sounds?.length) { 1758 | light.sounds.forEach(sound => { 1759 | if (this.audio.sounds[sound + j]?.loop()) { 1760 | this.audio.playSound(sound, light.x, light.y, true, j); 1761 | } 1762 | }); 1763 | } 1764 | } 1765 | } 1766 | 1767 | calculateLightValues() { 1768 | return new Promise((resolve, reject) => { 1769 | const total = maps.reduce( 1770 | (acc, value) => acc + value.lightSources.length * value.map[0].length * this.TILE_SIZE, 1771 | 0 1772 | ); 1773 | let progress = 0; 1774 | 1775 | const lightingVersionTransaction = this.db.transaction('lightingVersion'); 1776 | const lightingVersionObjectStore = lightingVersionTransaction.objectStore('lightingVersion'); 1777 | const lightingVersionGetRequest = lightingVersionObjectStore.get(0); 1778 | 1779 | lightingVersionGetRequest.onsuccess = () => { 1780 | let mapsCompleted = 0; 1781 | 1782 | if (lightingVersionGetRequest?.result === this.lightingVersionNum) { 1783 | for (let i = 0; i < maps.length; i++) { 1784 | const transaction = this.db.transaction('lighting'); 1785 | const objectStore = transaction.objectStore('lighting'); 1786 | const getRequest = objectStore.get(i); 1787 | 1788 | getRequest.onsuccess = () => { 1789 | this.mapLightValues[i] = getRequest.result?.mapLightValues || null; 1790 | this.mapLightRefs[i] = getRequest.result?.mapLightRefs || null; 1791 | this.mapLightSides[i] = getRequest.result?.mapLightSides || null; 1792 | console.log(`Lighting for map ${i} has been retrieved`); 1793 | mapsCompleted++; 1794 | if (mapsCompleted === maps.length) resolve(); 1795 | }; 1796 | getRequest.onerror = () => { 1797 | console.log(`Unable to retrieve lighting for map ${i}`); 1798 | reject(); 1799 | }; 1800 | } 1801 | } else { 1802 | (function mapLoop(i = 0) { 1803 | if (i < maps.length) { 1804 | if (!maps[i].lightSources.length) { 1805 | this.mapLightValues[i] = null; 1806 | this.mapLightRefs[i] = null; 1807 | this.mapLightSides[i] = null; 1808 | console.log(`Lighting for map ${i} has been retrieved`); 1809 | mapsCompleted++; 1810 | if (mapsCompleted === maps.length) resolve(); 1811 | } else { 1812 | document.querySelector('.loadingContainer').style.display = 'flex'; 1813 | console.log(`Processing lighting for map ${i}`); 1814 | const curLevel = maps[i]; 1815 | const curMap = curLevel.map; 1816 | const mapCols = curMap[0].length; 1817 | const mapRows = curMap.length; 1818 | const mapWidth = mapCols * this.TILE_SIZE; 1819 | const mapHeight = mapRows * this.TILE_SIZE; 1820 | 1821 | this.mapLightValues[i] = new Float32Array(mapWidth * mapHeight).fill(this.minBrightness); 1822 | this.mapLightRefs[i] = new Uint8Array(mapWidth * mapHeight); 1823 | this.mapLightSides[i] = new Int8Array(mapWidth * mapHeight).fill(-1); 1824 | 1825 | (function lightSourceLoop(j = 0) { 1826 | if (j < curLevel.lightSources.length) { 1827 | const light = curLevel.lightSources[j]; 1828 | const textureWidth = this.textures[light.texture].width; 1829 | const sourceCol = light.col; 1830 | const sourceRow = light.row; 1831 | let lightX; 1832 | let lightY; 1833 | 1834 | switch (light.side) { 1835 | case 0: 1836 | lightX = sourceCol * this.TILE_SIZE + this.TILE_SIZE / 2; 1837 | lightY = sourceRow * this.TILE_SIZE - textureWidth / 2; 1838 | break; 1839 | case 1: 1840 | lightX = sourceCol * this.TILE_SIZE + this.TILE_SIZE + textureWidth / 2; 1841 | lightY = sourceRow * this.TILE_SIZE + this.TILE_SIZE / 2; 1842 | break; 1843 | case 2: 1844 | lightX = sourceCol * this.TILE_SIZE + this.TILE_SIZE / 2; 1845 | lightY = sourceRow * this.TILE_SIZE + this.TILE_SIZE + textureWidth / 2; 1846 | break; 1847 | case 3: 1848 | lightX = sourceCol * this.TILE_SIZE - textureWidth / 2; 1849 | lightY = sourceRow * this.TILE_SIZE + this.TILE_SIZE / 2; 1850 | break; 1851 | default: 1852 | lightX = sourceCol * this.TILE_SIZE + this.TILE_SIZE / 2; 1853 | lightY = sourceRow * this.TILE_SIZE + this.TILE_SIZE / 2; 1854 | } 1855 | 1856 | (function xLoop(x = 0) { 1857 | if (x < mapWidth) { 1858 | setTimeout(() => { 1859 | document.querySelector('.loadingFill').style.width = `${~~( 1860 | (progress / total) * 1861 | 100 1862 | )}%`; 1863 | document.querySelector('.loadingValue').innerText = `${~~( 1864 | (progress / total) * 1865 | 100 1866 | )}%`; 1867 | 1868 | if (x >= this.TILE_SIZE && x <= mapWidth - this.TILE_SIZE) { 1869 | loop1: for (let y = 0; y < mapHeight; y++) { 1870 | if (y < this.TILE_SIZE || y > mapHeight - this.TILE_SIZE) continue loop1; 1871 | for (let row = 0; row < mapRows; row++) { 1872 | loop2: for (let col = 0; col < mapCols; col++) { 1873 | const tile = curMap[row][col]; 1874 | if (tile > 5) continue loop2; 1875 | 1876 | const x1 = col * this.TILE_SIZE; 1877 | const y1 = row * this.TILE_SIZE; 1878 | 1879 | const x2 = x1 + this.TILE_SIZE; 1880 | const y2 = y1; 1881 | 1882 | const x3 = x2; 1883 | const y3 = y1 + this.TILE_SIZE; 1884 | 1885 | const x4 = x1; 1886 | const y4 = y3; 1887 | 1888 | if (x > x1 && x < x2 && y > y1 && y < y3) continue loop1; 1889 | if ( 1890 | Math.abs(x1 + this.TILE_SIZE / 2 - x) > Math.abs(lightX - x) && 1891 | Math.abs(y1 + this.TILE_SIZE / 2 - y) > Math.abs(lightY - y) 1892 | ) { 1893 | continue loop2; 1894 | } 1895 | 1896 | let tX1 = 0; 1897 | let tY1 = 0; 1898 | let tX2 = 0; 1899 | let tY2 = 0; 1900 | 1901 | let sidesIntersected = 0; 1902 | let sharesCornerWithTile = false; 1903 | for (let i = 0; i < 4; i++) { 1904 | switch (i) { 1905 | case 0: 1906 | tX1 = x1; 1907 | tY1 = y1; 1908 | tX2 = x2; 1909 | tY2 = y2; 1910 | break; 1911 | case 1: 1912 | tX1 = x2; 1913 | tY1 = y2; 1914 | tX2 = x3; 1915 | tY2 = y3; 1916 | break; 1917 | case 2: 1918 | tX1 = x3; 1919 | tY1 = y3; 1920 | tX2 = x4; 1921 | tY2 = y4; 1922 | break; 1923 | case 3: 1924 | tX1 = x4; 1925 | tY1 = y4; 1926 | tX2 = x1; 1927 | tY2 = y1; 1928 | break; 1929 | } 1930 | 1931 | const denom = (tX1 - tX2) * (y - lightY) - (tY1 - tY2) * (x - lightX); 1932 | 1933 | if (denom !== 0) { 1934 | const t = ((tX1 - x) * (y - lightY) - (tY1 - y) * (x - lightX)) / denom; 1935 | const u = ((tX1 - x) * (tY1 - tY2) - (tY1 - y) * (tX1 - tX2)) / denom; 1936 | 1937 | if (t >= 0 && t <= 1 && u >= 0 && u <= 1) { 1938 | if ( 1939 | !sharesCornerWithTile && 1940 | ((x === tX1 && y === tY1) || (x === tX2 && y === tY2)) 1941 | ) { 1942 | sharesCornerWithTile = true; 1943 | sidesIntersected++; 1944 | } else if (!((x === tX1 && y === tY1) || (x === tX2 && y === tY2))) 1945 | sidesIntersected++; 1946 | } 1947 | if (sidesIntersected > 1) continue loop1; 1948 | } 1949 | } 1950 | } 1951 | } 1952 | 1953 | loop3: for (let k = 0; k < curLevel?.thinWalls?.length; k++) { 1954 | if (curLevel.thinWalls[k].transparent) continue loop3; 1955 | let xStart = curLevel.thinWalls[k].colStart * this.TILE_SIZE; 1956 | let yStart = curLevel.thinWalls[k].rowStart * this.TILE_SIZE; 1957 | let xEnd = curLevel.thinWalls[k].colEnd * this.TILE_SIZE; 1958 | let yEnd = curLevel.thinWalls[k].rowEnd * this.TILE_SIZE; 1959 | 1960 | if (yStart < yEnd) { 1961 | xStart += this.TILE_SIZE / 2; 1962 | xEnd += this.TILE_SIZE / 2; 1963 | } else if (yEnd < yStart) { 1964 | xStart += this.TILE_SIZE / 2; 1965 | xEnd += this.TILE_SIZE / 2; 1966 | } else if (xStart < xEnd) { 1967 | yStart += this.TILE_SIZE / 2; 1968 | yEnd += this.TILE_SIZE / 2; 1969 | } else if (xEnd < xStart) { 1970 | yStart += this.TILE_SIZE / 2; 1971 | yEnd += this.TILE_SIZE / 2; 1972 | } 1973 | if ( 1974 | (y === yStart && 1975 | x >= Math.min(xStart, xEnd) && 1976 | x <= Math.max(xStart, xEnd)) || 1977 | (x === xStart && y >= Math.min(yStart, yEnd) && y <= Math.max(yStart, yEnd)) 1978 | ) { 1979 | if (xStart === xEnd) { 1980 | if (x - lightX > 0) { 1981 | this.mapLightSides[i][y * mapWidth + x] = parseInt( 1982 | '' + this.mapLightSides[i][y * mapWidth + x] + 1 1983 | ); 1984 | } else 1985 | this.mapLightSides[i][y * mapWidth + x] = parseInt( 1986 | '' + this.mapLightSides[i][y * mapWidth + x] + 3 1987 | ); 1988 | } else { 1989 | if (y - lightY > 0) { 1990 | this.mapLightSides[i][y * mapWidth + x] = parseInt( 1991 | '' + this.mapLightSides[i][y * mapWidth + x] + 2 1992 | ); 1993 | } else 1994 | this.mapLightSides[i][y * mapWidth + x] = parseInt( 1995 | '' + this.mapLightSides[i][y * mapWidth + x] + 0 1996 | ); 1997 | } 1998 | 1999 | continue loop3; 2000 | } 2001 | 2002 | const denom = (xStart - xEnd) * (y - lightY) - (yStart - yEnd) * (x - lightX); 2003 | 2004 | if (denom !== 0) { 2005 | const t = 2006 | ((xStart - x) * (y - lightY) - (yStart - y) * (x - lightX)) / denom; 2007 | const u = 2008 | ((xStart - x) * (yStart - yEnd) - (yStart - y) * (xStart - xEnd)) / denom; 2009 | 2010 | if (t >= 0 && t <= 1 && u >= 0 && u <= 1) continue loop1; 2011 | } 2012 | } 2013 | 2014 | const d = 2015 | Math.sqrt((x - lightX) * (x - lightX) + (y - lightY) * (y - lightY)) || 0.001; 2016 | let s = d / light.strength / 1500; 2017 | if (s > 1) s = 1; 2018 | const maxIntensity = 1; 2019 | const falloffRate = 40; 2020 | this.mapLightValues[i][y * mapWidth + x] += 2021 | maxIntensity * (((1 - s * s) * (1 - s * s)) / (1 + falloffRate * (s * s))); 2022 | this.mapLightRefs[i][y * mapWidth + x] = j; 2023 | } 2024 | } 2025 | 2026 | if (j === curLevel.lightSources.length - 1 && x === mapWidth - 1) { 2027 | const transaction = this.db.transaction('lighting', 'readwrite'); 2028 | const objectStore = transaction.objectStore('lighting'); 2029 | const request = objectStore.put( 2030 | { 2031 | mapLightValues: this.mapLightValues[i], 2032 | mapLightRefs: this.mapLightRefs[i], 2033 | mapLightSides: this.mapLightSides[i], 2034 | }, 2035 | i 2036 | ); 2037 | 2038 | request.onsuccess = () => { 2039 | console.log(`Lighting calculations complete for map ${i} - added to database`); 2040 | mapsCompleted++; 2041 | 2042 | if (mapsCompleted === maps.length) { 2043 | const lightingVersionTransaction = this.db.transaction( 2044 | 'lightingVersion', 2045 | 'readwrite' 2046 | ); 2047 | const lightingVersionObjectStore = 2048 | lightingVersionTransaction.objectStore('lightingVersion'); 2049 | 2050 | const lightingVersionRequest = lightingVersionObjectStore.put( 2051 | this.lightingVersionNum, 2052 | 0 2053 | ); 2054 | 2055 | lightingVersionRequest.onsuccess = () => { 2056 | console.log(`Lighting updated to version ${this.lightingVersionNum}`); 2057 | }; 2058 | lightingVersionRequest.onerror = () => { 2059 | console.log( 2060 | `Unable to update lighting to version ${this.lightingVersionNum}` 2061 | ); 2062 | reject(); 2063 | }; 2064 | resolve(); 2065 | } 2066 | }; 2067 | request.onerror = () => { 2068 | console.log(`Unable to add lighting calculations to database for map ${i}`); 2069 | reject(); 2070 | }; 2071 | } 2072 | progress++; 2073 | xLoop.bind(this)(x + 1); 2074 | }, 1); 2075 | } 2076 | }).bind(this)(); 2077 | lightSourceLoop.bind(this)(j + 1); 2078 | } 2079 | }).bind(this)(); 2080 | } 2081 | mapLoop.bind(this)(i + 1); 2082 | } 2083 | }).bind(this)(); 2084 | } 2085 | }; 2086 | }); 2087 | } 2088 | 2089 | async init() { 2090 | await this.preloadTextures(); 2091 | await this.calculateLightValues(); 2092 | this.setNewMapData(); 2093 | this.setAngles(); 2094 | 2095 | if (!this.DEBUG) { 2096 | this.canvas.classList.add('fullscreen'); 2097 | } else { 2098 | const container = document.querySelector('.container'); 2099 | const newCanvas = document.createElement('canvas'); 2100 | newCanvas.id = 'debugCanvas'; 2101 | this.debugCanvas = newCanvas; 2102 | this.debugCanvas.width = this.mapWidth; 2103 | this.debugCanvas.height = this.mapHeight; 2104 | this.debugCanvasWidth = this.debugCanvas.width; 2105 | this.debugCanvasHeight = this.debugCanvas.height; 2106 | this.debugCtx = this.debugCanvas.getContext('2d', { alpha: false }); 2107 | container.appendChild(newCanvas); 2108 | } 2109 | 2110 | document.addEventListener( 2111 | 'pointerlockchange', 2112 | () => { 2113 | if (document.pointerLockElement === canvas) { 2114 | this.userIsInTab = true; 2115 | } else this.userIsInTab = false; 2116 | }, 2117 | false 2118 | ); 2119 | 2120 | document.addEventListener('mousedown', () => { 2121 | this.lockPointer(); 2122 | }); 2123 | 2124 | document.addEventListener('contextmenu', e => e.preventDefault()); 2125 | 2126 | window.addEventListener('beforeunload', e => { 2127 | if (this.preventPageReloadDialog) return; 2128 | e.preventDefault(); 2129 | return (e.returnValue = 'Exit Tab?'); 2130 | }); 2131 | } 2132 | } 2133 | --------------------------------------------------------------------------------