[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 31, 2024, 9:41 AM +0200) ef8b9252 74454628

+512 -129
+12 -9
index.html
··· 7 7 <meta name="apple-mobile-web-app-status-bar-style" content="black" /> 8 8 <meta name="apple-mobile-web-app-title" content="shadow shooter" /> 9 9 10 - 11 10 <link rel="icon" href="/icons/favicon.ico" sizes="32x32" /> 12 11 <link rel="icon" href="/icons/icon.svg" type="image/svg+xml" /> 13 12 <link rel="apple-touch-icon" href="/icons/apple-touch-icon.png" /> ··· 173 172 <div id="wave-text" class="text-8xl font-bold text-neutral-200">Wave</div> 174 173 </div> 175 174 176 - <div id="items" class="absolute top-0 left-0 right-0 h-1 w-full flex"> 175 + <div id="xp" class="absolute top-0 left-0 right-0 h-1 w-full flex"></div> 177 176 178 - </div> 177 + <div 178 + id="upgrades" 179 + class="hidden z-40 absolute inset-0 overflow-y-scroll py-16 sm:py-16 px-2 bg-black/90" 180 + > 181 + <div class="mx-auto max-w-5xl text-white text-4xl mb-8 font-bold">Choose your upgrade</div> 179 182 180 - <div id="upgrades" class="hidden z-50 absolute inset-0 overflow-y-scroll py-2 sm:py-16 px-2 bg-black/70"> 181 183 <div 182 - id="upgrades-container" 184 + id="upgrades-container" 183 185 class="overflow-hidden rounded-lg bg-white/5 mx-auto max-w-5xl backdrop-blur-sm shadow sm:grid sm:grid-cols-2" 184 - > 185 - 186 - </div> 186 + ></div> 187 187 </div> 188 188 189 - <div id="items" class="absolute bottom-1 right-3 text-white font-bold text-xl flex flex-col gap-1"></div> 189 + <div id="coins" class="absolute top-3 right-3 text-white font-bold text-xl z-50 flex items-center gap-2"> 190 + 191 + </div> 192 + 190 193 </body> 191 194 </html>
+14 -26
src/app.ts
··· 59 59 60 60 playing: boolean = false; 61 61 62 + paused: boolean = false; 63 + 62 64 controls: Controls; 63 65 64 66 scale: number = 1; ··· 90 92 }); 91 93 sound.add('music', { url: './music.mp3', loop: true, volume: 0.3 }); 92 94 sound.add('laser', { url: './laser.mp3', volume: 0.1 }); 93 - 94 - // get upgrade-container 95 - const upgradeContainer = document.getElementById('upgrades-container'); 96 - // if upgradeContainer exists 97 - if (upgradeContainer) { 98 - for (let i = 0; i < 6; i++) { 99 - let option = addUpgradeOption({ 100 - index: i, 101 - total: 6, 102 - iconName: 'heart', 103 - perk: 'Health', 104 - perkValue: '+10%', 105 - minusStat: 'Speed', 106 - minusStatValue: '-0.1', 107 - price: [ 108 - { color: 'bg-orange-400', amount: Math.floor(Math.random() * 10 + 1) }, 109 - { color: 'bg-sky-400', amount: Math.floor(Math.random() * 10 + 1) } 110 - ], 111 - empty: i == 5 112 - }); 113 - upgradeContainer.appendChild(option); 114 - } 115 - } 116 95 } 117 96 118 97 async setupPhysicsWorld() { ··· 198 177 this.mainContainer.addChild(noise); 199 178 200 179 app.ticker.add((ticker) => { 180 + if (this.paused) return; 201 181 if (this.stats) this.stats.begin(); 202 182 203 183 // get ellapsed time ··· 262 242 this.startMusic(); 263 243 264 244 this.playingTime = 0; 245 + 246 + this.upgradeManager.showUpgradeMenu(); 247 + this.paused = true; 265 248 }); 266 249 267 250 const playCoopButton = document.getElementById('play-coop'); ··· 338 321 this.spawnParticles(weapon.x, weapon.y, 10, weapon.color); 339 322 340 323 enemy.takeDamage(weapon.damage); 324 + 325 + console.log('enemy hit'); 341 326 } 342 327 343 328 if (enemy && player && enemy.hitPlayer) { ··· 362 347 } 363 348 364 349 spawnParticles(x: number, y: number, num: number, color: number = 0xffffff) { 350 + console.log('spawn particles', x, y, num, color); 365 351 for (let i = 0; i < num; i++) { 366 352 this.particleSystem?.createParticle( 367 353 x, ··· 396 382 397 383 if (this.invincible) { 398 384 for (let players of this.playerManager?.players ?? []) { 399 - players.health = players.maxHealth; 385 + players.health = players._maxHealth; 400 386 } 401 387 } 402 388 403 - if (this.upgradeManager.items >= this.upgradeManager.neededItems) { 389 + if (this.upgradeManager.canAdvance()) { 404 390 this.waveManager?.nextWave(); 405 391 this.upgradeManager.reset(); 392 + 393 + this.paused = this.upgradeManager.showUpgradeMenu(); 406 394 } 407 395 408 396 /* ··· 466 454 let counter = document.getElementById('counter'); 467 455 if (counter) counter.innerText = ''; 468 456 469 - this.upgradeManager.reset(); 457 + this.upgradeManager.resetAll(); 470 458 } 471 459 472 460 handleKeyDown(e: KeyboardEvent) {
+6 -5
src/enemy.ts
··· 297 297 298 298 shooting: boolean = false; 299 299 300 + burstNumber: number = 10; 301 + 300 302 constructor(game: Game) { 301 303 super(game); 302 304 ··· 346 348 } 347 349 348 350 if (this.shooting && this.cooldown <= 0 && this.weapon.cooldown <= 0) { 349 - for (let i = 0; i < 20; i++) { 351 + for (let i = 0; i < this.burstNumber; i++) { 350 352 this.weapon.cooldown = -1; 351 353 // get angle 352 - let angle = (Math.PI / 10) * i; 354 + let angle = (Math.PI / this.burstNumber) * 2 * i; 353 355 let x = Math.cos(angle) + this.position.x; 354 356 let y = Math.sin(angle) + this.position.y; 355 357 this.weapon.fire(this.position, { x, y }); ··· 487 489 color: this.color, 488 490 collisionGroups: 0x00100001, 489 491 projectileSpeed: 0.15, 490 - fireRate: 2000, 492 + fireRate: 5000, 491 493 projectileSize: 12, 492 494 damage: 10, 493 - lifetime: 8000, 494 - outline: true 495 + lifetime: 4000 495 496 }); 496 497 497 498 this.speed = 900;
+1 -1
src/item.ts
··· 42 42 this.game = game; 43 43 44 44 this.size = options.size; 45 - this.color = options.color; 45 + this.color = 0xf59e0b; //options.color; 46 46 47 47 this.type = options.type; 48 48
+23 -8
src/player.ts
··· 23 23 24 24 size: number; 25 25 26 - maxHealth: number; 27 - health: number; 26 + _maxHealth: number; 27 + _health: number; 28 28 29 29 playerContainer: PIXI.Container; 30 30 ··· 67 67 68 68 this.size = 25; 69 69 70 - this.maxHealth = 100; 71 - this.health = this.maxHealth; 70 + this._maxHealth = 100; 71 + this._health = this._maxHealth; 72 72 73 73 this.color = num === 0 ? 0xbe123c : 0x4f46e5; 74 74 ··· 94 94 this.weapon = new GunWeapon(this.game, { 95 95 color: this.color, 96 96 lifetime: 2000, 97 - piercing: 1, 97 + piercing: 0, 98 98 fireRate: 1000 99 99 }); 100 100 ··· 124 124 //this.weapons = [new BallWeapon(this.game, this.color), new Knife(this.game, this.color)]; 125 125 } 126 126 127 + set health(value: number) { 128 + this._health = value; 129 + if (this.healthBar) this.healthBar.width = this.size * (this._health / this._maxHealth); 130 + } 131 + get health() { 132 + return this._health; 133 + } 134 + 135 + set maxHealth(value: number) { 136 + this._maxHealth = value; 137 + if (this.healthBar) this.healthBar.width = this.size * (this._health / this._maxHealth); 138 + } 139 + get maxHealth() { 140 + return this._maxHealth; 141 + } 142 + 127 143 createHealthBar() { 128 144 const healthBarWidth = this.size; 129 145 const healthBarHeight = 5; ··· 225 241 this.y = closestPlayer?.y ?? 0; 226 242 227 243 this.dead = false; 228 - this.health = this.maxHealth; 244 + this._health = this._maxHealth; 229 245 230 246 this.playerContainer.alpha = 1; 231 247 } ··· 272 288 273 289 this.health -= amount; 274 290 275 - this.game.controls.rumble(this.num, (amount / this.maxHealth) * 50); 291 + this.game.controls.rumble(this.num, (amount / this._maxHealth) * 50); 276 292 277 293 if (this.health < 0) { 278 294 this.health = 0; 279 295 this.dead = true; 280 296 this.respawnTime = 5000; 281 297 } 282 - if (this.healthBar) this.healthBar.width = this.size * (this.health / this.maxHealth); 283 298 284 299 this.timeSinceLastDamage = 0; 285 300 }
+231 -19
src/upgrade-manager.ts
··· 1 1 import Game from './app'; 2 2 import { Item } from './item'; 3 + import Player from './player'; 4 + import { addUpgradeOption, Icon } from './upgrades'; 5 + import { BallWeapon } from './weapons/ball'; 6 + 7 + type Upgrade = { 8 + upgrade: (player: Player) => void; 9 + perk: string; 10 + perkValue: string; 11 + 12 + icon: Icon; 13 + color: string; 14 + upgraded?: boolean; 15 + 16 + price: number; 17 + description?: string; 18 + }; 19 + /* 20 + - weapon fire rate 21 + - weapon shooting distance 22 + 23 + - bullet speed 24 + - bullet damage 25 + - bullet piercing 26 + 27 + - movement speed 28 + - player health 29 + - items stay longer 30 + */ 31 + const allUpgrades: Upgrade[][] = [ 32 + [ 33 + { 34 + upgrade: (player: Player) => (player.health = player.maxHealth), 35 + perk: 'Restore Health to', 36 + perkValue: '100%', 37 + icon: 'health', 38 + color: 'text-amber-500', 39 + price: 1 40 + } 41 + ], 42 + [ 43 + { 44 + upgrade: (player: Player) => (player.maxHealth *= 2), 45 + perk: 'Max Health', 46 + perkValue: '+10%', 47 + icon: 'health', 48 + color: 'text-green-500', 49 + price: 10 50 + } 51 + ], 52 + [ 53 + { 54 + upgrade: (player: Player) => (player.weapon.fireRate *= 0.9), 55 + perk: 'Gun cooldown', 56 + perkValue: '-10%', 57 + icon: 'fireRate', 58 + color: 'text-blue-500', 59 + price: 10 60 + } 61 + ], 62 + [ 63 + { 64 + upgrade: (player: Player) => (player.weapon.piercing += 1), 65 + perk: 'Piercing', 66 + perkValue: '+1', 67 + icon: 'piercing', 68 + color: 'text-red-500', 69 + price: 20 70 + } 71 + ], 72 + [ 73 + { 74 + upgrade: (player: Player) => { 75 + player.weapons.push(new BallWeapon(player.game, player.color)); 76 + }, 77 + perk: 'Chainsaw', 78 + perkValue: 'New Weapon', 79 + icon: 'new', 80 + color: 'text-red-500', 81 + price: 100 82 + }, 83 + { 84 + upgrade: (player: Player) => { 85 + // find chainsaw weapon 86 + let chainsaw = player.weapons.find((w) => w instanceof BallWeapon); 87 + if (chainsaw) { 88 + chainsaw.speed *= 2; 89 + } 90 + }, 91 + perk: 'Chainsaw Speed', 92 + perkValue: '+100%', 93 + icon: 'new', 94 + color: 'text-red-500', 95 + price: 200 96 + }, 97 + { 98 + upgrade: (player: Player) => { 99 + // find chainsaw weapon 100 + let chainsaw = player.weapons.find((w) => w instanceof BallWeapon); 101 + if (chainsaw) { 102 + chainsaw.damage *= 2; 103 + } 104 + }, 105 + perk: 'Chainsaw Damage', 106 + perkValue: '+100%', 107 + icon: 'new', 108 + color: 'text-red-500', 109 + price: 400 110 + } 111 + ] 112 + ]; 3 113 4 114 export class UpgradeManager { 5 115 colors_classes = ['bg-sky-500', 'bg-pink-500', 'bg-emerald-500']; 6 116 colors: number[] = []; 7 117 8 - items: number = 0; 9 - neededItems: number = 4; 118 + coins: number = 10; 119 + 120 + xp: number = 0; 121 + neededXP: number = 10; 10 122 11 123 game: Game; 12 124 13 - itemsUI?: HTMLElement; 125 + xpUI?: HTMLElement; 126 + 127 + coinsUI?: HTMLElement; 14 128 15 129 constructor(game: Game) { 16 130 this.game = game; 17 131 18 - this.itemsUI = document.getElementById('items') ?? undefined; 19 - } 132 + this.xpUI = document.getElementById('xp') ?? undefined; 20 133 21 - createUI() {} 134 + this.coinsUI = document.getElementById('coins') ?? undefined; 135 + } 22 136 23 137 reset() { 24 - this.items = 0; 138 + this.coins += this.xp; 139 + this.xp = 0; 25 140 26 141 // remove all items from UI 27 - if (this.itemsUI) { 28 - this.itemsUI.innerHTML = ''; 142 + if (this.xpUI) { 143 + this.xpUI.innerHTML = ''; 29 144 } 30 145 } 31 146 147 + resetAll() { 148 + this.reset(); 149 + this.coins = 10; 150 + 151 + for (let upgrade of allUpgrades) { 152 + for (let u of upgrade) { 153 + u.upgraded = false; 154 + } 155 + } 156 + } 157 + 158 + canAdvance() { 159 + return this.xp >= this.neededXP; 160 + } 161 + 32 162 addItem(item: Item) { 33 - this.items += item.value; 163 + this.xp += item.value; 34 164 35 165 // get percentage of needed: 36 - const percentage = (item.value / this.neededItems) * 100; 166 + const percentage = ((item.value / this.neededXP) * 100).toFixed(2); 37 167 // add to UI 38 - if (this.itemsUI) { 168 + if (this.xpUI) { 39 169 let itemUI = document.createElement('div'); 40 170 41 - itemUI.classList.add( 42 - 'h-full', 43 - 'transition-all', 44 - 'duration-200', 45 - this.colors_classes[item.type] 46 - ); 171 + itemUI.classList.add('h-full', 'transition-all', 'duration-200', 'bg-amber-500'); 47 172 // set width to percentage dwv 48 173 itemUI.style.width = '0px'; 49 174 setTimeout(() => { 50 175 itemUI.style.width = `${percentage}%`; 51 176 }, 100); 52 177 53 - this.itemsUI.appendChild(itemUI); 178 + this.xpUI.appendChild(itemUI); 179 + } 180 + } 181 + 182 + showUpgradeMenu() { 183 + let upgrades = document.getElementById('upgrades'); 184 + if (upgrades) { 185 + upgrades.classList.remove('hidden'); 186 + 187 + // get upgrade-container 188 + const upgradeContainer = document.getElementById('upgrades-container'); 189 + // if upgradeContainer exists 190 + if (upgradeContainer) { 191 + upgradeContainer.innerHTML = ''; 192 + 193 + let num = 0; 194 + 195 + // find all upgrades for the player (first not upgraded in each category) 196 + for (let i = 0; i < allUpgrades.length; i++) { 197 + let upgrade = allUpgrades[i]; 198 + let found: Upgrade | undefined = undefined; 199 + 200 + for (let j = 0; j < upgrade.length; j++) { 201 + if (!upgrade[j].upgraded) { 202 + found = upgrade[j]; 203 + break; 204 + } 205 + } 206 + if (!found) found = upgrade[upgrade.length - 1]; 207 + 208 + if (found.price > this.coins) continue; 209 + 210 + let option = addUpgradeOption({ 211 + index: num, 212 + total: allUpgrades.length, 213 + icon: found.icon, 214 + perk: found.perk, 215 + perkValue: found.perkValue, 216 + buy: () => { 217 + this.game.paused = false; 218 + 219 + for (let player of this.game.playerManager?.players ?? []) { 220 + found.upgrade(player); 221 + } 222 + 223 + found.upgraded = true; 224 + 225 + this.coins -= found.price; 226 + 227 + upgrades.classList.add('hidden'); 228 + this.coinsUI?.classList.add('hidden'); 229 + }, 230 + color: found.color, 231 + description: found.description, 232 + price: [{ color: 'bg-amber-500', amount: found.price }] 233 + }); 234 + 235 + upgradeContainer.appendChild(option); 236 + num += 1; 237 + } 238 + 239 + if (num == 0) { 240 + upgrades.classList.add('hidden'); 241 + return false; 242 + } 243 + } 54 244 } 245 + 246 + this.updateCoinsUI(); 247 + return true; 248 + } 249 + 250 + updateCoinsUI() { 251 + if (!this.coinsUI) return; 252 + 253 + this.coinsUI.classList.remove('hidden'); 254 + 255 + this.coinsUI.innerHTML = ''; 256 + 257 + const stat1 = document.createElement('span'); 258 + stat1.className = 'h-3 w-3 rounded-full relative bg-amber-500'; 259 + 260 + this.coinsUI.innerHTML += Math.floor(this.coins); 261 + 262 + const statGlow = document.createElement('span'); 263 + statGlow.className = 'absolute -inset-0.5 h-4 w-4 rounded-full blur-sm bg-amber-500'; 264 + stat1.appendChild(statGlow); 265 + 266 + this.coinsUI.appendChild(stat1); 55 267 } 56 268 }
+183 -49
src/upgrades.ts
··· 1 - function getIcon(icon: string) { 2 - return ` 3 - <svg 4 - class="h-6 w-6" 5 - fill="none" 6 - viewBox="0 0 24 24" 7 - stroke-width="1.5" 8 - stroke="currentColor" 9 - aria-hidden="true" 10 - > 11 - <path 12 - stroke-linecap="round" 13 - stroke-linejoin="round" 14 - d="M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z" 15 - /> 16 - </svg> 17 - `; 1 + export type Icon = 2 + | 'movement' 3 + | 'fireRate' 4 + | 'bulletSpeed' 5 + | 'damage' 6 + | 'shootingRange' 7 + | 'health' 8 + | 'piercing' 9 + | 'itemTime' 10 + | 'new'; 11 + 12 + const icons: Record<Icon, string> = { 13 + movement: `<svg 14 + xmlns="http://www.w3.org/2000/svg" 15 + fill="none" 16 + viewBox="0 0 24 24" 17 + stroke-width="1.5" 18 + stroke="currentColor" 19 + class="size-6" 20 + > 21 + <path 22 + stroke-linecap="round" 23 + stroke-linejoin="round" 24 + d="m5.25 4.5 7.5 7.5-7.5 7.5m6-15 7.5 7.5-7.5 7.5" 25 + /> 26 + </svg>`, 27 + fireRate: `<svg 28 + xmlns="http://www.w3.org/2000/svg" 29 + fill="none" 30 + viewBox="0 0 24 24" 31 + stroke-width="1.5" 32 + stroke="currentColor" 33 + class="size-6" 34 + > 35 + <path 36 + stroke-linecap="round" 37 + stroke-linejoin="round" 38 + d="M6.75 12a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0ZM12.75 12a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0ZM18.75 12a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0Z" 39 + /> 40 + </svg>`, 41 + bulletSpeed: `<svg 42 + xmlns="http://www.w3.org/2000/svg" 43 + fill="none" 44 + viewBox="0 0 24 24" 45 + stroke-width="1.5" 46 + stroke="currentColor" 47 + class="size-6" 48 + > 49 + <path 50 + stroke-linecap="round" 51 + stroke-linejoin="round" 52 + d="m12.75 15 3-3m0 0-3-3m3 3h-7.5M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" 53 + /> 54 + </svg>`, 55 + damage: `<svg 56 + xmlns="http://www.w3.org/2000/svg" 57 + fill="none" 58 + viewBox="0 0 24 24" 59 + stroke-width="1.5" 60 + stroke="currentColor" 61 + class="size-6" 62 + > 63 + <path 64 + stroke-linecap="round" 65 + stroke-linejoin="round" 66 + d="m3.75 13.5 10.5-11.25L12 10.5h8.25L9.75 21.75 12 13.5H3.75Z" 67 + /> 68 + </svg>`, 69 + shootingRange: `<svg 70 + xmlns="http://www.w3.org/2000/svg" 71 + fill="none" 72 + viewBox="0 0 24 24" 73 + stroke-width="1.5" 74 + stroke="currentColor" 75 + class="size-6" 76 + > 77 + <path 78 + stroke-linecap="round" 79 + stroke-linejoin="round" 80 + d="M3.75 3.75v4.5m0-4.5h4.5m-4.5 0L9 9M3.75 20.25v-4.5m0 4.5h4.5m-4.5 0L9 15M20.25 3.75h-4.5m4.5 0v4.5m0-4.5L15 9m5.25 11.25h-4.5m4.5 0v-4.5m0 4.5L15 15" 81 + /> 82 + </svg>`, 83 + health: `<svg 84 + xmlns="http://www.w3.org/2000/svg" 85 + fill="none" 86 + viewBox="0 0 24 24" 87 + stroke-width="1.5" 88 + stroke="currentColor" 89 + class="size-6" 90 + > 91 + <path 92 + stroke-linecap="round" 93 + stroke-linejoin="round" 94 + d="M21 8.25c0-2.485-2.099-4.5-4.688-4.5-1.935 0-3.597 1.126-4.312 2.733-.715-1.607-2.377-2.733-4.313-2.733C5.1 3.75 3 5.765 3 8.25c0 7.22 9 12 9 12s9-4.78 9-12Z" 95 + /> 96 + </svg>`, 97 + piercing: `<svg 98 + xmlns="http://www.w3.org/2000/svg" 99 + fill="none" 100 + viewBox="0 0 24 24" 101 + stroke-width="1.5" 102 + stroke="currentColor" 103 + class="size-6" 104 + > 105 + <path 106 + stroke-linecap="round" 107 + stroke-linejoin="round" 108 + d="M12 9v3.75m0-10.036A11.959 11.959 0 0 1 3.598 6 11.99 11.99 0 0 0 3 9.75c0 5.592 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.31-.21-2.57-.598-3.75h-.152c-3.196 0-6.1-1.25-8.25-3.286Zm0 13.036h.008v.008H12v-.008Z" 109 + /> 110 + </svg>`, 111 + itemTime: `<svg 112 + xmlns="http://www.w3.org/2000/svg" 113 + fill="none" 114 + viewBox="0 0 24 24" 115 + stroke-width="1.5" 116 + stroke="currentColor" 117 + class="size-6" 118 + > 119 + <path 120 + stroke-linecap="round" 121 + stroke-linejoin="round" 122 + d="M12 6v6h4.5m4.5 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" 123 + /> 124 + </svg>`, 125 + new: `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6"> 126 + <path stroke-linecap="round" stroke-linejoin="round" d="M12 9v6m3-3H9m12 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" /> 127 + </svg>` 128 + }; 129 + 130 + function getIcon(icon: Icon) { 131 + return icons[icon]; 18 132 } 19 133 20 134 type UpgradeOption = { 21 135 index: number; 22 136 total: number; 23 137 24 - iconName?: string; 138 + icon?: Icon; 25 139 26 140 disabled?: boolean; 141 + 142 + description?: string; 27 143 28 144 perk?: string; 29 145 perkValue?: string; ··· 39 155 bought?: boolean; 40 156 41 157 empty?: boolean; 158 + 159 + buy?: () => void; 160 + 161 + color: string; 42 162 }; 43 163 44 164 export function addUpgradeOption(options: UpgradeOption) { ··· 51 171 // if index === total - 1 52 172 // rounded-bl-lg rounded-br-lg sm:rounded-bl-none 53 173 const upgrade = document.createElement('div'); 54 - upgrade.className = 55 - 'group relative ring-1 ring-inset ring-white/10 p-6 hover:ring-2 hover:ring-inset hover:ring-white'; 174 + upgrade.className = 'group relative ring-1 ring-inset ring-white/10 p-6 overflow-hidden'; 56 175 if (options.index === 0) { 57 176 upgrade.classList.add('rounded-tl-lg', 'rounded-tr-lg', 'sm:rounded-tr-none'); 58 177 } else if (options.index === 1) { ··· 63 182 upgrade.classList.add('rounded-bl-lg', 'rounded-br-lg', 'sm:rounded-bl-none'); 64 183 } 65 184 185 + // add hover glow 186 + let glow = document.createElement('div'); 187 + glow.className = 188 + 'absolute inset-0 ring-2 ring-inset ring-amber-500 blur-sm opacity-0 group-hover:opacity-50 transition-opacity duration-200'; 189 + upgrade.appendChild(glow); 190 + 66 191 if (options.disabled) { 67 192 upgrade.classList.add('opacity-30', 'pointer-events-none'); 68 193 } ··· 77 202 return upgrade; 78 203 } 79 204 80 - const icon = document.createElement('span'); 81 - icon.className = 'inline-flex rounded-lg bg-white/5 p-3 text-rose-500 ring-0'; 82 - icon.innerHTML = getIcon(options.iconName ?? ''); 205 + if (options.icon) { 206 + const icon = document.createElement('span'); 207 + icon.className = 'inline-flex rounded-lg bg-white/5 p-3 ring-0 ' + options.color; 208 + icon.innerHTML = getIcon(options.icon); 209 + 210 + upgrade.appendChild(icon); 211 + } 83 212 84 213 const div = document.createElement('div'); 85 214 div.className = 'mt-8'; ··· 94 223 const spanText = document.createElement('span'); 95 224 spanText.innerText = options.perk + ' '; 96 225 const spanPercentage = document.createElement('span'); 97 - spanPercentage.className = 'text-sky-500'; 226 + spanPercentage.className = options.color; 98 227 spanPercentage.innerText = options.perkValue ?? ''; 99 228 100 229 spanText.appendChild(spanPercentage); ··· 104 233 105 234 div.appendChild(title); 106 235 107 - const description = document.createElement('p'); 108 - description.innerHTML = options.minusStat + ' '; 109 - description.className = 'mt-2 text-sm text-gray-300 font-semibold'; 110 - const descriptionText = document.createElement('span'); 111 - descriptionText.className = 'text-rose-500'; 112 - descriptionText.innerText = options.minusStatValue ?? ''; 113 - description.appendChild(descriptionText); 236 + if (options.description) { 237 + const description = document.createElement('p'); 238 + description.innerHTML = options.description + ' '; 239 + description.className = 'mt-2 text-sm text-gray-300 font-semibold'; 240 + div.appendChild(description); 241 + } 114 242 115 - const stats = document.createElement('p'); 116 - stats.className = 'mt-2 text-sm text-gray-300 font-semibold flex items-center gap-2 justify-end'; 243 + if (options.price) { 244 + const stats = document.createElement('p'); 245 + stats.className = 246 + 'absolute bottom-6 right-6 mt-2 text-sm text-gray-300 font-semibold flex items-center gap-2 justify-end'; 117 247 118 - let count = (options.price ?? []).length; 119 - for (let i = 0; i < count; i++) { 120 - const stat1 = document.createElement('span'); 121 - stat1.className = 122 - 'h-3 w-3 rounded-full relative ' + options.price?.[i].color + (i < count - 1 ? ' mr-2' : ''); 248 + let count = (options.price ?? []).length; 249 + for (let i = 0; i < count; i++) { 250 + const stat1 = document.createElement('span'); 251 + stat1.className = 252 + 'h-3 w-3 rounded-full relative ' + 253 + options.price?.[i].color + 254 + (i < count - 1 ? ' mr-2' : ''); 255 + 256 + stats.innerHTML += options.price?.[i].amount; 123 257 124 - stats.innerHTML += options.price?.[i].amount; 258 + const statGlow = document.createElement('span'); 259 + statGlow.className = 260 + 'absolute -inset-0.5 h-4 w-4 rounded-full blur-sm ' + options.price?.[i].color; 261 + stat1.appendChild(statGlow); 125 262 126 - const statGlow = document.createElement('span'); 127 - statGlow.className = 128 - 'absolute -inset-0.5 h-4 w-4 rounded-full blur-sm ' + options.price?.[i].color; 129 - stat1.appendChild(statGlow); 263 + stats.appendChild(stat1); 264 + } 130 265 131 - stats.appendChild(stat1); 266 + div.appendChild(stats); 132 267 } 133 - 134 - upgrade.appendChild(icon); 135 - div.appendChild(title); 136 - div.appendChild(description); 137 - div.appendChild(stats); 138 268 139 269 upgrade.appendChild(div); 140 270 ··· 167 297 } 168 298 if (options.bought) { 169 299 return; 300 + } 301 + 302 + if (options.buy) { 303 + options.buy(); 170 304 } 171 305 172 306 // remove hover:ring-2 hover:ring-inset hover:ring-white
+29 -11
src/wave.ts
··· 102 102 this.collectEnemyUpgrades(); 103 103 104 104 this.currentWave++; 105 - this.game.upgradeManager.neededItems = this.data[this.currentWave].neededItems; 106 - console.log('Starting wave', this.currentWave, this.game.upgradeManager.neededItems); 105 + this.game.upgradeManager.neededXP = this.data[this.currentWave].neededItems; 107 106 } 108 107 } 109 108 ··· 143 142 { 144 143 enemies: [ 145 144 { 146 - type: PentagonEnemy, 145 + type: SphereEnemy, 147 146 spawnRate: 1, 148 147 upgradeFunction: (enemy) => { 149 148 enemy.health = 20; ··· 154 153 type: TriangleEnemy, 155 154 spawnRate: 1, 156 155 upgradeFunction: (enemy) => { 157 - enemy.speed *= 1.5; 156 + enemy.speed *= 1.2; 157 + enemy.value = 2; 158 158 } 159 159 } 160 160 ], 161 161 startDelay: 3, 162 - neededItems: 4 162 + neededItems: 10 163 163 }, 164 164 { 165 165 enemies: [ ··· 168 168 spawnRate: 1, 169 169 upgradeFunction: (enemy) => { 170 170 enemy.health = 30; 171 + enemy.value += 1; 172 + if (enemy instanceof SphereEnemy) { 173 + enemy.burstNumber *= 1.3; 174 + } 171 175 } 172 176 }, 173 177 { ··· 175 179 spawnRate: 0.1, 176 180 upgradeFunction: (enemy) => { 177 181 if (enemy instanceof PentagonEnemy) { 178 - enemy.weapon.fireRate = 1000; 182 + enemy.weapon.fireRate *= 0.9; 179 183 } 180 184 } 181 185 } ··· 189 193 type: PentagonEnemy, 190 194 spawnRate: 1, 191 195 upgradeFunction: (enemy) => { 192 - if (enemy instanceof PentagonEnemy) { 193 - enemy.weapon.fireRate = 100; 194 - } 196 + enemy.health *= 1.5; 195 197 } 196 198 }, 197 199 { 198 200 type: TriangleEnemy, 199 201 spawnRate: 2, 200 202 upgradeFunction: (enemy) => { 201 - enemy.speed *= 1.05; 203 + enemy.speed *= 1.1; 204 + enemy.value *= 1.5; 205 + } 206 + } 207 + ], 208 + startDelay: 3, 209 + neededItems: 50 210 + }, 211 + { 212 + enemies: [ 213 + { 214 + type: SphereEnemy, 215 + spawnRate: 3, 216 + upgradeFunction: (enemy) => { 217 + enemy.value *= 1.5; 218 + if (enemy instanceof SphereEnemy) { 219 + enemy.weapon.damage *= 1.5; 220 + } 202 221 } 203 222 } 204 223 ], 205 224 startDelay: 3, 206 225 neededItems: 100 207 226 } 208 - // Add more waves as needed 209 227 ];
+13 -1
src/weapons/ball.ts
··· 10 10 */ 11 11 export class BallWeapon extends Weapon { 12 12 distance: number = 200; 13 - speed: number = 0.002; 13 + speed: number = 0.001; 14 14 angle: number = 0; 15 15 16 16 constructor(game: Game, color: number) { ··· 56 56 set y(value: number) { 57 57 if (this.shape) this.shape.y = value; 58 58 this.rigidBody?.setTranslation({ x: this.shape?.x ?? 0, y: -value }, true); 59 + } 60 + 61 + get x() { 62 + if (this.shape) return this.shape.x; 63 + 64 + return this.rigidBody?.translation().x ?? 0; 65 + } 66 + 67 + get y() { 68 + if (this.shape) return this.shape.y; 69 + 70 + return -(this.rigidBody?.translation().y ?? 0); 59 71 } 60 72 61 73 update(deltaTime: number, x: number, y: number) {