/** * 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; } } }