Add full retention systems: daily hub, raids, prestige, seasons

- Graze slow-mo, perfect-wave FRENZY (x3 amber), ghost records,
  dynamic hunter titles
- Daily Hub: 7-day login calendar, animated lucky wheel, boss chest
  keys, pet eggs with rarity tiers + pity counter and real stat perks
- Seeded daily challenge (server-consistent modifiers, local top-5),
  hourly Golden Herd stampede, weekly Raid Colossus with damage tiers
- Rebirth prestige with Ancient Gem permanent perk shop
- Weapon mastery stars (+2%/level), bestiary milestone gems
- Weekly season points with Bronze..Diamond rank badges
- Shareable PNG result card on game over
This commit is contained in:
Primal Rampage Bot
2026-08-23 06:55:51 +00:00
parent 2024ea48b5
commit 7164cc798c
8 changed files with 979 additions and 28 deletions
+304 -1
View File
@@ -37,6 +37,11 @@
var s = US.save.get();
$('menu-coins').textContent = '$' + US.util.fmt(s.coins);
if ($('menu-gems')) $('menu-gems').textContent = '🔥 ' + (s.gems | 0);
if ($('menu-rank') && US.save.seasonRank) {
var sr2 = US.save.seasonRank();
$('menu-rank').textContent = sr2.tier.icon + ' ' +
L({ en: sr2.tier.en, vi: sr2.tier.vi });
}
if ($('dot-upgrades')) {
var canUp = Object.keys(US.UPGRADES || {}).some(function (k) {
var d = US.UPGRADES[k];
@@ -213,8 +218,10 @@
var desc = document.createElement('div');
desc.className = 'item-desc';
if (shopTab === 'weapons') {
var mw3 = US.save.masteryLevel ? US.save.masteryLevel(it.kind) : { level: 0, bonus: 0 };
desc.textContent = 'DMG ' + it.dmg + ' · ROF ' + it.rof + '/s' +
(it.pellets > 1 ? ' · ×' + it.pellets : '') + (it.pierce ? ' · PIERCE' : '') + (it.aoe ? ' · AoE' : '');
(it.pellets > 1 ? ' · ×' + it.pellets : '') + (it.pierce ? ' · PIERCE' : '') + (it.aoe ? ' · AoE' : '') +
(mw3.level > 0 ? ' · ⭐Lv' + mw3.level + ' (+' + Math.round(mw3.bonus * 100) + '%)' : '');
} else if (shopTab === 'heroes') {
desc.textContent = US.i18n.t('hero.statHp') + ' ' + it.hp + ' · ' +
US.i18n.t('hero.statSpeed') + ' ' + it.speed + ' · ' + it.talent;
@@ -406,7 +413,44 @@
function showPause() { show($('overlay-pause')); }
function hidePause() { hide($('overlay-pause')); }
var lastRunStats = null;
function buildShareCard(st) {
var cv = document.createElement('canvas');
cv.width = 520; cv.height = 280;
var c7 = cv.getContext('2d');
// dusk backdrop
var g7 = c7.createLinearGradient(0, 0, 0, 280);
g7.addColorStop(0, '#221033'); g7.addColorStop(0.55, '#b4581c'); g7.addColorStop(1, '#241206');
c7.fillStyle = g7; c7.fillRect(0, 0, 520, 280);
c7.fillStyle = 'rgba(255,217,143,0.9)';
c7.beginPath(); c7.arc(400, 90, 34, 0, Math.PI * 2); c7.fill();
// hero silhouette dot
c7.fillStyle = st.skinColor || '#e04b3a';
c7.beginPath(); c7.arc(96, 190, 26, 0, Math.PI * 2); c7.fill();
c7.strokeStyle = 'rgba(20,10,4,0.9)'; c7.lineWidth = 4; c7.stroke();
// text block
c7.fillStyle = '#fff3dd';
c7.font = 'bold 30px system-ui'; c7.textAlign = 'left';
c7.fillText('PRIMAL RAMPAGE', 150, 60);
c7.font = '16px system-ui';
c7.fillStyle = '#ffe4bd';
c7.fillText((st.name || US.save.get().name || 'Hunter') + ' · 🌍 ' + (st.biome || 'jungle'), 150, 92);
c7.font = 'bold 44px system-ui';
c7.fillStyle = '#ffd24a';
c7.fillText('Wave ' + (st.wave | 0), 150, 150);
c7.font = '18px system-ui';
c7.fillStyle = '#fff3dd';
c7.fillText('💀 ' + (st.kills | 0) + ' kills ×' + st.bestCombo + ' combo 💰' + (st.coins | 0), 150, 186);
if (st.petName) { c7.fillText('🐾 ' + st.petName, 150, 216); }
c7.font = '12px system-ui';
c7.fillStyle = 'rgba(255,233,201,0.75)';
c7.fillText('play: trycloudflare — hunt or be hunted', 150, 252);
return cv;
}
function gameOver(stats, mode) {
lastRunStats = stats;
$('over-title').textContent = US.i18n.t('over.title');
$('over-record').classList.toggle('hidden', !stats.record);
$('ov-score').textContent = US.util.fmt(stats.score);
@@ -510,6 +554,12 @@
return q.done && !q.claimed;
});
if ($('dot-quests')) $('dot-quests').classList.toggle('hidden', !claimable);
// daily hub glows when anything is waiting
var dailyReady =
!(sv.login.claimedToday && sv.login.last === US.save.today()) ||
US.save.canSpin() || sv.chest.keys > 0;
if ($('dot-daily')) $('dot-daily').classList.toggle('hidden', !dailyReady);
}
function renderQuests() {
@@ -596,6 +646,236 @@
}
}
/* ---------------- daily hub ---------------- */
function fmtReward(rw) {
var parts = [];
if (rw.coins) parts.push('💰' + rw.coins);
if (rw.gems) parts.push('🔥' + rw.gems);
if (rw.chestKey) parts.push('🗝️×' + rw.chestKey);
if (rw.egg) parts.push('🥚');
return parts.join(' ') || '—';
}
function renderDaily() {
var sv = US.save.get();
var L2 = function (k) { return US.i18n.t(k); };
// calendar strip
var strip = document.getElementById('login-strip');
if (strip) {
strip.innerHTML = '';
var dayIdx = Math.min(6, sv.login.streak - (sv.login.claimedToday ? 0 : 1));
for (var d = 0; d < 7; d++) {
var chip = document.createElement('div');
chip.className = 'cal-day' + (d < dayIdx ? ' past' : d === dayIdx ? ' today' : '');
chip.innerHTML = '<span>' + L2('daily.day') + ' ' + (d + 1) + '</span><b>' +
fmtReward(US.LOGIN_REWARDS[d]) + '</b>';
strip.appendChild(chip);
}
}
var btnClaim = document.getElementById('btn-claim-login');
if (btnClaim) {
var canClaim = !(sv.login.claimedToday && sv.login.last === US.save.today());
btnClaim.disabled = !canClaim;
btnClaim.textContent = canClaim ? L2('daily.claim') : L2('daily.claimed');
btnClaim.onclick = function () {
var res = US.save.claimLogin();
if (!res.ok) return;
US.audio.sfx('win');
document.getElementById('wheel-result').textContent =
'✅ ' + fmtReward(res.reward) + (res.pet ? ' 🥚 ' + res.pet.name : '');
renderDaily();
};
}
// wheel
drawWheel(document.getElementById('wheel-cv'), -Math.PI / 2);
var btnSpin = document.getElementById('btn-spin');
if (btnSpin) {
btnSpin.disabled = !US.save.canSpin();
btnSpin.textContent = US.save.canSpin() ? L2('daily.spin') : L2('daily.spun');
btnSpin.onclick = function () {
if (!US.save.canSpin()) return;
var res = US.save.spinWheel();
if (!res.ok) return;
US.audio.sfx('ui');
animateWheel(res.seg, function () {
US.audio.sfx('win');
document.getElementById('wheel-result').textContent =
'🎉 ' + fmtReward(res.reward) + (res.pet ? ' 🥚 ' + res.pet.name : '');
});
btnSpin.disabled = true;
btnSpin.textContent = L2('daily.spun');
};
}
// ---- daily challenge panel ----
var dinfo = US.save.dailyInfo();
var cm = document.getElementById('chal-mods');
if (cm) {
cm.innerHTML = dinfo.mods.map(function (mid) {
var md = US.DAILY_MOD_POOL.filter(function (m3) { return m3.id === mid; })[0] || {};
return '🎲 ' + L({ en: md.en, vi: md.vi });
}).join('<br>');
}
var cb = document.getElementById('chal-best');
var stCh = US.save.get().dailyChal;
if (cb) cb.textContent = (stCh.date === dinfo.date && stCh.bestScore > 0)
? L({ en: "Today's best", vi: 'Kỷ lục hôm nay' }) + ': ' + stCh.bestScore +
' · Top: ' + (stCh.board[0] ? stCh.board[0].score : '—')
: '';
var bp2 = document.getElementById('btn-play-chal');
if (bp2) bp2.onclick = function () {
US.audio.resume(); US.audio.sfx('ui');
US.game.start('campaign', { challenge: true });
showScreen(null);
};
// ---- weekly raid panel ----
var ri = US.save.raidInfo();
var pct = Math.min(100, Math.round(ri.damage / ri.pool * 100));
var fill = document.getElementById('raid-bar-fill');
if (fill) fill.style.width = pct + '%';
var rinfo = document.getElementById('raid-info');
if (rinfo) rinfo.innerHTML =
L({ en: 'Your damage', vi: 'Sát thương của bạn' }) + ': <b>' + ri.damage.toLocaleString() +
'</b> / ' + ri.pool.toLocaleString();
var tiersEl = document.getElementById('raid-tiers');
if (tiersEl) {
tiersEl.innerHTML = '';
ri.tiers.forEach(function (t4, ti4) {
var el = document.createElement('div');
var claimed = ri.claimedTiers.indexOf(ti4) >= 0;
var can = !claimed && ri.damage >= t4.dmg;
el.className = 'raid-tier' + (claimed ? ' claimed' : can ? ' claimable' : '');
el.innerHTML = '🔥' + t4.gems + '<br>' + t4.dmg.toLocaleString();
el.onclick = function () {
if (!can) return;
var r3 = US.save.claimRaidTier(ti4);
if (r3.ok) { US.audio.sfx('win'); renderDaily(); }
};
tiersEl.appendChild(el);
});
}
var br2 = document.getElementById('btn-play-raid');
if (br2) br2.onclick = function () {
US.audio.resume(); US.audio.sfx('ui');
US.game.start('campaign', { raid: true });
showScreen(null);
};
// ---- prestige / rebirth panel ----
var pi = US.save.prestigeInfo();
var prInfo = document.getElementById('pr-info');
if (prInfo) prInfo.innerHTML =
L({ en: 'Ancient Gems', vi: 'Ngọc Xưa' }) + ': <b>🟣 ' + pi.ngoc + '</b>' +
' · ' + L({ en: 'Rebirths', vi: 'Tái sinh' }) + ': <b>' + pi.count + '</b><br>' +
(pi.canPrestige
? L({ en: 'Rebirth now for', vi: 'Tái sinh nhận' }) + ' <b>+' + pi.ngocGain + '</b>'
: L({ en: 'Reach wave 50 to unlock Rebirth', vi: 'Đến wave 50 để mở Tái Sinh' }));
var perksEl = document.getElementById('pr-perks');
if (perksEl) {
perksEl.innerHTML = '';
Object.keys(pi.perkDefs).forEach(function (pk3) {
var def = pi.perkDefs[pk3];
var lv = pi.perks[pk3] || 0;
var cost = lv < def.cost.length ? def.cost[lv] : null;
var row = document.createElement('div');
row.className = 'pr-perk';
var label = pk3 === 'dmg' ? ('⚔️ +' + Math.round(def.per * 100) + '% DMG /lv')
: pk3 === 'hp' ? ('❤️ +' + def.per + ' HP /lv')
: ('💰 +' + Math.round(def.per * 100) + '% gold /lv');
row.innerHTML = '<span>' + label + '</span><b>Lv ' + lv + '/' + def.cost.length + '</b>';
var btn = document.createElement('button');
btn.className = 'btn sm';
btn.textContent = cost !== null ? '🟣' + cost : 'MAX';
btn.disabled = cost === null || pi.ngoc < cost;
btn.onclick = function () {
var r4 = US.save.buyPerk(pk3);
if (r4.ok) { US.audio.sfx('win'); renderDaily(); }
};
row.appendChild(btn);
perksEl.appendChild(row);
});
}
var btnReb = document.getElementById('btn-rebirth');
if (btnReb) {
btnReb.disabled = !pi.canPrestige;
btnReb.onclick = function () {
if (!pi.canPrestige) return;
var r5 = US.save.doPrestige();
if (r5.ok) {
US.audio.sfx('win');
toast('♻️ +' + r5.gained + ' ' + L({ en: 'Ancient Gems', vi: 'Ngọc Xưa' }));
renderDaily();
refreshMenuStats();
}
};
}
// chest
var info = document.getElementById('chest-info');
if (info) info.innerHTML = '🗝️ ×<b>' + sv.chest.keys + '</b>';
var btnChest = document.getElementById('btn-open-chest');
if (btnChest) {
var hasKey = sv.chest.keys > 0;
btnChest.disabled = !hasKey;
btnChest.textContent = hasKey ? L2('daily.open') : L2('daily.nokey');
btnChest.onclick = function () {
var res = US.save.openChest();
if (!res.ok) return;
US.audio.sfx('win');
document.getElementById('wheel-result').textContent =
'🧰 ' + fmtReward(res.reward) + (res.pet ? ' 🥚 ' + res.pet.name : '');
renderDaily();
};
}
}
function drawWheel(cv, rot) {
if (!cv || !cv.getContext) return;
var c2 = cv.getContext('2d');
var W2 = cv.width, H2 = cv.height, r = W2 / 2 - 6;
var cols = ['#c44a14', '#7a5230', '#b4581c', '#58a55c', '#9fd8ff', '#ffb938', '#b05f8f', '#ffd24a'];
c2.clearRect(0, 0, W2, H2);
c2.save();
c2.translate(W2 / 2, H2 / 2);
c2.rotate(rot || 0);
for (var i = 0; i < 8; i++) {
var a0 = i * Math.PI / 4, a1 = a0 + Math.PI / 4;
c2.beginPath(); c2.moveTo(0, 0); c2.arc(0, 0, r, a0, a1); c2.closePath();
c2.fillStyle = cols[i % cols.length]; c2.fill();
c2.strokeStyle = 'rgba(20,10,4,0.8)'; c2.lineWidth = 2; c2.stroke();
c2.save();
c2.rotate(a0 + Math.PI / 8);
c2.fillStyle = '#fff3dd';
c2.font = 'bold 11px system-ui';
c2.textAlign = 'right';
var lbl = fmtReward(US.save.WHEEL[i]);
c2.fillText(lbl, r - 10, 4);
c2.restore();
}
c2.restore();
c2.beginPath(); c2.arc(W2 / 2, H2 / 2, 16, 0, Math.PI * 2);
c2.fillStyle = '#241206'; c2.fill();
c2.strokeStyle = '#ffb938'; c2.lineWidth = 3; c2.stroke();
}
function animateWheel(targetSeg, done) {
var cv = document.getElementById('wheel-cv');
if (!cv) { if (done) done(); return; }
var start = performance.now(), dur = 2600;
var turns = 5;
var targetRot = turns * Math.PI * 2 + (Math.PI * 2 - targetSeg * Math.PI / 4 - Math.PI / 8);
(function step() {
var t = (performance.now() - start) / dur;
if (t >= 1) { drawWheel(cv, targetRot % (Math.PI * 2)); if (done) done(); return; }
var e2 = 1 - Math.pow(1 - t, 3);
drawWheel(cv, e2 * targetRot);
requestAnimationFrame(step);
})();
}
/* ---------------- bestiary ---------------- */
function renderBestiary() {
var sv = US.save.get();
@@ -720,12 +1000,35 @@
});
});
if ($('btn-daily')) $('btn-daily').onclick = function () { US.audio.sfx('ui'); renderDaily(); showScreen('daily'); };
if ($('btn-share')) $('btn-share').onclick = function () {
if (!lastRunStats) return;
var card2 = buildShareCard({
name: US.save.get().name,
wave: lastRunStats.wave, kills: lastRunStats.kills,
bestCombo: lastRunStats.bestCombo, coins: lastRunStats.coins,
biome: lastRunStats.biome, petName: lastRunStats.petName,
skinColor: lastRunStats.skinColor
});
try {
var a4 = document.createElement('a');
a4.download = 'primal-rampage-wave' + (lastRunStats.wave | 0) + '.png';
a4.href = card2.toDataURL('image/png');
document.body.appendChild(a4); a4.click(); a4.remove();
toast(L({ en: '📸 Share card saved!', vi: '📸 Đã lưu thẻ chia sẻ!' }));
} catch (e9) {
toast(L({ en: '📸 Card ready!', vi: '📸 Thẻ đã sẵn sàng!' }));
}
};
if ($('btn-quests')) $('btn-quests').onclick = function () { US.audio.sfx('ui'); renderQuests(); showScreen('quests'); };
if ($('btn-bestiary')) $('btn-bestiary').onclick = function () { US.audio.sfx('ui'); renderBestiary(); showScreen('bestiary'); };
if ($('btn-upgrades')) $('btn-upgrades').onclick = function () { US.audio.sfx('ui'); shopTab = 'upgrades'; renderShop(); showScreen('shop'); };
function setTxt(id, v) { if ($(id)) $(id).textContent = v; }
setTxt('lbl-quests', L({ en: 'Quests', vi: 'Nhiệm vụ' }));
setTxt('lbl-daily', L({ en: 'Daily Hub', vi: 'Ưu đãi hôm nay' }));
setTxt('lbl-bestiary', L({ en: 'Bestiary', vi: 'Ký sự thú' }));
setTxt('lbl-upgrades', L({ en: 'Upgrades', vi: 'Nâng cấp' }));
setTxt('lbl-tabup', L({ en: 'Upgrades', vi: 'Nâng cấp' }));