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
);
}
}

1080
kidney_lab/js/game.js Normal file

File diff suppressed because it is too large Load Diff

144
kidney_lab/js/highscore.js Normal file
View File

@@ -0,0 +1,144 @@
/**
* highscore.js
* High-score screen: displays final score(s), persists top-10 to localStorage,
* shows leaderboard, offers back-to-start button.
*
* show(names, scores, playerCount)
* names — string (1P) or array of strings (2P)
* scores — number (1P) or array of numbers (2P)
* playerCount — 1 or 2
*/
const HighScoreScreen = (() => {
const STORAGE_KEY = 'kidneylab_scores';
/* ── Persistence ─────────────────────────────────────────────── */
function loadScores() {
try {
return JSON.parse(localStorage.getItem(STORAGE_KEY)) || [];
} catch {
return [];
}
}
function saveScore(name, score) {
const scores = loadScores();
scores.push({ name: name.toUpperCase().slice(0, 12), score, date: new Date().toLocaleDateString() });
scores.sort((a, b) => b.score - a.score);
const top10 = scores.slice(0, 10);
localStorage.setItem(STORAGE_KEY, JSON.stringify(top10));
return top10;
}
/* ── Helpers ─────────────────────────────────────────────────── */
function rankLabel(rank) {
if (rank === 1) return '🏆 NEW HIGH SCORE!';
if (rank === 2) return '🥈 2nd place!';
if (rank === 3) return '🥉 3rd place!';
return `Rank #${rank}`;
}
function resultCardHTML(playerName, finalScore, scores, myRank) {
const isTop = myRank === 1;
return `
<div class="hs-result ${isTop ? 'hs-gold' : ''}">
<div class="hs-rank-label">${rankLabel(myRank)}</div>
<div class="hs-player">${playerName.toUpperCase()}</div>
<div class="hs-score-display">${finalScore >= 0 ? finalScore : 0}</div>
<div class="hs-score-label">POINTS</div>
</div>`;
}
function tableHTML(scores, highlightNames, highlightScores) {
const rowsHTML = scores.map((s, i) => {
// highlight any entry that matches one of the just-played players
const isMe = highlightNames.some((n, ni) =>
s.name === n.toUpperCase().slice(0, 12) && s.score === highlightScores[ni]
);
return `<tr class="${isMe ? 'my-row' : ''}">
<td class="rank-col">${i + 1}</td>
<td class="name-col">${s.name}</td>
<td class="score-col">${s.score}</td>
<td class="date-col">${s.date}</td>
</tr>`;
}).join('');
return `
<div class="hs-table-wrap">
<h2 class="hs-heading">HALL OF FAME</h2>
<table class="hs-table">
<thead><tr><th>#</th><th>NAME</th><th>SCORE</th><th>DATE</th></tr></thead>
<tbody>${rowsHTML}</tbody>
</table>
</div>`;
}
/* ── Public show ─────────────────────────────────────────────── */
function show(names, scores, playerCount) {
// Normalise to arrays
const nameArr = Array.isArray(names) ? names : [names];
const scoreArr = Array.isArray(scores) ? scores : [scores];
const count = playerCount || nameArr.length;
// Switch screen
document.querySelectorAll('.screen').forEach(s => s.classList.remove('active'));
document.getElementById('highscore-screen').classList.add('active');
// Save all players' scores; last save wins for the leaderboard table
let leaderboard;
nameArr.forEach((n, i) => {
leaderboard = saveScore(n, scoreArr[i]);
});
// Build rank info for each player
const rankInfo = nameArr.map((n, i) => {
const rank = leaderboard.findIndex(
s => s.name === n.toUpperCase().slice(0, 12) && s.score === scoreArr[i]
) + 1;
return { name: n, score: scoreArr[i], rank };
});
render(rankInfo, leaderboard, nameArr, scoreArr);
}
function render(rankInfo, leaderboard, nameArr, scoreArr) {
const el = document.getElementById('highscore-screen');
// One result card per player
const cardsHTML = rankInfo.map(r =>
resultCardHTML(r.name, r.score, leaderboard, r.rank)
).join('');
// Cards side-by-side for 2P, centred for 1P
const cardsWrap = rankInfo.length > 1
? `<div style="display:flex;gap:16px;justify-content:center;flex-wrap:wrap;">${cardsHTML}</div>`
: cardsHTML;
el.innerHTML = `
<div class="hs-inner">
<div class="hs-logo">
<span class="logo-gw">GAME &amp; WATCH</span>
<span class="logo-title">KIDNEY LAB</span>
</div>
${cardsWrap}
${tableHTML(leaderboard, nameArr, scoreArr)}
<div class="hs-actions">
<button id="back-btn" class="btn-primary">PLAY AGAIN</button>
</div>
</div>
`;
el.querySelector('#back-btn').addEventListener('click', () => {
IntroScreen.show();
});
}
return { show };
})();

