├── README.md ├── engine ├── data │ ├── images │ │ ├── bg.png │ │ ├── spritesheet_enemy.png │ │ ├── spritesheet_nums.png │ │ ├── spritesheet_player.png │ │ ├── spritesheet_player_attack.png │ │ ├── spritesheet_torch.png │ │ ├── spritesheet_ui.png │ │ ├── tilemap.png │ │ └── tilemap_items.png │ ├── scripts │ │ ├── data │ │ │ ├── data.js │ │ │ └── globals.js │ │ ├── input.js │ │ ├── libraries │ │ │ └── perlin.js │ │ ├── logic │ │ │ ├── enemies.js │ │ │ ├── entities.js │ │ │ ├── logic.js │ │ │ └── player.js │ │ ├── render.js │ │ └── settings.js │ └── styles │ │ └── style.css └── index.html └── repo └── v0.3.3_scr_1.png /README.md: -------------------------------------------------------------------------------- 1 | # Dendgeon V. 0.3.5 2 | 3 | Get it? It's a play on words of dungeon and engine... 😐 4 | 5 | ## See it in action [here!](https://nottimtam.github.io/dendgeon/engine/) 6 | 7 | # Big changes on the way in /dev. 8 | Finally, [the raycasting you've been waiting for!](https://github.com/NotTimTam/dendgeon/tree/dev) 9 | 10 | ## About 11 | 12 | ![V 0.3 in-engine screenshot](./repo/v0.3.3_scr_1.png) 13 | 14 | An in-engine screenshot from version 0.3.3. 15 | 16 | Did somebody say "lightweight, retro-themed, easy-to-understand, easier-to-learn, dungeon crawler engine"? 17 | 18 | N-no? 19 | 20 | Anyway, this is all of those things. First and foremost, it is a dungeon crawler game I am building in vanilla JS. That's right! No node, and no rendering libraries! Just good old fashioned JS and HTML Canvas. The entire engine is built from the ground up to maximize efficieny while running at a constant 60fps (on any mid-range computer hardware) 21 | 22 | Being HTML you can easily export this to an NW.js desktop application, mobile app, or anything else that can run an HTML page. 23 | 24 | There isn't official documentation yet, but everything is thoroughly commented and easy to read. The engine itself is relatively simple, and the game loop follows the common- 25 | 26 | 1. User input. 27 | 2. Game logic. 28 | 3. Game rendering. 29 | 30 | -game loop methodology. Every object in the game utilizes these three functions at their core, meaning implementing brand new functionality into the engine is easy. 31 | 32 | ### What Features Are Included Out of the Box? 33 | 34 | - **Player** with attacking, inventory, and health. 35 | - **Modifiable UI** with a built in minimap. 36 | 37 | - Efficient, complex **spritesheet-based image rendering system**. 38 | 39 | - One for **rendering static objects** from a spritesheet. (faster than having thousands of images that have to be loaded in individually) 40 | 41 | spritestrip 42 | 43 | - The other, for **rendering an animation**, or animations from a spritestrip. 44 | 45 | spritestrip 46 | 47 | - Tile based **rooms with events and triggers**. 48 | - A robust, but simple, **object-focused collision system**. (meaning each object can handle its own collisions differently, and handle what objects it will collide with) 49 | - (Potentially) **infinite room generation using simplex noise** and custom template-built rooms. (capped at a certain amount of rooms (default 25, but tested up to 500) but could theoretically go on forever, given enough memory...) 50 | - Multi-source **RAYTRACED lighting** system. (employs raytracing methods, does not require an RTX GPU) 51 | - Enemies with AI. 52 | 53 | #### Noteable Small Features 54 | 55 | - Raycasting/tracing. 56 | - A fully posable camera with smart rendering. (only renders what is on-screen) 57 | - Fully customizable item/loot system. 58 | - Fully customizable entity system. 59 | 60 | ### Changelog 61 | 62 | Newest changes on top. Once a major version change is completed all of its sub-version changes are compiled under one heading. 63 | 64 | #### V.0.3.5 - 2021-9-5 65 | 66 | - Added camera shaking. 67 | - Gave enemies the ability to attack the player at intervals. 68 | 69 | #### V.0.3.4 - 2021-9-5 70 | 71 | - Made animation functionality a global class for less repetative code. 72 | - Fixed bug where some game objects would have their render code run while they were off-screen. 73 | 74 | #### V.0.3.3 - 2021-9-4 75 | 76 | - Added raycasted lighting. 77 | 78 | #### V.0.3.1 - 2021-9-4 79 | 80 | - Added multiple light sources. 81 | 82 | #### V.0.2 - 2021-9-2 83 | 84 | - Added room generation. 85 | - Changed room textures. 86 | - Improved rendering efficiency. 87 | - Added simple player-focused "lighting." 88 | - Added enemies. 89 | - Added perlin noise to improve room randomness. 90 | - Removed projectiles. 91 | 92 | #### V.0.1 - 2021-8-22 93 | 94 | - Began file layout and imported common canvas properties. 95 | - Added a player object and rooms with tiles. 96 | - Added a collision system. 97 | - Added player animation control. 98 | - Added textures. 99 | - Added player animations. 100 | - Added items. 101 | - Added collisions. 102 | - Added projectiles. 103 | 104 | ### Roadmap of Potential Features 105 | 106 | By building the crawler out as a full game, I am able to implement features that are useful for people building their own game in the engine. Here are just a few of the big things I plan on adding: 107 | 108 | - More enemies. 109 | - Different size/shape rooms. 110 | - True 2.5d rendering. 111 | - Levels with increasing difficulty. 112 | - A boss room. 113 | - Better collision system that is global-focused rather than object-focused and more efficient. 114 | - Player dying/respawning. 115 | - Sound effects. 116 | - Move all spritesheets to new spritesheet loading system. 117 | - Doom-like 3D rendering with raycasting. 118 | - Chest room. 119 | - More kinds of loot. 120 | - Enemy drops. 121 | -------------------------------------------------------------------------------- /engine/data/images/bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NotTimTam/dendgeon/0313974c9671ea74d4dc91c18d51b4e3ef3a0a51/engine/data/images/bg.png -------------------------------------------------------------------------------- /engine/data/images/spritesheet_enemy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NotTimTam/dendgeon/0313974c9671ea74d4dc91c18d51b4e3ef3a0a51/engine/data/images/spritesheet_enemy.png -------------------------------------------------------------------------------- /engine/data/images/spritesheet_nums.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NotTimTam/dendgeon/0313974c9671ea74d4dc91c18d51b4e3ef3a0a51/engine/data/images/spritesheet_nums.png -------------------------------------------------------------------------------- /engine/data/images/spritesheet_player.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NotTimTam/dendgeon/0313974c9671ea74d4dc91c18d51b4e3ef3a0a51/engine/data/images/spritesheet_player.png -------------------------------------------------------------------------------- /engine/data/images/spritesheet_player_attack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NotTimTam/dendgeon/0313974c9671ea74d4dc91c18d51b4e3ef3a0a51/engine/data/images/spritesheet_player_attack.png -------------------------------------------------------------------------------- /engine/data/images/spritesheet_torch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NotTimTam/dendgeon/0313974c9671ea74d4dc91c18d51b4e3ef3a0a51/engine/data/images/spritesheet_torch.png -------------------------------------------------------------------------------- /engine/data/images/spritesheet_ui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NotTimTam/dendgeon/0313974c9671ea74d4dc91c18d51b4e3ef3a0a51/engine/data/images/spritesheet_ui.png -------------------------------------------------------------------------------- /engine/data/images/tilemap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NotTimTam/dendgeon/0313974c9671ea74d4dc91c18d51b4e3ef3a0a51/engine/data/images/tilemap.png -------------------------------------------------------------------------------- /engine/data/images/tilemap_items.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NotTimTam/dendgeon/0313974c9671ea74d4dc91c18d51b4e3ef3a0a51/engine/data/images/tilemap_items.png -------------------------------------------------------------------------------- /engine/data/scripts/data/data.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | // Tiles. 4 | const tiles = { 5 | map: undefined, 6 | tileSize: 8, 7 | 8 | load: () => { 9 | let img = new Image(); 10 | img.onload = () => { 11 | tiles.map = img; 12 | }; 13 | img.src = "./data/images/tilemap.png"; 14 | }, 15 | 16 | err: { 17 | solid: false, 18 | 19 | pos: { 20 | x: 0, 21 | y: 2, 22 | }, 23 | 24 | id: 0, 25 | }, 26 | 27 | ground_1: { 28 | solid: false, 29 | 30 | pos: { 31 | x: 0, 32 | y: 0, 33 | }, 34 | 35 | id: 1, 36 | }, 37 | ground_2: { 38 | solid: false, 39 | 40 | pos: { 41 | x: 1, 42 | y: 0, 43 | }, 44 | 45 | id: 2, 46 | }, 47 | ground_3: { 48 | solid: false, 49 | 50 | pos: { 51 | x: 2, 52 | y: 0, 53 | }, 54 | 55 | id: 9, 56 | }, 57 | ground_4: { 58 | solid: false, 59 | 60 | pos: { 61 | x: 3, 62 | y: 0, 63 | }, 64 | 65 | id: 8, 66 | }, 67 | 68 | // WALLS 69 | wall: { 70 | solid: true, 71 | 72 | pos: { 73 | x: 0, 74 | y: 1, 75 | }, 76 | 77 | id: 3, 78 | }, 79 | wall_ledge: { 80 | solid: true, 81 | 82 | pos: { 83 | x: 1, 84 | y: 1, 85 | }, 86 | 87 | id: 4, 88 | }, 89 | 90 | door_closed: { 91 | solid: true, 92 | 93 | pos: { x: 2, y: 1 }, 94 | 95 | id: 6, 96 | }, 97 | 98 | door_open: { 99 | solid: false, 100 | 101 | pos: { x: 3, y: 1 }, 102 | 103 | id: 7, 104 | }, 105 | }; 106 | // Load the tilemap texture image if it hasn't been already. 107 | tiles.load(); 108 | 109 | // Room templates. 110 | const rooms = { 111 | a: [ 112 | [3, 4, 4, 4, 4, 6, 4, 4, 4, 4, 3], 113 | [3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3], 114 | [3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3], 115 | [3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3], 116 | [3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3], 117 | [6, 1, 1, 1, 1, 1, 1, 1, 1, 1, 6], 118 | [3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3], 119 | [3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3], 120 | [3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3], 121 | [3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3], 122 | [3, 3, 3, 3, 3, 6, 3, 3, 3, 3, 3], 123 | ], 124 | 125 | ae: [ 126 | [3, 4, 4, 4, 4, 1, 4, 4, 4, 4, 3], 127 | [3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3], 128 | [3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3], 129 | [3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3], 130 | [3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3], 131 | [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], 132 | [3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3], 133 | [3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3], 134 | [3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3], 135 | [3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3], 136 | [3, 3, 3, 3, 3, 1, 3, 3, 3, 3, 3], 137 | ], 138 | 139 | u: [ 140 | [3, 4, 4, 4, 4, 6, 4, 4, 4, 4, 3], 141 | [3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3], 142 | [3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3], 143 | [3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3], 144 | [3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3], 145 | [3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3], 146 | [3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3], 147 | [3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3], 148 | [3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3], 149 | [3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3], 150 | [3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3], 151 | ], 152 | 153 | d: [ 154 | [3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 3], 155 | [3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3], 156 | [3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3], 157 | [3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3], 158 | [3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3], 159 | [3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3], 160 | [3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3], 161 | [3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3], 162 | [3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3], 163 | [3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3], 164 | [3, 3, 3, 3, 3, 6, 3, 3, 3, 3, 3], 165 | ], 166 | 167 | l: [ 168 | [3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 3], 169 | [3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3], 170 | [3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3], 171 | [3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3], 172 | [3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3], 173 | [6, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3], 174 | [3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3], 175 | [3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3], 176 | [3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3], 177 | [3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3], 178 | [3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3], 179 | ], 180 | 181 | r: [ 182 | [3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 3], 183 | [3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3], 184 | [3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3], 185 | [3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3], 186 | [3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3], 187 | [3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 6], 188 | [3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3], 189 | [3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3], 190 | [3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3], 191 | [3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3], 192 | [3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3], 193 | ], 194 | 195 | ud: [ 196 | [3, 4, 4, 4, 4, 6, 4, 4, 4, 4, 3], 197 | [3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3], 198 | [3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3], 199 | [3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3], 200 | [3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3], 201 | [3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3], 202 | [3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3], 203 | [3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3], 204 | [3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3], 205 | [3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3], 206 | [3, 3, 3, 3, 3, 6, 3, 3, 3, 3, 3], 207 | ], 208 | 209 | lr: [ 210 | [3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 3], 211 | [3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3], 212 | [3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3], 213 | [3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3], 214 | [3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3], 215 | [6, 1, 1, 1, 1, 1, 1, 1, 1, 1, 6], 216 | [3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3], 217 | [3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3], 218 | [3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3], 219 | [3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3], 220 | [3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3], 221 | ], 222 | 223 | ul: [ 224 | [3, 4, 4, 4, 4, 6, 4, 4, 4, 4, 3], 225 | [3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3], 226 | [3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3], 227 | [3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3], 228 | [3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3], 229 | [6, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3], 230 | [3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3], 231 | [3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3], 232 | [3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3], 233 | [3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3], 234 | [3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3], 235 | ], 236 | 237 | ur: [ 238 | [3, 4, 4, 4, 4, 6, 4, 4, 4, 4, 3], 239 | [3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3], 240 | [3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3], 241 | [3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3], 242 | [3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3], 243 | [3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 6], 244 | [3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3], 245 | [3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3], 246 | [3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3], 247 | [3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3], 248 | [3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3], 249 | ], 250 | 251 | dl: [ 252 | [3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 3], 253 | [3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3], 254 | [3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3], 255 | [3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3], 256 | [3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3], 257 | [6, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3], 258 | [3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3], 259 | [3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3], 260 | [3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3], 261 | [3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3], 262 | [3, 3, 3, 3, 3, 6, 3, 3, 3, 3, 3], 263 | ], 264 | 265 | dr: [ 266 | [3, 4, 4, 4, 4, 6, 4, 4, 4, 4, 3], 267 | [3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3], 268 | [3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3], 269 | [3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3], 270 | [3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3], 271 | [3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 6], 272 | [3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3], 273 | [3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3], 274 | [3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3], 275 | [3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3], 276 | [3, 3, 3, 3, 3, 6, 3, 3, 3, 3, 3], 277 | ], 278 | 279 | // hall_ud: [ 280 | // [3, 1, 1, 1, 3], 281 | // [3, 1, 1, 1, 3], 282 | // [3, 1, 1, 1, 3], 283 | // [3, 1, 1, 1, 3], 284 | // [3, 1, 1, 1, 3], 285 | // [3, 1, 1, 1, 3], 286 | // [3, 1, 1, 1, 3], 287 | // [3, 1, 1, 1, 3], 288 | // [3, 1, 1, 1, 3], 289 | // [3, 1, 1, 1, 3], 290 | // ], 291 | 292 | // hall_lr: [ 293 | // [4, 4, 4, 4, 4, 4, 4, 4, 4, 4], 294 | // [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], 295 | // [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], 296 | // [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], 297 | // [3, 3, 3, 3, 3, 3, 3, 3, 3, 3], 298 | // ], 299 | }; 300 | -------------------------------------------------------------------------------- /engine/data/scripts/data/globals.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | // Global Objects. 4 | 5 | // Tilemaps and Spritesheets. 6 | class Sheet { 7 | constructor(src, locs) { 8 | this.loaded = false; 9 | this.src = src; 10 | this.map = undefined; 11 | this.locs = locs; 12 | 13 | this.load(); 14 | } 15 | 16 | load = () => { 17 | let img = new Image(); 18 | img.onload = () => { 19 | this.map = img; 20 | this.loaded = true; 21 | }; 22 | img.src = `./data/images/${this.src}.png`; 23 | }; 24 | } 25 | 26 | // Spritestrips for animation. (should be one frame high) 27 | class Anim { 28 | constructor( 29 | src, 30 | firstAnim = "animation1", 31 | startingFrame = 0, 32 | speed = 4, 33 | frameCounts = { 34 | // How many frames are in each animation, and at what frame they start. 35 | animation1: { 36 | start: 0, 37 | end: 3, 38 | }, 39 | }, 40 | onFinish, 41 | extraProps 42 | ) { 43 | // Animation strip data. 44 | this.loaded = false; 45 | this.src = src; 46 | this.map = undefined; 47 | this.load(); 48 | 49 | // Animation data. 50 | this.name = firstAnim; // The name of the current animation. 51 | this.frame = startingFrame; // The frame of the current animation. 52 | this.speed = speed; // The speed of the current animation. (the lower the number, the faster the animation) 53 | this.tick = 0; // The current position in the frame. 54 | this.frameCounts = frameCounts; 55 | this.onFinish = onFinish; // A function to run when an animation finishes. 56 | 57 | // Load extra props. 58 | for (let prop in extraProps) { 59 | this[prop] = extraProps[prop]; 60 | } 61 | } 62 | 63 | // Load the sprite strip. 64 | load = () => { 65 | let img = new Image(); 66 | img.onload = () => { 67 | this.map = img; 68 | this.loaded = true; 69 | }; 70 | img.src = `./data/images/${this.src}.png`; 71 | }; 72 | 73 | // Handle animations 74 | animate = () => { 75 | // Check if the image for the animation has loaded yet. 76 | if (!this.loaded) return; 77 | 78 | // Update animation steps. 79 | this.tick++; // Add to the tick. 80 | 81 | // Move to the next frame if the speed/tick counter completes. 82 | if (this.tick > this.speed) { 83 | this.tick = 0; 84 | this.frame++; 85 | } 86 | 87 | // If we have finished an animation, restart it. 88 | if (this.frame > this.frameCounts[this.name].end) { 89 | // BREAKDOWN: 90 | /* 91 | where the data for framecounts is stored the name of the currently playing animation the frame start or end position of the animation. 92 | this.frameCounts [this.name] start/end 93 | */ 94 | this.frame = this.frameCounts[this.name].start; 95 | 96 | // If the animation has a defined onFinish function, we run it. (the animation needs to be passed through) 97 | if (this.onFinish !== undefined) { 98 | this.onFinish(this); 99 | } 100 | } 101 | }; 102 | } 103 | 104 | // Global functions. 105 | 106 | const worldToTile = (x, y) => { 107 | return { 108 | x: Math.round(x / 8 - 0.5), 109 | y: Math.round(y / 8 - 0.5), 110 | }; 111 | }; 112 | 113 | const worldToRoom = (x, y) => { 114 | return { 115 | x: Math.round(x / 88 - 0.5) * 88, 116 | y: Math.round(y / 88 - 0.5) * 88, 117 | }; 118 | }; 119 | 120 | const AABB = (rect1, rect2) => { 121 | if ( 122 | rect1.x < rect2.x + rect2.width && 123 | rect1.x + rect1.width > rect2.x && 124 | rect1.y < rect2.y + rect2.height && 125 | rect1.y + rect1.height > rect2.y 126 | ) { 127 | return true; 128 | } else { 129 | return false; 130 | } 131 | }; 132 | 133 | const isOnScreen = (object) => { 134 | if (object.x + object.width < player.camera.x) { 135 | return false; 136 | } else if (object.x > player.camera.x + canvas.width) { 137 | return false; 138 | } 139 | 140 | if (object.y + object.height < player.camera.y) { 141 | return false; 142 | } else if (object.y > player.camera.y + canvas.height) { 143 | return false; 144 | } 145 | 146 | return true; 147 | }; 148 | 149 | const angle = (object1, object2) => { 150 | return deg(Math.atan2(object1.y - object2.y, object1.x - object2.x)); 151 | }; 152 | 153 | const randRange = (min, max) => { 154 | return Math.floor(Math.random() * (max - min + 1)) + min; 155 | }; 156 | 157 | const distance = (x1, y1, x2, y2) => { 158 | return Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2); 159 | }; 160 | 161 | const deg = (radian) => { 162 | return radian * (180 / Math.PI); 163 | }; 164 | const rad = (degree) => { 165 | return degree * (Math.PI / 180); 166 | }; 167 | 168 | const randInt = (min, max) => { 169 | return Math.floor(Math.random() * (max - min + 1)) + min; 170 | }; 171 | 172 | const vector2 = (x, y) => { 173 | return { 174 | dirRadian: Math.atan2(y, x), 175 | dirDegree: deg(Math.atan2(y, x)), 176 | velocity: Math.sqrt(x ** 2 + y ** 2), 177 | }; 178 | }; 179 | 180 | const cartesian2 = (angle, velocity) => { 181 | return { 182 | x: velocity * Math.cos(rad(angle)), 183 | y: velocity * Math.sin(rad(angle)), 184 | }; 185 | }; 186 | 187 | const randomGroundTile = () => { 188 | let randy = randInt(0, 10); 189 | 190 | let retVal; 191 | 192 | if (randy === 0) { 193 | retVal = 1; 194 | } else if (randy === 1) { 195 | retVal = 2; 196 | } else if (randy > 1 && randy < 6) { 197 | retVal = 4; 198 | } else { 199 | retVal = 3; 200 | } 201 | 202 | return `ground_${retVal}`; 203 | }; 204 | -------------------------------------------------------------------------------- /engine/data/scripts/input.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | // Keyboard. 4 | let keyboard = []; 5 | 6 | window.onkeydown = (e) => { 7 | keyboard[e.key] = true; 8 | }; 9 | 10 | window.onkeyup = (e) => { 11 | keyboard[e.key] = false; 12 | }; 13 | -------------------------------------------------------------------------------- /engine/data/scripts/libraries/perlin.js: -------------------------------------------------------------------------------- 1 | // Thank you to josephg for this perlin noise library. 2 | // https://github.com/josephg/noisejs 3 | 4 | (function (t) { 5 | function o(t, o, r) { 6 | (this.x = t), (this.y = o), (this.z = r); 7 | } 8 | function r(t) { 9 | return t * t * t * (t * (6 * t - 15) + 10); 10 | } 11 | function n(t, o, r) { 12 | return (1 - r) * t + r * o; 13 | } 14 | var a = (t.noise = {}); 15 | (o.prototype.dot2 = function (t, o) { 16 | return this.x * t + this.y * o; 17 | }), 18 | (o.prototype.dot3 = function (t, o, r) { 19 | return this.x * t + this.y * o + this.z * r; 20 | }); 21 | var e = [ 22 | new o(1, 1, 0), 23 | new o(-1, 1, 0), 24 | new o(1, -1, 0), 25 | new o(-1, -1, 0), 26 | new o(1, 0, 1), 27 | new o(-1, 0, 1), 28 | new o(1, 0, -1), 29 | new o(-1, 0, -1), 30 | new o(0, 1, 1), 31 | new o(0, -1, 1), 32 | new o(0, 1, -1), 33 | new o(0, -1, -1), 34 | ], 35 | i = [ 36 | 151, 160, 137, 91, 90, 15, 131, 13, 201, 95, 96, 53, 194, 233, 7, 37 | 225, 140, 36, 103, 30, 69, 142, 8, 99, 37, 240, 21, 10, 23, 190, 6, 38 | 148, 247, 120, 234, 75, 0, 26, 197, 62, 94, 252, 219, 203, 117, 35, 39 | 11, 32, 57, 177, 33, 88, 237, 149, 56, 87, 174, 20, 125, 136, 171, 40 | 168, 68, 175, 74, 165, 71, 134, 139, 48, 27, 166, 77, 146, 158, 231, 41 | 83, 111, 229, 122, 60, 211, 133, 230, 220, 105, 92, 41, 55, 46, 245, 42 | 40, 244, 102, 143, 54, 65, 25, 63, 161, 1, 216, 80, 73, 209, 76, 43 | 132, 187, 208, 89, 18, 169, 200, 196, 135, 130, 116, 188, 159, 86, 44 | 164, 100, 109, 198, 173, 186, 3, 64, 52, 217, 226, 250, 124, 123, 5, 45 | 202, 38, 147, 118, 126, 255, 82, 85, 212, 207, 206, 59, 227, 47, 16, 46 | 58, 17, 182, 189, 28, 42, 223, 183, 170, 213, 119, 248, 152, 2, 44, 47 | 154, 163, 70, 221, 153, 101, 155, 167, 43, 172, 9, 129, 22, 39, 253, 48 | 19, 98, 108, 110, 79, 113, 224, 232, 178, 185, 112, 104, 218, 246, 49 | 97, 228, 251, 34, 242, 193, 238, 210, 144, 12, 191, 179, 162, 241, 50 | 81, 51, 145, 235, 249, 14, 239, 107, 49, 192, 214, 31, 181, 199, 51 | 106, 157, 184, 84, 204, 176, 115, 121, 50, 45, 127, 4, 150, 254, 52 | 138, 236, 205, 93, 222, 114, 67, 29, 24, 72, 243, 141, 128, 195, 78, 53 | 66, 215, 61, 156, 180, 54 | ], 55 | d = new Array(512), 56 | f = new Array(512); 57 | (a.seed = function (t) { 58 | t > 0 && t < 1 && (t *= 65536), 59 | (t = Math.floor(t)), 60 | t < 256 && (t |= t << 8); 61 | for (var o = 0; o < 256; o++) { 62 | var r; 63 | (r = 1 & o ? i[o] ^ (255 & t) : i[o] ^ ((t >> 8) & 255)), 64 | (d[o] = d[o + 256] = r), 65 | (f[o] = f[o + 256] = e[r % 12]); 66 | } 67 | }), 68 | a.seed(0); 69 | var h = 0.5 * (Math.sqrt(3) - 1), 70 | v = (3 - Math.sqrt(3)) / 6, 71 | u = 1 / 3, 72 | s = 1 / 6; 73 | (a.simplex2 = function (t, o) { 74 | var r, 75 | n, 76 | a, 77 | e, 78 | i, 79 | u = (t + o) * h, 80 | s = Math.floor(t + u), 81 | l = Math.floor(o + u), 82 | w = (s + l) * v, 83 | M = t - s + w, 84 | c = o - l + w; 85 | M > c ? ((e = 1), (i = 0)) : ((e = 0), (i = 1)); 86 | var p = M - e + v, 87 | y = c - i + v, 88 | x = M - 1 + 2 * v, 89 | m = c - 1 + 2 * v; 90 | (s &= 255), (l &= 255); 91 | var q = f[s + d[l]], 92 | z = f[s + e + d[l + i]], 93 | A = f[s + 1 + d[l + 1]], 94 | b = 0.5 - M * M - c * c; 95 | b < 0 ? (r = 0) : ((b *= b), (r = b * b * q.dot2(M, c))); 96 | var g = 0.5 - p * p - y * y; 97 | g < 0 ? (n = 0) : ((g *= g), (n = g * g * z.dot2(p, y))); 98 | var j = 0.5 - x * x - m * m; 99 | return ( 100 | j < 0 ? (a = 0) : ((j *= j), (a = j * j * A.dot2(x, m))), 101 | 70 * (r + n + a) 102 | ); 103 | }), 104 | (a.simplex3 = function (t, o, r) { 105 | var n, 106 | a, 107 | e, 108 | i, 109 | h, 110 | v, 111 | l, 112 | w, 113 | M, 114 | c, 115 | p = (t + o + r) * u, 116 | y = Math.floor(t + p), 117 | x = Math.floor(o + p), 118 | m = Math.floor(r + p), 119 | q = (y + x + m) * s, 120 | z = t - y + q, 121 | A = o - x + q, 122 | b = r - m + q; 123 | z >= A 124 | ? A >= b 125 | ? ((h = 1), (v = 0), (l = 0), (w = 1), (M = 1), (c = 0)) 126 | : z >= b 127 | ? ((h = 1), (v = 0), (l = 0), (w = 1), (M = 0), (c = 1)) 128 | : ((h = 0), (v = 0), (l = 1), (w = 1), (M = 0), (c = 1)) 129 | : A < b 130 | ? ((h = 0), (v = 0), (l = 1), (w = 0), (M = 1), (c = 1)) 131 | : z < b 132 | ? ((h = 0), (v = 1), (l = 0), (w = 0), (M = 1), (c = 1)) 133 | : ((h = 0), (v = 1), (l = 0), (w = 1), (M = 1), (c = 0)); 134 | var g = z - h + s, 135 | j = A - v + s, 136 | k = b - l + s, 137 | B = z - w + 2 * s, 138 | C = A - M + 2 * s, 139 | D = b - c + 2 * s, 140 | E = z - 1 + 3 * s, 141 | F = A - 1 + 3 * s, 142 | G = b - 1 + 3 * s; 143 | (y &= 255), (x &= 255), (m &= 255); 144 | var H = f[y + d[x + d[m]]], 145 | I = f[y + h + d[x + v + d[m + l]]], 146 | J = f[y + w + d[x + M + d[m + c]]], 147 | K = f[y + 1 + d[x + 1 + d[m + 1]]], 148 | L = 0.6 - z * z - A * A - b * b; 149 | L < 0 ? (n = 0) : ((L *= L), (n = L * L * H.dot3(z, A, b))); 150 | var N = 0.6 - g * g - j * j - k * k; 151 | N < 0 ? (a = 0) : ((N *= N), (a = N * N * I.dot3(g, j, k))); 152 | var O = 0.6 - B * B - C * C - D * D; 153 | O < 0 ? (e = 0) : ((O *= O), (e = O * O * J.dot3(B, C, D))); 154 | var P = 0.6 - E * E - F * F - G * G; 155 | return ( 156 | P < 0 ? (i = 0) : ((P *= P), (i = P * P * K.dot3(E, F, G))), 157 | 32 * (n + a + e + i) 158 | ); 159 | }), 160 | (a.perlin2 = function (t, o) { 161 | var a = Math.floor(t), 162 | e = Math.floor(o); 163 | (t -= a), (o -= e), (a &= 255), (e &= 255); 164 | var i = f[a + d[e]].dot2(t, o), 165 | h = f[a + d[e + 1]].dot2(t, o - 1), 166 | v = f[a + 1 + d[e]].dot2(t - 1, o), 167 | u = f[a + 1 + d[e + 1]].dot2(t - 1, o - 1), 168 | s = r(t); 169 | return n(n(i, v, s), n(h, u, s), r(o)); 170 | }), 171 | (a.perlin3 = function (t, o, a) { 172 | var e = Math.floor(t), 173 | i = Math.floor(o), 174 | h = Math.floor(a); 175 | (t -= e), (o -= i), (a -= h), (e &= 255), (i &= 255), (h &= 255); 176 | var v = f[e + d[i + d[h]]].dot3(t, o, a), 177 | u = f[e + d[i + d[h + 1]]].dot3(t, o, a - 1), 178 | s = f[e + d[i + 1 + d[h]]].dot3(t, o - 1, a), 179 | l = f[e + d[i + 1 + d[h + 1]]].dot3(t, o - 1, a - 1), 180 | w = f[e + 1 + d[i + d[h]]].dot3(t - 1, o, a), 181 | M = f[e + 1 + d[i + d[h + 1]]].dot3(t - 1, o, a - 1), 182 | c = f[e + 1 + d[i + 1 + d[h]]].dot3(t - 1, o - 1, a), 183 | p = f[e + 1 + d[i + 1 + d[h + 1]]].dot3(t - 1, o - 1, a - 1), 184 | y = r(t), 185 | x = r(o), 186 | m = r(a); 187 | return n( 188 | n(n(v, w, y), n(u, M, y), m), 189 | n(n(s, c, y), n(l, p, y), m), 190 | x 191 | ); 192 | }); 193 | })(this); 194 | 195 | noise.seed(Math.random()); 196 | -------------------------------------------------------------------------------- /engine/data/scripts/logic/enemies.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /* 4 | While enemies are technically entites. Due to being a large object in the game and having 5 | many very specific functions, they are excluded from the entity management system. They 6 | also are rendered above other entities, just like the player. 7 | */ 8 | 9 | // Enemies. 10 | class Enemy { 11 | constructor(x, y, origin, health = 1) { 12 | // Animation handling. 13 | this.animation = new Anim("spritesheet_enemy", "move", 0, 16, { 14 | // How many frames are in each animation, and at what frame they start. 15 | move: { 16 | start: 0, 17 | end: 1, 18 | }, 19 | hit: { 20 | start: 2, 21 | end: 3, 22 | }, 23 | }); 24 | 25 | // The room the enemy spawned in. 26 | this.origin = origin; 27 | 28 | // The enemy's position on-screen/in-game. 29 | this.x = x; 30 | this.y = y; 31 | 32 | // The enemy's size for collisions. 33 | this.width = 8; 34 | this.height = 8; 35 | 36 | // Physics values. Essentially read-only since there aren't any physics in the game. 37 | this.physics = { 38 | lastX: 0, 39 | lastY: 0, 40 | velocity: 0, 41 | }; 42 | this.dir = 3; 43 | 44 | // Max movement speed. 45 | this.speed = 1; 46 | this.speed > 8 ? (this.speed = 8) : ""; 47 | 48 | // The position around the player to move to. 49 | this.distanceToPlayer = 12; 50 | 51 | // enemy stats. 52 | this.health = health; 53 | this.maxHealth = health; 54 | this.damage = 0.5; 55 | this.attackFrequency = 150; // How often the enemy can hit the player. 56 | this.lastAttack = 999; 57 | this.gotHit = 0; // How long to display that the enemy got hit. 58 | } 59 | 60 | // Handle attacking. 61 | attack() { 62 | if (this.lastAttack >= this.attackFrequency) { 63 | player.camera.shake(5, 1); 64 | player.inventory.health -= this.damage; 65 | 66 | this.lastAttack = 0; 67 | } 68 | } 69 | 70 | // Handle animations 71 | animate() { 72 | // If we've been hit recently 73 | if (this.gotHit > 0) { 74 | this.animation.name = "hit"; 75 | } else { 76 | this.animation.name = "move"; 77 | } 78 | 79 | // Run the animation. 80 | this.animation.animate(); 81 | } 82 | 83 | logic() { 84 | // Subtract from the amount of time that we display we got hit. 85 | this.gotHit--; 86 | 87 | // Store the enemy's current position. 88 | this.physics.lastX = this.x; 89 | this.physics.lastY = this.y; 90 | 91 | let targetPos = cartesian2( 92 | angle(player, this), 93 | distance(this.x, this.y, player.x, player.y) / 6 94 | ); 95 | 96 | let otherTarget = cartesian2(Math.random() * 360, randInt(32, 64)); 97 | 98 | targetPos.x += otherTarget.x; 99 | targetPos.y += otherTarget.y; 100 | 101 | // Check for collisions in the direction of travel and then apply the travel if there are none. 102 | if ( 103 | distance( 104 | this.x + 4, 105 | this.y + 4, 106 | player.x + 4 + targetPos.x, 107 | player.y + 4 + targetPos.y 108 | ) > 14 109 | ) { 110 | if (player.y + 4 + targetPos.y < this.y) { 111 | this.y -= this.speed; 112 | this.dir = 0; 113 | } 114 | if (player.x + 4 + targetPos.x > this.x) { 115 | this.x += this.speed; 116 | this.dir = 1; 117 | } 118 | if (player.y + 4 + targetPos.y > this.y) { 119 | this.y += this.speed; 120 | this.dir = 2; 121 | } 122 | if (player.x + 4 + targetPos.x < this.x) { 123 | this.x -= this.speed; 124 | this.dir = 3; 125 | } 126 | } 127 | 128 | // Handle attacking. 129 | if (AABB(this, player)) { 130 | this.attack(); 131 | } 132 | this.lastAttack++; 133 | 134 | // Set the enemies animation direction. 135 | if (player.x > this.x) { 136 | this.animation.direction = 1; 137 | } else { 138 | this.animation.direction = 0; 139 | } 140 | 141 | // Check if we get hit by the player. 142 | if ( 143 | player.attackAnimation.attacking && 144 | player.attackAnimation.frame === 3 && 145 | AABB(this, player.attackAnimation.hitbox) 146 | ) { 147 | this.health -= player.hitStrength; 148 | 149 | // Knockback. 150 | let knockback = cartesian2(angle(this, player), player.knockback); 151 | 152 | this.x += knockback.x; 153 | this.y += knockback.y; 154 | 155 | this.gotHit = 60; 156 | } 157 | 158 | // Check the enemy's health. 159 | if (this.health <= 0) { 160 | this.origin.destroyEnemy(this); 161 | } 162 | 163 | this.x = Math.round(this.x); 164 | this.y = Math.round(this.y); 165 | 166 | // Calculate the enemy's velocity. 167 | this.physics.velocity = vector2( 168 | Math.abs(this.x - this.physics.lastX), 169 | Math.abs(this.y - this.physics.lastY) 170 | ); 171 | 172 | this.animate(); 173 | } 174 | 175 | render(ctx) { 176 | // Check if the enemy is on screen. 177 | if (!isOnScreen(this)) return; 178 | 179 | try { 180 | // Render enemy. 181 | ctx.beginPath(); 182 | 183 | ctx.drawImage( 184 | this.animation.map, // The tilemap image. 185 | this.animation.frame * 8 + 186 | (this.animation.direction === 1 && 32), // The x and y sub-coordinates to grab the tile's texture from the image. 187 | 0, 188 | 8, // The 8x8 pixel dimensions of that sub-image. 189 | 8, 190 | Math.round(this.x) - player.camera.x, // Proper placement of the tile on screen. 191 | Math.round(this.y) - player.camera.y, 192 | 8, // The size of the tile, as drawn on screen. 193 | 8 194 | ); 195 | 196 | ctx.closePath(); 197 | 198 | // Render health bar. 199 | ctx.beginPath(); // Background. 200 | 201 | ctx.fillStyle = "black"; 202 | 203 | ctx.fillRect( 204 | this.x - player.camera.x - 2, 205 | this.y - player.camera.y - 1, 206 | 12, 207 | 1 208 | ); 209 | 210 | ctx.closePath(); 211 | 212 | ctx.beginPath(); // Bar. 213 | 214 | ctx.fillStyle = 215 | this.health / this.maxHealth <= 0.25 ? "tomato" : "limegreen"; 216 | 217 | ctx.fillRect( 218 | this.x - player.camera.x - 2, 219 | this.y - player.camera.y - 1, 220 | 12 * (this.health / this.maxHealth), 221 | 1 222 | ); 223 | 224 | ctx.closePath(); 225 | } catch { 226 | return; 227 | } 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /engine/data/scripts/logic/entities.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /* 4 | An entity can have rendering, logic, user input, as well as all other properties. 5 | 6 | Such as: 7 | 8 | - Be a light source. 9 | - Drop items. 10 | 11 | 12 | Entities are a blank canvas for non-static objects and tiles in the world. 13 | 14 | */ 15 | 16 | // The global parent of all entities. Used for management. Unlike most global parents, this one does not create entities. It just stores them. 17 | class EntityManager { 18 | constructor() { 19 | this.entities = []; 20 | } 21 | 22 | // The entity manager doesn't require input. But we keep this function to avoid potential errors. 23 | input() {} 24 | 25 | logic() { 26 | // Loop through and run logic functions of all entities. 27 | for (let entity of this.entities) { 28 | entity.logic(); 29 | } 30 | } 31 | 32 | render(ctx) { 33 | // Loop through and run rendering functions of all entities. 34 | for (let entity of this.entities) { 35 | entity.render(ctx); 36 | } 37 | } 38 | } 39 | let entities = new EntityManager(); 40 | 41 | // An entity boilerplate. 42 | class Entity { 43 | constructor(x, y) { 44 | this.x = x; 45 | this.y = y; 46 | 47 | // Add this entity to the global parent. 48 | entities.entities.push(this); 49 | } 50 | 51 | // Placeholder rendering. Can be used to render a blank entity to show errors... Potentially. 52 | logic() {} 53 | render(ctx) { 54 | // Check if the entity is on screen. 55 | if (!isOnScreen(this)) return; 56 | } 57 | } 58 | 59 | // A torch object. 60 | class Torch extends Entity { 61 | constructor(x, y, lightPulses = true) { 62 | super(x, y); // Do regular entity stuff. 63 | 64 | // Lighting data. 65 | this.initLightStrength = randRange(6, 10); // STore the initial light strength value. 66 | this.lightStrength = this.initLightStrength; // The strength of the light this torch emits. 67 | this.lightDistance = randRange(24, 32); 68 | this.lightPulses = lightPulses; // Wether or not the light strength of the torch changes slightly to look like a flame growing and shrinking. 69 | 70 | // Add this to the global light sources so its light is applied to tiles. 71 | world.globalLights.push(this); 72 | 73 | // Collisions data. 74 | this.width = 8; 75 | this.height = 8; 76 | this.solid = true; // Torches can be walked through. 77 | 78 | // Animation data. 79 | this.animation = new Anim( 80 | "spritesheet_torch", 81 | "loop", 82 | 0, 83 | 4, 84 | { 85 | // How many frames are in each animation, and at what frame they start. 86 | loop: { 87 | start: 0, 88 | end: 2, 89 | }, 90 | }, 91 | undefined, 92 | {} 93 | ); 94 | } 95 | 96 | // Animate the torch. 97 | animate() { 98 | // Flicker the light if we should. 99 | if (this.lightPulses && this.animation.frame % 3) { 100 | this.lightStrength += randRange(-1, 1); 101 | } 102 | 103 | this.animation.animate(); 104 | } 105 | 106 | logic() { 107 | // Check if the entity is on screen. (since the only logic performed is for animation, it isn't necessary off-screen) 108 | if (!isOnScreen(this)) return; 109 | 110 | // Run animations. 111 | this.animate(); 112 | 113 | // Check that the light strength isn't out of bounds. 114 | if (this.lightStrength < this.initLightStrength / 2) { 115 | this.lightStrength = this.initLightStrength / 2; 116 | } else if (this.lightStrength > this.initLightStrength) { 117 | this.lightStrength = this.initLightStrength; 118 | } 119 | } 120 | 121 | render(ctx) { 122 | // Check if the entity is on screen. 123 | if (!isOnScreen(this)) return; 124 | 125 | // We attempt to render. The only reason this would fail is if the spritesheet hasn't loaded yet. Which for the first few frames after the engine starts, it hasn't. 126 | try { 127 | ctx.beginPath(); 128 | 129 | ctx.drawImage( 130 | this.animation.map, // The tilemap image. 131 | this.animation.frame * 8, // The x and y sub-coordinates to grab the animation's texture from the image. 132 | 0, 133 | 8, // The 8x8 pixel dimensions of that sub-image. 134 | 8, 135 | this.x - player.camera.x, // Proper placement of the animation on screen. 136 | this.y - player.camera.y, 137 | 8, // The size of the animation, as drawn on screen. 138 | 8 139 | ); 140 | 141 | ctx.closePath(); 142 | } catch { 143 | return; 144 | } 145 | } 146 | } 147 | 148 | // Debugging: 149 | // new Torch(8, 8, 3, false); 150 | // new Torch(72, 8, 3, false); 151 | // new Torch(72, 72, 3, false); 152 | // new Torch(8, 72, 3, false); 153 | -------------------------------------------------------------------------------- /engine/data/scripts/logic/logic.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | // Specific Objects. 4 | 5 | // Items. 6 | class Item { 7 | constructor(x, y, type = "coin", value = "1") { 8 | // Position on the screen. 9 | this.x = x; 10 | this.y = y; 11 | 12 | // Size for collision detection. 13 | this.width = 4; 14 | this.height = 4; 15 | 16 | // The type is the thing that is added to in the player's inventory. The value is how much is added. 17 | this.type = type; 18 | this.value = value; 19 | 20 | // The items expiration. Used to remove lingering items. 21 | this.expire = 99999; 22 | } 23 | 24 | // Destroy this item. 25 | destroy() { 26 | items.items.splice(items.items.indexOf(this), 1); 27 | } 28 | 29 | // Collect this item into the player's inventory and then destroy it. 30 | collect() { 31 | // Modify the player's inventory. 32 | if (!player.inventory[this.type]) { 33 | player.inventory[this.type] = this.value; 34 | } else { 35 | player.inventory[this.type] += this.value; 36 | } 37 | 38 | // Destroy this item. 39 | this.destroy(); 40 | } 41 | 42 | logic() { 43 | // Tick the item's expiration downward and destroy if necessary. 44 | this.expire--; 45 | this.expire <= 0 && this.destroy(); 46 | 47 | // Move towards the player. 48 | if (distance(this.x + 2, this.y + 2, player.x + 4, player.y + 4) < 16) { 49 | let newPos = cartesian2( 50 | angle( 51 | { x: player.x + 4, y: player.y + 4 }, 52 | { x: this.x + 2, y: this.y + 2 } 53 | ), 54 | distance(this.x + 2, this.y + 2, player.x + 4, player.y + 4) / 6 55 | ); 56 | 57 | this.x += newPos.x; 58 | this.y += newPos.y; 59 | 60 | // Check for collision with the player. 61 | if (AABB(this, player)) { 62 | this.collect(); 63 | } 64 | } 65 | } 66 | 67 | // Render this item. 68 | render(ctx) { 69 | // Check if the item is on screen. 70 | if (!isOnScreen(this)) return; 71 | 72 | try { 73 | ctx.beginPath(); 74 | 75 | ctx.drawImage( 76 | items.map, // The tilemap image. 77 | items.itemIndeces[this.type] !== undefined 78 | ? items.itemIndeces[this.type] 79 | : 0, // The x and y sub-coordinates to grab the tile's texture from the image. 80 | 0, 81 | 4, // The 4x4 pixel dimensions of that sub-image. 82 | 4, 83 | Math.round(this.x - player.camera.x), // Proper placement of the tile on screen. 84 | Math.round(this.y - player.camera.y), 85 | 4, // The size of the tile, as drawn on screen. 86 | 4 87 | ); 88 | 89 | ctx.closePath(); 90 | } catch { 91 | return; 92 | } 93 | } 94 | } 95 | 96 | // Item manager. 97 | class ItemManager { 98 | constructor() { 99 | this.items = []; 100 | 101 | this.itemIndeces = { 102 | coin: 0, 103 | }; 104 | 105 | this.map = undefined; 106 | this.load_tilemap(); 107 | 108 | // Debugging: creates lots of coins. 109 | // for (let i = 0; i < 100; i++) { 110 | // this.createItem( 111 | // 8 + Math.random() * 93, 112 | // 8 + Math.random() * 45, 113 | // "coin", 114 | // Math.ceil(Math.random() * 8) 115 | // ); 116 | // } 117 | } 118 | 119 | // Load the image source for the tilemap. Should be done before any rendering is attempted. But the rendering is given a try catch since JS is asynchronous. 120 | load_tilemap = () => { 121 | let img = new Image(); 122 | img.onload = () => { 123 | this.map = img; 124 | }; 125 | img.src = "./data/images/tilemap_items.png"; 126 | }; 127 | 128 | // Add an item to the game. 129 | createItem(x, y, type, value) { 130 | let item = new Item(x, y, type, value); 131 | 132 | this.items.push(item); 133 | 134 | return item; 135 | } 136 | 137 | logic() { 138 | // Check if the max item limit has been reached. 139 | while (this.items.length >= 100) { 140 | this.items.shift(); 141 | } 142 | 143 | // Run game logic for all items. 144 | for (let item of this.items) { 145 | // Check if the item is on screen. 146 | if (!isOnScreen(item)) continue; 147 | 148 | item.logic(); 149 | } 150 | } 151 | 152 | render(ctx) { 153 | // Render all items. 154 | for (let item of this.items) { 155 | // Check if the item is on screen. 156 | if (!isOnScreen(item)) continue; 157 | 158 | item.render(ctx); 159 | } 160 | } 161 | } 162 | const items = new ItemManager(); 163 | 164 | // A single tile, usually as part of a room, but can be secluded. (will not render unless in a room) 165 | class Tile { 166 | constructor(x, y, type) { 167 | // The tiles position on-screen. 168 | this.x = x; 169 | this.y = y; 170 | 171 | // Sizing, only used for collision detection. 172 | this.width = 8; 173 | this.height = 8; 174 | 175 | // Global alpha for lighting. 176 | this.globalAlpha = 0; 177 | 178 | // The type of tile it is... Used to grab tiledata from the tiles array. If the tile doesn't exist then we load an error tile. 179 | this.type = tiles[type] === undefined ? "err" : type; 180 | 181 | this.data = tiles[this.type]; 182 | } 183 | 184 | logic() { 185 | // Reset the globalAlpha of the tile each logic loop. 186 | this.globalAlpha = 0; 187 | } 188 | 189 | render(ctx) { 190 | // If the tile is not on-screen, we don't render it. 191 | if (!isOnScreen(this)) return; 192 | 193 | try { 194 | // Only apply lighting effects if the option is turned on. 195 | if (renderLighting) { 196 | ctx.save(); 197 | 198 | // Get and apply the lighting data for this tile. 199 | ctx.globalAlpha = this.globalAlpha; 200 | } 201 | 202 | ctx.beginPath(); 203 | 204 | ctx.drawImage( 205 | tiles.map, // The tilemap image. 206 | this.data.pos.x * 8, // The x and y sub-coordinates to grab the tile's texture from the image. 207 | this.data.pos.y * 8, 208 | 8, // The 8x8 pixel dimensions of that sub-image. 209 | 8, 210 | this.x - player.camera.x, // Proper placement of the tile on screen. 211 | this.y - player.camera.y, 212 | 8, // The size of the tile, as drawn on screen. 213 | 8 214 | ); 215 | 216 | ctx.closePath(); 217 | 218 | // We only need to restore canvas presets if the lighting is being rendered. 219 | if (renderLighting) { 220 | ctx.restore(); 221 | } 222 | } catch { 223 | return; 224 | } 225 | } 226 | } 227 | 228 | class Door extends Tile { 229 | constructor(x, y, type, exits, canCreateRooms = true) { 230 | super(x, y, type); 231 | 232 | // The room the door exits from. 233 | this.exits = exits; 234 | 235 | // The direction from the room that the door faces. 236 | this.direction = null; 237 | this.calculateDirection(); 238 | 239 | // Generate a hallway. 240 | // this.generateHallway(); 241 | 242 | // Determine if we can create rooms based on outside factors. 243 | if (world.roomCount <= 0) { 244 | canCreateRooms = false; 245 | } 246 | 247 | // Generate the next room. 248 | if (canCreateRooms) { 249 | let self = this; 250 | window.setTimeout(() => { 251 | self.generateNextRoom(); 252 | }, 1); 253 | } 254 | this.hasGenerated = false; 255 | 256 | // The status of the door. 257 | this.open = false; 258 | this.locked = false; 259 | } 260 | 261 | // Generate a room from the door based on it's direction. 262 | generateNextRoom() { 263 | // Check if this door has already generated a room. 264 | if (this.hasGenerated) { 265 | return "This door has already generated a room."; 266 | } 267 | 268 | // Apply the fact that this door has generated a room, even if it doesn't because there is already one here. 269 | this.hasGenerated = true; 270 | 271 | // Calculate where the room should be. 272 | let roomPos = { 273 | x: this.x, 274 | y: this.y, 275 | }; 276 | 277 | // Caluculate the type of room. 278 | let roomType; 279 | let perlin = (x, y) => { 280 | // Convert the x and y to room coordinates. (0 (px) === 0 (room pos), 88 (px) === 1 (room pos)) 281 | x /= 88; 282 | y /= 88; 283 | 284 | // Generate perlin noise for determining the room type. 285 | let resolution = 0.35; // Finally tunes the noise to different effects. 286 | let perlin_noise = noise.simplex2(x * resolution, y * resolution); 287 | 288 | // Return needs to be a binary value. (0 or 1) 289 | return perlin_noise > 0 ? 1 : 0; 290 | }; 291 | switch (this.direction) { 292 | case "u": 293 | roomPos.y -= 11 * 8; // Move the door towards the right distance. 294 | roomPos.x -= Math.round(11 * 3.6); // Line the doors up. 295 | 296 | roomType = ["dr", "dl"][perlin(roomPos.x, roomPos.y)]; // Determine the room type. 297 | 298 | break; 299 | case "d": 300 | roomPos.y += 8; // Move the door towards the right distance. 301 | roomPos.x -= Math.round(11 * 3.6); // Line the doors up. 302 | 303 | roomType = ["ur", "ul"][perlin(roomPos.x, roomPos.y)]; // Determine the room type. 304 | 305 | break; 306 | case "l": 307 | roomPos.x -= 11 * 8; // Move the door towards the right distance. 308 | roomPos.y -= Math.round(11 * 3.6); // Line the doors up. 309 | 310 | roomType = ["ur", "dr"][perlin(roomPos.x, roomPos.y)]; // Determine the room type. 311 | 312 | break; 313 | case "r": 314 | roomPos.x += 8; // Move the door towards the right distance. 315 | roomPos.y -= Math.round(11 * 3.6); // Line the doors up. 316 | 317 | roomType = ["ul", "dl"][perlin(roomPos.x, roomPos.y)]; // Determine the room type. 318 | 319 | break; 320 | default: 321 | // If the direction is wrong, we straight up return. 322 | return; 323 | } 324 | 325 | // Check if a room already exists here. If one does, we quit the function now. 326 | if (world.getRoom(roomPos.x, roomPos.y).length !== 0) { 327 | return; 328 | } 329 | 330 | // If there isn't a room here, we generate one. 331 | world.createRoomFromData(roomPos.x, roomPos.y, roomType, true, true); 332 | } 333 | 334 | // Calculate the direction the door faces going away from the room. 335 | calculateDirection() { 336 | // Determine the center of the room. 337 | let centerOfRoom = { 338 | x: this.exits.x + this.exits.width / 2 - 4, 339 | y: this.exits.y + this.exits.height / 2 - 4, 340 | }; 341 | 342 | // Determine the direction from the room. 343 | if (this.y === centerOfRoom.y) { 344 | // If the door is centered on the y-axis. 345 | if (this.x < centerOfRoom.x) { 346 | // We are on the left. 347 | this.direction = "l"; 348 | } else { 349 | // We are on the right. 350 | this.direction = "r"; 351 | } 352 | } else { 353 | // If the door is not cented on the y-axis. 354 | 355 | if (this.y < centerOfRoom.y) { 356 | // We are above. 357 | this.direction = "u"; 358 | } else { 359 | // We are below. 360 | this.direction = "d"; 361 | } 362 | } 363 | } 364 | 365 | // // Generate a hallway from the door based on it's direction. 366 | // generateHallway() { 367 | // // Calculate where the hallway should be. 368 | // let hallwayPos = { 369 | // x: this.x, 370 | // y: this.y, 371 | // }; 372 | 373 | // // Caluculate the type of hallway. 374 | // let hallwayType; 375 | // this.direction === "l" || this.direction === "r" 376 | // ? (hallwayType = "lr") // The hallway moves left and right. 377 | // : (hallwayType = "ud"); // The hallway moves up and down. 378 | 379 | // // Move the hallway so it lines up properly. 380 | // if (hallwayType === "lr") { 381 | // // If the hallway moves left and right. 382 | // if (this.direction === "l") { 383 | // // If the door is heading left. 384 | // hallwayPos.x -= rooms[hallwayType][0].length * 8 - 8; 385 | // } else { 386 | // // If the door is heading right. 387 | // hallwayPos.x += 8; 388 | // } 389 | 390 | // // Center the hallway. 391 | // hallwayPos.y -= (rooms[hallwayType].length / 4) * 8 - 6; 392 | // } else { 393 | // // If the hallway moves up and down. 394 | // if (this.direction === "u") { 395 | // // If the door is heading up. 396 | // hallwayPos.y -= rooms[hallwayType].length * 8 - 8; 397 | // } else { 398 | // // If the door is heading down. 399 | // hallwayPos.y += 8; 400 | // } 401 | 402 | // // Center the hallway. 403 | // hallwayPos.x -= (rooms[hallwayType][0].length / 4) * 8 - 6; 404 | // } 405 | 406 | // // Generate the hallway. 407 | // world.createRoomFromData( 408 | // hallwayPos.x, 409 | // hallwayPos.y, 410 | // `hall_${hallwayType}`, 411 | // true 412 | // ); 413 | // } 414 | 415 | logic() { 416 | // Reset the globalAlpha of the tile each logic loop. 417 | this.globalAlpha = 0; 418 | 419 | // Determine if the door should be open or closed. 420 | if (this.open) { 421 | this.data = tiles.door_open; 422 | } else { 423 | this.data = tiles[this.type]; 424 | } 425 | 426 | // Open the door with Z. 427 | let dt = distance(this.x + 4, this.y + 4, player.x + 4, player.y + 4); // The distance between the door and the player. 428 | 429 | // If the door is unlocked, but not open, we open it. 430 | if ( 431 | dt <= 9 && 432 | !this.open && 433 | !this.locked && 434 | (keyboard.x || keyboard.k) 435 | ) { 436 | this.open = true; 437 | } 438 | } 439 | 440 | render(ctx) { 441 | // If the tile is not on-screen, we don't render it. 442 | if (!isOnScreen(this)) return; 443 | 444 | try { 445 | // Only apply lighting effects if the option is turned on. 446 | if (renderLighting) { 447 | ctx.save(); 448 | 449 | // Get and apply the lighting data for this tile. 450 | ctx.globalAlpha = this.globalAlpha; 451 | } 452 | 453 | ctx.beginPath(); 454 | 455 | ctx.drawImage( 456 | tiles.map, // The tilemap image. 457 | this.data.pos.x * 8, // The x and y sub-coordinates to grab the tile's texture from the image. 458 | this.data.pos.y * 8, 459 | 8, // The 8x8 pixel dimensions of that sub-image. 460 | 8, 461 | this.x - player.camera.x, // Proper placement of the tile on screen. 462 | this.y - player.camera.y, 463 | 8, // The size of the tile, as drawn on screen. 464 | 8 465 | ); 466 | 467 | ctx.closePath(); 468 | 469 | // If we are close enough to the player, and we aren't open, then we get highlighted. 470 | if ( 471 | distance(this.x + 4, this.y + 4, player.x + 4, player.y + 4) <= 472 | 9 && 473 | !this.open 474 | ) { 475 | ctx.beginPath(); 476 | ctx.strokeStyle = this.locked ? "red" : "yellow"; 477 | ctx.lineWidth = 1; 478 | 479 | ctx.rect( 480 | Math.round(this.x - player.camera.x), 481 | Math.round(this.y - player.camera.y), 482 | 8, 483 | 8 484 | ); 485 | 486 | ctx.stroke(); 487 | ctx.closePath(); 488 | } 489 | 490 | // We only need to restore canvas presets if the lighting is being rendered. 491 | if (renderLighting) { 492 | ctx.restore(); 493 | } 494 | } catch { 495 | return; 496 | } 497 | } 498 | } 499 | 500 | // A room, contains tiles and events as well as handeling of loot and enemies. 501 | class Room { 502 | constructor( 503 | x, 504 | y, 505 | autogen = true, 506 | roomName, 507 | roomsDoorsCanGenerateRooms = true 508 | ) { 509 | // The room's position on-screen. 510 | this.x = x; 511 | this.y = y; 512 | 513 | this.width = 0; 514 | this.height = 0; 515 | 516 | // The room's tiles. 517 | this.tiles = []; 518 | 519 | // Determine wether or not the doors in this room can generate more rooms. 520 | this.doorsCanGenerateMoreRooms = roomsDoorsCanGenerateRooms; 521 | 522 | // Room events: 523 | this.type = "ambient"; 524 | // Determine the room type. 525 | let roomChance = Math.random() * 100; // Room chances span from 0-100. 526 | if (roomChance < 80) { 527 | this.type = "hostile"; 528 | } else if (roomChance >= 80) { 529 | this.type = "coins"; 530 | } 531 | // Determine the room data statuses. 532 | this.cleared = false; // Whether or not the player has cleared the room. 533 | this.active = false; // Whether or not the room activily needs to be cleared. 534 | this.triggered = false; // Whether or not the player has triggered the room. 535 | 536 | this.enemies = 0; 537 | this.enemyCache = []; 538 | 539 | if (this.type === "hostile") { 540 | this.enemies = randInt(1, 3); 541 | } 542 | 543 | // If the room is not autogenerated, generate the tiles from the roomName. 544 | if (!autogen) { 545 | // Populate tiles from existing room data. 546 | this.populateTiles(roomName); 547 | } else { 548 | // If the room is autogenerated, generate it and then populate tiles. :| 549 | this.autoGen(); 550 | } 551 | } 552 | 553 | // Creating enemies. 554 | createEnemy(x, y, health = 1) { 555 | let enemy = new Enemy(this.x + x, this.y + y, this, health); 556 | 557 | this.enemyCache.push(enemy); 558 | 559 | return enemy; 560 | } 561 | 562 | // Destroying enemies. 563 | destroyEnemy(enemy) { 564 | this.enemyCache.splice(this.enemyCache.indexOf(enemy), 1); 565 | } 566 | 567 | // Handle all events: loot, enemy spawning, etc. 568 | eventHandler() { 569 | if (this.cleared) return; // If the room has already been cleared we have nothing to do. 570 | 571 | // These happen every frame when the room is not cleared. 572 | // If the hostile type room is cleared of enemies. 573 | if ( 574 | this.active && 575 | this.type === "hostile" && 576 | this.enemyCache.length <= 0 577 | ) { 578 | this.cleared = true; 579 | } 580 | 581 | // If we have not been cleared, then we check if the player has entered the room, and activate it, unless it is already active. 582 | if ( 583 | AABB(this, player) && 584 | distance( 585 | this.x + this.width / 2, 586 | this.y + this.height / 2, 587 | player.x + 4, 588 | player.y + 4 589 | ) < 590 | this.width / 2 - 12 && 591 | !this.triggered && 592 | !this.active && 593 | !this.cleared 594 | ) { 595 | this.triggered = true; 596 | } 597 | 598 | // Once the room has been triggered: 599 | if (this.triggered && !this.cleared) { 600 | // If it has not been cleared, than we start the event. 601 | 602 | this.triggered = false; // Remove the trigger so this only happens once. 603 | this.active = true; // Activate the room. 604 | 605 | // Close and lock all the room's doors. 606 | let doors = this.tiles.filter( 607 | (tile) => 608 | tile.type === "door_closed" || tile.type === "door_open" 609 | ); 610 | 611 | for (let door of doors) { 612 | door.open = false; 613 | door.locked = true; 614 | } 615 | 616 | // Different trigger functionality. 617 | if (this.type === "coins") { 618 | // Coin type room generates a random amount of coins at random places inside itself. 619 | let coinCount = randInt(5, 10); 620 | 621 | for (let i = 0; i < coinCount; i++) { 622 | items.createItem( 623 | randInt(this.x + 12, this.x + this.width - 12), 624 | randInt(this.y + 12, this.y + this.width - 12), 625 | "coin", 626 | 1 627 | ); 628 | } 629 | 630 | // Coin type rooms are immediately cleared. 631 | this.cleared = true; 632 | } else if (this.type === "hostile") { 633 | for (let i = 0; i < this.enemies; i++) { 634 | this.createEnemy(16 + i * 8 + i, this.width / 2, 1); 635 | } 636 | } else if (this.type === "ambient") { 637 | // If the room is ambient. 638 | this.cleared = true; 639 | } 640 | } 641 | 642 | // When the room is cleared. This happens once. 643 | if (this.cleared && this.active) { 644 | // If it has been cleared, then we remove the doors. 645 | player.roomsCleared++; // Add to the count of how many rooms the player has cleared. 646 | 647 | this.triggered = false; // Remove the trigger so this only happens once. 648 | this.active = false; 649 | 650 | let doors = this.tiles.filter( 651 | (tile) => 652 | tile.type === "door_closed" || tile.type === "door_open" 653 | ); 654 | 655 | for (let door of doors) { 656 | this.destroyTile(door); 657 | this.createTile( 658 | door.x - this.x, 659 | door.y - this.y, 660 | randomGroundTile(), 661 | false 662 | ); 663 | } 664 | 665 | // Create torches. 666 | new Torch(this.x + 8, this.y + 8, true); 667 | new Torch(this.x + 72, this.y + 8, true); 668 | new Torch(this.x + 72, this.y + 72, true); 669 | new Torch(this.x + 8, this.y + 72, true); 670 | } 671 | } 672 | 673 | // Autogenerate a room. 674 | autoGen() { 675 | // The width and height of the room. 676 | let width = randInt(11, 22); 677 | let height = randInt(11, 22); 678 | 679 | // The room we generate. 680 | let genedRoom = []; 681 | 682 | // Loop through the width and height 683 | for (let y = 0; y < height; y++) { 684 | if (y !== 0 && y !== height - 1) { 685 | let xRow = []; // The row we add tiles to. 686 | 687 | for (let x = 0; x < width; x++) { 688 | if (x !== 0 && x !== width - 1) { 689 | xRow.push(randInt(1, 2)); 690 | } else { 691 | xRow.push(3); 692 | } // Add floor tiles if we are not on the edge. 693 | } 694 | 695 | genedRoom.push(xRow); 696 | } else { 697 | // Add top and bottom walls. 698 | let xRow = []; // The row we add tiles to. 699 | 700 | for (let x = 0; x < width; x++) { 701 | // Add regular walls, or lip walls if we are at the top. 702 | if (y === 0 && x >= 1 && x < width - 1) { 703 | xRow.push(4); 704 | } else { 705 | xRow.push(3); 706 | } 707 | } 708 | 709 | genedRoom.push(xRow); 710 | } 711 | } 712 | 713 | // Create the tiles from that room generation. 714 | this.populateTiles(genedRoom); 715 | } 716 | 717 | // Populate tiles from data. 718 | populateTiles(room) { 719 | // Check if the input is roomdata, or a room name. 720 | let roomData; 721 | if (typeof room === "string") { 722 | roomData = rooms[room]; 723 | } else { 724 | roomData = room; 725 | } 726 | 727 | // Apply the width and height. (accurate to pixel-dimensions) 728 | this.width = roomData[0].length * 8; 729 | this.height = roomData.length * 8; 730 | 731 | // Create all the tiles from the data. 732 | for (let y in roomData) { 733 | for (let x in roomData[y]) { 734 | let searchedTile = roomData[y][x]; 735 | 736 | // Grab the wanted tiledata from tiles using the desired tile's ID. 737 | for (let tile in tiles) { 738 | if (typeof tiles[tile] !== "object") { 739 | continue; 740 | } 741 | 742 | let tileData = tiles[tile]; 743 | 744 | if (!"id" in tileData) { 745 | continue; 746 | } 747 | 748 | // If we found the tile with the right ID, we grab it and then break. 749 | if (tileData.id === searchedTile) { 750 | searchedTile = tile; 751 | 752 | break; 753 | } 754 | } 755 | 756 | // Randomize ground tiles. 757 | if (searchedTile === "ground_1") { 758 | searchedTile = randomGroundTile(); 759 | } 760 | 761 | // At this point, searchedTile is the key for the tiledata we want. 762 | this.createTile( 763 | x * 8, 764 | y * 8, 765 | searchedTile, 766 | this.doorsCanGenerateMoreRooms 767 | ); 768 | } 769 | } 770 | } 771 | 772 | // Create a tile. 773 | createTile(x, y, type, doorsCanGenerateMoreRooms = true) { 774 | if (type !== "door_closed" && type !== "door_open") { 775 | // If it is a regular tile. 776 | let tile = new Tile(x + this.x, y + this.y, type); 777 | this.tiles.push(tile); 778 | world.globalTiles.push(tile); 779 | 780 | return tile; 781 | } else { 782 | // If the tile is a door. 783 | let door = new Door( 784 | x + this.x, 785 | y + this.y, 786 | type, 787 | this, 788 | doorsCanGenerateMoreRooms 789 | ); 790 | this.tiles.push(door); 791 | world.globalTiles.push(door); 792 | 793 | return door; 794 | } 795 | } 796 | 797 | // Destroy a tile. 798 | destroyTile(tile) { 799 | this.tiles.splice(this.tiles.indexOf(tile), 1); 800 | world.globalTiles.splice(world.globalTiles.indexOf(tile), 1); 801 | } 802 | 803 | // Render this room's enemies. 804 | renderEnemies(ctx) { 805 | // Render all enemies in the room. 806 | if (this.type === "hostile" && this.enemyCache.length > 0) { 807 | for (let enemy of this.enemyCache) { 808 | // Check if the enemy is on screen. 809 | if (!isOnScreen(enemy)) continue; 810 | enemy.render(ctx); 811 | } 812 | } 813 | } 814 | 815 | logic() { 816 | // Run the event handler for this room. 817 | this.eventHandler(); 818 | 819 | // Run the logic for all enemies in the room. 820 | if (this.type === "hostile" && this.enemyCache.length > 0) { 821 | for (let enemy of this.enemyCache) { 822 | enemy.logic(); 823 | } 824 | } 825 | 826 | // Update all tiles. 827 | for (let tile of this.tiles) { 828 | tile.logic(); 829 | } 830 | } 831 | 832 | render(ctx) { 833 | // Render a placeholder if there are no tiles. ---------------------------- DELETE AFTER DEBUGGING 834 | if (this.tiles.length === 0) { 835 | ctx.beginPath(); 836 | 837 | ctx.fillStyle = "green"; 838 | ctx.fillRect( 839 | this.x - player.camera.x, 840 | this.y - player.camera.y, 841 | 8, 842 | 8 843 | ); 844 | 845 | ctx.closePath(); 846 | } 847 | 848 | // Render all tiles. 849 | for (let tile of this.tiles) { 850 | // Check if the tile is on screen. 851 | if (!isOnScreen(tile)) continue; 852 | 853 | tile.render(ctx); 854 | } 855 | 856 | // Render all enemies. 857 | // this.renderEnemies(ctx); 858 | } 859 | } 860 | 861 | // The world, or level, which holds all rooms and major game logic. 862 | class World { 863 | constructor() { 864 | // A collection of all the rooms in the world. 865 | this.rooms = []; 866 | this.globalTiles = []; 867 | 868 | // Lighting. 869 | this.globalLights = []; 870 | 871 | // The maximum amount of rooms in the world. 872 | this.roomCount = 25; 873 | this.finishedGenerating = false; // If the world has finished generating. (disabled so you can see the world grow on the mini-map) 874 | this.positionalBounds = {}; // A place to store positional bounds after the world has been generated. 875 | } 876 | 877 | // OLD LIGHTING CODE. 878 | // Calculates the lighting of a tile based on light sources around it. 879 | // getLightingData(x, y) { 880 | // // Loop through all the lights and calculate the lighting values. This is done by getting the average of all light applied to the tile. 881 | // let finalizedLightValue = 0; 882 | 883 | // for (let lightSource of this.globalLights) { 884 | // // Calculate the strength of the light on the tile. If the light source doesn't have a defined light strength, we use 10. 885 | // let lightStrength = 886 | // (lightSource.lightStrength ? lightSource.lightStrength : 10) / 887 | // distance(x, y, lightSource.x, lightSource.y); 888 | 889 | // finalizedLightValue += lightStrength; 890 | // } 891 | 892 | // if (finalizedLightValue > 1) finalizedLightValue = 1; 893 | // else if (finalizedLightValue < 0) finalizedLightValue = 0; 894 | 895 | // return finalizedLightValue; 896 | // } 897 | 898 | // Cast rays and calculate lighting for all globalLights. 899 | getLighting() { 900 | for (let lightSource of this.globalLights) { 901 | // Cast lighting rays. 902 | this.castLightingRays(lightSource); 903 | } 904 | } 905 | 906 | // Cast all lighting rays and determine which tiles should be lit up. 907 | castLightingRays(lightSource) { 908 | if (!lightSource.lightStrength) { 909 | // If the light source doesn not have light properties, we return. 910 | return "This object does not have a lightStrength property."; 911 | } 912 | 913 | let rayArea = lightSource.lightDistance 914 | ? lightSource.lightDistance 915 | : canvas.width / 2; // The furthest we will cast rays in any direction is half the width of the screen, unless the light source has its own distance value. 916 | 917 | for (let tile of this.globalTiles) { 918 | // Check if the tile is on screen. 919 | if (!isOnScreen(tile)) continue; 920 | 921 | // Go through every tile and cast a ray to it, if it is close enough. 922 | if ( 923 | distance(tile.x, tile.y, lightSource.x, lightSource.y) < rayArea 924 | ) { 925 | // Cast a ray. 926 | let ray = this.castRay(lightSource, tile, 4); 927 | 928 | // If the ray did not hit anything before reaching the tile, than we increase that tile's global alpha. 929 | if (!ray.hit) { 930 | // Calculate the strength of the light on the tile. If the light source doesn't have a defined light strength, we use 10. 931 | let lightStrength = 932 | (lightSource.lightStrength 933 | ? lightSource.lightStrength 934 | : 10) / 935 | distance(tile.x, tile.y, lightSource.x, lightSource.y); 936 | 937 | tile.globalAlpha += lightStrength; 938 | 939 | if (tile.globalAlpha < 0.1) tile.globalAlpha = 0.1; 940 | } 941 | 942 | // Draw the ray. (debugging only) 943 | if (debugging && showRays) { 944 | if (ray.hit) { 945 | drawRay( 946 | lightSource.x + 4, 947 | lightSource.y + 4, 948 | ray.x, 949 | ray.y, 950 | "red" 951 | ); 952 | } else { 953 | drawRay( 954 | lightSource.x + 4, 955 | lightSource.y + 4, 956 | tile.x, 957 | tile.y, 958 | "green" 959 | ); 960 | } 961 | } 962 | } 963 | } 964 | } 965 | 966 | // Cast a ray and see if it collides. 967 | castRay(source, target, step = 1) { 968 | // Create a ray object. 969 | let ray = { 970 | x: source.x + 4, 971 | y: source.y + 4, 972 | width: 1, 973 | height: 1, 974 | }; 975 | 976 | // Keep moving and checking the ray until we reach the target. 977 | while (distance(ray.x, ray.y, target.x, target.y) > step) { 978 | // Calculate the new position of the ray using a vector. We move forward the number of steps that are predefined using step. The smaller the number the more likely the ray is to hit small objects instead of moving over them. 979 | let newPos = cartesian2(angle(target, ray), step); 980 | 981 | // Move the ray. 982 | ray.x += newPos.x; 983 | ray.y += newPos.y; 984 | 985 | // Get the ray's tile position. 986 | let tilePos = worldToTile(ray.x, ray.y); 987 | tilePos.x *= 8; 988 | tilePos.y *= 8; 989 | tilePos = this.getTile(tilePos.x, tilePos.y); 990 | 991 | // Do a collision check. 992 | if (tilePos !== undefined) { 993 | if ( 994 | AABB(ray, tilePos) && 995 | tilePos.data.solid && 996 | distance(ray.x, ray.y, tilePos.x + 4, tilePos.y + 4) <= 5 997 | ) { 998 | // Check if the tile we are aiming for is solid, and we are potentially ignoring it. 999 | if ( 1000 | tilePos.data.solid && 1001 | target.data.solid && 1002 | tilePos === target 1003 | ) { 1004 | return { hit: false }; 1005 | } 1006 | 1007 | return { hit: true, x: ray.x, y: ray.y }; // Return where the ray ended. Since this is a truthy value, it is still considered a return of "true," with the added benefit of knowing where the ray landed. 1008 | } 1009 | } 1010 | } 1011 | 1012 | return { hit: false }; // If the ray doesn't collide with anything, than we return false, meaning the cast was "successful." 1013 | } 1014 | 1015 | // Get the positional bounds of the entire map. 1016 | getPositionalBounds() { 1017 | let positions = { 1018 | lowX: 0, 1019 | lowY: 0, 1020 | highX: 0, 1021 | highY: 0, 1022 | }; 1023 | 1024 | for (let room of this.rooms) { 1025 | if (room.x < positions.lowX) { 1026 | positions.lowX = room.x; 1027 | } else if (room.x > positions.highX) { 1028 | positions.highX = room.x; 1029 | } 1030 | 1031 | if (room.y < positions.lowY) { 1032 | positions.lowY = room.y; 1033 | } else if (room.y > positions.highY) { 1034 | positions.highY = room.y; 1035 | } 1036 | } 1037 | 1038 | this.positionalBounds = positions; 1039 | 1040 | return positions; 1041 | } 1042 | 1043 | // Clean up the world after it has been generated. 1044 | worldCleanup() { 1045 | // Cleanup doors. 1046 | let doors = this.globalTiles.filter( 1047 | (tile) => tile.type === "door_opened" || tile.type === "door_closed" 1048 | ); 1049 | 1050 | // Loop through all doors and clean them up. 1051 | for (let door of doors) { 1052 | // Get the first tile after a door, in the direction that door opens. 1053 | let tileInDoorDirection = undefined; 1054 | switch (door.direction) { 1055 | case "u": 1056 | tileInDoorDirection = this.getTile(door.x, door.y - 8); 1057 | break; 1058 | case "d": 1059 | tileInDoorDirection = this.getTile(door.x, door.y + 8); 1060 | break; 1061 | case "l": 1062 | tileInDoorDirection = this.getTile(door.x - 8, door.y); 1063 | break; 1064 | case "r": 1065 | tileInDoorDirection = this.getTile(door.x + 8, door.y); 1066 | break; 1067 | default: 1068 | break; 1069 | } 1070 | 1071 | // If the adjacent tile is a wall or an empty tile, we destroy the door and replace it with a wall. 1072 | if ( 1073 | tileInDoorDirection === undefined || 1074 | tileInDoorDirection.type === "wall" || 1075 | tileInDoorDirection.type === "wall_ledge" 1076 | ) { 1077 | let room = door.exits; // Get the room the door is in. 1078 | 1079 | room.destroyTile(door); // Delete the door. 1080 | 1081 | // Replace the door with a wall piece. 1082 | room.createTile( 1083 | door.x - room.x, 1084 | door.y - room.y, 1085 | door.y - room.y === 0 ? "wall_ledge" : "wall", // Spawn a wall normally, or a ledge if its at the top of the room. 1086 | false 1087 | ); 1088 | } 1089 | } 1090 | 1091 | // Remove all the doors from the spawnpoint. 1092 | spawn.tiles.forEach((tile) => { 1093 | if (tile.type === "door_closed" || tile.type === "door_open") { 1094 | spawn.destroyTile(tile); 1095 | spawn.createTile( 1096 | tile.x - spawn.x, 1097 | tile.y - spawn.y, 1098 | randomGroundTile(), 1099 | false 1100 | ); 1101 | } 1102 | }); 1103 | 1104 | // Get and store the positional bounds. 1105 | this.getPositionalBounds(); 1106 | 1107 | // Set that the game has finished loading the world to true. 1108 | this.finishedGenerating = true; 1109 | } 1110 | 1111 | // Get a room by its position. 1112 | getRoom(x, y) { 1113 | return this.rooms.filter((room) => room.x === x && room.y === y); 1114 | } 1115 | 1116 | // Create a room. 1117 | createRoom(x, y, strict = true) { 1118 | // If there are already too many rooms, we don't make this room. 1119 | if (this.roomCount <= 0) return; 1120 | 1121 | // If strict mode is on, and there is a collision with another room, we do not create the room. 1122 | let room = new Room(x, y, true, null); 1123 | 1124 | // Loop through all rooms and check for collision if we are in strict mode. 1125 | if (strict) { 1126 | for (let checkedRoom of this.rooms) { 1127 | if (AABB(room, checkedRoom)) { 1128 | return "Could not create room. It intersects another room."; 1129 | } 1130 | } 1131 | } 1132 | 1133 | this.rooms.push(room); 1134 | 1135 | this.roomCount--; 1136 | if (this.roomCount <= 0) { 1137 | this.worldCleanup(); // If all rooms have been made, we clean up the map. 1138 | } 1139 | 1140 | return room; 1141 | } 1142 | 1143 | // Create a room from template data. 1144 | createRoomFromData( 1145 | x, 1146 | y, 1147 | roomName, 1148 | strict = true, 1149 | roomsDoorsCanGenerateRooms = true 1150 | ) { 1151 | // If there are already too many rooms, we don't make this room. 1152 | if (this.roomCount <= 0) return; 1153 | 1154 | // If strict mode is on, and there is a collision with another room, we do not create the room. 1155 | let room = new Room(x, y, false, roomName, roomsDoorsCanGenerateRooms); 1156 | 1157 | // Loop through all rooms and check for collision if we are in strict mode. 1158 | if (strict) { 1159 | for (let checkedRoom of this.rooms) { 1160 | if (AABB(room, checkedRoom)) { 1161 | return "Could not create room. It intersects another room."; 1162 | } 1163 | } 1164 | } 1165 | 1166 | this.rooms.push(room); 1167 | 1168 | this.roomCount--; 1169 | if (this.roomCount <= 0) { 1170 | this.worldCleanup(); // If all rooms have been made, we clean up the map. 1171 | } 1172 | 1173 | return room; 1174 | } 1175 | 1176 | // Destroy a room. 1177 | destroyRoom(room) { 1178 | this.rooms.splice(this.rooms.indexOf(room), 1); 1179 | } 1180 | 1181 | // Get a tile. 1182 | getTile(x, y) { 1183 | for (let tile of this.globalTiles) { 1184 | if (tile.x === x && tile.y === y) { 1185 | return tile; 1186 | } 1187 | } 1188 | 1189 | return undefined; 1190 | } 1191 | 1192 | logic() { 1193 | // Update all rooms. 1194 | for (let room of this.rooms) { 1195 | room.logic(); 1196 | } 1197 | } 1198 | 1199 | render(ctx) { 1200 | // Calculate lighting. 1201 | if (renderLighting) { 1202 | this.getLighting(); 1203 | } 1204 | 1205 | // Render all rooms. 1206 | for (let room of this.rooms) { 1207 | room.render(ctx); 1208 | } 1209 | } 1210 | } 1211 | 1212 | // Gamestate. 1213 | let world = new World(); 1214 | 1215 | // Create a spawn area. 1216 | let spawn = world.createRoomFromData( 1217 | 0, 1218 | 0, 1219 | // ["a", "u", "d", "l", "r", "ul", "ur", "dl", "dr"][ 1220 | // Math.floor(Math.random() * 9) 1221 | // ] 1222 | "a" 1223 | ); 1224 | spawn.type = "ambient"; 1225 | 1226 | // world.createRoom(0, 0); 1227 | -------------------------------------------------------------------------------- /engine/data/scripts/logic/player.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | // Player. 4 | class Player { 5 | constructor(x, y) { 6 | // Load the UI spritesheet. 7 | this.ui = new Sheet("spritesheet_ui", { 8 | heart: { x: 0, y: 0 }, 9 | coin: { x: 1, y: 0 }, 10 | }); 11 | 12 | // Animation handling. 13 | this.animation = new Anim( 14 | "spritesheet_player", 15 | "stand", 16 | 0, 17 | 0, 18 | { 19 | // How many frames are in each animation, and at what frame they start. 20 | stand: { 21 | start: 0, 22 | end: 0, 23 | }, 24 | walk: { 25 | start: 1, 26 | end: 3, 27 | }, 28 | }, 29 | undefined, 30 | { 31 | overallSpeed: 4, // How fast the animation moves in coorelation to the player speed. 32 | } 33 | ); 34 | 35 | // The player's position on-screen/in-game. 36 | this.x = x; 37 | this.y = y; 38 | 39 | // The player's size for collisions. 40 | this.width = 8; 41 | this.height = 8; 42 | 43 | // Physics values. Essentially read-only since there aren't any physics in the game. 44 | this.physics = { 45 | lastX: 0, 46 | lastY: 0, 47 | velocity: 0, 48 | }; 49 | this.dir = 3; 50 | 51 | // Light. 52 | this.lightStrength = 10; 53 | this.lightDistance = 72; 54 | world.globalLights.push(this); // Add the player to the global light sources. 55 | 56 | // Max movement speed. 57 | this.speed = 1; 58 | this.speed > 8 ? (this.speed = 8) : ""; 59 | 60 | // What tile the player is mostly standing on. 61 | this.tilePos = {}; 62 | 63 | // The room the player is in. 64 | this.roomPos = {}; 65 | 66 | // How many rooms the player has cleared. 67 | this.roomsCleared = 0; 68 | 69 | // The tile the player is moving towards. 70 | this.goalTile = {}; 71 | 72 | // Camera positioning. 73 | this.camera = { 74 | x: 0, // The camera's current position. 75 | y: 0, 76 | desX: 0, // The position the camera is smoothly moving towards. 77 | desY: 0, 78 | shake: (duration, strength) => { 79 | this.camera.shakeDuration = duration; 80 | this.camera.shakeStrength = strength; 81 | this.camera.shaking = true; 82 | this.camera.shakeStoredPos = { 83 | x: this.camera.x, 84 | y: this.camera.y, 85 | }; 86 | }, 87 | _shakeFrame: () => { 88 | // Shake the screen. 89 | this.camera.x += randRange( 90 | -this.camera.shakeStrength, 91 | this.camera.shakeStrength 92 | ); 93 | this.camera.y += randRange( 94 | -this.camera.shakeStrength, 95 | this.camera.shakeStrength 96 | ); 97 | 98 | this.camera.shakeDuration--; 99 | 100 | if (this.camera.shakeDuration <= 0 || !this.camera.shaking) { 101 | this.camera.shaking = false; 102 | return; 103 | } 104 | }, 105 | }; 106 | 107 | // Player inventory. 108 | this.inventory = { 109 | health: 3, 110 | coin: 0, 111 | }; 112 | 113 | // Player attack. 114 | this.attackAnimation = new Anim( 115 | "spritesheet_player_attack", 116 | "swing", 117 | 0, 118 | 0, 119 | 120 | // How many frames are in each animation, and at what frame they start. 121 | { 122 | swing: { 123 | start: 0, 124 | end: 6, 125 | }, 126 | }, 127 | 128 | // OnFinish 129 | (thisAnimation) => { 130 | thisAnimation.attacking = false; 131 | }, 132 | 133 | // Extra props. 134 | { 135 | hitbox: { 136 | x: 0, 137 | y: 0, 138 | width: 0, 139 | height: 0, 140 | }, 141 | attacking: false, 142 | } 143 | ); 144 | this.hitStrength = 1 * 0.25; // How much damage the player does. 145 | this.knockback = 4; 146 | } 147 | 148 | // Attack handling. 149 | attack() { 150 | if (!this.attackAnimation.attacking) { 151 | this.attackAnimation.attacking = true; 152 | } 153 | } 154 | 155 | animateAttacking() { 156 | // Only animate the attack animation if we are attacking. 157 | if (this.attackAnimation.attacking) { 158 | this.attackAnimation.animate(); 159 | } 160 | } 161 | 162 | // Handle animations 163 | animate() { 164 | // Match the animation speed to the player's movement speed. 165 | this.animation.speed = 166 | (this.physics.velocity.velocity / this.speed) * 167 | this.animation.overallSpeed; 168 | 169 | // Determine which animation should be playing. 170 | if (this.physics.velocity.velocity < 0.01) { 171 | this.animation.name = "stand"; 172 | } else { 173 | this.animation.name = "walk"; 174 | } 175 | 176 | // Trigger the animation. 177 | this.animation.animate(); 178 | 179 | // Animate attacking. 180 | this.animateAttacking(); 181 | } 182 | 183 | // Check for a collision in a direction. 184 | checkCol(dir) { 185 | // The hypothetical bounding box, or where the player would be if the movement was applied. 186 | let hbb = { 187 | x: this.x, 188 | y: this.y, 189 | width: 8, 190 | height: 8, 191 | }; 192 | 193 | // The tile position in the world the player would be in if we moved. 194 | let hbbTilePos = worldToTile(hbb.x, hbb.y); 195 | 196 | // Apply hypothetical movement to the hypothetical position and grab the tile that is in front of the player in that direction. 197 | switch (dir) { 198 | case 0: 199 | hbb.y -= this.speed; 200 | this.goalTile = { x: hbbTilePos.x, y: hbbTilePos.y - 1 }; 201 | break; 202 | case 1: 203 | hbb.x += this.speed; 204 | this.goalTile = { x: hbbTilePos.x + 1, y: hbbTilePos.y }; 205 | break; 206 | case 2: 207 | hbb.y += this.speed; 208 | this.goalTile = { x: hbbTilePos.x, y: hbbTilePos.y + 1 }; 209 | break; 210 | case 3: 211 | hbb.x -= this.speed; 212 | this.goalTile = { x: hbbTilePos.x - 1, y: hbbTilePos.y }; 213 | break; 214 | default: 215 | break; 216 | } 217 | 218 | // CHECK COLLISIONS AGAINST TILES. 219 | // Get the the actual tile from the global tile array. 220 | this.goalTile = world.getTile( 221 | Math.round(this.goalTile.x * 8), 222 | Math.round(this.goalTile.y * 8) 223 | ); 224 | 225 | // Check for collisions and return the outcome. 226 | if ( 227 | this.goalTile !== undefined && 228 | AABB(hbb, this.goalTile) && 229 | this.goalTile.data.solid 230 | ) { 231 | return true; 232 | } 233 | 234 | for (let tile of world.globalTiles) { 235 | if (distance(tile.x, tile.y, hbb.x, hbb.y) <= 16) { 236 | if (AABB(hbb, tile) && tile.data.solid) { 237 | return true; 238 | } 239 | } else { 240 | continue; 241 | } 242 | } 243 | 244 | // CHECK COLLISIONS AGAINST ENTITIES. 245 | for (let entity of entities.entities) { 246 | // If the entity is too far away, we don't even bother checking. 247 | if ( 248 | distance(entity.x, entity.y, hbb.x, hbb.y) > 249 | hbb.width + hbb.height + entity.width + entity.height + 2 250 | ) { 251 | continue; 252 | } 253 | 254 | // Check collisions. 255 | if (AABB(entity, hbb) && entity.solid) { 256 | return true; 257 | } 258 | } 259 | 260 | return false; 261 | } 262 | 263 | // Render a minimap. 264 | renderMiniMap(ctx) { 265 | // Where the top-left corner of the minimap is on the screen. 266 | let minimap = { 267 | x: canvas.width - 16 - player.roomPos.x / 88, 268 | y: canvas.height - 16 - player.roomPos.y / 88, 269 | }; 270 | 271 | // Draw all the rooms. 272 | for (let room of world.rooms) { 273 | ctx.save(); 274 | ctx.beginPath(); 275 | 276 | ctx.globalAlpha = 277 | 150 / distance(player.x, player.y, room.x, room.y); 278 | ctx.fillStyle = room.cleared ? "limegreen" : "maroon"; 279 | 280 | ctx.fillRect( 281 | Math.round(minimap.x + room.x / 88), 282 | Math.round(minimap.y + room.y / 88), 283 | 1, 284 | 1 285 | ); 286 | 287 | ctx.closePath(); 288 | ctx.restore(); 289 | } 290 | 291 | // Draw what room the player is in. 292 | ctx.beginPath(); 293 | ctx.fillStyle = "white"; 294 | ctx.fillRect( 295 | Math.round(minimap.x + player.roomPos.x / 88), 296 | Math.round(minimap.y + player.roomPos.y / 88), 297 | 1, 298 | 1 299 | ); 300 | ctx.closePath(); 301 | 302 | // Draw the number of rooms left. 303 | renderNumber( 304 | canvas.width - 305 | JSON.stringify(world.rooms.length - player.roomsCleared) 306 | .length * 307 | 9, 308 | canvas.height - 9, 309 | world.rooms.length - player.roomsCleared 310 | ); 311 | } 312 | 313 | // Render the player's ui. 314 | renderUI(ctx) { 315 | // Health. 316 | 317 | // Draw each heart. 318 | if (this.inventory.health > 0) { 319 | for (let i = 0; i < this.inventory.health; i++) { 320 | try { 321 | ctx.beginPath(); 322 | 323 | ctx.drawImage( 324 | this.ui.map, // The tilemap image. 325 | this.ui.locs.heart.x * 8, // The position of the sub-image in the map. 326 | this.ui.locs.heart.y * 8, 327 | 8, // The 8x8 pixel dimensions of that sub-image. 328 | 8, 329 | Math.round(1 + i * 8 + i), // Proper placement of the tile on screen. 330 | 1, 331 | 8, // The size of the tile, as drawn on screen. 332 | 8 333 | ); 334 | 335 | ctx.closePath(); 336 | } catch {} 337 | } 338 | } 339 | 340 | // Coins. 341 | try { 342 | ctx.beginPath(); 343 | 344 | ctx.drawImage( 345 | this.ui.map, // The tilemap image. 346 | this.ui.locs.coin.x * 8, // The position of the sub-image in the map. 347 | this.ui.locs.coin.y * 8, 348 | 8, // The 8x8 pixel dimensions of that sub-image. 349 | 8, 350 | 1, // Proper placement of the tile on screen. 351 | Math.round(canvas.height - 9), 352 | 8, // The size of the tile, as drawn on screen. 353 | 8 354 | ); 355 | 356 | ctx.closePath(); 357 | 358 | renderNumber( 359 | 9, 360 | canvas.height - 9, 361 | this.inventory.coin > 999 ? 999 : this.inventory.coin 362 | ); 363 | } catch {} 364 | } 365 | 366 | input() { 367 | // Store the player's current positon; 368 | this.physics.lastX = this.x; 369 | this.physics.lastY = this.y; 370 | 371 | // Sprinting. 372 | // if (keyboard.Shift) { 373 | // this.speed = 2; 374 | // } else { 375 | // this.speed = 1; 376 | // } 377 | 378 | // Check for collisions in the direction of travel and then apply the travel if there are none. 379 | if (keyboard.ArrowUp || keyboard.w) { 380 | if (!this.checkCol(0)) { 381 | this.y -= this.speed; 382 | } 383 | this.dir = 0; 384 | } 385 | if (keyboard.ArrowRight || keyboard.d) { 386 | if (!this.checkCol(1)) { 387 | this.x += this.speed; 388 | this.animation.direction = 1; // Set the player's animation direction. 389 | } 390 | this.dir = 1; 391 | } 392 | if (keyboard.ArrowDown || keyboard.s) { 393 | if (!this.checkCol(2)) { 394 | this.y += this.speed; 395 | } 396 | this.dir = 2; 397 | } 398 | if (keyboard.ArrowLeft || keyboard.a) { 399 | if (!this.checkCol(3)) { 400 | this.x -= this.speed; 401 | this.animation.direction = -1; // Set the player's animation direction. 402 | } 403 | this.dir = 3; 404 | } 405 | 406 | // Attack 407 | if (keyboard.j || keyboard.z) { 408 | this.attack(); 409 | } 410 | 411 | // Calculate the player's velocity. 412 | this.physics.velocity = vector2( 413 | Math.abs(this.x - this.physics.lastX), 414 | Math.abs(this.y - this.physics.lastY) 415 | ); 416 | } 417 | 418 | logic() { 419 | // Inventory data. 420 | this.inventory.health % 1 !== 0 && 421 | (this.inventory.health = Math.floor(this.inventory.health)); 422 | 423 | // Positional data. 424 | this.x = Math.round(this.x); 425 | this.y = Math.round(this.y); 426 | 427 | this.tilePos = worldToTile(this.x, this.y); 428 | this.roomPos = worldToRoom(this.x, this.y); 429 | 430 | // Set where the camera should be. 431 | this.camera.desX = Math.round(this.x + 4 - canvas.width / 2); 432 | this.camera.desY = Math.round(this.y + 4 - canvas.height / 2); 433 | 434 | let smoothspeed = 6; 435 | 436 | // Smooth camera movement. 437 | let newCamPos = cartesian2( 438 | angle({ x: this.camera.desX, y: this.camera.desY }, this.camera), 439 | distance( 440 | this.camera.x, 441 | this.camera.y, 442 | this.camera.desX, 443 | this.camera.desY 444 | ) / smoothspeed 445 | ); 446 | 447 | this.camera.x += newCamPos.x; 448 | this.camera.y += newCamPos.y; 449 | 450 | this.camera.x = Math.round(this.camera.x); 451 | this.camera.y = Math.round(this.camera.y); 452 | 453 | // Animate the camera shaking. 454 | if (this.camera.shaking === true) { 455 | this.camera._shakeFrame(); 456 | } 457 | 458 | // Animate 459 | this.animate(); 460 | } 461 | 462 | render(ctx) { 463 | // Check that the player is on-screen. 464 | if (!isOnScreen(this)) return; 465 | 466 | try { 467 | ctx.beginPath(); 468 | 469 | ctx.drawImage( 470 | this.animation.map, // The tilemap image. 471 | this.animation.frame * 8 + 472 | (this.animation.direction === 1 && 32), // The x and y sub-coordinates to grab the animation's texture from the image. 473 | 0, 474 | 8, // The 8x8 pixel dimensions of that sub-image. 475 | 8, 476 | this.x - this.camera.x, // Proper placement of the animation on screen. 477 | this.y - this.camera.y, 478 | 8, // The size of the animation, as drawn on screen. 479 | 8 480 | ); 481 | 482 | ctx.closePath(); 483 | 484 | // Render attack 485 | if (this.attackAnimation.attacking) { 486 | let pos = { 487 | x: 0, 488 | y: 0, 489 | }; 490 | switch (this.dir) { 491 | case 0: 492 | pos.y -= 7; 493 | this.attackAnimation.hitbox = { 494 | x: this.x + pos.x + 1, 495 | y: this.y + pos.y + 3, 496 | width: 7, 497 | height: 3, 498 | }; 499 | break; 500 | case 1: 501 | pos.x += 8; 502 | pos.y++; 503 | this.attackAnimation.hitbox = { 504 | x: this.x + pos.x + 1, 505 | y: this.y + pos.y + 1, 506 | width: 3, 507 | height: 7, 508 | }; 509 | break; 510 | case 2: 511 | pos.y += 8; 512 | this.attackAnimation.hitbox = { 513 | x: this.x + pos.x + 1, 514 | y: this.y + pos.y + 1, 515 | width: 7, 516 | height: 3, 517 | }; 518 | break; 519 | case 3: 520 | pos.x -= 8; 521 | pos.y++; 522 | this.attackAnimation.hitbox = { 523 | x: this.x + pos.x + 4, 524 | y: this.y + pos.y + 1, 525 | width: 3, 526 | height: 7, 527 | }; 528 | break; 529 | default: 530 | break; 531 | } 532 | 533 | ctx.beginPath(); 534 | 535 | ctx.drawImage( 536 | this.attackAnimation.map, // The tilemap image. 537 | this.attackAnimation.frame * 8 + this.dir * 56, // The x and y sub-coordinates to grab the animation's texture from the image. 538 | 0, 539 | 8, // The 8x8 pixel dimensions of that sub-image. 540 | 8, 541 | this.x + pos.x - this.camera.x, // Proper placement of the animation on screen. 542 | this.y + pos.y - this.camera.y, 543 | 8, // The size of the animation, as drawn on screen. 544 | 8 545 | ); 546 | 547 | ctx.closePath(); 548 | } 549 | } catch { 550 | return; 551 | } 552 | } 553 | } 554 | 555 | const player = new Player(8 * 5, 8 * 5); 556 | -------------------------------------------------------------------------------- /engine/data/scripts/render.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | // Canvas setup. 4 | const canvas = document.querySelector("canvas"); 5 | const ctx = canvas.getContext("2d"); 6 | let pixelRatio; 7 | 8 | // Resize canvas. 9 | function resizeCanvas() { 10 | pixelRatio = window.devicePixelRatio / ctx.backingStorePixelRatio; 11 | 12 | let screenSize = (window.innerWidth + window.innerHeight) / 2; 13 | while ( 14 | screenSize > window.innerWidth - 50 || 15 | screenSize > window.innerHeight - 50 16 | ) { 17 | screenSize--; 18 | } 19 | 20 | canvas.style.width = `${screenSize}px`; 21 | canvas.style.height = `${screenSize}px`; 22 | 23 | canvas.width = 128; 24 | canvas.height = 128; 25 | } 26 | resizeCanvas(); 27 | window.onresize = () => resizeCanvas(); 28 | 29 | // Clear canvas. 30 | function clearCanvas() { 31 | ctx.beginPath(); 32 | 33 | ctx.clearRect(0, 0, canvas.width, canvas.height); 34 | 35 | ctx.closePath(); 36 | } 37 | 38 | // Render text. 39 | const nums = new Sheet("spritesheet_nums", { 40 | 0: { x: 0, y: 0 }, 41 | 1: { x: 1, y: 0 }, 42 | 2: { x: 2, y: 0 }, 43 | 3: { x: 3, y: 0 }, 44 | 4: { x: 4, y: 0 }, 45 | 5: { x: 5, y: 0 }, 46 | 6: { x: 6, y: 0 }, 47 | 7: { x: 7, y: 0 }, 48 | 8: { x: 8, y: 0 }, 49 | 9: { x: 9, y: 0 }, 50 | }); 51 | 52 | // Render a string of numbers on the screen using pixel art. 53 | function renderNumber(x, y, number) { 54 | // Check that the number sheet has loaded. 55 | if (!nums.loaded) return; 56 | 57 | x = Math.round(x); 58 | y = Math.round(y); 59 | 60 | let numArray = number.toString().split(""); 61 | 62 | for (let i = 0; i < numArray.length; i++) { 63 | ctx.beginPath(); 64 | 65 | ctx.drawImage( 66 | nums.map, // The tilemap image. 67 | nums.locs[numArray[i]].x * 8, // The position of the sub-image in the map. 68 | nums.locs[numArray[i]].y * 8, 69 | 8, // The 8x8 pixel dimensions of that sub-image. 70 | 8, 71 | Math.round(x + i * 8 + i), // Proper placement of the tile on screen. 72 | Math.round(y), 73 | 8, // The size of the tile, as drawn on screen. 74 | 8 75 | ); 76 | 77 | ctx.closePath(); 78 | } 79 | } 80 | 81 | // Render a ray. 82 | function drawRay(x1, y1, x2, y2, color) { 83 | ctx.beginPath(); 84 | 85 | ctx.strokeStyle = color; 86 | ctx.lineWidth = 1; 87 | 88 | ctx.moveTo(x1 - player.camera.x, y1 - player.camera.y); 89 | ctx.lineTo(x2 - player.camera.x, y2 - player.camera.y); 90 | 91 | ctx.stroke(); 92 | ctx.closePath(); 93 | } 94 | 95 | // Render loop. 96 | 97 | function renderLoop() { 98 | // If the world hasn't finished generating, we don't render. 99 | if (!world.finishedGenerating) { 100 | clearCanvas(); 101 | ctx.beginPath(); 102 | 103 | ctx.font = "bold 12px consolas"; 104 | ctx.textAlign = "center"; 105 | ctx.textBaseline = "middle"; 106 | ctx.fillStyle = "white"; 107 | ctx.fillText( 108 | "LOADING", 109 | Math.round(canvas.width / 2), 110 | Math.round(canvas.height / 2) 111 | ); 112 | 113 | ctx.closePath(); 114 | } else { 115 | // INPUT. 116 | player.input(); 117 | 118 | // LOGIC. (only runs if the game isn't paused) 119 | if (!paused) { 120 | world.logic(); // Update the world. 121 | entities.logic(); // Update the entities. 122 | items.logic(); // Update the items. 123 | player.logic(); // Update the player. 124 | } 125 | 126 | // RENDER. 127 | clearCanvas(); 128 | world.render(ctx); // Render the world, all of its rooms and tiles. 129 | items.render(ctx); // Render all the items in the game. 130 | entities.render(ctx); // Render all the entities in the game. 131 | player.render(ctx); // Render the player. 132 | world.getRoom(player.roomPos.x, player.roomPos.y)[0].renderEnemies(ctx); // Render nearby enemies. 133 | 134 | // If the settings allow the rendering of UI. 135 | if (renderUI) { 136 | player.renderUI(ctx); // Render the player's UI. 137 | 138 | // Only render the mini-map if the UI is being loaded and the settings allow it. 139 | if (renderMiniMap) { 140 | player.renderMiniMap(ctx); 141 | } 142 | } 143 | } 144 | 145 | window.requestAnimationFrame(renderLoop); 146 | } 147 | 148 | window.requestAnimationFrame(renderLoop); 149 | -------------------------------------------------------------------------------- /engine/data/scripts/settings.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | // Graphics Settings 4 | 5 | // UI 6 | let renderUI = true; // Render ALL UI 7 | let renderMiniMap = true; // Render the minimap if the UI is being rendered. 8 | 9 | // EFFECTS 10 | let renderLighting = true; 11 | 12 | // Engine Settings 13 | let paused = false; // Whether or not logic functions can run. 14 | let debugging = false; // Show debugging information. 15 | let showRays = true; // Show each raycast when we are debugging. 16 | -------------------------------------------------------------------------------- /engine/data/styles/style.css: -------------------------------------------------------------------------------- 1 | *, 2 | *::before, 3 | *::after { 4 | padding: 0; 5 | margin: 0; 6 | box-sizing: border-box; 7 | } 8 | 9 | body, 10 | html { 11 | overflow: hidden; 12 | width: 100vw; 13 | height: 100vh; 14 | } 15 | 16 | body { 17 | background: linear-gradient(rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0.5)), 18 | url("../images/bg.png"); 19 | background-size: 64px 64px; 20 | } 21 | 22 | canvas { 23 | background: black; 24 | border-radius: calc((0.5vw + 0.5vh) / 2); 25 | border: calc((0.5vw + 0.5vh) / 2) solid #7c1934; 26 | 27 | position: absolute; 28 | top: 50%; 29 | left: 50%; 30 | transform: translate(-50%, -50%); 31 | 32 | transition-property: width height; 33 | transition-duration: 0.25s; 34 | transition-timing-function: ease-in-out; 35 | 36 | image-rendering: -moz-crisp-edges; 37 | image-rendering: -webkit-crisp-edges; 38 | image-rendering: pixelated; 39 | image-rendering: crisp-edges; 40 | 41 | width: 50vw; 42 | height: 50vw; 43 | } 44 | -------------------------------------------------------------------------------- /engine/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | teenydumdgeon 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /repo/v0.3.3_scr_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NotTimTam/dendgeon/0313974c9671ea74d4dc91c18d51b4e3ef3a0a51/repo/v0.3.3_scr_1.png --------------------------------------------------------------------------------