Merge pull request 'feat: animated resource counters — pulse on gain, shake on loss (#57)' (#71) from beacon/polish into main
Some checks failed
Smoke Test / smoke (push) Failing after 3s
Some checks failed
Smoke Test / smoke (push) Failing after 3s
This commit was merged in pull request #71.
This commit is contained in:
@@ -95,7 +95,15 @@ 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}
|
||||
#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}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -244,5 +252,6 @@ The light is on. The room is empty."
|
||||
</div>
|
||||
|
||||
<div id="toast-container"></div>
|
||||
<div id="custom-tooltip"></div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
35
js/engine.js
35
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);
|
||||
@@ -886,7 +913,7 @@ function renderBuildings() {
|
||||
|
||||
// Locked preview: show dimmed with unlock hint
|
||||
if (!isUnlocked) {
|
||||
html += `<div class="build-btn" style="opacity:0.25;cursor:default" title="${def.edu || ''}">`;
|
||||
html += `<div class="build-btn" style="opacity:0.25;cursor:default" data-edu="${def.edu || ''}" data-tooltip-label="${def.name} (Locked)">`;
|
||||
html += `<span class="b-name" style="color:#555">${def.name}</span>`;
|
||||
html += `<span class="b-count" style="color:#444">\u{1F512}</span>`;
|
||||
html += `<span class="b-cost" style="color:#444">Phase ${def.phase}: ${PHASES[def.phase]?.name || '?'}</span>`;
|
||||
@@ -927,7 +954,7 @@ function renderBuildings() {
|
||||
return boost !== 1 ? `+${fmt(boosted)}/${r}/s` : `+${v}/${r}/s`;
|
||||
}).join(', ') : '';
|
||||
|
||||
html += `<button class="build-btn ${afford ? 'can-buy' : ''}" onclick="buyBuilding('${def.id}')" title="${def.edu}" aria-label="Buy ${def.name}, cost ${costStr}">`;
|
||||
html += `<button class="build-btn ${afford ? 'can-buy' : ''}" onclick="buyBuilding('${def.id}')" data-edu="${def.edu || ''}" data-tooltip-label="${def.name}" aria-label="Buy ${def.name}, cost ${costStr}">`;
|
||||
html += `<span class="b-name">${def.name}</span>`;
|
||||
if (count > 0) html += `<span class="b-count">x${count}</span>`;
|
||||
html += `<span class="b-cost">Cost: ${costStr}</span>`;
|
||||
@@ -970,7 +997,7 @@ function renderProjects() {
|
||||
const afford = canAffordProject(pDef);
|
||||
const costStr = Object.entries(pDef.cost).map(([r, a]) => `${fmt(a)} ${r}`).join(', ');
|
||||
|
||||
html += `<button class="project-btn ${afford ? 'can-buy' : ''}" onclick="buyProject('${pDef.id}')" title="${pDef.edu || ''}" aria-label="Research ${pDef.name}, cost ${costStr}">`;
|
||||
html += `<button class="project-btn ${afford ? 'can-buy' : ''}" onclick="buyProject('${pDef.id}')" data-edu="${pDef.edu || ''}" data-tooltip-label="${pDef.name}" aria-label="Research ${pDef.name}, cost ${costStr}">`;
|
||||
html += `<span class="p-name">* ${pDef.name}</span>`;
|
||||
html += `<span class="p-cost">Cost: ${costStr}</span>`;
|
||||
html += `<span class="p-desc">${pDef.desc}</span></button>`;
|
||||
|
||||
40
js/main.js
40
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 += '<div class="tt-label">' + label + '</div>';
|
||||
if (edu) html += '<div class="tt-edu">' + edu + '</div>';
|
||||
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';
|
||||
});
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user