import { GameObject } from '../engine/GameObject.js'; import { Vector2D } from '../engine/Vector2D.js'; import { Bullet } from './Bullet.js'; export class Player extends GameObject { constructor( { input, container, viewport, moveSpeed = 420, fireRate = 0.2, recoilSpeed = 100, recoilDamping = 15, bulletSpeed = 800, bulletSize = 8, bulletCount = 1, bulletSpread = 0.09, damage = 1, maxUpgradeLevel = 3, health = 3, particles = null, sounds = null, size = 24, } = {}, ) { super(viewport.width / 2, viewport.height / 2); this.input = input; this.container = container; this.viewport = viewport; this.moveSpeed = moveSpeed; this.fireRate = fireRate; this.bulletSpeed = bulletSpeed; this.bulletSize = bulletSize; this.bulletCount = bulletCount; this.bulletSpread = bulletSpread; this.damage = damage; this.maxUpgradeLevel = maxUpgradeLevel; this.upgrades = { rapid: 0, damage: 0, multishot: 0, speed: 0 }; this.recoilSpeed = recoilSpeed; this.recoilDamping = recoilDamping; this.health = health; this.maxHealth = health; this.particles = particles; this.sounds = sounds; this.size = size; this._cooldown = 0; this._aimDirection = new Vector2D(1, 0); this._recoil = new Vector2D(0, 0); this._damageCooldown = 0; this._time = 0; } update(dt) { const { width, height } = this.viewport; this._time += dt; const movement = new Vector2D(0, 0); const { keys } = this.input; if (keys.has('w') || keys.has('arrowup')) movement.y -= 1; if (keys.has('s') || keys.has('arrowdown')) movement.y += 1; if (keys.has('a') || keys.has('arrowleft')) movement.x -= 1; if (keys.has('d') || keys.has('arrowright')) movement.x += 1; let moveVelocity = new Vector2D(0, 0); if (movement.lengthSqrt() > 0) { moveVelocity = movement.normalize().mulS(this.moveSpeed); } this.velocity = this._recoil.lengthSqrt() > 1 ? moveVelocity.addV(this._recoil) : moveVelocity; super.update(dt); const maxX = Math.max(this.width / 2, 0); const maxY = Math.max(this.height / 2, 0); this.pos.x = Math.min(Math.max(this.pos.x, maxX), width - maxX); this.pos.y = Math.min(Math.max(this.pos.y, maxY), height - maxY); let aim = this.input.mouse.subV(this.pos); if (aim.lengthSqrt() > 1) { this._aimDirection = aim.divS(aim.length()); } this._recoil = this._recoil.mulS(Math.max(0, 1 - dt * this.recoilDamping)); this._damageCooldown = Math.max(0, this._damageCooldown - dt); this._cooldown -= dt; if (this._cooldown <= 0) { this._cooldown = this.fireRate; this.shoot(); } } shoot() { const aimAngle = Math.atan2(this._aimDirection.y, this._aimDirection.x); this._recoil = this._aimDirection.mulS(-this.recoilSpeed); this.sounds?.shoot(); this.particles?.burst(this.pos.x, this.pos.y, { count: 6, speed: 120, direction: aimAngle, spread: 0.7, life: 0.25, size: 4, color: [1, 0.85, 0.3, 1], gravity: 0, }); this.particles?.burst(this.pos.x, this.pos.y, { count: 4, speed: 260, direction: aimAngle, spread: 0.25, life: 0.14, size: 5, color: [1, 1, 1, 1], gravity: 0, }); this.particles?.burst(this.pos.x, this.pos.y, { count: 5, speed: 70, spread: Math.PI * 2, life: 0.4, size: 4, color: [0.6, 0.6, 0.6, 1], gravity: 0, }); const offset = ((this.bulletCount - 1) / 2) * this.bulletSpread; for (let i = 0; i < this.bulletCount; i += 1) { const angle = aimAngle - offset + i * this.bulletSpread; const direction = new Vector2D(Math.cos(angle), Math.sin(angle)); const bullet = new Bullet(this.pos.x, this.pos.y, direction, { container: this.container, viewport: this.viewport, speed: this.bulletSpeed, size: this.bulletSize, damage: this.damage, }); this.container?.push(bullet); } } applyUpgrade(type) { if (!(type in this.upgrades)) return false; if (this.upgrades[type] >= this.maxUpgradeLevel) return false; this.upgrades[type] += 1; switch (type) { case 'rapid': this.fireRate = Math.max(0.06, this.fireRate * 0.8); break; case 'damage': this.damage += 1; break; case 'multishot': this.bulletCount = Math.min(7, this.bulletCount + 1); break; case 'speed': this.bulletSpeed = Math.min(1800, this.bulletSpeed * 1.15); break; default: break; } return true; } takeDamage() { if (this._damageCooldown > 0) return this.health; this._damageCooldown = 0.5; this.health -= 1; this.sounds?.damage(); return this.health; } draw(renderer) { const s = this.size; this.width = s; this.height = s; let alpha = 1; if (this._damageCooldown > 0) { alpha = Math.sin(this._time * 30) > 0 ? 1 : 0.25; } const moving = this.velocity.lengthSqrt() > 10; const facing = Math.atan2(this._aimDirection.y, this._aimDirection.x); const dirX = Math.cos(facing); const dirY = Math.sin(facing); const perpX = -dirY; const perpY = dirX; const bob = Math.sin(this._time * (moving ? 11 : 2)) * s * (moving ? 0.04 : 0.015); const lean = facing * 0.35; const pull = Math.min(1, this._recoil.lengthSqrt() / Math.max(1, this.recoilSpeed)); const kickX = -this._recoil.x * 0.04; const kickY = -this._recoil.y * 0.04; const ox = this.pos.x + kickX; const oy = this.pos.y + kickY + bob; const armor = [0.17, 0.21, 0.3, alpha]; const armorLight = [0.3, 0.36, 0.48, alpha]; const armorDark = [0.09, 0.11, 0.16, alpha]; const accent = [0.34, 0.96, 1, alpha]; const drawLimb = (ax, ay, bx, by, thickness, limbColor) => { const len = Math.hypot(bx - ax, by - ay); if (len < 1) return; renderer.drawTexture( renderer.whiteTexture, (ax + bx) / 2 - len / 2, (ay + by) / 2 - thickness / 2, len, thickness, limbColor, Math.atan2(by - ay, bx - ax), ); }; renderer.drawTexture( renderer.whiteTexture, this.pos.x - s * 0.85, this.pos.y - s * 0.3 + s * 0.14, s * 1.7, s * 0.56, [0, 0, 0, 0.35], ); const capeX = ox - dirX * s * (0.42 + 0.05 * Math.sin(this._time * (moving ? 6 : 2))); const capeY = oy - dirY * s * (0.42 + 0.05 * Math.sin(this._time * (moving ? 6 : 2))) + s * 0.05; renderer.drawTexture( renderer.whiteTexture, capeX - s * 0.26, capeY - s * 0.26, s * 0.52, s * 0.52, armorDark, facing, ); const gripX = ox + dirX * s * (0.22 - 0.14 * pull); const gripY = oy + dirY * s * (0.22 - 0.14 * pull); drawLimb(ox - dirX * s * 0.08, oy, gripX - dirX * s * 0.14, gripY, s * 0.11, armorDark); for (let i = 0; i < 2; i += 1) { const sign = i === 0 ? 1 : -1; const step = Math.sin(this._time * 13 + i * Math.PI) * s * (moving ? 0.1 : 0.02); const hx = ox + perpX * s * 0.14 * sign; const hy = oy + perpY * s * 0.14 * sign; renderer.drawTexture( renderer.whiteTexture, hx + dirX * step - s * 0.07, hy + s * 0.14 - s * 0.09, s * 0.14, s * 0.18, i === 0 ? armorDark : armor, ); } renderer.drawTexture( renderer.whiteTexture, ox - s * 0.25, oy - s * 0.31, s * 0.5, s * 0.62, armor, lean * 0.4, ); renderer.drawTexture( renderer.whiteTexture, ox + dirX * s * 0.03 - s * 0.18, oy - s * 0.25, s * 0.36, s * 0.4, armorLight, lean * 0.4, ); renderer.drawTexture( renderer.whiteTexture, ox - s * 0.03, oy - s * 0.08, s * 0.06, s * 0.06, accent, ); renderer.drawTexture( renderer.whiteTexture, ox - s * 0.25, oy + s * 0.1 - s * 0.03, s * 0.5, s * 0.06, armorDark, lean * 0.4, ); for (let i = 0; i < 2; i += 1) { const sign = i === 0 ? 1 : -1; renderer.drawTexture( renderer.whiteTexture, ox + perpX * s * 0.3 * sign - s * 0.11, oy + perpY * s * 0.3 * sign - s * 0.2 - s * 0.11, s * 0.22, s * 0.22, armorDark, ); } const hx = ox + dirX * s * 0.06; const hy = oy - s * 0.44; renderer.drawTexture( renderer.whiteTexture, hx - s * 0.18, hy - s * 0.17, s * 0.36, s * 0.34, armor, lean * 0.4, ); const visorLen = s * 0.26; const visorX = hx + dirX * s * 0.09 - visorLen / 2; const visorY = hy + dirY * s * 0.09 - s * 0.045; renderer.drawTexture( renderer.whiteTexture, visorX - s * 0.04, visorY - s * 0.04, visorLen + s * 0.08, s * 0.17, [accent[0], accent[1], accent[2], 0.22 * alpha], facing, ); renderer.drawTexture( renderer.whiteTexture, visorX, visorY, visorLen, s * 0.09, accent, facing, ); const reach = s * (0.5 - 0.18 * pull); const gunX = ox + dirX * (s * 0.18 + reach * 0.4); const gunY = oy + dirY * (s * 0.18 + reach * 0.4); const muzzleX = gunX + dirX * (reach * 0.6 + s * 0.4); const muzzleY = gunY + dirY * (reach * 0.6 + s * 0.4); drawLimb(gunX, gunY, muzzleX, muzzleY, s * 0.12, [0.06, 0.08, 0.12, alpha]); renderer.drawTexture( renderer.whiteTexture, muzzleX - s * 0.04, muzzleY - s * 0.04, s * 0.08, s * 0.08, accent, ); renderer.drawTexture( renderer.whiteTexture, gunX - dirX * s * 0.08 - s * 0.09, gunY - dirY * s * 0.08 - s * 0.09, s * 0.18, s * 0.18, armorDark, facing, ); } }