feat: obchod, černé můry, netopýr, pavouk a denní úkoly u chytání motýlků
CI / Generate TypeScript types (push) Successful in 15s
CI / Server unit tests (push) Successful in 23s
CI / Build server (push) Successful in 29s
CI / Build client (push) Successful in 1m2s
CI / Playwright E2E tests (push) Successful in 1m33s
CI / Build and push Docker image (push) Successful in 49s
CI / Notify (push) Successful in 1s
CI / Generate TypeScript types (push) Successful in 15s
CI / Server unit tests (push) Successful in 23s
CI / Build server (push) Successful in 29s
CI / Build client (push) Successful in 1m2s
CI / Playwright E2E tests (push) Successful in 1m33s
CI / Build and push Docker image (push) Successful in 49s
CI / Notify (push) Successful in 1s
- obchod s pomůckami a trvalými vylepšeními (větší síťka, strašák, zpevněná síťka) – mince mají trvalý smysl - černé můry se míchají mezi motýly; chycení (i magnetem) ubere hodně úlovků - ptáci silnější, plašič dražší a kratší, zloději chodí častěji podle bohatství - denní úkol s odměnou + achievementy/odznaky - netopýr v noci loví motýly, pavouk zamotá síťku pavučinou – oba na zaklikání - nákupní hlášky rozlišují nedostatek mincí od jiné chyby; herní smyčka se pozastaví při skryté záložce Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
e3f439bde0
commit
252d105408
22 files changed
+1384
-43
No files matched your search
@@ -43,7 +43,38 @@
|
||||
|
||||
.butterfly-leaderboard-heading {
|
||||
font-weight: 700;
|
||||
margin: 4px 0 8px;
|
||||
margin: 12px 0 8px;
|
||||
}
|
||||
|
||||
.butterfly-achievements {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.butterfly-achievement {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 8px;
|
||||
border-radius: 8px;
|
||||
background: rgba(0, 0, 0, 0.04);
|
||||
font-size: 0.78rem;
|
||||
opacity: 0.55;
|
||||
filter: grayscale(1);
|
||||
|
||||
&.unlocked {
|
||||
opacity: 1;
|
||||
filter: none;
|
||||
background: rgba(255, 203, 61, 0.18);
|
||||
}
|
||||
|
||||
.ach-icon { flex: 0 0 auto; }
|
||||
.ach-title {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.butterfly-leaderboard {
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Modal } from "react-bootstrap";
|
||||
import { ButterflyLeaderboardEntry, ButterflyStats, getButterflyLeaderboard } from "../../../../types";
|
||||
import {
|
||||
ButterflyLeaderboardEntry, ButterflyStats, ButterflyAchievement,
|
||||
getButterflyLeaderboard, getButterflyAchievements,
|
||||
} from "../../../../types";
|
||||
import { useAuth } from "../../context/auth";
|
||||
import "./ButterflyLeaderboardModal.scss";
|
||||
|
||||
@@ -12,20 +15,25 @@ type Props = {
|
||||
|
||||
const MEDALS = ['🥇', '🥈', '🥉'];
|
||||
|
||||
/** Modální dialog s osobními statistikami a týmovým žebříčkem chytačů motýlů. */
|
||||
/** Modální dialog s osobními statistikami, achievementy a týmovým žebříčkem. */
|
||||
export default function ButterflyLeaderboardModal({ isOpen, onClose, myStats }: Readonly<Props>) {
|
||||
const auth = useAuth();
|
||||
const [entries, setEntries] = useState<ButterflyLeaderboardEntry[]>();
|
||||
const [achievements, setAchievements] = useState<ButterflyAchievement[]>();
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) return;
|
||||
let cancelled = false;
|
||||
setLoading(true);
|
||||
getButterflyLeaderboard({ query: { limit: 20 } })
|
||||
.then(res => { if (!cancelled) setEntries(res.data ?? []); })
|
||||
.catch(() => { if (!cancelled) setEntries([]); })
|
||||
.finally(() => { if (!cancelled) setLoading(false); });
|
||||
Promise.all([
|
||||
getButterflyLeaderboard({ query: { limit: 20 } }).then(r => r.data ?? []).catch(() => []),
|
||||
getButterflyAchievements().then(r => r.data ?? []).catch(() => []),
|
||||
]).then(([lb, ach]) => {
|
||||
if (cancelled) return;
|
||||
setEntries(lb);
|
||||
setAchievements(ach);
|
||||
}).finally(() => { if (!cancelled) setLoading(false); });
|
||||
return () => { cancelled = true; };
|
||||
}, [isOpen]);
|
||||
|
||||
@@ -52,6 +60,24 @@ export default function ButterflyLeaderboardModal({ isOpen, onClose, myStats }:
|
||||
</div>
|
||||
)}
|
||||
|
||||
{achievements && achievements.length > 0 && (
|
||||
<>
|
||||
<h6 className="butterfly-leaderboard-heading">Odznaky</h6>
|
||||
<div className="butterfly-achievements">
|
||||
{achievements.map(a => (
|
||||
<div
|
||||
key={a.id}
|
||||
className={a.unlocked ? 'butterfly-achievement unlocked' : 'butterfly-achievement'}
|
||||
title={a.description}
|
||||
>
|
||||
<span className="ach-icon">{a.unlocked ? '🏅' : '🔒'}</span>
|
||||
<span className="ach-title">{a.title}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<h6 className="butterfly-leaderboard-heading">Žebříček týmu</h6>
|
||||
{loading && <p className="text-center text-muted mb-0">Načítám žebříček…</p>}
|
||||
{!loading && entries && entries.length === 0 && (
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
.shop-balance {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 700;
|
||||
color: #e0a200;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.shop-heading {
|
||||
font-weight: 700;
|
||||
margin: 14px 0 8px;
|
||||
}
|
||||
|
||||
.butterfly-shop-daily {
|
||||
padding: 10px 12px;
|
||||
border-radius: 12px;
|
||||
background: rgba(28, 126, 214, 0.12);
|
||||
border: 1px solid rgba(28, 126, 214, 0.35);
|
||||
margin-bottom: 12px;
|
||||
|
||||
.daily-title {
|
||||
font-weight: 700;
|
||||
font-size: 0.9rem;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.daily-bar {
|
||||
height: 8px;
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
border-radius: 999px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.daily-bar-inner {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #4dabf7, #1c7ed6);
|
||||
border-radius: 999px;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.daily-status {
|
||||
margin-top: 5px;
|
||||
font-size: 0.8rem;
|
||||
color: #495057;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
}
|
||||
|
||||
.butterfly-shop-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.butterfly-shop-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
border-radius: 10px;
|
||||
background: #fff;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
transition: background 0.12s ease, transform 0.08s ease;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: #f1f3f5;
|
||||
}
|
||||
|
||||
&:active:not(:disabled) {
|
||||
transform: scale(0.99);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.shop-item-label {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.shop-item-lvl {
|
||||
display: inline;
|
||||
color: #1c7ed6;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.shop-item-desc {
|
||||
font-weight: 400;
|
||||
font-size: 0.74rem;
|
||||
color: #868e96;
|
||||
}
|
||||
|
||||
.shop-item-cost {
|
||||
flex: 0 0 auto;
|
||||
font-weight: 800;
|
||||
font-variant-numeric: tabular-nums;
|
||||
color: #e0a200;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
import { Modal } from "react-bootstrap";
|
||||
import { ButterflyStats } from "../../../../types";
|
||||
import "./ButterflyShopModal.scss";
|
||||
|
||||
type UpgradeId = 'net' | 'scarecrow' | 'reinforced';
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
stats?: ButterflyStats;
|
||||
onBuyPremium: () => void;
|
||||
onWaspSpray: () => void;
|
||||
onRepellent: () => void;
|
||||
onBuyInsurance: () => void;
|
||||
onBuyUpgrade: (item: UpgradeId) => void;
|
||||
};
|
||||
|
||||
// Zrcadlí serverové hodnoty (server/src/butterflies.ts)
|
||||
const PREMIUM_BUY_COST = 40;
|
||||
const WASP_SPRAY_COST = 25;
|
||||
const REPELLENT_COST = 70;
|
||||
const INSURANCE_COST = 50;
|
||||
const UPGRADE_BASE: Record<UpgradeId, number> = { net: 100, scarecrow: 120, reinforced: 150 };
|
||||
const UPGRADE_MAX: Record<UpgradeId, number> = { net: 5, scarecrow: 4, reinforced: 3 };
|
||||
const UPGRADE_META: Record<UpgradeId, { icon: string; title: string; desc: string }> = {
|
||||
net: { icon: '🥅', title: 'Větší síťka', desc: 'Zvětší dosah chytání' },
|
||||
scarecrow: { icon: '🐦⬛', title: 'Strašák', desc: 'Ptáci přibývají pomaleji' },
|
||||
reinforced: { icon: '🧵', title: 'Zpevněná síťka', desc: 'Rychlejší samo-zašití' },
|
||||
};
|
||||
|
||||
function upgradeCost(item: UpgradeId, level: number): number {
|
||||
return UPGRADE_BASE[item] * Math.pow(2, level);
|
||||
}
|
||||
|
||||
/** Obchod: spotřební pomůcky a trvalá vylepšení za mince. */
|
||||
export default function ButterflyShopModal({
|
||||
isOpen, onClose, stats, onBuyPremium, onWaspSpray, onRepellent, onBuyInsurance, onBuyUpgrade,
|
||||
}: Readonly<Props>) {
|
||||
const coins = stats?.coins ?? 0;
|
||||
const insuranceActive = !!stats && stats.insuranceUntil > Date.now();
|
||||
|
||||
const consumable = (label: string, cost: number, onBuy: () => void, extra?: string) => (
|
||||
<button
|
||||
type="button"
|
||||
className="butterfly-shop-item"
|
||||
onClick={onBuy}
|
||||
disabled={coins < cost}
|
||||
>
|
||||
<span className="shop-item-label">{label}{extra ? ` ${extra}` : ''}</span>
|
||||
<span className="shop-item-cost">{cost} 🪙</span>
|
||||
</button>
|
||||
);
|
||||
|
||||
return (
|
||||
<Modal show={isOpen} onHide={onClose} centered>
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title>🛒 Obchod <span className="shop-balance">{coins} 🪙</span></Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
{stats?.daily && (
|
||||
<div className="butterfly-shop-daily">
|
||||
<div className="daily-title">🎯 Denní úkol: {stats.daily.title}</div>
|
||||
<div className="daily-bar">
|
||||
<div
|
||||
className="daily-bar-inner"
|
||||
style={{ width: `${Math.min(100, Math.round((stats.daily.progress / stats.daily.target) * 100))}%` }}
|
||||
/>
|
||||
</div>
|
||||
<div className="daily-status">
|
||||
{stats.daily.done
|
||||
? `✅ Splněno (+${stats.daily.reward} 🪙)`
|
||||
: `${stats.daily.progress}/${stats.daily.target} · odměna ${stats.daily.reward} 🪙`}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<h6 className="shop-heading">Pomůcky</h6>
|
||||
<div className="butterfly-shop-list">
|
||||
{consumable('🪄 Prémiová síťka (60 s)', PREMIUM_BUY_COST, onBuyPremium)}
|
||||
{consumable('💨 Vosí sprej (vyhubí vosy)', WASP_SPRAY_COST, onWaspSpray)}
|
||||
{consumable('🦅🚫 Plašič ptáků', REPELLENT_COST, onRepellent)}
|
||||
{consumable('🛡️ Pojistka proti zloději', INSURANCE_COST, onBuyInsurance, insuranceActive ? '(aktivní)' : '')}
|
||||
</div>
|
||||
|
||||
<h6 className="shop-heading">Trvalá vylepšení</h6>
|
||||
<div className="butterfly-shop-list">
|
||||
{(Object.keys(UPGRADE_META) as UpgradeId[]).map(item => {
|
||||
const level = stats?.upgrades?.[item] ?? 0;
|
||||
const max = UPGRADE_MAX[item];
|
||||
const maxed = level >= max;
|
||||
const cost = upgradeCost(item, level);
|
||||
const meta = UPGRADE_META[item];
|
||||
return (
|
||||
<button
|
||||
key={item}
|
||||
type="button"
|
||||
className="butterfly-shop-item upgrade"
|
||||
onClick={() => onBuyUpgrade(item)}
|
||||
disabled={maxed || coins < cost}
|
||||
>
|
||||
<span className="shop-item-label">
|
||||
{meta.icon} {meta.title} <span className="shop-item-lvl">{level}/{max}</span>
|
||||
<span className="shop-item-desc">{meta.desc}</span>
|
||||
</span>
|
||||
<span className="shop-item-cost">{maxed ? 'MAX' : `${cost} 🪙`}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Modal.Body>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user