Files
kaltaquise-gamification/kidney_lab/js/conveyor.js
2026-04-16 08:14:20 +02:00

201 lines
6.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* conveyor.js
* Left conveyor belts (3×, goods travel → right).
* Right conveyor belt (1×, medication travels ← left).
* Pure logic — no rendering, no DOM.
*/
/* ────────────────────────────── Good ──────────────────────────── */
const GoodState = Object.freeze({
SLIDING: 'sliding', // moving along belt
READY: 'ready', // at belt's right end, waiting for pickup
COLLECTED: 'collected', // grabbed by player
DROPPED: 'dropped', // fell — penalty already applied
});
class Good {
constructor(beltRow, isSpecial) {
this.id = Good._nextId++;
this.beltRow = beltRow;
this.isSpecial = isSpecial;
this.x = SETTINGS.LEFT_ZONE_START + SETTINGS.GOOD_SIZE / 2;
this.y = SETTINGS.BELT_ROWS[beltRow];
this.state = GoodState.SLIDING;
this.waitTimer = 0;
}
}
Good._nextId = 0;
/* ────────────────────────── LeftConveyorSystem ─────────────────── */
class LeftConveyorSystem {
constructor() {
// Belt strip animation offset (px, wraps at 40)
this.animOffset = 0;
// Per-belt state: goods array + countdown to next spawn
this.belts = SETTINGS.BELT_INIT_OFFSETS.map(offset => ({
goods: [],
spawnTimer: offset,
}));
}
_randomSpawnTime() {
return SETTINGS.BELT_MIN_SPAWN
+ Math.random() * (SETTINGS.BELT_MAX_SPAWN - SETTINGS.BELT_MIN_SPAWN);
}
/**
* Update all belts.
* @param {number} dt delta time in ms
* @param {function} onDrop called with (good) when a good drops off
*/
update(dt, onDrop) {
this.animOffset = (this.animOffset + SETTINGS.LEFT_BELT_SPEED * dt / 1000) % 40;
this.belts.forEach((belt, row) => {
// ── Spawn ──
belt.spawnTimer -= dt;
if (belt.spawnTimer <= 0) {
belt.spawnTimer = this._randomSpawnTime();
const isSpecial = Math.random() < SETTINGS.SPECIAL_GOOD_CHANCE;
belt.goods.push(new Good(row, isSpecial));
}
// ── Move & age goods ──
belt.goods.forEach(good => {
if (good.state === GoodState.SLIDING) {
good.x += SETTINGS.LEFT_BELT_SPEED * dt / 1000;
const pickupX = SETTINGS.LEFT_ZONE_END - SETTINGS.GOOD_SIZE * 0.5;
if (good.x >= pickupX) {
good.x = pickupX;
good.state = GoodState.READY;
good.waitTimer = SETTINGS.GOOD_WAIT_TIME;
}
} else if (good.state === GoodState.READY) {
good.waitTimer -= dt;
if (good.waitTimer <= 0) {
good.state = GoodState.DROPPED;
onDrop(good);
}
}
});
// ── Prune finished goods ──
belt.goods = belt.goods.filter(
g => g.state === GoodState.SLIDING || g.state === GoodState.READY
);
});
}
/**
* Try to collect the READY good on the given row.
* Returns the Good object on success, null otherwise.
*/
tryCollect(row) {
const belt = this.belts[row];
const good = belt.goods.find(g => g.state === GoodState.READY);
if (!good) return null;
good.state = GoodState.COLLECTED;
return good;
}
/** True if belt[row] has a READY good waiting at the pickup point. */
hasReadyGood(row) {
return this.belts[row].goods.some(g => g.state === GoodState.READY);
}
/** All goods in SLIDING or READY state across all belts (for rendering). */
allGoods() {
return this.belts.flatMap(b => b.goods);
}
}
/* ─────────────────────── Medication (right belt) ──────────────── */
const MedState = Object.freeze({
ON_BELT: 'on_belt', // sliding left
READY: 'ready', // at left end, waiting for player
PICKED: 'picked', // player has it
EXPIRED: 'expired', // timed out while waiting
});
class Medication {
constructor() {
this.id = Medication._nextId++;
// Start at right edge of right zone
this.x = SETTINGS.RIGHT_ZONE_END - SETTINGS.GOOD_SIZE;
this.y = SETTINGS.BELT_ROWS[1]; // always middle row
this.state = MedState.ON_BELT;
this.waitTimer = 0;
}
}
Medication._nextId = 0;
/* ────────────────────────── RightConveyorSystem ────────────────── */
class RightConveyorSystem {
constructor() {
this.animOffset = 0;
this.medications = [];
}
addMedication() {
this.medications.push(new Medication());
}
/**
* Update right belt medications.
* @param {number} dt delta time ms
*/
update(dt) {
this.animOffset = (this.animOffset + SETTINGS.RIGHT_BELT_SPEED * dt / 1000) % 40;
this.medications.forEach(med => {
if (med.state === MedState.ON_BELT) {
med.x -= SETTINGS.RIGHT_BELT_SPEED * dt / 1000;
const pickupX = SETTINGS.RIGHT_ZONE_START + SETTINGS.GOOD_SIZE * 0.5;
if (med.x <= pickupX) {
med.x = pickupX;
med.state = MedState.READY;
med.waitTimer = SETTINGS.MED_WAIT_TIME;
}
} else if (med.state === MedState.READY) {
med.waitTimer -= dt;
if (med.waitTimer <= 0) {
med.state = MedState.EXPIRED;
}
}
});
// Prune expired / picked medications
this.medications = this.medications.filter(
m => m.state === MedState.ON_BELT || m.state === MedState.READY
);
}
/** True if there is a READY medication at the pickup point (row 1 left end). */
hasReadyMedication() {
return this.medications.some(m => m.state === MedState.READY);
}
/**
* Try to pick up the READY medication.
* Returns true on success.
*/
tryPickup() {
const med = this.medications.find(m => m.state === MedState.READY);
if (!med) return false;
med.state = MedState.PICKED;
return true;
}
/** Visible medications for rendering (ON_BELT and READY). */
visibleMedications() {
return this.medications.filter(
m => m.state === MedState.ON_BELT || m.state === MedState.READY
);
}
}