feat(pet): прокачанный блок кастомизации + много контента

Контент:
- Узор тела (новая ось): пятнышки, полоски, градиент, галактика (клип по
  силуэту, рендер в обоих рендерерах) + миграция pet_pattern + /api/pet/pattern.
- +4 аксессуара: колпак, тёмные очки, шарф, цветок (все бесплатные).
- +3 фона: Сияние, Леденец, Сакура (CSS-градиенты + частицы: звёзды/пузыри/лепестки).

UI кастомизации:
- Вкладка «Узор» со свотчами-превью.
- Гардероб по зонам (Голова/Лицо/Шея/Уши/Акцент) + счётчик надетого,
  кнопки «Случайный образ» и «Снять всё».
- Фон — инлайн-сетка с превью/ценой/балансом (как раньше).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Maxim Dolgolyov
2026-06-05 13:58:12 +03:00
parent 152291aec8
commit 6880e1a55a
5 changed files with 251 additions and 42 deletions
+37 -1
View File
@@ -18,7 +18,7 @@
return `rgb(${r},${g},${b})`;
}
function renderPetSVG(level, mood, accessories = [], colorKey = 'purple', streak = 0) {
function renderPetSVG(level, mood, accessories = [], colorKey = 'purple', streak = 0, pattern = 'none') {
const col = PET_PALETTES[colorKey] || '#9B5DE5';
const dark = shadeColor(col, -45);
const light = shadeColor(col, 52);
@@ -143,6 +143,26 @@
/* ── Accessories (гардероб: строго по equipped-списку, без авто по уровню) ── */
let accSvg = '';
if (accessories.includes('party')) {
accSvg += `<path d="M55,1 L45,26 L65,26 Z" fill="${col}"/>
<path d="M55,1 L49,16 L61,16 Z" fill="${light}" opacity=".55"/>
<rect x="44" y="24" width="22" height="4" rx="2" fill="${dark}"/>
<circle cx="55" cy="1.5" r="3.5" fill="#F9C74F"/>`;
}
if (accessories.includes('sunglasses')) {
accSvg += `<rect x="28" y="46" width="22" height="13" rx="5" fill="#16181d"/>
<rect x="60" y="46" width="22" height="13" rx="5" fill="#16181d"/>
<rect x="49" y="50" width="12" height="3" rx="1.5" fill="#16181d"/>
<rect x="31" y="48" width="8" height="3" rx="1.5" fill="rgba(255,255,255,.25)"/>
<rect x="63" y="48" width="8" height="3" rx="1.5" fill="rgba(255,255,255,.25)"/>`;
}
if (accessories.includes('scarf')) {
accSvg += `<path d="M36,80 Q55,90 74,80 L74,86 Q55,96 36,86 Z" fill="${col}" stroke="${dark}" stroke-width="1"/>
<path d="M66,85 L74,99 L68,100 L62,87 Z" fill="${col}" stroke="${dark}" stroke-width="1"/>`;
}
if (accessories.includes('flower')) {
accSvg += `<g transform="translate(82,30)"><circle cx="0" cy="-5" r="3.2" fill="#FF8FB1"/><circle cx="5" cy="-1" r="3.2" fill="#FF8FB1"/><circle cx="3" cy="5" r="3.2" fill="#FF8FB1"/><circle cx="-3" cy="5" r="3.2" fill="#FF8FB1"/><circle cx="-5" cy="-1" r="3.2" fill="#FF8FB1"/><circle cx="0" cy="0" r="2.6" fill="#F9C74F"/></g>`;
}
if (accessories.includes('headphones')) {
accSvg += `<path d="M27,42 Q55,6 83,42" fill="none" stroke="#2a3340" stroke-width="4" stroke-linecap="round"/>
<rect x="21" y="38" width="11" height="17" rx="4.5" fill="#2a3340"/>
@@ -289,6 +309,17 @@
shimmer = `<ellipse cx="55" cy="58" rx="30" ry="35" fill="url(#${uid}sh)" opacity="0.18"/>`;
}
// Узор тела (клипуется по силуэту)
let patternSvg = '';
if (pattern && pattern !== 'none') {
let pin = '';
if (pattern === 'spots') pin = `<circle cx="38" cy="44" r="7" fill="${dark}" opacity=".18"/><circle cx="70" cy="40" r="6" fill="${dark}" opacity=".16"/><circle cx="60" cy="72" r="8" fill="${dark}" opacity=".18"/><circle cx="36" cy="74" r="6" fill="${dark}" opacity=".15"/><circle cx="76" cy="66" r="5" fill="${dark}" opacity=".15"/><circle cx="52" cy="34" r="5" fill="${light}" opacity=".3"/>`;
else if (pattern === 'stripes') pin = `<g transform="rotate(20 55 60)">${[-10,8,26,44,62,80,98].map(x => `<rect x="${x}" y="-10" width="9" height="140" fill="${dark}" opacity=".16"/>`).join('')}</g>`;
else if (pattern === 'galaxy') pin = `<ellipse cx="48" cy="52" rx="36" ry="42" fill="#1a0a3a" opacity=".42"/><circle cx="40" cy="44" r="1.4" fill="#fff"/><circle cx="66" cy="38" r="1.1" fill="#fff"/><circle cx="58" cy="64" r="1.5" fill="#fff"/><circle cx="36" cy="70" r="1" fill="#fff"/><circle cx="74" cy="60" r="1.2" fill="#fff"/><circle cx="52" cy="82" r="1" fill="#fff"/><circle cx="68" cy="74" r="1.3" fill="#cba6ff"/>`;
else if (pattern === 'gradient') pin = `<rect x="16" y="18" width="78" height="82" fill="url(#${uid}pg)"/>`;
patternSvg = `<clipPath id="${uid}clip"><path d="${bodyPath}"/></clipPath><g clip-path="url(#${uid}clip)">${pin}</g>`;
}
return `<svg viewBox="0 0 110 115" xmlns="http://www.w3.org/2000/svg">
<defs>
<radialGradient id="${uid}" cx="38%" cy="28%" r="70%">
@@ -304,6 +335,10 @@
<stop offset="0%" stop-color="#FFD700" stop-opacity="1"/>
<stop offset="100%" stop-color="#FFD700" stop-opacity="0"/>
</radialGradient>
<linearGradient id="${uid}pg" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="${light}" stop-opacity="0"/>
<stop offset="100%" stop-color="${dark}" stop-opacity="0.55"/>
</linearGradient>
</defs>
<ellipse cx="55" cy="111" rx="28" ry="4" fill="rgba(0,0,0,.18)"/>
${aura}
@@ -314,6 +349,7 @@
${antennae}
${paws}
<path d="${bodyPath}" fill="url(#${uid})"/>
${patternSvg}
${shimmer}
<ellipse cx="55" cy="70" rx="18" ry="12" fill="url(#${uid}b)"/>
${cheeks}${eyebrows}${eyeGroups}${nose}${mouth}${extras}