141 lines
3.9 KiB
JavaScript
141 lines
3.9 KiB
JavaScript
/**
|
|
* player.js
|
|
* Player state, grid movement, and inventory management.
|
|
* No rendering, no DOM — pure logic.
|
|
*/
|
|
|
|
class Player {
|
|
constructor() {
|
|
this.reset();
|
|
}
|
|
|
|
reset() {
|
|
// Grid position: col 0-3 (left → right), row 0-2 (top → bottom)
|
|
this.col = 0;
|
|
this.row = 1;
|
|
|
|
// Carried goods (max SETTINGS.MAX_CARRY)
|
|
this.goods = [];
|
|
|
|
// Whether the player is holding the medication vial
|
|
this.hasMedication = false;
|
|
|
|
// Movement cooldown (ms)
|
|
this.moveCooldown = 0;
|
|
|
|
// Walk-cycle: phase advances (radians) while moveCooldown > 0
|
|
this.walkPhase = 0;
|
|
this.animFrame = 0;
|
|
this.animTimer = 0;
|
|
|
|
// Flash timer when interacting
|
|
this.interactFlash = 0;
|
|
}
|
|
|
|
/* ── Helpers ──────────────────────────────────────────────── */
|
|
|
|
/** Pixel x centre of current column inside the middle zone. */
|
|
pixelX() {
|
|
// Evenly distribute columns across the middle zone with a 30px margin
|
|
// from each edge, so col 0 sits just right of the belt end and
|
|
// col (PLAYER_COLS-1) sits just left of the right-zone border.
|
|
const margin = 30;
|
|
const avail = SETTINGS.MIDDLE_ZONE_END - SETTINGS.MIDDLE_ZONE_START - margin * 2;
|
|
const step = avail / (SETTINGS.PLAYER_COLS - 1);
|
|
return SETTINGS.MIDDLE_ZONE_START + margin + this.col * step;
|
|
}
|
|
|
|
/** Pixel y centre of current row. */
|
|
pixelY() {
|
|
return SETTINGS.BELT_ROWS[this.row];
|
|
}
|
|
|
|
canMove() {
|
|
return this.moveCooldown <= 0;
|
|
}
|
|
|
|
canCarryMore() {
|
|
return this.goods.length < SETTINGS.MAX_CARRY;
|
|
}
|
|
|
|
/* ── Movement ─────────────────────────────────────────────── */
|
|
|
|
move(direction) {
|
|
if (!this.canMove()) return false;
|
|
|
|
let nc = this.col;
|
|
let nr = this.row;
|
|
|
|
switch (direction) {
|
|
case 'up': nr = Math.max(0, nr - 1); break;
|
|
case 'down': nr = Math.min(SETTINGS.PLAYER_ROWS - 1, nr + 1); break;
|
|
case 'left': nc = Math.max(0, nc - 1); break;
|
|
case 'right': nc = Math.min(SETTINGS.PLAYER_COLS - 1, nc + 1); break;
|
|
default: return false;
|
|
}
|
|
|
|
if (nc === this.col && nr === this.row) return false;
|
|
|
|
this.col = nc;
|
|
this.row = nr;
|
|
this.moveCooldown = SETTINGS.PLAYER_MOVE_COOLDOWN;
|
|
return true;
|
|
}
|
|
|
|
/* ── Inventory ────────────────────────────────────────────── */
|
|
|
|
/**
|
|
* Attempt to add a good to the inventory.
|
|
* Returns true on success, false if full.
|
|
*/
|
|
addGood(good) {
|
|
if (!this.canCarryMore()) return false;
|
|
this.goods.push(good);
|
|
this.interactFlash = 300;
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Remove and return all carried goods (for lab deposit).
|
|
*/
|
|
depositGoods() {
|
|
const deposited = [...this.goods];
|
|
this.goods = [];
|
|
this.interactFlash = 400;
|
|
return deposited;
|
|
}
|
|
|
|
pickupMedication() {
|
|
this.hasMedication = true;
|
|
this.interactFlash = 400;
|
|
}
|
|
|
|
/**
|
|
* Deliver medication to patient.
|
|
* Returns true if medication was delivered.
|
|
*/
|
|
deliverMedication() {
|
|
if (!this.hasMedication) return false;
|
|
this.hasMedication = false;
|
|
this.interactFlash = 600;
|
|
return true;
|
|
}
|
|
|
|
/* ── Update ───────────────────────────────────────────────── */
|
|
|
|
update(dt) {
|
|
if (this.moveCooldown > 0) {
|
|
this.moveCooldown -= dt;
|
|
// Advance walk cycle: ~π radians per 180 ms move cooldown → one leg swing per step
|
|
this.walkPhase += dt * 0.0175;
|
|
}
|
|
if (this.interactFlash > 0) this.interactFlash -= dt;
|
|
|
|
this.animTimer += dt;
|
|
if (this.animTimer >= 200) {
|
|
this.animTimer -= 200;
|
|
this.animFrame = (this.animFrame + 1) % 4;
|
|
}
|
|
}
|
|
}
|