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:
@@ -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' }));
|
||||
|
||||
Reference in New Issue
Block a user