Session 11: Golden Starbloom random event (clickable bonus + production boost)
This commit is contained in:
@@ -952,3 +952,75 @@ h2, h3 {
|
|||||||
border-color: var(--accent-purple);
|
border-color: var(--accent-purple);
|
||||||
background: linear-gradient(135deg, var(--bg-card) 0%, #2a1a3e 100%);
|
background: linear-gradient(135deg, var(--bg-card) 0%, #2a1a3e 100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
Golden Starbloom Random Event
|
||||||
|
============================================ */
|
||||||
|
|
||||||
|
/* Countdown badge in the header */
|
||||||
|
.golden-bloom-badge {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
font-weight: bold;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
padding: 4px 12px;
|
||||||
|
border-radius: 20px;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
white-space: nowrap;
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-6px);
|
||||||
|
transition: opacity 0.3s ease, transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.golden-bloom-badge.active {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
color: var(--accent-gold);
|
||||||
|
background: rgba(255, 215, 0, 0.12);
|
||||||
|
border-color: var(--accent-gold);
|
||||||
|
box-shadow: 0 0 12px rgba(255, 215, 0, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The floating bloom itself */
|
||||||
|
#golden-bloom-container {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.golden-bloom {
|
||||||
|
position: absolute;
|
||||||
|
font-size: 3rem;
|
||||||
|
cursor: pointer;
|
||||||
|
line-height: 1;
|
||||||
|
filter: drop-shadow(0 0 14px rgba(255, 215, 0, 0.9));
|
||||||
|
animation: goldenBloomFloat 2s ease-in-out infinite, goldenBloomPulse 1.4s ease-in-out infinite;
|
||||||
|
transition: transform 0.1s ease;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.golden-bloom:hover {
|
||||||
|
transform: scale(1.15);
|
||||||
|
filter: drop-shadow(0 0 22px rgba(255, 215, 0, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes goldenBloomFloat {
|
||||||
|
0%, 100% { translateY(0); }
|
||||||
|
50% { translateY(-8px); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes goldenBloomPulse {
|
||||||
|
0%, 100% { opacity: 0.85; }
|
||||||
|
50% { opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Golden particle effect styles */
|
||||||
|
.particle.golden-bloom-particle {
|
||||||
|
background: radial-gradient(circle, #fff2a8 0%, #ffd700 60%, transparent 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.particle.golden-sparkle {
|
||||||
|
background: radial-gradient(circle, #ffffff 0%, #ffe469 100%);
|
||||||
|
}
|
||||||
|
|||||||
@@ -294,3 +294,39 @@ Wrote a headless Node.js test (`/opt/data/idle-game-test/buy-max-test.mjs`, live
|
|||||||
- Consider adding "Buy Max" to upgrades too (upgrades are one-time so less critical, but a bulk "buy all affordable" could be nice).
|
- Consider adding "Buy Max" to upgrades too (upgrades are one-time so less critical, but a bulk "buy all affordable" could be nice).
|
||||||
- Potential new building tiers or prestige milestones.
|
- Potential new building tiers or prestige milestones.
|
||||||
- Sound variations or background music.
|
- Sound variations or background music.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Session 11 - 2026-08-23 (Morning)
|
||||||
|
**Status:** ✅ Golden Starbloom Random Event Added
|
||||||
|
|
||||||
|
#### What was done:
|
||||||
|
- Added a classic idle-game random event: the **Golden Starbloom**, a rare, time-limited bloom that occasionally appears in the click area. Clicking it grants a stardust bonus and a temporary production boost.
|
||||||
|
- Game state: added `state.goldenBlooms` (active flag, timer, lastSpawn timestamp, lifetime totalClaimed) plus `config.goldenBloom*` tuning values (min/max spawn interval 45-90s, 12s visible duration, bonus multiplier).
|
||||||
|
- Added a temporary **+50% production boost** (30 seconds) applied inside `getProductionPerSecond()`, alongside the existing prestige multiplier. Boost is expired-checked each tick and reflected in stats.
|
||||||
|
- Core logic in game.js: `claimGoldenBloom(x,y)` (awards `floor(production * mult)` stardust, min 1), `updateGoldenBloom(dtSeconds)` (schedules spawns on the tick clock; spawns once elapsed passes maxMs, with a small per-tick probability after minMs to avoid burst-spawning), `getProductionBoostInfo()`, and `getGoldenBloomRemaining()`.
|
||||||
|
- Wired `updateGoldenBloom()` into the main game tick so scheduling advances every 100ms.
|
||||||
|
- UI: added `UI.spawnGoldenBloom()` (floats a golden ⭐ at a random position in the click area, clickable to claim) and `UI.removeGoldenBloom()`, plus a header countdown badge (`golden-bloom-badge`) that glows while a bloom is live.
|
||||||
|
- Stats panel: added "Total Golden Blooms Claimed" stat and a "Production Boost" stat with a live progress bar (0-30s).
|
||||||
|
- Achievements: added 3 new achievements bringing the total from 30 to **33** — Lucky Bloom (first claim), Golden Collector (25 claims), Golden Enthusiast (100 claims). Updated the achievements-tab count in index.html.
|
||||||
|
- Visual + audio polish: `Particles.createGoldenEffect()` (golden ring burst + sparkle) and `Audio.playGoldenBloom()` (rising sweep + ascending shimmer chord). Added CSS for the bloom, its hover/floating/pulse animations, the badge, and golden particle styles.
|
||||||
|
- Wrote a headless Node.js test (`golden-bloom-test.mjs`) evaluating the real game.js in-process with DOM/UI/audio/particle stubs. **22/22 checks pass**, covering: no-op claim when inactive, bonus math, boost multiplier + expiry, countdown decrement + vanishing, forced spawn at maxMs, and no premature spawn before minMs. The existing buy-max test still passes (no regression).
|
||||||
|
|
||||||
|
#### Files modified:
|
||||||
|
- `js/game.js` - golden bloom state/config, claim/update/bloom/boost methods, tick wiring, production boost multiplier
|
||||||
|
- `js/ui.js` - spawnGoldenBloom(), removeGoldenBloom(), updateGoldenBloomBadge(), wired into update()
|
||||||
|
- `js/stats.js` - two new stat items + update logic (golden count, boost progress)
|
||||||
|
- `js/particles.js` - createGoldenEffect()
|
||||||
|
- `js/audio.js` - playGoldenBloom()
|
||||||
|
- `js/achievements.js` - 3 new golden achievements (Lucky Bloom, Golden Collector, Golden Enthusiast)
|
||||||
|
- `css/game.css` - bloom/badge/particle styling + keyframes
|
||||||
|
- `index.html` - golden-bloom-container in click area, golden-bloom-badge in header, achievement count 30->33
|
||||||
|
- `dev-journal.md` - appended Session 11 entry
|
||||||
|
|
||||||
|
#### Verification:
|
||||||
|
Headless test at /opt/data/idle-game-test/golden-bloom-test.mjs — **22/22 pass**. Existing buy-max test re-ran clean (6/6, no regression). All modified JS files pass `node --check`.
|
||||||
|
|
||||||
|
#### Next goals:
|
||||||
|
- Consider adding "Buy Max" to upgrades too.
|
||||||
|
- Potential new building tiers or prestige milestones.
|
||||||
|
- Sound variations or background music.
|
||||||
|
|||||||
+5
-1
@@ -17,6 +17,8 @@
|
|||||||
<div id="per-second">
|
<div id="per-second">
|
||||||
<span id="stardust-ps">0</span> ✨/sec
|
<span id="stardust-ps">0</span> ✨/sec
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Golden Starbloom countdown badge -->
|
||||||
|
<div id="golden-bloom-badge" class="golden-bloom-badge"></div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<!-- Main Layout -->
|
<!-- Main Layout -->
|
||||||
@@ -29,6 +31,8 @@
|
|||||||
<span class="btn-icon">✨</span>
|
<span class="btn-icon">✨</span>
|
||||||
<span class="btn-text">Collect Stardust</span>
|
<span class="btn-text">Collect Stardust</span>
|
||||||
</button>
|
</button>
|
||||||
|
<!-- Golden Starbloom random event appears here -->
|
||||||
|
<div id="golden-bloom-container"></div>
|
||||||
</div>
|
</div>
|
||||||
<div id="click-info">
|
<div id="click-info">
|
||||||
<p>Click power: <span id="click-power">1</span> ✨</p>
|
<p>Click power: <span id="click-power">1</span> ✨</p>
|
||||||
@@ -59,7 +63,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="achievements-tab" class="tab-content">
|
<div id="achievements-tab" class="tab-content">
|
||||||
<h3>Achievements (0/30)</h3>
|
<h3>Achievements (0/33)</h3>
|
||||||
<div id="achievements-list"></div>
|
<div id="achievements-list"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -234,6 +234,31 @@ const ACHIEVEMENTS = {
|
|||||||
description: 'Play for 1 hour total',
|
description: 'Play for 1 hour total',
|
||||||
icon: '⏰',
|
icon: '⏰',
|
||||||
condition: (state) => (Date.now() - state.startTime) >= 3600000
|
condition: (state) => (Date.now() - state.startTime) >= 3600000
|
||||||
|
},
|
||||||
|
|
||||||
|
// Golden Starbloom achievements
|
||||||
|
firstGoldenBloom: {
|
||||||
|
id: 'firstGoldenBloom',
|
||||||
|
name: 'Lucky Bloom',
|
||||||
|
description: 'Claim your first Golden Starbloom',
|
||||||
|
icon: '⭐',
|
||||||
|
condition: (state) => (state.goldenBlooms.totalClaimed || 0) >= 1
|
||||||
|
},
|
||||||
|
|
||||||
|
goldenCollector: {
|
||||||
|
id: 'goldenCollector',
|
||||||
|
name: 'Golden Collector',
|
||||||
|
description: 'Claim 25 Golden Blooms',
|
||||||
|
icon: '🌟',
|
||||||
|
condition: (state) => (state.goldenBlooms.totalClaimed || 0) >= 25
|
||||||
|
},
|
||||||
|
|
||||||
|
goldenEnthusiast: {
|
||||||
|
id: 'goldenEnthusiast',
|
||||||
|
name: 'Golden Enthusiast',
|
||||||
|
description: 'Claim 100 Golden Blooms',
|
||||||
|
icon: '💛',
|
||||||
|
condition: (state) => (state.goldenBlooms.totalClaimed || 0) >= 100
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+40
@@ -161,6 +161,46 @@ const Audio = {
|
|||||||
sparkle.start(startTime);
|
sparkle.start(startTime);
|
||||||
sparkle.stop(startTime + 0.2);
|
sparkle.stop(startTime + 0.2);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Play golden starbloom sound - an ethereal shimmering arpeggio with a
|
||||||
|
* rising sweep, to distinguish it from regular purchase sounds.
|
||||||
|
*/
|
||||||
|
playGoldenBloom() {
|
||||||
|
if (!this.ctx) return;
|
||||||
|
this.resume();
|
||||||
|
|
||||||
|
const now = this.ctx.currentTime;
|
||||||
|
|
||||||
|
// Rising magical sweep (like the prestige sound but brighter).
|
||||||
|
const sweep = this.ctx.createOscillator();
|
||||||
|
const sweepGain = this.ctx.createGain();
|
||||||
|
sweep.connect(sweepGain);
|
||||||
|
sweepGain.connect(this.ctx.destination);
|
||||||
|
sweep.type = 'sine';
|
||||||
|
sweep.frequency.setValueAtTime(300, now);
|
||||||
|
sweep.frequency.exponentialRampToValueAtTime(1200, now + 0.6);
|
||||||
|
sweepGain.gain.setValueAtTime(0.18, now);
|
||||||
|
sweepGain.gain.exponentialRampToValueAtTime(0.01, now + 0.7);
|
||||||
|
sweep.start(now);
|
||||||
|
sweep.stop(now + 0.7);
|
||||||
|
|
||||||
|
// A bright, ascending shimmer chord played quickly.
|
||||||
|
const shimmerNotes = [1046.5, 1318.5, 1568.0, 2093.0]; // C5, E5, G5, C6
|
||||||
|
shimmerNotes.forEach((freq, i) => {
|
||||||
|
const osc = this.ctx.createOscillator();
|
||||||
|
const gain = this.ctx.createGain();
|
||||||
|
osc.connect(gain);
|
||||||
|
gain.connect(this.ctx.destination);
|
||||||
|
osc.frequency.value = freq;
|
||||||
|
osc.type = 'triangle';
|
||||||
|
const startTime = now + 0.05 + i * 0.06;
|
||||||
|
gain.gain.setValueAtTime(0.1, startTime);
|
||||||
|
gain.gain.exponentialRampToValueAtTime(0.01, startTime + 0.25);
|
||||||
|
osc.start(startTime);
|
||||||
|
osc.stop(startTime + 0.25);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+105
-2
@@ -18,14 +18,26 @@ const Game = {
|
|||||||
stellarEssence: 0,
|
stellarEssence: 0,
|
||||||
prestigeCount: 0,
|
prestigeCount: 0,
|
||||||
totalPrestigeEssence: 0,
|
totalPrestigeEssence: 0,
|
||||||
prestigeUpgrades: []
|
prestigeUpgrades: [],
|
||||||
|
|
||||||
|
// Golden Starbloom random event state
|
||||||
|
goldenBlooms: {
|
||||||
|
active: false, // is a bloom currently visible on screen?
|
||||||
|
timer: 0, // seconds until the bloom vanishes
|
||||||
|
lastSpawn: Date.now(), // ms when we last (re)spawned one
|
||||||
|
totalClaimed: 0 // lifetime count of blooms the player has clicked
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// Configuration
|
// Configuration
|
||||||
config: {
|
config: {
|
||||||
tickRate: 100, // ms per tick (10 ticks/sec)
|
tickRate: 100, // ms per tick (10 ticks/sec)
|
||||||
autoSaveInterval: 30000, // 30 seconds
|
autoSaveInterval: 30000, // 30 seconds
|
||||||
costScaling: 1.15 // Cost multiplier per building level
|
costScaling: 1.15, // Cost multiplier per building level
|
||||||
|
goldenBloomMinMs: 45000, // min ms between bloom spawns (45s)
|
||||||
|
goldenBloomMaxMs: 90000, // max ms between bloom spawns (90s)
|
||||||
|
goldenBloomDuration: 12, // seconds a visible bloom stays before vanishing
|
||||||
|
goldenBloomBonusMult: 15 // production-per-second worth of stardust awarded
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -188,6 +200,10 @@ const Game = {
|
|||||||
const prestigeMultiplier = PRESTIGE.getMultiplier();
|
const prestigeMultiplier = PRESTIGE.getMultiplier();
|
||||||
production *= prestigeMultiplier;
|
production *= prestigeMultiplier;
|
||||||
|
|
||||||
|
// Apply Golden Starbloom temporary production boost (+50%)
|
||||||
|
if (this.state.productionBoostUntil && this.state.productionBoostUntil > Date.now()) {
|
||||||
|
production *= 1.5;
|
||||||
|
}
|
||||||
// Apply prestige upgrades
|
// Apply prestige upgrades
|
||||||
for (const upgradeId of (this.state.prestigeUpgrades || [])) {
|
for (const upgradeId of (this.state.prestigeUpgrades || [])) {
|
||||||
const upgrade = PRESTIGE_UPGRADES[upgradeId];
|
const upgrade = PRESTIGE_UPGRADES[upgradeId];
|
||||||
@@ -350,6 +366,90 @@ const Game = {
|
|||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Golden Starbloom - random event system.
|
||||||
|
* A rare, time-limited bloom occasionally appears on screen. Clicking it
|
||||||
|
* awards a stardust bonus and grants a temporary production boost.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Seconds remaining until the current bloom vanishes (0 if none active).
|
||||||
|
*/
|
||||||
|
getGoldenBloomRemaining() {
|
||||||
|
return this.state.goldenBlooms.active ? Math.ceil(this.state.goldenBlooms.timer) : 0;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Award a Golden Starbloom bonus and apply the production multiplier.
|
||||||
|
* Called when the player clicks a visible bloom. Returns the amount earned.
|
||||||
|
*/
|
||||||
|
claimGoldenBloom(x, y) {
|
||||||
|
const state = this.state.goldenBlooms;
|
||||||
|
|
||||||
|
// No active bloom - nothing to claim.
|
||||||
|
if (!state.active) return 0;
|
||||||
|
|
||||||
|
state.active = false;
|
||||||
|
state.timer = 0;
|
||||||
|
state.totalClaimed++;
|
||||||
|
|
||||||
|
// Bonus: goldenBloomBonusMult x current production per second (min 1).
|
||||||
|
const bonus = Math.max(1, Math.floor(this.getProductionPerSecond() * this.config.goldenBloomBonusMult));
|
||||||
|
this.addStardust(bonus);
|
||||||
|
|
||||||
|
// Temporary production boost: +50% for 30 seconds.
|
||||||
|
this.state.productionBoostUntil = Date.now() + 30000;
|
||||||
|
|
||||||
|
UI.showNotification('⭐ Golden Starbloom! +' + this.formatNumber(bonus) + ' ✨', 'achievement');
|
||||||
|
Audio.playGoldenBloom();
|
||||||
|
Particles.createGoldenEffect(x, y);
|
||||||
|
UI.update();
|
||||||
|
return bonus;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update golden bloom scheduling on each tick.
|
||||||
|
* Spawns a new bloom if none is active and enough time has elapsed;
|
||||||
|
* decrements the countdown of an active bloom until it vanishes.
|
||||||
|
*/
|
||||||
|
updateGoldenBloom(dtSeconds) {
|
||||||
|
const state = this.state.goldenBlooms;
|
||||||
|
|
||||||
|
// If a bloom is still on screen, count down its lifetime.
|
||||||
|
if (state.active) {
|
||||||
|
state.timer -= dtSeconds;
|
||||||
|
if (state.timer <= 0) {
|
||||||
|
state.active = false;
|
||||||
|
state.timer = 0;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, check whether it is time to spawn the next bloom.
|
||||||
|
const elapsed = Date.now() - state.lastSpawn;
|
||||||
|
const minMs = this.config.goldenBloomMinMs;
|
||||||
|
const maxMs = this.config.goldenBloomMaxMs;
|
||||||
|
if (elapsed >= minMs) {
|
||||||
|
// Wait until somewhere in [min, max] before spawning.
|
||||||
|
if (elapsed >= maxMs || Math.random() < 0.05) {
|
||||||
|
state.active = true;
|
||||||
|
state.timer = this.config.goldenBloomDuration;
|
||||||
|
state.lastSpawn = Date.now();
|
||||||
|
UI.spawnGoldenBloom();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the temporary production boost is currently active, and how
|
||||||
|
* many seconds remain.
|
||||||
|
*/
|
||||||
|
getProductionBoostInfo() {
|
||||||
|
const until = this.state.productionBoostUntil || 0;
|
||||||
|
if (until <= Date.now()) return { active: false, remaining: 0 };
|
||||||
|
return { active: true, remaining: Math.ceil((until - Date.now()) / 1000) };
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Game tick - called every config.tickRate ms
|
* Game tick - called every config.tickRate ms
|
||||||
*/
|
*/
|
||||||
@@ -361,6 +461,9 @@ const Game = {
|
|||||||
this.addStardust(production * tickSeconds);
|
this.addStardust(production * tickSeconds);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Advance the Golden Starbloom random event clock.
|
||||||
|
this.updateGoldenBloom(tickSeconds);
|
||||||
|
|
||||||
UI.update();
|
UI.update();
|
||||||
Achievements.check();
|
Achievements.check();
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -271,6 +271,85 @@ const Particles = {
|
|||||||
animation.onfinish = () => sparkle.remove();
|
animation.onfinish = () => sparkle.remove();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a Golden Starbloom burst - a shimmering golden explosion.
|
||||||
|
*/
|
||||||
|
createGoldenEffect(x, y) {
|
||||||
|
const colors = ['#ffd700', '#ffe469', '#fff2a8', '#ffcf3f', '#ffffff'];
|
||||||
|
|
||||||
|
// Outer ring of glowing golden particles
|
||||||
|
for (let i = 0; i < 24; i++) {
|
||||||
|
const particle = document.createElement('div');
|
||||||
|
particle.className = 'particle golden-bloom-particle';
|
||||||
|
|
||||||
|
const angle = (Math.PI * 2 * i) / 24;
|
||||||
|
const velocity = 90 + Math.random() * 70;
|
||||||
|
const size = 8 + Math.random() * 10;
|
||||||
|
const color = colors[Math.floor(Math.random() * colors.length)];
|
||||||
|
|
||||||
|
particle.style.cssText = `
|
||||||
|
left: ${x}px;
|
||||||
|
top: ${y}px;
|
||||||
|
width: ${size}px;
|
||||||
|
height: ${size}px;
|
||||||
|
background: ${color};
|
||||||
|
border-radius: 50%;
|
||||||
|
pointer-events: none;
|
||||||
|
position: fixed;
|
||||||
|
z-index: 998;
|
||||||
|
box-shadow: 0 0 12px ${color}, 0 0 24px #ffd700;
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.body.appendChild(particle);
|
||||||
|
|
||||||
|
const animation = particle.animate([
|
||||||
|
{ transform: 'translate(-50%, -50%) scale(1)', opacity: 1 },
|
||||||
|
{ transform: `translate(calc(-50% + ${Math.cos(angle) * velocity}px), calc(-50% + ${Math.sin(angle) * velocity}px)) scale(1.3)`, opacity: 0.85 },
|
||||||
|
{ transform: `translate(calc(-50% + ${Math.cos(angle) * velocity * 2}px), calc(-50% + ${Math.sin(angle) * velocity * 2}px)) scale(0)`, opacity: 0 }
|
||||||
|
], {
|
||||||
|
duration: 1200,
|
||||||
|
easing: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)'
|
||||||
|
});
|
||||||
|
|
||||||
|
animation.onfinish = () => particle.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Golden sparkle burst
|
||||||
|
for (let i = 0; i < 12; i++) {
|
||||||
|
const sparkle = document.createElement('div');
|
||||||
|
sparkle.className = 'particle golden-sparkle';
|
||||||
|
|
||||||
|
const angle = Math.random() * Math.PI * 2;
|
||||||
|
const velocity = 40 + Math.random() * 50;
|
||||||
|
const size = 3 + Math.random() * 4;
|
||||||
|
|
||||||
|
sparkle.style.cssText = `
|
||||||
|
left: ${x}px;
|
||||||
|
top: ${y}px;
|
||||||
|
width: ${size}px;
|
||||||
|
height: ${size}px;
|
||||||
|
background: #fff2a8;
|
||||||
|
border-radius: 50%;
|
||||||
|
pointer-events: none;
|
||||||
|
position: fixed;
|
||||||
|
z-index: 999;
|
||||||
|
box-shadow: 0 0 10px #ffd700;
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.body.appendChild(sparkle);
|
||||||
|
|
||||||
|
const animation = sparkle.animate([
|
||||||
|
{ transform: 'translate(-50%, -50%) scale(1)', opacity: 1 },
|
||||||
|
{ transform: `translate(calc(-50% + ${Math.cos(angle) * velocity}px), calc(-50% + ${Math.sin(angle) * velocity}px)) scale(0)`, opacity: 0 }
|
||||||
|
], {
|
||||||
|
duration: 700,
|
||||||
|
easing: 'ease-out'
|
||||||
|
});
|
||||||
|
|
||||||
|
animation.onfinish = () => sparkle.remove();
|
||||||
|
}
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Make globally accessible
|
// Make globally accessible
|
||||||
|
|||||||
+35
@@ -51,6 +51,17 @@ const Stats = {
|
|||||||
<div class="stat-label">Achievements Unlocked</div>
|
<div class="stat-label">Achievements Unlocked</div>
|
||||||
<div class="stat-value" id="stat-achievements">0/${Object.keys(ACHIEVEMENTS).length}</div>
|
<div class="stat-value" id="stat-achievements">0/${Object.keys(ACHIEVEMENTS).length}</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<div class="stat-label">Total Golden Blooms Claimed</div>
|
||||||
|
<div class="stat-value" id="stat-golden-blooms">0</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<div class="stat-label">Production Boost</div>
|
||||||
|
<div class="stat-value" id="stat-production-boost">Inactive</div>
|
||||||
|
<div class="stat-bar">
|
||||||
|
<div class="stat-bar-fill" id="stat-boost-progress" style="width: 0%"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="stat-item">
|
<div class="stat-item">
|
||||||
<div class="stat-label">Next Achievement</div>
|
<div class="stat-label">Next Achievement</div>
|
||||||
<div id="stat-next-achievement" style="font-size:0.85rem; color:var(--text-secondary); margin-top:4px">Loading...</div>
|
<div id="stat-next-achievement" style="font-size:0.85rem; color:var(--text-secondary); margin-top:4px">Loading...</div>
|
||||||
@@ -121,6 +132,30 @@ const Stats = {
|
|||||||
achievementsEl.textContent = `${Achievements.getCount()}/${Achievements.getTotal()}`;
|
achievementsEl.textContent = `${Achievements.getCount()}/${Achievements.getTotal()}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Total Golden Blooms claimed
|
||||||
|
const goldenBloomsEl = document.getElementById('stat-golden-blooms');
|
||||||
|
if (goldenBloomsEl) {
|
||||||
|
goldenBloomsEl.textContent = Game.state.goldenBlooms.totalClaimed || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Production boost status + progress bar
|
||||||
|
const boostInfo = Game.getProductionBoostInfo();
|
||||||
|
const boostEl = document.getElementById('stat-production-boost');
|
||||||
|
const boostProgressEl = document.getElementById('stat-boost-progress');
|
||||||
|
if (boostEl) {
|
||||||
|
if (boostInfo.active) {
|
||||||
|
boostEl.textContent = `Active (${boostInfo.remaining}s)`;
|
||||||
|
if (boostProgressEl) {
|
||||||
|
boostProgressEl.style.width = `${Math.min(100, (boostInfo.remaining / 30) * 100)}%`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
boostEl.textContent = 'Inactive';
|
||||||
|
if (boostProgressEl) {
|
||||||
|
boostProgressEl.style.width = '0%';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Game progress (based on total stardust milestones)
|
// Game progress (based on total stardust milestones)
|
||||||
const progressBarEl = document.getElementById('stat-progress-bar');
|
const progressBarEl = document.getElementById('stat-progress-bar');
|
||||||
if (progressBarEl) {
|
if (progressBarEl) {
|
||||||
|
|||||||
@@ -38,6 +38,9 @@ const UI = {
|
|||||||
|
|
||||||
// Prestige
|
// Prestige
|
||||||
PrestigeUI.update();
|
PrestigeUI.update();
|
||||||
|
|
||||||
|
// Golden Starbloom countdown badge
|
||||||
|
this.updateGoldenBloomBadge();
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -229,6 +232,71 @@ const UI = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Spawn a Golden Starbloom in the click area. Called by Game when the
|
||||||
|
* random event becomes active. The bloom floats at a random position and
|
||||||
|
* is clickable to claim its bonus.
|
||||||
|
*/
|
||||||
|
spawnGoldenBloom() {
|
||||||
|
const container = document.getElementById('golden-bloom-container');
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
// Remove any existing bloom first (shouldn't be one, but stay safe).
|
||||||
|
this.removeGoldenBloom();
|
||||||
|
|
||||||
|
const bloom = document.createElement('div');
|
||||||
|
bloom.className = 'golden-bloom';
|
||||||
|
bloom.textContent = '⭐';
|
||||||
|
bloom.title = 'Golden Starbloom! Click for a bonus!';
|
||||||
|
|
||||||
|
// Random position within the click area.
|
||||||
|
const rect = container.getBoundingClientRect();
|
||||||
|
const x = 20 + Math.random() * (Math.max(1, rect.width - 80));
|
||||||
|
const y = 20 + Math.random() * (Math.max(1, rect.height - 80));
|
||||||
|
bloom.style.left = `${x}px`;
|
||||||
|
bloom.style.top = `${y}px`;
|
||||||
|
|
||||||
|
// Click handler claims the bonus.
|
||||||
|
bloom.addEventListener('click', () => {
|
||||||
|
const earned = Game.claimGoldenBloom(rect.left + x, rect.top + y);
|
||||||
|
this.removeGoldenBloom();
|
||||||
|
if (earned > 0) {
|
||||||
|
UI.showFloatingText(
|
||||||
|
rect.left + x, rect.top + y,
|
||||||
|
`+${Game.formatNumber(earned)} ✨`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
container.appendChild(bloom);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove any visible Golden Starbloom from the click area.
|
||||||
|
*/
|
||||||
|
removeGoldenBloom() {
|
||||||
|
const container = document.getElementById('golden-bloom-container');
|
||||||
|
if (!container) return;
|
||||||
|
const existing = container.querySelector('.golden-bloom');
|
||||||
|
if (existing) existing.remove();
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the Golden Starbloom countdown badge shown in the header.
|
||||||
|
*/
|
||||||
|
updateGoldenBloomBadge() {
|
||||||
|
const el = document.getElementById('golden-bloom-badge');
|
||||||
|
const remaining = Game.getGoldenBloomRemaining();
|
||||||
|
if (!el) return;
|
||||||
|
|
||||||
|
if (remaining > 0) {
|
||||||
|
el.classList.add('active');
|
||||||
|
el.textContent = `⭐ Golden Bloom: ${remaining}s`;
|
||||||
|
} else {
|
||||||
|
el.classList.remove('active');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show floating text animation
|
* Show floating text animation
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user