/* ============================================================
* ui.js β HUD, portraits, sim panel, buy catalog, build bar,
* pie menus, toasts, career picker
* ============================================================ */
'use strict';
/* ---------------- toasts ---------------- */
function toast(msg, cls = '') {
AudioSys.sfx(cls === 'bad' ? 'error' : 'toast');
const box = document.getElementById('toasts');
const el = document.createElement('div');
el.className = 'toast ' + cls;
el.innerHTML = msg;
box.appendChild(el);
while (box.children.length > 4) box.removeChild(box.firstChild);
setTimeout(() => { el.style.opacity = '0'; el.style.transition = 'opacity .5s'; }, 4200);
setTimeout(() => el.remove(), 4800);
}
function toastBill(amount) {
AudioSys.sfx('bill');
const box = document.getElementById('toasts');
const el = document.createElement('div');
el.className = 'toast bad';
el.innerHTML = `π¬ Bills due: ${fmtMoney(amount)}PAY`;
box.appendChild(el);
document.getElementById('payBillsBtn').onclick = () => {
if (G.funds >= amount) { G.funds -= amount; G.billsPaid = true; G.mailBillsDue = false;
toast(`β Bills paid: ${fmtMoney(amount)}`); el.remove(); Bus.emit('fundsChanged'); }
else toast('β Not enough money for the bills!', 'bad');
};
}
/* ---------------- HUD ---------------- */
function updateHud() {
document.getElementById('fundsVal').textContent = Math.floor(G.funds).toLocaleString('en-US');
const t = G.time;
document.getElementById('clockTime').textContent =
`${Math.floor(t.hour)}:${String(Math.floor((t.hourFloat % 1) * 60)).padStart(2, '0')}` +
` ${t.hour >= 12 ? 'PM' : 'AM'}`;
document.getElementById('clockDay').textContent = `${DAY_NAMES[(t.day - 1) % 7]}, Day ${t.day}`;
}
/* ---------------- portraits ---------------- */
const portraitEls = new Map();
function rebuildPortraits() {
const row = document.getElementById('portraitRow');
row.innerHTML = '';
portraitEls.clear();
for (const s of G.sims.filter(s => !s.isVisitor)) {
const d = document.createElement('div');
d.className = 'portrait' + (s.selected ? ' selected' : '');
const cv = document.createElement('canvas');
cv.width = 70; cv.height = 62;
d.appendChild(cv);
const nm = document.createElement('div'); nm.className = 'pname'; nm.textContent = s.name.split(' ')[0];
d.appendChild(nm);
const pb = document.createElement('div'); pb.className = 'plumbob'; pb.textContent = 'π·';
d.appendChild(pb);
const mb = document.createElement('div'); mb.className = 'moodbar';
const mfill = document.createElement('div'); mb.appendChild(mfill);
d.appendChild(mb);
d.onclick = () => { selectSim(s); };
row.appendChild(d);
portraitEls.set(s.id, { root:d, cv, plumbob:pb, mfill });
}
refreshPortraits();
}
function refreshPortraits() {
for (const s of G.sims.filter(s => !s.isVisitor)) {
const pe = portraitEls.get(s.id);
if (!pe) continue;
pe.root.classList.toggle('selected', !!s.selected);
const nmEl = pe.root.querySelector('.pname');
if (nmEl) nmEl.textContent = s.name.split(' ')[0] + (s.sickUntil && G.time.absMin < s.sickUntil ? ' π€’' : '');
pe.plumbob.textContent = s.moodScore() > 60 ? 'π’' : s.moodScore() > 32 ? 'π‘' : 'π΄';
const m = s.moodScore();
pe.mfill.style.width = m + '%';
pe.mfill.style.background = m > 60 ? '#3ddc55' : m > 32 ? '#ffd23e' : '#ff4040';
drawSimToCanvas(pe.cv, s, { facing:0, scale: Math.min(70/90, 62/130) + .18, groundPad: 4 });
}
}
/* ---------------- sim selection ---------------- */
function selectSim(s) {
for (const o of G.sims) o.selected = false;
if (s) s.selected = true;
G.selectedSim = s;
refreshPortraits();
updateSimPanel();
}
/* ---------------- sim side panel ---------------- */
function updateSimPanel() {
const panel = document.getElementById('simPanel');
const s = G.selectedSim;
if (!s || G.mode === 'cas') { panel.classList.add('hidden'); return; }
panel.classList.remove('hidden');
document.getElementById('simPanelName').textContent = s.name + (s.isVisitor ? ' (visiting)' : '');
const m = s.moodScore();
document.getElementById('simPanelMood').textContent =
'Mood: ' + (m > 75 ? 'Elated π' : m > 55 ? 'Happy π' : m > 35 ? 'Uneasy π' : m > 18 ? 'Miserable π£' : 'Desperate π«');
drawSimToCanvas(document.getElementById('simPortrait'), s, { facing:0, scale:1, groundPad:6 });
const body = document.getElementById('simPanelBody');
const tab = panel.dataset.tab || 'needs';
let html = '';
if (tab === 'needs') {
for (const k in NEEDS) {
const meta = NEEDS[k];
const v = clamp(s.needs[k], 0, 100);
const col = v > 55 ? '#43c15a' : v > 28 ? '#e8a33d' : '#e05252';
html += `
${meta.icon} ${meta.label}${Math.round(v)}
`;
}
} else if (tab === 'wants') {
const asp = ASPIRATIONS[s.aspiration];
const lvl = Math.floor(G.aspirationPoints / 500);
const prog = G.aspirationPoints % 500;
html += `
${asp.icon} ${asp.name}Lvl ${lvl}
${prog}/500 to next level
`;
if (!s.wants || !s.wants.length) html += 'No whims right nowβ¦';
for (const w of (s.wants || [])) {
const t = w.tpl;
const pct = t.count ? Math.round(w.progress / t.count * 100) : (t.amount ? Math.min(100, Math.round(w.bank / t.amount * 100)) : 0);
html += `
${t.icon} ${t.label}+${t.reward}
${t.count || t.amount ? `
` : ''}
`;
}
html += `
Whims are guided by the ${asp.name} aspiration. Fulfil them for aspiration points!
`;
} else if (tab === 'skills') {
for (const sk of SKILLS) {
const lvl = Math.floor(s.skills[sk.id]);
let pips = '';
for (let i = 0; i < 10; i++) pips += i < lvl ? 'β' : 'Β·';
html += `
${sk.icon} ${sk.name}${pips}${lvl}
`;
}
} else if (tab === 'rels') {
const others = G.sims.filter(o => o !== s);
if (!others.length) html += 'No other sims around yet. Try the phone β Invite Neighbor!';
for (const o of others) {
const r = s.getRel(o);
const ltrCol = r.ltr >= 50 ? '#43c15a' : r.ltr <= -25 ? '#e05252' : '#7f9fd9';
const strCol = r.str >= 40 ? '#43c15a' : r.str <= -20 ? '#e05252' : '#c9a24a';
const badge = r.ltr >= 75 ? 'π' : r.ltr >= 50 ? 'π€' : r.ltr <= -40 ? 'βοΈ' : '';
html += `
${o.name} ${badge}${Math.round(r.ltr)}
`;
}
} else if (tab === 'career') {
if (s.atWork) html += `