[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 24, 2024, 9:16 AM +0200) 3ef367d6 e6cd3b15

+383 -178
+6
package-lock.json
··· 10 10 "dependencies": { 11 11 "@dimforge/rapier2d": "^0.11.2", 12 12 "@pixi/sound": "^6.0.0", 13 + "alea": "^1.0.1", 13 14 "pixi-filters": "^6.0.4", 14 15 "pixi.js": "^8.2.5", 15 16 "prettier": "^3.3.3", ··· 667 668 "engines": { 668 669 "node": ">=0.4.0" 669 670 } 671 + }, 672 + "node_modules/alea": { 673 + "version": "1.0.1", 674 + "resolved": "https://registry.npmjs.org/alea/-/alea-1.0.1.tgz", 675 + "integrity": "sha512-QU+wv+ziDXaMxRdsQg/aH7sVfWdhKps5YP97IIwFkHCsbDZA3k87JXoZ5/iuemf4ntytzIWeScrRpae8+lDrXA==" 670 676 }, 671 677 "node_modules/ansi-regex": { 672 678 "version": "6.0.1",
+1
package.json
··· 11 11 "dependencies": { 12 12 "@dimforge/rapier2d": "^0.11.2", 13 13 "@pixi/sound": "^6.0.0", 14 + "alea": "^1.0.1", 14 15 "pixi-filters": "^6.0.4", 15 16 "pixi.js": "^8.2.5", 16 17 "prettier": "^3.3.3",
+37 -10
src/app.ts
··· 34 34 35 35 particleSystem?: ParticleSystem; 36 36 37 + obstacleManager: ObstacleManager; 38 + 37 39 debug: boolean = false; 40 + 41 + showStats: boolean = false; 38 42 39 43 stats?: Stats; 40 44 ··· 42 46 43 47 controls: Controls; 44 48 49 + scale: number = 1; 50 + 51 + invincible = false; 52 + 45 53 constructor() { 46 54 this.setup(); 47 55 ··· 49 57 this.container = new PIXI.Container(); 50 58 51 59 this.controls = new Controls(this); 60 + 61 + this.obstacleManager = new ObstacleManager(this); 52 62 53 63 sound.add('music-intro', { 54 64 url: '/shadow-shmup/music-intro.mp3' ··· 106 116 app.stage.addChild(this.mainContainer); 107 117 this.mainContainer.addChild(this.container); 108 118 109 - this.container.position.set(window.innerWidth / 2, window.innerHeight / 2); 119 + this.mainContainer.position.set(window.innerWidth / 2, window.innerHeight / 2); 110 120 111 121 // add a resize event listener 112 122 window.addEventListener('resize', () => { 113 123 app.renderer.resize(window.innerWidth, window.innerHeight); 114 124 115 - this.container.position.set(window.innerWidth / 2, window.innerHeight / 2); 125 + this.mainContainer.position.set(window.innerWidth / 2, window.innerHeight / 2); 116 126 }); 127 + 128 + this.container.scale.set(this.scale); 117 129 118 130 app.ticker.add((ticker) => { 119 131 if (this.stats) this.stats.begin(); ··· 135 147 // move the container so that the player is always in the center 136 148 let position = this.playerManager?.getCenter(); 137 149 if (position) { 138 - position.x = -position.x + window.innerWidth / 2; 139 - position.y = -position.y + window.innerHeight / 2; 150 + position.x = -position.x; 151 + position.y = -position.y; 140 152 141 - this.container.x = this.container.x * 0.98 + position.x * 0.02; 142 - this.container.y = this.container.y * 0.98 + position.y * 0.02; 153 + this.container.x = this.container.x * 0.98 + position.x * 0.02 * this.scale; 154 + this.container.y = this.container.y * 0.98 + position.y * 0.02 * this.scale; 143 155 } 144 156 145 157 if (this.stats) this.stats.end(); ··· 153 165 window.addEventListener('keydown', this.handleKeyDown.bind(this)); 154 166 window.addEventListener('keyup', this.handleKeyUp.bind(this)); 155 167 156 - if (this.debug) { 168 + if (this.debug || this.showStats) { 157 169 this.stats = new Stats(); 158 170 document.body.appendChild(this.stats.dom); 159 171 } 160 - 161 - let manager = new ObstacleManager(this); 162 172 163 173 const ui = document.getElementById('ui'); 164 174 ··· 266 276 this.playerManager?.update(deltaTime, this.keys); 267 277 if (!this.playing) return; 268 278 279 + this.obstacleManager.update(deltaTime); 280 + 269 281 this.enemyManager?.update(deltaTime); 270 282 271 - if (Math.random() < deltaTime * 0.005) { 283 + if (this.invincible) { 284 + this.playerManager!.players[0].health = 100; 285 + } 286 + 287 + let playerManager = this.playerManager; 288 + 289 + if (playerManager) { 290 + let timeSinceLastDamage = playerManager.smallestTimeSinceLastDamage(); 291 + if (timeSinceLastDamage < 150) { 292 + this.container.alpha = playerManager.players[0].timeSinceLastDamage / 300; 293 + } else { 294 + this.container.alpha = 1; 295 + } 296 + } 297 + 298 + if (Math.random() < deltaTime * 0.004) { 272 299 this.enemyManager?.addEnemy(); 273 300 } 274 301
+2 -2
src/enemy-manager.ts
··· 1 1 import Game from './app.js'; 2 - import Enemy, { PentagonEnemy, TriangleEnemy } from './enemy.js'; 2 + import Enemy, { PentagonEnemy, SphereEnemy, TriangleEnemy } from './enemy.js'; 3 3 4 4 export default class EnemyManager { 5 5 game: Game; ··· 15 15 } 16 16 17 17 addEnemy() { 18 - let enemyTypes = [Enemy, TriangleEnemy, PentagonEnemy]; 18 + let enemyTypes = [SphereEnemy, TriangleEnemy, PentagonEnemy]; 19 19 const randomIndex = Math.floor(Math.random() * enemyTypes.length); 20 20 21 21 const enemy = new enemyTypes[randomIndex](this.game);
+130 -107
src/enemy.ts
··· 5 5 import Eye from './eye'; 6 6 import Player from './player'; 7 7 import { Weapon } from './weapon'; 8 + import { Projectile } from './projectile'; 8 9 9 10 interface PlayerHit { 10 11 hitPlayer?(player: Player): void; ··· 24 25 25 26 damage: number = 10; 26 27 27 - speed: number = 0.05; 28 + speed: number = 500; 28 29 29 30 shape?: PIXI.Graphics; 30 31 ··· 180 181 this.rigidBody?.applyImpulse({ x, y }, true); 181 182 } 182 183 183 - update(deltaTime: number) { 184 - let player = this.game.playerManager?.getClosestPlayer(this.position); 184 + 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; 185 187 186 - if (this.destroyed || !player) return; 188 + this.rigidBody?.applyImpulse({ x: x, y: -y }, true); 189 + } 187 190 188 - // move the player, wasd 189 - let dx = player.x - this.x; 190 - let dy = player.y - this.y; 191 - 192 - const distance = Math.sqrt(dx * dx + dy * dy); 193 - 194 - // get angle between enemy and player 191 + updateVisuals( 192 + deltaTime: number, 193 + nearestPlayer: Player, 194 + dx: number, 195 + dy: number, 196 + distance: number 197 + ) { 195 198 const angle = Math.atan2(dy, dx); 196 - // move eyes 199 + 197 200 this.leftEye?.move(angle); 198 201 this.rightEye?.move(angle); 199 202 200 - let alpha = Math.min(1, 1 - distance / 300); 201 - 203 + let alpha = Math.min(1, 1 - distance / (nearestPlayer.viewDistance * 1.25)); 202 204 this.leftEye?.update(deltaTime, alpha); 203 205 this.rightEye?.update(deltaTime, alpha); 204 206 205 - // Only move if not too close to the player and not exploding 206 - if (distance > this.size + 10 && !this.exploding) { 207 - // lets add some force instead of moving it directly, the further away the player, the more force 208 - const force = 500; 207 + this.enemyContainer.position.set(this.x, this.y); 208 + } 209 209 210 - const x = dx / distance; 211 - const y = dy / distance; 210 + attack(deltaTime: number, nearestPlayer: Player, dx: number, dy: number, distance: number) { 211 + console.log('attack'); 212 + } 212 213 213 - this.rigidBody?.applyImpulse({ x: x * force, y: -y * force }, true); 214 - } else if (!this.exploding) { 215 - this.exploding = true; 216 - this.destroyTime = 2000; 217 - } else { 218 - this.destroyTime -= deltaTime; 219 - if (this.destroyTime <= 0) { 220 - this.destroy(); 214 + update(deltaTime: number) { 215 + let player = this.game.playerManager?.getClosestPlayer(this.position); 221 216 222 - if (distance < this.size + 20) { 223 - player.takeDamage(20); 224 - } 225 - return; 226 - } 227 - } 217 + if (this.destroyed || !player) return; 228 218 229 - this.enemyContainer.position.set(this.x, this.y); 219 + const dx = player.x - this.x; 220 + const dy = player.y - this.y; 221 + const distance = Math.sqrt(dx * dx + dy * dy); 222 + 223 + this.move(deltaTime, player, dx, dy, distance); 224 + 225 + this.updateVisuals(deltaTime, player, dx, dy, distance); 226 + 227 + this.attack(deltaTime, player, dx, dy, distance); 230 228 } 231 229 232 230 destroy() { ··· 251 249 } 252 250 } 253 251 252 + export class SphereEnemy extends Enemy { 253 + weapon: Weapon; 254 + 255 + constructor(game: Game) { 256 + super(game); 257 + 258 + this.weapon = new Weapon(this.game, { 259 + color: this.color, 260 + collisionGroups: 0x00100001, 261 + projectileSpeed: 0.3, 262 + fireRate: 1000, 263 + projectileSize: 4, 264 + lifetime: 300, 265 + damage: 5, 266 + showParticles: true 267 + }); 268 + 269 + this.speed = 500; 270 + } 271 + 272 + attack(deltaTime: number, nearestPlayer: Player, dx: number, dy: number, distance: number): void { 273 + if (distance < 100 && this.weapon.cooldown < 0) { 274 + for (let i = 0; i < 20; i++) { 275 + this.weapon.cooldown = -1; 276 + // get angle 277 + let angle = (Math.PI / 10) * i; 278 + let x = Math.cos(angle) + this.position.x; 279 + let y = Math.sin(angle) + this.position.y; 280 + this.weapon.fire(this.position, { x, y }); 281 + } 282 + } 283 + 284 + this.weapon.update(deltaTime); 285 + } 286 + } 287 + 254 288 export class TriangleEnemy extends Enemy { 289 + weapon: Weapon; 290 + 291 + projectile: Projectile; 292 + 293 + constructor(game: Game) { 294 + super(game); 295 + 296 + this.speed = 600; 297 + 298 + this.weapon = new Weapon(this.game, { 299 + color: this.color, 300 + collisionGroups: 0x00100001, 301 + projectileSpeed: 0.0, 302 + fireRate: 30, 303 + projectileSize: 4, 304 + damage: 10, 305 + lifetime: 30, 306 + sound: false 307 + }); 308 + } 309 + 255 310 createEyes() { 256 311 this.color = 0x38bdf8; 257 312 super.createEyes(); ··· 287 342 this.rigidBody.userData = this; 288 343 } 289 344 290 - update(deltaTime: number) { 291 - let player = this.game.playerManager?.getClosestPlayer(this.position); 292 - 293 - if (this.destroyed || !player) return; 345 + updateVisuals( 346 + deltaTime: number, 347 + nearestPlayer: Player, 348 + dx: number, 349 + dy: number, 350 + distance: number 351 + ) { 352 + // move eyes 294 353 295 - // move the player, wasd 296 - let dx = player.x - this.x; 297 - let dy = player.y - this.y; 354 + const angle = Math.atan2(dy, dx); 298 355 299 - const distance = Math.sqrt(dx * dx + dy * dy); 356 + this.leftEye?.move(Math.PI / 2); 357 + this.rightEye?.move(Math.PI / 2); 300 358 301 - // get angle between enemy and player 302 - const angle = Math.atan2(dy, dx); 359 + // let rotationDelta = this.rotation - (angle - Math.PI / 2); 360 + // this.rigidBody?.applyTorqueImpulse(rotationDelta * 100, true); 303 361 304 - // apply rotation 305 - //let rotationDelta = this.rotation - (angle - Math.PI / 2); 306 - //this.rigidBody?.applyTorqueImpulse(rotationDelta * 1000000, true); 307 - 362 + // this.enemyContainer.rotation = angle - Math.PI / 2; 308 363 this.rotation = angle - Math.PI / 2; 309 364 310 - // move eyes 311 - this.leftEye?.move(Math.PI / 2); 312 - this.rightEye?.move(Math.PI / 2); 313 - 314 - let alpha = Math.min(1, 1 - distance / 300); 365 + let alpha = Math.min(1, 1 - distance / (nearestPlayer.viewDistance * 1.25)); 315 366 this.leftEye?.update(deltaTime, alpha); 316 367 this.rightEye?.update(deltaTime, alpha); 317 368 318 - const force = 600; 319 - 320 - const x = dx / distance; 321 - const y = dy / distance; 322 - 323 - this.rigidBody?.applyImpulse({ x: x * force, y: -y * force }, true); 324 - 325 369 this.enemyContainer.position.set(this.x, this.y); 326 - //this.enemyContainer.rotation = this.rotation; 327 - 328 - if (this.exploding) { 329 - this.destroy(); 330 - 331 - if (distance < this.size + 20) { 332 - player.takeDamage(10); 333 - } 334 - } 335 - 336 - if (distance < 50) { 337 - if (this.destroyTime < 0) this.destroyTime = 1000; 338 - else this.destroyTime -= deltaTime; 370 + } 339 371 340 - if (this.destroyTime < 0) this.exploding = true; 341 - } else { 342 - this.destroyTime = -1; 372 + attack(deltaTime: number, nearestPlayer: Player, dx: number, dy: number, distance: number): void { 373 + if (this.destroyed) return; 374 + if (distance < nearestPlayer.viewDistance) { 375 + const x = Math.cos(this.rotation + Math.PI / 2 - 0.0); 376 + const y = Math.sin(this.rotation + Math.PI / 2 - 0.0); 377 + const startPosition = { x: this.x + x * 20 - 2, y: this.y + y * 20 - 2 }; 378 + this.weapon.fire(startPosition, startPosition); 343 379 } 344 - } 345 380 346 - hitPlayer(player: Player) { 347 - // explode on next update() 348 - this.exploding = true; 381 + this.weapon.update(deltaTime); 349 382 } 350 383 } 351 384 ··· 360 393 collisionGroups: 0x00100001, 361 394 projectileSpeed: 0.2, 362 395 fireRate: 2000, 363 - projectileSize: 6 396 + projectileSize: 6, 397 + damage: 10 364 398 }); 399 + 400 + this.speed = 1000; 365 401 } 366 402 367 403 createEyes() { ··· 409 445 this.rigidBody.userData = this; 410 446 } 411 447 412 - update(deltaTime: number) { 413 - let player = this.game.playerManager?.getClosestPlayer(this.position); 414 - 415 - if (this.destroyed || !player) return; 416 - 417 - // move the player, wasd 418 - let dx = player.x - this.x; 419 - let dy = player.y - this.y; 420 - 421 - const distance = Math.sqrt(dx * dx + dy * dy); 422 - 423 - // get angle between enemy and player 448 + updateVisuals( 449 + deltaTime: number, 450 + nearestPlayer: Player, 451 + dx: number, 452 + dy: number, 453 + distance: number 454 + ): void { 424 455 const angle = Math.atan2(dy, dx); 425 456 426 457 const rotationSpeed = 0.001; ··· 430 461 this.leftEye?.move(-this.rotation + angle); 431 462 this.rightEye?.move(-this.rotation + angle); 432 463 433 - let alpha = Math.min(1, 1 - distance / 300); 464 + let alpha = Math.min(1, 1 - distance / (nearestPlayer.viewDistance * 1.25)); 434 465 this.leftEye?.update(deltaTime, alpha); 435 466 this.rightEye?.update(deltaTime, alpha); 436 467 437 - // Only move if not too close to the player and not exploding 438 - // lets add some force instead of moving it directly, the further away the player, the more force 439 - const force = 1000; 468 + this.enemyContainer.position.set(this.x, this.y); 469 + } 440 470 441 - const x = dx / distance; 442 - const y = dy / distance; 443 - 444 - this.rigidBody?.applyImpulse({ x: x * force, y: -y * force }, true); 445 - 446 - // if player is close enough, fire weapon 471 + attack(deltaTime: number, nearestPlayer: Player, dx: number, dy: number, distance: number): void { 447 472 if (distance < 400) { 448 - this.weapon.fire(this.position, player.position); 473 + this.weapon.fire(this.position, nearestPlayer.position); 449 474 } 450 475 451 476 this.weapon.update(deltaTime); 452 - 453 - this.enemyContainer.position.set(this.x, this.y); 454 477 } 455 478 }
+117 -16
src/obstacle-manager.ts
··· 2 2 import * as PIXI from 'pixi.js'; 3 3 import Obstacle from './obstacles'; 4 4 5 + import Alea from 'alea'; 6 + 7 + const CELL_SIZE = 900; 8 + const OBSTACLE_COUNT_PER_CELL = 30; 9 + 10 + type CellCoord = `${number},${number}`; 11 + 5 12 export class ObstacleManager { 6 13 container: PIXI.Container; 14 + obstacles: Map<CellCoord, Obstacle[]> = new Map(); 15 + game: Game; 16 + currentCells: Set<CellCoord> = new Set(); 17 + debugGraphics: Map<CellCoord, PIXI.Graphics> = new Map(); 7 18 8 - obstacles: Obstacle[] = []; 19 + lastActiveCell: CellCoord | null = null; 9 20 10 - game: Game; 11 21 constructor(game: Game) { 12 22 this.game = game; 13 - 14 23 this.container = new PIXI.Container(); 15 24 this.container.zIndex = 5; 25 + this.game.container.addChild(this.container); 26 + } 16 27 17 - this.game.container.addChild(this.container); 28 + update(deltaTime: number) { 29 + const position = this.game.playerManager?.getCenter(); 30 + if (!position) return; 31 + 32 + const currentCell = this.getCellCoord(position.x, position.y); 33 + 34 + console.log(currentCell); 35 + if (this.lastActiveCell === currentCell) return; 36 + 37 + this.lastActiveCell = currentCell; 38 + const cellsToRender = this.getSurroundingCells(currentCell); 39 + 40 + // Remove obstacles and debug graphics from cells that are no longer visible 41 + this.currentCells.forEach((cell) => { 42 + if (!cellsToRender.has(cell)) { 43 + this.removeCell(cell); 44 + } 45 + }); 46 + 47 + // Add new cells that have come into view 48 + cellsToRender.forEach((cell) => { 49 + if (!this.currentCells.has(cell)) { 50 + this.createCell(cell); 51 + } 52 + }); 53 + 54 + this.currentCells = cellsToRender; 55 + 56 + // Update debug visualization 57 + if (this.game.debug) { 58 + this.updateDebugVisualization(); 59 + } 60 + } 61 + 62 + private getCellCoord(x: number, y: number): CellCoord { 63 + const cellX = Math.floor(x / CELL_SIZE); 64 + const cellY = Math.floor(y / CELL_SIZE); 65 + return `${cellX},${cellY}`; 66 + } 18 67 19 - this.createObstacles(); 68 + private getSurroundingCells(centerCell: CellCoord): Set<CellCoord> { 69 + const [centerX, centerY] = centerCell.split(',').map(Number); 70 + const cells = new Set<CellCoord>(); 71 + for (let dx = -1; dx <= 1; dx++) { 72 + for (let dy = -1; dy <= 1; dy++) { 73 + cells.add(`${centerX + dx},${centerY + dy}`); 74 + } 75 + } 76 + return cells; 20 77 } 21 78 22 - createObstacles() { 23 - for (let i = 0; i < 50; i++) { 24 - const x = (Math.random() - 0.5) * 1400; 25 - const y = (Math.random() - 0.5) * 1400; 26 - const width = 60; 27 - const height = 60; 79 + private createCell(cellCoord: CellCoord) { 80 + const [cellX, cellY] = cellCoord.split(',').map(Number); 81 + const obstacles: Obstacle[] = []; 82 + 83 + // @ts-ignore 84 + let rng = new Alea(cellCoord); 85 + 86 + for (let i = 0; i < OBSTACLE_COUNT_PER_CELL; i++) { 87 + const x = cellX * CELL_SIZE + rng() * CELL_SIZE; 88 + const y = cellY * CELL_SIZE + rng() * CELL_SIZE; 89 + const width = rng() * 90 + 10; 90 + const height = rng() * 90 + 10; 28 91 29 92 const obstacle = new Obstacle(this.game, this.container, x, y, width, height); 30 - this.obstacles.push(obstacle); 93 + obstacles.push(obstacle); 94 + } 95 + 96 + this.obstacles.set(cellCoord, obstacles); 97 + 98 + if (this.game.debug) { 99 + this.createDebugGraphicsForCell(cellCoord); 31 100 } 32 101 } 33 102 34 - update(deltaTime: number) { 35 - const position = this.game.playerManager?.getCenter(); 103 + private removeCell(cellCoord: CellCoord) { 104 + const obstacles = this.obstacles.get(cellCoord); 105 + if (obstacles) { 106 + obstacles.forEach((obstacle) => obstacle.destroy()); 107 + this.obstacles.delete(cellCoord); 108 + } 109 + 110 + const debugGraphics = this.debugGraphics.get(cellCoord); 111 + if (debugGraphics) { 112 + this.container.removeChild(debugGraphics); 113 + this.debugGraphics.delete(cellCoord); 114 + } 115 + } 116 + 117 + private createDebugGraphicsForCell(cellCoord: CellCoord) { 118 + const [cellX, cellY] = cellCoord.split(',').map(Number); 119 + const graphics = new PIXI.Graphics(); 120 + graphics.rect(cellX * CELL_SIZE, cellY * CELL_SIZE, CELL_SIZE, CELL_SIZE); 121 + graphics.stroke({ width: 3, color: 0xff0000 }); 122 + this.container.addChild(graphics); 123 + this.debugGraphics.set(cellCoord, graphics); 124 + } 36 125 37 - if (!position) return; 126 + private updateDebugVisualization() { 127 + this.debugGraphics.forEach((graphics, cellCoord) => { 128 + if (!this.currentCells.has(cellCoord)) { 129 + this.container.removeChild(graphics); 130 + this.debugGraphics.delete(cellCoord); 131 + } 132 + }); 133 + 134 + this.currentCells.forEach((cellCoord) => { 135 + if (!this.debugGraphics.has(cellCoord)) { 136 + this.createDebugGraphicsForCell(cellCoord); 137 + } 138 + }); 38 139 } 39 - } 140 + }
+1 -5
src/obstacles.ts
··· 5 5 6 6 export default class Obstacle { 7 7 game: Game; 8 - 9 8 size: number = 60; 10 - 11 9 container: PIXI.Container; 12 - 13 10 collider: Collider; 14 - 15 11 graphics: PIXI.Graphics; 16 12 17 13 constructor( ··· 43 39 this.game.world.removeCollider(this.collider, false); 44 40 this.container.removeChild(this.graphics); 45 41 } 46 - } 42 + }
+10
src/player-manager.ts
··· 48 48 return { x: x / this.players.length, y: y / this.players.length }; 49 49 } 50 50 51 + smallestTimeSinceLastDamage() { 52 + let min = Infinity; 53 + 54 + for (let player of this.players) { 55 + min = Math.min(min, player.timeSinceLastDamage); 56 + } 57 + 58 + return min; 59 + } 60 + 51 61 getClosestPlayer( 52 62 position: { x: number; y: number }, 53 63 maxDist: number | undefined = undefined
+10 -2
src/player.ts
··· 45 45 46 46 num: number; 47 47 48 + viewDistance: number = 250; 49 + 50 + timeSinceLastDamage: number = 0; 51 + 48 52 constructor(game: Game, num: number) { 49 53 this.game = game; 50 54 this.num = num; ··· 91 95 this.light = PIXI.Sprite.from(texture); 92 96 this.light.tint = 0xfda4af; 93 97 this.light.anchor.set(0.5); 94 - this.light.scale.set(0.5); 98 + this.light.scale.set((0.5 * this.viewDistance) / 200); 95 99 this.light.zIndex = -1; 96 100 97 101 this.playerContainer.addChild(this.light); ··· 181 185 if (dx || dy) this.rigidBody?.applyImpulse({ x: dx * this.speed, y: -dy * this.speed }, true); 182 186 } 183 187 188 + this.timeSinceLastDamage += deltaTime; 189 + 184 190 if (this.light) { 185 - this.light.scale = 0.5 + Math.random() * 0.05; 191 + this.light.scale = (0.5 + Math.random() * 0.05) * (this.viewDistance / 200); 186 192 this.light.alpha = 0.2 + Math.random() * 0.01; 187 193 } 188 194 ··· 260 266 this.health = 0; 261 267 } 262 268 if (this.healthBar) this.healthBar.width = this.size * (this.health / this.maxHealth); 269 + 270 + this.timeSinceLastDamage = 0; 263 271 } 264 272 265 273 destroy() {
+6 -14
src/projectile-manager.ts
··· 5 5 position: { x: number; y: number }; 6 6 enemyPosition: { x: number; y: number }; 7 7 speed: number; 8 - size: number; 9 8 damage: number; 10 - color: number; 11 9 angleOffset: number; 12 - collisionGroups: number; 10 + size?: number; 11 + color?: number; 12 + lifetime?: number; 13 + collisionGroups?: number; 14 + showParticles?: boolean; 13 15 }; 14 16 15 17 export default class ProjectileManager { ··· 21 23 } 22 24 23 25 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 - ); 26 + const projectile = new Projectile(this.game, data); 35 27 36 28 this.projectiles.push(projectile); 37 29 }
+41 -19
src/projectile.ts
··· 1 1 import * as PIXI from 'pixi.js'; 2 2 import { RAPIER } from './rapier'; 3 3 import Game from './app'; 4 + import { ProjectileData } from './projectile-manager'; 4 5 5 6 export class Projectile { 6 7 game: Game; ··· 16 17 17 18 isProjectile: true; 18 19 20 + lifetime: number | undefined = undefined; 21 + 22 + showParticles: boolean = true; 23 + 19 24 constructor( 20 25 game: Game, 21 - position: { x: number; y: number }, 22 - enemyPosition: { x: number; y: number }, 23 - speed: number, 24 - size: number, 25 - damage: number, 26 - color: number, 27 - angleOffset: number, 28 - collisionGroups: number = 0x00040002 26 + data: ProjectileData 27 + // position: { x: number; y: number }, 28 + // enemyPosition: { x: number; y: number }, 29 + // speed: number, 30 + // size: number, 31 + // damage: number, 32 + // color: number, 33 + // angleOffset: number, 34 + // collisionGroups: number = 0x00040002 29 35 ) { 30 36 this.game = game; 31 37 const angle = 32 - Math.atan2(enemyPosition.y - position.y, enemyPosition.x - position.x) + angleOffset; 38 + Math.atan2(data.enemyPosition.y - data.position.y, data.enemyPosition.x - data.position.x) + 39 + data.angleOffset; 33 40 34 - this.vx = Math.cos(angle) * speed; 35 - this.vy = Math.sin(angle) * speed; 36 - this.damage = damage; 37 - this.size = size; 41 + this.vx = Math.cos(angle) * data.speed; 42 + this.vy = Math.sin(angle) * data.speed; 43 + this.damage = data.damage; 44 + this.size = data.size ?? 2; 38 45 39 - this.color = color; 46 + this.color = data.color ?? 0xffffff; 40 47 41 - this.shape = new PIXI.Graphics().rect(0, 0, size, size).fill(color); 48 + this.shape = new PIXI.Graphics().rect(0, 0, this.size, this.size).fill(this.color); 42 49 43 - this.shape.x = position.x; 44 - this.shape.y = position.y; 50 + this.shape.x = data.position.x; 51 + this.shape.y = data.position.y; 45 52 46 53 game.container.addChild(this.shape); 47 54 ··· 51 58 this.rigidBody = game.world.createRigidBody(rigidBodyDesc); 52 59 53 60 const colliderDesc = RAPIER() 54 - .ColliderDesc.ball(size) 55 - .setCollisionGroups(collisionGroups) 61 + .ColliderDesc.ball(this.size) 62 + .setCollisionGroups(data.collisionGroups ?? 0x00040002) 56 63 .setSensor(true) 57 64 .setActiveEvents(RAPIER().ActiveEvents.COLLISION_EVENTS); 58 65 ··· 60 67 61 68 this.rigidBody.userData = this; 62 69 70 + this.lifetime = data.lifetime; 71 + 72 + this.showParticles = data.showParticles ?? true; 73 + 63 74 this.isProjectile = true; 64 75 } 65 76 ··· 70 81 this.shape.y += this.vy * deltaTime; 71 82 72 83 this.rigidBody.setTranslation({ x: this.shape.x, y: -this.shape.y }); 84 + 85 + if (this.lifetime !== undefined) { 86 + this.lifetime -= deltaTime; 87 + 88 + if (this.lifetime <= 0) { 89 + this.destroy(); 90 + 91 + if (this.showParticles) this.game.spawnParticles(this.shape.x, this.shape.y, 4, this.color); 92 + } 93 + } 73 94 } 74 95 75 96 destroy() { 97 + if (this.destroyed) return; 76 98 this.destroyed = true; 77 99 this.game.world.removeRigidBody(this.rigidBody); 78 100 this.game.container.removeChild(this.shape);
+22 -3
src/weapon.ts
··· 10 10 projectileSize?: number; 11 11 color?: number; 12 12 collisionGroups?: number; 13 + 14 + lifetime?: number; 15 + 16 + sound?: boolean; 17 + 18 + showParticles?: boolean; 13 19 }; 14 20 15 21 export class Weapon { ··· 26 32 27 33 collisionGroups: number = 0x00040002; 28 34 35 + lifetime: number | undefined = undefined; 36 + 37 + sound: boolean = true; 38 + 39 + showParticles: boolean = false; 40 + 29 41 constructor(game: Game, options: WeaponOptions | undefined = undefined) { 30 42 this.game = game; 31 43 ··· 35 47 36 48 if (options.damage) this.damage = options.damage; 37 49 if (options.fireRate) this.fireRate = options.fireRate; 38 - if (options.projectileSpeed) this.projectileSpeed = options.projectileSpeed; 50 + if (options.projectileSpeed !== undefined) this.projectileSpeed = options.projectileSpeed; 39 51 if (options.projectileSize) this.projectileSize = options.projectileSize; 40 52 41 53 if (options.color) this.color = options.color; 42 54 if (options.collisionGroups) this.collisionGroups = options.collisionGroups; 55 + if (options.lifetime) this.lifetime = options.lifetime; 56 + 57 + if (options.sound !== undefined) this.sound = options.sound; 58 + 59 + if (options.showParticles !== undefined) this.showParticles = options.showParticles; 43 60 } 44 61 45 62 fire(position: { x: number; y: number }, enemyPosition: { x: number; y: number }) { ··· 52 69 size: this.projectileSize, 53 70 damage: this.damage, 54 71 color: this.color, 55 - collisionGroups: this.collisionGroups 72 + collisionGroups: this.collisionGroups, 73 + lifetime: this.lifetime, 74 + showParticles: this.showParticles 56 75 }); 57 76 this.cooldown = this.fireRate; 58 77 59 - sound.play('laser'); 78 + if (this.sound) sound.play('laser'); 60 79 } 61 80 } 62 81