document.addEventListener('DOMContentLoaded', () => { const grid = document.getElementById('inventory-grid'); const slotA = document.getElementById('slot-a'); const slotB = document.getElementById('slot-b'); const btnLoad = document.getElementById('btn-load-starters'); const btnClear = document.getElementById('btn-clear'); const btnBreed = document.getElementById('btn-breed'); const btnClone = document.getElementById('btn-clone'); let selectedSeeds = []; let inventory = []; // Toast feedback. fetch() does NOT reject on HTTP 4xx/5xx, so every call // must be routed through apiCall() to actually surface backend errors. function showToast(message, kind = 'info') { let host = document.getElementById('toast-host'); if (!host) { host = document.createElement('div'); host.id = 'toast-host'; document.body.appendChild(host); } const t = document.createElement('div'); t.className = `toast toast-${kind}`; t.textContent = message; host.appendChild(t); // force reflow so the entry transition runs void t.offsetWidth; t.classList.add('show'); setTimeout(() => { t.classList.remove('show'); setTimeout(() => t.remove(), 300); }, 3200); } async function apiCall(url, options) { try { const res = await fetch(url, options); let data = {}; try { data = await res.json(); } catch (_) { /* non-JSON response */ } if (!res.ok) { throw new Error(data.detail || `Request failed (${res.status})`); } return data; } catch (e) { showToast(e.message || 'Network error', 'error'); throw e; } } // Initialize fetchInventory(); // Event Listeners btnLoad.addEventListener('click', async () => { try { await apiCall('/api/inventory/starter', { method: 'POST' }); showToast('Baseline specimens initialized', 'success'); fetchInventory(); } catch (_) {} }); btnClear.addEventListener('click', async () => { try { await apiCall('/api/inventory/clear', { method: 'POST' }); selectedSeeds = []; updateSelection(); showToast('Registry purged', 'info'); fetchInventory(); } catch (_) {} }); btnBreed.addEventListener('click', async () => { if (selectedSeeds.length !== 2) return; try { await apiCall('/api/breed', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ parent1_id: selectedSeeds[0].id, parent2_id: selectedSeeds[1].id }) }); selectedSeeds = []; updateSelection(); showToast('Hybrid synthesized', 'success'); fetchInventory(); } catch (_) { /* error already shown; keep selection so user can retry */ } }); btnClone.addEventListener('click', async () => { if (selectedSeeds.length !== 1) return; try { await apiCall('/api/clone', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ seed_id: selectedSeeds[0].id }) }); showToast('Specimen replicated', 'success'); fetchInventory(); } catch (_) {} }); async function fetchInventory() { try { const res = await fetch('/api/inventory?t=' + Date.now()); const data = await res.json(); inventory = data.seeds || []; renderGrid(); } catch (e) { console.error('Error fetching inventory', e); } } function renderGrid() { if (inventory.length === 0) { grid.innerHTML = '
Registry empty. Initialize baseline specimens to begin observation.
'; return; } grid.innerHTML = ''; inventory.forEach(seed => { const isSelected = selectedSeeds.some(s => s.id === seed.id); const card = document.createElement('div'); card.className = `seed-card ${isSelected ? 'selected' : ''}`; // Build tooltip text const num = [['THC', seed.thc], ['CBD', seed.cbd], ['Yield', seed.yield], ['GrowTime', seed.grow_time]]; let tooltip = `${seed.name}\nType: ${seed.type}\n`; num.forEach(([k, v]) => { tooltip += `${k}: ${v} (Stab: ${seed.stabilities[k]})\n`; }); tooltip += `Bud: rgb(${seed.bud_color.join(',')})\nLeaf: rgb(${seed.leaf_color.join(',')})\n`; card.title = tooltip; card.innerHTML = ` seed
${seed.name}
${seed.type}
${seed.stage}
`; if (seed.stage !== 'MATURE') { const growBtn = document.createElement('button'); growBtn.className = 'grow-btn'; growBtn.textContent = 'ADVANCE STAGE'; growBtn.onclick = async (e) => { e.stopPropagation(); try { await apiCall(`/api/seed/${seed.id}/grow`, { method: 'POST' }); fetchInventory(); } catch (_) {} }; card.appendChild(growBtn); } card.onclick = () => toggleSelection(seed); grid.appendChild(card); }); } function toggleSelection(seed) { const idx = selectedSeeds.findIndex(s => s.id === seed.id); if (idx >= 0) { selectedSeeds.splice(idx, 1); } else { if (selectedSeeds.length >= 2) selectedSeeds.shift(); selectedSeeds.push(seed); } updateSelection(); renderGrid(); } function updateSelection() { renderSlot(slotA, selectedSeeds[0], 'Primary Specimen'); renderSlot(slotB, selectedSeeds[1], 'Secondary Specimen'); const canBreed = selectedSeeds.length === 2 && selectedSeeds.every(s => s.can_attempt); const canClone = selectedSeeds.length === 1 && selectedSeeds[0].can_attempt; btnBreed.disabled = !canBreed; btnClone.disabled = !canClone; } function renderSlot(slotElement, seed, placeholderText) { if (!seed) { slotElement.innerHTML = `
Select ${placeholderText}
`; return; } const swatch = (rgb) => ``; const num = [['THC', seed.thc], ['CBD', seed.cbd], ['Yield', seed.yield], ['GrowTime', seed.grow_time]]; let statsHtml = ''; num.forEach(([k, v]) => { statsHtml += `
${k} ${v} (${seed.stabilities[k]})
`; }); statsHtml += `
Bud${swatch(seed.bud_color)} ${seed.bud_color.join(',')}
Leaf${swatch(seed.leaf_color)} ${seed.leaf_color.join(',')}
`; let allelesHtml = ''; ['THC', 'CBD', 'Yield', 'GrowTime'].forEach(t => { if (seed.alleles[t]) { allelesHtml += `
${t} ${seed.alleles[t].join(' / ')}
`; } }); ['BudColor', 'LeafColor'].forEach(t => { const a = seed.alleles[t]; if (a && a.Hue) { allelesHtml += `
${t} Hue ${a.Hue.map(x => Math.round(x)).join(' / ')}
`; } }); slotElement.innerHTML = `
${placeholderText}
${seed.name}
${statsHtml}
ALLELES:
${allelesHtml}
Lineage Tree
`; const canvas = slotElement.querySelector('.lineage-canvas'); drawLineageTree(canvas, seed); } function drawLineageTree(canvas, seed) { const ctx = canvas.getContext('2d'); ctx.clearRect(0, 0, canvas.width, canvas.height); const parentCoords = [{x: 60, y: 30}, {x: 180, y: 30}]; const gpCoords = [{x: 20, y: 110}, {x: 100, y: 110}, {x: 160, y: 110}, {x: 240, y: 110}]; const parents = (seed.lineage || []).map(id => inventory.find(s => s.id === id)); let grandparents = []; parents.forEach(p => { if (p && p.lineage) { grandparents.push(inventory.find(s => s.id === p.lineage[0])); grandparents.push(inventory.find(s => s.id === p.lineage[1])); } else { grandparents.push(null, null); } }); // Draw Lines ctx.strokeStyle = '#39FF14'; ctx.lineWidth = 2; ctx.beginPath(); if (grandparents[0]) { ctx.moveTo(parentCoords[0].x + 24, parentCoords[0].y + 48); ctx.lineTo(gpCoords[0].x + 16, gpCoords[0].y); } if (grandparents[1]) { ctx.moveTo(parentCoords[0].x + 24, parentCoords[0].y + 48); ctx.lineTo(gpCoords[1].x + 16, gpCoords[1].y); } if (grandparents[2]) { ctx.moveTo(parentCoords[1].x + 24, parentCoords[1].y + 48); ctx.lineTo(gpCoords[2].x + 16, gpCoords[2].y); } if (grandparents[3]) { ctx.moveTo(parentCoords[1].x + 24, parentCoords[1].y + 48); ctx.lineTo(gpCoords[3].x + 16, gpCoords[3].y); } ctx.stroke(); // Draw Images const drawImg = (s, coords, size) => { if (!s) return; const img = new Image(); img.src = `/api/image_mature/${s.id}/${size}?sid=${window.WEEDSIM_SID}`; img.onload = () => ctx.drawImage(img, coords.x, coords.y, size, size); }; parents.forEach((p, i) => drawImg(p, parentCoords[i], 48)); grandparents.forEach((gp, i) => drawImg(gp, gpCoords[i], 32)); } });