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

Merge pull request #7 from flo-bit/main

pr

authored by

Florian and committed by
GitHub
(Jul 26, 2024, 4:02 PM +0200) bbf040b2 d2b1b6df

+720 -96
+46 -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 + - player size 73 + - bullet count? 74 + - bullet spread? 75 + - lights 76 + - make own light class and add to player 77 + - enemies should be lit by light distance instead of player distance 78 + - [ ] powerups 79 + - [ ] randomly spawn 80 + - [ ] disappear after a while 81 + - [ ] can be used for a limited time 82 + - [ ] different powerups: 83 + - [ ] invincibility 84 + - [ ] 2x, 4x damage 85 + - [ ] 2x, 4x fire rate 86 + - [ ] 2x, 4x bullet count 87 + - [ ] 2x shooting distance 88 + - [ ] shoot through enemies 89 + - [ ] 50%, 100% health refill 90 + - [x] scale canvas so that it always shows a 1000x1000 area or something like that 91 + - [ ] enemy upgrades (some enemies are upgraded every wave) 92 + - damage 93 + - speed 94 + - health 95 + - shooting range 96 + - bullet speed 97 + - bullet count 98 + - fire rate 99 + - [ ] indicator for sphere enemy shooting (+pause before shooting) 60 100 61 101 62 102 ## credits: 63 103 64 - - music: [Nicolas Palmer](https://soundcloud.com/nicholas-palmer-4) 104 + - music: [Nicholas Palmer](https://soundcloud.com/nicholas-palmer-4) 65 105 - sound effects: [Kenney](https://kenney.nl/)
+56 -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'; 22 + import { LightManager } from './light-manager.js'; 20 23 21 24 export default class Game { 22 25 container: PIXI.Container; ··· 31 34 playerManager?: PlayerManager; 32 35 waveManager?: WaveManager; 33 36 37 + itemManager: ItemManager; 38 + 39 + lightManager: LightManager; 40 + 34 41 projectileManager?: ProjectileManager; 35 42 36 43 particleSystem?: ParticleSystem; ··· 55 62 56 63 startWave = 0; 57 64 65 + minWidth = 700; 66 + minHeight = 1000; 67 + 68 + lightPlaced = false; 69 + 58 70 constructor() { 59 71 this.setup(); 60 72 ··· 62 74 this.container = new PIXI.Container(); 63 75 64 76 this.controls = new Controls(this); 65 - 66 77 this.obstacleManager = new ObstacleManager(this); 78 + this.itemManager = new ItemManager(this); 79 + this.lightManager = new LightManager(this); 67 80 68 81 sound.add('music-intro', { 69 82 url: './music-intro.mp3' ··· 134 147 this.mainContainer.addChild(this.container); 135 148 136 149 this.mainContainer.position.set(window.innerWidth / 2, window.innerHeight / 2); 150 + // scale the container 151 + this.scale = Math.min(window.innerWidth / this.minWidth, window.innerHeight / this.minHeight); 152 + this.mainContainer.scale.set(this.scale); 137 153 138 154 // add a resize event listener 139 155 window.addEventListener('resize', () => { 140 156 app.renderer.resize(window.innerWidth, window.innerHeight); 141 157 142 158 this.mainContainer.position.set(window.innerWidth / 2, window.innerHeight / 2); 159 + this.scale = Math.min(window.innerWidth / this.minWidth, window.innerHeight / this.minHeight); 160 + this.mainContainer.scale.set(this.scale); 143 161 }); 144 162 145 - this.container.scale.set(this.scale); 163 + //this.container.scale.set(this.scale); 146 164 147 165 app.ticker.add((ticker) => { 148 166 if (this.stats) this.stats.begin(); ··· 170 188 const interpolationSpeed = 0.05; 171 189 const interpolationFactor = 1 - Math.pow(1 - interpolationSpeed, deltaTime / (1000 / 60)); 172 190 173 - this.container.x += (position.x * this.scale - this.container.x) * interpolationFactor; 174 - this.container.y += (position.y * this.scale - this.container.y) * interpolationFactor; 191 + this.container.x += (position.x - this.container.x) * interpolationFactor; 192 + this.container.y += (position.y - this.container.y) * interpolationFactor; 175 193 } 176 194 177 195 if (this.stats) this.stats.end(); ··· 223 241 }); 224 242 } 225 243 244 + dropItem(options: ItemOptions) { 245 + this.itemManager.addItem(options); 246 + } 247 + 226 248 addProjectile(data: ProjectileData) { 227 249 if (!this.projectileManager) return; 228 250 ··· 246 268 let enemy: Enemy | undefined; 247 269 let projectile: Projectile | undefined; 248 270 let player: Player | undefined; 271 + let item: Item | undefined; 249 272 250 273 if (userData1 instanceof Enemy) enemy = userData1; 251 274 if (userData2 instanceof Enemy) enemy = userData2; ··· 256 279 if (userData1 instanceof Player) player = userData1; 257 280 if (userData2 instanceof Player) player = userData2; 258 281 282 + if (userData1 instanceof Item) item = userData1; 283 + if (userData2 instanceof Item) item = userData2; 284 + 259 285 if (enemy && projectile) { 260 286 this.spawnParticles(projectile.shape.x, projectile.shape.y, 10, projectile.color); 261 287 ··· 275 301 projectile.destroy(); 276 302 } 277 303 304 + console.log(player, item); 305 + if (player && item) { 306 + item.pickup(player); 307 + } 308 + 278 309 if (projectile) { 279 310 projectile.onHit(); 280 311 } ··· 315 346 316 347 this.enemyManager?.update(deltaTime); 317 348 349 + this.itemManager.update(deltaTime); 350 + 351 + this.lightManager.update(deltaTime); 352 + 318 353 if (this.invincible) { 319 354 for (let players of this.playerManager?.players ?? []) { 320 355 players.health = players.maxHealth; 321 356 } 357 + } 358 + 359 + if (this.keys['e']) { 360 + let player = this.playerManager?.players[0]; 361 + if (player && !this.lightPlaced) { 362 + this.lightManager.addLight({ 363 + color: player.color, 364 + x: player.x, 365 + y: player.y, 366 + alpha: 0.2, 367 + scale: 0.5 368 + }); 369 + this.lightPlaced = true; 370 + } 371 + } else { 372 + this.lightPlaced = false; 322 373 } 323 374 324 375 let playerManager = this.playerManager;
+206 -14
src/enemy.ts
··· 6 6 import Player from './player'; 7 7 import { Weapon } from './weapon'; 8 8 import { Projectile } from './projectile'; 9 + import { Light } from './light'; 9 10 10 11 interface PlayerHit { 11 12 hitPlayer?(player: Player): void; ··· 44 45 eyes?: PIXI.Container; 45 46 46 47 color: number = 0xfb923c; 48 + 49 + type: number = 0; 47 50 48 51 hitPlayer?(player: Player): void; 49 52 ··· 201 204 updateVisuals( 202 205 deltaTime: number, 203 206 nearestPlayer: Player, 207 + nearestLight: Light | undefined, 204 208 dx: number, 205 209 dy: number, 206 210 distance: number ··· 210 214 this.leftEye?.move(angle); 211 215 this.rightEye?.move(angle); 212 216 213 - let alpha = Math.min(1, 1 - distance / (nearestPlayer.viewDistance * 1.25)); 214 - this.leftEye?.update(deltaTime, alpha); 215 - this.rightEye?.update(deltaTime, alpha); 217 + if (nearestLight) { 218 + const light_distance = Math.hypot(nearestLight.x - this.x, nearestLight.y - this.y); 219 + 220 + let alpha = Math.min(1, 1 - light_distance / (nearestLight.scale * 350)); 221 + this.leftEye?.update(deltaTime, alpha); 222 + this.rightEye?.update(deltaTime, alpha); 223 + } else { 224 + this.leftEye?.update(deltaTime, 0); 225 + this.rightEye?.update(deltaTime, 0); 226 + } 216 227 217 228 this.enemyContainer.position.set(this.x, this.y); 218 229 } ··· 223 234 224 235 update(deltaTime: number) { 225 236 let player = this.game.playerManager?.getClosestPlayer(this.position); 237 + let light = this.game.lightManager?.getClosestLight(this.position); 226 238 227 239 if (this.destroyed || !player) return; 228 240 ··· 232 244 233 245 this.move(deltaTime, player, dx, dy, distance); 234 246 235 - this.updateVisuals(deltaTime, player, dx, dy, distance); 247 + this.updateVisuals(deltaTime, player, light, dx, dy, distance); 236 248 237 249 this.attack(deltaTime, player, dx, dy, distance); 238 250 } 239 251 240 252 destroy() { 241 253 if (this.destroyed) return; 254 + 255 + this.game.dropItem({ x: this.x, y: this.y, color: this.color, size: 10, type: this.type }); 256 + 242 257 this.game.spawnParticles(this.x, this.y, 50, this.color); 243 258 244 259 this.destroyed = true; ··· 264 279 265 280 constructor(game: Game) { 266 281 super(game); 282 + 283 + this.type = 1; 267 284 268 285 this.weapon = new Weapon(this.game, { 269 286 color: this.color, ··· 301 318 constructor(game: Game) { 302 319 super(game); 303 320 321 + this.type = 2; 304 322 this.speed = 3000; 305 323 306 324 let position = { x: this.x, y: this.y }; ··· 359 377 updateVisuals( 360 378 deltaTime: number, 361 379 nearestPlayer: Player, 380 + nearestLight: Light | undefined, 362 381 dx: number, 363 382 dy: number, 364 383 distance: number ··· 370 389 this.leftEye?.move(Math.PI / 2); 371 390 this.rightEye?.move(Math.PI / 2); 372 391 373 - // let rotationDelta = this.rotation - (angle - Math.PI / 2); 374 - // this.rigidBody?.applyTorqueImpulse(rotationDelta * 100, true); 392 + if (nearestLight) { 393 + const light_distance = Math.hypot(nearestLight.x - this.x, nearestLight.y - this.y); 375 394 376 - // this.enemyContainer.rotation = angle - Math.PI / 2; 395 + let alpha = Math.min(1, 1 - light_distance / (nearestLight.scale * 350)); 396 + this.leftEye?.update(deltaTime, alpha); 397 + this.rightEye?.update(deltaTime, alpha); 398 + } else { 399 + this.leftEye?.update(deltaTime, 0); 400 + this.rightEye?.update(deltaTime, 0); 401 + } 402 + 377 403 this.rotation = angle - Math.PI / 2; 378 404 379 - let alpha = Math.min(1, 1 - distance / (nearestPlayer.viewDistance * 1.25)); 380 - this.leftEye?.update(deltaTime, alpha); 381 - this.rightEye?.update(deltaTime, alpha); 382 - 383 405 this.enemyContainer.position.set(this.x, this.y); 384 406 } 385 407 ··· 409 431 410 432 constructor(game: Game) { 411 433 super(game); 434 + 435 + this.type = 3; 412 436 413 437 this.weapon = new Weapon(this.game, { 414 438 color: this.color, ··· 471 495 updateVisuals( 472 496 deltaTime: number, 473 497 nearestPlayer: Player, 498 + nearestLight: Light | undefined, 474 499 dx: number, 475 500 dy: number, 476 501 distance: number ··· 484 509 this.leftEye?.move(-this.rotation + angle); 485 510 this.rightEye?.move(-this.rotation + angle); 486 511 487 - let alpha = Math.min(1, 1 - distance / (nearestPlayer.viewDistance * 1.25)); 488 - this.leftEye?.update(deltaTime, alpha); 489 - this.rightEye?.update(deltaTime, alpha); 512 + if (nearestLight) { 513 + const light_distance = Math.hypot(nearestLight.x - this.x, nearestLight.y - this.y); 514 + 515 + let alpha = Math.min(1, 1 - light_distance / (nearestLight.scale * 350)); 516 + this.leftEye?.update(deltaTime, alpha); 517 + this.rightEye?.update(deltaTime, alpha); 518 + } else { 519 + this.leftEye?.update(deltaTime, 0); 520 + this.rightEye?.update(deltaTime, 0); 521 + } 490 522 491 523 this.enemyContainer.position.set(this.x, this.y); 492 524 } ··· 499 531 this.weapon.update(deltaTime); 500 532 } 501 533 } 534 + 535 + export class CrossEnemy extends Enemy { 536 + weapon: Weapon; 537 + 538 + shootAngle: number = 0; 539 + 540 + constructor(game: Game) { 541 + super(game); 542 + 543 + this.type = 4; 544 + 545 + this.weapon = new Weapon(this.game, { 546 + color: this.color, 547 + collisionGroups: 0x00100001, 548 + projectileSpeed: 0.2, 549 + fireRate: 100, 550 + projectileSize: 6, 551 + damage: 10, 552 + lifetime: 8000 553 + }); 554 + 555 + this.speed = 20000; 556 + this.health = 10000; 557 + } 558 + 559 + createEyes() { 560 + this.size = 500; 561 + this.color = 0x0284c7; 562 + this.eyes = new PIXI.Container(); 563 + this.enemyContainer.addChild(this.eyes); 564 + this.eyes.scale.set(5); 565 + 566 + this.leftEye = new Eye(this.eyes, -10, 0, this.color); 567 + this.rightEye = new Eye(this.eyes, 10, 0, this.color); 568 + } 569 + 570 + createShape(): void { 571 + this.size = 600; 572 + this.shape = new PIXI.Graphics().rect(0, 0, this.size, this.size / 8).fill(0); 573 + this.shape.pivot.set(this.size / 2, this.size / 16); 574 + let shape2 = new PIXI.Graphics().rect(0, 0, this.size / 8, this.size).fill(0); 575 + shape2.pivot.set(this.size / 16, this.size / 2); 576 + this.enemyContainer.addChild(this.shape); 577 + this.enemyContainer.addChild(shape2); 578 + } 579 + 580 + createRidigBody(): void { 581 + this.size = 600; 582 + const rigidBodyDesc = RAPIER() 583 + .RigidBodyDesc.dynamic() 584 + .setTranslation(this.x, -this.y) 585 + .lockRotations() 586 + .setLinearDamping(0.5); 587 + this.rigidBody = this.game.world.createRigidBody(rigidBodyDesc); 588 + 589 + const colliderDesc = RAPIER() 590 + .ColliderDesc.cuboid(this.size / 2, this.size / 16) 591 + .setActiveEvents(RAPIER().ActiveEvents.COLLISION_EVENTS) 592 + .setCollisionGroups(0x00020007); 593 + 594 + this.game.world.createCollider(colliderDesc, this.rigidBody); 595 + 596 + const colliderDesc2 = RAPIER() 597 + .ColliderDesc.cuboid(this.size / 16, this.size / 2) 598 + .setActiveEvents(RAPIER().ActiveEvents.COLLISION_EVENTS) 599 + .setCollisionGroups(0x00020007); 600 + 601 + this.game.world.createCollider(colliderDesc2, this.rigidBody); 602 + const colliderDesc3 = RAPIER() 603 + .ColliderDesc.cuboid(this.size / 4, this.size / 32) 604 + .setActiveEvents(RAPIER().ActiveEvents.COLLISION_EVENTS) 605 + .setCollisionGroups(0x00020007) 606 + .setTranslation(0, this.size / 3); 607 + this.game.world.createCollider(colliderDesc3, this.rigidBody); 608 + 609 + const colliderDesc4 = RAPIER() 610 + .ColliderDesc.cuboid(this.size / 4, this.size / 32) 611 + .setActiveEvents(RAPIER().ActiveEvents.COLLISION_EVENTS) 612 + .setCollisionGroups(0x00020007) 613 + .setTranslation(0, -this.size / 3); 614 + this.game.world.createCollider(colliderDesc4, this.rigidBody); 615 + 616 + const colliderDesc5 = RAPIER() 617 + .ColliderDesc.cuboid(this.size / 64, this.size / 8) 618 + .setActiveEvents(RAPIER().ActiveEvents.COLLISION_EVENTS) 619 + .setCollisionGroups(0x00020007) 620 + .setTranslation(this.size / 6, this.size / 3); 621 + 622 + this.game.world.createCollider(colliderDesc5, this.rigidBody); 623 + 624 + const colliderDesc6 = RAPIER() 625 + .ColliderDesc.cuboid(this.size / 64, this.size / 8) 626 + .setActiveEvents(RAPIER().ActiveEvents.COLLISION_EVENTS) 627 + .setCollisionGroups(0x00020007) 628 + .setTranslation(this.size / 6, -this.size / 3); 629 + 630 + this.game.world.createCollider(colliderDesc6, this.rigidBody); 631 + const colliderDesc7 = RAPIER() 632 + .ColliderDesc.cuboid(this.size / 64, this.size / 8) 633 + .setActiveEvents(RAPIER().ActiveEvents.COLLISION_EVENTS) 634 + .setCollisionGroups(0x00020007) 635 + .setTranslation(-this.size / 6, this.size / 3); 636 + 637 + this.game.world.createCollider(colliderDesc7, this.rigidBody); 638 + 639 + const colliderDesc8 = RAPIER() 640 + .ColliderDesc.cuboid(this.size / 64, this.size / 8) 641 + .setActiveEvents(RAPIER().ActiveEvents.COLLISION_EVENTS) 642 + .setCollisionGroups(0x00020007) 643 + .setTranslation(-this.size / 6, -this.size / 3); 644 + 645 + this.game.world.createCollider(colliderDesc8, this.rigidBody); 646 + 647 + this.rigidBody.userData = this; 648 + } 649 + 650 + updateVisuals( 651 + deltaTime: number, 652 + nearestPlayer: Player, 653 + nearestLight: Light | undefined, 654 + dx: number, 655 + dy: number, 656 + distance: number 657 + ): void { 658 + const angle = Math.atan2(dy, dx); 659 + 660 + const rotationSpeed = 0.0001; 661 + this.rotation += rotationSpeed * deltaTime; 662 + 663 + // move eyes 664 + this.leftEye?.move(-this.rotation + angle); 665 + this.rightEye?.move(-this.rotation + angle); 666 + 667 + if (nearestLight) { 668 + const light_distance = Math.hypot(nearestLight.x - this.x, nearestLight.y - this.y); 669 + 670 + let alpha = Math.min(1, 1 - light_distance / (nearestLight.scale * 350)); 671 + this.leftEye?.update(deltaTime, alpha); 672 + this.rightEye?.update(deltaTime, alpha); 673 + } else { 674 + this.leftEye?.update(deltaTime, 0); 675 + this.rightEye?.update(deltaTime, 0); 676 + } 677 + 678 + this.enemyContainer.position.set(this.x, this.y); 679 + } 680 + 681 + attack(deltaTime: number, nearestPlayer: Player, dx: number, dy: number, distance: number): void { 682 + if (this.weapon.cooldown <= 0) { 683 + // get angle 684 + let x = Math.cos(this.shootAngle) + this.position.x; 685 + let y = Math.sin(this.shootAngle) + this.position.y; 686 + this.weapon.fire(this.position, { x, y }); 687 + 688 + this.shootAngle += Math.PI / 10; 689 + } 690 + 691 + this.weapon.update(deltaTime); 692 + } 693 + }
+18
src/helper.ts
··· 1 + export function blendColors(color1: number, color2: number, blend: number): number { 2 + // Extract RGB components 3 + let r1 = (color1 >> 16) & 0xff; 4 + let g1 = (color1 >> 8) & 0xff; 5 + let b1 = color1 & 0xff; 6 + 7 + let r2 = (color2 >> 16) & 0xff; 8 + let g2 = (color2 >> 8) & 0xff; 9 + let b2 = color2 & 0xff; 10 + 11 + // Blend each component 12 + let r = Math.round(r1 + (r2 - r1) * blend); 13 + let g = Math.round(g1 + (g2 - g1) * blend); 14 + let b = Math.round(b1 + (b2 - b1) * blend); 15 + 16 + // Recombine blended components into hexadecimal 17 + return (r << 16) | (g << 8) | b; 18 + }
+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 + }
+106
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.lightManager?.getClosestLight({ 87 + x: this.shape.x, 88 + y: this.shape.y 89 + }); 90 + 91 + if (closestPlayer) { 92 + let dist = Math.hypot(closestPlayer.x - this.shape.x, closestPlayer.y - this.shape.y); 93 + this.shape.alpha = 1 - dist / (closestPlayer.scale * 300); 94 + } 95 + 96 + this.shape.alpha *= 1 - this.life / this.lifetime; 97 + } 98 + 99 + destroy() { 100 + if (this.destroyed) return; 101 + 102 + this.destroyed = true; 103 + this.shape.destroy(); 104 + this.game.world.removeRigidBody(this.rigidBody); 105 + } 106 + }
+49
src/light-manager.ts
··· 1 + import Game from './app'; 2 + import { Light, LightOptions } from './light'; 3 + 4 + export class LightManager { 5 + lights: Light[] = []; 6 + game: Game; 7 + 8 + constructor(game: Game) { 9 + this.game = game; 10 + } 11 + 12 + addLight(options: LightOptions) { 13 + const light = new Light(this.game, options); 14 + this.lights.push(light); 15 + return light; 16 + } 17 + 18 + update(deltaTime: number) { 19 + for (let light of this.lights) { 20 + light.update(deltaTime); 21 + } 22 + 23 + this.lights = this.lights.filter((item) => !item.destroyed); 24 + } 25 + 26 + getClosestLight( 27 + position: { x: number; y: number }, 28 + maxDist: number | undefined = undefined 29 + ): Light | undefined { 30 + let closestLight: Light | undefined; 31 + let closestDistance = Infinity; 32 + 33 + for (let light of this.lights) { 34 + if (light.destroyed) continue; 35 + console.log(light); 36 + const distance = Math.hypot(light.x - position.x, light.y - position.y); 37 + if (distance < closestDistance) { 38 + closestDistance = distance; 39 + closestLight = light; 40 + } 41 + } 42 + 43 + if (!maxDist || closestDistance < maxDist) { 44 + return closestLight; 45 + } 46 + 47 + return undefined; 48 + } 49 + }
+172
src/light.ts
··· 1 + import * as PIXI from 'pixi.js'; 2 + import { RAPIER } from './rapier'; 3 + import Game from './app'; 4 + 5 + export type LightOptions = { 6 + x?: number; 7 + y?: number; 8 + scale?: number; 9 + color?: number; 10 + collisionGroups?: number; 11 + lifetime?: number; 12 + 13 + alpha?: number; 14 + }; 15 + 16 + export class Light { 17 + game: Game; 18 + lightContainer: PIXI.Container; 19 + shadow: PIXI.Graphics; 20 + light?: PIXI.Sprite; 21 + 22 + _scale: number = 1; 23 + 24 + color: number = 0xffffff; 25 + 26 + collisionGroups: number = 0x000a000a; 27 + 28 + fade: boolean = false; 29 + lifetime: number = 10000; 30 + 31 + destroyed: boolean = false; 32 + 33 + _alpha: number = 1; 34 + 35 + flicker: boolean = true; 36 + 37 + constructor(game: Game, options: LightOptions) { 38 + this.game = game; 39 + 40 + this.lightContainer = new PIXI.Container(); 41 + this.shadow = new PIXI.Graphics(); 42 + 43 + this.lightContainer.addChild(this.shadow); 44 + 45 + if (options.x) this.x = options.x; 46 + if (options.y) this.y = options.y; 47 + if (options.scale) this.scale = options.scale; 48 + if (options.color) this.color = options.color; 49 + if (options.collisionGroups) this.collisionGroups = options.collisionGroups; 50 + if (options.lifetime) { 51 + this.fade = true; 52 + this.lifetime = options.lifetime; 53 + } 54 + if (options.alpha) this.alpha = options.alpha; 55 + 56 + this.createLight(); 57 + 58 + this.game.container.addChild(this.lightContainer); 59 + } 60 + 61 + get x() { 62 + return this.lightContainer.x; 63 + } 64 + set x(value) { 65 + this.lightContainer.x = value; 66 + } 67 + 68 + get y() { 69 + return this.lightContainer.y; 70 + } 71 + set y(value) { 72 + this.lightContainer.y = value; 73 + } 74 + 75 + get scale() { 76 + return this._scale; 77 + } 78 + 79 + set scale(value) { 80 + this._scale = value; 81 + 82 + if (this.light) { 83 + this.light.scale.set(value); 84 + } 85 + } 86 + 87 + get alpha() { 88 + return this._alpha; 89 + } 90 + 91 + set alpha(value) { 92 + this._alpha = value; 93 + 94 + if (this.light) { 95 + this.light.alpha = value; 96 + } 97 + } 98 + 99 + async createLight() { 100 + const texture = await PIXI.Assets.load('./light.png'); 101 + this.light = PIXI.Sprite.from(texture); 102 + this.light.tint = this.color; 103 + this.light.anchor.set(0.5); 104 + this.light.scale.set(this._scale); 105 + this.light.zIndex = -1; 106 + this.light.alpha = this._alpha; 107 + 108 + this.lightContainer.addChild(this.light); 109 + 110 + this.light.mask = this.shadow; 111 + } 112 + 113 + drawShadow() { 114 + // ray cast around the player to create a shadow 115 + this.shadow.clear(); 116 + 117 + const rays = 360; 118 + 119 + const angleStep = (Math.PI * 2) / rays; 120 + const rayLength = 100000; 121 + 122 + let firstPoint; 123 + 124 + for (let i = 0; i < rays; i++) { 125 + const angle = i * angleStep; 126 + 127 + const ray = new (RAPIER().Ray)( 128 + new (RAPIER().Vector2)(this.x, -this.y), 129 + new (RAPIER().Vector2)(Math.cos(angle), Math.sin(angle)) 130 + ); 131 + 132 + const hit = this.game.world.castRay(ray, rayLength, true, undefined, this.collisionGroups); 133 + 134 + let hitPoint = hit 135 + ? ray.pointAt(hit.toi) 136 + : { x: ray.dir.x * rayLength, y: ray.dir.y * rayLength }; 137 + 138 + let x = hitPoint.x - this.x; 139 + let y = -hitPoint.y - this.y; 140 + 141 + if (i === 0) { 142 + this.shadow.moveTo(x, y); 143 + firstPoint = { x, y }; 144 + } else { 145 + this.shadow.lineTo(x, y); 146 + } 147 + 148 + if (i === rays - 1 && firstPoint) { 149 + this.shadow.lineTo(firstPoint.x, firstPoint.y); 150 + } 151 + } 152 + this.shadow.fill(0); 153 + } 154 + 155 + update(deltaTime: number) { 156 + if (this.destroyed) return; 157 + 158 + this.drawShadow(); 159 + 160 + if (this.flicker && this.light) { 161 + this.light.alpha = this._alpha + Math.random() * 0.01; 162 + this.light.scale.set(this._scale + Math.random() * 0.05); 163 + } 164 + } 165 + 166 + destroy() { 167 + if (this.destroyed) return; 168 + 169 + this.destroyed = true; 170 + this.lightContainer.destroy(); 171 + } 172 + }
+22 -3
src/obstacle-manager.ts
··· 3 3 import Obstacle from './obstacles'; 4 4 5 5 import Alea from 'alea'; 6 + import { Light } from './light'; 6 7 7 8 const CELL_SIZE = 900; 8 9 const OBSTACLE_COUNT_PER_CELL = 30; 10 + const LIGHT_COUNT_PER_CELL = 2; 9 11 10 12 type CellCoord = `${number},${number}`; 11 13 12 14 export class ObstacleManager { 13 15 container: PIXI.Container; 14 - obstacles: Map<CellCoord, Obstacle[]> = new Map(); 16 + obstacles: Map<CellCoord, (Obstacle | Light)[]> = new Map(); 15 17 game: Game; 16 18 currentCells: Set<CellCoord> = new Set(); 17 19 debugGraphics: Map<CellCoord, PIXI.Graphics> = new Map(); 18 20 19 21 lastActiveCell: CellCoord | null = null; 20 22 23 + seed: string; 24 + 21 25 constructor(game: Game) { 22 26 this.game = game; 23 27 this.container = new PIXI.Container(); 24 28 this.container.zIndex = 5; 25 29 this.game.container.addChild(this.container); 30 + 31 + this.seed = Math.random().toFixed(5); 26 32 } 27 33 28 34 update(deltaTime: number) { ··· 77 83 78 84 private createCell(cellCoord: CellCoord) { 79 85 const [cellX, cellY] = cellCoord.split(',').map(Number); 80 - const obstacles: Obstacle[] = []; 86 + const obstacles: (Obstacle | Light)[] = []; 81 87 82 88 // @ts-ignore 83 - let rng = new Alea(cellCoord); 89 + let rng = new Alea(cellCoord + this.seed); 84 90 85 91 for (let i = 0; i < OBSTACLE_COUNT_PER_CELL; i++) { 86 92 const x = cellX * CELL_SIZE + rng() * CELL_SIZE; ··· 90 96 91 97 const obstacle = new Obstacle(this.game, this.container, x, y, width, height); 92 98 obstacles.push(obstacle); 99 + } 100 + for (let i = 0; i < LIGHT_COUNT_PER_CELL; i++) { 101 + const x = cellX * CELL_SIZE + rng() * CELL_SIZE; 102 + const y = cellY * CELL_SIZE + rng() * CELL_SIZE; 103 + 104 + const light = this.game.lightManager.addLight({ 105 + x, 106 + y, 107 + color: 0xffffff * rng(), 108 + alpha: 0.05 + rng() * 0.15, 109 + scale: 0.5 + rng() * 1 110 + }); 111 + obstacles.push(light); 93 112 } 94 113 95 114 this.obstacles.set(cellCoord, obstacles);
+8 -2
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; 84 90 85 91 for (let player of this.players) { 86 - const dist = (player.x - position.x) ** 2 + (player.y - position.y) ** 2; 92 + const dist = Math.hypot(player.x - position.x, player.y - position.y); 87 93 88 94 if (dist < closestDist && !player.dead) { 89 95 closestDist = dist; ··· 91 97 } 92 98 } 93 99 94 - if (!maxDist || closestDist < maxDist ** 2) { 100 + if (!maxDist || closestDist < maxDist) { 95 101 return closestPlayer; 96 102 } 97 103
+11 -65
src/player.ts
··· 5 5 import { RAPIER } from './rapier'; 6 6 import { type RigidBody } from '@dimforge/rapier2d'; 7 7 import Eye from './eye.js'; 8 + import { Light } from './light.js'; 9 + import { blendColors } from './helper.js'; 8 10 9 11 /** 10 12 * Player class ··· 22 24 23 25 playerContainer: PIXI.Container; 24 26 25 - shadow: PIXI.Graphics; 26 - 27 27 healthBar?: PIXI.Graphics; 28 28 29 29 speed: number; ··· 38 38 39 39 color: number; 40 40 41 - light?: PIXI.Sprite; 41 + light: Light; 42 42 43 43 leftEye: Eye; 44 44 rightEye: Eye; ··· 52 52 dead: boolean = false; 53 53 54 54 respawnTime: number = 0; 55 + 56 + items: number[] = [0, 0, 0, 0]; 55 57 56 58 constructor(game: Game, num: number) { 57 59 this.game = game; ··· 67 69 this.playerContainer = new PIXI.Container(); 68 70 game.container.addChild(this.playerContainer); 69 71 70 - this.shadow = new PIXI.Graphics(); 71 - this.playerContainer.addChild(this.shadow); 72 - 73 - this.createLight(); 72 + this.light = this.game.lightManager.addLight({ color: blendColors(this.color, 0xffffff, 0.3) }); 74 73 75 74 const shape = new PIXI.Graphics().rect(0, 0, this.size, this.size).fill(this.color); 76 75 shape.pivot.set(this.size / 2, this.size / 2); ··· 94 93 this.y = 0; 95 94 } 96 95 97 - async createLight() { 98 - const texture = await PIXI.Assets.load('./light.png'); 99 - this.light = PIXI.Sprite.from(texture); 100 - this.light.tint = 0xfda4af; 101 - this.light.anchor.set(0.5); 102 - this.light.scale.set((0.5 * this.viewDistance) / 200); 103 - this.light.zIndex = -1; 104 - 105 - this.playerContainer.addChild(this.light); 106 - 107 - this.light.mask = this.shadow; 108 - } 109 - 110 96 createHealthBar() { 111 97 const healthBarWidth = this.size; 112 98 const healthBarHeight = 5; ··· 130 116 131 117 const colliderDesc = RAPIER() 132 118 .ColliderDesc.cuboid(this.size / 2, this.size / 2) 133 - .setCollisionGroups(0x00010013); 119 + .setCollisionGroups(0x00010033); 134 120 this.game.world.createCollider(colliderDesc, this.rigidBody); 135 121 136 122 // Attach this Player instance to the rigid body's user data ··· 241 227 this.rightEye.move(angle); 242 228 } 243 229 244 - this.drawShadow(); 230 + this.light.x = this.x; 231 + this.light.y = this.y; 245 232 246 233 this.weapon.update(deltaTime); 247 234 248 235 this.playerContainer.position.set(this.x, this.y); 249 236 } 250 237 251 - drawShadow() { 252 - // ray cast around the player to create a shadow 253 - this.shadow.clear(); 254 - 255 - const rays = 1080 / 3; 256 - 257 - const angleStep = (Math.PI * 2) / rays; 258 - const rayLength = 100000; 259 - 260 - let firstPoint; 261 - 262 - for (let i = 0; i < rays; i++) { 263 - const angle = i * angleStep; 264 - 265 - const ray = new (RAPIER().Ray)( 266 - new (RAPIER().Vector2)(this.x, -this.y), 267 - new (RAPIER().Vector2)(Math.cos(angle), Math.sin(angle)) 268 - ); 269 - 270 - const hit = this.game.world.castRay(ray, rayLength, false, undefined, 0x000a000a); 271 - 272 - let hitPoint = hit 273 - ? ray.pointAt(hit.toi) 274 - : { x: ray.dir.x * rayLength, y: ray.dir.y * rayLength }; 275 - 276 - let x = hitPoint.x - this.x; 277 - let y = -hitPoint.y - this.y; 278 - 279 - if (i === 0) { 280 - this.shadow.moveTo(x, y); 281 - firstPoint = { x, y }; 282 - } else { 283 - this.shadow.lineTo(x, y); 284 - } 285 - 286 - if (i === rays - 1 && firstPoint) { 287 - this.shadow.lineTo(firstPoint.x, firstPoint.y); 288 - } 289 - } 290 - this.shadow.fill(0); 291 - } 292 - 293 238 takeDamage(amount: number) { 294 239 this.health -= amount; 295 240 if (this.health < 0) { ··· 304 249 305 250 destroy() { 306 251 if (this.rigidBody) this.game.world.removeRigidBody(this.rigidBody); 307 - this.game.container.removeChild(this.playerContainer); 252 + this.playerContainer.destroy(); 253 + this.light.destroy(); 308 254 } 309 255 }
+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: [