8db8171b97
fix(pet-sprite): уникальные id градиентов спрайта — фикс «пропадающего» тела uid градиента питомца строился детерминированно (pg+level+mood+colorKey), поэтому два питомца с одинаковыми параметрами на одной странице получали совпадающие id. url(#id) заливки тела резолвился в чужой градиент (часто в display:none-вьюхе) → тело без заливки, видны только контур/усики/аура. Проявлялось «случайно» — только при совпадении параметров (нарратор на карте vs на экране победы в /quantik). Теперь uid — глобальный счётчик (pg1, pg2, …), коллизий нет. Чинит и /pet, и /dashboard, и игру. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> @
415 lines
23 KiB
JavaScript
415 lines
23 KiB
JavaScript
'use strict';
|
|
/*
|
|
* PetSprite — единый источник модели питомца для /pet и /dashboard.
|
|
* Канонические PET_PALETTES + shadeColor + renderPetSVG. pet.html и
|
|
* dashboard.html используют window.PetSprite.render(...) — без дублей.
|
|
*/
|
|
(function () {
|
|
// Счётчик для УНИКАЛЬНЫХ id градиентов/клипов спрайта. Иначе два питомца с
|
|
// одинаковыми level/mood/colorKey дают совпадающие id, и url(#id) заливки тела
|
|
// резолвится в чужой (возможно display:none) градиент → тело без заливки.
|
|
let _petUidSeq = 0;
|
|
const PET_PALETTES = {
|
|
purple:'#9B5DE5', cyan:'#06D6E0', gold:'#F9C74F',
|
|
red:'#F94144', green:'#38D95A', blue:'#4A90D9',
|
|
pink:'#F15BB5', orange:'#FB8B24', teal:'#0CA5B8',
|
|
lime:'#7FB800', indigo:'#5E60CE',
|
|
};
|
|
|
|
function shadeColor(hex, pct) {
|
|
const n = parseInt(hex.slice(1), 16);
|
|
const r = Math.min(255,Math.max(0,(n>>16)+pct));
|
|
const g = Math.min(255,Math.max(0,((n>>8)&0xff)+pct));
|
|
const b = Math.min(255,Math.max(0,(n&0xff)+pct));
|
|
return `rgb(${r},${g},${b})`;
|
|
}
|
|
|
|
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);
|
|
const uid = `pg${(++_petUidSeq).toString(36)}`;
|
|
|
|
const bodyPath = 'M55,22 C70,22 86,37 87,56 C89,75 78,94 55,97 C32,94 21,75 23,56 C24,37 40,22 55,22 Z';
|
|
const eyeY = 52, eyeX1 = 40, eyeX2 = 70;
|
|
|
|
/* ── Eyebrows ── */
|
|
let eyebrows = '';
|
|
if (mood !== 'sleeping') {
|
|
if (mood === 'ecstatic') {
|
|
eyebrows = `<path d="M33,46 C37,42 43,42 47,46" fill="none" stroke="white" stroke-width="2.5" stroke-linecap="round"/>
|
|
<path d="M63,46 C67,42 73,42 77,46" fill="none" stroke="white" stroke-width="2.5" stroke-linecap="round"/>`;
|
|
} else if (mood === 'sad' || mood === 'hungry') {
|
|
eyebrows = `<path d="M33,47 C37,51 43,51 47,47" fill="none" stroke="white" stroke-width="2" stroke-linecap="round"/>
|
|
<path d="M63,47 C67,51 73,51 77,47" fill="none" stroke="white" stroke-width="2" stroke-linecap="round"/>`;
|
|
} else {
|
|
eyebrows = `<path d="M33,47 Q40,45 47,47" fill="none" stroke="white" stroke-width="2" stroke-linecap="round"/>
|
|
<path d="M63,47 Q70,45 77,47" fill="none" stroke="white" stroke-width="2" stroke-linecap="round"/>`;
|
|
}
|
|
}
|
|
|
|
/* ── Eyes ── */
|
|
let eyeGroups = '', cheeks = '', extras = '';
|
|
if (mood === 'sleeping') {
|
|
eyeGroups = `<path d="M${eyeX1-8},${eyeY} Q${eyeX1},${eyeY-8} ${eyeX1+8},${eyeY}" stroke="white" stroke-width="2.5" fill="none" stroke-linecap="round"/>
|
|
<path d="M${eyeX2-8},${eyeY} Q${eyeX2},${eyeY-8} ${eyeX2+8},${eyeY}" stroke="white" stroke-width="2.5" fill="none" stroke-linecap="round"/>`;
|
|
} else {
|
|
const ry1 = mood === 'ecstatic' ? 13 : mood === 'happy' ? 12 : 10.5;
|
|
const prx = mood === 'ecstatic' ? 7 : mood === 'happy' ? 6.5 : 5.5;
|
|
const pry = mood === 'ecstatic' ? 8 : mood === 'happy' ? 7.5 : 6.5;
|
|
const pOff = (mood === 'sad' || mood === 'hungry') ? 3 : 2;
|
|
eyeGroups = `
|
|
<g class="pet-eye-blink" style="transform-box:fill-box;transform-origin:center">
|
|
<ellipse cx="${eyeX1}" cy="${eyeY}" rx="10" ry="${ry1}" fill="white"/>
|
|
<ellipse class="pet-pupil" cx="${eyeX1+1}" cy="${eyeY+pOff}" rx="${prx}" ry="${pry}" fill="#111"/>
|
|
<circle cx="${eyeX1-3}" cy="${eyeY-3}" r="2" fill="white" opacity=".9"/>
|
|
</g>
|
|
<g class="pet-eye-blink2" style="transform-box:fill-box;transform-origin:center">
|
|
<ellipse cx="${eyeX2}" cy="${eyeY}" rx="10" ry="${ry1}" fill="white"/>
|
|
<ellipse class="pet-pupil" cx="${eyeX2+1}" cy="${eyeY+pOff}" rx="${prx}" ry="${pry}" fill="#111"/>
|
|
<circle cx="${eyeX2-3}" cy="${eyeY-3}" r="2" fill="white" opacity=".9"/>
|
|
</g>`;
|
|
}
|
|
|
|
/* ── Nose ── */
|
|
const nose = mood !== 'sleeping'
|
|
? `<ellipse cx="55" cy="62" rx="3.5" ry="2.5" fill="${dark}" opacity=".45"/>` : '';
|
|
|
|
/* ── Mouth + cheeks ── */
|
|
let mouth = '';
|
|
if (mood === 'sleeping') {
|
|
mouth = `<path d="M47,68 Q55,72 63,68" stroke="white" stroke-width="2.5" fill="none" stroke-linecap="round"/>`;
|
|
} else if (mood === 'ecstatic') {
|
|
mouth = `<path d="M43,67 Q55,82 67,67" stroke="white" stroke-width="2.5" fill="none" stroke-linecap="round"/>`;
|
|
cheeks = `<ellipse cx="24" cy="62" rx="9" ry="6" fill="${col}" opacity=".4"/>
|
|
<ellipse cx="86" cy="62" rx="9" ry="6" fill="${col}" opacity=".4"/>`;
|
|
} else if (mood === 'happy') {
|
|
mouth = `<path d="M44,68 Q55,80 66,68" stroke="white" stroke-width="2.5" fill="none" stroke-linecap="round"/>`;
|
|
cheeks = `<ellipse cx="25" cy="62" rx="8" ry="5.5" fill="${col}" opacity=".32"/>
|
|
<ellipse cx="85" cy="62" rx="8" ry="5.5" fill="${col}" opacity=".32"/>`;
|
|
} else if (mood === 'sad' || mood === 'hungry') {
|
|
mouth = `<path d="M44,72 Q55,64 66,72" stroke="white" stroke-width="2.5" fill="none" stroke-linecap="round"/>`;
|
|
if (mood === 'hungry')
|
|
extras = `<ellipse cx="78" cy="59" rx="2.5" ry="3.5" fill="rgba(130,195,255,.8)"/>`;
|
|
} else {
|
|
mouth = `<path d="M44,68 Q55,74 66,68" stroke="white" stroke-width="2.5" fill="none" stroke-linecap="round"/>`;
|
|
}
|
|
|
|
/* ── Animated tail (all levels) ── */
|
|
const tail = `<g>
|
|
<path d="M68,91 C78,88 92,86 90,101 C88,110 76,110 72,103 C68,97 66,95 68,91 Z" fill="${col}" stroke="${dark}" stroke-width="1.2"/>
|
|
<animateTransform attributeName="transform" type="rotate" values="-7 68 91; 7 68 91; -7 68 91" dur="0.75s" repeatCount="indefinite"/>
|
|
</g>`;
|
|
|
|
/* ── Ears (level 2+) ── */
|
|
let ears = '';
|
|
if (level >= 2) {
|
|
ears = `<ellipse cx="32" cy="34" rx="9" ry="12" fill="${col}" transform="rotate(-15 32 34)"/>
|
|
<ellipse cx="32" cy="35" rx="6" ry="8" fill="${light}" opacity=".4" transform="rotate(-15 32 35)"/>
|
|
<ellipse cx="78" cy="34" rx="9" ry="12" fill="${col}" transform="rotate(15 78 34)"/>
|
|
<ellipse cx="78" cy="35" rx="6" ry="8" fill="${light}" opacity=".4" transform="rotate(15 78 35)"/>`;
|
|
}
|
|
|
|
/* ── Antennae (level 3+) ── */
|
|
let antennae = '';
|
|
if (level >= 3) {
|
|
antennae = `<line x1="44" y1="27" x2="34" y2="11" stroke="${col}" stroke-width="2.5" stroke-linecap="round"/>
|
|
<circle cx="33" cy="9" r="5" fill="${col}"/>
|
|
<circle cx="31" cy="7" r="2" fill="white" opacity=".7"/>
|
|
<line x1="66" y1="27" x2="76" y2="11" stroke="${col}" stroke-width="2.5" stroke-linecap="round"/>
|
|
<circle cx="77" cy="9" r="5" fill="${col}"/>
|
|
<circle cx="75" cy="7" r="2" fill="white" opacity=".7"/>`;
|
|
}
|
|
|
|
/* ── Wings with flutter (level 4+) ── */
|
|
let wings = '';
|
|
if (level >= 4) {
|
|
wings = `<g>
|
|
<path d="M14,62 C2,46 2,30 16,38 C26,44 22,59 18,65 Z" fill="${light}" opacity=".65"/>
|
|
<path d="M16,62 C6,50 8,38 18,42 C26,46 24,57 20,62 Z" fill="white" opacity=".15"/>
|
|
<animateTransform attributeName="transform" type="rotate" values="0 14 62; -10 14 62; 0 14 62" dur="0.45s" repeatCount="indefinite"/>
|
|
</g>
|
|
<g>
|
|
<path d="M96,62 C108,46 108,30 94,38 C84,44 88,59 92,65 Z" fill="${light}" opacity=".65"/>
|
|
<path d="M94,62 C104,50 102,38 92,42 C84,46 86,57 90,62 Z" fill="white" opacity=".15"/>
|
|
<animateTransform attributeName="transform" type="rotate" values="0 96 62; 10 96 62; 0 96 62" dur="0.45s" repeatCount="indefinite"/>
|
|
</g>`;
|
|
}
|
|
|
|
/* ── Paws with fingers ── */
|
|
const paws = `
|
|
<ellipse cx="14" cy="76" rx="12" ry="8" fill="url(#${uid})" transform="rotate(-25 14 76)"/>
|
|
<circle cx="8" cy="82" r="2.8" fill="${dark}" opacity=".45"/>
|
|
<circle cx="13" cy="84" r="2.8" fill="${dark}" opacity=".45"/>
|
|
<circle cx="18" cy="83" r="2.8" fill="${dark}" opacity=".45"/>
|
|
<ellipse cx="96" cy="76" rx="12" ry="8" fill="url(#${uid})" transform="rotate(25 96 76)"/>
|
|
<circle cx="102" cy="82" r="2.8" fill="${dark}" opacity=".45"/>
|
|
<circle cx="97" cy="84" r="2.8" fill="${dark}" opacity=".45"/>
|
|
<circle cx="92" cy="83" r="2.8" fill="${dark}" opacity=".45"/>`;
|
|
|
|
/* ── 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('beanie')) {
|
|
accSvg += `<path d="M30,30 Q30,11 55,11 Q80,11 80,30 Z" fill="${col}"/>
|
|
<rect x="28" y="27" width="54" height="6" rx="3" fill="${light}" opacity=".6"/>
|
|
<circle cx="55" cy="10" r="4" fill="${light}"/>`;
|
|
}
|
|
if (accessories.includes('halo')) {
|
|
accSvg += `<ellipse cx="55" cy="9" rx="17" ry="5" fill="none" stroke="#FCD667" stroke-width="3"/>
|
|
<ellipse cx="55" cy="9" rx="17" ry="5" fill="none" stroke="#fff" stroke-width="1" opacity=".5"/>`;
|
|
}
|
|
if (accessories.includes('monocle')) {
|
|
accSvg += `<circle cx="70" cy="52" r="13" fill="rgba(255,255,255,.1)" stroke="#FCD667" stroke-width="2"/>
|
|
<path d="M70,65 Q66,75 59,79" stroke="#FCD667" stroke-width="1.4" fill="none"/>`;
|
|
}
|
|
if (accessories.includes('medal')) {
|
|
accSvg += `<path d="M50,76 L48,86 L55,82 L62,86 L60,76 Z" fill="#e0335e"/>
|
|
<circle cx="55" cy="89" r="6" fill="#FCD667" stroke="#d4920a" stroke-width="1.2"/>
|
|
<path d="M52.4,89 l2,2 3.6,-4.2" stroke="#7a5a00" stroke-width="1.4" fill="none" stroke-linecap="round" stroke-linejoin="round"/>`;
|
|
}
|
|
if (accessories.includes('earrings')) {
|
|
accSvg += `<line x1="30" y1="44" x2="30" y2="46.5" stroke="#FCD667" stroke-width="1.2"/><circle cx="30" cy="48.5" r="2.5" fill="#FCD667"/>
|
|
<line x1="80" y1="44" x2="80" y2="46.5" stroke="#FCD667" stroke-width="1.2"/><circle cx="80" cy="48.5" r="2.5" fill="#FCD667"/>`;
|
|
}
|
|
if (accessories.includes('wand')) {
|
|
accSvg += `<line x1="92" y1="92" x2="104" y2="66" stroke="#9b6a2f" stroke-width="2.4" stroke-linecap="round"/>
|
|
<path d="M104,60 l1.5,3.4 3.7,.3 -2.8,2.5 .9,3.6 -3.3,-2 -3.3,2 .9,-3.6 -2.8,-2.5 3.7,-.3 Z" fill="#FCD667"/>`;
|
|
}
|
|
if (accessories.includes('balloon')) {
|
|
accSvg += `<line x1="14" y1="78" x2="12" y2="40" stroke="rgba(150,150,160,.55)" stroke-width="1"/>
|
|
<ellipse cx="12" cy="32" rx="9" ry="11" fill="#F15BB5"/>
|
|
<ellipse cx="9" cy="28" rx="2.4" ry="3" fill="#fff" opacity=".45"/>
|
|
<path d="M12,43 l-2.4,3.4 4.8,0 Z" fill="#F15BB5"/>`;
|
|
}
|
|
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"/>
|
|
<rect x="78" y="38" width="11" height="17" rx="4.5" fill="#2a3340"/>
|
|
<rect x="24" y="41" width="5" height="11" rx="2.5" fill="${col}"/>
|
|
<rect x="81" y="41" width="5" height="11" rx="2.5" fill="${col}"/>`;
|
|
}
|
|
if (accessories.includes('grad')) {
|
|
accSvg += `<path d="M44,25 Q55,30 66,25 L66,30 Q55,35 44,30 Z" fill="#2a3340"/>
|
|
<path d="M28,21 L55,13 L82,21 L55,29 Z" fill="#1f2733"/>
|
|
<line x1="55" y1="21" x2="78" y2="22" stroke="#F9C74F" stroke-width="1.3"/>
|
|
<line x1="78" y1="22" x2="78" y2="33" stroke="#F9C74F" stroke-width="1.3"/>
|
|
<circle cx="78" cy="35" r="2.6" fill="#F9C74F"/>`;
|
|
}
|
|
if (accessories.includes('hat')) {
|
|
accSvg += `<rect x="36" y="22" width="38" height="6" rx="3" fill="#2a2a2a"/>
|
|
<rect x="42" y="6" width="26" height="17" rx="4" fill="#1a1a1a"/>
|
|
<rect x="42" y="6" width="26" height="5" rx="2" fill="#333" opacity=".6"/>`;
|
|
}
|
|
if (accessories.includes('crown')) {
|
|
accSvg += `<path d="M33,26 L41,10 L55,22 L69,10 L77,26 L78,32 L32,32 Z" fill="#F9C74F"/>
|
|
<rect x="32" y="28" width="46" height="6" rx="2" fill="#F9C74F"/>
|
|
<circle cx="41" cy="11" r="4" fill="#F94144"/>
|
|
<circle cx="55" cy="23" r="4" fill="#06D6E0"/>
|
|
<circle cx="69" cy="11" r="4" fill="#9B5DE5"/>`;
|
|
}
|
|
if (accessories.includes('glasses')) {
|
|
accSvg += `<circle cx="${eyeX1}" cy="${eyeY}" r="14" fill="none" stroke="rgba(255,255,255,.65)" stroke-width="2"/>
|
|
<circle cx="${eyeX2}" cy="${eyeY}" r="14" fill="none" stroke="rgba(255,255,255,.65)" stroke-width="2"/>
|
|
<line x1="54" y1="${eyeY}" x2="56" y2="${eyeY}" stroke="rgba(255,255,255,.65)" stroke-width="2"/>
|
|
<line x1="19" y1="${eyeY-3}" x2="26" y2="${eyeY}" stroke="rgba(255,255,255,.45)" stroke-width="1.5"/>
|
|
<line x1="91" y1="${eyeY-3}" x2="84" y2="${eyeY}" stroke="rgba(255,255,255,.45)" stroke-width="1.5"/>`;
|
|
}
|
|
if (accessories.includes('bowtie')) {
|
|
accSvg += `<path d="M55,85 L44,80 L44,90 Z" fill="${dark}"/>
|
|
<path d="M55,85 L66,80 L66,90 Z" fill="${dark}"/>
|
|
<rect x="52" y="82" width="6" height="6" rx="2" fill="${light}"/>`;
|
|
}
|
|
if (accessories.includes('star')) {
|
|
accSvg += `<polygon points="98,18 100,24 106,24 101,28 103,34 98,30 93,34 95,28 90,24 96,24" fill="#F9C74F"/>`;
|
|
}
|
|
|
|
/* ── Aura ring (level 4+) ── */
|
|
let aura = '';
|
|
if (level >= 4) {
|
|
aura = `<g>
|
|
<circle cx="55" cy="60" r="47" fill="none" stroke="${col}" stroke-width="2.5" stroke-dasharray="9 6">
|
|
<animate attributeName="opacity" values=".35;.55;.35" dur="2.5s" repeatCount="indefinite"/>
|
|
</circle>
|
|
<animateTransform attributeName="transform" type="rotate" from="0 55 60" to="360 55 60" dur="9s" repeatCount="indefinite"/>
|
|
</g>`;
|
|
}
|
|
|
|
/* ── B3 Rainbow collar (streak ≥ 7) ── */
|
|
let rainbowCollar = '';
|
|
if (streak >= 7) {
|
|
rainbowCollar = `<g style="animation:rbRot 3s linear infinite;transform-box:fill-box;transform-origin:55px 38px">
|
|
<defs>
|
|
<linearGradient id="${uid}rb" x1="0%" y1="0%" x2="100%" y2="0%">
|
|
<stop offset="0%" stop-color="#F94144"/>
|
|
<stop offset="20%" stop-color="#F9C74F"/>
|
|
<stop offset="40%" stop-color="#38D95A"/>
|
|
<stop offset="60%" stop-color="#06D6E0"/>
|
|
<stop offset="80%" stop-color="#9B5DE5"/>
|
|
<stop offset="100%" stop-color="#F94144"/>
|
|
</linearGradient>
|
|
</defs>
|
|
<ellipse cx="55" cy="38" rx="22" ry="8" fill="none" stroke="url(#${uid}rb)" stroke-width="3.5" opacity=".88"/>
|
|
</g>`;
|
|
}
|
|
|
|
/* ── Orbital particles (level 5+) ── */
|
|
let orbitals = '';
|
|
if (level >= 5) {
|
|
const oData = [
|
|
{ start: 0, dur: 2.8, r: 46, size: 4, fill: col, op: .85 },
|
|
{ start: 120, dur: 3.6, r: 43, size: 3, fill: light, op: .7 },
|
|
{ start: 240, dur: 2.2, r: 48, size: 3.5, fill: 'white', op: .6 },
|
|
];
|
|
// Extra orbitals for level 6+
|
|
if (level >= 6) {
|
|
oData.push(
|
|
{ start: 60, dur: 1.9, r: 51, size: 2.5, fill: light, op: .65 },
|
|
{ start: 180, dur: 4.1, r: 40, size: 2, fill: col, op: .55 },
|
|
{ start: 300, dur: 3.0, r: 54, size: 3, fill: 'white', op: .5 },
|
|
);
|
|
}
|
|
// Even more for level 8
|
|
if (level >= 8) {
|
|
oData.push(
|
|
{ start: 45, dur: 1.5, r: 56, size: 3.5, fill: '#FFD700', op: .9 },
|
|
{ start: 225, dur: 2.4, r: 38, size: 2.5, fill: '#FFD700', op: .75 },
|
|
);
|
|
}
|
|
orbitals = oData.map(o => `<g>
|
|
<circle cx="55" cy="${60 - o.r}" r="${o.size}" fill="${o.fill}" opacity="${o.op}"/>
|
|
<animateTransform attributeName="transform" type="rotate"
|
|
from="${o.start} 55 60" to="${o.start + 360} 55 60" dur="${o.dur}s" repeatCount="indefinite"/>
|
|
</g>`).join('\n');
|
|
}
|
|
|
|
/* ── Second aura ring (level 6+) ── */
|
|
if (level >= 6) {
|
|
aura += `<g>
|
|
<circle cx="55" cy="60" r="53" fill="none" stroke="${col}" stroke-width="1.5" stroke-dasharray="5 9" opacity="0.4">
|
|
<animate attributeName="opacity" values=".25;.45;.25" dur="3.2s" repeatCount="indefinite"/>
|
|
</circle>
|
|
<animateTransform attributeName="transform" type="rotate" from="360 55 60" to="0 55 60" dur="14s" repeatCount="indefinite"/>
|
|
</g>`;
|
|
}
|
|
|
|
/* ── Crystal halo (level 7+) ── */
|
|
let halo = '';
|
|
if (level >= 7) {
|
|
halo = `<g>
|
|
<ellipse cx="55" cy="18" rx="20" ry="5" fill="none" stroke="${col}" stroke-width="2.5" opacity=".75">
|
|
<animate attributeName="opacity" values=".55;.85;.55" dur="2s" repeatCount="indefinite"/>
|
|
</ellipse>
|
|
<ellipse cx="55" cy="18" rx="14" ry="3" fill="none" stroke="white" stroke-width="1" opacity=".4"/>
|
|
</g>`;
|
|
}
|
|
|
|
/* ── Second wing pair (level 7+) ── */
|
|
if (level >= 7) {
|
|
wings += `<g>
|
|
<path d="M18,55 C8,43 10,33 20,37 C27,40 25,50 22,55 Z" fill="${light}" opacity=".45"/>
|
|
<animateTransform attributeName="transform" type="rotate" values="0 18 55; -12 18 55; 0 18 55" dur="0.35s" repeatCount="indefinite"/>
|
|
</g>
|
|
<g>
|
|
<path d="M92,55 C102,43 100,33 90,37 C83,40 85,50 88,55 Z" fill="${light}" opacity=".45"/>
|
|
<animateTransform attributeName="transform" type="rotate" values="0 92 55; 12 92 55; 0 92 55" dur="0.35s" repeatCount="indefinite"/>
|
|
</g>`;
|
|
}
|
|
|
|
/* ── Third aura + body shimmer (level 8) ── */
|
|
let shimmer = '';
|
|
if (level >= 8) {
|
|
aura += `<g>
|
|
<circle cx="55" cy="60" r="58" fill="none" stroke="#FFD700" stroke-width="1" stroke-dasharray="3 12" opacity="0.35">
|
|
<animate attributeName="opacity" values=".2;.5;.2" dur="4s" repeatCount="indefinite"/>
|
|
</circle>
|
|
<animateTransform attributeName="transform" type="rotate" from="0 55 60" to="360 55 60" dur="20s" repeatCount="indefinite"/>
|
|
</g>`;
|
|
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)"/>`;
|
|
else if (pattern === 'hearts') pin = [[40,44],[68,40],[57,70],[36,73],[76,63],[52,33]].map(p => `<path d="M0,-1.6 C-2.2,-4.8 -6.4,-1.6 0,3.4 C6.4,-1.6 2.2,-4.8 0,-1.6 Z" transform="translate(${p[0]},${p[1]}) scale(1.5)" fill="${dark}" opacity=".22"/>`).join('');
|
|
else if (pattern === 'stars') pin = [[40,44],[68,40],[57,70],[36,73],[76,63],[52,33]].map(p => `<path d="M0,-4 L1.2,-1.2 4,-1.2 1.8,.7 2.6,3.6 0,1.7 -2.6,3.6 -1.8,.7 -4,-1.2 -1.2,-1.2 Z" transform="translate(${p[0]},${p[1]})" fill="${light}" opacity=".5"/>`).join('');
|
|
else if (pattern === 'checker') pin = Array.from({ length: 42 }, (_, i) => { const c = i % 6, r = (i / 6) | 0; return ((r + c) % 2) ? '' : `<rect x="${20 + c * 12}" y="${18 + r * 12}" width="12" height="12" fill="${dark}" opacity=".13"/>`; }).join('');
|
|
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%">
|
|
<stop offset="0%" stop-color="${light}"/>
|
|
<stop offset="55%" stop-color="${col}"/>
|
|
<stop offset="100%" stop-color="${dark}"/>
|
|
</radialGradient>
|
|
<radialGradient id="${uid}b" cx="50%" cy="55%" r="50%">
|
|
<stop offset="0%" stop-color="white" stop-opacity="0.22"/>
|
|
<stop offset="100%" stop-color="white" stop-opacity="0"/>
|
|
</radialGradient>
|
|
<radialGradient id="${uid}sh" cx="50%" cy="40%" r="60%">
|
|
<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}
|
|
${halo}
|
|
${tail}
|
|
${wings}
|
|
${ears}
|
|
${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}
|
|
${rainbowCollar}
|
|
${accSvg}
|
|
${orbitals}
|
|
</svg>`;
|
|
}
|
|
|
|
var MOOD_RU = {
|
|
ecstatic: 'в восторге', happy: 'в духе', neutral: 'бодр',
|
|
sad: 'скучает', hungry: 'голоден', sleeping: 'спит',
|
|
};
|
|
|
|
window.PetSprite = {
|
|
PALETTES: PET_PALETTES,
|
|
shade: shadeColor,
|
|
render: renderPetSVG,
|
|
moodLabel: function (m) { return MOOD_RU[m] || 'бодр'; },
|
|
};
|
|
})();
|