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

stuff

Florian (Jul 26, 2024, 5:46 PM +0200) dfb54559 b1aa73cf

+248 -72
+10 -23
src/app.ts
··· 9 9 10 10 import Stats from 'stats.js'; 11 11 12 - import { AdvancedBloomFilter } from 'pixi-filters'; 12 + import { AdvancedBloomFilter, OldFilmFilter } from 'pixi-filters'; 13 13 import PlayerManager from './player-manager.js'; 14 14 import ProjectileManager, { ProjectileData } from './projectile-manager.js'; 15 15 ··· 20 20 import { Item, ItemOptions } from './item.js'; 21 21 import { ItemManager } from './item-manager.js'; 22 22 import { LightManager } from './light-manager.js'; 23 + import { createNoiseSprite } from './helper.js'; 23 24 24 25 export default class Game { 25 26 container: PIXI.Container; ··· 46 47 47 48 debug: boolean = false; 48 49 49 - showStats: boolean = false; 50 + showStats: boolean = true; 50 51 51 52 stats?: Stats; 52 53 ··· 64 65 65 66 minWidth = 700; 66 67 minHeight = 1000; 67 - 68 - lightPlaced = false; 69 68 70 69 constructor() { 71 70 this.setup(); ··· 160 159 this.mainContainer.scale.set(this.scale); 161 160 }); 162 161 163 - //this.container.scale.set(this.scale); 162 + let noise = createNoiseSprite(2000, 2000); 163 + noise.anchor.set(0.5); 164 + noise.scale.set(1); 165 + noise.alpha = 0.3; 166 + noise.zIndex = 100; 167 + this.mainContainer.addChild(noise); 164 168 165 169 app.ticker.add((ticker) => { 166 170 if (this.stats) this.stats.begin(); ··· 334 338 update(deltaTime: number) { 335 339 // update game state here 336 340 this.playerManager?.update(deltaTime, this.keys); 337 - if (!this.playing) return; 338 341 339 342 this.playingTime += deltaTime; 340 343 if (Math.floor(this.playingTime / 1000) !== Math.floor((this.playingTime - deltaTime) / 1000)) { ··· 344 347 345 348 this.obstacleManager.update(deltaTime); 346 349 347 - this.enemyManager?.update(deltaTime); 350 + if (this.playing) this.enemyManager?.update(deltaTime); 348 351 349 352 this.itemManager.update(deltaTime); 350 353 ··· 354 357 for (let players of this.playerManager?.players ?? []) { 355 358 players.health = players.maxHealth; 356 359 } 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; 373 360 } 374 361 375 362 let playerManager = this.playerManager;
+31
src/helper.ts
··· 16 16 // Recombine blended components into hexadecimal 17 17 return (r << 16) | (g << 8) | b; 18 18 } 19 + 20 + import * as PIXI from 'pixi.js'; 21 + 22 + export function createNoiseSprite(width: number, height: number): PIXI.Sprite { 23 + // Create an empty PIXI texture with the given width and height 24 + const canvas = document.createElement('canvas'); 25 + canvas.width = width; 26 + canvas.height = height; 27 + const ctx = canvas.getContext('2d')!; 28 + 29 + // Fill the canvas with random noise 30 + const imageData = ctx.createImageData(width, height); 31 + const data = imageData.data; 32 + for (let i = 0; i < data.length; i += 4) { 33 + data[i] = 0; 34 + data[i + 1] = 0; 35 + data[i + 2] = 0; 36 + data[i + 3] = Math.floor(Math.random() * 256); // Alpha 37 + } 38 + ctx.putImageData(imageData, 0, 0); 39 + 40 + // Create a PIXI texture from the canvas 41 + const texture = PIXI.Texture.from(canvas); 42 + // Create a sprite from the texture 43 + const sprite = new PIXI.Sprite(texture); 44 + 45 + // remove canvas 46 + canvas.remove(); 47 + 48 + return sprite; 49 + }
+20 -5
src/item.ts
··· 33 33 34 34 life: number = 0; 35 35 36 + light?: PIXI.Sprite; 37 + 36 38 constructor(game: Game, options: ItemOptions) { 37 39 this.game = game; 38 40 ··· 43 45 44 46 this.lifetime = options.lifetime ?? 10000; 45 47 46 - this.shape = new PIXI.Graphics().rect(0, 0, this.size, this.size).fill(this.color); 48 + this.shape = new PIXI.Graphics().circle(0, 0, this.size / 2).fill(this.color); 47 49 48 50 this.shape.x = options.x; 49 51 this.shape.y = options.y; 50 - this.shape.pivot.set(this.size / 2, this.size / 2); 51 52 52 53 game.container.addChild(this.shape); 54 + 55 + this.addGlow(); 53 56 54 57 const rigidBodyDesc = RAPIER() 55 58 .RigidBodyDesc.dynamic() ··· 67 70 this.rigidBody.userData = this; 68 71 } 69 72 73 + async addGlow() { 74 + const texture = await PIXI.Assets.load('./light.png'); 75 + this.light = PIXI.Sprite.from(texture); 76 + this.light.tint = this.color; 77 + this.light.anchor.set(0.5); 78 + this.light.scale.set(0.1); 79 + this.light.zIndex = -1; 80 + this.light.alpha = 0.2; 81 + 82 + this.shape.addChild(this.light); 83 + } 84 + 70 85 pickup(player: Player) { 71 86 this.game.spawnParticles(this.shape.x, this.shape.y, 5, this.color); 72 87 ··· 82 97 this.destroy(); 83 98 return; 84 99 } 85 - 100 + /* 86 101 let closestPlayer = this.game.lightManager?.getClosestLight({ 87 102 x: this.shape.x, 88 103 y: this.shape.y ··· 91 106 if (closestPlayer) { 92 107 let dist = Math.hypot(closestPlayer.x - this.shape.x, closestPlayer.y - this.shape.y); 93 108 this.shape.alpha = 1 - dist / (closestPlayer.scale * 300); 94 - } 109 + }*/ 95 110 96 - this.shape.alpha *= 1 - this.life / this.lifetime; 111 + this.shape.scale.set(1 - this.life / this.lifetime + 0.2); 97 112 } 98 113 99 114 destroy() {
+25 -14
src/light-manager.ts
··· 27 27 position: { x: number; y: number }, 28 28 maxDist: number | undefined = undefined 29 29 ): Light | undefined { 30 - let closestLight: Light | undefined; 31 - let closestDistance = Infinity; 30 + position.y = -position.y; 31 + let solid = true; 32 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 - } 33 + let proj = this.game.world.projectPoint(position, solid, undefined, 0x10001000); 34 + 35 + if (proj) { 36 + let light = proj.collider.parent()?.userData as Light; 37 + 38 + return light; 41 39 } 42 40 43 - if (!maxDist || closestDistance < maxDist) { 44 - return closestLight; 45 - } 41 + // let closestLight: Light | undefined; 42 + // let closestDistance = Infinity; 43 + 44 + // for (let light of this.lights) { 45 + // if (light.destroyed) continue; 46 + // console.log(light); 47 + // const distance = Math.hypot(light.x - position.x, light.y - position.y); 48 + // if (distance < closestDistance) { 49 + // closestDistance = distance; 50 + // closestLight = light; 51 + // } 52 + // } 53 + 54 + // if (!maxDist || closestDistance < maxDist) { 55 + // return closestLight; 56 + // } 46 57 47 - return undefined; 58 + // return undefined; 48 59 } 49 60 }
+37 -3
src/light.ts
··· 1 1 import * as PIXI from 'pixi.js'; 2 2 import { RAPIER } from './rapier'; 3 3 import Game from './app'; 4 + import { RigidBody } from '@dimforge/rapier2d'; 5 + // import 'pixi.js/advanced-blend-modes'; 4 6 5 7 export type LightOptions = { 6 8 x?: number; ··· 11 13 lifetime?: number; 12 14 13 15 alpha?: number; 16 + 17 + detail?: number; 18 + 19 + flicker?: boolean; 14 20 }; 15 21 16 22 export class Light { ··· 33 39 _alpha: number = 1; 34 40 35 41 flicker: boolean = true; 42 + 43 + rigidBody?: RigidBody; 44 + 45 + detail: number = 120; 36 46 37 47 constructor(game: Game, options: LightOptions) { 38 48 this.game = game; ··· 52 62 this.lifetime = options.lifetime; 53 63 } 54 64 if (options.alpha) this.alpha = options.alpha; 65 + if (options.detail) this.detail = options.detail; 66 + if (options.flicker !== undefined) this.flicker = options.flicker; 55 67 56 68 this.createLight(); 57 69 ··· 63 75 } 64 76 set x(value) { 65 77 this.lightContainer.x = value; 78 + 79 + this.rigidBody?.setTranslation({ x: value, y: -this.y }, true); 66 80 } 67 81 68 82 get y() { ··· 70 84 } 71 85 set y(value) { 72 86 this.lightContainer.y = value; 87 + 88 + this.rigidBody?.setTranslation({ x: this.x, y: -value }, true); 73 89 } 74 90 75 91 get scale() { ··· 107 123 108 124 this.lightContainer.addChild(this.light); 109 125 126 + // this.lightContainer.blendMode = 'add'; 127 + 110 128 this.light.mask = this.shadow; 129 + 130 + const rigidBodyDesc = RAPIER() 131 + .RigidBodyDesc.kinematicPositionBased() 132 + .setTranslation(this.x, -this.y) 133 + .lockRotations(); 134 + this.rigidBody = this.game.world.createRigidBody(rigidBodyDesc); 135 + 136 + const colliderDesc = RAPIER().ColliderDesc.cuboid(10, 10).setCollisionGroups(0x10001000); 137 + this.game.world.createCollider(colliderDesc, this.rigidBody); 138 + 139 + this.rigidBody.userData = this; 111 140 } 112 141 113 142 drawShadow() { 114 143 // ray cast around the player to create a shadow 115 144 this.shadow.clear(); 116 145 117 - const rays = 360; 146 + const rays = this.detail; 118 147 119 148 const angleStep = (Math.PI * 2) / rays; 120 149 const rayLength = 100000; ··· 158 187 this.drawShadow(); 159 188 160 189 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); 190 + this.light.alpha = this._alpha + (Math.random() - 0.5) * 0.03; 191 + this.light.scale.set(this._scale + Math.random() * 0.1); 192 + 193 + if (Math.random() < 1 / deltaTime) { 194 + this.light.alpha = this._alpha * 0.5; 195 + } 163 196 } 164 197 } 165 198 ··· 168 201 169 202 this.destroyed = true; 170 203 this.lightContainer.destroy(); 204 + if (this.rigidBody) this.game.world.removeRigidBody(this.rigidBody); 171 205 } 172 206 }
+117 -22
src/obstacle-manager.ts
··· 6 6 import { Light } from './light'; 7 7 8 8 const CELL_SIZE = 900; 9 - const OBSTACLE_COUNT_PER_CELL = 30; 10 - const LIGHT_COUNT_PER_CELL = 2; 9 + const OBSTACLE_COUNT_PER_CELL = 20; 10 + const LIGHT_COUNT_PER_CELL = 3; 11 11 12 12 type CellCoord = `${number},${number}`; 13 + 14 + const title: { x: number; y: number; width: number; height: number }[] = [ 15 + { x: 77, y: 105, width: 171, height: 33 }, 16 + { x: 77, y: 194, width: 171, height: 33 }, 17 + { x: 77, y: 283, width: 171, height: 33 }, 18 + { x: 289, y: 194, width: 122, height: 33 }, 19 + { x: 520, y: 242, width: 171, height: 33 }, 20 + { x: 539, y: 105, width: 140, height: 33 }, 21 + { x: 78, y: 377, width: 104, height: 33 }, 22 + { x: 147, y: 410, width: 80, height: 33 }, 23 + { x: 197, y: 443, width: 80, height: 33 }, 24 + { x: 77, y: 541, width: 171, height: 33 }, 25 + { x: 311, y: 377, width: 171, height: 33 }, 26 + { x: 311, y: 541, width: 171, height: 33 }, 27 + { x: 545, y: 549, width: 106, height: 33 }, 28 + { x: 691, y: 549, width: 106, height: 33 }, 29 + { x: 619, y: 435, width: 106, height: 33 }, 30 + { x: 619, y: 435, width: 40, height: 147 }, 31 + { x: 77, y: 148, width: 40, height: 79 }, 32 + { x: 208, y: 194, width: 40, height: 81 }, 33 + { x: 691, y: 435, width: 40, height: 106 }, 34 + { x: 765, y: 360, width: 40, height: 222 }, 35 + { x: 545, y: 360, width: 40, height: 181 }, 36 + { x: 442, y: 377, width: 40, height: 197 }, 37 + { x: 311, y: 377, width: 40, height: 151 }, 38 + { x: 289, y: 119, width: 40, height: 197 }, 39 + { x: 422, y: 119, width: 40, height: 197 }, 40 + { x: 520, y: 138, width: 40, height: 178 }, 41 + { x: 659, y: 148, width: 40, height: 168 }, 42 + { x: 77, y: 377, width: 40, height: 197 }, 43 + { x: 237, y: 489, width: 40, height: 85 } 44 + ]; 45 + 46 + const titleLights: { x: number; y: number; size: number }[] = [ 47 + { x: 40, y: 144, size: 200 }, 48 + { x: 610, y: 185, size: 200 }, 49 + { x: 397, y: 477, size: 200 }, 50 + { x: 170, y: 477, size: 200 }, 51 + { x: 181, y: 262, size: 200 }, 52 + // { x: 116, y: 162, size: 200 }, 53 + { x: 381, y: 162, size: 200 }, 54 + { x: 410, y: 262, size: 200 }, 55 + { x: 610, y: 539, size: 200 }, 56 + { x: 748, y: 539, size: 200 }, 57 + { x: 647.5, y: 356.5, size: 589 }, 58 + // { x: 280.5, y: 344.5, size: 589 }, 59 + { x: 140, y: 50, size: 282 }, 60 + { x: 607, y: 44, size: 282 }, 61 + { x: 397, y: 44, size: 282 }, 62 + { x: 170, y: 618, size: 282 }, 63 + { x: 397, y: 627, size: 282 }, 64 + { x: 679, y: 627, size: 282 }, 65 + // { x: 678, y: 362, size: 282 }, 66 + { x: 29, y: 477, size: 282 }, 67 + { x: 851, y: 439, size: 282 }, 68 + { x: 759, y: 204, size: 282 }, 69 + // { x: 240, y: 362, size: 282 }, 70 + { x: 494, y: 216, size: 282 }, 71 + { x: 511, y: 477, size: 282 }, 72 + { x: 142, y: 161, size: 282 } 73 + ]; 74 + 75 + const titleColors = [ 76 + 0xf43f5e, 0xec4899, 0xd946ef, 0xa855f7, 0x8b5cf6, 0x6366f1, 0x3b82f6, 0x0ea5e9, 0xdc2626 77 + ]; 13 78 14 79 export class ObstacleManager { 15 80 container: PIXI.Container; ··· 86 151 const obstacles: (Obstacle | Light)[] = []; 87 152 88 153 // @ts-ignore 89 - let rng = new Alea(cellCoord + this.seed); 154 + let rng = new Alea(cellCoord + (cellCoord !== '0,0' ? this.seed : '1')); 155 + 156 + if (cellCoord === '0,01') { 157 + for (let rect of title) { 158 + const width = rect.width; 159 + const height = rect.height; 160 + const x = rect.x + width / 2; 161 + const y = rect.y + height / 2; 162 + 163 + const obstacle = new Obstacle(this.game, this.container, x, y, width, height); 164 + obstacles.push(obstacle); 165 + } 166 + 167 + for (let lightPos of titleLights) { 168 + const x = lightPos.x; 169 + const y = lightPos.y; 170 + const size = lightPos.size; 171 + 172 + const color = titleColors[Math.floor(rng() * titleColors.length)]; 90 173 91 - for (let i = 0; i < OBSTACLE_COUNT_PER_CELL; i++) { 92 - const x = cellX * CELL_SIZE + rng() * CELL_SIZE; 93 - const y = cellY * CELL_SIZE + rng() * CELL_SIZE; 94 - const width = rng() * 90 + 10; 95 - const height = rng() * 90 + 10; 174 + const light = this.game.lightManager.addLight({ 175 + x, 176 + y, 177 + color: color, 178 + alpha: 0.3, 179 + scale: size / 800, 180 + flicker: false 181 + }); 182 + obstacles.push(light); 183 + } 184 + } else { 185 + for (let i = 0; i < OBSTACLE_COUNT_PER_CELL; i++) { 186 + const x = cellX * CELL_SIZE + rng() * CELL_SIZE; 187 + const y = cellY * CELL_SIZE + rng() * CELL_SIZE; 188 + const width = rng() * 90 + 10; 189 + const height = rng() * 90 + 10; 96 190 97 - const obstacle = new Obstacle(this.game, this.container, x, y, width, height); 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; 191 + const obstacle = new Obstacle(this.game, this.container, x, y, width, height); 192 + obstacles.push(obstacle); 193 + } 194 + for (let i = 0; i < LIGHT_COUNT_PER_CELL; i++) { 195 + const x = cellX * CELL_SIZE + rng() * CELL_SIZE; 196 + const y = cellY * CELL_SIZE + rng() * CELL_SIZE; 103 197 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); 198 + const light = this.game.lightManager.addLight({ 199 + x, 200 + y, 201 + color: 0xffffff * rng(), 202 + alpha: 0.05 + rng() * 0.15, 203 + scale: 0.5 + rng() * 1 204 + }); 205 + obstacles.push(light); 206 + } 112 207 } 113 208 114 209 this.obstacles.set(cellCoord, obstacles);
+8 -5
src/player.ts
··· 64 64 this.maxHealth = 100; 65 65 this.health = this.maxHealth; 66 66 67 - this.color = num === 0 ? 0xe11d48 : 0x4f46e5; 67 + this.color = num === 0 ? 0xbe123c : 0x4f46e5; 68 68 69 69 this.playerContainer = new PIXI.Container(); 70 70 game.container.addChild(this.playerContainer); 71 71 72 - this.light = this.game.lightManager.addLight({ color: blendColors(this.color, 0xffffff, 0.3) }); 72 + this.light = this.game.lightManager.addLight({ 73 + color: blendColors(this.color, 0xffffff, 0.1), 74 + detail: 360 75 + }); 73 76 74 77 const shape = new PIXI.Graphics().rect(0, 0, this.size, this.size).fill(this.color); 75 78 shape.pivot.set(this.size / 2, this.size / 2); ··· 89 92 this.leftEye = new Eye(this.playerContainer, -this.size / 4, 0); 90 93 this.rightEye = new Eye(this.playerContainer, this.size / 4, 0); 91 94 92 - this.x = 0; 93 - this.y = 0; 95 + this.x = 280; 96 + this.y = 340; 94 97 } 95 98 96 99 createHealthBar() { ··· 205 208 206 209 if (this.light) { 207 210 this.light.scale = (0.5 + Math.random() * 0.05) * (this.viewDistance / 200); 208 - this.light.alpha = 0.2 + Math.random() * 0.01; 211 + this.light.alpha = 0.4; 209 212 } 210 213 211 214 this.leftEye.update(deltaTime, 1);