├── .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 | 
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 | 
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 |
YOU DIED
An honorable death.
[Enter] to Retry
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 |
14 |
15 |
16 |
YOU DIED
17 |
An honorable death.
18 |
19 |
23 |
24 |
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 |
--------------------------------------------------------------------------------