diff --git a/css/game.css b/css/game.css
index 4b3528c..051f835 100644
--- a/css/game.css
+++ b/css/game.css
@@ -952,3 +952,75 @@ h2, h3 {
border-color: var(--accent-purple);
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%);
+}
diff --git a/dev-journal.md b/dev-journal.md
index f7763bb..3499bd8 100644
--- a/dev-journal.md
+++ b/dev-journal.md
@@ -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).
- Potential new building tiers or prestige milestones.
- 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.
diff --git a/index.html b/index.html
index 276e524..1f99667 100644
--- a/index.html
+++ b/index.html
@@ -17,6 +17,8 @@
0 ✨/sec
+
+
@@ -29,6 +31,8 @@
✨
Collect Stardust
+
+
Click power: 1 ✨
@@ -59,7 +63,7 @@
-
Achievements (0/30)
+
Achievements (0/33)
diff --git a/js/achievements.js b/js/achievements.js
index 48dd06b..552b09a 100644
--- a/js/achievements.js
+++ b/js/achievements.js
@@ -234,6 +234,31 @@ const ACHIEVEMENTS = {
description: 'Play for 1 hour total',
icon: '⏰',
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
}
};
diff --git a/js/audio.js b/js/audio.js
index 0910286..a58c4b0 100644
--- a/js/audio.js
+++ b/js/audio.js
@@ -161,6 +161,46 @@ const Audio = {
sparkle.start(startTime);
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);
+ });
}
};
diff --git a/js/game.js b/js/game.js
index 4f164d1..5041f72 100644
--- a/js/game.js
+++ b/js/game.js
@@ -18,14 +18,26 @@ const Game = {
stellarEssence: 0,
prestigeCount: 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
config: {
tickRate: 100, // ms per tick (10 ticks/sec)
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();
production *= prestigeMultiplier;
+ // Apply Golden Starbloom temporary production boost (+50%)
+ if (this.state.productionBoostUntil && this.state.productionBoostUntil > Date.now()) {
+ production *= 1.5;
+ }
// Apply prestige upgrades
for (const upgradeId of (this.state.prestigeUpgrades || [])) {
const upgrade = PRESTIGE_UPGRADES[upgradeId];
@@ -350,6 +366,90 @@ const Game = {
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
*/
@@ -361,6 +461,9 @@ const Game = {
this.addStardust(production * tickSeconds);
}
+ // Advance the Golden Starbloom random event clock.
+ this.updateGoldenBloom(tickSeconds);
+
UI.update();
Achievements.check();
},
diff --git a/js/particles.js b/js/particles.js
index 29bcaa9..602285d 100644
--- a/js/particles.js
+++ b/js/particles.js
@@ -271,6 +271,85 @@ const Particles = {
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
diff --git a/js/stats.js b/js/stats.js
index 95909f9..ca8627e 100644
--- a/js/stats.js
+++ b/js/stats.js
@@ -51,6 +51,17 @@ const Stats = {
Achievements Unlocked
0/${Object.keys(ACHIEVEMENTS).length}
+
+
Total Golden Blooms Claimed
+
0
+
+
+
Production Boost
+
Inactive
+
+
Next Achievement
Loading...
@@ -121,6 +132,30 @@ const Stats = {
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)
const progressBarEl = document.getElementById('stat-progress-bar');
if (progressBarEl) {
diff --git a/js/ui.js b/js/ui.js
index ce083ba..fb72fe8 100644
--- a/js/ui.js
+++ b/js/ui.js
@@ -38,6 +38,9 @@ const UI = {
// Prestige
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
*/