Files
Maxim Dolgolyov 21cea72874 style/security: эмодзи→SVG, safeUrl в ассистенте, prefs в localStorage (Спринт3)
- Убраны эмодзи (правило: только inline SVG .ic): classes.html 🃏→layers,
  collection-rb.html →star, pet.html 😋/😢→текст (textContent не держит SVG).
- assistant.js: safeUrl() на динамических href (FAQ/поиск/RAG/правила) —
  блокирует javascript:/data:, пропускает /… и https://….
- LS.prefs: персистентность через localStorage (раньше sync был отключён,
  настройки терялись при перезагрузке). Грузим синхронно + flush на pagehide.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 23:00:49 +03:00

2295 lines
138 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Питомец — LearnSpace</title>
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
<link href="https://fonts.googleapis.com/css2?family=Unbounded:wght@400;700;800&family=Manrope:wght@400;500;600;700&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="/css/ls.css" />
<style>
.sb-content { padding: 0; }
.pet-wrap { min-height: 100vh; padding: 24px 20px 60px; max-width: 1020px; margin: 0 auto; }
/* ── Header ── */
.pet-header { display:flex; align-items:center; gap:14px; margin-bottom:24px; }
.pet-header-icon {
width:48px; height:48px; border-radius:13px; flex-shrink:0;
background: linear-gradient(135deg,rgba(249,199,79,.22),rgba(249,65,68,.14));
border:1.5px solid rgba(255,255,255,.1);
display:flex; align-items:center; justify-content:center;
}
.pet-header-icon svg { width:26px; height:26px; fill:none; stroke:#F9C74F; stroke-width:1.8; }
.pet-h-title { font-family:'Unbounded',sans-serif; font-size:1.25rem; font-weight:800; }
.pet-h-sub { font-size:.8rem; color:var(--text-2); margin-top:2px; }
/* ── Hero grid ── */
.pet-hero { display:grid; grid-template-columns:300px 1fr; gap:18px; margin-bottom:18px; }
/* ── Stage card ── */
.pet-stage {
background: var(--surface); border:1.5px solid rgba(255,255,255,.08);
border-radius:22px; padding:22px 18px 20px;
display:flex; flex-direction:column; align-items:center; gap:11px;
position:relative; overflow:hidden;
}
.pet-stage::before {
content:''; position:absolute; inset:0; border-radius:22px;
pointer-events:none; opacity:0; transition:opacity .8s, background .8s;
}
.pet-stage.glow-ecstatic::before { opacity:1; background:radial-gradient(ellipse at 50% 60%, rgba(249,199,79,.18) 0%, transparent 68%); }
.pet-stage.glow-happy::before { opacity:1; background:radial-gradient(ellipse at 50% 60%, rgba(56,217,90,.14) 0%, transparent 68%); }
.pet-stage.glow-neutral::before { opacity:1; background:radial-gradient(ellipse at 50% 60%, rgba(6,214,224,.1) 0%, transparent 68%); }
.pet-stage.glow-sad::before { opacity:1; background:radial-gradient(ellipse at 50% 60%, rgba(100,149,237,.12) 0%, transparent 68%); }
.pet-stage.glow-hungry::before { opacity:1; background:radial-gradient(ellipse at 50% 60%, rgba(249,130,49,.14) 0%, transparent 68%); }
.pet-stage.glow-sleeping::before { opacity:1; background:radial-gradient(ellipse at 50% 60%, rgba(155,93,229,.16) 0%, transparent 68%); }
/* Evolution burst */
@keyframes evoBurst {
0%{transform:scale(1)} 20%{transform:scale(1.06)} 50%{transform:scale(0.97)} 75%{transform:scale(1.03)} 100%{transform:scale(1)}
}
.evo-burst { animation:evoBurst .9s cubic-bezier(.34,1.56,.64,1); }
/* Speech bubble */
.pet-bubble {
background:rgba(255,255,255,.07); border:1.5px solid rgba(255,255,255,.12);
border-radius:14px; padding:9px 14px;
font-size:.82rem; color:var(--text); line-height:1.5; text-align:center;
width:100%; box-sizing:border-box; position:relative; z-index:1;
animation: bubIn .4s cubic-bezier(.34,1.56,.64,1);
}
.pet-bubble::after {
content:''; position:absolute; bottom:-9px; left:50%; transform:translateX(-50%);
border:5px solid transparent; border-top-color:rgba(255,255,255,.12);
}
@keyframes bubIn { from{opacity:0;transform:scale(.9) translateY(-4px)} to{opacity:1;transform:none} }
/* Pet scene */
.pet-scene { position:relative; width:200px; height:200px; margin:0 auto; z-index:1; transition:transform .25s ease; border-radius:50%; }
.pet-svg-wrap { width:200px; height:200px; filter:drop-shadow(0 10px 28px rgba(0,0,0,.3)); }
/* A1 — Breathing */
@keyframes breathe { 0%,100%{transform:scale(1)} 50%{transform:scale(1.013,1.022)} }
.pet-svg-wrap svg { animation:breathe 3s ease-in-out infinite; transform-box:fill-box; transform-origin:50% 75%; }
.mood-sleeping .pet-svg-wrap svg { animation-duration:5s; }
.mood-ecstatic .pet-svg-wrap svg { animation-duration:1.7s; }
/* A3 — Weather particles */
.pet-weather { position:absolute; inset:0; pointer-events:none; overflow:hidden; border-radius:50%; z-index:0; }
@keyframes wxRain { 0%{transform:translateY(-10px);opacity:0} 10%{opacity:.7} 90%{opacity:.55} 100%{transform:translateY(220px);opacity:0} }
@keyframes wxSparkle { 0%{transform:translate(0,50px) scale(0);opacity:0} 18%{opacity:.9;transform:translate(0,30px) scale(1)} 100%{transform:translate(var(--wx,0px),-30px) scale(.2);opacity:0} }
@keyframes wxFog { 0%,100%{opacity:.1;transform:translateX(0)} 50%{opacity:.22;transform:translateX(9px)} }
@keyframes wxTwinkle { 0%,100%{opacity:1} 50%{opacity:.15} }
/* A4 — Time of day scene backgrounds */
.pet-scene[data-tod="dawn"] { background:radial-gradient(ellipse at 50% 100%, rgba(255,100,60,.2) 0%, transparent 65%), linear-gradient(155deg,#1a0a2e,#4a1830); }
.pet-scene[data-tod="day"] { background:radial-gradient(ellipse at 50% 100%, rgba(155,93,229,.2) 0%, transparent 65%), linear-gradient(155deg,#0d0d1f,#1a1040); }
.pet-scene[data-tod="dusk"] { background:radial-gradient(ellipse at 50% 100%, rgba(249,100,50,.2) 0%, transparent 65%), linear-gradient(155deg,#1a0a2e,#5d1e10); }
.pet-scene[data-tod="night"] { background:radial-gradient(ellipse at 50% 100%, rgba(6,214,224,.1) 0%, transparent 65%), linear-gradient(155deg,#04040f,#0a0a20); }
/* B1 — Star mini-game */
.pet-star-game { position:absolute; pointer-events:none; z-index:12; transform:translate(-50%,-50%); }
.pet-star-game.active { pointer-events:auto; }
@keyframes starAppear { 0%{opacity:0;transform:scale(0) rotate(-30deg)} 25%{opacity:1;transform:scale(1.15) rotate(5deg)} 100%{transform:scale(1) rotate(0deg);opacity:1} }
@keyframes starPulse { 0%,100%{filter:drop-shadow(0 0 5px #F9C74F);transform:scale(1) rotate(0deg)} 50%{filter:drop-shadow(0 0 14px #F9C74F);transform:scale(1.18) rotate(18deg)} }
.pet-star-btn { background:none; border:none; cursor:pointer; padding:0; animation:starPulse 1.1s ease-in-out infinite; display:block; }
.pet-star-btn:active { transform:scale(0.8) !important; }
.pet-star-wrap { animation:starAppear .35s cubic-bezier(.34,1.56,.64,1) forwards; }
/* B4 — XP / coin floats */
@keyframes xpFloat { 0%{transform:translateY(0) scale(1);opacity:1} 100%{transform:translateY(-72px) scale(.75);opacity:0} }
.pet-xp-float { position:absolute; font-family:'Unbounded',sans-serif; font-size:.72rem; font-weight:800; pointer-events:none; z-index:20; animation:xpFloat 1.6s ease-out forwards; white-space:nowrap; }
.pet-float { animation:petFloat 3s ease-in-out infinite; }
.mood-ecstatic .pet-float { animation:petBounce .5s ease-in-out infinite alternate; }
.mood-sleeping .pet-float { animation:petSway 4s ease-in-out infinite; }
.mood-hungry .pet-float { animation:petFloat 6s ease-in-out infinite; }
@keyframes petFloat { 0%,100%{transform:translateY(0)} 50%{transform:translateY(-12px)} }
@keyframes petBounce { 0%{transform:translateY(0) scale(1)} 100%{transform:translateY(-16px) scale(1.05)} }
@keyframes petSway { 0%,100%{transform:rotate(0deg)} 50%{transform:rotate(3deg)} }
@keyframes petPet { 0%{transform:scale(1)} 30%{transform:scale(1.18) rotate(-6deg)} 60%{transform:scale(1.1) rotate(4deg)} 100%{transform:scale(1)} }
.pet-svg-wrap.petting { animation:petPet .5s ease-out forwards !important; }
@keyframes blink {
0%, 88%, 100% { transform: scaleY(1); }
94% { transform: scaleY(0.06); }
}
.pet-eye-blink { animation: blink 3.5s ease-in-out infinite; transform-box: fill-box; transform-origin: center; }
.pet-eye-blink2 { animation: blink 3.5s ease-in-out 0.08s infinite; transform-box: fill-box; transform-origin: center; }
@keyframes floatHeart {
0% { opacity:1; transform:translate(0,0) scale(1); }
100%{ opacity:0; transform:translate(var(--tx,0px),var(--ty,-60px)) scale(1.4); }
}
/* ZZZ */
.pet-zzz { position:absolute; top:10px; right:10px; display:none; flex-direction:column; align-items:flex-end; gap:2px; }
.mood-sleeping .pet-zzz { display:flex; }
.pet-zzz span { font-family:'Unbounded',sans-serif; font-weight:800; color:rgba(155,93,229,.65); animation:zzzFloat 2s ease-in-out infinite; }
.pet-zzz span:nth-child(1) { font-size:.65rem; animation-delay:0s; }
.pet-zzz span:nth-child(2) { font-size:.85rem; animation-delay:.5s; }
.pet-zzz span:nth-child(3) { font-size:1.05rem; animation-delay:1s; }
@keyframes zzzFloat { 0%,100%{opacity:.4;transform:translateY(0)} 50%{opacity:1;transform:translateY(-7px)} }
/* Hearts ambient */
.pet-hearts { position:absolute; top:0; left:0; width:100%; height:100%; pointer-events:none; display:none; }
.mood-ecstatic .pet-hearts, .mood-happy .pet-hearts { display:block; }
.pet-heart { position:absolute; animation:heartPop 1.5s ease-out infinite; opacity:0; }
.pet-heart:nth-child(1) { left:8%; top:20%; animation-delay:0s; }
.pet-heart:nth-child(2) { left:72%; top:14%; animation-delay:.5s; }
.pet-heart:nth-child(3) { left:46%; top:2%; animation-delay:1s; }
@keyframes heartPop { 0%{opacity:0;transform:scale(0)} 30%{opacity:1;transform:scale(1.2) translateY(-8px)} 100%{opacity:0;transform:scale(.8) translateY(-26px)} }
/* Name / rename */
.pet-name-wrap { display:flex; align-items:center; gap:6px; z-index:1; }
.pet-name { font-family:'Unbounded',sans-serif; font-size:1.1rem; font-weight:800; color:var(--text); }
.pet-rename-btn { background:transparent; border:none; cursor:pointer; padding:4px; border-radius:6px; color:var(--text-2); transition:color .15s; }
.pet-rename-btn:hover { color:#9B5DE5; }
.pet-rename-btn svg { width:14px; height:14px; stroke:currentColor; fill:none; stroke-width:2; }
.pet-rename-form { display:none; flex-direction:column; gap:8px; width:100%; z-index:1; }
.pet-rename-form.visible { display:flex; }
.pet-rename-input {
padding:8px 14px; border-radius:10px; border:1.5px solid rgba(155,93,229,.4);
background:rgba(155,93,229,.1); color:var(--text); font-family:'Manrope',sans-serif;
font-size:.9rem; font-weight:600; outline:none; text-align:center;
}
.pet-rename-input:focus { border-color:#9B5DE5; }
.pet-rename-actions { display:flex; gap:8px; justify-content:center; }
.pet-btn { padding:6px 16px; border-radius:99px; font-family:'Manrope',sans-serif; font-size:.8rem; font-weight:700; cursor:pointer; border:1.5px solid rgba(255,255,255,.15); background:transparent; color:var(--text); transition:all .15s; }
.pet-btn.primary { background:#9B5DE5; border-color:#9B5DE5; color:#fff; }
.pet-btn.primary:hover { background:#8347d4; }
.pet-btn:hover { background:rgba(255,255,255,.08); }
/* Mood badge */
.pet-mood { padding:5px 14px; border-radius:99px; font-size:.76rem; font-weight:700; display:inline-flex; align-items:center; gap:5px; z-index:1; }
.mood-ecstatic-badge { background:rgba(249,199,79,.15); color:#F9C74F; border:1px solid rgba(249,199,79,.3); }
.mood-happy-badge { background:rgba(56,217,90,.15); color:#38D95A; border:1px solid rgba(56,217,90,.3); }
.mood-neutral-badge { background:rgba(6,214,224,.12); color:#06D6E0; border:1px solid rgba(6,214,224,.25); }
.mood-sad-badge { background:rgba(249,65,68,.12); color:#F94144; border:1px solid rgba(249,65,68,.25); }
.mood-hungry-badge { background:rgba(249,130,49,.15); color:#F98231; border:1px solid rgba(249,130,49,.3); }
.mood-sleeping-badge { background:rgba(155,93,229,.12); color:#9B5DE5; border:1px solid rgba(155,93,229,.25); }
/* Evolution */
.pet-evo { display:flex; flex-direction:column; align-items:center; gap:4px; z-index:1; }
.pet-evo-lbl { font-size:.63rem; font-weight:700; color:var(--text-2); text-transform:uppercase; letter-spacing:.06em; }
.pet-evo-name { font-family:'Unbounded',sans-serif; font-size:.73rem; font-weight:800; }
.pet-lvl-dots { display:flex; gap:5px; margin-top:2px; }
/* Color picker */
.pet-color-row { display:flex; align-items:center; gap:10px; z-index:1; }
.pet-color-lbl { font-size:.64rem; font-weight:700; color:var(--text-2); text-transform:uppercase; letter-spacing:.05em; }
.pet-color-picker { display:flex; gap:7px; }
.pet-color-dot {
width:18px; height:18px; border-radius:50%; cursor:pointer;
border:2.5px solid transparent; transition:all .18s; flex-shrink:0;
}
.pet-color-dot.active { border-color:rgba(255,255,255,.85); transform:scale(1.25); }
.pet-color-dot:hover:not(.active) { transform:scale(1.12); }
/* Accessories */
.pet-accessories { display:flex; gap:5px; flex-wrap:wrap; justify-content:center; z-index:1; }
.pet-acc-badge { padding:3px 10px; border-radius:99px; font-size:.7rem; font-weight:700; background:rgba(249,199,79,.12); color:#F9C74F; border:1px solid rgba(249,199,79,.2); }
/* Petting button */
.pet-action-btn {
padding:8px 22px; border-radius:99px; font-family:'Manrope',sans-serif; font-size:.84rem; font-weight:700;
cursor:pointer; border:1.5px solid rgba(249,199,79,.35); background:rgba(249,199,79,.1); color:#F9C74F;
transition:all .18s; z-index:1; display:flex; align-items:center; gap:6px;
}
.pet-action-btn:hover:not(:disabled) { background:rgba(249,199,79,.2); border-color:rgba(249,199,79,.5); }
.pet-action-btn:disabled { opacity:.5; cursor:not-allowed; }
/* ── Right column ── */
.pet-right { display:flex; flex-direction:column; gap:12px; }
/* Mini stats 2×2 */
.pet-stats-row { display:grid; grid-template-columns:repeat(2,1fr); gap:8px; }
.pet-mini { background:var(--surface); border:1.5px solid rgba(255,255,255,.08); border-radius:14px; padding:12px 10px; display:flex; flex-direction:column; align-items:center; gap:3px; text-align:center; }
.pet-mini-ico { width:22px; height:22px; display:flex; align-items:center; justify-content:center; }
.pet-mini-val { font-family:'Unbounded',sans-serif; font-size:.95rem; font-weight:800; }
.pet-mini-lbl { font-size:.62rem; color:var(--text-2); font-weight:700; text-transform:uppercase; letter-spacing:.04em; }
/* Card generic */
.pet-card { background:var(--surface); border:1.5px solid rgba(255,255,255,.08); border-radius:14px; padding:14px 16px; }
.pet-card-title { font-size:.67rem; font-weight:800; color:var(--text-2); text-transform:uppercase; letter-spacing:.06em; margin-bottom:8px; display:flex; align-items:center; gap:5px; }
.pet-card-title svg { flex-shrink:0; opacity:.7; }
/* Quests */
.quest-item { display:flex; align-items:center; gap:10px; padding:7px 0; border-bottom:1px solid rgba(255,255,255,.05); }
.quest-item:last-child { border-bottom:none; }
.quest-ico { width:18px; height:18px; flex-shrink:0; display:flex; align-items:center; justify-content:center; }
.quest-body { flex:1; }
.quest-label { font-size:.77rem; font-weight:600; }
.quest-bar { height:4px; border-radius:99px; background:rgba(255,255,255,.1); margin-top:4px; overflow:hidden; }
.quest-fill { height:100%; border-radius:99px; background:linear-gradient(90deg,#9B5DE5,#06D6E0); transition:width .6s cubic-bezier(.34,1.56,.64,1); }
.quest-check { width:20px; height:20px; border-radius:50%; border:1.5px solid rgba(255,255,255,.2); display:flex; align-items:center; justify-content:center; font-size:.72rem; color:transparent; flex-shrink:0; transition:all .3s; }
.quest-item.done .quest-check { background:#38D95A; border-color:#38D95A; color:#fff; }
.quest-item.done .quest-label { color:var(--text-2); opacity:.6; }
/* XP bar */
.pet-xp-row { display:flex; justify-content:space-between; align-items:baseline; margin-bottom:6px; }
.pet-xp-lvl { font-family:'Unbounded',sans-serif; font-size:.88rem; font-weight:800; }
.pet-xp-nums { font-size:.74rem; color:var(--text-2); }
.pet-xp-nums span { color:#9B5DE5; font-weight:700; }
.pet-bar { height:8px; border-radius:99px; background:rgba(255,255,255,.1); overflow:hidden; }
.pet-bar-fill { height:100%; border-radius:99px; transition:width .8s cubic-bezier(.34,1.56,.64,1); }
/* Hunger */
.pet-hunger-row { display:flex; align-items:center; gap:8px; }
.pet-hunger { display:flex; gap:4px; }
.pet-hunger-heart { width:18px; height:18px; flex-shrink:0; }
.pet-hunger-heart.filled { filter:drop-shadow(0 0 4px currentColor); }
.pet-hunger-heart.empty { opacity:.18; }
.pet-hunger-msg { font-size:.76rem; color:var(--text-2); margin-left:auto; }
/* Mood tip */
.pet-tip { display:flex; gap:10px; align-items:flex-start; }
.pet-tip-ico { width:24px; height:24px; flex-shrink:0; display:flex; align-items:center; justify-content:center; color:var(--text-2); }
.pet-tip-title { font-size:.77rem; font-weight:700; margin-bottom:3px; }
.pet-tip-text { font-size:.74rem; color:var(--text-2); line-height:1.55; }
.pet-tip-forecast { font-size:.72rem; color:#F9C74F; margin-top:5px; font-weight:600; display:none; }
/* ── Bottom ── */
.pet-bottom { display:grid; grid-template-columns:1fr 1fr; gap:14px; }
.pet-bottom-left, .pet-bottom-right { display:flex; flex-direction:column; gap:14px; }
/* XP chart */
.pet-chart-title { font-size:.67rem; font-weight:800; color:var(--text-2); text-transform:uppercase; letter-spacing:.06em; margin-bottom:12px; display:flex; align-items:center; gap:5px; }
.chart-bars { display:flex; gap:5px; align-items:flex-end; height:72px; }
.chart-col { display:flex; flex-direction:column; align-items:center; gap:3px; flex:1; }
.chart-val { font-size:.58rem; color:var(--text-2); font-weight:600; height:13px; display:flex; align-items:flex-end; }
.chart-bar-wrap { width:100%; flex:1; display:flex; align-items:flex-end; }
.chart-fill { width:100%; border-radius:4px 4px 0 0; background:linear-gradient(180deg,#9B5DE5,rgba(155,93,229,.35)); min-height:3px; transition:height .6s ease; }
.chart-col.today .chart-fill { background:linear-gradient(180deg,#06D6E0,rgba(6,214,224,.35)); }
.chart-day { font-size:.62rem; color:var(--text-2); font-weight:600; white-space:nowrap; }
.chart-col.today .chart-day { color:#06D6E0; font-weight:800; }
/* Activity feed */
.pet-feed-title { font-size:.67rem; font-weight:800; color:var(--text-2); text-transform:uppercase; letter-spacing:.06em; margin-bottom:10px; display:flex; align-items:center; gap:5px; }
.pet-feed-item { display:flex; align-items:center; gap:10px; padding:5px 0; border-bottom:1px solid rgba(255,255,255,.05); }
.pet-feed-item:last-child { border-bottom:none; }
.pet-feed-xp { font-family:'Unbounded',sans-serif; font-size:.76rem; font-weight:800; color:#38D95A; min-width:36px; }
.pet-feed-lbl { font-size:.76rem; font-weight:600; flex:1; }
.pet-feed-time { font-size:.68rem; color:var(--text-2); white-space:nowrap; }
.pet-feed-empty { font-size:.8rem; color:var(--text-2); text-align:center; padding:18px 0; }
/* Quick links */
.pet-quick-title { font-size:.67rem; font-weight:800; color:var(--text-2); text-transform:uppercase; letter-spacing:.06em; margin-bottom:10px; display:flex; align-items:center; gap:5px; }
.pet-quick-grid { display:grid; grid-template-columns:1fr 1fr; gap:7px; }
.pet-quick-card { background:rgba(255,255,255,.04); border:1.5px solid rgba(255,255,255,.07); border-radius:11px; padding:9px 11px; display:flex; align-items:center; gap:8px; text-decoration:none; color:inherit; transition:all .15s; }
.pet-quick-card:hover { background:rgba(255,255,255,.08); transform:translateY(-1px); }
.pet-quick-ico { width:30px; height:30px; border-radius:8px; display:flex; align-items:center; justify-content:center; flex-shrink:0; }
.pet-quick-name { font-size:.76rem; font-weight:700; }
.pet-quick-xp { font-size:.64rem; color:var(--text-2); }
/* B2 — Scene backgrounds (vivid) */
.pet-scene.bg-space { background:radial-gradient(ellipse at 25% 20%,rgba(155,93,229,.75) 0%,transparent 40%),radial-gradient(ellipse at 80% 75%,rgba(80,50,220,.55) 0%,transparent 38%),radial-gradient(ellipse at 55% 55%,rgba(20,5,90,.85) 0%,transparent 70%),linear-gradient(170deg,#01010e,#060228,#0a0318) !important; }
.pet-scene.bg-forest { background:radial-gradient(ellipse at 50% 100%,rgba(56,217,90,.8) 0%,transparent 50%),radial-gradient(ellipse at 15% 60%,rgba(10,160,40,.5) 0%,transparent 40%),radial-gradient(ellipse at 85% 35%,rgba(80,200,60,.35) 0%,transparent 35%),linear-gradient(170deg,#010701,#041203,#082007) !important; }
.pet-scene.bg-aqua { background:radial-gradient(ellipse at 50% 100%,rgba(6,214,224,.8) 0%,transparent 50%),radial-gradient(ellipse at 80% 25%,rgba(6,170,220,.5) 0%,transparent 40%),radial-gradient(ellipse at 20% 50%,rgba(0,100,180,.4) 0%,transparent 35%),linear-gradient(170deg,#010810,#021422,#03203a) !important; }
.pet-scene.bg-sunset { background:radial-gradient(ellipse at 50% 95%,rgba(249,100,20,.9) 0%,transparent 50%),radial-gradient(ellipse at 30% 50%,rgba(249,160,20,.55) 0%,transparent 40%),radial-gradient(ellipse at 75% 30%,rgba(200,50,10,.45) 0%,transparent 35%),linear-gradient(170deg,#100201,#250602,#1e0404) !important; }
.pet-scene.bg-aurora { background:radial-gradient(ellipse at 30% 25%,rgba(56,217,140,.6) 0%,transparent 42%),radial-gradient(ellipse at 70% 30%,rgba(120,90,255,.55) 0%,transparent 44%),radial-gradient(ellipse at 50% 92%,rgba(6,214,180,.4) 0%,transparent 52%),linear-gradient(170deg,#02040f,#04122a,#0a0426) !important; }
.pet-scene.bg-candy { background:radial-gradient(ellipse at 30% 25%,rgba(255,170,210,.7) 0%,transparent 46%),radial-gradient(ellipse at 75% 70%,rgba(150,200,255,.6) 0%,transparent 46%),linear-gradient(170deg,#3a2540,#52324f,#3a2848) !important; }
.pet-scene.bg-sakura { background:radial-gradient(ellipse at 50% 100%,rgba(255,140,190,.7) 0%,transparent 52%),radial-gradient(ellipse at 25% 30%,rgba(255,180,210,.45) 0%,transparent 42%),linear-gradient(170deg,#1a0a14,#2e1226,#241026) !important; }
.pet-scene.bg-class { background:radial-gradient(ellipse at 50% 16%,rgba(255,220,150,.32) 0%,transparent 46%),radial-gradient(ellipse at 50% 100%,rgba(56,140,90,.45) 0%,transparent 52%),linear-gradient(170deg,#16241c,#1e3327,#16261d) !important; }
.pet-scene.bg-lab { background:radial-gradient(ellipse at 30% 28%,rgba(6,214,224,.45) 0%,transparent 44%),radial-gradient(ellipse at 72% 72%,rgba(56,217,90,.35) 0%,transparent 44%),linear-gradient(170deg,#06121e,#0a1c2c,#08202f) !important; }
.pet-scene.bg-winter { background:radial-gradient(ellipse at 50% 100%,rgba(180,220,255,.5) 0%,transparent 55%),radial-gradient(ellipse at 30% 25%,rgba(220,235,255,.35) 0%,transparent 42%),linear-gradient(170deg,#0a1430,#13243f,#1a2c4a) !important; }
.pet-scene.bg-rainbow{ background:linear-gradient(160deg,rgba(249,65,68,.34),rgba(249,199,79,.34) 28%,rgba(56,217,90,.32) 52%,rgba(6,214,224,.34) 74%,rgba(155,93,229,.36)),#0e1020 !important; }
/* B2 — BgFX particle container + keyframes */
.pet-bgfx { position:absolute; inset:0; pointer-events:none; overflow:hidden; border-radius:50%; z-index:1; }
@keyframes bgStarTwink { 0%,100%{opacity:1;transform:scale(1)} 50%{opacity:.05;transform:scale(.4)} }
@keyframes bgFirefly { 0%,100%{opacity:.08;transform:translate(0,0)} 50%{opacity:1;transform:translate(var(--fx,3px),var(--fy,-4px))} }
@keyframes bgBubble { 0%{transform:translateY(0) scale(1);opacity:.75} 100%{transform:translateY(-140px) scale(1.5);opacity:0} }
@keyframes bgEmber { 0%{transform:translate(0,0);opacity:1} 60%{opacity:.65} 100%{transform:translate(var(--ex,5px),-130px);opacity:0} }
@keyframes bgPetal { 0%{transform:translate(0,0) rotate(0);opacity:.9} 100%{transform:translate(var(--px,12px),190px) rotate(220deg);opacity:0} }
/* B2 — Shop modal */
.pet-shop-overlay { position:fixed;inset:0;background:rgba(0,0,0,.65);z-index:1000;display:flex;align-items:center;justify-content:center;backdrop-filter:blur(4px); }
.pet-shop-modal { background:var(--surface);border:1.5px solid rgba(255,255,255,.1);border-radius:20px;padding:22px;width:440px;max-width:95vw;box-shadow:0 24px 80px rgba(0,0,0,.5); }
.pet-shop-head { display:flex;justify-content:space-between;align-items:center;margin-bottom:16px; }
.pet-shop-title { font-family:'Unbounded',sans-serif;font-size:.85rem;font-weight:800; }
.pet-shop-coins { font-size:.75rem;color:#F9C74F;font-weight:700;display:flex;align-items:center;gap:5px; }
.pet-shop-close { background:none;border:none;cursor:pointer;color:var(--text-2);font-size:1.1rem;padding:2px 7px;border-radius:6px;transition:color .15s; }
.pet-shop-close:hover { color:var(--text); }
.pet-shop-grid { display:grid;grid-template-columns:repeat(3,1fr);gap:9px;margin-bottom:12px; }
.pet-bg-card { border-radius:12px;overflow:hidden;cursor:pointer;border:2px solid rgba(255,255,255,.1);transition:all .2s; }
.pet-bg-card:hover { border-color:rgba(155,93,229,.5);transform:translateY(-2px); }
.pet-bg-card.active { border-color:#9B5DE5;box-shadow:0 0 14px rgba(155,93,229,.4); }
.pet-bg-preview { height:62px; }
.pet-bg-info { padding:6px 8px;background:rgba(255,255,255,.04); }
.pet-bg-name { font-size:.72rem;font-weight:700;margin-bottom:2px; }
.pet-bg-status { font-size:.62rem;color:var(--text-2); }
.pet-bg-card.active .pet-bg-status { color:#9B5DE5; }
/* B3 — Rainbow collar keyframe */
@keyframes rbRot { to { transform:rotate(360deg); } }
/* ── Гардеробная (модалка) ── */
.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:800px; max-width:96vw; max-height:90vh; overflow:auto;
background:linear-gradient(180deg,rgba(155,93,229,.07),transparent 240px),var(--surface);
border:1.5px solid rgba(155,93,229,.22); border-radius:24px; box-shadow:0 30px 90px rgba(0,0,0,.55); padding:20px 24px 26px; }
.wr-modal-head { display:flex; align-items:center; gap:11px; margin-bottom:18px; padding-bottom:15px; border-bottom:1px solid var(--border); }
.wr-modal-titles { flex:1; min-width:0; }
.wr-modal-close { width:32px; height:32px; border:none; background:rgba(15,23,42,.05); 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(15,23,42,.1); color:var(--text); }
.wr-coins { display:inline-flex; align-items:center; gap:6px; padding:6px 14px; border-radius:99px; flex-shrink:0;
background:linear-gradient(135deg,#FCD667,#F2B01E); border:none; color:#4a3206; font:800 .82rem 'Manrope',sans-serif;
box-shadow:0 2px 9px rgba(242,176,30,.4); }
.wr-coins svg { width:14px; height:14px; color:#4a3206; }
.wr-modal-body { display:grid; grid-template-columns:252px 1fr; gap:24px; align-items:start; }
.wr-modal-preview { display:flex; flex-direction:column; align-items:center; gap:11px; position:sticky; top:0; }
.wr-prev-scene { width:100%; aspect-ratio:1; border-radius:22px; display:flex; align-items:center; justify-content:center; position:relative; overflow:hidden;
border:1.5px solid rgba(155,93,229,.22); background:linear-gradient(155deg,#0d0d1f,#1a1040);
box-shadow:inset 0 -34px 58px rgba(0,0,0,.42), inset 0 0 0 1px rgba(255,255,255,.05), 0 14px 40px rgba(0,0,0,.4); }
.wr-prev-scene::before { content:''; position:absolute; inset:0; pointer-events:none; z-index:0;
background:radial-gradient(ellipse at 50% 14%, rgba(255,255,255,.08), transparent 46%),
radial-gradient(ellipse at 50% 90%, rgba(155,93,229,.28), transparent 56%); }
.wr-prev-scene::after { content:''; position:absolute; inset:0; pointer-events:none; z-index:2;
background:radial-gradient(ellipse at 50% 50%, transparent 58%, rgba(0,0,0,.4)); }
#wr-preview-svg { width:82%; position:relative; z-index:1; display:flex; align-items:center; justify-content:center;
animation:wrFloat 3.6s ease-in-out infinite; }
#wr-preview-svg svg { width:100%; height:auto; display:block; filter:drop-shadow(0 10px 16px rgba(0,0,0,.45)); }
@keyframes wrFloat { 0%,100%{ transform:translateY(-3px); } 50%{ transform:translateY(5px); } }
.wr-prev-name { font-family:'Unbounded',sans-serif; font-weight:800; font-size:1.05rem;
background:linear-gradient(90deg,#9B5DE5,#0CA5C0); -webkit-background-clip:text; background-clip:text; -webkit-text-fill-color:transparent; }
.wr-prev-evo { font-size:.68rem; font-weight:700; color:var(--text-2); background:rgba(155,93,229,.14);
border:1px solid rgba(155,93,229,.25); padding:3px 12px; border-radius:99px; }
@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; }
.pc-head-ico { width:34px; height:34px; border-radius:11px; flex-shrink:0; display:flex; align-items:center; justify-content:center;
background:linear-gradient(135deg,rgba(155,93,229,.3),rgba(6,214,224,.2)); color:#c9a6ff; }
.pc-head-ico svg { width:17px; height:17px; }
.pc-head-title { font-family:'Unbounded',sans-serif; font-size:.92rem; font-weight:800; line-height:1.1; }
.pc-head-sub { font-size:.68rem; color:var(--text-3); margin-top:2px; }
.pc-tabs { display:flex; flex-wrap:wrap; gap:4px; padding:4px; background:rgba(15,23,42,.06); border-radius:13px; margin-bottom:16px; }
.pc-tab { flex:1 1 84px; display:inline-flex; align-items:center; justify-content:center; gap:5px; padding:9px 5px; border:none;
border-radius:9px; background:transparent; color:var(--text-2); font:700 .74rem 'Manrope',sans-serif; cursor:pointer; transition:all .18s; white-space:nowrap; }
.pc-tab svg { width:14px; height:14px; }
.pc-tab:hover { color:var(--text); background:rgba(15,23,42,.05); }
.pc-tab.active { background:linear-gradient(135deg,#9B5DE5,#7b4fd0); color:#fff; box-shadow:0 5px 16px rgba(155,93,229,.45); }
.pc-hint { font-size:.72rem; color:var(--text-3); margin-bottom:13px; line-height:1.5; }
.pc-panel { animation:pcFade .25s ease; }
@keyframes pcFade { from{opacity:0;transform:translateY(5px)} to{opacity:1;transform:translateY(0)} }
.pet-evo-legend { font-size:.6rem; color:var(--text-3); margin-top:7px; line-height:1.45; text-align:center;
cursor:help; max-width:230px; }
/* Гардероб по зонам */
#pet-accessories { display:block; }
.wr-bar { display:flex; align-items:center; justify-content:space-between; gap:8px; flex-wrap:wrap;
padding-bottom:12px; margin-bottom:13px; border-bottom:1px solid var(--border); }
.wr-count { font-size:.76rem; color:var(--text-2); font-weight:700; }
.wr-count b { color:var(--violet); }
.wr-actions { display:flex; gap:7px; flex-wrap:wrap; }
.wr-btn { display:inline-flex; align-items:center; gap:5px; padding:6px 13px; border-radius:99px; border:1.5px solid var(--border-h);
background:rgba(15,23,42,.025); color:var(--text-2); font:700 .72rem 'Manrope',sans-serif; cursor:pointer; transition:all .15s; }
.wr-btn svg { width:12px; height:12px; }
.wr-btn:hover { border-color:var(--violet); color:var(--violet); }
.wr-zone { display:flex; align-items:center; gap:13px; padding:10px 14px; margin-bottom:8px;
background:rgba(15,23,42,.03); border:1px solid var(--border); border-radius:13px; }
.wr-zone:last-child { margin-bottom:0; }
.wr-zone-lbl { width:56px; flex-shrink:0; font-size:.58rem; font-weight:800; color:var(--text-3);
text-transform:uppercase; letter-spacing:.07em; }
.wr-chips { display:flex; flex-wrap:wrap; gap:7px; flex:1; }
.wr-tile { display:inline-flex; align-items:center; gap:6px; padding:8px 14px; border-radius:12px; border:1.5px solid var(--border-h);
background:var(--surface); color:var(--text-2); font:600 .77rem 'Manrope',sans-serif; cursor:pointer; transition:all .16s; }
.wr-tile svg { width:12px; height:12px; flex-shrink:0; }
.wr-tile:hover:not(.locked) { border-color:var(--violet); color:var(--violet); transform:translateY(-1px); box-shadow:0 4px 14px rgba(155,93,229,.2); }
.wr-tile.on { border-color:#9B5DE5; background:linear-gradient(135deg,#9B5DE5,#7b4fd0); color:#fff; box-shadow:0 3px 12px rgba(155,93,229,.35); }
.wr-tile.on svg { color:#fff; }
.wr-tile.locked { opacity:.5; cursor:not-allowed; }
.wr-tile .wr-hint { font-size:.62rem; color:var(--text-3); }
/* Цвет */
.pc-panel .pet-color-picker { display:flex; gap:14px; flex-wrap:wrap; }
.pc-panel .pet-color-dot { width:40px; height:40px; border-width:3px; box-shadow:0 3px 10px rgba(0,0,0,.3); }
.pc-panel .pet-color-dot.active { transform:scale(1.12); box-shadow:0 0 0 3px var(--violet), 0 4px 14px rgba(155,93,229,.5); }
/* Узор — превью-плитки */
.pc-pattern-grid { display:flex; flex-wrap:wrap; gap:10px; }
.pc-swatch { display:flex; flex-direction:column; align-items:center; gap:8px; width:84px; padding:10px 6px; border-radius:15px;
border:1.5px solid var(--border-h); background:var(--surface); color:var(--text-2); cursor:pointer; transition:all .16s; }
.pc-swatch:hover { border-color:var(--violet); transform:translateY(-2px); box-shadow:0 6px 18px rgba(155,93,229,.2); }
.pc-swatch.active { border-color:var(--violet); background:rgba(155,93,229,.18); box-shadow:0 4px 14px rgba(155,93,229,.28); }
.pc-swatch-name { font:700 .68rem 'Manrope',sans-serif; }
.pc-swatch.active .pc-swatch-name { color:#fff; }
.pc-swatch-dot { width:52px; height:52px; border-radius:14px; flex-shrink:0; border:1.5px solid rgba(255,255,255,.18);
box-shadow:inset 0 0 0 1px rgba(255,255,255,.06), 0 2px 8px rgba(0,0,0,.3); }
.pc-swatch-dot.pat-none { background:#9B5DE5; }
.pc-swatch-dot.pat-spots { background:radial-gradient(circle at 32% 32%,#5a2da0 3px,transparent 3.2px),radial-gradient(circle at 70% 66%,#5a2da0 3px,transparent 3.2px),radial-gradient(circle at 45% 80%,#5a2da0 2.6px,transparent 2.8px),#9B5DE5; }
.pc-swatch-dot.pat-stripes { background:repeating-linear-gradient(45deg,#9B5DE5 0 5px,#5a2da0 5px 9px); }
.pc-swatch-dot.pat-gradient{ background:linear-gradient(180deg,#c9a6ec,#4a2398); }
.pc-swatch-dot.pat-galaxy { background:radial-gradient(circle at 62% 38%,#1a0a3a 50%,#9B5DE5); }
.pc-swatch-dot.pat-hearts { background:radial-gradient(circle at 35% 40%,#5a2da0 2.5px,transparent 2.7px),radial-gradient(circle at 68% 62%,#5a2da0 2.5px,transparent 2.7px),#9B5DE5; }
.pc-swatch-dot.pat-stars { background:radial-gradient(circle at 38% 38%,#fff 1.8px,transparent 2px),radial-gradient(circle at 66% 66%,#fff 1.8px,transparent 2px),#9B5DE5; }
.pc-swatch-dot.pat-checker { background:repeating-conic-gradient(#9B5DE5 0% 25%,#5a2da0 0% 50%) 50% / 16px 16px; }
/* Фон — превью-карточки крупнее */
.pc-bg-grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(124px,1fr)); gap:11px; }
.pc-bg-grid .pet-bg-card { border-radius:14px; transition:transform .16s, box-shadow .16s, border-color .16s; }
.pc-bg-grid .pet-bg-card:hover { transform:translateY(-3px); box-shadow:0 8px 22px rgba(0,0,0,.4); }
.pc-bg-grid .pet-bg-card.active { box-shadow:0 0 0 2px var(--violet), 0 8px 22px rgba(155,93,229,.4); }
.pc-bg-grid .pet-bg-preview { height:78px; }
/* Образы (наборы) */
.pc-sets-grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(150px,1fr)); gap:10px; }
.pc-set { display:flex; flex-direction:column; gap:3px; align-items:flex-start; padding:12px 14px; border-radius:14px;
border:1.5px solid var(--border-h); background:rgba(15,23,42,.03); cursor:pointer; transition:all .16s; text-align:left; }
.pc-set:hover { border-color:var(--violet); transform:translateY(-2px); box-shadow:0 6px 18px rgba(155,93,229,.2); }
.pc-set-name { font:800 .84rem 'Unbounded',sans-serif; color:var(--text); }
.pc-set-sub { font-size:.66rem; color:var(--text-3); }
@media(max-width:768px) {
.pet-hero { grid-template-columns:1fr; }
.pet-bottom { grid-template-columns:1fr; }
.pet-scene { width:170px; height:170px; }
.pet-svg-wrap { width:170px; height:170px; }
.pet-stats-row { grid-template-columns:repeat(4,1fr); }
}
@media(max-width:480px) {
.pet-stats-row { grid-template-columns:repeat(2,1fr); }
.pet-quick-grid{ grid-template-columns:1fr; }
}
</style>
</head>
<body>
<div class="app-layout">
<aside class="sidebar" id="app-sidebar"></aside>
<div class="notif-drop" id="notif-drop"></div>
<div class="sb-content">
<div class="pet-wrap">
<div class="pet-header">
<div class="pet-header-icon">
<svg viewBox="0 0 24 24"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/></svg>
</div>
<div>
<div class="pet-h-title">Мой питомец</div>
<div class="pet-h-sub">Заботься о нём — учись каждый день!</div>
</div>
</div>
<div class="pet-hero">
<!-- ── Left: stage ── -->
<div class="pet-stage" id="pet-stage">
<div class="pet-bubble" id="pet-bubble"></div>
<div class="pet-scene" id="pet-scene">
<div class="pet-weather" id="pet-weather"></div>
<div class="pet-bgfx" id="pet-bgfx"></div>
<div class="pet-svg-wrap pet-float" id="pet-svg-wrap"></div>
<div class="pet-zzz"><span>z</span><span>z</span><span>Z</span></div>
<div class="pet-hearts">
<div class="pet-heart"><svg width="13" height="13" viewBox="0 0 24 24" fill="#F94144" stroke="none"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/></svg></div>
<div class="pet-heart"><svg width="10" height="10" viewBox="0 0 24 24" fill="#F9C74F" stroke="none"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/></svg></div>
<div class="pet-heart"><svg width="8" height="8" viewBox="0 0 24 24" fill="#9B5DE5" stroke="none"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/></svg></div>
</div>
<div class="pet-star-game" id="pet-star-game"></div>
</div>
<div class="pet-name-wrap">
<div class="pet-name" id="pet-name"></div>
<button class="pet-rename-btn" onclick="toggleRename()" title="Переименовать">
<svg viewBox="0 0 24 24"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>
</button>
</div>
<div class="pet-rename-form" id="rename-form">
<input class="pet-rename-input" id="rename-input" maxlength="24" placeholder="Имя питомца…" />
<div class="pet-rename-actions">
<button class="pet-btn primary" onclick="saveName()">Сохранить</button>
<button class="pet-btn" onclick="toggleRename()">Отмена</button>
</div>
</div>
<div class="pet-mood" id="pet-mood-badge"></div>
<div class="pet-evo">
<div class="pet-evo-lbl">Эволюция</div>
<div class="pet-evo-name" id="pet-evo-name"></div>
<div class="pet-lvl-dots" id="pet-lvl-dots"></div>
<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>
Погладить
</button>
<button class="pet-action-btn" id="btn-feed" onclick="openFeedGame()" style="background:linear-gradient(135deg,rgba(249,199,79,.18),rgba(249,65,68,.1));border-color:rgba(249,199,79,.3)">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="#F9C74F" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 2v7c0 1.1.9 2 2 2h4a2 2 0 0 0 2-2V2"/><path d="M7 2v20"/><path d="M21 15V2v0a5 5 0 0 0-5 5v6c0 1.1.9 2 2 2h3Zm0 0v7"/></svg>
Покормить
</button>
</div>
<!-- ── Right column ── -->
<div class="pet-right">
<!-- Mini stats 2×2 -->
<div class="pet-stats-row">
<div class="pet-mini">
<div class="pet-mini-ico"><i data-lucide="flame" width="18" height="18"></i></div>
<div class="pet-mini-val" id="stat-streak"></div>
<div class="pet-mini-lbl">Серия</div>
</div>
<div class="pet-mini">
<div class="pet-mini-ico"><i data-lucide="trophy" width="18" height="18"></i></div>
<div class="pet-mini-val" id="stat-streak-best"></div>
<div class="pet-mini-lbl">Рекорд</div>
</div>
<div class="pet-mini">
<div class="pet-mini-ico"><i data-lucide="coins" width="18" height="18"></i></div>
<div class="pet-mini-val" id="stat-coins"></div>
<div class="pet-mini-lbl">Монеты</div>
</div>
<div class="pet-mini">
<div class="pet-mini-ico"><i data-lucide="heart-handshake" width="18" height="18"></i></div>
<div class="pet-mini-val" id="stat-petting"></div>
<div class="pet-mini-lbl">Поглажен</div>
</div>
</div>
<!-- Daily quests -->
<div class="pet-card">
<div class="pet-card-title"><i data-lucide="target" width="12" height="12"></i> Задания питомца</div>
<div id="pet-quests"><div style="color:var(--text-2);font-size:.8rem">Загрузка…</div></div>
</div>
<!-- XP -->
<div class="pet-card">
<div class="pet-card-title"><i data-lucide="zap" width="12" height="12"></i> Опыт</div>
<div class="pet-xp-row">
<div class="pet-xp-lvl" id="stat-level">Ур. —</div>
<div class="pet-xp-nums"><span id="xp-curr">0</span> / <span id="xp-next-val"></span> XP</div>
</div>
<div class="pet-bar"><div class="pet-bar-fill" id="xp-bar" style="width:0%;background:linear-gradient(90deg,#9B5DE5,#06D6E0)"></div></div>
</div>
<!-- Hunger -->
<div class="pet-card">
<div class="pet-card-title"><i data-lucide="heart" width="12" height="12"></i> Сытость</div>
<div class="pet-hunger-row">
<div class="pet-hunger" id="pet-hunger-dots"></div>
<div class="pet-hunger-msg" id="hunger-msg"></div>
</div>
</div>
<!-- Mood tip + forecast -->
<div class="pet-card">
<div class="pet-tip">
<div class="pet-tip-ico" id="tip-ico"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5"/><path d="M9 18h6"/><path d="M10 22h4"/></svg></div>
<div>
<div class="pet-tip-title" id="tip-title">Совет</div>
<div class="pet-tip-text" id="tip-text">Загрузка…</div>
<div class="pet-tip-forecast" id="tip-forecast"></div>
</div>
</div>
</div>
</div>
</div>
<!-- ── Bottom ── -->
<div class="pet-bottom">
<div class="pet-bottom-left">
<!-- XP chart -->
<div class="pet-card">
<div class="pet-chart-title"><i data-lucide="trending-up" width="12" height="12"></i> Активность за 7 дней</div>
<div class="chart-bars" id="xp-chart">
<div style="color:var(--text-2);font-size:.8rem;align-self:center">Загрузка…</div>
</div>
</div>
<!-- Activity feed -->
<div class="pet-card">
<div class="pet-feed-title"><i data-lucide="clock" width="12" height="12"></i> Последняя активность</div>
<div id="pet-feed-list"><div class="pet-feed-empty">Нет активности</div></div>
</div>
</div>
<div class="pet-bottom-right">
<!-- Quick links -->
<div class="pet-card">
<div class="pet-quick-title"><i data-lucide="utensils" width="12" height="12"></i> Накорми питомца</div>
<div class="pet-quick-grid">
<a href="/hangman" class="pet-quick-card">
<div class="pet-quick-ico" style="background:rgba(249,65,68,.12)"><i data-lucide="gamepad-2" width="18" height="18" style="color:#F94144"></i></div>
<div><div class="pet-quick-name">Виселица</div><div class="pet-quick-xp">+до 15 XP</div></div>
</a>
<a href="/crossword" class="pet-quick-card">
<div class="pet-quick-ico" style="background:rgba(6,214,224,.12)"><i data-lucide="grid-3x3" width="18" height="18" style="color:#06D6E0"></i></div>
<div><div class="pet-quick-name">Кроссворд</div><div class="pet-quick-xp">+до 20 XP</div></div>
</a>
<a href="/dashboard" class="pet-quick-card">
<div class="pet-quick-ico" style="background:rgba(155,93,229,.12)"><i data-lucide="file-text" width="18" height="18" style="color:#9B5DE5"></i></div>
<div><div class="pet-quick-name">Тест</div><div class="pet-quick-xp">XP за ответы</div></div>
</a>
<a href="/theory" class="pet-quick-card">
<div class="pet-quick-ico" style="background:rgba(249,199,79,.12)"><i data-lucide="book-open" width="18" height="18" style="color:#F9C74F"></i></div>
<div><div class="pet-quick-name">Теория</div><div class="pet-quick-xp">Читай уроки</div></div>
</a>
</div>
</div>
</div>
</div>
</div>
</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>
<div class="wr-coins" id="wr-coins" title="Монеты">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="8" cy="8" r="6"/><path d="M18.09 10.37A6 6 0 1 1 10.34 18"/><path d="M7 6h1v4"/><path d="m16.71 13.88.7.71-2.82 2.82"/></svg>
<span id="wr-coins-val"></span>
</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>
<button class="pc-tab" data-tab="sets" type="button"><i data-lucide="wand-2"></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 class="pc-panel" id="pc-sets" style="display:none">
<div class="pc-hint">Готовые образы — применяют аксессуары, узор и цвет одним кликом. Заблокированные предметы пропускаются.</div>
<div class="pc-sets-grid" id="pc-sets-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)">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:18px">
<div style="font-family:'Unbounded',sans-serif;font-size:.95rem;font-weight:800">Покорми питомца</div>
<button onclick="closeFeedGame()" style="background:none;border:none;cursor:pointer;color:var(--text-3);padding:4px"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg></button>
</div>
<div style="font-size:.78rem;color:var(--text-3);margin-bottom:14px">Ответь правильно, чтобы накормить питомца! <span style="color:#F9C74F;font-weight:700">+15 XP</span></div>
<div id="feed-q-text" style="font-size:.95rem;font-weight:700;color:var(--text);margin-bottom:16px;line-height:1.5"></div>
<div id="feed-opts" style="display:flex;flex-direction:column;gap:8px"></div>
<div id="feed-result" style="display:none;margin-top:14px;padding:12px 14px;border-radius:12px;font-size:.87rem;font-weight:700;text-align:center"></div>
<div id="feed-timer-bar" style="height:3px;background:#e0e6f0;border-radius:99px;margin-top:16px;overflow:hidden"><div id="feed-timer-fill" style="height:100%;border-radius:99px;background:linear-gradient(90deg,#F9C74F,#F94144);transition:width .25s linear;width:100%"></div></div>
</div>
</div>
<script src="/js/api.js"></script>
<script src="/js/imggen.js"></script>
<script src="/js/sidebar.js"></script>
<script src="https://cdn.jsdelivr.net/npm/lucide@0.469.0/dist/umd/lucide.min.js"></script>
<script>
/* ── Constants ── */
const EVO_COLORS = ['','#38D95A','#06D6E0','#9B5DE5','#F9C74F','#F94144','#FF6B6B','#C084FC','#FFD700'];
const EVO_STAGES = ['','Яйцо','Малыш','Подросток','Взрослый','Легенда','Архонт','Астрал','Вечный'];
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',
};
const PALETTE_LABELS = {
purple:'Фиолетовый', cyan:'Голубой', gold:'Золотой',
red:'Красный', green:'Зелёный', blue:'Синий',
pink:'Розовый', orange:'Оранжевый', teal:'Бирюзовый', lime:'Лаймовый', indigo:'Индиго',
};
const MOOD_LABELS = {
ecstatic:['Восторг!', 'mood-ecstatic-badge'],
happy: ['Счастлив', 'mood-happy-badge'],
neutral: ['Спокоен', 'mood-neutral-badge'],
sad: ['Грустит', 'mood-sad-badge'],
hungry: ['Голодает', 'mood-hungry-badge'],
sleeping:['Спит', 'mood-sleeping-badge'],
};
const BUBBLES = {
ecstatic:['Ура! Ты лучший! Так держать!','Я в восторге! Ты суперзвезда!','Wow! Продолжай учиться!'],
happy: ['Привет! Я рад тебя видеть!','Сегодня хороший день!','Ты молодец! Так держать!'],
neutral: ['Привет! Как дела?','Всё спокойно... Пройдём тест?','Хочется чего-нибудь интересного!'],
sad: ['Скучаю по тебе...','Где ты был? Я тебя ждал...','Заходи почаще!'],
hungry: ['Хочу есть... Сыграй в игру!','Живот урчит... учись скорее!','Голоден! Помоги мне — пройди тест!'],
sleeping:['Zzz... Zzz...','Давно не видел тебя... Zzz...','Просыпаюсь... ты наконец здесь?'],
};
const _svg = (d, s=20) => `<svg width="${s}" height="${s}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">${d}</svg>`;
const MOOD_TIPS = {
ecstatic:{ico:_svg('<path d="m12 3-1.912 5.813a2 2 0 0 1-1.275 1.275L3 12l5.813 1.912a2 2 0 0 1 1.275 1.275L12 21l1.912-5.813a2 2 0 0 1 1.275-1.275L21 12l-5.813-1.912a2 2 0 0 1-1.275-1.275L12 3Z"/>'),title:'Питомец счастлив!',text:'Серия дней отличная! Поддерживай ежедневную активность.'},
happy: {ico:_svg('<circle cx="12" cy="12" r="10"/><path d="M8 13s1.5 2 4 2 4-2 4-2"/><line x1="9" y1="9" x2="9.01" y2="9"/><line x1="15" y1="9" x2="15.01" y2="9"/>'),title:'Всё хорошо!',text:'Продолжай заходить каждый день и набирать XP в играх.'},
neutral: {ico:_svg('<path d="M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5"/><path d="M9 18h6"/><path d="M10 22h4"/>'),title:'Можно лучше!',text:'Пройди тест или сыграй в игру. Серия дней улучшает настроение!'},
sad: {ico:_svg('<circle cx="12" cy="12" r="10"/><path d="M16 16s-1.5-2-4-2-4 2-4 2"/><line x1="9" y1="9" x2="9.01" y2="9"/><line x1="15" y1="9" x2="15.01" y2="9"/>'),title:'Питомец грустит',text:'Войди хотя бы раз в день — серия дней поднимет настроение.'},
hungry: {ico:_svg('<path d="M3 2v7c0 1.1.9 2 2 2h4a2 2 0 0 0 2-2V2"/><path d="M7 2v20"/><path d="M21 15V2v0a5 5 0 0 0-5 5v6c0 1.1.9 2 2 2h3Zm0 0v7"/>'),title:'Питомец голодает!',text:'Прошло 3+ дня без активности. Виселица или Кроссворд дадут XP быстро.'},
sleeping:{ico:_svg('<path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z"/>'),title:'Питомец спит...',text:'Очень долгое отсутствие. Начни с простой игры — всего 5 минут!'},
};
const HUNGER_MSGS = ['Сыт!','Немного голоден','Хочет есть','Очень голоден!','Умирает!'];
const HAPPY_PHRASES = ['Мяу! Ещё, ещё!','Щекотно! Хи-хи!','Я тебя люблю!','Обними меня крепче!'];
/* ── Button SVG helpers ── */
const SVG_HAND = `<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>`;
const SVG_CLOCK = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>`;
const SVG_TIMER_ICO = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 2h4"/><path d="M12 14v-4"/><circle cx="12" cy="14" r="8"/></svg>`;
/* ── Quest icon SVGs ── */
const QUEST_ICONS = {
xp30: _svg('<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/>', 16),
test1: _svg('<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/>', 16),
streak2: _svg('<path d="M8.5 14.5A2.5 2.5 0 0 0 11 12c0-1.38-.5-2-1-3-1.072-2.143-.224-4.054 2-6 .5 2.5 2 4.9 4 6.5 2 1.6 3 3.5 3 5.5a7 7 0 1 1-14 0c0-1.153.433-2.294 1-3a2.5 2.5 0 0 0 2.5 2.5z"/>', 16),
};
let _petData = null;
let _petCooldown = false;
let _petCooldownTimer = null;
/* ── Init ── */
(async () => {
if (!LS.requireAuth()) return;
const user = LS.getUser();
LS.applyRoleSidebar(user);
if (user) {
LS.renderNavAvatar(document.getElementById('nav-avatar'), user);
document.getElementById('nav-user').textContent = user.name || '—';
LS.showBoardIfAllowed();
}
LS.sidebar?.init();
lucide.createIcons();
const feats = await LS.loadFeatures();
if (feats.pet === false) {
document.querySelector('.pet-wrap').innerHTML =
'<div style="color:var(--text-2);text-align:center;padding:80px 20px">Питомец отключён администратором.</div>';
LS.hideDisabledFeatures();
return;
}
LS.hideDisabledFeatures();
// Eye tracking
const stage = document.getElementById('pet-stage');
stage.addEventListener('mousemove', onStageMouse);
stage.addEventListener('mouseleave', () => {
applyPupilOffset(0, 0);
const scene = document.getElementById('pet-scene');
if (scene) scene.style.transform = '';
});
applyTimeOfDay();
scheduleNextStar();
setupCustomizeTabs();
loadPet();
})();
/* ── A4 Time of day ── */
function applyTimeOfDay() {
const h = new Date().getHours();
const tod = h >= 5 && h < 9 ? 'dawn' : h >= 9 && h < 18 ? 'day' : h >= 18 && h < 21 ? 'dusk' : 'night';
const scene = document.getElementById('pet-scene');
if (!scene) return;
scene.dataset.tod = tod;
if (tod === 'night') {
const ns = document.createElement('div');
ns.style.cssText = 'position:absolute;inset:0;pointer-events:none;overflow:hidden;border-radius:50%;z-index:0;';
for (let i = 0; i < 14; i++) {
const s = document.createElement('div');
const sz = 1 + Math.random() * 1.5;
s.style.cssText = `position:absolute;left:${Math.random()*90}%;top:${Math.random()*55}%;width:${sz}px;height:${sz}px;border-radius:50%;background:white;animation:wxTwinkle ${2+Math.random()*3}s ${Math.random()*4}s ease-in-out infinite;`;
ns.appendChild(s);
}
scene.prepend(ns);
}
}
/* ── A3 Weather particles ── */
function applyWeather(mood) {
const w = document.getElementById('pet-weather');
if (!w) return;
w.innerHTML = '';
if (mood === 'ecstatic' || mood === 'happy') {
for (let i = 0; i < 9; i++) {
const p = document.createElement('div');
p.style.cssText = `position:absolute;left:${8+Math.random()*82}%;bottom:${5+Math.random()*20}%;--wx:${(Math.random()-.5)*28}px;animation:wxSparkle ${2.2+Math.random()*2}s ${Math.random()*4}s ease-out infinite;`;
const sz = 4 + Math.random() * 4;
p.innerHTML = `<svg width="${sz}" height="${sz}" viewBox="0 0 24 24"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2" fill="#F9C74F" opacity=".8"/></svg>`;
w.appendChild(p);
}
} else if (mood === 'sad' || mood === 'hungry') {
for (let i = 0; i < 15; i++) {
const p = document.createElement('div');
const h = 7 + Math.random() * 7;
p.style.cssText = `position:absolute;left:${Math.random()*95}%;top:0;animation:wxRain ${1+Math.random()*.8}s ${Math.random()*3}s linear infinite;`;
p.innerHTML = `<svg width="2" height="${h}" viewBox="0 0 2 ${h}"><line x1="1" y1="0" x2="1" y2="${h}" stroke="rgba(120,180,255,.55)" stroke-width="1.5" stroke-linecap="round"/></svg>`;
w.appendChild(p);
}
} else if (mood === 'sleeping') {
for (let i = 0; i < 5; i++) {
const p = document.createElement('div');
const sz = 35 + Math.random() * 45;
p.style.cssText = `position:absolute;left:${Math.random()*75}%;top:${15+Math.random()*55}%;animation:wxFog ${4+Math.random()*3}s ${Math.random()*3}s ease-in-out infinite;`;
p.innerHTML = `<svg width="${sz}" height="${sz*.4}" viewBox="0 0 60 24"><ellipse cx="30" cy="12" rx="30" ry="12" fill="rgba(155,93,229,.13)"/></svg>`;
w.appendChild(p);
}
}
}
/* ── Применить фон сцены (пресет bg-<id> или кастомное изображение) ── */
function applyPetBg(id, customUrl) {
const scene = document.getElementById('pet-scene');
if (!scene) return;
scene.className = scene.className.replace(/\bbg-\S+/g, '').replace(/\s+/g, ' ').trim();
if (id === 'custom' && customUrl) {
scene.style.backgroundImage = `url("${customUrl}")`;
scene.style.backgroundSize = 'cover';
scene.style.backgroundPosition = 'center';
applyBgFX('default');
} else {
scene.style.backgroundImage = '';
scene.style.backgroundSize = '';
scene.style.backgroundPosition = '';
if (id && id !== 'default') scene.classList.add(`bg-${id}`);
applyBgFX(id || 'default');
}
}
/* ── B2 BgFX particles ── */
function applyBgFX(bgId) {
const c = document.getElementById('pet-bgfx');
if (!c) return;
c.innerHTML = '';
if (!bgId || bgId === 'default') return;
const rnd = (a, b) => (Math.random() * (b - a) + a).toFixed(1);
const rndI = (a, b) => Math.floor(Math.random() * (b - a) + a);
if (bgId === 'space') {
// 24 twinkling stars
for (let i = 0; i < 24; i++) {
const d = document.createElement('div');
const s = rnd(0.8, 3);
d.style.cssText = `position:absolute;left:${rnd(3,95)}%;top:${rnd(3,92)}%;width:${s}px;height:${s}px;border-radius:50%;background:#fff;animation:bgStarTwink ${rnd(1,3.5)}s ${rnd(0,4)}s ease-in-out infinite`;
c.appendChild(d);
}
// 2 nebula blobs
[{l:18,t:15,w:42,h:24,col:'rgba(155,93,229,.4)'},{l:62,t:62,w:36,h:20,col:'rgba(60,40,200,.35)'}].forEach(n=>{
const d = document.createElement('div');
d.style.cssText = `position:absolute;left:${n.l}%;top:${n.t}%;width:${n.w}px;height:${n.h}px;border-radius:50%;background:${n.col};filter:blur(9px)`;
c.appendChild(d);
});
} else if (bgId === 'forest') {
// 14 fireflies
for (let i = 0; i < 14; i++) {
const d = document.createElement('div');
const col = Math.random() > .5 ? '#90EE90' : '#CCFF44';
const fx = rndI(-6,7), fy = rndI(-7,1);
d.style.cssText = `position:absolute;left:${rnd(8,88)}%;top:${rnd(5,88)}%;width:3px;height:3px;border-radius:50%;background:${col};box-shadow:0 0 5px ${col};--fx:${fx}px;--fy:${fy}px;animation:bgFirefly ${rnd(0.8,2.5)}s ${rnd(0,5)}s ease-in-out infinite`;
c.appendChild(d);
}
} else if (bgId === 'aqua') {
// 11 rising bubbles
for (let i = 0; i < 11; i++) {
const d = document.createElement('div');
const s = rnd(4, 14);
d.style.cssText = `position:absolute;left:${rnd(8,88)}%;bottom:-14px;width:${s}px;height:${s}px;border-radius:50%;border:1.5px solid rgba(6,214,224,.75);animation:bgBubble ${rnd(2,4.5)}s ${rnd(0,6)}s ease-out infinite`;
c.appendChild(d);
}
} else if (bgId === 'sunset') {
// 13 rising embers
for (let i = 0; i < 13; i++) {
const d = document.createElement('div');
const s = rnd(1.5, 4.5);
const col = Math.random() > .5 ? '#F9C74F' : '#F97140';
const ex = rndI(-10,11);
d.style.cssText = `position:absolute;left:${rnd(8,88)}%;bottom:4%;width:${s}px;height:${s}px;border-radius:50%;background:${col};box-shadow:0 0 6px ${col};--ex:${ex}px;animation:bgEmber ${rnd(1.5,3)}s ${rnd(0,5)}s ease-out infinite`;
c.appendChild(d);
}
} else if (bgId === 'aurora') {
for (let i = 0; i < 20; i++) {
const d = document.createElement('div');
const s = rnd(0.8, 2.6);
d.style.cssText = `position:absolute;left:${rnd(3,95)}%;top:${rnd(3,72)}%;width:${s}px;height:${s}px;border-radius:50%;background:#aef7d8;box-shadow:0 0 4px #6ee7c0;animation:bgStarTwink ${rnd(1.2,3.5)}s ${rnd(0,4)}s ease-in-out infinite`;
c.appendChild(d);
}
} else if (bgId === 'candy') {
for (let i = 0; i < 11; i++) {
const d = document.createElement('div');
const s = rnd(5, 15);
const col = Math.random() > .5 ? 'rgba(255,150,200,.85)' : 'rgba(150,200,255,.85)';
d.style.cssText = `position:absolute;left:${rnd(8,88)}%;bottom:-16px;width:${s}px;height:${s}px;border-radius:50%;border:1.5px solid ${col};animation:bgBubble ${rnd(2.5,5)}s ${rnd(0,6)}s ease-out infinite`;
c.appendChild(d);
}
} else if (bgId === 'sakura') {
for (let i = 0; i < 14; i++) {
const d = document.createElement('div');
const s = rnd(4, 8);
d.style.cssText = `position:absolute;left:${rnd(2,94)}%;top:-12px;width:${s}px;height:${(s*.8).toFixed(1)}px;border-radius:60% 0 60% 0;background:rgba(255,160,200,.85);--px:${rndI(-18,18)}px;animation:bgPetal ${rnd(3,6)}s ${rnd(0,5)}s linear infinite`;
c.appendChild(d);
}
} else if (bgId === 'class') {
for (let i = 0; i < 12; i++) {
const d = document.createElement('div');
const s = rnd(1.5, 3);
d.style.cssText = `position:absolute;left:${rnd(5,92)}%;top:${rnd(5,80)}%;width:${s}px;height:${s}px;border-radius:50%;background:rgba(255,230,170,.8);animation:bgStarTwink ${rnd(2,4)}s ${rnd(0,4)}s ease-in-out infinite`;
c.appendChild(d);
}
} else if (bgId === 'lab') {
for (let i = 0; i < 11; i++) {
const d = document.createElement('div');
const s = rnd(4, 13);
const col = Math.random() > .5 ? 'rgba(6,214,224,.8)' : 'rgba(56,217,90,.75)';
d.style.cssText = `position:absolute;left:${rnd(8,88)}%;bottom:-15px;width:${s}px;height:${s}px;border-radius:50%;border:1.5px solid ${col};animation:bgBubble ${rnd(2.5,5)}s ${rnd(0,6)}s ease-out infinite`;
c.appendChild(d);
}
} else if (bgId === 'winter') {
for (let i = 0; i < 18; i++) {
const d = document.createElement('div');
const s = rnd(2.5, 5);
d.style.cssText = `position:absolute;left:${rnd(2,95)}%;top:-10px;width:${s}px;height:${s}px;border-radius:50%;background:rgba(255,255,255,.9);--px:${rndI(-14,14)}px;animation:bgPetal ${rnd(3.5,7)}s ${rnd(0,5)}s linear infinite`;
c.appendChild(d);
}
} else if (bgId === 'rainbow') {
const cols = ['#F94144','#F9C74F','#38D95A','#06D6E0','#9B5DE5'];
for (let i = 0; i < 16; i++) {
const d = document.createElement('div');
const s = rnd(2, 4);
d.style.cssText = `position:absolute;left:${rnd(4,93)}%;top:${rnd(4,86)}%;width:${s}px;height:${s}px;border-radius:50%;background:${cols[i % cols.length]};box-shadow:0 0 5px ${cols[i % cols.length]};animation:bgStarTwink ${rnd(1.3,3.2)}s ${rnd(0,4)}s ease-in-out infinite`;
c.appendChild(d);
}
}
}
/* ── B1 Star mini-game ── */
let _starActive = false, _starMissTimeout = null;
function scheduleNextStar() {
const last = parseInt(localStorage.getItem('ls_last_star') || '0');
const elapsed = Date.now() - last;
const cooldown = 60 * 60 * 1000;
setTimeout(spawnStar, elapsed > cooldown ? 8000 : cooldown - elapsed + 3000);
}
function spawnStar() {
if (_starActive) return;
_starActive = true;
const g = document.getElementById('pet-star-game');
g.style.left = (18 + Math.random() * 62) + '%';
g.style.top = (18 + Math.random() * 55) + '%';
g.className = 'pet-star-game active';
g.innerHTML = `<div class="pet-star-wrap">
<button class="pet-star-btn" onclick="catchStar()" title="+5 монет — поймай звезду!">
<svg width="34" height="34" viewBox="0 0 24 24" fill="#F9C74F" stroke="#d4920a" stroke-width=".8"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>
</button>
</div>`;
_starMissTimeout = setTimeout(missedStar, 4500);
}
async function catchStar() {
if (!_starActive) return;
_starActive = false;
clearTimeout(_starMissTimeout);
localStorage.setItem('ls_last_star', Date.now());
const g = document.getElementById('pet-star-game');
g.className = 'pet-star-game'; g.innerHTML = '';
const res = await LS.api('/api/pet/star', {method:'POST'}).catch(() => null);
const earned = res?.coins ?? 5;
if (_petData) { _petData.coins = (_petData.coins || 0) + earned; document.getElementById('stat-coins').textContent = _petData.coins; }
floatLabel(`+${earned} монет`, '#F9C74F', '38%');
spawnHearts();
scheduleNextStar();
}
function missedStar() {
_starActive = false;
const g = document.getElementById('pet-star-game');
g.className = 'pet-star-game'; g.innerHTML = '';
scheduleNextStar();
}
/* ── B4 XP / coin floats ── */
function floatLabel(text, color, left) {
const scene = document.getElementById('pet-scene');
if (!scene) return;
const el = document.createElement('div');
el.className = 'pet-xp-float';
el.textContent = text;
el.style.cssText = `left:${left || (30+Math.random()*38)+'%'};bottom:28%;color:${color||'#38D95A'};text-shadow:0 0 8px ${color||'#38D95A'}88;`;
scene.appendChild(el);
setTimeout(() => el.remove(), 1700);
}
/* ── B2 Pet backgrounds shop ── */
const BG_PREVIEWS = {
default: 'linear-gradient(155deg,#0d0d1f,#1a1040)',
space: 'radial-gradient(ellipse at 25% 20%,rgba(155,93,229,.75) 0%,transparent 55%),linear-gradient(155deg,#01010e,#060228)',
forest: 'radial-gradient(ellipse at 50% 100%,rgba(56,217,90,.8) 0%,transparent 55%),linear-gradient(155deg,#010701,#082007)',
aqua: 'radial-gradient(ellipse at 50% 100%,rgba(6,214,224,.8) 0%,transparent 55%),linear-gradient(155deg,#010810,#03203a)',
sunset: 'radial-gradient(ellipse at 50% 100%,rgba(249,100,20,.9) 0%,transparent 55%),linear-gradient(155deg,#100201,#1e0404)',
aurora: 'radial-gradient(ellipse at 35% 25%,rgba(56,217,140,.85) 0%,transparent 50%),radial-gradient(ellipse at 70% 35%,rgba(120,90,255,.7) 0%,transparent 50%),linear-gradient(155deg,#02040f,#0a0426)',
candy: 'radial-gradient(ellipse at 30% 25%,rgba(255,170,210,.9) 0%,transparent 55%),radial-gradient(ellipse at 75% 75%,rgba(150,200,255,.8) 0%,transparent 55%),linear-gradient(155deg,#3a2540,#3a2848)',
sakura: 'radial-gradient(ellipse at 50% 100%,rgba(255,140,190,.9) 0%,transparent 55%),linear-gradient(155deg,#1a0a14,#241026)',
class: 'radial-gradient(ellipse at 50% 18%,rgba(255,220,150,.5) 0%,transparent 50%),linear-gradient(155deg,#16241c,#16261d)',
lab: 'radial-gradient(ellipse at 32% 30%,rgba(6,214,224,.75) 0%,transparent 50%),radial-gradient(ellipse at 72% 72%,rgba(56,217,90,.6) 0%,transparent 50%),linear-gradient(155deg,#06121e,#08202f)',
winter: 'radial-gradient(ellipse at 50% 100%,rgba(180,220,255,.85) 0%,transparent 55%),linear-gradient(155deg,#0a1430,#1a2c4a)',
rainbow: 'linear-gradient(160deg,#f94144,#f9c74f 30%,#38d95a 55%,#06d6e0 78%,#9b5de5)',
};
const BG_NAMES = { default:'Стандарт', space:'Космос', forest:'Лес', aqua:'Океан', sunset:'Закат', aurora:'Сияние', candy:'Леденец', sakura:'Сакура', class:'Класс', lab:'Лаборатория', winter:'Зима', rainbow:'Радуга' };
async function openPetShop() {
const data = await LS.api('/api/pet/shop').catch(() => null);
if (!data) return;
const items = [{ id:'default', name:'Стандарт', price:0, owned:true }, ...data.items];
const el = document.createElement('div');
el.className = 'pet-shop-overlay';
el.innerHTML = `
<div class="pet-shop-modal">
<div class="pet-shop-head">
<div class="pet-shop-title">Фоны сцены</div>
<div style="display:flex;align-items:center;gap:14px">
<div class="pet-shop-coins">
<svg width="13" height="13" viewBox="0 0 24 24" fill="#F9C74F" stroke="none"><circle cx="12" cy="12" r="10"/><text x="12" y="16" text-anchor="middle" font-size="10" fill="#111" font-weight="bold">₿</text></svg>
<span id="shop-coins-val">${data.coins}</span> монет
</div>
<button class="pet-shop-close" onclick="this.closest('.pet-shop-overlay').remove()"><svg class="ic" viewBox="0 0 24 24"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg></button>
</div>
</div>
<div class="pet-shop-grid" id="shop-grid">
${items.map(item => {
const isActive = data.currentBg === item.id;
const isOwned = item.owned || item.price === 0;
return `<div class="pet-bg-card${isActive?' active':''}" data-id="${item.id}" onclick="selectBg('${item.id}',${item.price||0},${isOwned},this)">
<div class="pet-bg-preview" style="background:${BG_PREVIEWS[item.id]||BG_PREVIEWS.default}"></div>
<div class="pet-bg-info">
<div class="pet-bg-name">${item.name}</div>
<div class="pet-bg-status">${isActive ? 'Активен' : isOwned ? 'Куплено' : item.price+' монет'}</div>
</div>
</div>`;
}).join('')}
</div>
</div>`;
document.body.appendChild(el);
el.addEventListener('click', e => { if (e.target === el) el.remove(); });
}
async function selectBg(id, price, owned, card) {
const modal = card.closest('.pet-shop-overlay');
const endpoint = owned ? '/api/pet/bg' : '/api/pet/shop/buy';
const res = await LS.api(endpoint, { method: owned ? 'PATCH' : 'POST', body: JSON.stringify({ id }) }).catch(e => {
if (e.data?.error === 'insufficient_coins') LS.toast?.('Недостаточно монет', 'error');
return null;
});
if (!res?.ok) return;
// Update scene bg
if (_petData) _petData.petBg = id;
applyPetBg(id, _petData && _petData.petBgCustom);
// Update coins display
if (res.coins !== undefined) {
const cv = document.getElementById('shop-coins-val');
if (cv) cv.textContent = res.coins;
document.getElementById('stat-coins').textContent = res.coins;
if (_petData) _petData.coins = res.coins;
}
// Update card states in modal
modal.querySelectorAll('.pet-bg-card').forEach(c => {
const isThis = c.dataset.id === id;
c.classList.toggle('active', isThis);
c.querySelector('.pet-bg-status').textContent = isThis ? 'Активен' : (c.dataset.id === 'default' || owned ? 'Куплено' : '');
// mark newly bought as owned
if (isThis && !owned) { c.querySelector('.pet-bg-status').textContent = 'Активен'; }
});
// mark all previously-owned cards
if (!owned) {
card.onclick = () => selectBg(id, 0, true, card);
}
}
/* ── Кастомизация: вкладки ── */
function setupCustomizeTabs() {
const tabs = document.querySelectorAll('.pc-tab');
tabs.forEach(t => t.addEventListener('click', () => {
tabs.forEach(x => x.classList.remove('active'));
t.classList.add('active');
const which = t.dataset.tab;
document.getElementById('pc-acc').style.display = which === 'acc' ? '' : 'none';
document.getElementById('pc-color').style.display = which === 'color' ? '' : 'none';
document.getElementById('pc-pattern').style.display = which === 'pattern' ? '' : 'none';
document.getElementById('pc-bg').style.display = which === 'bg' ? '' : 'none';
document.getElementById('pc-sets').style.display = which === 'sets' ? '' : 'none';
if (which === 'bg') renderBgPicker();
if (which === 'sets') renderSets();
}));
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(); });
}
/* ── Фоны (инлайн-выбор во вкладке) ── */
async function renderBgPicker() {
const grid = document.getElementById('pc-bg-grid');
if (!grid) return;
const data = await LS.api('/api/pet/shop').catch(() => null);
if (!data) { grid.innerHTML = '<div style="color:var(--text-2);font-size:.8rem">Не удалось загрузить</div>'; return; }
const coinsEl = document.getElementById('pc-bg-coins');
if (coinsEl) coinsEl.innerHTML = `Монет: <b style="color:#F9C74F">${data.coins}</b>`;
const items = [{ id: 'default', name: 'Стандарт', price: 0, owned: true }, ...data.items];
// Карточка кастомного фона (генерация ИИ) — всегда первой
const hasCustom = _petData && _petData.petBgCustom;
const activeCustom = data.currentBg === 'custom';
const customPrev = hasCustom
? `background:center/cover url("${hasCustom}")`
: 'background:linear-gradient(135deg,#9B5DE5,#06D6E0);display:flex;align-items:center;justify-content:center';
const sparkle = hasCustom ? '' : '<svg viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2" style="width:26px;height:26px"><path d="M13 3l2.2 6.3L22 12l-6.8 2.7L13 21l-2.2-6.3L4 12l6.8-2.7z"/><path d="M5 4v3M3.5 5.5h3"/></svg>';
const customCard = `<div class="pet-bg-card${activeCustom ? ' active' : ''}" data-id="__gen__">
<div class="pet-bg-preview" style="${customPrev}">${sparkle}</div>
<div class="pet-bg-info"><div class="pet-bg-name">Свой фон (ИИ)</div>
<div class="pet-bg-status">${activeCustom ? 'Активен' : hasCustom ? 'Перерисовать' : 'Создать'}</div></div>
</div>`;
grid.innerHTML = customCard + items.map(item => {
const isActive = data.currentBg === item.id;
const isOwned = item.owned || item.price === 0;
const status = isActive ? 'Активен' : isOwned ? 'Выбрать' : item.price + ' монет';
return `<div class="pet-bg-card${isActive ? ' active' : ''}" data-id="${item.id}" data-price="${item.price || 0}" data-owned="${isOwned ? 1 : 0}">
<div class="pet-bg-preview" style="background:${BG_PREVIEWS[item.id] || BG_PREVIEWS.default}"></div>
<div class="pet-bg-info"><div class="pet-bg-name">${escHtml(item.name)}</div>
<div class="pet-bg-status">${status}</div></div>
</div>`;
}).join('');
grid.querySelectorAll('.pet-bg-card').forEach(c => {
if (c.dataset.id === '__gen__') { c.addEventListener('click', openCustomBgModal); return; }
c.addEventListener('click', () => selectBgInline(c.dataset.id, +c.dataset.price, c.dataset.owned === '1'));
});
}
/* ── Кастомный фон: генерация ИИ → /api/pet/bg/custom ── */
function openCustomBgModal() {
if (!LS.imagePromptModal) { LS.toast?.('Модуль генерации не загружен'); return; }
LS.imagePromptModal({
title: 'Свой фон для питомца',
placeholder: 'Уютная сцена: «звёздная ночь над горами, мягкие тёплые тона»',
useLabel: 'Поставить фоном',
onUse: async function (url) {
const res = await LS.api('/api/pet/bg/custom', { method: 'POST', body: JSON.stringify({ url }) }).catch(() => null);
if (!res?.ok) { LS.toast?.('Не удалось установить фон', 'error'); return; }
if (_petData) { _petData.petBg = 'custom'; _petData.petBgCustom = res.url; }
applyPetBg('custom', res.url);
updatePreviewScene();
renderBgPicker();
LS.toast?.('Фон установлен', 'success');
}
});
}
async function selectBgInline(id, price, owned) {
// покупка платного фона — подтверждение + предпроверка баланса
if (!owned && id !== 'default') {
const coins = _petData ? (_petData.coins ?? 0) : 0;
if (coins < price) { LS.toast?.('Недостаточно монет: нужно ' + price, 'warn'); return; }
const ok = await LS.confirm('Купить фон «' + (BG_NAMES[id] || id) + '» за ' + price + ' монет?',
{ title: 'Покупка фона', confirmText: 'Купить за ' + price, danger: false });
if (!ok) return;
}
const endpoint = owned ? '/api/pet/bg' : '/api/pet/shop/buy';
const res = await LS.api(endpoint, { method: owned ? 'PATCH' : 'POST', body: JSON.stringify({ id }) }).catch(e => {
if (e?.data?.error === 'insufficient_coins') LS.toast?.('Недостаточно монет', 'error');
return null;
});
if (!res?.ok) return;
if (_petData) _petData.petBg = id;
applyPetBg(id, _petData && _petData.petBgCustom);
updatePreviewScene();
if (res.coins !== undefined) {
document.getElementById('stat-coins').textContent = res.coins;
const cv = document.getElementById('wr-coins-val'); if (cv) cv.textContent = res.coins;
if (_petData) _petData.coins = res.coins;
}
renderBgPicker();
}
/* ── Eye tracking + A2 Parallax ── */
function onStageMouse(e) {
const wrap = document.getElementById('pet-svg-wrap');
if (!wrap) return;
const rect = wrap.getBoundingClientRect();
const cx = rect.left + rect.width / 2;
const cy = rect.top + rect.height / 2;
const dx = Math.max(-4, Math.min(4, (e.clientX - cx) / rect.width * 16));
const dy = Math.max(-3, Math.min(3, (e.clientY - cy) / rect.height * 12));
applyPupilOffset(dx, dy);
const scene = document.getElementById('pet-scene');
if (scene) scene.style.transform = `perspective(600px) rotateX(${-dy * .45}deg) rotateY(${dx * .45}deg)`;
}
function applyPupilOffset(dx, dy) {
document.getElementById('pet-svg-wrap')?.querySelectorAll('.pet-pupil').forEach(el => {
el.style.transform = `translate(${dx}px,${dy}px)`;
});
}
/* ── Load ── */
async function loadPet() {
const d = await LS.api('/api/pet').catch(() => null);
if (!d) return;
_petData = d;
renderPet(d);
}
/* ── Render ── */
function renderPet(d) {
// Mood glow on stage
const stage = document.getElementById('pet-stage');
stage.className = `pet-stage glow-${d.mood}`;
// Evolution burst (level-up detection)
const stored = parseInt(localStorage.getItem('ls_pet_level') || '0');
localStorage.setItem('ls_pet_level', d.petLevel);
if (stored > 0 && d.petLevel > stored) triggerEvoBurst(d.petLevel);
// Pet SVG (B3: pass streak for rainbow collar)
const wrap = document.getElementById('pet-svg-wrap');
wrap.className = `pet-svg-wrap pet-float mood-${d.mood}`;
wrap.innerHTML = renderPetSVG(d.petLevel, d.mood, d.accessories, d.petColor || 'purple', d.streakCurrent || 0, d.petPattern || 'none');
// Scene mood class + weather + tod + B2 background
const scene = document.getElementById('pet-scene');
const prevTod = scene.dataset.tod;
scene.className = `pet-scene mood-${d.mood}`;
if (prevTod) scene.dataset.tod = prevTod;
applyWeather(d.mood);
applyPetBg(d.petBg || 'default', d.petBgCustom);
// B4: XP float if XP increased since last visit
const cachedXP = parseInt(localStorage.getItem('ls_pet_xp') || '0');
if (cachedXP > 0 && d.xp > cachedXP) setTimeout(() => floatLabel(`+${d.xp - cachedXP} XP`, '#38D95A'), 900);
localStorage.setItem('ls_pet_xp', d.xp);
// Speech bubble
const lines = BUBBLES[d.mood] || BUBBLES.neutral;
document.getElementById('pet-bubble').textContent = lines[Math.floor(Math.random()*lines.length)];
// Name
document.getElementById('pet-name').textContent = d.petName || 'Квантик';
// Mood badge
const mbd = document.getElementById('pet-mood-badge');
const [lbl, cls] = MOOD_LABELS[d.mood] || ['—',''];
mbd.textContent = lbl; mbd.className = `pet-mood ${cls}`;
// Evolution
const evoName = document.getElementById('pet-evo-name');
evoName.textContent = EVO_STAGES[d.petLevel] || '—';
evoName.style.color = EVO_COLORS[d.petLevel] || '#9B5DE5';
// Level dots (up to 8 levels, shown in two rows)
const dots = document.getElementById('pet-lvl-dots');
dots.innerHTML = '';
dots.style.flexWrap = 'wrap';
for (let i = 1; i <= 8; i++) {
const dot = document.createElement('div');
const active = i <= d.petLevel;
const col = EVO_COLORS[Math.min(d.petLevel, EVO_COLORS.length - 1)];
dot.style.cssText = `width:9px;height:9px;border-radius:50%;transition:all .3s;
background:${active ? col : 'rgba(255,255,255,.1)'};
${active ? `box-shadow:0 0 6px ${col}90` : ''}`;
dots.appendChild(dot);
}
// Color picker
renderColorPicker(d.petColor || 'purple');
// Гардероб (интерактивный выбор аксессуаров) + узор
renderWardrobe(d.wardrobe || []);
renderPatternPicker(d.patterns || [], d.petPattern || 'none');
// Stats
document.getElementById('stat-streak').textContent = d.streakCurrent + ' дн.';
document.getElementById('stat-streak-best').textContent = d.streakBest + ' дн.';
document.getElementById('stat-coins').textContent = d.coins;
document.getElementById('stat-petting').textContent = (d.pettingStreak || 0) + ' дн.';
// XP
document.getElementById('stat-level').textContent = 'Ур. ' + d.level;
document.getElementById('xp-curr').textContent = d.xp;
if (d.xpForNextLevel) {
const pct = Math.min(100, Math.round((d.xp - d.xpForCurrLevel) / (d.xpForNextLevel - d.xpForCurrLevel) * 100));
document.getElementById('xp-next-val').textContent = d.xpForNextLevel;
document.getElementById('xp-bar').style.width = pct + '%';
} else {
document.getElementById('xp-next-val').textContent = 'MAX';
document.getElementById('xp-bar').style.width = '100%';
}
// Hunger hearts
const hunger = Math.min(4, d.daysSinceLogin);
const fullH = 5 - hunger;
const hCols = ['#38D95A','#06D6E0','#F9C74F','#F98231','#F94144'];
const hC = hCols[hunger];
const hPath = 'M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z';
const hEl = document.getElementById('pet-hunger-dots');
hEl.innerHTML = '';
for (let i = 0; i < 5; i++) {
const svg = document.createElementNS('http://www.w3.org/2000/svg','svg');
svg.setAttribute('viewBox','0 0 24 24');
svg.setAttribute('class','pet-hunger-heart '+(i<fullH?'filled':'empty'));
const p = document.createElementNS('http://www.w3.org/2000/svg','path');
p.setAttribute('d', hPath);
p.setAttribute('fill', i<fullH ? hC : 'rgba(255,255,255,.5)');
p.setAttribute('stroke',i<fullH ? hC : 'rgba(255,255,255,.35)');
p.setAttribute('stroke-width','1');
svg.style.color = hC;
svg.appendChild(p); hEl.appendChild(svg);
}
document.getElementById('hunger-msg').textContent = HUNGER_MSGS[Math.min(hunger,4)];
// Mood tip + forecast
const tip = MOOD_TIPS[d.mood] || MOOD_TIPS.neutral;
document.getElementById('tip-ico').innerHTML = tip.ico;
document.getElementById('tip-title').textContent = tip.title;
document.getElementById('tip-text').textContent = tip.text;
const fc = document.getElementById('tip-forecast');
if (d.moodForecast) {
const mn = d.moodForecast.mood === 'sad' ? 'грустить' : 'голодать';
const msg = d.moodForecast.inDays === 0
? 'Питомец скоро почувствует себя хуже — зайди сегодня!'
: `Через ${d.moodForecast.inDays} дн. питомец начнёт ${mn}`;
fc.innerHTML = `${SVG_TIMER_ICO} ${msg}`;
fc.style.display = 'flex';
fc.style.alignItems = 'center';
fc.style.gap = '4px';
} else {
fc.style.display = 'none';
}
// Quests
renderQuests(d.quests || []);
// XP chart
renderChart(d.weeklyXP || []);
// Activity feed
const feed = document.getElementById('pet-feed-list');
if (d.recentActivity?.length) {
feed.innerHTML = d.recentActivity.map(ev => `
<div class="pet-feed-item">
<div class="pet-feed-xp">+${ev.xp}</div>
<div class="pet-feed-lbl">${escHtml(ev.label)}</div>
<div class="pet-feed-time">${fmtAgo(ev.at)}</div>
</div>`).join('');
} else {
feed.innerHTML = '<div class="pet-feed-empty">Нет активности</div>';
}
// Petting cooldown from server
if (d.pettingCooldown > 0) {
_petCooldown = true;
startCooldown(d.pettingCooldown);
}
// Feed cooldown from server
if (d.feedCooldown > 0) startFeedCooldown(d.feedCooldown);
}
/* ── Quests ── */
function renderQuests(quests) {
const el = document.getElementById('pet-quests');
if (!quests.length) { el.innerHTML = '<div style="color:var(--text-2);font-size:.8rem">Нет заданий</div>'; return; }
el.innerHTML = quests.map(q => {
const bar = q.goal !== undefined
? `<div class="quest-bar"><div class="quest-fill" style="width:${Math.round((q.progress||0)/q.goal*100)}%"></div></div>` : '';
return `<div class="quest-item${q.done?' done':''}">
<div class="quest-ico">${QUEST_ICONS[q.id] || ''}</div>
<div class="quest-body"><div class="quest-label">${q.label}</div>${bar}</div>
<div class="quest-check"><svg class="ic" viewBox="0 0 24 24"><polyline points="20 6 9 17 4 12"/></svg></div>
</div>`;
}).join('');
}
/* ── Weekly XP chart ── */
function renderChart(data) {
const chart = document.getElementById('xp-chart');
if (!data || !data.length) {
chart.innerHTML = '<div style="color:var(--text-2);font-size:.8rem;align-self:center">Нет данных</div>';
return;
}
const max = Math.max(...data.map(d => d.xp), 1);
chart.innerHTML = data.map((d, i) => {
const pct = Math.max(3, Math.round(d.xp / max * 100));
const isToday = i === data.length - 1;
return `<div class="chart-col${isToday?' today':''}">
<div class="chart-val">${d.xp > 0 ? d.xp : ''}</div>
<div class="chart-bar-wrap"><div class="chart-fill" style="height:${pct}%"></div></div>
<div class="chart-day">${d.day}</div>
</div>`;
}).join('');
}
/* ── Color picker ── */
function renderColorPicker(currentColor) {
const picker = document.getElementById('color-picker');
picker.innerHTML = Object.entries(PET_PALETTES).map(([key, hex]) =>
`<div class="pet-color-dot${currentColor === key ? ' active' : ''}"
style="background:${hex}" data-color="${key}"
title="${PALETTE_LABELS[key] || key}"></div>`
).join('');
picker.querySelectorAll('.pet-color-dot').forEach(dot =>
dot.addEventListener('click', () => selectColor(dot.dataset.color))
);
}
async function selectColor(colorKey) {
if (!_petData || _petData.petColor === colorKey) return;
const res = await LS.api('/api/pet/color', {method:'PATCH', body: JSON.stringify({color:colorKey})}).catch(()=>null);
if (!res?.ok) return;
_petData.petColor = colorKey;
paintPet();
document.querySelectorAll('.pet-color-dot').forEach(d =>
d.classList.toggle('active', d.dataset.color === colorKey)
);
}
/* ── Гардероб (выбор аксессуаров, по зонам) ── */
const LOCK_ICO = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>';
const CHECK_ICO = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>';
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:'Уши', hands:'В лапах', 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) return;
if (_petData.petBg === 'custom' && _petData.petBgCustom) {
sc.style.background = `center/cover url("${_petData.petBgCustom}")`;
} else {
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;
const cv = document.getElementById('wr-coins-val'); if (cv) cv.textContent = _petData.coins ?? 0;
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>`;
return `<span class="wr-tile tgl${it.equipped ? ' on' : ''}" data-id="${it.id}" title="${it.equipped ? 'Снять' : 'Надеть'}">${it.equipped ? CHECK_ICO : ''}${escHtml(it.name)}</span>`;
}
function renderWardrobe(items) {
const el = document.getElementById('pet-accessories');
if (!el) return;
el.style.cssText = '';
if (!items.length) { el.innerHTML = ''; return; }
const onCount = items.filter(i => i.equipped).length;
let html = `<div class="wr-bar"><span class="wr-count">Надето: <b>${onCount}</b></span>
<span class="wr-actions">
<button type="button" class="wr-btn" id="wr-random">${ICO_SHUFFLE}Случайный образ</button>
<button type="button" class="wr-btn" id="wr-clear">${ICO_ERASE}Снять всё</button>
</span></div>`;
['head','face','neck','ears','hands','accent'].forEach(z => {
const zi = items.filter(i => i.slot === z);
if (!zi.length) return;
html += `<div class="wr-zone"><div class="wr-zone-lbl">${ZONE_LABELS[z]||z}</div><div class="wr-chips">${zi.map(wearChip).join('')}</div></div>`;
});
el.innerHTML = html;
el.querySelectorAll('.wr-tile.tgl').forEach(ch => ch.addEventListener('click', () => toggleEquip(ch.dataset.id)));
document.getElementById('wr-clear')?.addEventListener('click', () => setEquipped([]));
document.getElementById('wr-random')?.addEventListener('click', randomLook);
}
async function setEquipped(list) {
if (!_petData || !_petData.wardrobe) return;
const res = await LS.api('/api/pet/equip', { method:'PATCH', body: JSON.stringify({ equipped: list }) }).catch(() => null);
if (!res || !res.ok) return;
const final = res.equipped || list;
_petData.accessories = final;
_petData.wardrobe.forEach(w => { w.equipped = final.includes(w.id); });
paintPet();
renderWardrobe(_petData.wardrobe);
}
function toggleEquip(id) {
if (!_petData || !_petData.wardrobe) return;
const item = _petData.wardrobe.find(w => w.id === id);
if (!item || item.locked) return;
const slotOf = {}; _petData.wardrobe.forEach(w => { slotOf[w.id] = w.slot; });
let eq = _petData.wardrobe.filter(w => w.equipped).map(w => w.id);
if (item.equipped) eq = eq.filter(x => x !== id);
else { eq = eq.filter(x => slotOf[x] !== item.slot); eq.push(id); }
setEquipped(eq);
}
function randomLook() {
if (!_petData || !_petData.wardrobe) return;
const bySlot = {};
_petData.wardrobe.filter(w => !w.locked).forEach(w => { (bySlot[w.slot] = bySlot[w.slot] || []).push(w); });
const pick = [];
Object.values(bySlot).forEach(arr => { if (Math.random() < 0.7) pick.push(arr[Math.floor(Math.random()*arr.length)].id); });
setEquipped(pick);
}
/* ── Узор тела ── */
function renderPatternPicker(list, current) {
const el = document.getElementById('pc-pattern-grid');
if (!el) return;
el.innerHTML = (list || []).map(p => {
const on = p.id === current;
return `<button type="button" class="pc-swatch${on?' active':''}" data-pat="${p.id}"><span class="pc-swatch-dot pat-${p.id}"></span><span class="pc-swatch-name">${escHtml(p.name)}</span></button>`;
}).join('');
el.querySelectorAll('.pc-swatch').forEach(b => b.addEventListener('click', () => applyPattern(b.dataset.pat)));
}
async function applyPattern(id) {
if (!_petData) return;
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;
paintPet();
renderPatternPicker(_petData.patterns || [], id);
}
/* ── Готовые образы (наборы) ── */
const OUTFIT_SETS = [
{ id:'scientist', name:'Учёный', acc:['grad','glasses','medal'], pattern:'none', color:'cyan' },
{ id:'wizard', name:'Волшебник', acc:['party','wand'], pattern:'galaxy', color:'indigo' },
{ id:'champion', name:'Чемпион', acc:['crown','star','medal'], pattern:'gradient', color:'gold' },
{ id:'cutie', name:'Милашка', acc:['flower','bowtie','balloon'],pattern:'hearts', color:'pink' },
];
function renderSets() {
const el = document.getElementById('pc-sets-grid'); if (!el) return;
el.innerHTML = OUTFIT_SETS.map(s => `<button type="button" class="pc-set" data-set="${s.id}">
<span class="pc-set-name">${escHtml(s.name)}</span>
<span class="pc-set-sub">${s.acc.length} предмета · ${PALETTE_LABELS[s.color] || s.color}</span></button>`).join('');
el.querySelectorAll('.pc-set').forEach(b => b.addEventListener('click', () => applySet(b.dataset.set)));
}
async function applySet(id) {
const set = OUTFIT_SETS.find(s => s.id === id); if (!set || !_petData) return;
const unlocked = new Set((_petData.wardrobe || []).filter(w => !w.locked).map(w => w.id));
await setEquipped(set.acc.filter(a => unlocked.has(a)));
if (set.color && _petData.petColor !== set.color) await selectColor(set.color);
if (set.pattern && _petData.petPattern !== set.pattern) await applyPattern(set.pattern);
LS.toast?.('Образ «' + set.name + '» применён', 'success');
}
/* ── Petting ── */
async function petThePet() {
if (_petCooldown) return;
const btn = document.getElementById('btn-pet');
btn.disabled = true;
const res = await LS.api('/api/pet/pet', {method:'POST'}).catch(() => null);
if (!res?.ok) {
if (res?.remaining) startCooldown(res.remaining);
else btn.disabled = false;
return;
}
// Animate pet
const wrap = document.getElementById('pet-svg-wrap');
wrap.classList.add('petting');
setTimeout(() => wrap.classList.remove('petting'), 600);
// Scatter hearts
spawnHearts();
// Update counters
if (_petData) {
_petData.pettingStreak = res.pettingStreak;
_petData.coins = (_petData.coins || 0) + res.coins;
document.getElementById('stat-coins').textContent = _petData.coins;
document.getElementById('stat-petting').textContent = res.pettingStreak + ' дн.';
}
// B4: coin float
floatLabel(`+${res.coins} монет`, '#F9C74F');
// Happy bubble
document.getElementById('pet-bubble').textContent = HAPPY_PHRASES[Math.floor(Math.random()*HAPPY_PHRASES.length)];
startCooldown(60);
}
function startCooldown(secs) {
_petCooldown = true;
const btn = document.getElementById('btn-pet');
btn.disabled = true;
btn.innerHTML = `${SVG_CLOCK} ${secs}с`;
clearInterval(_petCooldownTimer);
_petCooldownTimer = setInterval(() => {
secs--;
if (secs <= 0) {
clearInterval(_petCooldownTimer);
btn.disabled = false;
btn.innerHTML = `${SVG_HAND} Погладить`;
_petCooldown = false;
if (_petData) {
const lines = BUBBLES[_petData.mood] || BUBBLES.neutral;
document.getElementById('pet-bubble').textContent = lines[Math.floor(Math.random()*lines.length)];
}
} else {
btn.innerHTML = `${SVG_CLOCK} ${secs}с`;
}
}, 1000);
}
/* ── Feed mini-game ──────────────────────────────────────────────────── */
const FEED_QUESTIONS = [
{ q: 'Химическая формула воды', a: 'H₂O', opts: ['H₂O', 'CO₂', 'NaCl', 'H₂SO₄'] },
{ q: 'Формула углекислого газа', a: 'CO₂', opts: ['CO₂', 'O₂', 'CH₄', 'N₂O'] },
{ q: 'Молекула кислорода', a: 'O₂', opts: ['O₂', 'O₃', 'H₂O₂', 'OH'] },
{ q: 'Хлорид натрия (поваренная соль)', a: 'NaCl', opts: ['NaCl', 'KCl', 'CaCl₂', 'MgCl₂'] },
{ q: 'Серная кислота', a: 'H₂SO₄', opts: ['H₂SO₄', 'HCl', 'HNO₃', 'H₃PO₄'] },
{ q: 'Аммиак', a: 'NH₃', opts: ['NH₃', 'NO₂', 'N₂H₄', 'NH₄OH'] },
{ q: 'Глюкоза', a: 'C₆H₁₂O₆', opts: ['C₆H₁₂O₆', 'C₁₂H₂₂O₁₁', 'C₅H₁₀O₅', 'C₂H₅OH'] },
{ q: 'Метан (природный газ)', a: 'CH₄', opts: ['CH₄', 'C₂H₆', 'C₃H₈', 'C₄H₁₀'] },
{ q: 'Озон', a: 'O₃', opts: ['O₃', 'O₂', 'O₄', 'OH'] },
{ q: 'Перекись водорода', a: 'H₂O₂', opts: ['H₂O₂', 'H₂O', 'H₂SO₄', 'HOH'] },
{ q: 'Этиловый спирт', a: 'C₂H₅OH', opts: ['C₂H₅OH', 'CH₃OH', 'C₃H₇OH', 'C₄H₉OH'] },
{ q: 'Соляная кислота', a: 'HCl', opts: ['HCl', 'HBr', 'HF', 'HI'] },
{ q: 'Гидроксид натрия (щёлочь)', a: 'NaOH', opts: ['NaOH', 'KOH', 'Ca(OH)₂', 'Mg(OH)₂'] },
{ q: 'АТФ — основа энергии клетки', a: 'C₁₀H₁₆N₅O₁₃P₃', opts: ['C₁₀H₁₆N₅O₁₃P₃', 'C₅H₁₀O₅', 'C₆H₁₂O₆', 'C₃H₇NO₂'] },
{ q: 'Азотная кислота', a: 'HNO₃', opts: ['HNO₃', 'H₂SO₄', 'HCl', 'H₃PO₄'] },
{ q: 'Ацетилсалициловая кислота (аспирин)', a: 'C₉H₈O₄', opts: ['C₉H₈O₄', 'C₇H₆O₃', 'C₁₁H₁₂O₄', 'C₈H₉NO₂'] },
];
let _feedTimerInterval = null, _feedTimerSecs = 0, _feedAnswered = false;
let _feedCooldownTimer = null;
function openFeedGame() {
const btn = document.getElementById('btn-feed');
if (btn.disabled) return;
if (_petData && _petData.feedCooldown > 0) { startFeedCooldown(_petData.feedCooldown); return; }
_feedAnswered = false;
const q = FEED_QUESTIONS[Math.floor(Math.random() * FEED_QUESTIONS.length)];
document.getElementById('feed-q-text').textContent = q.q;
document.getElementById('feed-result').style.display = 'none';
// shuffle opts
const opts = [...q.opts].sort(() => Math.random() - 0.5);
const optsEl = document.getElementById('feed-opts');
optsEl.innerHTML = opts.map(o => `
<button onclick="feedAnswer(this,${JSON.stringify(o).replace(/"/g,'&quot;')},${JSON.stringify(q.a).replace(/"/g,'&quot;')})"
style="padding:10px 14px;border:1.5px solid var(--border-h);border-radius:12px;background:transparent;font-family:'Manrope',sans-serif;font-size:.88rem;font-weight:600;color:var(--text);cursor:pointer;text-align:left;transition:all .2s"
onmouseover="this.style.borderColor='var(--violet)'" onmouseout="this.style.borderColor='var(--border-h)'">
${escHtml(o)}
</button>`).join('');
const overlay = document.getElementById('feed-overlay');
overlay.style.display = 'flex';
// timer: 15 seconds
_feedTimerSecs = 15;
document.getElementById('feed-timer-fill').style.width = '100%';
clearInterval(_feedTimerInterval);
_feedTimerInterval = setInterval(() => {
_feedTimerSecs--;
document.getElementById('feed-timer-fill').style.width = (_feedTimerSecs / 15 * 100) + '%';
if (_feedTimerSecs <= 0) {
clearInterval(_feedTimerInterval);
if (!_feedAnswered) showFeedResult(false, null);
}
}, 1000);
}
function closeFeedGame() {
clearInterval(_feedTimerInterval);
document.getElementById('feed-overlay').style.display = 'none';
}
async function feedAnswer(btn, chosen, correct) {
if (_feedAnswered) return;
_feedAnswered = true;
clearInterval(_feedTimerInterval);
document.getElementById('feed-opts').querySelectorAll('button').forEach(b => b.disabled = true);
const isRight = chosen === correct;
if (isRight) {
btn.style.cssText += ';border-color:#38D95A;background:rgba(56,217,90,.1);color:#38D95A';
const res = await LS.api('/api/pet/feed', {method:'POST'}).catch(() => null);
if (res?.ok) {
if (_petData) { _petData.xp = res.xp; _petData.coins = res.coins; _petData.feedCooldown = 1800; }
showFeedResult(true, res.xpAwarded || 15);
} else if (res?.remaining) {
showFeedResult(null, 0, 'Подожди ' + Math.ceil(res.remaining / 60) + ' мин. — питомец ещё сыт!');
startFeedCooldown(res.remaining);
} else {
showFeedResult(true, 15);
}
} else {
btn.style.cssText += ';border-color:#F94144;background:rgba(249,65,68,.08);color:#F94144';
// Highlight correct
document.getElementById('feed-opts').querySelectorAll('button').forEach(b => {
if (b.textContent.trim() === correct) b.style.cssText += ';border-color:#38D95A;background:rgba(56,217,90,.1);color:#38D95A';
});
showFeedResult(false, null);
}
}
function showFeedResult(correct, xp, customMsg) {
const el = document.getElementById('feed-result');
if (correct === null) {
el.style.cssText = 'display:block;padding:12px 14px;border-radius:12px;background:rgba(155,93,229,.1);border:1.5px solid rgba(155,93,229,.25);font-size:.87rem;font-weight:700;text-align:center;color:#9B5DE5';
el.textContent = customMsg || 'Время вышло!';
} else if (correct) {
el.style.cssText = 'display:block;padding:12px 14px;border-radius:12px;background:rgba(56,217,90,.1);border:1.5px solid rgba(56,217,90,.25);font-size:.87rem;font-weight:700;text-align:center;color:#38D95A';
el.textContent = `Правильно! +${xp} XP — питомец доволен!`;
floatLabel(`+${xp} XP`, '#38D95A');
document.getElementById('pet-bubble').textContent = 'Вкуснятина!';
setTimeout(() => { closeFeedGame(); startFeedCooldown(1800); }, 1800);
} else {
el.style.cssText = 'display:block;padding:12px 14px;border-radius:12px;background:rgba(249,65,68,.08);border:1.5px solid rgba(249,65,68,.2);font-size:.87rem;font-weight:700;text-align:center;color:#F94144';
el.textContent = 'Неверно — питомец остался голодным';
setTimeout(closeFeedGame, 1800);
}
}
const SVG_FOOD = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#F9C74F" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 2v7c0 1.1.9 2 2 2h4a2 2 0 0 0 2-2V2"/><path d="M7 2v20"/><path d="M21 15V2v0a5 5 0 0 0-5 5v6c0 1.1.9 2 2 2h3Zm0 0v7"/></svg>`;
function startFeedCooldown(secs) {
const btn = document.getElementById('btn-feed');
btn.disabled = true;
btn.innerHTML = `${SVG_CLOCK} ${Math.ceil(secs / 60)}м`;
clearInterval(_feedCooldownTimer);
_feedCooldownTimer = setInterval(() => {
secs -= 10;
if (secs <= 0) {
clearInterval(_feedCooldownTimer);
btn.disabled = false;
btn.innerHTML = `${SVG_FOOD} Покормить`;
if (_petData) _petData.feedCooldown = 0;
} else {
btn.innerHTML = `${SVG_CLOCK} ${Math.ceil(secs / 60)}м`;
}
}, 10000);
}
/* ── Scatter hearts (petting) ── */
function spawnHearts() {
const scene = document.getElementById('pet-scene');
const hearts = [
'<svg width="1em" height="1em" viewBox="0 0 24 24" fill="#F94144" stroke="none"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/></svg>',
'<svg width="1em" height="1em" viewBox="0 0 24 24" fill="#FF6B9D" stroke="none"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/></svg>',
'<svg width="1em" height="1em" viewBox="0 0 24 24" fill="#F9C74F" stroke="none"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/></svg>',
'<svg width="1em" height="1em" viewBox="0 0 24 24" fill="#9B5DE5" stroke="none"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/></svg>',
'<svg width="1em" height="1em" viewBox="0 0 24 24" fill="#F94144" stroke="none"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/></svg>',
'<svg width="1em" height="1em" viewBox="0 0 24 24" fill="#06D6A0" stroke="none"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/></svg>',
];
for (let i = 0; i < 5; i++) {
setTimeout(() => {
const el = document.createElement('div');
const angle = (i / 5) * Math.PI * 2 + Math.random() * 0.5;
const dist = 38 + Math.random() * 28;
const tx = Math.cos(angle) * dist;
const ty = Math.sin(angle) * dist - 20;
el.style.cssText = `
position:absolute; font-size:${1+Math.random()*.5}rem;
top:50%; left:50%; margin:-10px 0 0 -10px;
pointer-events:none; z-index:10;
--tx:${tx}px; --ty:${ty}px;
animation:floatHeart 1s ease-out forwards;`;
el.innerHTML = hearts[i % hearts.length];
scene.appendChild(el);
setTimeout(() => el.remove(), 1100);
}, i * 110);
}
}
/* ── Evolution burst ── */
function triggerEvoBurst(newLevel) {
const stage = document.getElementById('pet-stage');
const scene = document.getElementById('pet-scene');
stage.classList.add('evo-burst');
setTimeout(() => stage.classList.remove('evo-burst'), 1000);
const sparkles = [
'<svg width="1em" height="1em" viewBox="0 0 24 24" fill="#F9C74F" stroke="none"><path d="m12 3-1.9 5.8a2 2 0 0 1-1.3 1.3L3 12l5.8 1.9a2 2 0 0 1 1.3 1.3L12 21l1.9-5.8a2 2 0 0 1 1.3-1.3L21 12l-5.8-1.9a2 2 0 0 1-1.3-1.3z"/></svg>',
'<svg width="1em" height="1em" viewBox="0 0 24 24" fill="#FFD166" stroke="none"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>',
'<svg width="1em" height="1em" viewBox="0 0 24 24" fill="#9B5DE5" stroke="none"><path d="m12 3-1.9 5.8a2 2 0 0 1-1.3 1.3L3 12l5.8 1.9a2 2 0 0 1 1.3 1.3L12 21l1.9-5.8a2 2 0 0 1 1.3-1.3L21 12l-5.8-1.9a2 2 0 0 1-1.3-1.3z"/></svg>',
'<svg width="1em" height="1em" viewBox="0 0 24 24" fill="#06D6E0" stroke="none"><path d="m12 3-1.9 5.8a2 2 0 0 1-1.3 1.3L3 12l5.8 1.9a2 2 0 0 1 1.3 1.3L12 21l1.9-5.8a2 2 0 0 1 1.3-1.3L21 12l-5.8-1.9a2 2 0 0 1-1.3-1.3z"/></svg>',
'<svg width="1em" height="1em" viewBox="0 0 24 24" fill="#F9C74F" stroke="none"><path d="m12 3-1.9 5.8a2 2 0 0 1-1.3 1.3L3 12l5.8 1.9a2 2 0 0 1 1.3 1.3L12 21l1.9-5.8a2 2 0 0 1 1.3-1.3L21 12l-5.8-1.9a2 2 0 0 1-1.3-1.3z"/></svg>',
'<svg width="1em" height="1em" viewBox="0 0 24 24" fill="#FFD166" stroke="none"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>',
];
for (let i = 0; i < 10; i++) {
setTimeout(() => {
const el = document.createElement('div');
const angle = (i / 10) * Math.PI * 2 + Math.random() * 0.3;
const dist = 55 + Math.random() * 35;
el.style.cssText = `
position:absolute; font-size:${0.9+Math.random()*.5}rem;
top:50%; left:50%; margin:-10px 0 0 -10px;
pointer-events:none; z-index:20;
--tx:${Math.cos(angle)*dist}px; --ty:${Math.sin(angle)*dist}px;
animation:floatHeart 1.3s ease-out forwards;`;
el.innerHTML = sparkles[i % sparkles.length];
scene.appendChild(el);
setTimeout(() => el.remove(), 1400);
}, i * 70);
}
}
/* ── Rename ── */
function toggleRename() {
const form = document.getElementById('rename-form');
if (form.classList.contains('visible')) {
form.classList.remove('visible');
} else {
document.getElementById('rename-input').value = _petData?.petName || '';
form.classList.add('visible');
document.getElementById('rename-input').focus();
}
}
async function saveName() {
const name = document.getElementById('rename-input').value.trim();
if (!name) return;
const res = await LS.api('/api/pet/name', {method:'PATCH', body:{name}}).catch(()=>null);
if (res?.ok) {
document.getElementById('pet-name').textContent = name;
if (_petData) _petData.petName = name;
document.getElementById('rename-form').classList.remove('visible');
}
}
/* ── Helpers ── */
function escHtml(s) { return (s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }
function fmtAgo(dateStr) {
if (!dateStr) return '';
const diff = (Date.now() - new Date(dateStr)) / 1000;
if (diff < 60) return 'только что';
if (diff < 3600) return Math.floor(diff/60) + ' мин.';
if (diff < 86400) return Math.floor(diff/3600) + ' ч.';
const d = Math.floor(diff/86400);
return d === 1 ? 'вчера' : d + ' дн.';
}
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})`;
}
/* ── Pet SVG renderer ── */
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${level}${mood[0]}${colorKey[0]}`;
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>`;
}
</script>
<script src="/js/notifications.js"></script>
<script src="/js/search.js"></script>
<script src="/js/mobile.js"></script>
</body>
</html>