import { INTERNAL_WIDTH, INTERNAL_HEIGHT } from '../constants'; import type { Color, Sprite } from '../types'; export type BulletDirection = 'left' | 'forward' | 'right'; export interface Bullet { x: number; y: number; prevX: number; prevY: number; vx: number; vy: number; width: number; height: number; color: Color; active: boolean; isPlayerBullet: boolean; damage: number; poolIndex: number; // Track position in pool for O(1) deactivation } // Bullet pool for performance const MAX_BULLETS = 2000; // Bullet constants const PLAYER_BULLET_SPEED = 250; // pixels per second const PLAYER_BULLET_WIDTH = 3; const PLAYER_BULLET_HEIGHT = 8; const PLAYER_BULLET_COLOR: Color = { r: 1.0, g: 0.9, b: 0.3, a: 1.0 }; // Yellow // Direction angles (in radians from vertical) const DIRECTION_ANGLES: Record = { left: -Math.PI / 6, // -30 degrees forward: 0, // straight up right: Math.PI / 6, // +30 degrees }; export class BulletSystem { private bullets: Bullet[] = []; private activeCount = 0; // Free list for O(1) bullet allocation private freeList: number[] = []; private freeListHead = 0; // Cached arrays to avoid allocations private playerBulletCache: Bullet[] = []; private enemyBulletCache: Bullet[] = []; private spriteCache: Sprite[] = []; constructor() { // Pre-allocate bullet pool for (let i = 0; i < MAX_BULLETS; i++) { this.bullets.push({ x: 0, y: 0, prevX: 0, prevY: 0, vx: 0, vy: 0, width: PLAYER_BULLET_WIDTH, height: PLAYER_BULLET_HEIGHT, color: PLAYER_BULLET_COLOR, active: false, isPlayerBullet: true, damage: 1, poolIndex: i, }); // Initialize free list this.freeList.push(i); } this.freeListHead = MAX_BULLETS; // Pre-allocate cache arrays this.playerBulletCache = new Array(MAX_BULLETS); this.enemyBulletCache = new Array(MAX_BULLETS); this.spriteCache = new Array(MAX_BULLETS); for (let i = 0; i < MAX_BULLETS; i++) { this.spriteCache[i] = { x: 0, y: 0, width: 0, height: 0, color: { r: 0, g: 0, b: 0, a: 0 } }; } } spawnPlayerBullet( x: number, y: number, direction: BulletDirection, speedMultiplier: number = 1, damageMultiplier: number = 1 ): void { const bullet = this.getInactiveBullet(); if (!bullet) return; const angle = DIRECTION_ANGLES[direction]; const speed = PLAYER_BULLET_SPEED * speedMultiplier; bullet.x = x - PLAYER_BULLET_WIDTH / 2; bullet.y = y; bullet.prevX = bullet.x; bullet.prevY = bullet.y; bullet.vx = Math.sin(angle) * speed; bullet.vy = -Math.cos(angle) * speed; // Negative because Y goes down bullet.width = PLAYER_BULLET_WIDTH; bullet.height = PLAYER_BULLET_HEIGHT; bullet.color = PLAYER_BULLET_COLOR; bullet.active = true; bullet.isPlayerBullet = true; bullet.damage = damageMultiplier; } spawnEnemyBullet( x: number, y: number, vx: number, vy: number, color: Color = { r: 1.0, g: 0.3, b: 0.3, a: 1.0 } ): void { const bullet = this.getInactiveBullet(); if (!bullet) return; bullet.x = x; bullet.y = y; bullet.prevX = x; bullet.prevY = y; bullet.vx = vx; bullet.vy = vy; bullet.width = 4; bullet.height = 4; bullet.color = color; bullet.active = true; bullet.isPlayerBullet = false; bullet.damage = 1; } private getInactiveBullet(): Bullet | null { // O(1) allocation from free list if (this.freeListHead <= 0) { return null; } this.freeListHead--; const index = this.freeList[this.freeListHead]; return this.bullets[index]; } private returnToFreeList(bullet: Bullet): void { // O(1) return to free list if (this.freeListHead < MAX_BULLETS) { this.freeList[this.freeListHead] = bullet.poolIndex; this.freeListHead++; } } update(dt: number): void { this.activeCount = 0; let playerCount = 0; let enemyCount = 0; for (let i = 0; i < MAX_BULLETS; i++) { const bullet = this.bullets[i]; if (!bullet.active) continue; // Store previous position bullet.prevX = bullet.x; bullet.prevY = bullet.y; // Update position bullet.x += bullet.vx * dt; bullet.y += bullet.vy * dt; // Check bounds (with margin) const margin = 20; if ( bullet.x < -margin || bullet.x > INTERNAL_WIDTH + margin || bullet.y < -margin || bullet.y > INTERNAL_HEIGHT + margin ) { bullet.active = false; this.returnToFreeList(bullet); continue; } // Cache bullets by type for collision detection if (bullet.isPlayerBullet) { this.playerBulletCache[playerCount++] = bullet; } else { this.enemyBulletCache[enemyCount++] = bullet; } this.activeCount++; } // Store counts for getters (this as any)._playerBulletCount = playerCount; (this as any)._enemyBulletCount = enemyCount; } // Iterator for sprites - avoids array allocation *iterateSprites(alpha: number): Generator { for (let i = 0; i < MAX_BULLETS; i++) { const bullet = this.bullets[i]; if (!bullet.active) continue; // Interpolate position const x = bullet.prevX + (bullet.x - bullet.prevX) * alpha; const y = bullet.prevY + (bullet.y - bullet.prevY) * alpha; // Reuse cached sprite object const sprite = this.spriteCache[i]; sprite.x = x; sprite.y = y; sprite.width = bullet.width; sprite.height = bullet.height; sprite.color = bullet.color; yield sprite; } } // Batch render method - more efficient than getSprites forEachSprite(alpha: number, callback: (sprite: Sprite) => void): void { for (let i = 0; i < MAX_BULLETS; i++) { const bullet = this.bullets[i]; if (!bullet.active) continue; // Interpolate position const x = bullet.prevX + (bullet.x - bullet.prevX) * alpha; const y = bullet.prevY + (bullet.y - bullet.prevY) * alpha; // Reuse cached sprite object const sprite = this.spriteCache[i]; sprite.x = x; sprite.y = y; sprite.width = bullet.width; sprite.height = bullet.height; sprite.color = bullet.color; callback(sprite); } } getSprites(alpha: number): Sprite[] { // Backward compatible version - still allocates but reuses sprite data const sprites: Sprite[] = []; for (let i = 0; i < MAX_BULLETS; i++) { const bullet = this.bullets[i]; if (!bullet.active) continue; // Interpolate position const x = bullet.prevX + (bullet.x - bullet.prevX) * alpha; const y = bullet.prevY + (bullet.y - bullet.prevY) * alpha; sprites.push({ x, y, width: bullet.width, height: bullet.height, color: bullet.color, }); } return sprites; } // Get active bullets for collision detection - returns cached view getPlayerBullets(): Bullet[] { const count = (this as any)._playerBulletCount || 0; // Return a slice of the cached array (still allocates, but much smaller) return this.playerBulletCache.slice(0, count); } getEnemyBullets(): Bullet[] { const count = (this as any)._enemyBulletCount || 0; return this.enemyBulletCache.slice(0, count); } // Zero-allocation collision iteration forEachPlayerBullet(callback: (bullet: Bullet) => boolean | void): void { const count = (this as any)._playerBulletCount || 0; for (let i = 0; i < count; i++) { if (callback(this.playerBulletCache[i]) === false) break; } } forEachEnemyBullet(callback: (bullet: Bullet) => boolean | void): void { const count = (this as any)._enemyBulletCount || 0; for (let i = 0; i < count; i++) { if (callback(this.enemyBulletCache[i]) === false) break; } } getPlayerBulletCount(): number { return (this as any)._playerBulletCount || 0; } getEnemyBulletCount(): number { return (this as any)._enemyBulletCount || 0; } getBulletDamage(bullet: Bullet): number { return bullet.damage; } deactivateBullet(bullet: Bullet): void { if (bullet.active) { bullet.active = false; this.returnToFreeList(bullet); } } getActiveCount(): number { return this.activeCount; } clear(): void { // Reset free list this.freeListHead = MAX_BULLETS; for (let i = 0; i < MAX_BULLETS; i++) { this.bullets[i].active = false; this.freeList[i] = i; } this.activeCount = 0; (this as any)._playerBulletCount = 0; (this as any)._enemyBulletCount = 0; } }