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

- 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:
Stánek Pavel
2026-07-27 10:23:09 +02:00
co-authored by Claude Opus 4.8
parent e3f439bde0
commit 252d105408
22 changed files with 1384 additions and 43 deletions
+104
View File
@@ -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 });