[READ-ONLY] Mirror of https://github.com/flo-bit/shadow-shmup. Fun and casual top-down, roguelike shoot 'em up browsergame with shadow elements and colorful neon effects flo-bit.dev/shadow-shmup/
browsergame pixijs rapier2d roguelike shoot-em-up typescript webgame
0

Configure Feed

Select the types of activity you want to include in your feed.

waves

Florian (Jul 25, 2024, 3:55 AM +0200) 3cd06fca 14a3a899

+436 -44
+8 -2
index.html
··· 4 4 <meta charset="UTF-8" /> 5 5 <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 6 6 <meta name="apple-mobile-web-app-capable" content="yes"> 7 - <meta name="theme-color" content="black"/> 7 + <meta name="theme-color" content="#000000"/> 8 8 9 9 <link rel="icon" href="/icons/favicon.ico" sizes="32x32"> 10 10 <link rel="icon" href="/icons/icon.svg" type="image/svg+xml"> ··· 20 20 21 21 <div id="timer" class="absolute top-1 right-1 text-white font-bold">0</div> 22 22 23 + <div id="debug" class="absolute bottom-1 left-1 text-white font-bold hidden"></div> 24 + 23 25 <!-- ui: title, play button --> 24 26 <div 25 27 id="ui" 26 - class="absolute inset-0 h-[100dhv] w-screen z-10 text-white flex items-center justify-between flex-col" 28 + class="absolute inset-0 h-[100dhv] w-screen z-20 text-white flex items-center justify-between flex-col" 27 29 > 28 30 <div></div> 29 31 <div class="flex flex-col items-center"> ··· 155 157 </a> 156 158 </div> 157 159 </div> 160 + </div> 161 + 162 + <div id="wave" class="absolute inset-0 h-[100dhv] w-screen z-10 flex items-center justify-center hidden"> 163 + <div id="wave-text" class="text-6xl font-bold text-white">Wave</div> 158 164 </div> 159 165 </body> 160 166 </html>
+71 -7
src/app.ts
··· 8 8 import Enemy from './enemy.js'; 9 9 10 10 import Stats from 'stats.js'; 11 - import Obstacle from './obstacles.js'; 12 11 13 12 import { AdvancedBloomFilter } from 'pixi-filters'; 14 13 import PlayerManager from './player-manager.js'; ··· 17 16 import { sound } from '@pixi/sound'; 18 17 import Controls from './controls.js'; 19 18 import { ObstacleManager } from './obstacle-manager.js'; 19 + import { WaveManager } from './wave.js'; 20 20 21 21 export default class Game { 22 22 container: PIXI.Container; ··· 29 29 30 30 enemyManager?: EnemyManager; 31 31 playerManager?: PlayerManager; 32 + waveManager?: WaveManager; 32 33 33 34 projectileManager?: ProjectileManager; 34 35 ··· 51 52 invincible = false; 52 53 53 54 playingTime = 0; 55 + 56 + startWave = 0; 54 57 55 58 constructor() { 56 59 this.setup(); ··· 87 90 }); 88 91 } 89 92 93 + createStats() { 94 + this.stats = new Stats(); 95 + document.body.appendChild(this.stats.dom); 96 + } 97 + 98 + removeStats() { 99 + if (this.stats) { 100 + document.body.removeChild(this.stats.dom); 101 + this.stats = undefined; 102 + } 103 + } 104 + 90 105 async setup() { 91 106 await this.setupPhysicsWorld(); 92 107 ··· 152 167 position.x = -position.x; 153 168 position.y = -position.y; 154 169 155 - this.container.x = this.container.x * 0.98 + position.x * 0.02 * this.scale; 156 - this.container.y = this.container.y * 0.98 + position.y * 0.02 * this.scale; 170 + const interpolationSpeed = 0.05; 171 + const interpolationFactor = 1 - Math.pow(1 - interpolationSpeed, deltaTime / (1000 / 60)); 172 + 173 + this.container.x += (position.x * this.scale - this.container.x) * interpolationFactor; 174 + this.container.y += (position.y * this.scale - this.container.y) * interpolationFactor; 157 175 } 158 176 159 177 if (this.stats) this.stats.end(); 160 178 }); 161 179 162 180 this.playerManager = new PlayerManager(this); 163 - this.enemyManager = new EnemyManager(this, 10); 181 + this.enemyManager = new EnemyManager(this); 182 + this.waveManager = new WaveManager(this, this.startWave); 164 183 165 184 this.projectileManager = new ProjectileManager(this); 166 185 ··· 297 316 this.enemyManager?.update(deltaTime); 298 317 299 318 if (this.invincible) { 300 - this.playerManager!.players[1].health = 100; 319 + for (let players of this.playerManager?.players ?? []) { 320 + players.health = players.maxHealth; 321 + } 301 322 } 302 323 303 324 let playerManager = this.playerManager; ··· 311 332 } 312 333 } 313 334 314 - if (Math.random() < deltaTime * 0.004) { 315 - this.enemyManager?.addEnemy(); 335 + // if (Math.random() < deltaTime * 0.006) { 336 + // this.enemyManager?.addEnemy(); 337 + // } 338 + this.waveManager?.update(deltaTime); 339 + 340 + const wave = this.waveManager?.getCurrentWave(); 341 + const waveUI = document.getElementById('wave'); 342 + const waveText = document.getElementById('wave-text'); 343 + if (wave && !wave.isActive && waveText && this.playing) { 344 + waveUI?.classList.remove('hidden'); 345 + waveText.innerText = `Wave ${wave.index + 1}`; 346 + } else { 347 + waveUI?.classList.add('hidden'); 316 348 } 317 349 318 350 this.projectileManager?.update(deltaTime); ··· 329 361 const title = document.getElementById('title'); 330 362 // set inner text to "Game Over" 331 363 if (title) title.innerText = 'Game Over'; 364 + 365 + this.waveManager = new WaveManager(this, this.startWave); 366 + } 367 + 368 + if (this.debug) { 369 + // get debug text 370 + let debugText = document.getElementById('debug'); 371 + if (debugText) { 372 + debugText.innerText = `enemies: ${this.enemyManager?.enemies.length ?? 0}`; 373 + } 332 374 } 333 375 } 334 376 335 377 handleKeyDown(e: KeyboardEvent) { 336 378 this.keys[e.key.toLowerCase()] = true; 379 + 380 + if (e.key.toLowerCase() === 'f') { 381 + // full screen 382 + const elem = document.documentElement; 383 + if (elem.requestFullscreen) { 384 + elem.requestFullscreen(); 385 + } 386 + } 387 + 388 + if (e.key.toLowerCase() === 'p') { 389 + this.debug = !this.debug; 390 + this.debugGraphics?.clear(); 391 + 392 + let debugText = document.getElementById('debug'); 393 + if (this.debug) { 394 + if (debugText) debugText.classList.remove('hidden'); 395 + this.createStats(); 396 + } else { 397 + if (debugText) debugText.classList.add('hidden'); 398 + if (!this.showStats) this.removeStats(); 399 + } 400 + } 337 401 } 338 402 339 403 handleKeyUp(e: KeyboardEvent) {
+9 -9
src/enemy-manager.ts
··· 6 6 7 7 enemies: Enemy[] = []; 8 8 9 - constructor(game: Game, num: number) { 9 + enemyTypes = [SphereEnemy, TriangleEnemy, PentagonEnemy]; 10 + 11 + constructor(game: Game) { 10 12 this.game = game; 11 - 12 - for (let i = 0; i < num; i++) { 13 - this.addEnemy(); 14 - } 15 13 } 16 14 17 - addEnemy() { 18 - let enemyTypes = [SphereEnemy, TriangleEnemy, PentagonEnemy]; 19 - const randomIndex = Math.floor(Math.random() * enemyTypes.length); 15 + randomEnemyType() { 16 + const randomIndex = Math.floor(Math.random() * this.enemyTypes.length); 17 + return this.enemyTypes[randomIndex]; 18 + } 20 19 21 - const enemy = new enemyTypes[randomIndex](this.game); 20 + addEnemy(enemyType: typeof Enemy = this.randomEnemyType()) { 21 + const enemy = new enemyType(this.game); 22 22 this.enemies.push(enemy); 23 23 } 24 24
+34 -23
src/enemy.ts
··· 55 55 this.enemyContainer.x = Math.random() * 800 - 400; 56 56 this.enemyContainer.y = Math.random() * 800 - 400; 57 57 58 - // get player position 59 - let position = this.game.playerManager?.getClosestPlayer(this.position)?.position; 60 - if (position) { 61 - // create random direction vector 62 - let point = { x: Math.random() - 0.5, y: Math.random() - 0.5 }; 63 - let length = 1000; 64 - let norm = Math.sqrt(point.x * point.x + point.y * point.y); 65 - point.x = (point.x / norm) * length; 66 - point.y = (point.y / norm) * length; 67 - 68 - this.enemyContainer.x = position.x + point.x; 69 - this.enemyContainer.y = position.y + point.y; 70 - } 58 + this.setPositionNearPlayer(); 71 59 72 60 game.container.addChild(this.enemyContainer); 73 61 ··· 90 78 this.createEyes(); 91 79 } 92 80 81 + setPositionNearPlayer() { 82 + let position = this.game.playerManager?.getClosestPlayer(this.position)?.position; 83 + if (position) { 84 + // create random direction vector 85 + let point = { x: Math.random() - 0.5, y: Math.random() - 0.5 }; 86 + let length = 1000; 87 + let norm = Math.sqrt(point.x * point.x + point.y * point.y); 88 + point.x = (point.x / norm) * length; 89 + point.y = (point.y / norm) * length; 90 + 91 + this.x = position.x + point.x; 92 + this.y = position.y + point.y; 93 + } 94 + } 95 + 93 96 createEyes() { 94 97 this.eyes = new PIXI.Container(); 95 98 this.enemyContainer.addChild(this.eyes); ··· 117 120 createRidigBody() { 118 121 const rigidBodyDesc = RAPIER() 119 122 .RigidBodyDesc.dynamic() 120 - .setTranslation(this.x, this.y) 123 + .setTranslation(this.x, -this.y) 121 124 .lockRotations() 122 125 .setLinearDamping(0.5); 123 126 this.rigidBody = this.game.world.createRigidBody(rigidBodyDesc); ··· 182 185 } 183 186 184 187 move(deltaTime: number, nearestPlayer: Player, dx: number, dy: number, distance: number) { 185 - const x = (dx / distance) * this.speed; 186 - const y = (dy / distance) * this.speed; 188 + const mult = deltaTime * 120 * 0.001; 189 + const x = (dx / distance) * this.speed * mult; 190 + const y = (dy / distance) * this.speed * mult; 187 191 188 192 this.rigidBody?.applyImpulse({ x: x, y: -y }, true); 193 + 194 + // if too far away (> 2000), teleport to player 195 + if (distance > 2000) { 196 + this.setPositionNearPlayer(); 197 + console.log('teleporting', this.x, this.y); 198 + } 189 199 } 190 200 191 201 updateVisuals( ··· 375 385 376 386 attack(deltaTime: number, nearestPlayer: Player, dx: number, dy: number, distance: number): void { 377 387 if (this.destroyed) return; 388 + 389 + const x = Math.cos(this.rotation + Math.PI / 2); 390 + const y = Math.sin(this.rotation + Math.PI / 2); 391 + 392 + this.projectile.setPosition(this.x + x * 20 - 1, this.y + y * 20 - 1); 393 + 378 394 if (distance < nearestPlayer.viewDistance) { 379 395 this.projectile.shape.alpha = 1; 380 - const x = Math.cos(this.rotation + Math.PI / 2); 381 - const y = Math.sin(this.rotation + Math.PI / 2); 382 - 383 - this.projectile.setPosition(this.x + x * 20 - 1, this.y + y * 20 - 1); 384 396 } else { 385 397 this.projectile.shape.alpha = 0; 386 398 } 387 - 388 - // this.weapon.update(deltaTime); 389 399 } 390 400 391 401 destroy(): void { ··· 406 416 projectileSpeed: 0.2, 407 417 fireRate: 2000, 408 418 projectileSize: 6, 409 - damage: 10 419 + damage: 10, 420 + lifetime: 8000 410 421 }); 411 422 412 423 this.speed = 1000;
+9 -3
src/player.ts
··· 62 62 this.maxHealth = 100; 63 63 this.health = this.maxHealth; 64 64 65 - this.color = num === 0 ? 0xe11d48 : 0x7c3aed; 65 + this.color = num === 0 ? 0xe11d48 : 0x4f46e5; 66 66 67 67 this.playerContainer = new PIXI.Container(); 68 68 game.container.addChild(this.playerContainer); ··· 83 83 this.speed = 30000; 84 84 this.shape = shape; 85 85 86 - this.weapon = new Weapon(this.game, { color: this.color }); 86 + this.weapon = new Weapon(this.game, { color: this.color, lifetime: 2000 }); 87 87 88 88 this.isPlayer = true; 89 89 ··· 186 186 dx *= Math.SQRT1_2; 187 187 dy *= Math.SQRT1_2; 188 188 } 189 - if (dx || dy) this.rigidBody?.applyImpulse({ x: dx * this.speed, y: -dy * this.speed }, true); 189 + 190 + let mult = deltaTime * 120 * 0.001; 191 + if (dx || dy) 192 + this.rigidBody?.applyImpulse( 193 + { x: dx * this.speed * mult, y: -dy * this.speed * mult }, 194 + true 195 + ); 190 196 } 191 197 192 198 if (this.dead && this.respawnTime > 0) {
+305
src/wave.ts
··· 1 + /* 2 + 3 + each wave, we have a list of enemies that will spawn, 4 + each enemy has a type, and a number of enemies of that type that will spawn 5 + 6 + */ 7 + 8 + import Game from './app'; 9 + import Enemy, { PentagonEnemy, SphereEnemy, TriangleEnemy } from './enemy'; 10 + 11 + export class Wave { 12 + game: Game; 13 + 14 + enemies: { type: typeof Enemy; num: number; spawned: number }[] = []; 15 + 16 + spawnPerSecond: number = 1; 17 + spawnAccumulator: number = 0; 18 + startDelay: number; 19 + waveTime: number = 0; 20 + isActive: boolean = false; 21 + 22 + index: number; 23 + 24 + constructor( 25 + game: Game, 26 + enemies: { type: typeof Enemy; num: number }[], 27 + index: number, 28 + spawnPerSecond: number = 1, 29 + startDelay: number = 3 30 + ) { 31 + this.game = game; 32 + 33 + this.enemies = enemies.map((enemy) => ({ 34 + type: enemy.type, 35 + num: enemy.num, 36 + spawned: 0 37 + })); 38 + this.spawnPerSecond = spawnPerSecond; 39 + this.startDelay = startDelay; 40 + this.index = index; 41 + } 42 + 43 + randomEnemyIndex(): number { 44 + if (this.isComplete()) return -1; 45 + 46 + let i = Math.floor(Math.random() * this.enemies.length); 47 + let enemy = this.enemies[i]; 48 + if (enemy.spawned >= enemy.num * (this.game.playerManager?.players.length ?? 1)) { 49 + return this.randomEnemyIndex(); 50 + } 51 + 52 + return i; 53 + } 54 + 55 + update(deltaTime: number) { 56 + // Convert deltaTime to seconds if it's in milliseconds 57 + const deltaTimeSeconds = deltaTime / 1000; 58 + 59 + // Update wave time 60 + this.waveTime += deltaTimeSeconds; 61 + 62 + // Check if we should start spawning 63 + if (this.waveTime >= this.startDelay) { 64 + this.isActive = true; 65 + } 66 + 67 + // Only spawn if the wave is active 68 + if (this.isActive) { 69 + this.spawnAccumulator += deltaTimeSeconds; 70 + const spawnInterval = 71 + 1 / (this.spawnPerSecond * (this.game.playerManager?.players.length ?? 1)); 72 + 73 + while (this.spawnAccumulator >= spawnInterval) { 74 + const enemyIndex = this.randomEnemyIndex(); 75 + if (enemyIndex !== -1) { 76 + const enemy = this.enemies[enemyIndex]; 77 + this.game.enemyManager?.addEnemy(enemy.type); // Assuming there's an addEnemy method in the Game class 78 + enemy.spawned++; 79 + } 80 + this.spawnAccumulator -= spawnInterval; 81 + } 82 + } 83 + } 84 + 85 + isComplete(): boolean { 86 + const playerCount = this.game.playerManager?.players.length ?? 1; 87 + return this.isActive && this.enemies.every((e) => e.spawned >= e.num * playerCount); 88 + } 89 + } 90 + 91 + type WaveData = { 92 + enemies: { type: typeof Enemy; num: number }[]; 93 + spawnSpeed: number; 94 + startDelay?: number; 95 + }; 96 + 97 + export const defaultWaves: WaveData[] = [ 98 + { enemies: [{ type: TriangleEnemy, num: 5 }], spawnSpeed: 3 }, 99 + { 100 + enemies: [ 101 + { type: TriangleEnemy, num: 10 }, 102 + { type: SphereEnemy, num: 5 } 103 + ], 104 + spawnSpeed: 3 105 + }, 106 + { 107 + enemies: [ 108 + { type: TriangleEnemy, num: 15 }, 109 + { type: SphereEnemy, num: 10 } 110 + ], 111 + spawnSpeed: 2 112 + }, 113 + { 114 + enemies: [ 115 + { type: TriangleEnemy, num: 20 }, 116 + { type: SphereEnemy, num: 15 }, 117 + { type: PentagonEnemy, num: 5 } 118 + ], 119 + spawnSpeed: 3 120 + }, 121 + { enemies: [{ type: SphereEnemy, num: 50 }], spawnSpeed: 3 }, 122 + { enemies: [{ type: TriangleEnemy, num: 50 }], spawnSpeed: 2 }, 123 + { enemies: [{ type: PentagonEnemy, num: 50 }], spawnSpeed: 3 }, 124 + { 125 + enemies: [ 126 + { type: PentagonEnemy, num: 25 }, 127 + { type: TriangleEnemy, num: 25 }, 128 + { type: SphereEnemy, num: 25 } 129 + ], 130 + spawnSpeed: 3 131 + }, 132 + { 133 + enemies: [ 134 + { type: TriangleEnemy, num: 40 }, 135 + { type: SphereEnemy, num: 30 }, 136 + { type: PentagonEnemy, num: 15 } 137 + ], 138 + spawnSpeed: 4 139 + }, 140 + { 141 + enemies: [{ type: PentagonEnemy, num: 75 }], 142 + spawnSpeed: 3 143 + }, 144 + { 145 + enemies: [ 146 + { type: TriangleEnemy, num: 80 }, 147 + { type: SphereEnemy, num: 60 } 148 + ], 149 + spawnSpeed: 5 150 + }, 151 + { 152 + enemies: [ 153 + { type: TriangleEnemy, num: 50 }, 154 + { type: PentagonEnemy, num: 40 } 155 + ], 156 + spawnSpeed: 4 157 + }, 158 + { 159 + enemies: [ 160 + { type: SphereEnemy, num: 70 }, 161 + { type: PentagonEnemy, num: 50 } 162 + ], 163 + spawnSpeed: 5 164 + }, 165 + { 166 + enemies: [{ type: TriangleEnemy, num: 150 }], 167 + spawnSpeed: 6 168 + }, 169 + { 170 + enemies: [ 171 + { type: TriangleEnemy, num: 70 }, 172 + { type: SphereEnemy, num: 50 }, 173 + { type: PentagonEnemy, num: 30 } 174 + ], 175 + spawnSpeed: 5 176 + }, 177 + { 178 + enemies: [{ type: SphereEnemy, num: 120 }], 179 + spawnSpeed: 5 180 + }, 181 + { 182 + enemies: [ 183 + { type: PentagonEnemy, num: 60 }, 184 + { type: TriangleEnemy, num: 80 } 185 + ], 186 + spawnSpeed: 4 187 + }, 188 + { 189 + enemies: [ 190 + { type: TriangleEnemy, num: 100 }, 191 + { type: SphereEnemy, num: 75 }, 192 + { type: PentagonEnemy, num: 50 } 193 + ], 194 + spawnSpeed: 6 195 + }, 196 + // 10 new waves start here 197 + { 198 + enemies: [ 199 + { type: TriangleEnemy, num: 120 }, 200 + { type: SphereEnemy, num: 90 }, 201 + { type: PentagonEnemy, num: 60 } 202 + ], 203 + spawnSpeed: 7 204 + }, 205 + { 206 + enemies: [{ type: PentagonEnemy, num: 150 }], 207 + spawnSpeed: 5 208 + }, 209 + { 210 + enemies: [ 211 + { type: TriangleEnemy, num: 200 }, 212 + { type: SphereEnemy, num: 100 } 213 + ], 214 + spawnSpeed: 8 215 + }, 216 + { 217 + enemies: [ 218 + { type: SphereEnemy, num: 150 }, 219 + { type: PentagonEnemy, num: 100 } 220 + ], 221 + spawnSpeed: 6 222 + }, 223 + { 224 + enemies: [ 225 + { type: TriangleEnemy, num: 180 }, 226 + { type: PentagonEnemy, num: 90 } 227 + ], 228 + spawnSpeed: 7 229 + }, 230 + { 231 + enemies: [{ type: SphereEnemy, num: 250 }], 232 + spawnSpeed: 8 233 + }, 234 + { 235 + enemies: [ 236 + { type: TriangleEnemy, num: 150 }, 237 + { type: SphereEnemy, num: 100 }, 238 + { type: PentagonEnemy, num: 75 } 239 + ], 240 + spawnSpeed: 7 241 + }, 242 + { 243 + enemies: [ 244 + { type: PentagonEnemy, num: 120 }, 245 + { type: TriangleEnemy, num: 180 } 246 + ], 247 + spawnSpeed: 6 248 + }, 249 + { 250 + enemies: [ 251 + { type: TriangleEnemy, num: 200 }, 252 + { type: SphereEnemy, num: 150 }, 253 + { type: PentagonEnemy, num: 100 } 254 + ], 255 + spawnSpeed: 8 256 + }, 257 + { 258 + enemies: [ 259 + { type: TriangleEnemy, num: 250 }, 260 + { type: SphereEnemy, num: 200 }, 261 + { type: PentagonEnemy, num: 150 } 262 + ], 263 + spawnSpeed: 9 264 + } 265 + ]; 266 + 267 + export class WaveManager { 268 + game: Game; 269 + 270 + waves: Wave[]; 271 + 272 + currentWave: number = 0; 273 + 274 + constructor(game: Game, startWave: number = 0, waves: WaveData[] = defaultWaves) { 275 + this.game = game; 276 + 277 + this.currentWave = startWave; 278 + 279 + this.waves = waves.map( 280 + (wave, index) => new Wave(this.game, wave.enemies, index, wave.spawnSpeed, wave.startDelay) 281 + ); 282 + } 283 + 284 + update(deltaTime: number) { 285 + if (this.waves.length === 0) return; 286 + if (this.currentWave >= this.waves.length) return; 287 + 288 + if ( 289 + this.currentWave === -1 || 290 + (this.game.enemyManager?.enemies.length === 0 && this.waves[this.currentWave].isComplete()) 291 + ) { 292 + this.currentWave += 1; 293 + } 294 + 295 + if (this.currentWave >= 0 && this.currentWave < this.waves.length) { 296 + this.waves[this.currentWave].update(deltaTime); 297 + } 298 + } 299 + 300 + getCurrentWave(): Wave | undefined { 301 + if (this.currentWave >= 0 && this.currentWave < this.waves.length) { 302 + return this.waves[this.currentWave]; 303 + } 304 + } 305 + }