├── src ├── gfx │ ├── tiles.png │ └── tiles2.png ├── html │ └── index_dev.html ├── js │ ├── boulder.js │ ├── gunPistol.js │ ├── gunRifle.js │ ├── ammoBox.js │ ├── fx.js │ ├── mobGhost.js │ ├── blood.js │ ├── gunShotgun.js │ ├── gunMachinePistol.js │ ├── utilsLevel.js │ ├── corpse.js │ ├── sounds.js │ ├── mobZombie.js │ ├── mobVampire.js │ ├── mobBossZombie.js │ ├── pusher.js │ ├── bullet.js │ ├── music.js │ ├── mob_base.js │ ├── mobPlayer.js │ ├── DEFS.js │ ├── mob_enemy.js │ ├── ui.js │ ├── mapmanager.js │ ├── gun.js │ └── main.js └── maps │ ├── 2.json │ ├── 0.json │ └── 1.json ├── serveZip.sh ├── .prettierrc.json ├── tsconfig.json ├── package.json ├── .gitignore ├── LICENSE ├── .eslintrc.json ├── README.md └── GruntFile.js /src/gfx/tiles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sanojian/js13k_2022/HEAD/src/gfx/tiles.png -------------------------------------------------------------------------------- /src/gfx/tiles2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sanojian/js13k_2022/HEAD/src/gfx/tiles2.png -------------------------------------------------------------------------------- /serveZip.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | rm -rf ziptest 4 | mkdir ziptest 5 | cp dist/a.zip ziptest/ 6 | cd ziptest 7 | unzip -o a.zip 8 | http-server 9 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "requirePragma": true, 3 | "trailingComma": "es5", 4 | "semi": true, 5 | "singleQuote": false, 6 | "useTabs": true, 7 | "printWidth": 120 8 | } 9 | -------------------------------------------------------------------------------- /src/html/index_dev.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "dist/lib/engine.all.release.js", 4 | "src/js/**/*.js", 5 | "src/js/**/*.ts" 6 | ], 7 | "compilerOptions": { 8 | "pretty": false, 9 | "noEmit": true, 10 | "allowJs": true, 11 | "checkJs": true, 12 | "alwaysStrict": true, 13 | "lib": ["ES6", "DOM"], 14 | "target": "ES6" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "js13k_2022", 3 | "version": "0.0.1", 4 | "private": true, 5 | "devDependencies": { 6 | "google-closure-compiler": "*", 7 | "grunt": "*", 8 | "grunt-closure-tools": "*", 9 | "grunt-contrib-clean": "*", 10 | "grunt-contrib-concat": "*", 11 | "grunt-contrib-copy": "*", 12 | "grunt-contrib-watch": "*", 13 | "grunt-develop": "*", 14 | "grunt-http-server": "*", 15 | "grunt-image": "*", 16 | "matchdep": "*", 17 | "roadroller": "*" 18 | }, 19 | "dependencies": { 20 | "advzip-bin": "^2.0.0" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/js/boulder.js: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | class Boulder extends EngineObject { 4 | static destroyAllBoulders() { 5 | var c = 0; 6 | for (const o of engineObjects) { 7 | if (o instanceof Boulder) o.collideWithTile(); 8 | } 9 | } 10 | 11 | constructor(pos, tileIndex) { 12 | super(pos, vec2(1), tileIndex, TILE_SIZE, rand(-PI / 8, PI / 8), colorWhite); 13 | 14 | //vibrate(100); 15 | this.setCollision(true, true); 16 | this.isThrown = false; 17 | this.damping = 1; 18 | } 19 | 20 | collideWithTile() { 21 | fx_shakeScreen(0.5); 22 | //vibrate(200); 23 | 24 | soundBoulderDestroy.play(); 25 | makeParticles(this.pos, rand(0.4, 0.8), colorGrey); 26 | this.destroy(); 27 | return false; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/js/gunPistol.js: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | class Pistol extends Gun { 4 | constructor(pos) { 5 | super(pos, tileNumbers_pistol); 6 | // your object init code here 7 | this._distance = 0.7; 8 | this._speed = 0.4; 9 | 10 | this._maxAmmo = 6; 11 | this.ammo = this._maxAmmo; 12 | this._ammoIconTile = tileNumbers_bulletIcon; 13 | 14 | this.reloadTimePerBullet = 0.25; 15 | 16 | this._soundFire = soundPistol; 17 | } 18 | 19 | fire() { 20 | if (super.fire(colorBullet)) { 21 | let bullet = new Bullet(this.pos.copy(), 0, colorBullet, 28); 22 | bullet.velocity.x = Math.cos(-this.angle) * this._speed; 23 | bullet.velocity.y = Math.sin(-this.angle) * this._speed; 24 | } 25 | return true; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/js/gunRifle.js: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | class Rifle extends Gun { 4 | constructor(pos) { 5 | super(pos, tileNumbers_rifle); 6 | // your object init code here 7 | this._distance = 0.3; 8 | this._speed = 0.5; 9 | 10 | this._maxAmmo = 1; 11 | this._ammoIconTile = tileNumbers_rifleAmmoIcon; 12 | 13 | this.ammo = this._maxAmmo; 14 | this.reloadTimePerBullet = 2; 15 | 16 | this._soundFire = soundRifle; 17 | } 18 | 19 | fire() { 20 | if (super.fire(colorRifleRound)) { 21 | const penetration = 12; 22 | 23 | let bullet = new Bullet(this.pos.copy(), 0, colorRifleRound, 40, penetration); 24 | bullet.velocity.x = Math.cos(-this.angle) * this._speed; 25 | bullet.velocity.y = Math.sin(-this.angle) * this._speed; 26 | } 27 | return true; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/js/ammoBox.js: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | class AmmoBox extends EngineObject { 4 | static getCount() { 5 | var c = 0; 6 | for (const o of engineObjects) { 7 | if (o instanceof AmmoBox) c++; 8 | } 9 | 10 | return c; 11 | } 12 | 13 | constructor(pos, gunType) { 14 | super(pos, vec2(1), getAmmoForGunType(gunType), TILE_SIZE); 15 | 16 | this._hitbox = vec2(0.5); 17 | } 18 | 19 | update() { 20 | if (isOverlapping(this.pos, this._hitbox, g_player.pos, g_player._hitbox)) { 21 | if (this.tileIndex == tileNumbers_boxShells) { 22 | g_player.ammoShells += 12; 23 | } else if (this.tileIndex == tileNumbers_boxRifleAmmo) { 24 | g_player.ammoRifle += 6; 25 | } else { 26 | g_player.ammoBullets += 24; 27 | } 28 | 29 | soundPickup.play(this.pos); 30 | this.destroy(); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/js/fx.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | function fx_addSpark (pos) { 4 | makeParticles(pos, .1, colorSpark, 0.06) 5 | 6 | // for (let i = 0; i < 4; i++) { 7 | // g_sparks.push({ 8 | // pos: pos.copy(), 9 | // angle: rand(PI * 2), 10 | // life: 6, 11 | // }); 12 | // } 13 | 14 | } 15 | 16 | function fx_shakeScreen (amt) { 17 | g_screenShake = vec2(rand(-amt, amt), rand(-amt, amt)); 18 | } 19 | 20 | function fx_updateScreenShake () { 21 | 22 | if (g_screenShake.length() > 0.01) { 23 | g_screenShake = g_screenShake.multiply(vec2(-0.7)); 24 | } 25 | 26 | } 27 | 28 | function fx_splatter(pos) { 29 | // splatter on floor 30 | let splatterPattern = { 31 | pos: pos.add(randInCircle(0.2)), 32 | color: colorBlood.scale(rand()), 33 | pattern: [], 34 | }; 35 | for (let i = 0; i < 16; i++) { 36 | splatterPattern.pattern.push(rand() > 0.5 ? 1 : 0); 37 | } 38 | g_splatter.push(splatterPattern); 39 | } 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/js/mobGhost.js: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | class Ghost extends Enemy { 4 | constructor(pos) { 5 | super(pos, MOB_SIZE, tileNumbers_ghost, mobDefs.Ghost); 6 | 7 | this.miniFace = miniTileNumbers_miniFaceGhost; 8 | this.mass = 2; 9 | 10 | this._armColor = colorGrey.copy(); 11 | this.color = this._armColor; 12 | this.transformCount = rand(30, 120); 13 | this.solid = false; 14 | 15 | this.soundGroan = soundGhostGroan; 16 | } 17 | 18 | update() { 19 | this.transformCount--; 20 | 21 | // transform 22 | if (this.transformCount < 0) { 23 | this.transformCount = rand(30, 120); 24 | this.solid = !this.solid; 25 | this.color.a = this.solid ? 1 : 0.3; 26 | this._armColor.a = this.color.a; 27 | } 28 | 29 | this.setCollision(this.solid, this.solid, this.solid); 30 | 31 | super.update(); 32 | } 33 | 34 | postRender() { 35 | this.drawReachingArms(); 36 | 37 | super.postRender(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/js/blood.js: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | function makeParticles(pos, time, color, size) { 4 | let emitter = new ParticleEmitter( 5 | pos, // pos 6 | 0, // angle 7 | 0, // emitSize 8 | time, // emitTime 9 | rand(10, 20), // emitRate 10 | PI, // emiteConeAngle 11 | -1, // tileIndex 12 | undefined, // tileSize 13 | color || colorBlood, // colorStartA 14 | color || colorBlood, // colorStartB 15 | colorBlack, // colorEndA 16 | colorBlack, // colorEndB 17 | time * 2, // particleTime 18 | size || 0.12, // sizeStart 19 | size || 0.1, // sizeEnd 20 | rand(0.1, 0.4), // particleSpeed 21 | 2 / 12, // particleAngleSpeed 22 | 0.5, // damping 23 | 1, // angleDamping 24 | 0, // gravityScale 25 | PI, // particleCone 26 | 0.1, //fadeRate, 27 | 0.5, // randomness 28 | false, // collide 29 | false, // additive 30 | false, // randomColorLinear 31 | 1e8 // renderOrder 32 | ); 33 | 34 | return emitter; 35 | } 36 | -------------------------------------------------------------------------------- /src/js/gunShotgun.js: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | class Shotgun extends Gun { 3 | constructor(pos) { 4 | super(pos, tileNumbers_shotgun); 5 | this._distance = 0.3; 6 | this._speed = 0.3; 7 | this._maxAmmo = 2; 8 | this._ammoIconTile = tileNumbers_shellIcon; 9 | 10 | this.ammo = this._maxAmmo; 11 | this.reloadTimePerBullet = 0.6; 12 | 13 | this._soundFire = soundShotgun; 14 | } 15 | 16 | fire() { 17 | if (super.fire(colorShell)) { 18 | var bulletColor = colorShell; 19 | var bulletLife = 22; 20 | 21 | const bullets = 4; 22 | const spread = PI / 6; 23 | let angle = -this.angle - spread / 2; 24 | for (let i = 0; i < bullets; i++) { 25 | let bullet = new Bullet(this.pos.copy(), 0, bulletColor, bulletLife); 26 | bullet.velocity.x = Math.cos(angle) * this._speed; 27 | bullet.velocity.y = Math.sin(angle) * this._speed; 28 | angle += spread / bullets; 29 | } 30 | } 31 | return true; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # These are some examples of commonly ignored file patterns. 2 | # You should customize this list as applicable to your project. 3 | # Learn more about .gitignore: 4 | # https://www.atlassian.com/git/tutorials/saving-changes/gitignore 5 | 6 | # Project Customizations 7 | ziptest/* 8 | dist/* 9 | !/dist/lib 10 | src/js/*_GEN.js 11 | package-lock.json 12 | 13 | # Node artifact files 14 | node_modules/ 15 | 16 | # Compiled Java class files 17 | *.class 18 | 19 | # Compiled Python bytecode 20 | *.py[cod] 21 | 22 | # Log files 23 | *.log 24 | 25 | # Package files 26 | *.jar 27 | 28 | # Maven 29 | target/ 30 | 31 | # JetBrains IDE 32 | .idea/ 33 | 34 | # Unit test reports 35 | TEST*.xml 36 | 37 | # Generated by MacOS 38 | .DS_Store 39 | 40 | # Generated by Windows 41 | Thumbs.db 42 | 43 | # Applications 44 | *.app 45 | *.exe 46 | *.war 47 | 48 | # Large media files 49 | *.mp4 50 | *.tiff 51 | *.avi 52 | *.flv 53 | *.mov 54 | *.wmv 55 | 56 | #vs code stuff 57 | workspace.code-workspace -------------------------------------------------------------------------------- /src/js/gunMachinePistol.js: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | class MachinePistol extends Gun { 4 | constructor(pos) { 5 | super(pos, tileNumbers_smg); 6 | // your object init code here 7 | this._distance = 0.7; 8 | this._speed = 0.4; 9 | 10 | this._maxAmmo = 20; 11 | this.ammo = this._maxAmmo; 12 | this._ammoIconTile = tileNumbers_bulletIcon; 13 | 14 | this.reloadTimePerBullet = 0.02; 15 | 16 | this._soundFire = soundPistol; 17 | this.autoFire = true; 18 | 19 | this.fireDelay = 100; // ms between shots 20 | this.lastFireTime = 0; 21 | } 22 | 23 | fire() { 24 | var now = new Date().getTime(); 25 | 26 | if (now - this.lastFireTime < this.fireDelay) return; 27 | 28 | this.lastFireTime = now; 29 | 30 | if (super.fire(colorBullet)) { 31 | let bullet = new Bullet(this.pos.copy(), 0, colorBullet, 28); 32 | bullet.velocity.x = Math.cos(-this.angle) * this._speed; 33 | bullet.velocity.y = Math.sin(-this.angle) * this._speed; 34 | } 35 | return true; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/js/utilsLevel.js: -------------------------------------------------------------------------------- 1 | 2 | function getNextEnemySpawnClass() { 3 | let chance = 0; 4 | let rando = rand(); 5 | let type; 6 | for (let i = 0; i < g_levelDef.spawns.length; i++) { 7 | chance += g_levelDef.spawns[i].chance; 8 | if (rando <= chance) { 9 | type = g_levelDef.spawns[i].type; 10 | break; 11 | } 12 | } 13 | return type; 14 | } 15 | 16 | function generateMapFromLevel(level) { 17 | 18 | let mapToCopy = mapData[level]; 19 | 20 | let newMap = { 21 | w: mapToCopy.h, 22 | h: mapToCopy.w, 23 | data: [] 24 | }; 25 | 26 | for (let y = 0; y < newMap.h; y++) { 27 | for (let x = 0; x < newMap.w; x++) { 28 | newMap.data[(newMap.w - 1 - x) + newMap.w * (newMap.h - 1 - y)] = mapToCopy.data[y + mapToCopy.w * x]; 29 | } 30 | } 31 | 32 | mapData.push(newMap); 33 | 34 | } 35 | 36 | function getAmmoForGunType(gunType) { 37 | 38 | if (gunType == tileNumbers_rifle) { 39 | return tileNumbers_boxRifleAmmo; 40 | } else if (gunType == tileNumbers_shotgun) { 41 | return tileNumbers_boxShells; 42 | } 43 | 44 | return tileNumbers_boxBullets; 45 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Jonas Olmstead and Jesper Rasmussen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es6": true 4 | }, 5 | "extends": [ 6 | "eslint:recommended", 7 | "plugin:@typescript-eslint/recommended", 8 | "prettier" 9 | ], 10 | "parser": "@typescript-eslint/parser", 11 | "plugins": [ 12 | "@typescript-eslint" 13 | ], 14 | "rules": { 15 | "no-var": "off", 16 | "no-undef": "off", 17 | "no-empty": "warn", 18 | "no-tabs": "off", 19 | "no-redeclare": "off", // TODO: consider set to warn ! 20 | "no-useless-escape": "warn", 21 | "no-constant-condition": ["error", { "checkLoops": false }], 22 | "no-dupe-keys": "off", 23 | "no-inner-declarations": "off", 24 | "no-prototype-builtins": "off", 25 | 26 | "no-mixed-spaces-and-tabs": "off", // TODO: consider set to warn in some nice rosy future 27 | "@typescript-eslint/no-this-alias": "off", 28 | "@typescript-eslint/no-explicit-any": "off", 29 | "@typescript-eslint/no-unused-vars": "off", 30 | "@typescript-eslint/ban-ts-comment": "off", 31 | "@typescript-eslint/no-empty-function": "off", 32 | "@typescript-eslint/no-empty-interface": "off", 33 | "@typescript-eslint/no-misused-new": "off", 34 | "@typescript-eslint/no-var-requires": "off" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/js/corpse.js: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | class Corpse extends EngineObject { 3 | constructor(pos, size, tileIndex, tileSize) { 4 | super(pos, size, tileIndex, tileSize, 0, new Color(1, 1, 1, 0.7)); 5 | // your object init code here 6 | 7 | this._animLifetime = 10; 8 | 9 | this.timeAlive = 0; 10 | this.fallDirection = 1; 11 | this.finalAngle = PI; 12 | this.setCollision(false, false, true); 13 | 14 | this.bloodEmitter = makeParticles(this.pos, rand(0.5, 1)); 15 | this.splatterTime = 90; 16 | } 17 | 18 | update() { 19 | this.timeAlive++; 20 | 21 | this.angle = (this.fallDirection * min(1, this.timeAlive / this._animLifetime) * this.finalAngle) / 2; 22 | 23 | this.velocity.x = this.velocity.x * 0.9; 24 | this.velocity.y = this.velocity.y * 0.9; 25 | 26 | this.bloodEmitter.pos = this.pos; 27 | if (this.splatterTime) { 28 | if (rand() < 0.1) fx_splatter(this.pos); 29 | this.splatterTime--; 30 | } 31 | super.update(); // update object physics and position 32 | } 33 | 34 | // skip render so it stays in background 35 | render() {} 36 | 37 | renderNow() { 38 | super.render(); // draw object as a sprite 39 | } 40 | 41 | // postRender() { 42 | // // draw score 43 | // if (this.scoreObj && this.scoreObj.life) { 44 | // drawText( 45 | // "+" + this.scoreObj.score, 46 | // this.pos.add(vec2(0, this.scoreObj.y)), 47 | // 0.5, 48 | // colorScoreText, 49 | // 1 / 6, 50 | // undefined, 51 | // "center" 52 | // ); 53 | // this.scoreObj.y += (1 - (3 * this.scoreObj.y) / 4) / 12; 54 | // this.scoreObj.life--; 55 | // } 56 | // } 57 | 58 | pushCorpse(velocity /*, score*/) { 59 | this.velocity.x = velocity.x / 3; 60 | this.velocity.y = velocity.y / 3; 61 | this.fallDirection = velocity.x > 0 ? 1 : -1; 62 | this.finalAngle = -PI / 2 + rand(PI) + PI; 63 | 64 | // if (score) { 65 | // this.scoreObj = { 66 | // score: score, 67 | // y: 0, 68 | // life: 60, 69 | // }; 70 | // } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/js/sounds.js: -------------------------------------------------------------------------------- 1 | 2 | function soundPlayExtra(sound, pos, vol, pitch, rand = 0, delay = 0, repeat = 1, initDelay=0) { 3 | for (let i = 0; i < repeat; i++) { 4 | setTimeout( () => sound.play(pos, vol, pitch, rand), initDelay + delay * (i+1) ) 5 | } 6 | } 7 | 8 | 9 | 10 | 11 | /// Player 12 | 13 | var soundPlayerScream = new Sound([1,,440,,.1,1,3,1,-.2,,50,,,.4,,.05,.05,.9,.5,]); 14 | 15 | 16 | var soundPickup = new Sound([1,.1,200,,,,4,,,1.2,50,.57,,,,.2,.2,,.2,]); 17 | 18 | 19 | 20 | var soundLevelCleared = new Sound([2,0,685,0,.57,.3,0,3,.6,0,174,.08,.04,0,0,0,.15,.7,.1,.32]) 21 | 22 | 23 | // [1.29, .05, 685, .04, .13, .23, 2, 1.91, .6, 0, 174, .08, .04, 0, 0, 0, .06, .98, .28, .14]); 24 | 25 | 26 | /// Weapons 27 | 28 | var soundRifle = new Sound([3,,164.8138,,,,4,,,,,,,,,-.3]); 29 | var soundPistol = new Sound([1,,164.8138,,,,4,,,,,,,,,-.3]); 30 | var soundShotgun = new Sound([3,,352,.07,.01,.2,4,3,,.4,,,.15,1.5,,.7,.12,.2]); 31 | 32 | var soundBulletHit =new Sound([1,.1,137,.02,.02,.04,4,2.98,-0.9,-3.5,0,0,0,1.6,-2.3,.1,0,.87,0,0]); 33 | 34 | var soundGunReload = new Sound([,.3,,.01,,.01,4,,20,6,600,.07,.3,3.6,12,,,,,.12]); 35 | var soundGunEmpty = new Sound([1,,65,,,.02,4,,,,,,,2,,,,1,,0]); 36 | 37 | 38 | 39 | /// Monsters 40 | 41 | var soundEnemyGroan = new Sound([ 1,.5,329.6276,.16,.62,.33,,.5,,,-50,.14,.13,2.5,28,,,.9,.07,.12,]); 42 | var soundBossStep = new Sound([3.5,,15,.02,.1,.05,,3.67,,.6,,,.13,1.8,,.3,,.35,.01]); 43 | var soundBossThrow = new Sound([,,1650,.01,.09,.02,4,.5,1,,,.06,,,13,,,.9,.1]); 44 | var soundBossTearing = new Sound([1.08,,50,,.18,.45,4,,,,,,.01,.3,,1.2,,.3,.11]); 45 | var soundVampireGroan = new Sound([,.1,3665.40639,.12,.05,.09,3,.7,7.4,2.5,,.19,,.9,12,,.04,-.57,.13,]); 46 | var soundGhostGroan = new Sound([2, .1, 130, .06, .2, .5, 0, 1, 0, -2, 0, .3, .3, .1, 0, .1, .01, .8, .07, 0]); 47 | 48 | 49 | //.5, .1, 523.2511, .06, .2, .5, 0, 1, 0, -2, -100, .3, .3, .1, 100, .1, .01, .8, .07, 0]); 50 | 51 | //1.01, 0, 523.2511, .06, .2, .15, 0, .07, 0, -1, 0, 0, .3, 1, 0, 0, .1, .49, .07, .33]); 52 | 53 | 54 | var soundBoulderDestroy = new Sound([1.08,,50,,.18,.45,4,,,,,,.01,.3,,1.7,,.3,.11]); -------------------------------------------------------------------------------- /src/js/mobZombie.js: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | const JIT = 0.01; 4 | const RISE_FRAMES = 240; 5 | 6 | class Zombie extends Enemy { 7 | constructor(pos) { 8 | super(pos, MOB_SIZE, tileNumbers_zombie, mobDefs.Zombie); 9 | 10 | this.miniFace = miniTileNumbers_miniFaceZombie; 11 | 12 | this.mass = 2; 13 | this._armColor = colorZombie; 14 | 15 | this.riseFrames = RISE_FRAMES; 16 | this.pos.y -= 0.5; // for the rising to look good-ish 17 | 18 | this.enemyJitterForce = 0.01; 19 | this.enemyAccel = rand(0.2, 0.3); 20 | 21 | //this.pointingAngle = rand(2 * PI); 22 | this.damping = 0; // stand still until first move 23 | 24 | this.groan(1, rand(0.9, 1.1), 1); 25 | } 26 | 27 | update() { 28 | if (this.riseFrames > 0) { 29 | // dirt particles when rising 30 | if (this.riseFrames % 20 == 0) makeParticles(this.pos.subtract(vec2(0, this.size.y / 2)), 0.4, colorEarth); 31 | 32 | this.riseFrames--; 33 | let frac = 1 - this.riseFrames / RISE_FRAMES; 34 | this.tileSize = vec2(9, 12 * frac); 35 | this.size = vec2(this.size.x, 0.8 * frac); 36 | this.pos.y += 0.5 / RISE_FRAMES; 37 | 38 | if (this.riseFrames == 0) { 39 | this.groan(1, 1, 1.5); 40 | this.velocity = vec2(0); 41 | } 42 | return; 43 | } 44 | 45 | if (this.enemyToTarget) this.damping = 0.95; // now you can move 46 | 47 | super.update(); 48 | } 49 | 50 | hit(velocity, pos, dam) { 51 | //this.moveSpeed = rand(0.05, 0.2); 52 | this.enemyThinkPause += rand(10, 30); 53 | this.enemyToTarget = undefined; 54 | this.groan(1, 2, rand(2, 3)); 55 | return super.hit(velocity, pos, dam); 56 | } 57 | 58 | postRender() { 59 | if (this.riseFrames <= 0) { 60 | super.postRender(); // render face (and blood) 61 | } 62 | 63 | if (!this.enemyToTarget) this.pointingAngle = PI; 64 | 65 | this.drawReachingArms(); 66 | } 67 | } 68 | 69 | function angleNormalize(angleRad) { 70 | return (angleRad + 100 * PI) % (2 * PI); 71 | } 72 | 73 | function turnTowards(toRad, maxTurnRad) { 74 | toRad = angleNormalize(toRad); 75 | 76 | if (abs(toRad) < maxTurnRad) return toRad; 77 | 78 | if (toRad > 0 && abs(toRad) < PI) { 79 | return maxTurnRad; 80 | } else { 81 | return -maxTurnRad; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/js/mobVampire.js: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | class Vampire extends Enemy { 4 | constructor(pos) { 5 | super(pos, MOB_SIZE, tileNumbers_bat, mobDefs.Vampire); 6 | 7 | this.mass = 2; 8 | 9 | // before transform! ... BAT STATS 10 | 11 | this.enemyToTarget = undefined; 12 | this.enemyAccel = rand(0.3, 0.5); 13 | this.enemyJitterForce = 0.5; 14 | this._walkCycleFrames = 20; 15 | 16 | this.transformTimer = undefined; 17 | this.transforming = false; 18 | this.transformed = false; 19 | 20 | //this.pos.y -= 0.5; // 21 | 22 | this.soundGroan = soundVampireGroan; 23 | } 24 | 25 | update() { 26 | if (!this.transformed) { 27 | // flap wings 28 | this.angle = this.walkCyclePlace > this._walkCycleFrames / 2 ? 0 : PI; 29 | 30 | if (this.transforming) { 31 | if (this.transformTimer.elapsed()) { 32 | // transform! ... VAMPIRE STATS 33 | this.angle = 0; 34 | this.miniFace = miniTileNumbers_miniFaceVampire; 35 | this._walkCycleFrames = 15; 36 | makeParticles(this.pos, rand(), colorRifleRound); 37 | this.tileIndex = tileNumbers_vampire; 38 | this.hp += mobDefs.Vampire.addTransformHp + g_difficulty; 39 | this.mass = 2; 40 | this.enemyAccel = rand(0.4, 0.5); 41 | this.enemyThinkMin = 20; 42 | this.enemyThinkMax = 50; 43 | this.enemyJitterForce = 0; 44 | this.transformed = true; 45 | this.soundGroan = soundEnemyGroan; 46 | this.damping = 0.95; // on ground 47 | } 48 | } else { 49 | if (isOverlapping(this.pos, this._hitbox, g_player.pos, g_player._hitbox)) { 50 | // scary foreboding hint at what is coming 51 | g_transforms.push({ pos: this.pos.copy(), life: 60, tileIndex: tileNumbers_faceVampire }); 52 | this.transformTimer = new Timer(2); 53 | this.transforming = true; 54 | this.enemyAccel = -this.enemyAccel; // run from player while transforming ! 55 | } 56 | } 57 | } 58 | 59 | super.update(); // update object physics and position 60 | } 61 | 62 | render() { 63 | super.render(); 64 | } 65 | 66 | hit(velocity, pos, dam) { 67 | this.enemyToTarget = undefined; 68 | this.groan(1, this.transformed ? 1 : 0.3); 69 | return super.hit(velocity, pos, dam); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/js/mobBossZombie.js: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | class BossZombie extends Enemy { 4 | constructor(pos) { 5 | super(pos, vec2(1.5, 2), tileNumbers_beefyZombie, mobDefs.BossZombie); 6 | 7 | this.mass = 12; 8 | 9 | this.oldMirror = false; 10 | this.throwing = false; 11 | this.tearing = false; 12 | this.boulder = undefined; 13 | 14 | this.soundStep = soundBossStep; 15 | this.soundThrow = soundBossThrow; 16 | this.soundTearing = soundBossTearing; 17 | 18 | this.miniFace = miniTileNumbers_miniFaceBoss; 19 | } 20 | 21 | update() { 22 | if (g_state == STATE_DEAD) return; 23 | 24 | if (this.tearing) { 25 | this.miniFace = miniTileNumbers_miniFaceBossAngry; 26 | this.boulder.pos = this.pos.copy(); 27 | if (!this.tearingTimer.elapsed()) return; 28 | 29 | // throw chunk 30 | this.tearing = false; 31 | this.throwing = true; 32 | this.throwingTimer = new Timer(1); 33 | this.boulder.velocity = g_player.pos.subtract(this.pos).normalize(0.2); 34 | this.boulder.angleVelocity = rand(0.1, 0.2); 35 | this.boulder.isThrown = true; 36 | this.soundThrow.play(this.pos); 37 | } 38 | 39 | if (this.throwing) { 40 | if (!this.throwingTimer.elapsed()) return; 41 | this.thowing = false; 42 | this.miniFace = miniTileNumbers_miniFaceBoss; 43 | } 44 | 45 | super.update(); 46 | 47 | if (this.mirror != this.oldMirror) { 48 | fx_shakeScreen(0.5); 49 | //vibrate(200); 50 | this.soundStep.play(this.pos); 51 | this.oldMirror = this.mirror; 52 | } 53 | } 54 | 55 | postRender() { 56 | // draw face 57 | drawTile( 58 | this.pos.add(vec2((g_player.pos.subtract(this.pos).x > -1 ? 1 : 0) / 12, 7 / 12 + this.bumpWalk)), 59 | vec2(1 / 2), 60 | this.miniFace, 61 | vec2(6) 62 | ); 63 | this.drawBlood(); 64 | } 65 | 66 | collideWithObject(o) { 67 | return true; 68 | } 69 | 70 | collideWithTile(tileData, pos) { 71 | if (this.tearing) return; 72 | 73 | if (pos.x > 0 && pos.y > 0 && pos.x < tileLayer.size.x - 1 && pos.y < tileLayer.size.y - 1) { 74 | tileLayer.setData(pos, 0, true); 75 | setTileCollisionData(pos, 0); 76 | } 77 | 78 | this.tearing = true; 79 | this.soundTearing.play(this.pos); 80 | 81 | // tear out chunk 82 | this.boulder = new Boulder(this.pos.copy(), tileData); 83 | 84 | this.tearingTimer = new Timer(1); 85 | 86 | return true; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/js/pusher.js: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | const enableDrawPushers = false; 4 | 5 | const PushTo = { 6 | PLAYER: 1, 7 | ENEMIES: 2, 8 | ALL: 3, 9 | }; 10 | 11 | class Pusher { 12 | /** 13 | * At minDist the strenght of the push is pushStrength at maxDist it is zero. 14 | * 15 | * @param {Vector2} pos 16 | * @param {number} pushStrength 17 | * @param {number} minDist 18 | * @param {number} maxDist 19 | * @param {number} secs Seconds to live. If 0 or negative, live for ever. 20 | * @param {number} whoIsAffected (Affects) 21 | */ 22 | constructor(pos, pushStrength, minDist, maxDist, secs = 0, whoIsAffected = PushTo.ALL) { 23 | this.pos = pos; 24 | this.minDist = minDist; 25 | this.maxDist = maxDist; 26 | this.pushStrength = pushStrength; 27 | this.tics = Math.round(secs * 60); 28 | this.affects = whoIsAffected; 29 | } 30 | 31 | affectMob(e) { 32 | if (!e.collideWithTile) return; 33 | 34 | let toMob = e.pos.subtract(this.pos); 35 | 36 | let dist = toMob.length(); 37 | 38 | if (dist > this.maxDist) return; 39 | 40 | let strenght = this.pushStrength; 41 | 42 | if (dist > this.minDist) { 43 | let p = 1 - percent(dist, this.minDist, this.maxDist); 44 | //strenght *= p; // lineary falloff 45 | //strenght *= 1 + Math.cos(p * PI); // sigmoidal falloff 46 | strenght *= 2 * p * p; // squared falloff 47 | } 48 | 49 | // console.log("strenght", strenght); 50 | 51 | let force = toMob.normalize(rand(strenght)); 52 | 53 | e.applyForce(force); 54 | } 55 | 56 | update() { 57 | this.tics = this.tics - 1; 58 | 59 | if (this.affects & PushTo.ENEMIES) { 60 | for (const e of g_enemies) { 61 | this.affectMob(e); 62 | } 63 | } 64 | 65 | if (this.affects & PushTo.PLAYER) { 66 | this.affectMob(g_player); 67 | } 68 | } 69 | 70 | // draw() { 71 | // debugCircle(this.pos, this.minDist, "#f00", 1 / 60, false); 72 | // debugCircle(this.pos, this.maxDist, "#0f0", 1 / 60, false); 73 | // } 74 | } 75 | 76 | var pushers = []; 77 | 78 | function updatePushers() { 79 | for (let i = 0; i < pushers.length; i++) { 80 | let p = pushers[i]; 81 | p.update(); 82 | if (p.tics === 0) { 83 | pushers.splice(i, 1); 84 | i--; 85 | } 86 | } 87 | } 88 | 89 | function drawPushers() { 90 | if (!enableDrawPushers) return; 91 | for (const p of pushers) { 92 | p.draw(); 93 | } 94 | } 95 | 96 | function clearPushers() { 97 | pushers = []; 98 | } 99 | -------------------------------------------------------------------------------- /src/js/bullet.js: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | class Bullet extends EngineObject { 4 | constructor(pos, angle, color, lifetime, penetration) { 5 | let size = vec2(0.15); 6 | super(pos, size, -1, TILE_SIZE, angle, color); 7 | 8 | this._lifetime = lifetime; 9 | this._hitbox = vec2(0.25); 10 | 11 | this.penetration = penetration || 1; 12 | this.timeAlive = 0; 13 | this.setCollision(true, true, true); 14 | 15 | this.hitSound = soundBulletHit; 16 | } 17 | 18 | update() { 19 | this.timeAlive++; 20 | if (this.timeAlive > this._lifetime) { 21 | this.destroy(); 22 | return; 23 | } 24 | super.update(); 25 | } 26 | 27 | hitWall(big) { 28 | // bullet holes 29 | let pos = this.pos.add(this.velocity.normalize(rand(0.2, 0.6))); 30 | g_holes.push({ pos: pos, color: colorBlack }); 31 | for (let i = 0; i < 4; i++) { 32 | g_holes.push({ 33 | size: big ? 3 : 1, 34 | pos: pos.add(randInCircle(1 / 12)), 35 | color: new Color(0, 0, 0, rand(0.1, 0.5)), 36 | }); 37 | } 38 | 39 | fx_addSpark(pos.copy()); 40 | } 41 | 42 | collideWithTile(tileData, pos) { 43 | if (tileData == tileNumbers_door) { 44 | let idx = pos.x + "_" + pos.y; 45 | g_doors[idx].hp--; 46 | this.hitWall(true); 47 | soundBoulderDestroy.play(pos, 1.5); 48 | makeParticles(pos.add(vec2(0.5)), 0.1, colorWhite, 0.25); 49 | if (g_doors[idx].hp <= 0) { 50 | let floorTile = new TileLayerData( 51 | tileNumbers_floorStone, 52 | randInt(4), 53 | false, 54 | new Color(1, 1, 1, rand(0.2, 0.5)) 55 | ); 56 | tileLayer.setData(pos, floorTile, true); 57 | tileLayer.redraw(); // TODO: Could be SLOW ... find better solution ! 58 | soundBoulderDestroy.play(pos, 3); 59 | makeParticles(pos.add(vec2(0.5)), 0.3, colorWhite, 0.5); 60 | setTileCollisionData(pos, 0); 61 | } 62 | } 63 | this.hitWall(); 64 | this.destroy(); 65 | this.hitSound.play(); 66 | return false; // no more col resolve 67 | } 68 | 69 | collideWithObject(o) { 70 | //console.log("bullet hit : ", o); 71 | if (this.penetration <= 0) return false; 72 | 73 | if (o instanceof Enemy) { 74 | if (o.hp <= 0) return false; 75 | 76 | var damage = min(o.hp, this.penetration); 77 | 78 | o.hit(this.velocity.copy(), this.pos.copy(), damage); 79 | this.hitSound.play(this.pos); 80 | 81 | this.penetration -= damage; 82 | 83 | if (this.penetration <= 0) { 84 | this.destroy(); 85 | return; 86 | } 87 | } 88 | 89 | return false; // no auto resolve of collision 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/js/music.js: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | var songData = [ 4 | [ 5 | [, 0, 43, 0.01, , 0.3, 2, , , , , , , , , 0.02, 0.01], // 0 bass 6 | [20, 0, 170, 0.003, , 0.008, , 0.97, -35, 53, , , , , , 0.1], // 1 base drum 7 | [0.8, 0, 270, , , 0.12, 3, 1.65, -2, , , , , 4.5, , 0.02], // 2 snare 8 | 9 | // [, 0, 86, , , , , 0.7, , , , 0.5, , 6.7, 1, 0.05], // 3 hh 10 | // [, 0, 77, , , 0.7, 2, 0.41, , , , , , , , 0.06], // 4 bass 11 | // [, 0, 41, , 0.05, 0.4, 2, 0, , , 9, 0.01, , , , 0.08, 0.02], // 5 bass 12 | // [, 0, 2200, , , 0.04, 3, 2, , , 800, 0.02, , 4.8, , 0.01, 0.1], // 6 hh / click 13 | // [0.3, 0, 16, , , 0.3, 3], // 7 bass 14 | ], 15 | [[], []], 16 | [0, 1], // patterns 17 | 50, 18 | ]; 19 | 20 | function unfoldPattern(instrument, pan, startnode, pattern, starts) { 21 | var nodes = []; 22 | nodes.push(instrument); 23 | nodes.push(pan); 24 | 25 | for (const s of starts) { 26 | for (const b of pattern) { 27 | nodes.push(startnode + b + s); 28 | } 29 | } 30 | 31 | return nodes; 32 | } 33 | 34 | function createMinorIssues() { 35 | // prettier-ignore 36 | let chordStarts = [ 37 | undefined, undefined, undefined, undefined, 38 | 2, 2, 2, 2, 39 | 0, 0, 0, 0, 40 | 2, 2, 2, 2, 41 | 0, 0, 0, 0, 42 | 7, 7, 7, 7, 43 | 5, 5, 5, 5, 44 | 9, 9, 11, 11, 45 | 13, 15, 17, 19, 46 | ]; 47 | 48 | let bassPattern = [0, 12, 14, 15, 0, 12, 15, 14]; 49 | 50 | let bassNodes = unfoldPattern(0, -0.1, 7, bassPattern, chordStarts); 51 | songData[1][1].push(bassNodes); 52 | 53 | let bassNodes2 = unfoldPattern(0, 0.1, 7 + 7, bassPattern, chordStarts); 54 | songData[1][1].push(bassNodes2); 55 | 56 | // let drumStarts = Array(chordStarts.length / 2).fill(0); 57 | // let bdPattern = [0, , , , 0, 0, , , 0, , , , 0, 0, , ,]; 58 | // songData[1][0].push(unfoldPattern(1, 0, 8, bdPattern, drumStarts)); 59 | 60 | // let snarePattern = [, , 0, , , , 0, , , , 0, , , , 0, 0]; 61 | // songData[1][0].push(unfoldPattern(2, 0.1, 7, snarePattern, drumStarts)); 62 | 63 | let bdStarts = Array(chordStarts.length).fill(0); 64 | let snareStarts = Array(chordStarts.length / 2).fill(0); 65 | let bdPattern = [0, 0, undefined, undefined, 0, 0, undefined, undefined]; 66 | let snarePattern = [ 67 | undefined, 68 | undefined, 69 | 0, 70 | undefined, 71 | undefined, 72 | undefined, 73 | 0, 74 | undefined, 75 | undefined, 76 | undefined, 77 | 0, 78 | undefined, 79 | undefined, 80 | undefined, 81 | 0, 82 | 0, 83 | ]; 84 | 85 | songData[1][0].push(unfoldPattern(1, 0, 7, bdPattern, [0, 0, 0, 0])); 86 | 87 | songData[1][1].push(unfoldPattern(1, 0, 7, bdPattern, bdStarts)); 88 | songData[1][1].push(unfoldPattern(2, 0.1, 7, snarePattern, snareStarts)); 89 | } 90 | 91 | var vol = 0.6; //0.5; //.7; 92 | 93 | var music; 94 | var source; 95 | 96 | function musicStart() { 97 | //createBlues(); 98 | createMinorIssues(); 99 | 100 | if (music) return; 101 | 102 | music = new Music(songData); 103 | source = music.play(vol); 104 | } 105 | 106 | function musicResume() { 107 | if (!music) return; 108 | 109 | if (!source) { 110 | source = music.play(vol); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/js/mob_base.js: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | class Mob extends EngineObject { 3 | constructor(pos, size, tileIndex) { 4 | super(pos, size, tileIndex, vec2(9, 12)); 5 | 6 | this.walkCyclePlace = 0; 7 | this._walkCycleFrames = 60; 8 | this._hitbox = vec2(0.5); 9 | 10 | this.miniFace = undefined; 11 | this.setCollision(true, true); 12 | this.mass = 1; 13 | this.elasticity = 0.1; 14 | this.damping = 1; 15 | 16 | // this._maxSpeed = 0.4; 17 | 18 | this.bumpWalk = 0; 19 | this.mirror = false; 20 | this.hp = 3; 21 | 22 | this.blood = []; 23 | 24 | // for arms 25 | this.pointingAngle = rand(2 * PI); 26 | this._armColor = undefined; 27 | 28 | this.enemyToTarget = undefined; 29 | this.soundGroan = undefined; 30 | this.bloodEmitter = undefined; 31 | } 32 | 33 | applyDrag(dragConst) { 34 | let speed = this.velocity.length(); 35 | 36 | let drag = speed * speed * dragConst; 37 | 38 | if (drag > speed) drag = speed; 39 | 40 | let dragForce = this.velocity.normalize(drag); 41 | 42 | this.velocity = this.velocity.subtract(dragForce); 43 | } 44 | 45 | groan(chance, strength, pitch = 1, repeat = 1) { 46 | if (rand() > chance) return; 47 | 48 | const MAX_VOL = 0.5; 49 | 50 | let vol = MAX_VOL; 51 | 52 | if (this.enemyToTarget) { 53 | let d = this.enemyToTarget.length() / 10; 54 | vol = d < 1 ? MAX_VOL : MAX_VOL / (d * d); 55 | } 56 | 57 | soundPlayExtra(this.soundGroan, this.pos, strength * vol, pitch, 1, 50, repeat, 100); 58 | 59 | // this.soundGroan.play(this.pos, strength * vol, strength * rand(1, 2), 0.5); 60 | } 61 | 62 | update() { 63 | if (this.velocity.length() > 0.01) { 64 | this.walkCyclePlace = (this.walkCyclePlace + 1) % this._walkCycleFrames; 65 | this.mirror = this.walkCyclePlace > this._walkCycleFrames / 2 ? true : false; 66 | this.bumpWalk = this.walkCyclePlace > this._walkCycleFrames / 2 ? 0 : 1 / 12; 67 | } else { 68 | this.walkCyclePlace = 0; 69 | this.mirror = false; 70 | } 71 | 72 | if (this.bloodEmitter) this.bloodEmitter.pos = this.pos; 73 | 74 | super.update(); // update object physics and position 75 | } 76 | 77 | render() { 78 | drawTile( 79 | vec2(this.pos.x, this.pos.y + this.bumpWalk), 80 | this.size, 81 | this.tileIndex, 82 | this.tileSize, 83 | this.color, 84 | this.angle, 85 | this.mirror 86 | ); 87 | this.postRender(); 88 | } 89 | 90 | postRender() { 91 | // draw face 92 | if (this.miniFace && this.enemyToTarget && this.enemyToTarget.y <= 0) { 93 | drawTile( 94 | this.pos.add(vec2((this.enemyToTarget.x > 0 ? 1 : 0) / 12, 3 / 12 + this.bumpWalk)), 95 | vec2(1 / 4), 96 | this.miniFace, 97 | MINI_TILE_SIZE 98 | ); 99 | } 100 | this.drawBlood(); 101 | } 102 | 103 | drawBlood() { 104 | // blood 105 | for (let i = 0; i < this.blood.length; i++) { 106 | let blood = this.blood[i]; 107 | for (let j = 0; j < blood.pattern.length; j++) { 108 | if (blood.pattern[j]) { 109 | let x = this.pos.x + blood.pos.x - (j % 2) / 12; 110 | let y = this.pos.y + blood.pos.y - Math.floor(j / 2) / 12; 111 | drawRect(vec2(x, y), vec2(1 / 12), colorBlood); 112 | } 113 | } 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/maps/2.json: -------------------------------------------------------------------------------- 1 | { "compressionlevel":1, 2 | "editorsettings": 3 | { 4 | "export": 5 | { 6 | "format":"js" 7 | } 8 | }, 9 | "height":20, 10 | "infinite":false, 11 | "layers":[ 12 | { 13 | "data":[15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 14 | 15, 0, 0, 0, 0, 0, 11, 0, 0, 0, 0, 0, 0, 0, 0, 11, 0, 0, 0, 0, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 15 | 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 16 | 15, 0, 0, 0, 0, 0, 0, 0, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 0, 0, 0, 9, 0, 0, 9, 0, 0, 24, 0, 0, 15, 17 | 15, 0, 0, 0, 11, 0, 0, 0, 0, 0, 0, 0, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 9, 0, 0, 15, 18 | 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 15, 15, 15, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 0, 0, 15, 19 | 15, 0, 0, 0, 0, 0, 0, 0, 0, 11, 15, 24, 0, 21, 15, 0, 15, 15, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 11, 0, 0, 15, 20 | 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 0, 0, 15, 0, 0, 0, 0, 9, 0, 0, 0, 0, 0, 15, 21 | 15, 0, 0, 0, 0, 0, 0, 11, 0, 0, 15, 15, 12, 15, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 0, 0, 15, 22 | 15, 0, 0, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 9, 0, 0, 15, 23 | 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 24 | 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 25 | 15, 0, 0, 9, 9, 0, 0, 9, 9, 0, 0, 0, 0, 0, 0, 0, 0, 11, 0, 0, 0, 0, 0, 0, 9, 9, 0, 0, 9, 0, 0, 15, 26 | 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 15, 15, 0, 0, 0, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 15, 27 | 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 28 | 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 11, 0, 5, 0, 0, 0, 11, 11, 0, 0, 0, 0, 15, 29 | 15, 0, 0, 9, 0, 0, 0, 9, 9, 0, 0, 15, 15, 15, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 15, 30 | 15, 0, 24, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 31 | 15, 0, 0, 0, 0, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 0, 0, 0, 0, 0, 11, 0, 0, 0, 0, 0, 0, 0, 0, 15, 32 | 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15], 33 | "height":20, 34 | "id":1, 35 | "name":"Tile Layer 1", 36 | "opacity":1, 37 | "type":"tilelayer", 38 | "visible":true, 39 | "width":32, 40 | "x":0, 41 | "y":0 42 | }], 43 | "nextlayerid":2, 44 | "nextobjectid":1, 45 | "orientation":"orthogonal", 46 | "renderorder":"right-down", 47 | "tiledversion":"1.8.4", 48 | "tileheight":12, 49 | "tilesets":[ 50 | { 51 | "columns":5, 52 | "firstgid":1, 53 | "image":"..\/gfx\/tiles.png", 54 | "imageheight":64, 55 | "imagewidth":60, 56 | "margin":0, 57 | "name":"tiles", 58 | "spacing":0, 59 | "tilecount":25, 60 | "tileheight":12, 61 | "tilewidth":12 62 | }], 63 | "tilewidth":12, 64 | "type":"map", 65 | "version":"1.8", 66 | "width":32 67 | } -------------------------------------------------------------------------------- /src/maps/0.json: -------------------------------------------------------------------------------- 1 | { "compressionlevel":1, 2 | "editorsettings": 3 | { 4 | "export": 5 | { 6 | "format":"js" 7 | } 8 | }, 9 | "height":20, 10 | "infinite":false, 11 | "layers":[ 12 | { 13 | "data":[14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 15, 15, 15, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14 | 11, 11, 11, 11, 11, 11, 0, 0, 0, 0, 0, 11, 11, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 11, 11, 0, 0, 0, 11, 11, 11, 11, 15 | 11, 11, 11, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 11, 16 | 11, 11, 11, 11, 0, 0, 0, 10, 10, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 11, 17 | 11, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 0, 0, 0, 11, 0, 0, 11, 11, 18 | 11, 11, 0, 0, 0, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 0, 0, 0, 0, 0, 0, 0, 11, 19 | 11, 11, 0, 0, 0, 0, 0, 0, 15, 15, 15, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 20 | 11, 11, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 11, 21 | 11, 0, 0, 0, 0, 0, 0, 0, 15, 22, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 0, 0, 0, 0, 0, 11, 22 | 11, 0, 0, 0, 0, 0, 0, 0, 15, 15, 15, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 0, 0, 11, 23 | 11, 0, 0, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 24 | 11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 0, 0, 0, 0, 0, 0, 0, 11, 25 | 11, 0, 0, 0, 0, 0, 0, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 11, 26 | 11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 0, 0, 11, 0, 0, 0, 11, 11, 27 | 10, 0, 0, 0, 10, 10, 10, 10, 10, 0, 0, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 11, 28 | 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 15, 12, 15, 11, 29 | 10, 0, 0, 0, 10, 0, 14, 9, 10, 0, 0, 0, 0, 0, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 0, 0, 0, 11, 15, 22, 15, 11, 30 | 10, 0, 0, 0, 10, 0, 10, 10, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 0, 0, 11, 11, 15, 15, 15, 11, 31 | 10, 9, 16, 9, 10, 0, 0, 0, 0, 0, 0, 0, 0, 11, 0, 0, 0, 0, 0, 0, 0, 0, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 32 | 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 14, 14, 14, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10], 33 | "height":20, 34 | "id":1, 35 | "name":"Tile Layer 1", 36 | "opacity":1, 37 | "type":"tilelayer", 38 | "visible":true, 39 | "width":32, 40 | "x":0, 41 | "y":0 42 | }], 43 | "nextlayerid":2, 44 | "nextobjectid":1, 45 | "orientation":"orthogonal", 46 | "renderorder":"right-down", 47 | "tiledversion":"1.9.0", 48 | "tileheight":12, 49 | "tilesets":[ 50 | { 51 | "columns":5, 52 | "firstgid":1, 53 | "image":"..\/gfx\/tiles.png", 54 | "imageheight":64, 55 | "imagewidth":60, 56 | "margin":0, 57 | "name":"tiles", 58 | "spacing":0, 59 | "tilecount":25, 60 | "tileheight":12, 61 | "tilewidth":12 62 | }], 63 | "tilewidth":12, 64 | "type":"map", 65 | "version":1.1, 66 | "width":32 67 | } -------------------------------------------------------------------------------- /src/js/mobPlayer.js: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | class MobPlayer extends Mob { 3 | constructor(pos) { 4 | super(pos, MOB_SIZE, tileNumbers_player); 5 | 6 | this.miniFace = miniTileNumbers_miniFacePlayer; 7 | 8 | this._walkCycleFrames = 20; 9 | 10 | this.setCollision(true, true); 11 | this.mass = 1; 12 | this.damping = 0.95; 13 | this.mirror = false; 14 | this.gun = undefined; 15 | 16 | this.ammoBullets = 3; 17 | this.ammoShells = 0; 18 | this.ammoRifle = 0; 19 | 20 | this.hp = 1; 21 | 22 | this.soundScream = soundPlayerScream; 23 | } 24 | 25 | getAmmoForCurrentGun() { 26 | if (this.gun.tileIndex == tileNumbers_shotgun) { 27 | return this.gun.ammo + this.ammoShells; 28 | } else if (this.gun.tileIndex == tileNumbers_rifle) { 29 | return this.gun.ammo + this.ammoRifle; 30 | } 31 | return this.gun.ammo + this.ammoBullets; 32 | } 33 | 34 | update() { 35 | const speed = 0.01; 36 | 37 | super.update(); // update object physics and position 38 | 39 | if (this.hp > 0) { 40 | let dx = 0; 41 | let dy = 0; 42 | 43 | if (g_state == STATE_PLAYING) { 44 | if (isUsingGamepad) { 45 | dx = gamepadStick(0).x * speed; 46 | dy = gamepadStick(0).y * speed; 47 | } else { 48 | if (keyIsDown(38)) { 49 | // key w 50 | dy = speed; 51 | } 52 | if (keyIsDown(37)) { 53 | // key a 54 | dx = -speed; 55 | } 56 | if (keyIsDown(40)) { 57 | // key s 58 | dy = -speed; 59 | } 60 | if (keyIsDown(39)) { 61 | // key d 62 | dx = speed; 63 | } 64 | } 65 | } 66 | 67 | this.applyForce(new Vector2(dx, dy)); 68 | 69 | this.applyDrag(1.1); 70 | } else { 71 | if (this.gun) { 72 | this.gun.owner = null; 73 | this.gun = null; 74 | } 75 | this.damping = 0.1; 76 | } 77 | } 78 | 79 | collideWithObject(o) { 80 | if ( 81 | (o instanceof Vampire && o.transformed) || 82 | o instanceof Zombie || 83 | o instanceof Ghost || 84 | o instanceof BossZombie || 85 | o instanceof Boulder 86 | ) { 87 | let v = this.pos.subtract(o.pos); 88 | let d = v.length(); 89 | if (d < 0.5) { 90 | if (!g_CHEATMODE) this.hp--; 91 | v.normalize(0.001); 92 | this.applyForce(v); 93 | //vibrate(200); 94 | 95 | makeParticles(this.pos, 0.05); 96 | if (rand() < 0.3) { 97 | fx_splatter(this.pos.copy()); 98 | } 99 | 100 | if (this.hp == 0) { 101 | // WE DIE ! 102 | for (let i = 0; i < 10; i++) { 103 | fx_splatter(this.pos); 104 | makeParticles(this.pos, rand()); 105 | } 106 | 107 | this.angle = PI / 2; 108 | this.soundScream.play(this.pos); 109 | } 110 | } 111 | } 112 | 113 | return false; // no auto resolve of collision 114 | } 115 | 116 | render() { 117 | super.render(); // draw object as a sprite 118 | 119 | if (this.hp > 0) { 120 | // arms 121 | drawLine(this.pos.add(vec2(3 / 12, 2 / 16 + this.bumpWalk)), this.gun.pos, 1 / 12, colorBlood, !!glEnable); 122 | drawLine(this.pos.add(vec2(-3 / 12, 2 / 16 + this.bumpWalk)), this.gun.pos, 1 / 12, colorBlood, !!glEnable); 123 | // draw face 124 | let toCursor = this.gun.pos.subtract(this.pos); 125 | if (toCursor.y <= 0) { 126 | drawTile( 127 | this.pos.add(vec2((toCursor.x > 0 ? 1 : 0) / 12, 3 / 12 + this.bumpWalk)), 128 | vec2(1 / 4), 129 | this.miniFace, 130 | MINI_TILE_SIZE 131 | ); 132 | } 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | DEAD AGAIN 2 | ===== 3 | 4 | by sanojian and repsej 5 | 6 | 7 | Death was not enough to keep these evil spirits down. 8 | 9 | Defend yourself against the onslaught of zombies, vampires, ghosts ... and more. 10 | 11 | The longer you play, the stronger they get. 12 | 13 | Survive for as long as you can, but it is only a matter of time before you are ... DEAD AGAIN 14 | 15 | 16 | ---------------------------------- 17 | 18 | Desktop controls: 19 | 20 | - WASD or arrow keys to move. Mouse to aim. 21 | - R or SPACE to reload you weapon 22 | 23 | Mobile controls: 24 | 25 | - Left stick to move (release to reload) 26 | - Right stick to aim (release to shoot) 27 | 28 | ---------------------------------- 29 | 30 | Game tips: 31 | 32 | - Boarded up doors can be shot to pieces. 33 | - Be careful around corners. 34 | - Preserve your ammo. 35 | - Extra ammo is hidden around the maps. 36 | - A shotgun can kill multiple weak enemies with one shot. 37 | - Each weapon needs a specific type of ammo. 38 | - You can only carry one weapon at a time. 39 | 40 | ---------------------------------- 41 | 42 | Tools: 43 | 44 | - LittleJS 45 | - ZzFX 46 | - ZzFXM 47 | 48 | - grunt 49 | - UglifiyJS 50 | - google-closure-compiler 51 | - steamroller 52 | - advzip 53 | 54 | ---------------------------------- 55 | 56 | Contact and links: 57 | 58 | sanojian: 59 | 60 | Jonas Olmstead 61 | https://sanojian.itch.io/ 62 | 63 | 64 | repsej: 65 | 66 | Jesper Rasmussen 67 | https://onelifeleft.itch.io/ 68 | https://www.linkedin.com/in/repsej/ 69 | 70 | 71 | ---------------------------------- 72 | 73 | Thanks: 74 | 75 | - Thanks to "Andrei Moskvitin Josephsen" for composing the awesome music. 76 | 77 | - Thanks to our friends and colleagues at FRVR for beta testing and providing feedback. 78 | 79 | 80 | 81 | --------------------------------- 82 | 83 | Funny / interesting tricks used in the game: 84 | 85 | - Each mob only has one sprite frame. For most mobs we mirror it along the x axis to create the two frames of the "walk animation". For the bat (flapping it wings) we do it on the y-axis. 86 | 87 | - After the bat has bitten the player it will try to get away while it transforms. This behavior was created by setting the bats movement speed to a negative value after it hit the player. So, the poor bat "thinks" it is still attacking the player ... while it is actually moving away. 88 | 89 | - The shadow / line of sight effect was inspired by ye ole ZX Spectrum game "Out of the Shadows" (from 1984). 90 | 91 | - The game has a system of "force fields" that affects all enemies. 92 | - Each enemy is slightly repulsed by other enemies ... making them spread out more. 93 | - Each tile w collision (wall/tree/grave stone/etc) repulse enemies, making the enemies better at getting around these things. 94 | - The player leaves attractive footprints that sucks the enemies closer. Helping the enemies getting through doorways, etc. 95 | 96 | - There are only really three unique maps. Map 5 (w the boss) is unique. But, map 1 and 3 are "the same". Map 2 and 4 are "the same". They are just rotated 90 degrees and mirrored on the x-axis. 97 | 98 | - To make the gun sounds louder each sound is played multiple times on top of each other. 99 | 100 | - We ended up out commenting about half of the engine. Eg. we gained about 1kb by removing all GL support. Made the game a little slower and uglier (no color tinting) but much better and with mobile controls. 101 | 102 | -------------------------------------------------------------------------------- /src/maps/1.json: -------------------------------------------------------------------------------- 1 | { "compressionlevel":1, 2 | "editorsettings": 3 | { 4 | "export": 5 | { 6 | "format":"js" 7 | } 8 | }, 9 | "height":32, 10 | "infinite":false, 11 | "layers":[ 12 | { 13 | "data":[15, 15, 15, 15, 11, 11, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 14 | 15, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 15, 0, 0, 9, 0, 23, 15, 15 | 15, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 0, 0, 15, 16 | 15, 0, 0, 9, 0, 0, 9, 0, 0, 9, 0, 0, 0, 15, 0, 0, 0, 0, 0, 15, 17 | 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 12, 0, 0, 9, 0, 0, 15, 18 | 15, 0, 0, 0, 0, 0, 17, 0, 0, 0, 0, 0, 0, 15, 15, 15, 15, 15, 15, 15, 19 | 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 15, 20 | 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 0, 0, 0, 0, 0, 0, 0, 15, 21 | 15, 0, 0, 9, 0, 0, 9, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 22 | 15, 0, 0, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 23 | 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 0, 0, 0, 0, 0, 0, 15, 24 | 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 25 | 15, 0, 0, 0, 0, 10, 10, 10, 10, 10, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 26 | 15, 11, 0, 0, 0, 10, 0, 0, 23, 10, 0, 0, 0, 0, 0, 15, 0, 0, 0, 15, 27 | 15, 0, 0, 0, 0, 10, 0, 10, 9, 10, 0, 0, 0, 0, 0, 0, 0, 0, 11, 15, 28 | 15, 0, 0, 0, 0, 10, 0, 10, 10, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 29 | 15, 0, 0, 0, 0, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 30 | 15, 0, 0, 0, 0, 10, 10, 10, 10, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 31 | 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 9, 0, 0, 15, 32 | 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 33 | 15, 0, 0, 11, 0, 0, 0, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 34 | 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 15, 35 | 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 11, 15, 15, 36 | 15, 15, 15, 15, 15, 15, 15, 12, 15, 0, 0, 0, 11, 11, 11, 11, 15, 15, 15, 15, 37 | 15, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 11, 11, 15, 15, 15, 38 | 15, 0, 0, 9, 0, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 11, 11, 15, 39 | 15, 0, 0, 9, 0, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 40 | 15, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 11, 15, 15, 11, 0, 0, 0, 0, 15, 41 | 15, 0, 0, 9, 0, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 42 | 15, 0, 23, 9, 0, 0, 0, 0, 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 15, 43 | 15, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 11, 15, 15, 44 | 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 11, 11, 15, 15, 15, 15, 15], 45 | "height":32, 46 | "id":1, 47 | "name":"Tile Layer 1", 48 | "opacity":1, 49 | "type":"tilelayer", 50 | "visible":true, 51 | "width":20, 52 | "x":0, 53 | "y":0 54 | }], 55 | "nextlayerid":2, 56 | "nextobjectid":1, 57 | "orientation":"orthogonal", 58 | "renderorder":"right-down", 59 | "tiledversion":"1.8.4", 60 | "tileheight":12, 61 | "tilesets":[ 62 | { 63 | "columns":5, 64 | "firstgid":1, 65 | "image":"..\/gfx\/tiles.png", 66 | "imageheight":64, 67 | "imagewidth":60, 68 | "margin":0, 69 | "name":"tiles", 70 | "spacing":0, 71 | "tilecount":25, 72 | "tileheight":12, 73 | "tilewidth":12 74 | }], 75 | "tilewidth":12, 76 | "type":"map", 77 | "version":"1.8", 78 | "width":20 79 | } -------------------------------------------------------------------------------- /GruntFile.js: -------------------------------------------------------------------------------- 1 | // NOTE: To uglify, roadroll and pack everything run the ./build.sh script 2 | 3 | module.exports = function (grunt) { 4 | 5 | 6 | // Load Grunt tasks declared in the package.json file 7 | require('matchdep').filterDev('grunt-*').forEach(grunt.loadNpmTasks); 8 | 9 | // Project configuration. 10 | grunt.initConfig({ 11 | 12 | watch: { 13 | scripts: { 14 | files: [ 15 | 'src/js/**/*.js', 16 | 'src/maps/**/*.json', 17 | 'src/gfx/**/*', 18 | 'dist/lib/*.js', 19 | '!src/js/start_GEN.js' 20 | ], 21 | tasks: ['build'] 22 | }, 23 | pages: { 24 | files: [ 25 | 'src/html/*.*' 26 | ], 27 | tasks: ['copy:html'] 28 | } 29 | }, 30 | 31 | 'http-server': { 32 | dev: { 33 | root: 'dist', 34 | port: 3116, 35 | runInBackground: true 36 | } 37 | }, 38 | 39 | image: { 40 | dev: { 41 | options: { 42 | optipng: true, 43 | pngquant: false, 44 | zopflipng: false, 45 | }, 46 | files: { 47 | 'dist/t.png': 'src/gfx/tiles.png', 48 | } 49 | }, 50 | prod: { 51 | options: { 52 | optipng: ['-o 7', '-zc 7'], 53 | //pngquant: ['-s1', '--quality=40-60'], 54 | pngquant: ['-s1'], 55 | zopflipng: ['-m'] 56 | }, 57 | files: { 58 | 'dist/t.png': 'src/gfx/tiles.png', 59 | } 60 | }, 61 | }, 62 | 63 | closureCompiler: { 64 | options: { 65 | compilerFile: 'node_modules/google-closure-compiler-java/compiler.jar', 66 | compilerOpts: { 67 | compilation_level: 'ADVANCED_OPTIMIZATIONS', 68 | language_out: 'ECMASCRIPT_2019', 69 | jscomp_off: 'checkVars', 70 | assume_function_wrapper: true 71 | }, 72 | }, 73 | targetName: { 74 | src: 'dist/js/index_prod.js', 75 | dest: 'dist/js/i.js' 76 | } 77 | }, 78 | 79 | clean: ['dist/*.html', 'dist/*.zip', 'dist/*.js', 'dist/*.png', 'dist/js/'], 80 | 81 | concat: { 82 | dev: { 83 | files: { 84 | 'dist/index.html': [ 85 | 'src/html/index_dev.html' 86 | ], 87 | } 88 | }, 89 | shared: { 90 | files: { 91 | 'dist/js/index.js': [ 92 | 'src/js/lib/*.js', 93 | 'src/js/main.js', 94 | 'src/js/DEFS.js', 95 | 'src/js/**/*.js', 96 | 'src/js/start_GEN.js', 97 | ] 98 | } 99 | }, 100 | prod: { 101 | files: { 102 | 'dist/index.html': [ 103 | 'src/html/index_prod.html' 104 | ], 105 | 'dist/js/index_prod.js': [ 106 | 'dist/lib/engine.all.release.js', 107 | 'dist/js/index.js' 108 | ] 109 | } 110 | } 111 | }, 112 | }); 113 | 114 | // These plugins provide necessary tasks. 115 | grunt.loadNpmTasks('grunt-contrib-watch'); 116 | 117 | grunt.registerTask('rollup', 'combine html and js', function () { 118 | 119 | let src = grunt.file.read('dist/i.roadrolled.js'); 120 | 121 | grunt.file.write('dist/index.html', ''); 122 | 123 | }); 124 | 125 | grunt.registerTask('processMap', 'get map data from Tiled', function () { 126 | 127 | // ADD MAPS HERE! 128 | let maps = ["0", "1", "2"]; 129 | 130 | let str = '// THIS FILE IS GENERATED BY THE BUILD SCRIPT\n'; 131 | str += 'let mapData = [];\n'; 132 | 133 | for (let i = 0; i < maps.length; i++) { 134 | let mapJson = grunt.file.readJSON('src/maps/' + maps[i] + '.json'); 135 | str += 'mapData[' + i + '] = { w: ' + mapJson.width + ', h: ' + mapJson.height + ', data: [' 136 | + mapJson.layers[0].data.toString().replaceAll(',0,', ',,').replaceAll(',0,', ',,') + '] }; \n'; 137 | } 138 | 139 | str += '\ninit();' 140 | 141 | grunt.file.write('src/js/start_GEN.js', str); 142 | }); 143 | 144 | grunt.registerTask('dev', [ 145 | 'watch' 146 | ]); 147 | 148 | grunt.registerTask('build', ['clean', 'processMap', 'concat:dev', 'concat:shared', 'image:dev']); 149 | 150 | grunt.registerTask('default', ['build', 'http-server', 'dev']); 151 | 152 | grunt.registerTask('prod', ['clean', 'processMap', 'image:prod', 'concat:shared', 'concat:prod', 'closureCompiler']); 153 | 154 | grunt.registerTask('web', ['http-server', 'dev']); 155 | }; -------------------------------------------------------------------------------- /src/js/DEFS.js: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | const STATE_CLICK_TO_START = 0; 4 | const STATE_PLAYING = 1; 5 | const STATE_DEAD = 2; 6 | const STATE_CLEARED = 3; 7 | 8 | const TILE_SIZE = vec2(12); 9 | const MINI_TILE_SIZE = vec2(4); 10 | const MOB_SIZE = vec2(0.6, 0.8); 11 | const TILES_PER_SCREEN = 13; 12 | 13 | fontDefault = "Courier New"; 14 | 15 | // "Courier"; 16 | // "American Typewriter"; 17 | // "Courier New"; 18 | // "Luminari" 19 | 20 | const g_CHEATMODE = 0; 21 | let g_score = 0; 22 | let g_level = 0; 23 | let g_levelDef = undefined; 24 | 25 | let g_screenShake = vec2(0); 26 | 27 | const tileNumbers_player = 0; 28 | const tileNumbers_zombie = 1; 29 | const tileNumbers_vampire = 2; 30 | const tileNumbers_bat = 3; 31 | const tileNumbers_ghost = 4; 32 | const tileNumbers_beefyZombie = 5; 33 | const tileNumbers_bossPlaceholder = 4; 34 | const tileNumbers_facePlayer = 5; 35 | const tileNumbers_faceZombie = 6; 36 | const tileNumbers_faceVampire = 7; 37 | const tileNumbers_pistol = 15; 38 | const tileNumbers_shotgun = 16; 39 | const tileNumbers_shellIcon = 17; 40 | const tileNumbers_bulletIcon = 18; 41 | const tileNumbers_rifleAmmoIcon = 19; 42 | const tileNumbers_rifle = 20; 43 | const tileNumbers_smg = 25; 44 | const tileNumbers_boxBullets = 21; 45 | const tileNumbers_boxShells = 22; 46 | const tileNumbers_boxRifleAmmo = 23; 47 | const tileNumbers_floorStone = 12; 48 | const tileNumbers_door = 11; 49 | 50 | let g_player = null; 51 | 52 | var g_difficulty = 0; 53 | 54 | var g_enemies = []; 55 | var g_doors = {}; 56 | var g_splatter = []; 57 | var g_holes = []; 58 | //var g_sparks = []; 59 | var g_corpses = []; 60 | var g_shells = []; 61 | var g_moss = []; 62 | var g_shadows = {}; 63 | var g_transforms = []; 64 | 65 | var colorBlack = new Color(0, 0, 0); 66 | var colorWhite = new Color(); 67 | var colorGrey = colorWhite.scale(0.5); 68 | 69 | var colorBlood = new Color(172 / 255, 50 / 255, 50 / 255); 70 | var colorBullet = new Color(217 / 255, 160 / 255, 102 / 255); 71 | //var colorScoreText = new Color(106 / 255, 190 / 255, 48 / 255); 72 | var colorShell = new Color(217 / 255, 87 / 255, 99 / 255); 73 | var colorRifleRound = new Color(99 / 255, 155 / 255, 255 / 255); 74 | 75 | var colorSpark = new Color(251 / 255, 242 / 255, 54 / 255); 76 | 77 | var colorEarth = colorBullet; // new Color(143 / 255, 86 / 255, 59 / 255); 78 | 79 | var colorZombie = new Color(55 / 255, 148 / 255, 110 / 255); 80 | 81 | var g_state = STATE_CLICK_TO_START; 82 | 83 | const miniTileNumbers_miniFacePlayer = 18 * 15 + 0; 84 | const miniTileNumbers_miniFaceZombie = 18 * 15 + 1; 85 | const miniTileNumbers_miniFaceVampire = 18 * 15 + 2; 86 | const miniTileNumbers_miniFaceGhost = 18 * 15 + 3; 87 | const miniTileNumbers_miniFaceBoss = 9; 88 | const miniTileNumbers_miniFaceBossAngry = 19; 89 | const miniTileNumbers_moss = 18 * 15 + 4; 90 | 91 | const mobDefs = { 92 | Zombie: { 93 | hp: 2, 94 | hpGainPerlevel: 0, 95 | maxSpeed: 0.06, 96 | }, 97 | BossZombie: { 98 | hp: 22, 99 | hpGainPerlevel: 5, 100 | maxSpeed: 0.5, 101 | }, 102 | Vampire: { 103 | hp: 1, 104 | hpGainPerlevel: 0, 105 | maxSpeed: 0.2, 106 | addTransformHp: 5, 107 | }, 108 | Ghost: { 109 | hp: 1, 110 | hpGainPerlevel: 0, 111 | maxSpeed: 0.05, 112 | }, 113 | }; 114 | 115 | const levelDefs = [ 116 | { 117 | // start 118 | map: 0, 119 | enemiesToSpawn: 10, 120 | enemiesMaxAlive: 3, 121 | spawns: [{ chance: 1 }], 122 | }, 123 | { 124 | // graveyard 125 | map: 1, 126 | enemiesToSpawn: 12, 127 | enemiesMaxAlive: 3, 128 | spawns: [{ chance: 1 }], 129 | }, 130 | { 131 | // intro to vampires 132 | map: 3, 133 | enemiesToSpawn: 15, 134 | enemiesMaxAlive: 4, 135 | spawns: [{ chance: 0.5 }, { type: "v", chance: 0.5 }], 136 | }, 137 | { 138 | // intro to ghosts 139 | map: 4, 140 | enemiesToSpawn: 15, 141 | enemiesMaxAlive: 4, 142 | spawns: [{ chance: 0.5 }, { type: "g", chance: 0.5 }], 143 | }, 144 | { 145 | // final boss 146 | map: 2, 147 | enemiesToSpawn: Infinity, 148 | enemiesMaxAlive: 5, 149 | spawns: [{ chance: 0.4 }, { type: "v", chance: 0.3 }, { type: "g", chance: 0.3 }], 150 | }, 151 | ]; 152 | -------------------------------------------------------------------------------- /src/js/mob_enemy.js: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | class Enemy extends Mob { 4 | constructor(pos, size, tileIndex, mobDef) { 5 | super(pos, size, tileIndex); 6 | 7 | this._maxSpeed = mobDef.maxSpeed + mobDef.maxSpeed * 0.1 * g_difficulty; 8 | this.hp = mobDef.hp + Math.floor(g_difficulty * mobDef.hpGainPerlevel); 9 | 10 | //this.enemyThinkPause = 0; 11 | this.enemyThinkMin = 20; 12 | this.enemyThinkMax = 100; 13 | this.enemyThinkPause = rand(this.enemyThinkMin, this.enemyThinkMax); 14 | this.enemyAccel = 0.1; 15 | this.enemyJitterForce = 0.01; 16 | this.enemyDrag = 1.5; 17 | this.enemyToTarget = undefined; 18 | this.lastTilePos = undefined; 19 | 20 | this.soundGroan = soundEnemyGroan; 21 | } 22 | 23 | update() { 24 | // think and look 25 | if (this.enemyThinkPause-- <= 0) { 26 | this.enemyToTarget = g_player.pos.subtract(this.pos); 27 | this.enemyThinkPause = rand(this.enemyThinkMin, this.enemyThinkMax); 28 | this.groan(0.3, rand(0.9, 1.2)); 29 | } 30 | 31 | // take a step 32 | if (rand() < 0.1) { 33 | let force = vec2(0); 34 | if (this.enemyToTarget) force = this.enemyToTarget.normalize(this.enemyAccel); 35 | 36 | let jitter = randInCircle(this.enemyJitterForce); 37 | force = force.add(jitter); 38 | 39 | this.applyForce(force); 40 | } 41 | 42 | this.applyDrag(this.enemyDrag); 43 | this.velocity = this.velocity.clampLength(this._maxSpeed); 44 | 45 | super.update(); 46 | } 47 | 48 | collideWithTile(tileData, tilePos) { 49 | if (this.lastTilePos && tilePos.distanceSquared(this.lastTilePos) < 0.01) { 50 | if (rand() < 0.1) { 51 | let colPos = tilePos.add(vec2(0.5)); 52 | colPos = colPos.lerp(this.pos, 0.5); 53 | pushers.push(new Pusher(colPos, 0.02, 0, rand(1, 2), rand(1, 2), PushTo.ENEMIES)); 54 | } 55 | } 56 | 57 | this.lastTilePos = tilePos.copy(); 58 | return true; 59 | } 60 | 61 | collideWithObject(o) { 62 | if (o instanceof Enemy) { 63 | const TOO_CLOSE = 0.7; 64 | 65 | let toOther = o.pos.subtract(this.pos); 66 | if (toOther.length() < TOO_CLOSE) { 67 | let pushForce = toOther.normalize(rand(0.1) / (toOther.length() + 0.001)); 68 | o.applyForce(pushForce); 69 | this.groan(0.1, 0.9, 1); 70 | } 71 | } 72 | 73 | if (o instanceof MobPlayer) { 74 | // eating the corpse 75 | this.groan(0.1, 0.3, rand(2, 3)); 76 | //vibrate(200); 77 | } 78 | 79 | if (o instanceof Boulder) { 80 | if (o.isThrown) this.hit(o.velocity, o.pos, 1); 81 | } 82 | 83 | return false; 84 | } 85 | 86 | hit(velocity, pos, dam) { 87 | if (this.hp <= 0) return; 88 | 89 | this.hp -= dam; 90 | this.groan(1, 2, rand(1, 1.5)); 91 | 92 | this.applyForce(velocity.scale(1 + dam)); 93 | 94 | this.bloodEmitter = makeParticles(this.pos, rand(dam / 4, dam / 2)); 95 | 96 | for (let i = dam; i--; ) fx_splatter(pos); 97 | 98 | if (this.hp <= 0) { 99 | this.groan(1, 3, rand(2, 2.5), 2 + rand(3)); 100 | 101 | let corpse = new Corpse(this.pos.copy(), this.size.copy(), this.tileIndex, this.tileSize); 102 | corpse.pushCorpse(velocity /* 1 + g_difficulty */); 103 | g_corpses.push(corpse); 104 | 105 | let i = g_enemies.indexOf(this); 106 | g_enemies.splice(i, 1); 107 | 108 | if (this instanceof BossZombie) { 109 | // kill all enemies and complete level 110 | 111 | while (g_enemies.length > 0) { 112 | g_enemies[0].hp = 1; 113 | g_enemies[0].hit(velocity, g_enemies[0].pos, 1); 114 | } 115 | 116 | Boulder.destroyAllBoulders(); 117 | 118 | g_score += 10; 119 | levelCleared = true; 120 | } 121 | 122 | this.destroy(); 123 | 124 | g_score++; 125 | return true; 126 | } 127 | 128 | // splatter on mob 129 | for (let d = dam; d--; ) { 130 | let wound = { pos: pos.subtract(this.pos).scale(0.33), pattern: [] }; 131 | wound.pos = wound.pos.add(randInCircle(0.2)); 132 | 133 | for (let i = 4; i--; ) wound.pattern.push(rand(2) | 0); 134 | this.blood.push(wound); 135 | } 136 | return false; 137 | } 138 | 139 | drawReachingArms() { 140 | const armLenght = 4 / 12; 141 | 142 | let toPlayer = this.enemyToTarget || g_player.pos.subtract(this.pos); 143 | let toPlayerAngle = toPlayer.angle(); 144 | 145 | this.pointingAngle += turnTowards(toPlayerAngle - this.pointingAngle, (2 * PI) / 100); //turnTowards(this.pointingAngle, toPlayerAngle, rand(2 * PI) / 100); 146 | let pointing = vec2(1).setAngle(this.pointingAngle, armLenght); 147 | 148 | // draw arms 149 | const chestWidth = 3 / 12; 150 | const chestHeight = 1.5 / 12; 151 | 152 | let pos = this.pos.add(vec2(chestWidth, chestHeight)); 153 | drawLine(pos, pos.add(pointing), 1.2 / 12, this._armColor, !!glEnable); 154 | pos = this.pos.add(vec2(-chestWidth, chestHeight)); 155 | drawLine(pos, pos.add(pointing), 1.2 / 12, this._armColor, !!glEnable); 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /src/js/ui.js: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | // this draws the ui on top of everything (in theory) 4 | 5 | var ui_fadeTarget = 0; 6 | var ui_fade = 1; 7 | var ui_onFaded = undefined; 8 | 9 | const ui_clearColors = [ 10 | [0.5, 0, 0, 0.25], 11 | [0.25, 0.25, 0, 0.25], 12 | [0.5, 0.5, 0, 0.25], 13 | [0, 0, 0.5, 0.25], 14 | [0, 0.5, 0.5, 0.25], 15 | ]; 16 | 17 | const ui_fadeCol = new Color(0.2, 0, 0); // when screen is all faded out ! 18 | 19 | function uiFadeOutAndCall(fadeFunc) { 20 | if (!ui_onFaded) { 21 | ui_onFaded = fadeFunc; 22 | ui_fadeTarget = 1; 23 | } 24 | } 25 | 26 | function uiFading() { 27 | // fade 28 | const FADE_TIME = 0.5; 29 | const fadeSpeed = 1 / (FADE_TIME * 60); 30 | let dFade = ui_fadeTarget == 0 ? -fadeSpeed / 1.5 : fadeSpeed; // fading in looks slower somehow ... strange but true 31 | 32 | ui_fade = clamp(ui_fade + dFade, 0, 1); 33 | 34 | if (ui_fade == 1) { 35 | ui_fadeTarget = 0; 36 | if (ui_onFaded) { 37 | ui_onFaded(); 38 | ui_onFaded = undefined; 39 | } 40 | } 41 | } 42 | 43 | var ui_flashColor = undefined; 44 | var ui_flashFrames = 0; 45 | 46 | function uiflashScreen(colorString, frames) { 47 | ui_flashColor = colorString; 48 | ui_flashFrames = frames; 49 | } 50 | 51 | function gameRenderPost() { 52 | // called after objects are rendered 53 | // draw effects or hud that appear above all objects 54 | scaleCameraToScreenSize(); 55 | 56 | if (g_player) { 57 | let pos = vec2(0); 58 | 59 | mapMan.renderFOW(); 60 | 61 | // scary transforms 62 | for (let i = 0; i < g_transforms.length; i++) { 63 | let trans = g_transforms[i]; 64 | drawTile(trans.pos, vec2((60 - trans.life) / 6), trans.tileIndex, TILE_SIZE, colorWhite.scale(0.15)); 65 | trans.life--; 66 | if (trans.life <= 0) { 67 | g_transforms.splice(i, 1); 68 | i--; 69 | } 70 | } 71 | 72 | // score texts 73 | // for (let i = 0; i < g_corpses.length; i++) { 74 | // g_corpses[i].postRender(); 75 | // } 76 | 77 | // make sure UI can fit onto screen 78 | let scaleUI = min(1, overlayCanvas.width / (12 * cameraScale)); 79 | 80 | pos = vec2( 81 | cameraPos.x - overlayCanvas.width / (cameraScale * 2) + 6 * scaleUI, 82 | cameraPos.y + overlayCanvas.height / (cameraScale * 2) - scaleUI 83 | ); 84 | 85 | // UI background 86 | drawRect(pos, vec2(8, 2).scale(scaleUI), colorWhite.scale(0.4)); 87 | 88 | // portrait 89 | let scaleX = frame % 240 > 200 ? -2 : 2; 90 | drawTile( 91 | vec2(pos.x - 5 * scaleUI, pos.y), 92 | vec2(scaleX, 2).scale(scaleUI), 93 | tileNumbers_facePlayer, 94 | TILE_SIZE, 95 | colorWhite.scale(0.7) 96 | ); 97 | 98 | // total ammo 99 | const rectCol = colorWhite.scale(0.2, 1); // new Color(0.2, 0.2, 0.2); 100 | const boxSize = vec2(1.5, 0.5).scale(scaleUI); 101 | drawRect(pos.subtract(vec2(3 * scaleUI, -0.6 * scaleUI)), boxSize, rectCol); 102 | drawRect(pos.subtract(vec2(3 * scaleUI, 0)), boxSize, rectCol); 103 | drawRect(pos.subtract(vec2(3 * scaleUI, 0.6 * scaleUI)), boxSize, rectCol); 104 | 105 | const iconSize = vec2(0.45).scale(scaleUI); 106 | drawTile(pos.subtract(vec2(3.5 * scaleUI, -0.6 * scaleUI)), iconSize, tileNumbers_bulletIcon, TILE_SIZE); 107 | drawTile(pos.subtract(vec2(3.5 * scaleUI, 0)), iconSize, tileNumbers_rifleAmmoIcon, TILE_SIZE); 108 | drawTile(pos.subtract(vec2(3.5 * scaleUI, 0.6 * scaleUI)), iconSize, tileNumbers_shellIcon, TILE_SIZE); 109 | 110 | const txtSize = 0.4 * scaleUI; 111 | const txtCol = colorWhite.scale(0.9); // new Color(0.9, 0.9, 0.9); 112 | const txtDx = 2.5 * scaleUI; 113 | let y = pos.y - 0.025; 114 | 115 | drawText(g_player.ammoBullets, vec2(pos.x - txtDx, y + 0.6 * scaleUI), txtSize, txtCol, -1, undefined, "right"); 116 | drawText(g_player.ammoRifle, vec2(pos.x - txtDx, y), txtSize, txtCol, -1, undefined, "right"); 117 | drawText(g_player.ammoShells, vec2(pos.x - txtDx, y - 0.6 * scaleUI), txtSize, txtCol, -1, undefined, "right"); 118 | 119 | // ammo 120 | if (g_player.gun) { 121 | for (let i = 0; i < g_player.gun._maxAmmo; i++) { 122 | drawTile( 123 | vec2(pos.x - 1.4 * scaleUI + i * 0.95 * scaleUI, pos.y), 124 | vec2(scaleUI), 125 | g_player.gun._ammoIconTile, 126 | TILE_SIZE, 127 | i + 1 > g_player.gun.ammo ? colorWhite.scale(0.2) : colorWhite 128 | ); 129 | } 130 | } 131 | 132 | // score 133 | drawTextWithOutline( 134 | g_score.toString(), 135 | vec2(overlayCanvas.width - 2 * cameraScale * scaleUI, cameraScale * scaleUI), 136 | cameraScale * 2, 137 | colorBlood 138 | ); 139 | } 140 | 141 | textsDraw(); 142 | 143 | // FADE THE SCREEN 144 | 145 | let ui_clearCol = new Color(...ui_clearColors[g_level % levelDefs.length]); 146 | 147 | let col = ui_clearCol.lerp(ui_fadeCol, ui_fade); 148 | 149 | let color = col.getHex(); 150 | let alpha = col.a; 151 | 152 | if (ui_flashFrames > 0) { 153 | ui_flashFrames--; 154 | alpha = 0.5; 155 | color = ui_flashColor; 156 | } 157 | 158 | overlayContext.rect(0, 0, mainCanvasSize.x, mainCanvasSize.y); 159 | overlayContext.globalAlpha = alpha; 160 | overlayContext.fillStyle = color; 161 | overlayContext.fill(); 162 | 163 | //overlayContext.globalAlpha = 0.5; 164 | 165 | drawPushers(); 166 | } 167 | -------------------------------------------------------------------------------- /src/js/mapmanager.js: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | var tileLayer; 4 | var playerSpawn; 5 | 6 | class MapManager { 7 | constructor() { 8 | this.createMap(); 9 | } 10 | 11 | createMap() { 12 | let theMap = mapData[g_levelDef.map]; 13 | let w = theMap.w; 14 | let h = theMap.h; 15 | 16 | g_doors = {}; 17 | 18 | // TileLayer is an EngineObject and will render with the other engineObjects (and respect renderOrder if given) 19 | tileLayer = new TileLayer(vec2(0), vec2(w, h), TILE_SIZE); 20 | initTileCollision(vec2(w, h)); 21 | 22 | for (let y = 0; y < h; y++) { 23 | for (let x = 0; x < w; x++) { 24 | // floor 25 | let floorTile = new TileLayerData( 26 | tileNumbers_floorStone, 27 | randInt(4), 28 | false, 29 | new Color(1, 1, 1, rand(0.2, 0.5)) 30 | ); 31 | tileLayer.setData(vec2(x, h - 1 - y), floorTile); 32 | 33 | let t = theMap.data[x + y * w]; 34 | if (t) { 35 | t -= 1; 36 | 37 | let offsetVec = vec2(x + 0.5, h - 1 - y + 0.5); 38 | 39 | if (t == tileNumbers_player) { 40 | playerSpawn = offsetVec; 41 | continue; 42 | } else if (t == tileNumbers_pistol) { 43 | new Pistol(offsetVec); 44 | continue; 45 | } else if (t == tileNumbers_shotgun) { 46 | new Shotgun(offsetVec); 47 | continue; 48 | } else if (t == tileNumbers_rifle) { 49 | new Rifle(offsetVec); 50 | continue; 51 | } else if (t == tileNumbers_smg) { 52 | new MachinePistol(offsetVec); 53 | continue; 54 | } else if (t == tileNumbers_bossPlaceholder) { 55 | g_enemies.push(new BossZombie(offsetVec)); 56 | continue; 57 | } else if (t == tileNumbers_boxBullets) { 58 | new AmmoBox(offsetVec, tileNumbers_pistol); 59 | continue; 60 | } else if (t == tileNumbers_boxShells) { 61 | new AmmoBox(offsetVec, tileNumbers_shotgun); 62 | continue; 63 | } else if (t == tileNumbers_boxRifleAmmo) { 64 | new AmmoBox(offsetVec, tileNumbers_rifle); 65 | continue; 66 | } 67 | 68 | if (t == tileNumbers_door) { 69 | g_doors[x + "_" + (h - 1 - y)] = { hp: 3 }; 70 | } else { 71 | // pushers on all collision stuff except doors 72 | pushers.push(new Pusher(offsetVec, 0.01, 0.5, 1.5, 0, PushTo.ALL)); 73 | 74 | // moss (not on doors) 75 | g_moss.push({ 76 | pos: offsetVec.add(randInCircle(5 / 12)), 77 | tileIndex: miniTileNumbers_moss + randInt(11), 78 | angle: rand(PI * 2), 79 | }); 80 | } 81 | 82 | setTileCollisionData(vec2(x, h - 1 - y), t); 83 | /*let tint = new Color(rand(0.8, 1), rand(0.8, 1), rand(0.8, 1)); 84 | if (g_level % 2 == 0) { 85 | // brown houses 86 | tint = tint.add(new Color(217 / 255, 160 / 255, 102 / 255)); 87 | } 88 | if (g_level % mapData.length == 2 || g_level % mapData.length == 3) { 89 | // swap wall and roof tiles 90 | if (t == 9) t = 13; 91 | else if (t == 13) t = 9; 92 | else if (t == 14) t = 24; 93 | else if (t == 24) t = 14; 94 | }*/ 95 | // roofs and hedgdetops 96 | try { 97 | if ((t == 14 || t == 9) && t == theMap.data[x + (y + 1) * w] - 1) { 98 | t = t == 14 ? 24 : 13; 99 | } 100 | } catch (ex) {} // off edge of map 101 | 102 | let tld = new TileLayerData(t, 0, t == 24 ? false : rand() < 0.5); 103 | tileLayer.setData(vec2(x, h - 1 - y), tld); 104 | } 105 | } 106 | } 107 | 108 | // Draw the whole tilemap to the offscreen buffer 109 | tileLayer.redraw(); 110 | } 111 | 112 | render() { 113 | tileLayer.renderNow(); 114 | } 115 | 116 | // STUPID FOG OF WAR / LINE OF SIGHT 117 | renderFOW() { 118 | let theMap = mapData[g_levelDef.map]; 119 | 120 | let pos = vec2(0); 121 | for (let x = 0; x < theMap.w; x++) { 122 | for (let y = 0; y < theMap.h; y++) { 123 | let cx = x + 0.5; 124 | let cy = y + 0.5; 125 | // check if this tile is onscreen 126 | if ( 127 | abs(cx - cameraPos.x) - 1 > overlayCanvas.width / (cameraScale * 2) || 128 | abs(cy - cameraPos.y) - 1 > overlayCanvas.height / (cameraScale * 2) 129 | ) { 130 | continue; 131 | } 132 | 133 | let dVec = vec2(g_player.pos.x - cx, g_player.pos.y - cy); 134 | dVec = dVec.clampLength(min(1.5, dVec.length())); 135 | pos.x = cx + dVec.x; 136 | pos.y = cy + dVec.y; 137 | let pos2 = tileCollisionRaycast(g_player.pos, pos); 138 | // if collision and the collision is not this tile 139 | if (pos2 && !(pos2.x == cx && pos2.y == cy)) { 140 | let shadow = g_shadows[x + "_" + y] || { 141 | x: cx, 142 | y: cy, 143 | alpha: 1, 144 | }; 145 | shadow.alpha = min(1, shadow.alpha + 0.1); 146 | 147 | g_shadows[x + "_" + y] = shadow; 148 | //drawRect(pos, vec2(0.1), new Color(1, 0, 0)); 149 | } else { 150 | //drawRect(pos, vec2(0.1), new Color(0, 1, 0)); 151 | } 152 | } 153 | } 154 | 155 | const shadowSize = vec2(1.01); 156 | let color = colorBlack.copy(); 157 | for (let key in g_shadows) { 158 | let shadow = g_shadows[key]; 159 | // fade 160 | shadow.alpha -= (0.01 * 60) / frameRate; 161 | if (shadow.alpha <= 0) { 162 | delete g_shadows[key]; 163 | } else { 164 | pos.x = shadow.x; 165 | pos.y = shadow.y; 166 | color.a = shadow.alpha; 167 | drawRect(pos, shadowSize, color); 168 | } 169 | } 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /src/js/gun.js: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | class Gun extends EngineObject { 4 | constructor(pos, tileIndex) { 5 | super(pos, vec2(1), tileIndex, TILE_SIZE); 6 | // your object init code here 7 | this._distance = 0.7; 8 | this._mysize = this.size.y; 9 | this._speed = 0.4; 10 | 11 | this._maxAmmo = 6; 12 | this._ammoIconTile = tileNumbers_bulletIcon; 13 | this._hitbox = vec2(0.4); 14 | 15 | this.ammo = this._maxAmmo; 16 | this.reloading = false; 17 | this.reloadTimer = undefined; 18 | this.reloadTimePerBullet = 0.25; 19 | 20 | this._soundFire = undefined; 21 | this.noExtraAmmo = false; 22 | 23 | this.soundReload = soundGunReload; 24 | this.soundEmpty = soundGunEmpty; 25 | 26 | this.autoFire = false; 27 | 28 | this.spread = 0; 29 | } 30 | 31 | update() { 32 | if (this.spread > 0) this.spread /= 1.1; 33 | 34 | if (this.owner && this.owner.hp > 0) { 35 | // key r or space 36 | if (isTouchDevice && gamepadWasPressed(0)) { 37 | this.reload(); 38 | return; 39 | } else if (keyWasReleased(82) || keyWasReleased(32)) { 40 | this.reload(); 41 | return; 42 | } 43 | 44 | let angle = -this.angle; 45 | 46 | if (isUsingGamepad) { 47 | if (gamepadStick(1).length() > 0.1) { 48 | angle = Math.atan2(gamepadStick(1).y, gamepadStick(1).x); 49 | } 50 | } else if (!isTouchDevice) { 51 | // use mouse position 52 | angle = Math.atan2(mousePos.y - this.owner.pos.y, mousePos.x - this.owner.pos.x); 53 | } 54 | 55 | this.pos.x = this.owner.pos.x + this._distance * Math.cos(angle); 56 | this.pos.y = this.owner.pos.y + this._distance * Math.sin(angle); 57 | 58 | this.angle = -angle; 59 | this.size.y = abs(this.angle) > PI / 2 ? -this._mysize : this._mysize; 60 | 61 | if (g_state == STATE_PLAYING) { 62 | var triggerPulled = isTouchDevice ? gamepadWasReleased(1) || keyWasPressed(13) : mouseWasPressed(0); 63 | 64 | var triggerIsHeld = isTouchDevice ? gamepadIsDown(1) || keyIsDown(13) : mouseIsDown(0); 65 | 66 | this.angle += (Math.random() - 0.5) * this.spread; 67 | 68 | if (this.autoFire) { 69 | if (triggerIsHeld) this.fire(); 70 | } else { 71 | if (triggerPulled) this.fire(); 72 | } 73 | } 74 | 75 | if (this.reloading) { 76 | if (this.reloadTimer.elapsed()) { 77 | this.noExtraAmmo = false; 78 | 79 | if (this.tileIndex == tileNumbers_rifle) { 80 | this.noExtraAmmo = g_player.ammoRifle <= 0; 81 | g_player.ammoRifle = max(0, g_player.ammoRifle - 1); 82 | } else if (this.tileIndex == tileNumbers_shotgun) { 83 | this.noExtraAmmo = g_player.ammoShells <= 0; 84 | g_player.ammoShells = max(0, g_player.ammoShells - 1); 85 | } else { 86 | // pistol 87 | this.noExtraAmmo = g_player.ammoBullets <= 0; 88 | g_player.ammoBullets = max(0, g_player.ammoBullets - 1); 89 | } 90 | 91 | if (!this.noExtraAmmo) { 92 | this.soundReload.play(); 93 | this.ammo = min(this._maxAmmo, this.ammo + 1); 94 | this.reloadTimer.set(this.reloadTimePerBullet); 95 | } 96 | if (this.ammo == this._maxAmmo || this.noExtraAmmo) { 97 | this.reloadTimer.unset(); 98 | this.reloading = false; 99 | } 100 | } 101 | } 102 | 103 | if (this.ammo <= 0 && !this.reloading) { 104 | //this.soundEmpty.play(); 105 | this.reload(); 106 | } 107 | } else if (!this.owner) { 108 | // look for owner 109 | 110 | let playerTouch = isOverlapping(this.pos, this._hitbox, g_player.pos, g_player._hitbox); 111 | 112 | if (g_player.hp > 0 && playerTouch && !this.playerTouchLastFrame) { 113 | this.setOwner(g_player); 114 | } 115 | 116 | this.playerTouchLastFrame = playerTouch; 117 | } 118 | 119 | super.update(); // update object physics and position 120 | } 121 | 122 | render() { 123 | // draw laser 124 | if (this.owner && (isTouchDevice || this.tileIndex == tileNumbers_rifle) && !this.reloading && rand() > 0.4) { 125 | const laserLength = 100; 126 | var laserEndPoint = vec2(); 127 | 128 | laserEndPoint = g_player.pos.add(vec2(laserLength * Math.cos(-this.angle), laserLength * Math.sin(-this.angle))); 129 | 130 | let hitPoint = tileCollisionRaycast(g_player.pos, laserEndPoint); 131 | let distToHit = hitPoint.subtract(g_player.pos).length() - rand(1, 1.2); 132 | 133 | laserEndPoint = this.pos.add(vec2(distToHit * Math.cos(-this.angle), distToHit * Math.sin(-this.angle))); 134 | 135 | drawLine(this.pos, laserEndPoint, 0.02, colorBlood); 136 | } 137 | 138 | super.render(); 139 | } 140 | 141 | setOwner(player) { 142 | if (player.gun) { 143 | // throw current gun 144 | player.gun.size.y = this._mysize; 145 | player.gun.angle = 0; 146 | player.gun.owner = null; 147 | player.gun.pos = this.pos.copy(); 148 | soundPickup.play(this.pos); 149 | } 150 | this.owner = player; 151 | player.gun = this; 152 | } 153 | 154 | fire(color) { 155 | musicResume(); 156 | if (this.reloading || this.ammo <= 0) { 157 | this.soundEmpty.play(this.pos); 158 | return false; 159 | } 160 | 161 | this.spread += 0.35; 162 | 163 | !isTouchDevice && uiflashScreen("#fff", 1); 164 | 165 | //fx.shakeScreen(0.1); 166 | fx_addSpark(this.pos.add(this.pos.subtract(this.owner.pos).normalize(1 - this._distance))); 167 | 168 | this.ammo--; 169 | 170 | const shotVol = 3; 171 | 172 | // playing the sound multiple times to make it "bigger" and louder 173 | this._soundFire.play(this.pos, shotVol, 0.5); 174 | this._soundFire.play(this.pos, shotVol, 1); 175 | this._soundFire.play(this.pos, shotVol, 1.02); 176 | 177 | // eject shell 178 | g_shells.push({ 179 | pos: this.pos.copy(), 180 | velocity: vec2(rand(-1 / 30, 1 / 30), 1.1 / 12), 181 | angularVelocity: 0.3, 182 | angle: 0, 183 | color: color, 184 | life: randInt(20, 40), 185 | }); 186 | 187 | return true; 188 | } 189 | 190 | reload() { 191 | // if (g_CHEATMODE) this.reloadTimePerBullet = 0.1; 192 | 193 | if (this.reloading || this.ammo == this._maxAmmo) { 194 | return; 195 | } 196 | 197 | this.reloadTimer = new Timer(this.reloadTimePerBullet); 198 | this.reloading = true; 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /src/js/main.js: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | var mapMan; 4 | 5 | function init() { 6 | if (!document.body) { 7 | // html document is not ready yet (safari mobile) 8 | return setTimeout(init, 100); 9 | } 10 | 11 | // generate new maps 12 | generateMapFromLevel(0); 13 | generateMapFromLevel(1); 14 | 15 | // startup LittleJS with your game functions after the tile image is loaded 16 | engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRenderPost, "t.png"); 17 | } 18 | 19 | function gameInit() { 20 | scaleCameraToScreenSize(); 21 | document.body.style.cursor = "crosshair"; 22 | 23 | touchGamepadEnable = 1; 24 | touchGamepadAnalog = 1; 25 | //vibrateEnable = 1; 26 | startNewGame(); 27 | } 28 | 29 | function scaleCameraToScreenSize() { 30 | // try to fit same tiles on a screen 31 | let tiles = TILES_PER_SCREEN; 32 | 33 | // smaller on mobile 34 | //if (isTouchDevice) tiles = tiles - 3; 35 | 36 | cameraScale = min(window.innerWidth, window.innerHeight) / tiles; 37 | 38 | touchGamepadSize = (80 * cameraScale) / 32; 39 | } 40 | 41 | function startNewGame() { 42 | g_score = 0; 43 | g_level = 0; 44 | g_player = null; 45 | } 46 | 47 | function startNextLevel() { 48 | // save gun and ammo 49 | let ammoPistol = g_player ? g_player.ammoBullets : 12; 50 | let ammoShotgun = g_player ? g_player.ammoShells : 0; 51 | let ammoRifle = g_player ? g_player.ammoRifle : 0; 52 | let currentGun = g_player ? g_player.gun.tileIndex : tileNumbers_pistol; // default 53 | let gunAmmo = g_player ? g_player.gun.ammo : 6; 54 | 55 | /////////////////// 56 | // Clean up 57 | 58 | clearPushers(); 59 | 60 | g_player = undefined; 61 | 62 | g_moss = []; 63 | g_shadows = {}; 64 | g_enemies = []; 65 | 66 | g_splatter = []; 67 | g_holes = []; 68 | // g_sparks = []; 69 | g_corpses = []; 70 | g_shells = []; 71 | 72 | enemiesSpawned = 0; 73 | 74 | engineObjectsDestroy(); // destroy all objects handled by the engine 75 | 76 | ///////////////////// 77 | // Setup new level 78 | 79 | g_levelDef = levelDefs[g_level % levelDefs.length]; 80 | 81 | mapMan = new MapManager(); 82 | 83 | g_player = new MobPlayer(playerSpawn); 84 | cameraPos = playerSpawn.copy(); 85 | 86 | if (g_level >= 5 && g_level % 5 == 0) { 87 | new MachinePistol(g_player.pos.add(vec2(1, 1))); 88 | } 89 | 90 | // give player saved equipment 91 | g_player.ammoBullets = ammoPistol; 92 | g_player.ammoShells = ammoShotgun; 93 | g_player.ammoRifle = ammoRifle; 94 | let theGun; 95 | if (currentGun == tileNumbers_smg) { 96 | theGun = new MachinePistol(g_player.pos); 97 | } else if (currentGun == tileNumbers_rifle) { 98 | theGun = new Rifle(g_player.pos); 99 | } else if (currentGun == tileNumbers_shotgun) { 100 | theGun = new Shotgun(g_player.pos); 101 | } else { 102 | theGun = new Pistol(g_player.pos); 103 | } 104 | theGun.ammo = gunAmmo; 105 | 106 | levelCleared = false; 107 | 108 | musicStart(); 109 | } 110 | 111 | function findFreePos(minDistToPlayer) { 112 | let pos, dist2player, inTileCol; 113 | 114 | do { 115 | pos = vec2(rand(mapData[g_levelDef.map].w), rand(mapData[g_levelDef.map].h)); 116 | pos.x = Math.floor(pos.x) + 0.5; 117 | pos.y = Math.floor(pos.y) + 0.5; 118 | 119 | dist2player = pos.distance(g_player.pos); 120 | inTileCol = tileCollisionTest(pos, vec2(1)); 121 | } while (dist2player < minDistToPlayer || inTileCol); 122 | 123 | return pos; 124 | } 125 | 126 | var enemiesSpawned = 0; 127 | function spawnEnemy() { 128 | var p = findFreePos(5); 129 | let enemyClass = getNextEnemySpawnClass(); 130 | 131 | let enemy; 132 | if (enemyClass == "v") { 133 | enemy = new Vampire(p); 134 | } else if (enemyClass == "g") { 135 | enemy = new Ghost(p); 136 | } else { 137 | enemy = new Zombie(p); 138 | } 139 | g_enemies.push(enemy); 140 | enemiesSpawned++; 141 | } 142 | 143 | function gameUpdate() { 144 | uiFading(); 145 | 146 | if (g_state == STATE_CLICK_TO_START) { 147 | updateStateClickToStart(); 148 | } else if (g_state == STATE_PLAYING) { 149 | updateStatePlaying(); 150 | } else if (g_state == STATE_DEAD) { 151 | updateStateDead(); 152 | } else if (g_state == STATE_CLEARED) { 153 | updateStateCleared(); 154 | } 155 | 156 | // CAN BE REMOVED 157 | //mouseWasPressed(2) && toggleFullscreen(); 158 | } 159 | 160 | function uiSound(f = 5) { 161 | for (let i = 0; i < f; i++) { 162 | soundRifle.play(cameraPos.add(vec2(10, 0)), 1, 0.5 + i / 10); 163 | setTimeout(() => soundEnemyGroan.play(cameraPos.add(vec2(-10, 0)), 0.5, 2 + i / 5, 0.5), 300 + i * 50); 164 | } 165 | } 166 | 167 | function updateStateClickToStart() { 168 | drawTile( 169 | cameraPos, 170 | vec2(4), 171 | tileNumbers_faceZombie, 172 | TILE_SIZE, 173 | new Color(1, 1, 1, max(0, 0.2 * Math.sin((frame * PI) / 1000))) 174 | ); 175 | 176 | textTitle = "DEAD AGAIN"; 177 | 178 | if (g_score) { 179 | textMiddle = "Score: " + g_score + " Top: " + localStorage.da_t; 180 | } 181 | 182 | textBottom = "Click to start"; 183 | 184 | if (mouseWasReleased(0) || gamepadWasPressed(0)) { 185 | uiSound(); 186 | if (isTouchDevice && !isFullscreen()) toggleFullscreen(); 187 | uiFadeOutAndCall(() => { 188 | startNewGame(); 189 | startNextLevel(); 190 | changeState(STATE_PLAYING); 191 | }); 192 | } 193 | } 194 | 195 | function updateStateDead() { 196 | textMiddle = "YOU DIED"; 197 | 198 | if (getMsSinceStateChange() > 4000) { 199 | changeState(STATE_CLICK_TO_START); 200 | } 201 | } 202 | 203 | function updateStateCleared() { 204 | textMiddle = "Level " + (g_level + 1) + " cleared"; 205 | 206 | if (getMsSinceStateChange() > 2000) { 207 | textBottom = "Click to continue"; 208 | 209 | if (mouseWasPressed(0) || gamepadWasPressed(0)) { 210 | uiSound(3); 211 | uiFadeOutAndCall(() => { 212 | g_level++; 213 | startNextLevel(); 214 | changeState(STATE_PLAYING); 215 | }); 216 | } 217 | } 218 | } 219 | 220 | var stateChangedTime = new Date().getTime(); 221 | function changeState(newState) { 222 | textsClear(); 223 | stateChangedTime = new Date().getTime(); 224 | g_state = newState; 225 | } 226 | 227 | function getMsSinceStateChange() { 228 | return new Date().getTime() - stateChangedTime; 229 | } 230 | 231 | var ticsToSpawn = 0; 232 | var ammoSpawned; 233 | 234 | var levelCleared = false; 235 | 236 | function updateStatePlaying() { 237 | // game gets more difficult as you play 238 | g_difficulty = 1 + ((g_level / levelDefs.length) | 0); 239 | 240 | // enemies are a tiny bit repulsed by each other ... and thus try to spread out 241 | for (const e of g_enemies) { 242 | pushers.push(new Pusher(e.pos, 0.002, 1, 3, 2 / 60, PushTo.ENEMIES)); 243 | } 244 | 245 | // player leaves foot prints that attracts monsters 246 | if (rand() < 0.1) pushers.push(new Pusher(g_player.pos, -0.001, 0, 5, rand(5), PushTo.ENEMIES)); 247 | 248 | updatePushers(); 249 | 250 | textMiddle = getMsSinceStateChange() > 3000 ? "" : "Level " + (g_level + 1); 251 | 252 | ticsToSpawn--; 253 | 254 | let enemiesToSpawn = (g_levelDef.enemiesToSpawn * (1 + g_difficulty)) / 2; 255 | let enemiesMaxAlive = g_levelDef.enemiesMaxAlive * g_difficulty; 256 | 257 | let enemiesLeft = enemiesToSpawn - enemiesSpawned + g_enemies.length; 258 | //console.log("enemiesLeft", enemiesLeft); 259 | 260 | if (enemiesLeft <= 3) textBottom = enemiesLeft + " left"; 261 | 262 | if (enemiesLeft <= 0) levelCleared = true; 263 | 264 | if (levelCleared) { 265 | changeState(STATE_CLEARED); 266 | g_player.gun.reload(); 267 | //g_level++; 268 | soundPlayExtra(soundLevelCleared, cameraPos, 2, 0.8, 0, 1000); 269 | return; 270 | } 271 | 272 | if (!levelCleared && g_enemies.length < enemiesMaxAlive && enemiesSpawned < enemiesToSpawn && ticsToSpawn <= 0) { 273 | spawnEnemy(); 274 | ticsToSpawn = rand(120); 275 | } 276 | 277 | if (g_player.hp <= 0) { 278 | changeState(STATE_DEAD); 279 | localStorage.da_t = max(g_score, localStorage.da_t || 0); 280 | return; 281 | } 282 | 283 | if (g_player.gun && frame % 150 == 0) { 284 | if (!ammoSpawned && g_player.getAmmoForCurrentGun() == 0) { 285 | // spawn more ammo 286 | if (AmmoBox.getCount() < 4) { 287 | let newAmmo = new AmmoBox(findFreePos(7), g_player.gun.tileIndex); 288 | soundLevelCleared.play(newAmmo.pos, 0.5, 3); 289 | ammoSpawned = true; 290 | } 291 | } else if (g_player.getAmmoForCurrentGun() != 0) { 292 | // allow ammo to spawn again when player is empty 293 | ammoSpawned = false; 294 | } 295 | } 296 | 297 | let newPoint = mousePos; 298 | if (isTouchDevice && g_player.gun) { 299 | // halfway from player to screen 300 | newPoint = g_player.pos.add( 301 | vec2(0) 302 | .setAngle(g_player.gun.angle + PI / 2) 303 | .normalize(TILES_PER_SCREEN - 7) 304 | ); 305 | } 306 | // camera goes halfway between player and mouse 307 | cameraPos = cameraPos.lerp(g_player.pos.add(newPoint.subtract(g_player.pos).scale(0.5)), 0.03); 308 | 309 | fx_updateScreenShake(); 310 | 311 | cameraPos = cameraPos.add(g_screenShake); 312 | 313 | if (g_CHEATMODE && mouseWasPressed(1)) { 314 | g_level++; 315 | startNextLevel(); 316 | changeState(STATE_PLAYING); 317 | soundPlayExtra(soundLevelCleared, cameraPos, 2, 0.8, 0, 1000); 318 | } 319 | } 320 | 321 | function gameUpdatePost() { 322 | // called after physics and objects are updated 323 | // setup camera and prepare for render 324 | } 325 | 326 | var textTitle; 327 | var textMiddle; 328 | var textBottom; 329 | 330 | function textsClear() { 331 | textTitle = undefined; 332 | textMiddle = undefined; 333 | textBottom = undefined; 334 | } 335 | 336 | function drawTextWithOutline(text, pos, size, textColor, outlineColor = colorBlack) { 337 | drawTextScreen(text, pos, size, textColor, size / 15, outlineColor); 338 | } 339 | 340 | function testForMiscount() { 341 | let e = 0; 342 | for (const o of engineObjects) { 343 | if (o instanceof Enemy) e++; 344 | } 345 | 346 | if (e != g_enemies.length) { 347 | debugger; 348 | } 349 | } 350 | 351 | function textsDraw() { 352 | debug && testForMiscount(); 353 | 354 | if (g_CHEATMODE) { 355 | drawTextScreen("CHEAT MODE ON ", vec2(100, 25), 20, colorWhite, 0, undefined, "left"); 356 | drawTextScreen("enemies: " + g_enemies.length, vec2(100, 50), 20, colorWhite, 0, undefined, "left"); 357 | } 358 | 359 | if (textTitle) { 360 | let flicker = (1.5 + Math.sin(frame / 50)) * 0.007; 361 | 362 | for (let i = 0; i < 10; i++) { 363 | drawTextScreen( 364 | textTitle, 365 | vec2( 366 | (rand(1 - flicker, 1 + flicker) * mainCanvas.width) / 2, 367 | (rand(1 - flicker, 1 + flicker) * mainCanvas.height) / 3 368 | ), 369 | mainCanvas.width / 10, 370 | colorBlack.lerp(colorBlood, i / 10) 371 | ); 372 | } 373 | } 374 | 375 | if (textMiddle) { 376 | drawTextWithOutline( 377 | textMiddle, 378 | vec2(mainCanvas.width / 2, mainCanvas.height / 2), 379 | mainCanvas.width / 20, 380 | colorBlood 381 | ); 382 | } 383 | 384 | if (textBottom) { 385 | let amt = 0.5 + Math.sin(frame / 10) / 2; 386 | 387 | if (g_state == STATE_PLAYING) amt = 1; 388 | 389 | //let col = new Color((amt * 172) / 255, (amt * 50) / 255, (amt * 50) / 255); 390 | 391 | let col = colorBlood.scale(amt); 392 | 393 | drawTextWithOutline( 394 | textBottom, 395 | vec2(mainCanvas.width / 2, (4 * mainCanvas.height) / 5), 396 | mainCanvas.width / 30, 397 | col 398 | ); 399 | } 400 | } 401 | 402 | function gameRender() { 403 | mapMan?.render(); 404 | 405 | // called before objects are rendered 406 | // draw any background effects that appear behind objects 407 | 408 | for (let i = 0; i < g_corpses.length; i++) { 409 | g_corpses[i].renderNow(); 410 | } 411 | 412 | // if (g_shells.length > 32) { 413 | // // clean up old casings 414 | // g_shells.splice(0, 1); 415 | // } 416 | for (let i = 0; i < g_shells.length; i++) { 417 | let shell = g_shells[i]; 418 | drawRect(shell.pos, vec2(1 / 12, 2 / 12), shell.color, shell.angle); 419 | if (shell.life > 0) { 420 | shell.pos = shell.pos.add(shell.velocity); 421 | shell.velocity.y -= 1 / 144; 422 | shell.angle += shell.angularVelocity; 423 | shell.life--; 424 | } 425 | } 426 | 427 | // if (g_splatter.length > 1024) { 428 | // // clean up old splatter 429 | // g_splatter.splice(0, 1); 430 | // } 431 | for (let i = 0; i < g_splatter.length; i++) { 432 | for (let j = 0; j < g_splatter[i].pattern.length; j++) { 433 | if (g_splatter[i].pattern[j]) { 434 | let x = g_splatter[i].pos.x - (2 + (j % 4)) / 12; 435 | let y = g_splatter[i].pos.y - (2 + Math.floor(j / 4)) / 12; 436 | drawRect(vec2(x, y), vec2(1.1 / 12), g_splatter[i].color); 437 | } 438 | } 439 | } 440 | 441 | // moss 442 | for (let i = 0; i < g_moss.length; i++) { 443 | let moss = g_moss[i]; 444 | drawTile(moss.pos, vec2(1 / 3), moss.tileIndex, MINI_TILE_SIZE, colorWhite, moss.angle); 445 | } 446 | 447 | // bullet holes 448 | for (let i = 0; i < g_holes.length; i++) { 449 | let hole = g_holes[i]; 450 | drawRect(hole.pos, vec2(hole.size / 12), hole.color); 451 | } 452 | 453 | // // sparks 454 | // for (let i = 0; i < g_sparks.length; i++) { 455 | // let spark = g_sparks[i]; 456 | // spark.pos.x += Math.cos(spark.angle) / 32; 457 | // spark.pos.y += Math.sin(spark.angle) / 32; 458 | // drawRect(spark.pos, vec2(1 / 24), colorSpark); 459 | // if (--spark.life <= 0) { 460 | // g_sparks.splice(i, 1); 461 | // } 462 | // } 463 | 464 | //textsDraw(); 465 | } 466 | --------------------------------------------------------------------------------