feat: rozšíření chytání motýlků o progres, mince a škůdce
CI / Generate TypeScript types (push) Successful in 10s
CI / Server unit tests (push) Successful in 23s
CI / Build server (push) Successful in 27s
CI / Build client (push) Successful in 46s
CI / Playwright E2E tests (push) Successful in 1m25s
CI / Build and push Docker image (push) Successful in 44s
CI / Notify (push) Successful in 2s
CI / Generate TypeScript types (push) Successful in 10s
CI / Server unit tests (push) Successful in 23s
CI / Build server (push) Successful in 27s
CI / Build client (push) Successful in 46s
CI / Playwright E2E tests (push) Successful in 1m25s
CI / Build and push Docker image (push) Successful in 44s
CI / Notify (push) Successful in 2s
- serverová perzistence statistik (mince, úrovně, zlatí motýli) + týmový žebříček - prémiová zlatá síťka za milník (větší, magnetická, odolná proti ptákům) - perzistentní škůdci: vosy na zaklikání a ptáci trhající síťku (F5-proof) - placený plašič ptáků s odpočtem, denní bonus, kombo - WOW zlatý motýl + osobní statistiky v modalu žebříčku - oprava zobrazení ikonek (root-absolutní cesty k public assetům) 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
c8c5ecc60c
commit
c5ca6c9d41
@@ -0,0 +1,263 @@
|
||||
import { resetMemoryStorage } from '../storage/memory';
|
||||
import getStorage from '../storage';
|
||||
import {
|
||||
getStats,
|
||||
recordCatches,
|
||||
repairNet,
|
||||
killWasp,
|
||||
buyRepellent,
|
||||
reportNetTorn,
|
||||
getLeaderboard,
|
||||
growPests,
|
||||
levelForCaught,
|
||||
titleForLevel,
|
||||
comboBonus,
|
||||
InsufficientCoinsError,
|
||||
COIN_BASE,
|
||||
GOLD_VALUE,
|
||||
REPAIR_COST,
|
||||
DAILY_BONUS,
|
||||
PREMIUM_MILESTONE,
|
||||
WASP_MAX,
|
||||
BIRD_MAX,
|
||||
WASP_GROWTH_MS,
|
||||
BIRD_GROWTH_MS,
|
||||
REPELLENT_COST,
|
||||
REPELLENT_DURATION_MS,
|
||||
} from '../butterflies';
|
||||
import { formatDate } from '../utils';
|
||||
|
||||
const USER = 'tomas';
|
||||
const OTHER = 'petr';
|
||||
|
||||
beforeEach(() => {
|
||||
resetMemoryStorage();
|
||||
});
|
||||
|
||||
/** Nastaví uživateli přímo uložené statistiky (simulace zásahu do dat). */
|
||||
async function seed(login: string, data: any) {
|
||||
const storage = getStorage();
|
||||
const all = (await storage.getData<any>('butterflyStats')) ?? {};
|
||||
all[login] = data;
|
||||
await storage.setData('butterflyStats', all);
|
||||
}
|
||||
|
||||
describe('levelForCaught / titleForLevel', () => {
|
||||
test('začíná na úrovni 1', () => {
|
||||
expect(levelForCaught(0)).toBe(1);
|
||||
expect(titleForLevel(1)).toBe('Začátečník se síťkou');
|
||||
});
|
||||
|
||||
test('roste s počtem chycených', () => {
|
||||
expect(levelForCaught(9)).toBe(1);
|
||||
expect(levelForCaught(10)).toBe(2);
|
||||
expect(levelForCaught(30)).toBe(3);
|
||||
expect(levelForCaught(10000)).toBe(8);
|
||||
});
|
||||
});
|
||||
|
||||
describe('comboBonus', () => {
|
||||
test('malá dávka nedává kombo', () => {
|
||||
expect(comboBonus(1)).toBe(0);
|
||||
expect(comboBonus(2)).toBe(0);
|
||||
});
|
||||
|
||||
test('větší dávka dává rostoucí bonus se stropem', () => {
|
||||
expect(comboBonus(3)).toBe(1);
|
||||
expect(comboBonus(4)).toBe(2);
|
||||
expect(comboBonus(1000)).toBe(20); // COMBO_CAP
|
||||
});
|
||||
});
|
||||
|
||||
describe('getStats', () => {
|
||||
test('pro nového uživatele vrací výchozí prázdné statistiky', async () => {
|
||||
const stats = await getStats(USER);
|
||||
expect(stats.caught).toBe(0);
|
||||
expect(stats.coins).toBe(0);
|
||||
expect(stats.goldenCaught).toBe(0);
|
||||
expect(stats.level).toBe(1);
|
||||
expect(stats.title).toBe('Začátečník se síťkou');
|
||||
});
|
||||
});
|
||||
|
||||
describe('recordCatches – mince', () => {
|
||||
test('běžný motýl dá základní minci', async () => {
|
||||
const result = await recordCatches(USER, 1, 0);
|
||||
// 1 běžný + první úlovek dne => denní bonus
|
||||
expect(result.coinsAwarded).toBe(COIN_BASE + DAILY_BONUS);
|
||||
expect(result.dailyBonusApplied).toBe(true);
|
||||
expect(result.stats.caught).toBe(1);
|
||||
expect(result.stats.coins).toBe(COIN_BASE + DAILY_BONUS);
|
||||
});
|
||||
|
||||
test('zlatý motýl má vyšší hodnotu a počítá se zvlášť', async () => {
|
||||
// předvyplníme dnešní den, aby nezasáhl denní bonus
|
||||
await seed(USER, { caught: 5, coins: 0, goldenCaught: 0, lastCatchDay: formatDate(new Date()) });
|
||||
const result = await recordCatches(USER, 0, 1);
|
||||
expect(result.coinsAwarded).toBe(GOLD_VALUE);
|
||||
expect(result.dailyBonusApplied).toBe(false);
|
||||
expect(result.stats.goldenCaught).toBe(1);
|
||||
expect(result.stats.caught).toBe(6);
|
||||
});
|
||||
|
||||
test('kombo se připočítá u větší dávky', async () => {
|
||||
await seed(USER, { caught: 5, coins: 0, goldenCaught: 0, lastCatchDay: formatDate(new Date()) });
|
||||
const result = await recordCatches(USER, 4, 0);
|
||||
// 4 * COIN_BASE + comboBonus(4)=2
|
||||
expect(result.coinsAwarded).toBe(4 * COIN_BASE + 2);
|
||||
});
|
||||
|
||||
test('sanity limit ořízne absurdní dávku a odmítne záporné', async () => {
|
||||
const result = await recordCatches(USER, 100000, -5);
|
||||
expect(result.stats.caught).toBe(100); // MAX_BATCH
|
||||
});
|
||||
});
|
||||
|
||||
describe('recordCatches – denní bonus', () => {
|
||||
test('denní bonus se připíše jen jednou za den', async () => {
|
||||
const first = await recordCatches(USER, 1, 0);
|
||||
expect(first.dailyBonusApplied).toBe(true);
|
||||
const second = await recordCatches(USER, 1, 0);
|
||||
expect(second.dailyBonusApplied).toBe(false);
|
||||
expect(second.coinsAwarded).toBe(COIN_BASE);
|
||||
});
|
||||
|
||||
test('bonus se znovu připíše po změně dne', async () => {
|
||||
await seed(USER, { caught: 3, coins: 3, goldenCaught: 0, lastCatchDay: '2000-01-01' });
|
||||
const result = await recordCatches(USER, 1, 0);
|
||||
expect(result.dailyBonusApplied).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('recordCatches – progres a milníky', () => {
|
||||
test('leveledUp se nastaví při překročení hranice úrovně', async () => {
|
||||
await seed(USER, { caught: 9, coins: 0, goldenCaught: 0, lastCatchDay: formatDate(new Date()) });
|
||||
const result = await recordCatches(USER, 1, 0); // 9 -> 10 => úroveň 2
|
||||
expect(result.leveledUp).toBe(true);
|
||||
expect(result.stats.level).toBe(2);
|
||||
});
|
||||
|
||||
test('premiumUnlocked se nastaví při překročení milníku', async () => {
|
||||
await seed(USER, {
|
||||
caught: PREMIUM_MILESTONE - 1,
|
||||
coins: 0,
|
||||
goldenCaught: 0,
|
||||
lastCatchDay: formatDate(new Date()),
|
||||
});
|
||||
const result = await recordCatches(USER, 1, 0);
|
||||
expect(result.premiumUnlocked).toBe(true);
|
||||
});
|
||||
|
||||
test('bez překročení milníku premiumUnlocked zůstane false', async () => {
|
||||
await seed(USER, { caught: 5, coins: 0, goldenCaught: 0, lastCatchDay: formatDate(new Date()) });
|
||||
const result = await recordCatches(USER, 1, 0);
|
||||
expect(result.premiumUnlocked).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('repairNet', () => {
|
||||
test('odečte cenu opravy při dostatku mincí a zruší protržení', async () => {
|
||||
await seed(USER, { caught: 20, coins: REPAIR_COST + 5, goldenCaught: 0, netTorn: true });
|
||||
const stats = await repairNet(USER);
|
||||
expect(stats.coins).toBe(5);
|
||||
expect(stats.netTorn).toBe(false);
|
||||
});
|
||||
|
||||
test('vyhodí chybu při nedostatku mincí', async () => {
|
||||
await seed(USER, { caught: 1, coins: REPAIR_COST - 1, goldenCaught: 0, netTorn: true });
|
||||
await expect(repairNet(USER)).rejects.toThrow(InsufficientCoinsError);
|
||||
// mince zůstanou nezměněné a síťka stále protržená
|
||||
const stats = await getStats(USER);
|
||||
expect(stats.coins).toBe(REPAIR_COST - 1);
|
||||
expect(stats.netTorn).toBe(true);
|
||||
});
|
||||
|
||||
test('neúčtuje, když síťka není protržená', async () => {
|
||||
await seed(USER, { caught: 1, coins: 50, goldenCaught: 0, netTorn: false });
|
||||
const stats = await repairNet(USER);
|
||||
expect(stats.coins).toBe(50);
|
||||
});
|
||||
});
|
||||
|
||||
describe('growPests', () => {
|
||||
test('vosy přibývají v čase do maxima', () => {
|
||||
const s: any = { wasps: 0, birds: 0, repellentUntil: 0, lastWaspAt: 0, lastBirdAt: 0 };
|
||||
growPests(s, 2 * WASP_GROWTH_MS + 100);
|
||||
expect(s.wasps).toBe(2);
|
||||
growPests(s, 1_000_000 * WASP_GROWTH_MS);
|
||||
expect(s.wasps).toBe(WASP_MAX);
|
||||
});
|
||||
|
||||
test('zachovává rozpracovaný čas (dvě poloviční okna = jedna vosa)', () => {
|
||||
const s: any = { wasps: 0, birds: 0, repellentUntil: 0, lastWaspAt: 0, lastBirdAt: 0 };
|
||||
growPests(s, Math.floor(WASP_GROWTH_MS * 0.6));
|
||||
expect(s.wasps).toBe(0);
|
||||
growPests(s, Math.floor(WASP_GROWTH_MS * 1.2));
|
||||
expect(s.wasps).toBe(1);
|
||||
});
|
||||
|
||||
test('ptáci nepřibývají při aktivním plašiči', () => {
|
||||
const now = 1_000_000;
|
||||
const s: any = { wasps: 0, birds: 0, repellentUntil: now + 10 * BIRD_GROWTH_MS, lastWaspAt: now, lastBirdAt: now };
|
||||
growPests(s, now + 3 * BIRD_GROWTH_MS);
|
||||
expect(s.birds).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('killWasp', () => {
|
||||
test('sníží počet vos a zvýší statistiku zahubených', async () => {
|
||||
await seed(USER, { caught: 0, coins: 0, goldenCaught: 0, wasps: 3, lastWaspAt: Date.now() });
|
||||
const stats = await killWasp(USER);
|
||||
expect(stats.wasps).toBe(2);
|
||||
expect(stats.waspsKilled).toBe(1);
|
||||
});
|
||||
|
||||
test('při nula vosách nic nepokazí', async () => {
|
||||
await seed(USER, { caught: 0, coins: 0, goldenCaught: 0, wasps: 0, lastWaspAt: Date.now() });
|
||||
const stats = await killWasp(USER);
|
||||
expect(stats.wasps).toBe(0);
|
||||
expect(stats.waspsKilled).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('buyRepellent', () => {
|
||||
test('vyžene ptáky, aktivuje plašič a odečte mince', async () => {
|
||||
const now = 5_000_000;
|
||||
await seed(USER, { caught: 0, coins: REPELLENT_COST + 3, goldenCaught: 0, birds: 3, lastBirdAt: now });
|
||||
const stats = await buyRepellent(USER, now);
|
||||
expect(stats.coins).toBe(3);
|
||||
expect(stats.birds).toBe(0);
|
||||
expect(stats.birdsScared).toBe(3);
|
||||
expect(stats.repellentUntil).toBe(now + REPELLENT_DURATION_MS);
|
||||
});
|
||||
|
||||
test('vyhodí chybu při nedostatku mincí', async () => {
|
||||
await seed(USER, { caught: 0, coins: REPELLENT_COST - 1, goldenCaught: 0, birds: 2 });
|
||||
await expect(buyRepellent(USER)).rejects.toThrow(InsufficientCoinsError);
|
||||
});
|
||||
});
|
||||
|
||||
describe('reportNetTorn', () => {
|
||||
test('nastaví protrženou síťku (perzistentně)', async () => {
|
||||
await seed(USER, { caught: 0, coins: 0, goldenCaught: 0 });
|
||||
const stats = await reportNetTorn(USER);
|
||||
expect(stats.netTorn).toBe(true);
|
||||
// stav přežije další čtení (F5)
|
||||
expect((await getStats(USER)).netTorn).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getLeaderboard', () => {
|
||||
test('řadí sestupně dle počtu chycených a respektuje limit', async () => {
|
||||
await seed(USER, { caught: 100, coins: 0, goldenCaught: 2 });
|
||||
await seed(OTHER, { caught: 200, coins: 0, goldenCaught: 0 });
|
||||
await seed('jana', { caught: 50, coins: 0, goldenCaught: 10 });
|
||||
|
||||
const all = await getLeaderboard(10);
|
||||
expect(all.map(e => e.login)).toEqual([OTHER, USER, 'jana']);
|
||||
|
||||
const top = await getLeaderboard(2);
|
||||
expect(top).toHaveLength(2);
|
||||
expect(top[0].login).toBe(OTHER);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user