140
kidney_lab/js/intro.js Normal file
View File

@@ -0,0 +1,140 @@
/**
* intro.js
* Introduction screen — two-step flow:
* Step 1: select 1 or 2 players
* Step 2: enter name(s), then start
* Owns the #intro-screen element.
*/
const IntroScreen = (() => {
function show() {
document.querySelectorAll('.screen').forEach(s => s.classList.remove('active'));
document.getElementById('intro-screen').classList.add('active');
renderStep1();
}
/* ── Shared header (logo + rules) ─────────────────────────── */
function headerHTML() {
return `
<div class="intro-logo">
<span class="logo-gw">GAME &amp; WATCH</span>
<span class="logo-title">KIDNEY LAB</span>
<span class="logo-model">MW-56</span>
</div>
<div class="intro-description">
<p>Help <strong>the Doctor</strong> collect kidneys from the supply belts
and deliver them to the laboratory for analysis.</p>
<p>Bring special medication to the patient to earn bonus points!</p>
</div>
<div class="intro-rules">
<table>
<tr><td>Kidney delivered to lab</td><td class="pts pos">+1 pt</td></tr>
<tr><td>Special med to patient</td><td class="pts pos">+5 pts</td></tr>
<tr><td>Kidney dropped</td><td class="pts neg">1 pt</td></tr>
<tr><td>Special kidney dropped</td><td class="pts neg">3 pts</td></tr>
</table>
<p class="rule-note">Carry up to <strong>5 kidneys</strong> · 1 in 7 chance is special · <strong>3 minutes</strong> on the clock</p>
</div>
<div class="intro-controls">
<p>Move with <kbd>↑↓←→</kbd> or <kbd>W A S D</kbd></p>
<p>Stand at <em>column&nbsp;0</em> (left) to collect · at <em>column&nbsp;3</em> (right) to deposit / pickup / deliver</p>
</div>
`;
}
/* ── Step 1: player count selection ───────────────────────── */
function renderStep1() {
const el = document.getElementById('intro-screen');
el.innerHTML = `
<div class="intro-inner">
${headerHTML()}
<div class="player-select">
<p class="player-select-label">SELECT NUMBER OF PLAYERS</p>
<div class="player-select-btns">
<button class="btn-player-count" id="btn-1p">
<span class="pcount">1</span>
<span class="pcount-label">PLAYER</span>
</button>
<button class="btn-player-count" id="btn-2p">
<span class="pcount">2</span>
<span class="pcount-label">PLAYERS</span>
</button>
</div>
</div>
</div>
`;
el.querySelector('#btn-1p').addEventListener('click', () => renderStep2(1));
el.querySelector('#btn-2p').addEventListener('click', () => renderStep2(2));
}
/* ── Step 2: name entry ────────────────────────────────────── */
function renderStep2(playerCount) {
const el = document.getElementById('intro-screen');
const nameFields = playerCount === 2
? `
<div class="name-row">
<label for="name-p1">Player 1 Name</label>
<input type="text" id="name-p1" maxlength="12"
placeholder="DOCTOR" autocomplete="off" spellcheck="false"/>
</div>
<div class="name-row">
<label for="name-p2">Player 2 Name</label>
<input type="text" id="name-p2" maxlength="12"
placeholder="NURSE" autocomplete="off" spellcheck="false"/>
</div>`
: `
<div class="name-row">
<label for="name-p1">Your Name</label>
<input type="text" id="name-p1" maxlength="12"
placeholder="DOCTOR" autocomplete="off" spellcheck="false"/>
</div>`;
el.innerHTML = `
<div class="intro-inner">
${headerHTML()}
<div class="intro-form">
<div class="mode-badge">${playerCount === 2 ? '2 PLAYER MODE' : '1 PLAYER MODE'}</div>
${nameFields}
<div class="form-actions">
<button class="btn-secondary" id="back-btn">← BACK</button>
<button class="btn-primary" id="start-btn">START GAME</button>
</div>
</div>
</div>
`;
const p1Input = el.querySelector('#name-p1');
p1Input.focus();
el.querySelector('#back-btn').addEventListener('click', renderStep1);
el.querySelector('#start-btn').addEventListener('click', () => startGame(playerCount));
// Enter on last field starts game
const lastInput = el.querySelector(playerCount === 2 ? '#name-p2' : '#name-p1');
lastInput.addEventListener('keydown', e => {
if (e.key === 'Enter') startGame(playerCount);
});
}
/* ── Launch game ───────────────────────────────────────────── */
function startGame(playerCount) {
const el = document.getElementById('intro-screen');
const name1 = (el.querySelector('#name-p1').value.trim() || 'DOCTOR').toUpperCase();
const name2 = playerCount === 2
? ((el.querySelector('#name-p2').value.trim() || 'NURSE').toUpperCase())
: null;
document.querySelectorAll('.screen').forEach(s => s.classList.remove('active'));
document.getElementById('game-screen').classList.add('active');
Game.init(name1, name2);
}
return { show };
})();

