import { el, $, toast } from '../util.js'; import { G } from '../sim/state.js'; import { nextDay } from '../sim/daycycle.js'; import { closeDayAccounting } from '../sim/economy.js'; import { clearCustomers } from '../sim/customerAI.js'; import { showModal, closeModal, updateHud, updateTimeControls } from './ui.js'; import { audio } from '../audio/audio.js'; import { PRODUCT_BY_ID } from '../data/products.js'; import { fmtGold } from '../util.js'; import { emit } from '../util.js'; import { saveTo, unlockAchievement } from '../save.js'; import { on } from '../util.js'; // ============================================================ // END-OF-DAY REPORT β€” the satisfying part // ============================================================ let pendingClose = false; export function initReport() { on('requestClose', () => { if (pendingClose) return; pendingClose = true; const n = G.customers.length; let m; if (n > 0 && G.shop.minutes < 19 * 60 - 5) { m = showModal(`

⏰ Closing early?

There ${n === 1 ? 'is 1 customer' : `are ${n} customers`} still browsing. Send them home and settle the books?
`); } else { m = showModal(`

πŸŒ™ Closing time…

The lamps dim, the last coins clink. Settle today's books?
`); } m.querySelector('#rc-yes').onclick = () => { closeModal(); doCloseDay(); }; m.querySelector('#rc-no')?.addEventListener('click', () => { pendingClose = false; closeModal(); }); // reset guard when modal dismissed via other means setTimeout(() => { if (!document.getElementById('rc-yes')) pendingClose = false; }, 100); }); } function doCloseDay() { clearCustomers(); import('../sim/staff.js').then(({ staffMorning }) => {}); // (morning runs at next day) const sum = closeDayAccounting(); showReport(sum); } export function showReport(sum) { const d = G.data.today; const stars = Math.round(Math.min(5, Math.max(1, (sum.sat * 4.4 + Math.min(1.2, d.served / 14) * 0.8)))); const starStr = 'β˜…'.repeat(stars) + '' + 'β˜…'.repeat(5 - stars) + ''; // achievements checks if (d.served >= 15 && stars >= 5) unlockAchievement('perfect_day'); if (G.data.stats.totalProfit > 0 || sum.profit > 0) unlockAchievement('first_profit'); if (G.gold >= 1000) unlockAchievement('thousand_gold'); const m = showModal(`

πŸ“– Daily Report

Day ${G.shop.day} Β· β€œ${['A quiet little day.', 'Steady business!', 'A very good day!', 'The town is buzzing!', 'LEGENDARY!'][Math.min(4, Math.floor(stars - 1))]}”
${starStr}
Customers
0
Revenue
0
Expenses
0
Profit
0
Breakdown
wages βˆ’${sum.wages}g Β· rent βˆ’${sum.rent}g${sum.tax ? ` Β· tax βˆ’${sum.tax}g` : ''}${sum.stolen ? ` Β· πŸ’Έ stolen βˆ’${Math.round(sum.stolen)}g` : ''}
${sum.bestSeller ? `
Best seller
${PRODUCT_BY_ID[sum.bestSeller.pid].emoji} ${PRODUCT_BY_ID[sum.bestSeller.pid].name}
+${fmtGold(sum.bestSeller.value)}g (${sum.bestSeller.units} sold)
` : ''} ${sum.worstSeller ? `
Needs love
${PRODUCT_BY_ID[sum.worstSeller.pid].emoji} ${PRODUCT_BY_ID[sum.worstSeller.pid].name}
${sum.worstSeller.units} sold
` : ''}
Last 7 days revenue
${(G.data.history.revenue.slice(-7)).map((v, i, arr) => `
`).join('')}
Reputation
${sum.repDelta >= 0 ? '+' : ''}${sum.repDelta} ⭐
`, 'report'); // animate counters m.querySelectorAll('[data-count]').forEach(nodeEl => { const target = +nodeEl.dataset.count; const suffix = nodeEl.dataset.suffix || ''; const t0 = performance.now(), dur = 900; const tick = (t) => { const k = Math.min(1, (t - t0) / dur); const eased = 1 - Math.pow(1 - k, 3); nodeEl.textContent = fmtGold(target * eased) + suffix; if (k < 1) requestAnimationFrame(tick); }; requestAnimationFrame(tick); }); audio.register(); if (stars >= 4) setTimeout(() => audio.levelup(), 500); saveTo('autosave'); m.querySelector('#next-day').onclick = () => { closeModal(); nextDay(); import('../sim/staff.js').then(({ staffMorning }) => staffMorning()); updateHud(); updateTimeControls(); emit('newDay'); toast(`Day ${G.shop.day}. Fresh stock ideas, fresh faces!`, '', 'πŸŒ…'); }; }