391 lines
10 KiB
JavaScript
391 lines
10 KiB
JavaScript
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,
|
|
);
|
|
}
|
|
} |