[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.

commit

Florian (Jul 23, 2024, 6:34 AM +0200) 9ed2cbbb 62c03e10

+260 -114
+4 -5
Readme.md
··· 31 31 32 32 - count enemies killed 33 33 - enemies attack 34 - - shoot 35 - - ram 34 + - [x] shoot (pentagon) 36 35 - pulse 37 - - explode 38 - - die 36 + - [x] ram/explode (triangle) 37 + - [x] die 39 38 - weapons 40 39 - recoil on player 41 40 - impulse on bullet impact ··· 43 42 - show weapon 44 43 - [x] change player rigidbody to dynamic 45 44 - different enemies 46 - - different shapes, attacks 45 + - [x] different shapes, attacks 47 46 - waves 48 47 - [x] eyes blinking 49 48 - [x] obstacles
+7 -18
index.html
··· 5 5 <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 6 6 <title>shadow shooter</title> 7 7 8 - <style> 9 - body { 10 - margin: 0; 11 - padding: 0; 12 - overflow: hidden; 13 - background-color: 'red'; 14 - } 8 + <script src="https://cdn.tailwindcss.com"></script> 15 9 16 - #game-container { 17 - width: 100vw; 18 - height: 100vh; 19 - } 20 - 21 - #gameCanvas { 22 - display: block; 23 - width: 100%; 24 - height: 100%; 25 - } 26 - </style> 27 10 </head> 28 11 <body> 29 12 <script src="src/app.ts" type="module"></script> 13 + 14 + <!-- ui: title, play button --> 15 + <div id="ui" class="absolute inset-0 h-screen w-screen z-10 text-white flex items-center justify-center flex-col"> 16 + <h1 id="title" class="font-bold text-5xl mb-8">Shadow Shooter</h1> 17 + <button id="play" class="text-2xl font-semibold border border-white py-2 px-4 bg-white/10 hover:bg-white/20">Play</button> 18 + </div> 30 19 </body> 31 20 </html>
+57
src/app.ts
··· 12 12 13 13 import { AdvancedBloomFilter } from 'pixi-filters'; 14 14 import PlayerManager from './player-manager.js'; 15 + import ProjectileManager, { ProjectileData } from './projectile-manager.js'; 15 16 16 17 export default class Game { 17 18 container: PIXI.Container; ··· 24 25 enemyManager?: EnemyManager; 25 26 playerManager?: PlayerManager; 26 27 28 + projectileManager?: ProjectileManager; 29 + 27 30 particleSystem?: ParticleSystem; 28 31 29 32 debug: boolean = false; 30 33 31 34 stats?: Stats; 35 + 36 + playing: boolean = false; 32 37 33 38 constructor() { 34 39 this.setup(); ··· 109 114 this.playerManager = new PlayerManager(this); 110 115 this.enemyManager = new EnemyManager(this, 10); 111 116 117 + this.projectileManager = new ProjectileManager(this); 118 + 112 119 window.addEventListener('keydown', this.handleKeyDown.bind(this)); 113 120 window.addEventListener('keyup', this.handleKeyUp.bind(this)); 114 121 ··· 120 127 for (let i = 0; i < 50; i++) { 121 128 let obstacle = new Obstacle(this); 122 129 } 130 + 131 + const ui = document.getElementById('ui'); 132 + 133 + // get play button (id:play) 134 + const playButton = document.getElementById('play'); 135 + playButton?.addEventListener('click', () => { 136 + this.playing = !this.playing; 137 + 138 + // add hidden class to ui 139 + ui?.classList.add('hidden'); 140 + 141 + let player = this.playerManager?.players[0]; 142 + if (player) player.health = player.maxHealth; 143 + }); 144 + } 145 + 146 + addProjectile(data: ProjectileData) { 147 + if (!this.projectileManager) return; 148 + 149 + this.projectileManager.addProjectile(data); 123 150 } 124 151 125 152 handleCollisionEvents(eventQueue: EventQueue) { ··· 160 187 if (enemy && player && enemy.hitPlayer) { 161 188 enemy.hitPlayer(player); 162 189 } 190 + 191 + if (player && projectile) { 192 + this.spawnParticles(projectile.shape.x, projectile.shape.y, 10, projectile.color); 193 + 194 + player.takeDamage(projectile.damage); 195 + projectile.destroy(); 196 + } 163 197 }); 164 198 } 165 199 ··· 183 217 * @param {number} deltaTime 184 218 */ 185 219 update(deltaTime: number) { 220 + if (!this.playing) return; 221 + 186 222 // update game state here 187 223 this.playerManager?.update(deltaTime, this.keys); 188 224 ··· 193 229 } 194 230 195 231 if (this.debug) this.renderPhysicsDebug(); 232 + 233 + // let player = this.playerManager?.players[0]; 234 + // if (player) { 235 + // player.health += deltaTime * 0.1; 236 + // player.health = Math.min(player.health, player.maxHealth); 237 + // } 238 + 239 + this.projectileManager?.update(deltaTime); 240 + 241 + if (this.playerManager?.players[0].health <= 0) { 242 + this.enemyManager?.killAll(); 243 + 244 + this.playing = false; 245 + 246 + const ui = document.getElementById('ui'); 247 + ui?.classList.remove('hidden'); 248 + 249 + const title = document.getElementById('title'); 250 + // set inner text to "Game Over" 251 + if (title) title.innerText = 'Game Over'; 252 + } 196 253 } 197 254 198 255 handleKeyDown(e: KeyboardEvent) {
+8
src/enemy-manager.ts
··· 50 50 } 51 51 } 52 52 } 53 + 54 + killAll() { 55 + this.enemies.forEach((enemy) => { 56 + enemy.destroy(); 57 + }); 58 + 59 + this.enemies = []; 60 + } 53 61 }
+75 -22
src/enemy.ts
··· 37 37 38 38 rigidBody?: RigidBody; 39 39 40 - leftEye: Eye; 41 - rightEye: Eye; 40 + leftEye?: Eye; 41 + rightEye?: Eye; 42 42 43 - eyes: PIXI.Container; 43 + eyes?: PIXI.Container; 44 + 45 + color: number = 0xfb923c; 44 46 45 47 hitPlayer?(player: Player): void; 46 48 ··· 84 86 85 87 this.isEnemy = true; 86 88 89 + this.createEyes(); 90 + } 91 + 92 + createEyes() { 87 93 this.eyes = new PIXI.Container(); 88 94 this.enemyContainer.addChild(this.eyes); 89 95 90 - this.leftEye = new Eye(this.eyes, -this.size / 4, 0); 91 - this.rightEye = new Eye(this.eyes, this.size / 4, 0); 96 + this.leftEye = new Eye(this.eyes, -this.size / 4, 0, this.color); 97 + this.rightEye = new Eye(this.eyes, this.size / 4, 0, this.color); 92 98 } 99 + 93 100 createShape() { 94 101 this.shape = new PIXI.Graphics().circle(0, 0, this.size / 2).fill(0); 95 102 this.enemyContainer.addChild(this.shape); ··· 187 194 // get angle between enemy and player 188 195 const angle = Math.atan2(dy, dx); 189 196 // move eyes 190 - this.leftEye.move(angle); 191 - this.rightEye.move(angle); 197 + this.leftEye?.move(angle); 198 + this.rightEye?.move(angle); 192 199 193 200 let alpha = Math.min(1, 1 - distance / 300); 194 201 195 - this.leftEye.update(deltaTime, alpha); 196 - this.rightEye.update(deltaTime, alpha); 202 + this.leftEye?.update(deltaTime, alpha); 203 + this.rightEye?.update(deltaTime, alpha); 197 204 198 205 // Only move if not too close to the player and not exploding 199 206 if (distance > this.size + 10 && !this.exploding) { ··· 223 230 } 224 231 225 232 destroy() { 226 - this.game.spawnParticles(this.x, this.y, 50, 0xe11d48); 233 + this.game.spawnParticles(this.x, this.y, 50, this.color); 227 234 228 235 this.destroyed = true; 229 236 // remove the square ··· 244 251 } 245 252 246 253 export class TriangleEnemy extends Enemy { 254 + createEyes() { 255 + this.color = 0x38bdf8; 256 + super.createEyes(); 257 + } 258 + 247 259 createShape(): void { 248 260 this.size = 20; 249 261 this.shape = new PIXI.Graphics() ··· 288 300 // get angle between enemy and player 289 301 const angle = Math.atan2(dy, dx); 290 302 303 + // apply rotation 304 + //let rotationDelta = this.rotation - (angle - Math.PI / 2); 305 + //this.rigidBody?.applyTorqueImpulse(rotationDelta * 1000000, true); 306 + 291 307 this.rotation = angle - Math.PI / 2; 292 308 293 309 // move eyes 294 - this.leftEye.move(Math.PI / 2); 295 - this.rightEye.move(Math.PI / 2); 310 + this.leftEye?.move(Math.PI / 2); 311 + this.rightEye?.move(Math.PI / 2); 296 312 297 313 let alpha = Math.min(1, 1 - distance / 300); 298 - this.leftEye.update(deltaTime, alpha); 299 - this.rightEye.update(deltaTime, alpha); 314 + this.leftEye?.update(deltaTime, alpha); 315 + this.rightEye?.update(deltaTime, alpha); 300 316 301 317 const force = 600; 302 318 ··· 306 322 this.rigidBody?.applyImpulse({ x: x * force, y: -y * force }, true); 307 323 308 324 this.enemyContainer.position.set(this.x, this.y); 325 + //this.enemyContainer.rotation = this.rotation; 326 + 327 + if (this.exploding) { 328 + this.destroy(); 329 + 330 + if (distance < this.size + 20) { 331 + player.takeDamage(10); 332 + } 333 + } 334 + 335 + if (distance < 50) { 336 + if (this.destroyTime < 0) this.destroyTime = 1000; 337 + else this.destroyTime -= deltaTime; 338 + 339 + if (this.destroyTime < 0) this.exploding = true; 340 + } else { 341 + this.destroyTime = -1; 342 + } 309 343 } 310 344 311 345 hitPlayer(player: Player) { 312 - console.log('hit player'); 346 + // explode on next update() 347 + this.exploding = true; 313 348 } 314 349 } 315 350 316 351 export class PentagonEnemy extends Enemy { 317 - //weapon: Weapon; 352 + weapon: Weapon; 318 353 319 354 constructor(game: Game) { 320 355 super(game); 321 356 322 - //this.weapon = new Weapon(this.game, 0x00ff00, 0x00040001); 357 + this.weapon = new Weapon(this.game, { 358 + color: this.color, 359 + collisionGroups: 0x00100001, 360 + projectileSpeed: 0.2, 361 + fireRate: 2000, 362 + projectileSize: 6 363 + }); 364 + } 365 + 366 + createEyes() { 367 + this.color = 0x4ade80; 368 + super.createEyes(); 323 369 } 324 370 325 371 createShape(): void { ··· 380 426 this.rotation += rotationSpeed * deltaTime; 381 427 382 428 // move eyes 383 - this.leftEye.move(-this.rotation + angle); 384 - this.rightEye.move(-this.rotation + angle); 429 + this.leftEye?.move(-this.rotation + angle); 430 + this.rightEye?.move(-this.rotation + angle); 385 431 386 432 let alpha = Math.min(1, 1 - distance / 300); 387 - this.leftEye.update(deltaTime, alpha); 388 - this.rightEye.update(deltaTime, alpha); 433 + this.leftEye?.update(deltaTime, alpha); 434 + this.rightEye?.update(deltaTime, alpha); 389 435 390 436 // Only move if not too close to the player and not exploding 391 437 // lets add some force instead of moving it directly, the further away the player, the more force ··· 396 442 397 443 this.rigidBody?.applyImpulse({ x: x * force, y: -y * force }, true); 398 444 445 + // if player is close enough, fire weapon 446 + if (distance < 400) { 447 + this.weapon.fire(this.position, player.position); 448 + } 449 + 450 + this.weapon.update(deltaTime); 451 + 399 452 this.enemyContainer.position.set(this.x, this.y); 400 453 } 401 - } 454 + }
+6 -2
src/eye.ts
··· 18 18 dx: number = 0; 19 19 dy: number = 0; 20 20 21 - constructor(container: PIXI.Container, shiftX: number, shiftY: number) { 21 + color: number; 22 + 23 + constructor(container: PIXI.Container, shiftX: number, shiftY: number, color = 0xffffff) { 22 24 this.eyeContainer = new PIXI.Container(); 23 25 24 26 this.eyeContainer.position.set(shiftX, shiftY); 25 27 26 28 container.addChild(this.eyeContainer); 29 + 30 + this.color = color; 27 31 this.loadLayers(); 28 32 } 29 33 30 34 async loadLayers() { 31 - const base = new PIXI.Graphics().ellipse(0, 0, 5, 4).fill(0xffffff); //.fill(0xe11d48); 35 + const base = new PIXI.Graphics().ellipse(0, 0, 5, 4).fill(this.color); //.fill(0xe11d48); 32 36 this.base = base; 33 37 this.base.alpha = 0; 34 38 this.eyeContainer.addChild(base);
+2 -2
src/player.ts
··· 72 72 this.speed = 30000; 73 73 this.shape = shape; 74 74 75 - this.weapon = new Weapon(this.game, this.color); 75 + this.weapon = new Weapon(this.game, { color: this.color }); 76 76 77 77 this.isPlayer = true; 78 78 ··· 116 116 117 117 const colliderDesc = RAPIER() 118 118 .ColliderDesc.cuboid(this.size / 2, this.size / 2) 119 - .setCollisionGroups(0x00010003); 119 + .setCollisionGroups(0x00010013); 120 120 this.game.world.createCollider(colliderDesc, this.rigidBody); 121 121 122 122 // Attach this Player instance to the rigid body's user data
+67
src/projectile-manager.ts
··· 1 + import Game from './app'; 2 + import { Projectile } from './projectile'; 3 + 4 + export type ProjectileData = { 5 + position: { x: number; y: number }; 6 + enemyPosition: { x: number; y: number }; 7 + speed: number; 8 + size: number; 9 + damage: number; 10 + color: number; 11 + angleOffset: number; 12 + collisionGroups: number; 13 + }; 14 + 15 + export default class ProjectileManager { 16 + projectiles: Projectile[] = []; 17 + 18 + game: Game; 19 + constructor(game: Game) { 20 + this.game = game; 21 + } 22 + 23 + addProjectile(data: ProjectileData) { 24 + const projectile = new Projectile( 25 + this.game, 26 + data.position, 27 + data.enemyPosition, 28 + data.speed, 29 + data.size, 30 + data.damage, 31 + data.color, 32 + data.angleOffset, 33 + data.collisionGroups 34 + ); 35 + 36 + this.projectiles.push(projectile); 37 + } 38 + 39 + update(deltaTime: number) { 40 + for (let i = this.projectiles.length - 1; i >= 0; i--) { 41 + let projectile = this.projectiles[i]; 42 + if (projectile.destroyed) { 43 + this.projectiles.splice(i, 1); 44 + continue; 45 + } 46 + projectile.update(deltaTime); 47 + 48 + // Check if projectile is too far from 0, 0 49 + const dx = projectile.shape.x; 50 + const dy = projectile.shape.y; 51 + const distanceSquared = dx * dx + dy * dy; 52 + 53 + if (distanceSquared > 1000000) { 54 + // 1000 units squared 55 + projectile.destroy(); 56 + this.projectiles.splice(i, 1); 57 + } 58 + } 59 + } 60 + 61 + clearAllProjectiles() { 62 + for (let projectile of this.projectiles) { 63 + projectile.destroy(); 64 + } 65 + this.projectiles = []; 66 + } 67 + }
+34 -65
src/weapon.ts
··· 1 1 import Game from './app'; 2 2 import { Projectile } from './projectile'; 3 3 4 + export type WeaponOptions = { 5 + damage?: number; 6 + fireRate?: number; 7 + projectileSpeed?: number; 8 + projectileSize?: number; 9 + color?: number; 10 + collisionGroups?: number; 11 + }; 12 + 4 13 export class Weapon { 5 14 game: Game; 6 15 7 - cooldown: number; 8 - projectiles: Projectile[]; 16 + cooldown: number = 0; 9 17 10 - damage: number; 11 - fireRate: number; 12 - projectileSpeed: number; 13 - projectileSize: number; 18 + damage: number = 20; 19 + fireRate: number = 100; 20 + projectileSpeed: number = 0.3; 21 + projectileSize: number = 5; 14 22 15 - color: number; 23 + color: number = 0xffffff; 16 24 17 - collisionGroups: number; 25 + collisionGroups: number = 0x00040002; 18 26 19 - constructor(game: Game, color: number, collisionGroups: number = 0x00040002) { 27 + constructor(game: Game, options: WeaponOptions | undefined = undefined) { 20 28 this.game = game; 21 29 22 30 this.cooldown = 0; 23 - this.projectiles = []; 24 31 25 - this.damage = 20; 26 - this.fireRate = 100; 27 - this.projectileSpeed = 0.3; 28 - this.projectileSize = 5; 32 + if (!options) return; 29 33 30 - this.color = color; 34 + if (options.damage) this.damage = options.damage; 35 + if (options.fireRate) this.fireRate = options.fireRate; 36 + if (options.projectileSpeed) this.projectileSpeed = options.projectileSpeed; 37 + if (options.projectileSize) this.projectileSize = options.projectileSize; 31 38 32 - this.collisionGroups = collisionGroups; 39 + if (options.color) this.color = options.color; 40 + if (options.collisionGroups) this.collisionGroups = options.collisionGroups; 33 41 } 34 42 35 43 fire(position: { x: number; y: number }, enemyPosition: { x: number; y: number }) { 36 44 if (this.cooldown <= 0) { 37 - console.log('Firing'); 38 - this.createProjectile(position, enemyPosition, 0); 45 + this.game.addProjectile({ 46 + position, 47 + enemyPosition, 48 + angleOffset: 0, 49 + speed: this.projectileSpeed, 50 + size: this.projectileSize, 51 + damage: this.damage, 52 + color: this.color, 53 + collisionGroups: this.collisionGroups 54 + }); 39 55 this.cooldown = this.fireRate; 40 56 } 41 57 } 42 58 43 - createProjectile( 44 - position: { x: number; y: number }, 45 - enemyPosition: { x: number; y: number }, 46 - angleOffset: number 47 - ) { 48 - const projectile = new Projectile( 49 - this.game, 50 - position, 51 - enemyPosition, 52 - this.projectileSpeed, 53 - this.projectileSize, 54 - this.damage, 55 - this.color, 56 - angleOffset, 57 - this.collisionGroups 58 - ); 59 - 60 - this.projectiles.push(projectile); 61 - } 62 - 63 59 update(deltaTime: number) { 64 60 this.cooldown -= deltaTime; 65 - 66 - for (let i = this.projectiles.length - 1; i >= 0; i--) { 67 - let projectile = this.projectiles[i]; 68 - if (projectile.destroyed) { 69 - this.projectiles.splice(i, 1); 70 - continue; 71 - } 72 - projectile.update(deltaTime); 73 - 74 - // Check if projectile is too far from 0, 0 75 - const dx = projectile.shape.x; 76 - const dy = projectile.shape.y; 77 - const distanceSquared = dx * dx + dy * dy; 78 - 79 - if (distanceSquared > 100000000) { 80 - // 1000 units squared 81 - projectile.destroy(); 82 - this.projectiles.splice(i, 1); 83 - } 84 - } 85 - } 86 - 87 - clearAllProjectiles() { 88 - for (let projectile of this.projectiles) { 89 - projectile.destroy(); 90 - } 91 - this.projectiles = []; 92 61 } 93 62 }