import React, { useState, useEffect, useRef, useMemo } from 'react';
import cl from './GameSettingsModal.module.css';
import Icon from './Icon';
import { BillboardUiManager } from './engine/BillboardUiManager';
/**
* BillboardEditorModal — редактор контента 3D-таблички (billboard primitive).
*
* Открывается из InspectorPanel при клике «Редактировать табличку…».
* Слева — форма (пресет + контент-поля), справа — живое превью на canvas
* (тот же рендер что в BillboardUiManager._render).
*
* Пресеты:
* shop-item — иконка | заголовок + sub | кнопка цены
* shop-purchase — иконка | название | sub | кнопка
* banner — крупный текст по центру
* sign — простой указатель
*
* Props:
* open — boolean
* primitiveData — { id, billboard: {template, face, content, elements} }
* onClose
* onApply(opts) — { template, face, content }
*/
const TEMPLATES = BillboardUiManager.getAvailableTemplates();
const ICONS = BillboardUiManager.getAvailableIcons();
// 8 готовых цветовых пресетов градиента — чтобы дети не подбирали HEX'ы.
const GRADIENT_PRESETS = [
{ name: 'Красный', from: '#ff5a5a', to: '#ff8a3d' },
{ name: 'Синий', from: '#3b82f6', to: '#0ea5e9' },
{ name: 'Зелёный', from: '#10b981', to: '#34d399' },
{ name: 'Фиолетовый', from: '#7c3aed', to: '#a855f7' },
{ name: 'Жёлтый', from: '#f59e0b', to: '#fbbf24' },
{ name: 'Розовый', from: '#ec4899', to: '#f472b6' },
{ name: 'Бирюзовый', from: '#06b6d4', to: '#22d3ee' },
{ name: 'Тёмный', from: '#1f2937', to: '#374151' },
];
const BillboardEditorModal = ({ open, primitiveData, onClose, onApply }) => {
const initial = primitiveData?.billboard || {};
const [template, setTemplate] = useState(initial.template || 'shop-item');
const [face, setFace] = useState(initial.face || 'camera');
const initContent = initial.content || {};
const [icon, setIcon] = useState(initContent.icon || 'hammer');
const [title, setTitle] = useState(initContent.title || 'Апгрейд');
const [sub, setSub] = useState(initContent.sub || '1 > 2');
const [price, setPrice] = useState(initContent.price || '$10,000');
const [gradFrom, setGradFrom] = useState(
(initContent.gradient && initContent.gradient[0]) || '#ff5a5a');
const [gradTo, setGradTo] = useState(
(initContent.gradient && initContent.gradient[1]) || '#ff8a3d');
// Перезагружаем поля при открытии новой таблички
useEffect(() => {
if (!open) return;
const init = primitiveData?.billboard || {};
const c = init.content || {};
setTemplate(init.template || 'shop-item');
setFace(init.face || 'camera');
setIcon(c.icon || 'hammer');
setTitle(c.title || (init.template === 'banner' ? 'Заголовок баннера' : 'Апгрейд'));
setSub(c.sub || '1 > 2');
setPrice(c.price || '$10,000');
if (c.gradient) {
setGradFrom(c.gradient[0] || '#ff5a5a');
setGradTo(c.gradient[1] || '#ff8a3d');
}
}, [open, primitiveData]);
const templateDef = useMemo(
() => TEMPLATES.find(t => t.id === template) || TEMPLATES[0],
[template]);
// Поля доступные для текущего шаблона
const fields = templateDef.fields;
// === Живое превью на canvas ===
const canvasRef = useRef(null);
const previewMgrRef = useRef(null);
useEffect(() => {
if (!open || !canvasRef.current) return;
// Создаём отдельный canvas-based рендер. Мы НЕ используем сам BillboardUiManager
// (он требует Babylon scene). Дублируем minimal-логику рисования здесь,
// вызывая те же приватные методы рендера через статический хелпер.
// Проще всего — сделать temp-инстанс с фиктивной scene и вызвать _render.
// Но _render требует dyn.getContext()/.update() — Babylon API. Поэтому
// рисуем напрямую на 2D canvas с упрощённой копией логики ниже.
renderPreview(canvasRef.current, template, {
icon, title, sub, price,
gradient: [gradFrom, gradTo],
});
}, [open, template, icon, title, sub, price, gradFrom, gradTo]);
if (!open) return null;
const handleApply = () => {
const content = {};
if (fields.includes('icon')) content.icon = icon;
if (fields.includes('title')) content.title = title;
if (fields.includes('sub')) content.sub = sub;
if (fields.includes('price')) content.price = price;
if (fields.includes('gradient')) content.gradient = [gradFrom, gradTo];
onApply({ template, face, content });
};
return (
e.stopPropagation()}
style={{ maxWidth: 920, width: '90vw' }}>
{/* Левая колонка — форма */}
{TEMPLATES.map(t => (
))}
{fields.includes('icon') && (
)}
{fields.includes('title') && (
)}
{fields.includes('sub') && (
)}
{fields.includes('price') && (
)}
{fields.includes('gradient') && (
)}
{/* Правая колонка — превью */}
Превью:
{templateDef.hasButton && 'В Play кнопка кликабельна — подпишись через game.billboard.onClick(ref, "buy", fn).'}
);
};
// ─── Утилиты UI ─────────────────────────────────────────────────────────────
function Section({ title, children }) {
return (
);
}
const inputStyle = {
width: '100%',
padding: '6px 10px',
background: '#1f2937',
border: '1px solid #374151',
borderRadius: 4,
color: '#e5e7eb',
fontSize: 13,
boxSizing: 'border-box',
};
function pillStyle(active) {
return {
padding: '6px 12px',
background: active ? '#3357ff' : '#1f2937',
border: '1px solid ' + (active ? '#3357ff' : '#374151'),
borderRadius: 4,
color: active ? '#fff' : '#9ca3af',
fontSize: 12,
cursor: 'pointer',
flex: 1,
whiteSpace: 'nowrap',
};
}
// ─── Рендер превью (зеркалит BillboardUiManager._render*) ──────────────────
const PREVIEW_ICONS = {
hammer: '🔨', saw: '🪚', drop: '💧', seed: '🌱', cube: '🧊',
coin: '💰', home: '🏠', rocket: '🚀', sprinkler: '⛲', sparkle: '✨',
star: '⭐', bag: '🎒', diamond: '💎', fire: '🔥', lightning: '⚡',
heart: '❤', key: '🔑', shield: '🛡',
};
const W = 512, H = 320;
const BTN = { x: 332, y: 200, w: 160, h: 90 };
function renderPreview(canvas, template, content) {
const ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, W, H);
switch (template) {
case 'shop-item': drawShopItem(ctx, content); break;
case 'shop-purchase': drawShopPurchase(ctx, content); break;
case 'banner': drawBanner(ctx, content); break;
case 'sign': drawSign(ctx, content); break;
default: drawBanner(ctx, content);
}
}
function roundedGrad(ctx, x, y, w, h, opts) {
const r = opts.radius ?? 24;
ctx.beginPath();
ctx.moveTo(x + r, y);
ctx.arcTo(x + w, y, x + w, y + h, r);
ctx.arcTo(x + w, y + h, x, y + h, r);
ctx.arcTo(x, y + h, x, y, r);
ctx.arcTo(x, y, x + w, y, r);
ctx.closePath();
const grad = ctx.createLinearGradient(x, y, x, y + h);
const [from, to] = opts.gradient || ['#333', '#111'];
grad.addColorStop(0, from);
grad.addColorStop(1, to);
ctx.fillStyle = grad;
ctx.fill();
if (opts.stroke) {
ctx.lineWidth = opts.stroke.width ?? 3;
ctx.strokeStyle = opts.stroke.color || '#000';
ctx.stroke();
}
}
function drawShopItem(ctx, c) {
roundedGrad(ctx, 8, 8, W - 16, H - 16, {
gradient: c.gradient || ['#ff5a5a', '#ff8a3d'],
radius: 28, stroke: { color: '#0008', width: 4 },
});
ctx.beginPath();
ctx.arc(110, 130, 70, 0, Math.PI * 2);
ctx.fillStyle = 'rgba(0,0,0,0.18)';
ctx.fill();
ctx.font = 'bold 96px "Segoe UI Emoji", Arial';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillStyle = '#fff';
ctx.fillText(PREVIEW_ICONS[c.icon] || '🧊', 110, 132);
ctx.font = 'bold 36px Arial, sans-serif';
ctx.textAlign = 'left';
ctx.textBaseline = 'top';
ctx.fillText((c.title || '').slice(0, 18), 200, 50);
if (c.sub) {
ctx.font = 'bold 28px Arial, sans-serif';
ctx.fillStyle = '#a7f3d0';
ctx.fillText(c.sub, 200, 105);
}
roundedGrad(ctx, BTN.x, BTN.y, BTN.w, BTN.h, {
gradient: ['#fbbf24', '#f59e0b'], radius: 16,
stroke: { color: '#000', width: 3 },
});
ctx.font = 'bold 36px Arial, sans-serif';
ctx.fillStyle = '#1c1917';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(c.price || '$0', BTN.x + BTN.w / 2, BTN.y + BTN.h / 2);
}
function drawShopPurchase(ctx, c) {
roundedGrad(ctx, 8, 8, W - 16, H - 16, {
gradient: c.gradient || ['#3b82f6', '#0ea5e9'],
radius: 28, stroke: { color: '#0008', width: 4 },
});
ctx.font = 'bold 110px "Segoe UI Emoji", Arial';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillStyle = '#fff';
ctx.fillText(PREVIEW_ICONS[c.icon] || '🎒', 110, 140);
ctx.font = 'bold 34px Arial, sans-serif';
ctx.textAlign = 'left';
ctx.textBaseline = 'top';
ctx.fillText((c.title || '').slice(0, 16), 200, 50);
if (c.sub) {
ctx.font = 'bold 26px Arial, sans-serif';
ctx.fillStyle = '#dbeafe';
ctx.fillText(c.sub, 200, 100);
}
roundedGrad(ctx, BTN.x, BTN.y, BTN.w, BTN.h, {
gradient: ['#a855f7', '#7c3aed'], radius: 16,
stroke: { color: '#000', width: 3 },
});
ctx.font = 'bold 34px Arial, sans-serif';
ctx.fillStyle = '#fff';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(c.price || '0 R', BTN.x + BTN.w / 2, BTN.y + BTN.h / 2);
}
function drawBanner(ctx, c) {
roundedGrad(ctx, 8, 8, W - 16, H - 16, {
gradient: c.gradient || ['#7c3aed', '#a855f7'],
radius: 28, stroke: { color: '#0008', width: 4 },
});
ctx.font = 'bold 46px Arial, sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillStyle = '#fff';
ctx.shadowColor = 'rgba(0,0,0,0.45)';
ctx.shadowBlur = 8;
ctx.shadowOffsetY = 3;
const lines = wrapText(ctx, c.title || '', W - 80);
const lh = 56;
const sy = H / 2 - (lines.length - 1) * lh / 2;
for (let i = 0; i < lines.length; i++) {
ctx.fillText(lines[i], W / 2, sy + i * lh);
}
ctx.shadowColor = 'transparent';
ctx.shadowBlur = 0;
ctx.shadowOffsetY = 0;
}
function drawSign(ctx, c) {
roundedGrad(ctx, 8, 8, W - 16, H - 16, {
gradient: c.gradient || ['#1f2937', '#374151'],
radius: 20, stroke: { color: '#fff', width: 4 },
});
ctx.font = 'bold 64px Arial, sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillStyle = '#fff';
ctx.fillText((c.title || '').slice(0, 14), W / 2, H / 2);
}
function wrapText(ctx, text, maxW) {
const words = (text || '').split(' ');
const lines = [];
let cur = '';
for (const w of words) {
const t = cur ? cur + ' ' + w : w;
if (ctx.measureText(t).width <= maxW) cur = t;
else { if (cur) lines.push(cur); cur = w; }
}
if (cur) lines.push(cur);
return lines.slice(0, 3);
}
export default BillboardEditorModal;