initial commit

This commit is contained in:
armel committed 2026-09-07 20:37:40 +02:00
commit e8c3f9862a
193 files changed
+29795

No files matched your search

+107
View File
@@ -0,0 +1,107 @@
import { Zombie } from './Zombie.js';
export class Boss extends Zombie {
constructor(
x,
y,
target,
{
container,
viewport,
speed = 38,
size = 90,
color = [0.45, 0.05, 0.1, 1],
health = 80,
knockback = 40,
scoreValue = 50,
spawnInterval = 5,
spawnCount = 1,
spawnZombie = () => {},
onDeath = null,
} = {},
) {
super(x, y, target, {
container,
viewport,
speed,
size,
color,
health,
knockback,
scoreValue,
onDeath,
});
this.spawnInterval = spawnInterval;
this.spawnCount = spawnCount;
this.spawnTimer = 3;
this.spawnZombie = spawnZombie;
this._time = 0;
this._hurt = 0;
}
hit(impulse, damage = 1) {
const dead = super.hit(impulse, damage);
this._flash = 0.18;
this._hurt = 0.18;
return dead;
}
update(dt) {
super.update(dt);
this._time += dt;
this._hurt = Math.max(0, this._hurt - dt);
this.spawnTimer -= dt;
if (this.spawnTimer <= 0) {
this.spawnTimer = this.spawnInterval;
const minions = this.container.filter(
(obj) => obj instanceof Zombie && !(obj instanceof Boss),
);
if (minions.length < 15) {
for (let i = 0; i < this.spawnCount; i += 1) {
const angle = (i / this.spawnCount) * Math.PI * 2 + Math.random() * 0.6;
this.spawnZombie(
this.pos.x + Math.cos(angle) * 70,
this.pos.y + Math.sin(angle) * 70,
this.target,
this.container,
);
}
}
}
}
draw(renderer) {
this.width = this.size;
this.height = this.size;
const pulse = 1 + 0.05 * Math.sin(this._time * 4);
const hurtRatio = this._hurt / 0.18;
const s = this.size * pulse * (1 + hurtRatio * 0.14);
const hpRatio = Math.max(0, this.health / this.maxHealth);
const color = [
Math.min(1, this.baseColor[0] + (1 - hpRatio) * 0.3),
this.baseColor[1] * hpRatio,
this.baseColor[2] * hpRatio,
1,
];
renderer.drawTexture(
renderer.whiteTexture,
this.pos.x - s / 2,
this.pos.y - s / 2,
s,
s,
this._flash > 0 ? [1, 1, 1, 1] : color,
);
const core = s * (0.35 + (1 - hpRatio) * 0.4);
renderer.drawTexture(
renderer.whiteTexture,
this.pos.x - core / 2,
this.pos.y - core / 2,
core,
core,
[0.12, 0.02, 0.03, 0.9],
);
}
}
+46
View File
@@ -0,0 +1,46 @@
import { GameObject } from '../engine/GameObject.js';
import { Vector2D } from '../engine/Vector2D.js';
export class Bullet extends GameObject {
constructor(
x,
y,
direction,
{ container, viewport, speed = 800, size = 8, damage = 1, color = [1, 0.2, 0.2, 1] } = {},
) {
super(x, y);
this.velocity = direction.mulS(speed);
this.container = container;
this.viewport = viewport;
this.size = size;
this.damage = damage;
this.color = color;
this.width = size;
this.height = size;
}
update(dt) {
super.update(dt);
const { width, height } = this.viewport;
if (
this.pos.x < -this.size ||
this.pos.x > width + this.size ||
this.pos.y < -this.size ||
this.pos.y > height + this.size
) {
this.remove(this.container);
}
}
draw(renderer) {
renderer.drawTexture(
renderer.whiteTexture,
this.pos.x - this.size / 2,
this.pos.y - this.size / 2,
this.size,
this.size,
this.color,
);
}
}
+263
View File
@@ -0,0 +1,263 @@
import { Zombie } from './Zombie.js';
import { GameObject } from '../engine/GameObject.js';
import { Vector2D } from '../engine/Vector2D.js';
export class Leaper extends Zombie {
constructor(
x,
y,
target,
{
container,
viewport,
speed = 95,
size = 28,
color = [0.15, 0.25, 0.75, 1],
health = 3,
knockback = 240,
scoreValue = 6,
dashRange = 300,
windupTime = 0.35,
dashTime = 0.28,
dashSpeed = 680,
dashCooldown = 1.8,
emergeTime = 0,
} = {},
) {
super(x, y, target, {
container,
viewport,
speed,
size,
color,
health,
knockback,
scoreValue,
emergeTime,
});
this.dashRange = dashRange;
this.windupTime = windupTime;
this.dashTime = dashTime;
this.dashSpeed = dashSpeed;
this.dashCooldown = dashCooldown;
this._dashTimer = 0;
this._windupTimer = 0;
this._dashCooldown = 0;
this._dashDirection = new Vector2D(1, 0);
this._time = 0;
}
update(dt) {
this._time += dt;
if (this._updateEmerge(dt)) return;
this._dashCooldown = Math.max(0, this._dashCooldown - dt);
const delta = this.target.pos.subV(this.pos);
const distance = delta.length();
let move = new Vector2D(0, 0);
if (this._windupTimer > 0) {
this._windupTimer -= dt;
if (this._windupTimer <= 0) {
this._dashTimer = this.dashTime;
this._dashDirection = distance > 1 ? delta.divS(distance) : new Vector2D(1, 0);
}
} else if (this._dashTimer > 0) {
this._dashTimer -= dt;
move = this._dashDirection.mulS(this.dashSpeed);
} else if (distance > 1) {
if (distance < this.dashRange && this._dashCooldown <= 0) {
this._windupTimer = this.windupTime;
} else {
move = delta.divS(distance).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);
}
draw(renderer) {
this._drawEmerge(renderer);
const s = this.size * this._emergeSize();
const dashing = this._dashTimer > 0;
const winding = this._windupTimer > 0;
let color = this.baseColor;
if (this._flash > 0) {
color = [1, 1, 1, 1];
} else if (winding || dashing) {
color = [
this.baseColor[0] + (1 - this.baseColor[0]) * 0.8,
this.baseColor[1] + (1 - this.baseColor[1]) * 0.8,
this.baseColor[2],
1,
];
}
const towards = dashing ? this._dashDirection : this.target.pos.subV(this.pos);
const facing = Math.atan2(towards.y, towards.x);
const dirX = Math.cos(facing);
const dirY = Math.sin(facing);
const perpX = -dirY;
const perpY = dirX;
const shade = [color[0] * 0.65, color[1] * 0.65, color[2] * 0.65, 1];
const shadeDark = [color[0] * 0.4, color[1] * 0.4, color[2] * 0.4, 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),
);
};
const bodyLen = s * (dashing ? 1.45 : winding ? 0.9 : 1.15);
const bodyH = s * (dashing ? 0.42 : winding ? 0.7 : 0.55);
renderer.drawTexture(
renderer.whiteTexture,
this.pos.x - s * 0.72,
this.pos.y - s * 0.26 + s * 0.08,
s * 1.44,
s * 0.52,
[0, 0, 0, 0.35],
);
if (winding) {
renderer.drawTexture(
renderer.whiteTexture,
this.pos.x - s * 0.9,
this.pos.y - s * 0.6,
s * 1.8,
s * 1.2,
[1, 1, 1, 0.3],
);
}
const tailBaseX = this.pos.x - dirX * bodyLen * 0.45;
const tailBaseY = this.pos.y - dirY * bodyLen * 0.45;
const tailSway = dashing ? 0 : Math.sin(this._time * 16) * s * 0.14;
const tailTipX = tailBaseX - dirX * (dashing ? s * 0.6 : s * 0.42) + perpX * tailSway;
const tailTipY = tailBaseY - dirY * (dashing ? s * 0.6 : s * 0.42) + perpY * tailSway;
drawLimb(tailBaseX, tailBaseY, tailTipX, tailTipY, s * 0.11, shadeDark);
drawLimb(
tailTipX, tailTipY,
tailTipX + perpX * tailSway * 0.5 - dirX * s * 0.08,
tailTipY + perpY * tailSway * 0.5 - dirY * s * 0.08,
s * 0.15,
shade,
);
const legL = s * (dashing ? 0.3 : winding ? 0.16 : 0.4);
const legW = s * 0.11;
for (let pair = 0; pair < 2; pair += 1) {
const front = pair === 0;
const baseFx = this.pos.x + dirX * bodyLen * (front ? 0.4 : -0.4);
const baseFy = this.pos.y + dirY * bodyLen * (front ? 0.4 : -0.4);
for (let i = 0; i < 2; i += 1) {
const sign = i === 0 ? 1 : -1;
const bx = baseFx + perpX * bodyH * 0.5 * sign;
const by = baseFy + perpY * bodyH * 0.5 * sign;
let fx = bx;
let fy = by;
if (dashing) {
fx = bx - dirX * legL;
fy = by - dirY * legL;
} else if (winding) {
fx = bx + perpX * sign * s * 0.1;
fy = by + s * 0.13;
} else {
const run = Math.sin(this._time * 18 + (front ? 0 : Math.PI)) * s * 0.16;
fx = bx + dirX * run * 0.7 + perpX * sign * s * 0.04;
fy = by + legL;
}
drawLimb(bx, by, fx, fy, legW, shadeDark);
}
}
renderer.drawTexture(
renderer.whiteTexture,
this.pos.x - bodyLen / 2,
this.pos.y - bodyH / 2,
bodyLen,
bodyH,
color,
facing,
);
const headLen = s * (dashing ? 0.38 : 0.44);
const headH = s * 0.3;
const headCx = this.pos.x + dirX * (bodyLen * 0.5 - s * 0.03);
const headCy = this.pos.y + dirY * (bodyLen * 0.5 - s * 0.03);
renderer.drawTexture(
renderer.whiteTexture,
headCx - headLen / 2,
headCy - headH / 2,
headLen,
headH,
light,
facing,
);
const snoutLen = s * 0.28;
const snoutCx = headCx + dirX * (headLen * 0.5 + snoutLen * 0.4);
const snoutCy = headCy + dirY * (headLen * 0.5 + snoutLen * 0.4);
drawLimb(headCx, headCy, snoutCx, snoutCy, s * 0.18, shade);
const noseS = s * 0.08;
renderer.drawTexture(
renderer.whiteTexture,
snoutCx + dirX * snoutLen * 0.5 - noseS / 2,
snoutCy + dirY * snoutLen * 0.5 - noseS / 2,
noseS,
noseS,
[0.03, 0.05, 0.12, 1],
);
const eyeS = s * 0.08;
const eyeCx = headCx + dirX * headLen * 0.28;
const eyeCy = headCy + dirY * headLen * 0.28;
for (let i = 0; i < 2; i += 1) {
const sign = i === 0 ? 1 : -1;
renderer.drawTexture(
renderer.whiteTexture,
eyeCx + perpX * eyeS * 1.5 * sign - eyeS / 2,
eyeCy + perpY * eyeS * 1.5 * sign - eyeS / 2,
eyeS,
eyeS,
[0.03, 0.05, 0.12, 1],
);
}
const earS = s * 0.13;
const earBack = dashing || winding ? 0.6 : 1;
for (let i = 0; i < 2; i += 1) {
const sign = i === 0 ? 1 : -1;
const eax = headCx + perpX * earS * 1.55 * sign;
const eay = headCy + perpY * earS * 1.55 * sign;
drawLimb(
eax, eay,
eax - dirX * earS * earBack,
eay - dirY * earS * earBack,
earS * 0.5,
shadeDark,
);
}
}
}
+44
View File
@@ -0,0 +1,44 @@
import { Zombie } from './Zombie.js';
export class MegaTank extends Zombie {
constructor(
x,
y,
target,
{
container,
viewport,
speed = 30,
size = 64,
color = [0.55, 0.28, 0.12, 1],
health = 18,
knockback = 90,
scoreValue = 15,
spawnOnDeath = () => {},
spawnCount = 3,
} = {},
) {
super(x, y, target, {
container,
viewport,
speed,
size,
color,
health,
knockback,
scoreValue,
onDeath: (mega) => {
for (let i = 0; i < spawnCount; i += 1) {
const angle = (i / spawnCount) * Math.PI * 2;
const offset = mega.size + 28;
spawnOnDeath(
mega.pos.x + Math.cos(angle) * offset,
mega.pos.y + Math.sin(angle) * offset,
target,
container,
);
}
},
});
}
}
+163
View File
@@ -0,0 +1,163 @@
import { Witch } from './Witch.js';
import { WitchProjectile } from './WitchProjectile.js';
import { Vector2D } from '../engine/Vector2D.js';
export class MegaWitch extends Witch {
constructor(
x,
y,
target,
{
container,
viewport,
speed = 55,
size = 52,
color = [0.8, 0.15, 0.7, 1],
health = 22,
knockback = 130,
scoreValue = 12,
preferredDistance = 240,
fireInterval = 2,
projectileSpeed = 240,
projectileCount = 3,
projectileSpread = 0.35,
projectileColor = [1, 0.3, 0.3, 1],
novaInterval = 5,
novaCount = 16,
novaProjectileSpeed = 250,
spawnProjectile = (p) => {},
spawnOnDeath = () => {},
spawnCount = 3,
} = {},
) {
super(x, y, target, {
container,
viewport,
speed,
size,
color,
health,
knockback,
scoreValue,
preferredDistance,
fireInterval,
projectileSpeed,
projectileCount,
projectileSpread,
projectileColor,
spawnProjectile,
onDeath: (mega) => {
for (let i = 0; i < spawnCount; i += 1) {
const angle = (i / spawnCount) * Math.PI * 2;
const offset = mega.size + 30;
spawnOnDeath(
mega.pos.x + Math.cos(angle) * offset,
mega.pos.y + Math.sin(angle) * offset,
target,
container,
);
}
},
});
this.novaInterval = novaInterval;
this.novaCount = novaCount;
this.novaProjectileSpeed = novaProjectileSpeed;
this._novaTimer = novaInterval * 0.5;
this._novaRotation = Math.random() * Math.PI * 2;
this._pendingNova = [];
}
update(dt) {
super.update(dt);
if (this.target.health <= 0) return;
for (let i = this._pendingNova.length - 1; i >= 0; i -= 1) {
const entry = this._pendingNova[i];
entry.remaining -= dt;
if (entry.remaining <= 0) {
this._pendingNova.splice(i, 1);
entry.fire();
}
}
this._novaTimer -= dt;
if (this._novaTimer <= 0) {
this._novaTimer = this.novaInterval * (0.85 + Math.random() * 0.3);
this.nova();
}
}
nova() {
const lost = 1 - Math.max(0, this.health / this.maxHealth);
const count = Math.max(6, Math.round(this.novaCount * (0.75 + 0.25 * lost)));
const speed = this.novaProjectileSpeed * (1 + lost * 0.45);
const size = this.size * 0.3;
const color = [1, 0.5, 0.3, 1];
this._novaRotation += 0.36;
const fireRing = (offset) => {
for (let i = 0; i < count; i += 1) {
const angle = (i / count) * Math.PI * 2 + this._novaRotation + offset;
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,
size,
color,
});
this.spawnProjectile(projectile);
}
};
fireRing(0);
this._pendingNova.push({
remaining: 0.22,
fire: () => fireRing(Math.PI / count),
});
if (lost > 0.34) {
this._pendingNova.push({
remaining: 0.44,
fire: () => fireRing(0),
});
}
this._cast = 0.32;
}
draw(renderer) {
const charge = 1 - Math.max(0, this._novaTimer) / this.novaInterval;
if (charge > 0.72) {
const hp = Math.max(0, this.health / this.maxHealth);
const punch = (charge - 0.72) / 0.28;
const r = this.size * (0.6 + punch * 1.7) * (1.4 - hp * 0.6);
renderer.setBlendMode('additive');
renderer.drawTexture(
renderer.glowTexture,
this.pos.x - r,
this.pos.y - r,
r * 2,
r * 2,
[1, 0.3, 0.15, punch * 0.7],
);
renderer.drawTexture(
renderer.ringTexture,
this.pos.x - r * 1.5,
this.pos.y - r * 1.5,
r * 3,
r * 3,
[1, 0.45, 0.2, punch * 0.9],
);
renderer.drawTexture(
renderer.ringTexture,
this.pos.x - r * 2.1,
this.pos.y - r * 2.1,
r * 4.2,
r * 4.2,
[1, 0.25, 0.1, punch * 0.35],
);
renderer.setBlendMode('normal');
}
super.draw(renderer);
}
}
+75
View File
@@ -0,0 +1,75 @@
import { GameObject } from '../engine/GameObject.js';
export const PICKUP_TYPES = {
rapid: { name: 'Rapid Fire', color: [1, 0.8, 0.2, 1] },
damage: { name: 'Damage +1', color: [1, 0.3, 0.3, 1] },
multishot: { name: 'Multishot', color: [0.2, 0.8, 1, 1] },
speed: { name: 'Bullet Speed', color: [0.6, 0.9, 1, 1] },
};
const PICKUP_KEYS = Object.keys(PICKUP_TYPES);
export const randomPickupType = (types = PICKUP_KEYS) => (
types[Math.floor(Math.random() * types.length)]
);
export class Pickup extends GameObject {
constructor(x, y, type, { container, viewport, life = 10, collectRadius = 46 } = {}) {
super(x, y);
this.type = type;
this.container = container;
this.viewport = viewport;
this.life = life;
this.collectRadius = collectRadius;
this._time = 0;
this.size = 16;
this.width = this.size;
this.height = this.size;
this.color = PICKUP_TYPES[type].color;
}
intersect(other) {
const dx = this.pos.x - other.pos.x;
const dy = this.pos.y - other.pos.y;
return dx * dx + dy * dy <= this.collectRadius * this.collectRadius
|| super.intersect(other);
}
update(dt) {
this._time += dt;
if (this._time >= this.life) {
this.remove(this.container);
}
}
draw(renderer, textRenderer) {
const remaining = this.life - this._time;
let alpha = 1;
if (remaining < 2) {
alpha = 0.3 + 0.7 * Math.max(0, Math.sin(remaining * 6));
}
const pulse = 1 + 0.15 * Math.sin(this._time * 6);
const s = this.size * pulse;
renderer.drawTexture(
renderer.whiteTexture,
this.pos.x - s / 2,
this.pos.y - s / 2,
s,
s,
[...this.color.slice(0, 3), alpha],
);
const label = PICKUP_TYPES[this.type].name;
const opts = { size: 12, fillStyle: '#fff', bold: true };
const entry = textRenderer?.measure(label, opts);
const textHeight = entry ? entry.height : 0;
textRenderer?.draw(
renderer,
label,
this.pos.x - (entry ? entry.width / 2 : label.length * 4.5),
this.pos.y - s / 2 - textHeight - 4,
opts,
);
}
}
+391
View File
@@ -0,0 +1,391 @@
import { GameObject } from '../engine/GameObject.js';
import { Vector2D } from '../engine/Vector2D.js';
import { Bullet } from './Bullet.js';
import { Zombie } from './Zombie.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,
autoAim = false,
} = {},
) {
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;
this.autoAim = autoAim;
}
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 (this.autoAim) {
const nearest = this._nearestZombie();
if (nearest) {
const toEnemy = nearest.pos.subV(this.pos);
if (toEnemy.lengthSqrt() > 1) {
aim = toEnemy;
}
}
}
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();
}
}
_nearestZombie() {
let best = null;
let bestDistSq = Infinity;
for (const obj of this.container || []) {
if (!(obj instanceof Zombie)) continue;
const delta = obj.pos.subV(this.pos);
const distSq = delta.lengthSqrt();
if (distSq < bestDistSq) {
bestDistSq = distSq;
best = obj;
}
}
return best;
}
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,
);
}
}
+265
View File
@@ -0,0 +1,265 @@
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');
}
}
}
+49
View File
@@ -0,0 +1,49 @@
import { GameObject } from '../engine/GameObject.js';
import { Vector2D } from '../engine/Vector2D.js';
export class WitchProjectile extends GameObject {
constructor(
x,
y,
direction,
{ container, viewport, speed = 220, size = 12, color = [0.7, 0.2, 1, 1] } = {},
) {
super(x, y);
this.velocity = direction.mulS(speed);
this.container = container;
this.viewport = viewport;
this.size = size;
this.color = color;
this.width = size;
this.height = size;
this._time = 0;
}
update(dt) {
super.update(dt);
this._time += dt;
const { width, height } = this.viewport;
if (
this.pos.x < -this.size ||
this.pos.x > width + this.size ||
this.pos.y < -this.size ||
this.pos.y > height + this.size
) {
this.remove(this.container);
}
}
draw(renderer) {
const pulse = 1 + 0.15 * Math.sin(this._time * 12);
const s = this.size * pulse;
renderer.drawTexture(
renderer.whiteTexture,
this.pos.x - s / 2,
this.pos.y - s / 2,
s,
s,
this.color,
);
}
}
+377
View File
@@ -0,0 +1,377 @@
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],
);
}
}