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
+271 -1
View File
@@ -36,9 +36,49 @@
sfx: 0.8, music: 0.5, autofire: true, autoAim: false, onehit: false,
shake: true, shakeAmt: 1, quality: 'high'
},
board: [] // [{name,score,wave,ts}]
board: [], // [{name,score,wave,ts}]
// --- v3: retention systems ---
login: { last: '', streak: 0, claimedToday: false },
wheel: { last: '' },
chest: { keys: 0, pending: 0 },
eggs: { hatched: 0, pity: 0 },
prestige: { count: 0, ngoc: 0, perks: {} }, // perks: {dmg:lv,hp:lv,gold:lv}
mastery: {}, // weaponKind -> kills
dailyChal: { date: '', bestScore: 0, board: [] }, // [{name,score,wave,ts}] top5
season: { week: '', points: 0, claimedMilestones: [] },
raid: { week: '', damage: 0, claimedTiers: [] }
};
function isoWeekKey(d) {
var dt = new Date(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate()));
var day = dt.getUTCDay() || 7;
dt.setUTCDate(dt.getUTCDate() + 4 - day);
var y0 = new Date(Date.UTC(dt.getUTCFullYear(), 0, 1));
var wk = Math.ceil(((dt - y0) / 864e5 + 1) / 7);
return dt.getUTCFullYear() + '-W' + wk;
}
var PRESTIGE_PERKS = {
dmg: { per: 0.04, cost: [1, 2, 4, 8] },
hp: { per: 8, cost: [1, 2, 4, 8] },
gold: { per: 0.03, cost: [1, 2, 4, 8] }
};
var SEASON_RANKS = [
{ at: 0, en: 'Unranked', vi: 'Chưa Xếp Hạng', icon: '·' },
{ at: 1000, en: 'Bronze', vi: 'Đồng', icon: '🥉' },
{ at: 3000, en: 'Silver', vi: 'Bạc', icon: '🥈' },
{ at: 8000, en: 'Gold', vi: 'Vàng', icon: '🥇' },
{ at: 20000, en: 'Diamond', vi: 'Kim Cương', icon: '💎' }
];
var BESTIARY_MS = [50, 200, 500];
var BESTIARY_MS_GEMS = [5, 12, 30];
var RAID_TIERS = [
{ dmg: 1000, gems: 20 },
{ dmg: 5000, gems: 50 },
{ dmg: 20000, gems: 120 }
];
function clone(o) { return JSON.parse(JSON.stringify(o)); }
function isPlain(o) { return o && typeof o === 'object' && !(o instanceof Array); }
@@ -174,6 +214,236 @@
},
/* ---------- live-ops ---------- */
isoWeekKey: function (d) { return isoWeekKey(d || new Date()); },
// ---- eggs with rarity + pity ----
hatchEgg: function (forceRoll) {
var st = this.get();
st.eggs.hatched++;
st.eggs.pity++;
var rar = 'common';
var roll = (forceRoll !== undefined) ? forceRoll : Math.random() * 100;
if (roll >= 95 || st.eggs.pity >= 30) rar = 'legendary';
else if (roll >= 70) rar = 'rare';
if (rar !== 'common') st.eggs.pity = 0;
var pool = US.PETS[rar];
var pick = pool[(Math.random() * pool.length) | 0];
st.pet = { name: pick.name, color: pick.color, rarity: rar, perk: pick.perk || null };
this.save();
return { pet: clone(st.pet), rarity: rar };
},
// ---- 7-day login calendar ----
claimLogin: function () {
var st = this.get();
var today = this.today();
if (st.login.claimedToday && st.login.last === today) return { ok: false, reason: 'claimed' };
var yest = new Date(); yest.setDate(yest.getDate() - 1);
var yKey = [yest.getFullYear(), String(yest.getMonth() + 1).padStart(2, '0'),
String(yest.getDate()).padStart(2, '0')].join('-');
st.login.streak = (st.login.last === yKey) ? st.login.streak + 1 : 1;
st.login.last = today;
st.login.claimedToday = true;
var dayIdx = Math.min(6, st.login.streak - 1);
var rw = US.LOGIN_REWARDS[dayIdx];
if (rw.coins) st.coins += rw.coins;
if (rw.gems) this.addGems(rw.gems);
if (rw.chestKey) st.chest.keys += rw.chestKey;
var hatched = null;
if (rw.egg) hatched = this.hatchEgg(rw.egg === 'rare' ? 80 : 96);
this.save();
return { ok: true, day: dayIdx + 1, streak: st.login.streak, reward: rw, pet: hatched && hatched.pet };
},
// ---- lucky wheel, one free spin per day ----
WHEEL: [
{ coins: 250 }, { gems: 5 }, { coins: 500 }, { chestKey: 1 },
{ coins: 750 }, { gems: 15 }, { coins: 1200 }, { egg: 1 }
],
canSpin: function () { var st = this.get(); return st.wheel.last !== this.today(); },
spinWheel: function (idx) {
if (!this.canSpin()) return { ok: false, reason: 'spun' };
var st = this.get();
st.wheel.last = this.today();
var seg = (idx !== undefined) ? idx : (Math.random() * this.WHEEL.length) | 0;
var rw = this.WHEEL[seg % this.WHEEL.length];
var petRes = null;
if (rw.coins) st.coins += rw.coins;
if (rw.gems) this.addGems(rw.gems);
if (rw.chestKey) st.chest.keys += rw.chestKey;
if (rw.egg) petRes = this.hatchEgg();
this.save();
return { ok: true, seg: seg % this.WHEEL.length, reward: rw, pet: petRes && petRes.pet };
},
// ---- seeded daily challenge ----
DAILY_MODS: null, // filled below after US.DAILY_MOD_POOL exists
dailyInfo: function () {
var date = this.today();
var seed = 0;
for (var i = 0; i < date.length; i++) seed = (seed * 31 + date.charCodeAt(i)) >>> 0;
// mulberry32
function rnd() {
seed |= 0; seed = (seed + 0x6D2B79F5) | 0;
var t2 = Math.imul(seed ^ (seed >>> 15), 1 | seed);
t2 = (t2 + Math.imul(t2 ^ (t2 >>> 7), 61 | t2)) ^ t2;
return ((t2 ^ (t2 >>> 14)) >>> 0) / 4294967296;
}
var pool = US.DAILY_MOD_POOL;
var a = pool[(rnd() * pool.length) | 0];
var b = pool[(rnd() * pool.length) | 0];
var mods = [a, b !== a ? b : pool[(pool.indexOf(a) + 1) % pool.length]];
return { date: date, mods: mods, seed: seed };
},
submitChallengeScore: function (score, wave) {
var st = this.get();
var info = this.dailyInfo();
if (st.dailyChal.date !== info.date) {
st.dailyChal.date = info.date;
st.dailyChal.bestScore = 0;
st.dailyChal.board = [];
}
st.dailyChal.bestScore = Math.max(st.dailyChal.bestScore, score | 0);
st.dailyChal.board.push({ name: st.name, score: score | 0, wave: wave | 0, ts: Date.now() });
st.dailyChal.board.sort(function (p, q2) { return q2.score - p.score; });
st.dailyChal.board = st.dailyChal.board.slice(0, 5);
this.save();
return { best: st.dailyChal.bestScore, rank: st.dailyChal.board.length };
},
// ---- weekly raid ----
raidPoolBase: 50000,
raidInfo: function () {
var st = this.get();
var wk = isoWeekKey(new Date());
if (st.raid.week !== wk) { st.raid.week = wk; st.raid.damage = 0; st.raid.claimedTiers = []; }
var weekNum = parseInt(wk.split('-W')[1], 10) || 1;
return {
week: wk,
pool: this.raidPoolBase * weekNum,
damage: st.raid.damage,
tiers: RAID_TIERS,
claimedTiers: st.raid.claimedTiers.slice()
};
},
addRaidDamage: function (dmg) {
var st = this.get();
var wk = isoWeekKey(new Date());
if (st.raid.week !== wk) { st.raid.week = wk; st.raid.damage = 0; st.raid.claimedTiers = []; }
st.raid.damage += Math.round(dmg);
this.save();
},
claimRaidTier: function (idx) {
var st = this.get();
var wk = isoWeekKey(new Date());
if (st.raid.week !== wk) return { ok: false, reason: 'week' };
if (st.raid.claimedTiers.indexOf(idx) >= 0) return { ok: false, reason: 'claimed' };
var tier = RAID_TIERS[idx];
if (!tier || st.raid.damage < tier.dmg) return { ok: false, reason: 'locked' };
st.raid.claimedTiers.push(idx);
this.addGems(tier.gems);
this.save();
return { ok: true, gems: tier.gems };
},
// ---- prestige / rebirth ----
prestigeInfo: function () {
var st = this.get();
var gain = st.bestWave >= 50 ? Math.max(1, Math.floor((st.bestWave - 40) / 5)) : 0;
return {
canPrestige: st.bestWave >= 50,
bestWave: st.bestWave,
ngocGain: gain,
ngoc: st.prestige.ngoc,
count: st.prestige.count,
perks: clone(st.prestige.perks),
perkDefs: PRESTIGE_PERKS
};
},
doPrestige: function () {
var st = this.get();
var info = this.prestigeInfo();
if (!info.canPrestige) return { ok: false, reason: 'wave50' };
st.prestige.ngoc += info.ngocGain;
st.prestige.count++;
st.upgrades = {};
st.coins = 0;
st.bombs = 1;
this.save();
return { ok: true, gained: info.ngocGain };
},
buyPerk: function (id) {
var def = PRESTIGE_PERKS[id];
if (!def) return { ok: false, reason: 'noid' };
var st = this.get();
var lv = st.prestige.perks[id] || 0;
if (lv >= def.cost.length) return { ok: false, reason: 'max' };
var cost = def.cost[lv];
if (st.prestige.ngoc < cost) return { ok: false, reason: 'poor' };
st.prestige.ngoc -= cost;
st.prestige.perks[id] = lv + 1;
this.save();
return { ok: true, level: lv + 1 };
},
// ---- weapon mastery ----
masteryLevel: function (kind) {
var kills = (this.get().mastery || {})[kind] || 0;
var lvl = Math.min(15, Math.floor(Math.sqrt(kills / 5)));
return { level: lvl, bonus: lvl * 0.02, kills: kills };
},
// ---- bestiary milestones ----
bestiaryMilestoneCheck: function (spKey) {
var st = this.get();
var entry = st.bestiary[spKey];
if (!entry) return 0;
entry.ms = entry.ms || [];
var granted = 0;
BESTIARY_MS.forEach(function (m4, i4) {
if (entry.kills >= m4 && entry.ms.indexOf(i4) < 0) {
entry.ms.push(i4);
granted += BESTIARY_MS_GEMS[i4];
}
});
if (granted > 0) this.addGems(granted);
return granted;
},
// ---- season points & rank ----
addSeasonPoints: function (n) {
var st = this.get();
var wk = isoWeekKey(new Date());
if (st.season.week !== wk) { st.season.week = wk; st.season.points = 0; }
st.season.week = wk;
st.season.points += n | 0;
this.save();
return st.season.points;
},
seasonRank: function () {
var st = this.get();
var pts = st.season.points || 0;
var tier = SEASON_RANKS[0];
for (var i5 = SEASON_RANKS.length - 1; i5 >= 0; i5--) {
if (pts >= SEASON_RANKS[i5].at) { tier = SEASON_RANKS[i5]; break; }
}
return { points: pts, week: st.season.week, tier: clone(tier) };
},
// ---- boss chest ----
openChest: function () {
var st = this.get();
if (st.chest.keys < 1) return { ok: false, reason: 'nokey' };
st.chest.keys--;
var r = Math.random();
var reward;
var petRes = null;
if (r < 0.55) { var c2 = 450 + ((Math.random() * 500) | 0); st.coins += c2; reward = { coins: c2 }; }
else if (r < 0.85) { var g2 = 10 + ((Math.random() * 12) | 0); this.addGems(g2); reward = { gems: g2 }; }
else { petRes = this.hatchEgg(); reward = { egg: true }; }
this.save();
return { ok: true, reward: reward, pet: petRes && petRes.pet };
},
isWeekend: function () {
var d = new Date().getDay();
return d === 0 || d === 6;