| 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 = []; |
|
|
| |
| |
| 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); |
| |
| 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 (_) { } |
| if (!res.ok) { |
| throw new Error(data.detail || `Request failed (${res.status})`); |
| } |
| return data; |
| } catch (e) { |
| showToast(e.message || 'Network error', 'error'); |
| throw e; |
| } |
| } |
|
|
| |
| fetchInventory(); |
|
|
| |
| 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 (_) { } |
| }); |
|
|
| 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 = '<div class="loading-state">Registry empty. Initialize baseline specimens to begin observation.</div>'; |
| 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' : ''}`; |
| |
| |
| 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 = ` |
| <img src="/api/image/${seed.id}/96?stage=${seed.stage}&sid=${window.WEEDSIM_SID}" class="seed-img" alt="seed"> |
| <div class="seed-name" style="color: rgb(${seed.bud_color.join(',')})">${seed.name}</div> |
| <div class="seed-type">${seed.type}</div> |
| <div class="seed-stage">${seed.stage}</div> |
| `; |
|
|
| 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 = `<div class="empty-slot">Select ${placeholderText}</div>`; |
| return; |
| } |
|
|
| const swatch = (rgb) => `<span style="display:inline-block;width:12px;height:12px;border:1px solid #555;vertical-align:middle;background:rgb(${rgb.join(',')})"></span>`; |
| const num = [['THC', seed.thc], ['CBD', seed.cbd], ['Yield', seed.yield], ['GrowTime', seed.grow_time]]; |
| let statsHtml = ''; |
| num.forEach(([k, v]) => { |
| statsHtml += ` |
| <div class="stat-row"> |
| <span class="stat-label">${k}</span> |
| <span class="stat-val">${v} (${seed.stabilities[k]})</span> |
| </div>`; |
| }); |
| statsHtml += ` |
| <div class="stat-row"><span class="stat-label">Bud</span><span class="stat-val">${swatch(seed.bud_color)} ${seed.bud_color.join(',')}</span></div> |
| <div class="stat-row"><span class="stat-label">Leaf</span><span class="stat-val">${swatch(seed.leaf_color)} ${seed.leaf_color.join(',')}</span></div>`; |
|
|
| let allelesHtml = ''; |
| ['THC', 'CBD', 'Yield', 'GrowTime'].forEach(t => { |
| if (seed.alleles[t]) { |
| allelesHtml += ` |
| <div class="stat-row" style="margin-top: 4px;"> |
| <span class="stat-label">${t}</span> |
| <span class="stat-val" style="color: white;">${seed.alleles[t].join(' / ')}</span> |
| </div>`; |
| } |
| }); |
| ['BudColor', 'LeafColor'].forEach(t => { |
| const a = seed.alleles[t]; |
| if (a && a.Hue) { |
| allelesHtml += ` |
| <div class="stat-row" style="margin-top: 4px;"> |
| <span class="stat-label">${t} Hue</span> |
| <span class="stat-val" style="color: white;">${a.Hue.map(x => Math.round(x)).join(' / ')}</span> |
| </div>`; |
| } |
| }); |
|
|
| slotElement.innerHTML = ` |
| <div style="margin-bottom: 12px; font-weight: bold; color: var(--accent-lime);">${placeholderText}</div> |
| <div class="parent-details"> |
| <img src="/api/image_mature/${seed.id}/128?v=1&sid=${window.WEEDSIM_SID}" class="parent-img"> |
| <div class="parent-stats"> |
| <div style="font-size: 1.1rem; font-weight: bold; margin-bottom: 8px;">${seed.name}</div> |
| ${statsHtml} |
| <div style="margin-top: 8px; font-size: 0.8rem; font-weight: bold; text-decoration: underline;">ALLELES:</div> |
| ${allelesHtml} |
| </div> |
| </div> |
| <div style="margin-top: 16px; font-weight: bold; color: var(--text-muted); font-size: 0.9rem;">Lineage Tree</div> |
| <canvas class="lineage-canvas" width="250" height="160" style="margin-top: 8px; border-radius: 0; background: rgba(0,0,0,0.5);"></canvas> |
| `; |
|
|
| 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); |
| } |
| }); |
|
|
| |
| 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(); |
|
|
| |
| 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)); |
| } |
| }); |
|
|