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
+217 -17
View File
@@ -23,6 +23,7 @@
var g = {
mode: mode, // 'campaign' | 'arena'
runOpts: opts || {},
state: 'playing', // playing | cards | paused | revive | over
time: 0,
over: false,
@@ -113,6 +114,7 @@
var budget = waveBudget(wave);
if (wave > 25) budget = Math.floor(budget * (1 + (wave - 25) * 0.09));
if (g.bloodMoon) budget = Math.floor(budget * 1.25);
if (g.mods && g.mods.swarm) budget = Math.ceil(budget * 1.4);
var pool = availableEnemies(wave);
var q = [];
var t = 0;
@@ -187,12 +189,28 @@
function scheduleNextWave(g) {
g.wave++;
g.waveActive = true;
g.waveDamaged = false;
// ghost record banner the moment this run passes the old best
if (!g.ghostShown && g.ghostWave > 0 && g.wave > g.ghostWave) {
g.ghostShown = true;
announce(g, L({ en: '👻 GHOST BUSTED — wave ' + g.ghostWave + ' beaten!',
vi: '👻 PHÁ KỶ LỤC — vượt wave ' + g.ghostWave + '!' }, metaLang()), '#7ee787');
}
g.spawnQueue = buildWaveQueue(g, g.wave);
// act biomes: jungle -> swamp -> ashlands
var wantBio = biomeFor(g.wave);
if (g.biome !== wantBio) enterBiome(g, wantBio);
// weekly raid: one colossal bruiser carrying the remaining pool
if (g.raid) {
var ri = US.save.raidInfo();
var remain = Math.max(2000, ri.pool - ri.damage);
g.spawnQueue = [{ defId: 'bruiser', t: 0.5, mods: { hp: Math.min(6, remain / 2600), speed: 0.55 } }];
g.raidBossPending = true;
return;
}
if (g.wave % 5 === 0) {
announce(g, US.i18n.t('announce.boss'), '#e04b3a');
US.audio.sfx('bossRoar');
@@ -498,6 +516,7 @@
function damageEnemy(g, e, dmg, crit, silent) {
if (!e.alive) return;
e.hp -= dmg;
if (g.raidBoss && e === g.raidBoss) US.save.addRaidDamage(dmg);
e.hitFlash = 0.1;
if (!silent) {
US.entities.floatText(e.x + util.rand(-8, 8), e.y - e.radius - 6,
@@ -633,6 +652,29 @@
}
/* ---------------- the tribe's pet raptor -------------------------------- */
function spawnGoldenHerd(g) {
if (g.herdDone) return;
g.herdDone = true;
for (var gh = 0; gh < 6; gh++) {
var gp = spawnEdgePoint(g);
spawnEnemyAt(g, 'runner', gp.x, gp.y, { hp: 0.5 });
var ge2 = g.enemies[g.enemies.length - 1];
ge2.golden = true; ge2.color = '#ffd24a';
ge2.value = (ge2.value || 1) * 8;
ge2.speed = (ge2.speed || 160) * 1.15;
}
announce(g, L({ en: '✨ GOLDEN HERD STAMPEDE — ×8 amber!',
vi: '✨ ĐÀN VÀNG LỘT XUYÊN — hổ phách ×8!' }, metaLang()), '#ffd24a');
}
function applyPetPerk(stats, perk) {
if (!perk || !stats) return;
if (perk.dmg) stats.dmgMul = (stats.dmgMul || 1) * (1 + perk.dmg);
if (perk.rof) stats.rofMul = (stats.rofMul || 1) * (1 + perk.rof);
if (perk.gold) stats.coinMul = (stats.coinMul || 1) * (1 + perk.gold);
if (perk.hp) stats.maxHp = (stats.maxHp || 100) + perk.hp;
}
function makePet(g) {
var pd = US.save.get().pet || { name: 'Rex Jr', color: '#5f8f3e' };
return {
@@ -701,11 +743,52 @@
g.bestCombo = Math.max(g.bestCombo, g.combo);
g.sinceKill = 0;
// unscathed kill streak -> hunter titles
g.killStreak = (g.killStreak || 0) + 1;
var TITLES = [
[15, { en: 'Storm Hunter', vi: 'Thợ Săn Bão Tố' }],
[30, { en: 'Apex Predator', vi: 'Kẻ Săn Đỉnh Cao' }],
[60, { en: 'Legend of the Valley', vi: 'Huyền Thoại Thung Lũng' }]
];
for (var tt3 = TITLES.length - 1; tt3 >= 0; tt3--) {
if (g.killStreak >= TITLES[tt3][0]) {
if (g.curTitle !== TITLES[tt3][1].en) {
g.curTitle = TITLES[tt3][1].en;
announce(g, L({ en: '🏅 Title earned: ' + TITLES[tt3][1].en,
vi: '🏅 Danh hiệu: ' + TITLES[tt3][1].vi }, metaLang()), '#ffd24a');
}
break;
}
}
// weapon mastery XP (kills with current weapon kind)
var wk2 = g.player.weapon && g.player.weapon.kind;
if (wk2) {
var mst = US.save.get().mastery;
mst[wk2] = (mst[wk2] || 0) + 1;
}
// boss falls -> chest key drop
if (g.raidBoss === e) {
announce(g, L({ en: '🏆 RAID COLOSSUS FELLED!', vi: '🏆 KOLOSS SỤP ĐỘ!' }, metaLang()), '#ffd24a');
endRun(g);
return;
}
if (e.isBoss) {
US.save.get().chest.keys += 1;
announce(g, L({ en: '🗝️ Boss dropped a CHEST KEY!', vi: '🗝️ Trùm đánh rơi CHÌA KHÓA RƯƠNG!' }, metaLang()), '#ffd24a');
}
// ---- meta progression on every slain beast ----
var spKey = speciesOf(e);
if (spKey && US.BESTIARY[spKey]) {
var bDef = US.BESTIARY[spKey];
US.save.bumpBestiary(spKey);
var msGems = US.save.bestiaryMilestoneCheck(spKey);
if (msGems > 0) {
announce(g, L({ en: '📖 ' + spKey.toUpperCase() + ' milestone — +' + msGems + ' 🔥',
vi: '📖 Cột mốc ' + spKey.toUpperCase() + ' — +' + msGems + ' 🔥' }, metaLang()), '#7ee787');
}
var bEntry = US.save.get().bestiary[spKey];
if (bEntry.kills >= bDef.goal && US.save.bestiaryDone(spKey, function () {
US.save.addGems(bDef.gem);
@@ -805,6 +888,17 @@
}
function hurtPlayer(g, dmg) {
if (g.enemyDmgMul) dmg *= g.enemyDmgMul;
g.lastHurt = { t: Math.round(g.time * 10) / 10, dmg: Math.round(dmg * 10) / 10,
hpAfter: Math.round((g.player ? g.player.hp - dmg : 0)) };
g.waveDamaged = true;
if (g.perfectChain) g.perfectChain = 0;
if (g.frenzyT) g.frenzyT = 0;
if ((g.killStreak || 0) >= 10 && !g.titleLostShown) {
g.titleLostShown = true;
}
g.killStreak = 0;
g.curTitle = null;
var p = g.player;
if (!p.alive || p.invuln > 0) return;
if (US.save.get().settings.onehit && g.mode === 'campaign') dmg = p.stats.maxHp;
@@ -857,6 +951,9 @@
var save = US.save.get();
save.bombs = g.mode === 'campaign' ? g.player.bombs : save.bombs;
var record = US.save.submitRun(g.score, g.wave || 0);
if (g.challenge) US.save.submitChallengeScore(g.score, g.wave || 0);
var seasonPts = Math.round(g.score / 100) + (g.wave | 0);
US.save.addSeasonPoints(seasonPts);
US.save.addCoins(g.runCoins);
US.save.stat('runs', 1);
checkAch(g);
@@ -867,7 +964,10 @@
kills: g.kills,
bestCombo: g.bestCombo,
coins: g.runCoins,
record: record
record: record,
biome: g.biome || 'jungle',
petName: US.save.get().pet ? US.save.get().pet.name : null,
skinColor: g.player.skin.body
}, g.mode);
}
@@ -916,8 +1016,47 @@
function update(g, dt) {
if (!g.metaInit) {
g.metaInit = true;
if (g.runOpts && g.runOpts.challenge) {
g.challenge = true;
var dinfo = US.save.dailyInfo();
g.mods = {};
dinfo.mods.forEach(function (m2) { g.mods[m2] = true; });
announce(g, L({ en: '🎯 DAILY CHALLENGE', vi: '🎯 THỬ THÁCH HÔM NAY' }, metaLang()), '#7ee787');
}
if (g.runOpts && g.runOpts.raid) {
g.raid = true;
var rinfo = US.save.raidInfo();
g.mods = { raid: true };
announce(g, L({ en: '⚔️ WEEKLY RAID — strike the colossus!',
vi: '⚔️ RAID TUẦN — đánh koloss!' }, metaLang()), '#ff9b3d');
}
if (g.mods && g.mods.glasshero) {
g.player.stats.maxHp = Math.ceil(g.player.stats.maxHp * 0.6);
g.player.hp = Math.min(g.player.hp, g.player.stats.maxHp);
}
if (g.mods && g.mods.venomwind) g.enemyDmgMul = 1.3;
// permanent prestige perks
var pvk = US.save.get().prestige.perks;
var pdefs = { dmg: 0.04, hp: 8, gold: 0.03 };
if (pvk.dmg) g.player.stats.dmgMul *= 1 + pdefs.dmg * pvk.dmg;
if (pvk.hp) {
g.player.stats.maxHp += pdefs.hp * pvk.hp;
g.player.hp = Math.min(g.player.hp + pdefs.hp * pvk.hp, g.player.stats.maxHp);
}
if (pvk.gold) g.player.stats.coinMul *= 1 + pdefs.gold * pvk.gold;
// weapon mastery of the equipped weapon
var mw2 = US.save.masteryLevel(g.player.weapon.kind);
if (mw2.bonus > 0) g.player.stats.dmgMul *= 1 + mw2.bonus;
g.waveDamaged = false;
g.perfectChain = 0;
g.killStreak = 0;
g.ghostWave = US.save.get().bestWave || 0;
g.player.bombs += g.player.stats.startBombs | 0;
if (US.save.get().pet) g.pet = makePet(g);
if (US.save.get().pet) {
g.pet = makePet(g);
applyPetPerk(g.player.stats, US.save.get().pet.perk);
g.player.hp = Math.min(g.player.hp, g.player.stats.maxHp);
}
ensureQuests(US.save.get());
if (US.save.isWeekend && US.save.isWeekend()) {
g.bloodMoon = true;
@@ -930,6 +1069,32 @@
updatePet(g, dt);
if (g.state === 'over') return;
// ---- GOLDEN HERD: hourly stampede of gilded compys ----
var nowMin = new Date().getMinutes();
if (!g.herdDone && nowMin < 10 && g.state === 'playing' && g.enemies.length < 40) {
spawnGoldenHerd(g);
}
// ---- GRAZE: skim death without touching it ----
if (g.frenzyT > 0) g.frenzyT -= dt;
if (g.player && g.player.alive) {
for (var gz = 0; gz < g.enemies.length; gz++) {
var ge = g.enemies[gz];
if (ge.alive === false || ge.spawnAnim > 0 || ge.grazed) continue;
var gr_ = ge.radius + g.player.radius;
var gd2 = util.dist2(ge.x, ge.y, g.player.x, g.player.y);
if (gd2 < (gr_ + 18) * (gr_ + 18) && gd2 > gr_ * gr_) {
ge.grazed = true;
g.slowmo = Math.max(g.slowmo || 0, 0.16);
g.score += 5;
g.grazes = (g.grazes || 0) + 1;
US.entities.spawnParticles(null, (ge.x + g.player.x) / 2, (ge.y + g.player.y) / 2, '#ffe9c9', 3, 0.4, 2);
US.entities.floatText((ge.x + g.player.x) / 2, (ge.y + g.player.y) / 2 - 8,
L({ en: 'graze!', vi: 'sượt!' }, metaLang()), '#ffe9c9');
}
}
}
// announcements always tick
for (var a = g.announces.length - 1; a >= 0; a--) {
g.announces[a].t += dt;
@@ -1059,10 +1224,17 @@
g.spawnQueue[q].t -= dt;
if (g.spawnQueue[q].t <= 0) {
var item = g.spawnQueue[q];
if (g.mods && g.mods.fastfoes) {
item.mods = item.mods || {};
item.mods.speed = (item.mods.speed || 1) * 1.25;
}
var pos = spawnEdgePoint(g);
pos.x = util.clamp(pos.x, WALL + 30, W - WALL - 30);
pos.y = util.clamp(pos.y, WALL + 30, H - WALL - 30);
spawnEnemyAt(g, item.defId, pos.x, pos.y, item.mods);
if (g.raid && !g.raidBoss && g.enemies[g.enemies.length - 1].isBoss) {
g.raidBoss = g.enemies[g.enemies.length - 1];
}
g.spawnQueue.splice(q, 1);
q--;
}
@@ -1250,6 +1422,20 @@
function onWaveClear(g) {
var bonus = 15 + g.wave * 3;
// PERFECT chain: untouched waves stack toward a FRENZY gold rush
if (!g.waveDamaged && g.wave > 1) {
g.perfectChain = (g.perfectChain || 0) + 1;
var pb = 20 * g.perfectChain;
g.runCoins += pb;
announce(g, L({ en: 'PERFECT ×' + g.perfectChain + ' +' + pb, vi: 'HOÀN HẢO ×' + g.perfectChain + ' +' + pb }, metaLang()), '#7ee787');
if (g.perfectChain >= 3 && !(g.frenzyT > 0)) {
g.frenzyT = 20;
announce(g, L({ en: '🔥 FRENZY — AMBER ×3!', vi: '🔥 CUỒNG NỘ — HỔ PHÁCH ×3!' }, metaLang()), '#ff9b3d');
}
} else {
g.perfectChain = 0;
}
g.runCoins += bonus;
g.score += 50 * g.wave;
announce(g, US.i18n.t('announce.waveClear'), '#8ac74a');
@@ -1319,10 +1505,13 @@
}
function collectPickup(g, pk) {
var frenzyMult = (g.frenzyT > 0) ? 3 : 1;
var p = g.player;
if (pk.kind === 'coin') {
var amt = Math.max(1, Math.round(pk.amount * p.stats.coinMul));
if (g.bloodMoon) amt *= 2;
if (g.mods && g.mods.richherds) amt *= 2;
if (frenzyMult > 1) amt *= frenzyMult;
g.runCoins += amt;
g.score += 2;
US.entities.floatText(pk.x, pk.y - 10, '+' + amt, '#ffb938');
@@ -1334,16 +1523,20 @@
p.bombs++;
US.entities.floatText(pk.x, pk.y - 10, '+1 💣', '#ff8a3d');
} else if (pk.kind === 'egg') {
var names = ['Cơm Nếp', 'Xanh Lá', 'Bonnie', 'Mập Mạp', 'Tia Chớp', 'Bông'];
var cols = ['#c98a3d', '#5f8f3e', '#b05f8f', '#7a5230', '#58a55c', '#9fd8ff'];
var pi = (Math.random() * names.length) | 0;
US.save.get().pet = { name: names[pi], color: cols[pi] };
US.save.save();
var res2 = US.save.hatchEgg();
g.pet = makePet(g);
applyPetPerk(g.player, res2.pet.perk);
var rarCol = res2.rarity === 'legendary' ? '#ff9b3d'
: res2.rarity === 'rare' ? '#7ee787' : '#ffd24a';
announce(g, L({
en: '🥚 A raptor hatched: "' + names[pi] + '"!',
vi: '🥚 Raptor con nở rồi: "' + names[pi] + '"!'
}, metaLang()), '#ffd24a');
en: '🥚 ' + res2.rarity.toUpperCase() + ' pet hatched: "' + res2.pet.name + '"!',
vi: '🥚 Thú cưng ' + (res2.rarity === 'legendary' ? 'HUYỀN THOẠI' : res2.rarity === 'rare' ? 'HIẾM' : 'thường') +
' nở rồi: "' + res2.pet.name + '"!'
}, metaLang()), rarCol);
if (res2.rarity !== 'common') {
g.flashWhite = 0.5;
boomFx(g, p.x, p.y, rarCol, 26, 160);
}
US.audio.sfx('win');
checkAch(g);
}
@@ -1893,6 +2086,13 @@
resume: function () { if (current && current.state === 'paused') { current.state = 'playing'; return true; } return false; },
castUlt: function () { if (current) castUlt(current); },
ensureQuests: function () { ensureQuests(US.save.get()); },
triggerHerd: function () { if (current && current.state === 'playing') spawnGoldenHerd(current); },
toggleAutoAim: function () {
var st = US.save.get();
st.settings.autoAim = !st.settings.autoAim;
US.save.save();
return st.settings.autoAim;
},
jumpWave: function (n) {
if (!current || current.state === 'over') return false;
current.state = 'playing';
@@ -1924,7 +2124,13 @@
}
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
if (current) {
update(current, dt);
// graze slow-mo: world breathes while a near-miss plays out
var effDt = dt;
if (current.slowmo > 0) {
current.slowmo -= dt;
effDt = dt * 0.35;
}
update(current, effDt);
render(current, ctx, vw, vh);
} else {
// painted dusk scene behind the menus
@@ -1945,12 +2151,6 @@
// wire entity callbacks that need game context
US.entities._hooks = {
nearestEnemy: function (x, y, d, ex) { return current ? nearestEnemy(current, x, y, d, ex) : null; },
toggleAutoAim: function () {
var st = US.save.get();
st.settings.autoAim = !st.settings.autoAim;
US.save.save();
return st.settings.autoAim;
},
damageEnemy: function (e, dmg, crit, silent) { if (current) damageEnemy(current, e, dmg, crit, silent); },
explode: function (x, y, r, dmg) { if (current) explodeAt(current, x, y, r, dmg); },
collect: function (pk) { if (current) collectPickup(current, pk); },