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
+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ů).
|
||||
|
||||
Reference in New Issue
Block a user