import { Zombie } from './Zombie.js'; import { GameObject } from '../engine/GameObject.js'; import { Vector2D } from '../engine/Vector2D.js'; import { WitchProjectile } from './WitchProjectile.js'; export class Witch extends Zombie { constructor( x, y, target, { container, viewport, speed = 85, size = 28, color = [0.55, 0.2, 0.8, 1], health = 5, knockback = 180, scoreValue = 4, preferredDistance = 220, fireInterval = 2.2, projectileSpeed = 220, projectileCount = 1, projectileSpread = 0.08, projectileColor = [0.7, 0.2, 1, 1], emergeTime = 0, spawnProjectile = (p) => {}, onDeath = null, } = {}, ) { super(x, y, target, { container, viewport, speed, size, color, health, knockback, scoreValue, emergeTime, onDeath, }); this.preferredDistance = preferredDistance; this.fireInterval = fireInterval; this.projectileSpeed = projectileSpeed; this.projectileCount = projectileCount; this.projectileSpread = projectileSpread; this.projectileColor = projectileColor; this.spawnProjectile = spawnProjectile; this._fireTimer = 0; this._time = 0; this._cast = 0; } update(dt) { this._time += dt; this._cast = Math.max(0, this._cast - dt); if (this._updateEmerge(dt)) { return; } this._fireTimer -= dt; const delta = this.target.pos.subV(this.pos); const distance = delta.length(); let move = new Vector2D(0, 0); if (distance > 1) { const dir = delta.divS(distance); if (distance < this.preferredDistance) { move = dir.mulS(-this.speed); } else if (distance > this.preferredDistance + 40) { move = dir.mulS(this.speed); } } this.velocity = move.addV(this._hitKnockback); GameObject.prototype.update.call(this, dt); this._hitKnockback = this._hitKnockback.mulS(Math.max(0, 1 - dt * this.knockbackDamping)); this._flash = Math.max(0, this._flash - dt); if (this._fireTimer <= 0 && this.target.health > 0 && distance < 480) { this._fireTimer = this.fireInterval * (0.8 + Math.random() * 0.4); this.fire(); } } _onEmergenceComplete() { this._fireTimer = this.fireInterval * (0.4 + Math.random() * 0.4); } fire() { this._cast = 0.22; const aim = this.target.pos.subV(this.pos); const aimAngle = aim.lengthSqrt() > 1 ? Math.atan2(aim.y, aim.x) : this._time; const offset = ((this.projectileCount - 1) / 2) * this.projectileSpread; for (let i = 0; i < this.projectileCount; i += 1) { const angle = aimAngle - offset + i * this.projectileSpread; const direction = new Vector2D(Math.cos(angle), Math.sin(angle)); const projectile = new WitchProjectile(this.pos.x, this.pos.y, direction, { container: this.container, viewport: this.viewport, speed: this.projectileSpeed, color: this.projectileColor, }); this.spawnProjectile(projectile); } } draw(renderer) { this._drawEmerge(renderer); let s = this.size * this._emergeSize(); let bodyColor = this.baseColor; if (this._flash > 0) { bodyColor = [1, 1, 1, 1]; } else if (this.maxHealth > 1) { const hpRatio = this.health / this.maxHealth; bodyColor = [ this.baseColor[0] + (1 - this.baseColor[0]) * (1 - hpRatio), this.baseColor[1] * hpRatio, this.baseColor[2] * hpRatio, 1, ]; } const bob = Math.sin(this._time * 3.4) * s * 0.09; const sway = Math.sin(this._time * 1.7) * 0.09; const castRatio = Math.min(1, this._cast / 0.22); const charge = Math.max( 0, Math.min(1, (this.fireInterval - this._fireTimer) / this.fireInterval), ); const targetSide = this.target.pos.x < this.pos.x ? -1 : 1; const flightSide = Math.abs(this.velocity.x) > 20 ? (this.velocity.x < 0 ? -1 : 1) : targetSide; const facing = flightSide; const lean = facing * s * 0.05; 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), ); }; const robe = s * 0.9; const robeW = robe * (1 + Math.sin(this._time * 3.4) * 0.05); const cy = this.pos.y + bob; renderer.drawTexture( renderer.whiteTexture, this.pos.x + lean - robeW / 2, cy - robe / 2, robeW, robe, bodyColor, ); const armColor = [bodyColor[0] * 0.72, bodyColor[1] * 0.72, bodyColor[2] * 0.72, 1]; const armW = s * 0.16; const armLen = s * (0.3 + castRatio * 0.26); const armSpread = s * (0.36 + castRatio * 0.14); const armY = cy - robe * 0.2 - castRatio * s * 0.14; for (let i = 0; i < 2; i += 1) { const sign = i === 0 ? 1 : -1; const raise = Math.sin(this._time * 6) * castRatio * s * 0.06; renderer.drawTexture( renderer.whiteTexture, this.pos.x + sign * armSpread - armW / 2, armY - armLen / 2 + sign * raise, armW, armLen, armColor, ); } const hatW = s * 0.7; const hatH = s * 0.9; const hatCx = this.pos.x + lean + sway * s * 0.14; const hatCy = cy - robe / 2 - hatH * 0.55 + bob * 0.4; const hatAngle = sway * 0.8 + castRatio * -0.08; renderer.drawTexture( renderer.whiteTexture, hatCx - hatW / 2, hatCy - hatH / 2, hatW, hatH, [0.15, 0.08, 0.2, 1], hatAngle, ); const brimW = s * 0.98; const brimH = s * 0.14; renderer.drawTexture( renderer.whiteTexture, hatCx - brimW / 2, hatCy + hatH * 0.1 - brimH / 2, brimW, brimH, [0.15, 0.08, 0.2, 1], hatAngle, ); const seatY = cy + robe * 0.14 + Math.sin(this._time * 2.3) * s * 0.02; const seatX = this.pos.x + lean; const handleX = seatX + facing * s * 0.5; const bristleX = seatX - facing * s * 0.24; drawLimb(handleX, seatY, bristleX, seatY, s * 0.13, [0.4, 0.3, 0.15, 1]); drawLimb(bristleX, seatY - s * 0.02, bristleX - facing * s * 0.14, seatY - s * 0.09, s * 0.18, [0.62, 0.48, 0.24, 1]); if (charge > 0.25) { const eyeFade = (charge - 0.25) / 0.75; const eyeY = hatCy + hatH * 0.32; const eyeSpread = s * 0.13; const eyeS = s * 0.11; const eyeCx = this.pos.x + lean + targetSide * s * 0.06; renderer.setBlendMode('additive'); renderer.drawTexture( renderer.glowTexture, eyeCx - eyeSpread - eyeS, eyeY - eyeS, eyeS * 2, eyeS * 2, [0.85, 0.35, 1, 0.75 * eyeFade], ); renderer.drawTexture( renderer.glowTexture, eyeCx + eyeSpread - eyeS, eyeY - eyeS, eyeS * 2, eyeS * 2, [0.85, 0.35, 1, 0.75 * eyeFade], ); renderer.setBlendMode('normal'); } if (castRatio > 0) { renderer.setBlendMode('additive'); const fx = this.pos.x; const fy = hatCy - hatH * 0.72; renderer.drawTexture( renderer.glowTexture, fx - s * 0.5, fy - s * 0.5, s, s, [0.8, 0.4, 1, 0.9 * castRatio], ); renderer.setBlendMode('normal'); } } }