From e20707efeabd6c10422c37bbf96f0bd3c886e903 Mon Sep 17 00:00:00 2001 From: Alexander Whitestone Date: Sat, 11 Apr 2026 19:46:47 -0400 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20animated=20resource=20counters=20?= =?UTF-8?q?=E2=80=94=20pulse=20on=20gain,=20shake=20on=20loss=20(#57)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add CSS keyframes: res-pulse (scale up + blue flash) and res-shake (horizontal shake + red flash) - Track previous resource values in _prevRes object - Detect gain/loss on each renderResources() call and trigger appropriate animation - Add rate color coding: green for positive, red for negative, dim for zero - Clean up animation classes after 400ms to allow re-triggering - No external dependencies, pure CSS + vanilla JS --- index.html | 4 ++++ js/engine.js | 29 ++++++++++++++++++++++++++++- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/index.html b/index.html index 93f3824..591a56a 100644 --- a/index.html +++ b/index.html @@ -95,6 +95,10 @@ body{background:var(--bg);color:var(--text);font-family:'SF Mono','Cascadia Code .toast-info{background:rgba(74,158,255,0.12);border-color:#4a9eff;color:#80bfff} @keyframes toast-in{from{transform:translateX(40px);opacity:0}to{transform:translateX(0);opacity:0.95}} @keyframes toast-out{from{opacity:0.95;transform:translateX(0)}to{opacity:0;transform:translateX(40px)}} +@keyframes res-pulse{0%{transform:scale(1)}50%{transform:scale(1.18);color:#80d0ff}100%{transform:scale(1)}} +@keyframes res-shake{0%,100%{transform:translateX(0)}20%{transform:translateX(-3px)}40%{transform:translateX(3px)}60%{transform:translateX(-2px)}80%{transform:translateX(2px)}} +.r-val.pulse{animation:res-pulse 0.35s ease-out} +.r-val.shake{animation:res-shake 0.3s ease-out;color:var(--red)!important} ::-webkit-scrollbar{width:4px}::-webkit-scrollbar-track{background:var(--bg)}::-webkit-scrollbar-thumb{background:var(--border);border-radius:2px} diff --git a/js/engine.js b/js/engine.js index 73476e7..7a1ceaf 100644 --- a/js/engine.js +++ b/js/engine.js @@ -739,16 +739,43 @@ function tickSprint(dt) { } // === RENDERING === +// Track previous resource values for gain/loss animations +const _prevRes = {}; + +function _animRes(id, val) { + const el = document.getElementById(id); + if (!el) return; + const prev = _prevRes[id]; + if (prev !== undefined && val !== prev) { + // Remove any running animation + el.classList.remove('pulse', 'shake'); + void el.offsetWidth; // force reflow + if (val > prev) { + el.classList.add('pulse'); + } else { + el.classList.add('shake'); + } + // Clean up class after animation ends + clearTimeout(el._animTimer); + el._animTimer = setTimeout(() => el.classList.remove('pulse', 'shake'), 400); + } + _prevRes[id] = val; +} + function renderResources() { const set = (id, val, rate) => { const el = document.getElementById(id); if (el) { + _animRes(id, val); el.textContent = fmt(val); // Show full spelled-out number on hover for educational value el.title = val >= 1000 ? spellf(Math.floor(val)) : ''; } const rEl = document.getElementById(id + '-rate'); - if (rEl) rEl.textContent = (rate >= 0 ? '+' : '') + fmt(rate) + '/s'; + if (rEl) { + rEl.textContent = (rate >= 0 ? '+' : '') + fmt(rate) + '/s'; + rEl.style.color = rate > 0 ? '#4caf50' : rate < 0 ? '#f44336' : '#444'; + } }; set('r-code', G.code, G.codeRate); From ecee3174a3e167c5523322c74cd6b69423055d42 Mon Sep 17 00:00:00 2001 From: Timmy Date: Sun, 12 Apr 2026 00:44:43 -0400 Subject: [PATCH 2/2] feat: custom tooltip system for buildings and projects (#57) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace native browser title= tooltips with styled custom tooltips that match the game's dark theme. Tooltips appear instantly on hover with building/project name and educational content. - Add CSS for #custom-tooltip with dark theme styling - Add tooltip div to HTML body - Add event delegation in main.js for [data-edu] elements - Convert renderBuildings and renderProjects to use data-edu and data-tooltip-label attrs instead of title= - Tooltip follows cursor with screen-edge clamping Refs: Epic #57 — Night of Polish, Task 4 (Tooltip system) --- index.html | 5 +++++ js/engine.js | 6 +++--- js/main.js | 40 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 48 insertions(+), 3 deletions(-) diff --git a/index.html b/index.html index 591a56a..6226888 100644 --- a/index.html +++ b/index.html @@ -100,6 +100,10 @@ body{background:var(--bg);color:var(--text);font-family:'SF Mono','Cascadia Code .r-val.pulse{animation:res-pulse 0.35s ease-out} .r-val.shake{animation:res-shake 0.3s ease-out;color:var(--red)!important} ::-webkit-scrollbar{width:4px}::-webkit-scrollbar-track{background:var(--bg)}::-webkit-scrollbar-thumb{background:var(--border);border-radius:2px} +#custom-tooltip{position:fixed;pointer-events:none;z-index:300;max-width:280px;padding:8px 12px;background:#0e1420;border:1px solid #1a3a5a;border-radius:6px;font-size:10px;color:#80bfff;line-height:1.5;opacity:0;transition:opacity 0.15s;box-shadow:0 4px 16px rgba(0,0,0,0.5)} +#custom-tooltip.visible{opacity:1} +#custom-tooltip .tt-label{color:var(--accent);font-weight:600;margin-bottom:3px;font-size:10px} +#custom-tooltip .tt-edu{color:#888;font-style:italic;font-size:9px;border-top:1px solid #1a2a3a;padding-top:4px;margin-top:4px} @@ -248,5 +252,6 @@ The light is on. The room is empty."
+
diff --git a/js/engine.js b/js/engine.js index 7a1ceaf..361f1aa 100644 --- a/js/engine.js +++ b/js/engine.js @@ -913,7 +913,7 @@ function renderBuildings() { // Locked preview: show dimmed with unlock hint if (!isUnlocked) { - html += `
`; + html += `
`; html += `${def.name}`; html += `\u{1F512}`; html += `Phase ${def.phase}: ${PHASES[def.phase]?.name || '?'}`; @@ -954,7 +954,7 @@ function renderBuildings() { return boost !== 1 ? `+${fmt(boosted)}/${r}/s` : `+${v}/${r}/s`; }).join(', ') : ''; - html += ``; diff --git a/js/main.js b/js/main.js index c27ca41..d3cb012 100644 --- a/js/main.js +++ b/js/main.js @@ -109,3 +109,43 @@ document.addEventListener('visibilitychange', function () { window.addEventListener('beforeunload', function () { saveGame(); }); + +// === CUSTOM TOOLTIP SYSTEM (#57) === +// Replaces native title= tooltips with styled, instant-appearing tooltips. +// Elements opt in via data-edu="..." and data-tooltip-label="..." attributes. +(function () { + const tip = document.getElementById('custom-tooltip'); + if (!tip) return; + + document.addEventListener('mouseover', function (e) { + const el = e.target.closest('[data-edu]'); + if (!el) return; + const label = el.getAttribute('data-tooltip-label') || ''; + const edu = el.getAttribute('data-edu') || ''; + let html = ''; + if (label) html += '
' + label + '
'; + if (edu) html += '
' + edu + '
'; + if (!html) return; + tip.innerHTML = html; + tip.classList.add('visible'); + }); + + document.addEventListener('mouseout', function (e) { + const el = e.target.closest('[data-edu]'); + if (el) tip.classList.remove('visible'); + }); + + document.addEventListener('mousemove', function (e) { + if (!tip.classList.contains('visible')) return; + const pad = 12; + let x = e.clientX + pad; + let y = e.clientY + pad; + // Keep tooltip on screen + const tw = tip.offsetWidth; + const th = tip.offsetHeight; + if (x + tw > window.innerWidth - 8) x = e.clientX - tw - pad; + if (y + th > window.innerHeight - 8) y = e.clientY - th - pad; + tip.style.left = x + 'px'; + tip.style.top = y + 'px'; + }); +})();