├── LICENSE ├── README.md ├── game.js ├── gameEngine.js ├── gameEngineDebug.js ├── index.html ├── screenshot.png └── tiles.png /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | 294 | Copyright (C) 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | , 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## When life gets you down, it's never too late to... 2 | # B O U N C E B A C K 3 | ### A Boomerang Roguelite Game for JS13k by Frank Force 4 | 5 | # [PLAY BOUNCE BACK!](https://js13kgames.com/entries/bounce-back) 6 | 7 | ## [Game Design Postmortem](http://frankforce.com/?p=6936) 8 | 9 | ![Bounceback Image](/screenshot.png) 10 | 11 | ### Controls 12 | * WASD = Move 13 | * Mouse = Aim 14 | * Click = Throw 15 | * Space = Dash 16 | 17 | ### Hints 18 | * Boosting protects you from damage. 19 | * Buy items to help you survive. 20 | * You don't lose money when you die! 21 | * There are only 10 levels. 22 | * Lost boomerangs show up big on the map. 23 | * Enemies are slowed by sand. 24 | * Yellow boormang can grab pickups. 25 | * Blue boomerang does double damage. 26 | * Win to unlock speed run mode. 27 | 28 | ## Game Features 29 | * Boomerang physics & boost ability 30 | * Procedural level generation 31 | * 3 Enemy types 32 | * 7 types of pickups 33 | * Giant and invisible enemy variants 34 | * Final boss battle 35 | * Saves gems earned and max level reached 36 | * Shop system for buying items 37 | * Minimap 38 | * Footsteps, blood, and persistant effects system 39 | * 16 Different sound effects with zzfx 40 | * Procedurally generated music 41 | * Speed run mode doesn't effect normal save 42 | * Low health warning system 43 | * Level transition effect 44 | 45 | ### Engine Features 46 | * Engine is separated from game code 47 | * Object oriented architecture 48 | * 2D game object system with pseudo 3D 49 | * Physics and level tile collision 50 | * Tile rendering system 51 | * Cached level rendering 52 | * Particle system 53 | * 3D shadows 54 | * Input processing system 55 | * Debug rendering system 56 | 57 | ### Engine Debug Features 58 | * Debug console 59 | * Debug rendering 60 | * Debug controls 61 | * Save snapshot 62 | 63 | ### Minification Notes 64 | * The official release is under 13k for the game, engine, art and music! 65 | * The tile.png file has 14 color palette exported from Gimp with all extra save data disabled 66 | * First combine all javascript together 67 | * Remove all debug code, godMode, soundEnable, and controls description 68 | * Use Google Closure on Advanced https://closure-compiler.appspot.com/home 69 | * Use terser with extra compression turned off https://xem.github.io/terser-online/ 70 | * Put eveything into the same html file and remove any whitespace 71 | * Zip the index.html and tiles.png files 72 | * Zip the zip with advzip using the settings "-z -4 -i 1000" https://github.com/amadvance/advancecomp 73 | * Say a small prayer to the gods of JavaScript 74 | * The final result should hopefully be under 13k! 75 | -------------------------------------------------------------------------------- /game.js: -------------------------------------------------------------------------------- 1 | /* 2 | Bounce Back ~ A boomerang roguelike for JS13k 3 | Copyright (C) 2019 Frank Force 4 | 5 | This program is free software; you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation; either version 2 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | */ 15 | /* 16 | Bounce Back Javascript Game 17 | By Frank Force 2019 18 | 19 | Game Features 20 | - Boomerang physics & dash ability 21 | - Procedural level generation 22 | - 3 Enemy types 23 | - 7 types of pickups 24 | - Giant and invisible enemy variants 25 | - Final boss battle 26 | - Saves gems earned and max level reached 27 | - Shop system for buying items 28 | - Minimap 29 | - Footsteps, blood, and persistant effects system 30 | - 16 Different sound effects with zzfx 31 | - Procedurally generated music 32 | - Speed run mode doesn't effect normal save 33 | - Low health warning system 34 | - Level transition effect 35 | */ 36 | 37 | "use strict"; // strict mode 38 | /////////////////////////////////////////////////////////////////////////////// 39 | // debug config 40 | 41 | //godMode=1; 42 | //debug=1; 43 | //debugCanvas=1; 44 | //debugCollision=1; 45 | //soundEnable=0; 46 | 47 | /////////////////////////////////////////////////////////////////////////////// 48 | // init 49 | 50 | let startLevel=0; 51 | let level; 52 | let levelNumber; 53 | let nextLevel; 54 | let warpLevel=0; 55 | let isFinalLevel; 56 | let isStartLevel; 57 | let levelTimer = new Timer(); 58 | let endLevelTimer = new Timer(); 59 | let levelExit; 60 | let loadNextLevel; 61 | let levelMaze = []; 62 | let levelMazeSize = 4; 63 | let levelColor = new Color(); 64 | let levelFrame; 65 | 66 | let boss; 67 | let player; 68 | let playerData; 69 | let playerStartPos; 70 | let winTimer = new Timer(); 71 | let healthWarning = new Timer(); 72 | let buyTimer = new Timer(); 73 | let mainCanvas = c1; 74 | let speedRunMode; 75 | let speedRunTime=0; 76 | let speedRunBestTime=0; 77 | let coinSoundTimer = new Timer(); 78 | 79 | class PlayerData 80 | { 81 | // track player data between levels (when player is destroyed) 82 | constructor() 83 | { 84 | this.health = 3; 85 | this.healthMax = 3; 86 | this.boomerangs = 1; 87 | this.bigBoomerangs = 0; 88 | this.coins = 0; 89 | } 90 | } 91 | 92 | function Init() 93 | { 94 | EngineInit(); 95 | 96 | // clear canvas to black so transition starts on a black screen 97 | mainCanvasContext.fillRect(0,0,mainCanvasSize.x, mainCanvasSize.y); 98 | 99 | Reset(); 100 | NextLevel(); 101 | EngineUpdate(); 102 | } 103 | 104 | function Reset() 105 | { 106 | // load local storage 107 | playerData = new PlayerData(); 108 | if (localStorage.kbap_coins) 109 | playerData.coins = parseInt(localStorage.kbap_coins, 10); 110 | if (localStorage.kbap_warp) 111 | warpLevel = parseInt(localStorage.kbap_warp, 10); 112 | if (localStorage.kbap_bestTime) 113 | speedRunBestTime = parseInt(localStorage.kbap_bestTime, 10); 114 | nextLevel = startLevel; 115 | } 116 | 117 | function NextLevel() 118 | { 119 | // go to next level 120 | levelFrame = 0; 121 | levelNumber = nextLevel; 122 | nextLevel = (nextLevel+1)%11; 123 | 124 | // track highest level reached 125 | if (!speedRunMode && levelNumber>warpLevel) 126 | warpLevel = levelNumber; 127 | localStorage.kbap_warp = warpLevel; 128 | 129 | // check if is special level 130 | isFinalLevel = levelNumber==10; 131 | isStartLevel = !levelNumber; 132 | if (isStartLevel) 133 | speedRunMode = 0; // reset speed run on start level 134 | 135 | InitLevel(); 136 | } 137 | 138 | function InitLevel() 139 | { 140 | // reset level stuff 141 | levelTimer.Set(); 142 | winTimer.UnSet(); 143 | endLevelTimer.UnSet(); 144 | cameraScale=2; 145 | levelExit = 0; 146 | boss = 0; 147 | 148 | // clear everything 149 | StartTransiton(); 150 | ClearGameObjects(); 151 | 152 | // prevent player being stuck with no boomerangs 153 | if (!playerData.boomerangs && !playerData.bigBoomerangs) 154 | playerData.boomerangs=1; 155 | 156 | // create the level and player 157 | GenerateLevel(); 158 | player = new Player(playerStartPos); 159 | 160 | // spawn debug objects 161 | //new Pickup(new Vector2(13,10),0); 162 | //new SlimeEnemy(new Vector2(8,10),3); 163 | //new JumpingEnemy(new Vector2(10,10)); 164 | //new ShieldEnemy(new Vector2(12,10)); 165 | //SpawnPickups(playerStartPos.Clone().AddXY(3,-6),1,100); 166 | } 167 | 168 | function SpawnPickups(pos, chance=1, count=1) 169 | { 170 | // random chance to not drop 171 | if (Rand()>chance) 172 | return; 173 | 174 | for(let i=0;i1) 184 | p.velocity = RandVector(.1 + Clamp(i,3,30)*.03*Rand()); 185 | } 186 | } 187 | 188 | // collide with level object if one exists and return if bounced 189 | function DestroyLevelObject(pos,bounceRock=1) 190 | { 191 | // is something solid there? 192 | let data = level.GetDataFromPos(pos); 193 | if (!data.IsSolid()) 194 | return 0; 195 | 196 | // did it hit an object? 197 | let bounce = 1; 198 | let type = data.object; 199 | if (type==1 || type==2) 200 | { 201 | // clear out the tile 202 | level.GetDataFromPos(pos).object = 0; 203 | level.DrawTileData(pos.x,pos.y); 204 | 205 | // small chance of dropping a pickup 206 | SpawnPickups(pos, .05); 207 | 208 | if (type==1) 209 | { 210 | // bush 211 | PlaySound(5); 212 | level.DrawEllipse(pos,RandBetween(.1,.15),RGBA(0,0,0,RandBetween(.3,.6))); 213 | bounce=0; 214 | } 215 | else 216 | { 217 | // rock 218 | PlaySound(14); 219 | for(let i=9;i--;) 220 | level.DrawEllipse(pos.Clone().Add(RandVector(.2)),RandBetween(.1,.2),RGBA(.2,.1,.05,RandBetween(.3,.6))); 221 | bounce = bounceRock; 222 | } 223 | 224 | // particle effects 225 | new ParticleEmitter 226 | ( 227 | pos, .5, .1, // pos, emitter size, particle size 228 | type==1 ? new Color(.4,.8,.1,1) : new Color(.4,.2,.1,1), 229 | type==1 ? new Color(0,.1,0,1) : new Color(0,0,0,1) 230 | ); 231 | } 232 | 233 | return bounce; 234 | } 235 | 236 | /////////////////////////////////////////////////////////////////////////////// 237 | // update/render 238 | 239 | function Update() 240 | { 241 | ++levelFrame; 242 | UpdateAudio(); 243 | 244 | // save data 245 | if (!speedRunMode) 246 | localStorage.kbap_coins = playerData.coins; 247 | 248 | // update speed run time 249 | if (!paused && !winTimer.IsSet() && !player.IsDead()) 250 | speedRunTime += timeDelta; 251 | 252 | // restart if dead or won 253 | if ((player.IsDead() || winTimer.IsSet()) && KeyWasPressed(27)) 254 | loadNextLevel = 2; 255 | 256 | // load next level when ready 257 | if (endLevelTimer.IsSet() && endLevelTimer.Elapsed()) 258 | loadNextLevel = 1; 259 | 260 | // debug key N to load next level 261 | if (debug && KeyWasPressed(78)) 262 | loadNextLevel = 1; 263 | 264 | // zoom out on final level 265 | if (isFinalLevel) 266 | cameraScale = Max(cameraScale-.001,1); 267 | 268 | if (isStartLevel) 269 | { 270 | // title screen 271 | let pos = new Vector2(8,3.7); 272 | let t = levelTimer.Get(); 273 | let p = Percent(t,0,3.3) 274 | let b = Math.abs(3-4*p)/1.7; 275 | let c1 =`hsla(${t*99},99%,50%)`; 276 | level.DrawText('BOUNCE', pos.Clone().AddXY(0,-b), 33*p,'center',2,'#000',c1); 277 | level.DrawText('BACK', pos.Clone().AddXY(0,b+1.2), 33*p,'center',2,'#000',c1); 278 | if (levelFrame==200) 279 | level.DrawText('A JS13k 2019 Game', new Vector2(8,9.5), 14); 280 | if (levelFrame==260) 281 | level.DrawText('By Frank Force', new Vector2(8,10.5), 14); 282 | } 283 | } 284 | 285 | function PreRender() 286 | { 287 | // camera is always centered on player 288 | cameraPos.Copy(player.pos); 289 | 290 | // clear canvas to level color 291 | mainCanvasContext.fillStyle=levelColor.RGBA(); 292 | mainCanvasContext.fillRect(0,0,mainCanvasSize.x, mainCanvasSize.y); 293 | 294 | if (isStartLevel) 295 | { 296 | // background starfield in start level 297 | mainCanvasContext.fillStyle='#999'; 298 | for(let i=2e3;i--;) 299 | mainCanvasContext.fillRect((Math.sin(i)*1e9-time*(i+1e3)/50)%(mainCanvas.width+9)-9,i*9%mainCanvas.height,i%7,i%7); 300 | } 301 | 302 | // draw the level (bottom layer) 303 | level.Render(); 304 | } 305 | 306 | function PostRender() 307 | { 308 | if (loadNextLevel) 309 | { 310 | // hook to load next level is here so transition effects work! 311 | if (loadNextLevel==2) 312 | Reset(); 313 | loadNextLevel = 0; 314 | NextLevel(); 315 | } 316 | 317 | UpdateTransiton(); 318 | 319 | // centered hud text 320 | let bigText = ''; 321 | if (paused) 322 | bigText = '-paused-' 323 | if (winTimer.IsSet()) 324 | bigText = 'You Win!'; 325 | if (player.IsDead()) 326 | { 327 | bigText = 'Game Over!' 328 | DrawText('Press Escape',mainCanvasSize.x/2, mainCanvasSize.y/2+80, 42); 329 | } 330 | DrawText(bigText,mainCanvasSize.x/2, mainCanvasSize.y/2-80, 72, 'center', 2); 331 | 332 | { 333 | // hud 334 | let iconSize = 16; 335 | let y = iconSize; 336 | 337 | for(let i=0;i i) 344 | t = player.health-i>=1?3:2; 345 | DrawScreenTile(iconSize+2*iconSize*i,y,s,t,5); 346 | } 347 | 348 | y += 2*iconSize; 349 | //if (playerData.boomerangs) 350 | { 351 | DrawScreenTile(iconSize,y,iconSize,0,5); 352 | DrawText(playerData.boomerangs, 34, y+2, 32, 'left'); 353 | } 354 | if (playerData.bigBoomerangs) 355 | { 356 | DrawScreenTile(iconSize+60,y,iconSize,7,5); 357 | DrawText(playerData.bigBoomerangs, 34+60, y+2, 32, 'left'); 358 | } 359 | //if (playerData.coins) 360 | { 361 | y += 2*iconSize; 362 | DrawScreenTile(iconSize,5*iconSize,iconSize,5,5); 363 | DrawText(playerData.coins, 34, y+2, 32, 'left'); 364 | } 365 | 366 | if (speedRunMode) 367 | { 368 | // show time if speed run mode is activated 369 | let c = (player.IsDead() || winTimer.IsSet())? '#F00' : '#000'; 370 | DrawText(FormatTime(speedRunTime), mainCanvas.width/2, 28, 40, 'center',1,c); 371 | } 372 | 373 | RenderMap(); 374 | } 375 | 376 | // mouse cursor 377 | mainCanvas.style.cursor='none'; 378 | let mx = mousePos.x|0; 379 | let my = mousePos.y|0; 380 | let mw = 2; 381 | let mh = 15; 382 | mainCanvasContext.globalCompositeOperation = 'difference'; 383 | mainCanvasContext.fillStyle='#FFF' 384 | mainCanvasContext.fillRect(mx-mw,my-mh,mw*2,mh*2); 385 | mainCanvasContext.fillRect(mx-mh,my-mw,mh*2,mw*2); 386 | mainCanvasContext.globalCompositeOperation = 'source-over'; 387 | } 388 | 389 | function MazeDataPos(pos) 390 | { 391 | // get the index into the maze array 392 | let cellRatio = levelMazeSize / levelSize; 393 | pos = pos.Clone(cellRatio); 394 | return (pos.x|0) + (pos.y|0) * levelMazeSize; 395 | } 396 | 397 | function RenderMap() 398 | { 399 | if ((isStartLevel || isFinalLevel) && !debug) 400 | return; 401 | 402 | let iconSize = 16; 403 | let y = iconSize; 404 | let w = 24; 405 | let o = mainCanvasSize.x-levelMazeSize*w-10; 406 | 407 | // show level number 408 | y += 2*iconSize; 409 | if (!isStartLevel) 410 | DrawText('L-'+ levelNumber, o-10, 24, 32,'right'); 411 | 412 | // mark room player is in as visited 413 | if (levelMaze[MazeDataPos(player.pos)]) 414 | levelMaze[MazeDataPos(player.pos)] = -1; 415 | 416 | let cellWidth = levelSize / levelMazeSize; 417 | let playerMazeX = player.pos.x / cellWidth | 0; 418 | let playerMazeY = player.pos.y / cellWidth | 0; 419 | 420 | // render minimap 421 | mainCanvasContext.strokeStyle='#000'; 422 | mainCanvasContext.lineWidth=2; 423 | mainCanvasContext.strokeRect(o,10,w*levelMazeSize,w*levelMazeSize); 424 | for(let y=levelMazeSize;y--;) 425 | for(let x=levelMazeSize;x--;) 426 | { 427 | let m = levelMaze[x+y*levelMazeSize]; 428 | 429 | let c = '#0004'; // unexplored invalid 430 | if (m>0) 431 | c = '#333'; // unexplored valid 432 | if (m==-1) 433 | c = '#FFF'; // explored 434 | if (x == playerMazeX && y == playerMazeY) 435 | c = '#F00'; // player location 436 | 437 | mainCanvasContext.fillStyle=c; 438 | mainCanvasContext.fillRect(o+x*w,10+y*w,w,w); 439 | if (m) 440 | mainCanvasContext.strokeRect(o+x*w,10+y*w,w,w); 441 | } 442 | 443 | // draw the objects on the minimap 444 | gameObjects.forEach(object=> 445 | { 446 | let r = object.radarSize; 447 | if (r && levelMaze[MazeDataPos(object.pos)]<0) 448 | { 449 | let p = object.pos.Clone(w/cellWidth).AddXY(o,10).Round(); 450 | mainCanvasContext.fillStyle=object==player?'#FFF':'#000'; 451 | mainCanvasContext.fillRect(p.x-r-1,p.y-r-1,2*r+2,2*r+2); 452 | mainCanvasContext.fillStyle=object==player?'#000':object.isEnemy?'#F00':'#FFF'; 453 | mainCanvasContext.fillRect(p.x-r,p.y-r,2*r,2*r); 454 | } 455 | }); 456 | } 457 | 458 | /////////////////////////////////////////////////////////////////////////////// 459 | // game objects 460 | 461 | class MyGameObject extends GameObject 462 | { 463 | constructor(pos,tileX=0,tileY=0,size=.5,collisionSize=0,health=1) 464 | { 465 | super(pos,tileX,tileY,size,collisionSize,health); 466 | this.walkFrame=0; 467 | this.rotation=1; 468 | this.radarSize=1; 469 | this.isInvisible = 0; 470 | this.bloodColor = new Color(.8,0,.05,.5); 471 | this.bloodAdditive = 0; 472 | } 473 | 474 | UpdateWalk() 475 | { 476 | // footprints 477 | let lastWalkFrame = this.walkFrame; 478 | this.walkFrame += 1.5*this.velocity.Length(); 479 | if (lastWalkFrame%2 < 1 && this.walkFrame%2 >1 || this.walkFrame%2 < lastWalkFrame%2) 480 | { 481 | let isOnSand = this.IsOnSand(); 482 | let angle = this.rotation * PI/2; 483 | let side = this.walkFrame%2 < lastWalkFrame%2? 1 : -1 484 | let offset = (new Vector2(0,side/8)).Rotate(angle); 485 | let footPos = this.pos.Clone().Add(offset); 486 | let c = isOnSand?'#4215':'#2223'; 487 | let s = new Vector2(.2,isOnSand?.2:.1); 488 | if (isOnSand) 489 | { 490 | s.y*=RandBetween(1,1.5); 491 | s.x*=RandBetween(1,1.5); 492 | } 493 | footPos.y+=.3; 494 | level.DrawEllipse(footPos,s,c,angle); 495 | } 496 | } 497 | 498 | BloodSplat(scale=1,particles=1) 499 | { 500 | // draw a bunch of random ellipses 501 | if (this.bloodAdditive) 502 | levelCanvasContext.globalCompositeOperation='screen'; 503 | for(let i=30;i--;) 504 | { 505 | let pos = this.pos.Clone().Add(RandVector(Rand(scale*this.size.x))); 506 | let size = new Vector2(this.size.x*RandBetween(.2,.5),this.size.y*RandBetween(.2,.5)) 507 | let angle = RandBetween(0,2*PI); 508 | level.DrawEllipse(pos,size.Multiply(scale),this.bloodColor.RGBA(),angle); 509 | } 510 | levelCanvasContext.globalCompositeOperation='source-over'; 511 | 512 | // kick off a particle effect 513 | if (particles) 514 | { 515 | let s = scale*this.size.x; 516 | let p = new ParticleEmitter 517 | ( 518 | this.pos, s*.6, s*.2, // pos, emitter size, particle size 519 | this.bloodColor.Clone().SetAlpha(1), 520 | this.bloodColor.Clone(this.bloodAdditive?3:.5).SetAlpha(1) 521 | ); 522 | } 523 | } 524 | 525 | Kill() 526 | { 527 | this.BloodSplat(); 528 | PlaySound(9); 529 | super.Kill(); 530 | } 531 | 532 | Render() 533 | { 534 | // invisible objects become visible when damaged 535 | if (this.isInvisible && !shadowRenderPass && !hitRenderPass) 536 | mainCanvasContext.globalAlpha= .1 + this.GetDamageFlashPercent(); 537 | super.Render(); 538 | } 539 | 540 | IsOnSand() { return level.GetDataFromPos(this.pos).type == 2; } 541 | } 542 | 543 | class Player extends MyGameObject 544 | { 545 | constructor(pos) 546 | { 547 | super(pos,0,4,.5,.4,playerData.healthMax); 548 | this.health = playerData.health; 549 | this.dashTimer = new Timer(); 550 | this.throwTimer = new Timer(); 551 | this.inputTimer = new Timer(); 552 | this.playerDamageTimer = new Timer(); 553 | this.inputTimer.Set(); 554 | this.throwRotation = 0; 555 | this.posBuffer = []; 556 | this.dashWaitTime = 3; 557 | this.radarSize=2; 558 | } 559 | 560 | IsDashing() { return !this.dashTimer.Elapsed(); } 561 | 562 | CollideLevel(data, pos) 563 | { 564 | // destroy level if dashing 565 | if (this.IsDashing()) 566 | return DestroyLevelObject(pos); 567 | else 568 | return super.CollideLevel(data, pos); 569 | } 570 | 571 | IsIntro() { return (levelTimer.Get() < 1.5) } 572 | 573 | Update() 574 | { 575 | // keep player data updated 576 | playerData.health = this.health; 577 | if (this.IsDead() || endLevelTimer.IsSet() || this.IsIntro()) 578 | { 579 | // stop and do no more 580 | return; 581 | } 582 | 583 | if (this.health <= 1 && healthWarning.Get() > this.health) 584 | { 585 | // health warning 586 | healthWarning.Set(); 587 | PlaySound(11); 588 | } 589 | 590 | if (MouseWasPressed() && (playerData.boomerangs|| playerData.bigBoomerangs)) 591 | { 592 | // throw boomerang 593 | let isBig = 0; 594 | if (playerData.bigBoomerangs) 595 | { 596 | --playerData.bigBoomerangs; 597 | isBig = 1; 598 | } 599 | else 600 | --playerData.boomerangs; 601 | let b = new Boomerang(this.pos,isBig); 602 | this.throwRotation= b.Throw(this, mousePosWorld); 603 | this.throwTimer.Set(.4); 604 | } 605 | 606 | // move input 607 | let acceleration = new Vector2(); 608 | if (KeyIsDown(65)) 609 | acceleration.x -= 1,this.rotation=0; 610 | if (KeyIsDown(68)) 611 | acceleration.x += 1,this.rotation=2; 612 | if (KeyIsDown(87)) 613 | acceleration.y -= 1,this.rotation=3; 614 | if (KeyIsDown(83)) 615 | acceleration.y += 1,this.rotation=1; 616 | 617 | let isOnSand = this.IsOnSand(); 618 | if (this.IsDashing()) 619 | { 620 | // update dash 621 | if (!acceleration.x && !acceleration.y) 622 | acceleration.Set(-1,0).Rotate(-this.rotation*PI/2); 623 | 624 | // no damage or slow from sand while dashing 625 | this.damageTimer.Set(); 626 | isOnSand = 0; 627 | 628 | // track players position for the dash render effect 629 | if (frame%3==0) 630 | this.posBuffer.push(this.pos.Clone()); 631 | if (this.posBuffer.length > 20) 632 | this.posBuffer.shift(); 633 | } 634 | else 635 | { 636 | // update non dash 637 | this.posBuffer = []; 638 | 639 | if (this.dashTimer.IsSet() && this.dashTimer.Get()>this.dashWaitTime) 640 | { 641 | // play sound when dash is ready again 642 | this.dashTimer.UnSet(); 643 | PlaySound(16); 644 | } 645 | 646 | if ((KeyWasPressed(32)||KeyWasPressed(16)) && !this.dashTimer.IsSet()) 647 | { 648 | // start dash 649 | PlaySound(12); 650 | this.dashTimer.Set(.5); 651 | } 652 | } 653 | 654 | if (acceleration.x || acceleration.y) 655 | { 656 | // apply acceleration 657 | acceleration.Normalize(.016*(isOnSand?.5:1)); 658 | if (this.IsDashing()) 659 | acceleration.Multiply(2); 660 | this.velocity.Add(acceleration); 661 | this.inputTimer.Set(); 662 | } 663 | 664 | // reset walk frame when input isnt pressed for a while 665 | if (this.inputTimer.Get() > 1) 666 | this.walkFrame = 0; 667 | 668 | // update walk if not throwing or dashing 669 | if (this.throwTimer.Elapsed() && !this.IsDashing()) 670 | this.UpdateWalk(); 671 | 672 | super.Update(); 673 | } 674 | 675 | Render() 676 | { 677 | if (endLevelTimer.IsSet()) 678 | return; 679 | 680 | if (this.IsDead() || this.IsIntro() && isStartLevel) 681 | { 682 | // set to dead tile 683 | this.tileX = 7; 684 | this.tileY = 3; 685 | super.Render(); 686 | return; 687 | } 688 | 689 | // figure out the tile, rotation and mirror 690 | this.tileY = 4; 691 | if (this.rotation&1) 692 | { 693 | // facing left or right 694 | // walk by toggling betwen 2 frames and mirror to face direction 695 | this.tileX = this.rotation==1?2:3; 696 | this.mirror = this.walkFrame%2|0; 697 | if (!this.throwTimer.Elapsed()||!this.dashTimer.Elapsed()) 698 | this.tileX += 3; // throw/dash frame 699 | else if (this.inputTimer.Get() > 1 && this.rotation==1) 700 | { 701 | // idle 702 | this.tileX = 7; 703 | this.mirror = (this.inputTimer.Get()/2|0)&1; 704 | } 705 | } 706 | else 707 | { 708 | // facing up or down 709 | // walk by toggling mirror and select frame to face direction 710 | this.mirror = this.rotation!=2; 711 | this.tileX = this.walkFrame%2|0; 712 | if (!this.throwTimer.Elapsed()||!this.dashTimer.Elapsed()) 713 | this.tileX = 4; // throw/dash frame 714 | } 715 | 716 | let hit = hitRenderPass; 717 | if (!this.throwTimer.Elapsed()) 718 | { 719 | // use the throw rotation if throwing 720 | this.rotation = this.throwRotation; 721 | if (this.rotation&1) 722 | this.mirror = this.rotation==1; 723 | } 724 | if (!shadowRenderPass && hit) 725 | { 726 | // draw the position buffer during the hit render pass when dashing 727 | mainCanvasContext.globalCompositeOperation = 'screen'; 728 | for(let i=this.posBuffer.length;i--;) 729 | { 730 | hitRenderPass = hit*(i/this.posBuffer.length + .01); 731 | DrawTile(this.posBuffer[i],this.size,this.tileX,this.tileY,this.angle,this.mirror,this.height); 732 | } 733 | hitRenderPass = hit; 734 | mainCanvasContext.globalCompositeOperation = 'difference'; 735 | } 736 | 737 | let d = this.dashTimer.Get(); 738 | if (!shadowRenderPass && d 4*this.damageFlashTime) 839 | this.damageTimer.Set(-this.damageFlashTime/2); // sparkle 840 | } 841 | else 842 | { 843 | if (endLevelTimer.IsSet()) 844 | endLevelTimer.Set(1); // wait for boomerang to end level 845 | 846 | // apply throw acceleration 847 | let a = this.GetLifeTime(); 848 | if (!mouseIsDown || !this.throwFrames) 849 | this.throwAccel=0; 850 | else if (this.throwAccel) 851 | this.velocity.Add(this.throwAccel); 852 | 853 | // reduce angular velocity 854 | this.angleVelocity -= .002; 855 | if (this.angleVelocity < .1) 856 | { 857 | // slow it down even faster 858 | this.angleVelocity -= .005; 859 | this.velocity.Multiply(.8); 860 | if (this.angleVelocity < 0) 861 | this.angleVelocity = 0; 862 | } 863 | 864 | // height is proportional to angular velocity 865 | this.height = this.angleVelocity/2; 866 | if (!this.throwAccel && this.height > 0) 867 | { 868 | // pull to player 869 | let d = player.pos.Clone(); 870 | d.Subtract(this.pos).Multiply(0.004 * this.angleVelocity); 871 | this.velocity.Add(d); 872 | } 873 | 874 | gameObjects.forEach(o=> 875 | { 876 | if (!this.isBig && o.isSmallPickup && o.GetLifeTime() > .5 && !o.isHeld && !this.heldPickup && o.IsTouching(this)) 877 | { 878 | // grab object 879 | o.isHeld = 1; 880 | this.heldPickup = o; 881 | } 882 | else if ((o.isEnemy||o.isStore) && o.IsTouching(this)) 883 | { 884 | // hit object 885 | if (this.bounceObject == o || o.ReflectDamage(this.velocity)) 886 | { 887 | if (this.bounceObject != o) 888 | { 889 | // reflect 890 | PlaySound(15); 891 | this.bounceObject = o; 892 | this.velocity.Multiply(-.4); 893 | this.angleVelocity*=.4; 894 | this.damageTimer.Set(); 895 | this.throwAccel=0; 896 | } 897 | } 898 | else if (o.Damage(1+this.isBig)) 899 | { 900 | // apply damage 901 | if (!isStartLevel) // dont push in hub 902 | o.velocity.Add(this.velocity.Clone(.5)); 903 | this.damageTimer.Set(); 904 | } 905 | } 906 | }); 907 | } 908 | 909 | // let player pick it up 910 | if ((!this.angleVelocity || this.GetLifeTime() > .5) && !player.IsDead() && player.Distance(this) < .6) 911 | this.Pickup(); 912 | 913 | super.Update(); 914 | 915 | // set all pickups to match our position 916 | if (this.heldPickup) 917 | this.heldPickup.pos.Copy(this.pos).AddXY(0,-.001); 918 | } 919 | 920 | Pickup() 921 | { 922 | PlaySound(6); 923 | if (this.isBig) 924 | playerData.bigBoomerangs++; 925 | else 926 | playerData.boomerangs++; 927 | player.throwTimer.Set(.3); 928 | player.throwRotation=this.pos.Clone().Subtract(player.pos).Rotation(); 929 | this.Destroy(); 930 | } 931 | } 932 | 933 | /////////////////////////////////////////////////////////////////////////////// 934 | 935 | class Pickup extends MyGameObject 936 | { 937 | // type: 0=half, 1=whole, 2=container, 3=coin, 4=large coin 938 | 939 | constructor(pos, type=0) 940 | { 941 | super(pos,2+type,5,.5,.3); 942 | this.type = type; 943 | this.timeOffset = Rand(9); 944 | this.isSmallPickup = type != 2; 945 | this.isHeld=0; 946 | this.differenceFlash = 0; 947 | this.damageFlashTime = .8; 948 | this.radarSize = this.isSmallPickup?1:3; 949 | } 950 | 951 | Update() 952 | { 953 | // random sparkles 954 | if (Rand() < .005 && this.damageTimer.Get() > 4*this.damageFlashTime) 955 | this.damageTimer.Set(-this.damageFlashTime/2); 956 | 957 | // bob up and down 958 | this.height = .1+.1*Math.sin(2*time+this.timeOffset); 959 | 960 | // let player pick it up 961 | if (!player.IsDead() && player.IsTouching(this)) 962 | this.Pickup(); 963 | else if (boss && boss.IsTouching(this)) 964 | { 965 | // boss destroys pickups 966 | PlaySound(14); 967 | this.Destroy(); 968 | } 969 | 970 | super.Update(); 971 | } 972 | 973 | Pickup() 974 | { 975 | if (this.type==2) 976 | { 977 | // heart container 978 | ++playerData.healthMax; 979 | player.healthMax = playerData.healthMax; 980 | player.Heal(1); 981 | PlaySound(4); 982 | } 983 | else if (this.type==3) 984 | { 985 | // 1 coin 986 | PlaySound(10); 987 | ++playerData.coins; 988 | } 989 | else if (this.type==4) 990 | { 991 | // 5 coin 992 | PlaySound(10); 993 | playerData.coins+=5; 994 | } 995 | else 996 | { 997 | // half or whole heart 998 | player.Heal(.5+ this.type/2) 999 | PlaySound(3); 1000 | } 1001 | this.Destroy(); 1002 | } 1003 | } 1004 | 1005 | /////////////////////////////////////////////////////////////////////////////// 1006 | 1007 | class Enemy extends MyGameObject 1008 | { 1009 | constructor(pos,tileX=0,tileY=0,size=.5,collisionSize=0,health=1,big=0) 1010 | { 1011 | super(pos,tileX,tileY,size,collisionSize,health); 1012 | this.isEnemy = 1 1013 | this.spawnPickup = 1; 1014 | this.bloodAdditive = 1; 1015 | this.isBig = big; 1016 | this.radarSize=big?2:1; 1017 | } 1018 | 1019 | Damage(damage) 1020 | { 1021 | let damageDone = super.Damage(damage); 1022 | if (damageDone && !this.IsDead()) 1023 | { 1024 | this.BloodSplat(.5); 1025 | PlaySound(8); 1026 | } 1027 | 1028 | return damageDone; 1029 | } 1030 | 1031 | Update() 1032 | { 1033 | if (player.IsTouching(this)) 1034 | if (player.Damage(.5)) 1035 | { 1036 | // push player when damaged 1037 | let accel = player.pos.Clone(); 1038 | accel.Subtract(this.pos).Normalize(.1); 1039 | player.velocity.Add(accel); 1040 | } 1041 | 1042 | super.Update(); 1043 | } 1044 | 1045 | Kill() 1046 | { 1047 | super.Kill(); 1048 | 1049 | // spawn portal if no enemies left 1050 | if (!levelExit && !isFinalLevel && !player.IsDead()) 1051 | { 1052 | if (!gameObjects.some(o=>o.isEnemy)) 1053 | { 1054 | levelExit = new LevelExit(this.pos); 1055 | if (!this.isBig) 1056 | this.spawnPickup = 0; 1057 | } 1058 | } 1059 | 1060 | // spawn pickups 1061 | let count = 1; 1062 | if (this.isBig) 1063 | count = RandIntBetween(3, 5); 1064 | if (this.isInvisible) 1065 | count = count * 2; 1066 | SpawnPickups(this.pos, this.spawnPickup, count); 1067 | } 1068 | } 1069 | 1070 | /////////////////////////////////////////////////////////////////////////////// 1071 | 1072 | class SlimeEnemy extends Enemy 1073 | { 1074 | constructor(pos,healthLevel,difficulty=1) 1075 | { 1076 | let size = .25*healthLevel; 1077 | super(pos,5+difficulty,0,size,size*.8,healthLevel*difficulty,healthLevel>3); 1078 | this.bloodColor = difficulty>1?new Color(1,0,.5,.5):new Color(0,.5,1,.5); 1079 | this.randMoveTimer = new Timer(); 1080 | this.randAccel = new Vector2(); 1081 | this.spawnPickup = healthLevel == 1? difficulty/2 : 0; 1082 | this.difficulty = difficulty; 1083 | this.healthLevel = healthLevel; 1084 | this.baseSize = size; 1085 | this.damping = .9; 1086 | } 1087 | 1088 | Update() 1089 | { 1090 | let playerDistance = player.pos.Distance(this.pos); 1091 | if (playerDistance > 20) 1092 | return; 1093 | 1094 | // draw additive trail 1095 | levelCanvasContext.globalCompositeOperation='screen'; 1096 | let trailColor = this.bloodColor.Clone().SetAlpha(.05).RGBA(); 1097 | level.DrawEllipse(this.pos,(new Vector2(.6,.4)).Multiply(this.size),trailColor); 1098 | levelCanvasContext.globalCompositeOperation='source-over'; 1099 | 1100 | // random movement 1101 | if (this.randMoveTimer.Elapsed()) 1102 | { 1103 | this.randMoveTimer.Set(RandBetween(.5,1)); 1104 | this.randAccel = RandVector(1.3); 1105 | } 1106 | 1107 | // calculate acceleration 1108 | let accel = new Vector2(); 1109 | if (!player.IsDead() && playerDistance < 15) 1110 | accel.Copy(player.pos) 1111 | .Subtract(this.pos) 1112 | .Normalize(); 1113 | accel.Add(this.randAccel) 1114 | .Multiply(this.difficulty>1?.004:.003) 1115 | .Multiply(this.IsOnSand()?.5:1); 1116 | this.velocity.Add(accel); 1117 | 1118 | // change shape as it moves 1119 | let s = Math.sin(10 * this.GetLifeTime()); 1120 | let sx = .9+.1*(1-s); 1121 | let sy = .9+.1*s; 1122 | this.size.Set(this.baseSize*sx,this.baseSize*sy); 1123 | 1124 | super.Update(); 1125 | } 1126 | 1127 | Kill() 1128 | { 1129 | if (this.healthLevel > 1) 1130 | { 1131 | // spawn baby slimes 1132 | for(let i=2;i--;) 1133 | { 1134 | let s = new SlimeEnemy(this.pos, this.healthLevel-1, this.difficulty); 1135 | s.damageTimer.Set(); // prevent them taking damage right away 1136 | s.isInvisible = this.isInvisible; 1137 | s.velocity = this.velocity.Clone(); 1138 | } 1139 | } 1140 | 1141 | super.Kill(); 1142 | } 1143 | } 1144 | 1145 | /////////////////////////////////////////////////////////////////////////////// 1146 | 1147 | class JumpingEnemy extends Enemy 1148 | { 1149 | constructor(pos,isBig=0) 1150 | { 1151 | super(pos,2,2,isBig?1:.5,isBig?.8:.4,isBig?12:4,isBig); 1152 | this.landTimer = new Timer(); 1153 | this.jumpWaitTimer = new Timer(); 1154 | this.jumpWaitTimer.Set(RandBetween(1,3)); 1155 | this.zVelocity = 0; 1156 | this.randOffset = new Vector2(); 1157 | this.bloodColor = new Color(1,.5,0,.1); 1158 | this.speed = isBig?.012:.01; 1159 | } 1160 | 1161 | Update() 1162 | { 1163 | let playerDistance = player.pos.Distance(this.pos); 1164 | if (playerDistance > 20) 1165 | return; 1166 | 1167 | if (this.jumpWaitTimer.Elapsed() && this.height <= 0) 1168 | { 1169 | // jump 1170 | this.zVelocity = RandBetween(.15,.2); 1171 | if (this.isBig) 1172 | this.zVelocity *= 1.2; 1173 | this.jumpWaitTimer.Set(RandBetween(1.5,3)); 1174 | this.randOffset = RandVector(RandBetween(0,1)); 1175 | this.landTimer.UnSet(); 1176 | } 1177 | 1178 | // update jump 1179 | this.height += this.zVelocity; 1180 | this.zVelocity -= .005; 1181 | if (this.height <= 0) 1182 | { 1183 | // is on ground 1184 | if (!this.landTimer.IsSet()) 1185 | { 1186 | // just landed 1187 | this.landTimer.Set(.3); 1188 | this.BloodSplat(.8,0); 1189 | } 1190 | this.height = this.zVelocity = 0; 1191 | } 1192 | else 1193 | { 1194 | // is in the air 1195 | let accel = new Vector2(); 1196 | if (!player.IsDead() && playerDistance < 15) 1197 | { 1198 | // move towards player 1199 | accel.Copy(player.pos) 1200 | .Subtract(this.pos) 1201 | .ClampLength(1) 1202 | .Add(this.randOffset); 1203 | } 1204 | else 1205 | accel.Copy(this.randOffset); 1206 | this.velocity.Add(accel.Multiply(this.speed)); 1207 | 1208 | //DebugPoint(player.pos.Clone().Add(this.randOffset)); 1209 | } 1210 | 1211 | // set draw tile when jumping or landed 1212 | this.tileX = 2; 1213 | if (this.jumpWaitTimer.Get() > -.25 || !this.landTimer.Elapsed()) 1214 | ++this.tileX; 1215 | 1216 | super.Update(); 1217 | } 1218 | } 1219 | 1220 | /////////////////////////////////////////////////////////////////////////////// 1221 | 1222 | class ShieldEnemy extends Enemy 1223 | { 1224 | constructor(pos, type=0, isBig=0) 1225 | { 1226 | super(pos,4,2,isBig?1:.5,isBig?.8:.4,type?50:isBig?6:2,isBig); 1227 | this.moveTimer = new Timer(); 1228 | this.dashTimer = new Timer(); 1229 | this.damping=.8; 1230 | this.bumped=0; 1231 | this.type = type; 1232 | this.moveBackwards = 0; 1233 | this.speed = isBig?.015:.012; 1234 | if (type) 1235 | { 1236 | boss = this; 1237 | this.speed = .018; 1238 | this.bloodAdditive = 0; 1239 | } 1240 | else 1241 | this.bloodColor = new Color(.3,1,0,.5); 1242 | 1243 | this.bossIntro = this.type && !isStartLevel; 1244 | } 1245 | 1246 | ReflectDamage(direction) 1247 | { 1248 | if (this.damageTimer.Get() < .5) 1249 | return 0; 1250 | 1251 | // figure out if damge should be reflected 1252 | let d = new Vector2(1,0).Rotate(this.rotation*PI/2); 1253 | let a = direction.Clone().Normalize().DotProduct(d); 1254 | return this.type? (a > .4) : (a < -.4); 1255 | } 1256 | 1257 | CollideLevel(data, pos) 1258 | { 1259 | let small = !this.isBig && !this.type; 1260 | if (data.IsSolid()) 1261 | { 1262 | if (small) 1263 | { 1264 | if (this.dashTimer.Elapsed()) // change direction if not dashing 1265 | this.rotation = (this.rotation+2)%4; 1266 | } 1267 | this.velocity.Multiply(0); 1268 | } 1269 | 1270 | // break level objects 1271 | return DestroyLevelObject(pos, !this.type); 1272 | } 1273 | 1274 | Damage(damage) 1275 | { 1276 | // prevent player killing the boss after dying 1277 | if (this.type && player.IsDead()) 1278 | return 0; 1279 | 1280 | return super.Damage(damage); 1281 | } 1282 | 1283 | Update() 1284 | { 1285 | let lifeTime = this.GetLifeTime(); 1286 | let isOnSand = !this.type && this.IsOnSand(); 1287 | let playerDistance = player.pos.Distance(this.pos); 1288 | 1289 | if (this.type && isStartLevel) 1290 | { 1291 | // title screen - run towards the level exit 1292 | let d = this.pos.x - levelExit.pos.x; 1293 | this.rotation = Math.abs(d) < .5? 1: 0; 1294 | if (this.Distance(levelExit) < 1) 1295 | this.Destroy(); 1296 | } 1297 | else if (this.bossIntro) 1298 | { 1299 | // boss intro, run left and up 1300 | if (this.pos.x>24) 1301 | { 1302 | this.pos.x = 24 1303 | this.rotation = 3; 1304 | } 1305 | else 1306 | this.rotation = 0; 1307 | } 1308 | else 1309 | { 1310 | if (playerDistance > 20 && !this.type) 1311 | return; 1312 | 1313 | // update ai 1314 | if (this.moveTimer.Elapsed() && this.dashTimer.Elapsed()) 1315 | { 1316 | this.moveBackwards = 0; 1317 | if (!player.IsDead() && (playerDistance < 15 || this.type)) 1318 | { 1319 | // get player direction 1320 | let d = player.pos.Clone().Subtract(this.pos); 1321 | let r = d.Rotation(); 1322 | if (!(r&1)) 1323 | r=(r+2)%4; // left/right is backwards 1324 | this.rotation = r; 1325 | 1326 | // boss can randomly move backwards 1327 | if (this.type) 1328 | this.moveBackwards = Rand()<.5; 1329 | if (this.moveBackwards) 1330 | this.rotation = (this.rotation+2)%4 1331 | 1332 | // randomly decide to dash 1333 | if (Rand()<.2) 1334 | this.dashTimer.Set(2); 1335 | } 1336 | else 1337 | this.rotation = RandInt(4); 1338 | this.moveTimer.Set(RandBetween(.8,2)); 1339 | } 1340 | } 1341 | 1342 | // apply move acceleration 1343 | let moveAccel = new Vector2(this.speed*(isOnSand?.5:1),0).Rotate(this.rotation*PI/2); 1344 | if (!this.dashTimer.Elapsed()) 1345 | moveAccel.Multiply((this.dashTimer.Get() < -1)?0:2); 1346 | 1347 | if (this.type && isStartLevel) 1348 | { 1349 | // title screen 1350 | if (playerDistance > 10.5) 1351 | moveAccel.Multiply(0); 1352 | } 1353 | else if (this.type) 1354 | { 1355 | // boss fight 1356 | if (lifeTime > 10 && this.health < this.healthMax || lifeTime > 20 && playerDistance < 10 ) 1357 | this.bossIntro = 0; 1358 | 1359 | if (this.bossIntro) 1360 | { 1361 | if (playerDistance > 14) 1362 | moveAccel.Multiply(0); 1363 | if (this.pos.y < 21) 1364 | { 1365 | // wait to get hit 1366 | moveAccel.Multiply(0); 1367 | this.rotation = 1; 1368 | this.walkFrame += .021; 1369 | } 1370 | } 1371 | else 1372 | { 1373 | this.bossIntro = 0; 1374 | if (this.size.x<2) 1375 | { 1376 | // grow giant 1377 | this.size.AddXY(.005,.005); 1378 | this.collisionSize = this.size.x*.8; 1379 | moveAccel.Multiply(0); 1380 | this.walkFrame += .1; 1381 | if (frame%10==0) 1382 | this.rotation = (this.rotation+1)%4; 1383 | moveAccel.Multiply(0); 1384 | } 1385 | else 1386 | this.size.Set(2,2); 1387 | } 1388 | } 1389 | 1390 | // apply acceleration 1391 | this.velocity.Add(moveAccel.Multiply(this.moveBackwards?-1:1)); 1392 | 1393 | // set the tile and mirror 1394 | if (this.rotation&1) 1395 | { 1396 | // facing left or right 1397 | this.tileX = (this.rotation==1)?6:7; 1398 | this.mirror = this.walkFrame%2|0; 1399 | } 1400 | else 1401 | { 1402 | // facing up or down 1403 | this.mirror = this.rotation; 1404 | this.tileX = 4 + (this.walkFrame%2|0); 1405 | } 1406 | 1407 | if (this.type) 1408 | { 1409 | // if boss, offset the tile position 1410 | this.tileY=3; 1411 | this.tileX-=2; 1412 | } 1413 | 1414 | this.UpdateWalk(); 1415 | super.Update(); 1416 | } 1417 | 1418 | Kill() 1419 | { 1420 | super.Kill(); 1421 | 1422 | if (this.type) 1423 | { 1424 | boss = 0; 1425 | if (isFinalLevel) 1426 | { 1427 | // player win 1428 | new Pickup(this.pos, 2); 1429 | SpawnPickups(this.pos,1,40); 1430 | winTimer.Set(); 1431 | localStorage.kbap_warp=0; 1432 | localStorage.kbap_won=1; 1433 | speedRunTime=speedRunTime|0; 1434 | if (speedRunMode && (!speedRunBestTime || speedRunTime < speedRunBestTime)) 1435 | { 1436 | // track best speed run time 1437 | speedRunBestTime = speedRunTime; 1438 | localStorage.kbap_bestTime=speedRunBestTime; 1439 | } 1440 | PlaySound(2); 1441 | } 1442 | } 1443 | } 1444 | } 1445 | 1446 | /////////////////////////////////////////////////////////////////////////////// 1447 | 1448 | class Store extends MyGameObject 1449 | { 1450 | constructor(pos) 1451 | { 1452 | super(pos,7,1,.5,.5); 1453 | 1454 | // spawn random items 1455 | this.count = isStartLevel||isFinalLevel?3:2 + RandInt(2); 1456 | let o = 1-this.count; 1457 | for(let i=this.count;i--;) 1458 | { 1459 | let item = RandInt(4); 1460 | if (isStartLevel || isFinalLevel) 1461 | item = i+1; 1462 | else 1463 | { 1464 | if (i==0) 1465 | item = RandIntBetween(0,1); 1466 | else if (i==1) 1467 | item = RandIntBetween(2,3); 1468 | } 1469 | new StoreItem(pos.Clone().AddXY(i*2+o,0),item,this); 1470 | } 1471 | 1472 | this.pos.y-=2; 1473 | this.isStore = 1; 1474 | level.FillCircleType(pos,3,1); // clear area 1475 | level.FillCircleObject(pos,5,0); // clear area 1476 | } 1477 | 1478 | Kill() 1479 | { 1480 | SpawnPickups(this.pos); 1481 | super.Kill(); 1482 | } 1483 | 1484 | Update() 1485 | { 1486 | // draw carpet after being spawned 1487 | if (this.GetLifeTime()<.1) 1488 | { 1489 | let p = this.pos.Clone().AddXY(0,2).Multiply(tileSize); 1490 | let w = this.count*16; 1491 | levelCanvasContext.fillStyle='#CCC'; 1492 | levelCanvasContext.fillRect(p.x-w-2,p.y-30-2,w*2+4,40+4); 1493 | levelCanvasContext.fillStyle='#329'; 1494 | levelCanvasContext.fillRect(p.x-w,p.y-30,w*2,40); 1495 | } 1496 | 1497 | this.mirror = player.pos.x{if (o.isEnemy && o.IsTouching(this))this.Kill();}); 1507 | 1508 | super.Update(); 1509 | } 1510 | } 1511 | 1512 | class StoreItem extends MyGameObject 1513 | { 1514 | constructor(pos,type,owner) 1515 | { 1516 | // 0 = whole heart 1517 | // 1 = heart container 1518 | // 2 = boomerang 1519 | // 3 = big boomerang 1520 | 1521 | super(pos,type+3,5,.5,.2); 1522 | this.owner = owner; 1523 | this.type = type; 1524 | this.cost = 5; 1525 | this.wasTouching=0; 1526 | 1527 | // set up tile and cost 1528 | if (this.type == 1) 1529 | this.cost = 50; 1530 | if (this.type == 2) 1531 | { 1532 | this.cost = 40; 1533 | this.tileX = 0; 1534 | } 1535 | if (this.type == 3) 1536 | { 1537 | this.cost = 90; 1538 | this.tileX = 7; 1539 | } 1540 | 1541 | // randomize cost 1542 | if (!isStartLevel) 1543 | this.cost *= RandBetween(.5,1.5); 1544 | this.cost = Clamp(this.cost, 1, 99); 1545 | this.cost |= 0; 1546 | } 1547 | 1548 | Update() 1549 | { 1550 | // let player pickup 1551 | if (!player.IsDead() && player.IsTouching(this)) 1552 | { 1553 | if (!this.wasTouching) 1554 | this.Pickup(); 1555 | this.wasTouching = 1; 1556 | } 1557 | else 1558 | this.wasTouching = 0; 1559 | 1560 | if (this.owner && this.owner.IsDead()) 1561 | this.Destroy(); 1562 | 1563 | super.Update(); 1564 | } 1565 | 1566 | Pickup() 1567 | { 1568 | if (this.cost>playerData.coins) 1569 | { 1570 | // player doesn't have enough money 1571 | PlaySound(15); 1572 | this.damageTimer.Set(); 1573 | return; 1574 | } 1575 | 1576 | // give player the item 1577 | if (this.type < 2) 1578 | (new Pickup(this.pos,this.type+1)).Pickup(); 1579 | else 1580 | (new Boomerang(this.pos,this.type == 3)).Pickup(); 1581 | 1582 | buyTimer.Set(1); 1583 | playerData.coins-=this.cost; 1584 | this.Destroy(); 1585 | } 1586 | 1587 | Render() 1588 | { 1589 | if (!shadowRenderPass && !hitRenderPass) 1590 | { 1591 | // draw the price 1592 | SetCanvasTransform(this.pos.Clone().AddXY(0,-1), this.size); 1593 | DrawText(this.cost,-6,0,14,'left',.5,this.GetDamageTime()<.5?"#F00":"#000"); 1594 | DrawScreenTile(-10,-1,4,5,5); 1595 | mainCanvasContext.restore(); 1596 | } 1597 | 1598 | super.Render(); 1599 | } 1600 | } 1601 | 1602 | /////////////////////////////////////////////////////////////////////////////// 1603 | 1604 | class LevelExit extends MyGameObject 1605 | { 1606 | constructor(pos,type=0) 1607 | { 1608 | super(pos,0,0,0,.5); 1609 | this.type=type; 1610 | this.radarSize=2; 1611 | this.closeTimer = new Timer(); 1612 | this.pos.y+=.01; 1613 | } 1614 | 1615 | Update() 1616 | { 1617 | // bob and spin 1618 | this.height =.1+.1*Math.sin(5*time); 1619 | this.angleVelocity =.05/(this.size+.1); 1620 | 1621 | let playerOffset = this.pos.Clone().Subtract(player.pos); 1622 | let playerDistance = playerOffset.Length(); 1623 | let radius = this.size.x; 1624 | if (this.type==1) 1625 | { 1626 | // incomming portal as player spawns in 1627 | if (levelFrame==60) 1628 | PlaySound(13); 1629 | 1630 | // get smaller and go away 1631 | radius = Max(0,1 - levelTimer.Get()/2); 1632 | if (radius <= 0) 1633 | this.Destroy(); 1634 | } 1635 | else if (this.closeTimer.IsSet()) 1636 | { 1637 | // get smaller if closing 1638 | radius = -this.closeTimer.Get(); 1639 | } 1640 | else if (this.GetLifeTime() < 1) 1641 | { 1642 | // open up when it first appears 1643 | let t = this.GetLifeTime(); 1644 | radius = Min(t*2,1); 1645 | } 1646 | else if (!player.IsDead() && playerDistance < 3 && player.dashTimer.Elapsed()) 1647 | { 1648 | // player is close to portal 1649 | if (playerDistance < .5) 1650 | { 1651 | // player entered portal 1652 | if (isStartLevel && this.type == 3) 1653 | { 1654 | // speed run portal 1655 | speedRunMode = 1; 1656 | speedRunTime = 0; 1657 | playerData = new PlayerData(); 1658 | } 1659 | if (this.type == 2) 1660 | { 1661 | // warp portal 1662 | nextLevel = warpLevel; 1663 | } 1664 | PlaySound(13); 1665 | endLevelTimer.Set(1); 1666 | this.closeTimer.Set(1); 1667 | } 1668 | else 1669 | { 1670 | // pull player into portal 1671 | player.velocity.Add(playerOffset.Normalize(.005/playerDistance)); 1672 | } 1673 | } 1674 | 1675 | radius = Max(0,radius); 1676 | this.size.Set(radius,radius); 1677 | 1678 | super.Update(); 1679 | } 1680 | 1681 | Render() 1682 | { 1683 | SetCanvasTransform(this.pos,this.size,this.angle,this.height); 1684 | 1685 | // draw wrap portal effect 1686 | mainCanvasContext.lineWidth=1; 1687 | let color; 1688 | for(let i=19;i--;mainCanvasContext.stroke()) 1689 | { 1690 | mainCanvasContext.beginPath(); 1691 | color=`hsla(${i*9+time*99},99%,${shadowRenderPass?0:50}%,${shadowRenderPass?.5:1})`; 1692 | mainCanvasContext.strokeStyle=color; 1693 | for(let j=8;j--;) 1694 | { 1695 | let a=time-j*PI/3+5*Math.sin(i/2+time*2)/19; 1696 | mainCanvasContext.arc(0,0,i*this.size.x,a,a) 1697 | } 1698 | } 1699 | 1700 | if (this.type > 1 && !shadowRenderPass && !this.closeTimer.IsSet()) 1701 | { 1702 | // render warp or speed run text 1703 | let text = (this.type == 2)? 'Warp '+warpLevel : 'Speed Run'; 1704 | DrawText(text,0,-24,14,'center',1,color,'#000'); 1705 | if (this.type == 3 && speedRunBestTime) 1706 | DrawText('Best '+FormatTime(speedRunBestTime),0,-36,12,'center',1,color,'#000'); 1707 | 1708 | } 1709 | mainCanvasContext.restore(); 1710 | } 1711 | } 1712 | 1713 | /////////////////////////////////////////////////////////////////////////////// 1714 | // level builder 1715 | 1716 | function GenerateMaze(cellCount) 1717 | { 1718 | // 2d maze 1719 | let size = levelMazeSize; 1720 | let cells = []; 1721 | cells.length = size*size; 1722 | cells.fill(0); 1723 | let GetCell=(x,y)=>cells[x+y*size]; 1724 | let SetCell=(x,y,v=1)=>cells[x+y*size]=v; 1725 | 1726 | // set start pos (it may change when generating the level) 1727 | let xStart=RandInt(levelMazeSize); 1728 | let yStart=RandInt(levelMazeSize); 1729 | 1730 | if (isStartLevel) 1731 | { 1732 | // hub level 1733 | xStart=yStart=0; 1734 | SetCell(0,0); 1735 | SetCell(1,0); 1736 | } 1737 | if (isFinalLevel) 1738 | { 1739 | // boss level 1740 | xStart=0; 1741 | yStart=3; 1742 | SetCell(0,3); 1743 | SetCell(1,3); 1744 | SetCell(2,3); 1745 | SetCell(1,2); 1746 | for(let i=4;i--;) 1747 | for(let j=2;j--;) 1748 | SetCell(i,j); 1749 | } 1750 | 1751 | playerStartPos = new Vector2(xStart, yStart); 1752 | if (isStartLevel || isFinalLevel) 1753 | return cells; 1754 | 1755 | // depth first search style maze generation 1756 | // https://en.wikipedia.org/wiki/Maze_generation_algorithm#Depth-first_search 1757 | let IsOpen=(x,y)=>(IsArrayValid(x,y,size)? cells[x+y*size] : 0); 1758 | let OpenNeighborCount=(xo,yo)=> 1759 | { 1760 | let n = 0; 1761 | n += IsOpen(xo+1,yo); 1762 | n += IsOpen(xo-1,yo); 1763 | n += IsOpen(xo,yo+1); 1764 | n += IsOpen(xo,yo-1); 1765 | return n; 1766 | } 1767 | 1768 | let CheckMove=(xo,yo,xd,yd)=> 1769 | !( 1770 | !IsArrayValid(xo+xd,yo+yd,size) || // must be valid cell 1771 | IsOpen(xo+xd,yo+yd) || // must be solid 1772 | 1773 | // surrounding cells in that direction must be solid 1774 | IsOpen(xo+xd*2,yo+yd*2) || 1775 | IsOpen(xo+xd*2+yd,yo+yd*2+xd) || 1776 | IsOpen(xo+xd*2-yd,yo+yd*2-xd) || 1777 | IsOpen(xo+xd+yd,yo+yd+xd) || 1778 | IsOpen(xo+xd-yd,yo+yd-xd) 1779 | ); 1780 | 1781 | let x=xStart; 1782 | let y=yStart; 1783 | SetCell(x,y); 1784 | 1785 | // generate a maze 1786 | let stack = []; 1787 | let endCount = 0; 1788 | for(let i=0; i0) 1919 | { 1920 | if (++tries>1e4) 1921 | return 0; 1922 | 1923 | // pick random pos 1924 | let pos = new Vector2(RandBetween(0,levelSize),RandBetween(0,levelSize)); 1925 | 1926 | // must be open maze cell except level 1 1927 | let m = levelMaze[MazeDataPos(pos)] 1928 | if (!m || (m==2 && levelNumber>1)) 1929 | continue; 1930 | 1931 | // must not be near player 1932 | if (pos.Distance(playerStartPos) < 15) 1933 | continue; 1934 | 1935 | // must be clear 1936 | if (tries > 500) 1937 | level.FillCircleObject(pos,2,0); // clear area if necessary 1938 | if (!level.IsAreaClear(pos,2)) 1939 | continue; 1940 | 1941 | // spawn enemy 1942 | let enemyPower = 0; 1943 | let e; 1944 | if (Rand() < .33 || levelNumber<=1) 1945 | { 1946 | // slime enemy 1947 | let healthLevel = RandIntBetween(1,3); 1948 | if (Rand() < .1 && levelNumber > 5) 1949 | healthLevel = 4; 1950 | 1951 | let difficulty = levelNumber<5||Rand()<.5?1:2; 1952 | e = new SlimeEnemy(pos,healthLevel,difficulty); 1953 | enemyPower = difficulty*healthLevel; 1954 | } 1955 | else if (levelNumber != 3 && (Rand() < .5 || levelNumber<=2)) 1956 | { 1957 | // jumping enemy 1958 | let isBig = Rand() < .1 && levelNumber > 3; 1959 | e = new JumpingEnemy(pos,isBig); 1960 | enemyPower = isBig?6:2; 1961 | } 1962 | else // shield enemy 1963 | { 1964 | let isBig = Rand() < .1 && levelNumber > 4; 1965 | e = new ShieldEnemy(pos,0,isBig); 1966 | enemyPower = isBig?9:3; 1967 | } 1968 | if (levelNumber > 4 && Rand()<.1) 1969 | { 1970 | // random invisible enemy 1971 | e.isInvisible = 1; 1972 | enemyPower *= 2; 1973 | } 1974 | 1975 | totalEnemyPower -= enemyPower; 1976 | } 1977 | } 1978 | 1979 | // spawn stores and other special objects 1980 | if (isStartLevel) 1981 | { 1982 | // title screen 1983 | new Store(new Vector2(24,3.5)); 1984 | new ShieldEnemy(playerStartPos.Clone(), 1); 1985 | new Pickup(playerStartPos.Clone().Add(new Vector2(16,0)), 2); 1986 | levelExit = new LevelExit(new Vector2(24,13)); 1987 | 1988 | if (warpLevel>1) 1989 | new LevelExit(new Vector2(29.5,8),2); // warp 1990 | if (localStorage.kbap_won) 1991 | new LevelExit(new Vector2(29.5,13),3); // speed run 1992 | } 1993 | else if (isFinalLevel) 1994 | { 1995 | // boss level 1996 | new Store(new Vector2(43,47)); 1997 | new ShieldEnemy(playerStartPos.Clone().Add(new Vector2(.5,0)), 1); 1998 | new Pickup(new Vector2(24,19.5), 2); 1999 | } 2000 | else 2001 | { 2002 | // spawn stores and special powerups 2003 | for(let x=levelMazeSize;x--;) 2004 | for(let y=levelMazeSize;y--;) 2005 | { 2006 | let m = levelMaze[x+y*levelMazeSize]; 2007 | if (m==2 && levelNumber > 1) 2008 | { 2009 | let p = new Vector2(x+.5,y+.5).Multiply(levelSize/levelMazeSize); 2010 | let d = p.Distance(playerStartPos); 2011 | if (d>30&&Rand()<.3) 2012 | { 2013 | // random powerup spawn 2014 | if (Rand()>.5) 2015 | new Pickup(p, 2); 2016 | else 2017 | new Boomerang(p); 2018 | 2019 | level.FillCircleType(p,RandIntBetween(2,4),1); 2020 | level.FillCircleObject(p,RandIntBetween(3,5),0); 2021 | } 2022 | else 2023 | new Store(p); 2024 | } 2025 | } 2026 | 2027 | new LevelExit(playerStartPos,1); 2028 | } 2029 | 2030 | // draw the level 2031 | level.ClearBorder(); 2032 | level.ApplyTiling(); 2033 | level.Redraw(); 2034 | 2035 | return 1; 2036 | } 2037 | 2038 | /////////////////////////////////////////////////////////////////////////////// 2039 | // level transition system 2040 | 2041 | let transitionTimer = new Timer(); 2042 | let transitionCanvas = c2; 2043 | let transitionCanvasContext = transitionCanvas.getContext('2d'); 2044 | function StartTransiton() 2045 | { 2046 | // copy main canvas to transition canvas 2047 | transitionTimer.Set(); 2048 | transitionCanvas.width = mainCanvasSize.x; 2049 | transitionCanvas.height = mainCanvasSize.y; 2050 | transitionCanvasContext.drawImage(mainCanvas,0,0); 2051 | } 2052 | 2053 | function UpdateTransiton() 2054 | { 2055 | let transitionTime = transitionTimer.Get(); 2056 | if (transitionTime > 2) 2057 | return; 2058 | 2059 | // render stored main canvas with circle transition effect 2060 | mainCanvasContext.save(); 2061 | mainCanvasContext.beginPath(); 2062 | let r = transitionTime*mainCanvasSize.x/2; 2063 | mainCanvasContext.rect(0,0,mainCanvasSize.x,mainCanvasSize.y); 2064 | mainCanvasContext.arc(mainCanvasSize.x/2,mainCanvasSize.y/2,r,0,7); 2065 | mainCanvasContext.clip('evenodd'); 2066 | mainCanvasContext.drawImage(transitionCanvas,0,0); 2067 | mainCanvasContext.restore(); 2068 | } 2069 | 2070 | /////////////////////////////////////////////////////////////////////////////// 2071 | // ZzFXmicro - Zuper Zmall Zound Zynth - MIT License - Copyright 2019 Frank Force 2072 | let zzfx_v=.2; 2073 | let zzfx_x=0; 2074 | let zzfx=(e,f,a,b=1,d=.1,g=0,h=0,k=0,l=0)=>{if(!zzfx_x)return;let S=44100;a*=2*PI/S;a*=1+RandBetween(-f,f);g*=1E3*PI/(S**2);b=S*b|0;d=d*b|0;k*=2*PI/S;l*=PI;f=[];for(let m=0,n=0,c=0;c15 && (!(beatCount&1) || RandInt(2))) 2103 | { 2104 | if (beatCount%8==0) 2105 | lastNote = 1; // return to root note every 8 beats 2106 | 2107 | // play the note 2108 | zzfx(.4,0,220*2**(scale[lastNote]/12), (RandInt(2)+1)/2, .05, 0, .4); 2109 | 2110 | // random walk to another note in the scale 2111 | lastNote = (lastNote + (RandInt(6)-2)+scale.length)%scale.length; 2112 | } 2113 | 2114 | // precussion 2115 | if (beatCount%2==0||beatCount&18||!RandInt(20)) 2116 | { 2117 | if (beatCount%4==0) 2118 | zzfx(.3,.2,1e3,.08,.05,.8,21,51); // ZzFX highhat 2119 | else 2120 | zzfx(.8,.2,150,.04,.002,.1,1,.5,.15); // ZzFX 17553 kick 2121 | } 2122 | } 2123 | } 2124 | 2125 | function PlaySound(sound, p=0) 2126 | { 2127 | switch(sound) 2128 | { 2129 | case 1: // player hit 2130 | zzfx(1,.1,4504,.3,.1,-30,.5,.5,.33); // ZzFX 36695 2131 | break; 2132 | 2133 | case 2: // player die 2134 | zzfx(.7,0,500,4,.01,-0.2,3,3,0); // ZzFX 23250 2135 | break; 2136 | 2137 | case 3: // get heart 2138 | zzfx(1,0,1504,.3,.17,1.7,.5,.4,.33); // ZzFX 36695 2139 | break; 2140 | 2141 | case 4: // get heart container 2142 | zzfx(1,0,805,1.1,.71,.5,1.5,.5); // ZzFX 16886 2143 | break; 2144 | 2145 | case 5: // boomerang cut 2146 | zzfx(.3,.2,370,.2,.1,3.9,13,27,.12); // ZzFX 23473 2147 | break; 2148 | 2149 | case 6: // boomerang catch 2150 | zzfx(1,.1,0,.2,.23,2,.4,.6,.9); // ZzFX 20183 2151 | break; 2152 | 2153 | case 7: // boomerang throw 2154 | zzfx(1,.1,53,.2,.26,0,.1,7.5,.58); // ZzFX 24904 2155 | break; 2156 | 2157 | case 8: // enemy hit 2158 | zzfx(1,.2,370,.1,.23,4.5,2.8,27.4,.12); // ZzFX 23473 2159 | break; 2160 | 2161 | case 9: // enemy kill 2162 | zzfx(1,.1,1138,.2,.02,0,4,1.2,.1); // ZzFX 10015 2163 | break; 2164 | 2165 | case 10: // coin 2166 | if (!coinSoundTimer.IsSet()) 2167 | coinSoundTimer.Set(.05); // trigger coin sound to play again 2168 | zzfx(1,.01,800+p,.2,.05); // ZzFX 98600 2169 | break; 2170 | 2171 | case 11: // low health 2172 | zzfx(.4,.1,418,.1,.79,5,0,1.9,.74); // ZzFX 7364 2173 | break; 2174 | 2175 | case 12: // dash 2176 | zzfx(1,.1,319,.4,.08,6.6,3.2,2.6,.59); // ZzFX 79527 2177 | break; 2178 | 2179 | case 13: // teleport 2180 | zzfx(1,.1,7,1,.97,0,.6,21.7,.5); // ZzFX 60532 2181 | break; 2182 | 2183 | case 14: // boomerang hit solid 2184 | zzfx(.8,.1,70,.1,.23,4.5,2.8,27,.12); // ZzFX 23473 2185 | break; 2186 | 2187 | case 15: // boomerang reflect 2188 | zzfx(1,.1,800,.2,.02,-0.3); // ZzFX 14772 2189 | break; 2190 | 2191 | case 16: // dodge recharge 2192 | zzfx(1,.1,0,.1,.1,1,.1,100); // ZzFX 88949 2193 | break; 2194 | } 2195 | } 2196 | 2197 | // load texture and kick off init! 2198 | let tileImage = new Image(); 2199 | tileImage.onload=_=>Init(); 2200 | tileImage.src = 'tiles.png'; -------------------------------------------------------------------------------- /gameEngine.js: -------------------------------------------------------------------------------- 1 | /* 2 | Bounce Back ~ A boomerang roguelike for JS13k 3 | Copyright (C) 2019 Frank Force 4 | 5 | This program is free software; you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation; either version 2 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | */ 15 | /* 16 | Javascript Game Engine 17 | By Frank Force 2019 18 | 19 | Engine Features 20 | - Engine is separate from game code, I kept it super simple. 21 | - Object oriented system with base class game object. 22 | - Base class object handles physics, collision, rendering, shadows, etc. 23 | - Objects collide with level tiles and can bounce off. 24 | - Engine helper classes and functions like Vector2, Color, and Timer. 25 | - Level is composed of a grid of tiles that can optionally have objects on them (bushes/rocks) 26 | - Automatically tiles level based on what is there. 27 | - Level & ground is cached to offscreen buffer, so all the level, trees, blood splats is only 1 draw call. 28 | - Sound effects audio with my tiny sound lib zzfx. 29 | - Input processing system. 30 | - A simple particle effect system. 31 | */ 32 | 33 | "use strict"; // strict mode 34 | /////////////////////////////////////////////////////////////////////////////// 35 | // config 36 | 37 | let godMode = 0; 38 | let debug = 0; 39 | let soundEnable = 1; 40 | let debugCollision = 0; 41 | 42 | /////////////////////////////////////////////////////////////////////////////// 43 | // helper functions 44 | 45 | let RGBA = (r=0,g=0,b=0,a=1)=>(`rgba(${r*255|0},${g*255|0},${b*255|0},${a})`); 46 | let PI = Math.PI; 47 | let Rand = (m=1)=>Math.random()*m; 48 | let RandInt = m=>Rand(m)|0; 49 | let RandBetween = (a,b)=>a+Rand(b-a); 50 | let RandIntBetween = (a,b)=>a+RandInt(b-a+1); 51 | let RandVector = (scale=1)=> (new Vector2(scale,0)).Rotate(Rand(2*PI)); 52 | let RandColorBetween = (c1,c2)=> c1.Clone().Lerp(c2,Rand()); 53 | let IsArrayValid = (x,y,size)=> (x>=0 && y>=0 && x < size && y < size); 54 | 55 | let Min=(a, b)=> (a (a>b)? a : b; 57 | let Clamp=(v, min, max)=> Min(Max(v, min), max); 58 | let Percent=(v, a, b)=> (a==b)? 0 : Clamp((v-a)/(b-a), 0, 1); 59 | let Lerp=(p, a, b)=> a + Clamp(p, 0, 1) * (b-a); 60 | let FormatTime=(t)=> 61 | { 62 | let s = (t|0)%60; 63 | return (t/60|0)+':'+(s<10?'0':'')+s; 64 | } 65 | 66 | class Timer 67 | { 68 | constructor() { this.endTime=0; } 69 | Set(timeLeft=0) { this.endTime = time + timeLeft; } 70 | Get() { return this.IsSet()? time - this.endTime : 1e9; } 71 | IsSet() { return this.endTime > 0; } 72 | UnSet() { this.endTime = 0; } 73 | Elapsed() { return !this.IsSet() || time > this.endTime; } 74 | } 75 | 76 | class Vector2 77 | { 78 | constructor(x=0, y=0) { this.x = x; this.y = y; } 79 | Copy(v) { this.x = v.x; this.y = v.y; return this; } 80 | Clone(s=1) { return (new Vector2(this.x, this.y)).Multiply(s); } 81 | Add(v) { (v instanceof Vector2)? (this.x += v.x, this.y += v.y) : (this.x += v, this.y += v); return this; } 82 | Subtract(v) { (this.x -= v.x, this.y -= v.y) ; return this; } 83 | Multiply(v) { (v instanceof Vector2)? (this.x *= v.x, this.y *= v.y) : (this.x *= v, this.y *= v); return this; } 84 | Set(x, y) { this.x = x; this.y = y; return this; } 85 | AddXY(x, y) { this.x += x; this.y += y; return this; } 86 | Normalize(scale=1) { let l = this.Length(); return l > 0 ? this.Multiply(scale/l) : this.Set(scale,y=0); } 87 | ClampLength(length) { let l = this.Length(); return l > length ? this.Multiply(length/l) : this; } 88 | Rotate(a) { let c=Math.cos(a);let s=Math.sin(a);return this.Set(this.x*c - this.y*s,this.x*s - this.y*c); } 89 | Round() { this.x = Math.round(this.x); this.y = Math.round(this.y); return this; } 90 | Length() { return Math.hypot(this.x, this.y ); } 91 | Distance(v) { return Math.hypot(this.x - v.x, this.y - v.y ); } 92 | Angle() { return Math.atan2(this.y, this.x); }; 93 | Rotation() { return (Math.abs(this.x)>Math.abs(this.y))?(this.x>0?2:0):(this.y>0?1:3); } 94 | Lerp(v,p) { return this.Add(v.Clone().Subtract(this).Multiply(p)); } 95 | DotProduct(v) { return this.x*v.x+this.y*v.y; } 96 | } 97 | 98 | class Color 99 | { 100 | constructor(r=0,g=0,b=0,a=1) { this.r=r;this.g=g;this.b=b;this.a=a; } 101 | Copy(c) { this.r=c.r;this.g=c.g;this.b=c.b;this.a=c.a; return this; } 102 | Clone(s=1) { return new Color(this.r*s, this.g*s, this.b*s, this.a*s); } 103 | //Add(c) { this.r+=c.r;this.g+=c.g;this.b+=c.b;this.a+=c.a; return this; } 104 | Subtract(c) { this.r-=c.r;this.g-=c.g;this.b-=c.b;this.a-=c.a; return this; } 105 | //Multiply(c) { (c instanceof Color)? (this.r*=c.r,this.g*=c.g,this.b*=c.b,this.a*=c.a) : (this.r*=c,this.g*=c,this.b*=c,this.a*=c); return this; } 106 | SetAlpha(a) { this.a=a; return this; } 107 | Lerp(c,p) { return c.Clone().Subtract(c.Clone().Subtract(this).Clone(1-p)); } 108 | RGBA() { return RGBA(this.r, this.g, this.b, this.a); } 109 | } 110 | 111 | /////////////////////////////////////////////////////////////////////////////// 112 | // game object 113 | 114 | class GameObject 115 | { 116 | constructor(pos,tileX,tileY,size=.5,collisionSize=0,health=1) 117 | { 118 | this.pos = pos.Clone(); 119 | this.tileX = tileX; 120 | this.tileY = tileY; 121 | this.size = new Vector2(size,size); 122 | this.collisionSize = collisionSize; 123 | this.health = health; 124 | this.healthMax = health; 125 | this.damageTimer = new Timer(); 126 | this.lifeTimer = new Timer(); 127 | this.lifeTimer.Set(); 128 | this.velocity = new Vector2(); 129 | this.angle = 0; 130 | this.angleVelocity = 0; 131 | this.damping = .8; 132 | this.mirror = 0; 133 | this.height = 0; 134 | this.damageFlashTime = .5; 135 | this.differenceFlash = 1; 136 | 137 | gameObjects.push(this); 138 | } 139 | 140 | Update() 141 | { 142 | // apply velocity 143 | let oldPos = this.pos; 144 | let newPos = this.pos.Clone(); 145 | newPos.Add(this.velocity); 146 | 147 | // check collision 148 | let size = this.collisionSize; 149 | let clear = level.IsAreaClear(newPos,size,this); 150 | if (!clear) 151 | { 152 | // test which side we bounced off of (or both) 153 | let isClearX = level.IsAreaClear(new Vector2(newPos.x,oldPos.y),size); 154 | let isClearY = level.IsAreaClear(new Vector2(oldPos.x,newPos.y),size); 155 | if (!isClearX || isClearY) 156 | { 157 | newPos.x = oldPos.x; 158 | this.velocity.x *= -.5; 159 | } 160 | if (!isClearY || isClearX) 161 | { 162 | newPos.y = oldPos.y; 163 | this.velocity.y *= -.5; 164 | } 165 | } 166 | this.pos = newPos; 167 | 168 | // apply physics 169 | this.velocity.Multiply(this.damping); 170 | this.angle += this.angleVelocity; 171 | 172 | if (debugCollision) 173 | DebugRect(this.pos,new Vector2(this.collisionSize,this.collisionSize),'#F00'); 174 | } 175 | 176 | Render() { DrawTile(this.pos,this.size,this.tileX,this.tileY,this.angle,this.mirror,this.height);} 177 | 178 | Heal(health) 179 | { 180 | if (this.IsDead()) 181 | return 0; 182 | 183 | // apply healing 184 | let startHealth = this.health; 185 | this.health = Min(this.health+health,this.healthMax); 186 | return this.health - startHealth; 187 | } 188 | 189 | Damage(damage) 190 | { 191 | if (this.IsDead() || this.GetDamageTime() < .5) 192 | return 0; 193 | 194 | // apply damage 195 | this.damageTimer.Set(); 196 | let startHealth = this.health; 197 | this.health = Max(this.health-damage,0); 198 | if (!this.health) 199 | this.Kill(); 200 | 201 | return startHealth - this.health; 202 | } 203 | 204 | ReflectDamage(direction){ return 0; } 205 | GetLifeTime() { return this.lifeTimer.Get(); } 206 | GetDamageTime() { return this.damageTimer.Get(); } 207 | GetDamageFlashPercent() { return Clamp(1- this.GetDamageTime()/this.damageFlashTime,0,1); } 208 | IsTouching(object) { return this.Distance(object) < object.collisionSize + this.collisionSize; } 209 | IsDead() { return !this.health; } 210 | Kill() { this.health = 0; this.Destroy(); } 211 | Destroy() { gameObjects.splice(gameObjects.indexOf(this), 1); } 212 | Distance(object) 213 | { 214 | // get distance between objects accounting for height 215 | let p1 = this.pos; let p2 = object.pos; 216 | return Math.hypot(p1.x - p2.x, p1.y - p2.y, this.height - object.height); 217 | } 218 | CollideLevel(data,pos) 219 | { 220 | if (!data.type) 221 | return 1; 222 | 223 | // allow jumping over objects 224 | if (this.height > 1) 225 | return 0; 226 | 227 | return data.object; 228 | } 229 | } 230 | 231 | /////////////////////////////////////////////////////////////////////////////// 232 | // core engine 233 | 234 | let cameraScale = 1; 235 | let cameraPos = new Vector2(); 236 | let frame = 0; 237 | let time = 1; 238 | let paused = 0; 239 | let timeDelta = 1/60; 240 | let shadowRenderPass = 0; 241 | let hitRenderPass = 0; 242 | let mainCanvasContext; 243 | let tileMaskCanvas; 244 | let tileMaskCanvasContext; 245 | let hitCanvas; 246 | let hitCanvasContext; 247 | let levelCanvas; 248 | let levelCanvasContext; 249 | let mainCanvasSize = new Vector2(); 250 | let tileSize = 16; 251 | let levelSize = 64; 252 | 253 | function EngineInit() 254 | { 255 | // set the main canvas size to half size of the window 256 | mainCanvasContext = mainCanvas.getContext('2d'); 257 | mainCanvasSize.Set(window.innerWidth/2|0,window.innerHeight/2|0); 258 | mainCanvas.width = mainCanvasSize.x; 259 | mainCanvas.height = mainCanvasSize.y; 260 | 261 | // create level canvas to cache level image and groun effects 262 | levelCanvas = document.createElement('canvas'); 263 | levelCanvasContext = levelCanvas.getContext('2d'); 264 | levelCanvas.display='none'; 265 | levelCanvasContext.imageSmoothingEnabled = 0; 266 | 267 | // crate tile mask used for shadows and hit effects 268 | tileMaskCanvas = document.createElement('canvas'); 269 | tileMaskCanvasContext = tileMaskCanvas.getContext('2d'); 270 | tileMaskCanvas.display='none'; 271 | tileMaskCanvas.width = tileImage.width*2; 272 | tileMaskCanvas.height = tileImage.height; 273 | 274 | // draw white mask sprites 275 | tileMaskCanvasContext.fillStyle='#FFF'; 276 | tileMaskCanvasContext.fillRect(0,0,tileMaskCanvas.width,tileMaskCanvas.height); 277 | tileMaskCanvasContext.globalCompositeOperation = 'destination-atop'; 278 | tileMaskCanvasContext.drawImage(tileImage,0,0); 279 | 280 | // draw black mask sprites 281 | tileMaskCanvasContext.globalCompositeOperation = 'source-over'; 282 | tileMaskCanvasContext.drawImage(tileMaskCanvas,tileImage.width,0); 283 | tileMaskCanvasContext.globalCompositeOperation = 'difference'; 284 | tileMaskCanvasContext.drawImage(tileMaskCanvas,tileImage.width,0); 285 | 286 | InitDebug(); 287 | } 288 | 289 | function EngineUpdate() 290 | { 291 | paused = !debug && !document.hasFocus() 292 | if (paused) 293 | { 294 | // prevent stuck input if focus is lost 295 | mouseIsDown = mouseWasDown = 0; 296 | keyInputData.map(k=>k.wasDown=k.isDown=0); 297 | } 298 | 299 | // fit canvas to window 300 | mainCanvasSize.Set(window.innerWidth/2,window.innerHeight/2); 301 | mainCanvas.width = mainCanvasSize.x; 302 | mainCanvas.height = mainCanvasSize.y; 303 | mainCanvasContext.imageSmoothingEnabled = 0; 304 | 305 | // get mouse world pos 306 | mousePosWorld.Copy(mousePos).Subtract(mainCanvasSize.Clone(.5)).Multiply(1/cameraScale*tileSize).Add(cameraPos); 307 | 308 | // main update 309 | if (!paused) 310 | { 311 | // debug speed up / slow down 312 | let frames = 1; 313 | if (debug && KeyIsDown(107)) 314 | frames = 4; 315 | if (debug && KeyIsDown(109)) 316 | frames = (debugFrame%4==0); 317 | while(frames--) 318 | { 319 | time = 1+ ++frame * timeDelta 320 | Update(); 321 | UpdateGameObjects(); 322 | } 323 | } 324 | 325 | // main render 326 | let SortGameObjects = (a,b)=> a.pos.y-b.pos.y; 327 | gameObjects.sort(SortGameObjects); 328 | PreRender(); 329 | shadowRenderPass = 1; 330 | RenderGameObjects(); 331 | shadowRenderPass = 0; 332 | RenderGameObjects(); 333 | PostRender(); 334 | UpdateDebug(); 335 | 336 | // clear input 337 | mouseWasDown = mouseIsDown; 338 | keyInputData.map(k=>k.wasDown=k.isDown); 339 | requestAnimationFrame(EngineUpdate); 340 | } 341 | 342 | /////////////////////////////////////////////////////////////////////////////// 343 | // game object system 344 | 345 | let gameObjects = []; 346 | function ClearGameObjects() { gameObjects = []; } 347 | function UpdateGameObjects() { gameObjects.forEach(o=>o.Update()); } 348 | function RenderGameObjects() 349 | { 350 | gameObjects.forEach(o=> 351 | { 352 | o.Render(); 353 | if (!shadowRenderPass) 354 | { 355 | // draw the hit flash overlay 356 | hitRenderPass = o.GetDamageFlashPercent(); 357 | if (hitRenderPass) 358 | { 359 | if (o.differenceFlash) 360 | mainCanvasContext.globalCompositeOperation = 'difference'; 361 | o.Render(); 362 | mainCanvasContext.globalCompositeOperation = 'source-over'; 363 | hitRenderPass = 0; 364 | } 365 | } 366 | }); 367 | } 368 | 369 | /////////////////////////////////////////////////////////////////////////////// 370 | // input 371 | 372 | let mouseIsDown = 0; 373 | let mouseWasDown = 0; 374 | let keyInputData = []; 375 | let mousePos = new Vector2(); 376 | let mousePosWorld = new Vector2(); 377 | 378 | oncontextmenu = function(e) { e.preventDefault(); } 379 | onmousedown = function(e) { mouseIsDown=1; } 380 | onmouseup = function(e) { mouseIsDown=0; } 381 | onmousemove = function(e) 382 | { 383 | // convert mouse pos to canvas space 384 | let rect = mainCanvas.getBoundingClientRect(); 385 | mousePos.Set 386 | ( 387 | (e.clientX - rect.left) / rect.width, 388 | (e.clientY - rect.top) / rect.height 389 | ).Multiply(mainCanvasSize); 390 | } 391 | onkeydown = function(e) 392 | { 393 | if (debug && e.keyCode==192) 394 | e.preventDefault(),ToggleDebugConsole(); 395 | if (debug && document.activeElement && document.activeElement.type == 'textarea') 396 | return; 397 | 398 | keyInputData[e.keyCode]={isDown:1}; 399 | } 400 | onkeyup = function(e) 401 | { 402 | if (debug && document.activeElement && document.activeElement.type == 'textarea') 403 | return; 404 | 405 | if ( keyInputData[e.keyCode] ) keyInputData[e.keyCode].isDown=0; 406 | } 407 | 408 | function MouseWasPressed() { return mouseIsDown && !mouseWasDown; } 409 | function KeyIsDown(key) { return keyInputData[key]? keyInputData[key].isDown : 0; } 410 | function KeyWasPressed(key) { return KeyIsDown(key) && !keyInputData[key].wasDown; } 411 | function ClearInput() { keyInputData.map(k=>k.wasDown=k.isDown=0);mouseIsDown=mouseWasDown=0; } 412 | 413 | /////////////////////////////////////////////////////////////////////////////// 414 | // rendering 415 | 416 | // shadow settings 417 | let shadowAlpha = .5; 418 | let shadowSkew = .7; 419 | let shadowScale = .7; 420 | 421 | function DrawScreenTile(x,y,size,tileX,tileY) 422 | { 423 | mainCanvasContext.drawImage(tileImage,tileX*tileSize,tileY*tileSize,tileSize,tileSize, x-size, y-size, 2*size, 2*size); 424 | } 425 | 426 | function SetCanvasTransform(pos,size,angle=0,height=0) 427 | { 428 | // create canvas transform from world space to screen space 429 | mainCanvasContext.save(); 430 | let drawPos = pos.Clone(); 431 | if (shadowRenderPass) 432 | drawPos.AddXY(-height*shadowSkew/2, -height*shadowScale/2); 433 | else 434 | drawPos.y -= height; 435 | drawPos.Subtract(cameraPos).Multiply(tileSize*cameraScale); 436 | drawPos.Add(mainCanvasSize.Clone(.5)); 437 | mainCanvasContext.translate(drawPos.x|0, drawPos.y|0); 438 | 439 | let s = size.Clone(tileSize); 440 | if (shadowRenderPass) 441 | mainCanvasContext.transform(1,0,shadowSkew,shadowScale,-shadowSkew*cameraScale*s.x,cameraScale*(1-shadowScale)*s.y); 442 | if (angle) 443 | mainCanvasContext.rotate(angle); 444 | mainCanvasContext.scale(cameraScale,cameraScale); 445 | } 446 | 447 | function DrawTile(pos,size,tileX,tileY,angle=0,mirror=0,height=0) 448 | { 449 | // render a tile at a world space position 450 | SetCanvasTransform(pos,size,angle,height); 451 | 452 | let image = tileImage; 453 | if (shadowRenderPass) 454 | { 455 | image = tileMaskCanvas; 456 | mainCanvasContext.globalAlpha *= shadowAlpha; 457 | tileX+=tileImage.width/tileSize; // shift over to shadow position 458 | } 459 | else if (hitRenderPass) 460 | { 461 | image = tileMaskCanvas; 462 | mainCanvasContext.globalAlpha *= hitRenderPass; 463 | } 464 | 465 | // shrink size of tile to fix bleeding on edges 466 | let renderTileShrink = .25; 467 | 468 | /// render the tile 469 | let s = size.Clone(2*tileSize); 470 | mainCanvasContext.scale(mirror?-s.x:s.x,s.y); 471 | mainCanvasContext.drawImage(image, 472 | tileX*tileSize+renderTileShrink, 473 | tileY*tileSize+renderTileShrink, 474 | tileSize-2*renderTileShrink, 475 | tileSize-2*renderTileShrink, -.5, -.5, 1, 1); 476 | mainCanvasContext.restore(); 477 | mainCanvasContext.globalAlpha = 1; 478 | } 479 | 480 | function DrawText(text, x, y, size,textAlign='center',lineWidth=1,color='#000',strokeColor='#FFF',context=mainCanvasContext) 481 | { 482 | context.fillStyle=color; 483 | context.font = `900 ${size}px arial` 484 | context.textAlign=textAlign; 485 | context.textBaseline='middle'; 486 | context.fillText(text,x,y); 487 | if (lineWidth) 488 | { 489 | context.lineWidth=lineWidth; 490 | context.strokeStyle=strokeColor; 491 | context.strokeText(text,x,y); 492 | } 493 | } 494 | 495 | /////////////////////////////////////////////////////////////////////////////// 496 | // tile level system 497 | 498 | class LevelData 499 | { 500 | constructor() 501 | { 502 | 503 | this.type = 0; // 0=solid, 1=grass, 2=sand 504 | this.object = 0; // 0=none, 1=bush, 2=rock 505 | this.tile = 0; 506 | this.rotation = 0; 507 | } 508 | 509 | Clear() { this.type=this.object=0; } 510 | IsSolid() { return !this.type || this.object; } 511 | } 512 | 513 | class Level 514 | { 515 | constructor() 516 | { 517 | levelCanvas.height=levelCanvas.width=levelSize*tileSize; 518 | this.data = []; 519 | for(let i = 0; i x+size) 555 | xo = x+size; 556 | } 557 | 558 | if (yo==y+size) 559 | break; 560 | ++yo; 561 | if (yo > y+size) 562 | yo = y+size; 563 | } 564 | return 1; 565 | } 566 | 567 | FillCircleObject(pos,r,object) { this.FillCircleCallback(pos,r,d=>d.object=d.type?object:d.object); } 568 | FillCircleType(pos,r,type) { this.FillCircleCallback(pos,r,d=>d.type=type); } 569 | 570 | FillCircleCallback(pos,r,callback) 571 | { 572 | // fill a circle of tiles using the provided callback 573 | for(let i=-r;i<=r;i++) 574 | { 575 | let h = (r**2-(i+.5)**2)**.5; 576 | for(let j=pos.y-h;j=2) 610 | t++; 611 | if (neighbors==1) 612 | { 613 | if (dl) r = 1; 614 | else if (du) r = 2; 615 | else if (dr) r = 3; 616 | } 617 | else if (neighbors==2) 618 | { 619 | if (dr && dl) t--, r = 1; 620 | else if (du && dd) t--; 621 | else if (dl && dd) r = 1; 622 | else if (dl && du) r = 2; 623 | else if (dr && du) r = 3; 624 | } 625 | else if (neighbors==3) 626 | { 627 | if (!dr) r = 1; 628 | else if (!dd) r = 2; 629 | else if (!dl) r = 3; 630 | } 631 | } 632 | 633 | d.tile = t; 634 | d.rotation = r; 635 | } 636 | 637 | // add tile randomization 638 | for(let y = 0; y .05) 642 | continue; 643 | 644 | let d = level.GetData(x,y); 645 | if (d.tile == 13) 646 | { 647 | d.tile++; 648 | d.rotation=RandInt(4); 649 | } 650 | if (d.tile == 16) 651 | { 652 | d.tile+=1; 653 | d.rotation=RandInt(4); 654 | } 655 | } 656 | } 657 | 658 | ClearBorder() 659 | { 660 | // set to solid around outside edge 661 | let w = levelSize; 662 | for(let i = 0; i this.lifeTime) 763 | this.emitter.particles.splice(this.emitter.particles.indexOf(this),1); 764 | 765 | if (debugCollision) 766 | DebugRect(this.pos, new Vector2(this.size,this.size), '#0FF'); 767 | } 768 | 769 | Render() 770 | { 771 | // get the color 772 | let p = Percent(this.lifeTimer.Get(), 0, this.lifeTime); 773 | let c = this.startColor.Clone().Lerp(this.endColor, p); 774 | c.a *= p<.1? p /.1 : 1; // fade in alpha 775 | mainCanvasContext.fillStyle=c.RGBA(); 776 | 777 | // get the size 778 | let size = this.size * cameraScale * tileSize * Lerp(p,1,.5); 779 | 780 | // get the screen pos and render 781 | let pos = this.pos.Clone() 782 | .Subtract(cameraPos) 783 | .Multiply(tileSize*cameraScale) 784 | .Add(mainCanvasSize.Clone(.5)) 785 | .Add(-size); 786 | mainCanvasContext.fillRect(pos.x, pos.y, 2*size, 2*size); 787 | } 788 | } 789 | 790 | class ParticleEmitter extends GameObject 791 | { 792 | constructor( pos, emitSize, particleSize, color1, color2 ) 793 | { 794 | super(pos,0,0,emitSize); 795 | this.particleSize=particleSize; 796 | this.color1=color1.Clone(); 797 | this.color2=color2.Clone(); 798 | this.particles=[]; 799 | this.emitTimeBuffer=0; 800 | } 801 | 802 | Update() 803 | { 804 | // update particles 805 | this.particles.forEach(particle=>particle.Update()); 806 | 807 | if (this.GetLifeTime() <= .05) 808 | { 809 | // emit new particles 810 | let secondsPerEmit = 1/200; 811 | this.emitTimeBuffer += timeDelta; 812 | while (this.emitTimeBuffer > secondsPerEmit) 813 | { 814 | this.emitTimeBuffer -= secondsPerEmit; 815 | this.AddParticle(); 816 | } 817 | } 818 | else if (!this.particles.length) 819 | { 820 | // go away when all particles are gone 821 | this.Destroy(); 822 | } 823 | 824 | if (debugCollision) 825 | DebugRect(this.pos, new Vector2(this.size,this.size), '#00F'); 826 | 827 | super.Update(); 828 | } 829 | 830 | Render() { this.particles.forEach(p=>p.Render()); } 831 | 832 | AddParticle() 833 | { 834 | // create a new particle with random settings 835 | this.particles.push 836 | ( 837 | new Particle 838 | ( 839 | this, 840 | this.pos.Clone().Add(RandVector(Rand(this.size.x))), 841 | RandVector(Rand(.2)), 842 | RandBetween(this.particleSize,2*this.particleSize), 843 | RandBetween(.5,1), 844 | RandColorBetween(this.color1,this.color2), 845 | RandColorBetween(this.color1,this.color2).SetAlpha(0) 846 | ) 847 | ); 848 | } 849 | } 850 | -------------------------------------------------------------------------------- /gameEngineDebug.js: -------------------------------------------------------------------------------- 1 | /* 2 | Bounce Back ~ A boomerang roguelike for JS13k 3 | Copyright (C) 2019 Frank Force 4 | 5 | This program is free software; you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation; either version 2 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | */ 15 | /* 16 | Javascript Game Engine Debug 17 | By Frank Force 2019 18 | 19 | Debug Features 20 | - debug console 21 | - debug rendering 22 | - debug controls 23 | - save snapshot 24 | */ 25 | 26 | "use strict"; // strict mode 27 | /////////////////////////////////////////////////////////////////////////////// 28 | // debug 29 | 30 | let debugFrame = 0; 31 | let debugConsole = 0; 32 | let debugRects=[]; 33 | let debugConsoleTextArea; 34 | let debugConsoleDisplayTextArea; 35 | let downloadLink; 36 | let debugCanvas = 0; 37 | 38 | function DebugPrint(string) 39 | { 40 | let o = '-> '+string + '\n'; 41 | o += debugConsoleDisplayTextArea.value; 42 | debugConsoleDisplayTextArea.value = o; 43 | } 44 | 45 | function ToggleDebugConsole() 46 | { 47 | debugConsole = !debugConsole; 48 | if (debugConsole) 49 | { 50 | debugConsoleTextArea.style.display='block'; 51 | debugConsoleTextArea.focus(); 52 | } 53 | else 54 | mainCanvas.focus(); 55 | } 56 | 57 | function DebugConsoleKeyDown(e) 58 | { 59 | if (e.keyCode != 13) 60 | return; 61 | 62 | e.preventDefault(); 63 | let v = debugConsoleTextArea.value; 64 | let o = '-> eval('+v + ')\n'; 65 | 66 | try { o += eval(v); } 67 | catch (e) 68 | { 69 | if (e instanceof Error) 70 | o += e.message? 'Error: ' + e.message : e; 71 | else 72 | o += 'Unknown error'; 73 | } 74 | 75 | o += '\n\n' + debugConsoleDisplayTextArea.value; 76 | 77 | debugConsoleTextArea.value = ''; 78 | debugConsoleDisplayTextArea.value = o; 79 | } 80 | 81 | function InitDebug() 82 | { 83 | debugConsoleTextArea = document.createElement('textarea'); 84 | debugConsoleTextArea.style="height:30px;width:90%;display:none;color:#FFF;background-color:#000;" 85 | mainCanvas.before(debugConsoleTextArea); 86 | debugConsoleTextArea.onkeydown=e=>DebugConsoleKeyDown(e) 87 | 88 | debugConsoleDisplayTextArea = document.createElement('textarea'); 89 | debugConsoleDisplayTextArea.style="height:100px;width:90%;display:none;color:#FFF;background-color:#000;" 90 | mainCanvas.before(debugConsoleDisplayTextArea); 91 | 92 | downloadLink = document.createElement('a'); 93 | downloadLink.display='none'; 94 | downloadLink.before(debugConsoleTextArea); 95 | 96 | if (debugCanvas) 97 | { 98 | document.body.style.overflow='visible'; 99 | document.body.style.background='#400' 100 | mainCanvas.style.border='2px solid #F00'; 101 | mainCanvas.style.width=mainCanvas.width+'px'; 102 | mainCanvas.style.height=mainCanvas.height+'px'; 103 | 104 | document.body.appendChild(levelCanvas); 105 | levelCanvas.style.border='2px solid #F00'; 106 | levelCanvas.style.width=(levelCanvas.width)+'px'; 107 | levelCanvas.style.height=(levelCanvas.height)+'px'; 108 | levelCanvas.style.display ='block'; 109 | 110 | document.body.appendChild(tileMaskCanvas); 111 | tileMaskCanvas.style.border='2px solid #F00'; 112 | tileMaskCanvas.style.width=(tileMaskCanvas.width)+'px'; 113 | tileMaskCanvas.style.height=(tileMaskCanvas.height)+'px'; 114 | tileMaskCanvas.style.display ='block'; 115 | } 116 | } 117 | 118 | function DebugRect(pos,size,color="#F00") 119 | { 120 | if (debug) debugRects.push({pos:pos.Clone(),size:size.Clone(),color:color.slice(0)}); 121 | } 122 | 123 | function DebugPoint(pos,size=.1,color="#F00") 124 | { 125 | if (debug) DebugRect(pos, new Vector2(size,size), color) 126 | } 127 | 128 | function RenderDebugRects() 129 | { 130 | mainCanvasContext.lineWidth=1; 131 | function RenderDebugRect(pos,size,color) 132 | { 133 | size.Multiply(tileSize); 134 | let renderPos = pos.Clone(); 135 | renderPos.Subtract(cameraPos); 136 | renderPos.Multiply(tileSize); 137 | renderPos.Subtract(size); 138 | renderPos.Multiply(cameraScale); 139 | mainCanvasContext.strokeStyle=color; 140 | mainCanvasContext.save(); 141 | mainCanvasContext.translate(renderPos.x + mainCanvas.width/2|0, renderPos.y + mainCanvas.height/2|0); 142 | mainCanvasContext.scale(cameraScale, cameraScale); 143 | mainCanvasContext.strokeRect(0, 0, 2*size.x, 2*size.y); 144 | mainCanvasContext.restore(); 145 | } 146 | 147 | for( let d of debugRects ) 148 | RenderDebugRect(d.pos,d.size,d.color); 149 | debugRects = []; 150 | } 151 | 152 | function UpdateDebug() 153 | { 154 | ++debugFrame; 155 | UpdateDebugControls(); 156 | 157 | if (debugCollision) 158 | { 159 | let s = tileSize*level.size; 160 | DebugRect(new Vector2(s/2,s/2), new Vector2(s/2,s/2),'#F00'); 161 | } 162 | 163 | debugConsoleTextArea.style.display=debugConsole?'block':'none'; 164 | debugConsoleDisplayTextArea.style.display=debugConsole?'block':'none'; 165 | 166 | if (!debug) 167 | { 168 | debugRects = []; 169 | return; 170 | } 171 | RenderDebugRects(); 172 | } 173 | 174 | /////////////////////////////////////////////////////////////////////////////// 175 | // debug controls 176 | 177 | function UpdateDebugControls() 178 | { 179 | if (debug) 180 | { 181 | if (KeyWasPressed(50)) // 2 182 | SaveSnapshot(); 183 | if (KeyWasPressed(51)) // 3 184 | debugCollision = !debugCollision; 185 | if (KeyWasPressed(52)) // 4 186 | debugParticles = !debugParticles; 187 | if (KeyWasPressed(53)) // 5 188 | godMode = !godMode; 189 | if (KeyWasPressed(145)) // scroll lock 190 | debug = 0; 191 | if (KeyWasPressed(71)) // g 192 | godMode=1; 193 | } 194 | 195 | if (KeyIsDown(70)&&KeyIsDown(82)&&KeyIsDown(65)&&KeyIsDown(78)&&KeyIsDown(75)) 196 | { 197 | // dev mode - press all keys to spell "FRANK" 198 | if (!debug) 199 | PlaySound(4); 200 | debug = 1; 201 | } 202 | } 203 | 204 | function SaveSnapshot() 205 | { 206 | downloadLink.download="snapshot.png"; 207 | downloadLink.href=mainCanvas.toDataURL("image/png").replace("image/png", "image/octet-stream"); 208 | downloadLink.click(); 209 | } -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 16 | 39 | 40 | 41 | 42 | Bounce Back 43 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KilledByAPixel/BounceBack/395fd046338e6c5d5840d10e7fbd74132f90b8f6/screenshot.png -------------------------------------------------------------------------------- /tiles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KilledByAPixel/BounceBack/395fd046338e6c5d5840d10e7fbd74132f90b8f6/tiles.png --------------------------------------------------------------------------------