feat: obchod, černé můry, netopýr, pavouk a denní úkoly u chytání motýlků
CI / Generate TypeScript types (push) Successful in 15s
CI / Server unit tests (push) Successful in 23s
CI / Build server (push) Successful in 29s
CI / Build client (push) Successful in 1m2s
CI / Playwright E2E tests (push) Successful in 1m33s
CI / Build and push Docker image (push) Successful in 49s
CI / Notify (push) Successful in 1s

- obchod s pomůckami a trvalými vylepšeními (větší síťka, strašák, zpevněná síťka) – mince mají trvalý smysl
- černé můry se míchají mezi motýly; chycení (i magnetem) ubere hodně úlovků
- ptáci silnější, plašič dražší a kratší, zloději chodí častěji podle bohatství
- denní úkol s odměnou + achievementy/odznaky
- netopýr v noci loví motýly, pavouk zamotá síťku pavučinou – oba na zaklikání
- nákupní hlášky rozlišují nedostatek mincí od jiné chyby; herní smyčka se pozastaví při skryté záložce

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Stánek Pavel
2026-07-27 10:23:09 +02:00
co-authored by Claude Opus 4.8
parent e3f439bde0
commit 252d105408
22 changed files with 1384 additions and 43 deletions
+18
View File
@@ -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

+34
View File
@@ -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

+27
View File
@@ -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

