function startIncidentGenerator() { // Initial seed generateIncident(); // Loop setInterval(() => { if (state.incidents.length < 4) { generateIncident(); } }, 15000); } async function generateIncident() { // Don't generate if queue full if (state.incidents.length >= 4) return; const p = window.globalPrompts.incidentGen; try { const res = await genText(p.systemPrompt, p.userPrompt, p.responseSchema); if (res && res.title) { const incident = { id: crypto.randomUUID(), ...res, timestamp: new Date(), status: 'PENDING' }; state.incidents.push(incident); genVibration(200); playSoundEffect('alert'); renderIncidents(); } } catch (e) { console.error("Failed to gen incident", e); } } // --- Core Logic --- function handleDispatch(incidentId, vehicleId) { playSoundEffect('click'); const incident = state.incidents.find(i => i.id === incidentId); const vehicle = state.vehicles.find(v => v.id === vehicleId); if (!incident || !vehicle) return; if (vehicle.status !== 'IDLE') return; // Type mismatch check (optional, let's just warn or allow for "chaos" gameplay) // Actually, let's enforce simple rules for better gameplay let successChance = 1.0; if (incident.type !== vehicle.type) successChance = 0.4; // Update state incident.status = 'RESPONDING'; vehicle.status = 'EN_ROUTE'; vehicle.assignedIncident = incidentId; state.activeDispatch = { incident, vehicle, progress: 0 }; renderVehicles(); renderIncidents(); showSirenModal(vehicle, incident); // Trigger Voice generateRadioChatter(vehicle.type); } async function generateRadioChatter(type) { const p = window.globalPrompts.radioChatter; try { // We pass the type in the user prompt to customize context const contextPrompt = `${p.userPrompt} (Role: ${type})`; const res = await genText(p.systemPrompt, contextPrompt, p.responseSchema); if (res && res.message) { // Play voice (dummy ID 'glitch' or provided from config, here generic) // Since voiceId is unknown, we rely on parent providing a default or try a common ID // Actually, let's assume parent provided voice config or handle generic. // For this constrained env, we send request and hope parent handles default. const audioUrl = await genVoice(res.message, 'default'); if (audioUrl) { const audio = new Audio(audioUrl); audio.play(); } } } catch(e) { console.error("Voice gen failed", e); } } function showSirenModal(vehicle, incident) { const modal = document.getElementById('siren-modal'); modal.classList.remove('hidden'); modal.classList.add('flex'); document.getElementById('siren-vehicle-name').innerText = vehicle.name; document.getElementById('siren-dest').innerText = incident.location; // Start transit timer let progress = 0; const interval = setInterval(() => { progress += 1; document.getElementById('transit-bar').style.width = `${progress}%`; if (progress >= 100) { clearInterval(interval); completeMission(vehicle, incident); } }, 50); // 5 seconds transit time roughly } async function completeMission(vehicle, incident) { // Stop Sirens toggleSiren('OFF'); const modal = document.getElementById('siren-modal'); modal.classList.add('hidden'); modal.classList.remove('flex'); // Calc Result const match = vehicle.type === incident.type; const xp = match ? 100 * incident.severity : 25; state.score += xp; // Update State vehicle.status = 'RETURNING'; vehicle.timer = 5; // Cooldown seconds incident.status = 'RESOLVED'; // Remove incident state.incidents = state.incidents.filter(i => i.id !== incident.id); renderHeader(); renderVehicles(); renderIncidents(); playSoundEffect('success'); genVibration(100); // Cooldown Logic const cooldown = setInterval(() => { vehicle.timer--; if (vehicle.timer <= 0) { vehicle.status = 'IDLE'; vehicle.assignedIncident = null; clearInterval(cooldown); } renderVehicles(); }, 1000); // Chance for visual reward if (match && incident.severity >= 4) { // Generate image of the scene try { const imgPrompt = `A monster high style scene: ${incident.description} at ${incident.location}. ${vehicle.name} is on the scene. Colorful, cartoon gothic style.`; const imgUrl = await genImage(imgPrompt); if (imgUrl) { showRewardModal(imgUrl, incident.title); } } catch(e) {} } } function showRewardModal(url, title) { const el = document.getElementById('reward-modal'); const img = document.getElementById('reward-img'); const txt = document.getElementById('reward-title'); img.src = url; txt.innerText = "MISSION SUCCESS: " + title; el.classList.remove('hidden'); el.classList.add('flex'); } function closeRewardModal() { document.getElementById('reward-modal').classList.add('hidden'); document.getElementById('reward-modal').classList.remove('flex'); } // --- Rendering --- function renderApp() { renderHeader(); renderVehicles(); renderIncidents(); } function renderHeader() { document.getElementById('score-display').innerText = `XP: ${state.score}`; } function renderVehicles() { const container = document.getElementById('fleet-grid'); container.innerHTML = ''; state.vehicles.forEach(v => { const div = document.createElement('div'); div.className = `bg-gray-900 border ${v.status === 'IDLE' ? 'border-gray-700 hover:border-white' : 'border-gray-800 opacity-75'} rounded-lg p-3 flex flex-col items-center gap-2 vehicle-card relative overflow-hidden`; // Status badge const statusColors = { 'IDLE': 'bg-green-500', 'EN_ROUTE': 'bg-yellow-500', 'RETURNING': 'bg-blue-500' }; div.innerHTML = `
${v.type}
${v.name}
${v.status === 'IDLE' ? 'READY' : (v.status === 'RETURNING' ? `COOLDOWN ${v.timer}s` : 'BUSY')}
`; if (state.activeDispatch && state.activeDispatch.vehicle.id === v.id) { div.classList.add('ring-2', 'ring-offset-2', 'ring-offset-black', 'ring-yellow-500'); } container.appendChild(div); }); } function renderIncidents() { const container = document.getElementById('incident-list'); container.innerHTML = ''; if (state.incidents.length === 0) { container.innerHTML = `
NO ACTIVE EMERGENCIES.
MONITORING SCANNERS...
`; return; } state.incidents.forEach(inc => { if (inc.status === 'RESOLVED') return; const isResponding = inc.status === 'RESPONDING'; const div = document.createElement('div'); div.className = `relative p-4 border-l-4 ${getTypeColor(inc.type)} bg-gray-900 mb-3 rounded-r-lg shadow-lg ${isResponding ? 'opacity-50' : 'hover:bg-gray-800'} transition-colors`; div.innerHTML = `
${inc.title} ${inc.type}
${inc.location}

${inc.description}

${!isResponding ? `
${generateDispatchButtons(inc.id)}
` : `
UNIT DISPATCHED
`} `; container.appendChild(div); }); } function getTypeColor(type) { if (type === 'POLICE') return 'border-blue-500'; if (type === 'FIRE') return 'border-red-500'; return 'border-pink-500'; // Medical } function generateDispatchButtons(incidentId) { // Filter idle vehicles const idle = state.vehicles.filter(v => v.status === 'IDLE'); if (idle.length === 0) return `
NO UNITS AVAILABLE
`; return idle.map(v => ` `).join(''); } function updateSirenUI(activeType) { const btns = document.querySelectorAll('.siren-btn'); btns.forEach(btn => { if (btn.dataset.type === activeType) { btn.classList.add('bg-red-600', 'text-white', 'scale-110', 'glow-pink'); btn.classList.remove('bg-gray-800', 'text-gray-400'); } else { btn.classList.remove('bg-red-600', 'text-white', 'scale-110', 'glow-pink'); btn.classList.add('bg-gray-800', 'text-gray-400'); } }); }

GHOUL DISPATCH 911

STORYBOOK EDITION

SINGLE PLAYER
XP: 0

Response Units