feat: obchod, černé můry, netopýr, pavouk a denní úkoly u chytání motýlků
CI / Generate TypeScript types (push) Successful in 15s
CI / Server unit tests (push) Successful in 23s
CI / Build server (push) Successful in 29s
CI / Build client (push) Successful in 1m2s
CI / Playwright E2E tests (push) Successful in 1m33s
CI / Build and push Docker image (push) Successful in 49s
CI / Notify (push) Successful in 1s
CI / Generate TypeScript types (push) Successful in 15s
CI / Server unit tests (push) Successful in 23s
CI / Build server (push) Successful in 29s
CI / Build client (push) Successful in 1m2s
CI / Playwright E2E tests (push) Successful in 1m33s
CI / Build and push Docker image (push) Successful in 49s
CI / Notify (push) Successful in 1s
- obchod s pomůckami a trvalými vylepšeními (větší síťka, strašák, zpevněná síťka) – mince mají trvalý smysl - černé můry se míchají mezi motýly; chycení (i magnetem) ubere hodně úlovků - ptáci silnější, plašič dražší a kratší, zloději chodí častěji podle bohatství - denní úkol s odměnou + achievementy/odznaky - netopýr v noci loví motýly, pavouk zamotá síťku pavučinou – oba na zaklikání - nákupní hlášky rozlišují nedostatek mincí od jiné chyby; herní smyčka se pozastaví při skryté záložce Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
e3f439bde0
commit
252d105408
@@ -0,0 +1,8 @@
|
||||
[
|
||||
"Nový obchod (tlačítko 🛒): utrácej mince za pomůcky i trvalá vylepšení – větší síťka, strašák na ptáky, zpevněná síťka, prémiová síťka na přání, vosí sprej, plašič a pojistka proti zloději",
|
||||
"Pozor na černé můry – míchají se mezi motýly a když nějakou omylem chytíš (i magnetem), přijdeš o hodně úlovků",
|
||||
"Ptáci jsou znovu výzva: je jich víc, přibývají rychleji a plašič je dražší a kratší",
|
||||
"Zloději chodí častěji a lákají je bohatí hráči – vyplatí se koupit pojistku",
|
||||
"Denní úkol za odměnu mincí a odznaky (achievementy) za milníky – najdeš je po kliknutí na počítadlo",
|
||||
"V noci občas přilétne netopýr, který loví motýly, a pavouk umí zamotat síťku pavučinou – odeženeš je klikáním"
|
||||
]
|
||||
+229
-11
@@ -40,6 +40,15 @@ interface StoredButterflyStats {
|
||||
catchWindowStart: number;
|
||||
/** Počet započtených úlovků v aktuálním okně */
|
||||
catchWindowCount: number;
|
||||
|
||||
/** Kolikrát hráč omylem chytil černou můru */
|
||||
mothsHit: number;
|
||||
/** Trvalá vylepšení z obchodu */
|
||||
upgrades: { net: number; scarecrow: number; reinforced: number };
|
||||
/** Do kdy platí pojistka proti zloději (ms epoch), 0 = neaktivní */
|
||||
insuranceUntil: number;
|
||||
/** Denní úkol */
|
||||
daily: { day: string; taskId: string; progress: number; target: number; done: boolean };
|
||||
}
|
||||
|
||||
const storage = getStorage();
|
||||
@@ -62,15 +71,15 @@ export const PREMIUM_MILESTONE = 75;
|
||||
/** Maximální počet současně poletujících vos */
|
||||
export const WASP_MAX = 6;
|
||||
/** Maximální počet současně poletujících ptáků */
|
||||
export const BIRD_MAX = 5;
|
||||
export const BIRD_MAX = 7;
|
||||
/** Jak často (ms) přibude jedna vosa */
|
||||
export const WASP_GROWTH_MS = 30_000;
|
||||
/** Jak často (ms) přibude jeden pták */
|
||||
export const BIRD_GROWTH_MS = 60_000;
|
||||
/** Jak často (ms) přibude jeden pták (základ; zpomaluje strašák) */
|
||||
export const BIRD_GROWTH_MS = 40_000;
|
||||
/** Cena plašiče ptáků v mincích */
|
||||
export const REPELLENT_COST = 60;
|
||||
export const REPELLENT_COST = 70;
|
||||
/** Jak dlouho (ms) plašič ptáků drží ptáky pryč */
|
||||
export const REPELLENT_DURATION_MS = 120_000;
|
||||
export const REPELLENT_DURATION_MS = 75_000;
|
||||
/** Za jak dlouho (ms) se protržená síťka sama zašije */
|
||||
export const AUTO_REPAIR_MS = 60_000;
|
||||
/** Jaký podíl mincí ukradne zloděj, když se dostane k penězům (0–1) */
|
||||
@@ -85,6 +94,55 @@ export const CATERPILLAR_EATS = 20;
|
||||
export const CATERPILLAR_REWARD = 15;
|
||||
/** Kolik zásahů (kliknutí) housenka vydrží */
|
||||
export const CATERPILLAR_HP = 18;
|
||||
/** O kolik úlovků přijdeš, když omylem chytíš černou můru */
|
||||
export const MOTH_PENALTY = 15;
|
||||
|
||||
// --- Obchod: spotřební ---
|
||||
/** Cena aktivace prémiové síťky na přání */
|
||||
export const PREMIUM_BUY_COST = 40;
|
||||
/** Cena vosího spreje (vyhubí všechny vosy) */
|
||||
export const WASP_SPRAY_COST = 25;
|
||||
/** Cena pojistky proti zloději */
|
||||
export const INSURANCE_COST = 50;
|
||||
/** Jak dlouho (ms) pojistka platí */
|
||||
export const INSURANCE_DURATION_MS = 300_000;
|
||||
/** Podíl mincí ukradený zlodějem, když je aktivní pojistka */
|
||||
export const ROBBERY_FRACTION_INSURED = 0.2;
|
||||
|
||||
// --- Obchod: trvalá vylepšení ---
|
||||
export type UpgradeId = 'net' | 'scarecrow' | 'reinforced';
|
||||
/** Maximální úrovně jednotlivých vylepšení */
|
||||
export const UPGRADE_MAX: Record<UpgradeId, number> = { net: 5, scarecrow: 4, reinforced: 3 };
|
||||
/** Základní cena vylepšení (další úroveň je 2× dražší) */
|
||||
const UPGRADE_BASE: Record<UpgradeId, number> = { net: 100, scarecrow: 120, reinforced: 150 };
|
||||
|
||||
/** Vrátí cenu příští úrovně daného vylepšení (level = aktuální úroveň). */
|
||||
export function upgradeCost(item: UpgradeId, level: number): number {
|
||||
return UPGRADE_BASE[item] * Math.pow(2, level);
|
||||
}
|
||||
|
||||
// --- Denní úkoly ---
|
||||
/** Odměna v mincích za splnění denního úkolu */
|
||||
export const DAILY_TASK_REWARD = 30;
|
||||
const DAILY_TASKS: { id: string; target: number }[] = [
|
||||
{ id: 'catch', target: 60 },
|
||||
{ id: 'golden', target: 3 },
|
||||
{ id: 'thief', target: 1 },
|
||||
{ id: 'wasp', target: 15 },
|
||||
];
|
||||
const DAILY_TITLES: Record<string, string> = {
|
||||
catch: 'Nachytej {n} motýlů',
|
||||
golden: 'Chyť {n} zlatých motýlů',
|
||||
thief: 'Poraz {n} zloděje',
|
||||
wasp: 'Zabij {n} vos',
|
||||
};
|
||||
|
||||
/** Deterministický výběr úkolu podle data (aby byl pro všechny stejný a stabilní). */
|
||||
function dailyTaskForDay(day: string): { id: string; target: number } {
|
||||
let h = 0;
|
||||
for (let i = 0; i < day.length; i++) h = (h * 31 + day.charCodeAt(i)) | 0;
|
||||
return DAILY_TASKS[Math.abs(h) % DAILY_TASKS.length];
|
||||
}
|
||||
|
||||
/** Od jaké velikosti dávky se začíná počítat kombo bonus */
|
||||
const COMBO_THRESHOLD = 4;
|
||||
@@ -183,6 +241,10 @@ function defaultStats(now: number): StoredButterflyStats {
|
||||
repellentUntil: 0, netTornUntil: 0,
|
||||
lastWaspAt: now, lastBirdAt: now,
|
||||
catchWindowStart: now, catchWindowCount: 0,
|
||||
mothsHit: 0,
|
||||
upgrades: { net: 0, scarecrow: 0, reinforced: 0 },
|
||||
insuranceUntil: 0,
|
||||
daily: { day: '', taskId: 'catch', progress: 0, target: 0, done: false },
|
||||
};
|
||||
}
|
||||
|
||||
@@ -205,9 +267,47 @@ function normalize(s: StoredButterflyStats, now: number): StoredButterflyStats {
|
||||
lastBirdAt: s.lastBirdAt ?? now,
|
||||
catchWindowStart: s.catchWindowStart ?? now,
|
||||
catchWindowCount: s.catchWindowCount ?? 0,
|
||||
mothsHit: s.mothsHit ?? 0,
|
||||
upgrades: {
|
||||
net: s.upgrades?.net ?? 0,
|
||||
scarecrow: s.upgrades?.scarecrow ?? 0,
|
||||
reinforced: s.upgrades?.reinforced ?? 0,
|
||||
},
|
||||
insuranceUntil: s.insuranceUntil ?? 0,
|
||||
daily: s.daily ?? { day: '', taskId: 'catch', progress: 0, target: 0, done: false },
|
||||
};
|
||||
}
|
||||
|
||||
/** Efektivní interval růstu ptáků (zpomaluje strašák). */
|
||||
function effectiveBirdGrowth(s: StoredButterflyStats): number {
|
||||
return Math.round(BIRD_GROWTH_MS * (1 + 0.35 * (s.upgrades?.scarecrow ?? 0)));
|
||||
}
|
||||
|
||||
/** Efektivní doba samo-zašití síťky (zkracuje zpevněná síťka). */
|
||||
function effectiveAutoRepair(s: StoredButterflyStats): number {
|
||||
return Math.round(AUTO_REPAIR_MS * (1 - 0.25 * (s.upgrades?.reinforced ?? 0)));
|
||||
}
|
||||
|
||||
/** Zajistí, že denní úkol odpovídá dnešnímu dni (jinak vygeneruje nový). */
|
||||
function ensureDaily(s: StoredButterflyStats, now: number): void {
|
||||
const day = formatDate(new Date(now));
|
||||
if (!s.daily || s.daily.day !== day) {
|
||||
const def = dailyTaskForDay(day);
|
||||
s.daily = { day, taskId: def.id, progress: 0, target: def.target, done: false };
|
||||
}
|
||||
}
|
||||
|
||||
/** Přičte progres dennímu úkolu daného druhu a případně vyplatí odměnu. */
|
||||
function bumpDaily(s: StoredButterflyStats, kind: string, amount: number): void {
|
||||
if (s.daily && !s.daily.done && s.daily.taskId === kind && amount > 0) {
|
||||
s.daily.progress += amount;
|
||||
if (s.daily.progress >= s.daily.target) {
|
||||
s.daily.done = true;
|
||||
s.coins += DAILY_TASK_REWARD;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Nechá v čase „dorůst" škůdce do maxima. Zachovává rozpracovaný čas (posouvá
|
||||
* razítko jen o spotřebované celé intervaly). Ptáci nerostou, když je aktivní plašič.
|
||||
@@ -232,10 +332,11 @@ export function growPests(s: StoredButterflyStats, now: number): void {
|
||||
const repellentActive = s.repellentUntil > now;
|
||||
if (!repellentActive) {
|
||||
if (s.birds < BIRD_MAX) {
|
||||
const add = Math.floor((now - s.lastBirdAt) / BIRD_GROWTH_MS);
|
||||
const birdGrowth = effectiveBirdGrowth(s);
|
||||
const add = Math.floor((now - s.lastBirdAt) / birdGrowth);
|
||||
if (add > 0) {
|
||||
s.birds = Math.min(s.birds + add, BIRD_MAX);
|
||||
s.lastBirdAt = s.birds >= BIRD_MAX ? now : s.lastBirdAt + add * BIRD_GROWTH_MS;
|
||||
s.lastBirdAt = s.birds >= BIRD_MAX ? now : s.lastBirdAt + add * birdGrowth;
|
||||
}
|
||||
} else {
|
||||
s.lastBirdAt = now;
|
||||
@@ -260,8 +361,19 @@ function toDto(s: StoredButterflyStats): ButterflyStats {
|
||||
birdsScared: s.birdsScared,
|
||||
thievesDefeated: s.thievesDefeated,
|
||||
caterpillarsDefeated: s.caterpillarsDefeated,
|
||||
mothsHit: s.mothsHit,
|
||||
repellentUntil: s.repellentUntil,
|
||||
netTornUntil: s.netTornUntil,
|
||||
insuranceUntil: s.insuranceUntil,
|
||||
upgrades: { ...s.upgrades },
|
||||
daily: {
|
||||
taskId: s.daily.taskId,
|
||||
title: (DAILY_TITLES[s.daily.taskId] ?? '{n}').replace('{n}', String(s.daily.target)),
|
||||
progress: s.daily.progress,
|
||||
target: s.daily.target,
|
||||
done: s.daily.done,
|
||||
reward: DAILY_TASK_REWARD,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -283,6 +395,7 @@ async function mutateUser(
|
||||
const all = current ?? {};
|
||||
const mine = normalize(all[login] ?? defaultStats(now), now);
|
||||
growPests(mine, now);
|
||||
ensureDaily(mine, now);
|
||||
mutator(mine);
|
||||
all[login] = mine;
|
||||
return all;
|
||||
@@ -347,6 +460,9 @@ export async function recordCatches(login: string, normal: number, golden: numbe
|
||||
s.goldenCaught += creditGolden;
|
||||
if (credited > 0) s.lastCatchDay = today;
|
||||
|
||||
bumpDaily(s, 'catch', credited);
|
||||
bumpDaily(s, 'golden', creditGolden);
|
||||
|
||||
leveledUp = levelForCaught(newCaught) > oldLevel;
|
||||
premiumUnlocked = Math.floor(newCaught / PREMIUM_MILESTONE) > Math.floor(oldCaught / PREMIUM_MILESTONE);
|
||||
});
|
||||
@@ -377,6 +493,7 @@ export async function killWasp(login: string, now: number = Date.now()): Promise
|
||||
if (s.wasps > 0) {
|
||||
s.wasps -= 1;
|
||||
s.waspsKilled += 1;
|
||||
bumpDaily(s, 'wasp', 1);
|
||||
}
|
||||
});
|
||||
return toDto(mine);
|
||||
@@ -407,24 +524,28 @@ export async function buyRepellent(login: string, now: number = Date.now()): Pro
|
||||
*/
|
||||
export async function reportNetTorn(login: string, now: number = Date.now()): Promise<ButterflyStats> {
|
||||
const mine = await mutateUser(login, now, (s) => {
|
||||
if (s.netTornUntil <= now) s.netTornUntil = now + AUTO_REPAIR_MS;
|
||||
if (s.netTornUntil <= now) s.netTornUntil = now + effectiveAutoRepair(s);
|
||||
});
|
||||
return toDto(mine);
|
||||
}
|
||||
|
||||
/** Zloděj se dostal k penězům a ukradl podíl mincí. */
|
||||
/** Zloděj se dostal k penězům a ukradl podíl mincí (méně, když je aktivní pojistka). */
|
||||
export async function robbery(login: string, now: number = Date.now()): Promise<ButterflyStats> {
|
||||
const mine = await mutateUser(login, now, (s) => {
|
||||
s.coins = Math.round(s.coins * (1 - ROBBERY_FRACTION));
|
||||
const insured = s.insuranceUntil > now;
|
||||
const fraction = insured ? ROBBERY_FRACTION_INSURED : ROBBERY_FRACTION;
|
||||
s.coins = Math.round(s.coins * (1 - fraction));
|
||||
if (insured) s.insuranceUntil = 0; // pojistka se spotřebuje
|
||||
});
|
||||
return toDto(mine);
|
||||
}
|
||||
|
||||
/** Hráč porazil zloděje – malá odměna a statistika. */
|
||||
/** Hráč porazil zloděje – malá odměna, statistika a progres denního úkolu. */
|
||||
export async function defeatThief(login: string, now: number = Date.now()): Promise<ButterflyStats> {
|
||||
const mine = await mutateUser(login, now, (s) => {
|
||||
s.thievesDefeated += 1;
|
||||
s.coins += THIEF_REWARD;
|
||||
bumpDaily(s, 'thief', 1);
|
||||
});
|
||||
return toDto(mine);
|
||||
}
|
||||
@@ -446,6 +567,103 @@ export async function defeatCaterpillar(login: string, now: number = Date.now())
|
||||
return toDto(mine);
|
||||
}
|
||||
|
||||
/** Hráč omylem chytil černou můru – přijde o část úlovků. */
|
||||
export async function mothHit(login: string, now: number = Date.now()): Promise<ButterflyStats> {
|
||||
const mine = await mutateUser(login, now, (s) => {
|
||||
s.caught = Math.max(0, s.caught - MOTH_PENALTY);
|
||||
s.mothsHit += 1;
|
||||
});
|
||||
return toDto(mine);
|
||||
}
|
||||
|
||||
// --- Obchod ------------------------------------------------------------------
|
||||
|
||||
/** Koupí okamžitou aktivaci prémiové síťky (efekt řídí klient). */
|
||||
export async function buyPremium(login: string, now: number = Date.now()): Promise<ButterflyStats> {
|
||||
let failed = false;
|
||||
const mine = await mutateUser(login, now, (s) => {
|
||||
if (s.coins < PREMIUM_BUY_COST) { failed = true; return; }
|
||||
s.coins -= PREMIUM_BUY_COST;
|
||||
});
|
||||
if (failed) throw new InsufficientCoinsError('Nedostatek mincí na prémiovou síťku');
|
||||
return toDto(mine);
|
||||
}
|
||||
|
||||
/** Koupí vosí sprej – vyhubí všechny vosy. */
|
||||
export async function waspSpray(login: string, now: number = Date.now()): Promise<ButterflyStats> {
|
||||
let failed = false;
|
||||
const mine = await mutateUser(login, now, (s) => {
|
||||
if (s.coins < WASP_SPRAY_COST) { failed = true; return; }
|
||||
s.coins -= WASP_SPRAY_COST;
|
||||
s.waspsKilled += s.wasps;
|
||||
s.wasps = 0;
|
||||
});
|
||||
if (failed) throw new InsufficientCoinsError('Nedostatek mincí na vosí sprej');
|
||||
return toDto(mine);
|
||||
}
|
||||
|
||||
/** Koupí pojistku proti zloději (dočasně sníží ztrátu při okradení). */
|
||||
export async function buyInsurance(login: string, now: number = Date.now()): Promise<ButterflyStats> {
|
||||
let failed = false;
|
||||
const mine = await mutateUser(login, now, (s) => {
|
||||
if (s.coins < INSURANCE_COST) { failed = true; return; }
|
||||
s.coins -= INSURANCE_COST;
|
||||
s.insuranceUntil = now + INSURANCE_DURATION_MS;
|
||||
});
|
||||
if (failed) throw new InsufficientCoinsError('Nedostatek mincí na pojistku');
|
||||
return toDto(mine);
|
||||
}
|
||||
|
||||
/** Chyba pro neplatnou/vyprodanou koupi vylepšení. */
|
||||
export class UpgradeError extends Error { }
|
||||
|
||||
/** Koupí další úroveň trvalého vylepšení (cena se s úrovní zdvojnásobuje). */
|
||||
export async function buyUpgrade(login: string, item: UpgradeId, now: number = Date.now()): Promise<ButterflyStats> {
|
||||
if (!(item in UPGRADE_MAX)) throw new UpgradeError('Neznámé vylepšení');
|
||||
let failed = false;
|
||||
let maxed = false;
|
||||
const mine = await mutateUser(login, now, (s) => {
|
||||
const level = s.upgrades[item] ?? 0;
|
||||
if (level >= UPGRADE_MAX[item]) { maxed = true; return; }
|
||||
const cost = upgradeCost(item, level);
|
||||
if (s.coins < cost) { failed = true; return; }
|
||||
s.coins -= cost;
|
||||
s.upgrades[item] = level + 1;
|
||||
});
|
||||
if (maxed) throw new UpgradeError('Vylepšení je na maximální úrovni');
|
||||
if (failed) throw new InsufficientCoinsError('Nedostatek mincí na vylepšení');
|
||||
return toDto(mine);
|
||||
}
|
||||
|
||||
// --- Achievementy (odvozené z aktuálních statistik) --------------------------
|
||||
|
||||
export interface Achievement {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
unlocked: boolean;
|
||||
}
|
||||
|
||||
/** Vrátí seznam achievementů odvozený z aktuálních statistik uživatele. */
|
||||
export async function getAchievements(login: string, now: number = Date.now()): Promise<Achievement[]> {
|
||||
const s = await mutateUser(login, now, () => { /* jen načíst/dorůst */ });
|
||||
const level = levelForCaught(s.caught);
|
||||
const def: Achievement[] = [
|
||||
{ id: 'first-golden', title: 'Zlatý úlovek', description: 'Chyť prvního zlatého motýla', unlocked: s.goldenCaught >= 1 },
|
||||
{ id: 'golden-10', title: 'Zlatokop', description: 'Chyť 10 zlatých motýlů', unlocked: s.goldenCaught >= 10 },
|
||||
{ id: 'catch-500', title: 'Sběratel', description: 'Nachytej 500 motýlů', unlocked: s.caught >= 500 },
|
||||
{ id: 'catch-2000', title: 'Motýlí magnát', description: 'Nachytej 2000 motýlů', unlocked: s.caught >= 2000 },
|
||||
{ id: 'level-10', title: 'Zkušený chytač', description: 'Dosáhni úrovně 10', unlocked: level >= 10 },
|
||||
{ id: 'level-20', title: 'Legenda louky', description: 'Dosáhni úrovně 20', unlocked: level >= 20 },
|
||||
{ id: 'thief-10', title: 'Postrach zlodějů', description: 'Poraz 10 zlodějů', unlocked: s.thievesDefeated >= 10 },
|
||||
{ id: 'caterpillar-10', title: 'Zahradník', description: 'Poraz 10 housenek', unlocked: s.caterpillarsDefeated >= 10 },
|
||||
{ id: 'wasp-50', title: 'Plácačka', description: 'Zabij 50 vos', unlocked: s.waspsKilled >= 50 },
|
||||
{ id: 'bird-25', title: 'Strašák', description: 'Vyplaš 25 ptáků', unlocked: s.birdsScared >= 25 },
|
||||
{ id: 'net-max', title: 'Obří síť', description: 'Vylepši síťku na maximum', unlocked: (s.upgrades?.net ?? 0) >= UPGRADE_MAX.net },
|
||||
];
|
||||
return def;
|
||||
}
|
||||
|
||||
/**
|
||||
* Vrátí žebříček nejlepších chytačů seřazený sestupně dle počtu chycených
|
||||
* (při shodě dle počtu zlatých motýlů).
|
||||
|
||||
@@ -12,10 +12,18 @@ import {
|
||||
defeatThief,
|
||||
caterpillarAte,
|
||||
defeatCaterpillar,
|
||||
mothHit,
|
||||
buyPremium,
|
||||
waspSpray,
|
||||
buyInsurance,
|
||||
buyUpgrade,
|
||||
getAchievements,
|
||||
getLeaderboard,
|
||||
InsufficientCoinsError,
|
||||
UpgradeError,
|
||||
UpgradeId,
|
||||
} from "../butterflies";
|
||||
import { CatchButterfliesData } from "../../../types";
|
||||
import { CatchButterfliesData, BuyUpgradeData } from "../../../types";
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
@@ -112,6 +120,68 @@ router.post("/defeatCaterpillar", async (req: Request, res, next) => {
|
||||
} catch (e: any) { next(e) }
|
||||
});
|
||||
|
||||
router.post("/moth", async (req: Request, res, next) => {
|
||||
try {
|
||||
const login = getLogin(parseToken(req));
|
||||
const data = await mothHit(login);
|
||||
res.status(200).json(data);
|
||||
} catch (e: any) { next(e) }
|
||||
});
|
||||
|
||||
router.post("/buyPremium", async (req: Request, res, next) => {
|
||||
try {
|
||||
const login = getLogin(parseToken(req));
|
||||
const data = await buyPremium(login);
|
||||
res.status(200).json(data);
|
||||
} catch (e: any) {
|
||||
if (e instanceof InsufficientCoinsError) return res.status(402).json({ error: e.message });
|
||||
next(e);
|
||||
}
|
||||
});
|
||||
|
||||
router.post("/waspSpray", async (req: Request, res, next) => {
|
||||
try {
|
||||
const login = getLogin(parseToken(req));
|
||||
const data = await waspSpray(login);
|
||||
res.status(200).json(data);
|
||||
} catch (e: any) {
|
||||
if (e instanceof InsufficientCoinsError) return res.status(402).json({ error: e.message });
|
||||
next(e);
|
||||
}
|
||||
});
|
||||
|
||||
router.post("/insurance", async (req: Request, res, next) => {
|
||||
try {
|
||||
const login = getLogin(parseToken(req));
|
||||
const data = await buyInsurance(login);
|
||||
res.status(200).json(data);
|
||||
} catch (e: any) {
|
||||
if (e instanceof InsufficientCoinsError) return res.status(402).json({ error: e.message });
|
||||
next(e);
|
||||
}
|
||||
});
|
||||
|
||||
router.post("/buyUpgrade", async (req: Request<{}, any, BuyUpgradeData["body"]>, res, next) => {
|
||||
try {
|
||||
const login = getLogin(parseToken(req));
|
||||
if (!req.body?.item) return res.status(400).json({ error: "Chybné parametry volání" });
|
||||
const data = await buyUpgrade(login, req.body.item as UpgradeId);
|
||||
res.status(200).json(data);
|
||||
} catch (e: any) {
|
||||
if (e instanceof InsufficientCoinsError) return res.status(402).json({ error: e.message });
|
||||
if (e instanceof UpgradeError) return res.status(400).json({ error: e.message });
|
||||
next(e);
|
||||
}
|
||||
});
|
||||
|
||||
router.get("/achievements", async (req: Request, res, next) => {
|
||||
try {
|
||||
const login = getLogin(parseToken(req));
|
||||
const data = await getAchievements(login);
|
||||
res.status(200).json(data);
|
||||
} catch (e: any) { next(e) }
|
||||
});
|
||||
|
||||
router.get("/leaderboard", async (req: Request, res, next) => {
|
||||
try {
|
||||
getLogin(parseToken(req));
|
||||
|
||||
@@ -11,6 +11,13 @@ import {
|
||||
defeatThief,
|
||||
caterpillarAte,
|
||||
defeatCaterpillar,
|
||||
mothHit,
|
||||
buyPremium,
|
||||
waspSpray,
|
||||
buyInsurance,
|
||||
buyUpgrade,
|
||||
getAchievements,
|
||||
upgradeCost,
|
||||
getLeaderboard,
|
||||
growPests,
|
||||
levelForCaught,
|
||||
@@ -37,6 +44,14 @@ import {
|
||||
CATERPILLAR_EATS,
|
||||
CATERPILLAR_REWARD,
|
||||
AUTO_REPAIR_MS,
|
||||
MOTH_PENALTY,
|
||||
PREMIUM_BUY_COST,
|
||||
WASP_SPRAY_COST,
|
||||
INSURANCE_COST,
|
||||
INSURANCE_DURATION_MS,
|
||||
ROBBERY_FRACTION_INSURED,
|
||||
UPGRADE_MAX,
|
||||
DAILY_TASK_REWARD,
|
||||
} from '../butterflies';
|
||||
import { formatDate } from '../utils';
|
||||
|
||||
@@ -366,6 +381,95 @@ describe('housenka', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('černé můry', () => {
|
||||
test('chycení můry ubere úlovky (nikdy pod nulu) a zvýší statistiku', async () => {
|
||||
await seed(USER, { caught: MOTH_PENALTY + 4, coins: 0, goldenCaught: 0 });
|
||||
const stats = await mothHit(USER);
|
||||
expect(stats.caught).toBe(4);
|
||||
expect(stats.mothsHit).toBe(1);
|
||||
});
|
||||
test('nesnese caught pod nulu', async () => {
|
||||
await seed(USER, { caught: 2, coins: 0, goldenCaught: 0 });
|
||||
const stats = await mothHit(USER);
|
||||
expect(stats.caught).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('obchod – spotřební', () => {
|
||||
test('buyPremium odečte cenu', async () => {
|
||||
await seed(USER, { caught: 0, coins: PREMIUM_BUY_COST + 5, goldenCaught: 0 });
|
||||
const stats = await buyPremium(USER);
|
||||
expect(stats.coins).toBe(5);
|
||||
});
|
||||
test('buyPremium bez mincí selže', async () => {
|
||||
await seed(USER, { caught: 0, coins: PREMIUM_BUY_COST - 1, goldenCaught: 0 });
|
||||
await expect(buyPremium(USER)).rejects.toThrow(InsufficientCoinsError);
|
||||
});
|
||||
test('waspSpray vyhubí všechny vosy a odečte cenu', async () => {
|
||||
await seed(USER, { caught: 0, coins: WASP_SPRAY_COST, goldenCaught: 0, wasps: 4, lastWaspAt: Date.now() });
|
||||
const stats = await waspSpray(USER);
|
||||
expect(stats.coins).toBe(0);
|
||||
expect(stats.wasps).toBe(0);
|
||||
});
|
||||
test('pojistka sníží ztrátu při okradení a spotřebuje se', async () => {
|
||||
const now = 3_000_000;
|
||||
await seed(USER, { caught: 0, coins: INSURANCE_COST + 100, goldenCaught: 0 });
|
||||
await buyInsurance(USER, now);
|
||||
const robbed = await robbery(USER, now + 1000);
|
||||
// po koupi zbývá 100 mincí; pojistka → ztráta jen ROBBERY_FRACTION_INSURED
|
||||
expect(robbed.coins).toBe(Math.round(100 * (1 - ROBBERY_FRACTION_INSURED)));
|
||||
expect(robbed.insuranceUntil).toBe(0); // spotřebováno
|
||||
});
|
||||
});
|
||||
|
||||
describe('obchod – trvalá vylepšení', () => {
|
||||
test('cena roste s úrovní a vylepšení se aplikuje', async () => {
|
||||
await seed(USER, { caught: 0, coins: upgradeCost('net', 0) + upgradeCost('net', 1) + 5, goldenCaught: 0 });
|
||||
const a = await buyUpgrade(USER, 'net');
|
||||
expect(a.upgrades.net).toBe(1);
|
||||
const b = await buyUpgrade(USER, 'net');
|
||||
expect(b.upgrades.net).toBe(2);
|
||||
expect(b.coins).toBe(5);
|
||||
});
|
||||
test('nelze koupit nad maximum', async () => {
|
||||
await seed(USER, { caught: 0, coins: 1_000_000, goldenCaught: 0, upgrades: { net: UPGRADE_MAX.net, scarecrow: 0, reinforced: 0 } });
|
||||
await expect(buyUpgrade(USER, 'net')).rejects.toThrow();
|
||||
});
|
||||
test('bez mincí koupě selže', async () => {
|
||||
await seed(USER, { caught: 0, coins: 0, goldenCaught: 0 });
|
||||
await expect(buyUpgrade(USER, 'net')).rejects.toThrow(InsufficientCoinsError);
|
||||
});
|
||||
});
|
||||
|
||||
describe('denní úkol', () => {
|
||||
test('progres se plní a po splnění se vyplatí odměna jednou', async () => {
|
||||
const now = 4_000_000;
|
||||
const day = formatDate(new Date(now));
|
||||
await seed(USER, {
|
||||
caught: 0, coins: 0, goldenCaught: 0, wasps: 5, lastWaspAt: now,
|
||||
daily: { day, taskId: 'wasp', progress: 0, target: 2, done: false },
|
||||
});
|
||||
await killWasp(USER, now);
|
||||
let stats = await killWasp(USER, now);
|
||||
expect(stats.daily.done).toBe(true);
|
||||
expect(stats.coins).toBe(DAILY_TASK_REWARD);
|
||||
// další zabití už odměnu nepřidá
|
||||
stats = await killWasp(USER, now);
|
||||
expect(stats.coins).toBe(DAILY_TASK_REWARD);
|
||||
});
|
||||
});
|
||||
|
||||
describe('achievementy', () => {
|
||||
test('odemykají se podle statistik', async () => {
|
||||
await seed(USER, { caught: 600, coins: 0, goldenCaught: 1 });
|
||||
const list = await getAchievements(USER);
|
||||
const byId = Object.fromEntries(list.map(a => [a.id, a.unlocked]));
|
||||
expect(byId['first-golden']).toBe(true);
|
||||
expect(byId['catch-500']).toBe(true);
|
||||
expect(byId['catch-2000']).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getLeaderboard', () => {
|
||||
test('řadí sestupně dle počtu chycených a respektuje limit', async () => {
|
||||
await seed(USER, { caught: 100, coins: 0, goldenCaught: 2 });
|
||||
|
||||
Reference in New Issue
Block a user