From 26ee47c2dbd46c3513703ea10e0ce6ed68a1824b Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Sun, 23 Aug 2026 00:24:53 +0000 Subject: [PATCH] Session 11: Golden Starbloom random event (clickable bonus + production boost) --- css/game.css | 72 ++++++++++++++++++++++++++++++ dev-journal.md | 36 +++++++++++++++ index.html | 6 ++- js/achievements.js | 25 +++++++++++ js/audio.js | 40 +++++++++++++++++ js/game.js | 107 ++++++++++++++++++++++++++++++++++++++++++++- js/particles.js | 79 +++++++++++++++++++++++++++++++++ js/stats.js | 35 +++++++++++++++ js/ui.js | 68 ++++++++++++++++++++++++++++ 9 files changed, 465 insertions(+), 3 deletions(-) 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 */