/**
* SkinShopOverlay — встроенный магазин скинов игрока (задача 07).
*
* Готовый GUI-кит: полноэкранная витрина карточек скинов. Открывается
* клавишей B в Play или через game.player.openSkinShop(). Логика покупки
* (списание локальных рубликов проекта, unlock, setSkin) живёт в GameRuntime;
* этот компонент только рендерит состояние и шлёт намерение «купить/надеть».
*
* Подписка на состояние — rAF-поллинг scene.getSkinShopState() (как ModalOverlay):
* { open, rev, data: { all:[{slug,name,kind,category,price}], unlocked:[slug],
* current, coins, shopVisible } }
*
* Превью скина — цветная плашка по категории + крупная самописная SVG-иконка
* (правило проекта: без эмодзи в UI). Категории: human/animal/food/vehicle/robot.
*/
import React, { useEffect, useState, useMemo } from 'react';
// Палитра градиентов по категории — чтобы витрина была живой и читаемой.
const CAT_THEME = {
human: { from: '#3b6cff', to: '#1e2da5', label: 'Люди' },
animal: { from: '#46b34a', to: '#1f6b2a', label: 'Животные' },
food: { from: '#ff8a3d', to: '#c2410c', label: 'Еда' },
vehicle: { from: '#9b59ff', to: '#5b21b6', label: 'Транспорт' },
robot: { from: '#22c6d6', to: '#0e7490', label: 'Роботы' },
custom: { from: '#fbbf24', to: '#b45309', label: 'Свои' },
};
const CAT_ORDER = ['all', 'human', 'animal', 'food', 'vehicle', 'robot', 'custom'];
// Самописные SVG-иконки категорий (viewBox 24×24, обводка currentColor).
function CatGlyph({ cat, size = 46 }) {
const st = { fill: 'none', stroke: 'currentColor', strokeWidth: 1.6, strokeLinecap: 'round', strokeLinejoin: 'round' };
let body;
switch (cat) {
case 'human':
body = (<>>);
break;
case 'animal': // мордочка зверя с ушами
body = (<>>);
break;
case 'food': // пончик
body = (<>>);
break;
case 'vehicle': // машинка
body = (<>>);
break;
case 'robot': // голова робота
body = (<>>);
break;
default: // custom — звезда
body = ();
}
return ();
}
// Монета-рублик (для баланса/цены).
function CoinIcon({ size = 16 }) {
return (
);
}
export default function SkinShopOverlay({ scene }) {
const [snap, setSnap] = useState(null);
const [cat, setCat] = useState('all');
// rAF-поллинг состояния магазина из сцены.
useEffect(() => {
if (!scene?.getSkinShopState) return;
let cancelled = false;
let lastRev = -1;
const tick = () => {
if (cancelled) return;
const s = scene.getSkinShopState?.();
if (s && s.rev !== lastRev) {
lastRev = s.rev;
setSnap({
open: s.open,
data: s.data,
buyResult: s.buyResult,
});
} else if (!s && lastRev !== -1) {
lastRev = -1;
setSnap(null);
}
requestAnimationFrame(tick);
};
tick();
return () => { cancelled = true; };
}, [scene]);
const data = snap?.data || null;
// Список скинов с категориями (фильтрованный).
const skins = useMemo(() => {
const all = (data?.all) || [];
if (cat === 'all') return all;
return all.filter(s => (s.category || 'human') === cat);
}, [data, cat]);
// Какие категории реально есть — для табов.
const cats = useMemo(() => {
const present = new Set((data?.all || []).map(s => s.category || 'human'));
return CAT_ORDER.filter(c => c === 'all' || present.has(c));
}, [data]);
if (!snap || !snap.open || !data) return null;
const unlocked = new Set(data.unlocked || []);
const current = data.current;
const coins = data.coins || 0;
const close = () => { try { scene._closeSkinShop?.(); } catch (e) {} };
const onCardClick = (s) => {
const owned = unlocked.has(s.slug);
const price = s.price || 0;
if (!owned && coins < price) return; // не хватает — карточка покажет это
try { scene.requestBuySkin?.(s.slug, price); } catch (e) {}
};
return (
e.stopPropagation()}
style={{
width: 'min(880px, 92vw)', maxHeight: '86vh',
background: 'linear-gradient(160deg, #161b2e 0%, #0c1020 100%)',
border: '2px solid #2b3a66', borderRadius: 20,
boxShadow: '0 24px 60px rgba(0,0,0,0.55)',
display: 'flex', flexDirection: 'column', overflow: 'hidden',
}}
>
{/* Шапка */}
Магазин скинов
{/* Баланс */}
{coins}
{/* Закрыть */}
{/* Табы категорий */}
{cats.map(c => {
const active = c === cat;
const label = c === 'all' ? 'Все' : (CAT_THEME[c]?.label || c);
return (
);
})}
{/* Сетка карточек */}
{skins.map(s => {
const theme = CAT_THEME[s.category] || CAT_THEME.human;
const owned = unlocked.has(s.slug);
const isActive = current === s.slug;
const price = s.price || 0;
const canAfford = owned || coins >= price;
return (
onCardClick(s)}
style={{
borderRadius: 16, overflow: 'hidden', cursor: canAfford ? 'pointer' : 'not-allowed',
border: isActive ? '3px solid #22ff88' : '2px solid rgba(255,255,255,0.10)',
background: 'rgba(255,255,255,0.04)',
opacity: canAfford ? 1 : 0.55,
transition: 'transform 0.1s, border-color 0.15s',
position: 'relative',
}}
onMouseEnter={(e) => { if (canAfford) e.currentTarget.style.transform = 'translateY(-3px)'; }}
onMouseLeave={(e) => { e.currentTarget.style.transform = 'none'; }}
>
{/* Превью-плашка с иконкой категории */}
{/* Бейдж активного/купленного */}
{isActive && (
Надет
)}
{!isActive && owned && (
Куплено
)}
{/* Низ карточки: имя + цена/статус */}
{s.name || s.slug}
{isActive ? (
Активен
) : owned ? (
Нажми, чтобы надеть
) : price === 0 ? (
Бесплатно
) : (
{price}
)}
);
})}
{skins.length === 0 && (
В этой категории пока нет скинов
)}
{/* Подвал-подсказка */}
Нажми B или Esc, чтобы закрыть
);
}
function badgeStyle(bg, fg) {
return {
position: 'absolute', top: 8, right: 8,
background: bg, color: fg,
fontSize: 11, fontWeight: 900, padding: '3px 8px', borderRadius: 999,
boxShadow: '0 2px 6px rgba(0,0,0,0.4)',
};
}