84
kidney_lab/js/lab.js Normal file
View File

@@ -0,0 +1,84 @@
/**
* lab.js
* Laboratory logic: accept goods, analyse for special goods,
* trigger medication production. No rendering, no DOM.
*/
const LabState = Object.freeze({
IDLE: 'idle',
ANALYZING: 'analyzing',
});
class Lab {
constructor() {
this.reset();
}
reset() {
this.state = LabState.IDLE;
this.analyzeTimer = 0;
this.totalProcessed = 0;
this.flashTimer = 0; // brief visual flash on deposit
this._onMedReady = null;
}
/**
* Deposit an array of goods into the lab.
* @param {Good[]} goods goods from player inventory
* @param {function} onMedicationReady called (no args) when special analysis completes
* @returns {number} count of goods deposited
*/
depositGoods(goods, onMedicationReady) {
if (!goods.length) return 0;
let hasSpecial = false;
goods.forEach(good => {
this.totalProcessed++;
if (good.isSpecial) hasSpecial = true;
});
// Flash feedback
this.flashTimer = 500;
// Start analysis only if we got a special good and aren't already analysing
if (hasSpecial && this.state === LabState.IDLE) {
this.state = LabState.ANALYZING;
this.analyzeTimer = SETTINGS.LAB_ANALYZE_TIME;
this._onMedReady = onMedicationReady;
}
return goods.length;
}
/** True briefly after goods are deposited (visual feedback). */
isFlashing() {
return this.flashTimer > 0;
}
/** True while analysing a special good. */
isAnalyzing() {
return this.state === LabState.ANALYZING;
}
/** Progress 01 of the current analysis (0 if idle). */
analyzeProgress() {
if (this.state !== LabState.ANALYZING) return 0;
return 1 - this.analyzeTimer / SETTINGS.LAB_ANALYZE_TIME;
}
update(dt) {
if (this.flashTimer > 0) this.flashTimer -= dt;
if (this.state === LabState.ANALYZING) {
this.analyzeTimer -= dt;
if (this.analyzeTimer <= 0) {
this.analyzeTimer = 0;
this.state = LabState.IDLE;
if (this._onMedReady) {
this._onMedReady();
this._onMedReady = null;
}
}
}
}
}

140
kidney_lab/js/player.js Normal file
View File

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

101
kidney_lab/js/settings.js Normal file
View File