+81
View File
@@ -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) ----------------------------------------------------
.butterfly-critter {
@@ -93,6 +104,42 @@
cursor: crosshair;
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í --------------------------------------------------------
@@ -139,6 +186,11 @@
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.
// Má přednost před grabbed kurzorem.
&.stunned {
@@ -215,6 +267,17 @@
font-size: 1.35rem;
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 {
@@ -255,6 +318,24 @@
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 {
display: inline-flex;
flex-direction: column;
+301 -20
View File
@@ -1,6 +1,7 @@
import React, { useEffect, useRef, useCallback, useState } from 'react';
import { useButterflyStats, RewardEvent } from './hooks/useButterflyStats';
import ButterflyLeaderboardModal from './components/modals/ButterflyLeaderboardModal';
import ButterflyShopModal from './components/modals/ButterflyShopModal';
// Barevné varianty motýlů (soubory v public/, referencované root-absolutně)
const BUTTERFLY_VARIANTS = [
@@ -12,6 +13,8 @@ const BUTTERFLY_VARIANTS = [
const GOLDEN_VARIANT = '/butterfly-golden.svg';
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_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)
const PREMIUM_DURATION_MS = 60_000;
export const REPAIR_COST = 20;
export const REPELLENT_COST = 60;
export const REPELLENT_COST = 70;
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)
const THIEF_HP = 22;
@@ -43,10 +49,23 @@ interface ButterflyData {
x: number; y: number;
dir: number; speed: number; size: number;
golden: boolean;
/** Černá můra chycení stojí úlovky (nepočítá se jako úlovek) */
moth: boolean;
bobFreq1: number; bobPhase1: number; bobAmp1: 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. */
interface CritterData {
el: HTMLDivElement;
@@ -79,15 +98,19 @@ interface SceneCallbacks {
onCatch: (golden: boolean) => void;
getCoins: () => number;
getCaught: () => number;
getNetUpgrade: () => number;
onTornChange: (torn: boolean) => void;
onCombo: (count: number) => void;
onSting: () => void;
onSwatWasp: () => void;
onMothCaught: () => void;
onRobbery: () => void;
onDefeatThief: () => void;
onCaterpillarAte: () => void;
onDefeatCaterpillar: () => void;
onStalkerAppear: (type: 'thief' | 'caterpillar') => void;
onBatShooed: () => void;
onWebCleared: () => void;
}
interface FlyingButterfliesProps {
@@ -118,6 +141,7 @@ class ButterflyScene {
private netX: number = 0;
private netY: number = 0;
private cb: SceneCallbacks;
private handleVisibility: () => void;
private torn: boolean = false;
private premiumUntil: number = 0;
@@ -126,6 +150,13 @@ class ButterflyScene {
private nextCatchAt: number = 0;
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 lastCatchTs: 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_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) {
this.viewport = el;
this.world = document.createElement('div');
@@ -166,11 +206,22 @@ class ButterflyScene {
this.width = this.viewport.offsetWidth;
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 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 hoopOffsetY(): number { return this.netSize * ButterflyScene.HOOP_FRAC_Y; }
@@ -189,14 +240,23 @@ class ButterflyScene {
b.bobPhase2 = Math.random() * Math.PI * 2;
b.bobAmp2 = Math.random() * 0.6 + 0.4;
b.golden = Math.random() < GOLDEN_CHANCE;
if (b.golden) {
// Nejdřív můra, jinak vzácně zlatý, jinak běžná varianta
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.classList.remove('moth');
b.sprite.classList.add('golden');
b.size = Math.random() * 0.5 + 1.0;
} else {
b.sprite.style.backgroundImage = `url(${this.variants[Math.floor(Math.random() * this.variants.length)]})`;
b.sprite.classList.remove('golden');
b.sprite.classList.remove('moth');
}
b.sprite.style.animationDuration = `${Math.random() * 0.25 + 0.3}s`;
};
@@ -267,11 +327,28 @@ class ButterflyScene {
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 => {
this.cb.onCatch(b.golden);
this.spawnFx(b.golden ? `+${GOLD_VALUE}` : '+1', b.golden ? 'butterfly-catch-fx golden' : 'butterfly-catch-fx', atX, atY);
this.resetButterfly(b);
this.nextCatchAt = this.timer + ButterflyScene.CATCH_COOLDOWN;
const now = Date.now();
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)
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 half = ButterflyScene.BASE_SIZE / 2;
for (const b of this.butterflies) {
const dx = (b.x + half * b.size) - this.netX;
const dy = (b.y + half * b.size) - this.netY;
if (dx * dx + dy * dy < r2) {
this.registerCatch(b, this.netX, this.netY);
this.catchOne(b, this.netX, this.netY);
this.popNet();
return; // jen jeden za cooldown
}
@@ -300,7 +377,7 @@ class ButterflyScene {
};
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;
this.lastMagnetAt = this.timer;
const half = ButterflyScene.BASE_SIZE / 2;
@@ -312,7 +389,7 @@ class ButterflyScene {
const d2 = dx * dx + dy * dy;
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 => {
@@ -506,10 +583,19 @@ class ButterflyScene {
this.nextStalkerAt = this.timer + 600; // zkus to znovu za ~10 s
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.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));
this.nextStalkerAt = this.timer + Math.round(gap * wealthFactor);
};
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 ------------------------------------------------------
private initNet = (): void => {
@@ -627,7 +844,7 @@ class ButterflyScene {
sprite.className = 'butterfly-sprite';
el.appendChild(sprite);
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,
};
this.resetButterfly(b);
@@ -637,6 +854,7 @@ class ButterflyScene {
this.viewport.appendChild(this.world);
if (this.netEnabled) this.initNet();
window.addEventListener('resize', this.handleResize);
document.addEventListener('visibilitychange', this.handleVisibility);
};
private wasPremium: boolean = false;
@@ -654,6 +872,10 @@ class ButterflyScene {
this.updateBirds();
this.trySpawnStalker();
this.updateStalker();
this.trySpawnBat();
this.updateBat();
this.trySpawnWeb();
this.updateWeb();
const stunned = this.timer < this.stunUntil;
if (stunned !== this.wasStunned) {
@@ -674,6 +896,8 @@ class ButterflyScene {
if (this.comboTimer) window.clearTimeout(this.comboTimer);
for (const w of this.wasps) w.cleanup?.();
this.removeStalker();
this.removeBat();
this.removeWeb();
if (this.world && this.world.parentNode) this.world.parentNode.removeChild(this.world);
this.net.removeEventListener('pointerdown', this.onPointerDown);
window.removeEventListener('pointermove', this.onPointerMove);
@@ -681,6 +905,7 @@ class ButterflyScene {
window.removeEventListener('pointercancel', this.onPointerUp);
if (this.net.parentNode) this.net.parentNode.removeChild(this.net);
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 [flash, setFlash] = useState<string | null>(null);
const [leaderboardOpen, setLeaderboardOpen] = useState(false);
const [shopOpen, setShopOpen] = useState(false);
const [nowTs, setNowTs] = useState(() => Date.now());
const flashTimer = useRef<number | null>(null);
@@ -726,23 +952,29 @@ const FlyingButterflies: React.FC<FlyingButterfliesProps> = ({
const {
stats, displayCaught, coinsRef, reportCatch, repair,
killWasp, buyRepellent, reportTear, reportRobbery, defeatThief,
caterpillarAte, defeatCaterpillar,
caterpillarAte, defeatCaterpillar, mothHit, buyPremium, waspSpray, buyInsurance, buyUpgrade,
} = useButterflyStats(handleReward);
const netUpgradeRef = useRef(0);
netUpgradeRef.current = stats?.upgrades?.net ?? 0;
const noop = () => { };
const callbacksRef = useRef<SceneCallbacks>({
onCatch: () => { }, getCoins: () => 0, getCaught: () => 0, onTornChange: () => { },
onCombo: () => { }, onSting: () => { }, onSwatWasp: () => { },
onRobbery: () => { }, onDefeatThief: () => { }, onCaterpillarAte: () => { },
onDefeatCaterpillar: () => { }, onStalkerAppear: () => { },
onCatch: noop, getCoins: () => 0, getCaught: () => 0, getNetUpgrade: () => 0, onTornChange: noop,
onCombo: noop, onSting: noop, onSwatWasp: noop, onMothCaught: noop,
onRobbery: noop, onDefeatThief: noop, onCaterpillarAte: noop,
onDefeatCaterpillar: noop, onStalkerAppear: noop, onBatShooed: noop, onWebCleared: noop,
});
callbacksRef.current = {
onCatch: (golden) => reportCatch(golden),
getCoins: () => coinsRef.current,
getCaught: () => stats?.caught ?? 0,
getNetUpgrade: () => netUpgradeRef.current,
onTornChange: (t) => { if (t) void reportTear(); },
onCombo: (c) => setCombo(c),
onSting: () => showFlash('🐝 Au! Vosa tě žihla síťka teď 5 s nechytá. Zaklikej vosy!'),
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.'); },
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ů!'); },
@@ -750,6 +982,8 @@ const FlyingButterflies: React.FC<FlyingButterfliesProps> = ({
onStalkerAppear: (type) => showFlash(type === 'thief'
? '🦹 Zloděj! Míří k tvým mincím zaklikej ho (chce to hodně ran)!'
: '🐛 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(() => {
@@ -758,15 +992,19 @@ const FlyingButterflies: React.FC<FlyingButterfliesProps> = ({
onCatch: (g) => callbacksRef.current.onCatch(g),
getCoins: () => callbacksRef.current.getCoins(),
getCaught: () => callbacksRef.current.getCaught(),
getNetUpgrade: () => callbacksRef.current.getNetUpgrade(),
onTornChange: (t) => callbacksRef.current.onTornChange(t),
onCombo: (c) => callbacksRef.current.onCombo(c),
onSting: () => callbacksRef.current.onSting(),
onSwatWasp: () => callbacksRef.current.onSwatWasp(),
onMothCaught: () => callbacksRef.current.onMothCaught(),
onRobbery: () => callbacksRef.current.onRobbery(),
onDefeatThief: () => callbacksRef.current.onDefeatThief(),
onCaterpillarAte: () => callbacksRef.current.onCaterpillarAte(),
onDefeatCaterpillar: () => callbacksRef.current.onDefeatCaterpillar(),
onStalkerAppear: (t) => callbacksRef.current.onStalkerAppear(t),
onBatShooed: () => callbacksRef.current.onBatShooed(),
onWebCleared: () => callbacksRef.current.onWebCleared(),
});
sceneRef.current.init();
sceneRef.current.render();
@@ -818,10 +1056,34 @@ const FlyingButterflies: React.FC<FlyingButterfliesProps> = ({
if (ok) sceneRef.current?.repairNetExternally();
}, [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 ok = await buyRepellent();
if (ok) showFlash('🦅🚫 Plašič ptáků koupen ptáci na chvíli zmizeli.');
}, [buyRepellent, showFlash]);
flashBuy(await buyRepellent(), '🦅🚫 Plašič ptáků koupen ptáci na chvíli zmizeli.');
}, [buyRepellent, flashBuy]);
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 (
<>
@@ -899,7 +1161,26 @@ const FlyingButterflies: React.FC<FlyingButterfliesProps> = ({
)}
</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} />
<ButterflyShopModal
isOpen={shopOpen}
onClose={() => setShopOpen(false)}
stats={stats}
onBuyPremium={onBuyPremium}
onWaspSpray={onWaspSpray}
onRepellent={onRepellentClick}
onBuyInsurance={onBuyInsurance}
onBuyUpgrade={onBuyUpgrade}
/>
</div>
)}
</>
@@ -43,7 +43,38 @@
.butterfly-leaderboard-heading {
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 {
@@ -1,6 +1,9 @@
import { useEffect, useState } from "react";
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 "./ButterflyLeaderboardModal.scss";
@@ -12,20 +15,25 @@ type Props = {
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>) {
const auth = useAuth();
const [entries, setEntries] = useState<ButterflyLeaderboardEntry[]>();
const [achievements, setAchievements] = useState<ButterflyAchievement[]>();
const [loading, setLoading] = useState(false);
useEffect(() => {
if (!isOpen) return;
let cancelled = false;
setLoading(true);
getButterflyLeaderboard({ query: { limit: 20 } })
.then(res => { if (!cancelled) setEntries(res.data ?? []); })
.catch(() => { if (!cancelled) setEntries([]); })
.finally(() => { if (!cancelled) setLoading(false); });
Promise.all([
getButterflyLeaderboard({ query: { limit: 20 } }).then(r => r.data ?? []).catch(() => []),
getButterflyAchievements().then(r => r.data ?? []).catch(() => []),
]).then(([lb, ach]) => {
if (cancelled) return;
setEntries(lb);
setAchievements(ach);
}).finally(() => { if (!cancelled) setLoading(false); });
return () => { cancelled = true; };
}, [isOpen]);
@@ -52,6 +60,24 @@ export default function ButterflyLeaderboardModal({ isOpen, onClose, myStats }:
</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>
{loading && <p className="text-center text-muted mb-0">Načítám žebříček</p>}
{!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>
);
}
+44 -4
View File
@@ -11,8 +11,19 @@ import {
defeatThief as defeatThiefApi,
reportCaterpillarAte as caterpillarAteApi,
defeatCaterpillar as defeatCaterpillarApi,
reportMothHit as mothHitApi,
buyPremiumNet as buyPremiumApi,
buyWaspSpray as waspSprayApi,
buyInsurance as buyInsuranceApi,
buyUpgrade as buyUpgradeApi,
} 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. */
const FLUSH_DEBOUNCE_MS = 1500;
/** 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 */ }
}, [applyStats]);
const buyRepellent = useCallback(async (): Promise<boolean> => {
const buyRepellent = useCallback(async (): Promise<BuyResult> => {
try {
const res = await buyRepellentApi();
if (res.data) { applyStats(res.data); return true; }
return false;
} catch { return false; }
if (res.data) { applyStats(res.data); return 'ok'; }
return res.response?.status === 402 ? 'poor' : 'error';
} catch { return 'error'; }
}, [applyStats]);
const reportTear = useCallback(async () => {
@@ -147,6 +158,34 @@ export function useButterflyStats(onReward?: (e: RewardEvent) => void) {
} catch { /* ignore */ }
}, [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. */
const defeatCaterpillar = useCallback(async () => {
try {
@@ -203,5 +242,6 @@ export function useButterflyStats(onReward?: (e: RewardEvent) => void) {
return {
stats, displayCaught, coinsRef, reportCatch, repair, killWasp, buyRepellent,
reportTear, reportRobbery, defeatThief, caterpillarAte, defeatCaterpillar,
mothHit, buyPremium, waspSpray, buyInsurance, buyUpgrade,
};
}