/** * GdDecoPreview — превью декораций ландшафта (10 эпох × 10 = 100). * Маршрут: /admin-preview/gd-deco * Multi-select: до 5 моделей на эпоху. Выбор → kubikon3d_savegame * (project_id=295, namespace='gd_deco_choices'). data = { 1: ['d1_v1','d1_v3',...] }. */ import React, { useEffect, useRef, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { useAuth } from '../auth/AuthContext.jsx'; import { jwtDecode } from 'jwt-decode'; import axios from 'axios'; import { STORYS_addres } from '../api/API'; import { Engine } from '@babylonjs/core/Engines/engine'; import { Scene } from '@babylonjs/core/scene'; import { ArcRotateCamera } from '@babylonjs/core/Cameras/arcRotateCamera'; import { HemisphericLight } from '@babylonjs/core/Lights/hemisphericLight'; import { DirectionalLight } from '@babylonjs/core/Lights/directionalLight'; import { Vector3, Color3, Color4 } from '@babylonjs/core/Maths/math'; import { MeshBuilder } from '@babylonjs/core/Meshes/meshBuilder'; import { StandardMaterial } from '@babylonjs/core/Materials/standardMaterial'; import { DECO_CATALOG, EPOCH_INFO, getDecoByEpoch } from './gdDeco/decoFactories'; const CHOICES_PID = 295; const CHOICES_NS = 'gd_deco_choices'; const MAX_PER_EPOCH = 5; function getUserId() { try { const t = localStorage.getItem('Authorization'); if (!t) return 0; return Number(jwtDecode(t).id) || 0; } catch (e) { return 0; } } const api = axios.create({ baseURL: STORYS_addres, timeout: 15000 }); api.interceptors.request.use((cfg) => { try { const token = localStorage.getItem('Authorization'); if (token) cfg.headers.Authorization = token; } catch (e) {} return cfg; }); function DecoCard({ deco, isChosen, onToggle, disabled }) { const wrapRef = useRef(null); const canvasRef = useRef(null); const [isVisible, setIsVisible] = useState(false); useEffect(() => { if (!wrapRef.current) return; const io = new IntersectionObserver((entries) => { for (const e of entries) setIsVisible(e.isIntersecting); }, { rootMargin: '200px', threshold: 0.01 }); io.observe(wrapRef.current); return () => io.disconnect(); }, []); useEffect(() => { if (!isVisible || !canvasRef.current) return; let engine = null, scene = null; try { engine = new Engine(canvasRef.current, true, { stencil: false, antialias: true }); scene = new Scene(engine); scene.clearColor = new Color4(0.05, 0.07, 0.12, 1); const camera = new ArcRotateCamera('cam', -Math.PI / 2.5, Math.PI / 2.7, 5, new Vector3(0, 1.2, 0), scene); camera.attachControl(canvasRef.current, false); camera.minZ = 0.1; camera.maxZ = 50; new HemisphericLight('hemi', new Vector3(0, 1, 0), scene).intensity = 0.7; const sun = new DirectionalLight('sun', new Vector3(-0.5, -1, -0.3), scene); sun.intensity = 0.8; const floor = MeshBuilder.CreateGround('floor', { width: 4, height: 4 }, scene); const fmat = new StandardMaterial('fmat', scene); fmat.diffuseColor = new Color3(0.18, 0.22, 0.20); floor.material = fmat; const handle = deco.make(scene, `prev_${deco.id}`); if (handle && handle.root) handle.root.position.y = 0; scene.onBeforeRenderObservable.add(() => { if (handle && handle.root) handle.root.rotation.y += 0.008; }); engine.runRenderLoop(() => scene.render()); return () => { try { engine && engine.stopRenderLoop(); } catch (e) {} try { handle && handle.dispose && handle.dispose(); } catch (e) {} try { scene && scene.dispose(); } catch (e) {} try { engine && engine.dispose(); } catch (e) {} }; } catch (e) { console.warn('[DecoCard]', e); return () => {}; } }, [isVisible, deco]); return (
{ if (!disabled || isChosen) onToggle(); }} style={{ ...cardStyle, border: isChosen ? '3px solid #22ff66' : '2px solid #2a3146', boxShadow: isChosen ? '0 0 16px rgba(34,255,102,0.4)' : 'none', cursor: disabled && !isChosen ? 'not-allowed' : 'pointer', opacity: disabled && !isChosen ? 0.45 : 1, }} > {isVisible ? :
•••
}
{deco.title}
{deco.id}
{isChosen && }
); } export default function GdDecoPreview() { const { userRole, isLoading } = useAuth(); const navigate = useNavigate(); // choices: { '1': ['d1_v1','d1_v4',...], '2': [...], ... } const [choices, setChoices] = useState({}); const [status, setStatus] = useState('idle'); useEffect(() => { const uid = getUserId(); if (!uid) return; api.get(`/kubikon3d/savegame/${CHOICES_PID}/${uid}/${CHOICES_NS}`) .then((r) => setChoices(r.data?.data || {})) .catch(() => {}); }, []); const toggle = (epoch, decoId) => { setChoices(prev => { const arr = prev[epoch] || []; const has = arr.includes(decoId); let next; if (has) { next = arr.filter(x => x !== decoId); } else { if (arr.length >= MAX_PER_EPOCH) return prev; next = [...arr, decoId]; } return { ...prev, [epoch]: next }; }); setStatus('idle'); }; const save = async () => { const uid = getUserId(); if (!uid) { setStatus('error'); return; } setStatus('loading'); try { await api.post(`/kubikon3d/savegame/${CHOICES_PID}/${uid}/${CHOICES_NS}`, { data: choices }); setStatus('saved'); } catch (e) { console.warn('[GdDecoPreview] save failed', e); setStatus('error'); } }; if (isLoading) return
Загрузка...
; if (userRole !== 'admin') { return (

Доступ только для администратора

); } const totalChosen = Object.values(choices).reduce((s, arr) => s + (arr?.length || 0), 0); return (

GD — Декорации ландшафта ({DECO_CATALOG.length})

Выбрано: {totalChosen} (до 5 на эпоху)
{status === 'error' && Ошибка}
{EPOCH_INFO.map(epoch => { const items = getDecoByEpoch(epoch.n); const arr = choices[epoch.n] || []; const fullyChosen = arr.length >= MAX_PER_EPOCH; return (

{epoch.emoji} Эпоха {epoch.n} — {epoch.name} L{(epoch.n - 1) * 10 + 1} – L{epoch.n * 10} выбрано {arr.length}/{MAX_PER_EPOCH}

{items.map(deco => ( toggle(epoch.n, deco.id)} disabled={fullyChosen} /> ))}
); })}
); } const cardStyle = { background: '#0e1525', borderRadius: 10, overflow: 'hidden', transition: 'all 0.15s' }; const btnStyle = { padding: '10px 18px', background: '#2a3146', color: '#cdd4e0', border: 'none', borderRadius: 8, cursor: 'pointer', fontWeight: 600, fontSize: 14 }; const btnPrimary = { padding: '10px 24px', background: 'linear-gradient(135deg, #22ff66, #44aaff)', color: '#000', border: 'none', borderRadius: 8, cursor: 'pointer', fontWeight: 700, fontSize: 15 };