21cea72874
- Убраны эмодзи (правило: только 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>
2295 lines
138 KiB
HTML
2295 lines
138 KiB
HTML
<!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,'"')},${JSON.stringify(q.a).replace(/"/g,'"')})"
|
||
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,'&').replace(/</g,'<').replace(/>/g,'>'); }
|
||
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>
|