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
@@ -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>
|
||||
)}
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user