Session 11: Golden Starbloom random event (clickable bonus + production boost)

This commit is contained in:
2026-08-23 00:24:53 +00:00
parent 478b9741a7
commit 26ee47c2db
9 changed files with 465 additions and 3 deletions
+25
View File
@@ -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
}
};
+40
View File
@@ -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);
});
}
};
+105 -2
View File
@@ -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();
},
+79
View File
@@ -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
+35
View File
@@ -51,6 +51,17 @@ const Stats = {
<div class="stat-label">Achievements Unlocked</div>
<div class="stat-value" id="stat-achievements">0/${Object.keys(ACHIEVEMENTS).length}</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-label">Next Achievement</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()}`;
}
// 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) {
+68
View File
@@ -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
*/