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,18 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="100" height="100">
|
||||||
|
<!-- Netopýr (čelem doprava; JS natáčí dle směru letu) -->
|
||||||
|
<g fill="#2B2733" stroke="#15121A" stroke-width="1.5" stroke-linejoin="round">
|
||||||
|
<!-- křídla -->
|
||||||
|
<path d="M50,52 C34,30 18,30 6,40 C16,42 18,52 14,62 C24,54 34,52 44,58 Z"/>
|
||||||
|
<path d="M50,52 C66,30 82,30 94,40 C84,42 82,52 86,62 C76,54 66,52 56,58 Z"/>
|
||||||
|
<!-- tělo -->
|
||||||
|
<ellipse cx="50" cy="56" rx="9" ry="13"/>
|
||||||
|
<!-- uši -->
|
||||||
|
<path d="M44,44 L40,32 L48,42 Z"/>
|
||||||
|
<path d="M56,44 L60,32 L52,42 Z"/>
|
||||||
|
</g>
|
||||||
|
<!-- oči -->
|
||||||
|
<g fill="#F5C518">
|
||||||
|
<circle cx="46" cy="50" r="1.8"/>
|
||||||
|
<circle cx="54" cy="50" r="1.8"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 716 B |
@@ -0,0 +1,34 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="100" height="100">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="mothWing" x1="0" y1="0" x2="0.6" y2="1">
|
||||||
|
<stop offset="0%" stop-color="#5A5560"/>
|
||||||
|
<stop offset="60%" stop-color="#3A3540"/>
|
||||||
|
<stop offset="100%" stop-color="#211E26"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<g stroke="#15121A" stroke-width="1.5" stroke-linejoin="round">
|
||||||
|
<path fill="url(#mothWing)" d="M48,34 C32,16 10,20 10,36 C10,50 30,50 48,48 Z"/>
|
||||||
|
<path fill="url(#mothWing)" d="M52,34 C68,16 90,20 90,36 C90,50 70,50 52,48 Z"/>
|
||||||
|
<path fill="url(#mothWing)" d="M47,49 C35,52 20,56 22,70 C24,80 42,78 47,62 Z"/>
|
||||||
|
<path fill="url(#mothWing)" d="M53,49 C65,52 80,56 78,70 C76,80 58,78 53,62 Z"/>
|
||||||
|
</g>
|
||||||
|
<!-- zlověstné oční skvrny -->
|
||||||
|
<g fill="#C9302C" stroke="#3a0d0b" stroke-width="1">
|
||||||
|
<circle cx="24" cy="34" r="4"/>
|
||||||
|
<circle cx="76" cy="34" r="4"/>
|
||||||
|
</g>
|
||||||
|
<g fill="#1a0605">
|
||||||
|
<circle cx="24" cy="34" r="1.6"/>
|
||||||
|
<circle cx="76" cy="34" r="1.6"/>
|
||||||
|
</g>
|
||||||
|
<!-- huňatá tykadla -->
|
||||||
|
<g stroke="#15121A" stroke-width="1.8" fill="none" stroke-linecap="round">
|
||||||
|
<path d="M49,30 C44,20 39,16 34,14"/>
|
||||||
|
<path d="M51,30 C56,20 61,16 66,14"/>
|
||||||
|
</g>
|
||||||
|
<!-- tělo -->
|
||||||
|
<g fill="#15121A" stroke="none">
|
||||||
|
<rect x="47" y="26" width="6" height="46" rx="3"/>
|
||||||
|
<circle cx="50" cy="26" r="5"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1,27 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="100" height="100">
|
||||||
|
<!-- vlákno -->
|
||||||
|
<line x1="50" y1="0" x2="50" y2="30" stroke="#8A8A8A" stroke-width="1.5"/>
|
||||||
|
<g stroke="#15121A" stroke-width="3" fill="none" stroke-linecap="round">
|
||||||
|
<!-- nohy -->
|
||||||
|
<path d="M40,52 C24,44 18,52 12,46"/>
|
||||||
|
<path d="M40,58 C24,58 16,66 10,64"/>
|
||||||
|
<path d="M40,64 C26,70 20,80 12,82"/>
|
||||||
|
<path d="M40,70 C30,80 28,90 22,94"/>
|
||||||
|
<path d="M60,52 C76,44 82,52 88,46"/>
|
||||||
|
<path d="M60,58 C76,58 84,66 90,64"/>
|
||||||
|
<path d="M60,64 C74,70 80,80 88,82"/>
|
||||||
|
<path d="M60,70 C70,80 72,90 78,94"/>
|
||||||
|
</g>
|
||||||
|
<!-- tělo -->
|
||||||
|
<g stroke="#15121A" stroke-width="1.5">
|
||||||
|
<ellipse cx="50" cy="46" rx="11" ry="9" fill="#2B2733"/>
|
||||||
|
<ellipse cx="50" cy="64" rx="15" ry="16" fill="#211E26"/>
|
||||||
|
<!-- přesýpací hodiny (varovná značka) -->
|
||||||
|
<path d="M50,58 l5,6 l-5,6 l-5,-6 z" fill="#C9302C" stroke="none"/>
|
||||||
|
</g>
|
||||||
|
<!-- oči -->
|
||||||
|
<g fill="#F5C518">
|
||||||
|
<circle cx="46" cy="44" r="1.6"/>
|
||||||
|
<circle cx="54" cy="44" r="1.6"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -73,6 +73,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Černá můra – zlověstný tmavý nádech
|
||||||
|
.butterfly-sprite.moth {
|
||||||
|
filter: drop-shadow(0 0 4px rgba(120, 60, 160, 0.6)) drop-shadow(0 1px 1px rgba(0, 0, 0, 0.4));
|
||||||
|
animation: butterfly-flap 0.5s ease-in-out infinite, butterfly-moth-pulse 1.6s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes butterfly-moth-pulse {
|
||||||
|
0%, 100% { filter: drop-shadow(0 0 4px rgba(120, 60, 160, 0.5)) drop-shadow(0 1px 1px rgba(0, 0, 0, 0.4)); }
|
||||||
|
50% { filter: drop-shadow(0 0 9px rgba(150, 40, 40, 0.8)) drop-shadow(0 1px 1px rgba(0, 0, 0, 0.4)); }
|
||||||
|
}
|
||||||
|
|
||||||
// --- Havěť (ptáci a vosy) ----------------------------------------------------
|
// --- Havěť (ptáci a vosy) ----------------------------------------------------
|
||||||
|
|
||||||
.butterfly-critter {
|
.butterfly-critter {
|
||||||
@@ -93,6 +104,42 @@
|
|||||||
cursor: crosshair;
|
cursor: crosshair;
|
||||||
touch-action: none;
|
touch-action: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Netopýr jde zahnat klikáním
|
||||||
|
&.bat {
|
||||||
|
pointer-events: auto;
|
||||||
|
cursor: crosshair;
|
||||||
|
touch-action: none;
|
||||||
|
z-index: 5;
|
||||||
|
filter: drop-shadow(0 0 6px rgba(60, 40, 90, 0.8));
|
||||||
|
}
|
||||||
|
|
||||||
|
&.hit {
|
||||||
|
animation: butterfly-stalker-hit 0.18s ease-out;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pavučina přes síťku – blokuje chytání, dokud ji hráč nestrhne
|
||||||
|
.butterfly-web {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 120px;
|
||||||
|
height: 120px;
|
||||||
|
background-image: url(/spider.svg);
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center;
|
||||||
|
background-size: contain;
|
||||||
|
pointer-events: auto;
|
||||||
|
cursor: crosshair;
|
||||||
|
touch-action: none;
|
||||||
|
z-index: 6;
|
||||||
|
opacity: 0.9;
|
||||||
|
filter: drop-shadow(0 0 4px rgba(0, 0, 0, 0.4));
|
||||||
|
|
||||||
|
&.hit {
|
||||||
|
animation: butterfly-stalker-hit 0.15s ease-out;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Síťka na chytání --------------------------------------------------------
|
// --- Síťka na chytání --------------------------------------------------------
|
||||||
@@ -139,6 +186,11 @@
|
|||||||
animation: butterfly-net-stung 0.4s ease-in-out;
|
animation: butterfly-net-stung 0.4s ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Záblesk při chycení černé můry (chyba)
|
||||||
|
&.moth-hit {
|
||||||
|
animation: butterfly-net-moth 0.4s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
// Omráčená síťka (po žihnutí vosou) – po celou dobu, kdy nejde chytat.
|
// Omráčená síťka (po žihnutí vosou) – po celou dobu, kdy nejde chytat.
|
||||||
// Má přednost před grabbed kurzorem.
|
// Má přednost před grabbed kurzorem.
|
||||||
&.stunned {
|
&.stunned {
|
||||||
@@ -215,6 +267,17 @@
|
|||||||
font-size: 1.35rem;
|
font-size: 1.35rem;
|
||||||
text-shadow: 0 0 6px rgba(255, 220, 100, 0.9), 0 1px 2px rgba(0, 0, 0, 0.3);
|
text-shadow: 0 0 6px rgba(255, 220, 100, 0.9), 0 1px 2px rgba(0, 0, 0, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.moth {
|
||||||
|
color: #c92a2a;
|
||||||
|
font-size: 1.35rem;
|
||||||
|
text-shadow: 0 0 6px rgba(120, 40, 160, 0.7), 0 1px 2px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes butterfly-net-moth {
|
||||||
|
0%, 100% { filter: drop-shadow(0 2px 3px rgba(0, 0, 0, 0.25)) brightness(1); }
|
||||||
|
30% { filter: drop-shadow(0 0 9px rgba(150, 40, 160, 0.9)) brightness(0.8); }
|
||||||
}
|
}
|
||||||
|
|
||||||
.butterfly-tear-fx {
|
.butterfly-tear-fx {
|
||||||
@@ -255,6 +318,24 @@
|
|||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.butterfly-shop-open {
|
||||||
|
pointer-events: auto;
|
||||||
|
align-self: flex-start;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: #f59e1e;
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2);
|
||||||
|
transition: background 0.15s ease, transform 0.08s ease;
|
||||||
|
|
||||||
|
&:hover { background: #e8890a; }
|
||||||
|
&:active { transform: scale(0.96); }
|
||||||
|
}
|
||||||
|
|
||||||
.butterfly-counter {
|
.butterfly-counter {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React, { useEffect, useRef, useCallback, useState } from 'react';
|
import React, { useEffect, useRef, useCallback, useState } from 'react';
|
||||||
import { useButterflyStats, RewardEvent } from './hooks/useButterflyStats';
|
import { useButterflyStats, RewardEvent } from './hooks/useButterflyStats';
|
||||||
import ButterflyLeaderboardModal from './components/modals/ButterflyLeaderboardModal';
|
import ButterflyLeaderboardModal from './components/modals/ButterflyLeaderboardModal';
|
||||||
|
import ButterflyShopModal from './components/modals/ButterflyShopModal';
|
||||||
|
|
||||||
// Barevné varianty motýlů (soubory v public/, referencované root-absolutně)
|
// Barevné varianty motýlů (soubory v public/, referencované root-absolutně)
|
||||||
const BUTTERFLY_VARIANTS = [
|
const BUTTERFLY_VARIANTS = [
|
||||||
@@ -12,6 +13,8 @@ const BUTTERFLY_VARIANTS = [
|
|||||||
|
|
||||||
const GOLDEN_VARIANT = '/butterfly-golden.svg';
|
const GOLDEN_VARIANT = '/butterfly-golden.svg';
|
||||||
const GOLDEN_CHANCE = 0.015; // ~1,5 % motýlů je zlatých (vzácnější)
|
const GOLDEN_CHANCE = 0.015; // ~1,5 % motýlů je zlatých (vzácnější)
|
||||||
|
const MOTH_VARIANT = '/butterfly-moth.svg';
|
||||||
|
const MOTH_CHANCE = 0.14; // ~14 % je černá můra – chytit ji je chyba
|
||||||
|
|
||||||
const NET_NORMAL = '/butterfly-net.svg';
|
const NET_NORMAL = '/butterfly-net.svg';
|
||||||
const NET_GOLDEN = '/butterfly-net-golden.svg';
|
const NET_GOLDEN = '/butterfly-net-golden.svg';
|
||||||
@@ -20,8 +23,11 @@ const NET_TORN = '/butterfly-net-torn.svg';
|
|||||||
// Herní konstanty (zrcadlí serverové hodnoty v server/src/butterflies.ts)
|
// Herní konstanty (zrcadlí serverové hodnoty v server/src/butterflies.ts)
|
||||||
const PREMIUM_DURATION_MS = 60_000;
|
const PREMIUM_DURATION_MS = 60_000;
|
||||||
export const REPAIR_COST = 20;
|
export const REPAIR_COST = 20;
|
||||||
export const REPELLENT_COST = 60;
|
export const REPELLENT_COST = 70;
|
||||||
const GOLD_VALUE = 25;
|
const GOLD_VALUE = 25;
|
||||||
|
const MOTH_PENALTY = 15;
|
||||||
|
/** Přírůstek dosahu chytání za úroveň vylepšení „větší síťka" */
|
||||||
|
const NET_UPGRADE_RADIUS = 9;
|
||||||
|
|
||||||
// „Bossové" (zloděj / housenka)
|
// „Bossové" (zloděj / housenka)
|
||||||
const THIEF_HP = 22;
|
const THIEF_HP = 22;
|
||||||
@@ -43,10 +49,23 @@ interface ButterflyData {
|
|||||||
x: number; y: number;
|
x: number; y: number;
|
||||||
dir: number; speed: number; size: number;
|
dir: number; speed: number; size: number;
|
||||||
golden: boolean;
|
golden: boolean;
|
||||||
|
/** Černá můra – chycení stojí úlovky (nepočítá se jako úlovek) */
|
||||||
|
moth: boolean;
|
||||||
bobFreq1: number; bobPhase1: number; bobAmp1: number;
|
bobFreq1: number; bobPhase1: number; bobAmp1: number;
|
||||||
bobFreq2: number; bobPhase2: number; bobAmp2: number;
|
bobFreq2: number; bobPhase2: number; bobAmp2: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Netopýr v noci – loví poletující motýly, odháníš klikáním. */
|
||||||
|
interface BatData {
|
||||||
|
el: HTMLDivElement;
|
||||||
|
x: number; y: number;
|
||||||
|
dir: number; speed: number; size: number;
|
||||||
|
bobPhase: number; bobAmp: number;
|
||||||
|
hp: number;
|
||||||
|
nextEatAt: number;
|
||||||
|
cleanup: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
/** Létající havěť (pták nebo vosa) – nalétává na síťku. */
|
/** Létající havěť (pták nebo vosa) – nalétává na síťku. */
|
||||||
interface CritterData {
|
interface CritterData {
|
||||||
el: HTMLDivElement;
|
el: HTMLDivElement;
|
||||||
@@ -79,15 +98,19 @@ interface SceneCallbacks {
|
|||||||
onCatch: (golden: boolean) => void;
|
onCatch: (golden: boolean) => void;
|
||||||
getCoins: () => number;
|
getCoins: () => number;
|
||||||
getCaught: () => number;
|
getCaught: () => number;
|
||||||
|
getNetUpgrade: () => number;
|
||||||
onTornChange: (torn: boolean) => void;
|
onTornChange: (torn: boolean) => void;
|
||||||
onCombo: (count: number) => void;
|
onCombo: (count: number) => void;
|
||||||
onSting: () => void;
|
onSting: () => void;
|
||||||
onSwatWasp: () => void;
|
onSwatWasp: () => void;
|
||||||
|
onMothCaught: () => void;
|
||||||
onRobbery: () => void;
|
onRobbery: () => void;
|
||||||
onDefeatThief: () => void;
|
onDefeatThief: () => void;
|
||||||
onCaterpillarAte: () => void;
|
onCaterpillarAte: () => void;
|
||||||
onDefeatCaterpillar: () => void;
|
onDefeatCaterpillar: () => void;
|
||||||
onStalkerAppear: (type: 'thief' | 'caterpillar') => void;
|
onStalkerAppear: (type: 'thief' | 'caterpillar') => void;
|
||||||
|
onBatShooed: () => void;
|
||||||
|
onWebCleared: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FlyingButterfliesProps {
|
interface FlyingButterfliesProps {
|
||||||
@@ -118,6 +141,7 @@ class ButterflyScene {
|
|||||||
private netX: number = 0;
|
private netX: number = 0;
|
||||||
private netY: number = 0;
|
private netY: number = 0;
|
||||||
private cb: SceneCallbacks;
|
private cb: SceneCallbacks;
|
||||||
|
private handleVisibility: () => void;
|
||||||
|
|
||||||
private torn: boolean = false;
|
private torn: boolean = false;
|
||||||
private premiumUntil: number = 0;
|
private premiumUntil: number = 0;
|
||||||
@@ -126,6 +150,13 @@ class ButterflyScene {
|
|||||||
private nextCatchAt: number = 0;
|
private nextCatchAt: number = 0;
|
||||||
private nextStalkerAt: number = 1800; // první možný boss ~30 s
|
private nextStalkerAt: number = 1800; // první možný boss ~30 s
|
||||||
|
|
||||||
|
// Netopýr (v noci) a pavučina
|
||||||
|
private bat: BatData | null = null;
|
||||||
|
private nextBatAt: number = 1200;
|
||||||
|
private web: HTMLDivElement | null = null;
|
||||||
|
private webClicks: number = 0;
|
||||||
|
private nextSpiderAt: number = 2400;
|
||||||
|
|
||||||
private comboCount: number = 0;
|
private comboCount: number = 0;
|
||||||
private lastCatchTs: number = 0;
|
private lastCatchTs: number = 0;
|
||||||
private comboTimer: number = 0;
|
private comboTimer: number = 0;
|
||||||
@@ -152,6 +183,15 @@ class ButterflyScene {
|
|||||||
private static readonly STALKER_MIN_GAP = 3600; // 60 s
|
private static readonly STALKER_MIN_GAP = 3600; // 60 s
|
||||||
private static readonly STALKER_MAX_GAP = 9000; // 150 s
|
private static readonly STALKER_MAX_GAP = 9000; // 150 s
|
||||||
|
|
||||||
|
private static readonly BAT_SIZE = 60;
|
||||||
|
private static readonly BAT_HP = 3;
|
||||||
|
private static readonly BAT_EAT_INTERVAL = 150; // ~2,5 s mezi sežráním motýla
|
||||||
|
private static readonly BAT_MIN_GAP = 2400;
|
||||||
|
private static readonly BAT_MAX_GAP = 6000;
|
||||||
|
private static readonly WEB_CLICKS = 5; // kolik kliknutí strhne pavučinu
|
||||||
|
private static readonly SPIDER_MIN_GAP = 4200;
|
||||||
|
private static readonly SPIDER_MAX_GAP = 9000;
|
||||||
|
|
||||||
constructor(el: HTMLElement, numButterflies: number, variants: readonly string[], netEnabled: boolean, cb: SceneCallbacks) {
|
constructor(el: HTMLElement, numButterflies: number, variants: readonly string[], netEnabled: boolean, cb: SceneCallbacks) {
|
||||||
this.viewport = el;
|
this.viewport = el;
|
||||||
this.world = document.createElement('div');
|
this.world = document.createElement('div');
|
||||||
@@ -166,11 +206,22 @@ class ButterflyScene {
|
|||||||
this.width = this.viewport.offsetWidth;
|
this.width = this.viewport.offsetWidth;
|
||||||
this.height = this.viewport.offsetHeight;
|
this.height = this.viewport.offsetHeight;
|
||||||
};
|
};
|
||||||
|
// Pozastavení animační smyčky, když není záložka vidět (šetří CPU)
|
||||||
|
this.handleVisibility = () => {
|
||||||
|
if (document.hidden) {
|
||||||
|
if (this.animationId) { cancelAnimationFrame(this.animationId); this.animationId = null; }
|
||||||
|
} else if (this.animationId === null) {
|
||||||
|
this.animationId = requestAnimationFrame(this.render);
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private get premiumActive(): boolean { return Date.now() < this.premiumUntil; }
|
private get premiumActive(): boolean { return Date.now() < this.premiumUntil; }
|
||||||
private get netSize(): number { return this.premiumActive ? ButterflyScene.NET_SIZE_PREMIUM : ButterflyScene.NET_SIZE_NORMAL; }
|
private get netSize(): number { return this.premiumActive ? ButterflyScene.NET_SIZE_PREMIUM : ButterflyScene.NET_SIZE_NORMAL; }
|
||||||
private get catchRadius(): number { return this.premiumActive ? ButterflyScene.CATCH_RADIUS_PREMIUM : ButterflyScene.CATCH_RADIUS_NORMAL; }
|
private get catchRadius(): number {
|
||||||
|
const base = this.premiumActive ? ButterflyScene.CATCH_RADIUS_PREMIUM : ButterflyScene.CATCH_RADIUS_NORMAL;
|
||||||
|
return base + NET_UPGRADE_RADIUS * this.cb.getNetUpgrade();
|
||||||
|
}
|
||||||
private get hoopOffsetX(): number { return this.netSize * ButterflyScene.HOOP_FRAC_X; }
|
private get hoopOffsetX(): number { return this.netSize * ButterflyScene.HOOP_FRAC_X; }
|
||||||
private get hoopOffsetY(): number { return this.netSize * ButterflyScene.HOOP_FRAC_Y; }
|
private get hoopOffsetY(): number { return this.netSize * ButterflyScene.HOOP_FRAC_Y; }
|
||||||
|
|
||||||
@@ -189,14 +240,23 @@ class ButterflyScene {
|
|||||||
b.bobPhase2 = Math.random() * Math.PI * 2;
|
b.bobPhase2 = Math.random() * Math.PI * 2;
|
||||||
b.bobAmp2 = Math.random() * 0.6 + 0.4;
|
b.bobAmp2 = Math.random() * 0.6 + 0.4;
|
||||||
|
|
||||||
b.golden = Math.random() < GOLDEN_CHANCE;
|
// Nejdřív můra, jinak vzácně zlatý, jinak běžná varianta
|
||||||
if (b.golden) {
|
b.moth = Math.random() < MOTH_CHANCE;
|
||||||
|
b.golden = !b.moth && Math.random() < GOLDEN_CHANCE;
|
||||||
|
if (b.moth) {
|
||||||
|
b.sprite.style.backgroundImage = `url(${MOTH_VARIANT})`;
|
||||||
|
b.sprite.classList.remove('golden');
|
||||||
|
b.sprite.classList.add('moth');
|
||||||
|
b.size = Math.random() * 0.3 + 0.9;
|
||||||
|
} else if (b.golden) {
|
||||||
b.sprite.style.backgroundImage = `url(${GOLDEN_VARIANT})`;
|
b.sprite.style.backgroundImage = `url(${GOLDEN_VARIANT})`;
|
||||||
|
b.sprite.classList.remove('moth');
|
||||||
b.sprite.classList.add('golden');
|
b.sprite.classList.add('golden');
|
||||||
b.size = Math.random() * 0.5 + 1.0;
|
b.size = Math.random() * 0.5 + 1.0;
|
||||||
} else {
|
} else {
|
||||||
b.sprite.style.backgroundImage = `url(${this.variants[Math.floor(Math.random() * this.variants.length)]})`;
|
b.sprite.style.backgroundImage = `url(${this.variants[Math.floor(Math.random() * this.variants.length)]})`;
|
||||||
b.sprite.classList.remove('golden');
|
b.sprite.classList.remove('golden');
|
||||||
|
b.sprite.classList.remove('moth');
|
||||||
}
|
}
|
||||||
b.sprite.style.animationDuration = `${Math.random() * 0.25 + 0.3}s`;
|
b.sprite.style.animationDuration = `${Math.random() * 0.25 + 0.3}s`;
|
||||||
};
|
};
|
||||||
@@ -267,11 +327,28 @@ class ButterflyScene {
|
|||||||
this.net.classList.add('catch-pop');
|
this.net.classList.add('catch-pop');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Zpracuje kontakt síťky s letícím tvorem – buď úlovek, nebo penalta za můru. */
|
||||||
|
private catchOne = (b: ButterflyData, atX: number, atY: number): void => {
|
||||||
|
this.nextCatchAt = this.timer + ButterflyScene.CATCH_COOLDOWN;
|
||||||
|
if (b.moth) {
|
||||||
|
// Chytit černou můru je chyba – penalta, žádné kombo
|
||||||
|
this.cb.onMothCaught();
|
||||||
|
this.spawnFx(`−${MOTH_PENALTY}`, 'butterfly-catch-fx moth', atX, atY);
|
||||||
|
this.net.classList.remove('moth-hit');
|
||||||
|
void this.net.offsetWidth;
|
||||||
|
this.net.classList.add('moth-hit');
|
||||||
|
this.comboCount = 0;
|
||||||
|
this.cb.onCombo(0);
|
||||||
|
this.resetButterfly(b);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.registerCatch(b, atX, atY);
|
||||||
|
};
|
||||||
|
|
||||||
private registerCatch = (b: ButterflyData, atX: number, atY: number): void => {
|
private registerCatch = (b: ButterflyData, atX: number, atY: number): void => {
|
||||||
this.cb.onCatch(b.golden);
|
this.cb.onCatch(b.golden);
|
||||||
this.spawnFx(b.golden ? `+${GOLD_VALUE}` : '+1', b.golden ? 'butterfly-catch-fx golden' : 'butterfly-catch-fx', atX, atY);
|
this.spawnFx(b.golden ? `+${GOLD_VALUE}` : '+1', b.golden ? 'butterfly-catch-fx golden' : 'butterfly-catch-fx', atX, atY);
|
||||||
this.resetButterfly(b);
|
this.resetButterfly(b);
|
||||||
this.nextCatchAt = this.timer + ButterflyScene.CATCH_COOLDOWN;
|
|
||||||
|
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
this.comboCount = (now - this.lastCatchTs < ButterflyScene.COMBO_WINDOW_MS) ? this.comboCount + 1 : 1;
|
this.comboCount = (now - this.lastCatchTs < ButterflyScene.COMBO_WINDOW_MS) ? this.comboCount + 1 : 1;
|
||||||
@@ -285,14 +362,14 @@ class ButterflyScene {
|
|||||||
|
|
||||||
// Chytá max 1 motýla za CATCH_COOLDOWN snímků (fér nezávisle na velikosti okna)
|
// Chytá max 1 motýla za CATCH_COOLDOWN snímků (fér nezávisle na velikosti okna)
|
||||||
private checkCatches = (): void => {
|
private checkCatches = (): void => {
|
||||||
if (this.torn || this.timer < this.stunUntil || this.timer < this.nextCatchAt) return;
|
if (this.torn || this.webbed || this.timer < this.stunUntil || this.timer < this.nextCatchAt) return;
|
||||||
const r2 = this.catchRadius * this.catchRadius;
|
const r2 = this.catchRadius * this.catchRadius;
|
||||||
const half = ButterflyScene.BASE_SIZE / 2;
|
const half = ButterflyScene.BASE_SIZE / 2;
|
||||||
for (const b of this.butterflies) {
|
for (const b of this.butterflies) {
|
||||||
const dx = (b.x + half * b.size) - this.netX;
|
const dx = (b.x + half * b.size) - this.netX;
|
||||||
const dy = (b.y + half * b.size) - this.netY;
|
const dy = (b.y + half * b.size) - this.netY;
|
||||||
if (dx * dx + dy * dy < r2) {
|
if (dx * dx + dy * dy < r2) {
|
||||||
this.registerCatch(b, this.netX, this.netY);
|
this.catchOne(b, this.netX, this.netY);
|
||||||
this.popNet();
|
this.popNet();
|
||||||
return; // jen jeden za cooldown
|
return; // jen jeden za cooldown
|
||||||
}
|
}
|
||||||
@@ -300,7 +377,7 @@ class ButterflyScene {
|
|||||||
};
|
};
|
||||||
|
|
||||||
private runMagnet = (): void => {
|
private runMagnet = (): void => {
|
||||||
if (this.torn || this.timer < this.stunUntil || this.timer < this.nextCatchAt) return;
|
if (this.torn || this.webbed || this.timer < this.stunUntil || this.timer < this.nextCatchAt) return;
|
||||||
if (this.timer - this.lastMagnetAt < ButterflyScene.MAGNET_INTERVAL) return;
|
if (this.timer - this.lastMagnetAt < ButterflyScene.MAGNET_INTERVAL) return;
|
||||||
this.lastMagnetAt = this.timer;
|
this.lastMagnetAt = this.timer;
|
||||||
const half = ButterflyScene.BASE_SIZE / 2;
|
const half = ButterflyScene.BASE_SIZE / 2;
|
||||||
@@ -312,7 +389,7 @@ class ButterflyScene {
|
|||||||
const d2 = dx * dx + dy * dy;
|
const d2 = dx * dx + dy * dy;
|
||||||
if (d2 < nearestD2) { nearestD2 = d2; nearest = b; }
|
if (d2 < nearestD2) { nearestD2 = d2; nearest = b; }
|
||||||
}
|
}
|
||||||
if (nearest) { this.registerCatch(nearest, this.netX, this.netY); this.popNet(); }
|
if (nearest) { this.catchOne(nearest, this.netX, this.netY); this.popNet(); }
|
||||||
};
|
};
|
||||||
|
|
||||||
public activatePremiumNet = (): void => {
|
public activatePremiumNet = (): void => {
|
||||||
@@ -506,10 +583,19 @@ class ButterflyScene {
|
|||||||
this.nextStalkerAt = this.timer + 600; // zkus to znovu za ~10 s
|
this.nextStalkerAt = this.timer + 600; // zkus to znovu za ~10 s
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const type = options[Math.floor(Math.random() * options.length)];
|
// Bohatý hráč láká zloděje: víc mincí → častěji a spíš zloděj
|
||||||
|
const coins = this.cb.getCoins();
|
||||||
|
let type: 'thief' | 'caterpillar';
|
||||||
|
if (canThief && coins >= 200 && Math.random() < 0.7) {
|
||||||
|
type = 'thief';
|
||||||
|
} else {
|
||||||
|
type = options[Math.floor(Math.random() * options.length)];
|
||||||
|
}
|
||||||
this.spawnStalker(type);
|
this.spawnStalker(type);
|
||||||
this.nextStalkerAt = this.timer + ButterflyScene.STALKER_MIN_GAP
|
const wealthFactor = coins >= 300 ? 0.4 : coins >= 150 ? 0.65 : 1;
|
||||||
|
const gap = ButterflyScene.STALKER_MIN_GAP
|
||||||
+ Math.floor(Math.random() * (ButterflyScene.STALKER_MAX_GAP - ButterflyScene.STALKER_MIN_GAP));
|
+ Math.floor(Math.random() * (ButterflyScene.STALKER_MAX_GAP - ButterflyScene.STALKER_MIN_GAP));
|
||||||
|
this.nextStalkerAt = this.timer + Math.round(gap * wealthFactor);
|
||||||
};
|
};
|
||||||
|
|
||||||
private spawnStalker = (type: 'thief' | 'caterpillar'): void => {
|
private spawnStalker = (type: 'thief' | 'caterpillar'): void => {
|
||||||
@@ -602,6 +688,137 @@ class ButterflyScene {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// --- Netopýr (v noci) ----------------------------------------------------
|
||||||
|
|
||||||
|
private get isNight(): boolean {
|
||||||
|
const h = new Date().getHours();
|
||||||
|
return h >= 18 || h < 7;
|
||||||
|
}
|
||||||
|
|
||||||
|
private trySpawnBat = (): void => {
|
||||||
|
if (this.timer < this.nextBatAt) return;
|
||||||
|
this.nextBatAt = this.timer + ButterflyScene.BAT_MIN_GAP
|
||||||
|
+ Math.floor(Math.random() * (ButterflyScene.BAT_MAX_GAP - ButterflyScene.BAT_MIN_GAP));
|
||||||
|
if (this.bat || !this.isNight) return;
|
||||||
|
|
||||||
|
const size = ButterflyScene.BAT_SIZE;
|
||||||
|
const el = document.createElement('div');
|
||||||
|
el.className = 'butterfly-critter bat';
|
||||||
|
el.style.width = `${size}px`;
|
||||||
|
el.style.height = `${size}px`;
|
||||||
|
el.style.backgroundImage = 'url(/bat.svg)';
|
||||||
|
el.title = 'Zažeň netopýra!';
|
||||||
|
const dir = Math.random() > 0.5 ? 1 : -1;
|
||||||
|
const bat: BatData = {
|
||||||
|
el, x: dir === 1 ? -size : this.width + size,
|
||||||
|
y: Math.random() * (this.height * 0.4) + this.height * 0.08,
|
||||||
|
dir, speed: Math.random() * 1.5 + 2, size,
|
||||||
|
bobPhase: Math.random() * Math.PI * 2, bobAmp: Math.random() * 1.2 + 0.8,
|
||||||
|
hp: ButterflyScene.BAT_HP, nextEatAt: this.timer + 60,
|
||||||
|
cleanup: () => { /* nahrazeno */ },
|
||||||
|
};
|
||||||
|
const hit = (ev: Event) => {
|
||||||
|
ev.preventDefault();
|
||||||
|
ev.stopPropagation();
|
||||||
|
bat.hp -= 1;
|
||||||
|
el.classList.remove('hit');
|
||||||
|
void el.offsetWidth;
|
||||||
|
el.classList.add('hit');
|
||||||
|
if (bat.hp <= 0) {
|
||||||
|
this.spawnFx('💨', 'butterfly-swat-fx', bat.x + size / 2, bat.y + size / 2);
|
||||||
|
this.removeBat();
|
||||||
|
this.cb.onBatShooed();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
el.addEventListener('pointerdown', hit);
|
||||||
|
bat.cleanup = () => el.removeEventListener('pointerdown', hit);
|
||||||
|
this.world.appendChild(el);
|
||||||
|
this.bat = bat;
|
||||||
|
};
|
||||||
|
|
||||||
|
private removeBat = (): void => {
|
||||||
|
if (!this.bat) return;
|
||||||
|
this.bat.cleanup();
|
||||||
|
if (this.bat.el.parentNode) this.bat.el.parentNode.removeChild(this.bat.el);
|
||||||
|
this.bat = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
private updateBat = (): void => {
|
||||||
|
const bat = this.bat;
|
||||||
|
if (!bat) return;
|
||||||
|
bat.x += bat.dir * bat.speed;
|
||||||
|
bat.y += Math.sin(this.timer * 0.06 + bat.bobPhase) * bat.bobAmp;
|
||||||
|
const flipY = bat.dir === -1 ? -1 : 1;
|
||||||
|
bat.el.style.transform = `translate(${bat.x}px, ${bat.y}px) scaleX(${flipY})`;
|
||||||
|
// Občas sežere nejbližšího motýla (resetne ho pryč)
|
||||||
|
if (this.timer >= bat.nextEatAt && this.butterflies.length) {
|
||||||
|
bat.nextEatAt = this.timer + ButterflyScene.BAT_EAT_INTERVAL;
|
||||||
|
let nearest: ButterflyData | null = null;
|
||||||
|
let bd2 = Infinity;
|
||||||
|
for (const b of this.butterflies) {
|
||||||
|
const dx = b.x - bat.x, dy = b.y - bat.y;
|
||||||
|
const d2 = dx * dx + dy * dy;
|
||||||
|
if (d2 < bd2) { bd2 = d2; nearest = b; }
|
||||||
|
}
|
||||||
|
if (nearest) {
|
||||||
|
this.spawnFx('🦇', 'butterfly-swat-fx', nearest.x, nearest.y);
|
||||||
|
this.resetButterfly(nearest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (bat.x > this.width + bat.size * 2 || bat.x < -bat.size * 2) this.removeBat();
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Pavouk s pavučinou --------------------------------------------------
|
||||||
|
|
||||||
|
private get webbed(): boolean { return this.web !== null; }
|
||||||
|
|
||||||
|
private trySpawnWeb = (): void => {
|
||||||
|
if (this.timer < this.nextSpiderAt) return;
|
||||||
|
this.nextSpiderAt = this.timer + ButterflyScene.SPIDER_MIN_GAP
|
||||||
|
+ Math.floor(Math.random() * (ButterflyScene.SPIDER_MAX_GAP - ButterflyScene.SPIDER_MIN_GAP));
|
||||||
|
if (this.web) return;
|
||||||
|
|
||||||
|
const el = document.createElement('div');
|
||||||
|
el.className = 'butterfly-web';
|
||||||
|
el.title = 'Strhni pavučinu!';
|
||||||
|
el.style.left = `${this.netX - 60}px`;
|
||||||
|
el.style.top = `${this.netY - 60}px`;
|
||||||
|
this.webClicks = 0;
|
||||||
|
const strip = (ev: Event) => {
|
||||||
|
ev.preventDefault();
|
||||||
|
ev.stopPropagation();
|
||||||
|
this.webClicks += 1;
|
||||||
|
el.classList.remove('hit');
|
||||||
|
void el.offsetWidth;
|
||||||
|
el.classList.add('hit');
|
||||||
|
if (this.webClicks >= ButterflyScene.WEB_CLICKS) {
|
||||||
|
this.spawnFx('✔', 'butterfly-swat-fx', this.netX, this.netY);
|
||||||
|
this.removeWeb();
|
||||||
|
this.cb.onWebCleared();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
el.addEventListener('pointerdown', strip);
|
||||||
|
(el as any)._cleanup = () => el.removeEventListener('pointerdown', strip);
|
||||||
|
this.viewport.appendChild(el);
|
||||||
|
this.web = el;
|
||||||
|
this.spawnFx('🕸️ Pavučina!', 'butterfly-tear-fx', this.netX, this.netY - 20);
|
||||||
|
};
|
||||||
|
|
||||||
|
private removeWeb = (): void => {
|
||||||
|
if (!this.web) return;
|
||||||
|
(this.web as any)._cleanup?.();
|
||||||
|
if (this.web.parentNode) this.web.parentNode.removeChild(this.web);
|
||||||
|
this.web = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
private updateWeb = (): void => {
|
||||||
|
if (this.web) {
|
||||||
|
// pavučina drží u obruče síťky
|
||||||
|
this.web.style.left = `${this.netX - 60}px`;
|
||||||
|
this.web.style.top = `${this.netY - 60}px`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// --- Init / render ------------------------------------------------------
|
// --- Init / render ------------------------------------------------------
|
||||||
|
|
||||||
private initNet = (): void => {
|
private initNet = (): void => {
|
||||||
@@ -627,7 +844,7 @@ class ButterflyScene {
|
|||||||
sprite.className = 'butterfly-sprite';
|
sprite.className = 'butterfly-sprite';
|
||||||
el.appendChild(sprite);
|
el.appendChild(sprite);
|
||||||
const b: ButterflyData = {
|
const b: ButterflyData = {
|
||||||
el, sprite, x: 0, y: 0, dir: 1, speed: 1, size: 1, golden: false,
|
el, sprite, x: 0, y: 0, dir: 1, speed: 1, size: 1, golden: false, moth: false,
|
||||||
bobFreq1: 0, bobPhase1: 0, bobAmp1: 0, bobFreq2: 0, bobPhase2: 0, bobAmp2: 0,
|
bobFreq1: 0, bobPhase1: 0, bobAmp1: 0, bobFreq2: 0, bobPhase2: 0, bobAmp2: 0,
|
||||||
};
|
};
|
||||||
this.resetButterfly(b);
|
this.resetButterfly(b);
|
||||||
@@ -637,6 +854,7 @@ class ButterflyScene {
|
|||||||
this.viewport.appendChild(this.world);
|
this.viewport.appendChild(this.world);
|
||||||
if (this.netEnabled) this.initNet();
|
if (this.netEnabled) this.initNet();
|
||||||
window.addEventListener('resize', this.handleResize);
|
window.addEventListener('resize', this.handleResize);
|
||||||
|
document.addEventListener('visibilitychange', this.handleVisibility);
|
||||||
};
|
};
|
||||||
|
|
||||||
private wasPremium: boolean = false;
|
private wasPremium: boolean = false;
|
||||||
@@ -654,6 +872,10 @@ class ButterflyScene {
|
|||||||
this.updateBirds();
|
this.updateBirds();
|
||||||
this.trySpawnStalker();
|
this.trySpawnStalker();
|
||||||
this.updateStalker();
|
this.updateStalker();
|
||||||
|
this.trySpawnBat();
|
||||||
|
this.updateBat();
|
||||||
|
this.trySpawnWeb();
|
||||||
|
this.updateWeb();
|
||||||
|
|
||||||
const stunned = this.timer < this.stunUntil;
|
const stunned = this.timer < this.stunUntil;
|
||||||
if (stunned !== this.wasStunned) {
|
if (stunned !== this.wasStunned) {
|
||||||
@@ -674,6 +896,8 @@ class ButterflyScene {
|
|||||||
if (this.comboTimer) window.clearTimeout(this.comboTimer);
|
if (this.comboTimer) window.clearTimeout(this.comboTimer);
|
||||||
for (const w of this.wasps) w.cleanup?.();
|
for (const w of this.wasps) w.cleanup?.();
|
||||||
this.removeStalker();
|
this.removeStalker();
|
||||||
|
this.removeBat();
|
||||||
|
this.removeWeb();
|
||||||
if (this.world && this.world.parentNode) this.world.parentNode.removeChild(this.world);
|
if (this.world && this.world.parentNode) this.world.parentNode.removeChild(this.world);
|
||||||
this.net.removeEventListener('pointerdown', this.onPointerDown);
|
this.net.removeEventListener('pointerdown', this.onPointerDown);
|
||||||
window.removeEventListener('pointermove', this.onPointerMove);
|
window.removeEventListener('pointermove', this.onPointerMove);
|
||||||
@@ -681,6 +905,7 @@ class ButterflyScene {
|
|||||||
window.removeEventListener('pointercancel', this.onPointerUp);
|
window.removeEventListener('pointercancel', this.onPointerUp);
|
||||||
if (this.net.parentNode) this.net.parentNode.removeChild(this.net);
|
if (this.net.parentNode) this.net.parentNode.removeChild(this.net);
|
||||||
window.removeEventListener('resize', this.handleResize);
|
window.removeEventListener('resize', this.handleResize);
|
||||||
|
document.removeEventListener('visibilitychange', this.handleVisibility);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -703,6 +928,7 @@ const FlyingButterflies: React.FC<FlyingButterfliesProps> = ({
|
|||||||
const [combo, setCombo] = useState(0);
|
const [combo, setCombo] = useState(0);
|
||||||
const [flash, setFlash] = useState<string | null>(null);
|
const [flash, setFlash] = useState<string | null>(null);
|
||||||
const [leaderboardOpen, setLeaderboardOpen] = useState(false);
|
const [leaderboardOpen, setLeaderboardOpen] = useState(false);
|
||||||
|
const [shopOpen, setShopOpen] = useState(false);
|
||||||
const [nowTs, setNowTs] = useState(() => Date.now());
|
const [nowTs, setNowTs] = useState(() => Date.now());
|
||||||
const flashTimer = useRef<number | null>(null);
|
const flashTimer = useRef<number | null>(null);
|
||||||
|
|
||||||
@@ -726,23 +952,29 @@ const FlyingButterflies: React.FC<FlyingButterfliesProps> = ({
|
|||||||
const {
|
const {
|
||||||
stats, displayCaught, coinsRef, reportCatch, repair,
|
stats, displayCaught, coinsRef, reportCatch, repair,
|
||||||
killWasp, buyRepellent, reportTear, reportRobbery, defeatThief,
|
killWasp, buyRepellent, reportTear, reportRobbery, defeatThief,
|
||||||
caterpillarAte, defeatCaterpillar,
|
caterpillarAte, defeatCaterpillar, mothHit, buyPremium, waspSpray, buyInsurance, buyUpgrade,
|
||||||
} = useButterflyStats(handleReward);
|
} = useButterflyStats(handleReward);
|
||||||
|
|
||||||
|
const netUpgradeRef = useRef(0);
|
||||||
|
netUpgradeRef.current = stats?.upgrades?.net ?? 0;
|
||||||
|
|
||||||
|
const noop = () => { };
|
||||||
const callbacksRef = useRef<SceneCallbacks>({
|
const callbacksRef = useRef<SceneCallbacks>({
|
||||||
onCatch: () => { }, getCoins: () => 0, getCaught: () => 0, onTornChange: () => { },
|
onCatch: noop, getCoins: () => 0, getCaught: () => 0, getNetUpgrade: () => 0, onTornChange: noop,
|
||||||
onCombo: () => { }, onSting: () => { }, onSwatWasp: () => { },
|
onCombo: noop, onSting: noop, onSwatWasp: noop, onMothCaught: noop,
|
||||||
onRobbery: () => { }, onDefeatThief: () => { }, onCaterpillarAte: () => { },
|
onRobbery: noop, onDefeatThief: noop, onCaterpillarAte: noop,
|
||||||
onDefeatCaterpillar: () => { }, onStalkerAppear: () => { },
|
onDefeatCaterpillar: noop, onStalkerAppear: noop, onBatShooed: noop, onWebCleared: noop,
|
||||||
});
|
});
|
||||||
callbacksRef.current = {
|
callbacksRef.current = {
|
||||||
onCatch: (golden) => reportCatch(golden),
|
onCatch: (golden) => reportCatch(golden),
|
||||||
getCoins: () => coinsRef.current,
|
getCoins: () => coinsRef.current,
|
||||||
getCaught: () => stats?.caught ?? 0,
|
getCaught: () => stats?.caught ?? 0,
|
||||||
|
getNetUpgrade: () => netUpgradeRef.current,
|
||||||
onTornChange: (t) => { if (t) void reportTear(); },
|
onTornChange: (t) => { if (t) void reportTear(); },
|
||||||
onCombo: (c) => setCombo(c),
|
onCombo: (c) => setCombo(c),
|
||||||
onSting: () => showFlash('🐝 Au! Vosa tě žihla – síťka teď 5 s nechytá. Zaklikej vosy!'),
|
onSting: () => showFlash('🐝 Au! Vosa tě žihla – síťka teď 5 s nechytá. Zaklikej vosy!'),
|
||||||
onSwatWasp: () => void killWasp(),
|
onSwatWasp: () => void killWasp(),
|
||||||
|
onMothCaught: () => { void mothHit(); showFlash('🖤 Chytil jsi černou můru! Přišel jsi o úlovky – vyhýbej se jim.'); },
|
||||||
onRobbery: () => { void reportRobbery(); showFlash('💸 Zloděj ti ukradl většinu mincí! Příště ho zaklikej.'); },
|
onRobbery: () => { void reportRobbery(); showFlash('💸 Zloděj ti ukradl většinu mincí! Příště ho zaklikej.'); },
|
||||||
onDefeatThief: () => { void defeatThief(); showFlash('💰 Zloděj poražen! Malá odměna a mince v bezpečí.'); },
|
onDefeatThief: () => { void defeatThief(); showFlash('💰 Zloděj poražen! Malá odměna a mince v bezpečí.'); },
|
||||||
onCaterpillarAte: () => { void caterpillarAte(); showFlash('🐛 Housenka ti sežrala část úlovků!'); },
|
onCaterpillarAte: () => { void caterpillarAte(); showFlash('🐛 Housenka ti sežrala část úlovků!'); },
|
||||||
@@ -750,6 +982,8 @@ const FlyingButterflies: React.FC<FlyingButterfliesProps> = ({
|
|||||||
onStalkerAppear: (type) => showFlash(type === 'thief'
|
onStalkerAppear: (type) => showFlash(type === 'thief'
|
||||||
? '🦹 Zloděj! Míří k tvým mincím – zaklikej ho (chce to hodně ran)!'
|
? '🦹 Zloděj! Míří k tvým mincím – zaklikej ho (chce to hodně ran)!'
|
||||||
: '🐛 Housenka! Plíží se k tvým úlovkům – zaklikej ji!'),
|
: '🐛 Housenka! Plíží se k tvým úlovkům – zaklikej ji!'),
|
||||||
|
onBatShooed: () => showFlash('🦇 Netopýr zahnán!'),
|
||||||
|
onWebCleared: () => showFlash('🕸️ Pavučina stržena – zase můžeš chytat.'),
|
||||||
};
|
};
|
||||||
|
|
||||||
const initialize = useCallback(() => {
|
const initialize = useCallback(() => {
|
||||||
@@ -758,15 +992,19 @@ const FlyingButterflies: React.FC<FlyingButterfliesProps> = ({
|
|||||||
onCatch: (g) => callbacksRef.current.onCatch(g),
|
onCatch: (g) => callbacksRef.current.onCatch(g),
|
||||||
getCoins: () => callbacksRef.current.getCoins(),
|
getCoins: () => callbacksRef.current.getCoins(),
|
||||||
getCaught: () => callbacksRef.current.getCaught(),
|
getCaught: () => callbacksRef.current.getCaught(),
|
||||||
|
getNetUpgrade: () => callbacksRef.current.getNetUpgrade(),
|
||||||
onTornChange: (t) => callbacksRef.current.onTornChange(t),
|
onTornChange: (t) => callbacksRef.current.onTornChange(t),
|
||||||
onCombo: (c) => callbacksRef.current.onCombo(c),
|
onCombo: (c) => callbacksRef.current.onCombo(c),
|
||||||
onSting: () => callbacksRef.current.onSting(),
|
onSting: () => callbacksRef.current.onSting(),
|
||||||
onSwatWasp: () => callbacksRef.current.onSwatWasp(),
|
onSwatWasp: () => callbacksRef.current.onSwatWasp(),
|
||||||
|
onMothCaught: () => callbacksRef.current.onMothCaught(),
|
||||||
onRobbery: () => callbacksRef.current.onRobbery(),
|
onRobbery: () => callbacksRef.current.onRobbery(),
|
||||||
onDefeatThief: () => callbacksRef.current.onDefeatThief(),
|
onDefeatThief: () => callbacksRef.current.onDefeatThief(),
|
||||||
onCaterpillarAte: () => callbacksRef.current.onCaterpillarAte(),
|
onCaterpillarAte: () => callbacksRef.current.onCaterpillarAte(),
|
||||||
onDefeatCaterpillar: () => callbacksRef.current.onDefeatCaterpillar(),
|
onDefeatCaterpillar: () => callbacksRef.current.onDefeatCaterpillar(),
|
||||||
onStalkerAppear: (t) => callbacksRef.current.onStalkerAppear(t),
|
onStalkerAppear: (t) => callbacksRef.current.onStalkerAppear(t),
|
||||||
|
onBatShooed: () => callbacksRef.current.onBatShooed(),
|
||||||
|
onWebCleared: () => callbacksRef.current.onWebCleared(),
|
||||||
});
|
});
|
||||||
sceneRef.current.init();
|
sceneRef.current.init();
|
||||||
sceneRef.current.render();
|
sceneRef.current.render();
|
||||||
@@ -818,10 +1056,34 @@ const FlyingButterflies: React.FC<FlyingButterfliesProps> = ({
|
|||||||
if (ok) sceneRef.current?.repairNetExternally();
|
if (ok) sceneRef.current?.repairNetExternally();
|
||||||
}, [repair]);
|
}, [repair]);
|
||||||
|
|
||||||
|
/** Zobrazí hlášku podle výsledku nákupu (rozliší nedostatek mincí od jiné chyby). */
|
||||||
|
const flashBuy = useCallback((r: 'ok' | 'poor' | 'error', okMsg: string) => {
|
||||||
|
if (r === 'ok') showFlash(okMsg);
|
||||||
|
else if (r === 'poor') showFlash('🪙 Nemáš dost mincí.');
|
||||||
|
else showFlash('⚠️ Nákup se nezdařil – zkus obnovit stránku (F5). Server možná běží starou verzi.');
|
||||||
|
}, [showFlash]);
|
||||||
|
|
||||||
const onRepellentClick = useCallback(async () => {
|
const onRepellentClick = useCallback(async () => {
|
||||||
const ok = await buyRepellent();
|
flashBuy(await buyRepellent(), '🦅🚫 Plašič ptáků koupen – ptáci na chvíli zmizeli.');
|
||||||
if (ok) showFlash('🦅🚫 Plašič ptáků koupen – ptáci na chvíli zmizeli.');
|
}, [buyRepellent, flashBuy]);
|
||||||
}, [buyRepellent, showFlash]);
|
|
||||||
|
const onBuyPremium = useCallback(async () => {
|
||||||
|
const r = await buyPremium();
|
||||||
|
if (r === 'ok') sceneRef.current?.activatePremiumNet();
|
||||||
|
flashBuy(r, '🪄 Zlatá síťka aktivována!');
|
||||||
|
}, [buyPremium, flashBuy]);
|
||||||
|
|
||||||
|
const onWaspSpray = useCallback(async () => {
|
||||||
|
flashBuy(await waspSpray(), '💨 Vosy zlikvidovány!');
|
||||||
|
}, [waspSpray, flashBuy]);
|
||||||
|
|
||||||
|
const onBuyInsurance = useCallback(async () => {
|
||||||
|
flashBuy(await buyInsurance(), '🛡️ Pojistka proti zloději aktivní.');
|
||||||
|
}, [buyInsurance, flashBuy]);
|
||||||
|
|
||||||
|
const onBuyUpgrade = useCallback(async (item: 'net' | 'scarecrow' | 'reinforced') => {
|
||||||
|
flashBuy(await buyUpgrade(item), '✅ Vylepšení koupeno!');
|
||||||
|
}, [buyUpgrade, flashBuy]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -899,7 +1161,26 @@ const FlyingButterflies: React.FC<FlyingButterfliesProps> = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="butterfly-shop-open"
|
||||||
|
onClick={() => setShopOpen(true)}
|
||||||
|
title="Otevřít obchod"
|
||||||
|
>
|
||||||
|
🛒 Obchod
|
||||||
|
</button>
|
||||||
|
|
||||||
<ButterflyLeaderboardModal isOpen={leaderboardOpen} onClose={() => setLeaderboardOpen(false)} myStats={stats} />
|
<ButterflyLeaderboardModal isOpen={leaderboardOpen} onClose={() => setLeaderboardOpen(false)} myStats={stats} />
|
||||||
|
<ButterflyShopModal
|
||||||
|
isOpen={shopOpen}
|
||||||
|
onClose={() => setShopOpen(false)}
|
||||||
|
stats={stats}
|
||||||
|
onBuyPremium={onBuyPremium}
|
||||||
|
onWaspSpray={onWaspSpray}
|
||||||
|
onRepellent={onRepellentClick}
|
||||||
|
onBuyInsurance={onBuyInsurance}
|
||||||
|
onBuyUpgrade={onBuyUpgrade}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -43,7 +43,38 @@
|
|||||||
|
|
||||||
.butterfly-leaderboard-heading {
|
.butterfly-leaderboard-heading {
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
margin: 4px 0 8px;
|
margin: 12px 0 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.butterfly-achievements {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.butterfly-achievement {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 6px 8px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: rgba(0, 0, 0, 0.04);
|
||||||
|
font-size: 0.78rem;
|
||||||
|
opacity: 0.55;
|
||||||
|
filter: grayscale(1);
|
||||||
|
|
||||||
|
&.unlocked {
|
||||||
|
opacity: 1;
|
||||||
|
filter: none;
|
||||||
|
background: rgba(255, 203, 61, 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ach-icon { flex: 0 0 auto; }
|
||||||
|
.ach-title {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.butterfly-leaderboard {
|
.butterfly-leaderboard {
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Modal } from "react-bootstrap";
|
import { Modal } from "react-bootstrap";
|
||||||
import { ButterflyLeaderboardEntry, ButterflyStats, getButterflyLeaderboard } from "../../../../types";
|
import {
|
||||||
|
ButterflyLeaderboardEntry, ButterflyStats, ButterflyAchievement,
|
||||||
|
getButterflyLeaderboard, getButterflyAchievements,
|
||||||
|
} from "../../../../types";
|
||||||
import { useAuth } from "../../context/auth";
|
import { useAuth } from "../../context/auth";
|
||||||
import "./ButterflyLeaderboardModal.scss";
|
import "./ButterflyLeaderboardModal.scss";
|
||||||
|
|
||||||
@@ -12,20 +15,25 @@ type Props = {
|
|||||||
|
|
||||||
const MEDALS = ['🥇', '🥈', '🥉'];
|
const MEDALS = ['🥇', '🥈', '🥉'];
|
||||||
|
|
||||||
/** Modální dialog s osobními statistikami a týmovým žebříčkem chytačů motýlů. */
|
/** Modální dialog s osobními statistikami, achievementy a týmovým žebříčkem. */
|
||||||
export default function ButterflyLeaderboardModal({ isOpen, onClose, myStats }: Readonly<Props>) {
|
export default function ButterflyLeaderboardModal({ isOpen, onClose, myStats }: Readonly<Props>) {
|
||||||
const auth = useAuth();
|
const auth = useAuth();
|
||||||
const [entries, setEntries] = useState<ButterflyLeaderboardEntry[]>();
|
const [entries, setEntries] = useState<ButterflyLeaderboardEntry[]>();
|
||||||
|
const [achievements, setAchievements] = useState<ButterflyAchievement[]>();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isOpen) return;
|
if (!isOpen) return;
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
getButterflyLeaderboard({ query: { limit: 20 } })
|
Promise.all([
|
||||||
.then(res => { if (!cancelled) setEntries(res.data ?? []); })
|
getButterflyLeaderboard({ query: { limit: 20 } }).then(r => r.data ?? []).catch(() => []),
|
||||||
.catch(() => { if (!cancelled) setEntries([]); })
|
getButterflyAchievements().then(r => r.data ?? []).catch(() => []),
|
||||||
.finally(() => { if (!cancelled) setLoading(false); });
|
]).then(([lb, ach]) => {
|
||||||
|
if (cancelled) return;
|
||||||
|
setEntries(lb);
|
||||||
|
setAchievements(ach);
|
||||||
|
}).finally(() => { if (!cancelled) setLoading(false); });
|
||||||
return () => { cancelled = true; };
|
return () => { cancelled = true; };
|
||||||
}, [isOpen]);
|
}, [isOpen]);
|
||||||
|
|
||||||
@@ -52,6 +60,24 @@ export default function ButterflyLeaderboardModal({ isOpen, onClose, myStats }:
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{achievements && achievements.length > 0 && (
|
||||||
|
<>
|
||||||
|
<h6 className="butterfly-leaderboard-heading">Odznaky</h6>
|
||||||
|
<div className="butterfly-achievements">
|
||||||
|
{achievements.map(a => (
|
||||||
|
<div
|
||||||
|
key={a.id}
|
||||||
|
className={a.unlocked ? 'butterfly-achievement unlocked' : 'butterfly-achievement'}
|
||||||
|
title={a.description}
|
||||||
|
>
|
||||||
|
<span className="ach-icon">{a.unlocked ? '🏅' : '🔒'}</span>
|
||||||
|
<span className="ach-title">{a.title}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
<h6 className="butterfly-leaderboard-heading">Žebříček týmu</h6>
|
<h6 className="butterfly-leaderboard-heading">Žebříček týmu</h6>
|
||||||
{loading && <p className="text-center text-muted mb-0">Načítám žebříček…</p>}
|
{loading && <p className="text-center text-muted mb-0">Načítám žebříček…</p>}
|
||||||
{!loading && entries && entries.length === 0 && (
|
{!loading && entries && entries.length === 0 && (
|
||||||
|
|||||||
@@ -0,0 +1,106 @@
|
|||||||
|
.shop-balance {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #e0a200;
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shop-heading {
|
||||||
|
font-weight: 700;
|
||||||
|
margin: 14px 0 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.butterfly-shop-daily {
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: rgba(28, 126, 214, 0.12);
|
||||||
|
border: 1px solid rgba(28, 126, 214, 0.35);
|
||||||
|
margin-bottom: 12px;
|
||||||
|
|
||||||
|
.daily-title {
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.daily-bar {
|
||||||
|
height: 8px;
|
||||||
|
background: rgba(0, 0, 0, 0.1);
|
||||||
|
border-radius: 999px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.daily-bar-inner {
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg, #4dabf7, #1c7ed6);
|
||||||
|
border-radius: 999px;
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.daily-status {
|
||||||
|
margin-top: 5px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: #495057;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.butterfly-shop-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.butterfly-shop-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||||
|
border-radius: 10px;
|
||||||
|
background: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: left;
|
||||||
|
transition: background 0.12s ease, transform 0.08s ease;
|
||||||
|
|
||||||
|
&:hover:not(:disabled) {
|
||||||
|
background: #f1f3f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active:not(:disabled) {
|
||||||
|
transform: scale(0.99);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shop-item-label {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shop-item-lvl {
|
||||||
|
display: inline;
|
||||||
|
color: #1c7ed6;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shop-item-desc {
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 0.74rem;
|
||||||
|
color: #868e96;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shop-item-cost {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
font-weight: 800;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
color: #e0a200;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
import { Modal } from "react-bootstrap";
|
||||||
|
import { ButterflyStats } from "../../../../types";
|
||||||
|
import "./ButterflyShopModal.scss";
|
||||||
|
|
||||||
|
type UpgradeId = 'net' | 'scarecrow' | 'reinforced';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
stats?: ButterflyStats;
|
||||||
|
onBuyPremium: () => void;
|
||||||
|
onWaspSpray: () => void;
|
||||||
|
onRepellent: () => void;
|
||||||
|
onBuyInsurance: () => void;
|
||||||
|
onBuyUpgrade: (item: UpgradeId) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Zrcadlí serverové hodnoty (server/src/butterflies.ts)
|
||||||
|
const PREMIUM_BUY_COST = 40;
|
||||||
|
const WASP_SPRAY_COST = 25;
|
||||||
|
const REPELLENT_COST = 70;
|
||||||
|
const INSURANCE_COST = 50;
|
||||||
|
const UPGRADE_BASE: Record<UpgradeId, number> = { net: 100, scarecrow: 120, reinforced: 150 };
|
||||||
|
const UPGRADE_MAX: Record<UpgradeId, number> = { net: 5, scarecrow: 4, reinforced: 3 };
|
||||||
|
const UPGRADE_META: Record<UpgradeId, { icon: string; title: string; desc: string }> = {
|
||||||
|
net: { icon: '🥅', title: 'Větší síťka', desc: 'Zvětší dosah chytání' },
|
||||||
|
scarecrow: { icon: '🐦⬛', title: 'Strašák', desc: 'Ptáci přibývají pomaleji' },
|
||||||
|
reinforced: { icon: '🧵', title: 'Zpevněná síťka', desc: 'Rychlejší samo-zašití' },
|
||||||
|
};
|
||||||
|
|
||||||
|
function upgradeCost(item: UpgradeId, level: number): number {
|
||||||
|
return UPGRADE_BASE[item] * Math.pow(2, level);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Obchod: spotřební pomůcky a trvalá vylepšení za mince. */
|
||||||
|
export default function ButterflyShopModal({
|
||||||
|
isOpen, onClose, stats, onBuyPremium, onWaspSpray, onRepellent, onBuyInsurance, onBuyUpgrade,
|
||||||
|
}: Readonly<Props>) {
|
||||||
|
const coins = stats?.coins ?? 0;
|
||||||
|
const insuranceActive = !!stats && stats.insuranceUntil > Date.now();
|
||||||
|
|
||||||
|
const consumable = (label: string, cost: number, onBuy: () => void, extra?: string) => (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="butterfly-shop-item"
|
||||||
|
onClick={onBuy}
|
||||||
|
disabled={coins < cost}
|
||||||
|
>
|
||||||
|
<span className="shop-item-label">{label}{extra ? ` ${extra}` : ''}</span>
|
||||||
|
<span className="shop-item-cost">{cost} 🪙</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal show={isOpen} onHide={onClose} centered>
|
||||||
|
<Modal.Header closeButton>
|
||||||
|
<Modal.Title>🛒 Obchod <span className="shop-balance">{coins} 🪙</span></Modal.Title>
|
||||||
|
</Modal.Header>
|
||||||
|
<Modal.Body>
|
||||||
|
{stats?.daily && (
|
||||||
|
<div className="butterfly-shop-daily">
|
||||||
|
<div className="daily-title">🎯 Denní úkol: {stats.daily.title}</div>
|
||||||
|
<div className="daily-bar">
|
||||||
|
<div
|
||||||
|
className="daily-bar-inner"
|
||||||
|
style={{ width: `${Math.min(100, Math.round((stats.daily.progress / stats.daily.target) * 100))}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="daily-status">
|
||||||
|
{stats.daily.done
|
||||||
|
? `✅ Splněno (+${stats.daily.reward} 🪙)`
|
||||||
|
: `${stats.daily.progress}/${stats.daily.target} · odměna ${stats.daily.reward} 🪙`}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<h6 className="shop-heading">Pomůcky</h6>
|
||||||
|
<div className="butterfly-shop-list">
|
||||||
|
{consumable('🪄 Prémiová síťka (60 s)', PREMIUM_BUY_COST, onBuyPremium)}
|
||||||
|
{consumable('💨 Vosí sprej (vyhubí vosy)', WASP_SPRAY_COST, onWaspSpray)}
|
||||||
|
{consumable('🦅🚫 Plašič ptáků', REPELLENT_COST, onRepellent)}
|
||||||
|
{consumable('🛡️ Pojistka proti zloději', INSURANCE_COST, onBuyInsurance, insuranceActive ? '(aktivní)' : '')}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h6 className="shop-heading">Trvalá vylepšení</h6>
|
||||||
|
<div className="butterfly-shop-list">
|
||||||
|
{(Object.keys(UPGRADE_META) as UpgradeId[]).map(item => {
|
||||||
|
const level = stats?.upgrades?.[item] ?? 0;
|
||||||
|
const max = UPGRADE_MAX[item];
|
||||||
|
const maxed = level >= max;
|
||||||
|
const cost = upgradeCost(item, level);
|
||||||
|
const meta = UPGRADE_META[item];
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={item}
|
||||||
|
type="button"
|
||||||
|
className="butterfly-shop-item upgrade"
|
||||||
|
onClick={() => onBuyUpgrade(item)}
|
||||||
|
disabled={maxed || coins < cost}
|
||||||
|
>
|
||||||
|
<span className="shop-item-label">
|
||||||
|
{meta.icon} {meta.title} <span className="shop-item-lvl">{level}/{max}</span>
|
||||||
|
<span className="shop-item-desc">{meta.desc}</span>
|
||||||
|
</span>
|
||||||
|
<span className="shop-item-cost">{maxed ? 'MAX' : `${cost} 🪙`}</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</Modal.Body>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -11,8 +11,19 @@ import {
|
|||||||
defeatThief as defeatThiefApi,
|
defeatThief as defeatThiefApi,
|
||||||
reportCaterpillarAte as caterpillarAteApi,
|
reportCaterpillarAte as caterpillarAteApi,
|
||||||
defeatCaterpillar as defeatCaterpillarApi,
|
defeatCaterpillar as defeatCaterpillarApi,
|
||||||
|
reportMothHit as mothHitApi,
|
||||||
|
buyPremiumNet as buyPremiumApi,
|
||||||
|
buyWaspSpray as waspSprayApi,
|
||||||
|
buyInsurance as buyInsuranceApi,
|
||||||
|
buyUpgrade as buyUpgradeApi,
|
||||||
} from '../../../types';
|
} from '../../../types';
|
||||||
|
|
||||||
|
/** Druhy trvalých vylepšení v obchodě. */
|
||||||
|
export type UpgradeItem = 'net' | 'scarecrow' | 'reinforced';
|
||||||
|
|
||||||
|
/** Výsledek nákupu: úspěch / nedostatek mincí / jiná chyba (např. starý server). */
|
||||||
|
export type BuyResult = 'ok' | 'poor' | 'error';
|
||||||
|
|
||||||
/** Jak dlouho se čeká, než se nasbíraná dávka úlovků odešle na server. */
|
/** Jak dlouho se čeká, než se nasbíraná dávka úlovků odešle na server. */
|
||||||
const FLUSH_DEBOUNCE_MS = 1500;
|
const FLUSH_DEBOUNCE_MS = 1500;
|
||||||
/** Jak často se přenačte stav ze serveru (kvůli přibývajícím škůdcům). */
|
/** Jak často se přenačte stav ze serveru (kvůli přibývajícím škůdcům). */
|
||||||
@@ -108,12 +119,12 @@ export function useButterflyStats(onReward?: (e: RewardEvent) => void) {
|
|||||||
} catch { /* ignore */ }
|
} catch { /* ignore */ }
|
||||||
}, [applyStats]);
|
}, [applyStats]);
|
||||||
|
|
||||||
const buyRepellent = useCallback(async (): Promise<boolean> => {
|
const buyRepellent = useCallback(async (): Promise<BuyResult> => {
|
||||||
try {
|
try {
|
||||||
const res = await buyRepellentApi();
|
const res = await buyRepellentApi();
|
||||||
if (res.data) { applyStats(res.data); return true; }
|
if (res.data) { applyStats(res.data); return 'ok'; }
|
||||||
return false;
|
return res.response?.status === 402 ? 'poor' : 'error';
|
||||||
} catch { return false; }
|
} catch { return 'error'; }
|
||||||
}, [applyStats]);
|
}, [applyStats]);
|
||||||
|
|
||||||
const reportTear = useCallback(async () => {
|
const reportTear = useCallback(async () => {
|
||||||
@@ -147,6 +158,34 @@ export function useButterflyStats(onReward?: (e: RewardEvent) => void) {
|
|||||||
} catch { /* ignore */ }
|
} catch { /* ignore */ }
|
||||||
}, [applyStats]);
|
}, [applyStats]);
|
||||||
|
|
||||||
|
/** Hráč omylem chytil černou můru – server sníží úlovky. */
|
||||||
|
const mothHit = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const res = await mothHitApi();
|
||||||
|
if (res.data) applyStats(res.data);
|
||||||
|
} catch { /* ignore */ }
|
||||||
|
}, [applyStats]);
|
||||||
|
|
||||||
|
/** Vyhodnotí výsledek nákupu z odpovědi (402 = nedostatek mincí, jinak chyba). */
|
||||||
|
const buyResult = useCallback((res: { data?: unknown; response?: Response }): BuyResult => {
|
||||||
|
if (res.data) { applyStats(res.data as ButterflyStats); return 'ok'; }
|
||||||
|
return res.response?.status === 402 ? 'poor' : 'error';
|
||||||
|
}, [applyStats]);
|
||||||
|
|
||||||
|
/** Obchod: koupě spotřebního předmětu / vylepšení. */
|
||||||
|
const buyPremium = useCallback(async (): Promise<BuyResult> => {
|
||||||
|
try { return buyResult(await buyPremiumApi()); } catch { return 'error'; }
|
||||||
|
}, [buyResult]);
|
||||||
|
const waspSpray = useCallback(async (): Promise<BuyResult> => {
|
||||||
|
try { return buyResult(await waspSprayApi()); } catch { return 'error'; }
|
||||||
|
}, [buyResult]);
|
||||||
|
const buyInsurance = useCallback(async (): Promise<BuyResult> => {
|
||||||
|
try { return buyResult(await buyInsuranceApi()); } catch { return 'error'; }
|
||||||
|
}, [buyResult]);
|
||||||
|
const buyUpgrade = useCallback(async (item: UpgradeItem): Promise<BuyResult> => {
|
||||||
|
try { return buyResult(await buyUpgradeApi({ body: { item } })); } catch { return 'error'; }
|
||||||
|
}, [buyResult]);
|
||||||
|
|
||||||
/** Hráč porazil housenku – odměna a statistika. */
|
/** Hráč porazil housenku – odměna a statistika. */
|
||||||
const defeatCaterpillar = useCallback(async () => {
|
const defeatCaterpillar = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
@@ -203,5 +242,6 @@ export function useButterflyStats(onReward?: (e: RewardEvent) => void) {
|
|||||||
return {
|
return {
|
||||||
stats, displayCaught, coinsRef, reportCatch, repair, killWasp, buyRepellent,
|
stats, displayCaught, coinsRef, reportCatch, repair, killWasp, buyRepellent,
|
||||||
reportTear, reportRobbery, defeatThief, caterpillarAte, defeatCaterpillar,
|
reportTear, reportRobbery, defeatThief, caterpillarAte, defeatCaterpillar,
|
||||||
|
mothHit, buyPremium, waspSpray, buyInsurance, buyUpgrade,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
catchWindowStart: number;
|
||||||
/** Počet započtených úlovků v aktuálním okně */
|
/** Počet započtených úlovků v aktuálním okně */
|
||||||
catchWindowCount: number;
|
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();
|
const storage = getStorage();
|
||||||
@@ -62,15 +71,15 @@ export const PREMIUM_MILESTONE = 75;
|
|||||||
/** Maximální počet současně poletujících vos */
|
/** Maximální počet současně poletujících vos */
|
||||||
export const WASP_MAX = 6;
|
export const WASP_MAX = 6;
|
||||||
/** Maximální počet současně poletujících ptáků */
|
/** 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 */
|
/** Jak často (ms) přibude jedna vosa */
|
||||||
export const WASP_GROWTH_MS = 30_000;
|
export const WASP_GROWTH_MS = 30_000;
|
||||||
/** Jak často (ms) přibude jeden pták */
|
/** Jak často (ms) přibude jeden pták (základ; zpomaluje strašák) */
|
||||||
export const BIRD_GROWTH_MS = 60_000;
|
export const BIRD_GROWTH_MS = 40_000;
|
||||||
/** Cena plašiče ptáků v mincích */
|
/** 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č */
|
/** 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 */
|
/** Za jak dlouho (ms) se protržená síťka sama zašije */
|
||||||
export const AUTO_REPAIR_MS = 60_000;
|
export const AUTO_REPAIR_MS = 60_000;
|
||||||
/** Jaký podíl mincí ukradne zloděj, když se dostane k penězům (0–1) */
|
/** 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;
|
export const CATERPILLAR_REWARD = 15;
|
||||||
/** Kolik zásahů (kliknutí) housenka vydrží */
|
/** Kolik zásahů (kliknutí) housenka vydrží */
|
||||||
export const CATERPILLAR_HP = 18;
|
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 */
|
/** Od jaké velikosti dávky se začíná počítat kombo bonus */
|
||||||
const COMBO_THRESHOLD = 4;
|
const COMBO_THRESHOLD = 4;
|
||||||
@@ -183,6 +241,10 @@ function defaultStats(now: number): StoredButterflyStats {
|
|||||||
repellentUntil: 0, netTornUntil: 0,
|
repellentUntil: 0, netTornUntil: 0,
|
||||||
lastWaspAt: now, lastBirdAt: now,
|
lastWaspAt: now, lastBirdAt: now,
|
||||||
catchWindowStart: now, catchWindowCount: 0,
|
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,
|
lastBirdAt: s.lastBirdAt ?? now,
|
||||||
catchWindowStart: s.catchWindowStart ?? now,
|
catchWindowStart: s.catchWindowStart ?? now,
|
||||||
catchWindowCount: s.catchWindowCount ?? 0,
|
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á
|
* 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č.
|
* 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;
|
const repellentActive = s.repellentUntil > now;
|
||||||
if (!repellentActive) {
|
if (!repellentActive) {
|
||||||
if (s.birds < BIRD_MAX) {
|
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) {
|
if (add > 0) {
|
||||||
s.birds = Math.min(s.birds + add, BIRD_MAX);
|
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 {
|
} else {
|
||||||
s.lastBirdAt = now;
|
s.lastBirdAt = now;
|
||||||
@@ -260,8 +361,19 @@ function toDto(s: StoredButterflyStats): ButterflyStats {
|
|||||||
birdsScared: s.birdsScared,
|
birdsScared: s.birdsScared,
|
||||||
thievesDefeated: s.thievesDefeated,
|
thievesDefeated: s.thievesDefeated,
|
||||||
caterpillarsDefeated: s.caterpillarsDefeated,
|
caterpillarsDefeated: s.caterpillarsDefeated,
|
||||||
|
mothsHit: s.mothsHit,
|
||||||
repellentUntil: s.repellentUntil,
|
repellentUntil: s.repellentUntil,
|
||||||
netTornUntil: s.netTornUntil,
|
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 all = current ?? {};
|
||||||
const mine = normalize(all[login] ?? defaultStats(now), now);
|
const mine = normalize(all[login] ?? defaultStats(now), now);
|
||||||
growPests(mine, now);
|
growPests(mine, now);
|
||||||
|
ensureDaily(mine, now);
|
||||||
mutator(mine);
|
mutator(mine);
|
||||||
all[login] = mine;
|
all[login] = mine;
|
||||||
return all;
|
return all;
|
||||||
@@ -347,6 +460,9 @@ export async function recordCatches(login: string, normal: number, golden: numbe
|
|||||||
s.goldenCaught += creditGolden;
|
s.goldenCaught += creditGolden;
|
||||||
if (credited > 0) s.lastCatchDay = today;
|
if (credited > 0) s.lastCatchDay = today;
|
||||||
|
|
||||||
|
bumpDaily(s, 'catch', credited);
|
||||||
|
bumpDaily(s, 'golden', creditGolden);
|
||||||
|
|
||||||
leveledUp = levelForCaught(newCaught) > oldLevel;
|
leveledUp = levelForCaught(newCaught) > oldLevel;
|
||||||
premiumUnlocked = Math.floor(newCaught / PREMIUM_MILESTONE) > Math.floor(oldCaught / PREMIUM_MILESTONE);
|
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) {
|
if (s.wasps > 0) {
|
||||||
s.wasps -= 1;
|
s.wasps -= 1;
|
||||||
s.waspsKilled += 1;
|
s.waspsKilled += 1;
|
||||||
|
bumpDaily(s, 'wasp', 1);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return toDto(mine);
|
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> {
|
export async function reportNetTorn(login: string, now: number = Date.now()): Promise<ButterflyStats> {
|
||||||
const mine = await mutateUser(login, now, (s) => {
|
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);
|
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> {
|
export async function robbery(login: string, now: number = Date.now()): Promise<ButterflyStats> {
|
||||||
const mine = await mutateUser(login, now, (s) => {
|
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);
|
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> {
|
export async function defeatThief(login: string, now: number = Date.now()): Promise<ButterflyStats> {
|
||||||
const mine = await mutateUser(login, now, (s) => {
|
const mine = await mutateUser(login, now, (s) => {
|
||||||
s.thievesDefeated += 1;
|
s.thievesDefeated += 1;
|
||||||
s.coins += THIEF_REWARD;
|
s.coins += THIEF_REWARD;
|
||||||
|
bumpDaily(s, 'thief', 1);
|
||||||
});
|
});
|
||||||
return toDto(mine);
|
return toDto(mine);
|
||||||
}
|
}
|
||||||
@@ -446,6 +567,103 @@ export async function defeatCaterpillar(login: string, now: number = Date.now())
|
|||||||
return toDto(mine);
|
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
|
* 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ů).
|
* (při shodě dle počtu zlatých motýlů).
|
||||||
|
|||||||
@@ -12,10 +12,18 @@ import {
|
|||||||
defeatThief,
|
defeatThief,
|
||||||
caterpillarAte,
|
caterpillarAte,
|
||||||
defeatCaterpillar,
|
defeatCaterpillar,
|
||||||
|
mothHit,
|
||||||
|
buyPremium,
|
||||||
|
waspSpray,
|
||||||
|
buyInsurance,
|
||||||
|
buyUpgrade,
|
||||||
|
getAchievements,
|
||||||
getLeaderboard,
|
getLeaderboard,
|
||||||
InsufficientCoinsError,
|
InsufficientCoinsError,
|
||||||
|
UpgradeError,
|
||||||
|
UpgradeId,
|
||||||
} from "../butterflies";
|
} from "../butterflies";
|
||||||
import { CatchButterfliesData } from "../../../types";
|
import { CatchButterfliesData, BuyUpgradeData } from "../../../types";
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
@@ -112,6 +120,68 @@ router.post("/defeatCaterpillar", async (req: Request, res, next) => {
|
|||||||
} catch (e: any) { next(e) }
|
} 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) => {
|
router.get("/leaderboard", async (req: Request, res, next) => {
|
||||||
try {
|
try {
|
||||||
getLogin(parseToken(req));
|
getLogin(parseToken(req));
|
||||||
|
|||||||
@@ -11,6 +11,13 @@ import {
|
|||||||
defeatThief,
|
defeatThief,
|
||||||
caterpillarAte,
|
caterpillarAte,
|
||||||
defeatCaterpillar,
|
defeatCaterpillar,
|
||||||
|
mothHit,
|
||||||
|
buyPremium,
|
||||||
|
waspSpray,
|
||||||
|
buyInsurance,
|
||||||
|
buyUpgrade,
|
||||||
|
getAchievements,
|
||||||
|
upgradeCost,
|
||||||
getLeaderboard,
|
getLeaderboard,
|
||||||
growPests,
|
growPests,
|
||||||
levelForCaught,
|
levelForCaught,
|
||||||
@@ -37,6 +44,14 @@ import {
|
|||||||
CATERPILLAR_EATS,
|
CATERPILLAR_EATS,
|
||||||
CATERPILLAR_REWARD,
|
CATERPILLAR_REWARD,
|
||||||
AUTO_REPAIR_MS,
|
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';
|
} from '../butterflies';
|
||||||
import { formatDate } from '../utils';
|
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', () => {
|
describe('getLeaderboard', () => {
|
||||||
test('řadí sestupně dle počtu chycených a respektuje limit', async () => {
|
test('řadí sestupně dle počtu chycených a respektuje limit', async () => {
|
||||||
await seed(USER, { caught: 100, coins: 0, goldenCaught: 2 });
|
await seed(USER, { caught: 100, coins: 0, goldenCaught: 2 });
|
||||||
|
|||||||
@@ -102,6 +102,18 @@ paths:
|
|||||||
$ref: "./paths/butterflies/caterpillarAte.yml"
|
$ref: "./paths/butterflies/caterpillarAte.yml"
|
||||||
/butterflies/defeatCaterpillar:
|
/butterflies/defeatCaterpillar:
|
||||||
$ref: "./paths/butterflies/defeatCaterpillar.yml"
|
$ref: "./paths/butterflies/defeatCaterpillar.yml"
|
||||||
|
/butterflies/moth:
|
||||||
|
$ref: "./paths/butterflies/moth.yml"
|
||||||
|
/butterflies/buyPremium:
|
||||||
|
$ref: "./paths/butterflies/buyPremium.yml"
|
||||||
|
/butterflies/waspSpray:
|
||||||
|
$ref: "./paths/butterflies/waspSpray.yml"
|
||||||
|
/butterflies/insurance:
|
||||||
|
$ref: "./paths/butterflies/insurance.yml"
|
||||||
|
/butterflies/buyUpgrade:
|
||||||
|
$ref: "./paths/butterflies/buyUpgrade.yml"
|
||||||
|
/butterflies/achievements:
|
||||||
|
$ref: "./paths/butterflies/achievements.yml"
|
||||||
/butterflies/leaderboard:
|
/butterflies/leaderboard:
|
||||||
$ref: "./paths/butterflies/leaderboard.yml"
|
$ref: "./paths/butterflies/leaderboard.yml"
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
get:
|
||||||
|
operationId: getButterflyAchievements
|
||||||
|
summary: Vrátí seznam achievementů (odznaků) přihlášeného uživatele.
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Seznam achievementů se stavem odemčení.
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "../../schemas/_index.yml#/ButterflyAchievementList"
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
post:
|
||||||
|
operationId: buyPremiumNet
|
||||||
|
summary: Koupí okamžitou aktivaci prémiové síťky za mince.
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Aktualizované statistiky po koupi.
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "../../schemas/_index.yml#/ButterflyStats"
|
||||||
|
"402":
|
||||||
|
description: Nedostatek mincí.
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
post:
|
||||||
|
operationId: buyUpgrade
|
||||||
|
summary: Koupí další úroveň trvalého vylepšení (cena se s úrovní zdvojnásobuje).
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "../../schemas/_index.yml#/ButterflyUpgradeRequest"
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Aktualizované statistiky po koupi vylepšení.
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "../../schemas/_index.yml#/ButterflyStats"
|
||||||
|
"400":
|
||||||
|
description: Neplatné vylepšení nebo maximální úroveň.
|
||||||
|
"402":
|
||||||
|
description: Nedostatek mincí.
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
post:
|
||||||
|
operationId: buyInsurance
|
||||||
|
summary: Koupí pojistku proti zloději (dočasně sníží ztrátu při okradení).
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Aktualizované statistiky po koupi pojistky.
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "../../schemas/_index.yml#/ButterflyStats"
|
||||||
|
"402":
|
||||||
|
description: Nedostatek mincí.
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
post:
|
||||||
|
operationId: reportMothHit
|
||||||
|
summary: Hráč omylem chytil černou můru – přijde o část úlovků.
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Aktualizované statistiky po chycení můry.
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "../../schemas/_index.yml#/ButterflyStats"
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
post:
|
||||||
|
operationId: buyWaspSpray
|
||||||
|
summary: Koupí vosí sprej – vyhubí všechny vosy.
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Aktualizované statistiky po použití spreje.
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "../../schemas/_index.yml#/ButterflyStats"
|
||||||
|
"402":
|
||||||
|
description: Nedostatek mincí.
|
||||||
@@ -945,8 +945,12 @@ ButterflyStats:
|
|||||||
- birdsScared
|
- birdsScared
|
||||||
- thievesDefeated
|
- thievesDefeated
|
||||||
- caterpillarsDefeated
|
- caterpillarsDefeated
|
||||||
|
- mothsHit
|
||||||
- repellentUntil
|
- repellentUntil
|
||||||
- netTornUntil
|
- netTornUntil
|
||||||
|
- insuranceUntil
|
||||||
|
- upgrades
|
||||||
|
- daily
|
||||||
properties:
|
properties:
|
||||||
caught:
|
caught:
|
||||||
description: Celkový počet chycených motýlů
|
description: Celkový počet chycených motýlů
|
||||||
@@ -987,12 +991,104 @@ ButterflyStats:
|
|||||||
caterpillarsDefeated:
|
caterpillarsDefeated:
|
||||||
description: Celkový počet poražených housenek
|
description: Celkový počet poražených housenek
|
||||||
type: integer
|
type: integer
|
||||||
|
mothsHit:
|
||||||
|
description: Kolikrát hráč omylem chytil černou můru
|
||||||
|
type: integer
|
||||||
repellentUntil:
|
repellentUntil:
|
||||||
description: Časové razítko (ms epoch), do kdy platí plašič ptáků; 0 = neaktivní
|
description: Časové razítko (ms epoch), do kdy platí plašič ptáků; 0 = neaktivní
|
||||||
type: integer
|
type: integer
|
||||||
netTornUntil:
|
netTornUntil:
|
||||||
description: Časové razítko (ms epoch), do kdy je síťka protržená; po uplynutí se sama zašije. 0 = celá
|
description: Časové razítko (ms epoch), do kdy je síťka protržená; po uplynutí se sama zašije. 0 = celá
|
||||||
type: integer
|
type: integer
|
||||||
|
insuranceUntil:
|
||||||
|
description: Časové razítko (ms epoch), do kdy platí pojistka proti zloději; 0 = neaktivní
|
||||||
|
type: integer
|
||||||
|
upgrades:
|
||||||
|
$ref: "#/ButterflyUpgrades"
|
||||||
|
daily:
|
||||||
|
$ref: "#/ButterflyDaily"
|
||||||
|
ButterflyUpgrades:
|
||||||
|
description: Úrovně trvalých vylepšení z obchodu
|
||||||
|
type: object
|
||||||
|
additionalProperties: false
|
||||||
|
required:
|
||||||
|
- net
|
||||||
|
- scarecrow
|
||||||
|
- reinforced
|
||||||
|
properties:
|
||||||
|
net:
|
||||||
|
description: Úroveň vylepšení velikosti síťky
|
||||||
|
type: integer
|
||||||
|
scarecrow:
|
||||||
|
description: Úroveň strašáka (zpomaluje přibývání ptáků)
|
||||||
|
type: integer
|
||||||
|
reinforced:
|
||||||
|
description: Úroveň zpevněné síťky (rychlejší samo-zašití)
|
||||||
|
type: integer
|
||||||
|
ButterflyDaily:
|
||||||
|
description: Denní úkol hráče
|
||||||
|
type: object
|
||||||
|
additionalProperties: false
|
||||||
|
required:
|
||||||
|
- taskId
|
||||||
|
- title
|
||||||
|
- progress
|
||||||
|
- target
|
||||||
|
- done
|
||||||
|
- reward
|
||||||
|
properties:
|
||||||
|
taskId:
|
||||||
|
description: Identifikátor druhu úkolu
|
||||||
|
type: string
|
||||||
|
title:
|
||||||
|
description: Popis úkolu pro uživatele
|
||||||
|
type: string
|
||||||
|
progress:
|
||||||
|
description: Aktuální postup
|
||||||
|
type: integer
|
||||||
|
target:
|
||||||
|
description: Cíl úkolu
|
||||||
|
type: integer
|
||||||
|
done:
|
||||||
|
description: Zda je úkol splněný (a odměna vyplacená)
|
||||||
|
type: boolean
|
||||||
|
reward:
|
||||||
|
description: Odměna v mincích za splnění
|
||||||
|
type: integer
|
||||||
|
ButterflyUpgradeRequest:
|
||||||
|
description: Požadavek na koupi trvalého vylepšení
|
||||||
|
type: object
|
||||||
|
additionalProperties: false
|
||||||
|
required:
|
||||||
|
- item
|
||||||
|
properties:
|
||||||
|
item:
|
||||||
|
description: Které vylepšení koupit
|
||||||
|
type: string
|
||||||
|
enum: [net, scarecrow, reinforced]
|
||||||
|
ButterflyAchievement:
|
||||||
|
description: Jeden achievement (odznak)
|
||||||
|
type: object
|
||||||
|
additionalProperties: false
|
||||||
|
required:
|
||||||
|
- id
|
||||||
|
- title
|
||||||
|
- description
|
||||||
|
- unlocked
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: string
|
||||||
|
title:
|
||||||
|
type: string
|
||||||
|
description:
|
||||||
|
type: string
|
||||||
|
unlocked:
|
||||||
|
type: boolean
|
||||||
|
ButterflyAchievementList:
|
||||||
|
description: Seznam achievementů
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: "#/ButterflyAchievement"
|
||||||
ButterflyCatchRequest:
|
ButterflyCatchRequest:
|
||||||
description: Dávka nachytaných motýlů k zaznamenání
|
description: Dávka nachytaných motýlů k zaznamenání
|
||||||
type: object
|
type: object
|
||||||
|
|||||||
Reference in New Issue
Block a user