Game/Menu/MainMenu.razor
@using System;
@using Sandbox;
@using Sandbox.Services;
@using Sandbox.UI;
@using Sandbox.Utility
@inherits PanelComponent
<root>
@* ============================================================ *@
@* ================ FIRST RUN PET UNLOCK POPUP ================ *@
@* ============================================================ *@
@if(_showPetUnlock)
{
<div class="mm-pet-unlock-overlay">
<div class="mm-pet-unlock-popup">
<div class="mm-pet-unlock-eyebrow">A wild companion appeared!</div>
<image src="ui/icons/egg/egg_1.png" class="mm-pet-unlock-egg"/>
<div class="mm-pet-unlock-title">A mysterious egg</div>
<div class="mm-pet-unlock-desc">
It appeared after your third run.<br/>
Play more to hatch it.
</div>
<button class="mm-gift-close" style="pointer-events:all; margin-top:24px;"
onclick=@OnClosePetUnlock>
Meet my pet ▶
</button>
</div>
</div>
}
@* ============================================================ *@
@* =============== EVOLUTION CINEMATIC OVERLAY ================ *@
@* ============================================================ *@
@if(_showEvolution)
{
<div class="mm-evo-bg @(_evoPhase)">
@* Dramatic text at top *@
@if(!string.IsNullOrEmpty(_evoText))
{
<div class="mm-evo-text @(_evoPhase)">@_evoText</div>
}
@* Pet — centered *@
<div class="mm-evo-pet @(_evoPhase)">
@if(_petUnlockChestPhase)
{
@("📦")
}
else if(_evoShowNew)
{
<image [email protected](_save?.PetLevel ?? 1, _petFrameToggle)/>
}
else
{
<image [email protected](_evoFromLevel, _petFrameToggle)/>
}
</div>
@* Flash overlay *@
@if(_evoPhase == "evo-flash")
{
<div class="mm-evo-flash"></div>
}
@* Reveal buttons *@
@if(_evoPhase == "evo-reveal")
{
<div class="mm-evo-name">@GetPetName()</div>
<div class="mm-evo-level">Level @(_save?.PetLevel ?? 1)</div>
<button class="mm-gift-close"
style="pointer-events:all; margin-top:24px;"
onclick=@OnCloseEvolution>
Amazing! ▶
</button>
}
</div>
}
@* ============================================================ *@
@* ==================== WELCOME GIFT OVERLAY ================== *@
@* ============================================================ *@
@if(_showWelcomeGift)
{
<div class="mm-welcome-bg"></div>
@* Confetti — full screen, loops until dismissed *@
<div class="mm-confetti-wrap">
@for(int i = 0; i < 30; i++)
{
float left = (i * 3.7f) % 100f;
float dur = 2.0f + (i * 0.15f) % 1.5f;
float delay = (i * 0.13f) % 2.0f;
int size = (i % 3 == 0) ? 10 : (i % 2 == 0) ? 7 : 5;
string color = (i % 5 == 0) ? "rgba(251,191,36,0.95)"
: (i % 4 == 0) ? "rgba(167,139,250,0.95)"
: (i % 3 == 0) ? "rgba(52,211,153,0.95)"
: (i % 2 == 0) ? "rgba(244,114,182,0.95)"
: "rgba(96,165,250,0.95)";
float rot = (i * 23f) % 360f;
<div class="mm-confetti-piece"
style="left:@left.ToString("F1")%;
width:@(size)px;height:@(size)px;
background-color:@color;
animation-duration:@dur.ToString("F1")s;
animation-delay:@delay.ToString("F1")s;
transform:rotate(@rot.ToString("F0")deg);">
</div>
}
</div>
<div class="mm-welcome-bg"></div>
<div class="mm-welcome-gift">
<div class="mm-gift-title">🎉 WELCOME!</div>
<div class="mm-gift-subtitle">Open your starter gifts</div>
<div class="mm-gift-chests">
@for(int i = 0; i < _welcomeGifts.Count; i++)
{
int idx = i;
var gift = _welcomeGifts[idx];
bool canOpen = !gift.Opened && idx == _giftStage;
<div class="mm-gift-chest @(gift.Opened ? "opened" : "") @(canOpen ? "active" : "")"
onclick=@(() => { if(canOpen) OnOpenChest(idx); })>
@* What's shown when gift not opened yet *@
@if(!gift.Opened)
{
<div class="mm-chest-icon">🎁</div>
<div class="mm-chest-label">@(canOpen ? "CLICK!" : "")</div>
}
@* Show gift after click *@
else
{
<div class="mm-gift-icon">@gift.GiftIcon</div>
<div class="mm-gift-name">@gift.Title</div>
<div class="mm-gift-desc">@gift.Desc</div>
}
</div>
}
</div>
@if(_giftStage >= _welcomeGifts.Count)
{
<button class="mm-gift-close" onclick=@OnCloseWelcome>
Let's play! ▶
</button>
}
</div>
}
@* ============================================================================ *@
@* ============================= SURVIVAL LOADOUT ============================= *@
@* ============================================================================ *@
@if(_showSurvivalLoadout)
{
<div class="mm-welcome-bg"></div>
<div class="mm-welcome-gift survival-loadout-card">
<div class="mm-gift-title" style="font-size:28px;">⚡ SURVIVAL LOADOUT</div>
<div class="mm-gift-subtitle" style="margin-bottom:4px;">Chests never stop coming. Choose your bombs wisely.</div>
<div class="survival-loadout-hint">Pick 1–3 bomb types · @_survivalSelectedBombs.Count/3 selected</div>
<div class="survival-bomb-grid">
@foreach (var bomb in BombCatalog.All.Where(b => _save?.OwnsBomb(b.Type) ?? false)
.Prepend(new BombShopItem { Type = GridManager.BombType.Cross, Name = "Cross", Icon = "🎯", Hint = "+ shape, 1 tile range" }))
{
bool selected = _survivalSelectedBombs.Contains(bomb.Type);
<div class="survival-bomb-card @(selected ? "selected" : "")"
onclick=@(() => ToggleSurvivalBomb(bomb.Type))>
@if(selected)
{
<div class="survival-bomb-check">✓</div>
}
<div class="survival-bomb-icon">@bomb.Icon</div>
<div class="survival-bomb-name">@bomb.Name</div>
<div class="survival-bomb-hint">@bomb.Hint</div>
</div>
}
</div>
@if (ShowsClashWarning())
{
<div class="survival-tip-box">
<span class="survival-tip-icon">💡</span>
<span class="survival-tip-text">Diagonal + Cross can be awkward to combo — sticking to one type often makes cleaner chains.</span>
</div>
}
<button class="mm-gift-close survival-start-btn"
disabled="@(_survivalSelectedBombs.Count == 0)"
onclick=@ConfirmSurvivalLoadoutAndStart>
▶ Start Survival
</button>
</div>
}
@* ----------------------------------------------------------------- *@
@* --------------------------- MAIN MENU --------------------------- *@
@* ----------------------------------------------------------------- *@
@if( !_gameStarted )
{
<div class="mm-bg">
@* ── ANIMATED BACKGROUND ── *@
<div class="mm-grid-bg">
@for(int i = 0; i < 120; i++)
{
int col = i % 13;
int row = i / 13;
float cx = 6f;
float cy = 4.5f;
float dist = MathF.Sqrt((col - cx) * (col - cx) + (row - cy) * (row - cy));
@* Pick cell type — bomb islands + chain connectors + idle fill *@
string cls = "mm-gc-idle";
if (i == 21) cls = "mm-gc-expl2";
else if (i == 59) cls = "mm-gc-expl";
else if (i == 13 || i == 28 || i == 42 || i == 55 || i == 80 || i == 93 || i == 107) cls = "mm-gc-purp";
else if (i == 8 || i == 19 || i == 44 || i == 57 || i == 83 || i == 96 || i == 112) cls = "mm-gc-gold";
else if (i == 2 || i == 15 || i == 39 || i == 52 || i == 65 || i == 91 || i == 117) cls = "mm-gc-red";
else if (i == 14 || i == 27 || i == 32 || i == 33 || i == 34 || i == 40 ||
i == 45 || i == 46 || i == 47 || i == 53 || i == 56 || i == 58 ||
i == 66 || i == 71 || i == 72 || i == 73 || i == 75 || i == 84 ||
i == 85 || i == 86 || i == 87 || i == 88) cls = "mm-gc-chain";
@* Per-type animation period for phase-correct delay *@
float period = cls switch {
"mm-gc-expl" => 7f,
"mm-gc-expl2" => 9f,
"mm-gc-purp" => 4.5f,
"mm-gc-gold" => 5f,
"mm-gc-red" => 4.2f,
"mm-gc-chain" => 3.2f,
_ => 4f
};
float delay = (dist * 0.28f) % period;
<div class="mm-gc @cls"
style="animation-delay: -@(delay.ToString("F2"))s;">
</div>
}
</div>
@* Background Animated Orbs (2) *@
@* <div class="mm-orb-1 @(_idleActive ? "orb-active" : "")"></div>
<div class="mm-orb-2 @(_idleActive ? "orb-active" : "")"></div> *@
@* Diagonal drifting shapes *@
@for(int i = 0; i < 14; i++)
{
float shapeLeft = (i * 7.3f) % 100f;
float shapeTop = (i * 11.7f) % 100f;
float shapeDur = 12f + (i * 1.3f) % 10f;
float shapeDel = (i * 1.1f) % 8f;
int shapeSz = (i % 3 == 0) ? 18 : (i % 2 == 0) ? 12 : 8;
float shapeOp = (i % 3 == 0) ? 0.12f : (i % 2 == 0) ? 0.08f : 0.06f;
string shapeCol = (i % 5 == 0) ? $"rgba(251,191,36,{shapeOp:F2})"
: (i % 3 == 0) ? $"rgba(167,139,250,{shapeOp:F2})"
: (i % 2 == 0) ? $"rgba(244,114,182,{shapeOp:F2})"
: $"rgba(52,211,153,{shapeOp:F2})";
<div class="mm-shape sq"
style="left:@shapeLeft.ToString("F1")%;top:@shapeTop.ToString("F1")%;
width:@(shapeSz)px;height:@(shapeSz)px;
background-color:@shapeCol;
animation-duration:@shapeDur.ToString("F1")s;
animation-delay:@shapeDel.ToString("F1")s;">
</div>
}
<div class="mm-overlay"></div>
@* Particles *@
@for(int i = 0; i < 50; i++)
{
float pLeft = (i * 5.3f) % 100f;
float pDur = 7f + (i * 0.55f) % 5f;
float pDel = (i * 0.85f) % 9f;
int pSz = (i % 4 == 0) ? 4 : 2;
float pOp = (i % 2 == 0) ? 0.65f : 0.20f;
string pBc = (i % 5 == 0) ? "rgba(251,191,36,"
: (i % 3 == 0) ? "rgba(244,114,182,"
: "rgba(167,139,250,";
string pCol = pBc + pOp.ToString("F1") + ")";
<div class="mm-particle"
style="left:@pLeft.ToString("F1")%;width:@(pSz)px;height:@(pSz)px;background-color:@pCol;border-radius:50%;animation-duration:@pDur.ToString("F1")s;animation-delay:@pDel.ToString("F1")s;">
</div>
}
</div>
@* ============================================================================== *@
@* ============================== MAIN UI LAYOUT ================================ *@
@* ============================================================================== *@
<div class="mm-ui">
@* ------------------------------------------------------------------------- *@
@* ------------------------- LEFT UI : title + stats ---------------------- *@
@* ------------------------------------------------------------------------- *@
<div class="mm-left">
<div class="mm-eyebrow">FAST PACE GAME</div>
<div class="mm-title-chain">10</div>
<div class="mm-title-reaction-wrap">
<div class="mm-title-reaction">XPLOSION</div>s
</div>
<div class="mm-tagline">Place · Detonate · Survive</div>
@* ------------- Personal stats card (only if save exists) ------------- *@
@* If a save file exists, display the personal stats section*@
@if( _saveLoaded && (_save?.TotalRuns ?? 0) > 0 )
{
<div class="mm-stats-personal">
@* Best Score *@
<div class="mm-sp-row">
<span class="mm-sp-icon">🏆</span>
<span class="mm-sp-val">@(_save?.BestScore.ToString("N0") ?? "0")</span>
<span class="mm-sp-lbl">best score</span>
</div>
@* Best Wave *@
<div class="mm-sp-row">
<span class="mm-sp-icon">🌊</span>
<span class="mm-sp-val">@(_save?.BestWave ?? 0)</span>
<span class="mm-sp-lbl">best wave</span>
</div>
@* Total Frags *@
<div class="mm-sp-row">
<span class="mm-sp-icon">💠</span>
<span class="mm-sp-val">@(_save?.TotalFragments ?? 0)</span>
<span class="mm-sp-lbl">fragments</span>
</div>
@* Best chain length *@
<div class="mm-sp-row">
<span class="mm-sp-icon">⛓️</span>
<span class="mm-sp-val">@(_save?.BestChainLength ?? 0)</span>
<span class="mm-sp-lbl">best chain XPLOSION</span>
</div>
</div>
}
@* Hall of Fame (TOP 3 PLAYERS) *@
@if(_board?.Entries != null && _board.Entries.Length > 0)
{
<div class="mm-stats-personal mm-hof-card">
@for(int i = 0; i < Math.Min(3, _board.Entries.Length); i++)
{
var entry = _board.Entries[i];
string medal = i==0?"🥇":i==1?"🥈":"🥉";
<div class="mm-sp-row">
<span class="mm-sp-icon">@medal</span>
<span class="mm-sp-val">@GetName(entry)</span>
<span class="mm-sp-lbl">@entry.Value.ToString("N0")</span>
</div>
}
</div>
}
@* SIMULATION - TEMP — replace the hall of fame block with this to test layout *@
@* <div class="mm-hall-of-fame">
<div class="mm-hof-title">🏆 Hall of Fame</div>
<div class="mm-hof-row">
<span class="mm-hof-medal">🥇</span>
<span class="mm-hof-name">TestPlayer1</span>
<span class="mm-hof-score">1,234,567</span>
</div>
<div class="mm-hof-row">
<span class="mm-hof-medal">🥈</span>
<span class="mm-hof-name">TestPlayer2</span>
<span class="mm-hof-score">987,654</span>
</div>
<div class="mm-hof-row">
<span class="mm-hof-medal">🥉</span>
<span class="mm-hof-name">TestPlayer3</span>
<span class="mm-hof-score">456,789</span>
</div>
</div> *@
@* SIMULATION - TEMP - Replace the force panel block to test layout
<div class="mm-force-panel">
<div class="mm-force-panel-lbl">⚔️ TOP 100 COMBINED FORCE</div>
<div class="mm-force-bar-track">
<div class="mm-force-bar-fill" style="width:67%;"></div>
</div>
<div class="mm-force-panel-sub">67.0% toward next milestone</div>
</div> *@
@* TOP 100 CONTRIBUTOR PROGRESS BAR *@
@if(_forceBarLoaded)
{
<div class="mm-force-panel">
<div class="mm-force-panel-lbl">⚔️ TOP 100 COMBINED FORCE</div>
<div class="mm-force-bar-track">
<div class="mm-force-bar-fill" style="width:@GetForceBarPct().ToString("F1")%;"></div>
</div>
<div class="mm-force-panel-sub">@GetForceBarPct().ToString("F1")% toward next milestone</div>
</div>
}
<div class="mm-ver">10XPLOSION · Early Access</div>
</div>
@* ------------------------------------------------------------------------- *@
@* ----------------- MIDDLE UI : tab panels rendered here ------------------ *@
@* ------------------------------------------------------------------------- *@
<div class="mm-center">
@* Render Panels for tabs clicked buttons 'Pets, Leaderboard, Shop...'*@
@* ============================================================ *@
@* ================ TAB : COMMUNITY =========================== *@
@* ============================================================ *@
@if(_tab == "community")
{
<div class="mm-panel">
<div class="mm-content-card mm-center-enter">
<div class="mm-comm-from">FROM ME (the dev)</div>
<div class="mm-comm-greeting">Hey! :) 👋</div>
<div class="mm-comm-body">You're playing during Early Access — that means you're one of the first, and your feedback genuinely shapes what gets built next. Thanks for that!</div>
@if(!string.IsNullOrEmpty(_community?.Devnote))
{
<div class="mm-comm-devnote">
<span class="mm-comm-devnote-label">THIS WEEK</span>
<span class="mm-comm-devnote-text">@_community.Devnote</span>
</div>
}
@*<div class="mm-letter-divider"></div>*@
<div class="mm-letter-section-title">📅 What's next</div>
@foreach(var ev in (_community?.Events ?? new List<CommunityEvent>()))
{
if(string.IsNullOrEmpty(ev.Text)) continue;
string badgeClass = ev.Color switch {
"green" => "mm-badge-green",
"purple" => "mm-badge-purple",
"yellow" => "mm-badge-yellow",
_ => "mm-badge-gray"
};
<div class="mm-community-event-row">
<span class="mm-event-badge @badgeClass">@ev.Badge</span>
<span class="mm-event-text">@ev.Text</span>
</div>
}
<div class="mm-comm-discord-row">
<div class="mm-comm-discord-text">Got ideas, bugs, or just want to share a wild chain? Come say hi — it's just me on the other side.</div>
<button class="mm-discord-btn @(_discordCopied ? "copied" : "")"
onclick=@OnDiscordCopy
style="pointer-events:all;">
@(_discordCopied ? "✓ Link Copied!" : "💬 Join Discord")
</button>
</div>
</div>
</div>
}
@* ============================================================ *@
@* ============== TAB : LEADERBOARD =========================== *@
@* ============================================================ *@
@if(_tab == "lb")
{
<div class="mm-panel">
<div class="mm-content-card mm-center-enter">
<div class="mm-panel-hd">🏅 Leaderboard</div>
@* Mode toggle *@
<div class="mm-lb-mode-tabs">
<button class="mm-lb-mode-btn @(_lbMode == "score" ? "on" : "")"
onclick=@(() => { _lbMode = "score"; _ = LoadLb(); StateHasChanged(); })>
🏆 Score
</button>
<button class="mm-lb-mode-btn @(_lbMode == "pet" ? "on" : "")"
onclick=@(() => { _lbMode = "pet"; _ = LoadPetLb(); StateHasChanged(); })>
🐾 Pet
</button>
<button class="mm-lb-mode-btn @(_lbMode == "chain" ? "on" : "")"
onclick=@(() => { _lbMode = "chain"; _ = LoadChainLb(); StateHasChanged(); })>
⛓ Chain
</button>
<button class="mm-lb-mode-btn @(_lbMode == "reactor" ? "on" : "")"
onclick=@(() => { _lbMode = "reactor"; _ = LoadReactorLb(); StateHasChanged(); })>
⚛️ Reactor
</button>
<button class="mm-lb-mode-btn @(_lbMode == "force" ? "on" : "")"
onclick=@(() => { _lbMode = "force"; _ = LoadForceLb(); StateHasChanged(); })>
⚔️ Force
</button>
</div>
@* Leaderboard - SCORE *@
@if(_lbMode == "score")
{
@if(_loadingLb)
{
<div class="mm-panel-empty">Loading...</div>
}
else if(_board?.Entries == null || _board.Entries.Length == 0)
{
<div class="mm-panel-empty">No entries yet — be the first!</div>
}
else
{
@for(int i = 0; i < _board.Entries.Length; i++)
{
var e2 = _board.Entries[i];
int rk = i + 1;
string medal = rk==1?"🥇":rk==2?"🥈":rk==3?"🥉":$"#{rk}";
<div class="mm-lb-row @(IsMe(e2)?"me":"")">
<span class="mm-lb-rank">@medal</span>
<div class="mm-lb-name-col">
<span class="mm-lb-name">@GetName(e2)</span>
</div>
<span class="mm-lb-score">@(((int)e2.Value).ToString("N0"))</span>
</div>
}
}
}
@* Leaderboard - Pet XP & Level *@
else if (_lbMode == "pet")
{
@if(_loadingLb)
{
<div class="mm-panel-empty">Loading...</div>
}
else if(_petBoard?.Entries == null || _petBoard.Entries.Length == 0)
{
<div class="mm-panel-empty">No pet data yet!</div>
}
else
{
@for(int i = 0; i < _petBoard.Entries.Length; i++)
{
var e2 = _petBoard.Entries[i];
int rk = i + 1;
string medal = rk==1?"🥇":rk==2?"🥈":rk==3?"🥉":$"#{rk}";
int encoded = (int)e2.Value;
int lvl = encoded / 1_000_000;
int xp = encoded % 1_000_000;
string petEmoji = lvl switch {
1 => "ui/icons/egg/egg_1.png", 2 => "🐣", 3 => "🐥", 4 => "🦅", 5 => "🐉", _ => "👑"
};
<div class="mm-lb-row @(IsMe(e2)?"me":"")">
<span class="mm-lb-rank">@medal</span>
<div class="mm-lb-name-col">
<span class="mm-lb-name">@GetName(e2)</span>
</div>
<span class="mm-lb-pet-badge">
@if(lvl == 1)
{
<image src="ui/icons/egg/egg_1.png" class="mm-lb-pet-badge-img"/>
}
else
{
@petEmoji
}
@($"Lv{lvl}")
</span>
<span class="mm-lb-score">@xp xp</span>
</div>
}
}
}
@* Leaderboard - Best Chain Reaction Sequence (+score - players)*@
else if(_lbMode == "chain")
{
@if(_loadingLb)
{
<div class="mm-panel-empty">Loading...</div>
}
else if(_chainBoard?.Entries == null || _chainBoard.Entries.Length == 0)
{
<div class="mm-panel-empty">No chain data yet!</div>
}
else
{
@for(int i = 0; i < _chainBoard.Entries.Length; i++)
{
var entry = _chainBoard.Entries[i];
int rk = i + 1;
string medal = rk==1?"🥇":rk==2?"🥈":rk==3?"🥉":$"#{rk}";
int encoded = (int)entry.Value;
int chainLen = encoded / 10_000_000;
int tieScore = encoded % 10_000_000;
<div class="mm-lb-row @(IsMe(entry)?"me":"")">
<span class="mm-lb-rank">@medal</span>
<div class="mm-lb-name-col">
<span class="mm-lb-name">@GetName(entry)</span>
<span class="mm-lb-chain-title">@GetChainTitle(chainLen)</span>
</div>
<span class="mm-lb-chain-badge">⛓ @chainLen</span>
<span class="mm-lb-score">@tieScore.ToString("N0")</span>
</div>
}
}
}
@* Leaderboard - REACTOR (total Flux earned) *@
else if(_lbMode == "reactor")
{
@if(_loadingLb)
{
<div class="mm-panel-empty">Loading...</div>
}
else if(_reactorBoard?.Entries == null || _reactorBoard.Entries.Length == 0)
{
<div class="mm-panel-empty">No reactors online yet — be the first!</div>
}
else
{
@for(int i = 0; i < _reactorBoard.Entries.Length; i++)
{
var entry = _reactorBoard.Entries[i];
int rk = i + 1;
string medal = rk==1?"🥇":rk==2?"🥈":rk==3?"🥉":$"#{rk}";
long flux = (long)entry.Value;
<div class="mm-lb-row @(IsMe(entry)?"me":"")">
<span class="mm-lb-rank">@medal</span>
<div class="mm-lb-name-col">
<span class="mm-lb-name">@GetName(entry)</span>
<span class="mm-lb-chain-title">@GetReactorTitle(flux)</span>
</div>
<span class="mm-lb-chain-badge">🔷 @flux.ToString("N0")</span>
</div>
}
}
}
@* TOP 100 GLOBAL XP BAR CONTRIBUTORS *@
else if(_lbMode == "force")
{
@if(_loadingLb)
{
<div class="mm-panel-empty">Loading...</div>
}
else if(_forceBoard?.Entries == null || _forceBoard.Entries.Length == 0)
{
<div class="mm-panel-empty">No force data yet!</div>
}
else
{
@* Progress summary at top *@
<div class="mm-force-summary">
<span class="mm-force-summary-lbl">⚔️ Combined Force</span>
<div class="mm-force-bar-track">
<div class="mm-force-bar-fill" style="width:@GetForceBarPct().ToString("F1")%;"></div>
</div>
<span class="mm-force-summary-pct">@GetForceBarPct().ToString("F1")%</span>
</div>
@for(int i = 0; i < _forceBoard.Entries.Length; i++)
{
var entry = _forceBoard.Entries[i];
int rk = i + 1;
string medal = rk==1?"🥇":rk==2?"🥈":rk==3?"🥉":$"#{rk}";
// Decode medal from score if encoded
long score = (long)entry.Value;
string myMedal = IsMe(entry) ? GetMedalEmoji(_save?.ForceMedalTier ?? 0) : "";
float pct = _forceTotalScore > 0 ? (float)score / _forceTotalScore * 100f : 0f;
<div class="mm-lb-row @(IsMe(entry)?"me":"")">
<span class="mm-lb-rank">@medal</span>
<div class="mm-lb-name-col">
<span class="mm-lb-name">
@(myMedal != "" ? myMedal + " " : "")@GetName(entry)
</span>
</div>
<div class="mm-force-bar-mini-wrap">
<div class="mm-force-bar-mini">
<div class="mm-force-bar-mini-fill" style="width:@pct.ToString("F1")%;"></div>
</div>
<span class="mm-force-pct">@pct.ToString("F1")%</span>
</div>
<span class="mm-lb-score">@score.ToString("N0")</span>
</div>
}
}
}
</div>
</div>
}
@* ============================================================ *@
@* ============== TAB : STATS ================================= *@
@* ============================================================ *@
else if( _tab == "stats" )
{
<div class="mm-panel">
<div class="mm-content-card mm-center-enter"> @*Card bg surrounding div*@
<div class="mm-panel-hd">📊 Lifetime Stats</div>
<div class="mm-srow"><span class="mm-slbl">Total Runs</span> <span class="mm-sval">@(_save?.TotalRuns??0)</span></div>
<div class="mm-srow"><span class="mm-slbl">Chests Cleared</span> <span class="mm-sval">@(_save?.TotalChestsCleared??0)</span></div>
<div class="mm-srow"><span class="mm-slbl">Challenges Done</span><span class="mm-sval">@(_save?.TotalChallengesCompleted??0)</span></div>
<div class="mm-srow"><span class="mm-slbl">Time Played</span> <span class="mm-sval">@GetTimePlayed()</span></div>
<div class="mm-srow"><span class="mm-slbl">Best Score</span> <span class="mm-sval">@(_save?.BestScore.ToString("N0")??"0")</span></div>
<div class="mm-srow"><span class="mm-slbl">Best Wave</span> <span class="mm-sval">@(_save?.BestWave??0)</span></div>
</div>
</div>
}
@* ============================================================ *@
@* ============== TAB : SHOP ================================== *@
@* ============================================================ *@
else if( _tab == "shop" )
{
<div class="mm-panel">
<div class="mm-content-card mm-center-enter"> @*Card bg surrounding div*@
<div class="mm-panel-hd">💠 Fragment Shop</div>
@* --------- Shop section tabs --------- *@
@* Lets the player switch between different shop categories (modifiers / consumables / pets / bombs)*@
<div class="mm-shop-tabs">
@* ── Modifiers tab button ── *@
<button class="mm-shop-tab @(_shopTab == "mods" ? "on" : "") @(MainMenuTutorial.Instance?.IsActive == true ? "mm-tab-tut-locked" : "")"
onclick=@(() => {
if(MainMenuTutorial.Instance?.IsActive != true)
{ _shopTab = "mods"; StateHasChanged(); Sound.Play("UI_Button"); } })>
🎲 Modifiers
</button>
@* ── Consumables tab button ── *@
<button class="mm-shop-tab @(_shopTab == "consumables" ? "on" : "") @(MainMenuTutorial.Instance?.IsActive == true ? "mm-tab-tut-locked" : "")"
onclick=@(() => { if(MainMenuTutorial.Instance?.IsActive != true)
{ _shopTab = "consumables"; StateHasChanged(); Sound.Play("UI_Button"); } })>
🎁 Consumables
</button>
<button class="mm-shop-tab @(_shopTab == "pets" ? "on" : "") @(MainMenuTutorial.Instance?.IsActive == true ? "mm-tab-tut-locked" : "")"
onclick=@(() => { if(MainMenuTutorial.Instance?.IsActive != true)
{ _shopTab = "pets"; StateHasChanged();
Sound.Play("UI_Button"); Sound.Play("Meow_cat");} })>
@(_save?.PetUnlockShown == true ? "🐾 Pets" : "📦 ???")
</button>
@* ── Bombs tab button ── *@
<button class="mm-shop-tab @(_shopTab == "bombs" ? "on" : "") @(MainMenuTutorial.Instance?.IsActive == true ? "mm-tab-tut-locked" : "")"
onclick=@(() => {
if(MainMenuTutorial.Instance?.IsActive != true) {
_shopTab = "bombs"; StateHasChanged(); Sound.Play("UI_Button"); } })>
💣 Bombs
</button>
</div>
@* ---------------------------------------------------- *@
@* ----- Shop sub-section : MODIFIERS ----------------- *@
@* ---------------------------------------------------- *@
@if(_shopTab == "mods")
{
@* --------- Shop balance --------- *@
@* Displays our amount of fragments (update in real time)*@
<div class="mm-shop-balance">
<span class="mm-shop-balance-icon">💠</span>
<span class="mm-shop-balance-val">@(_save?.TotalFragments ?? 0)</span>
<span class="mm-shop-balance-lbl">fragments</span>
</div>
@* Let player know how much modifier he still can equip *@
<div class="mm-shop-slots">
<span class="mm-shop-slots-lbl">Active modifiers</span>
<span class="mm-shop-slots-val">
@(_save?.ActiveModifiers?.Count ?? 0) / @PlayerSave.MaxActiveModifiers
</span>
</div>
@* --------- Shop grid --------- *@
@* Displays the grid of modifiers elements that can be bought with frags*@
<div class="mm-shop-grid">
@* Loop through all our modifiers available in catalog to filter them*@
@foreach(var mod in ShopCatalog.All)
{
// do we already own it ?
bool owned = _save?.OwnsModifier(mod.Id) ?? false;
// do we have it active ?
bool active = _save?.ModifierActive(mod.Id) ?? false;
// can we afford it ? (if not bought yet)
bool canAfford = (_save?.TotalFragments ?? 0) >= mod.Cost;
// retrieves the rarity for this modifier
string rarityClass = mod.Rarity switch
{
ModifierRarity.Rare => "rarity-rare",
ModifierRarity.Epic => "rarity-epic",
_ => "rarity-common"
};
// ----- A single modifier card in the shop grid ----- //
<div class="mm-mod-card @rarityClass @(owned ? "owned" : "") @(active ? "active" : "")
@(!active && owned && (_save?.ActiveModifiers?.Count ?? 0) >= PlayerSave.MaxActiveModifiers ? "capped" : "")
@(!owned && (_save?.ActiveModifiers?.Count ?? 0) >= PlayerSave.MaxActiveModifiers ? "capped-unowned" : "")">
@* these above, show the different states of the cards
i.e : The 4th tells us if we still can equip a modifier (otherwise it's capped and card turns red)
i.e : The 5th tells us that, if we don't have this modifier, and we still can equip, it returns a diff color 'capped-unowned'*@
@* Add icons ; name ; desc ; footer - on that card*@
<div class="mm-mod-icon">@mod.Icon</div>
<div class="mm-mod-name">@mod.Name</div>
<div class="mm-mod-desc">@mod.Description</div>
@* ── Pushes button to bottom ── *@
<div class="mm-mod-footer">
<div>
@* Is this modifier owned or not*@
@* Yes : Show Active Button (or unlocked if bomb slot) *@
@if(owned)
{
// Bomb slots are auto-applied — show "Unlocked" instead of toggleable
bool isBombSlot = mod.Id == ModifierId.BombSlot2
|| mod.Id == ModifierId.BombSlot3
|| mod.Id == ModifierId.BombSlot4;
@if(isBombSlot)
{
<button class="mm-mod-btn btn-active" disabled="disabled">
<span>@(mod.Id == ModifierId.BombSlot2 ? "🎁 Gift" : "✓ Unlocked")</span>
</button>
}
else
{
// Do we already have 3 modifiers equipped ? (max)
bool capReached = !active && (_save?.ActiveModifiers?.Count ?? 0) >= PlayerSave.MaxActiveModifiers;
// Button for toggling modifiers that we own already (active/inactive/capped (if already 3 active 🟢))
<button class="mm-mod-btn @(active ? "btn-active" : capReached ? "btn-capped" : "btn-inactive")"
onclick=@(() => OnToggleMod(mod.Id))>
@(active ? "🟢 Active" : capReached ? "🔴 Slots Full" : "⚔️ Equip") @*🔌*@
</button>
}
}
else // No : Show Buy Button
{
<button class="mm-mod-btn btn-buy @(canAfford? "" : "cant-afford")"
onclick="@(() => OnBuyMod(mod.Id, mod.Cost))">
💠 @mod.Cost
</button>
}
</div>
</div>
</div>
}
</div>
}
@* ---------------------------------------------------- *@
@* ----- Shop sub-section : CONSUMABLES -------------- *@
@* ---------------------------------------------------- *@
@if (_shopTab == "consumables")
{
@* ── Balance ── *@
<div class="mm-shop-balance">
<span class="mm-shop-balance-icon">💠</span>
<span class="mm-shop-balance-val">@(_save?.TotalFragments ?? 0)</span>
<span class="mm-shop-balance-lbl">fragments</span>
</div>
@* ── Carry cap info ── *@
<div class="mm-shop-slots">
<span class="mm-shop-slots-lbl">Max 3 consumables per run (any mix)</span>
<span class="mm-shop-slots-val">@($"{_save?.TotalConsumablesOwned ?? 0} / 3")</span>
</div>
@* ── Consumables grid ── *@
<div class="mm-shop-grid">
@foreach (var cons in ConsumableCatalog.All)
{
int owned = _save?.GetConsumableCount(cons.Id) ?? 0;
bool canAfford = (_save?.TotalFragments ?? 0) >= cons.Cost;
bool atCap = (_save?.TotalConsumablesOwned ?? 0) >= 3;
// lock every button except Heart consumable buy btn (menu tuto)
bool tutLocked = MainMenuTutorial.Instance?.IsActive == true
&& MainMenuTutorial.Instance?.Step == 4
&& cons.Id != ConsumableId.Heart;
// Heart-specific cap based on pet level
bool atHeartCap = cons.Id == ConsumableId.Heart &&
owned >= (_save?.MaxHeartConsumables ?? 2);
<div class="mm-mod-card mm-consumable-card">
<div class="mm-mod-icon">@cons.Icon</div>
<div class="mm-mod-name">@cons.Name</div>
<div class="mm-mod-desc">@cons.Description</div>
<div class="mm-bomb-footer">
@* Stock badge — how many you currently own *@
@if (owned > 0)
{
<div class="mm-consumable-stock">
@($"In stock: {owned}")
</div>
}
@* Buy button — disabled if at cap or can't afford *@
<button class="mm-mod-btn btn-buy @(canAfford && !atCap && !atHeartCap && !tutLocked ? "" : "cant-afford")"
onclick=@(() => { if(!tutLocked) OnBuyConsumable(cons.Id, cons.Cost); })
disabled="@(atCap || !canAfford || atHeartCap || tutLocked)">
@* if this consumable card is a heart consu, and cap is reached*@
@if(atHeartCap)
{
@("❤️ Limit Reached")
}
@* if the cap for all consumables is reached *@
else if (atCap && owned == 0)
{
@("🔴 Slots Full")
}
// otherwise, show cost if can buy
else
{
@($"💠 {cons.Cost}")
}
</button>
</div>
</div>
}
</div>
}
@* ----- Future shop sub-sections will go here -------- *@
@* Consumables / Pets / Bombs *@
@* ---------------------------------------------------- *@
@if (_shopTab == "pets")
{
<div class="mm-shop-balance">
<span class="mm-shop-balance-icon">💠</span>
<span class="mm-shop-balance-val">@(_save?.TotalFragments ?? 0)</span>
<span class="mm-shop-balance-lbl">fragments</span>
</div>
@if(_save?.PetUnlockShown != true)
{
@* ── Locked — pet not yet revealed ── *@
<div class="mm-pet-display">
<div class="mm-pet-emoji">📦</div>
<div class="mm-pet-name">???</div>
<div class="mm-pet-level">Unlock after 3 runs</div>
</div>
<div class="mm-shop-slots">
<span class="mm-shop-slots-lbl">🎨 Cosmetic skins — coming soon</span>
</div>
}
else
{
@* ── Unlocked — show full pet ── *@
<div class="mm-pet-display">
<div class="mm-pet-emoji">
@if(GetPetImagePath() != null)
{
<image src=@GetPetImagePath() class="@((_save?.PetLevel ?? 1) == 2 ? "cat-sprite-anim" : "")"/>
}
else
{
@GetPetEmoji()
}
</div>
<div class="mm-pet-name">@GetPetName()</div>
<div class="mm-pet-level">Level @(_save?.PetLevel ?? 1)</div>
<div class="mm-pet-xp-track">
<div class="mm-pet-xp-fill" style="width:@GetPetXpPct()%;"></div>
</div>
<div class="mm-pet-xp-label">
@($"{_save?.PetXp ?? 0} / {GetPetXpNeeded()} XP to next level")
</div>
</div>
@if ((_save?.PetLevel ?? 1) < 6)
{
<div class="mm-pet-next">
<span class="mm-pet-next-label">Next evolution</span>
<span class="mm-pet-next-emoji">@GetNextPetEmoji()</span>
<span class="mm-pet-next-hint">Keep playing to evolve!</span>
</div>
}
else
{
<div class="mm-pet-next">
<span class="mm-pet-next-label">🌟 Maximum level reached!</span>
</div>
}
<div class="mm-shop-slots">
<span class="mm-shop-slots-lbl">🎨 Cosmetic skins — coming soon</span>
</div>
}
}
@* ---------------------------------------------------- *@
@* ------------ Shop sub-section : BOMBS -------------- *@
@* ---------------------------------------------------- *@
@if (_shopTab == "bombs")
{
@* ── Balance display ── *@
<div class="mm-shop-balance">
<span class="mm-shop-balance-icon">💠</span>
<span class="mm-shop-balance-val">@(_save?.TotalFragments ?? 0)</span>
<span class="mm-shop-balance-lbl">fragments</span>
</div>
@* ── Info line ── *@
<div class="mm-shop-slots">
<span class="mm-shop-slots-lbl">Owned bombs appear in your hand each wave</span>
</div>
@* ── Bombs grid ── *@
<div class="mm-shop-grid">
@foreach (var bomb in BombCatalog.All)
{
// Only show trial badge for bombs that are part of the trial
bool isTrialBomb = bomb.Type == GridManager.BombType.Sniper;
bool owned = _save?.OwnsBomb(bomb.Type) ?? false;
bool canAfford = (_save?.TotalFragments ?? 0) >= bomb.Cost;
bool isTrial = (_save?.IsInTrial == true) && isTrialBomb && !owned;
@* ── Single bomb card ── *@
<div class="mm-mod-card mm-bomb-card @(owned ? "owned" : "")">
@* Icon *@
<div class="mm-mod-icon">@bomb.Icon</div>
@* Name *@
<div class="mm-mod-name">@bomb.Name</div>
@* Blast pattern hint *@
<div class="mm-bomb-hint">@bomb.Hint</div>
@* Description *@
<div class="mm-mod-desc">@bomb.Description</div>
@* Trial badge — shown if player still has trial runs left and doesn't own it *@
@* ── Pushes badge + button to bottom of card ── *@
<div class="mm-bomb-footer">
@if (isTrial)
{
<div class="mm-bomb-trial-badge">
@($"🎁 Trial: {_save?.TrialBombsRunsLeft ?? 0} runs left")
</div>
}
@* Buy / Owned button *@
<div>
@if (owned)
{
bool isStarterBomb = bomb.Type == GridManager.BombType.Diagonal;
<button class="mm-mod-btn btn-active" disabled="disabled">
@(isStarterBomb ? "🎁 Gift" : "✓ Unlocked")
</button>
}
else
{
<button class="mm-mod-btn btn-buy @(canAfford ? "" : "cant-afford")"
onclick=@(() => OnBuyBomb(bomb.Type, bomb.Cost))>
💠 @bomb.Cost
</button>
}
</div>
</div>
</div>
}
</div>
}
</div>
</div>
}
@* ============================================================ *@
@* ============== TAB : PET FARM (idle grid) ================== *@
@* ============================================================ *@
else if( _tab == "farm" )
{
<div class="mm-panel">
<div class="mm-content-card mm-center-enter">
@* ── LOCKED STATE — shown before 3 runs completed ── *@
@if(_save?.PetUnlockShown != true)
{
<div class="mm-pet-locked-card">
@* Big mystery chest *@
<div class="mm-pet-locked-chest">📦</div>
@* Title *@
<div class="mm-pet-locked-title">???</div>
@* How many runs left *@
<div class="mm-pet-locked-desc">
Complete <span class="mm-pet-locked-count">@(MathF.Max(0, 3 - (_save?.TotalRuns ?? 0)).ToString("F0"))</span>
<span> more run@((3 - (_save?.TotalRuns ?? 0)) == 1 ? "" : "s") to reveal</span>
</div>
@* Progress bar + pips *@
<div class="mm-pet-locked-progress">
<div class="mm-pet-locked-bar">
<div class="mm-pet-locked-fill"
style="width:@(MathF.Min(100f, (_save?.TotalRuns ?? 0) / 3f * 100f).ToString("F0"))%;">
</div>
</div>
<div class="mm-pet-locked-pips">
@for(int i = 0; i < 3; i++)
{
bool done = (_save?.TotalRuns ?? 0) > i;
<div class="mm-pet-locked-pip @(done ? "done" : "")">
@(done ? "✓" : (i + 1).ToString())
</div>
}
</div>
</div>
</div>
}
@* ── UNLOCKED STATE — shown after pet is revealed ── *@
else
{
@* Header *@
<div class="mm-panel-hd">🐾 Your Pet</div>
@* ── Pet card : emoji + name + rename button ── *@
<div class="mm-pet-card">
@* Big pet emoji *@
<div class="mm-pet-hero-emoji">
@if(GetPetImagePath() != null)
{
<Image src=@GetPetImagePath()/>
}
else
{
@GetPetEmoji()
}
</div>
@* Name editing mode *@
@if(_editingPetName)
{
<div class="mm-pet-name-edit">
<div class="mm-pet-name-prompt">Name your companion</div>
<div @ref="_nameEntryContainer" class="mm-pet-name-input-wrap"></div>
<div class="mm-pet-name-hint">Type · click ✓ to confirm</div>
<div class="mm-pet-name-buttons">
<button class="mm-pet-name-confirm" style="pointer-events:all;"
onclick=@OnConfirmPetName>✓ Confirm</button>
<button class="mm-pet-name-cancel" style="pointer-events:all;"
onclick=@(() => { _editingPetName = false; CloseNameEntry(); StateHasChanged(); })>✕</button>
</div>
</div>
}
@* Name display mode *@
else
{
<div class="mm-pet-name-row">
<div class="mm-pet-hero-name">@GetDisplayPetName()</div>
<button class="mm-pet-rename-btn" onclick=@(() => {
_editingPetName = true;
_petNameInput = GetDisplayPetName();
StateHasChanged();
OpenNameEntry();
})>✏️</button>
</div>
}
</div>
@* ── Level label ── *@
<div class="mm-pet-hero-level">Level @(_save?.PetLevel ?? 1)</div>
@* ── XP progress bar ── *@
<div class="mm-pet-xp-track">
<div class="mm-pet-xp-fill" style="width:@GetPetXpPct()%;"></div>
</div>
@* ── XP label ── *@
<div class="mm-pet-xp-label">
@($"{_save?.PetXp ?? 0} / {GetPetXpNeeded()} XP to evolve!")
</div>
@* ── Stat bonuses — one row per level ── *@
<div class="mm-pet-stats">
@* Lv 1 — always active *@
<div class="mm-pet-stat-row">
<span class="mm-pet-stat-icon">💠</span>
<span class="mm-pet-stat-lbl">Fragments per 20 waves</span>
<span class="mm-pet-stat-val">+5 frags</span>
</div>
@* Lv 2 *@
<div class="mm-pet-stat-row">
<span class="mm-pet-stat-icon">❤️</span>
<span class="mm-pet-stat-lbl">Bonus starting life</span>
@if((_save?.PetLevel ?? 1) >= 2)
{ <span class="mm-pet-stat-val">+1 life</span> }
else
{ <span class="mm-pet-stat-val locked">Unlocks at Lv 2</span> }
</div>
@* Lv 3 *@
<div class="mm-pet-stat-row">
<span class="mm-pet-stat-icon">💥</span>
<span class="mm-pet-stat-lbl">Multi-hit chests pre-damaged</span>
@if((_save?.PetLevel ?? 1) >= 3)
{ <span class="mm-pet-stat-val">-1 hit on spawn</span> }
else
{ <span class="mm-pet-stat-val locked">Unlocks at Lv 3</span> }
</div>
@* Lv 4 *@
<div class="mm-pet-stat-row">
<span class="mm-pet-stat-icon">🌊</span>
<span class="mm-pet-stat-lbl">Double frags on wave 10, 20...</span>
@if((_save?.PetLevel ?? 1) >= 4)
{ <span class="mm-pet-stat-val">x2 frags</span> }
else
{ <span class="mm-pet-stat-val locked">Unlocks at Lv 4</span> }
</div>
@* Lv 5 *@
<div class="mm-pet-stat-row">
<span class="mm-pet-stat-icon">🐉</span>
<span class="mm-pet-stat-lbl">5+ chest chain score bonus</span>
@if((_save?.PetLevel ?? 1) >= 5)
{ <span class="mm-pet-stat-val">+15% score</span> }
else
{ <span class="mm-pet-stat-val locked">Unlocks at Lv 5</span> }
</div>
@* Lv 6 *@
<div class="mm-pet-stat-row">
<span class="mm-pet-stat-icon">👑</span>
<span class="mm-pet-stat-lbl">Second bonus life</span>
@if((_save?.PetLevel ?? 1) >= 6)
{ <span class="mm-pet-stat-val">+1 more life (3 total)</span> }
else
{ <span class="mm-pet-stat-val locked">Unlocks at Lv 6</span> }
</div>
</div>
@* ── Next evolution teaser ── *@
@if((_save?.PetLevel ?? 1) < 6)
{
<div class="mm-pet-next-hint">
Next: @GetNextPetEmoji() @GetNextPetName() — @(GetPetXpNeeded() - (_save?.PetXp ?? 0)) runs away
</div>
}
else
{
<div class="mm-pet-next-hint">🌟 Maximum level — fully evolved</div>
}
}
</div>
</div>
}
@* ============================================================ *@
@* ==================== TAB : THE REACTOR ===================== *@
@* ============================================================ *@
else if( _tab == "reactor" )
{
<div class="mm-panel">
<div class="mm-content-card mm-center-enter">
<div class="mm-panel-hd">⚛️ The Reactor</div>
@if(!(_save?.TutorialCompleted ?? false))
{
<div class="mm-reactor-locked">
<div class="mm-reactor-locked-icon">🔒</div>
<div class="mm-reactor-locked-title">Sealed</div>
<div class="mm-reactor-locked-desc">
Finish one run in Wave mode and the Reactor comes online.
</div>
</div>
}
else
{
var idl = _save.Idle;
double pending = IdleReactor.PreviewOfflineCores(idl);
double away = IdleReactor.PreviewOfflineSeconds(idl);
float tank = IdleReactor.PreviewStorageFraction(idl);
bool daily = IdleReactor.DailyAvailable(idl);
float capH = IdleBalance.OfflineCapHours(
idl.Level(IdleUpgradeId.OfflineCap),
idl.PerkLevel(IdlePerkId.DeepStorage));
int fragCap = IdleBalance.DailyFragmentCap(
idl.PerkLevel(IdlePerkId.FragmentCondenser), _save.PetLevel);
double petPct = (IdleBalance.PetBonus(_save.PetLevel, _save.PetUnlockShown,
idl.PerkLevel(IdlePerkId.PetLink)) - 1.0) * 100.0;
<div class="mm-reactor-blurb">
A board that plays itself. Chests drop, drones detonate them, chain
reactions print Cores — while you watch, while you play another
mode, and while the game is closed.
</div>
<div class="mm-reactor-tank">
<div class="mm-reactor-tank-top">
<span class="mm-reactor-tank-lbl">🛢️ OFFLINE STORAGE</span>
<span class="mm-reactor-tank-val">@IdleFormat.Duration(away) / @capH.ToString("F0")h</span>
</div>
<div class="mm-reactor-tank-track">
<div class="mm-reactor-tank-fill @(tank >= 1f ? "full" : "")"
style="width:@((tank * 100f).ToString("F1"))%;"></div>
</div>
@if(pending > 0)
{
<div class="mm-reactor-pending">
⚛️ @IdleFormat.Short(pending) Cores waiting to be collected
</div>
}
@if(pending <= 0)
{
<div class="mm-reactor-pending dim">
Nothing banked yet — run the reactor once to start production.
</div>
}
@if(tank >= 1f)
{
<div class="mm-reactor-warn">⚠️ Tanks are full — production is being wasted.</div>
}
</div>
@if(daily)
{
<div class="mm-reactor-daily">📅 Daily bonus ready — Day @(Math.Min(7, idl.DailyStreak + 1))</div>
}
<div class="mm-reactor-stats">
<div class="mm-reactor-stat">
<span class="mm-reactor-stat-val">@IdleFormat.Short(idl.Cores)</span>
<span class="mm-reactor-stat-lbl">⚛️ Cores</span>
</div>
<div class="mm-reactor-stat">
<span class="mm-reactor-stat-val">@IdleFormat.Short(idl.MeasuredCps)</span>
<span class="mm-reactor-stat-lbl">📈 per sec</span>
</div>
<div class="mm-reactor-stat">
<span class="mm-reactor-stat-val">@idl.Flux</span>
<span class="mm-reactor-stat-lbl">🔷 Flux</span>
</div>
<div class="mm-reactor-stat">
<span class="mm-reactor-stat-val">@idl.Meltdowns</span>
<span class="mm-reactor-stat-lbl">☢️ Meltdowns</span>
</div>
<div class="mm-reactor-stat">
<span class="mm-reactor-stat-val">@idl.BestChain</span>
<span class="mm-reactor-stat-lbl">⛓️ Best chain</span>
</div>
<div class="mm-reactor-stat">
<span class="mm-reactor-stat-val">@IdleAchievements.CompletedCount(idl) / @IdleAchievements.All.Count</span>
<span class="mm-reactor-stat-lbl">📜 Reactor Log</span>
</div>
</div>
<div class="mm-reactor-link">
<div class="mm-reactor-link-row">
<span class="mm-reactor-link-icon">💠</span>
<span class="mm-reactor-link-txt">
Cores condense into Fragments for the shop —
@idl.FragmentsToday / @fragCap today
</span>
</div>
<div class="mm-reactor-link-row">
<span class="mm-reactor-link-icon">🐾</span>
<span class="mm-reactor-link-txt">
Your pet boosts reactor output by +@petPct.ToString("F0")%
— level it up in Wave mode
</span>
</div>
</div>
<button class="mm-reactor-enter" style="pointer-events:all;"
onclick=@(() => { OnPlayReactor(); Sound.Play("PlayButton"); })>
⚛️ ENTER THE REACTOR ▶
</button>
}
</div>
</div>
}
@* ============================================================ *@
@* =================== TAB : CREDITS PAGE ===================== *@
@* ============================================================ *@
else if( _tab == "credits" )
{
<div class="mm-panel">
<div class="mm-content-card mm-center-enter">
<div class="mm-panel-hd">🎖️ Credits</div>
<div class="mm-credits-section">
<div class="mm-credits-role">🎮 Game Design, Code & Art :</div>
<div class="mm-credits-name">Purevsion</div>
</div>
<div class="mm-credits-section">
<div class="mm-credits-role">🔊 Sound Effects :</div>
<div class="mm-credits-name">Pixabay — pixabay.com /</div>
<div class="mm-credits-name">Freesound — freesound.org</div>
</div>
@* <div class="mm-credits-section">
<div class="mm-credits-role">🎵 Music</div>
<div class="mm-credits-name">Matt Deconick</div>
<button class="mm-musiclink-btn @(_musicLinkCopied ? "copied" : "")"
onclick=@OnMusicLinkCopy
style="pointer-events:all; margin-top:-8px;">
@(_musicLinkCopied ? "✓ Link Copied!" : "🎵 Copy Profile Link")
</button>
</div> *@
<div class="mm-credits-section">
<div class="mm-credits-role">💙 Special Thanks :</div>
<div class="mm-credits-name">Everyone in the S&Box community playing during Early Access 💙</div>
@* <div class="mm-credits-name"></div> *@
</div>
<div class="mm-credits-footer">
Made solo with 💥 and too much caffein..
</div>
</div>
</div>
}
@* ── End of mm-center panels ── *@
</div>
@* ------------------------------------------------------------------------- *@
@* ------------------ RIGHT UI : play button + tabs ----------------------- *@
@* ------------------------------------------------------------------------- *@
<div class="mm-right">
@* Dev menu — visible only in dev build *@
@if(DEV_MODE)
{
<div class="mm-dev-menu">
<button class="mm-dev-btn" onclick=@OnWipeSave>🗑️ Wipe Save</button>
<button class="mm-dev-btn" onclick=@OnAddFrags>💠 +1000 Frags</button>
<button class="mm-dev-btn" onclick=@OnUnlockAll>🔓 Unlock All</button>
<button class="mm-dev-btn" onclick=@(() => {
_evoFromLevel = _save?.PetLevel ?? 1;
_save.PetLevel++; // simulate level up
TriggerEvolution();
StateHasChanged();
})>🐣 Test Evo</button>
</div>
}
@* Big Play Button ▶ *@ @* Prevents clicking on it if still on MainMenu Tutorial *@
<div class="mm-play-wrap">
<button class="mm-play @(MainMenuTutorial.Instance?.IsActive == true ? "mm-play-locked" : "")"
onclick=@(() => { if(MainMenuTutorial.Instance?.IsActive != true) OnPlay();
Sound.Play("PlayButton"); })>
<div class="mm-play-shine"></div>
<span>▶</span>
<span class="mm-play-label">PLAY</span>
</button>
</div>
@* =================== SURVIVAL MODE =================== *@
@{
bool survivalUnlocked = _save?.TutorialCompleted ?? false;
}
<div class="mm-multiplayer-wrap">
<button class="mm-multiplayer @(survivalUnlocked && MainMenuTutorial.Instance?.IsActive != true ? "" : "mm-mode-locked")"
onclick=@(() => {
if(survivalUnlocked && MainMenuTutorial.Instance?.IsActive != true)
{ OnPlaySurvival(); Sound.Play("PlayButton"); } })>
@if(survivalUnlocked)
{
<span>⚡</span>
<span class="mm-multiplayer-label">SURVIVAL MODE</span>
}
else
{
<span>🔒</span>
<span class="mm-multiplayer-label">Complete a run to unlock</span>
}
</button>
</div>
@* =================== REACTOR (IDLE) MODE =================== *@
@{
bool reactorUnlocked = _save?.TutorialCompleted ?? false;
var idleSave = _save?.Idle;
bool reactorAlert = reactorUnlocked && idleSave != null
&& (IdleReactor.PreviewOfflineCores(idleSave) > 0
|| IdleReactor.DailyAvailable(idleSave));
}
<div class="mm-reactor-wrap">
<button class="mm-reactor @(reactorUnlocked && MainMenuTutorial.Instance?.IsActive != true ? "" : "mm-mode-locked")"
onclick=@(() => {
if(reactorUnlocked && MainMenuTutorial.Instance?.IsActive != true)
{ OnPlayReactor(); Sound.Play("PlayButton"); } })>
@if(reactorUnlocked)
{
<span>⚛️</span>
<span class="mm-reactor-label">THE REACTOR</span>
<span class="mm-reactor-sub">idle · always running</span>
@if(reactorAlert)
{
<span class="mm-reactor-dot"></span>
}
}
else
{
<span>🔒</span>
<span class="mm-reactor-label">Complete a run to unlock</span>
}
</button>
</div>
@* =================== END =================== *@
@* =================== END =================== *@
@* ------------------------- Tab buttons (right side) ------------------------- *@
@* Tabs buttons — clicking sets _tab variable, .on class shows active state *@
@* ------------------------- Tab buttons (right side) ------------------------- *@
<div class="mm-tabs">
@* ---------------- Community tab ----------------- *@
<button class="mm-tab @(_tab == "community" ? "on" : "") @(MainMenuTutorial.Instance?.IsActive == true ? "mm-tab-tut-locked" : "")"
onclick=@(() => {
if(MainMenuTutorial.Instance?.IsActive != true) SetTab("community");
Sound.Play("UI_Button"); })>
💬 Community
</button>
@* ── Pet tab — always visible, locked state before 3 runs ── *@
<button class="mm-tab @(_tab == "farm" ? "on" : "") @(_save?.PetUnlockShown != true ? "mm-tab-locked" : "") @(MainMenuTutorial.Instance?.IsActive == true ? "mm-tab-tut-locked" : "")"
onclick=@(() => {
if(MainMenuTutorial.Instance?.IsActive != true) SetTab("farm");
Sound.Play("UI_Button"); Sound.Play("Meow_cat"); })>
@if(_save?.PetUnlockShown != true)
{
@("📦")
<span class="mm-tab-lock-hint">Unlocks after 3 runs</span>
}
else
{
@("🐾 Pet")
}
</button>
@* ── Reactor tab ── *@
@{
var idleTabSave = _save?.Idle;
bool idleTabAlert = (_save?.TutorialCompleted ?? false) && idleTabSave != null
&& (IdleReactor.PreviewOfflineCores(idleTabSave) > 0
|| IdleReactor.DailyAvailable(idleTabSave));
}
<button class="mm-tab @(_tab == "reactor" ? "on" : "") @(MainMenuTutorial.Instance?.IsActive == true ? "mm-tab-tut-locked" : "")"
onclick=@(() => {
if(MainMenuTutorial.Instance?.IsActive != true) SetTab("reactor");
Sound.Play("UI_Button"); })>
⚛️ Reactor
@if(idleTabAlert)
{
<span class="mm-frags-pill">!</span>
}
</button>
@* ── Leaderboard ── *@
<button class="mm-tab @(_tab == "lb" ? "on" : "") @(MainMenuTutorial.Instance?.IsActive == true ? "mm-tab-tut-locked" : "")"
onclick=@(() => {
if(MainMenuTutorial.Instance?.IsActive != true) SetTab("lb");
Sound.Play("UI_Button"); Sound.Play("leaderboard_tab"); })>
🏅 Leaderboard
</button>
@* ── Stats ── *@
<button class="mm-tab @(_tab == "stats" ? "on" : "") @(MainMenuTutorial.Instance?.IsActive == true ? "mm-tab-tut-locked" : "")"
onclick=@(() => {
if(MainMenuTutorial.Instance?.IsActive != true) SetTab("stats");
Sound.Play("UI_Button"); })>
📊 Stats
</button>
@* ── Shop — shows fragment count pill when player has frags ── *@
<button class="mm-tab @(_tab == "shop" ? "on" : "") @(MainMenuTutorial.Instance?.IsActive == true ? "mm-tab-tut-locked" : "")"
onclick=@(() => { if(MainMenuTutorial.Instance?.IsActive != true) SetTab("shop");
Sound.Play("UI_Button"); Sound.Play("shop_tab"); })>
💠 Shop
@if((_save?.TotalFragments ?? 0) > 0)
{
<span class="mm-frags-pill">@(_save?.TotalFragments ?? 0)</span>
}
</button>
@* ── Credits ── *@
<button class="mm-tab @(_tab == "credits" ? "on" : "") @(MainMenuTutorial.Instance?.IsActive == true ? "mm-tab-tut-locked" : "")"
onclick=@(() => { if(MainMenuTutorial.Instance?.IsActive != true) SetTab("credits");
Sound.Play("UI_Button"); })>
🎖️ Credits
</button>
</div>
</div>
</div>
}
</root>
@code {
const bool DEV_MODE = false; // set to false before publishing
ChainReactionGame Game => ChainReactionGame.Instance;
public static MainMenu Instance { get; private set; }
// ------------------------- Main menu tuto -------------------------- //
public void SetTabPublic(string tab) => SetTab(tab);
public void SetShopTabPublic(string tab) { _shopTab = tab; StateHasChanged(); }
public void StartGamePublic() => Game?.StartGame(GameMode.Wave, null);
// ------------------------------------------------------------------- //
PlayerSave _save;
// useful for MainMenu Hud tuto (when buying Heart consumable)
public void ReloadSave()
{
_save = PlayerSave.Load();
_showWelcomeGift = false; // prevent OnUpdate from re-triggering welcome
StateHasChanged();
}
bool _saveLoaded = false;
Sandbox.Services.Leaderboards.Board2 _board;
string _tab = "";
bool _loadingLb = false;
bool _gameStarted = false;
/* ── Idle grid state ─────────────────────────────────── */
// Cell types: 0=empty, 1=chest, 2=gem, 3=bomb-chest, 4=bomb-placed, 5=blasting, 6=destroyed
int[,] _cells = new int[5,5];
bool[,] _inBlast = new bool[5,5];
bool[,] _bombed = new bool[5,5];
enum IdlePhase { Idle, BombPlace, Detonate, ChestPop, Reward, Reset }
IdlePhase _phase = IdlePhase.Reset;
float _phaseTimer = 0f;
float _rollTimer = 0f;
// Bomb placement
int _bombR = -1, _bombC = -1;
List<(int r, int c)> _blastCells = new();
List<(int r, int c)> _hitChests = new();
int _chestPopIdx = 0;
float _chestPopTimer = 0f;
int _fragsEarned = 0;
int _todayFrags = 0;
int _displayTodayFrags = 0;
// particles burst when clicking activate idle grid
bool _justActivated = false;
float _activatedTimer = 0f;
// Toggle button for idle farm (preview grid)
bool _idleActive = false;
// Leaerboard
string _lbMode = "score"; // "score" or "pet"
Leaderboards.Board2 _chainBoard = null;
Leaderboards.Board2 _petBoard = null;
Leaderboards.Board2 _reactorBoard = null;
const string GameIdent = "playnplay.10_xplosion";
// Popups
class IdlePopup
{
public string Text;
public string CssClass;
public float X, Y;
public bool Active;
public float Life;
}
List<IdlePopup> _popups = new();
Random _rng = new Random();
string _shopTab = "mods"; // default to modifiers
// Survival Mode
bool _showSurvivalLoadout = false;
HashSet<GridManager.BombType> _survivalSelectedBombs = new();
/* ── Save / LB ──────────────────────────────────────── */
protected override void OnStart()
{
Instance = this;
_save = PlayerSave.Load();
// Safety — BombSlot2 is a permanent welcome gift, always ensure it's granted
if(!_save.OwnsModifier(ModifierId.BombSlot2))
{
_save.OwnedModifiers.Add("BombSlot2");
_save.OwnedBombs.Add(GridManager.BombType.Diagonal.ToString());
_save.Save();
Log.Info("StartGame — BombSlot2 was missing, re-granted");
}
_saveLoaded = true;
_todayFrags = _save.GetTodayIdleFragments(); // load persisted value
_displayTodayFrags = _todayFrags; // snap, don't animate from 0
_idleActive = _save?.PassiveIdleGridActivated ?? false;
// Welcome gift for first-time players
if(!_save.WelcomeShown && !_welcomeGiftDismissed)
{
_showWelcomeGift = true;
_giftStage = _save.WelcomeChestsOpened; // restore progress from disk
for(int i = 0; i < _giftStage && i < _welcomeGifts.Count; i++)
_welcomeGifts[i].Opened = true; // mark already-opened chests
Sound.Play("fireworks");
Sound.Play("gift");
PlaySoundWithDelay(1000, "pearl_stars");
PlaySoundWithDelay(1000, "crowd_2");
PlaySoundWithDelay(2000, "applause_1");
}
// Welcome already done but tutorial not completed — resume tutorial
else if(!_save.MenuTutorialCompleted)
{
_ = ActivateTutorialNextFrame();
}
// force both flags off on start - Pet
_showPetUnlock = false;
_showEvolution = false;
_ = LoadForceLb();
ResetGrid();
if(!_save.PetUnlockShown)
SetTab("community"); // activates first tab button
else
SetTab("community");
// Hardcode community content — update with each game release
_community = new CommunityData
{
Welcome = "Hey! Welcome to 10XPLOSION Early Access 🎉\n\nThanks for playing — you're one of the first.\n\nYour feedback shapes what gets built next.",
Devnote = "Working on balancing the game and fixing bugs this week. Drop ideas in Discord!",
Discord = "",
Events = new List<CommunityEvent>
{
new CommunityEvent { Badge="NEW", Text="Tutorial system — now live!", Color="green" },
new CommunityEvent { Badge="NEW", Text="Finishing Pet interaction system + hand-made pixel art", Color="green" },
new CommunityEvent { Badge="NEW", Text="SURVIVAL MODE IS LIVE (Alpha - needs adjustments from feedbacks)", Color="green" },
@* new CommunityEvent { Badge="SOON", Text="", Color="purple" }, *@
new CommunityEvent { Badge="PLANNED", Text="More bomb types + modifiers", Color="gray" },
}
};
StateHasChanged();
}
float _explosionTimer = 0f; // menu visual vfx
bool _petFrameToggle = false;
float _petFrameTimer = 0f;
protected override void OnUpdate()
{
bool s = ChainReactionGame.Instance?.GameStarted ?? false;
if ( s != _gameStarted )
{
_gameStarted = s;
if ( !s )
{
_save = PlayerSave.Load();
_todayFrags = _save.GetTodayIdleFragments();
_displayTodayFrags = _todayFrags; // no animation on return, just snap
// Re-evaluate welcome state from save
if (_save.WelcomeShown)
_showWelcomeGift = false; // ← hard close if save says done
}
StateHasChanged();
}
if ( _gameStarted ) return;
// PET ANIMATION
_petFrameTimer -= Time.Delta;
if (_petFrameTimer <= 0f)
{
_petFrameTimer = 0.4f;
_petFrameToggle = !_petFrameToggle;
StateHasChanged();
}
// PET NAME - TextField Entry
if(_editingPetName && _nameEntry != null)
{
string current = _nameEntry.Text ?? "";
if(current != _petNameInput)
{
_petNameInput = current;
StateHasChanged();
}
}
// ------------- FAKE BOMBS - Main Menu -------------- //
_explosionTimer -= Time.Delta;
if (_explosionTimer <= 0f)
{
_explosionTimer = 1.5f + (float)_rng.NextDouble() * 3f; // every 1.5-4.5s
TriggerMenuExplosion();
StateHasChanged();
}
bool explosionChanged = false;
for (int i = _menuExplosions.Count - 1; i >= 0; i--)
{
var e = _menuExplosions[i];
_menuExplosions[i] = (e.x, e.y, e.life - Time.Delta, e.emoji);
if (_menuExplosions[i].life <= 0f) { _menuExplosions.RemoveAt(i); explosionChanged = true; }
else explosionChanged = true;
}
if (explosionChanged) StateHasChanged();
// ---------------------------------------------------- //
// Idle grid farm pet
TickIdleGrid();
// Check if THREE RUN DONE for PET UNLOCK
// Only check after welcome gift is fully done
if(!_showWelcomeGift
&& !_showPetUnlock
&& !_showEvolution
&& (_save?.TotalRuns ?? 0) >= 3
&& !(_save?.PetUnlockShown ?? false))
{
_ = TriggerPetUnlockCinematic();
}
// Check evolution
CheckPetEvolution();
}
async System.Threading.Tasks.Task ActivateTutorialNextFrame()
{
// Wait one frame for MainMenuTutorial.Instance to register itself
await GameTask.DelaySeconds(0.1f);
// Make sure welcome overlay is hidden when resuming tutorial (i.e: if resuming after a crash)
_showWelcomeGift = false;
_welcomeGiftDismissed = true; // hard lock
_giftStage = _welcomeGifts.Count; // mark all as opened
foreach(var g in _welcomeGifts) g.Opened = true;
MainMenuTutorial.Instance?.Activate(_save);
StateHasChanged();
}
string GetPetImagePath() => PetVisuals.GetImagePath(
_save?.PetLevel ?? 1,
_save?.PetUnlockShown ?? false,
_petFrameToggle
);
async System.Threading.Tasks.Task TriggerPetUnlockCinematic()
{
_showPetUnlock = true;
_petUnlockChestPhase = true;
_showEvolution = true;
_evoShowNew = false;
_evoText = "Something is calling out to you...";
_evoPhase = "evo-text";
Sound.Play("pearl_stars");
Sound.Play("Evolution_Pet");
MusicManager.Instance?.StopMenuMusic();
StateHasChanged();
await GameTask.DelaySeconds(3.0f);
_evoText = "A mysterious chest appeared!";
_evoPhase = "evo-start";
StateHasChanged();
await GameTask.DelaySeconds(3.0f);
_evoText = "It's trembling...";
_evoPhase = "evo-shake";
StateHasChanged();
Sound.Play("evo_rumble");
await GameTask.DelaySeconds(2.5f);
// Flash — chest becomes egg
_evoText = "";
_evoPhase = "evo-flash";
_petUnlockChestPhase = false; // switch to egg
_evoShowNew = false;
StateHasChanged();
await GameTask.DelaySeconds(1.0f);
_evoPhase = "evo-reveal";
_evoText = "A mysterious egg appeared!";
StateHasChanged();
}
/* ── Idle grid tick ─────────────────────────────────── */
void TickIdleGrid()
{
if(!_idleActive) return;
// If idle cap reached, force stop and don't tick
int maxIdle = _save?.MaxIdleFragsPerDay ?? 80;
if(_save != null && _save.GetTodayIdleFragments() >= maxIdle)
{
_idleActive = false;
if(_save != null)
{
_save.PassiveIdleGridActivated = false;
_save.Save();
}
StateHasChanged();
return;
}
_rollTimer += Time.Delta;
// Burst particles when toggling
if(_activatedTimer > 0)
{
_activatedTimer -= Time.Delta;
if(_activatedTimer <= 0)
{
_justActivated = false;
StateHasChanged();
}
}
// Tick popups
bool popupChanged = false;
for(int i = _popups.Count - 1; i >= 0; i--)
{
_popups[i].Life -= Time.Delta;
if(_popups[i].Life <= 0) { _popups.RemoveAt(i); popupChanged = true; }
}
switch(_phase)
{
case IdlePhase.Reset:
ResetGrid();
_phase = IdlePhase.Idle;
_phaseTimer = 1.5f;
StateHasChanged();
break;
case IdlePhase.Idle:
_phaseTimer -= Time.Delta;
if(_phaseTimer <= 0)
{
PlaceBomb();
_phase = IdlePhase.BombPlace;
_phaseTimer = 0.8f;
StateHasChanged();
}
break;
case IdlePhase.BombPlace:
_phaseTimer -= Time.Delta;
if(_phaseTimer <= 0)
{
Detonate();
_phase = IdlePhase.Detonate;
_phaseTimer = 0.6f;
StateHasChanged();
}
break;
case IdlePhase.Detonate:
_phaseTimer -= Time.Delta;
if(_phaseTimer <= 0)
{
_chestPopIdx = 0;
_chestPopTimer = 0f;
_phase = IdlePhase.ChestPop;
StateHasChanged();
}
break;
case IdlePhase.ChestPop:
_chestPopTimer -= Time.Delta;
if(_chestPopTimer <= 0 && _chestPopIdx < _hitChests.Count)
{
var (r,c) = _hitChests[_chestPopIdx];
_cells[r,c] = 6; // destroyed
SpawnPopup(r, c, _chestPopIdx);
_chestPopIdx++;
_chestPopTimer = 0.25f;
StateHasChanged();
}
else if(_chestPopIdx >= _hitChests.Count && _chestPopTimer <= 0)
{
_phase = IdlePhase.Reward;
_phaseTimer = 1.5f;
StateHasChanged();
}
break;
case IdlePhase.Reward:
_phaseTimer -= Time.Delta;
if(_phaseTimer <= 0)
{
_phase = IdlePhase.Reset;
_rollTimer = 0f;
StateHasChanged();
}
break;
}
if(popupChanged) StateHasChanged();
}
void ResetGrid()
{
for(int r=0;r<5;r++)
for(int c=0;c<5;c++)
_cells[r,c] = 0;
_inBlast = new bool[5,5];
_bombed = new bool[5,5];
_blastCells.Clear();
_hitChests.Clear();
_chestPopIdx = 0;
_fragsEarned = 0;
_bombR = -1; _bombC = -1;
// Randomly place 4-7 chests
var empties = new List<(int,int)>();
for(int r = 0; r < 5; r++)
for(int c = 0; c < 5; c++)
empties.Add((r,c));
Shuffle(empties);
int count = _rng.Next(5, 9); // was 4-7, now 5-8 — denser
// Spawn chances
for(int i = 0; i < count && i < empties.Count; i++)
{
var (r,c) = empties[i];
int roll = _rng.Next(100);
// was: roll < 70 chest, < 90 gem, else bomb-chest
// now: more bomb chests for chain reactions in preview
_cells[r,c] = roll < 55 ? 1 : roll < 78 ? 2 : 3;
}
}
void PlaceBomb()
{
// Find empty cell adjacent to the most chests — makes chains more likely
var empties = new List<(int r, int c)>();
for(int r = 0; r < 5; r++)
for(int c = 0; c < 5; c++)
if(_cells[r,c] == 0) empties.Add((r,c));
if(empties.Count == 0) return;
// Score each empty cell by how many chests are adjacent
(int r, int c) best = empties[0];
int bestScore = -1;
// adjacent coords
int[] dr = {-1,1,0,0};
int[] dc = {0,0,-1,1};
foreach(var (er, ec) in empties)
{
int score = 0;
for(int d = 0; d < 4; d++)
{
int nr = er + dr[d];
int nc = ec + dc[d];
if((nr >= 0 && nr < 5) && (nc >= 0 && nc < 5) && (_cells[nr,nc] >= 1))
score++;
}
// Add some randomness so it's not always identical
score += _rng.Next(3);
if(score > bestScore) { bestScore = score; best = (er, ec); }
}
(_bombR, _bombC) = best;
_cells[_bombR, _bombC] = 4;
}
void Detonate()
{
if(_bombR < 0) return;
_blastCells.Clear();
_hitChests.Clear();
var fired = new HashSet<(int,int)>();
var queue = new Queue<(int,int)>();
// seed with the placed bomb
fired.Add((_bombR, _bombC));
queue.Enqueue((_bombR, _bombC));
_cells[_bombR, _bombC] = 5; // blasting
int[] dr = {-1, 1, 0, 0, 0};
int[] dc = { 0, 0,-1, 1, 0};
while(queue.Count > 0)
{
var (br, bc) = queue.Dequeue();
for(int d = 0; d < 5; d++)
{
int nr = br + dr[d];
int nc = bc + dc[d];
if(nr < 0 || nr >= 5 || nc < 0 || nc >= 5) continue;
if(!_blastCells.Contains((nr, nc)))
_blastCells.Add((nr, nc));
_inBlast[nr, nc] = true;
int cell = _cells[nr, nc];
// chest types 1/2/3 — hit
if(cell >= 1 && cell <= 3 && !_hitChests.Contains((nr, nc)))
_hitChests.Add((nr, nc));
// bomb chest (type 3) triggers chain — queue it if not already fired
if(cell == 3 && !fired.Contains((nr, nc)))
{
fired.Add((nr, nc));
_cells[nr, nc] = 5; // also blasting
queue.Enqueue((nr, nc));
}
}
}
// Calculate reward
int hits = _hitChests.Count;
// Convert hits to fragments earned
_fragsEarned = hits == 0 ? 0
: hits <= 2 ? 1
: hits <= 4 ? _rng.Next(1, 3)
: hits <= 6 ? _rng.Next(2, 4)
: _rng.Next(3, 6); // max 5 frags
// ── Daily cap check ──
int maxIdle = _save?.MaxIdleFragsPerDay ?? 80; //
// Check daily cap BEFORE adding fragments
int currentDailyFrags = _save?.GetTodayIdleFragments() ?? 0;
if(currentDailyFrags >= maxIdle)
{
_fragsEarned = 0;
_idleActive = false; // auto-stop
_save.PassiveIdleGridActivated = false;
_save.Save();
SpawnCenteredMsg("💤 Pet is tired — come back tomorrow", "idle-msg-mega");
StateHasChanged();
return;
}
// Clamp fragments to not exceed cap
// note: When I build the pet upgrade system later,
// I'll just have to increase PetLevel in the save and the cap scales automatically.
int remaining = maxIdle - currentDailyFrags;
if(_fragsEarned > remaining) _fragsEarned = remaining;
if(_hitChests.Count == 0)
{
SpawnCenteredMsg("No chests hit...", "idle-msg-neutral");
}
else if(_fragsEarned >= 15)
{
SpawnCenteredMsg($"💥 MEGA CHAIN! +{_fragsEarned} 💠", "idle-msg-mega");
}
else if(_fragsEarned >= 8)
{
SpawnCenteredMsg($"🔥 Chain! +{_fragsEarned} 💠", "idle-msg-chain");
}
else if(_fragsEarned >= 3)
{
SpawnCenteredMsg($"✨ +{_fragsEarned} 💠 nice!", "idle-msg-nice");
}
else
{
SpawnCenteredMsg($"+{_fragsEarned} 💠", "idle-msg-small");
}
//TODO: Add sound
if(_fragsEarned > 0)
{
// Reward chime — use a short positive sound asset
Sound.Play("fragments_coins");
// Or trigger through GridRenderer if you have one accessible
_save?.AddIdleFragments(_fragsEarned);
_todayFrags = _save?.GetTodayIdleFragments() ?? 0; // sync display target
StateHasChanged(); // shop pill + left stats refresh
}
}
// grid pop up msg
void SpawnCenteredMsg(string text, string css)
{
// Dead center of the grid = 50%, slightly above middle = 40%
_popups.Add(new IdlePopup {
Text = text, CssClass = css,
X = 50f, Y = 40f,
Active = true, Life = 2.2f
});
}
void SpawnPopup(int r, int c, int idx)
{
// Convert grid cell to % position within mm-grid-preview-wrap
// Cell size 106px + gap 12px = 118px step, grid total 578px
float cellStep = 118f;
float gridSize = 578f;
float xPct = (c * cellStep + 53f) / gridSize * 100f;
float yPct = (r * cellStep + 53f) / gridSize * 100f;
string text;
string css;
if(idx == _hitChests.Count - 1 && _fragsEarned > 0)
{
if(_fragsEarned >= 15) { text = $"MEGA! +{_fragsEarned}💠"; css = "popup-mega"; }
else if(_fragsEarned >= 8) { text = $"CHAIN! +{_fragsEarned}💠"; css = "popup-chain"; }
else if(_fragsEarned >= 3) { text = $"+{_fragsEarned}💠 NICE!"; css = "popup-nice"; }
else { text = $"+{_fragsEarned}💠"; css = "popup-small"; }
}
else
{
string[] smalls = { "💥", "✓", "Gone!", "💣" };
text = smalls[_rng.Next(smalls.Length)];
css = "popup-hit";
}
_popups.Add(new IdlePopup {
Text = text, CssClass = css,
X = xPct, Y = yPct,
Active = true, Life = 1.8f
});
}
void Shuffle<T>(List<T> list)
{
for(int i=list.Count-1;i>0;i--)
{ int j=_rng.Next(i+1); (list[i],list[j])=(list[j],list[i]); }
}
/* ── Cell rendering helpers ─────────────────────────── */
string GetLiveCellClass(int r, int c) => _cells[r,c] switch {
1 => "ct-chest",
2 => "ct-gem",
3 => "ct-bomb-chest",
4 => "ct-bomb",
5 => "ct-blast",
6 => "ct-destroyed",
_ => "ct-empty"
};
string GetLiveAnimClass(int r, int c)
{
if(_cells[r,c] == 5) return "ca-detonate";
if(_cells[r,c] == 6) return "ca-destroy";
if(_inBlast[r,c] && _cells[r,c] == 0) return "ca-blast-empty";
return "";
}
string GetLiveIcon(int r, int c) => _cells[r,c] switch {
1 => "🎁",
2 => "💎",
3 => "💣",
4 => "💥",
_ => ""
};
/* ── Play / tabs / LB ───────────────────────────────── */
void OnPlay() => Game?.StartGame(GameMode.Wave, null);
// Reactor (Idle) mode — no loadout screen, it is always mid-run
void OnPlayReactor()
{
if (_save?.TutorialCompleted != true) return;
Game?.StartGame(GameMode.Idle, null);
}
void SetTab(string tab)
{
_tab = tab; //_tab == tab ? "" : tab; <- this was allowing toggling (visible/invisible) on same tab btn
StateHasChanged();
if(_tab == "lb")
{
if(_lbMode == "score") _ = LoadLb();
else if(_lbMode == "pet") _ = LoadPetLb();
else if(_lbMode == "chain") _ = LoadChainLb();
else if(_lbMode == "reactor")_ = LoadReactorLb();
else if(_lbMode == "force") _ = LoadForceLb();
}
// Community
// if (_tab == "community") _ = LoadCommunity();
}
async System.Threading.Tasks.Task LoadLb()
{
try
{
string statName = _lbMode switch
{
"score" => "chain_reaction_score",
"pet" => "chain_reaction_pet",
"chain" => "chain_reaction_chain",
_ => "chain_reaction_score"
};
var board = Sandbox.Services.Leaderboards.GetFromStat(GameIdent, statName);
board.SetAggregationMax(); // keep each player's BEST, not the sum
board.SetSortDescending(); // highest first
board.MaxEntries = 100;
await board.Refresh();
Log.Info($"LB '{statName}' — entries: {board.Entries?.Length ?? -1}");
_board = board;
}
catch (Exception ex)
{
Log.Warning($"LB error: {ex.Message}");
_board = null;
}
StateHasChanged();
}
string GetTimePlayed()
{
float m = _save?.TotalMinutesPlayed ?? 0f;
return m < 60f ? $"{(int)m}m" : $"{(int)(m/60f)}h {(int)(m%60f)}m";
}
protected override int BuildHash()
{
int a = System.HashCode.Combine(_tab, _saveLoaded, _loadingLb,
_board?.Entries?.Length ?? 0, _gameStarted,
_phase, _chestPopIdx, _popups.Count);
int b = System.HashCode.Combine(
_save?.ActiveModifiers?.Count ?? 0,
_save?.OwnedModifiers?.Count ?? 0,
_save?.OwnedBombs?.Count ?? 0,
_save?.TotalConsumablesOwned ?? 0);
int c = System.HashCode.Combine(
_editingPetName,
_showPetUnlock,
_showEvolution,
_evoPhase,
_evoText,
_discordCopied);
int d = System.HashCode.Combine(
_save?.TotalFragments ?? 0,
_save?.PetLevel ?? 1,
_save?.PetUnlockShown ?? false,
_musicLinkCopied, _menuExplosions.Count , _petFrameToggle);
return a ^ b ^ c ^ d;
}
// --------------------------------------------------------------------------- //
// -------------------------- Toggle Idle Farm Grid -------------------------- //
// --------------------------------------------------------------------------- //
void OnToggleIdle()
{
_idleActive = !_idleActive;
// play burst particle (removed bc it sucks for now)
@* if(_idleActive)
{
_justActivated = true;
_activatedTimer = 1.2f;
SpawnActivateBurst();
} *@
StateHasChanged();
}
void SpawnActivateBurst()
{
// Spawn a ring of centered popups that fly outward
string[] words = { "💠", "✨", "💠", "✨", "💠" };
float[] xs = { 35f, 42f, 50f, 58f, 65f };
float[] ys = { 55f, 45f, 40f, 45f, 55f };
for(int i = 0; i < words.Length; i++)
{
float xi = xs[i], yi = ys[i];
int ii = i;
_popups.Add(new IdlePopup {
Text = words[ii],
CssClass = "popup-chain",
X = xi,
Y = yi,
Active = true,
Life = 1.4f
});
}
}
// --------------------------------------------------------------------------- //
// -------------------------- SHOP BUTTONS on Shop -------------------------- //
// --------------------------------------------------------------------------- //
void OnBuyMod(ModifierId id, int cost)
{
if(_save == null) return;
bool bought = _save.TryBuyModifier(id, cost);
if(bought) StateHasChanged();
Sound.Play("BuyShop");
}
void OnToggleMod(ModifierId id)
{
_save?.ToggleModifier(id);
StateHasChanged();
}
// --------------------------------------------------------------------------- //
// ------------------------------- DEBUG BUTTONS ---------------------------- //
// --------------------------------------------------------------------------- //
void OnWipeSave()
{
PlayerSave.DEBUG_WipeSave();
_save = PlayerSave.Load();
_todayFrags = _save.GetTodayIdleFragments();
_displayTodayFrags = _todayFrags;
// Reset all welcome gift in-memory state
_showWelcomeGift = false;
_welcomeGiftDismissed = false;
_giftStage = 0;
foreach(var g in _welcomeGifts) g.Opened = false;
// Reset tutorial
MainMenuTutorial.Instance?.ForceReset();
// Mirror OnStart — re-show welcome if fresh save
if(!_save.WelcomeShown)
{
_giftStage = 0; // always 0 on fresh wipe
_showWelcomeGift = true;
Sound.Play("fireworks");
Sound.Play("gift");
PlaySoundWithDelay(1000, "pearl_stars");
}
StateHasChanged();
}
void OnAddFrags()
{
if(_save == null) return;
_save.TotalFragments += 1000;
_save.Save();
StateHasChanged();
}
void OnUnlockAll()
{
if(_save == null) return;
foreach(var mod in ShopCatalog.All)
{
if(!_save.OwnsModifier(mod.Id))
{
_save.OwnedModifiers.Add(mod.Id.ToString());
}
}
_save.Save();
StateHasChanged();
}
// ------------------------------------------------------------------------ //
// ----------------------- GIFTS SPECIALS SECTION ------------------------- //
// ------------------------------------------------------------------------ //
bool _showWelcomeGift = false;
int _giftStage = 0; // 0=intro, 1=chest1 open, 2=chest2 open, 3=chest3 open, 4=done
class GiftReward
{
public string ChestIcon { get; set; } = "🎁";
public string GiftIcon { get; set; } = "";
public string Title { get; set; } = "";
public string Desc { get; set; } = "";
public bool Opened { get; set; } = false;
}
List<GiftReward> _welcomeGifts = new()
{
new GiftReward {
GiftIcon = "💣💣",
Title = "Twin Loader",
Desc = "+1 bomb slot, permanent"
},
new GiftReward {
GiftIcon = "🎯",
Title = "Sniper Trial",
Desc = "Sniper bombs for 3 runs"
},
new GiftReward {
GiftIcon = "✕",
Title = "Diagonal Trial",
Desc = "+1 bomb type, permanent"
},
};
void OnOpenChest(int idx)
{
if(idx >= _welcomeGifts.Count) return;
if(_welcomeGifts[idx].Opened) return;
// Play open chest, then last chest if last gift in the list
Sound.Play(idx == _welcomeGifts.Count - 1 ? "open_last_chest" : "open_chest_1");
Sound.Play("Chest_open");
//Log.Warning($"OPEN CHEST : {idx}");
_welcomeGifts[idx].Opened = true;
_giftStage = idx + 1;
//mark as shown when last chest opened, don't wait for button click
if(_giftStage >= _welcomeGifts.Count)
{
_save.WelcomeShown = true;
}
// Persist progress so closing the game mid-flow doesn't reset
_save.WelcomeChestsOpened = _giftStage;
_save.Save();
StateHasChanged();
}
bool _welcomeGiftDismissed = false; // set once, never resets during session
void OnCloseWelcome()
{
_showWelcomeGift = false;
_welcomeGiftDismissed = true; // hard lock for this session
_save.WelcomeShown = true;
_save.WelcomeChestsOpened = _welcomeGifts.Count;
_save.OwnedModifiers.Add("BombSlot2"); // grant the modifier
_save.OwnedBombs.Add(GridManager.BombType.Diagonal.ToString());
_save.TrialBombsRunsLeft = 3; // grant trial runs
_save.Save();
// Start menu tutorial after welcome gifts close
MainMenuTutorial.Instance?.Activate(_save);
Sound.Play("Pop_Button_1");
StateHasChanged();
}
async void PlaySoundWithDelay(int delay, string soundName)
{
await Task.Delay(delay);
Sound.Play(soundName);
}
// ------------------------------------------------------------------------ //
// -------------------------- BOMBS SHOP SECTION -------------------------- //
// ------------------------------------------------------------------------ //
void OnBuyBomb(GridManager.BombType type, int cost)
{
if (_save == null) return;
bool bought = _save.TryBuyBomb(type, cost);
if (bought)
{
Sound.Play("BuyShop"); // or a dedicated purchase sound
Sound.Play("climax_explosion_lv5");
StateHasChanged();
}
}
// ------------------------------------------------------------------------ //
// ---------------------- Consumables SHOP SECTION ------------------------ //
// ------------------------------------------------------------------------ //
void OnBuyConsumable(ConsumableId id, int cost)
{
if (_save == null) return;
if ((_save.TotalConsumablesOwned) >= 3) return; // cap check
bool bought = _save.TryBuyConsumable(id, cost);
if (bought)
{
Sound.Play("BuyShop");
StateHasChanged();
Log.Warning(_save.TotalConsumablesOwned);
// Notify tutorial
if (id == ConsumableId.Heart)
{
MainMenuTutorial.Instance?.OnHeartPurchased();
Sound.Play("");
}
else if (id == ConsumableId.ChainNuke)
Sound.Play("usenukebomb");
else if(id == ConsumableId.FuseExtension)
Sound.Play("usetimeup");
else if(id == ConsumableId.ColumnSweep)
Sound.Play("usesweep");
else if(id == ConsumableId.Overdrive)
Sound.Play("useoverdrive");
else if(id == ConsumableId.Overload)
Sound.Play("Whoosh_bubble");
else if(id == ConsumableId.Redraw)
Sound.Play("useredraw");
else if(id == ConsumableId.Reveal)
Sound.Play("usereveal");
else if(id == ConsumableId.ClusterDrop)
Sound.Play("useclusterdrop");
}
}
// ------------------------------------------------------------------------ //
// ------------------------------ PET SYSTEM ------------------------------ //
// ------------------------------------------------------------------------ //
string GetPetEmoji() => (_save?.PetUnlockShown == true)
? (_save?.PetLevel ?? 1) switch
{
1 => "🥚",
2 => "🐣",
3 => "🐥",
4 => "🦅",
_ => "🐉"
}
: "📦";
// Shows what comes AFTER current level — reads current level correctly
string GetNextPetEmoji() => (_save?.PetLevel ?? 1) switch
{
1 => "🐣",
2 => "🐥",
3 => "🦅",
4 => "🐉",
_ => "🐉" // level 5 = no next
};
// Shows the form BEFORE evolution — uses _evoFromLevel, not save
string GetPrevPetEmoji() => _evoFromLevel switch
{
1 => "🥚",
2 => "🐣",
3 => "🐥",
4 => "🦅",
_ => "🐉" // fallback to dragon
};
string GetNextPetName() => (_save?.PetLevel ?? 1) switch
{
1 => "Mystery Egg",
2 => "Hatchling",
3 => "Sparky",
4 => "Storm",
5 => "Inferno",
6 => "Sovereign",
_ => ""
};
string GetPetName() => (_save?.PetLevel ?? 1) switch
{
1 => "Mystery Egg",
2 => "Hatchling",
3 => "Sparky",
4 => "Storm",
5 => "Inferno",
6 => "Sovereign",
_ => "Sovereign"
};
float GetPetXpPct()
{
int needed = GetPetXpNeeded();
int current = _save?.PetXp ?? 0;
return MathF.Min(100f, (current / (float)needed) * 100f);
}
int GetPetXpNeeded() => PlayerSave.GetXpNeeded(_save?.PetLevel ?? 1);
// +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// ---------- pet (next) handle gift and evolution animation -------
// +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// ── Pet naming ──
bool _editingPetName = false;
string _petNameInput = "";
// ── First run pet unlock popup ──
bool _showPetUnlock = false;
bool _petUnlockChestPhase = false; // showing chest before egg
// ── Evolution cinematic ──
bool _showEvolution = false;
string _evoPhase = ""; // "evo-start" → "evo-shake" → "evo-flash" → "evo-reveal"
bool _evoShowNew = false;
// Returns custom name if set, otherwise default
string GetDisplayPetName()
{
string custom = _save?.PetCustomName ?? "";
return string.IsNullOrWhiteSpace(custom) ? GetPetName() : custom;
}
void OnConfirmPetName()
{
if(_save == null) return;
string trimmed = (_nameEntry?.Text ?? "").Trim();
if(trimmed.Length == 0) trimmed = GetPetName();
_save.PetCustomName = trimmed;
_save.Save();
_editingPetName = false;
CloseNameEntry();
StateHasChanged();
}
// ── First run pet unlock ──
void OnClosePetUnlock()
{
_showPetUnlock = false;
_save.PetUnlockShown = true;
_save.Save();
SetTab("farm"); // take them straight to pet tab
Sound.Play("UI_Button");
Sound.Play("Meow_cat");
StateHasChanged();
}
// ── Call this from OnUpdate, after TickIdleGrid ──
// Check if pet just leveled up this session
int _evoFromLevel = 1;
void CheckPetEvolution()
{
if(_save == null) return;
if(_save.PendingEvolution && !_showEvolution)
{
_evoFromLevel = _save.PetLevel - 1; // only here
Log.Info($"Evo: from {_evoFromLevel} to {_save.PetLevel}");
_save.PendingEvolution = false;
_save.Save();
TriggerEvolution();
}
}
// ── Kicks off the evolution sequence ──
string _evoText = "";
async void TriggerEvolution()
{
_showEvolution = true;
_evoShowNew = false;
_evoText = "";
MusicManager.Instance?.StopMenuMusic();
// Phase 1 — dramatic text appears
_evoPhase = "evo-text";
_evoText = _evoFromLevel switch {
1 => "Wait... what's happening?!",
2 => "Huh.. What.. again?!",
3 => "No way it could be that again..!",
4 => "The pressure of the air suddenly changed..",
_ => ""
};
Sound.Play("Evolution_Pet");
StateHasChanged();
await GameTask.DelaySeconds(3.0f);
// Phase 2 — text changes, egg wobbles
_evoText = _evoFromLevel switch {
1 => "The egg is trembling...",
2 => "Something stirs within...",
3 => "A surge of energy!",
4 => "I can sense it's.. warmth..",
_ => ""
};
_evoPhase = "evo-start";
StateHasChanged();
await GameTask.DelaySeconds(3.0f);
// Phase 3 — faster shake, tension builds
_evoText = _evoFromLevel switch {
1 => "A tiny crack appears!",
2 => "It's growing fast!",
3 => "Power is building...",
4 => "Something legendary emerges!",
_ => ""
};
_evoPhase = "evo-shake";
StateHasChanged();
await GameTask.DelaySeconds(2.5f);
// Phase 4 — white flash, swap to new form
_evoText = "";
_evoPhase = "evo-flash";
_evoShowNew = true;
//Sound.Play("evo_flash");
StateHasChanged();
await GameTask.DelaySeconds(1.0f);
// Phase 5 — reveal with fanfare
_evoPhase = "evo-reveal";
_evoText = $"{GetDisplayPetName()} evolved!";
StateHasChanged();
}
void OnCloseEvolution()
{
_showEvolution = false;
_showPetUnlock = false;
_petUnlockChestPhase = false;
_evoPhase = "";
_evoShowNew = false;
MusicManager.Instance?.PlayMenuMusic(); // just play music back
Sound.Play("UI_Button");
// If this was the pet unlock cinematic, mark it done
if(_save != null && !_save.PetUnlockShown)
{
_save.PetUnlockShown = true;
_save.Save();
SetTab("farm");
}
_save?.Save();
StateHasChanged();
}
////////////////////// TEXT ENTRY FIELD FOR PET NAME ///////////////////////
// Add this field
TextEntry _nameEntry;
// Call this when _editingPetName becomes true
void OpenNameEntry()
{
_nameEntry?.Delete();
_nameEntry = null;
// Wait one frame for the container div to exist in DOM
_ = AttachNameEntryNextFrame();
}
Panel _nameEntryContainer;
async System.Threading.Tasks.Task AttachNameEntryNextFrame()
{
await GameTask.DelaySeconds(0.05f);
// Find the container we stamped with @ref
var container = _nameEntryContainer;
if(container == null)
{
Log.Warning("Name entry container not found");
return;
}
_nameEntry = container.AddChild<TextEntry>();
_nameEntry.AddClass("mm-pet-name-input");
_nameEntry.MaxLength = 16;
_nameEntry.Text = GetDisplayPetName();
_nameEntry.AcceptsFocus = true;
_nameEntry.Style.PointerEvents = PointerEvents.All;
await GameTask.DelaySeconds(0.1f);
_nameEntry?.Focus();
Log.Info($"Name entry focused: {_nameEntry?.HasFocus}");
}
void CloseNameEntry()
{
_nameEntry?.Delete();
_nameEntry = null;
}
// Leaderboard
async System.Threading.Tasks.Task LoadPetLb()
{
_loadingLb = true;
_petBoard = null;
StateHasChanged();
try
{
var b = Sandbox.Services.Leaderboards.GetFromStat(GameIdent, "chain_reaction_pet");
b.SetAggregationMax();
b.SetSortDescending();
b.MaxEntries = 100;
await b.Refresh();
_petBoard = b;
}
catch(Exception ex) { Log.Warning($"Pet LB: {ex.Message}"); }
_loadingLb = false;
StateHasChanged();
}
// ---------------------------------------------------- //
// Leaderboard - Sequence top chain reaction guy
// ---------------------------------------------------- //
async System.Threading.Tasks.Task LoadReactorLb()
{
_loadingLb = true;
_reactorBoard = null;
StateHasChanged();
try
{
var b = Sandbox.Services.Leaderboards.GetFromStat(GameIdent, "chain_reaction_reactor");
b.SetAggregationMax();
b.SetSortDescending();
b.MaxEntries = 100;
await b.Refresh();
_reactorBoard = b;
}
catch(Exception ex) { Log.Warning($"Reactor LB: {ex.Message}"); }
_loadingLb = false;
StateHasChanged();
}
static string GetReactorTitle(long flux) => flux switch {
>= 5000 => "☢️ Singularity",
>= 2000 => "🌟 Core Architect",
>= 750 => "⚛️ Reactor Chief",
>= 250 => "🔷 Fluxbound",
>= 60 => "⚙️ Engineer",
>= 10 => "🔧 Technician",
_ => "🧪 Intern",
};
async System.Threading.Tasks.Task LoadChainLb()
{
_loadingLb = true;
_chainBoard = null;
StateHasChanged();
try
{
var b = Sandbox.Services.Leaderboards.GetFromStat(GameIdent, "chain_reaction_chain");
b.SetAggregationMax();
b.SetSortDescending();
b.MaxEntries = 100;
await b.Refresh();
_chainBoard = b;
}
catch(Exception ex) { Log.Warning($"Chain LB: {ex.Message}"); }
_loadingLb = false;
StateHasChanged();
}
string GetChainTitle(int chainLen) => chainLen switch {
>= 20 => "💀 God of Destruction",
>= 15 => "🔥 Chain Sovereign",
>= 12 => "⚡ Storm Caller",
>= 10 => "💣 Demolition King",
>= 8 => "🎯 Precision Bomber",
>= 6 => "🔗 Chain Breaker",
>= 4 => "💥 Detonator",
_ => "🎁 Chest Hunter"
};
/////////////////////////////////////////////////////////////////////////
// ----------------- 100 TOP CONTRIBUTOR XP BAR ---------------------- //
/////////////////////////////////////////////////////////////////////////
// Force leaderboard fields
Leaderboards.Board2 _forceBoard;
long _forceTotalScore = 0;
long _forceTarget = 1_000_000_000L; // 1 billion
int _forceMedalTier = 0; // 0=none, 1=bronze, 2=silver, 3=gold
bool _forceBarLoaded = false;
async System.Threading.Tasks.Task LoadForceLb()
{
_loadingLb = true;
_forceBoard = null;
StateHasChanged();
try
{
var b = Sandbox.Services.Leaderboards.GetFromStat(GameIdent, "chain_reaction_score");
if(b != null)
{
b.SetAggregationMax();
b.SetSortDescending();
b.MaxEntries = 100;
await b.Refresh();
_forceBoard = b; // for force board (in tab)
_board = b; // for hall of fame (left side screen)
// Sum all entries
_forceTotalScore = 0;
foreach(var entry in b.Entries)
_forceTotalScore += (long)entry.Value;
// Subtract baseline so bar resets visually after each fill
long baseline = _save?.ForceBaseline ?? 0L;
long adjusted = _forceTotalScore - baseline;
// Detect fill — how many times has it filled since baseline?
long fills = adjusted / _forceTarget;
if(fills > (_save?.ForceFillCount ?? 0))
{
// Bar just filled — award fragments if in top 10
bool inTop10 = b.Entries.Take(10).Any(e => IsMe(e));
if(inTop10 && _save != null)
{
_save.TotalFragments += 500;
_save.ForceMedalTier = (int)MathF.Max(_save.ForceMedalTier, GetMedalTierForRank(b));
_save.ForceFillCount = (int)fills;
_save.Save();
}
}
// Compute local player medal tier for display
_forceMedalTier = _save?.ForceMedalTier ?? 0;
_forceBarLoaded = true;
}
}
catch(Exception ex) { Log.Warning($"Force LB: {ex.Message}"); }
_loadingLb = false;
StateHasChanged();
}
int GetMedalTierForRank(Leaderboards.Board2 b)
{
for(int i = 0; i < b.Entries.Length; i++)
{
if(!IsMe(b.Entries[i])) continue;
if(i < 3) return 3;
if(i < 10) return 2;
return 1;
}
return 0;
}
bool IsMe(Leaderboards.Board2.Entry entry)
{
var localId = Sandbox.Connection.Local?.SteamId;
return entry.SteamId == localId;
}
bool IsMe(Leaderboards.Entry entry)
{
var localId = Sandbox.Connection.Local?.SteamId;
return entry.SteamId == localId;
}
float GetForceBarPct()
{
long baseline = _save?.ForceBaseline ?? 0L;
long adjusted = _forceTotalScore - baseline;
long progress = adjusted % _forceTarget;
return MathF.Min(1f, (float)progress / _forceTarget) * 100f;
}
string GetMedalEmoji(int tier) => tier switch {
3 => "🥇",
2 => "🥈",
1 => "🥉",
_ => ""
};
/////////////////////////////////////////////////////////////////////////
// ----------------- COMMUNITY TAB INFORMATIONS ---------------------- //
/////////////////////////////////////////////////////////////////////////
class CommunityData
{
public string Welcome { get; set; } = "";
public string Discord { get; set; } = "";
public string Devnote { get; set; } = "";
public List<CommunityEvent> Events { get; set; } = new();
}
class CommunityEvent
{
public string Badge { get; set; } = "";
public string Text { get; set; } = "";
public string Color { get; set; } = "gray";
}
CommunityData _community = null;
bool _communityLoading = false;
bool _communityError = false;
@* async System.Threading.Tasks.Task LoadCommunity()
{
if (_communityLoading) return;
_communityLoading = true;
_communityError = false;
StateHasChanged();
try
{
string url = "https://gist.githubusercontent.com/YOURUSER/GISTID/raw/community.json";
string json = await Http.RequestStringAsync(url);
_community = Json.Deserialize<CommunityData>(json);
}
catch (Exception ex)
{
Log.Warning($"Community fetch failed: {ex.Message}");
_communityError = true;
}
_communityLoading = false;
StateHasChanged();
} *@
bool _discordCopied = false;
async void OnDiscordCopy()
{
string discordUrl = "https://discord.gg/tGvWgHUsYu";
Clipboard.SetText(discordUrl);
_discordCopied = true;
Sound.Play("UI_Button");
Sound.Play("Notification_1");
StateHasChanged();
await GameTask.DelaySeconds(2.5f);
_discordCopied = false;
StateHasChanged();
}
bool _musicLinkCopied = false;
async void OnMusicLinkCopy()
{
Clipboard.SetText("https://yourcomposer.com/profile");
_musicLinkCopied = true;
Sound.Play("UI_Button");
Sound.Play("Notification_1");
StateHasChanged();
await GameTask.DelaySeconds(2.5f);
_musicLinkCopied = false;
StateHasChanged();
}
// ----------------------- //
// Main menu fake bomb vfx //
// ----------------------- //
// Explosion state
List<(float x, float y, float life, string emoji)> _menuExplosions = new();
void TriggerMenuExplosion()
{
Sound.Play("climax_explosion_lv1_lowpass");
// Spawn 5-8 particles around the title area
int count = 5 + _rng.Next(4);
string[] emojis = { "💥", "✨", "💣", "⚡", "🔥" };
for (int i = 0; i < count; i++)
{
_menuExplosions.Add((
x: 15f + (float)_rng.NextDouble() * 25f, // left side where title is
y: 20f + (float)_rng.NextDouble() * 40f,
life: 1.0f,
emoji: emojis[_rng.Next(emojis.Length)]
));
}
}
// ============================== SURVIVAL MODE ============================== //
void OnPlaySurvival()
{
if (_save?.TutorialCompleted != true) return;
_survivalSelectedBombs.Clear();
_survivalSelectedBombs.Add(GridManager.BombType.Cross); // Cross always available by default
_showSurvivalLoadout = true;
StateHasChanged();
}
void ToggleSurvivalBomb(GridManager.BombType type)
{
if (_survivalSelectedBombs.Contains(type))
{
if (_survivalSelectedBombs.Count > 1) // must keep at least 1
{
_survivalSelectedBombs.Remove(type);
Sound.Play("UI_Button");
}
}
else
{
if (_survivalSelectedBombs.Count < 3) // cap at 3
{
_survivalSelectedBombs.Add(type);
Sound.Play("BuyShop");
}
}
StateHasChanged();
}
bool ShowsClashWarning() =>
_survivalSelectedBombs.Contains(GridManager.BombType.Diagonal) &&
_survivalSelectedBombs.Contains(GridManager.BombType.Cross) &&
_survivalSelectedBombs.Count > 1;
void ConfirmSurvivalLoadoutAndStart()
{
if (_survivalSelectedBombs.Count == 0) return;
_showSurvivalLoadout = false;
Game?.StartGame(GameMode.Survival, _survivalSelectedBombs.ToList());
Sound.Play("PlayButton");
StateHasChanged();
}
string GetName(Leaderboards.Board2.Entry entry)
{
if(!string.IsNullOrWhiteSpace(entry.DisplayName)) return entry.DisplayName;
try
{
var friend = new Sandbox.Friend(entry.SteamId);
if(!string.IsNullOrWhiteSpace(friend.Name)) return friend.Name;
}
catch { }
return $"Player {entry.SteamId}";
}
string GetName(Leaderboards.Entry entry)
{
if(!string.IsNullOrWhiteSpace(entry.DisplayName)) return entry.DisplayName;
try
{
var friend = new Sandbox.Friend(entry.SteamId);
if(!string.IsNullOrWhiteSpace(friend.Name)) return friend.Name;
}
catch { }
return $"Player {entry.SteamId}";
}
}