import type { CommandBus } from '@application/handlers/CommandBus'; import type { QueryBus } from '@application/handlers/QueryBus'; import { RACES_DATA, GEN_COLORS, RACE_GEN, BREEDING_RECIPES } from '@domain/value-objects/Race'; import { getDDImage } from '@presentation/helpers/dd-image'; import { esc } from '@presentation/helpers/format'; interface ApproNeeds { total: number; m: number; f: number; } interface ApproStep { baby: string; parentA: string; parentB: string; couples: number; gen: number; } interface ApproState { target: string; qty: number; repro: Record; inverted: Record; genFilter: number; search: string; } export class ReapproView { private el: HTMLElement | null = null; private approState: ApproState = { target: '', qty: 1, repro: {}, inverted: {}, genFilter: 0, search: '' }; private dirty = true; constructor( private commandBus: CommandBus, private queryBus: QueryBus, ) {} render(container: HTMLElement): void { this.el = document.createElement('div'); this.el.className = 'reappro-view-new'; container.appendChild(this.el); this.dirty = true; this.update(); } update(): void { if (!this.el || !this.dirty) return; this.dirty = false; const { target } = this.approState; if (target) { this.renderResults(); } else { this.renderTargetSelection(); } } destroy(): void { this.el?.remove(); this.el = null; } /* ── Target selection ── */ private renderTargetSelection(): void { if (!this.el) return; const { genFilter, search } = this.approState; const allRaces: { name: string; gen: number }[] = []; for (const [g, rs] of Object.entries(RACES_DATA)) { const gen = parseInt(g); if (gen < 2) continue; for (const r of rs) allRaces.push({ name: r.name, gen }); } const q = search.trim().toLowerCase(); const filtered = allRaces.filter(r => (genFilter > 0 ? r.gen === genFilter : true) && (q ? r.name.toLowerCase().includes(q) : true) ); let html = ''; // Header html += `

Sélectionne ta cible

`; for (let g = 2; g <= 10; g++) { html += ``; } html += `
`; // Search html += `
${search ? `` : ''}
`; // Race grid if (filtered.length === 0) { html += `
Aucune race trouvée
`; } else { html += `
`; for (const race of filtered) { const genCol = GEN_COLORS[race.gen] ?? '#888'; html += `
${getDDImage(race.name)}
GEN ${race.gen}
${esc(race.name)}
`; } html += `
`; } this.el.innerHTML = html; this.bindTargetSelectionEvents(); if (search) { const inp = this.el.querySelector('#appro-search-input'); if (inp) { inp.focus(); inp.setSelectionRange(inp.value.length, inp.value.length); } } } private bindTargetSelectionEvents(): void { if (!this.el) return; const searchInput = this.el.querySelector('#appro-search-input'); if (searchInput) { searchInput.addEventListener('input', () => { this.approState.search = searchInput.value; this.dirty = true; this.update(); }); } const clearBtn = this.el.querySelector('.accoup-search-clear'); if (clearBtn) { clearBtn.addEventListener('click', () => { this.approState.search = ''; this.dirty = true; this.update(); }); } this.el.querySelectorAll('.accoup-gen-chip').forEach(btn => { btn.addEventListener('click', () => { this.approState.genFilter = parseInt((btn as HTMLElement).dataset.gen ?? '0'); this.dirty = true; this.update(); }); }); this.el.querySelectorAll('.accoup-race-card').forEach(card => { card.addEventListener('click', () => { this.approState.target = (card as HTMLElement).dataset.race!; this.approState.qty = 1; this.approState.repro = {}; this.approState.inverted = {}; this.approState.search = ''; this.dirty = true; this.update(); }); }); } /* ── Results view ── */ private renderResults(): void { if (!this.el) return; const { target, qty } = this.approState; const targetGen = RACE_GEN[target] ?? 0; const { materials, steps } = this.calcAppro(target, qty); // Group steps by generation const stepsByGen = new Map(); for (const step of steps) { const list = stepsByGen.get(step.gen) ?? []; list.push(step); stepsByGen.set(step.gen, list); } const sortedGens = [...stepsByGen.keys()].sort((a, b) => a - b); let html = ''; // Top bar: back + quantity (top-left) html += `
`; // Materials (Gen 1) if (materials.length > 0) { const totalMat = materials.reduce((s, m) => s + m.m + m.f, 0); html += `
`; html += `

1 MATIÈRES PREMIÈRES — GÉNÉRATION 1

Total : ${totalMat} dragodindes requises
`; html += `
`; for (const mat of materials) { html += this.renderMaterialCard(mat.race, mat.m, mat.f); } html += `
`; // Arrow separator html += `
expand_more
`; } // Steps grouped by generation let stepNum = 2; for (let gi = 0; gi < sortedGens.length; gi++) { const gen = sortedGens[gi]; const genSteps = stepsByGen.get(gen)!; const isFinalGen = gi === sortedGens.length - 1; const genCol = GEN_COLORS[gen] ?? '#888'; const panelClass = isFinalGen ? 'reappro-step-panel reappro-step-final' : 'reappro-step-panel'; const headerClass = isFinalGen ? 'reappro-step-header reappro-step-header-final' : 'reappro-step-header'; const badgeClass = isFinalGen ? 'reappro-step-badge reappro-badge-primary' : 'reappro-step-badge'; html += `
`; html += `

${stepNum} ${isFinalGen ? 'ÉTAPE FINALE' : 'CROISEMENTS'} — GÉNÉRATION ${gen}

${genSteps.length} croisement${genSteps.length > 1 ? 's' : ''}
`; html += `
`; for (const step of genSteps) { const invKey = step.baby; const isInverted = this.approState.inverted[invKey] ?? false; const reproCount = this.approState.repro[invKey] ?? 0; const isLast = isFinalGen && genSteps.length === 1; html += `
`; html += `
`; // Parents const pA = isInverted ? step.parentB : step.parentA; const pB = isInverted ? step.parentA : step.parentB; html += this.renderCrossingParent(pA, '♂', step.couples); html += `add`; html += this.renderCrossingParent(pB, '♀', step.couples); html += `arrow_forward`; // Baby result const babyGen = RACE_GEN[step.baby] ?? 0; html += `
${getDDImage(step.baby)} G${babyGen}
${esc(step.baby)} ×${step.couples}
`; html += `
`; // .reappro-crossing-row // Controls row html += `
${step.couples} couple${step.couples > 1 ? 's' : ''}
`; html += `
`; // .reappro-crossing-card } html += `
`; // .reappro-crossings-grid html += `
`; // .reappro-step-panel stepNum++; // Arrow between gen panels (not after last) if (!isFinalGen) { html += `
expand_more
`; } } // Sticky target bar at bottom with save button html += `
auto_awesome
Cible de réapprovisionnement
${esc(target)} (Génération ${targetGen})
`; this.el.innerHTML = html; this.bindResultsEvents(); } private renderMaterialCard(race: string, m: number, f: number): string { const gen = RACE_GEN[race] ?? 1; const genCol = GEN_COLORS[gen] ?? '#888'; const parts: string[] = []; if (m > 0) parts.push(`♂ ${m}`); if (f > 0) parts.push(`♀ ${f}`); return `
${getDDImage(race)}
${esc(race)}
${parts.join(' ')}
`; } private renderCrossingParent(race: string, gender: string, qty: number): string { const genderColor = gender === '♂' ? '#50a0ff' : '#ff64a0'; return `
${getDDImage(race)}
${esc(race)} ${gender} ${qty}
`; } private bindResultsEvents(): void { if (!this.el) return; // Back buttons this.el.querySelectorAll('#appro-back').forEach(btn => { btn.addEventListener('click', () => { this.approState.target = ''; this.dirty = true; this.update(); }); }); const qtyInput = this.el.querySelector('#appro-qty') as HTMLInputElement | null; if (qtyInput) { let prevQty = qtyInput.value; qtyInput.addEventListener('focus', () => { prevQty = qtyInput.value; qtyInput.value = ''; }); qtyInput.addEventListener('blur', () => { if (qtyInput.value === '') qtyInput.value = prevQty; this.approState.qty = Math.max(1, parseInt(qtyInput.value) || 1); qtyInput.value = String(this.approState.qty); this.dirty = true; this.update(); }); } this.el.querySelectorAll('.reappro-repro-input').forEach(inp => { const inpEl = inp as HTMLInputElement; let prev = inpEl.value; inpEl.addEventListener('focus', () => { prev = inpEl.value; inpEl.value = ''; }); inpEl.addEventListener('blur', () => { if (inpEl.value === '') inpEl.value = prev; const race = inpEl.dataset.race!; this.approState.repro[race] = Math.max(0, parseInt(inpEl.value) || 0); this.dirty = true; this.update(); }); }); this.el.querySelectorAll('.reappro-invert-btn').forEach(btn => { btn.addEventListener('click', () => { const race = (btn as HTMLElement).dataset.race!; this.approState.inverted[race] = !this.approState.inverted[race]; this.dirty = true; this.update(); }); }); const saveBtn = this.el.querySelector('#appro-save'); if (saveBtn) { saveBtn.addEventListener('click', () => { const { target, qty, repro } = this.approState; const { materials, steps } = this.calcAppro(target, qty); this.commandBus.execute({ type: 'save-workflow', target, qty, materials, steps, repro: { ...repro }, }); (saveBtn as HTMLButtonElement).innerHTML = 'check Sauvegardé !'; setTimeout(() => { (saveBtn as HTMLButtonElement).innerHTML = 'save Sauvegarder ce plan'; }, 2000); }); } } /* ── Breeding plan calculation ── */ private calcAppro(target: string, qty: number): { materials: { race: string; m: number; f: number }[]; steps: ApproStep[] } { const targetGen = RACE_GEN[target] ?? 0; if (targetGen < 2 || !BREEDING_RECIPES[target]) { return { materials: [], steps: [] }; } const needs: Record = {}; needs[target] = { total: qty, m: 0, f: 0 }; const steps: ApproStep[] = []; for (let g = targetGen; g >= 2; g--) { const racesAtGen: string[] = []; for (const [rg, rs] of Object.entries(RACES_DATA)) { if (parseInt(rg) === g) { for (const r of rs) { if (needs[r.name]) racesAtGen.push(r.name); } } } for (const race of racesAtGen) { const recipe = BREEDING_RECIPES[race]; if (!recipe) continue; const need = needs[race]; if (!need || need.total <= 0) continue; const reproCount = this.approState.repro[race] ?? 0; const couples = Math.max(1, need.total - reproCount); const isInverted = this.approState.inverted[race] ?? false; const [recipeA, recipeB] = recipe; const parentA = isInverted ? recipeB : recipeA; const parentB = isInverted ? recipeA : recipeB; if (!needs[parentA]) needs[parentA] = { total: 0, m: 0, f: 0 }; needs[parentA].total += couples; needs[parentA].m += couples; if (!needs[parentB]) needs[parentB] = { total: 0, m: 0, f: 0 }; needs[parentB].total += couples; needs[parentB].f += couples; steps.push({ baby: race, parentA, parentB, couples, gen: g }); } } const materials: { race: string; m: number; f: number }[] = []; for (const [race, need] of Object.entries(needs)) { if (!BREEDING_RECIPES[race] && need.total > 0) { materials.push({ race, m: need.m, f: need.f }); } } steps.reverse(); return { materials, steps }; } }