initial commit
This commit is contained in:
commit
e8c3f9862a
193 files changed
+29795
No files matched your search
@@ -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],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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],
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user