Added kidney_labe and Cyste_kid

This commit is contained in:
verboomp
2026-04-16 08:14:20 +02:00
parent aa66c030f8
commit 9cc8ac8cad
40 changed files with 6762 additions and 0 deletions

200
kidney_lab/js/conveyor.js Normal file
View File

@@ -0,0 +1,200 @@
/**
* 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
);
}
}