@@ -0,0 +1,101 @@
/**
* settings.js
* Static game constants — all tunable values live here.
* Do not import game state; this file has zero side-effects.
*/
const SETTINGS = Object.freeze({
/* ── Canvas ───────────────────────────────────────────────── */
CANVAS_WIDTH: 900,
CANVAS_HEIGHT: 520,
HEADER_HEIGHT: 60,
/* ── Timing ───────────────────────────────────────────────── */
GAME_DURATION: 180, // seconds
PLAYER_MOVE_COOLDOWN: 180, // ms between tile moves
LAB_ANALYZE_TIME: 3000, // ms to analyze a special good
GOOD_WAIT_TIME: 2500, // ms a good waits at belt end before dropping
MED_WAIT_TIME: 3500, // ms medication waits at pickup point
/* ── Scoring ──────────────────────────────────────────────── */
POINTS_GOOD_LAB: 1,
POINTS_SPECIAL_PATIENT: 5,
POINTS_DROP_GOOD: -1,
POINTS_DROP_SPECIAL: -3,
/* ── Carry limit ──────────────────────────────────────────── */
MAX_CARRY: 5,
/* ── Special good probability (1 in 7) ───────────────────── */
SPECIAL_GOOD_CHANCE: 1 / 7,
/* ── Layout — zone x boundaries ──────────────────────────── */
LEFT_ZONE_START: 0,
LEFT_ZONE_END: 350,
MIDDLE_ZONE_START: 350,
MIDDLE_ZONE_END: 600,
RIGHT_ZONE_START: 600,
RIGHT_ZONE_END: 900,
/* ── Vertical rows (y centres, shared across all zones) ───── */
// Row 0 = top, Row 1 = middle, Row 2 = bottom
BELT_ROWS: [155, 300, 445],
/* ── Player grid (4 cols × 3 rows inside middle zone) ──────── */
PLAYER_COLS: 3,
PLAYER_ROWS: 3,
PLAYER_COL_WIDTH: 100, // px between column centres
PLAYER_SIZE: 36, // bounding box size for drawing
BELT_HEIGHT: 13, // visual belt strip height (px)
/* ── Belt speeds ──────────────────────────────────────────── */
LEFT_BELT_SPEED: 45, // px / sec (goods move → right)
RIGHT_BELT_SPEED: 33, // px / sec (medication moves ← left)
/* ── Left belt spawn intervals (ms) ──────────────────────── */
BELT_MIN_SPAWN: 3200,
BELT_MAX_SPAWN: 6500,
/* ── Staggered initial spawn offsets per belt (ms) ─────────── */
BELT_INIT_OFFSETS: [600, 2400, 4200],
/* ── Item sizes ───────────────────────────────────────────── */
GOOD_SIZE: 26,
/* ── Patient happy flash duration ────────────────────────── */
PATIENT_HAPPY_DURATION: 1800, // ms
/* ── Colours ──────────────────────────────────────────────── */
COLOR_BG: '#0a1628',
COLOR_HEADER: '#0d1e38',
COLOR_HEADER_LINE: '#1a4070',
COLOR_BELT: '#152840',
COLOR_BELT_STRIPE: '#1e3d5c',
COLOR_BELT_EDGE: '#2a5a80',
COLOR_BELT_R: '#14283c',
COLOR_BELT_STRIPE_R:'#1c3650',
COLOR_GOOD: '#f0a030',
COLOR_GOOD_OUTLINE: '#c07010',
COLOR_SPECIAL: '#ff44aa',
COLOR_SPECIAL_OUT: '#cc1177',
COLOR_MED: '#44ffaa',
COLOR_MED_OUT: '#11cc77',
COLOR_PLAYER: '#4499ff',
COLOR_PLAYER_DARK: '#2266cc',
COLOR_PLAYER_CARRY: '#88ccff',
COLOR_LAB: '#1a4830',
COLOR_LAB_LIGHT: '#22aa55',
COLOR_LAB_OUTLINE: '#33dd77',
COLOR_PATIENT: '#ff8844',
COLOR_PATIENT_DARK: '#cc5511',
COLOR_PATIENT_HAPPY:'#ffdd44',
COLOR_TEXT: '#88ccff',
COLOR_SCORE_VAL: '#ffdd44',
COLOR_TIMER_OK: '#44dd88',
COLOR_TIMER_WARN: '#ffaa22',
COLOR_TIMER_DANGER: '#ff4444',
COLOR_ZONE_DIV: '#1a3a5c',
COLOR_ZONE_DIV2: '#0f2a40',
COLOR_SHADOW: 'rgba(0,0,0,0.45)',
});