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
New Chapter Written
Title