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;
|
||||
z-index: 2;
|
||||
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 {
|
||||
@@ -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
|
||||
.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));
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
// --- 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;
|
||||
/** Přírůstek dosahu chytání za úroveň vylepšení „větší síťka" */
|
||||
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)
|
||||
const THIEF_HP = 22;
|
||||
@@ -43,6 +51,17 @@ function formatRemaining(ms: number): string {
|
||||
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 {
|
||||
el: HTMLDivElement;
|
||||
sprite: HTMLDivElement;
|
||||
@@ -51,6 +70,8 @@ interface ButterflyData {
|
||||
golden: boolean;
|
||||
/** Černá můra – chycení stojí úlovky (nepočítá se jako úlovek) */
|
||||
moth: boolean;
|
||||
/** Korunovaný motýl pro anti-bot inspekci */
|
||||
royal: boolean;
|
||||
bobFreq1: number; bobPhase1: number; bobAmp1: number;
|
||||
bobFreq2: number; bobPhase2: number; bobAmp2: number;
|
||||
}
|
||||
@@ -99,6 +120,8 @@ interface SceneCallbacks {
|
||||
getCoins: () => number;
|
||||
getCaught: () => number;
|
||||
getNetUpgrade: () => number;
|
||||
/** Do kdy platí serverový ban (ms epoch); 0 = bez banu */
|
||||
getBanUntil: () => number;
|
||||
onTornChange: (torn: boolean) => void;
|
||||
onCombo: (count: number) => void;
|
||||
onSting: () => void;
|
||||
@@ -111,6 +134,8 @@ interface SceneCallbacks {
|
||||
onStalkerAppear: (type: 'thief' | 'caterpillar') => void;
|
||||
onBatShooed: () => 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 {
|
||||
@@ -123,10 +148,13 @@ interface FlyingButterfliesProps {
|
||||
class ButterflyScene {
|
||||
private viewport: HTMLElement;
|
||||
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 birds: CritterData[] = [];
|
||||
private wasps: CritterData[] = [];
|
||||
private stalker: StalkerData | null = null;
|
||||
private interactive: boolean = false;
|
||||
private numButterflies: number;
|
||||
private variants: readonly string[];
|
||||
private width: number;
|
||||
@@ -157,6 +185,16 @@ class ButterflyScene {
|
||||
private webClicks: number = 0;
|
||||
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 lastCatchTs: number = 0;
|
||||
private comboTimer: number = 0;
|
||||
@@ -195,6 +233,8 @@ class ButterflyScene {
|
||||
constructor(el: HTMLElement, numButterflies: number, variants: readonly string[], netEnabled: boolean, cb: SceneCallbacks) {
|
||||
this.viewport = el;
|
||||
this.world = document.createElement('div');
|
||||
this.gameLayer = document.createElement('div');
|
||||
this.gameLayer.className = 'butterfly-game-layer';
|
||||
this.numButterflies = numButterflies;
|
||||
this.variants = variants;
|
||||
this.netEnabled = netEnabled;
|
||||
@@ -202,6 +242,8 @@ class ButterflyScene {
|
||||
this.width = this.viewport.offsetWidth;
|
||||
this.height = this.viewport.offsetHeight;
|
||||
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.width = this.viewport.offsetWidth;
|
||||
this.height = this.viewport.offsetHeight;
|
||||
@@ -217,7 +259,10 @@ class ButterflyScene {
|
||||
}
|
||||
|
||||
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 {
|
||||
const base = this.premiumActive ? ButterflyScene.CATCH_RADIUS_PREMIUM : ButterflyScene.CATCH_RADIUS_NORMAL;
|
||||
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 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 --------------------------------------------------------------
|
||||
|
||||
private resetButterfly = (b: ButterflyData): void => {
|
||||
@@ -240,6 +300,19 @@ class ButterflyScene {
|
||||
b.bobPhase2 = Math.random() * Math.PI * 2;
|
||||
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
|
||||
b.moth = Math.random() < MOTH_CHANCE;
|
||||
b.golden = !b.moth && Math.random() < GOLDEN_CHANCE;
|
||||
@@ -295,6 +368,9 @@ class ButterflyScene {
|
||||
|
||||
private onPointerDown = (e: PointerEvent): void => {
|
||||
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.net.classList.add('grabbed');
|
||||
this.netX = e.clientX; this.netY = e.clientY;
|
||||
@@ -302,6 +378,7 @@ class ButterflyScene {
|
||||
};
|
||||
private onPointerMove = (e: PointerEvent): void => {
|
||||
if (!this.netGrabbed) return;
|
||||
if (!e.isTrusted) { this.syntheticSeen = true; this.trustedControl = false; }
|
||||
this.netX = e.clientX; this.netY = e.clientY;
|
||||
this.updateNetTransform();
|
||||
};
|
||||
@@ -330,6 +407,13 @@ class ButterflyScene {
|
||||
/** 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;
|
||||
// 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) {
|
||||
// Chytit černou můru je chyba – penalta, žádné kombo
|
||||
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)
|
||||
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 half = ButterflyScene.BASE_SIZE / 2;
|
||||
for (const b of this.butterflies) {
|
||||
@@ -377,7 +463,8 @@ class ButterflyScene {
|
||||
};
|
||||
|
||||
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;
|
||||
this.lastMagnetAt = this.timer;
|
||||
const half = ButterflyScene.BASE_SIZE / 2;
|
||||
@@ -392,6 +479,54 @@ class ButterflyScene {
|
||||
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 => {
|
||||
this.premiumUntil = Date.now() + PREMIUM_DURATION_MS;
|
||||
this.applyNetAppearance();
|
||||
@@ -460,7 +595,7 @@ class ButterflyScene {
|
||||
c.cleanup = () => el.removeEventListener('pointerdown', swat);
|
||||
}
|
||||
|
||||
this.world.appendChild(el);
|
||||
this.gameLayer.appendChild(el);
|
||||
return c;
|
||||
};
|
||||
|
||||
@@ -647,7 +782,7 @@ class ButterflyScene {
|
||||
el.addEventListener('pointerdown', hit);
|
||||
s.cleanup = () => el.removeEventListener('pointerdown', hit);
|
||||
|
||||
this.viewport.appendChild(el);
|
||||
this.gameLayer.appendChild(el);
|
||||
this.stalker = s;
|
||||
this.cb.onStalkerAppear(type);
|
||||
};
|
||||
@@ -732,7 +867,7 @@ class ButterflyScene {
|
||||
};
|
||||
el.addEventListener('pointerdown', hit);
|
||||
bat.cleanup = () => el.removeEventListener('pointerdown', hit);
|
||||
this.world.appendChild(el);
|
||||
this.gameLayer.appendChild(el);
|
||||
this.bat = bat;
|
||||
};
|
||||
|
||||
@@ -799,7 +934,7 @@ class ButterflyScene {
|
||||
};
|
||||
el.addEventListener('pointerdown', strip);
|
||||
(el as any)._cleanup = () => el.removeEventListener('pointerdown', strip);
|
||||
this.viewport.appendChild(el);
|
||||
this.gameLayer.appendChild(el);
|
||||
this.web = el;
|
||||
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('pointerup', this.onPointerUp);
|
||||
window.addEventListener('pointercancel', this.onPointerUp);
|
||||
this.viewport.appendChild(this.net);
|
||||
this.gameLayer.appendChild(this.net);
|
||||
};
|
||||
|
||||
public init = (): void => {
|
||||
@@ -844,7 +979,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, 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,
|
||||
};
|
||||
this.resetButterfly(b);
|
||||
@@ -852,6 +987,8 @@ class ButterflyScene {
|
||||
this.world.appendChild(el);
|
||||
}
|
||||
this.viewport.appendChild(this.world);
|
||||
this.viewport.appendChild(this.gameLayer);
|
||||
this.gameLayer.style.display = this.interactive ? '' : 'none';
|
||||
if (this.netEnabled) this.initNet();
|
||||
window.addEventListener('resize', this.handleResize);
|
||||
document.addEventListener('visibilitychange', this.handleVisibility);
|
||||
@@ -859,13 +996,19 @@ class ButterflyScene {
|
||||
|
||||
private wasPremium: boolean = false;
|
||||
private wasStunned: boolean = false;
|
||||
private lastNetUpgrade: number = 0;
|
||||
|
||||
public render = (): void => {
|
||||
for (const b of this.butterflies) this.updateButterfly(b);
|
||||
|
||||
if (this.netEnabled) {
|
||||
if (this.netEnabled && this.interactive) {
|
||||
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.updateWasps();
|
||||
@@ -883,6 +1026,19 @@ class ButterflyScene {
|
||||
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 (nowPremium) this.runMagnet();
|
||||
}
|
||||
@@ -903,7 +1059,7 @@ class ButterflyScene {
|
||||
window.removeEventListener('pointermove', this.onPointerMove);
|
||||
window.removeEventListener('pointerup', 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);
|
||||
document.removeEventListener('visibilitychange', this.handleVisibility);
|
||||
};
|
||||
@@ -932,6 +1088,20 @@ const FlyingButterflies: React.FC<FlyingButterfliesProps> = ({
|
||||
const [nowTs, setNowTs] = useState(() => Date.now());
|
||||
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) => {
|
||||
setFlash(msg);
|
||||
if (flashTimer.current) window.clearTimeout(flashTimer.current);
|
||||
@@ -949,27 +1119,33 @@ const FlyingButterflies: React.FC<FlyingButterfliesProps> = ({
|
||||
}
|
||||
}, [showFlash]);
|
||||
|
||||
const [inspecting, setInspecting] = useState(false);
|
||||
|
||||
const {
|
||||
stats, displayCaught, coinsRef, reportCatch, repair,
|
||||
killWasp, buyRepellent, reportTear, reportRobbery, defeatThief,
|
||||
caterpillarAte, defeatCaterpillar, mothHit, buyPremium, waspSpray, buyInsurance, buyUpgrade,
|
||||
caterpillarAte, defeatCaterpillar, mothHit, reportInspectionFail,
|
||||
buyPremium, waspSpray, buyInsurance, buyUpgrade,
|
||||
} = useButterflyStats(handleReward);
|
||||
|
||||
const netUpgradeRef = useRef(0);
|
||||
netUpgradeRef.current = stats?.upgrades?.net ?? 0;
|
||||
const banUntilRef = useRef(0);
|
||||
banUntilRef.current = stats?.banUntil ?? 0;
|
||||
|
||||
const noop = () => { };
|
||||
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,
|
||||
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 = {
|
||||
onCatch: (golden) => reportCatch(golden),
|
||||
getCoins: () => coinsRef.current,
|
||||
getCaught: () => stats?.caught ?? 0,
|
||||
getNetUpgrade: () => netUpgradeRef.current,
|
||||
getBanUntil: () => banUntilRef.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!'),
|
||||
@@ -984,8 +1160,17 @@ const FlyingButterflies: React.FC<FlyingButterfliesProps> = ({
|
||||
: '🐛 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.'),
|
||||
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(() => {
|
||||
if (containerRef.current) {
|
||||
sceneRef.current = new ButterflyScene(containerRef.current, numButterflies, butterflyVariants, enableNet, {
|
||||
@@ -993,6 +1178,7 @@ const FlyingButterflies: React.FC<FlyingButterfliesProps> = ({
|
||||
getCoins: () => callbacksRef.current.getCoins(),
|
||||
getCaught: () => callbacksRef.current.getCaught(),
|
||||
getNetUpgrade: () => callbacksRef.current.getNetUpgrade(),
|
||||
getBanUntil: () => callbacksRef.current.getBanUntil(),
|
||||
onTornChange: (t) => callbacksRef.current.onTornChange(t),
|
||||
onCombo: (c) => callbacksRef.current.onCombo(c),
|
||||
onSting: () => callbacksRef.current.onSting(),
|
||||
@@ -1005,10 +1191,14 @@ const FlyingButterflies: React.FC<FlyingButterfliesProps> = ({
|
||||
onStalkerAppear: (t) => callbacksRef.current.onStalkerAppear(t),
|
||||
onBatShooed: () => callbacksRef.current.onBatShooed(),
|
||||
onWebCleared: () => callbacksRef.current.onWebCleared(),
|
||||
onInspection: (s) => callbacksRef.current.onInspection(s),
|
||||
});
|
||||
sceneRef.current.init();
|
||||
sceneRef.current.setInteractive(interactiveRef.current);
|
||||
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]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -1019,6 +1209,9 @@ const FlyingButterflies: React.FC<FlyingButterfliesProps> = ({
|
||||
};
|
||||
}, [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 birds = stats?.birds ?? 0;
|
||||
const netTornUntil = stats?.netTornUntil ?? 0;
|
||||
@@ -1042,14 +1235,17 @@ const FlyingButterflies: React.FC<FlyingButterfliesProps> = ({
|
||||
const repellentUntil = stats?.repellentUntil ?? 0;
|
||||
const repellentRemainingMs = Math.max(0, repellentUntil - nowTs);
|
||||
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(() => {
|
||||
const until = Math.max(repellentUntil, netTornUntil);
|
||||
const until = Math.max(repellentUntil, netTornUntil, banUntil);
|
||||
if (until <= Date.now()) return;
|
||||
const id = window.setInterval(() => setNowTs(Date.now()), 1000);
|
||||
return () => window.clearInterval(id);
|
||||
}, [repellentUntil, netTornUntil]);
|
||||
}, [repellentUntil, netTornUntil, banUntil]);
|
||||
|
||||
const onRepairClick = useCallback(async () => {
|
||||
const ok = await repair();
|
||||
@@ -1087,8 +1283,38 @@ const FlyingButterflies: React.FC<FlyingButterfliesProps> = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<div ref={containerRef} className={className} />
|
||||
<div ref={containerRef} className={interactive ? `${className} playing` : className} />
|
||||
|
||||
{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">
|
||||
{flash && (
|
||||
<div className="butterfly-flash" onClick={() => setFlash(null)} title="Klikni pro zavření">
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
reportCaterpillarAte as caterpillarAteApi,
|
||||
defeatCaterpillar as defeatCaterpillarApi,
|
||||
reportMothHit as mothHitApi,
|
||||
reportInspectionFail as inspectionFailApi,
|
||||
buyPremiumNet as buyPremiumApi,
|
||||
buyWaspSpray as waspSprayApi,
|
||||
buyInsurance as buyInsuranceApi,
|
||||
@@ -166,6 +167,14 @@ export function useButterflyStats(onReward?: (e: RewardEvent) => void) {
|
||||
} catch { /* ignore */ }
|
||||
}, [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). */
|
||||
const buyResult = useCallback((res: { data?: unknown; response?: Response }): BuyResult => {
|
||||
if (res.data) { applyStats(res.data as ButterflyStats); return 'ok'; }
|
||||
@@ -242,6 +251,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,
|
||||
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ě */
|
||||
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 */
|
||||
mothsHit: number;
|
||||
/** Trvalá vylepšení z obchodu */
|
||||
@@ -156,6 +164,13 @@ export const RATE_CAP_PER_MIN = 90;
|
||||
/** Délka okna pro rate-cap (ms) */
|
||||
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
|
||||
* (prestiž), takže postup nikdy „nedojde".
|
||||
@@ -241,6 +256,7 @@ function defaultStats(now: number): StoredButterflyStats {
|
||||
repellentUntil: 0, netTornUntil: 0,
|
||||
lastWaspAt: now, lastBirdAt: now,
|
||||
catchWindowStart: now, catchWindowCount: 0,
|
||||
inspectionFails: 0, inspectionWindowStart: now, banUntil: 0,
|
||||
mothsHit: 0,
|
||||
upgrades: { net: 0, scarecrow: 0, reinforced: 0 },
|
||||
insuranceUntil: 0,
|
||||
@@ -267,6 +283,9 @@ function normalize(s: StoredButterflyStats, now: number): StoredButterflyStats {
|
||||
lastBirdAt: s.lastBirdAt ?? now,
|
||||
catchWindowStart: s.catchWindowStart ?? now,
|
||||
catchWindowCount: s.catchWindowCount ?? 0,
|
||||
inspectionFails: s.inspectionFails ?? 0,
|
||||
inspectionWindowStart: s.inspectionWindowStart ?? now,
|
||||
banUntil: s.banUntil ?? 0,
|
||||
mothsHit: s.mothsHit ?? 0,
|
||||
upgrades: {
|
||||
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 {
|
||||
// Síťka se sama zašije po uplynutí odpočtu
|
||||
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
|
||||
if (s.wasps < WASP_MAX) {
|
||||
@@ -365,6 +386,7 @@ function toDto(s: StoredButterflyStats): ButterflyStats {
|
||||
repellentUntil: s.repellentUntil,
|
||||
netTornUntil: s.netTornUntil,
|
||||
insuranceUntil: s.insuranceUntil,
|
||||
banUntil: s.banUntil,
|
||||
upgrades: { ...s.upgrades },
|
||||
daily: {
|
||||
taskId: s.daily.taskId,
|
||||
@@ -434,6 +456,9 @@ export async function recordCatches(login: string, normal: number, golden: numbe
|
||||
let premiumUnlocked = false;
|
||||
|
||||
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 oldLevel = levelForCaught(oldCaught);
|
||||
|
||||
@@ -529,6 +554,27 @@ export async function reportNetTorn(login: string, now: number = Date.now()): Pr
|
||||
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). */
|
||||
export async function robbery(login: string, now: number = Date.now()): Promise<ButterflyStats> {
|
||||
const mine = await mutateUser(login, now, (s) => {
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
caterpillarAte,
|
||||
defeatCaterpillar,
|
||||
mothHit,
|
||||
reportInspectionFail,
|
||||
buyPremium,
|
||||
waspSpray,
|
||||
buyInsurance,
|
||||
@@ -128,6 +129,14 @@ router.post("/moth", async (req: Request, res, next) => {
|
||||
} 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) => {
|
||||
try {
|
||||
const login = getLogin(parseToken(req));
|
||||
|
||||
@@ -20,8 +20,19 @@ function loadAllChangelogs(): Record<string, string[]> {
|
||||
for (const file of files) {
|
||||
const date = file.replace(".json", "");
|
||||
if (!cache[date]) {
|
||||
const content = fs.readFileSync(path.join(CHANGELOGS_DIR, file), "utf-8");
|
||||
cache[date] = JSON.parse(content);
|
||||
try {
|
||||
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,
|
||||
defeatCaterpillar,
|
||||
mothHit,
|
||||
reportInspectionFail,
|
||||
buyPremium,
|
||||
waspSpray,
|
||||
buyInsurance,
|
||||
@@ -44,6 +45,9 @@ import {
|
||||
CATERPILLAR_EATS,
|
||||
CATERPILLAR_REWARD,
|
||||
AUTO_REPAIR_MS,
|
||||
BAN_FAIL_THRESHOLD,
|
||||
BAN_DURATION_MS,
|
||||
BAN_WINDOW_MS,
|
||||
MOTH_PENALTY,
|
||||
PREMIUM_BUY_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', () => {
|
||||
test('řadí sestupně dle počtu chycených a respektuje limit', async () => {
|
||||
await seed(USER, { caught: 100, coins: 0, goldenCaught: 2 });
|
||||
|
||||
@@ -104,6 +104,8 @@ paths:
|
||||
$ref: "./paths/butterflies/defeatCaterpillar.yml"
|
||||
/butterflies/moth:
|
||||
$ref: "./paths/butterflies/moth.yml"
|
||||
/butterflies/inspectionFailed:
|
||||
$ref: "./paths/butterflies/inspectionFailed.yml"
|
||||
/butterflies/buyPremium:
|
||||
$ref: "./paths/butterflies/buyPremium.yml"
|
||||
/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
|
||||
- netTornUntil
|
||||
- insuranceUntil
|
||||
- banUntil
|
||||
- upgrades
|
||||
- daily
|
||||
properties:
|
||||
@@ -1003,6 +1004,9 @@ ButterflyStats:
|
||||
insuranceUntil:
|
||||
description: Časové razítko (ms epoch), do kdy platí pojistka proti zloději; 0 = neaktivní
|
||||
type: integer
|
||||
banUntil:
|
||||
description: Časové razítko (ms epoch), do kdy platí ban za podvádění; 0 = bez banu
|
||||
type: integer
|
||||
upgrades:
|
||||
$ref: "#/ButterflyUpgrades"
|
||||
daily:
|
||||
|
||||
Reference in New Issue
Block a user