├── .gitignore ├── LICENSE ├── README.md ├── dist ├── build.zip └── index.html ├── media ├── logo_large.png ├── logo_large_enlarged.png ├── logo_small.png └── logo_small_enlarged.png ├── package-lock.json ├── package.json ├── postbuild.js ├── src ├── app │ ├── ZzFX.micro.js │ ├── components │ │ ├── HUD.ts │ │ ├── Spawner.ts │ │ ├── TileMap.ts │ │ ├── WaveController.ts │ │ ├── entities │ │ │ ├── BodyPart.ts │ │ │ ├── Hoplite.ts │ │ │ ├── HopliteHead.ts │ │ │ ├── Leonidus.ts │ │ │ ├── Sword.ts │ │ │ ├── enemies │ │ │ │ ├── Athenian.ts │ │ │ │ └── Giant.ts │ │ │ ├── hopliteAnimationConfigs.ts │ │ │ └── veg │ │ │ │ ├── Grass.ts │ │ │ │ └── Tree.ts │ │ ├── scenes │ │ │ ├── GameScene.ts │ │ │ └── TitleScene.ts │ │ └── tiles │ │ │ ├── BaseTile.ts │ │ │ ├── GrassTile.ts │ │ │ ├── WaterTile.ts │ │ │ └── tilePregen.ts │ ├── config.ts │ ├── constants.ts │ ├── core │ │ ├── Animation.ts │ │ ├── Camera.ts │ │ ├── Emitter.ts │ │ ├── GameNode.ts │ │ ├── GameObject.ts │ │ ├── InputController.ts │ │ ├── Particle.ts │ │ ├── Perlin.js │ │ ├── Scene.ts │ │ ├── SimpleCollision │ │ │ └── index.ts │ │ ├── V2.ts │ │ ├── ai │ │ │ ├── Behavior.ts │ │ │ ├── FleeBehavior.ts │ │ │ ├── FlockBehavior.ts │ │ │ ├── SeekBehavior.ts │ │ │ ├── SteeringManager.ts │ │ │ └── WanderBehavior.ts │ │ ├── physics │ │ │ ├── DistanceConstraint.ts │ │ │ ├── PointMass.ts │ │ │ └── shapes │ │ │ │ └── Cloth.ts │ │ └── utils.ts │ ├── main.ts │ └── sounds.ts ├── index.html ├── index.js └── styles │ └── main.css ├── tsconfig.json └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | *node_modules 2 | 3 | .DS_Store 4 | 5 | */dist -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Michael Ferron 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![The Last Spartan](https://github.com/ferronsays/js13k-TheLastSpartan/blob/master/media/logo_small_enlarged.png) 2 | 3 | # The Last Spartan 4 | 5 | Arcade hack n' slash survival game set in ancient Sparta, 404 B.C. An entry for the 2020 js13kgames competition -- themed "404." 6 | 7 | ## Description 8 | 9 | Cut your enemies to pieces and defend your homeland! Can you, the last Spartan Hoplite of your battalion, earn an honorable death across thousands of procedurally generated battlefields? 10 | 11 | No retreat. No surrender. No way out alive. Kill as many Athenians as you can before you meet the same fate. Then try again. 12 | 13 | ![The Last Spartan](https://github.com/ferronsays/js13k-TheLastSpartan/blob/master/media/logo_large_enlarged.png) 14 | 15 | ## Controls 16 | 17 | _For QWERTY keyboards_ 18 | 19 | - Move - W, A, S, D 20 | - Attack - J or Left Mouse Button (LMB) 21 | - Block - K or Right Mouse Button (RMB) 22 | - Jump - Space 23 | - Spartan Charge - K + J / RMB + LMB 24 | - Ground Pound - Space + J / Space + LMB 25 | - Pause - P 26 | 27 | Otherwise, follow the onscreen prompts. 28 | 29 | Rest from battle to regain your health and stamina. Health and stamina are represented onscreen by the red and blue bars, respectively. 30 | 31 | Use your spartan charge and ground pound attacks to stun your enemies and gain the upper hand. 32 | 33 | ## Browser Support 34 | 35 | Chrome latest, Edge latest, Safari latest, FireFox latest 36 | 37 | For best results, use Chrome. 38 | 39 | ## Credits: 40 | 41 | Noise: [NoiseJs by Seph Gentle](https://github.com/josephg/noisejs) 42 | 43 | SFX: [ZzFX by Frank Force](https://github.com/KilledByAPixel/ZzFX) 44 | 45 | 46 | 47 | 48 | ## Running the game 49 | 50 | ``` 51 | npm i 52 | npm start 53 | ``` 54 | -------------------------------------------------------------------------------- /dist/build.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ferronsays/js13k-TheLastSpartan/2fd8dc38a6c0869e895b995b4090d5a9d117fbfe/dist/build.zip -------------------------------------------------------------------------------- /dist/index.html: -------------------------------------------------------------------------------- 1 |
[P] to Resume

YOU DIED

An honorable death.


Kills
Survived
[Enter] to Retry
The Last
Spartan
404 B.C.
No retreat.
No surrender.
No way out alive.

Go down swinging.
[Enter] to Start
by @ferronsays
Move : WASD
Attack : J
Block : K
Jump : SPACE
Pause : P
Spartan Charge : K + J
Ground Pound : SPACE + J
-------------------------------------------------------------------------------- /media/logo_large.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ferronsays/js13k-TheLastSpartan/2fd8dc38a6c0869e895b995b4090d5a9d117fbfe/media/logo_large.png -------------------------------------------------------------------------------- /media/logo_large_enlarged.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ferronsays/js13k-TheLastSpartan/2fd8dc38a6c0869e895b995b4090d5a9d117fbfe/media/logo_large_enlarged.png -------------------------------------------------------------------------------- /media/logo_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ferronsays/js13k-TheLastSpartan/2fd8dc38a6c0869e895b995b4090d5a9d117fbfe/media/logo_small.png -------------------------------------------------------------------------------- /media/logo_small_enlarged.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ferronsays/js13k-TheLastSpartan/2fd8dc38a6c0869e895b995b4090d5a9d117fbfe/media/logo_small_enlarged.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "js13k-404", 3 | "private": true, 4 | "scripts": { 5 | "start": "webpack-dev-server --mode development", 6 | "build": "webpack --mode production", 7 | "postbuild": "node postbuild.js" 8 | }, 9 | "devDependencies": { 10 | "archiver": "^5.0.0", 11 | "closure-webpack-plugin": "^2.3.0", 12 | "css-loader": "^4.0.0", 13 | "google-closure-compiler": "^20200719.0.0", 14 | "html-webpack-inline-source-plugin": "0.0.10", 15 | "html-webpack-plugin": "3.2.0", 16 | "mini-css-extract-plugin": "^0.9.0", 17 | "optimize-css-assets-webpack-plugin": "^5.0.3", 18 | "style-loader": "^1.2.1", 19 | "terser-webpack-plugin": "^4.1.0", 20 | "ts-loader": "^8.0.2", 21 | "typescript": "^3.9.7", 22 | "webpack": "^4.44.0", 23 | "webpack-cli": "^3.3.12", 24 | "webpack-dev-server": "^3.11.0" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /postbuild.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const archiver = require('archiver') 3 | 4 | fs.unlinkSync('./dist/main.js') 5 | fs.unlinkSync('./dist/main.css') 6 | 7 | let output = fs.createWriteStream('./dist/build.zip') 8 | let archive = archiver('zip', { 9 | zlib: { level: 9 } // set compression to best 10 | }) 11 | 12 | const MAX = 13 * 1024 // 13kb 13 | 14 | output.on('close', function () { 15 | const bytes = archive.pointer() 16 | const percent = (bytes / MAX * 100).toFixed(2) 17 | if (bytes > MAX) { 18 | console.error(`Size overflow: ${bytes} bytes (${percent}%)`) 19 | } else { 20 | console.log(`Size: ${bytes} bytes (${percent}%)`) 21 | } 22 | }) 23 | 24 | archive.on('warning', function (err) { 25 | if (err.code === 'ENOENT') { 26 | console.warn(err) 27 | } else { 28 | throw err 29 | } 30 | }) 31 | 32 | archive.on('error', function (err) { 33 | throw err 34 | }) 35 | 36 | archive.pipe(output) 37 | archive.append( 38 | fs.createReadStream('./dist/index.html'), { 39 | name: 'index.html' 40 | } 41 | ) 42 | 43 | archive.finalize() 44 | -------------------------------------------------------------------------------- /src/app/ZzFX.micro.js: -------------------------------------------------------------------------------- 1 | // ZzFX - Zuper Zmall Zound Zynth - Micro Edition 2 | // MIT License - Copyright 2019 Frank Force 3 | // https://github.com/KilledByAPixel/ZzFX 4 | 5 | // This is a tiny build of zzfx with only a zzfx function to play sounds. 6 | // You can use zzfxV to set volume. 7 | // There is a small bit of optional code to improve compatibility. 8 | // Feel free to minify it further for your own needs! 9 | var zzfx, zzfxV, zzfxX, zzfxR; 10 | 11 | // ZzFXMicro - Zuper Zmall Zound Zynth 12 | zzfxV = 0.3; // volume 13 | zzfx = ( // play sound 14 | q = 1, 15 | k = 0.05, 16 | c = 220, 17 | e = 0, 18 | t = 0, 19 | u = 0.1, 20 | r = 0, 21 | F = 1, 22 | v = 0, 23 | z = 0, 24 | w = 0, 25 | A = 0, 26 | l = 0, 27 | B = 0, 28 | x = 0, 29 | G = 0, 30 | d = 0, 31 | y = 1, 32 | m = 0, 33 | C = 0 34 | ) => { 35 | var b = 2 * Math.PI, 36 | H = (v *= (500 * b) / zzfxR ** 2), 37 | I = ((0 < x ? 1 : -1) * b) / 4, 38 | D = (c *= ((1 + 2 * k * Math.random() - k) * b) / zzfxR), 39 | Z = [], 40 | g = 0, 41 | E = 0, 42 | a = 0, 43 | n = 1, 44 | J = 0, 45 | K = 0, 46 | f = 0, 47 | p, 48 | h; 49 | e = 99 + zzfxR * e; 50 | m *= zzfxR; 51 | t *= zzfxR; 52 | u *= zzfxR; 53 | d *= zzfxR; 54 | z *= (500 * b) / zzfxR ** 3; 55 | x *= b / zzfxR; 56 | w *= b / zzfxR; 57 | A *= zzfxR; 58 | l = (zzfxR * l) | 0; 59 | for (h = (e + m + t + u + d) | 0; a < h; Z[a++] = f) 60 | ++K % ((100 * G) | 0) || 61 | ((f = r 62 | ? 1 < r 63 | ? 2 < r 64 | ? 3 < r 65 | ? Math.sin((g % b) ** 3) 66 | : Math.max(Math.min(Math.tan(g), 1), -1) 67 | : 1 - (((((2 * g) / b) % 2) + 2) % 2) 68 | : 1 - 4 * Math.abs(Math.round(g / b) - g / b) 69 | : Math.sin(g)), 70 | (f = 71 | (l ? 1 - C + C * Math.sin((2 * Math.PI * a) / l) : 1) * 72 | (0 < f ? 1 : -1) * 73 | Math.abs(f) ** F * 74 | q * 75 | zzfxV * 76 | (a < e 77 | ? a / e 78 | : a < e + m 79 | ? 1 - ((a - e) / m) * (1 - y) 80 | : a < e + m + t 81 | ? y 82 | : a < h - d 83 | ? ((h - a - d) / u) * y 84 | : 0)), 85 | (f = d 86 | ? f / 2 + 87 | (d > a ? 0 : ((a < h - d ? 1 : (h - a) / d) * Z[(a - d) | 0]) / 2) 88 | : f)), 89 | (p = (c += v += z) * Math.sin(E * x - I)), 90 | (g += p - p * B * (1 - ((1e9 * (Math.sin(a) + 1)) % 2))), 91 | (E += p - p * B * (1 - ((1e9 * (Math.sin(a) ** 2 + 1)) % 2))), 92 | n && ++n > A && ((c += w), (D += w), (n = 0)), 93 | !l || ++J % l || ((c = D), (v = H), (n = n || 1)); 94 | q = zzfxX.createBuffer(1, h, zzfxR); 95 | q.getChannelData(0).set(Z); 96 | c = zzfxX.createBufferSource(); 97 | c.buffer = q; 98 | c.connect(zzfxX.destination); 99 | c.start(); 100 | return c; 101 | }; 102 | zzfxX = new (window.AudioContext || webkitAudioContext) // audio context 103 | zzfxR = 44100; // sample rate 104 | export default zzfx; 105 | -------------------------------------------------------------------------------- /src/app/components/HUD.ts: -------------------------------------------------------------------------------- 1 | import GameObject from "../core/GameObject"; 2 | import V2 from "../core/V2"; 3 | import HopliteHead from "./entities/HopliteHead"; 4 | import { WIDTH } from "../constants"; 5 | 6 | class HUD extends GameObject { 7 | public _player: any; 8 | public _headIcon: HopliteHead; 9 | 10 | constructor(player) { 11 | super(); 12 | 13 | this._player = player; 14 | 15 | this._headIcon = new HopliteHead( 16 | new V2(WIDTH - 18, 34), 17 | new V2(12, 14), 18 | 0 19 | ); 20 | } 21 | 22 | _strokeText(ctx, text, x, y) { 23 | ctx.miterLimit = 2; 24 | ctx.font = "20px monospace"; 25 | ctx.strokeStyle = "#000"; 26 | ctx.lineWidth = 4; 27 | ctx.strokeText(text, x, y); 28 | ctx._fillStyle("#fff"); 29 | ctx.fillText(text, x, y); 30 | } 31 | 32 | _draw(ctx) { 33 | // bar backgrounds 34 | ctx._fillStyle("#fff"); 35 | ctx._fillRect(9, 9, 202, 22); 36 | ctx._fillRect(9, 34, 202, 12); 37 | 38 | // health bar 39 | ctx._fillStyle("#d11141"); 40 | ctx._fillRect(10, 10, (this._player._hp / this._player._maxHp) * 200, 20); 41 | 42 | // stamina 43 | ctx._fillStyle("#00aedb"); 44 | ctx._fillRect(10, 35, (this._player._stamina / 100) * 200, 10); 45 | 46 | ctx.textAlign = "right"; 47 | this._strokeText(ctx, (this._age / 1000).toFixed(2), WIDTH - 6, 20); 48 | this._strokeText(ctx, this._player._kills, WIDTH - 6, 40); 49 | 50 | this._headIcon._draw( 51 | ctx, 52 | { _position: new V2(`${this._player._kills}`.length * -12, 0), r: 0 }, 53 | 0 54 | ); 55 | } 56 | } 57 | 58 | export default HUD; 59 | -------------------------------------------------------------------------------- /src/app/components/Spawner.ts: -------------------------------------------------------------------------------- 1 | import GameNode from "../core/GameNode"; 2 | import Athenian from "./entities/enemies/Athenian"; 3 | import V2 from "../core/V2"; 4 | import { Game } from "../main"; 5 | import { rndRng, walkTile, i2c, waterTile } from "../core/utils"; 6 | import Giant from "./entities/enemies/Giant"; 7 | import Hoplite from "./entities/Hoplite"; 8 | import { mapDim } from "../constants"; 9 | 10 | export var getRandomMapCoords = () => { 11 | var map = Game._scene._tileMap._map; 12 | while (true) { 13 | // TODO these are set sizes that dont need the lookup 14 | var y = rndRng(0, mapDim - 1); 15 | var x = rndRng(0, mapDim - 1); 16 | var tileType = map[y][x]._tileType; 17 | 18 | var pt = i2c(new V2(x, y)); 19 | if (walkTile(tileType) && !waterTile(tileType) && !Game._scene._inViewport(pt)) { 20 | return pt; 21 | } 22 | } 23 | }; 24 | 25 | class Spawner extends GameNode { 26 | public _totalSpawned : number = 0; 27 | public _maxEntitiesAtOnce: number = 40; 28 | public _spawnDelay: number = 3000; 29 | public _spawnDelayCounter: number = 3000; 30 | 31 | public _entities: Hoplite[] = []; 32 | 33 | _update(dt) { 34 | this._entities = this._entities.filter((e) => e._active); 35 | 36 | if (this._spawnDelayCounter >= this._spawnDelay) { 37 | this._spawnDelayCounter = 0; 38 | var l = this._entities.length; 39 | if (l < this._maxEntitiesAtOnce) { 40 | this._totalSpawned++; 41 | if (this._totalSpawned && this._totalSpawned % 10 === 0) { 42 | // spawn a big guy 43 | this._spawn(true); 44 | } else { 45 | this._spawn(); 46 | } 47 | } 48 | } 49 | this._spawnDelayCounter += dt; 50 | } 51 | 52 | _spawn(big?) { 53 | this._spawnDelay = Math.max(1000, this._spawnDelay - 10); 54 | var p = getRandomMapCoords(); 55 | var ent = big ? new Giant(p) : new Athenian(p); 56 | this._parent._addChild(ent); 57 | this._entities.push(ent); 58 | } 59 | 60 | // randomSpawnCoords() { 61 | // while(true) { 62 | // var p = this.getRandomMapCoords(); 63 | 64 | // if (meetsCondition) { 65 | // return p; 66 | // } 67 | // } 68 | // } 69 | } 70 | 71 | export default Spawner; 72 | -------------------------------------------------------------------------------- /src/app/components/TileMap.ts: -------------------------------------------------------------------------------- 1 | import GameObject from "../core/GameObject"; 2 | import Perlin from "../core/Perlin"; 3 | import { Game } from "../main"; 4 | import BaseTile from "./tiles/BaseTile"; 5 | import WaterTile from "./tiles/WaterTile"; 6 | import GrassTile from "./tiles/GrassTile"; 7 | import { mapDim } from "../constants"; 8 | import V2 from "../core/V2"; 9 | 10 | var impulseDuration = 400; 11 | 12 | var perlinOctave = (x, y, z, octaves, persistence) => { 13 | var total = 0; 14 | var frequency = 1; 15 | var amplitude = 1; 16 | var maxValue = 0; // Used for normalizing result to 0.0 - 1.0 17 | for (var i = 0; i < octaves; i++) { 18 | total += 19 | Perlin._simplex3(x * frequency, y * frequency, z * frequency) * amplitude; 20 | 21 | maxValue += amplitude; 22 | 23 | amplitude *= persistence; 24 | frequency *= 2; 25 | } 26 | 27 | return total / maxValue; 28 | }; 29 | 30 | class TileMap extends GameObject { 31 | public _map: any[] = []; 32 | public _impulseIsoPosition: V2; 33 | // public _impulseDelayCounter: number; 34 | public _impulseRadius: number; 35 | public _impulseCounter: number = 0; 36 | 37 | constructor(spritesheet, interactiveLayer) { 38 | super(); 39 | 40 | for (var i = mapDim; i--; ) { 41 | var tileMapRow = []; 42 | for (var j = mapDim; j--; ) { 43 | var tile; 44 | 45 | var p = perlinOctave(i / 85, j / 85, Game._seed, 3, 1); 46 | 47 | var val = 9; 48 | 49 | if (p < 0) { 50 | val = 1; // water 51 | } else if (p < 0.05) { 52 | val = 2; // sand 53 | } else if (p < 0.1) { 54 | val = 3; // dirt 55 | } else if (p < 1) { 56 | val = 4; // grass 57 | } 58 | 59 | p = Math.abs(p); 60 | if (val === 0 || val === 1) { 61 | tile = new WaterTile(j, i, 20, spritesheet, val); 62 | } else if (val === 2 || val === 3) { 63 | tile = new BaseTile(j, i, 20 + p * 200, spritesheet, val); 64 | } else if (val === 4) { 65 | tile = new GrassTile( 66 | j, 67 | i, 68 | // TODO 99 -> 1 - max height of pre-rendered tiles 69 | Math.min(99, 20 + p * 200), 70 | spritesheet, 71 | val, 72 | interactiveLayer 73 | ); 74 | } 75 | 76 | this._addChild(tile); 77 | tileMapRow.push(tile); 78 | } 79 | 80 | this._map.push(tileMapRow.reverse()); 81 | } 82 | 83 | this._map.reverse(); 84 | } 85 | 86 | _impulseAt(tileIsoPosition) { 87 | this._impulseIsoPosition = tileIsoPosition; 88 | this._impulseRadius = 1; 89 | this._impulseCounter = impulseDuration; 90 | } 91 | 92 | _update(dt) { 93 | this._impulseCounter = Math.max(this._impulseCounter - dt, 0); 94 | 95 | this._impulseRadius += 0.15; 96 | 97 | if (this._impulseIsoPosition && this._impulseRadius < 20) { 98 | for (var i = this._map.length; i--; ) { 99 | var tileMapRow = this._map[i]; 100 | for (var j = tileMapRow.length; j--; ) { 101 | var tile: BaseTile = tileMapRow[j]; 102 | // if (!waterTile(tile._tileType)) { 103 | var distanceImpulseCenterToTile = Math.sqrt( 104 | Math.pow(this._impulseIsoPosition.x - tile._isoPosition.x, 2) + 105 | Math.pow(this._impulseIsoPosition.y - tile._isoPosition.y, 2) 106 | ); 107 | 108 | var diffRadiusToDistance = Math.abs( 109 | this._impulseRadius - distanceImpulseCenterToTile 110 | ); 111 | 112 | var maxEffectDistance = 1; 113 | 114 | tile._height = 115 | diffRadiusToDistance < 1 116 | ? Math.min( 117 | 119, 118 | tile._baseHeight + 119 | 40 * 120 | (1 - diffRadiusToDistance / maxEffectDistance) * 121 | (this._impulseCounter / impulseDuration) 122 | ) 123 | : tile._baseHeight; 124 | // } 125 | } 126 | } 127 | } 128 | 129 | super._update(dt); 130 | } 131 | } 132 | 133 | export default TileMap; 134 | -------------------------------------------------------------------------------- /src/app/components/WaveController.ts: -------------------------------------------------------------------------------- 1 | // import GameNode from "../core/GameNode"; 2 | // import Spawner from "./Spawner"; 3 | 4 | // class WaveController extends GameNode { 5 | // public _currentWaveIndex: number = 0; 6 | // public _waveIncrementEnemies: number; 7 | // public _currentWave: Spawner; 8 | 9 | // public _budgetPerWave: number = 10; 10 | // public _budgetPerWaveIncrement: number = 1.5; 11 | 12 | // public _spawnDelay: number = 3000; 13 | // public _spawnDelayIncrement: number = 0.9; 14 | 15 | // public _bigProbability: number = 0; 16 | // public _bigProbabilityIncrement: number = .05; 17 | 18 | // _nextWave() { 19 | // this._currentWaveIndex++; 20 | // if (this._currentWaveIndex > 1) { 21 | // this._budgetPerWave = ~~( 22 | // this._budgetPerWave * this._budgetPerWaveIncrement 23 | // ); 24 | // this._spawnDelay = ~~(this._spawnDelay * this._spawnDelayIncrement); 25 | // this._bigProbability = Math.max(.5, this._bigProbability + this._bigProbabilityIncrement) 26 | // } 27 | 28 | // this._currentWave = new Spawner( 29 | // this._currentWaveIndex, 30 | // this._budgetPerWave, 31 | // this._spawnDelay, 32 | // this._bigProbability, 33 | // this._parent 34 | // ); 35 | // this._addChild(this._currentWave); 36 | // } 37 | 38 | // _update(dt) { 39 | // if (!this._currentWave) { 40 | // this._nextWave(); 41 | // } 42 | 43 | // if ( 44 | // this._currentWave._budget <= 0 && 45 | // this._currentWave._entities.length === 0 46 | // ) { 47 | // this._currentWave._active = false; 48 | // this._nextWave(); 49 | // } 50 | 51 | // super._update(dt); 52 | // } 53 | // } 54 | 55 | // export default WaveController; 56 | -------------------------------------------------------------------------------- /src/app/components/entities/BodyPart.ts: -------------------------------------------------------------------------------- 1 | import GameObject from "../../core/GameObject"; 2 | import V2 from "../../core/V2"; 3 | 4 | class BodyPart extends GameObject { 5 | public _size: V2; 6 | public _shouldRenderShadow: boolean = false; 7 | 8 | constructor(p, size) { 9 | super(p); 10 | this._size = size; 11 | this._calcSlide = false; 12 | } 13 | 14 | _update(dt) { 15 | if (this._z === 0) { 16 | this._v._reset(); 17 | this._shouldRenderShadow = false; 18 | } 19 | 20 | var timeLeft = this._lifeSpan - this._age; 21 | if (this._lifeSpan !== -1 && timeLeft < this._lifeSpan * 0.1) { 22 | this._opacity = timeLeft / (this._lifeSpan * 0.1); 23 | } 24 | 25 | super._update(dt); 26 | } 27 | 28 | _renderShadow(ctx) { 29 | ctx._fillStyle("rgba(0,0,0,0.5"); 30 | ctx._fillRect( 31 | -this._size.x * 0.4, 32 | this._z, 33 | this._size.x * 0.8, 34 | this._size.y / 2 35 | ); 36 | } 37 | } 38 | 39 | export default BodyPart; 40 | -------------------------------------------------------------------------------- /src/app/components/entities/Hoplite.ts: -------------------------------------------------------------------------------- 1 | import V2 from "../../core/V2"; 2 | import GameObject from "../../core/GameObject"; 3 | import Animation from "../../core/Animation"; 4 | import HopliteHead from "./HopliteHead"; 5 | import Sword from "./Sword"; 6 | import { CollisionRect } from "../../core/SimpleCollision/index"; 7 | import { Game } from "../../main"; 8 | import { sfx } from "../../sounds"; 9 | import { rndPN, rndRng, waterTile } from "../../core/utils"; 10 | import Emitter from "../../core/Emitter"; 11 | import { 12 | idle, 13 | shieldIdle, 14 | shieldWalk, 15 | walk, 16 | attack, 17 | } from "./hopliteAnimationConfigs"; 18 | import Cloth from "../../core/physics/shapes/Cloth"; 19 | import BodyPart from "./BodyPart"; 20 | import { shadeColor } from "../tiles/BaseTile"; 21 | import Leonidus from "./Leonidus"; 22 | 23 | class Hoplite extends GameObject { 24 | public _sizeHead: V2 = new V2(16, 18); 25 | public _sizeBody: V2 = new V2(12, 18); 26 | public _sizeSword: V2 = new V2(40, 4); 27 | public _sizeShield: V2 = new V2(20, 20); 28 | public _sizeLeg: V2 = new V2(4, 8); 29 | 30 | public _stamina: number = 100; 31 | 32 | public _staminaDelayCounter: number = 0; 33 | public _attackDelay: number = 350; 34 | public _attackDelayCounter: number = 0; 35 | public _hitResponseCounter: number = 0; 36 | 37 | public _stunDelayCounter: number = 0; 38 | 39 | public _shieldActive: boolean = false; 40 | 41 | // TODO Giant hitbox innacurate 42 | public _hitBox: any = new CollisionRect( 43 | this, 44 | new V2(-9, -35), 45 | 18, 46 | 45, 47 | -1, 48 | true 49 | ); 50 | 51 | public _idleAnim: Animation; 52 | public _walkAnim: Animation; 53 | public _attackAnim: Animation; 54 | public _shieldIdleAnim: Animation; 55 | public _shieldWalkAnim: Animation; 56 | public _currentAnim: Animation; 57 | 58 | public _altAttackDelay: number = 500; 59 | public _altAttackDelayCounter: number = 0; 60 | public _altAttackAnim: Animation; 61 | 62 | public _specialAttackDelay: number = 500; 63 | public _specialAttackDelayCounter: number = 0; 64 | public _specialAttackAnim: Animation; 65 | 66 | public _head: HopliteHead; 67 | public _sword: Sword; 68 | public _cloth: Cloth; 69 | 70 | public _facingRight: boolean; 71 | public _color: any; 72 | 73 | public _walkSoundDelay: number = 0; 74 | public _kills: number = 0; 75 | 76 | constructor(p) { 77 | super(p); 78 | 79 | this._speed = 200; 80 | this._maxSpeed = 200; 81 | 82 | this._zgrav = 0.3; 83 | 84 | // this._nullAnim = new Animation(1, {}); 85 | this._idleAnim = new Animation(20, idle); 86 | this._shieldIdleAnim = new Animation(20, shieldIdle); 87 | this._shieldWalkAnim = new Animation(20, shieldWalk); 88 | this._walkAnim = new Animation(20, walk); 89 | this._attackAnim = new Animation(16, attack, false); 90 | 91 | // current animation 92 | this._currentAnim = this._idleAnim; 93 | 94 | this._facingRight = true; 95 | 96 | this._setParts(); 97 | 98 | this._setYOff(); 99 | } 100 | 101 | _setParts() { 102 | this._head = new HopliteHead( 103 | new V2(0, -this._sizeBody.y - (this._sizeHead.y / 2) * 0.8), 104 | this._sizeHead, 105 | false 106 | ); 107 | 108 | this._sword = new Sword( 109 | new V2(-4, -this._sizeBody.y / 2 + 4), 110 | this._sizeSword 111 | ); 112 | 113 | this._head._parent = this._sword._parent = this; 114 | } 115 | 116 | _attack() { 117 | if (this._attackDelayCounter > 0 || !this._active || ~~this._stamina === 0) 118 | return; 119 | 120 | this._decrementStamina(5, 600); 121 | 122 | sfx([ 123 | 0.1, 124 | 1, 125 | 1123, 126 | 0.15, 127 | , 128 | 0.03, 129 | 4, 130 | 0.69, 131 | , 132 | -84.1, 133 | , 134 | 0.01, 135 | , 136 | 0.8, 137 | -4, 138 | -0.1, 139 | , 140 | 0.32, 141 | 0.04, 142 | 0.01, 143 | ]); 144 | 145 | this._currentAnim = this._attackAnim; 146 | this._attackAnim._currentFrame = 0; 147 | 148 | var dir = this._facingRight ? 1 : -1; 149 | 150 | var enemiesToDamage = Game._scene._collisions._overlapRect( 151 | new CollisionRect( 152 | this, 153 | new V2( 154 | dir * (this._sizeBody.x / 2 + 10 + (this._facingRight ? 0 : 36)), 155 | -30 156 | ), 157 | 36, 158 | 20 159 | ) 160 | ); 161 | 162 | enemiesToDamage.forEach((enemy) => { 163 | enemy._object._hitBy(this); 164 | }); 165 | 166 | this._attackDelayCounter = this._attackDelay; 167 | } 168 | 169 | _gibPart(part: BodyPart, z, zv, rvDivisor) { 170 | part._setYOff(); 171 | part._z = z; 172 | part._zv = zv; 173 | part._v = V2._fromAngle(Math.random() * Math.PI); 174 | part._rv = rndPN() / rvDivisor; 175 | part._shouldRenderShadow = true; 176 | } 177 | 178 | _destroy() { 179 | this._hitBox._active = false; 180 | 181 | var newHead = new HopliteHead( 182 | this._position._copy(), 183 | this._sizeHead, 184 | false 185 | ); 186 | newHead._bleed(); 187 | newHead._lifeSpan = 300000; 188 | 189 | var newSword = new Sword(this._position._copy(), this._sizeSword); 190 | newSword._lifeSpan = 30000; 191 | 192 | this._gibPart(newSword as BodyPart, 40, rndRng(0.5, 1.5), 4); 193 | 194 | this._gibPart(newHead as any, 40, rndRng(1, 4), 5); 195 | 196 | Game._scene._addParticle(newHead); 197 | Game._scene._addParticle(newSword); 198 | 199 | super._destroy(); 200 | } 201 | 202 | _hitBy(from, alt, special = false) { 203 | var dX = V2._subtract(this._position, from._position)._normalize().x; 204 | 205 | if (from instanceof Leonidus) { 206 | Game._scene._cam._shake = 50; 207 | } 208 | 209 | var hpDiff = alt || special ? from._damage/3 : from._damage; 210 | var blood = alt || special ? false : true; 211 | if ( 212 | this._shieldActive && 213 | this._stamina > 0 && 214 | ((dX < 0 && this._facingRight) || (dX > 0 && !this._facingRight)) 215 | ) { 216 | hpDiff = 0; 217 | blood = false; 218 | this._decrementStamina(from._damage * 1.5, 1000); 219 | } 220 | 221 | this._a.x += this._shieldActive ? 0 : dX * 10; 222 | this._zv += special ? 5 : 3; 223 | this._hp = Math.max(0, this._hp - hpDiff); 224 | 225 | if (this._hp === 0) { 226 | from._kills += 1; 227 | } 228 | 229 | this._hitResponseCounter = 200; 230 | 231 | this._stunDelayCounter += alt ? 2000 : special ? 1200 : 0; 232 | 233 | sfx( 234 | hpDiff 235 | ? /* hit */ [ 236 | , 237 | 1, 238 | 287, 239 | , 240 | , 241 | 0.03, 242 | 2, 243 | 1.88, 244 | -6.4, 245 | -27, 246 | , 247 | , 248 | , 249 | , 250 | , 251 | , 252 | , 253 | 0.39, 254 | 0.04, 255 | 0.39, 256 | ] 257 | : /* clang */ [ 258 | , 259 | , 260 | 456, 261 | , 262 | , 263 | 0.14, 264 | , 265 | 1.76, 266 | , 267 | 6.5, 268 | , 269 | , 270 | , 271 | 1.8, 272 | -0.6, 273 | 0.3, 274 | 0.11, 275 | 0.53, 276 | 0.05, 277 | 0.23, 278 | ] 279 | ); 280 | 281 | var hitParticles = new Emitter(); 282 | Object.assign(hitParticles, { 283 | _position: new V2(0, -this._sizeBody.y / 2), 284 | _addToScene: true, 285 | _color: blood ? "#db0000" : "#FFDA00", 286 | _zv: blood ? 1 : 3, 287 | _zvVariance: 2, 288 | _maxParticles: blood ? 12 : 4, 289 | _size: 12, 290 | _particleLifetime: 2000, 291 | _particleLifetimeVariance: 1000, 292 | _speed: blood ? 0.25 : 20, 293 | _speedVariance: blood ? 0.2 : 6, 294 | _rVariance: Math.PI * 2, 295 | _duration: 0, 296 | _zStart: this._sizeBody.y, 297 | }); 298 | 299 | this._addChild(hitParticles); 300 | } 301 | 302 | _setAnimation() { 303 | if ( 304 | !( 305 | [ 306 | this._altAttackAnim, 307 | this._attackAnim, 308 | this._specialAttackAnim, 309 | ].includes(this._currentAnim) && !this._currentAnim._finished 310 | ) 311 | ) { 312 | if (this._v._magnitude() > 0.1) { 313 | if (this._shieldActive) { 314 | this._currentAnim = this._shieldWalkAnim; 315 | } else { 316 | this._currentAnim = this._walkAnim; 317 | } 318 | } else { 319 | if (this._shieldActive) { 320 | this._currentAnim = this._shieldIdleAnim; 321 | } else { 322 | this._currentAnim = this._idleAnim; 323 | } 324 | } 325 | } 326 | } 327 | 328 | _decrementStamina(amt, delay) { 329 | this._stamina = Math.max(0, this._stamina - amt); 330 | this._staminaDelayCounter = Math.max(this._staminaDelayCounter, delay); 331 | } 332 | 333 | _update(dt) { 334 | this._staminaDelayCounter = Math.max(this._staminaDelayCounter - dt, 0); 335 | this._attackDelayCounter = Math.max(this._attackDelayCounter - dt, 0); 336 | this._hitResponseCounter = Math.max(this._hitResponseCounter - dt, 0); 337 | this._stunDelayCounter = Math.max(this._stunDelayCounter - dt, 0); 338 | 339 | if (this._staminaDelayCounter === 0) { 340 | this._stamina = Math.min(100, this._stamina + 1); 341 | } 342 | 343 | super._update(dt); 344 | 345 | this._setAnimation(); 346 | 347 | this._currentAnim._update(); 348 | 349 | if (this._currentAnim === this._walkAnim && this._walkSoundDelay < 0) { 350 | var volume = Math.max( 351 | 0, 352 | 1 - 353 | V2._subtract( 354 | this._position, 355 | Game._scene._player._position 356 | )._magnitude() / 357 | 300 358 | ); 359 | 360 | var tileIsWater = waterTile(this._currentTileType); 361 | 362 | volume > 0.2 && 363 | this._z <= 4 && 364 | sfx( 365 | tileIsWater 366 | ? [ 367 | volume, 368 | , 369 | 60, 370 | 0.01, 371 | , 372 | 0.07, 373 | , 374 | 2.47, 375 | -0.4, 376 | 50.8, 377 | 100, 378 | 0.02, 379 | , 380 | -0.2, 381 | 0.1, 382 | , 383 | 0.02, 384 | , 385 | 0.04, 386 | 0.29, 387 | ] 388 | : [ 389 | volume, 390 | 0.1, 391 | 346, 392 | , 393 | , 394 | 0.06, 395 | , 396 | 2.23, 397 | , 398 | -2.8, 399 | 140, 400 | , 401 | 0.01, 402 | , 403 | , 404 | , 405 | 0.21, 406 | 0.25, 407 | , 408 | 0.04, 409 | ], 410 | true 411 | ); 412 | this._walkSoundDelay = 160; 413 | } 414 | 415 | this._walkSoundDelay -= dt; 416 | } 417 | 418 | _draw(ctx) { 419 | if ( 420 | Game._scene._inViewport( 421 | V2._add(this._position, new V2(0, -this._verticalOffset)) 422 | ) 423 | ) { 424 | super._draw(ctx); 425 | 426 | var colorOverride = this._hitResponseCounter > 0 ? "#fff" : undefined; 427 | 428 | colorOverride = 429 | Math.sin(this._stunDelayCounter * 0.02) > 0 ? "#1520a6" : colorOverride; 430 | 431 | var bodyColor = colorOverride || "#E0AC69"; 432 | 433 | var anim = this._currentAnim._current; 434 | 435 | ctx.s(); 436 | ctx.lineWidth = 1; 437 | 438 | // cape 439 | if (this._cloth) { 440 | this._cloth._draw(ctx, this._color); 441 | } 442 | 443 | var tileIsWater = waterTile(this._currentTileType); 444 | 445 | ctx._translate( 446 | this._position.x, 447 | this._position.y - 448 | (tileIsWater ? 0 : this._sizeLeg.y) - 449 | this._verticalOffset 450 | ); 451 | 452 | // shadow 453 | if (!tileIsWater || this._z > 10) { 454 | ctx._fillStyle("rgba(0,0,0,0.5"); 455 | ctx._fillRect(-this._sizeHead.x / 2, 5 + this._z, this._sizeHead.x, 6); 456 | } 457 | 458 | if (!this._facingRight) { 459 | ctx.scale(-1, 1); 460 | } 461 | 462 | // shield 463 | ctx.s(); 464 | ctx._fillStyle(colorOverride || "#A87D37"); 465 | ctx.rotate(anim.shield.r); 466 | ctx._fillRect( 467 | -this._sizeShield.x / 2 + 4 + anim.shield._position.x, 468 | -this._sizeShield.y - 469 | this._sizeBody.y * 0.1 + 470 | anim.shield._position.y - 471 | (tileIsWater ? 8 : 0), 472 | this._sizeShield.x, 473 | this._sizeShield.y 474 | ); 475 | ctx.r(); 476 | 477 | // feet color 478 | ctx._fillStyle(colorOverride || "#D0814E"); 479 | 480 | // legs 481 | if (!tileIsWater || this._z > this._sizeLeg.y) { 482 | // left 483 | ctx.s(); 484 | ctx._translate(-this._sizeLeg.x / 2 - this._sizeBody.x / 3, -2); 485 | ctx.rotate(anim.legL.r); 486 | ctx._fillRect(0, this._sizeLeg.y, this._sizeLeg.x, this._sizeLeg.y / 3); 487 | ctx.r(); 488 | 489 | // right 490 | ctx.s(); 491 | ctx._translate(-this._sizeLeg.x / 2 + this._sizeBody.x / 3, -2); 492 | ctx.rotate(anim.legR.r); 493 | ctx._fillRect(0, this._sizeLeg.y, this._sizeLeg.x, this._sizeLeg.y / 3); 494 | ctx.r(); 495 | } 496 | 497 | // body 498 | ctx.s(); // body save 499 | ctx.rotate(anim.body.r); 500 | ctx._fillStyle(bodyColor); 501 | ctx._fillRect( 502 | -this._sizeBody.x / 2 + anim.body._position.x, 503 | -this._sizeBody.y + anim.body._position.y, 504 | this._sizeBody.x, 505 | this._sizeBody.y * 0.6 506 | ); 507 | if (!tileIsWater || this._z > this._sizeBody.y * 0.45) { 508 | // skirt 509 | ctx._fillStyle(colorOverride || shadeColor(this._color, 10)); 510 | ctx._fillRect( 511 | -this._sizeBody.x / 2 + anim.body._position.x, 512 | -this._sizeBody.y * 0.4 + anim.body._position.y, 513 | this._sizeBody.x, 514 | this._sizeBody.y * 0.45 515 | ); 516 | } 517 | 518 | // head 519 | this._head._draw(ctx, anim.head, colorOverride); 520 | ctx.r(); // body restore 521 | 522 | // sword 523 | this._sword._draw(ctx, anim.sword, colorOverride, tileIsWater); 524 | ctx.r(); 525 | } 526 | } 527 | } 528 | 529 | export default Hoplite; 530 | -------------------------------------------------------------------------------- /src/app/components/entities/HopliteHead.ts: -------------------------------------------------------------------------------- 1 | import BodyPart from "./BodyPart"; 2 | import Emitter from "../../core/Emitter"; 3 | import V2 from "../../core/V2"; 4 | 5 | class HopliteHead extends BodyPart { 6 | public _headdress: boolean; 7 | 8 | constructor(p, size, headdress) { 9 | super(p, size); 10 | 11 | this._headdress = headdress; 12 | } 13 | 14 | _bleed() { 15 | var bleeder = new Emitter(); 16 | Object.assign(bleeder, { 17 | _position: new V2(0, this._size.y/2), 18 | _rotation: Math.PI / 2, 19 | _addToScene: true, 20 | _color: "#db0000", 21 | _maxParticles: 24, 22 | _size: 12, 23 | _particleLifetime: 2600, 24 | _particleLifetimeVariance: 800, 25 | _speed: 0.2, 26 | _speedVariance: 0.1, 27 | _rVariance: 0.5, 28 | _duration: 3200, 29 | }); 30 | 31 | this._addChild(bleeder); 32 | } 33 | 34 | // @ts-ignore 35 | _draw(ctx, offsets = { _position: new V2(), r: 0 }, colorOverride) { 36 | super._draw(ctx); 37 | 38 | ctx.s(); 39 | ctx._translate( 40 | this._position.x + offsets._position.x, 41 | this._position.y + offsets._position.y - this._verticalOffset 42 | ); 43 | this._shouldRenderShadow && this._renderShadow(ctx); 44 | ctx.rotate(offsets.r + this._rotation); 45 | if (this._headdress) { 46 | ctx._fillStyle(colorOverride || "#900"); 47 | ctx._fillRect( 48 | -this._size.x * 0.7, 49 | -this._size.y * 0.7, 50 | this._size.x * 0.8, 51 | this._size.y 52 | ); 53 | } 54 | 55 | ctx._fillStyle(colorOverride || "#fbca03"); 56 | ctx._fillRect( 57 | -this._size.x / 2, 58 | -this._size.y / 2, 59 | this._size.x, 60 | this._size.y 61 | ); 62 | 63 | ctx._beginPath(); 64 | ctx.strokeStyle = colorOverride || "#222"; 65 | ctx.lineWidth = 2; 66 | ctx.moveTo(0, 0); 67 | ctx._lineTo(this._size.x / 2, 0); 68 | ctx.stroke(); 69 | ctx.moveTo(this._size.x / 3, 0); 70 | ctx._lineTo(this._size.x / 3, this._size.y / 2); 71 | ctx.stroke(); 72 | ctx.r(); 73 | } 74 | } 75 | 76 | export default HopliteHead; 77 | -------------------------------------------------------------------------------- /src/app/components/entities/Leonidus.ts: -------------------------------------------------------------------------------- 1 | import InputController from "../../core/InputController"; 2 | import Hoplite from "./Hoplite"; 3 | import Cloth from "../../core/physics/shapes/Cloth"; 4 | import Animation from "../../core/Animation"; 5 | import { altAttack, specialAttack } from "./hopliteAnimationConfigs"; 6 | import { Game } from "../../main"; 7 | import { CollisionRect } from "../../core/SimpleCollision/index"; 8 | import V2 from "../../core/V2"; 9 | import { sfx } from "../../sounds"; 10 | import Emitter from "../../core/Emitter"; 11 | 12 | class Leonidus extends Hoplite { 13 | public _healDelayCounter: number = 0; 14 | public _timeSinceLastJump: number = 0; 15 | 16 | constructor(p) { 17 | super(p); 18 | 19 | this._maxForce = 4; 20 | this._damage = 25; 21 | this._head._headdress = true; 22 | this._color = "#990000"; 23 | this._hp = this._maxHp = 100; 24 | this._cloth = new Cloth(p.x, p.y - this._sizeBody.y, 26, 37.5, 13); 25 | 26 | this._altAttackAnim = new Animation(16, altAttack, false); 27 | this._specialAttackAnim = new Animation(36, specialAttack, false); 28 | } 29 | 30 | _hitBy(from, alt) { 31 | this._healDelayCounter = 1000; 32 | super._hitBy(from, alt); 33 | } 34 | 35 | _specialAttack() { 36 | if ( 37 | this._timeSinceLastJump > 1000 || 38 | this._specialAttackDelayCounter > 0 || 39 | !this._active || 40 | ~~this._stamina === 0 41 | ) 42 | return; 43 | 44 | this._decrementStamina(50, 600); 45 | 46 | this._maxSpeed = 2000; 47 | this._maxForce = 2000; 48 | 49 | setTimeout(() => { 50 | this._zv = -14; 51 | Game._scene._tileMap && 52 | Game._scene._tileMap._impulseAt(this._currentTile._isoPosition); 53 | 54 | var enemiesToDamage = Game._scene._collisions._overlapRect( 55 | new CollisionRect(this, new V2(-100, -100), 200, 200) 56 | ); 57 | 58 | enemiesToDamage.forEach((enemy) => { 59 | enemy._object._hitBy(this, false, true); 60 | }); 61 | 62 | Game._scene._cam._shake = 100; 63 | 64 | sfx([ 65 | , 66 | , 67 | 463, 68 | , 69 | , 70 | 0.45, 71 | 2, 72 | 0.38, 73 | -3.9, 74 | , 75 | , 76 | , 77 | , 78 | 1.2, 79 | , 80 | 0.1, 81 | 0.1, 82 | 0.72, 83 | 0.02, 84 | ]); 85 | }, 200); 86 | 87 | this._currentAnim = this._specialAttackAnim; 88 | this._specialAttackAnim._currentFrame = 0; 89 | 90 | this._specialAttackDelayCounter = this._specialAttackDelay; 91 | this._attackDelayCounter = this._attackDelay; 92 | } 93 | 94 | _bashAttack() { 95 | if ( 96 | this._altAttackDelayCounter > 0 || 97 | !this._active || 98 | ~~this._stamina === 0 99 | ) 100 | return; 101 | 102 | this._decrementStamina(10, 600); 103 | 104 | this._maxSpeed = 2000; 105 | this._a.x += this._facingRight ? 5 : -5; 106 | 107 | sfx([ 108 | , 109 | 0.5, 110 | 310, 111 | 0.04, 112 | 0.01, 113 | 0.13, 114 | , 115 | 0.29, 116 | -3.2, 117 | , 118 | , 119 | , 120 | , 121 | , 122 | , 123 | , 124 | , 125 | 0.74, 126 | 0.02, 127 | ]); 128 | 129 | // only do the smoke trail in the game scene 130 | if (Game._scene._tileMap) { 131 | var smoke = new Emitter(); 132 | Object.assign(smoke, { 133 | _rotation: -Math.PI / 2, 134 | _addToScene: true, 135 | _color: "rgba(205,190,171,0.8)", 136 | _maxParticles: 40, 137 | _endSize: 12, 138 | _endSizeVariance: 6, 139 | _particleLifetime: 600, 140 | _particleLifetimeVariance: 200, 141 | _zgrav: 0, 142 | _duration: 360, 143 | }); 144 | 145 | this._addChild(smoke); 146 | } 147 | 148 | this._currentAnim = this._altAttackAnim; 149 | this._altAttackAnim._currentFrame = 0; 150 | 151 | var dir = this._facingRight ? 1 : -1; 152 | 153 | var enemiesToDamage = Game._scene._collisions._overlapRect( 154 | new CollisionRect( 155 | this, 156 | new V2( 157 | dir * (this._sizeBody.x / 2 + 10 + (this._facingRight ? 0 : 48)), 158 | -24 159 | ), 160 | 48, 161 | 12 162 | ) 163 | ); 164 | 165 | enemiesToDamage.forEach((enemy) => { 166 | enemy._object._hitBy(this, true); 167 | if (enemy._object instanceof Hoplite) { 168 | this._decrementStamina(10, 600); 169 | } 170 | }); 171 | 172 | this._altAttackDelayCounter = this._altAttackDelay; 173 | } 174 | 175 | _update(dt) { 176 | this._timeSinceLastJump += dt; 177 | this._healDelayCounter = Math.max(this._healDelayCounter - dt, 0); 178 | this._altAttackDelayCounter = Math.max(this._altAttackDelayCounter - dt, 0); 179 | this._specialAttackDelayCounter = Math.max( 180 | this._specialAttackDelayCounter - dt, 181 | 0 182 | ); 183 | 184 | if (this._healDelayCounter === 0 && this._stamina === 100) { 185 | this._hp = Math.min(this._maxHp, this._hp + 0.2); 186 | } 187 | 188 | var isBashing = this._currentAnim === this._altAttackAnim; 189 | 190 | if (!isBashing) { 191 | if (InputController._KeyW) { 192 | this._a.y -= 3; 193 | } 194 | 195 | if (InputController._KeyA) { 196 | this._a.x -= 3; 197 | this._facingRight = false; 198 | } 199 | 200 | if (InputController._KeyS) { 201 | this._a.y += 3; 202 | } 203 | 204 | if (InputController._KeyD) { 205 | this._a.x += 3; 206 | this._facingRight = true; 207 | } 208 | 209 | if (InputController._KeyK) { 210 | this._shieldActive = true; 211 | this._maxForce = 2.5; 212 | if (InputController._KeyJ) { 213 | this._bashAttack(); 214 | } 215 | } else { 216 | this._shieldActive = false; 217 | this._maxForce = 4; 218 | 219 | if (InputController._KeyJ) { 220 | if (this._z > 20) { 221 | this._specialAttack(); 222 | } else { 223 | this._attack(); 224 | } 225 | } 226 | } 227 | } 228 | 229 | // Jump 230 | if (InputController._Space && this._stamina > 0 && this._z === 0) { 231 | this._timeSinceLastJump = 0; 232 | this._zv = 6; 233 | this._decrementStamina(5, 600); 234 | sfx([ 235 | 0.5, 236 | 1, 237 | 377, 238 | 0.03, 239 | 0.09, 240 | , 241 | , 242 | 0.86, 243 | 2.7, 244 | , 245 | -50, 246 | , 247 | , 248 | , 249 | , 250 | , 251 | , 252 | 0.57, 253 | 0.06, 254 | ]); 255 | } 256 | 257 | // redeclare as it could have changed 258 | isBashing = this._currentAnim === this._altAttackAnim; 259 | 260 | if (!isBashing) { 261 | this._v._scale(0); 262 | // limit to a max 263 | this._a._normalize()._scale(3); 264 | this._maxSpeed = this._shieldActive ? 100 : 200; 265 | this._zgrav = 0.3; 266 | this._maxForce = 4; 267 | } 268 | 269 | this._cloth._update( 270 | dt, 271 | this._position.x, 272 | this._position.y - 273 | this._sizeBody.y - 274 | this._sizeLeg.y - 275 | this._verticalOffset 276 | ); 277 | 278 | super._update(dt); 279 | } 280 | } 281 | 282 | export default Leonidus; 283 | -------------------------------------------------------------------------------- /src/app/components/entities/Sword.ts: -------------------------------------------------------------------------------- 1 | import BodyPart from "./BodyPart"; 2 | import V2 from "../../core/V2"; 3 | 4 | class Sword extends BodyPart { 5 | // @ts-ignore 6 | _draw(ctx, offsets = { _position: new V2(), r: 0 }, colorOverride, tileIsWater = false) { 7 | ctx.s(); 8 | ctx.lineWidth = 1; 9 | ctx.globalAlpha = this._opacity; 10 | ctx._translate( 11 | this._position.x + offsets._position.x, 12 | this._position.y + 13 | offsets._position.y - 14 | this._verticalOffset - 15 | (tileIsWater ? 6 : 0) 16 | ); 17 | this._shouldRenderShadow && this._renderShadow(ctx); 18 | ctx.rotate(offsets.r + this._rotation); 19 | // hilt 20 | ctx._fillStyle(colorOverride || "#963"); 21 | ctx._fillRect(-6, -1, 6, 2); 22 | // blade 23 | ctx.lineWidth = 2; 24 | // TODO change this color 25 | ctx._fillStyle(colorOverride || "#d8d8d8"); 26 | ctx._fillRect(0, -2, this._size.x - 6, this._size.y); 27 | ctx.r(); 28 | } 29 | } 30 | 31 | export default Sword; 32 | -------------------------------------------------------------------------------- /src/app/components/entities/enemies/Athenian.ts: -------------------------------------------------------------------------------- 1 | import { Game } from "../../../main"; 2 | import Hoplite from "../Hoplite"; 3 | 4 | class Athenian extends Hoplite { 5 | public _retreatDelayCounter: number = 0; 6 | public _attackRange = 40; 7 | 8 | constructor(p) { 9 | super(p); 10 | this._speed = 5; 11 | this._maxForce = 1; 12 | this._maxSpeed = 3000; 13 | this._visionRange = 550; 14 | this._damage = 7; 15 | this._hp = this._maxHp = 50; 16 | this._color = "#84b9d1"; 17 | this._steering._bWander._config._circleDistance = 0.15; 18 | } 19 | 20 | _update(dt) { 21 | if (this._stunDelayCounter > 0 || this._hitResponseCounter > 0) { 22 | if (this._z <= 4) { 23 | this._v._scale(.5); 24 | } 25 | super._update(dt); 26 | return; 27 | } 28 | 29 | var distToPlayer = this._distanceToPlayer(); 30 | 31 | if (this._retreatDelayCounter > 0) { 32 | this._retreatDelayCounter -= dt; 33 | this._steering._flee(Game._scene._player, 3); 34 | } else { 35 | if (distToPlayer < this._visionRange) { 36 | if (distToPlayer < this._attackRange) { 37 | this._attack(); 38 | setTimeout(() => { 39 | this._retreatDelayCounter = 600; 40 | }, 260); // wait until animation complete (~260ms) 41 | } else { 42 | this._steering._seek(Game._scene._player, 3); 43 | } 44 | } else { 45 | this._steering._wander(0.25); 46 | } 47 | 48 | if (this._steering._force.x > 0) { 49 | this._facingRight = true; 50 | } else if (this._steering._force.x < 0) { 51 | this._facingRight = false; 52 | } 53 | } 54 | 55 | this._steering._flock(); 56 | 57 | // prevent sliding around 58 | this._v._scale(0); 59 | 60 | super._update(dt); 61 | } 62 | } 63 | 64 | export default Athenian; 65 | -------------------------------------------------------------------------------- /src/app/components/entities/enemies/Giant.ts: -------------------------------------------------------------------------------- 1 | import V2 from "../../../core/V2"; 2 | import Athenian from "./Athenian"; 3 | 4 | class Giant extends Athenian { 5 | constructor(p) { 6 | super(p); 7 | this._speed = 3; 8 | this._maxForce = 0.5; 9 | this._maxSpeed = 1500; 10 | this._visionRange = 700; 11 | this._damage = 24; 12 | this._hp = this._maxHp = 200; 13 | this._attackRange = 60; 14 | 15 | this._sizeBody = new V2(24, 36); 16 | this._sizeHead = new V2(28, 36); 17 | this._sizeLeg = new V2(8, 8); 18 | this._sizeSword = new V2(60, 6); 19 | this._sizeShield = new V2(40, 40); 20 | 21 | this._setParts(); 22 | } 23 | } 24 | 25 | export default Giant; 26 | -------------------------------------------------------------------------------- /src/app/components/entities/hopliteAnimationConfigs.ts: -------------------------------------------------------------------------------- 1 | export var idle = { 2 | body: [ 3 | { 4 | f: 0, 5 | p: [0, 0], 6 | }, 7 | { 8 | f: 10, 9 | p: [0, -1], 10 | }, 11 | { 12 | f: 20, 13 | p: [0, 0], 14 | }, 15 | ], 16 | head: [ 17 | { 18 | f: 0, 19 | p: [0, 0], 20 | }, 21 | { 22 | f: 5, 23 | p: [0, 1], 24 | }, 25 | { 26 | f: 20, 27 | p: [0, 0], 28 | }, 29 | ], 30 | sword: [ 31 | { 32 | f: 0, 33 | r: 0, 34 | }, 35 | { 36 | f: 10, 37 | r: -0.045, 38 | }, 39 | { 40 | f: 20, 41 | r: 0, 42 | }, 43 | ], 44 | shield: [ 45 | { 46 | f: 0, 47 | r: 0.14, 48 | p: [0, 2.5], 49 | }, 50 | { 51 | f: 10, 52 | r: 0.16, 53 | p: [0.5, 3], 54 | }, 55 | { 56 | f: 20, 57 | r: 0.14, 58 | p: [0, 2.5], 59 | }, 60 | ], 61 | }; 62 | 63 | export var shieldIdle = { 64 | body: [ 65 | { 66 | f: 0, 67 | r: 0.1, 68 | p: [0, 4], 69 | }, 70 | { 71 | f: 10, 72 | r: 0.1, 73 | p: [0, 3.5], 74 | }, 75 | { 76 | f: 20, 77 | r: 0.1, 78 | p: [0, 4], 79 | }, 80 | ], 81 | head: [ 82 | { 83 | f: 0, 84 | p: [0, 5], 85 | }, 86 | { 87 | f: 5, 88 | p: [0, 4.5], 89 | }, 90 | { 91 | f: 20, 92 | p: [0, 5], 93 | }, 94 | ], 95 | sword: [ 96 | { 97 | f: 0, 98 | r: 0.25, 99 | p: [-6, 2], 100 | }, 101 | { 102 | f: 10, 103 | r: 0.25, 104 | p: [-6, 1.5], 105 | }, 106 | { 107 | f: 20, 108 | r: 0.25, 109 | p: [-6, 2], 110 | }, 111 | ], 112 | shield: [ 113 | { 114 | f: 0, 115 | p: [8, 1.5], 116 | }, 117 | { 118 | f: 0, 119 | p: [8.25, 1.75], 120 | }, 121 | { 122 | f: 20, 123 | p: [8, 1.5], 124 | }, 125 | ], 126 | }; 127 | 128 | export var shieldWalk = { 129 | body: [ 130 | { 131 | f: 0, 132 | r: 0.2, 133 | p: [0, 2], 134 | }, 135 | { 136 | f: 3, 137 | r: 0.2, 138 | p: [0, 3], 139 | }, 140 | { 141 | f: 7, 142 | r: 0.2, 143 | p: [0, 2], 144 | }, 145 | { 146 | f: 13, 147 | r: 0.2, 148 | p: [0, 3], 149 | }, 150 | { 151 | f: 20, 152 | r: 0.2, 153 | p: [0, 2], 154 | }, 155 | ], 156 | head: [ 157 | { 158 | f: 0, 159 | r: -0.1, 160 | p: [1, 4], 161 | }, 162 | { 163 | f: 3, 164 | r: -0.1, 165 | p: [1, 5], 166 | }, 167 | { 168 | f: 7, 169 | r: -0.1, 170 | p: [1, 4], 171 | }, 172 | { 173 | f: 13, 174 | r: -0.1, 175 | p: [1, 5], 176 | }, 177 | { 178 | f: 20, 179 | r: -0.1, 180 | p: [1, 4], 181 | }, 182 | ], 183 | sword: [ 184 | { 185 | f: 0, 186 | r: 0.2, 187 | p: [-3, 1], 188 | }, 189 | { 190 | f: 10, 191 | r: 0.25, 192 | p: [-6, 1], 193 | }, 194 | { 195 | f: 20, 196 | r: 0.2, 197 | p: [-3, 1], 198 | }, 199 | ], 200 | shield: [ 201 | { 202 | f: 0, 203 | p: [13.5, -1.75], 204 | }, 205 | { 206 | f: 10, 207 | p: [14, -2], 208 | }, 209 | { 210 | f: 20, 211 | p: [13.5, -1.75], 212 | }, 213 | ], 214 | legL: [ 215 | { 216 | f: 0, 217 | r: 1, 218 | }, 219 | { 220 | f: 10, 221 | r: -1, 222 | }, 223 | { 224 | f: 20, 225 | r: 1, 226 | }, 227 | ], 228 | legR: [ 229 | { 230 | f: 0, 231 | r: -1, 232 | }, 233 | { 234 | f: 10, 235 | r: 1, 236 | }, 237 | { 238 | f: 20, 239 | r: -1, 240 | }, 241 | ], 242 | }; 243 | 244 | export var walk = { 245 | body: [ 246 | { 247 | f: 0, 248 | r: 0.1, 249 | p: [0, 0], 250 | }, 251 | { 252 | f: 3, 253 | r: 0.1, 254 | p: [0, 2], 255 | }, 256 | { 257 | f: 7, 258 | r: 0.1, 259 | p: [0, 0], 260 | }, 261 | { 262 | f: 13, 263 | r: 0.1, 264 | p: [0, 2], 265 | }, 266 | { 267 | f: 20, 268 | r: 0.1, 269 | p: [0, 0], 270 | }, 271 | ], 272 | head: [ 273 | { 274 | f: 0, 275 | r: -0.1, 276 | p: [0, 0], 277 | }, 278 | { 279 | f: 3, 280 | r: -0.1, 281 | p: [0, 2], 282 | }, 283 | { 284 | f: 7, 285 | r: -0.1, 286 | p: [0, 0], 287 | }, 288 | { 289 | f: 13, 290 | r: -0.1, 291 | p: [0, 2], 292 | }, 293 | { 294 | f: 20, 295 | r: -0.1, 296 | p: [0, 0], 297 | }, 298 | ], 299 | sword: [ 300 | { 301 | f: 0, 302 | r: -0.24, 303 | p: [3, -4], 304 | }, 305 | { 306 | f: 10, 307 | r: -0.2, 308 | p: [0, -4], 309 | }, 310 | { 311 | f: 20, 312 | r: -0.24, 313 | p: [3, -4], 314 | }, 315 | ], 316 | shield: [ 317 | { 318 | f: 0, 319 | r: 0.24, 320 | p: [0, 0], 321 | }, 322 | { 323 | f: 10, 324 | r: 0.2, 325 | p: [3, 0], 326 | }, 327 | { 328 | f: 20, 329 | r: 0.24, 330 | p: [0, 0], 331 | }, 332 | ], 333 | legL: [ 334 | { 335 | f: 0, 336 | r: 1.4, 337 | }, 338 | { 339 | f: 10, 340 | r: -1.4, 341 | }, 342 | { 343 | f: 20, 344 | r: 1.4, 345 | }, 346 | ], 347 | legR: [ 348 | { 349 | f: 0, 350 | r: -1.4, 351 | }, 352 | { 353 | f: 10, 354 | r: 1.4, 355 | }, 356 | { 357 | f: 20, 358 | r: -1.4, 359 | }, 360 | ], 361 | }; 362 | 363 | export var attack = { 364 | body: [ 365 | { 366 | f: 0, 367 | r: 0.1, 368 | }, 369 | { 370 | f: 16, 371 | r: 0.1, 372 | }, 373 | ], 374 | sword: [ 375 | { 376 | f: 0, 377 | r: -1.5, 378 | p: [0, -8], 379 | }, 380 | { 381 | f: 2, 382 | r: -1.5, 383 | p: [0, -8], 384 | }, 385 | { 386 | f: 8, 387 | r: 0, 388 | p: [18, -4], 389 | }, 390 | { 391 | f: 12, 392 | r: 0.25, 393 | p: [12, -4], 394 | }, 395 | { 396 | f: 16, 397 | r: 0, 398 | p: [0, -4], 399 | }, 400 | ], 401 | shield: [ 402 | { 403 | f: 0, 404 | p: [0, 0], 405 | }, 406 | { 407 | f: 16, 408 | p: [-8, 0], 409 | }, 410 | ], 411 | legL: [ 412 | { 413 | f: 0, 414 | r: 0, 415 | }, 416 | { 417 | f: 8, 418 | r: 1, 419 | }, 420 | { 421 | f: 16, 422 | r: 0, 423 | }, 424 | ], 425 | legR: [ 426 | { 427 | f: 0, 428 | r: 0, 429 | }, 430 | { 431 | f: 8, 432 | r: -1, 433 | }, 434 | { 435 | f: 16, 436 | r: 0, 437 | }, 438 | ], 439 | }; 440 | 441 | export var altAttack = { 442 | ...attack, 443 | body: [ 444 | { 445 | f: 0, 446 | r: 0.3, 447 | p: [0, 2], 448 | }, 449 | { 450 | f: 16, 451 | r: 0.3, 452 | p: [0, 2], 453 | }, 454 | ], 455 | head: [ 456 | { 457 | f: 0, 458 | r: -0.1, 459 | p: [1, 4], 460 | }, 461 | { 462 | f: 16, 463 | r: -0.1, 464 | p: [1, 4], 465 | }, 466 | ], 467 | sword: [ 468 | { 469 | f: 0, 470 | r: 0.2, 471 | p: [0, 0], 472 | }, 473 | { 474 | f: 16, 475 | r: 0.2, 476 | p: [-8, 0], 477 | }, 478 | ], 479 | shield: [ 480 | { 481 | f: 0, 482 | p: [14, 0], 483 | }, 484 | { 485 | f: 4, 486 | p: [24, 0], 487 | }, 488 | { 489 | f: 10, 490 | p: [24, 0], 491 | }, 492 | { 493 | f: 16, 494 | p: [14, 0], 495 | }, 496 | ], 497 | }; 498 | 499 | export var specialAttack = { 500 | body: [ 501 | { 502 | f: 0, 503 | r: -0.1, 504 | p: [0, 0] 505 | }, 506 | { 507 | f: 6, 508 | r: -0.1, 509 | p: [0, 0] 510 | }, 511 | { 512 | f: 24, 513 | r: 0.5, 514 | p: [0, 5] 515 | }, 516 | { 517 | f: 36, 518 | r: 0.5, 519 | p: [0, 5] 520 | } 521 | ], 522 | head: [ 523 | { 524 | f: 0, 525 | r: -0.1, 526 | p: [0, 0] 527 | }, 528 | { 529 | f: 6, 530 | r: -0.1, 531 | p: [0, 0] 532 | }, 533 | { 534 | f: 24, 535 | r: -0.3, 536 | p: [0, 5] 537 | }, 538 | { 539 | f: 36, 540 | r: -0.3, 541 | p: [0, 5] 542 | } 543 | ], 544 | sword: [ 545 | { 546 | f: 0, 547 | r: -Math.PI/2, 548 | p: [0, -10] 549 | }, 550 | { 551 | f: 6, 552 | r: -Math.PI/2, 553 | p: [0, -10] 554 | }, 555 | { 556 | f: 20, 557 | r: -Math.PI/2, 558 | p: [10, 10] 559 | }, 560 | { 561 | f: 36, 562 | r: -Math.PI/2, 563 | p: [10, 10] 564 | } 565 | ], 566 | shield: [ 567 | { 568 | f: 0, 569 | r: -0.1, 570 | p: [10, -10] 571 | }, 572 | { 573 | f: 6, 574 | r: -0.1, 575 | p: [10, -10] 576 | }, 577 | { 578 | f: 20, 579 | r: 0, 580 | p: [16, 12] 581 | }, 582 | { 583 | f: 36, 584 | r: 0, 585 | p: [16, 12] 586 | } 587 | ], 588 | legL: [ 589 | { 590 | f: 0, 591 | r: 1, 592 | }, 593 | { 594 | f: 6, 595 | r: 1.2, 596 | }, 597 | { 598 | f: 24, 599 | r: .5, 600 | }, 601 | { 602 | f: 36, 603 | r: 0.5, 604 | } 605 | ], 606 | legR: [ 607 | { 608 | f: 0, 609 | r: -1, 610 | }, 611 | { 612 | f: 6, 613 | r: -1.2, 614 | }, 615 | { 616 | f: 24, 617 | r: 0, 618 | }, 619 | { 620 | f: 36, 621 | r: 0, 622 | } 623 | ] 624 | } -------------------------------------------------------------------------------- /src/app/components/entities/veg/Grass.ts: -------------------------------------------------------------------------------- 1 | import GameObject from "../../../core/GameObject"; 2 | import { rndRng, rndInArray } from "../../../core/utils"; 3 | import V2 from "../../../core/V2"; 4 | import Perlin from "../../../core/Perlin"; 5 | import { CollisionRect } from "../../../core/SimpleCollision/index"; 6 | import { Game } from "../../../main"; 7 | import { sfx } from "../../../sounds"; 8 | import Emitter from "../../../core/Emitter"; 9 | 10 | var colors = ["#e3fd98", "#d9f669", "#d4ee4b", "#beea41", "#a9ce21"]; 11 | 12 | class Grass extends GameObject { 13 | public _rotationVariance: number = 0; 14 | public _length: number = rndRng(24, 38); 15 | public _color: string = rndInArray(colors); 16 | 17 | constructor(p) { 18 | super(p); 19 | 20 | this._rotation = -Math.PI / 2; 21 | this._rotationVariance = 0; 22 | 23 | this._hitBox = new CollisionRect( 24 | this, 25 | new V2(-10, -this._length), 26 | 20, 27 | this._length, 28 | null, 29 | true 30 | ); 31 | 32 | this._calcSlide = false; 33 | this._zgrav = 10; 34 | } 35 | 36 | _hitBy() { 37 | sfx([ 38 | 0.3, 39 | 1, 40 | 300, 41 | 0.04, 42 | , 43 | 0.08, 44 | 2, 45 | 2.6, 46 | -26, 47 | , 48 | -222, 49 | 0.02, 50 | , 51 | , 52 | -6, 53 | 0.1, 54 | , 55 | 0.47, 56 | 0.03, 57 | ]); 58 | 59 | var hitParticles = new Emitter(); 60 | Object.assign(hitParticles, { 61 | _addToScene: true, 62 | _color: this._color, 63 | _zv: 1, 64 | _zvVariance: 0.5, 65 | _maxParticles: 1, 66 | _size: 14, 67 | _particleLifetime: 1200, 68 | _particleLifetimeVariance: 200, 69 | _speed: 0.4, 70 | _speedVariance: 0.2, 71 | _rVariance: Math.PI*2, 72 | _duration: 0, 73 | _zgrav: 0.02, 74 | }); 75 | 76 | this._addChild(hitParticles); 77 | setTimeout(() => { 78 | this._destroy(); 79 | }, 10); 80 | } 81 | 82 | _update(dt) { 83 | if ( 84 | Game._scene._inViewport( 85 | V2._add(this._position, new V2(0, -this._verticalOffset - this._length)) 86 | ) 87 | ) { 88 | this._rotationVariance = 89 | Perlin._simplex3( 90 | this._position.x / 200, 91 | this._position.y / 200, 92 | performance.now() / 2200 93 | ) * 0.5; 94 | 95 | super._update(dt); 96 | } 97 | } 98 | 99 | _draw(ctx) { 100 | if ( 101 | Game._scene._inViewport(V2._add(this._position, new V2(0, -this._verticalOffset))) 102 | ) { 103 | ctx.s(); 104 | ctx.lineWidth = 10; 105 | ctx._translate(this._position.x, this._position.y - this._verticalOffset); 106 | ctx._beginPath(); 107 | ctx.moveTo(0, 0); 108 | var v = new V2( 109 | Math.cos(this._rotation + this._rotationVariance), 110 | Math.sin(this._rotation + this._rotationVariance) 111 | ) 112 | ._normalize() 113 | ._scale(this._length); 114 | ctx._lineTo(v.x, v.y); 115 | ctx.strokeStyle = this._color; 116 | ctx.stroke(); 117 | ctx.r(); 118 | 119 | super._draw(ctx); 120 | } 121 | } 122 | } 123 | 124 | export default Grass; 125 | -------------------------------------------------------------------------------- /src/app/components/entities/veg/Tree.ts: -------------------------------------------------------------------------------- 1 | import GameObject from "../../../core/GameObject"; 2 | import { rndRng } from "../../../core/utils"; 3 | import V2 from "../../../core/V2"; 4 | import Perlin from "../../../core/Perlin"; 5 | import { Game } from "../../../main"; 6 | 7 | class Tree extends GameObject { 8 | public _length: number = rndRng(100, 200); 9 | public _size: V2; 10 | 11 | constructor(p) { 12 | super(p); 13 | 14 | this._size = new V2(0.75, 0.5)._scale(this._length); 15 | this._zgrav = 10; 16 | 17 | this._calcSlide = false; 18 | } 19 | 20 | _draw(ctx) { 21 | if ( 22 | Game._scene._inViewport( 23 | V2._add(this._position, new V2(0, -this._verticalOffset - this._length / 2)) 24 | ) 25 | ) { 26 | var s = 27 | Perlin._simplex3( 28 | this._position.x / 200, 29 | this._position.y / 200, 30 | performance.now() / 2200 31 | ) * 32 | Math.PI * 33 | 2; 34 | 35 | var sk = new V2(Math.cos(s), Math.sin(s))._normalize(); 36 | 37 | ctx.s(); 38 | ctx.lineWidth = 20; 39 | ctx._translate(this._position.x, this._position.y - this._verticalOffset); 40 | ctx._beginPath(); 41 | ctx.moveTo(0, 0); 42 | ctx._lineTo(0, -this._length); 43 | ctx.strokeStyle = "#8b632f"; 44 | ctx.stroke(); 45 | 46 | ctx.s(); 47 | ctx.globalAlpha = 0.9; 48 | ctx.transform(1 + sk.x / 60, sk.x / 60, sk.y / 60, 1 + sk.y / 60, 0, 0); 49 | ctx._fillStyle("#a6da39"); 50 | ctx._fillRect( 51 | -this._size.x / 2, 52 | -this._length - this._size.y / 2, 53 | this._size.x, 54 | this._size.y 55 | ); 56 | ctx.r(); 57 | 58 | ctx.r(); 59 | super._draw(ctx); 60 | } 61 | } 62 | } 63 | 64 | export default Tree; 65 | -------------------------------------------------------------------------------- /src/app/components/scenes/GameScene.ts: -------------------------------------------------------------------------------- 1 | import Scene from "../../core/Scene"; 2 | import { WIDTH, HEIGHT } from "../../constants"; 3 | import GameObject from "../../core/GameObject"; 4 | import Leonidus from "../entities/Leonidus"; 5 | import HUD from "../HUD"; 6 | import Spawner, { getRandomMapCoords } from "../Spawner"; 7 | import TileMap from "../TileMap"; 8 | 9 | class GameScene extends Scene { 10 | public _interactiveLayer: GameObject; 11 | public _HUD: GameObject; 12 | public _spawner: Spawner; 13 | 14 | constructor(spriteSheet) { 15 | super(); 16 | 17 | setTimeout(() => { 18 | this._interactiveLayer = new GameObject(); 19 | this._tileMap = new TileMap(spriteSheet, this._interactiveLayer); 20 | 21 | this._addChild(this._interactiveLayer); 22 | this._addChild(this._tileMap); 23 | 24 | this._player = new Leonidus(getRandomMapCoords()); 25 | 26 | this._cam._lookat = this._player._position._copy(); 27 | // this._cam._updateViewPort(); 28 | 29 | this._HUD = new HUD(this._player); 30 | 31 | this._interactiveLayer._addChild(this._player); 32 | 33 | this._spawner = new Spawner(); 34 | this._interactiveLayer._addChild(this._spawner); 35 | }, 10); 36 | } 37 | 38 | _addParticle(p) { 39 | this._interactiveLayer._addChild(p); 40 | } 41 | 42 | _update(dt) { 43 | this._HUD._update(dt); 44 | 45 | this._interactiveLayer._children = this._interactiveLayer._children.sort( 46 | (a, b) => b._position.y - a._position.y 47 | ); 48 | 49 | if (this._player._hp === 0) { 50 | this._done = true; 51 | } 52 | 53 | super._update(dt); 54 | } 55 | 56 | _draw(ctx) { 57 | this._cam._moveTo( 58 | this._player._position.x, 59 | this._player._position.y - this._player._verticalOffset 60 | ); 61 | 62 | this._cam._begin(ctx); 63 | 64 | super._draw(ctx); 65 | 66 | // this._collisions._draw(ctx) 67 | 68 | this._cam._end(ctx); 69 | 70 | this._HUD._draw(ctx); 71 | } 72 | } 73 | 74 | export default GameScene; 75 | -------------------------------------------------------------------------------- /src/app/components/scenes/TitleScene.ts: -------------------------------------------------------------------------------- 1 | import Scene from "../../core/Scene"; 2 | import V2 from "../../core/V2"; 3 | import Leonidus from "../entities/Leonidus"; 4 | 5 | class TitleScene extends Scene { 6 | constructor() { 7 | super(); 8 | 9 | this._cam._lookat = new V2(24, -22); 10 | this._cam._targetDistance = this._cam._distance = 106; 11 | 12 | // TODO scene doesn't exist yet but needs to on the gameinstacne for some 13 | // of this to function. fix this 14 | setTimeout(() => { 15 | this._player = new Leonidus(new V2()); 16 | 17 | this._addChild(this._player); 18 | }, 0); 19 | } 20 | 21 | _draw(ctx) { 22 | this._cam._begin(ctx); 23 | 24 | super._draw(ctx); 25 | 26 | this._cam._end(ctx); 27 | } 28 | } 29 | 30 | export default TitleScene; 31 | -------------------------------------------------------------------------------- /src/app/components/tiles/BaseTile.ts: -------------------------------------------------------------------------------- 1 | import GameObject from "../../core/GameObject"; 2 | import V2 from "../../core/V2"; 3 | import { i2c } from "../../core/utils"; 4 | import { Game } from "../../main"; 5 | import { TILESIZE } from "../../constants"; 6 | 7 | // Colour adjustment function 8 | // Nicked from http://stackoverflow.com/questions/5560248 9 | export var shadeColor = (color, percent) => { 10 | color = color.substr(1); 11 | var num = parseInt(color, 16), 12 | amt = Math.round(2.55 * percent), 13 | R = (num >> 16) + amt, 14 | G = ((num >> 8) & 0x00ff) + amt, 15 | B = (num & 0x0000ff) + amt; 16 | return ( 17 | "#" + 18 | ( 19 | 0x1000000 + 20 | (R < 255 ? (R < 1 ? 0 : R) : 255) * 0x10000 + 21 | (G < 255 ? (G < 1 ? 0 : G) : 255) * 0x100 + 22 | (B < 255 ? (B < 1 ? 0 : B) : 255) 23 | ) 24 | .toString(16) 25 | .slice(1) 26 | ); 27 | }; 28 | 29 | class BaseTile extends GameObject { 30 | public _spriteSheet: ImageData; 31 | public _tileType: number; 32 | public _height: number; 33 | public _isoPosition: V2; 34 | public _baseHeight: number; 35 | 36 | constructor(isox, isoy, height = 0, spriteSheet, spriteSheetYOffset) { 37 | super(); 38 | 39 | this._spriteSheet = spriteSheet; 40 | this._tileType = spriteSheetYOffset; 41 | 42 | this._isoPosition = new V2(isox, isoy); 43 | this._position = i2c(this._isoPosition); 44 | this._height = this._baseHeight = height; 45 | } 46 | 47 | _draw(ctx) { 48 | if (Game._scene._inViewport(this._position)) { 49 | var h = ~~this._height; 50 | ctx.drawImage( 51 | this._spriteSheet, 52 | Math.floor(this._tileType * (TILESIZE.x + 1)), 53 | Math.floor(TILESIZE.y * h + (h * (h + 1)) / 2 - h), 54 | Math.floor(TILESIZE.x), 55 | Math.floor(TILESIZE.y + h), 56 | Math.floor(this._position.x - TILESIZE.x / 2), 57 | Math.floor(this._position.y - h), 58 | Math.floor(TILESIZE.x), 59 | Math.floor(TILESIZE.y + h) 60 | ); 61 | } 62 | super._draw(ctx); 63 | } 64 | } 65 | 66 | export default BaseTile; 67 | -------------------------------------------------------------------------------- /src/app/components/tiles/GrassTile.ts: -------------------------------------------------------------------------------- 1 | import V2 from "../../core/V2"; 2 | import Grass from "../entities/veg/Grass"; 3 | import BaseTile from "./BaseTile"; 4 | import { i2c } from "../../core/utils"; 5 | import Perlin from "../../core/Perlin"; 6 | import { Game } from "../../main"; 7 | import Tree from "../entities/veg/Tree"; 8 | 9 | class GrassTile extends BaseTile { 10 | constructor(isox, isoy, height, spriteSheet, tileType, parent) { 11 | super(isox, isoy, height, spriteSheet, tileType); 12 | 13 | var g = Math.abs(Perlin._simplex3(isox / 10, isoy / 10, Game._seed)); 14 | 15 | if (g > 0.94) { 16 | var pt = i2c(new V2(isox+ Math.random(), isoy+ Math.random())); 17 | parent._addChild(new Tree(pt)); 18 | } else if (g > 0.5) { 19 | var c = ~~(((g - 0.5) / .45) * 8); 20 | for (var i = 0; i < c; i++) { 21 | var pt = i2c( 22 | new V2(isox + Math.random(), isoy + Math.random()) 23 | ); 24 | parent._addChild(new Grass(pt)); 25 | } 26 | } 27 | } 28 | } 29 | 30 | export default GrassTile; 31 | -------------------------------------------------------------------------------- /src/app/components/tiles/WaterTile.ts: -------------------------------------------------------------------------------- 1 | import BaseTile from "./BaseTile"; 2 | import Perlin from "../../core/Perlin"; 3 | import { Game } from "../../main"; 4 | 5 | class WaterTile extends BaseTile { 6 | _update(dt) { 7 | if (Game._scene._inViewport(this._position)) { 8 | this._height = 9 | Math.abs( 10 | Perlin._simplex3( 11 | this._position.x / 300, 12 | this._position.y / 300, 13 | performance.now() / 2600 14 | ) 15 | ) * 30; 16 | 17 | this._tileType = this._height > 16 ? 1 : 0; 18 | } 19 | } 20 | } 21 | 22 | export default WaterTile; 23 | -------------------------------------------------------------------------------- /src/app/components/tiles/tilePregen.ts: -------------------------------------------------------------------------------- 1 | import { shadeColor } from "./BaseTile"; 2 | import { TILESIZE } from "../../constants"; 3 | 4 | // draw tile 5 | var drawT = (ctx, x, y, h, color) => { 6 | var wx = TILESIZE.x / 2; 7 | var wy = TILESIZE.x / 2; 8 | 9 | // diag distance 10 | var dDist = wx * 0.5 + wy * 0.5; 11 | 12 | ctx.s(); 13 | 14 | // left side / x axis 15 | ctx._beginPath(); 16 | ctx.moveTo(x, y + dDist); // tr 17 | ctx._lineTo(x - wx, y + dDist - wx * 0.5); // tl 18 | ctx._lineTo(x - wx, y + dDist - h - wx * 0.5); // bl 19 | ctx._lineTo(x, y + dDist - h * 1); // br 20 | ctx.closePath(); 21 | // ctx.fillStyle = shadeColor(color, -10); 22 | // ctx.strokeStyle = color; 23 | ctx.fillStyle = ctx.strokeStyle = shadeColor(color, -20); 24 | // ctx.strokeStyle = shadeColor(color, -10); 25 | ctx.stroke(); 26 | ctx.fill(); 27 | 28 | // right side / y axis 29 | ctx._beginPath(); 30 | ctx.moveTo(x, y + dDist); // tl 31 | ctx._lineTo(x + wy, y + dDist - wy * 0.5); // tr 32 | ctx._lineTo(x + wy, y + dDist - h - wy * 0.5); // br 33 | ctx._lineTo(x, y + dDist - h * 1); // bl 34 | ctx.closePath(); 35 | // ctx.fillStyle = shadeColor(color, 10); 36 | // ctx.strokeStyle = shadeColor(color, 50); 37 | ctx.fillStyle = ctx.strokeStyle = shadeColor(color, -10); 38 | // ctx.strokeStyle = shadeColor(color, 30); 39 | ctx.stroke(); 40 | ctx.fill(); 41 | 42 | // top 43 | ctx._beginPath(); 44 | ctx.moveTo(x, y - h); // top 45 | ctx._lineTo(x - wx, y - h + wx * 0.5); // left 46 | ctx._lineTo(x - wx + wy, y - h + dDist); // bottom 47 | ctx._lineTo(x + wy, y - h + wy * 0.5); // right 48 | ctx.closePath(); 49 | // ctx.fillStyle = shadeColor(color, 20); 50 | // ctx.strokeStyle = shadeColor(color, 60); 51 | ctx.fillStyle = ctx.strokeStyle = color; 52 | ctx.strokeStyle = shadeColor(color, -10); 53 | ctx.stroke(); 54 | ctx.fill(); 55 | 56 | ctx.r(); 57 | }; 58 | 59 | var tC = () => { 60 | var tmpC = document.createElement("canvas"); 61 | var tmpCtx = tmpC.getContext("2d"); 62 | 63 | return { 64 | tmpC, 65 | tmpCtx, 66 | }; 67 | }; 68 | 69 | export var tilePregen = () => { 70 | var { tmpC, tmpCtx } = tC(); 71 | 72 | // water, water, sand, dirt, grass, 73 | var colors = ["#1ba5e1", "#1eb8fa", "#e5d9c2", "#564d40", "#48893e"]; 74 | 75 | var total = 120; 76 | tmpC.width = (TILESIZE.x + 1) * colors.length; 77 | tmpC.height = TILESIZE.y * total + (total * (total + 1)) / 2; 78 | 79 | for (var j = 0; j < colors.length; j++) { 80 | var color = colors[j]; 81 | var x = j * (TILESIZE.x + 1) + (TILESIZE.x + 1) / 2; 82 | for (var i = 0; i < total; i++) { 83 | var h = i; 84 | var y = TILESIZE.y * i + (i * (i + 1)) / 2; 85 | 86 | drawT(tmpCtx, x, y, h, color); 87 | } 88 | } 89 | 90 | // var image = new Image(); 91 | 92 | // image.src = tmpC.toDataURL(); 93 | 94 | return tmpC; 95 | }; 96 | 97 | // export var borderTilePregen = () => { 98 | // var { tmpC, tmpCtx } = tC(); 99 | 100 | // var color = "#444952"; 101 | // var tiles = 10; 102 | // var hStart = 100; 103 | // var hStep = 20; 104 | 105 | // tmpC.width = (TILESIZE.x + 1) * tiles; 106 | // tmpC.height = TILESIZE.y + hStart + hStep * tiles; 107 | 108 | // for (var j = 0; j < tiles; j++) { 109 | // var x = j * (TILESIZE.x + 1) + (TILESIZE.x + 1) / 2; 110 | // var h = hStart + hStep * j; 111 | // var y = h; 112 | 113 | // drawT(tmpCtx, x, y, h, color); 114 | // } 115 | 116 | // var image = new Image(); 117 | 118 | // image.src = tmpC.toDataURL(); 119 | 120 | // return image; 121 | // }; 122 | -------------------------------------------------------------------------------- /src/app/config.ts: -------------------------------------------------------------------------------- 1 | var _canvasProto = CanvasRenderingContext2D.prototype as any; 2 | 3 | _canvasProto.s = _canvasProto.save; 4 | _canvasProto.r = _canvasProto.restore; 5 | _canvasProto._fillRect = _canvasProto.fillRect; 6 | _canvasProto._lineTo = _canvasProto.lineTo; 7 | _canvasProto._translate = _canvasProto.translate; 8 | _canvasProto._beginPath = _canvasProto.beginPath; 9 | _canvasProto._fillStyle = function(x) { 10 | this.fillStyle = x; 11 | }; 12 | -------------------------------------------------------------------------------- /src/app/constants.ts: -------------------------------------------------------------------------------- 1 | import V2 from "./core/V2"; 2 | 3 | export var WIDTH = 768; 4 | export var HEIGHT = 432; 5 | 6 | export var TILESIZE = new V2(60, 30); 7 | 8 | export var mapDim = 60; 9 | -------------------------------------------------------------------------------- /src/app/core/Animation.ts: -------------------------------------------------------------------------------- 1 | import V2 from "./V2"; 2 | 3 | class Animation { 4 | public _totalFrames: number; 5 | public _currentFrame: number = 0; 6 | public _animations: any[] = []; 7 | public _repeats: boolean; 8 | 9 | constructor(tFrames, config, repeats = true) { 10 | // duration in frames 11 | this._totalFrames = tFrames; 12 | this._repeats = repeats; 13 | 14 | ["body", "head", "sword", "shield", "legL", "legR"].forEach((key) => { 15 | // component keyframes 16 | var cKeyFrames = config[key] || [{ f: 0 }, { f: tFrames }]; 17 | 18 | for (var i = 1; i < cKeyFrames.length; i++) { 19 | var prev = cKeyFrames[i - 1]; 20 | var curr = cKeyFrames[i]; 21 | 22 | // frame duration 23 | var fDuration = curr.f - prev.f; 24 | 25 | for (var j = 0; j < fDuration; j++) { 26 | // interpolation t-value 27 | var t = j / fDuration; 28 | 29 | var exists = this._animations[prev.f + j]; 30 | var frameData = exists || {}; 31 | 32 | // component frame data 33 | var cFrameData = {} as any; 34 | 35 | // position 36 | if (prev.hasOwnProperty("p")) { 37 | // linear interpolate between keyframes 38 | var nx = prev.p[0] + (curr.p[0] - prev.p[0]) * t; 39 | var ny = prev.p[1] + (curr.p[1] - prev.p[1]) * t; 40 | cFrameData._position = new V2(nx, ny); 41 | } else { 42 | cFrameData._position = new V2(0, 0); 43 | } 44 | // rotation 45 | if (prev.hasOwnProperty("r")) { 46 | // linear interpolate between keyframes 47 | var nv = prev.r + (curr.r - prev.r) * t; 48 | cFrameData.r = nv; 49 | } else { 50 | cFrameData.r = 0; 51 | } 52 | 53 | frameData[key] = cFrameData; 54 | 55 | if (!exists) { 56 | this._animations.push(frameData); 57 | } 58 | } 59 | } 60 | }); 61 | } 62 | 63 | get _current() { 64 | return this._animations[this._currentFrame]; 65 | } 66 | 67 | get _finished() { 68 | if (this._repeats) { 69 | return false; 70 | } 71 | return this._currentFrame === this._totalFrames - 1; 72 | } 73 | 74 | _update() { 75 | this._currentFrame += 1; 76 | 77 | if (this._currentFrame > this._totalFrames - 1) { 78 | this._currentFrame = 0; 79 | } 80 | } 81 | } 82 | 83 | export default Animation; 84 | -------------------------------------------------------------------------------- /src/app/core/Camera.ts: -------------------------------------------------------------------------------- 1 | import V2 from "./V2"; 2 | import { rect, rndPN } from "./utils"; 3 | 4 | export class Camera { 5 | public _distance: number = 80; 6 | public _targetDistance: number = 576; 7 | public _lookat: V2 = new V2(); 8 | public _fov: number = Math.PI / 4.0; 9 | public _vpRect = new rect(0, 0, 0, 0); 10 | public _vpScale = new V2(1, 1); 11 | 12 | public _lerp = true; 13 | public _lerpD = 0.15; 14 | public _viewportWidth: number; 15 | public _viewportHeight: number; 16 | public _aspectRatio: number; 17 | 18 | public _shake: number; 19 | 20 | constructor(viewportWidth, viewportHeight) { 21 | this._shake = 0; 22 | this._updateViewPort(); 23 | 24 | // viewport dimentions 25 | this._viewportWidth = viewportWidth; 26 | this._viewportHeight = viewportHeight; 27 | // aspect ratio 28 | this._aspectRatio = viewportWidth / viewportHeight; 29 | 30 | // this.renderMoveBounds = false; 31 | } 32 | 33 | _begin(ctx) { 34 | ctx.s(); 35 | this._scale(ctx); 36 | this._translate(ctx); 37 | } 38 | 39 | _end(ctx) { 40 | // if (this.renderMoveBounds) { 41 | // var mid = this._vpRect._mid; 42 | // ctx._beginPath(); 43 | // ctx.arc( 44 | // mid.x, 45 | // mid.y, 46 | // this.moveBoundsLen, 47 | // 0, 48 | // Math.PI * 2, 49 | // 0 50 | // ); 51 | // ctx.strokeStyle = "#0f0"; 52 | // ctx.stroke(); 53 | // } 54 | 55 | ctx.r(); 56 | } 57 | 58 | _scale(ctx) { 59 | ctx.scale(this._vpScale.x, this._vpScale.y); 60 | } 61 | 62 | _translate(ctx) { 63 | ctx._translate(-this._vpRect._left, -this._vpRect._top); 64 | } 65 | 66 | _update(dt) { 67 | this._shake = Math.max(this._shake - dt, 0); 68 | this._zoomTo(this._distance + (this._targetDistance - this._distance) * 0.05); 69 | } 70 | 71 | // _update viewport 72 | _updateViewPort() { 73 | this._vpRect.set( 74 | this._lookat.x - this._vpRect._width / 2.0 + (this._shake ? rndPN() * 6: 0), 75 | this._lookat.y - this._vpRect._height / 2.0 + (this._shake ? rndPN() * 6: 0), 76 | this._distance * Math.tan(this._fov), 77 | this._vpRect._width / this._aspectRatio 78 | ); 79 | 80 | this._vpScale.x = this._viewportWidth / this._vpRect._width; 81 | this._vpScale.y = this._viewportHeight / this._vpRect._height; 82 | } 83 | 84 | // boundsFollow(pos) { 85 | // var bMid = this._vpRect._mid; 86 | // var diff = V2._subtract(pos, bMid); 87 | // var d = diff.len() - this.moveBoundsLen; 88 | 89 | // if (d > 0) { 90 | // this._lookat._add(diff._normalize().scale(d)); 91 | // } 92 | 93 | // this._updateViewPort(); 94 | // } 95 | 96 | _zoomTo(z) { 97 | this._distance = z; 98 | this._updateViewPort(); 99 | } 100 | 101 | _moveTo(x, y) { 102 | var vec = new V2(x,y); 103 | if (this._lerp) { 104 | this._lookat._subtract(V2._subtract(this._lookat, vec)._scale(this._lerpD)); 105 | } else { 106 | this._lookat = vec._copy(); 107 | } 108 | 109 | this._updateViewPort(); 110 | } 111 | 112 | // lockBounds() { 113 | // this._vpRect._left = App.clamp( 114 | // this._vpRect._left, 115 | // this.boundsRect.left, 116 | // this.boundsRect.right - this._vpRect._width 117 | // ); 118 | // this._vpRect._top = App.clamp( 119 | // this._vpRect._top, 120 | // this.boundsRect.top, 121 | // this.boundsRect.bottom - this._vpRect._height 122 | // ); 123 | // } 124 | 125 | // screenToWorld(x, y, obj) { 126 | // obj = obj || new V2(); 127 | // obj.x = x / this._vpScale.x + this._vpRect._left; 128 | // obj.y = y / this._vpScale.y + this._vpRect._top; 129 | // return obj; 130 | // } 131 | 132 | // worldToScreen(x, y, obj) { 133 | // obj = obj || new V2(); 134 | // obj.x = (x - this._vpRect._left) * this._vpScale.x; 135 | // obj.y = (y - this._vpRect._top) * this._vpScale.y; 136 | // return obj; 137 | // } 138 | } 139 | -------------------------------------------------------------------------------- /src/app/core/Emitter.ts: -------------------------------------------------------------------------------- 1 | import { Game } from "../main"; 2 | import V2 from "./V2"; 3 | import GameObject from "./GameObject"; 4 | import { rndPN } from "./utils"; 5 | import Particle from "./Particle"; 6 | 7 | class Emitter extends GameObject { 8 | public _positionVariance: V2 = new V2(); 9 | public _zStart: number = 0; 10 | public _zv: number = 0; 11 | public _zvVariance: number = 0; 12 | public _zgrav: number; 13 | 14 | // rotation variance 15 | public _rVariance: number = 0; 16 | 17 | public _maxParticles: number = 0; 18 | 19 | public _speed: number = 0; 20 | public _speedVariance: number = 0; 21 | 22 | public _size: number = 0; 23 | public _sizeVariance: number = 0; 24 | 25 | public _endSize: number = 0; 26 | public _endSizeVariance: number = 0; 27 | 28 | public _particleLifetime: number = 0; 29 | public _particleLifetimeVariance: number = 0; 30 | 31 | public _color: string = "#fff"; 32 | 33 | public _emitCounter: number = 0; 34 | 35 | public _elapsedTime: number = 0; 36 | 37 | public _duration: number = -1; 38 | 39 | public _addToScene: boolean = false; 40 | 41 | public _particles: Particle[] = []; 42 | 43 | _update(dt) { 44 | //explosion case 45 | if (this._duration === 0) { 46 | while (this._particles.length < this._maxParticles) { 47 | this.emit(); 48 | } 49 | } 50 | 51 | super._update(dt); 52 | 53 | this._particles = this._particles.filter((p) => p._active); 54 | 55 | var emissionRate = this._maxParticles / this._particleLifetime; 56 | 57 | if (this._active && emissionRate > 0) { 58 | var rate = 1 / emissionRate; 59 | 60 | this._emitCounter += dt; 61 | 62 | while ( 63 | this._particles.length < this._maxParticles && 64 | this._emitCounter > rate 65 | ) { 66 | this.emit(); 67 | this._emitCounter -= rate; 68 | } 69 | this._elapsedTime += dt; 70 | 71 | if (this._duration !== -1 && this._duration < this._elapsedTime) { 72 | this._destroy(); 73 | } 74 | } 75 | } 76 | 77 | emit() { 78 | var pVariance = this._positionVariance._copy()._scale(rndPN()); 79 | 80 | if (this._addToScene) { 81 | pVariance = V2._rotateAroundOrigin(pVariance, this._globalAngle()); 82 | } 83 | 84 | var rPos = this._addToScene 85 | ? this._globalPosition() 86 | : this._position._copy(); 87 | rPos.x += pVariance.x; 88 | rPos.y += pVariance.y; 89 | 90 | var baseAngle = this._addToScene ? this._globalAngle() : this._rotation; 91 | var rAngle = baseAngle + this._rVariance * rndPN(); 92 | var rSpeed = this._speed + this._speedVariance * rndPN(); 93 | 94 | var rDir = new V2(Math.cos(rAngle), Math.sin(rAngle))._scale(rSpeed); 95 | 96 | var rSize = this._size + this._sizeVariance * rndPN(); 97 | rSize = rSize < 0 ? 0 : ~~rSize; 98 | 99 | var rEndSize = this._endSize + this._endSizeVariance * rndPN(); 100 | rEndSize = rEndSize < 0 ? 0 : ~~rEndSize; 101 | 102 | var rDeltaZ = this._zv + this._zvVariance * rndPN(); 103 | rDeltaZ = rDeltaZ < 0 ? 0 : ~~rDeltaZ; 104 | 105 | var rLife = 106 | this._particleLifetime + this._particleLifetimeVariance * rndPN(); 107 | 108 | var rDeltaSize = (rEndSize - rSize) / rLife; 109 | 110 | var particle = new Particle( 111 | rPos, 112 | this._zStart, 113 | rDeltaZ, 114 | rDir, 115 | rSize, 116 | rDeltaSize, 117 | rLife, 118 | this._color 119 | ); 120 | 121 | if (this._zgrav !== undefined) { 122 | particle._zgrav = this._zgrav; 123 | } 124 | 125 | if (this._addToScene) { 126 | particle._z = this._globalZ(); 127 | Game._scene._addParticle(particle); 128 | } else { 129 | this._addChild(particle); 130 | } 131 | 132 | this._particles.push(particle); 133 | } 134 | } 135 | 136 | export default Emitter; 137 | -------------------------------------------------------------------------------- /src/app/core/GameNode.ts: -------------------------------------------------------------------------------- 1 | import V2 from "./V2"; 2 | 3 | class GameNode { 4 | public _children: GameNode[] = []; 5 | public _position: V2 = new V2(); 6 | public _z: number = 0; 7 | public _rotation: number = 0; 8 | public _rv: number = 0; 9 | public _active: boolean = true; 10 | public _parent: GameNode; 11 | public _id: number = Math.floor(Math.random() * 9999); 12 | 13 | _globalPosition() { 14 | var pos = this._position._copy(); 15 | var parent = this._parent; 16 | while (parent) { 17 | pos = V2._rotateAroundOrigin(pos, parent._rotation) 18 | pos._add(parent._position); 19 | parent = parent._parent; 20 | } 21 | 22 | return pos; 23 | } 24 | 25 | _globalAngle() { 26 | var r = this._rotation; 27 | var parent = this._parent; 28 | while (parent) { 29 | r += parent._rotation; 30 | parent = parent._parent; 31 | } 32 | 33 | return r; 34 | } 35 | 36 | _globalZ() { 37 | var r = this._z; 38 | var parent = this._parent; 39 | while (parent) { 40 | r += parent._z; 41 | parent = parent._parent; 42 | } 43 | 44 | return r; 45 | } 46 | 47 | _addChild(child) { 48 | child._parent = this; 49 | this._children.push(child); 50 | } 51 | 52 | _update(dt) { 53 | this._rotation += this._rv; 54 | 55 | var length = this._children.length; 56 | while (length--) { 57 | var child = this._children[length]; 58 | child._update(dt); 59 | if (!child._active) { 60 | this._children.splice(length, 1); 61 | continue; 62 | } 63 | } 64 | } 65 | 66 | _draw(ctx) { 67 | var length = this._children.length; 68 | 69 | if (length === 0) { 70 | return; 71 | } 72 | ctx.s(); 73 | ctx._translate(this._position.x, this._position.y - this._z); 74 | ctx.rotate(this._rotation); 75 | 76 | while (length--) { 77 | this._children[length]._draw(ctx); 78 | } 79 | ctx.r(); 80 | } 81 | } 82 | 83 | export default GameNode; 84 | -------------------------------------------------------------------------------- /src/app/core/GameObject.ts: -------------------------------------------------------------------------------- 1 | import V2 from "./V2"; 2 | import GameNode from "./GameNode"; 3 | import { Game } from "../main"; 4 | import SteeringManager from "./ai/SteeringManager"; 5 | import { c2i, walkTile, i2c } from "./utils"; 6 | import { CollisionRect } from "./SimpleCollision/index"; 7 | import BaseTile from "../components/tiles/BaseTile"; 8 | import { mapDim } from "../constants"; 9 | 10 | class GameObject extends GameNode { 11 | public _v: V2 = new V2(); 12 | public _a: V2 = new V2(); 13 | public _zv: number = 0; 14 | public _zgrav: number = 0.098; 15 | public _speed: number = 10; 16 | public _maxSpeed: number = 100; 17 | public _maxForce: number = 1; 18 | public _maxHp: number = Infinity; 19 | public _hp: number; 20 | public _damage: number; 21 | public _lifeSpan: number = -1; 22 | public _age: number = 0; 23 | public _hitBox: CollisionRect; 24 | public _steering: SteeringManager; 25 | public _visionRange: number = 100; 26 | public _tileYOffset: number = 0; 27 | // TODO just use tile and get type from it? 28 | public _currentTile: BaseTile; 29 | public _currentTileType: number; 30 | public _calcSlide: boolean = true; 31 | public _opacity: number = 1; 32 | 33 | get _verticalOffset() { 34 | return this._tileYOffset + this._z; 35 | } 36 | 37 | constructor(p = new V2()) { 38 | super(); 39 | this._position = p; 40 | this._steering = new SteeringManager(this); 41 | } 42 | 43 | _addChild(child) { 44 | child._parent = this; 45 | this._children.push(child); 46 | } 47 | 48 | _destroy() { 49 | this._active = false; 50 | 51 | if (this._hitBox) { 52 | this._hitBox._destroy(); 53 | } 54 | 55 | this._parent = undefined; 56 | } 57 | 58 | _update(dt) { 59 | if (!this._active) return; 60 | 61 | this._age += dt; 62 | 63 | if ( 64 | this._hp <= 0 || 65 | (this._lifeSpan && this._lifeSpan !== -1 && this._age >= this._lifeSpan) 66 | ) { 67 | this._destroy(); 68 | return; 69 | } 70 | 71 | // var tt = dt / 1000; 72 | 73 | var previousPosition = this._position._copy(); 74 | 75 | this._a._add(this._steering._force._limit(this._maxForce)); 76 | 77 | this._v._add(this._a)._limit(this._maxSpeed * (dt / 1000)); 78 | this._position._add(this._v); 79 | 80 | this._a._reset(); 81 | this._steering._force._reset(); 82 | 83 | this._zv -= this._zgrav; 84 | this._z = Math.max(0, this._z + this._zv); 85 | if (this._z === 0) { 86 | // TODO should probably have an acc value so this hack isn't necessary 87 | this._zv = 0; 88 | this._rv = 0; 89 | } 90 | 91 | this._calcSlide && this._slide(previousPosition); 92 | 93 | this._setYOff(); 94 | 95 | super._update(dt); 96 | } 97 | 98 | _getTile(isoP): BaseTile { 99 | if ( 100 | !Game._scene._tileMap || 101 | Game._scene._tileMap._map.length === 0 || 102 | isoP.x >= mapDim || 103 | isoP.x < 0 || 104 | isoP.y >= mapDim || 105 | isoP.y < 0 106 | ) { 107 | return null; 108 | } 109 | return Game._scene._tileMap._map[isoP.y][isoP.x]; 110 | } 111 | 112 | _distanceToPlayer(): number { 113 | return V2._distance(this._position, Game._scene._player._position); 114 | } 115 | 116 | _slide(curr) { 117 | var currC = c2i(curr); 118 | var nextC = c2i(this._position); 119 | 120 | // TODO performance measure 121 | var nTile = this._getTile(nextC._copy()._floor()); 122 | if (nTile && walkTile(nTile._tileType)) { 123 | return; 124 | } 125 | 126 | var newP = currC._copy(); 127 | 128 | // get the vector that represents the change in pos 129 | var diffVec = V2._subtract(nextC, currC); 130 | 131 | // check x 132 | var xVec = diffVec._copy(); 133 | xVec.y = 0; 134 | var tile = this._getTile(V2._add(currC, xVec)._floor()); 135 | if (tile && walkTile(tile._tileType)) { 136 | newP._add(xVec); 137 | } 138 | 139 | // check y 140 | var yVec = diffVec._copy(); 141 | yVec.x = 0; 142 | tile = this._getTile(V2._add(currC, yVec)._floor()); 143 | if (tile && walkTile(tile._tileType)) { 144 | newP._add(yVec); 145 | } 146 | 147 | this._position = i2c(newP); 148 | } 149 | 150 | _setYOff() { 151 | var currentTilePos = c2i(this._globalPosition())._floor(); 152 | var tile = this._getTile(currentTilePos); 153 | 154 | if (tile) { 155 | var diff = tile._height - this._tileYOffset; 156 | if (diff < 0 && walkTile(tile)) { 157 | // dropping down 158 | this._z += this._tileYOffset - tile._height; 159 | } 160 | this._tileYOffset = tile._height; 161 | this._currentTileType = tile._tileType; 162 | this._currentTile = tile; 163 | 164 | if (diff > 0) this._z = Math.max(0, this._z - diff); 165 | } 166 | } 167 | } 168 | 169 | export default GameObject; 170 | -------------------------------------------------------------------------------- /src/app/core/InputController.ts: -------------------------------------------------------------------------------- 1 | class InputController { 2 | public _KeyW: boolean; 3 | public _KeyA: boolean; 4 | public _KeyS: boolean; 5 | public _KeyD: boolean; 6 | public _KeyJ: boolean; 7 | public _KeyK: boolean; 8 | public _Space: boolean; 9 | 10 | constructor() { 11 | 12 | var listener = (flag) => (e) => { 13 | if(e.which === 1) { 14 | this._KeyJ = flag; 15 | } else if (e.which === 3) { 16 | e.preventDefault(); 17 | this._KeyK = flag; 18 | } 19 | 20 | switch (e.code) { 21 | case "KeyD": //d 22 | this._KeyD = flag; 23 | break; 24 | case "KeyS": //s 25 | this._KeyS = flag; 26 | break; 27 | case "KeyA": //a 28 | this._KeyA = flag; 29 | break; 30 | case "KeyW": //w 31 | this._KeyW = flag; 32 | break; 33 | case "Space": //space 34 | this._Space = flag; 35 | break; 36 | case "KeyJ": //j 37 | this._KeyJ = flag; 38 | break; 39 | case "KeyK": //k 40 | this._KeyK = flag; 41 | break; 42 | } 43 | } 44 | 45 | window.addEventListener("keydown", listener(true)); 46 | window.addEventListener("keyup", listener(false)); 47 | window.addEventListener("mousedown", listener(true)); 48 | window.addEventListener("mouseup", listener(false)); 49 | } 50 | 51 | // init(canvas) { 52 | // function getMousePos(canvas, evt) { 53 | // var rect = canvas.getBoundingClientRect(); 54 | // return { 55 | // x: evthis.clientX - recthis.left, 56 | // y: evthis.clientY - recthis.top, 57 | // }; 58 | // } 59 | 60 | // canvas.addEventListener( 61 | // "mousemove", 62 | // (evt) => { 63 | // this.mousePosition = getMousePos(canvas, evt); 64 | // }, 65 | // false 66 | // ); 67 | 68 | // canvas.addEventListener("mousedown", (evt) => { 69 | // this.mouseDown = true; 70 | // }); 71 | 72 | // canvas.addEventListener("mouseup", (evt) => { 73 | // this.mouseDown = false; 74 | // }); 75 | // } 76 | } 77 | 78 | export default new InputController(); 79 | -------------------------------------------------------------------------------- /src/app/core/Particle.ts: -------------------------------------------------------------------------------- 1 | import GameObject from "./GameObject"; 2 | 3 | class Particle extends GameObject { 4 | public _size: number; 5 | public _deltaSize: number; 6 | public _color: number; 7 | 8 | constructor(p, z, zv, _v, size, deltaSize, lifeSpan, color) { 9 | super(p); 10 | 11 | this._z = z; 12 | this._zv = zv; 13 | this._v = _v; 14 | this._size = size; 15 | this._deltaSize = deltaSize; 16 | this._lifeSpan = lifeSpan; 17 | this._color = color; 18 | 19 | this._setYOff(); 20 | } 21 | 22 | _update(dt) { 23 | super._update(dt); 24 | 25 | this._size += this._deltaSize * dt; 26 | } 27 | 28 | _draw(ctx) { 29 | ctx._fillStyle("rgba(0,0,0,0.5"); 30 | ctx._fillRect( 31 | this._position.x - this._size / 4, 32 | this._position.y + this._size / 4 - this._tileYOffset, 33 | this._size / 2, 34 | this._size / 2 35 | ); 36 | 37 | ctx._fillStyle(this._color); 38 | ctx._fillRect( 39 | this._position.x - this._size / 2, 40 | this._position.y - this._size / 2 - this._verticalOffset, 41 | this._size, 42 | this._size 43 | ); 44 | 45 | super._draw(ctx); 46 | } 47 | } 48 | 49 | export default Particle; 50 | -------------------------------------------------------------------------------- /src/app/core/Perlin.js: -------------------------------------------------------------------------------- 1 | /* 2 | * A speed-improved perlin and simplex noise algorithms for 2D. 3 | * 4 | * Based on example code by Stefan Gustavson (stegu@itn.liu.se). 5 | * Optimisations by Peter Eastman (peastman@drizzle.stanford.edu). 6 | * Better rank ordering method by Stefan Gustavson in 2012. 7 | * Converted to Javascript by Joseph Gentle. 8 | * 9 | * Version 2012-03-09 10 | * 11 | * This code was placed in the public domain by its original author, 12 | * Stefan Gustavson. You may use it as you see fit, but 13 | * attribution is appreciated. 14 | * 15 | */ 16 | 17 | (function (global, factory) { 18 | if (typeof exports === "object") { 19 | module.exports = factory(global); 20 | } else { 21 | global.noise = factory(global); 22 | } 23 | })(window, function (global) { 24 | "use strict"; 25 | 26 | var module = {}; 27 | 28 | function Grad(x, y, z) { 29 | this.x = x; 30 | this.y = y; 31 | this.z = z; 32 | } 33 | 34 | Grad.prototype._dot3 = function (x, y, z) { 35 | return this.x * x + this.y * y + this.z * z; 36 | }; 37 | 38 | var grad3 = [ 39 | new Grad(1, 1, 0), 40 | new Grad(-1, 1, 0), 41 | new Grad(1, -1, 0), 42 | new Grad(-1, -1, 0), 43 | new Grad(1, 0, 1), 44 | new Grad(-1, 0, 1), 45 | new Grad(1, 0, -1), 46 | new Grad(-1, 0, -1), 47 | new Grad(0, 1, 1), 48 | new Grad(0, -1, 1), 49 | new Grad(0, 1, -1), 50 | new Grad(0, -1, -1), 51 | ]; 52 | 53 | var p = [ 54 | 151, 55 | 160, 56 | 137, 57 | 91, 58 | 90, 59 | 15, 60 | 131, 61 | 13, 62 | 201, 63 | 95, 64 | 96, 65 | 53, 66 | 194, 67 | 233, 68 | 7, 69 | 225, 70 | 140, 71 | 36, 72 | 103, 73 | 30, 74 | 69, 75 | 142, 76 | 8, 77 | 99, 78 | 37, 79 | 240, 80 | 21, 81 | 10, 82 | 23, 83 | 190, 84 | 6, 85 | 148, 86 | 247, 87 | 120, 88 | 234, 89 | 75, 90 | 0, 91 | 26, 92 | 197, 93 | 62, 94 | 94, 95 | 252, 96 | 219, 97 | 203, 98 | 117, 99 | 35, 100 | 11, 101 | 32, 102 | 57, 103 | 177, 104 | 33, 105 | 88, 106 | 237, 107 | 149, 108 | 56, 109 | 87, 110 | 174, 111 | 20, 112 | 125, 113 | 136, 114 | 171, 115 | 168, 116 | 68, 117 | 175, 118 | 74, 119 | 165, 120 | 71, 121 | 134, 122 | 139, 123 | 48, 124 | 27, 125 | 166, 126 | 77, 127 | 146, 128 | 158, 129 | 231, 130 | 83, 131 | 111, 132 | 229, 133 | 122, 134 | 60, 135 | 211, 136 | 133, 137 | 230, 138 | 220, 139 | 105, 140 | 92, 141 | 41, 142 | 55, 143 | 46, 144 | 245, 145 | 40, 146 | 244, 147 | 102, 148 | 143, 149 | 54, 150 | 65, 151 | 25, 152 | 63, 153 | 161, 154 | 1, 155 | 216, 156 | 80, 157 | 73, 158 | 209, 159 | 76, 160 | 132, 161 | 187, 162 | 208, 163 | 89, 164 | 18, 165 | 169, 166 | 200, 167 | 196, 168 | 135, 169 | 130, 170 | 116, 171 | 188, 172 | 159, 173 | 86, 174 | 164, 175 | 100, 176 | 109, 177 | 198, 178 | 173, 179 | 186, 180 | 3, 181 | 64, 182 | 52, 183 | 217, 184 | 226, 185 | 250, 186 | 124, 187 | 123, 188 | 5, 189 | 202, 190 | 38, 191 | 147, 192 | 118, 193 | 126, 194 | 255, 195 | 82, 196 | 85, 197 | 212, 198 | 207, 199 | 206, 200 | 59, 201 | 227, 202 | 47, 203 | 16, 204 | 58, 205 | 17, 206 | 182, 207 | 189, 208 | 28, 209 | 42, 210 | 223, 211 | 183, 212 | 170, 213 | 213, 214 | 119, 215 | 248, 216 | 152, 217 | 2, 218 | 44, 219 | 154, 220 | 163, 221 | 70, 222 | 221, 223 | 153, 224 | 101, 225 | 155, 226 | 167, 227 | 43, 228 | 172, 229 | 9, 230 | 129, 231 | 22, 232 | 39, 233 | 253, 234 | 19, 235 | 98, 236 | 108, 237 | 110, 238 | 79, 239 | 113, 240 | 224, 241 | 232, 242 | 178, 243 | 185, 244 | 112, 245 | 104, 246 | 218, 247 | 246, 248 | 97, 249 | 228, 250 | 251, 251 | 34, 252 | 242, 253 | 193, 254 | 238, 255 | 210, 256 | 144, 257 | 12, 258 | 191, 259 | 179, 260 | 162, 261 | 241, 262 | 81, 263 | 51, 264 | 145, 265 | 235, 266 | 249, 267 | 14, 268 | 239, 269 | 107, 270 | 49, 271 | 192, 272 | 214, 273 | 31, 274 | 181, 275 | 199, 276 | 106, 277 | 157, 278 | 184, 279 | 84, 280 | 204, 281 | 176, 282 | 115, 283 | 121, 284 | 50, 285 | 45, 286 | 127, 287 | 4, 288 | 150, 289 | 254, 290 | 138, 291 | 236, 292 | 205, 293 | 93, 294 | 222, 295 | 114, 296 | 67, 297 | 29, 298 | 24, 299 | 72, 300 | 243, 301 | 141, 302 | 128, 303 | 195, 304 | 78, 305 | 66, 306 | 215, 307 | 61, 308 | 156, 309 | 180, 310 | ]; 311 | // To remove the need for index wrapping, double the permutation table length 312 | var perm = new Array(512); 313 | var gradP = new Array(512); 314 | 315 | // This isn't a very good seeding function, but it works ok. It supports 2^16 316 | // different seed values. Write something better if you need more seeds. 317 | module._seed = (seed) => { 318 | // if(seed > 0 && seed < 1) { 319 | // // Scale the seed out 320 | // seed *= 65536; 321 | // } 322 | 323 | // seed = Math.floor(seed); 324 | // if (seed < 256) { 325 | seed |= seed << 8; 326 | // } 327 | 328 | for (var i = 0; i < 256; i++) { 329 | var v; 330 | if (i & 1) { 331 | v = p[i] ^ (seed & 255); 332 | } else { 333 | v = p[i] ^ ((seed >> 8) & 255); 334 | } 335 | 336 | perm[i] = perm[i + 256] = v; 337 | gradP[i] = gradP[i + 256] = grad3[v % 12]; 338 | } 339 | }; 340 | 341 | module._seed(0); 342 | 343 | /* 344 | for(var i=0; i<256; i++) { 345 | perm[i] = perm[i + 256] = p[i]; 346 | gradP[i] = gradP[i + 256] = grad3[perm[i] % 12]; 347 | }*/ 348 | 349 | // Skewing and unskewing factors for 2, 3, and 4 dimensions 350 | // var F2 = 0.5*(Math.sqrt(3)-1); 351 | // var G2 = (3-Math.sqrt(3))/6; 352 | 353 | var F3 = 1 / 3; 354 | var G3 = 1 / 6; 355 | 356 | // 3D simplex noise 357 | module._simplex3 = (xin, yin, zin) => { 358 | var n0, n1, n2, n3; // Noise contributions from the four corners 359 | 360 | // Skew the input space to determine which simplex cell we're in 361 | var s = (xin + yin + zin) * F3; // Hairy factor for 2D 362 | var i = Math.floor(xin + s); 363 | var j = Math.floor(yin + s); 364 | var k = Math.floor(zin + s); 365 | 366 | var t = (i + j + k) * G3; 367 | var x0 = xin - i + t; // The x,y distances from the cell origin, unskewed. 368 | var y0 = yin - j + t; 369 | var z0 = zin - k + t; 370 | 371 | // For the 3D case, the simplex shape is a slightly irregular tetrahedron. 372 | // Determine which simplex we are in. 373 | var i1, j1, k1; // Offsets for second corner of simplex in (i,j,k) coords 374 | var i2, j2, k2; // Offsets for third corner of simplex in (i,j,k) coords 375 | if (x0 >= y0) { 376 | if (y0 >= z0) { 377 | i1 = 1; 378 | j1 = 0; 379 | k1 = 0; 380 | i2 = 1; 381 | j2 = 1; 382 | k2 = 0; 383 | } else if (x0 >= z0) { 384 | i1 = 1; 385 | j1 = 0; 386 | k1 = 0; 387 | i2 = 1; 388 | j2 = 0; 389 | k2 = 1; 390 | } else { 391 | i1 = 0; 392 | j1 = 0; 393 | k1 = 1; 394 | i2 = 1; 395 | j2 = 0; 396 | k2 = 1; 397 | } 398 | } else { 399 | if (y0 < z0) { 400 | i1 = 0; 401 | j1 = 0; 402 | k1 = 1; 403 | i2 = 0; 404 | j2 = 1; 405 | k2 = 1; 406 | } else if (x0 < z0) { 407 | i1 = 0; 408 | j1 = 1; 409 | k1 = 0; 410 | i2 = 0; 411 | j2 = 1; 412 | k2 = 1; 413 | } else { 414 | i1 = 0; 415 | j1 = 1; 416 | k1 = 0; 417 | i2 = 1; 418 | j2 = 1; 419 | k2 = 0; 420 | } 421 | } 422 | // A step of (1,0,0) in (i,j,k) means a step of (1-c,-c,-c) in (x,y,z), 423 | // a step of (0,1,0) in (i,j,k) means a step of (-c,1-c,-c) in (x,y,z), and 424 | // a step of (0,0,1) in (i,j,k) means a step of (-c,-c,1-c) in (x,y,z), where 425 | // c = 1/6. 426 | var x1 = x0 - i1 + G3; // Offsets for second corner 427 | var y1 = y0 - j1 + G3; 428 | var z1 = z0 - k1 + G3; 429 | 430 | var x2 = x0 - i2 + 2 * G3; // Offsets for third corner 431 | var y2 = y0 - j2 + 2 * G3; 432 | var z2 = z0 - k2 + 2 * G3; 433 | 434 | var x3 = x0 - 1 + 3 * G3; // Offsets for fourth corner 435 | var y3 = y0 - 1 + 3 * G3; 436 | var z3 = z0 - 1 + 3 * G3; 437 | 438 | // Work out the hashed gradient indices of the four simplex corners 439 | i &= 255; 440 | j &= 255; 441 | k &= 255; 442 | var gi0 = gradP[i + perm[j + perm[k]]]; 443 | var gi1 = gradP[i + i1 + perm[j + j1 + perm[k + k1]]]; 444 | var gi2 = gradP[i + i2 + perm[j + j2 + perm[k + k2]]]; 445 | var gi3 = gradP[i + 1 + perm[j + 1 + perm[k + 1]]]; 446 | 447 | // Calculate the contribution from the four corners 448 | var t0 = 0.6 - x0 * x0 - y0 * y0 - z0 * z0; 449 | if (t0 < 0) { 450 | n0 = 0; 451 | } else { 452 | t0 *= t0; 453 | n0 = t0 * t0 * gi0._dot3(x0, y0, z0); // (x,y) of grad3 used for 2D gradient 454 | } 455 | var t1 = 0.6 - x1 * x1 - y1 * y1 - z1 * z1; 456 | if (t1 < 0) { 457 | n1 = 0; 458 | } else { 459 | t1 *= t1; 460 | n1 = t1 * t1 * gi1._dot3(x1, y1, z1); 461 | } 462 | var t2 = 0.6 - x2 * x2 - y2 * y2 - z2 * z2; 463 | if (t2 < 0) { 464 | n2 = 0; 465 | } else { 466 | t2 *= t2; 467 | n2 = t2 * t2 * gi2._dot3(x2, y2, z2); 468 | } 469 | var t3 = 0.6 - x3 * x3 - y3 * y3 - z3 * z3; 470 | if (t3 < 0) { 471 | n3 = 0; 472 | } else { 473 | t3 *= t3; 474 | n3 = t3 * t3 * gi3._dot3(x3, y3, z3); 475 | } 476 | // Add contributions from each corner to get the final noise value. 477 | // The result is scaled to return values in the interval [-1,1]. 478 | return 32 * (n0 + n1 + n2 + n3); 479 | }; 480 | 481 | // ##### Perlin noise stuff 482 | 483 | // function fade(t) { 484 | // return t*t*t*(t*(t*6-15)+10); 485 | // } 486 | 487 | // function lerp(a, b, t) { 488 | // return (1-t)*a + t*b; 489 | // } 490 | 491 | // 3D Perlin Noise 492 | // module.perlin3 = function(x, y, z) { 493 | // // Find unit grid cell containing point 494 | // var X = Math.floor(x), Y = Math.floor(y), Z = Math.floor(z); 495 | // // Get relative xyz coordinates of point within that cell 496 | // x = x - X; y = y - Y; z = z - Z; 497 | // // Wrap the integer cells at 255 (smaller integer period can be introduced here) 498 | // X = X & 255; Y = Y & 255; Z = Z & 255; 499 | 500 | // // Calculate noise contributions from each of the eight corners 501 | // var n000 = gradP[X+ perm[Y+ perm[Z ]]]._dot3(x, y, z); 502 | // var n001 = gradP[X+ perm[Y+ perm[Z+1]]]._dot3(x, y, z-1); 503 | // var n010 = gradP[X+ perm[Y+1+perm[Z ]]]._dot3(x, y-1, z); 504 | // var n011 = gradP[X+ perm[Y+1+perm[Z+1]]]._dot3(x, y-1, z-1); 505 | // var n100 = gradP[X+1+perm[Y+ perm[Z ]]]._dot3(x-1, y, z); 506 | // var n101 = gradP[X+1+perm[Y+ perm[Z+1]]]._dot3(x-1, y, z-1); 507 | // var n110 = gradP[X+1+perm[Y+1+perm[Z ]]]._dot3(x-1, y-1, z); 508 | // var n111 = gradP[X+1+perm[Y+1+perm[Z+1]]]._dot3(x-1, y-1, z-1); 509 | 510 | // // Compute the fade curve value for x, y, z 511 | // var u = fade(x); 512 | // var v = fade(y); 513 | // var w = fade(z); 514 | 515 | // // Interpolate 516 | // return lerp( 517 | // lerp( 518 | // lerp(n000, n100, u), 519 | // lerp(n001, n101, u), w), 520 | // lerp( 521 | // lerp(n010, n110, u), 522 | // lerp(n011, n111, u), w), 523 | // v); 524 | // }; 525 | 526 | return module; 527 | }); 528 | -------------------------------------------------------------------------------- /src/app/core/Scene.ts: -------------------------------------------------------------------------------- 1 | import { i2c, inRng } from "./utils"; 2 | import { Camera } from "./Camera"; 3 | import { WIDTH, HEIGHT } from "../constants"; 4 | import V2 from "./V2"; 5 | import { SimpleCollision } from "./SimpleCollision/index"; 6 | import GameObject from "./GameObject"; 7 | import Leonidus from "../components/entities/Leonidus"; 8 | import TileMap from "../components/TileMap"; 9 | 10 | export default class Scene extends GameObject { 11 | public _collisions: any = new SimpleCollision(); 12 | public _cam: Camera; 13 | public _tileMap: TileMap; 14 | public _player: Leonidus; 15 | 16 | public _done: boolean; 17 | 18 | constructor() { 19 | super(); 20 | 21 | this._cam = new Camera(WIDTH, HEIGHT); 22 | 23 | this._cam._lookat = i2c(new V2(5, 5)); 24 | } 25 | 26 | _update(dt) { 27 | this._cam._update(dt); 28 | 29 | this._collisions._update(dt); 30 | 31 | super._update(dt); 32 | } 33 | 34 | _addParticle(p) { 35 | this._addChild(p); 36 | } 37 | 38 | _inViewport(p) { 39 | return ( 40 | inRng( 41 | p.x, 42 | this._cam._vpRect._left - 100 /*- obj.radius*/, 43 | this._cam._vpRect._right + 100 /*+ obj.radius*/ 44 | ) && 45 | inRng( 46 | p.y, 47 | this._cam._vpRect._top - 100 /*- obj.radius*/, 48 | this._cam._vpRect._bottom + 100 /*+ obj.radius*/ 49 | ) 50 | ); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/app/core/SimpleCollision/index.ts: -------------------------------------------------------------------------------- 1 | import V2 from "../V2"; 2 | import GameNode from "../GameNode"; 3 | import { Game } from "../../main"; 4 | import GameObject from "../GameObject"; 5 | 6 | export class SimpleCollision extends GameNode { 7 | _overlapRect(rect) { 8 | return this._children.filter((child) => { 9 | return this._check(rect, child); 10 | }); 11 | } 12 | 13 | _check(a, b) { 14 | if (a === b || a._object._id === b._object._id) { 15 | return; 16 | } 17 | return this._rectRect(a, b); 18 | } 19 | 20 | _rectRect(a, b) { 21 | // aabb -- must be axis aligned 22 | if ( 23 | a.p.x < b.p.x + b.w && 24 | a.p.x + a.w > b.p.x && 25 | a.p.y < b.p.y + b.h && 26 | a.p.y + a.h > b.p.y 27 | ) { 28 | return true; 29 | } 30 | 31 | return false; 32 | } 33 | 34 | // collision(a, b) { 35 | // a.obj.collide(b.obj, b); 36 | // b.obj.collide(a.obj, a); 37 | // } 38 | } 39 | export class CollisionRect extends GameObject { 40 | public p: V2; 41 | public _object: GameObject; 42 | 43 | public _offset: V2; 44 | 45 | public w: number; 46 | public h: number; 47 | 48 | constructor(obj, offset, w, h, lifeSpan = -1, add = false) { 49 | super(); 50 | this.w = w; 51 | this.h = h; 52 | this._object = obj; 53 | this._offset = offset || new V2(); 54 | 55 | this._update(0); 56 | 57 | this._lifeSpan = lifeSpan; 58 | 59 | if (add) { 60 | Game._scene._collisions._addChild(this); 61 | } 62 | } 63 | 64 | _update(dt) { 65 | super._update(dt); 66 | this.p = this._object._position._copy()._add(this._offset)._add(new V2(0, -this._object._z)); 67 | } 68 | 69 | // _draw(ctx) { 70 | // ctx.save(); 71 | // ctx.lineWidth = 1; 72 | // ctx.strokeStyle = "#f00"; 73 | // ctx._beginPath(); 74 | // ctx.rect(this.p.x, this.p.y, this.w, this.h); 75 | // ctx.stroke(); 76 | // ctx.r(); 77 | // } 78 | } 79 | -------------------------------------------------------------------------------- /src/app/core/V2.ts: -------------------------------------------------------------------------------- 1 | class V2 { 2 | public x: number; 3 | public y: number; 4 | 5 | static _add(a, b) { 6 | return new V2(a.x + b.x, a.y + b.y); 7 | } 8 | 9 | static _subtract(a, b) { 10 | return new V2(a.x - b.x, a.y - b.y); 11 | } 12 | 13 | static _scale(a, b) { 14 | return b instanceof V2 15 | ? new V2(a.x * b.x, a.y * b.y) 16 | : new V2(a.x * b, a.y * b); 17 | } 18 | 19 | // static dot(a, b) { 20 | // return a.x * b.x + a.y * b.y; 21 | // } 22 | 23 | // static cross(a, b) { 24 | // return a.x * b.y - a.y * b.x; 25 | // } 26 | 27 | // static equals(a, b) { 28 | // return a.x === b.x && a.y === b.y; 29 | // } 30 | 31 | // static midPoint(a, b) { 32 | // return V2._scale(V2._add(a, b), 0.5); 33 | // } 34 | 35 | static _distance(a, b) { 36 | return Math.sqrt(Math.pow(b.x - a.x, 2) + Math.pow(b.y - a.y, 2)); 37 | } 38 | 39 | static _fromAngle(r) { 40 | return new V2(Math.cos(r), Math.sin(r)); 41 | } 42 | 43 | // Rotates a vector around the origin. Shorthand for a rotation matrix 44 | static _rotateAroundOrigin(v, a) { 45 | return new V2( 46 | v.x * Math.cos(a) - v.y * Math.sin(a), 47 | v.x * Math.sin(a) + v.y * Math.cos(a) 48 | ); 49 | } 50 | 51 | // Rotates a vector around a given point. 52 | // static _rotateAroundPoint(v, cp, a) { 53 | // var v2 = V2._subtract(v, cp); 54 | // return V2._add( 55 | // cp, 56 | // new V2( 57 | // v2.x * Math.cos(a) - v2.y * Math.sin(a), 58 | // v2.x * Math.sin(a) + v2.y * Math.cos(a) 59 | // ) 60 | // ); 61 | // } 62 | 63 | // 64 | constructor(x = 0, y = 0) { 65 | this.x = x; 66 | this.y = y; 67 | } 68 | 69 | _copy() { 70 | return new V2(this.x, this.y); 71 | } 72 | 73 | // get magnitude of a vector 74 | _magnitude() { 75 | return Math.sqrt(this.x * this.x + this.y * this.y); 76 | } 77 | 78 | // lenSquared() { 79 | // return this.x * this.x + this.y * this.y; 80 | // } 81 | 82 | // get the normal of a vector 83 | // normal() { 84 | // return new V2(-this.y, this.x); 85 | // } 86 | 87 | // get a point along v2-v1, t is % along line 88 | // towards(v, t) { 89 | // var dVec = v._subtract(this); 90 | // var m = dVec.len(); 91 | 92 | // return this._add(dVec._normalize()._scale(t * m)); 93 | // } 94 | 95 | // angle(v) { 96 | // return Math.atan2(this.x * v.y - this.y * v.x, this.x * v.x + this.y * v.y); 97 | // } 98 | 99 | // angle2(vLeft, vRight) { 100 | // return vLeft._subtract(this).angle(vRight._subtract(this)); 101 | // } 102 | 103 | _normalize() { 104 | var m = this._magnitude(); 105 | 106 | return m > 0 ? this._scale(1 / m) : this; 107 | } 108 | 109 | _limit(max) { 110 | if (this._magnitude() > max) { 111 | this._normalize(); 112 | 113 | return this._scale(max); 114 | } 115 | 116 | return this; 117 | } 118 | 119 | _add(v) { 120 | this.x += v.x; 121 | this.y += v.y; 122 | 123 | return this; 124 | } 125 | 126 | _subtract(v) { 127 | this.x -= v.x; 128 | this.y -= v.y; 129 | 130 | return this; 131 | } 132 | 133 | // _negative() { 134 | // return this._scale(-1); 135 | // } 136 | 137 | _scale(sc) { 138 | this.x *= sc; 139 | this.y *= sc; 140 | 141 | return this; 142 | } 143 | 144 | _floor() { 145 | this.x = Math.floor(this.x); 146 | this.y = Math.floor(this.y); 147 | 148 | return this; 149 | } 150 | 151 | _reset() { 152 | this.x = 0; 153 | this.y = 0; 154 | return this; 155 | } 156 | } 157 | 158 | export default V2; 159 | -------------------------------------------------------------------------------- /src/app/core/ai/Behavior.ts: -------------------------------------------------------------------------------- 1 | import GameObject from "../GameObject"; 2 | 3 | class Behavior { 4 | public _actor: GameObject; 5 | public _config: any; 6 | 7 | constructor(actor) { 8 | this._actor = actor; 9 | this._config = {}; 10 | } 11 | 12 | // turnToFace(vec) { 13 | // var desiredAngle = this.getAngleToPoint(vec); 14 | 15 | // var difference = wrapAngle(desiredAngle - this.actor.direction); 16 | 17 | // difference = clamp(difference, -this.actor._turnSpeed, this.actor._turnSpeed); 18 | // this.actor.r = wrapAngle(this.actor.r + difference); 19 | // } 20 | 21 | // turnAwayFrom(vec) { 22 | // var desiredAngle = this.getAngleToPoint(vec); 23 | // desiredAngle -= Math.PI; 24 | 25 | // var difference = wrapAngle(desiredAngle - this.actor.r); 26 | 27 | // difference = clamp(difference, -this.actor._turnSpeed, this.actor._turnSpeed); 28 | // this.actor.r = wrapAngle(this.actor.r + difference); 29 | // } 30 | 31 | // getAngleToPoint(point) { 32 | // var x = point.x - this.actor.p.x; 33 | // var y = point.y - this.actor.p.y; 34 | 35 | // return Math.atan2(y, x); 36 | // } 37 | } 38 | 39 | export default Behavior; -------------------------------------------------------------------------------- /src/app/core/ai/FleeBehavior.ts: -------------------------------------------------------------------------------- 1 | import SeekBehavior from "./SeekBehavior"; 2 | 3 | class FleeBehavior extends SeekBehavior { 4 | constructor(actor) { 5 | super(actor); 6 | 7 | this._config = { 8 | _strength: 2, 9 | }; 10 | } 11 | 12 | _run(target, strengthMod) { 13 | return super._run(target, strengthMod)._scale(-1); 14 | } 15 | } 16 | 17 | export default FleeBehavior; 18 | -------------------------------------------------------------------------------- /src/app/core/ai/FlockBehavior.ts: -------------------------------------------------------------------------------- 1 | import Behavior from "./Behavior"; 2 | import V2 from "../V2"; 3 | import { Game } from "../../main"; 4 | import Hoplite from "../../components/entities/Hoplite"; 5 | 6 | class FlockBehavior extends Behavior { 7 | constructor(actor) { 8 | super(actor); 9 | 10 | this._config = { 11 | _separationWeight: 5, 12 | // _alignmentWeight: 1, 13 | _cohesionWeight: 0.75, 14 | _desiredSeparation: 75, 15 | _neighborRadius: 300, // TODO match vision range? 16 | _strength: 1, 17 | }; 18 | } 19 | 20 | // @ts-ignore 21 | _run(strengthMod) { 22 | return this._flock(Game._scene._spawner._entities) 23 | ._normalize() 24 | ._scale(strengthMod || this._config._strength); 25 | } 26 | 27 | _flock(neighbors) { 28 | var separation = this._separate(neighbors)._scale( 29 | this._config._separationWeight 30 | ); 31 | // var alignment = this._align(neighbors)._scale( 32 | // this._config._alignmentWeight 33 | // ); 34 | var cohesion = this._cohere(neighbors)._scale(this._config._cohesionWeight); 35 | 36 | return ( 37 | separation 38 | // ._add(alignment) 39 | ._add(cohesion) 40 | ); 41 | } 42 | 43 | // _align(neighbors) { 44 | // return new V2(); 45 | // } 46 | 47 | _cohere(neighbors) { 48 | var sum = new V2(0, 0); 49 | var count = 0; 50 | 51 | neighbors.forEach((boid: Hoplite) => { 52 | var d = V2._distance(this._actor._position, boid._position); 53 | if (d > 0 && d < this._config._neighborRadius) { 54 | sum._add(boid._position); 55 | count++; 56 | } 57 | }); 58 | 59 | if (count > 0) sum._scale(1 / count); 60 | 61 | var desired = V2._subtract(sum, this._actor._position); 62 | var len = desired._magnitude(); 63 | 64 | if (len < 100) desired._scale(len / 100); 65 | 66 | return desired._normalize()._limit(0.05); 67 | } 68 | 69 | _separate(neighbors) { 70 | var mean = new V2(); 71 | var count = 0; 72 | 73 | neighbors.forEach((boid: Hoplite) => { 74 | var d = V2._distance(this._actor._position, boid._position); 75 | if (d > 0 && d < this._config._desiredSeparation) { 76 | mean._add( 77 | V2._subtract(this._actor._position, boid._position) 78 | ._normalize() 79 | ._scale(1 / d) 80 | ); 81 | count++; 82 | } 83 | }); 84 | 85 | if (count > 0) mean._scale(1 / count); 86 | 87 | return mean; 88 | } 89 | } 90 | 91 | export default FlockBehavior; 92 | -------------------------------------------------------------------------------- /src/app/core/ai/SeekBehavior.ts: -------------------------------------------------------------------------------- 1 | import V2 from "../V2"; 2 | import Behavior from "./Behavior"; 3 | 4 | class SeekBehavior extends Behavior { 5 | constructor(actor) { 6 | super(actor); 7 | 8 | this._config = { 9 | _strength: 2, 10 | }; 11 | } 12 | 13 | // @ts-ignore 14 | _run(target, strengthMod) { 15 | var desiredVelocity = V2._subtract(target._position, this._actor._position) 16 | ._normalize() 17 | ._scale(strengthMod || this._config._strength); 18 | 19 | return desiredVelocity; 20 | } 21 | } 22 | 23 | export default SeekBehavior; 24 | -------------------------------------------------------------------------------- /src/app/core/ai/SteeringManager.ts: -------------------------------------------------------------------------------- 1 | import V2 from "../V2"; 2 | import WanderBehavior from "./WanderBehavior"; 3 | import SeekBehavior from "./SeekBehavior"; 4 | import FleeBehavior from "./FleeBehavior"; 5 | import GameObject from "../GameObject"; 6 | import FlockBehavior from "./FlockBehavior"; 7 | 8 | class SteeringManager { 9 | public _actor: GameObject; 10 | public _force: V2 = new V2(); 11 | public _bWander: WanderBehavior; 12 | public _bSeek: SeekBehavior; 13 | public _bFlee: FleeBehavior; 14 | public _bFlock: FlockBehavior; 15 | 16 | constructor(actor) { 17 | this._actor = actor; 18 | 19 | this._bWander = new WanderBehavior(actor); 20 | this._bSeek = new SeekBehavior(actor); 21 | this._bFlee = new FleeBehavior(actor); 22 | this._bFlock = new FlockBehavior(actor); 23 | } 24 | 25 | _wander(strengthMod) { 26 | this._force._add(this._bWander._run(strengthMod)); 27 | } 28 | 29 | _seek(target, strengthMod) { 30 | this._force._add(this._bSeek._run(target, strengthMod)); 31 | } 32 | 33 | _flee(target, strengthMod) { 34 | this._force._add(this._bFlee._run(target, strengthMod)); 35 | } 36 | 37 | _flock(strengthMod?) { 38 | this._force._add(this._bFlock._run(strengthMod)); 39 | } 40 | } 41 | 42 | export default SteeringManager; 43 | -------------------------------------------------------------------------------- /src/app/core/ai/WanderBehavior.ts: -------------------------------------------------------------------------------- 1 | import Behavior from "./Behavior"; 2 | import V2 from "../V2"; 3 | 4 | class WanderBehavior extends Behavior { 5 | public _wanderAngle: number; 6 | constructor(actor, _circleDistance = 6, _strength = 1, _angleChange = Math.PI /4) { 7 | super(actor); 8 | 9 | this._config = { 10 | _circleDistance, 11 | _strength, // aka circle radius for wander circle 12 | _angleChange, 13 | } 14 | 15 | this._wanderAngle = actor._rotation; 16 | } 17 | 18 | // @ts-ignore 19 | _run(strengthMod) { 20 | var circleCenter = this._actor._v._copy(); 21 | circleCenter._normalize(); 22 | circleCenter._scale(this._config._circleDistance); 23 | 24 | var displacement = new V2(0, -1); 25 | displacement._scale(strengthMod || this._config._strength); 26 | 27 | var len = displacement._magnitude(); 28 | displacement.x = Math.cos(this._wanderAngle) * len; 29 | displacement.y = Math.sin(this._wanderAngle) * len; 30 | 31 | this._wanderAngle += Math.random() * this._config._angleChange - this._config._angleChange * 0.5; 32 | 33 | return V2._add(circleCenter, displacement); 34 | } 35 | } 36 | 37 | export default WanderBehavior; -------------------------------------------------------------------------------- /src/app/core/physics/DistanceConstraint.ts: -------------------------------------------------------------------------------- 1 | import V2 from "../V2"; 2 | import PointMass from "./PointMass"; 3 | 4 | class DistanceConstraint { 5 | public _p1: PointMass; 6 | public _p2: PointMass; 7 | public _stiffness: number; 8 | public _restingDistance: number; 9 | 10 | constructor(p1, p2) { 11 | this._p1 = p1; 12 | this._p2 = p2; 13 | this._stiffness = 0.5; 14 | 15 | // if distance unspecified, use distance between pointmasses 16 | this._restingDistance = V2._distance(p1._position, p2._position); 17 | } 18 | 19 | _resolve() { 20 | var p1vec = this._p1._position, 21 | p2vec = this._p2._position; 22 | 23 | var delta = V2._subtract(p1vec, p2vec); 24 | var d = delta._magnitude(); 25 | 26 | var restingRatio = 27 | d === 0 ? this._restingDistance : (this._restingDistance - d) / d; 28 | 29 | var scalarP1 = 0.5 * this._stiffness; 30 | var scalarP2 = this._stiffness - scalarP1; 31 | 32 | //push/pull based on mass 33 | var p1VecDiff = V2._scale(delta, scalarP1 * restingRatio); 34 | if (!this._p1._fixed) { 35 | p1vec.x += p1VecDiff.x; 36 | p1vec.y += p1VecDiff.y; 37 | } 38 | 39 | var p2VecDiff = V2._scale(delta, scalarP2 * restingRatio); 40 | if (!this._p2._fixed) { 41 | p2vec.x -= p2VecDiff.x; 42 | p2vec.y -= p2VecDiff.y; 43 | } 44 | return d; 45 | } 46 | 47 | // _draw(ctx) { 48 | // ctx.strokeStyle = "rgba(150,150,150,0.9)"; 49 | // ctx._beginPath(); 50 | // ctx.moveTo(this.p1.p.x, this.p1.p.y); 51 | // ctx._lineTo(this.p2.p.x, this.p2.p.y); 52 | // ctx.stroke(); 53 | // } 54 | } 55 | 56 | export default DistanceConstraint; 57 | -------------------------------------------------------------------------------- /src/app/core/physics/PointMass.ts: -------------------------------------------------------------------------------- 1 | import V2 from "../V2"; 2 | 3 | class PointMass { 4 | public _previousPosition: V2; 5 | public _position: V2; 6 | public _a: V2 = new V2(); 7 | 8 | public _mass: number = 1; 9 | 10 | public _fixed: boolean; 11 | 12 | constructor(x, y, fixed = false) { 13 | this._position = new V2(x, y); 14 | 15 | this._previousPosition = this._position._copy(); 16 | this._fixed = fixed; 17 | } 18 | 19 | _update(delta) { 20 | if (this._fixed) return; 21 | 22 | // var x = this._position.x; 23 | // var y = this._position.y; 24 | var pCopy = this._position._copy(); 25 | 26 | this._a._scale(delta * delta); 27 | 28 | var fric = 0.015; 29 | 30 | // this._position.x = (2 - fric) * x - (1 - fric) * this._previousPosition.x + this._a.x; 31 | // this._position.y = (2 - fric) * y - (1 - fric) * this._previousPosition.y + this._a.y; 32 | 33 | this._position 34 | ._scale(2 - fric) 35 | ._subtract(this._previousPosition._copy()._scale(1 - fric)) 36 | ._add(this._a); 37 | 38 | this._a._reset(); 39 | 40 | this._previousPosition = pCopy; 41 | 42 | // this._previousPosition.x = x; 43 | // this._previousPosition.y = y; 44 | } 45 | 46 | // resolveConstraints() { 47 | // var i = this.constraints.length; 48 | // while (i--) this.constraints[i].resolve(); 49 | 50 | // // this._position.x > boundsx ? this._position.x = 2 * boundsx - this._position.x : 1 > this._position.x && (this._position.x = 2 - this._position.x); 51 | // // this._position.y < 1 ? this._position.y = 2 - this._position.y : this._position.y > boundsy && (this._position.y = 2 * boundsy - this._position.y); 52 | // }; 53 | 54 | // enforceBounds(x, y, w, h) { 55 | // this._position.x = Math.max(x + 1, Math.min(w - 1, this._position.x)); 56 | // this._position.y = Math.max(y + 1, Math.min(h - 1, this._position.y)); 57 | 58 | // if (this._position.y >= h - 1) 59 | // this._position.x -= (this._position.x - this._previousPosition.x + this._a.x); 60 | // } 61 | 62 | // attachElastic(pointmass, length, stiff, tear) { 63 | // this.constraints.push( 64 | // new ElasticConstraint(this, pointmass, length, stiff, tear) 65 | // ); 66 | // }; 67 | 68 | // attachDistance(pointmass, length) { 69 | // var constraint = new DistanceConstraint(this, pointmass, length); 70 | // this.constraints.push(constraint); 71 | // return constraint; 72 | // }; 73 | 74 | // move(mV) { 75 | // if (this._fixed) return; 76 | 77 | // this._position._add(mV); 78 | // } 79 | 80 | _addForce(fV) { 81 | // acceleration = (1/mass) * force 82 | // or 83 | // acceleration = force / mass 84 | this._a._add(fV); 85 | } 86 | 87 | // removeAllConstraints() { 88 | // this.constraints = []; 89 | // } 90 | 91 | // destroy() { 92 | // this.removeAllConstraints(); 93 | 94 | // // TODO ??? 95 | // var i = this._positionhysics.points.length; 96 | // while (i--) { 97 | // if (points[i] == this) { 98 | // points.splice(i, 1); 99 | // this == null; 100 | // } 101 | // } 102 | // } 103 | 104 | // _draw(ctx) { 105 | // ctx._beginPath(); 106 | // ctx.arc(this._positionosition.x, this._positionosition.y, 1, 0, 2 * Math.PI, 0); 107 | // ctx.fillStyle = "#000"; 108 | // ctx.fill(); 109 | // }; 110 | } 111 | 112 | export default PointMass; 113 | -------------------------------------------------------------------------------- /src/app/core/physics/shapes/Cloth.ts: -------------------------------------------------------------------------------- 1 | import V2 from "../../V2"; 2 | import Perlin from "../../Perlin"; 3 | import _PointMass from "../PointMass"; 4 | import _DistanceConstraint from "../DistanceConstraint"; 5 | 6 | class Cloth { 7 | public _points: any[]; 8 | public _constraints: any[]; 9 | public _segsY: number; 10 | public _segsX: number; 11 | 12 | public _fixedDeltaTime = 16; 13 | public _constraintAccuracy = 5; 14 | 15 | public _gravity: V2; 16 | 17 | // xstart, ystart, w, h, dim 18 | constructor(sX, sY, w, h, dim) { 19 | this._gravity = new V2(0, 0.25); 20 | 21 | this._points = []; 22 | this._constraints = []; 23 | 24 | this._segsY = Math.ceil(h / dim); 25 | this._segsX = Math.ceil(w / dim); 26 | 27 | for (var y = 0; y < this._segsY; y++) { 28 | for (var x = 0; x < this._segsX; x++) { 29 | var p = new _PointMass(sX + x * dim, sY + y * dim, y === 0); 30 | 31 | if (x) { 32 | this._constraints.push( 33 | new _DistanceConstraint(p, this._points[this._points.length - 1]) 34 | ); 35 | } 36 | 37 | if (y) { 38 | this._constraints.push( 39 | new _DistanceConstraint( 40 | p, 41 | this._points[x + this._segsX * (y - 1)] 42 | ) 43 | ); 44 | } 45 | 46 | this._points.push(p); 47 | } 48 | } 49 | } 50 | 51 | _update(dt, x, y) { 52 | // break up the elapsed time into manageable chunks 53 | var timeSteps = Math.floor(dt /* + this.leftOverDeltaTime */ / this._fixedDeltaTime); 54 | 55 | // store however much time is leftover for the next frame 56 | // this.leftOverDeltaTime = dt - timeSteps * this._fixedDeltaTime; 57 | 58 | // _update physics 59 | for (var i = 0; i < timeSteps; i++) { 60 | // _update each PointMass's position 61 | for (var j = 0; j < this._points.length; j++) { 62 | var pm = this._points[j]; 63 | // pm.enforceBounds(0, 0, WIDTH, HEIGHT); 64 | 65 | pm._addForce(this._gravity); 66 | pm._update(1 / timeSteps); 67 | } 68 | 69 | // solve the cs multiple times 70 | // the more it's solved, the more accurate. 71 | for (var k = 0; k < this._constraintAccuracy; k++) { 72 | this._constraints.forEach((constraint) => constraint._resolve()); 73 | 74 | // for (var p = 0; p < this._points.length; p++) { 75 | // this._points[p].resolvecs(); 76 | // } 77 | } 78 | } 79 | 80 | this._points[0]._position = new V2(x - 5, y); 81 | this._points[1]._position = new V2(x + 5, y); 82 | 83 | var r = 84 | Perlin._simplex3(x / 1000, (y - 50) / 1000, performance.now() / 2200) * 85 | Math.PI - 86 | Math.PI / 2; 87 | var heading = new V2(Math.cos(r), Math.sin(r))._normalize()._scale(0.1); 88 | 89 | this._points.map((p) => p._addForce(heading)); 90 | } 91 | 92 | _draw(ctx, color) { 93 | var x, y; 94 | for (y = 1; y < this._segsY; ++y) { 95 | for (x = 1; x < this._segsX; ++x) { 96 | ctx._beginPath(); 97 | 98 | var i1 = (y - 1) * this._segsX + x - 1; 99 | var i2 = y * this._segsX + x; 100 | 101 | ctx.moveTo(this._points[i1]._position.x, this._points[i1]._position.y); 102 | ctx._lineTo(this._points[i1 + 1]._position.x, this._points[i1 + 1]._position.y); 103 | 104 | ctx._lineTo(this._points[i2]._position.x, this._points[i2]._position.y); 105 | ctx._lineTo(this._points[i2 - 1]._position.x, this._points[i2 - 1]._position.y); 106 | 107 | ctx._fillStyle(color); 108 | 109 | ctx.fill(); 110 | } 111 | } 112 | } 113 | } 114 | 115 | export default Cloth; 116 | -------------------------------------------------------------------------------- /src/app/core/utils.ts: -------------------------------------------------------------------------------- 1 | import V2 from "./V2"; 2 | import { TILESIZE } from "../constants"; 3 | 4 | export var i2c = (pt) => { 5 | var cartPt = new V2(0, 0); 6 | cartPt.x = ((pt.x - pt.y) * TILESIZE.x) / 2; 7 | cartPt.y = ((pt.x + pt.y) * TILESIZE.y) / 2; 8 | return cartPt; 9 | }; 10 | 11 | export var c2i = (pt) => { 12 | var map = new V2(); 13 | map.x = pt.x / TILESIZE.x + pt.y / TILESIZE.y; 14 | map.y = pt.y / TILESIZE.y - pt.x / TILESIZE.x; 15 | return map; 16 | }; 17 | 18 | // export var degreesToRadians = (degrees) => { 19 | // return (degrees * Math.PI) / 180; 20 | // }; 21 | 22 | // export var radiansToDegrees = (radians) => { 23 | // return (radians * 180) / Math.PI; 24 | // }; 25 | 26 | // export var wrapAngle = (r) => { 27 | // while (r < -Math.PI) { 28 | // r += Math.PI * 2; 29 | // } 30 | // while (r > Math.PI) { 31 | // r -= Math.PI * 2; 32 | // } 33 | 34 | // return r; 35 | // }; 36 | 37 | export var rndPN = () => { 38 | return Math.random() * 2 - 1; 39 | }; 40 | 41 | export var rndRng = (from, to) => { 42 | return ~~(Math.random() * (to - from + 1) + from); 43 | }; 44 | 45 | export var inRng = (value, min, max) => { 46 | return value >= min && value <= max; 47 | }; 48 | 49 | export var rndInArray = (a) => a[~~(Math.random() * a.length)]; 50 | 51 | export var waterTile = (t) => { 52 | return t <= 1; 53 | }; 54 | 55 | export var walkTile = (t) => { 56 | if (/*t === 0 || t === 1 || t === 9 ||*/ t === undefined || t === null) { 57 | return false; 58 | } 59 | 60 | return true; 61 | }; 62 | 63 | export var debounce = (func, wait, immediate) => { 64 | var timeout; 65 | return (...args: any[]) => { 66 | var context = this; 67 | var later = () => { 68 | timeout = null; 69 | if (!immediate) func.apply(context, args); 70 | }; 71 | var callNow = immediate && !timeout; 72 | clearTimeout(timeout); 73 | timeout = setTimeout(later, wait); 74 | if (callNow) func.apply(context, args); 75 | }; 76 | }; 77 | 78 | export class rect { 79 | public _left: number; 80 | public _top: number; 81 | public _width: number; 82 | public _height: number; 83 | public _right: number; 84 | public _bottom: number; 85 | 86 | constructor(left, top, width, height) { 87 | this._left = left || 0; 88 | this._top = top || 0; 89 | this._width = width || 0; 90 | this._height = height || 0; 91 | this._right = this._left + this._width; 92 | this._bottom = this._top + this._height; 93 | } 94 | 95 | set(_left, _top, /*optional*/ _width, /*optional*/ _height) { 96 | this._left = _left; 97 | this._top = _top; 98 | this._width = _width || this._width; 99 | this._height = _height || this._height; 100 | this._right = this._left + this._width; 101 | this._bottom = this._top + this._height; 102 | } 103 | 104 | // get mid() { 105 | // return new V2(this._left + this._width / 2, this._top + this._height / 2); 106 | // } 107 | 108 | // within(r) { 109 | // return ( 110 | // r._left <= this._left && 111 | // r._right >= this._right && 112 | // r._top <= this._top && 113 | // r._bottom >= this._bottom 114 | // ); 115 | // } 116 | 117 | // overlaps(r) { 118 | // return ( 119 | // this._left < r._right && 120 | // r._left < this._right && 121 | // this._top < r._bottom && 122 | // r._top < this._bottom 123 | // ); 124 | // } 125 | 126 | // TODO REMOVE 127 | // _draw(ctx) { 128 | // ctx._wrap(() => { 129 | // ctx.line_Width = 1; 130 | // ctx.strokeStyle = "#f00"; 131 | // ctx._beginPath(); 132 | // ctx.rect(this._left, this._top, this._width, this._height); 133 | // ctx.stroke(); 134 | // }); 135 | // } 136 | } 137 | -------------------------------------------------------------------------------- /src/app/main.ts: -------------------------------------------------------------------------------- 1 | import GameScene from "./components/scenes/GameScene"; 2 | import TitleScene from "./components/scenes/TitleScene"; 3 | import { rndRng } from "./core/utils"; 4 | import { tilePregen } from "./components/tiles/tilePregen"; 5 | import "./config"; 6 | import { WIDTH, HEIGHT } from "./constants"; 7 | 8 | var _localStorageKey = "mjf_tls"; 9 | 10 | class _Game { 11 | public _seed: number; 12 | public _currentTime: number; 13 | public _canvas: any; 14 | public _ctx: any; 15 | public _scene: any; 16 | public _started: boolean = false; 17 | public _paused: boolean = false; 18 | public _animationFrame: any; 19 | public _spritesheet: any; 20 | 21 | // screens 22 | public _$pz: HTMLElement; // pause 23 | public _$tt: HTMLElement; // title 24 | public _$gg: HTMLElement; // game over 25 | 26 | public _$k: HTMLElement; 27 | public _$s: HTMLElement; 28 | public _$kr: HTMLElement; 29 | public _$sr: HTMLElement; 30 | 31 | public _storage: Storage = window.localStorage; 32 | 33 | constructor() { 34 | // console.log(`GAME SEED: ${this.seed}`); 35 | 36 | this._currentTime = performance.now(); 37 | 38 | this._canvas = document.getElementById("c"); 39 | this._ctx = this._canvas.getContext("2d"); 40 | 41 | this._canvas.width = WIDTH; 42 | this._canvas.height = HEIGHT; 43 | this._canvas.oncontextmenu = () => false; 44 | 45 | // screens 46 | this._$tt = document.getElementById("tt"); 47 | this._$pz = document.getElementById("pz"); 48 | this._$gg = document.getElementById("gg"); 49 | 50 | this._$k = document.getElementById("k"); 51 | this._$s = document.getElementById("s"); 52 | this._$kr = document.getElementById("kr"); 53 | this._$sr = document.getElementById("sr"); 54 | 55 | // pause the game if the player gets distracted 56 | window.onblur = () => this._pause(); 57 | 58 | window.onkeydown = (e) => { 59 | switch (e.code) { 60 | case "KeyP": 61 | if (this._scene._done) return; 62 | if (this._paused) { 63 | this._resume(); 64 | } else { 65 | this._pause(); 66 | } 67 | break; 68 | case "Enter": 69 | if (!this._started) { 70 | this._disable(); 71 | this._started = true; 72 | this._toggleScreens(null, this._$tt); 73 | this._restart(); 74 | } else if (this._scene._done) { 75 | this._restart(); 76 | } 77 | break; 78 | } 79 | }; 80 | 81 | this._spritesheet = tilePregen(); 82 | 83 | // this._spritesheet.onload = () => { 84 | this._scene = new TitleScene(); 85 | this._enable(); 86 | // }; 87 | } 88 | 89 | _enable() { 90 | this._animationFrame = requestAnimationFrame(this._animate.bind(this)); 91 | } 92 | 93 | _disable() { 94 | cancelAnimationFrame(this._animationFrame); 95 | } 96 | 97 | _animate(time) { 98 | this._animationFrame = requestAnimationFrame(this._animate.bind(this)); 99 | 100 | var dt = Math.max(0, time - this._currentTime); 101 | this._update(dt); 102 | this._currentTime = time; 103 | } 104 | 105 | _toggleScreens(on, off?) { 106 | if (on) on.style.display = "flex"; 107 | if (off) off.style.display = "none"; 108 | } 109 | 110 | _pause() { 111 | if (!this._started) return; 112 | 113 | this._disable(); 114 | this._toggleScreens(this._$pz); 115 | this._paused = true; 116 | } 117 | 118 | _resume() { 119 | this._currentTime = performance.now(); 120 | this._enable(); 121 | this._toggleScreens(null, this._$pz); 122 | this._paused = false; 123 | } 124 | 125 | _end() { 126 | this._disable(); 127 | 128 | var kills = this._scene._player._kills; 129 | var time = this._scene._age; 130 | 131 | this._$k.innerText = kills; 132 | this._$s.innerText = `${(time / 1000).toFixed(0)}s`; 133 | 134 | this._toggleScreens(this._$gg, null); 135 | 136 | var highscore = JSON.parse( 137 | this._storage.getItem(_localStorageKey) || '{"kills": 0, "time": 0}' 138 | ); 139 | 140 | this._$kr.innerText = `Record: ${highscore.kills}`; 141 | this._$sr.innerText = `Record: ${(highscore.time / 1000).toFixed(0)}s`; 142 | 143 | if (kills > highscore.kills) { 144 | highscore.kills = kills; 145 | } 146 | if (time > highscore.time) { 147 | highscore.time = time; 148 | } 149 | 150 | this._storage.setItem( 151 | _localStorageKey, 152 | JSON.stringify(highscore) 153 | ); 154 | 155 | } 156 | 157 | _restart() { 158 | this._toggleScreens(null, this._$gg); 159 | this._seed = rndRng(0, 99999); 160 | this._scene = new GameScene(this._spritesheet); 161 | setTimeout(() => this._enable(), 50); 162 | } 163 | 164 | _update(dt) { 165 | this._scene._update(dt); 166 | if (this._scene._done) { 167 | this._end(); 168 | } 169 | this._draw(); 170 | } 171 | 172 | _draw() { 173 | var { _ctx, _scene } = this; 174 | 175 | _ctx.clearRect(0, 0, WIDTH, HEIGHT); 176 | _ctx._fillStyle("rgba(0,0,0,0.85)"); 177 | _ctx._fillRect(0, 0, WIDTH, HEIGHT); 178 | 179 | _ctx.s(); 180 | _scene._draw(_ctx); 181 | _ctx.r(); 182 | } 183 | } 184 | 185 | export var Game = new _Game(); 186 | -------------------------------------------------------------------------------- /src/app/sounds.ts: -------------------------------------------------------------------------------- 1 | import { debounce } from "./core/utils"; 2 | import zzfx from "./ZzFX.micro"; 3 | 4 | var dbSFX = debounce(zzfx, 48, true); 5 | export var sfx = (data, debounce = false) => 6 | debounce ? dbSFX(...data) : zzfx(...data); -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 |
10 | 11 |
12 |
[P] to Resume
13 |
14 |
15 |
16 |

YOU DIED

17 |
An honorable death.
18 |

19 |
20 |
Kills
21 |
22 |
23 |
24 |
25 |
Survived
26 |
27 |
28 |
29 |
[Enter] to Retry
30 |
31 |
32 |
33 |
34 | The Last
Spartan 35 |
404 B.C.
36 |
37 |
38 | No retreat.
No surrender.
No way out alive.

Go 39 | down swinging. 40 |
41 |
[Enter] to Start
42 | 43 |
by @ferronsays
44 |
45 |
46 |
47 |
48 |
Move : WASD
49 |
Attack : J
50 |
Block : K
51 |
Jump : SPACE
52 |
Pause : P
53 |
54 |
55 |
Spartan Charge : K + J
56 |
Ground Pound : SPACE + J
57 |
58 |
59 |
60 | 61 | 62 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import './app/main'; 2 | import './styles/main.css'; -------------------------------------------------------------------------------- /src/styles/main.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | user-select: none; 4 | } 5 | 6 | html, 7 | body { 8 | width: 100%; 9 | height: 100%; 10 | margin: 0; 11 | padding: 0; 12 | } 13 | 14 | html { 15 | background-image: url("data:image/svg+xml,%3Csvg width='64' height='128' viewBox='0 0 32 64' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M0 28h20V16h-4v8H4V4h28v28h-4V8H8v12h4v-8h12v20H0v-4zm12 8h20v4H16v24H0v-4h12V36zm16 12h-4v12h8v4H20V44h12v12h-4v-8zM0 36h8v20H0v-4h4V40H0v-4z' fill='%23d8b685' fill-rule='evenodd'/%3E%3C/svg%3E"); 16 | background-color: #e0ddd5; 17 | } 18 | 19 | body { 20 | padding-top: 40px; 21 | } 22 | 23 | #w { 24 | width: 768px; 25 | margin: 0 auto; 26 | position: relative; 27 | 28 | font-family: "Herculanum"; 29 | box-shadow: 0px 8px 8px 3px rgba(0, 0, 0, 0.5); 30 | } 31 | 32 | #w1 { 33 | width: 100%; 34 | height: 432px; 35 | position: relative; 36 | } 37 | 38 | #c { 39 | width: 100%; 40 | height: 432px; 41 | position: absolute; 42 | transform: translateZ(0); 43 | } 44 | 45 | #pz, 46 | #tt, 47 | #gg { 48 | position: absolute; 49 | width: 100%; 50 | height: 100%; 51 | padding: 24px; 52 | } 53 | 54 | #pz, 55 | #gg { 56 | display: none; 57 | background: rgb(0 0 0 / 80%); 58 | align-items: center; 59 | justify-content: center; 60 | text-align: center; 61 | color: #fff; 62 | } 63 | 64 | #tt { 65 | text-align: right; 66 | display: flex; 67 | flex-direction: column; 68 | color: #fff; 69 | } 70 | 71 | /* title */ 72 | #t { 73 | font-size: 80px; 74 | line-height: 60px; 75 | flex-grow: 1; 76 | } 77 | 78 | #t2 { 79 | font-size: 32px; 80 | } 81 | 82 | #t3{ 83 | text-align: right; 84 | font-size: 18px; 85 | margin-bottom: 24px; 86 | } 87 | 88 | #t4, 89 | #pz, 90 | #rty { 91 | font-size: 32px; 92 | font-style: italic; 93 | } 94 | 95 | #g { 96 | font-size: 72px; 97 | color: #fff; 98 | margin: 0; 99 | } 100 | 101 | #i { 102 | font-family: monospace; 103 | background: #e0ddd5; 104 | color: #703529; 105 | font-weight: bold; 106 | font-size: 18px; 107 | padding: 12px; 108 | } 109 | 110 | #i > div { 111 | display: flex; 112 | justify-content: space-evenly; 113 | } 114 | 115 | .m { 116 | display: flex; 117 | justify-content: space-between; 118 | font-size: 32px; 119 | font-family: monospace; 120 | } 121 | 122 | #kr, #sr { 123 | text-align: right; 124 | margin-bottom: 16px; 125 | font-family: monospace; 126 | } 127 | 128 | #z { 129 | font-size: 12px; 130 | position: absolute; 131 | left: 6px; 132 | bottom: 6px; 133 | } 134 | 135 | /* .pls { 136 | animation: ps8 1s ease-out infinite; 137 | opacity: 0.5; 138 | } 139 | @keyframes ps8 { 140 | 0% { 141 | opacity: 0.5; 142 | } 143 | 50% { 144 | opacity: 1.0; 145 | } 146 | 100% { 147 | opacity: 0.5; 148 | } 149 | } */ -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist/", 4 | "sourceMap": true, 5 | "noImplicitAny": false, 6 | "target": "es6", 7 | "allowJs": true, 8 | "allowSyntheticDefaultImports": true 9 | }, 10 | "include": ["src/**/*", "typedefs/*"] 11 | } 12 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const HtmlWebpackPlugin = require("html-webpack-plugin"); 2 | const MiniCssExtractPlugin = require("mini-css-extract-plugin"); 3 | const HtmlWebpackInlineSourcePlugin = require("html-webpack-inline-source-plugin"); 4 | const OptimizeCssAssetsPlugin = require("optimize-css-assets-webpack-plugin"); 5 | const TerserJSPlugin = require("terser-webpack-plugin"); 6 | 7 | const isProduction = process.env.npm_lifecycle_event === "build"; 8 | 9 | module.exports = { 10 | entry: "./src", 11 | optimization: { 12 | minimize: isProduction, 13 | minimizer: [ 14 | new TerserJSPlugin({ 15 | terserOptions: { 16 | ecma: 2020, 17 | mangle: { 18 | module: true, 19 | properties: { 20 | regex: /^_/, 21 | }, 22 | }, 23 | module: true, 24 | compress: { 25 | toplevel: true, 26 | module: true, 27 | passes: 10, 28 | unsafe_arrows: true, 29 | unsafe: true, 30 | unsafe_comps: true, 31 | unsafe_Function: true, 32 | unsafe_math: true, 33 | unsafe_symbols: true, 34 | unsafe_methods: true, 35 | unsafe_proto: true, 36 | unsafe_regexp: true, 37 | unsafe_undefined: true, 38 | }, 39 | }, 40 | }), 41 | ], 42 | }, 43 | devtool: !isProduction && "source-map", 44 | resolve: { extensions: [".ts", ".js"] }, 45 | module: { 46 | rules: [ 47 | { 48 | test: /\.ts$/, 49 | use: "ts-loader", 50 | exclude: /node_modules/, 51 | }, 52 | { 53 | test: /\.css$/, 54 | use: [ 55 | MiniCssExtractPlugin.loader, 56 | { 57 | loader: "css-loader", 58 | }, 59 | ], 60 | }, 61 | ], 62 | }, 63 | plugins: [ 64 | new HtmlWebpackPlugin({ 65 | template: "src/index.html", 66 | minify: isProduction && { 67 | collapseWhitespace: true, 68 | }, 69 | inlineSource: isProduction && ".(js|css)$", 70 | }), 71 | new HtmlWebpackInlineSourcePlugin(), 72 | new OptimizeCssAssetsPlugin({}), 73 | new MiniCssExtractPlugin({ 74 | filename: "[name].css", 75 | }), 76 | ], 77 | devServer: { 78 | stats: "minimal", 79 | overlay: true, 80 | }, 81 | }; 82 | --------------------------------------------------------------------------------