import { GameObject } from '../engine/GameObject.js'; import { Vector2D } from '../engine/Vector2D.js'; export class Zombie extends GameObject { constructor( x, y, target, { container, viewport, speed = 110, size = 24, color = [0.15, 0.65, 0.2, 1], health = 1, knockback = 260, knockbackDamping = 10, scoreValue = 1, emergeTime = 0, onDeath = null, } = {}, ) { super(x, y); this.target = target; this.container = container; this.viewport = viewport; this.speed = speed; this.size = size; this.baseColor = [...color]; this.health = health; this.maxHealth = health; this.knockback = knockback; this.knockbackDamping = knockbackDamping; this.scoreValue = scoreValue; this.onDeath = onDeath; this._hitKnockback = new Vector2D(0, 0); this._flash = 0; this._time = 0; this._broken = Math.random() * Math.PI * 2; this._emergeTotal = emergeTime; this._emerge = emergeTime; this.width = size; this.height = size; } die() { if (this.onDeath) { this.onDeath(this); } this.remove(this.container); } hit(impulse, damage = 1) { this.health -= damage; this._hitKnockback = impulse.mulS(this.knockback); this._flash = 0.08; return this.health <= 0; } update(dt) { this._time += dt; if (this._updateEmerge(dt)) return; const delta = this.target.pos.subV(this.pos); const distance = delta.length(); let chaseVelocity = new Vector2D(0, 0); if (distance > 1) { chaseVelocity = delta.divS(distance).mulS(this.speed); } this.velocity = chaseVelocity.addV(this._hitKnockback); super.update(dt); this._hitKnockback = this._hitKnockback.mulS(Math.max(0, 1 - dt * this.knockbackDamping)); this._flash = Math.max(0, this._flash - dt); } _updateEmerge(dt) { if (this._emerge <= 0) return false; this._emerge -= dt; if (this._emerge <= 0) { this._emerge = 0; if (this._onEmergenceComplete) this._onEmergenceComplete(); } return true; } _emergeSize() { if (this._emerge > 0 && this._emergeTotal > 0) { return 0.3 + 0.7 * (1 - this._emerge / this._emergeTotal); } return 1; } _drawEmerge(renderer) { if (this._emerge <= 0) return; const gear = 1 - this._emerge / this._emergeTotal; renderer.setBlendMode('additive'); const g = this.size * (3.4 - gear * 1.4); renderer.drawTexture( renderer.glowTexture, this.pos.x - g / 2, this.pos.y - g / 2, g, g, [this.baseColor[0], this.baseColor[1], this.baseColor[2], 0.5 * (1 - gear)], ); renderer.setBlendMode('normal'); } draw(renderer) { this.width = this.size; this.height = this.size; let color = this.baseColor; if (this._flash > 0) { color = [1, 1, 1, 1]; } else if (this.maxHealth > 1) { const hpRatio = this.health / this.maxHealth; color = [ this.baseColor[0] + (1 - this.baseColor[0]) * (1 - hpRatio), this.baseColor[1] * hpRatio, this.baseColor[2] * hpRatio, 1, ]; } if (this.maxHealth > 1) { this._drawJuggernaut(renderer, color); return; } this._drawEmerge(renderer); const s = this.size * this._emergeSize(); const lean = s <= 16; const facing = Math.atan2( this.target.pos.y - this.pos.y, this.target.pos.x - this.pos.x, ); const cosF = Math.cos(facing); const sinF = Math.sin(facing); const walk = Math.sin(this._time * (lean ? 14 : 10) + this._broken); const shade = [color[0] * 0.6, color[1] * 0.6, color[2] * 0.6, 1]; const shadeDark = [color[0] * 0.38, color[1] * 0.38, color[2] * 0.38, 1]; const light = [ Math.min(1, color[0] + 0.25), Math.min(1, color[1] + 0.25), Math.min(1, color[2] + 0.25), 1, ]; 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), ); }; if (!lean) { renderer.drawTexture( renderer.whiteTexture, this.pos.x - s * 0.62, this.pos.y - s * 0.5 + s * 0.06, s * 1.24, s * 0.92, [0, 0, 0, 0.35], ); } const legLen = s * (lean ? 0.32 : 0.4); const legSpread = s * 0.22; const hipX = this.pos.x - cosF * s * 0.18; const hipY = this.pos.y - sinF * s * 0.18; for (let i = 0; i < 2; i += 1) { const sign = i === 0 ? 1 : -1; const step = sign * walk * s * (lean ? 0.3 : 0.18); const footX = hipX + cosF * legLen - sinF * legSpread * sign + cosF * step; const footY = hipY + sinF * legLen + cosF * legSpread * sign + sinF * step; drawLimb(hipX, hipY, footX, footY, s * 0.15, shadeDark); } const torsoW = s * (lean ? 0.5 : 0.62); const torsoH = s * (lean ? 0.46 : 0.55); renderer.drawTexture( renderer.whiteTexture, this.pos.x - torsoW / 2, this.pos.y - torsoH / 2, torsoW, torsoH, color, ); const armLen = s * 0.5; const armSpread = s * 0.27; for (let i = 0; i < 2; i += 1) { const sign = i === 0 ? 1 : -1; const shX = this.pos.x - sinF * armSpread * sign; const shY = this.pos.y + cosF * armSpread * sign - s * 0.02; const reach = 1 + walk * 0.1; const handX = shX + cosF * armLen * reach; const handY = shY + sinF * armLen * reach; drawLimb(shX, shY, handX, handY, s * 0.17, shade); } const headSize = s * (lean ? 0.4 : 0.44); const headX = this.pos.x; const headY = this.pos.y - s * (lean ? 0.34 : 0.4) + walk * s * 0.03; renderer.drawTexture( renderer.whiteTexture, headX - headSize / 2, headY - headSize / 2, headSize, headSize, light, ); const eyeSize = headSize * 0.2; const eyeSpread = headSize * 0.24; const look = lean ? 0.1 : 0.04; const eyeY = headY + sinF * headSize * look; renderer.drawTexture( renderer.whiteTexture, headX - eyeSize / 2 - sinF * eyeSpread, eyeY - eyeSize / 2 + cosF * eyeSpread, eyeSize, eyeSize, [0.05, 0.05, 0.05, 1], ); renderer.drawTexture( renderer.whiteTexture, headX - eyeSize / 2 + sinF * eyeSpread, eyeY - eyeSize / 2 - cosF * eyeSpread, eyeSize, eyeSize, [0.05, 0.05, 0.05, 1], ); } _drawJuggernaut(renderer, color) { const s = this.size; const facing = Math.atan2( this.target.pos.y - this.pos.y, this.target.pos.x - this.pos.x, ); const cosF = Math.cos(facing); const sinF = Math.sin(facing); const walk = Math.sin(this._time * 5 + this._broken); const shade = [color[0] * 0.6, color[1] * 0.6, color[2] * 0.6, 1]; const shadeDark = [color[0] * 0.38, color[1] * 0.38, color[2] * 0.38, 1]; const light = [ Math.min(1, color[0] + 0.25), Math.min(1, color[1] + 0.25), Math.min(1, color[2] + 0.25), 1, ]; const mix = (a, b, t) => a + (b - a) * t; renderer.drawTexture( renderer.whiteTexture, this.pos.x - s * 0.78, this.pos.y - s * 0.55 + s * 0.08, s * 1.56, s * 1.14, [0, 0, 0, 0.35], ); const footSize = s * 0.16; const footBaseY = this.pos.y + s * 0.46; for (let i = 0; i < 2; i += 1) { const sign = i === 0 ? 1 : -1; const stepX = -sinF * s * 0.2 * sign + cosF * walk * s * 0.06 * sign; const stepY = cosF * s * 0.2 * sign + sinF * walk * s * 0.06 * sign; renderer.drawTexture( renderer.whiteTexture, this.pos.x + stepX - footSize / 2, footBaseY + stepY - footSize / 2, footSize, footSize, shadeDark, ); } const shellW = s * 1.0; const shellH = s * 0.95; const bottomY = this.pos.y + s * 0.4; const topY = this.pos.y - s * 0.5; const layers = 6; for (let i = 0; i < layers; i += 1) { const u = i / (layers - 1); const w = shellW * (1 - u * 0.66); const h = (shellH / layers) * 1.4; const cy = bottomY - u * (bottomY - topY) + walk * s * 0.02; renderer.drawTexture( renderer.whiteTexture, this.pos.x - w / 2, cy - h / 2, w, h, [ mix(shadeDark[0], light[0], u), mix(shadeDark[1], light[1], u), mix(shadeDark[2], light[2], u), 1, ], ); } for (let i = 0; i < 3; i += 1) { const rx = this.pos.x + (i - 1) * s * 0.16; renderer.drawTexture( renderer.whiteTexture, rx - s * 0.045, topY + s * 0.04, s * 0.09, shellH * 0.9, shadeDark, ); } const rivet = s * 0.055; renderer.drawTexture( renderer.whiteTexture, this.pos.x - s * 0.44, bottomY - rivet * 0.4, rivet, rivet, shadeDark, ); renderer.drawTexture( renderer.whiteTexture, this.pos.x + s * 0.44 - rivet, bottomY - rivet * 0.4, rivet, rivet, shadeDark, ); const headSize = s * 0.24; const headX = this.pos.x + cosF * s * 0.52; const headY = this.pos.y + sinF * s * 0.52; renderer.drawTexture( renderer.whiteTexture, headX - headSize / 2, headY - headSize / 2, headSize, headSize, light, ); const eyeSize = headSize * 0.22; const eyeSpread = headSize * 0.3; renderer.drawTexture( renderer.whiteTexture, headX - eyeSize / 2 - sinF * eyeSpread, headY - eyeSize / 2 + cosF * eyeSpread, eyeSize, eyeSize, [0.05, 0.05, 0.05, 1], ); renderer.drawTexture( renderer.whiteTexture, headX - eyeSize / 2 + sinF * eyeSpread, headY - eyeSize / 2 - cosF * eyeSpread, eyeSize, eyeSize, [0.05, 0.05, 0.05, 1], ); } }