// ============ staff.js — handymen, mechanics, guards, jesters ============ import { STAFF_TYPES } from '../core/config.js'; import { uid, clamp, choice } from '../core/util.js'; import { findPath, randomNearbyPath, snapToPath } from '../world/path.js'; import { pay } from './economy.js'; const rng = Math.random; export function hireStaff(state, typeId) { const def = STAFF_TYPES[typeId]; if (!def || state.cash < 100) return null; pay(state, 100, 'wages', 'hire'); // spawn at entrance plaza const m = state.map; const x = m.entranceX + Math.floor(rng() * 3 - 1), y = m.entranceY - 3; const s = { id: uid(), kind: 'staff', type: typeId, def, name: `${def.name} #${state.staff.filter(q => q.type === typeId).length + 1}`, x, y, path: [], state: 'idle', taskT: 0, workT: 0, targetTask: null, speed: 1.6 + rng() * 0.5, wage: def.wage, }; state.staff.push(s); return s; } export function fireStaff(state, id) { const i = state.staff.findIndex(s => s.id === id); if (i >= 0) { state.staff.splice(i, 1); return true; } return false; } export function updateStaff(state, dt) { for (const s of state.staff) stepStaff(state, s, dt); } function stepStaff(state, s, dt) { if (s.path.length) { moveAlong(s, dt); if (!s.path.length) onArrive(state, s); return; } s.taskT -= dt; if (s.state === 'working') { s.workT -= dt; doWork(state, s, dt); if (s.workT <= 0) { s.state = 'idle'; s.taskT = 0.4; } return; } if (s.taskT > 0) return; findTask(state, s); } function moveAlong(s, dt) { const [tx, ty] = s.path[0]; const dx = tx - s.x, dy = ty - s.y; const d = Math.hypot(dx, dy); const stepLen = s.speed * dt; if (d <= stepLen) { s.x = tx; s.y = ty; s.path.shift(); } else { s.x += dx / d * stepLen; s.y += dy / d * stepLen; } } function onArrive(state, s) { if (!s.targetTask) { s.state = 'idle'; s.taskT = 0.5; return; } s.state = 'working'; s.workT = s.targetTask.kind === 'repair' ? 3 : 2.2; } function findTask(state, s) { switch (s.type) { case 'handyman': findCleanTask(state, s); break; case 'mechanic': findRepairTask(state, s); break; case 'guard': patrolNear(state, s, 'shop'); break; case 'entertainer': patrolNear(state, s, 'ride'); break; } if (!s.path?.length && !s.targetTask) s.taskT = 1 + rng() * 2; } function walkTo(state, s, tx, ty) { const [sx, sy] = snapToPath(state.map, s.x, s.y); const p = findPath(state.map, sx, sy, tx, ty, 5000); if (p) s.path = p; else { // staff can walk anywhere slowly (they know shortcuts) s.x = tx; s.y = ty; // teleport fallback keeps game playable } } // handyman: nearest litter/vomit tile function findCleanTask(state, s) { let best = null, bestD = Infinity; const m = state.map; const cx = Math.round(s.x), cy = Math.round(s.y); const R = 26; for (let y = Math.max(0, cy - R); y < Math.min(m.size, cy + R); y++) { for (let x = Math.max(0, cx - R); x < Math.min(m.size, cx + R); x++) { const i = m.idx(x, y); if (!m.pathType[i]) continue; const dirt = m.litter[i] + m.vomit[i] * 2; if (dirt < 0.3) continue; const d = Math.hypot(x - cx, y - cy); if (d < bestD) { bestD = d; best = [x, y]; } } } if (best) { s.targetTask = { kind: 'clean' }; walkTo(state, s, best[0], best[1]); } else if (rng() < 0.25) { wanderStaff(state, s); } } // mechanic: broken ride needing repair function findRepairTask(state, s) { const broken = state.rides.filter(r => r.status === 'broken'); if (!broken.length) { // preventive maintenance on random ride if (rng() < 0.02 && state.rides.length) { const r = choice(rng, state.rides); const adj = entranceAdj(state, r); if (adj) { s.targetTask = { kind: 'service', ride: r.id }; walkTo(state, s, adj[0], adj[1]); } } return; } const r = broken[0]; const adj = entranceAdj(state, r); if (adj) { s.targetTask = { kind: 'repair', ride: r.id }; walkTo(state, s, adj[0], adj[1]); } } function entranceAdj(state, r) { const cands = [[r.entranceX + 1, r.entranceY], [r.entranceX - 1, r.entranceY], [r.entranceX, r.entranceY + 1], [r.entranceX, r.entranceY - 1]]; for (const c of cands) if (state.map.isPath(c[0], c[1])) return c; return null; } function patrolNear(state, s, kind) { let pool; if (kind === 'shop') pool = [...state.shops]; else pool = state.rides.filter(r => r.status === 'open' && r.queue.length > 0); if (!pool.length) pool = state.rides.concat(state.shops.map(s2 => ({ entranceX: s2.x, entranceY: s2.y }))); if (!pool.length) { wanderStaff(state, s); return; } const t = choice(rng, pool); const ex = t.entranceX ?? t.x, ey = t.entranceY ?? t.y; const cands = [[ex + 1, ey], [ex - 1, ey], [ex, ey + 1], [ex, ey - 1]].filter(c => state.map.isPath(c[0], c[1])); if (!cands.length) { wanderStaff(state, s); return; } const c = choice(rng, cands); s.targetTask = { kind: kind === 'shop' ? 'guard' : 'entertain', x: c[0], y: c[1] }; walkTo(state, s, c[0], c[1]); } function wanderStaff(state, s) { const dest = randomNearbyPath(state.map, rng, Math.round(s.x), Math.round(s.y), 3, 10); if (dest) walkTo(state, s, dest[0], dest[1]); } function doWork(state, s, dt) { const task = s.targetTask; if (!task) return; const m = state.map; switch (task.kind) { case 'clean': { const i = m.idx(Math.round(s.x), Math.round(s.y)); m.litter[i] = Math.max(0, m.litter[i] - dt * 0.9); m.vomit[i] = Math.max(0, m.vomit[i] - dt * 0.7); break; } case 'repair': { const r = state.rides.find(r => r.id === task.ride); if (r && r.status === 'broken') { r.breakdownT -= dt * 2; if (r.breakdownT <= 0) fixRide(state, r); } break; } case 'service': { const r = state.rides.find(r => r.id === task.ride); if (r) r.reliability = clamp(r.reliability + dt * 0.05, 0, 0.99); break; } case 'guard': { state.vandalism = Math.max(0, state.vandalism - dt * 0.08); break; } case 'entertain': { for (const gid of collectQueuedGuests(state, s.x, s.y)) { const g = state.guests.find(g => g.id === gid); if (g) g.happiness = clamp(g.happiness + dt * 1.8, 0, 100); } break; } } } function collectQueuedGuests(state, x, y) { const out = []; for (const r of state.rides) { if (Math.hypot(r.entranceX - x, r.entranceY - y) > 12) continue; out.push(...r.queue); } return out.slice(0, 30); } export function fixRide(state, r) { r.status = 'closed'; // reopen manually or auto after check r.breakdownT = 0; r.brokenCount++; state.toasts.push({ kind: 'good', title: `${r.name} repaired`, text: 'Ready to reopen.' }); }