feat: anti-bot inspekce a vězení + přepínač hraní/objednávání
CI / Generate TypeScript types (push) Successful in 15s
CI / Server unit tests (push) Successful in 24s
CI / Build server (push) Successful in 31s
CI / Build client (push) Successful in 1m0s
CI / Playwright E2E tests (push) Successful in 1m30s
CI / Build and push Docker image (push) Successful in 1m6s
CI / Notify (push) Successful in 5s
CI / Generate TypeScript types (push) Successful in 15s
CI / Server unit tests (push) Successful in 24s
CI / Build server (push) Successful in 31s
CI / Build client (push) Successful in 1m0s
CI / Playwright E2E tests (push) Successful in 1m30s
CI / Build and push Docker image (push) Successful in 1m6s
CI / Notify (push) Successful in 5s
- přepínač Hraní/Objednávání; v objednávání hra nic neruší, v hraní herní vrstva odstíní kliknutí - scéna žije trvale (přepnutí režimu neobejde pavučinu, omráčení, zabavenou síťku ani ban) - anti-bot: ovládání jen reálnou myší (isTrusted/webdriver), občasná motýlí inspekce (chyť korunovaného) - neúspěch inspekce = zabavená síťka 45 s (opticky skrytá); opakovaně = serverový ban s mříží a odpočtem - během banu server nekredituje úlovky (F5/přepnutí-proof) - větší síťka se teď viditelně zvětší hned po koupi - changelog route přeskočí vadný JSON (nespadne celý endpoint) + oprava changelogu 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
252d105408
commit
377a350211
@@ -7,6 +7,49 @@
|
|||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
|
// V režimu hraní herní vrstva zachytává kliknutí, aby se nedalo omylem
|
||||||
|
// překliknout do objednávek. V režimu objednávání propouští vše dál (none).
|
||||||
|
&.playing {
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Přepínač režimu hraní / objednávání
|
||||||
|
.butterfly-mode-toggle {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 16px;
|
||||||
|
right: 16px;
|
||||||
|
z-index: 7;
|
||||||
|
pointer-events: auto;
|
||||||
|
padding: 8px 14px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(33, 37, 41, 0.88);
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.25);
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
transition: background 0.15s ease, transform 0.08s ease;
|
||||||
|
|
||||||
|
&:hover { background: rgba(33, 37, 41, 1); }
|
||||||
|
&:active { transform: scale(0.96); }
|
||||||
|
|
||||||
|
&.playing {
|
||||||
|
background: #2f9e44;
|
||||||
|
&:hover { background: #2b8a3e; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vrstva herních prvků (síťka, škůdci, pavučina) – skrývá se v režimu objednávání
|
||||||
|
.butterfly-game-layer {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.butterfly-scene {
|
.butterfly-scene {
|
||||||
@@ -73,6 +116,20 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Korunovaný motýl (anti-bot inspekce) – nepřehlédnutelná duhová záře
|
||||||
|
.butterfly-sprite.royal {
|
||||||
|
animation: butterfly-flap 0.35s ease-in-out infinite, butterfly-royal-glow 0.9s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes butterfly-royal-glow {
|
||||||
|
0%, 100% {
|
||||||
|
filter: drop-shadow(0 0 8px rgba(255, 215, 80, 1)) drop-shadow(0 0 16px rgba(120, 200, 255, 0.8));
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
filter: drop-shadow(0 0 16px rgba(255, 120, 220, 1)) drop-shadow(0 0 28px rgba(120, 255, 160, 0.9));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Černá můra – zlověstný tmavý nádech
|
// Černá můra – zlověstný tmavý nádech
|
||||||
.butterfly-sprite.moth {
|
.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));
|
filter: drop-shadow(0 0 4px rgba(120, 60, 160, 0.6)) drop-shadow(0 1px 1px rgba(0, 0, 0, 0.4));
|
||||||
@@ -626,3 +683,67 @@
|
|||||||
filter: saturate(1.3) drop-shadow(0 0 8px rgba(255, 215, 80, 0.95)) drop-shadow(0 0 16px rgba(255, 190, 50, 0.7));
|
filter: saturate(1.3) drop-shadow(0 0 8px rgba(255, 215, 80, 0.95)) drop-shadow(0 0 16px rgba(255, 190, 50, 0.7));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Anti-bot inspekce a vězení ---------------------------------------------
|
||||||
|
|
||||||
|
.butterfly-inspection {
|
||||||
|
position: fixed;
|
||||||
|
top: 16px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
z-index: 8;
|
||||||
|
pointer-events: none;
|
||||||
|
max-width: 92vw;
|
||||||
|
padding: 10px 16px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(28, 126, 214, 0.95);
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
box-shadow: 0 3px 12px rgba(0, 0, 0, 0.3);
|
||||||
|
animation: butterfly-flash-in 0.25s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.butterfly-jail {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 6;
|
||||||
|
pointer-events: none;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Svislé mříže přes celou obrazovku
|
||||||
|
.butterfly-jail-bars {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background:
|
||||||
|
repeating-linear-gradient(
|
||||||
|
90deg,
|
||||||
|
rgba(20, 22, 28, 0.92) 0px,
|
||||||
|
rgba(20, 22, 28, 0.92) 14px,
|
||||||
|
rgba(20, 22, 28, 0) 14px,
|
||||||
|
rgba(20, 22, 28, 0) 70px
|
||||||
|
);
|
||||||
|
box-shadow: inset 0 0 120px rgba(0, 0, 0, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.butterfly-jail-note {
|
||||||
|
position: relative;
|
||||||
|
text-align: center;
|
||||||
|
padding: 22px 26px;
|
||||||
|
border-radius: 16px;
|
||||||
|
background: rgba(255, 255, 255, 0.94);
|
||||||
|
box-shadow: 0 6px 24px rgba(0, 0, 0, 0.4);
|
||||||
|
max-width: 90vw;
|
||||||
|
|
||||||
|
.jail-emoji { font-size: 2.4rem; }
|
||||||
|
.jail-title { font-weight: 800; font-size: 1.2rem; color: #c92a2a; margin-top: 4px; }
|
||||||
|
.jail-sub { margin-top: 6px; font-size: 0.95rem; color: #333; font-variant-numeric: tabular-nums; }
|
||||||
|
.jail-hint { margin-top: 8px; font-size: 0.8rem; color: #868e96; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.butterfly-sprite.royal { animation: none; }
|
||||||
|
}
|
||||||
|
|||||||
@@ -28,6 +28,14 @@ const GOLD_VALUE = 25;
|
|||||||
const MOTH_PENALTY = 15;
|
const MOTH_PENALTY = 15;
|
||||||
/** Přírůstek dosahu chytání za úroveň vylepšení „větší síťka" */
|
/** Přírůstek dosahu chytání za úroveň vylepšení „větší síťka" */
|
||||||
const NET_UPGRADE_RADIUS = 9;
|
const NET_UPGRADE_RADIUS = 9;
|
||||||
|
/** Přírůstek vizuální velikosti síťky za úroveň vylepšení „větší síťka" (px) */
|
||||||
|
const NET_UPGRADE_SIZE = 20;
|
||||||
|
|
||||||
|
// Anti-bot „inspekce" – kontrola, že hraje člověk
|
||||||
|
const INSPECT_WINDOW_FRAMES = 8 * 60; // ~8 s na splnění
|
||||||
|
const INSPECT_PAUSE_FRAMES = 45 * 60; // ~45 s zabavená síťka při selhání
|
||||||
|
const CHECK_EVERY_CLEAN = 220; // po kolika úlovcích kontrola u „čistého" hraní
|
||||||
|
const CHECK_EVERY_SUSPICIOUS = 25; // při botích signálech mnohem dřív
|
||||||
|
|
||||||
// „Bossové" (zloděj / housenka)
|
// „Bossové" (zloděj / housenka)
|
||||||
const THIEF_HP = 22;
|
const THIEF_HP = 22;
|
||||||
@@ -43,6 +51,17 @@ function formatRemaining(ms: number): string {
|
|||||||
return m > 0 ? `${m}:${String(ss).padStart(2, '0')}` : `${ss} s`;
|
return m > 0 ? `${m}:${String(ss).padStart(2, '0')}` : `${ss} s`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Delší čas (ban) jako „H:MM:SS" nebo „M:SS". */
|
||||||
|
function formatBan(ms: number): string {
|
||||||
|
const s = Math.ceil(ms / 1000);
|
||||||
|
const h = Math.floor(s / 3600);
|
||||||
|
const m = Math.floor((s % 3600) / 60);
|
||||||
|
const ss = s % 60;
|
||||||
|
const mm = String(m).padStart(2, '0');
|
||||||
|
const s2 = String(ss).padStart(2, '0');
|
||||||
|
return h > 0 ? `${h}:${mm}:${s2}` : `${m}:${s2}`;
|
||||||
|
}
|
||||||
|
|
||||||
interface ButterflyData {
|
interface ButterflyData {
|
||||||
el: HTMLDivElement;
|
el: HTMLDivElement;
|
||||||
sprite: HTMLDivElement;
|
sprite: HTMLDivElement;
|
||||||
@@ -51,6 +70,8 @@ interface ButterflyData {
|
|||||||
golden: boolean;
|
golden: boolean;
|
||||||
/** Černá můra – chycení stojí úlovky (nepočítá se jako úlovek) */
|
/** Černá můra – chycení stojí úlovky (nepočítá se jako úlovek) */
|
||||||
moth: boolean;
|
moth: boolean;
|
||||||
|
/** Korunovaný motýl pro anti-bot inspekci */
|
||||||
|
royal: 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;
|
||||||
}
|
}
|
||||||
@@ -99,6 +120,8 @@ interface SceneCallbacks {
|
|||||||
getCoins: () => number;
|
getCoins: () => number;
|
||||||
getCaught: () => number;
|
getCaught: () => number;
|
||||||
getNetUpgrade: () => number;
|
getNetUpgrade: () => number;
|
||||||
|
/** Do kdy platí serverový ban (ms epoch); 0 = bez banu */
|
||||||
|
getBanUntil: () => number;
|
||||||
onTornChange: (torn: boolean) => void;
|
onTornChange: (torn: boolean) => void;
|
||||||
onCombo: (count: number) => void;
|
onCombo: (count: number) => void;
|
||||||
onSting: () => void;
|
onSting: () => void;
|
||||||
@@ -111,6 +134,8 @@ interface SceneCallbacks {
|
|||||||
onStalkerAppear: (type: 'thief' | 'caterpillar') => void;
|
onStalkerAppear: (type: 'thief' | 'caterpillar') => void;
|
||||||
onBatShooed: () => void;
|
onBatShooed: () => void;
|
||||||
onWebCleared: () => void;
|
onWebCleared: () => void;
|
||||||
|
/** Stav anti-bot inspekce: začátek / úspěch / neúspěch (zabavená síťka) */
|
||||||
|
onInspection: (state: 'start' | 'pass' | 'fail') => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FlyingButterfliesProps {
|
interface FlyingButterfliesProps {
|
||||||
@@ -123,10 +148,13 @@ interface FlyingButterfliesProps {
|
|||||||
class ButterflyScene {
|
class ButterflyScene {
|
||||||
private viewport: HTMLElement;
|
private viewport: HTMLElement;
|
||||||
private world: HTMLDivElement;
|
private world: HTMLDivElement;
|
||||||
|
/** Vrstva se všemi herními prvky (síťka, škůdci, pavučina) – jde skrýt v režimu objednávání */
|
||||||
|
private gameLayer: HTMLDivElement;
|
||||||
private butterflies: ButterflyData[] = [];
|
private butterflies: ButterflyData[] = [];
|
||||||
private birds: CritterData[] = [];
|
private birds: CritterData[] = [];
|
||||||
private wasps: CritterData[] = [];
|
private wasps: CritterData[] = [];
|
||||||
private stalker: StalkerData | null = null;
|
private stalker: StalkerData | null = null;
|
||||||
|
private interactive: boolean = false;
|
||||||
private numButterflies: number;
|
private numButterflies: number;
|
||||||
private variants: readonly string[];
|
private variants: readonly string[];
|
||||||
private width: number;
|
private width: number;
|
||||||
@@ -157,6 +185,16 @@ class ButterflyScene {
|
|||||||
private webClicks: number = 0;
|
private webClicks: number = 0;
|
||||||
private nextSpiderAt: number = 2400;
|
private nextSpiderAt: number = 2400;
|
||||||
|
|
||||||
|
// Anti-bot inspekce
|
||||||
|
private trustedControl: boolean = false; // síťku ovládá reálná (isTrusted) myš
|
||||||
|
private syntheticSeen: boolean = false; // zaznamenán syntetický event / webdriver
|
||||||
|
private catchesSinceCheck: number = 0;
|
||||||
|
private nextCheckThreshold: number = CHECK_EVERY_CLEAN;
|
||||||
|
private inspecting: boolean = false;
|
||||||
|
private inspectUntil: number = 0;
|
||||||
|
private inspectPauseUntil: number = 0; // do kdy je síťka „zabavená"
|
||||||
|
private royal: ButterflyData | null = null;
|
||||||
|
|
||||||
private comboCount: number = 0;
|
private comboCount: number = 0;
|
||||||
private lastCatchTs: number = 0;
|
private lastCatchTs: number = 0;
|
||||||
private comboTimer: number = 0;
|
private comboTimer: number = 0;
|
||||||
@@ -195,6 +233,8 @@ class ButterflyScene {
|
|||||||
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');
|
||||||
|
this.gameLayer = document.createElement('div');
|
||||||
|
this.gameLayer.className = 'butterfly-game-layer';
|
||||||
this.numButterflies = numButterflies;
|
this.numButterflies = numButterflies;
|
||||||
this.variants = variants;
|
this.variants = variants;
|
||||||
this.netEnabled = netEnabled;
|
this.netEnabled = netEnabled;
|
||||||
@@ -202,6 +242,8 @@ class ButterflyScene {
|
|||||||
this.width = this.viewport.offsetWidth;
|
this.width = this.viewport.offsetWidth;
|
||||||
this.height = this.viewport.offsetHeight;
|
this.height = this.viewport.offsetHeight;
|
||||||
this.net = document.createElement('div');
|
this.net = document.createElement('div');
|
||||||
|
// Automatizované prohlížeče (Playwright/Puppeteer/Selenium) se prozradí
|
||||||
|
if (typeof navigator !== 'undefined' && (navigator as any).webdriver) this.syntheticSeen = true;
|
||||||
this.handleResize = () => {
|
this.handleResize = () => {
|
||||||
this.width = this.viewport.offsetWidth;
|
this.width = this.viewport.offsetWidth;
|
||||||
this.height = this.viewport.offsetHeight;
|
this.height = this.viewport.offsetHeight;
|
||||||
@@ -217,7 +259,10 @@ class ButterflyScene {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
||||||
|
const base = this.premiumActive ? ButterflyScene.NET_SIZE_PREMIUM : ButterflyScene.NET_SIZE_NORMAL;
|
||||||
|
return base + NET_UPGRADE_SIZE * this.cb.getNetUpgrade();
|
||||||
|
}
|
||||||
private get catchRadius(): number {
|
private get catchRadius(): number {
|
||||||
const base = this.premiumActive ? ButterflyScene.CATCH_RADIUS_PREMIUM : ButterflyScene.CATCH_RADIUS_NORMAL;
|
const base = this.premiumActive ? ButterflyScene.CATCH_RADIUS_PREMIUM : ButterflyScene.CATCH_RADIUS_NORMAL;
|
||||||
return base + NET_UPGRADE_RADIUS * this.cb.getNetUpgrade();
|
return base + NET_UPGRADE_RADIUS * this.cb.getNetUpgrade();
|
||||||
@@ -225,6 +270,21 @@ class ButterflyScene {
|
|||||||
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; }
|
||||||
|
|
||||||
|
/** Do kdy je síťka „zabavená" (po neúspěšné inspekci) – nejde chytat ani ji vidět. */
|
||||||
|
private get confiscated(): boolean { return this.timer < this.inspectPauseUntil; }
|
||||||
|
/** Serverový ban (za opakované odhalení) – blokuje chytání. */
|
||||||
|
private get banned(): boolean { return Date.now() < this.cb.getBanUntil(); }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Přepne interaktivní režim (hraní vs. jen dekorace). Herní vrstvu jen skryje/ukáže,
|
||||||
|
* takže aktivní penalizace (pavučina, omráčení, zabavená síťka) přepnutím neobejdeš.
|
||||||
|
*/
|
||||||
|
public setInteractive = (v: boolean): void => {
|
||||||
|
this.interactive = v;
|
||||||
|
this.gameLayer.style.display = v ? '' : 'none';
|
||||||
|
if (!v) { this.netGrabbed = false; this.net.classList.remove('grabbed'); }
|
||||||
|
};
|
||||||
|
|
||||||
// --- Motýli --------------------------------------------------------------
|
// --- Motýli --------------------------------------------------------------
|
||||||
|
|
||||||
private resetButterfly = (b: ButterflyData): void => {
|
private resetButterfly = (b: ButterflyData): void => {
|
||||||
@@ -240,6 +300,19 @@ 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;
|
||||||
|
|
||||||
|
// Korunovaný motýl (inspekce) má přednost a zůstává jím, dokud inspekce trvá
|
||||||
|
if (b.royal) {
|
||||||
|
b.moth = false;
|
||||||
|
b.golden = false;
|
||||||
|
b.sprite.style.backgroundImage = `url(${GOLDEN_VARIANT})`;
|
||||||
|
b.sprite.classList.remove('golden', 'moth');
|
||||||
|
b.sprite.classList.add('royal');
|
||||||
|
b.size = 1.25;
|
||||||
|
b.sprite.style.animationDuration = `${Math.random() * 0.25 + 0.3}s`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
b.sprite.classList.remove('royal');
|
||||||
|
|
||||||
// Nejdřív můra, jinak vzácně zlatý, jinak běžná varianta
|
// Nejdřív můra, jinak vzácně zlatý, jinak běžná varianta
|
||||||
b.moth = Math.random() < MOTH_CHANCE;
|
b.moth = Math.random() < MOTH_CHANCE;
|
||||||
b.golden = !b.moth && Math.random() < GOLDEN_CHANCE;
|
b.golden = !b.moth && Math.random() < GOLDEN_CHANCE;
|
||||||
@@ -295,6 +368,9 @@ class ButterflyScene {
|
|||||||
|
|
||||||
private onPointerDown = (e: PointerEvent): void => {
|
private onPointerDown = (e: PointerEvent): void => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
// Síťku smí ovládat jen reálná myš (syntetické eventy mají isTrusted=false)
|
||||||
|
this.trustedControl = e.isTrusted && !(typeof navigator !== 'undefined' && (navigator as any).webdriver);
|
||||||
|
if (!e.isTrusted) this.syntheticSeen = true;
|
||||||
this.netGrabbed = true;
|
this.netGrabbed = true;
|
||||||
this.net.classList.add('grabbed');
|
this.net.classList.add('grabbed');
|
||||||
this.netX = e.clientX; this.netY = e.clientY;
|
this.netX = e.clientX; this.netY = e.clientY;
|
||||||
@@ -302,6 +378,7 @@ class ButterflyScene {
|
|||||||
};
|
};
|
||||||
private onPointerMove = (e: PointerEvent): void => {
|
private onPointerMove = (e: PointerEvent): void => {
|
||||||
if (!this.netGrabbed) return;
|
if (!this.netGrabbed) return;
|
||||||
|
if (!e.isTrusted) { this.syntheticSeen = true; this.trustedControl = false; }
|
||||||
this.netX = e.clientX; this.netY = e.clientY;
|
this.netX = e.clientX; this.netY = e.clientY;
|
||||||
this.updateNetTransform();
|
this.updateNetTransform();
|
||||||
};
|
};
|
||||||
@@ -330,6 +407,13 @@ class ButterflyScene {
|
|||||||
/** Zpracuje kontakt síťky s letícím tvorem – buď úlovek, nebo penalta za můru. */
|
/** 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 => {
|
private catchOne = (b: ButterflyData, atX: number, atY: number): void => {
|
||||||
this.nextCatchAt = this.timer + ButterflyScene.CATCH_COOLDOWN;
|
this.nextCatchAt = this.timer + ButterflyScene.CATCH_COOLDOWN;
|
||||||
|
// Během inspekce: chyť jen korunovaného, jinak neúspěch
|
||||||
|
if (this.inspecting) {
|
||||||
|
if (b.royal) this.passInspection();
|
||||||
|
else this.failInspection();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.catchesSinceCheck += 1;
|
||||||
if (b.moth) {
|
if (b.moth) {
|
||||||
// Chytit černou můru je chyba – penalta, žádné kombo
|
// Chytit černou můru je chyba – penalta, žádné kombo
|
||||||
this.cb.onMothCaught();
|
this.cb.onMothCaught();
|
||||||
@@ -362,7 +446,9 @@ 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.webbed || this.timer < this.stunUntil || this.timer < this.nextCatchAt) return;
|
// Síťku musí ovládat reálná myš; při zabavení/banu/pavučině/omráčení se nechytá
|
||||||
|
if (!this.trustedControl || this.confiscated || this.banned || 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) {
|
||||||
@@ -377,7 +463,8 @@ class ButterflyScene {
|
|||||||
};
|
};
|
||||||
|
|
||||||
private runMagnet = (): void => {
|
private runMagnet = (): void => {
|
||||||
if (this.torn || this.webbed || this.timer < this.stunUntil || this.timer < this.nextCatchAt) return;
|
if (this.inspecting || !this.trustedControl || this.confiscated || this.banned || 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;
|
||||||
@@ -392,6 +479,54 @@ class ButterflyScene {
|
|||||||
if (nearest) { this.catchOne(nearest, this.netX, this.netY); this.popNet(); }
|
if (nearest) { this.catchOne(nearest, this.netX, this.netY); this.popNet(); }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// --- Anti-bot inspekce ---------------------------------------------------
|
||||||
|
|
||||||
|
private scheduleNextCheck = (): void => {
|
||||||
|
this.catchesSinceCheck = 0;
|
||||||
|
const base = this.syntheticSeen ? CHECK_EVERY_SUSPICIOUS : CHECK_EVERY_CLEAN;
|
||||||
|
this.nextCheckThreshold = base + Math.floor(Math.random() * base * 0.4);
|
||||||
|
};
|
||||||
|
|
||||||
|
private clearRoyal = (): void => {
|
||||||
|
if (this.royal) {
|
||||||
|
this.royal.royal = false;
|
||||||
|
this.royal.sprite.classList.remove('royal');
|
||||||
|
this.resetButterfly(this.royal);
|
||||||
|
this.royal = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private startInspection = (): void => {
|
||||||
|
if (!this.butterflies.length) { this.scheduleNextCheck(); return; }
|
||||||
|
const b = this.butterflies[Math.floor(Math.random() * this.butterflies.length)];
|
||||||
|
b.royal = true;
|
||||||
|
b.moth = false;
|
||||||
|
b.golden = false;
|
||||||
|
b.sprite.style.backgroundImage = `url(${GOLDEN_VARIANT})`;
|
||||||
|
b.sprite.classList.remove('moth', 'golden');
|
||||||
|
b.sprite.classList.add('royal');
|
||||||
|
this.royal = b;
|
||||||
|
this.inspecting = true;
|
||||||
|
this.inspectUntil = this.timer + INSPECT_WINDOW_FRAMES;
|
||||||
|
this.cb.onInspection('start');
|
||||||
|
};
|
||||||
|
|
||||||
|
private passInspection = (): void => {
|
||||||
|
this.inspecting = false;
|
||||||
|
this.clearRoyal();
|
||||||
|
this.scheduleNextCheck();
|
||||||
|
this.spawnFx('👍', 'butterfly-swat-fx', this.netX, this.netY);
|
||||||
|
this.cb.onInspection('pass');
|
||||||
|
};
|
||||||
|
|
||||||
|
private failInspection = (): void => {
|
||||||
|
this.inspecting = false;
|
||||||
|
this.clearRoyal();
|
||||||
|
this.inspectPauseUntil = this.timer + INSPECT_PAUSE_FRAMES;
|
||||||
|
this.scheduleNextCheck();
|
||||||
|
this.cb.onInspection('fail');
|
||||||
|
};
|
||||||
|
|
||||||
public activatePremiumNet = (): void => {
|
public activatePremiumNet = (): void => {
|
||||||
this.premiumUntil = Date.now() + PREMIUM_DURATION_MS;
|
this.premiumUntil = Date.now() + PREMIUM_DURATION_MS;
|
||||||
this.applyNetAppearance();
|
this.applyNetAppearance();
|
||||||
@@ -460,7 +595,7 @@ class ButterflyScene {
|
|||||||
c.cleanup = () => el.removeEventListener('pointerdown', swat);
|
c.cleanup = () => el.removeEventListener('pointerdown', swat);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.world.appendChild(el);
|
this.gameLayer.appendChild(el);
|
||||||
return c;
|
return c;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -647,7 +782,7 @@ class ButterflyScene {
|
|||||||
el.addEventListener('pointerdown', hit);
|
el.addEventListener('pointerdown', hit);
|
||||||
s.cleanup = () => el.removeEventListener('pointerdown', hit);
|
s.cleanup = () => el.removeEventListener('pointerdown', hit);
|
||||||
|
|
||||||
this.viewport.appendChild(el);
|
this.gameLayer.appendChild(el);
|
||||||
this.stalker = s;
|
this.stalker = s;
|
||||||
this.cb.onStalkerAppear(type);
|
this.cb.onStalkerAppear(type);
|
||||||
};
|
};
|
||||||
@@ -732,7 +867,7 @@ class ButterflyScene {
|
|||||||
};
|
};
|
||||||
el.addEventListener('pointerdown', hit);
|
el.addEventListener('pointerdown', hit);
|
||||||
bat.cleanup = () => el.removeEventListener('pointerdown', hit);
|
bat.cleanup = () => el.removeEventListener('pointerdown', hit);
|
||||||
this.world.appendChild(el);
|
this.gameLayer.appendChild(el);
|
||||||
this.bat = bat;
|
this.bat = bat;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -799,7 +934,7 @@ class ButterflyScene {
|
|||||||
};
|
};
|
||||||
el.addEventListener('pointerdown', strip);
|
el.addEventListener('pointerdown', strip);
|
||||||
(el as any)._cleanup = () => el.removeEventListener('pointerdown', strip);
|
(el as any)._cleanup = () => el.removeEventListener('pointerdown', strip);
|
||||||
this.viewport.appendChild(el);
|
this.gameLayer.appendChild(el);
|
||||||
this.web = el;
|
this.web = el;
|
||||||
this.spawnFx('🕸️ Pavučina!', 'butterfly-tear-fx', this.netX, this.netY - 20);
|
this.spawnFx('🕸️ Pavučina!', 'butterfly-tear-fx', this.netX, this.netY - 20);
|
||||||
};
|
};
|
||||||
@@ -831,7 +966,7 @@ class ButterflyScene {
|
|||||||
window.addEventListener('pointermove', this.onPointerMove);
|
window.addEventListener('pointermove', this.onPointerMove);
|
||||||
window.addEventListener('pointerup', this.onPointerUp);
|
window.addEventListener('pointerup', this.onPointerUp);
|
||||||
window.addEventListener('pointercancel', this.onPointerUp);
|
window.addEventListener('pointercancel', this.onPointerUp);
|
||||||
this.viewport.appendChild(this.net);
|
this.gameLayer.appendChild(this.net);
|
||||||
};
|
};
|
||||||
|
|
||||||
public init = (): void => {
|
public init = (): void => {
|
||||||
@@ -844,7 +979,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, moth: false,
|
el, sprite, x: 0, y: 0, dir: 1, speed: 1, size: 1, golden: false, moth: false, royal: 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);
|
||||||
@@ -852,6 +987,8 @@ class ButterflyScene {
|
|||||||
this.world.appendChild(el);
|
this.world.appendChild(el);
|
||||||
}
|
}
|
||||||
this.viewport.appendChild(this.world);
|
this.viewport.appendChild(this.world);
|
||||||
|
this.viewport.appendChild(this.gameLayer);
|
||||||
|
this.gameLayer.style.display = this.interactive ? '' : 'none';
|
||||||
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);
|
document.addEventListener('visibilitychange', this.handleVisibility);
|
||||||
@@ -859,13 +996,19 @@ class ButterflyScene {
|
|||||||
|
|
||||||
private wasPremium: boolean = false;
|
private wasPremium: boolean = false;
|
||||||
private wasStunned: boolean = false;
|
private wasStunned: boolean = false;
|
||||||
|
private lastNetUpgrade: number = 0;
|
||||||
|
|
||||||
public render = (): void => {
|
public render = (): void => {
|
||||||
for (const b of this.butterflies) this.updateButterfly(b);
|
for (const b of this.butterflies) this.updateButterfly(b);
|
||||||
|
|
||||||
if (this.netEnabled) {
|
if (this.netEnabled && this.interactive) {
|
||||||
const nowPremium = this.premiumActive;
|
const nowPremium = this.premiumActive;
|
||||||
if (this.wasPremium && !nowPremium) this.applyNetAppearance();
|
const nu = this.cb.getNetUpgrade();
|
||||||
|
// Přepočítej vzhled síťky při změně prémiové síťky i po koupi vylepšení
|
||||||
|
if ((this.wasPremium && !nowPremium) || nu !== this.lastNetUpgrade) {
|
||||||
|
this.lastNetUpgrade = nu;
|
||||||
|
this.applyNetAppearance();
|
||||||
|
}
|
||||||
this.wasPremium = nowPremium;
|
this.wasPremium = nowPremium;
|
||||||
|
|
||||||
this.updateWasps();
|
this.updateWasps();
|
||||||
@@ -883,6 +1026,19 @@ class ButterflyScene {
|
|||||||
this.wasStunned = stunned;
|
this.wasStunned = stunned;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Zabavená síťka (neúspěch inspekce) nebo ban → síťku opticky skryj, běží jen nápis
|
||||||
|
this.net.style.visibility = (this.confiscated || this.banned) ? 'hidden' : '';
|
||||||
|
|
||||||
|
// Anti-bot inspekce: konec časového okna = neúspěch
|
||||||
|
if (this.inspecting && this.timer >= this.inspectUntil) {
|
||||||
|
this.failInspection();
|
||||||
|
}
|
||||||
|
// Spuštění inspekce po dosažení prahu (jen při reálném hraní)
|
||||||
|
if (!this.inspecting && !this.confiscated && !this.banned && this.netGrabbed
|
||||||
|
&& this.trustedControl && this.catchesSinceCheck >= this.nextCheckThreshold) {
|
||||||
|
this.startInspection();
|
||||||
|
}
|
||||||
|
|
||||||
if (this.netGrabbed) this.checkCatches();
|
if (this.netGrabbed) this.checkCatches();
|
||||||
if (nowPremium) this.runMagnet();
|
if (nowPremium) this.runMagnet();
|
||||||
}
|
}
|
||||||
@@ -903,7 +1059,7 @@ class ButterflyScene {
|
|||||||
window.removeEventListener('pointermove', this.onPointerMove);
|
window.removeEventListener('pointermove', this.onPointerMove);
|
||||||
window.removeEventListener('pointerup', this.onPointerUp);
|
window.removeEventListener('pointerup', this.onPointerUp);
|
||||||
window.removeEventListener('pointercancel', this.onPointerUp);
|
window.removeEventListener('pointercancel', this.onPointerUp);
|
||||||
if (this.net.parentNode) this.net.parentNode.removeChild(this.net);
|
if (this.gameLayer.parentNode) this.gameLayer.parentNode.removeChild(this.gameLayer);
|
||||||
window.removeEventListener('resize', this.handleResize);
|
window.removeEventListener('resize', this.handleResize);
|
||||||
document.removeEventListener('visibilitychange', this.handleVisibility);
|
document.removeEventListener('visibilitychange', this.handleVisibility);
|
||||||
};
|
};
|
||||||
@@ -932,6 +1088,20 @@ const FlyingButterflies: React.FC<FlyingButterfliesProps> = ({
|
|||||||
const [nowTs, setNowTs] = useState(() => Date.now());
|
const [nowTs, setNowTs] = useState(() => Date.now());
|
||||||
const flashTimer = useRef<number | null>(null);
|
const flashTimer = useRef<number | null>(null);
|
||||||
|
|
||||||
|
// Režim: 'order' = jen dekorace (appka bez rušení), 'play' = plná hra
|
||||||
|
const [mode, setMode] = useState<'order' | 'play'>(() => {
|
||||||
|
try { return localStorage.getItem('butterflyMode') === 'play' ? 'play' : 'order'; }
|
||||||
|
catch { return 'order'; }
|
||||||
|
});
|
||||||
|
const interactive = enableNet && mode === 'play';
|
||||||
|
const toggleMode = useCallback(() => {
|
||||||
|
setMode(m => {
|
||||||
|
const next = m === 'play' ? 'order' : 'play';
|
||||||
|
try { localStorage.setItem('butterflyMode', next); } catch { /* ignore */ }
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
const showFlash = useCallback((msg: string) => {
|
const showFlash = useCallback((msg: string) => {
|
||||||
setFlash(msg);
|
setFlash(msg);
|
||||||
if (flashTimer.current) window.clearTimeout(flashTimer.current);
|
if (flashTimer.current) window.clearTimeout(flashTimer.current);
|
||||||
@@ -949,27 +1119,33 @@ const FlyingButterflies: React.FC<FlyingButterfliesProps> = ({
|
|||||||
}
|
}
|
||||||
}, [showFlash]);
|
}, [showFlash]);
|
||||||
|
|
||||||
|
const [inspecting, setInspecting] = useState(false);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
stats, displayCaught, coinsRef, reportCatch, repair,
|
stats, displayCaught, coinsRef, reportCatch, repair,
|
||||||
killWasp, buyRepellent, reportTear, reportRobbery, defeatThief,
|
killWasp, buyRepellent, reportTear, reportRobbery, defeatThief,
|
||||||
caterpillarAte, defeatCaterpillar, mothHit, buyPremium, waspSpray, buyInsurance, buyUpgrade,
|
caterpillarAte, defeatCaterpillar, mothHit, reportInspectionFail,
|
||||||
|
buyPremium, waspSpray, buyInsurance, buyUpgrade,
|
||||||
} = useButterflyStats(handleReward);
|
} = useButterflyStats(handleReward);
|
||||||
|
|
||||||
const netUpgradeRef = useRef(0);
|
const netUpgradeRef = useRef(0);
|
||||||
netUpgradeRef.current = stats?.upgrades?.net ?? 0;
|
netUpgradeRef.current = stats?.upgrades?.net ?? 0;
|
||||||
|
const banUntilRef = useRef(0);
|
||||||
|
banUntilRef.current = stats?.banUntil ?? 0;
|
||||||
|
|
||||||
const noop = () => { };
|
const noop = () => { };
|
||||||
const callbacksRef = useRef<SceneCallbacks>({
|
const callbacksRef = useRef<SceneCallbacks>({
|
||||||
onCatch: noop, getCoins: () => 0, getCaught: () => 0, getNetUpgrade: () => 0, onTornChange: noop,
|
onCatch: noop, getCoins: () => 0, getCaught: () => 0, getNetUpgrade: () => 0, getBanUntil: () => 0, onTornChange: noop,
|
||||||
onCombo: noop, onSting: noop, onSwatWasp: noop, onMothCaught: noop,
|
onCombo: noop, onSting: noop, onSwatWasp: noop, onMothCaught: noop,
|
||||||
onRobbery: noop, onDefeatThief: noop, onCaterpillarAte: noop,
|
onRobbery: noop, onDefeatThief: noop, onCaterpillarAte: noop,
|
||||||
onDefeatCaterpillar: noop, onStalkerAppear: noop, onBatShooed: noop, onWebCleared: noop,
|
onDefeatCaterpillar: noop, onStalkerAppear: noop, onBatShooed: noop, onWebCleared: noop, onInspection: 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,
|
getNetUpgrade: () => netUpgradeRef.current,
|
||||||
|
getBanUntil: () => banUntilRef.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!'),
|
||||||
@@ -984,8 +1160,17 @@ const FlyingButterflies: React.FC<FlyingButterfliesProps> = ({
|
|||||||
: '🐛 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!'),
|
onBatShooed: () => showFlash('🦇 Netopýr zahnán!'),
|
||||||
onWebCleared: () => showFlash('🕸️ Pavučina stržena – zase můžeš chytat.'),
|
onWebCleared: () => showFlash('🕸️ Pavučina stržena – zase můžeš chytat.'),
|
||||||
|
onInspection: (state) => {
|
||||||
|
if (state === 'start') { setInspecting(true); }
|
||||||
|
else if (state === 'pass') { setInspecting(false); showFlash('🕵️ Inspekce OK – hraj dál!'); }
|
||||||
|
else { setInspecting(false); void reportInspectionFail(); showFlash('👮 Inspekce neúspěšná – síťka zabavena na 45 s!'); }
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Aktuální interaktivita pro init (bez recreate scény při přepnutí režimu)
|
||||||
|
const interactiveRef = useRef(interactive);
|
||||||
|
interactiveRef.current = interactive;
|
||||||
|
|
||||||
const initialize = useCallback(() => {
|
const initialize = useCallback(() => {
|
||||||
if (containerRef.current) {
|
if (containerRef.current) {
|
||||||
sceneRef.current = new ButterflyScene(containerRef.current, numButterflies, butterflyVariants, enableNet, {
|
sceneRef.current = new ButterflyScene(containerRef.current, numButterflies, butterflyVariants, enableNet, {
|
||||||
@@ -993,6 +1178,7 @@ const FlyingButterflies: React.FC<FlyingButterfliesProps> = ({
|
|||||||
getCoins: () => callbacksRef.current.getCoins(),
|
getCoins: () => callbacksRef.current.getCoins(),
|
||||||
getCaught: () => callbacksRef.current.getCaught(),
|
getCaught: () => callbacksRef.current.getCaught(),
|
||||||
getNetUpgrade: () => callbacksRef.current.getNetUpgrade(),
|
getNetUpgrade: () => callbacksRef.current.getNetUpgrade(),
|
||||||
|
getBanUntil: () => callbacksRef.current.getBanUntil(),
|
||||||
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(),
|
||||||
@@ -1005,10 +1191,14 @@ const FlyingButterflies: React.FC<FlyingButterfliesProps> = ({
|
|||||||
onStalkerAppear: (t) => callbacksRef.current.onStalkerAppear(t),
|
onStalkerAppear: (t) => callbacksRef.current.onStalkerAppear(t),
|
||||||
onBatShooed: () => callbacksRef.current.onBatShooed(),
|
onBatShooed: () => callbacksRef.current.onBatShooed(),
|
||||||
onWebCleared: () => callbacksRef.current.onWebCleared(),
|
onWebCleared: () => callbacksRef.current.onWebCleared(),
|
||||||
|
onInspection: (s) => callbacksRef.current.onInspection(s),
|
||||||
});
|
});
|
||||||
sceneRef.current.init();
|
sceneRef.current.init();
|
||||||
|
sceneRef.current.setInteractive(interactiveRef.current);
|
||||||
sceneRef.current.render();
|
sceneRef.current.render();
|
||||||
}
|
}
|
||||||
|
// Scéna žije trvale; režim se přepíná přes setInteractive (viz efekt níže),
|
||||||
|
// aby přepnutí neobešlo aktivní penalizace.
|
||||||
}, [numButterflies, butterflyVariants, enableNet]);
|
}, [numButterflies, butterflyVariants, enableNet]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -1019,6 +1209,9 @@ const FlyingButterflies: React.FC<FlyingButterfliesProps> = ({
|
|||||||
};
|
};
|
||||||
}, [initialize]);
|
}, [initialize]);
|
||||||
|
|
||||||
|
// Přepnutí režimu hraní/objednávání jen přepne interaktivitu (bez recreate)
|
||||||
|
useEffect(() => { sceneRef.current?.setInteractive(interactive); }, [interactive]);
|
||||||
|
|
||||||
const wasps = stats?.wasps ?? 0;
|
const wasps = stats?.wasps ?? 0;
|
||||||
const birds = stats?.birds ?? 0;
|
const birds = stats?.birds ?? 0;
|
||||||
const netTornUntil = stats?.netTornUntil ?? 0;
|
const netTornUntil = stats?.netTornUntil ?? 0;
|
||||||
@@ -1042,14 +1235,17 @@ const FlyingButterflies: React.FC<FlyingButterfliesProps> = ({
|
|||||||
const repellentUntil = stats?.repellentUntil ?? 0;
|
const repellentUntil = stats?.repellentUntil ?? 0;
|
||||||
const repellentRemainingMs = Math.max(0, repellentUntil - nowTs);
|
const repellentRemainingMs = Math.max(0, repellentUntil - nowTs);
|
||||||
const repellentActive = repellentRemainingMs > 0;
|
const repellentActive = repellentRemainingMs > 0;
|
||||||
|
const banUntil = stats?.banUntil ?? 0;
|
||||||
|
const banned = banUntil > nowTs;
|
||||||
|
const banRemainingMs = Math.max(0, banUntil - nowTs);
|
||||||
|
|
||||||
// Tik po sekundách pro odpočty plašiče i protržené síťky (jen dokud běží)
|
// Tik po sekundách pro odpočty plašiče, protržené síťky a banu (jen dokud běží)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const until = Math.max(repellentUntil, netTornUntil);
|
const until = Math.max(repellentUntil, netTornUntil, banUntil);
|
||||||
if (until <= Date.now()) return;
|
if (until <= Date.now()) return;
|
||||||
const id = window.setInterval(() => setNowTs(Date.now()), 1000);
|
const id = window.setInterval(() => setNowTs(Date.now()), 1000);
|
||||||
return () => window.clearInterval(id);
|
return () => window.clearInterval(id);
|
||||||
}, [repellentUntil, netTornUntil]);
|
}, [repellentUntil, netTornUntil, banUntil]);
|
||||||
|
|
||||||
const onRepairClick = useCallback(async () => {
|
const onRepairClick = useCallback(async () => {
|
||||||
const ok = await repair();
|
const ok = await repair();
|
||||||
@@ -1087,8 +1283,38 @@ const FlyingButterflies: React.FC<FlyingButterfliesProps> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div ref={containerRef} className={className} />
|
<div ref={containerRef} className={interactive ? `${className} playing` : className} />
|
||||||
|
|
||||||
{enableNet && (
|
{enableNet && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={interactive ? 'butterfly-mode-toggle playing' : 'butterfly-mode-toggle'}
|
||||||
|
onClick={toggleMode}
|
||||||
|
title={interactive ? 'Přepnout na objednávání (vypne hru)' : 'Zapnout hraní s motýly'}
|
||||||
|
>
|
||||||
|
{interactive ? '🍽️ Objednávat' : '🎮 Hrát'}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{interactive && inspecting && !banned && (
|
||||||
|
<div className="butterfly-inspection">
|
||||||
|
🕵️ Inspekce! Chyť <b>zářícího královského motýla 👑</b> — a jiného se nedotkni!
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{interactive && banned && (
|
||||||
|
<div className="butterfly-jail">
|
||||||
|
<div className="butterfly-jail-bars" aria-hidden="true" />
|
||||||
|
<div className="butterfly-jail-note">
|
||||||
|
<div className="jail-emoji">🚔👮</div>
|
||||||
|
<div className="jail-title">Ve vězení za podvádění!</div>
|
||||||
|
<div className="jail-sub">Inspektor tě přistihl. Pustí tě za <b>{formatBan(banRemainingMs)}</b>.</div>
|
||||||
|
<div className="jail-hint">Chytání je zastavené. Objednávat můžeš dál – přepni vpravo dole.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{interactive && (
|
||||||
<div className="butterfly-hud">
|
<div className="butterfly-hud">
|
||||||
{flash && (
|
{flash && (
|
||||||
<div className="butterfly-flash" onClick={() => setFlash(null)} title="Klikni pro zavření">
|
<div className="butterfly-flash" onClick={() => setFlash(null)} title="Klikni pro zavření">
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
reportCaterpillarAte as caterpillarAteApi,
|
reportCaterpillarAte as caterpillarAteApi,
|
||||||
defeatCaterpillar as defeatCaterpillarApi,
|
defeatCaterpillar as defeatCaterpillarApi,
|
||||||
reportMothHit as mothHitApi,
|
reportMothHit as mothHitApi,
|
||||||
|
reportInspectionFail as inspectionFailApi,
|
||||||
buyPremiumNet as buyPremiumApi,
|
buyPremiumNet as buyPremiumApi,
|
||||||
buyWaspSpray as waspSprayApi,
|
buyWaspSpray as waspSprayApi,
|
||||||
buyInsurance as buyInsuranceApi,
|
buyInsurance as buyInsuranceApi,
|
||||||
@@ -166,6 +167,14 @@ export function useButterflyStats(onReward?: (e: RewardEvent) => void) {
|
|||||||
} catch { /* ignore */ }
|
} catch { /* ignore */ }
|
||||||
}, [applyStats]);
|
}, [applyStats]);
|
||||||
|
|
||||||
|
/** Selhání anti-bot inspekce – server počítá selhání a případně udělí ban. */
|
||||||
|
const reportInspectionFail = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const res = await inspectionFailApi();
|
||||||
|
if (res.data) applyStats(res.data);
|
||||||
|
} catch { /* ignore */ }
|
||||||
|
}, [applyStats]);
|
||||||
|
|
||||||
/** Vyhodnotí výsledek nákupu z odpovědi (402 = nedostatek mincí, jinak chyba). */
|
/** Vyhodnotí výsledek nákupu z odpovědi (402 = nedostatek mincí, jinak chyba). */
|
||||||
const buyResult = useCallback((res: { data?: unknown; response?: Response }): BuyResult => {
|
const buyResult = useCallback((res: { data?: unknown; response?: Response }): BuyResult => {
|
||||||
if (res.data) { applyStats(res.data as ButterflyStats); return 'ok'; }
|
if (res.data) { applyStats(res.data as ButterflyStats); return 'ok'; }
|
||||||
@@ -242,6 +251,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,
|
mothHit, reportInspectionFail, buyPremium, waspSpray, buyInsurance, buyUpgrade,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
[
|
||||||
|
"Přepínač Hraní / Objednávání: ve výchozím režimu motýli jen poletují a nic neruší objednávání; hru zapneš tlačítkem vpravo dole a herní vrstva pak odstíní kliknutí, aby ses při hraní neproklikl do objednávky",
|
||||||
|
"Vylepšení „větší síťka“ teď síťku i viditelně zvětší (a projeví se hned po koupi)",
|
||||||
|
"Občas proběhne rychlá motýlí inspekce – chyť označeného zářícího motýla; férových hráčů se to skoro nedotkne, na roboty ale platí (a recidivisty čeká i chvilka ve vězení)"
|
||||||
|
]
|
||||||
@@ -41,6 +41,14 @@ interface StoredButterflyStats {
|
|||||||
/** 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;
|
||||||
|
|
||||||
|
// --- Anti-bot ban (opakované selhání inspekce) ---
|
||||||
|
/** Počet selhání inspekce v aktuálním okně */
|
||||||
|
inspectionFails: number;
|
||||||
|
/** Začátek okna pro počítání selhání (ms epoch) */
|
||||||
|
inspectionWindowStart: number;
|
||||||
|
/** Do kdy platí ban za podvádění (ms epoch); 0 = bez banu */
|
||||||
|
banUntil: number;
|
||||||
|
|
||||||
/** Kolikrát hráč omylem chytil černou můru */
|
/** Kolikrát hráč omylem chytil černou můru */
|
||||||
mothsHit: number;
|
mothsHit: number;
|
||||||
/** Trvalá vylepšení z obchodu */
|
/** Trvalá vylepšení z obchodu */
|
||||||
@@ -156,6 +164,13 @@ export const RATE_CAP_PER_MIN = 90;
|
|||||||
/** Délka okna pro rate-cap (ms) */
|
/** Délka okna pro rate-cap (ms) */
|
||||||
const RATE_WINDOW_MS = 60_000;
|
const RATE_WINDOW_MS = 60_000;
|
||||||
|
|
||||||
|
/** Kolik selhání inspekce v okně vede k banu */
|
||||||
|
export const BAN_FAIL_THRESHOLD = 3;
|
||||||
|
/** Okno, ve kterém se selhání inspekce počítají (ms) */
|
||||||
|
export const BAN_WINDOW_MS = 10 * 60_000;
|
||||||
|
/** Jak dlouho trvá ban za podvádění (ms) */
|
||||||
|
export const BAN_DURATION_MS = 90 * 60_000;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tituly úrovní. Pro úrovně nad rámec pole se použije poslední titul s hvězdičkami
|
* Tituly úrovní. Pro úrovně nad rámec pole se použije poslední titul s hvězdičkami
|
||||||
* (prestiž), takže postup nikdy „nedojde".
|
* (prestiž), takže postup nikdy „nedojde".
|
||||||
@@ -241,6 +256,7 @@ 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,
|
||||||
|
inspectionFails: 0, inspectionWindowStart: now, banUntil: 0,
|
||||||
mothsHit: 0,
|
mothsHit: 0,
|
||||||
upgrades: { net: 0, scarecrow: 0, reinforced: 0 },
|
upgrades: { net: 0, scarecrow: 0, reinforced: 0 },
|
||||||
insuranceUntil: 0,
|
insuranceUntil: 0,
|
||||||
@@ -267,6 +283,9 @@ 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,
|
||||||
|
inspectionFails: s.inspectionFails ?? 0,
|
||||||
|
inspectionWindowStart: s.inspectionWindowStart ?? now,
|
||||||
|
banUntil: s.banUntil ?? 0,
|
||||||
mothsHit: s.mothsHit ?? 0,
|
mothsHit: s.mothsHit ?? 0,
|
||||||
upgrades: {
|
upgrades: {
|
||||||
net: s.upgrades?.net ?? 0,
|
net: s.upgrades?.net ?? 0,
|
||||||
@@ -315,6 +334,8 @@ function bumpDaily(s: StoredButterflyStats, kind: string, amount: number): void
|
|||||||
export function growPests(s: StoredButterflyStats, now: number): void {
|
export function growPests(s: StoredButterflyStats, now: number): void {
|
||||||
// Síťka se sama zašije po uplynutí odpočtu
|
// Síťka se sama zašije po uplynutí odpočtu
|
||||||
if (s.netTornUntil && s.netTornUntil <= now) s.netTornUntil = 0;
|
if (s.netTornUntil && s.netTornUntil <= now) s.netTornUntil = 0;
|
||||||
|
// Vypršelý ban vyčistíme
|
||||||
|
if (s.banUntil && s.banUntil <= now) s.banUntil = 0;
|
||||||
|
|
||||||
// Vosy
|
// Vosy
|
||||||
if (s.wasps < WASP_MAX) {
|
if (s.wasps < WASP_MAX) {
|
||||||
@@ -365,6 +386,7 @@ function toDto(s: StoredButterflyStats): ButterflyStats {
|
|||||||
repellentUntil: s.repellentUntil,
|
repellentUntil: s.repellentUntil,
|
||||||
netTornUntil: s.netTornUntil,
|
netTornUntil: s.netTornUntil,
|
||||||
insuranceUntil: s.insuranceUntil,
|
insuranceUntil: s.insuranceUntil,
|
||||||
|
banUntil: s.banUntil,
|
||||||
upgrades: { ...s.upgrades },
|
upgrades: { ...s.upgrades },
|
||||||
daily: {
|
daily: {
|
||||||
taskId: s.daily.taskId,
|
taskId: s.daily.taskId,
|
||||||
@@ -434,6 +456,9 @@ export async function recordCatches(login: string, normal: number, golden: numbe
|
|||||||
let premiumUnlocked = false;
|
let premiumUnlocked = false;
|
||||||
|
|
||||||
const mine = await mutateUser(login, now, (s) => {
|
const mine = await mutateUser(login, now, (s) => {
|
||||||
|
// Ban za podvádění: během banu se nic nezapočítává
|
||||||
|
if (s.banUntil > now) return;
|
||||||
|
|
||||||
const oldCaught = s.caught;
|
const oldCaught = s.caught;
|
||||||
const oldLevel = levelForCaught(oldCaught);
|
const oldLevel = levelForCaught(oldCaught);
|
||||||
|
|
||||||
@@ -529,6 +554,27 @@ export async function reportNetTorn(login: string, now: number = Date.now()): Pr
|
|||||||
return toDto(mine);
|
return toDto(mine);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Zaznamená selhání anti-bot inspekce. Po BAN_FAIL_THRESHOLD selháních v okně
|
||||||
|
* BAN_WINDOW_MS udělí ban na BAN_DURATION_MS (perzistentní – nejde obejít
|
||||||
|
* obnovením stránky ani přepnutím režimu).
|
||||||
|
*/
|
||||||
|
export async function reportInspectionFail(login: string, now: number = Date.now()): Promise<ButterflyStats> {
|
||||||
|
const mine = await mutateUser(login, now, (s) => {
|
||||||
|
if (s.banUntil > now) return; // už zabanovaný
|
||||||
|
if (now - s.inspectionWindowStart > BAN_WINDOW_MS) {
|
||||||
|
s.inspectionWindowStart = now;
|
||||||
|
s.inspectionFails = 0;
|
||||||
|
}
|
||||||
|
s.inspectionFails += 1;
|
||||||
|
if (s.inspectionFails >= BAN_FAIL_THRESHOLD) {
|
||||||
|
s.banUntil = now + BAN_DURATION_MS;
|
||||||
|
s.inspectionFails = 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return toDto(mine);
|
||||||
|
}
|
||||||
|
|
||||||
/** Zloděj se dostal k penězům a ukradl podíl mincí (méně, když je aktivní pojistka). */
|
/** 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) => {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
caterpillarAte,
|
caterpillarAte,
|
||||||
defeatCaterpillar,
|
defeatCaterpillar,
|
||||||
mothHit,
|
mothHit,
|
||||||
|
reportInspectionFail,
|
||||||
buyPremium,
|
buyPremium,
|
||||||
waspSpray,
|
waspSpray,
|
||||||
buyInsurance,
|
buyInsurance,
|
||||||
@@ -128,6 +129,14 @@ router.post("/moth", async (req: Request, res, next) => {
|
|||||||
} catch (e: any) { next(e) }
|
} catch (e: any) { next(e) }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
router.post("/inspectionFailed", async (req: Request, res, next) => {
|
||||||
|
try {
|
||||||
|
const login = getLogin(parseToken(req));
|
||||||
|
const data = await reportInspectionFail(login);
|
||||||
|
res.status(200).json(data);
|
||||||
|
} catch (e: any) { next(e) }
|
||||||
|
});
|
||||||
|
|
||||||
router.post("/buyPremium", async (req: Request, res, next) => {
|
router.post("/buyPremium", async (req: Request, res, next) => {
|
||||||
try {
|
try {
|
||||||
const login = getLogin(parseToken(req));
|
const login = getLogin(parseToken(req));
|
||||||
|
|||||||
@@ -20,8 +20,19 @@ function loadAllChangelogs(): Record<string, string[]> {
|
|||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
const date = file.replace(".json", "");
|
const date = file.replace(".json", "");
|
||||||
if (!cache[date]) {
|
if (!cache[date]) {
|
||||||
const content = fs.readFileSync(path.join(CHANGELOGS_DIR, file), "utf-8");
|
try {
|
||||||
cache[date] = JSON.parse(content);
|
const content = fs.readFileSync(path.join(CHANGELOGS_DIR, file), "utf-8");
|
||||||
|
const parsed = JSON.parse(content);
|
||||||
|
// Očekáváme pole řetězců; jiný obsah přeskočíme
|
||||||
|
if (Array.isArray(parsed)) {
|
||||||
|
cache[date] = parsed;
|
||||||
|
} else {
|
||||||
|
console.warn(`Changelog ${file} není pole – přeskočeno.`);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Vadný JSON nesmí shodit celý changelog endpoint – přeskočíme ho
|
||||||
|
console.warn(`Changelog ${file} má neplatný JSON – přeskočeno:`, (e as Error).message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
caterpillarAte,
|
caterpillarAte,
|
||||||
defeatCaterpillar,
|
defeatCaterpillar,
|
||||||
mothHit,
|
mothHit,
|
||||||
|
reportInspectionFail,
|
||||||
buyPremium,
|
buyPremium,
|
||||||
waspSpray,
|
waspSpray,
|
||||||
buyInsurance,
|
buyInsurance,
|
||||||
@@ -44,6 +45,9 @@ import {
|
|||||||
CATERPILLAR_EATS,
|
CATERPILLAR_EATS,
|
||||||
CATERPILLAR_REWARD,
|
CATERPILLAR_REWARD,
|
||||||
AUTO_REPAIR_MS,
|
AUTO_REPAIR_MS,
|
||||||
|
BAN_FAIL_THRESHOLD,
|
||||||
|
BAN_DURATION_MS,
|
||||||
|
BAN_WINDOW_MS,
|
||||||
MOTH_PENALTY,
|
MOTH_PENALTY,
|
||||||
PREMIUM_BUY_COST,
|
PREMIUM_BUY_COST,
|
||||||
WASP_SPRAY_COST,
|
WASP_SPRAY_COST,
|
||||||
@@ -470,6 +474,37 @@ describe('achievementy', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('anti-bot ban', () => {
|
||||||
|
test('po prahu selhání inspekce udělí ban', async () => {
|
||||||
|
const now = 6_000_000;
|
||||||
|
await seed(USER, { caught: 0, coins: 0, goldenCaught: 0, inspectionWindowStart: now });
|
||||||
|
let stats;
|
||||||
|
for (let i = 0; i < BAN_FAIL_THRESHOLD - 1; i++) {
|
||||||
|
stats = await reportInspectionFail(USER, now + i * 1000);
|
||||||
|
expect(stats.banUntil).toBe(0);
|
||||||
|
}
|
||||||
|
stats = await reportInspectionFail(USER, now + BAN_FAIL_THRESHOLD * 1000);
|
||||||
|
expect(stats.banUntil).toBe(now + BAN_FAIL_THRESHOLD * 1000 + BAN_DURATION_MS);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('selhání mimo okno se nesčítají do banu', async () => {
|
||||||
|
const now = 7_000_000;
|
||||||
|
await seed(USER, { caught: 0, coins: 0, goldenCaught: 0, inspectionWindowStart: now });
|
||||||
|
await reportInspectionFail(USER, now);
|
||||||
|
// daleko za oknem → počítadlo se resetuje
|
||||||
|
const stats = await reportInspectionFail(USER, now + BAN_WINDOW_MS + 1000);
|
||||||
|
expect(stats.banUntil).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('během banu se úlovky nezapočítávají', async () => {
|
||||||
|
const now = 8_000_000;
|
||||||
|
await seed(USER, { caught: 10, coins: 5, goldenCaught: 0, banUntil: now + BAN_DURATION_MS, lastCatchDay: formatDate(new Date(now)) });
|
||||||
|
const result = await recordCatches(USER, 20, 0, now);
|
||||||
|
expect(result.stats.caught).toBe(10); // beze změny
|
||||||
|
expect(result.coinsAwarded).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
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 });
|
||||||
|
|||||||
@@ -104,6 +104,8 @@ paths:
|
|||||||
$ref: "./paths/butterflies/defeatCaterpillar.yml"
|
$ref: "./paths/butterflies/defeatCaterpillar.yml"
|
||||||
/butterflies/moth:
|
/butterflies/moth:
|
||||||
$ref: "./paths/butterflies/moth.yml"
|
$ref: "./paths/butterflies/moth.yml"
|
||||||
|
/butterflies/inspectionFailed:
|
||||||
|
$ref: "./paths/butterflies/inspectionFailed.yml"
|
||||||
/butterflies/buyPremium:
|
/butterflies/buyPremium:
|
||||||
$ref: "./paths/butterflies/buyPremium.yml"
|
$ref: "./paths/butterflies/buyPremium.yml"
|
||||||
/butterflies/waspSpray:
|
/butterflies/waspSpray:
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
post:
|
||||||
|
operationId: reportInspectionFail
|
||||||
|
summary: >-
|
||||||
|
Zaznamená selhání anti-bot inspekce. Po několika selháních v krátkém okně
|
||||||
|
udělí perzistentní ban za podvádění.
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Aktualizované statistiky (případně s aktivním banem).
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "../../schemas/_index.yml#/ButterflyStats"
|
||||||
@@ -949,6 +949,7 @@ ButterflyStats:
|
|||||||
- repellentUntil
|
- repellentUntil
|
||||||
- netTornUntil
|
- netTornUntil
|
||||||
- insuranceUntil
|
- insuranceUntil
|
||||||
|
- banUntil
|
||||||
- upgrades
|
- upgrades
|
||||||
- daily
|
- daily
|
||||||
properties:
|
properties:
|
||||||
@@ -1003,6 +1004,9 @@ ButterflyStats:
|
|||||||
insuranceUntil:
|
insuranceUntil:
|
||||||
description: Časové razítko (ms epoch), do kdy platí pojistka proti zloději; 0 = neaktivní
|
description: Časové razítko (ms epoch), do kdy platí pojistka proti zloději; 0 = neaktivní
|
||||||
type: integer
|
type: integer
|
||||||
|
banUntil:
|
||||||
|
description: Časové razítko (ms epoch), do kdy platí ban za podvádění; 0 = bez banu
|
||||||
|
type: integer
|
||||||
upgrades:
|
upgrades:
|
||||||
$ref: "#/ButterflyUpgrades"
|
$ref: "#/ButterflyUpgrades"
|
||||||
daily:
|
daily:
|
||||||
|
|||||||
Reference in New Issue
Block a user