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