feat(pet): кастомизация вынесена в модалку-«гардеробную» с живым превью

Кнопка «Нарядить» на сцене открывает модалку: слева крупное живое превью
питомца (обновляется мгновенно при любой смене — аксессуар/цвет/узор/фон),
справа вкладки Аксессуары/Цвет/Узор/Фон. Превью-сцена отражает выбранный
фон. Закрытие крестиком, кликом по фону, Esc. Инлайн-карточка убрана со
страницы; все рендереры идут через общий paintPet (сцена + превью).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Maxim Dolgolyov
2026-06-05 14:15:59 +03:00
parent cac352b355
commit ac618b3fb1
+111 -40
View File
@@ -319,7 +319,31 @@
/* B3 — Rainbow collar keyframe */
@keyframes rbRot { to { transform:rotate(360deg); } }
/* ── Customize panel (полностью переработанный вид) ── */
/* ── Гардеробная (модалка) ── */
.wr-modal-overlay { position:fixed; inset:0; z-index:1100; display:flex; align-items:center; justify-content:center;
background:rgba(0,0,0,.66); backdrop-filter:blur(6px); padding:20px; animation:pcFade .2s ease; }
.wr-modal { width:780px; max-width:96vw; max-height:90vh; overflow:auto; background:var(--surface);
border:1.5px solid rgba(155,93,229,.22); border-radius:22px; box-shadow:0 30px 90px rgba(0,0,0,.55); padding:20px 22px 24px; }
.wr-modal-head { display:flex; align-items:center; gap:11px; margin-bottom:18px; }
.wr-modal-titles { flex:1; min-width:0; }
.wr-modal-close { width:32px; height:32px; border:none; background:rgba(255,255,255,.06); border-radius:9px; color:var(--text-2);
cursor:pointer; display:flex; align-items:center; justify-content:center; flex-shrink:0; transition:all .15s; }
.wr-modal-close svg { width:17px; height:17px; }
.wr-modal-close:hover { background:rgba(255,255,255,.12); color:var(--text); }
.wr-modal-body { display:grid; grid-template-columns:236px 1fr; gap:22px; align-items:start; }
.wr-modal-preview { display:flex; flex-direction:column; align-items:center; gap:8px; position:sticky; top:0; }
.wr-prev-scene { width:100%; aspect-ratio:1; border-radius:20px; display:flex; align-items:center; justify-content:center;
overflow:hidden; border:1.5px solid rgba(255,255,255,.08); background:linear-gradient(155deg,#0d0d1f,#1a1040); }
.wr-prev-scene svg { width:74%; height:auto; }
.wr-prev-name { font-family:'Unbounded',sans-serif; font-weight:800; font-size:.95rem; }
.wr-prev-evo { font-size:.7rem; color:var(--text-3); }
@media (max-width:640px) {
.wr-modal-body { grid-template-columns:1fr; gap:16px; }
.wr-modal-preview { position:static; }
.wr-prev-scene { max-width:190px; margin:0 auto; }
}
/* ── Customize controls (внутри модалки) ── */
.pet-customize { background:linear-gradient(165deg,rgba(155,93,229,.10),rgba(6,214,224,.045)),var(--surface);
border:1.5px solid rgba(155,93,229,.2); border-radius:20px; padding:16px 18px 20px; margin-bottom:18px; }
.pc-head { display:flex; align-items:center; gap:11px; margin-bottom:15px; }
@@ -463,6 +487,10 @@
<div class="pet-evo-legend" title="Облик растёт сам с уровнем (за XP): Ур.2 — уши, Ур.3 — антенны, Ур.4 — крылья и аура, Ур.5 — корона-сияние и орбиты, Ур.6 — вторая аура, Ур.7 — нимб и вторые крылья, Ур.8 — золотое сияние.">облик растёт с XP — наведи, чтобы узнать, что добавляется на каждом уровне</div>
</div>
<button class="pet-action-btn" id="btn-customize" onclick="openWardrobe()" style="background:linear-gradient(135deg,rgba(155,93,229,.2),rgba(6,214,224,.12));border-color:rgba(155,93,229,.4);color:#c9a6ff">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20.38 3.46 16 2a4 4 0 0 1-8 0L3.62 3.46a2 2 0 0 0-1.34 2.23l.58 3.47a1 1 0 0 0 .99.84H6v10c0 1.1.9 2 2 2h8a2 2 0 0 0 2-2V10h2.15a1 1 0 0 0 .99-.84l.58-3.47a2 2 0 0 0-1.34-2.23z"/></svg>
Нарядить
</button>
<button class="pet-action-btn" id="btn-pet" onclick="petThePet()">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 11V6a2 2 0 0 0-4 0v0"/><path d="M14 10V4a2 2 0 0 0-4 0v2"/><path d="M10 10.5V6a2 2 0 0 0-4 0v8"/><path d="M6 14v0a2 2 0 0 0-2-2H2v5l6.5 6.5a2 2 0 0 0 2.8 0l3.7-3.7a2 2 0 0 0 0-2.8L18 18"/></svg>
Погладить
@@ -540,39 +568,6 @@
</div>
</div>
<!-- ── Кастомизация ── -->
<div class="pet-customize">
<div class="pc-head">
<div class="pc-head-ico"><i data-lucide="palette"></i></div>
<div>
<div class="pc-head-title">Кастомизация</div>
<div class="pc-head-sub">Наряди питомца: аксессуары, цвет, узор и фон</div>
</div>
</div>
<div class="pc-tabs">
<button class="pc-tab active" data-tab="acc" type="button"><i data-lucide="shirt"></i> Аксессуары</button>
<button class="pc-tab" data-tab="color" type="button"><i data-lucide="droplet"></i> Цвет</button>
<button class="pc-tab" data-tab="pattern" type="button"><i data-lucide="grid-2x2"></i> Узор</button>
<button class="pc-tab" data-tab="bg" type="button"><i data-lucide="image"></i> Фон</button>
</div>
<div class="pc-panel" id="pc-acc">
<div class="pc-hint">Нажми, чтобы надеть или снять. Заблокированные открываются за достижения. По одному предмету на зону.</div>
<div id="pet-accessories"></div>
</div>
<div class="pc-panel" id="pc-color" style="display:none">
<div class="pc-hint">Основной цвет питомца.</div>
<div class="pet-color-picker" id="color-picker"></div>
</div>
<div class="pc-panel" id="pc-pattern" style="display:none">
<div class="pc-hint">Узор на теле питомца.</div>
<div class="pc-pattern-grid" id="pc-pattern-grid"></div>
</div>
<div class="pc-panel" id="pc-bg" style="display:none">
<div class="pc-hint">Фон сцены. <span id="pc-bg-coins"></span></div>
<div class="pc-bg-grid" id="pc-bg-grid"><div style="color:var(--text-2);font-size:.8rem">Загрузка…</div></div>
</div>
</div>
<!-- ── Bottom ── -->
<div class="pet-bottom">
@@ -622,6 +617,53 @@
</div>
</div>
<!-- ── Гардеробная (модалка) ── -->
<div id="wardrobe-modal" class="wr-modal-overlay" style="display:none">
<div class="wr-modal">
<div class="wr-modal-head">
<div class="pc-head-ico"><i data-lucide="shirt"></i></div>
<div class="wr-modal-titles">
<div class="pc-head-title">Гардеробная</div>
<div class="pc-head-sub">Наряди питомца — изменения видно сразу</div>
</div>
<button class="wr-modal-close" onclick="closeWardrobe()" title="Закрыть">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
</button>
</div>
<div class="wr-modal-body">
<div class="wr-modal-preview">
<div class="wr-prev-scene" id="wr-preview-scene"><div id="wr-preview-svg"></div></div>
<div class="wr-prev-name" id="wr-preview-name">Квантик</div>
<div class="wr-prev-evo" id="wr-preview-evo"></div>
</div>
<div class="wr-modal-controls">
<div class="pc-tabs">
<button class="pc-tab active" data-tab="acc" type="button"><i data-lucide="shirt"></i> Аксессуары</button>
<button class="pc-tab" data-tab="color" type="button"><i data-lucide="droplet"></i> Цвет</button>
<button class="pc-tab" data-tab="pattern" type="button"><i data-lucide="grid-2x2"></i> Узор</button>
<button class="pc-tab" data-tab="bg" type="button"><i data-lucide="image"></i> Фон</button>
</div>
<div class="pc-panel" id="pc-acc">
<div class="pc-hint">Нажми, чтобы надеть или снять. Заблокированные открываются за достижения. По одному предмету на зону.</div>
<div id="pet-accessories"></div>
</div>
<div class="pc-panel" id="pc-color" style="display:none">
<div class="pc-hint">Основной цвет питомца.</div>
<div class="pet-color-picker" id="color-picker"></div>
</div>
<div class="pc-panel" id="pc-pattern" style="display:none">
<div class="pc-hint">Узор на теле питомца.</div>
<div class="pc-pattern-grid" id="pc-pattern-grid"></div>
</div>
<div class="pc-panel" id="pc-bg" style="display:none">
<div class="pc-hint">Фон сцены. <span id="pc-bg-coins"></span></div>
<div class="pc-bg-grid" id="pc-bg-grid"></div>
</div>
</div>
</div>
</div>
</div>
<!-- ── Feed mini-game overlay ── -->
<div id="feed-overlay" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,.65);z-index:1000;align-items:center;justify-content:center;backdrop-filter:blur(6px)">
<div style="background:var(--surface);border:1.5px solid rgba(255,255,255,.1);border-radius:22px;padding:28px 26px;width:380px;max-width:94vw;box-shadow:0 24px 80px rgba(0,0,0,.5)">
@@ -1013,6 +1055,9 @@ function setupCustomizeTabs() {
document.getElementById('pc-bg').style.display = which === 'bg' ? '' : 'none';
if (which === 'bg') renderBgPicker();
}));
const ov = document.getElementById('wardrobe-modal');
if (ov) ov.addEventListener('click', e => { if (e.target === ov) closeWardrobe(); });
document.addEventListener('keydown', e => { if (e.key === 'Escape') closeWardrobe(); });
}
/* ── Фоны (инлайн-выбор во вкладке) ── */
@@ -1048,6 +1093,7 @@ async function selectBgInline(id, price, owned) {
scene.className = scene.className.replace(/\bbg-\S+/g, '') + (id !== 'default' ? ` bg-${id}` : '');
if (_petData) _petData.petBg = id;
applyBgFX(id);
updatePreviewScene();
if (res.coins !== undefined) {
document.getElementById('stat-coins').textContent = res.coins;
if (_petData) _petData.coins = res.coins;
@@ -1288,8 +1334,7 @@ async function selectColor(colorKey) {
const res = await LS.api('/api/pet/color', {method:'PATCH', body: JSON.stringify({color:colorKey})}).catch(()=>null);
if (!res?.ok) return;
_petData.petColor = colorKey;
document.getElementById('pet-svg-wrap').innerHTML =
renderPetSVG(_petData.petLevel, _petData.mood, _petData.accessories, colorKey, _petData.streakCurrent || 0, _petData.petPattern || 'none');
paintPet();
document.querySelectorAll('.pet-color-dot').forEach(d =>
d.classList.toggle('active', d.dataset.color === colorKey)
);
@@ -1301,6 +1346,34 @@ const CHECK_ICO = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" st
const ICO_SHUFFLE = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 3h5v5"/><path d="M4 20 21 3"/><path d="M21 16v5h-5"/><path d="m15 15 6 6"/><path d="M4 4l5 5"/></svg>';
const ICO_ERASE = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m7 21-4.3-4.3a1 1 0 0 1 0-1.4L13 5l6 6-9 9"/><path d="M22 21H7"/></svg>';
const ZONE_LABELS = { head:'Голова', face:'Лицо', neck:'Шея', ears:'Уши', accent:'Акцент' };
/* ── Гардеробная: модалка + живое превью ── */
function petSvgHTML() {
return renderPetSVG(_petData.petLevel, _petData.mood, _petData.accessories, _petData.petColor || 'purple', _petData.streakCurrent || 0, _petData.petPattern || 'none');
}
function paintPet() {
if (!_petData) return;
const svg = petSvgHTML();
const m = document.getElementById('pet-svg-wrap'); if (m) m.innerHTML = svg;
const p = document.getElementById('wr-preview-svg'); if (p) p.innerHTML = svg;
}
function updatePreviewScene() {
const sc = document.getElementById('wr-preview-scene');
if (sc && _petData) sc.style.background = BG_PREVIEWS[_petData.petBg] || BG_PREVIEWS.default;
}
function openWardrobe() {
if (!_petData) return;
document.getElementById('wr-preview-name').textContent = _petData.petName || 'Квантик';
const evoEl = document.getElementById('wr-preview-evo');
if (evoEl) evoEl.textContent = (EVO_STAGES[_petData.petLevel] || '') + ' · ур. ' + _petData.petLevel;
renderWardrobe(_petData.wardrobe || []);
renderColorPicker(_petData.petColor || 'purple');
renderPatternPicker(_petData.patterns || [], _petData.petPattern || 'none');
paintPet(); updatePreviewScene();
document.getElementById('wardrobe-modal').style.display = 'flex';
}
function closeWardrobe() { const m = document.getElementById('wardrobe-modal'); if (m) m.style.display = 'none'; }
function wearChip(it) {
if (it.locked)
return `<span class="wr-tile locked" title="Откроется: ${escHtml(it.hint)}">${LOCK_ICO}${escHtml(it.name)}${it.hint ? ` <span class="wr-hint">${escHtml(it.hint)}</span>` : ''}</span>`;
@@ -1334,8 +1407,7 @@ async function setEquipped(list) {
const final = res.equipped || list;
_petData.accessories = final;
_petData.wardrobe.forEach(w => { w.equipped = final.includes(w.id); });
document.getElementById('pet-svg-wrap').innerHTML =
renderPetSVG(_petData.petLevel, _petData.mood, final, _petData.petColor || 'purple', _petData.streakCurrent || 0, _petData.petPattern || 'none');
paintPet();
renderWardrobe(_petData.wardrobe);
}
function toggleEquip(id) {
@@ -1372,8 +1444,7 @@ async function applyPattern(id) {
const res = await LS.api('/api/pet/pattern', { method:'PATCH', body: JSON.stringify({ pattern: id }) }).catch(() => null);
if (!res || !res.ok) return;
_petData.petPattern = id;
document.getElementById('pet-svg-wrap').innerHTML =
renderPetSVG(_petData.petLevel, _petData.mood, _petData.accessories, _petData.petColor || 'purple', _petData.streakCurrent || 0, id);
paintPet();
renderPatternPicker(_petData.patterns || [], id);
}