feat: obtížnější chytání motýlků + zloděj, housenka a agresivní škůdci
CI / Generate TypeScript types (push) Successful in 12s
CI / Server unit tests (push) Successful in 22s
CI / Build server (push) Successful in 29s
CI / Build client (push) Successful in 41s
CI / Playwright E2E tests (push) Successful in 1m25s
CI / Build and push Docker image (push) Successful in 45s
CI / Notify (push) Successful in 2s
CI / Generate TypeScript types (push) Successful in 12s
CI / Server unit tests (push) Successful in 22s
CI / Build server (push) Successful in 29s
CI / Build client (push) Successful in 41s
CI / Playwright E2E tests (push) Successful in 1m25s
CI / Build and push Docker image (push) Successful in 45s
CI / Notify (push) Successful in 2s
- 60 úrovní se strmou křivkou, dražší vylepšení, vzácnější zlatý motýl - ochrana proti podvádění (rate cap + cooldown chytání) - perzistentní škůdci přibývají rychleji; vosy nalétávají do síťky, ptáci ji protrhnou vždy - protržená síťka se sama zašije za minutu (nebo okamžitě za mince) - zloděj krade mince a housenka žere úlovky – oba se plíží k počítadlu a jdou zaklikat - plašič ptáků s odpočtem, level progress bar, nová sezóna (reset statistik) 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
c5ca6c9d41
commit
e3f439bde0
@@ -7,18 +7,14 @@ import {
|
||||
killWasp as killWaspApi,
|
||||
buyRepellent as buyRepellentApi,
|
||||
reportNetTorn as reportTearApi,
|
||||
reportRobbery as reportRobberyApi,
|
||||
defeatThief as defeatThiefApi,
|
||||
reportCaterpillarAte as caterpillarAteApi,
|
||||
defeatCaterpillar as defeatCaterpillarApi,
|
||||
} from '../../../types';
|
||||
|
||||
/** Starý klíč, pod kterým se počet chycených ukládal jen do localStorage. */
|
||||
const LEGACY_KEY = 'flyingButterfliesCaught';
|
||||
/** Příznak, že už proběhla jednorázová migrace localStorage → server. */
|
||||
const MIGRATED_KEY = 'flyingButterfliesMigrated';
|
||||
/** Jak dlouho se čeká, než se nasbíraná dávka úlovků odešle na server. */
|
||||
const FLUSH_DEBOUNCE_MS = 1500;
|
||||
/** Server přijme max 100 motýlů na dávku – migraci proto posíláme po částech. */
|
||||
const MIGRATION_CHUNK = 100;
|
||||
/** Bezpečnostní strop migrovaných úlovků (proti nesmyslným hodnotám v localStorage). */
|
||||
const MIGRATION_MAX = 1000;
|
||||
/** Jak často se přenačte stav ze serveru (kvůli přibývajícím škůdcům). */
|
||||
const POLL_INTERVAL_MS = 30_000;
|
||||
|
||||
@@ -33,12 +29,15 @@ export interface RewardEvent {
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook spravující serverovou perzistenci chytání motýlků: načtení stavu,
|
||||
* jednorázovou migraci z localStorage, dávkové odesílání úlovků, hubení vos,
|
||||
* plašič ptáků, protržení/opravu síťky a periodické přenačítání (růst škůdců).
|
||||
* Hook spravující serverovou perzistenci chytání motýlků: načtení stavu, dávkové
|
||||
* odesílání úlovků, hubení vos, plašič ptáků, protržení/opravu síťky, zloděje a
|
||||
* periodické přenačítání (růst škůdců). Server je zdroj pravdy; počítadlo se
|
||||
* zobrazuje jako serverová hodnota + zatím neodeslané úlovky.
|
||||
*/
|
||||
export function useButterflyStats(onReward?: (e: RewardEvent) => void) {
|
||||
const [stats, setStats] = useState<ButterflyStats | undefined>();
|
||||
/** Optimisticky zobrazený přírůstek chycených, který ještě neodešel na server. */
|
||||
const [pendingShown, setPendingShown] = useState(0);
|
||||
/** Živá hodnota mincí pro scénu (aby nečetla zastaralý stav přes closure). */
|
||||
const coinsRef = useRef(0);
|
||||
const pending = useRef({ normal: 0, golden: 0 });
|
||||
@@ -46,18 +45,9 @@ export function useButterflyStats(onReward?: (e: RewardEvent) => void) {
|
||||
const onRewardRef = useRef(onReward);
|
||||
onRewardRef.current = onReward;
|
||||
|
||||
/** Zapíše statistiky ze serveru, ale nedovolí „couvnout" počtu chycených
|
||||
* (kvůli optimistickému počítadlu, které může být napřed před flushem). */
|
||||
const applyStats = useCallback((incoming: ButterflyStats) => {
|
||||
setStats(prev => {
|
||||
const merged: ButterflyStats = { ...incoming };
|
||||
if (prev && prev.caught > incoming.caught) {
|
||||
merged.caught = prev.caught;
|
||||
merged.goldenCaught = Math.max(prev.goldenCaught, incoming.goldenCaught);
|
||||
}
|
||||
coinsRef.current = merged.coins;
|
||||
return merged;
|
||||
});
|
||||
coinsRef.current = incoming.coins;
|
||||
setStats(incoming);
|
||||
}, []);
|
||||
|
||||
const flush = useCallback(async () => {
|
||||
@@ -66,12 +56,15 @@ export function useButterflyStats(onReward?: (e: RewardEvent) => void) {
|
||||
flushTimer.current = null;
|
||||
}
|
||||
const batch = pending.current;
|
||||
if (batch.normal === 0 && batch.golden === 0) return;
|
||||
const batchTotal = batch.normal + batch.golden;
|
||||
if (batchTotal === 0) return;
|
||||
pending.current = { normal: 0, golden: 0 };
|
||||
try {
|
||||
const res = await catchButterflies({ body: { normal: batch.normal, golden: batch.golden } });
|
||||
if (res.data) {
|
||||
applyStats(res.data.stats);
|
||||
// Odeslané úlovky už jsou v serverové hodnotě → sundáme je z optimistického přírůstku
|
||||
setPendingShown(p => Math.max(0, p - batchTotal));
|
||||
onRewardRef.current?.({
|
||||
coinsAwarded: res.data.coinsAwarded,
|
||||
dailyBonusApplied: res.data.dailyBonusApplied,
|
||||
@@ -80,8 +73,11 @@ export function useButterflyStats(onReward?: (e: RewardEvent) => void) {
|
||||
newLevel: res.data.stats.level,
|
||||
newTitle: res.data.stats.title,
|
||||
});
|
||||
} else {
|
||||
setPendingShown(p => Math.max(0, p - batchTotal));
|
||||
}
|
||||
} catch {
|
||||
// Při chybě vrátíme dávku zpět (přírůstek necháme zobrazený)
|
||||
pending.current.normal += batch.normal;
|
||||
pending.current.golden += batch.golden;
|
||||
}
|
||||
@@ -91,15 +87,12 @@ export function useButterflyStats(onReward?: (e: RewardEvent) => void) {
|
||||
const reportCatch = useCallback((golden: boolean) => {
|
||||
if (golden) pending.current.golden += 1;
|
||||
else pending.current.normal += 1;
|
||||
setStats(prev => prev
|
||||
? { ...prev, caught: prev.caught + 1, goldenCaught: prev.goldenCaught + (golden ? 1 : 0) }
|
||||
: prev);
|
||||
setPendingShown(p => p + 1);
|
||||
if (!flushTimer.current) {
|
||||
flushTimer.current = window.setTimeout(() => { void flush(); }, FLUSH_DEBOUNCE_MS);
|
||||
}
|
||||
}, [flush]);
|
||||
|
||||
/** Zaplatí opravu síťky. Vrátí true při úspěchu, false při nedostatku mincí/chybě. */
|
||||
const repair = useCallback(async (): Promise<boolean> => {
|
||||
try {
|
||||
const res = await repairNetApi();
|
||||
@@ -108,7 +101,6 @@ export function useButterflyStats(onReward?: (e: RewardEvent) => void) {
|
||||
} catch { return false; }
|
||||
}, [applyStats]);
|
||||
|
||||
/** Zahubí jednu vosu (plácačka). */
|
||||
const killWasp = useCallback(async () => {
|
||||
try {
|
||||
const res = await killWaspApi();
|
||||
@@ -116,7 +108,6 @@ export function useButterflyStats(onReward?: (e: RewardEvent) => void) {
|
||||
} catch { /* ignore */ }
|
||||
}, [applyStats]);
|
||||
|
||||
/** Koupí plašič ptáků. Vrátí true při úspěchu, false při nedostatku mincí. */
|
||||
const buyRepellent = useCallback(async (): Promise<boolean> => {
|
||||
try {
|
||||
const res = await buyRepellentApi();
|
||||
@@ -125,7 +116,6 @@ export function useButterflyStats(onReward?: (e: RewardEvent) => void) {
|
||||
} catch { return false; }
|
||||
}, [applyStats]);
|
||||
|
||||
/** Nahlásí serveru, že pták protrhl síťku (perzistentně). */
|
||||
const reportTear = useCallback(async () => {
|
||||
try {
|
||||
const res = await reportTearApi();
|
||||
@@ -133,7 +123,38 @@ export function useButterflyStats(onReward?: (e: RewardEvent) => void) {
|
||||
} catch { /* ignore */ }
|
||||
}, [applyStats]);
|
||||
|
||||
/** Přenačte stav ze serveru (kvůli přibývajícím škůdcům). */
|
||||
/** Zloděj se dostal k penězům – server strhne podíl mincí. */
|
||||
const reportRobbery = useCallback(async () => {
|
||||
try {
|
||||
const res = await reportRobberyApi();
|
||||
if (res.data) applyStats(res.data);
|
||||
} catch { /* ignore */ }
|
||||
}, [applyStats]);
|
||||
|
||||
/** Hráč porazil zloděje – odměna a statistika. */
|
||||
const defeatThief = useCallback(async () => {
|
||||
try {
|
||||
const res = await defeatThiefApi();
|
||||
if (res.data) applyStats(res.data);
|
||||
} catch { /* ignore */ }
|
||||
}, [applyStats]);
|
||||
|
||||
/** Housenka se dostala k úlovkům – server sníží počet chycených. */
|
||||
const caterpillarAte = useCallback(async () => {
|
||||
try {
|
||||
const res = await caterpillarAteApi();
|
||||
if (res.data) applyStats(res.data);
|
||||
} catch { /* ignore */ }
|
||||
}, [applyStats]);
|
||||
|
||||
/** Hráč porazil housenku – odměna a statistika. */
|
||||
const defeatCaterpillar = useCallback(async () => {
|
||||
try {
|
||||
const res = await defeatCaterpillarApi();
|
||||
if (res.data) applyStats(res.data);
|
||||
} catch { /* ignore */ }
|
||||
}, [applyStats]);
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
try {
|
||||
const res = await getButterflyStats();
|
||||
@@ -141,29 +162,13 @@ export function useButterflyStats(onReward?: (e: RewardEvent) => void) {
|
||||
} catch { /* ignore */ }
|
||||
}, [applyStats]);
|
||||
|
||||
// Načtení stavu + jednorázová migrace z localStorage
|
||||
// Načtení stavu
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
(async () => {
|
||||
try {
|
||||
const res = await getButterflyStats();
|
||||
if (cancelled || !res.data) return;
|
||||
let current = res.data;
|
||||
try {
|
||||
if (!localStorage.getItem(MIGRATED_KEY)) {
|
||||
const legacy = Number(localStorage.getItem(LEGACY_KEY));
|
||||
let diff = Number.isFinite(legacy) ? Math.min(legacy - current.caught, MIGRATION_MAX) : 0;
|
||||
while (diff > 0 && !cancelled) {
|
||||
const chunk = Math.min(diff, MIGRATION_CHUNK);
|
||||
const migRes = await catchButterflies({ body: { normal: chunk, golden: 0 } });
|
||||
if (migRes.data) current = migRes.data.stats;
|
||||
diff -= chunk;
|
||||
}
|
||||
localStorage.setItem(MIGRATED_KEY, '1');
|
||||
localStorage.removeItem(LEGACY_KEY);
|
||||
}
|
||||
} catch { /* localStorage nedostupné – migraci přeskočíme */ }
|
||||
if (!cancelled) applyStats(current);
|
||||
if (!cancelled && res.data) applyStats(res.data);
|
||||
} catch { /* server nedostupný – hra jede dál bez perzistence */ }
|
||||
})();
|
||||
return () => { cancelled = true; };
|
||||
@@ -193,5 +198,10 @@ export function useButterflyStats(onReward?: (e: RewardEvent) => void) {
|
||||
};
|
||||
}, [flush]);
|
||||
|
||||
return { stats, coinsRef, reportCatch, repair, killWasp, buyRepellent, reportTear };
|
||||
const displayCaught = (stats?.caught ?? 0) + pendingShown;
|
||||
|
||||
return {
|
||||
stats, displayCaught, coinsRef, reportCatch, repair, killWasp, buyRepellent,
|
||||
reportTear, reportRobbery, defeatThief, caterpillarAte, defeatCaterpillar,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user