[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 22, 2024, 4:03 AM +0200) 012fed02 3b48f52d

+554 -209
light.png

This is a binary file and will not be displayed.

+96 -63
package-lock.json
··· 9 9 "version": "0.0.0", 10 10 "dependencies": { 11 11 "@dimforge/rapier2d": "^0.11.2", 12 - "@dimforge/rapier2d-compat": "^0.14.0", 12 + "@dimforge/rapier2d-compat": "^0.11.2", 13 + "@pixi/filter-bloom": "^5.1.1", 13 14 "@pixi/filter-glow": "^5.2.1", 15 + "pixi-filters": "^6.0.4", 14 16 "pixi.js": "^8.2.5", 15 17 "prettier": "^3.3.3", 16 18 "vite": "4.5.0", ··· 42 44 "integrity": "sha512-sCq8Kdg4w+J8XwE1Jkb0Un5eduVXOuLWzBTZfiiwY4CZu12QfJE+2zUdZs3bYsNREP1Ois8IxpiEIL8ZBEyesg==" 43 45 }, 44 46 "node_modules/@dimforge/rapier2d-compat": { 45 - "version": "0.14.0", 46 - "resolved": "https://registry.npmjs.org/@dimforge/rapier2d-compat/-/rapier2d-compat-0.14.0.tgz", 47 - "integrity": "sha512-sljQVPstRS63hVLnVNphsZUjH51TZoptVM0XlglKAdZ8CT+kWnmA6olwjkF7omPWYrlKMd/nHORxOUdJDOSoAQ==" 47 + "version": "0.11.2", 48 + "resolved": "https://registry.npmjs.org/@dimforge/rapier2d-compat/-/rapier2d-compat-0.11.2.tgz", 49 + "integrity": "sha512-HOyo/akJqvZhgB4iaIzeysBiXrewpIHVDpf71HDxC6UwfjL5hFS8CKoW4Lex30KD+ntA865Yz3664e24QJPpqA==" 48 50 }, 49 51 "node_modules/@esbuild/android-arm": { 50 52 "version": "0.18.20", ··· 402 404 } 403 405 }, 404 406 "node_modules/@pixi/color": { 405 - "version": "7.3.2", 406 - "resolved": "https://registry.npmjs.org/@pixi/color/-/color-7.3.2.tgz", 407 - "integrity": "sha512-jur5PvdOtUBEUTjmPudW5qdQq6yYGlVGsi3HyhasJw14bN+GKJwiCKgIsyrsiNL5HBUXmje4ICwQohf6BqKqxA==", 407 + "version": "7.4.2", 408 + "resolved": "https://registry.npmjs.org/@pixi/color/-/color-7.4.2.tgz", 409 + "integrity": "sha512-av1LOvhHsiaW8+T4n/FgnOKHby55/w7VcA1HzPIHRBtEcsmxvSCDanT1HU2LslNhrxLPzyVx18nlmalOyt5OBg==", 408 410 "peer": true, 409 411 "dependencies": { 410 412 "@pixi/colord": "^2.9.6" ··· 416 418 "integrity": "sha512-nezytU2pw587fQstUu1AsJZDVEynjskwOL+kibwcdxsMBFqPsFFNA7xl0ii/gXuDi6M0xj3mfRJj8pBSc2jCfA==" 417 419 }, 418 420 "node_modules/@pixi/constants": { 419 - "version": "7.3.2", 420 - "resolved": "https://registry.npmjs.org/@pixi/constants/-/constants-7.3.2.tgz", 421 - "integrity": "sha512-Q8W3ncsFxmfgC5EtokpG92qJZabd+Dl+pbQAdHwiPY3v+8UNq77u4VN2qtl1Z04864hCcg7AStIYEDrzqTLF6Q==", 421 + "version": "7.4.2", 422 + "resolved": "https://registry.npmjs.org/@pixi/constants/-/constants-7.4.2.tgz", 423 + "integrity": "sha512-N9vn6Wpz5WIQg7ugUg2+SdqD2u2+NM0QthE8YzLJ4tLH2Iz+/TrnPKUJzeyIqbg3sxJG5ZpGGPiacqIBpy1KyA==", 422 424 "peer": true 423 425 }, 424 426 "node_modules/@pixi/core": { 425 - "version": "7.3.2", 426 - "resolved": "https://registry.npmjs.org/@pixi/core/-/core-7.3.2.tgz", 427 - "integrity": "sha512-Pta3ee8MtJ3yKxGXzglBWgwbEOKMB6Eth+FpLTjL0rgxiqTB550YX6jsNEQQAzcGjCBlO3rC/IF57UZ2go/X6w==", 427 + "version": "7.4.2", 428 + "resolved": "https://registry.npmjs.org/@pixi/core/-/core-7.4.2.tgz", 429 + "integrity": "sha512-UbMtgSEnyCOFPzbE6ThB9qopXxbZ5GCof2ArB4FXOC5Xi/83MOIIYg5kf5M8689C5HJMhg2SrJu3xLKppF+CMg==", 428 430 "peer": true, 429 431 "dependencies": { 430 - "@pixi/color": "7.3.2", 431 - "@pixi/constants": "7.3.2", 432 - "@pixi/extensions": "7.3.2", 433 - "@pixi/math": "7.3.2", 434 - "@pixi/runner": "7.3.2", 435 - "@pixi/settings": "7.3.2", 436 - "@pixi/ticker": "7.3.2", 437 - "@pixi/utils": "7.3.2", 438 - "@types/offscreencanvas": "^2019.6.4" 432 + "@pixi/color": "7.4.2", 433 + "@pixi/constants": "7.4.2", 434 + "@pixi/extensions": "7.4.2", 435 + "@pixi/math": "7.4.2", 436 + "@pixi/runner": "7.4.2", 437 + "@pixi/settings": "7.4.2", 438 + "@pixi/ticker": "7.4.2", 439 + "@pixi/utils": "7.4.2" 439 440 }, 440 441 "funding": { 441 442 "type": "opencollective", ··· 443 444 } 444 445 }, 445 446 "node_modules/@pixi/extensions": { 446 - "version": "7.3.2", 447 - "resolved": "https://registry.npmjs.org/@pixi/extensions/-/extensions-7.3.2.tgz", 448 - "integrity": "sha512-Qw84ADfvmVu4Mwj+zTik/IEEK9lWS5n4trbrpQCcEZ+Mb8oRAXWvKz199mi1s7+LaZXDqeCY1yr2PHQaFf1KBA==", 447 + "version": "7.4.2", 448 + "resolved": "https://registry.npmjs.org/@pixi/extensions/-/extensions-7.4.2.tgz", 449 + "integrity": "sha512-Hmx2+O0yZ8XIvgomHM9GZEGcy9S9Dd8flmtOK5Aa3fXs/8v7xD08+ANQpN9ZqWU2Xs+C6UBlpqlt2BWALvKKKA==", 449 450 "peer": true 451 + }, 452 + "node_modules/@pixi/filter-alpha": { 453 + "version": "7.4.2", 454 + "resolved": "https://registry.npmjs.org/@pixi/filter-alpha/-/filter-alpha-7.4.2.tgz", 455 + "integrity": "sha512-9OsKJ+yvY2wIcQXwswj5HQBiwNGymwmqdxfp7mo+nZSBoDmxUqvMZzE9UNJ3eUlswuNvNRO8zNOsQvwdz7WFww==", 456 + "peer": true, 457 + "peerDependencies": { 458 + "@pixi/core": "7.4.2" 459 + } 460 + }, 461 + "node_modules/@pixi/filter-bloom": { 462 + "version": "5.1.1", 463 + "resolved": "https://registry.npmjs.org/@pixi/filter-bloom/-/filter-bloom-5.1.1.tgz", 464 + "integrity": "sha512-4/i+tMxAQdgezahxsVCqzkAyBAH4TxtuY/zo1wuCJybEqkKFIzOJ76Y4R/lJevEHS9CGpCTrvjRpup0Hze8k0Q==", 465 + "peerDependencies": { 466 + "@pixi/core": "^7.0.0-X", 467 + "@pixi/filter-alpha": "^7.0.0-X", 468 + "@pixi/filter-blur": "^7.0.0-X" 469 + } 470 + }, 471 + "node_modules/@pixi/filter-blur": { 472 + "version": "7.4.2", 473 + "resolved": "https://registry.npmjs.org/@pixi/filter-blur/-/filter-blur-7.4.2.tgz", 474 + "integrity": "sha512-gOXBbIUx6CRZP1fmsis2wLzzSsofrqmIHhbf1gIkZMIQaLsc9T7brj+PaLTTiOiyJgnvGN5j20RZnkERWWKV0Q==", 475 + "peer": true, 476 + "peerDependencies": { 477 + "@pixi/core": "7.4.2" 478 + } 450 479 }, 451 480 "node_modules/@pixi/filter-glow": { 452 481 "version": "5.2.1", ··· 457 486 } 458 487 }, 459 488 "node_modules/@pixi/math": { 460 - "version": "7.3.2", 461 - "resolved": "https://registry.npmjs.org/@pixi/math/-/math-7.3.2.tgz", 462 - "integrity": "sha512-dutoZ0IVJ5ME7UtYNo2szu4D7qsgtJB7e3ylujBVu7BOP2e710BVtFwFSFV768N14h9H5roGnuzVoDiJac2u+w==", 489 + "version": "7.4.2", 490 + "resolved": "https://registry.npmjs.org/@pixi/math/-/math-7.4.2.tgz", 491 + "integrity": "sha512-7jHmCQoYk6e0rfSKjdNFOPl0wCcdgoraxgteXJTTHv3r0bMNx2pHD9FJ0VvocEUG7XHfj55O3+u7yItOAx0JaQ==", 463 492 "peer": true 464 493 }, 465 494 "node_modules/@pixi/runner": { 466 - "version": "7.3.2", 467 - "resolved": "https://registry.npmjs.org/@pixi/runner/-/runner-7.3.2.tgz", 468 - "integrity": "sha512-maKotoKJCQiQGBJwfM+iYdQKjrPN/Tn9+72F4WIf706zp/5vKoxW688Rsktg5BX4Mcn7ZkZvcJYTxj2Mv87lFA==", 495 + "version": "7.4.2", 496 + "resolved": "https://registry.npmjs.org/@pixi/runner/-/runner-7.4.2.tgz", 497 + "integrity": "sha512-LPBpwym4vdyyDY5ucF4INQccaGyxztERyLTY1YN6aqJyyMmnc7iqXlIKt+a0euMBtNoLoxy6MWMvIuZj0JfFPA==", 469 498 "peer": true 470 499 }, 471 500 "node_modules/@pixi/settings": { 472 - "version": "7.3.2", 473 - "resolved": "https://registry.npmjs.org/@pixi/settings/-/settings-7.3.2.tgz", 474 - "integrity": "sha512-vtxzuARDTbFe0fRYSqB53B+mPpX7v+QjjnCUmVMVvZiWr3QcngMWVml6c6dQDln7IakWoKZRrNG4FpggvDgLVg==", 501 + "version": "7.4.2", 502 + "resolved": "https://registry.npmjs.org/@pixi/settings/-/settings-7.4.2.tgz", 503 + "integrity": "sha512-pMN+L6aWgvUbwhFIL/BTHKe2ShYGPZ8h9wlVBnFHMtUcJcFLMF1B3lzuvCayZRepOphs6RY0TqvnDvVb585JhQ==", 475 504 "peer": true, 476 505 "dependencies": { 477 - "@pixi/constants": "7.3.2", 478 - "@types/css-font-loading-module": "^0.0.7", 506 + "@pixi/constants": "7.4.2", 507 + "@types/css-font-loading-module": "^0.0.12", 479 508 "ismobilejs": "^1.1.0" 480 509 } 481 510 }, 482 511 "node_modules/@pixi/ticker": { 483 - "version": "7.3.2", 484 - "resolved": "https://registry.npmjs.org/@pixi/ticker/-/ticker-7.3.2.tgz", 485 - "integrity": "sha512-5kIPhBeXwDJohCzKzJJ6T7f1oAGbHAgeiwOjlTO+9lNXUX8ZPj0407V3syuF+64kFqJzIBCznBRpI+fmT4c9SA==", 512 + "version": "7.4.2", 513 + "resolved": "https://registry.npmjs.org/@pixi/ticker/-/ticker-7.4.2.tgz", 514 + "integrity": "sha512-cAvxCh/KI6IW4m3tp2b+GQIf+DoSj9NNmPJmsOeEJ7LzvruG8Ps7SKI6CdjQob5WbceL1apBTDbqZ/f77hFDiQ==", 486 515 "peer": true, 487 516 "dependencies": { 488 - "@pixi/extensions": "7.3.2", 489 - "@pixi/settings": "7.3.2", 490 - "@pixi/utils": "7.3.2" 517 + "@pixi/extensions": "7.4.2", 518 + "@pixi/settings": "7.4.2", 519 + "@pixi/utils": "7.4.2" 491 520 } 492 521 }, 493 522 "node_modules/@pixi/utils": { 494 - "version": "7.3.2", 495 - "resolved": "https://registry.npmjs.org/@pixi/utils/-/utils-7.3.2.tgz", 496 - "integrity": "sha512-KhNvj9YcY7Zi2dTKZgDpx8C6OxKKR541vwtG6JgdBZZYDeMBOIghN2Vi5zn4diW5BhDfHBmdSJ1wZXEtE2MDwg==", 523 + "version": "7.4.2", 524 + "resolved": "https://registry.npmjs.org/@pixi/utils/-/utils-7.4.2.tgz", 525 + "integrity": "sha512-aU/itcyMC4TxFbmdngmak6ey4kC5c16Y5ntIYob9QnjNAfD/7GTsYIBnP6FqEAyO1eq0MjkAALxdONuay1BG3g==", 497 526 "peer": true, 498 527 "dependencies": { 499 - "@pixi/color": "7.3.2", 500 - "@pixi/constants": "7.3.2", 501 - "@pixi/settings": "7.3.2", 528 + "@pixi/color": "7.4.2", 529 + "@pixi/constants": "7.4.2", 530 + "@pixi/settings": "7.4.2", 502 531 "@types/earcut": "^2.1.0", 503 532 "earcut": "^2.2.4", 504 533 "eventemitter3": "^4.0.0", ··· 580 609 "dev": true 581 610 }, 582 611 "node_modules/@types/css-font-loading-module": { 583 - "version": "0.0.7", 584 - "resolved": "https://registry.npmjs.org/@types/css-font-loading-module/-/css-font-loading-module-0.0.7.tgz", 585 - "integrity": "sha512-nl09VhutdjINdWyXxHWN/w9zlNCfr60JUqJbd24YXUuCwgeL0TpFSdElCwb6cxfB6ybE19Gjj4g0jsgkXxKv1Q==", 586 - "peer": true 612 + "version": "0.0.12", 613 + "resolved": "https://registry.npmjs.org/@types/css-font-loading-module/-/css-font-loading-module-0.0.12.tgz", 614 + "integrity": "sha512-x2tZZYkSxXqWvTDgveSynfjq/T2HyiZHXb00j/+gy19yp70PHCizM48XFdjBCWH7eHBD0R5i/pw9yMBP/BH5uA==" 587 615 }, 588 616 "node_modules/@types/earcut": { 589 617 "version": "2.1.4", ··· 597 625 "dev": true, 598 626 "peer": true 599 627 }, 628 + "node_modules/@types/gradient-parser": { 629 + "version": "0.1.5", 630 + "resolved": "https://registry.npmjs.org/@types/gradient-parser/-/gradient-parser-0.1.5.tgz", 631 + "integrity": "sha512-r7K3NkJz3A95WkVVmjs0NcchhHstC2C/VIYNX4JC6tieviUNo774FFeOHjThr3Vw/WCeMP9kAT77MKbIRlO/4w==" 632 + }, 600 633 "node_modules/@types/node": { 601 634 "version": "20.14.11", 602 635 "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.11.tgz", ··· 605 638 "dependencies": { 606 639 "undici-types": "~5.26.4" 607 640 } 608 - }, 609 - "node_modules/@types/offscreencanvas": { 610 - "version": "2019.7.3", 611 - "resolved": "https://registry.npmjs.org/@types/offscreencanvas/-/offscreencanvas-2019.7.3.tgz", 612 - "integrity": "sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A==", 613 - "peer": true 614 641 }, 615 642 "node_modules/@types/pixi.js": { 616 643 "version": "5.0.0", ··· 978 1005 "url": "https://github.com/sponsors/jonschlinkert" 979 1006 } 980 1007 }, 1008 + "node_modules/pixi-filters": { 1009 + "version": "6.0.4", 1010 + "resolved": "https://registry.npmjs.org/pixi-filters/-/pixi-filters-6.0.4.tgz", 1011 + "integrity": "sha512-W9SjOTF4yo+v9t5YOBtsWhJoFLLRM6DsIk1C2YBNiQhdyX7J/5UIJtPlTMhZ7wQKoFUiyeUAaCzTEdmw/TVD6w==", 1012 + "dependencies": { 1013 + "@types/gradient-parser": "^0.1.2" 1014 + }, 1015 + "peerDependencies": { 1016 + "pixi.js": ">=8.0.0-0" 1017 + } 1018 + }, 981 1019 "node_modules/pixi.js": { 982 1020 "version": "8.2.5", 983 1021 "resolved": "https://registry.npmjs.org/pixi.js/-/pixi.js-8.2.5.tgz", ··· 993 1031 "ismobilejs": "^1.1.1", 994 1032 "parse-svg-path": "^0.1.2" 995 1033 } 996 - }, 997 - "node_modules/pixi.js/node_modules/@types/css-font-loading-module": { 998 - "version": "0.0.12", 999 - "resolved": "https://registry.npmjs.org/@types/css-font-loading-module/-/css-font-loading-module-0.0.12.tgz", 1000 - "integrity": "sha512-x2tZZYkSxXqWvTDgveSynfjq/T2HyiZHXb00j/+gy19yp70PHCizM48XFdjBCWH7eHBD0R5i/pw9yMBP/BH5uA==" 1001 1034 }, 1002 1035 "node_modules/pixi.js/node_modules/eventemitter3": { 1003 1036 "version": "5.0.1",
+3 -1
package.json
··· 10 10 }, 11 11 "dependencies": { 12 12 "@dimforge/rapier2d": "^0.11.2", 13 - "@dimforge/rapier2d-compat": "^0.14.0", 13 + "@dimforge/rapier2d-compat": "^0.11.2", 14 + "@pixi/filter-bloom": "^5.1.1", 14 15 "@pixi/filter-glow": "^5.2.1", 16 + "pixi-filters": "^6.0.4", 15 17 "pixi.js": "^8.2.5", 16 18 "prettier": "^3.3.3", 17 19 "vite": "4.5.0",
+59 -18
src/app.ts
··· 1 1 import Player from './player.js'; 2 2 import EnemyManager from './enemy-manager.js'; 3 3 import * as PIXI from 'pixi.js'; 4 - //import RAPIER from '@dimforge/rapier2d-compat'; 4 + import { type World, EventQueue } from '@dimforge/rapier2d'; 5 + import { RAPIER } from './rapier.js'; 6 + import ParticleSystem from './particles.js'; 7 + import { Projectile } from './projectile.js'; 8 + import Enemy from './enemy.js'; 9 + 10 + import { AdvancedBloomFilter, OldFilmFilter, PixelateFilter } from 'pixi-filters'; 5 11 6 12 export default class Game { 7 13 container: PIXI.Container; 8 - world?: RAPIER.World; 14 + world!: World; 9 15 10 16 keys: Record<string, boolean> = {}; 11 17 12 18 debugGraphics?: PIXI.Graphics; 19 + 20 + player?: Player; 21 + enemyManager?: EnemyManager; 22 + 23 + particleSystem?: ParticleSystem; 13 24 14 25 constructor() { 15 26 this.setup(); ··· 23 34 async setupPhysicsWorld() { 24 35 const RAPIER = await import('@dimforge/rapier2d'); 25 36 26 - //await RAPIER.init(); 27 37 let gravity = new RAPIER.Vector2(0.0, 0.0); 28 38 let world = new RAPIER.World(gravity); 29 39 ··· 33 43 } 34 44 35 45 async setup() { 46 + await this.setupPhysicsWorld(); 47 + 36 48 const app = new PIXI.Application(); 37 49 // set to full screen 38 50 await app.init({ ··· 42 54 backgroundColor: 0 43 55 }); 44 56 57 + this.particleSystem = new ParticleSystem(app); 58 + this.container.addChild(this.particleSystem.container); 59 + 45 60 // add the canvas to the HTML document 46 61 document.body.appendChild(app.canvas); 47 62 ··· 50 65 51 66 this.container.position.set(window.innerWidth / 2, window.innerHeight / 2); 52 67 68 + //this.container.filters = [new AdvancedBloomFilter(), new PixelateFilter(6)]; 69 + 53 70 // add a resize event listener 54 71 window.addEventListener('resize', () => { 55 72 app.renderer.resize(window.innerWidth, window.innerHeight); ··· 57 74 this.container.position.set(window.innerWidth / 2, window.innerHeight / 2); 58 75 }); 59 76 60 - await this.setupPhysicsWorld(); 61 - 62 77 app.ticker.add((ticker) => { 63 78 // get ellapsed time 64 79 const deltaTime = ticker.deltaMS; 65 80 66 81 if (this.world) { 67 - const eventQueue = new RAPIER.EventQueue(true); 82 + const eventQueue = new (RAPIER().EventQueue)(true); 68 83 this.world.timestep = deltaTime * 0.001; 69 84 this.world.step(eventQueue); 70 85 this.handleCollisionEvents(eventQueue); 71 86 } 72 - 87 + this.particleSystem?.update(deltaTime); 73 88 this.update(deltaTime); 74 89 }); 75 90 ··· 81 96 window.addEventListener('keyup', this.handleKeyUp.bind(this)); 82 97 } 83 98 84 - handleCollisionEvents(eventQueue: RAPIER.EventQueue) { 99 + handleCollisionEvents(eventQueue: EventQueue) { 85 100 eventQueue.drainCollisionEvents((handle1, handle2, started) => { 86 101 if (!started) return; 87 102 if (!this.world) return; ··· 95 110 96 111 if (!userData1 || !userData2) return; 97 112 98 - // if one is an enemy, the other a projectile, destroy the projectile, take damage 99 - if (userData1.isEnemy && userData2.isProjectile) { 100 - userData1.takeDamage(userData2.damage); 101 - userData2.destroy(); 113 + let enemy: Enemy | undefined; 114 + let projectile: Projectile | undefined; 115 + 116 + // new way to check with instanceof 117 + if (userData1 instanceof Enemy && userData2 instanceof Projectile) { 118 + enemy = userData1; 119 + projectile = userData2; 120 + } else if (userData2 instanceof Enemy && userData1 instanceof Projectile) { 121 + enemy = userData2; 122 + projectile = userData1; 102 123 } 103 - if (userData2.isEnemy && userData1.isProjectile) { 104 - userData2.takeDamage(userData1.damage); 105 - userData1.destroy(); 124 + 125 + if (enemy && projectile) { 126 + this.spawnParticles(projectile.shape.x, projectile.shape.y, 10); 127 + 128 + enemy.takeDamage(projectile.damage); 129 + projectile.destroy(); 106 130 } 107 131 }); 132 + } 133 + 134 + spawnParticles(x: number, y: number, num: number) { 135 + for (let i = 0; i < num; i++) { 136 + this.particleSystem?.createParticle( 137 + x, 138 + y, 139 + Math.random() * 5 + 5, // size 140 + this.player?.color ?? 0xffffff, // color 141 + Math.random() * 0.5 + 0.5, // alpha 142 + (Math.random() - 0.5) * 0.3, // speedX 143 + (Math.random() - 0.5) * 0.3, // speedY 144 + 300 145 + ); 146 + } 108 147 } 109 148 110 149 /** ··· 113 152 */ 114 153 update(deltaTime: number) { 115 154 // update game state here 116 - this.player.update(deltaTime, this.keys, this.enemyManager); 155 + this.player?.update(deltaTime, this.keys); 117 156 118 - this.enemyManager.update(deltaTime, this.player); 157 + this.enemyManager?.update(deltaTime); 119 158 120 159 if (Math.random() < deltaTime * 0.002) { 121 - this.enemyManager.addEnemy(); 160 + this.enemyManager?.addEnemy(); 122 161 } 162 + 163 + //this.renderPhysicsDebug(); 123 164 } 124 165 125 166 handleKeyDown(e: KeyboardEvent) {
+15 -8
src/enemy-manager.ts
··· 1 + import Game from './app.js'; 1 2 import Enemy from './enemy.js'; 3 + import Player from './player.js'; 2 4 3 5 export default class EnemyManager { 4 - constructor(num) { 5 - this.enemies = []; 6 + game: Game; 7 + 8 + enemies: Enemy[] = []; 9 + 10 + constructor(game: Game, num: number) { 11 + this.game = game; 6 12 7 13 for (let i = 0; i < num; i++) { 8 14 this.addEnemy(); ··· 10 16 } 11 17 12 18 addEnemy() { 13 - const enemy = new Enemy(this.container); 19 + const enemy = new Enemy(this.game); 14 20 this.enemies.push(enemy); 15 21 } 16 22 17 - update(deltaTime, player) { 23 + update(deltaTime: number) { 18 24 // delete enemies that are destroyed 19 25 this.enemies = this.enemies.filter((enemy) => !enemy.destroyed); 20 26 21 27 this.enemies.forEach((enemy) => { 22 - enemy.update(deltaTime, player); 28 + enemy.update(deltaTime); 23 29 }); 24 30 } 25 31 26 - getClosestEnemy(position: { x: number; y: number }) { 32 + getClosestEnemy(position: { x: number; y: number }): Enemy | undefined { 27 33 let point = { x: position.x, y: -position.y }; 28 34 let solid = true; 29 35 30 - let proj = world.projectPoint(point, solid, null, 0x00020002); 36 + let proj = this.game.world.projectPoint(point, solid, undefined, 0x00020002); 31 37 32 38 if (proj) { 33 - return proj.collider._parent.userData; 39 + let enemy = proj.collider.parent()?.userData as Enemy; 40 + return enemy; 34 41 } 35 42 } 36 43 }
+79 -23
src/enemy.ts
··· 1 1 import * as PIXI from 'pixi.js'; 2 + import Game from './app'; 3 + import { RAPIER } from './rapier'; 4 + import { type RigidBody } from '@dimforge/rapier2d'; 2 5 3 6 export default class Enemy { 4 - constructor() { 7 + game: Game; 8 + 9 + size: number; 10 + 11 + enemyContainer: PIXI.Container; 12 + 13 + health: number; 14 + maxHealth: number; 15 + 16 + healthBar?: PIXI.Graphics; 17 + 18 + damage: number; 19 + 20 + speed: number; 21 + 22 + shape: PIXI.Graphics; 23 + 24 + exploding: boolean; 25 + destroyTime: number; 26 + 27 + destroyed: boolean; 28 + 29 + isEnemy: true; 30 + 31 + rigidBody?: RigidBody; 32 + 33 + constructor(game: Game) { 34 + this.game = game; 5 35 // add a square 6 36 7 37 this.size = 30; ··· 12 42 13 43 game.container.addChild(this.enemyContainer); 14 44 15 - const shape = new PIXI.Graphics().rect(0, 0, this.size, this.size).fill(0); 16 - shape.pivot.set(this.size / 2, this.size / 2); 17 - this.enemyContainer.addChild(shape); 45 + this.shape = new PIXI.Graphics().circle(0, 0, this.size / 2).fill(0x070707); 46 + //this.shape.pivot.set(this.size / 2, this.size / 2); 47 + this.enemyContainer.addChild(this.shape); 18 48 19 49 this.maxHealth = 100; 20 50 this.health = this.maxHealth; ··· 26 56 this.damage = 10; // Damage dealt to player on contact 27 57 28 58 this.speed = 0.05; 29 - this.shape = shape; 30 59 31 60 this.exploding = false; 32 61 this.destroyTime = -1; ··· 48 77 } 49 78 50 79 createRidigBody() { 51 - const rigidBodyDesc = RAPIER.RigidBodyDesc.dynamic() 80 + const rigidBodyDesc = RAPIER() 81 + .RigidBodyDesc.dynamic() 52 82 .setTranslation(this.x, this.y) 53 - .lockRotations(); 54 - this.rigidBody = world.createRigidBody(rigidBodyDesc); 83 + .lockRotations() 84 + .setLinearDamping(0.5); 85 + this.rigidBody = this.game.world.createRigidBody(rigidBodyDesc); 55 86 56 - const colliderDesc = RAPIER.ColliderDesc.cuboid(this.size / 2, this.size / 2) 57 - .setActiveEvents(RAPIER.ActiveEvents.COLLISION_EVENTS) 58 - .setCollisionGroups(0x00020006); 59 - this.collider = world.createCollider(colliderDesc, this.rigidBody); 87 + const colliderDesc = RAPIER() 88 + .ColliderDesc.ball(this.size / 2) 89 + .setActiveEvents(RAPIER().ActiveEvents.COLLISION_EVENTS) 90 + .setCollisionGroups(0x00020007); 91 + 92 + this.game.world.createCollider(colliderDesc, this.rigidBody); 60 93 61 94 this.rigidBody.userData = this; 62 95 } 63 96 64 97 get x() { 98 + if (this.rigidBody) return this.rigidBody.translation().x; 99 + 65 100 return this.enemyContainer.x; 66 101 } 67 102 set x(value) { 68 103 this.enemyContainer.x = value; 69 - this.rigidBody.setTranslation({ x: value, y: -this.y }); 104 + this.rigidBody?.setTranslation({ x: value, y: -this.y }, true); 70 105 } 71 106 72 107 get y() { 108 + if (this.rigidBody) return -this.rigidBody.translation().y; 109 + 73 110 return this.enemyContainer.y; 74 111 } 75 112 set y(value) { 76 113 this.enemyContainer.y = value; 77 114 78 - this.rigidBody.setTranslation({ x: this.x, y: -value }); 115 + this.rigidBody?.setTranslation({ x: this.x, y: -value }, true); 79 116 } 80 117 81 118 get position() { 119 + if (this.rigidBody) { 120 + let v = this.rigidBody.translation(); 121 + return { x: v.x, y: -v.y }; 122 + } 123 + 82 124 return this.enemyContainer.position; 83 125 } 84 126 set position(value) { 85 127 this.enemyContainer.position = value; 86 - this.rigidBody.setTranslation({ x: this.x, y: -this.y }); 128 + this.rigidBody?.setTranslation({ x: this.x, y: -this.y }, true); 87 129 } 88 130 89 - update(deltaTime, player) { 90 - if (this.destroyed) return; 131 + update(deltaTime: number) { 132 + let player = this.game.player; 133 + 134 + if (this.destroyed || !player) return; 91 135 92 136 // move the player, wasd 93 137 let dx = player.x - this.x; ··· 97 141 98 142 // Only move if not too close to the player and not exploding 99 143 if (distance > this.size + 10 && !this.exploding) { 100 - this.x += (dx / distance) * this.speed * deltaTime; 101 - this.y += (dy / distance) * this.speed * deltaTime; 144 + // this.x += (dx / distance) * this.speed * deltaTime; 145 + // this.y += (dy / distance) * this.speed * deltaTime; 146 + 147 + // lets add some force instead of moving it directly, the further away the player, the more force 148 + const force = 500; 149 + 150 + const x = dx / distance; 151 + const y = dy / distance; 152 + 153 + this.rigidBody?.applyImpulse({ x: x * force, y: -y * force }, true); 102 154 } else if (!this.exploding) { 103 - this.exploding = true; 155 + //this.exploding = true; 104 156 this.destroyTime = 2000; 105 157 } else { 106 158 this.destroyTime -= deltaTime; ··· 112 164 } 113 165 } 114 166 } 167 + 168 + this.enemyContainer.position.set(this.x, this.y); 115 169 } 116 170 117 171 destroy() { 118 172 this.destroyed = true; 119 173 // remove the square 120 174 this.shape.destroy(); 121 - world.removeRigidBody(this.rigidBody); 175 + if (this.rigidBody) this.game.world.removeRigidBody(this.rigidBody); 122 176 } 123 177 124 - takeDamage(amount) { 178 + takeDamage(amount: number) { 125 179 this.health -= amount; 126 - this.healthBar.width = (this.health / this.maxHealth) * this.size; 180 + 181 + if (this.healthBar) this.healthBar.width = (this.health / this.maxHealth) * this.size; 182 + 127 183 if (this.health <= 0) { 128 184 this.destroy(); 129 185 }
+95
src/particles.ts
··· 1 + import * as PIXI from 'pixi.js'; 2 + 3 + interface ParticleSprite extends PIXI.Sprite { 4 + speedX: number; 5 + speedY: number; 6 + age: number; 7 + maxAge: number; 8 + initialSize: number; 9 + } 10 + 11 + export default class ParticleSystem { 12 + private app: PIXI.Application; 13 + private maxParticles: number; 14 + public container: PIXI.Container; 15 + private particles: ParticleSprite[]; 16 + private particlePool: ParticleSprite[]; 17 + private baseGraphics: PIXI.Graphics; 18 + private baseTexture: PIXI.Texture; 19 + 20 + constructor(app: PIXI.Application, maxParticles: number = 5000) { 21 + this.app = app; 22 + this.maxParticles = maxParticles; 23 + this.container = new PIXI.Container(); 24 + this.container.zIndex = 10; 25 + this.particles = []; 26 + this.particlePool = []; 27 + this.baseGraphics = new PIXI.Graphics(); 28 + this.baseGraphics.rect(0, 0, 1, 1).fill(0xffffff); 29 + this.baseTexture = this.app.renderer.generateTexture(this.baseGraphics); 30 + } 31 + 32 + createParticle( 33 + x: number, 34 + y: number, 35 + size: number, 36 + color: number, 37 + alpha: number, 38 + speedX: number, 39 + speedY: number, 40 + maxAge: number 41 + ): ParticleSprite | null { 42 + let particle: ParticleSprite; 43 + if (this.particlePool.length > 0) { 44 + particle = this.particlePool.pop()!; 45 + } else if (this.particles.length < this.maxParticles) { 46 + particle = new PIXI.Sprite(this.baseTexture) as ParticleSprite; 47 + } else { 48 + return null; 49 + } 50 + 51 + particle.x = x; 52 + particle.y = y; 53 + particle.width = particle.height = size; 54 + particle.tint = color; 55 + particle.alpha = alpha; 56 + particle.speedX = speedX; 57 + particle.speedY = speedY; 58 + particle.age = 0; 59 + particle.maxAge = maxAge; 60 + particle.initialSize = size; 61 + 62 + this.container.addChild(particle); 63 + this.particles.push(particle); 64 + 65 + return particle; 66 + } 67 + 68 + removeParticle(particle: ParticleSprite): void { 69 + const index = this.particles.indexOf(particle); 70 + if (index !== -1) { 71 + this.particles.splice(index, 1); 72 + this.container.removeChild(particle); 73 + this.particlePool.push(particle); 74 + } 75 + } 76 + 77 + update(delta: number): void { 78 + console.log('update particles', this.particles.length); 79 + for (let i = this.particles.length - 1; i >= 0; i--) { 80 + const particle = this.particles[i]; 81 + particle.x += particle.speedX * delta; 82 + particle.y += particle.speedY * delta; 83 + particle.age += delta; 84 + 85 + // Shrink the particle 86 + const lifeRatio = 1 - particle.age / particle.maxAge; 87 + particle.width = particle.height = particle.initialSize * lifeRatio; 88 + 89 + // Remove particle if it's too old or off-screen 90 + if (particle.age >= particle.maxAge) { 91 + this.removeParticle(particle); 92 + } 93 + } 94 + } 95 + }
+82 -39
src/player.ts
··· 1 - import { Weapon } from './weapon'; 1 + import { Weapon } from './weapon.js'; 2 2 import * as PIXI from 'pixi.js'; 3 3 4 - import Game from './game.js'; 4 + import Game from './app'; 5 + import { RAPIER } from './rapier'; 6 + import { type RigidBody } from '@dimforge/rapier2d'; 7 + import EnemyManager from './enemy-manager.js'; 5 8 6 9 /** 7 10 * Player class ··· 10 13 * @class Player 11 14 */ 12 15 export default class Player { 16 + game: Game; 17 + 18 + size: number; 19 + 20 + maxHealth: number; 21 + health: number; 22 + 23 + playerContainer: PIXI.Container; 24 + 25 + shadow: PIXI.Graphics; 26 + 27 + healthBar?: PIXI.Graphics; 28 + 29 + speed: number; 30 + 31 + shape: PIXI.Graphics; 32 + 33 + weapon: Weapon; 34 + 35 + isPlayer: true; 36 + 37 + rigidBody?: RigidBody; 38 + 39 + color: number; 40 + 13 41 constructor(game: Game) { 42 + this.game = game; 43 + 14 44 this.size = 20; 15 45 16 46 this.maxHealth = 100; 17 47 this.health = this.maxHealth; 18 48 49 + this.color = 0xe11d48; 50 + 19 51 this.playerContainer = new PIXI.Container(); 20 52 game.container.addChild(this.playerContainer); 21 53 22 - const light = new PIXI.Graphics(); 23 - 24 - // make a light source by drawing a circle with some opacity and getting smaller and smaller 25 - for (let i = 0; i < 100; i++) { 26 - const alpha = 0.02; 27 - const radius = i * 4; 28 - light.circle(0, 0, radius).fill({ color: 0xffffff, alpha }); 29 - } 30 - 31 - light.x = 0; 32 - light.y = 0; 33 - this.playerContainer.addChild(light); 34 - 35 54 this.shadow = new PIXI.Graphics(); 36 55 this.playerContainer.addChild(this.shadow); 37 - light.mask = this.shadow; 38 56 39 - const shape = new PIXI.Graphics().rect(0, 0, this.size, this.size).fill(0x00ff00); 57 + this.createLight(); 58 + 59 + const shape = new PIXI.Graphics().rect(0, 0, this.size, this.size).fill(this.color); 40 60 shape.pivot.set(this.size / 2, this.size / 2); 41 61 this.playerContainer.addChild(shape); 42 - 43 - this.contactDamage = 10; 44 62 45 63 this.createRigidBody(); 46 64 ··· 49 67 this.speed = 0.4; 50 68 this.shape = shape; 51 69 52 - this.weapon = new Weapon('Pistol'); 70 + this.weapon = new Weapon(this.game, this.color); 53 71 54 72 this.isPlayer = true; 73 + } 74 + 75 + async createLight() { 76 + // const light = new PIXI.Graphics(); 77 + 78 + // // make a light source by drawing a circle with some opacity and getting smaller and smaller 79 + // for (let i = 0; i < 100; i++) { 80 + // const alpha = 0.1 * Math.pow(1 - i / 100, 4); 81 + // const radius = i * 4; 82 + // light.circle(0, 0, radius).fill({ color: 0xffffff, alpha }); 83 + // } 84 + 85 + // this.playerContainer.addChild(light); 86 + 87 + const texture = await PIXI.Assets.load('/light.png'); 88 + const light = PIXI.Sprite.from(texture); 89 + light.anchor.set(0.5); 90 + light.scale.set(0.5); 91 + light.zIndex = -1; 92 + 93 + this.playerContainer.addChild(light); 94 + 95 + light.mask = this.shadow; 55 96 } 56 97 57 98 createHealthBar() { 58 99 const healthBarWidth = this.size; 59 100 const healthBarHeight = 5; 60 101 61 - this.healthBar = new PIXI.Graphics().rect(0, 0, healthBarWidth, healthBarHeight).fill(0x00ff00); 102 + this.healthBar = new PIXI.Graphics() 103 + .rect(0, 0, healthBarWidth, healthBarHeight) 104 + .fill(this.color); 62 105 this.healthBar.position.set(0, -this.size); 63 106 this.healthBar.pivot.set(healthBarWidth / 2, healthBarHeight / 2); 64 107 ··· 66 109 } 67 110 68 111 createRigidBody() { 69 - const rigidBodyDesc = RAPIER.RigidBodyDesc.kinematicPositionBased() 112 + const rigidBodyDesc = RAPIER() 113 + .RigidBodyDesc.kinematicPositionBased() 70 114 .setTranslation(0, 0) 71 115 .lockRotations(); 72 - this.rigidBody = world.createRigidBody(rigidBodyDesc); 116 + this.rigidBody = this.game.world.createRigidBody(rigidBodyDesc); 73 117 74 - const colliderDesc = RAPIER.ColliderDesc.cuboid( 75 - this.size / 2, 76 - this.size / 2 77 - ).setCollisionGroups(0x00010000); 78 - this.collider = world.createCollider(colliderDesc, this.rigidBody); 118 + const colliderDesc = RAPIER() 119 + .ColliderDesc.cuboid(this.size / 2, this.size / 2) 120 + .setCollisionGroups(0x00010003); 121 + this.game.world.createCollider(colliderDesc, this.rigidBody); 79 122 80 123 // Attach this Player instance to the rigid body's user data 81 124 this.rigidBody.userData = this; ··· 86 129 } 87 130 set x(value) { 88 131 this.playerContainer.x = value; 89 - this.rigidBody.setTranslation({ x: value, y: -this.y }); 132 + this.rigidBody?.setTranslation({ x: value, y: -this.y }, true); 90 133 } 91 134 92 135 get y() { ··· 94 137 } 95 138 set y(value) { 96 139 this.playerContainer.y = value; 97 - this.rigidBody.setTranslation({ x: this.x, y: -value }); 140 + this.rigidBody?.setTranslation({ x: this.x, y: -value }, true); 98 141 } 99 142 100 143 get position() { ··· 103 146 set position(value) { 104 147 this.playerContainer.position = value; 105 148 106 - this.rigidBody.setTranslation({ x: this.x, y: -this.y }); 149 + this.rigidBody?.setTranslation({ x: this.x, y: -this.y }, true); 107 150 } 108 151 109 - update(deltaTime, keys, enemyManager) { 152 + update(deltaTime: number, keys: Record<string, boolean>) { 110 153 // move the player, wasd 111 154 let dx = 0, 112 155 dy = 0; ··· 125 168 this.y += dy * this.speed * deltaTime; 126 169 127 170 // get closest enemy 128 - const closestEnemy = enemyManager.getClosestEnemy(this.position); 171 + const closestEnemy = this.game.enemyManager?.getClosestEnemy(this.position); 129 172 130 173 if (closestEnemy) { 131 174 this.weapon.fire(this.position, closestEnemy.position); ··· 140 183 // ray cast around the player to create a shadow 141 184 this.shadow.clear(); 142 185 143 - const rays = 360; 186 + const rays = 3600; 144 187 145 188 const angleStep = (Math.PI * 2) / rays; 146 189 const rayLength = 1000; ··· 150 193 for (let i = 0; i < rays; i++) { 151 194 const angle = i * angleStep; 152 195 153 - const ray = new RAPIER.Ray( 196 + const ray = new (RAPIER().Ray)( 154 197 { x: this.x, y: -this.y }, 155 198 { x: Math.cos(angle), y: Math.sin(angle) } 156 199 ); 157 200 158 - const hit = world.castRay(ray, rayLength, true, null, 0x00020002); 201 + const hit = this.game.world.castRay(ray, rayLength, true, undefined, 0x00020002); 159 202 160 203 let hitPoint = hit 161 204 ? ray.pointAt(hit.toi) ··· 171 214 this.shadow.lineTo(x, y); 172 215 } 173 216 174 - if (i === rays - 1) { 217 + if (i === rays - 1 && firstPoint) { 175 218 this.shadow.lineTo(firstPoint.x, firstPoint.y); 176 219 } 177 220 } 178 221 this.shadow.fill(0); 179 222 } 180 223 181 - takeDamage(amount) { 224 + takeDamage(amount: number) { 182 225 this.health -= amount; 183 226 if (this.health < 0) { 184 227 this.health = 0; 185 228 } 186 - this.healthBar.width = this.size * (this.health / this.maxHealth); 229 + if (this.healthBar) this.healthBar.width = this.size * (this.health / this.maxHealth); 187 230 } 188 231 }
+76
src/projectile.ts
··· 1 + import * as PIXI from 'pixi.js'; 2 + import { RAPIER } from './rapier'; 3 + import Game from './app'; 4 + 5 + export class Projectile { 6 + game: Game; 7 + vx: number; 8 + vy: number; 9 + damage: number; 10 + size: number; 11 + shape: PIXI.Graphics; 12 + rigidBody: any; 13 + collider: any; 14 + destroyed: boolean = false; 15 + 16 + isProjectile: true; 17 + 18 + constructor( 19 + game: Game, 20 + position: { x: number; y: number }, 21 + enemyPosition: { x: number; y: number }, 22 + speed: number, 23 + size: number, 24 + damage: number, 25 + color: number, 26 + angleOffset: number 27 + ) { 28 + this.game = game; 29 + const angle = 30 + Math.atan2(enemyPosition.y - position.y, enemyPosition.x - position.x) + angleOffset; 31 + 32 + this.vx = Math.cos(angle) * speed; 33 + this.vy = Math.sin(angle) * speed; 34 + this.damage = damage; 35 + this.size = size; 36 + 37 + this.shape = new PIXI.Graphics().rect(0, 0, size, size).fill(color); 38 + 39 + this.shape.x = position.x; 40 + this.shape.y = position.y; 41 + 42 + game.container.addChild(this.shape); 43 + 44 + const rigidBodyDesc = RAPIER() 45 + .RigidBodyDesc.dynamic() 46 + .setTranslation(this.shape.x, this.shape.y); 47 + this.rigidBody = game.world.createRigidBody(rigidBodyDesc); 48 + 49 + const colliderDesc = RAPIER() 50 + .ColliderDesc.ball(size) 51 + .setCollisionGroups(0x00040002) 52 + .setSensor(true) 53 + .setActiveEvents(RAPIER().ActiveEvents.COLLISION_EVENTS); 54 + 55 + this.collider = game.world.createCollider(colliderDesc, this.rigidBody); 56 + 57 + this.rigidBody.userData = this; 58 + 59 + this.isProjectile = true; 60 + } 61 + 62 + update(deltaTime: number) { 63 + if (this.destroyed) return; 64 + 65 + this.shape.x += this.vx * deltaTime; 66 + this.shape.y += this.vy * deltaTime; 67 + 68 + this.rigidBody.setTranslation({ x: this.shape.x, y: -this.shape.y }); 69 + } 70 + 71 + destroy() { 72 + this.destroyed = true; 73 + this.game.world.removeRigidBody(this.rigidBody); 74 + this.game.container.removeChild(this.shape); 75 + } 76 + }
+6
src/rapier.ts
··· 1 + export function RAPIER(): typeof import('@dimforge/rapier2d') { 2 + if (!window.RAPIER) { 3 + throw new Error('RAPIER is not initialized yet. Make sure to call setupPhysicsWorld() first.'); 4 + } 5 + return window.RAPIER; 6 + }
+41 -56
src/weapon.ts
··· 1 1 import * as PIXI from 'pixi.js'; 2 + import Game from './app'; 3 + import { Projectile } from './projectile'; 2 4 3 5 export class Weapon { 4 - constructor(type) { 5 - this.container = game.container; 6 + game: Game; 6 7 7 - this.type = type; 8 + cooldown: number; 9 + projectiles: Projectile[]; 10 + 11 + damage: number; 12 + fireRate: number; 13 + projectileSpeed: number; 14 + projectileSize: number; 15 + 16 + color: number; 17 + 18 + constructor(game: Game, color: number) { 19 + this.game = game; 20 + 8 21 this.cooldown = 0; 9 22 this.projectiles = []; 10 - this.setStats(); 11 - } 12 23 13 - setStats() { 14 24 this.damage = 20; 15 25 this.fireRate = 100; 16 26 this.projectileSpeed = 0.3; 17 27 this.projectileSize = 5; 28 + 29 + this.color = color; 18 30 } 19 31 20 - fire(position, enemyPosition) { 32 + fire(position: { x: number; y: number }, enemyPosition: { x: number; y: number }) { 21 33 if (this.cooldown <= 0) { 22 34 this.createProjectile(position, enemyPosition, 0); 23 35 this.cooldown = this.fireRate; 24 36 } 25 37 } 26 38 27 - createProjectile(position, enemyPosition, angleOffset) { 28 - const angle = 29 - Math.atan2(enemyPosition.y - position.y, enemyPosition.x - position.x) + angleOffset; 30 - 31 - const shape = new PIXI.Graphics() 32 - .rect(0, 0, this.projectileSize, this.projectileSize) 33 - .fill(0xffff00); 34 - 35 - shape.x = position.x; 36 - shape.y = position.y; 37 - 38 - this.container.addChild(shape); 39 - 40 - const rigidBodyDesc = RAPIER.RigidBodyDesc.dynamic().setTranslation(shape.x, shape.y); 41 - const rigidBody = world.createRigidBody(rigidBodyDesc); 42 - 43 - const colliderDesc = RAPIER.ColliderDesc.ball(this.projectileSize) 44 - .setCollisionGroups(0x00040002) 45 - .setSensor(true) 46 - .setActiveEvents(RAPIER.ActiveEvents.COLLISION_EVENTS); 47 - 48 - const collider = world.createCollider(colliderDesc, rigidBody); 49 - 50 - const projectile = { 51 - vx: Math.cos(angle) * this.projectileSpeed, 52 - vy: Math.sin(angle) * this.projectileSpeed, 53 - damage: this.damage, 54 - size: this.projectileSize, 55 - shape: shape, 56 - rigidBody: rigidBody, 57 - collider: collider, 58 - destroy: () => { 59 - projectile.destroyed = true; 60 - world.removeRigidBody(rigidBody); 61 - game.container.removeChild(shape); 62 - }, 63 - isProjectile: true, 64 - destroyed: false 65 - }; 66 - 67 - rigidBody.userData = projectile; 39 + createProjectile( 40 + position: { x: number; y: number }, 41 + enemyPosition: { x: number; y: number }, 42 + angleOffset: number 43 + ) { 44 + const projectile = new Projectile( 45 + this.game, 46 + position, 47 + enemyPosition, 48 + this.projectileSpeed, 49 + this.projectileSize, 50 + this.damage, 51 + this.color, 52 + angleOffset 53 + ); 68 54 69 55 this.projectiles.push(projectile); 70 56 } 71 57 72 - update(deltaTime) { 58 + update(deltaTime: number) { 73 59 this.cooldown -= deltaTime; 74 60 75 61 for (let i = this.projectiles.length - 1; i >= 0; i--) { ··· 78 64 this.projectiles.splice(i, 1); 79 65 continue; 80 66 } 81 - projectile.shape.x += projectile.vx * deltaTime; 82 - projectile.shape.y += projectile.vy * deltaTime; 83 - 84 - projectile.rigidBody.setTranslation({ x: projectile.shape.x, y: -projectile.shape.y }); 67 + projectile.update(deltaTime); 85 68 86 69 // Check if projectile is too far from the player 87 - const playerPos = game.player.shape.position; 70 + const playerPos = this.game.player?.position; 71 + if (!playerPos) return; 72 + 88 73 const dx = projectile.shape.x - playerPos.x; 89 74 const dy = projectile.shape.y - playerPos.y; 90 75 const distanceSquared = dx * dx + dy * dy; 91 76 92 77 if (distanceSquared > 1000000) { 93 78 // 1000 units squared 94 - projectile.destroy(); 79 + projectile.destroy(this.game); 95 80 this.projectiles.splice(i, 1); 96 81 } 97 82 } ··· 99 84 100 85 clearAllProjectiles() { 101 86 for (let projectile of this.projectiles) { 102 - projectile.destroy(); 87 + projectile.destroy(this.game); 103 88 } 104 89 this.projectiles = []; 105 90 }
+2 -1
src/types/global.d.ts
··· 1 1 // src/types/global.d.ts 2 + import * as RAPIER from '@dimforge/rapier2d'; 2 3 3 4 export {}; 4 5 ··· 6 7 interface Window { 7 8 container: PIXI.Container; 8 9 game: Game; 9 - RAPIER: any; 10 + RAPIER: RAPIER; 10 11 world: any; 11 12 } 12 13 }