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

items, enemy boss

Florian (Jul 26, 2024, 8:36 AM +0200) dd2632fb 1c734112

+381 -13
+44 -6
Readme.md
··· 29 29 30 30 # shadow shmup 31 31 32 - - count enemies killed 32 + - count kills, deaths, damage taken, damage dealt 33 33 - enemies attack 34 34 - [x] shoot (pentagon) 35 35 - pulse ··· 46 46 - waves 47 47 - [x] eyes blinking 48 48 - [x] obstacles 49 - - [ ] enemies drop stuff, player can pick it up 50 - - [ ] music 49 + - [x] enemies drop stuff, player can pick it up 50 + - [x] music 51 51 - [ ] sound effects 52 52 - [ ] player shooting 53 53 - [ ] player getting hit ··· 55 55 - [ ] enemy shooting 56 56 - [ ] enemy dying 57 57 - [ ] special effects 58 - - [ ] player getting hit (flash screen) 59 - - [ ] menu: solo, co-op, credits 58 + - [x] player getting hit (flash screen) 59 + - [x] menu: solo, co-op, credits 60 + - [x] credits 61 + - [x] items ("potions") 62 + - [x] disappear after a while 63 + - [x] different potions (depending on monster killed?) 64 + - [ ] player upgrades (all upgrades use potions, and have one positive and one negative effect) 65 + - movement speed 66 + - fire rate 67 + - bullet speed 68 + - bullet damage 69 + - player health 70 + - viewing distance 71 + - shooting distance 72 + - bullet count? 73 + - bullet spread? 74 + - lights 75 + - make own light class and add to player 76 + - enemies should be lit by light distance instead of player distance 77 + - [ ] powerups 78 + - [ ] randomly spawn 79 + - [ ] disappear after a while 80 + - [ ] can be used for a limited time 81 + - [ ] different powerups: 82 + - [ ] invincibility 83 + - [ ] 2x, 4x damage 84 + - [ ] 2x, 4x fire rate 85 + - [ ] 2x, 4x bullet count 86 + - [ ] 2x shooting distance 87 + - [ ] shoot through enemies 88 + - [ ] 50%, 100% health refill 89 + - [x] scale canvas so that it always shows a 1000x1000 area or something like that 90 + - [ ] enemy upgrades (some enemies are upgraded every wave) 91 + - damage 92 + - speed 93 + - health 94 + - shooting range 95 + - bullet speed 96 + - bullet count 97 + - fire rate 60 98 61 99 62 100 ## credits: 63 101 64 - - music: [Nicolas Palmer](https://soundcloud.com/nicholas-palmer-4) 102 + - music: [Nicholas Palmer](https://soundcloud.com/nicholas-palmer-4) 65 103 - sound effects: [Kenney](https://kenney.nl/)
+32 -5
src/app.ts
··· 5 5 import { RAPIER } from './rapier.js'; 6 6 import ParticleSystem from './particles.js'; 7 7 import { Projectile } from './projectile.js'; 8 - import Enemy from './enemy.js'; 8 + import Enemy, { CrossEnemy } from './enemy.js'; 9 9 10 10 import Stats from 'stats.js'; 11 11 ··· 17 17 import Controls from './controls.js'; 18 18 import { ObstacleManager } from './obstacle-manager.js'; 19 19 import { WaveManager } from './wave.js'; 20 + import { Item, ItemOptions } from './item.js'; 21 + import { ItemManager } from './item-manager.js'; 20 22 21 23 export default class Game { 22 24 container: PIXI.Container; ··· 31 33 playerManager?: PlayerManager; 32 34 waveManager?: WaveManager; 33 35 36 + itemManager: ItemManager; 37 + 34 38 projectileManager?: ProjectileManager; 35 39 36 40 particleSystem?: ParticleSystem; ··· 55 59 56 60 startWave = 0; 57 61 62 + minWidth = 700; 63 + minHeight = 1000; 64 + 58 65 constructor() { 59 66 this.setup(); 60 67 ··· 62 69 this.container = new PIXI.Container(); 63 70 64 71 this.controls = new Controls(this); 65 - 66 72 this.obstacleManager = new ObstacleManager(this); 73 + this.itemManager = new ItemManager(this); 67 74 68 75 sound.add('music-intro', { 69 76 url: './music-intro.mp3' ··· 134 141 this.mainContainer.addChild(this.container); 135 142 136 143 this.mainContainer.position.set(window.innerWidth / 2, window.innerHeight / 2); 144 + // scale the container 145 + this.scale = Math.min(window.innerWidth / this.minWidth, window.innerHeight / this.minHeight); 146 + this.mainContainer.scale.set(this.scale); 137 147 138 148 // add a resize event listener 139 149 window.addEventListener('resize', () => { 140 150 app.renderer.resize(window.innerWidth, window.innerHeight); 141 151 142 152 this.mainContainer.position.set(window.innerWidth / 2, window.innerHeight / 2); 153 + this.scale = Math.min(window.innerWidth / this.minWidth, window.innerHeight / this.minHeight); 154 + this.mainContainer.scale.set(this.scale); 143 155 }); 144 156 145 - this.container.scale.set(this.scale); 157 + //this.container.scale.set(this.scale); 146 158 147 159 app.ticker.add((ticker) => { 148 160 if (this.stats) this.stats.begin(); ··· 170 182 const interpolationSpeed = 0.05; 171 183 const interpolationFactor = 1 - Math.pow(1 - interpolationSpeed, deltaTime / (1000 / 60)); 172 184 173 - this.container.x += (position.x * this.scale - this.container.x) * interpolationFactor; 174 - this.container.y += (position.y * this.scale - this.container.y) * interpolationFactor; 185 + this.container.x += (position.x - this.container.x) * interpolationFactor; 186 + this.container.y += (position.y - this.container.y) * interpolationFactor; 175 187 } 176 188 177 189 if (this.stats) this.stats.end(); ··· 223 235 }); 224 236 } 225 237 238 + dropItem(options: ItemOptions) { 239 + this.itemManager.addItem(options); 240 + } 241 + 226 242 addProjectile(data: ProjectileData) { 227 243 if (!this.projectileManager) return; 228 244 ··· 246 262 let enemy: Enemy | undefined; 247 263 let projectile: Projectile | undefined; 248 264 let player: Player | undefined; 265 + let item: Item | undefined; 249 266 250 267 if (userData1 instanceof Enemy) enemy = userData1; 251 268 if (userData2 instanceof Enemy) enemy = userData2; ··· 256 273 if (userData1 instanceof Player) player = userData1; 257 274 if (userData2 instanceof Player) player = userData2; 258 275 276 + if (userData1 instanceof Item) item = userData1; 277 + if (userData2 instanceof Item) item = userData2; 278 + 259 279 if (enemy && projectile) { 260 280 this.spawnParticles(projectile.shape.x, projectile.shape.y, 10, projectile.color); 261 281 ··· 273 293 274 294 player.takeDamage(projectile.damage); 275 295 projectile.destroy(); 296 + } 297 + 298 + console.log(player, item); 299 + if (player && item) { 300 + item.pickup(player); 276 301 } 277 302 278 303 if (projectile) { ··· 314 339 this.obstacleManager.update(deltaTime); 315 340 316 341 this.enemyManager?.update(deltaTime); 342 + 343 + this.itemManager.update(deltaTime); 317 344 318 345 if (this.invincible) { 319 346 for (let players of this.playerManager?.players ?? []) {
+162
src/enemy.ts
··· 45 45 46 46 color: number = 0xfb923c; 47 47 48 + type: number = 0; 49 + 48 50 hitPlayer?(player: Player): void; 49 51 50 52 constructor(game: Game) { ··· 239 241 240 242 destroy() { 241 243 if (this.destroyed) return; 244 + 245 + this.game.dropItem({ x: this.x, y: this.y, color: this.color, size: 10, type: this.type }); 246 + 242 247 this.game.spawnParticles(this.x, this.y, 50, this.color); 243 248 244 249 this.destroyed = true; ··· 265 270 constructor(game: Game) { 266 271 super(game); 267 272 273 + this.type = 1; 274 + 268 275 this.weapon = new Weapon(this.game, { 269 276 color: this.color, 270 277 collisionGroups: 0x00100001, ··· 301 308 constructor(game: Game) { 302 309 super(game); 303 310 311 + this.type = 2; 304 312 this.speed = 3000; 305 313 306 314 let position = { x: this.x, y: this.y }; ··· 410 418 constructor(game: Game) { 411 419 super(game); 412 420 421 + this.type = 3; 422 + 413 423 this.weapon = new Weapon(this.game, { 414 424 color: this.color, 415 425 collisionGroups: 0x00100001, ··· 499 509 this.weapon.update(deltaTime); 500 510 } 501 511 } 512 + 513 + export class CrossEnemy extends Enemy { 514 + weapon: Weapon; 515 + 516 + shootAngle: number = 0; 517 + 518 + constructor(game: Game) { 519 + super(game); 520 + 521 + this.type = 4; 522 + 523 + this.weapon = new Weapon(this.game, { 524 + color: this.color, 525 + collisionGroups: 0x00100001, 526 + projectileSpeed: 0.2, 527 + fireRate: 100, 528 + projectileSize: 6, 529 + damage: 10, 530 + lifetime: 8000 531 + }); 532 + 533 + this.speed = 20000; 534 + this.health = 10000; 535 + } 536 + 537 + createEyes() { 538 + this.size = 500; 539 + this.color = 0x0284c7; 540 + this.eyes = new PIXI.Container(); 541 + this.enemyContainer.addChild(this.eyes); 542 + this.eyes.scale.set(5); 543 + 544 + this.leftEye = new Eye(this.eyes, -10, 0, this.color); 545 + this.rightEye = new Eye(this.eyes, 10, 0, this.color); 546 + } 547 + 548 + createShape(): void { 549 + this.size = 600; 550 + this.shape = new PIXI.Graphics().rect(0, 0, this.size, this.size / 8).fill(0); 551 + this.shape.pivot.set(this.size / 2, this.size / 16); 552 + let shape2 = new PIXI.Graphics().rect(0, 0, this.size / 8, this.size).fill(0); 553 + shape2.pivot.set(this.size / 16, this.size / 2); 554 + this.enemyContainer.addChild(this.shape); 555 + this.enemyContainer.addChild(shape2); 556 + } 557 + 558 + createRidigBody(): void { 559 + this.size = 600; 560 + const rigidBodyDesc = RAPIER() 561 + .RigidBodyDesc.dynamic() 562 + .setTranslation(this.x, -this.y) 563 + .lockRotations() 564 + .setLinearDamping(0.5); 565 + this.rigidBody = this.game.world.createRigidBody(rigidBodyDesc); 566 + 567 + const colliderDesc = RAPIER() 568 + .ColliderDesc.cuboid(this.size / 2, this.size / 16) 569 + .setActiveEvents(RAPIER().ActiveEvents.COLLISION_EVENTS) 570 + .setCollisionGroups(0x00020007); 571 + 572 + this.game.world.createCollider(colliderDesc, this.rigidBody); 573 + 574 + const colliderDesc2 = RAPIER() 575 + .ColliderDesc.cuboid(this.size / 16, this.size / 2) 576 + .setActiveEvents(RAPIER().ActiveEvents.COLLISION_EVENTS) 577 + .setCollisionGroups(0x00020007); 578 + 579 + this.game.world.createCollider(colliderDesc2, this.rigidBody); 580 + const colliderDesc3 = RAPIER() 581 + .ColliderDesc.cuboid(this.size / 4, this.size / 32) 582 + .setActiveEvents(RAPIER().ActiveEvents.COLLISION_EVENTS) 583 + .setCollisionGroups(0x00020007) 584 + .setTranslation(0, this.size / 3); 585 + this.game.world.createCollider(colliderDesc3, this.rigidBody); 586 + 587 + const colliderDesc4 = RAPIER() 588 + .ColliderDesc.cuboid(this.size / 4, this.size / 32) 589 + .setActiveEvents(RAPIER().ActiveEvents.COLLISION_EVENTS) 590 + .setCollisionGroups(0x00020007) 591 + .setTranslation(0, -this.size / 3); 592 + this.game.world.createCollider(colliderDesc4, this.rigidBody); 593 + 594 + const colliderDesc5 = RAPIER() 595 + .ColliderDesc.cuboid(this.size / 64, this.size / 8) 596 + .setActiveEvents(RAPIER().ActiveEvents.COLLISION_EVENTS) 597 + .setCollisionGroups(0x00020007) 598 + .setTranslation(this.size / 6, this.size / 3); 599 + 600 + this.game.world.createCollider(colliderDesc5, this.rigidBody); 601 + 602 + const colliderDesc6 = RAPIER() 603 + .ColliderDesc.cuboid(this.size / 64, this.size / 8) 604 + .setActiveEvents(RAPIER().ActiveEvents.COLLISION_EVENTS) 605 + .setCollisionGroups(0x00020007) 606 + .setTranslation(this.size / 6, -this.size / 3); 607 + 608 + this.game.world.createCollider(colliderDesc6, this.rigidBody); 609 + const colliderDesc7 = RAPIER() 610 + .ColliderDesc.cuboid(this.size / 64, this.size / 8) 611 + .setActiveEvents(RAPIER().ActiveEvents.COLLISION_EVENTS) 612 + .setCollisionGroups(0x00020007) 613 + .setTranslation(-this.size / 6, this.size / 3); 614 + 615 + this.game.world.createCollider(colliderDesc7, this.rigidBody); 616 + 617 + const colliderDesc8 = RAPIER() 618 + .ColliderDesc.cuboid(this.size / 64, this.size / 8) 619 + .setActiveEvents(RAPIER().ActiveEvents.COLLISION_EVENTS) 620 + .setCollisionGroups(0x00020007) 621 + .setTranslation(-this.size / 6, -this.size / 3); 622 + 623 + this.game.world.createCollider(colliderDesc8, this.rigidBody); 624 + 625 + this.rigidBody.userData = this; 626 + } 627 + 628 + updateVisuals( 629 + deltaTime: number, 630 + nearestPlayer: Player, 631 + dx: number, 632 + dy: number, 633 + distance: number 634 + ): void { 635 + const angle = Math.atan2(dy, dx); 636 + 637 + const rotationSpeed = 0.0001; 638 + this.rotation += rotationSpeed * deltaTime; 639 + 640 + // move eyes 641 + this.leftEye?.move(-this.rotation + angle); 642 + this.rightEye?.move(-this.rotation + angle); 643 + 644 + let alpha = Math.min(1, 1 - distance / (nearestPlayer.viewDistance * 5)); 645 + this.leftEye?.update(deltaTime, alpha); 646 + this.rightEye?.update(deltaTime, alpha); 647 + 648 + this.enemyContainer.position.set(this.x, this.y); 649 + } 650 + 651 + attack(deltaTime: number, nearestPlayer: Player, dx: number, dy: number, distance: number): void { 652 + if (this.weapon.cooldown <= 0) { 653 + // get angle 654 + let x = Math.cos(this.shootAngle) + this.position.x; 655 + let y = Math.sin(this.shootAngle) + this.position.y; 656 + this.weapon.fire(this.position, { x, y }); 657 + 658 + this.shootAngle += Math.PI / 10; 659 + } 660 + 661 + this.weapon.update(deltaTime); 662 + } 663 + }
+24
src/item-manager.ts
··· 1 + import Game from './app'; 2 + import { Item, ItemOptions } from './item'; 3 + 4 + export class ItemManager { 5 + items: Item[] = []; 6 + game: Game; 7 + 8 + constructor(game: Game) { 9 + this.game = game; 10 + } 11 + 12 + addItem(options: ItemOptions) { 13 + const item = new Item(this.game, options); 14 + this.items.push(item); 15 + } 16 + 17 + update(deltaTime: number) { 18 + for (let item of this.items) { 19 + item.update(deltaTime); 20 + } 21 + 22 + this.items = this.items.filter((item) => !item.destroyed); 23 + } 24 + }
+108
src/item.ts
··· 1 + import { Collider, RigidBody } from '@dimforge/rapier2d'; 2 + import Game from './app'; 3 + import * as PIXI from 'pixi.js'; 4 + import { RAPIER } from './rapier'; 5 + import Player from './player'; 6 + 7 + export type ItemOptions = { 8 + x: number; 9 + y: number; 10 + size: number; 11 + color: number; 12 + type: number; 13 + collisionGroups?: number; 14 + lifetime?: number; 15 + }; 16 + 17 + export class Item { 18 + game: Game; 19 + 20 + shape: PIXI.Graphics; 21 + size: number; 22 + color: number; 23 + 24 + rigidBody: RigidBody; 25 + 26 + collider: Collider; 27 + 28 + destroyed: boolean = false; 29 + 30 + type: number; 31 + 32 + lifetime: number; 33 + 34 + life: number = 0; 35 + 36 + constructor(game: Game, options: ItemOptions) { 37 + this.game = game; 38 + 39 + this.size = options.size; 40 + this.color = options.color; 41 + 42 + this.type = options.type; 43 + 44 + this.lifetime = options.lifetime ?? 10000; 45 + 46 + this.shape = new PIXI.Graphics().rect(0, 0, this.size, this.size).fill(this.color); 47 + 48 + this.shape.x = options.x; 49 + this.shape.y = options.y; 50 + this.shape.pivot.set(this.size / 2, this.size / 2); 51 + 52 + game.container.addChild(this.shape); 53 + 54 + const rigidBodyDesc = RAPIER() 55 + .RigidBodyDesc.dynamic() 56 + .setTranslation(this.shape.x, -this.shape.y); 57 + this.rigidBody = game.world.createRigidBody(rigidBodyDesc); 58 + 59 + const colliderDesc = RAPIER() 60 + .ColliderDesc.cuboid(this.size / 2, this.size / 2) 61 + .setCollisionGroups(options.collisionGroups ?? 0x00200001) 62 + .setSensor(true) 63 + .setActiveEvents(RAPIER().ActiveEvents.COLLISION_EVENTS); 64 + 65 + this.collider = game.world.createCollider(colliderDesc, this.rigidBody); 66 + 67 + this.rigidBody.userData = this; 68 + } 69 + 70 + pickup(player: Player) { 71 + this.game.spawnParticles(this.shape.x, this.shape.y, 5, this.color); 72 + 73 + player.items[this.type] += 1; 74 + this.destroy(); 75 + } 76 + 77 + update(deltaTime: number) { 78 + if (this.destroyed) return; 79 + this.life += deltaTime; 80 + 81 + if (this.life >= this.lifetime) { 82 + this.destroy(); 83 + return; 84 + } 85 + 86 + let closestPlayer = this.game.playerManager?.getClosestPlayer({ 87 + x: this.shape.x, 88 + y: this.shape.y 89 + }); 90 + 91 + if (closestPlayer) { 92 + let dist = Math.sqrt( 93 + (closestPlayer.x - this.shape.x) ** 2 + (closestPlayer.y - this.shape.y) ** 2 94 + ); 95 + this.shape.alpha = 1 - dist / closestPlayer.viewDistance; 96 + } 97 + 98 + this.shape.alpha *= 1 - this.life / this.lifetime; 99 + } 100 + 101 + destroy() { 102 + if (this.destroyed) return; 103 + 104 + this.destroyed = true; 105 + this.shape.destroy(); 106 + this.game.world.removeRigidBody(this.rigidBody); 107 + } 108 + }
+6
src/player-manager.ts
··· 78 78 position: { x: number; y: number }, 79 79 maxDist: number | undefined = undefined 80 80 ): Player | undefined { 81 + if (this.players.length === 1) { 82 + const player = this.players[0]; 83 + const dist = (player.x - position.x) ** 2 + (player.y - position.y) ** 2; 84 + if ((!maxDist || dist < maxDist ** 2) && !player.dead) return player; 85 + } 86 + 81 87 // go through all players and find the closest one 82 88 let closestPlayer: Player | undefined; 83 89 let closestDist = Infinity;
+3 -1
src/player.ts
··· 53 53 54 54 respawnTime: number = 0; 55 55 56 + items: number[] = [0, 0, 0, 0]; 57 + 56 58 constructor(game: Game, num: number) { 57 59 this.game = game; 58 60 this.num = num; ··· 130 132 131 133 const colliderDesc = RAPIER() 132 134 .ColliderDesc.cuboid(this.size / 2, this.size / 2) 133 - .setCollisionGroups(0x00010013); 135 + .setCollisionGroups(0x00010033); 134 136 this.game.world.createCollider(colliderDesc, this.rigidBody); 135 137 136 138 // Attach this Player instance to the rigid body's user data
+2 -1
src/wave.ts
··· 6 6 */ 7 7 8 8 import Game from './app'; 9 - import Enemy, { PentagonEnemy, SphereEnemy, TriangleEnemy } from './enemy'; 9 + import Enemy, { CrossEnemy, PentagonEnemy, SphereEnemy, TriangleEnemy } from './enemy'; 10 10 11 11 export class Wave { 12 12 game: Game; ··· 95 95 }; 96 96 97 97 export const defaultWaves: WaveData[] = [ 98 + //{ enemies: [{ type: CrossEnemy, num: 1 }], spawnSpeed: 1 }, 98 99 { enemies: [{ type: TriangleEnemy, num: 5 }], spawnSpeed: 3 }, 99 100 { 100 101 enemies: [