Files
Learn_System/frontend/pet.html
T
Maxim Dolgolyov be4d43105e LearnSpace: full-stack educational whiteboard platform
Node.js/Express backend + vanilla JS frontend.
Features: real-time collaborative whiteboard (SSE), multi-page support,
LaTeX formulas, shapes/connectors, coordinate systems, number lines,
compass, zoom/pan, Catmull-Rom pencil smoothing, ruler/protractor with
rotation & resize controls, minimap navigation overlay, auto-measurements,
multi-page thumbnails sidebar, PNG export, page templates.
Student/teacher workflows: classes, assignments, library, dashboard.
Mobile responsive. SQLite (better-sqlite3).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-12 10:10:37 +03:00

1756 lines
99 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Питомец — LearnSpace</title>
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
<link href="https://fonts.googleapis.com/css2?family=Unbounded:wght@400;700;800&family=Manrope:wght@400;500;600;700&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="/css/ls.css" />
<style>
.sb-content { padding: 0; }
.pet-wrap { min-height: 100vh; padding: 24px 20px 60px; max-width: 1020px; margin: 0 auto; }
/* ── Header ── */
.pet-header { display:flex; align-items:center; gap:14px; margin-bottom:24px; }
.pet-header-icon {
width:48px; height:48px; border-radius:13px; flex-shrink:0;
background: linear-gradient(135deg,rgba(249,199,79,.22),rgba(249,65,68,.14));
border:1.5px solid rgba(255,255,255,.1);
display:flex; align-items:center; justify-content:center;
}
.pet-header-icon svg { width:26px; height:26px; fill:none; stroke:#F9C74F; stroke-width:1.8; }
.pet-h-title { font-family:'Unbounded',sans-serif; font-size:1.25rem; font-weight:800; }
.pet-h-sub { font-size:.8rem; color:var(--text-2); margin-top:2px; }
/* ── Hero grid ── */
.pet-hero { display:grid; grid-template-columns:300px 1fr; gap:18px; margin-bottom:18px; }
/* ── Stage card ── */
.pet-stage {
background: var(--surface); border:1.5px solid rgba(255,255,255,.08);
border-radius:22px; padding:22px 18px 20px;
display:flex; flex-direction:column; align-items:center; gap:11px;
position:relative; overflow:hidden;
}
.pet-stage::before {
content:''; position:absolute; inset:0; border-radius:22px;
pointer-events:none; opacity:0; transition:opacity .8s, background .8s;
}
.pet-stage.glow-ecstatic::before { opacity:1; background:radial-gradient(ellipse at 50% 60%, rgba(249,199,79,.18) 0%, transparent 68%); }
.pet-stage.glow-happy::before { opacity:1; background:radial-gradient(ellipse at 50% 60%, rgba(56,217,90,.14) 0%, transparent 68%); }
.pet-stage.glow-neutral::before { opacity:1; background:radial-gradient(ellipse at 50% 60%, rgba(6,214,224,.1) 0%, transparent 68%); }
.pet-stage.glow-sad::before { opacity:1; background:radial-gradient(ellipse at 50% 60%, rgba(100,149,237,.12) 0%, transparent 68%); }
.pet-stage.glow-hungry::before { opacity:1; background:radial-gradient(ellipse at 50% 60%, rgba(249,130,49,.14) 0%, transparent 68%); }
.pet-stage.glow-sleeping::before { opacity:1; background:radial-gradient(ellipse at 50% 60%, rgba(155,93,229,.16) 0%, transparent 68%); }
/* Evolution burst */
@keyframes evoBurst {
0%{transform:scale(1)} 20%{transform:scale(1.06)} 50%{transform:scale(0.97)} 75%{transform:scale(1.03)} 100%{transform:scale(1)}
}
.evo-burst { animation:evoBurst .9s cubic-bezier(.34,1.56,.64,1); }
/* Speech bubble */
.pet-bubble {
background:rgba(255,255,255,.07); border:1.5px solid rgba(255,255,255,.12);
border-radius:14px; padding:9px 14px;
font-size:.82rem; color:var(--text); line-height:1.5; text-align:center;
width:100%; box-sizing:border-box; position:relative; z-index:1;
animation: bubIn .4s cubic-bezier(.34,1.56,.64,1);
}
.pet-bubble::after {
content:''; position:absolute; bottom:-9px; left:50%; transform:translateX(-50%);
border:5px solid transparent; border-top-color:rgba(255,255,255,.12);
}
@keyframes bubIn { from{opacity:0;transform:scale(.9) translateY(-4px)} to{opacity:1;transform:none} }
/* Pet scene */
.pet-scene { position:relative; width:200px; height:200px; margin:0 auto; z-index:1; transition:transform .25s ease; border-radius:50%; }
.pet-svg-wrap { width:200px; height:200px; filter:drop-shadow(0 10px 28px rgba(0,0,0,.3)); }
/* A1 — Breathing */
@keyframes breathe { 0%,100%{transform:scale(1)} 50%{transform:scale(1.013,1.022)} }
.pet-svg-wrap svg { animation:breathe 3s ease-in-out infinite; transform-box:fill-box; transform-origin:50% 75%; }
.mood-sleeping .pet-svg-wrap svg { animation-duration:5s; }
.mood-ecstatic .pet-svg-wrap svg { animation-duration:1.7s; }
/* A3 — Weather particles */
.pet-weather { position:absolute; inset:0; pointer-events:none; overflow:hidden; border-radius:50%; z-index:0; }
@keyframes wxRain { 0%{transform:translateY(-10px);opacity:0} 10%{opacity:.7} 90%{opacity:.55} 100%{transform:translateY(220px);opacity:0} }
@keyframes wxSparkle { 0%{transform:translate(0,50px) scale(0);opacity:0} 18%{opacity:.9;transform:translate(0,30px) scale(1)} 100%{transform:translate(var(--wx,0px),-30px) scale(.2);opacity:0} }
@keyframes wxFog { 0%,100%{opacity:.1;transform:translateX(0)} 50%{opacity:.22;transform:translateX(9px)} }
@keyframes wxTwinkle { 0%,100%{opacity:1} 50%{opacity:.15} }
/* A4 — Time of day scene backgrounds */
.pet-scene[data-tod="dawn"] { background:radial-gradient(ellipse at 50% 100%, rgba(255,100,60,.2) 0%, transparent 65%), linear-gradient(155deg,#1a0a2e,#4a1830); }
.pet-scene[data-tod="day"] { background:radial-gradient(ellipse at 50% 100%, rgba(155,93,229,.2) 0%, transparent 65%), linear-gradient(155deg,#0d0d1f,#1a1040); }
.pet-scene[data-tod="dusk"] { background:radial-gradient(ellipse at 50% 100%, rgba(249,100,50,.2) 0%, transparent 65%), linear-gradient(155deg,#1a0a2e,#5d1e10); }
.pet-scene[data-tod="night"] { background:radial-gradient(ellipse at 50% 100%, rgba(6,214,224,.1) 0%, transparent 65%), linear-gradient(155deg,#04040f,#0a0a20); }
/* B1 — Star mini-game */
.pet-star-game { position:absolute; pointer-events:none; z-index:12; transform:translate(-50%,-50%); }
.pet-star-game.active { pointer-events:auto; }
@keyframes starAppear { 0%{opacity:0;transform:scale(0) rotate(-30deg)} 25%{opacity:1;transform:scale(1.15) rotate(5deg)} 100%{transform:scale(1) rotate(0deg);opacity:1} }
@keyframes starPulse { 0%,100%{filter:drop-shadow(0 0 5px #F9C74F);transform:scale(1) rotate(0deg)} 50%{filter:drop-shadow(0 0 14px #F9C74F);transform:scale(1.18) rotate(18deg)} }
.pet-star-btn { background:none; border:none; cursor:pointer; padding:0; animation:starPulse 1.1s ease-in-out infinite; display:block; }
.pet-star-btn:active { transform:scale(0.8) !important; }
.pet-star-wrap { animation:starAppear .35s cubic-bezier(.34,1.56,.64,1) forwards; }
/* B4 — XP / coin floats */
@keyframes xpFloat { 0%{transform:translateY(0) scale(1);opacity:1} 100%{transform:translateY(-72px) scale(.75);opacity:0} }
.pet-xp-float { position:absolute; font-family:'Unbounded',sans-serif; font-size:.72rem; font-weight:800; pointer-events:none; z-index:20; animation:xpFloat 1.6s ease-out forwards; white-space:nowrap; }
.pet-float { animation:petFloat 3s ease-in-out infinite; }
.mood-ecstatic .pet-float { animation:petBounce .5s ease-in-out infinite alternate; }
.mood-sleeping .pet-float { animation:petSway 4s ease-in-out infinite; }
.mood-hungry .pet-float { animation:petFloat 6s ease-in-out infinite; }
@keyframes petFloat { 0%,100%{transform:translateY(0)} 50%{transform:translateY(-12px)} }
@keyframes petBounce { 0%{transform:translateY(0) scale(1)} 100%{transform:translateY(-16px) scale(1.05)} }
@keyframes petSway { 0%,100%{transform:rotate(0deg)} 50%{transform:rotate(3deg)} }
@keyframes petPet { 0%{transform:scale(1)} 30%{transform:scale(1.18) rotate(-6deg)} 60%{transform:scale(1.1) rotate(4deg)} 100%{transform:scale(1)} }
.pet-svg-wrap.petting { animation:petPet .5s ease-out forwards !important; }
@keyframes blink {
0%, 88%, 100% { transform: scaleY(1); }
94% { transform: scaleY(0.06); }
}
.pet-eye-blink { animation: blink 3.5s ease-in-out infinite; transform-box: fill-box; transform-origin: center; }
.pet-eye-blink2 { animation: blink 3.5s ease-in-out 0.08s infinite; transform-box: fill-box; transform-origin: center; }
@keyframes floatHeart {
0% { opacity:1; transform:translate(0,0) scale(1); }
100%{ opacity:0; transform:translate(var(--tx,0px),var(--ty,-60px)) scale(1.4); }
}
/* ZZZ */
.pet-zzz { position:absolute; top:10px; right:10px; display:none; flex-direction:column; align-items:flex-end; gap:2px; }
.mood-sleeping .pet-zzz { display:flex; }
.pet-zzz span { font-family:'Unbounded',sans-serif; font-weight:800; color:rgba(155,93,229,.65); animation:zzzFloat 2s ease-in-out infinite; }
.pet-zzz span:nth-child(1) { font-size:.65rem; animation-delay:0s; }
.pet-zzz span:nth-child(2) { font-size:.85rem; animation-delay:.5s; }
.pet-zzz span:nth-child(3) { font-size:1.05rem; animation-delay:1s; }
@keyframes zzzFloat { 0%,100%{opacity:.4;transform:translateY(0)} 50%{opacity:1;transform:translateY(-7px)} }
/* Hearts ambient */
.pet-hearts { position:absolute; top:0; left:0; width:100%; height:100%; pointer-events:none; display:none; }
.mood-ecstatic .pet-hearts, .mood-happy .pet-hearts { display:block; }
.pet-heart { position:absolute; animation:heartPop 1.5s ease-out infinite; opacity:0; }
.pet-heart:nth-child(1) { left:8%; top:20%; animation-delay:0s; }
.pet-heart:nth-child(2) { left:72%; top:14%; animation-delay:.5s; }
.pet-heart:nth-child(3) { left:46%; top:2%; animation-delay:1s; }
@keyframes heartPop { 0%{opacity:0;transform:scale(0)} 30%{opacity:1;transform:scale(1.2) translateY(-8px)} 100%{opacity:0;transform:scale(.8) translateY(-26px)} }
/* Name / rename */
.pet-name-wrap { display:flex; align-items:center; gap:6px; z-index:1; }
.pet-name { font-family:'Unbounded',sans-serif; font-size:1.1rem; font-weight:800; color:var(--text); }
.pet-rename-btn { background:transparent; border:none; cursor:pointer; padding:4px; border-radius:6px; color:var(--text-2); transition:color .15s; }
.pet-rename-btn:hover { color:#9B5DE5; }
.pet-rename-btn svg { width:14px; height:14px; stroke:currentColor; fill:none; stroke-width:2; }
.pet-rename-form { display:none; flex-direction:column; gap:8px; width:100%; z-index:1; }
.pet-rename-form.visible { display:flex; }
.pet-rename-input {
padding:8px 14px; border-radius:10px; border:1.5px solid rgba(155,93,229,.4);
background:rgba(155,93,229,.1); color:var(--text); font-family:'Manrope',sans-serif;
font-size:.9rem; font-weight:600; outline:none; text-align:center;
}
.pet-rename-input:focus { border-color:#9B5DE5; }
.pet-rename-actions { display:flex; gap:8px; justify-content:center; }
.pet-btn { padding:6px 16px; border-radius:99px; font-family:'Manrope',sans-serif; font-size:.8rem; font-weight:700; cursor:pointer; border:1.5px solid rgba(255,255,255,.15); background:transparent; color:var(--text); transition:all .15s; }
.pet-btn.primary { background:#9B5DE5; border-color:#9B5DE5; color:#fff; }
.pet-btn.primary:hover { background:#8347d4; }
.pet-btn:hover { background:rgba(255,255,255,.08); }
/* Mood badge */
.pet-mood { padding:5px 14px; border-radius:99px; font-size:.76rem; font-weight:700; display:inline-flex; align-items:center; gap:5px; z-index:1; }
.mood-ecstatic-badge { background:rgba(249,199,79,.15); color:#F9C74F; border:1px solid rgba(249,199,79,.3); }
.mood-happy-badge { background:rgba(56,217,90,.15); color:#38D95A; border:1px solid rgba(56,217,90,.3); }
.mood-neutral-badge { background:rgba(6,214,224,.12); color:#06D6E0; border:1px solid rgba(6,214,224,.25); }
.mood-sad-badge { background:rgba(249,65,68,.12); color:#F94144; border:1px solid rgba(249,65,68,.25); }
.mood-hungry-badge { background:rgba(249,130,49,.15); color:#F98231; border:1px solid rgba(249,130,49,.3); }
.mood-sleeping-badge { background:rgba(155,93,229,.12); color:#9B5DE5; border:1px solid rgba(155,93,229,.25); }
/* Evolution */
.pet-evo { display:flex; flex-direction:column; align-items:center; gap:4px; z-index:1; }
.pet-evo-lbl { font-size:.63rem; font-weight:700; color:var(--text-2); text-transform:uppercase; letter-spacing:.06em; }
.pet-evo-name { font-family:'Unbounded',sans-serif; font-size:.73rem; font-weight:800; }
.pet-lvl-dots { display:flex; gap:5px; margin-top:2px; }
/* Color picker */
.pet-color-row { display:flex; align-items:center; gap:10px; z-index:1; }
.pet-color-lbl { font-size:.64rem; font-weight:700; color:var(--text-2); text-transform:uppercase; letter-spacing:.05em; }
.pet-color-picker { display:flex; gap:7px; }
.pet-color-dot {
width:18px; height:18px; border-radius:50%; cursor:pointer;
border:2.5px solid transparent; transition:all .18s; flex-shrink:0;
}
.pet-color-dot.active { border-color:rgba(255,255,255,.85); transform:scale(1.25); }
.pet-color-dot:hover:not(.active) { transform:scale(1.12); }
/* Accessories */
.pet-accessories { display:flex; gap:5px; flex-wrap:wrap; justify-content:center; z-index:1; }
.pet-acc-badge { padding:3px 10px; border-radius:99px; font-size:.7rem; font-weight:700; background:rgba(249,199,79,.12); color:#F9C74F; border:1px solid rgba(249,199,79,.2); }
/* Petting button */
.pet-action-btn {
padding:8px 22px; border-radius:99px; font-family:'Manrope',sans-serif; font-size:.84rem; font-weight:700;
cursor:pointer; border:1.5px solid rgba(249,199,79,.35); background:rgba(249,199,79,.1); color:#F9C74F;
transition:all .18s; z-index:1; display:flex; align-items:center; gap:6px;
}
.pet-action-btn:hover:not(:disabled) { background:rgba(249,199,79,.2); border-color:rgba(249,199,79,.5); }
.pet-action-btn:disabled { opacity:.5; cursor:not-allowed; }
/* ── Right column ── */
.pet-right { display:flex; flex-direction:column; gap:12px; }
/* Mini stats 2×2 */
.pet-stats-row { display:grid; grid-template-columns:repeat(2,1fr); gap:8px; }
.pet-mini { background:var(--surface); border:1.5px solid rgba(255,255,255,.08); border-radius:14px; padding:12px 10px; display:flex; flex-direction:column; align-items:center; gap:3px; text-align:center; }
.pet-mini-ico { width:22px; height:22px; display:flex; align-items:center; justify-content:center; }
.pet-mini-val { font-family:'Unbounded',sans-serif; font-size:.95rem; font-weight:800; }
.pet-mini-lbl { font-size:.62rem; color:var(--text-2); font-weight:700; text-transform:uppercase; letter-spacing:.04em; }
/* Card generic */
.pet-card { background:var(--surface); border:1.5px solid rgba(255,255,255,.08); border-radius:14px; padding:14px 16px; }
.pet-card-title { font-size:.67rem; font-weight:800; color:var(--text-2); text-transform:uppercase; letter-spacing:.06em; margin-bottom:8px; display:flex; align-items:center; gap:5px; }
.pet-card-title svg { flex-shrink:0; opacity:.7; }
/* Quests */
.quest-item { display:flex; align-items:center; gap:10px; padding:7px 0; border-bottom:1px solid rgba(255,255,255,.05); }
.quest-item:last-child { border-bottom:none; }
.quest-ico { width:18px; height:18px; flex-shrink:0; display:flex; align-items:center; justify-content:center; }
.quest-body { flex:1; }
.quest-label { font-size:.77rem; font-weight:600; }
.quest-bar { height:4px; border-radius:99px; background:rgba(255,255,255,.1); margin-top:4px; overflow:hidden; }
.quest-fill { height:100%; border-radius:99px; background:linear-gradient(90deg,#9B5DE5,#06D6E0); transition:width .6s cubic-bezier(.34,1.56,.64,1); }
.quest-check { width:20px; height:20px; border-radius:50%; border:1.5px solid rgba(255,255,255,.2); display:flex; align-items:center; justify-content:center; font-size:.72rem; color:transparent; flex-shrink:0; transition:all .3s; }
.quest-item.done .quest-check { background:#38D95A; border-color:#38D95A; color:#fff; }
.quest-item.done .quest-label { color:var(--text-2); opacity:.6; }
/* XP bar */
.pet-xp-row { display:flex; justify-content:space-between; align-items:baseline; margin-bottom:6px; }
.pet-xp-lvl { font-family:'Unbounded',sans-serif; font-size:.88rem; font-weight:800; }
.pet-xp-nums { font-size:.74rem; color:var(--text-2); }
.pet-xp-nums span { color:#9B5DE5; font-weight:700; }
.pet-bar { height:8px; border-radius:99px; background:rgba(255,255,255,.1); overflow:hidden; }
.pet-bar-fill { height:100%; border-radius:99px; transition:width .8s cubic-bezier(.34,1.56,.64,1); }
/* Hunger */
.pet-hunger-row { display:flex; align-items:center; gap:8px; }
.pet-hunger { display:flex; gap:4px; }
.pet-hunger-heart { width:18px; height:18px; flex-shrink:0; }
.pet-hunger-heart.filled { filter:drop-shadow(0 0 4px currentColor); }
.pet-hunger-heart.empty { opacity:.18; }
.pet-hunger-msg { font-size:.76rem; color:var(--text-2); margin-left:auto; }
/* Mood tip */
.pet-tip { display:flex; gap:10px; align-items:flex-start; }
.pet-tip-ico { width:24px; height:24px; flex-shrink:0; display:flex; align-items:center; justify-content:center; color:var(--text-2); }
.pet-tip-title { font-size:.77rem; font-weight:700; margin-bottom:3px; }
.pet-tip-text { font-size:.74rem; color:var(--text-2); line-height:1.55; }
.pet-tip-forecast { font-size:.72rem; color:#F9C74F; margin-top:5px; font-weight:600; display:none; }
/* ── Bottom ── */
.pet-bottom { display:grid; grid-template-columns:1fr 1fr; gap:14px; }
.pet-bottom-left, .pet-bottom-right { display:flex; flex-direction:column; gap:14px; }
/* XP chart */
.pet-chart-title { font-size:.67rem; font-weight:800; color:var(--text-2); text-transform:uppercase; letter-spacing:.06em; margin-bottom:12px; display:flex; align-items:center; gap:5px; }
.chart-bars { display:flex; gap:5px; align-items:flex-end; height:72px; }
.chart-col { display:flex; flex-direction:column; align-items:center; gap:3px; flex:1; }
.chart-val { font-size:.58rem; color:var(--text-2); font-weight:600; height:13px; display:flex; align-items:flex-end; }
.chart-bar-wrap { width:100%; flex:1; display:flex; align-items:flex-end; }
.chart-fill { width:100%; border-radius:4px 4px 0 0; background:linear-gradient(180deg,#9B5DE5,rgba(155,93,229,.35)); min-height:3px; transition:height .6s ease; }
.chart-col.today .chart-fill { background:linear-gradient(180deg,#06D6E0,rgba(6,214,224,.35)); }
.chart-day { font-size:.62rem; color:var(--text-2); font-weight:600; white-space:nowrap; }
.chart-col.today .chart-day { color:#06D6E0; font-weight:800; }
/* Activity feed */
.pet-feed-title { font-size:.67rem; font-weight:800; color:var(--text-2); text-transform:uppercase; letter-spacing:.06em; margin-bottom:10px; display:flex; align-items:center; gap:5px; }
.pet-feed-item { display:flex; align-items:center; gap:10px; padding:5px 0; border-bottom:1px solid rgba(255,255,255,.05); }
.pet-feed-item:last-child { border-bottom:none; }
.pet-feed-xp { font-family:'Unbounded',sans-serif; font-size:.76rem; font-weight:800; color:#38D95A; min-width:36px; }
.pet-feed-lbl { font-size:.76rem; font-weight:600; flex:1; }
.pet-feed-time { font-size:.68rem; color:var(--text-2); white-space:nowrap; }
.pet-feed-empty { font-size:.8rem; color:var(--text-2); text-align:center; padding:18px 0; }
/* Quick links */
.pet-quick-title { font-size:.67rem; font-weight:800; color:var(--text-2); text-transform:uppercase; letter-spacing:.06em; margin-bottom:10px; display:flex; align-items:center; gap:5px; }
.pet-quick-grid { display:grid; grid-template-columns:1fr 1fr; gap:7px; }
.pet-quick-card { background:rgba(255,255,255,.04); border:1.5px solid rgba(255,255,255,.07); border-radius:11px; padding:9px 11px; display:flex; align-items:center; gap:8px; text-decoration:none; color:inherit; transition:all .15s; }
.pet-quick-card:hover { background:rgba(255,255,255,.08); transform:translateY(-1px); }
.pet-quick-ico { width:30px; height:30px; border-radius:8px; display:flex; align-items:center; justify-content:center; flex-shrink:0; }
.pet-quick-name { font-size:.76rem; font-weight:700; }
.pet-quick-xp { font-size:.64rem; color:var(--text-2); }
/* B2 — Scene backgrounds (vivid) */
.pet-scene.bg-space { background:radial-gradient(ellipse at 25% 20%,rgba(155,93,229,.75) 0%,transparent 40%),radial-gradient(ellipse at 80% 75%,rgba(80,50,220,.55) 0%,transparent 38%),radial-gradient(ellipse at 55% 55%,rgba(20,5,90,.85) 0%,transparent 70%),linear-gradient(170deg,#01010e,#060228,#0a0318) !important; }
.pet-scene.bg-forest { background:radial-gradient(ellipse at 50% 100%,rgba(56,217,90,.8) 0%,transparent 50%),radial-gradient(ellipse at 15% 60%,rgba(10,160,40,.5) 0%,transparent 40%),radial-gradient(ellipse at 85% 35%,rgba(80,200,60,.35) 0%,transparent 35%),linear-gradient(170deg,#010701,#041203,#082007) !important; }
.pet-scene.bg-aqua { background:radial-gradient(ellipse at 50% 100%,rgba(6,214,224,.8) 0%,transparent 50%),radial-gradient(ellipse at 80% 25%,rgba(6,170,220,.5) 0%,transparent 40%),radial-gradient(ellipse at 20% 50%,rgba(0,100,180,.4) 0%,transparent 35%),linear-gradient(170deg,#010810,#021422,#03203a) !important; }
.pet-scene.bg-sunset { background:radial-gradient(ellipse at 50% 95%,rgba(249,100,20,.9) 0%,transparent 50%),radial-gradient(ellipse at 30% 50%,rgba(249,160,20,.55) 0%,transparent 40%),radial-gradient(ellipse at 75% 30%,rgba(200,50,10,.45) 0%,transparent 35%),linear-gradient(170deg,#100201,#250602,#1e0404) !important; }
/* 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} }
/* 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); } }
@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">
<div class="sb-brand">
<a href="/dashboard" class="sb-logo"><span class="sb-lbl">Learn<span>Space</span></span></a>
<button class="sb-toggle" title="Свернуть меню"><i data-lucide="chevron-left" class="sb-icon"></i></button>
</div>
<nav class="sb-nav">
<button class="sb-link" onclick="lsSearchOpen()" title="Ctrl+K"><i data-lucide="search" class="sb-icon"></i><span class="sb-lbl">Поиск</span></button>
<a href="/dashboard" class="sb-link"><i data-lucide="home" class="sb-icon"></i><span class="sb-lbl">Дашборд</span></a>
<a href="/board" class="sb-link" id="sbl-board" style="display:none"><i data-lucide="layout-dashboard" class="sb-icon"></i><span class="sb-lbl">Доска</span></a>
<a href="/classes" class="sb-link" id="sbl-classes" style="display:none"><i data-lucide="graduation-cap" class="sb-icon"></i><span class="sb-lbl">Классы</span></a>
<a href="/library" class="sb-link"><i data-lucide="book-open" class="sb-icon"></i><span class="sb-lbl">Библиотека</span></a>
<a href="/theory" class="sb-link"><i data-lucide="brain" class="sb-icon"></i><span class="sb-lbl">Теория</span></a>
<a href="/lab" class="sb-link"><i data-lucide="atom" class="sb-icon"></i><span class="sb-lbl">Лаборатория</span></a>
<a href="/biochem" class="sb-link"><i data-lucide="flask-conical" class="sb-icon"></i><span class="sb-lbl">Биохимия</span></a>
<a href="/hangman" class="sb-link"><i data-lucide="gamepad-2" class="sb-icon"></i><span class="sb-lbl">Виселица</span></a>
<a href="/crossword" class="sb-link"><i data-lucide="grid-3x3" class="sb-icon"></i><span class="sb-lbl">Кроссворд</span></a>
<span class="sb-link active"><i data-lucide="heart" class="sb-icon"></i><span class="sb-lbl">Питомец</span></span>
<a href="/collection" class="sb-link"><i data-lucide="layers" class="sb-icon"></i><span class="sb-lbl">Коллекция</span></a>
<a href="/knowledge-map" class="sb-link"><i data-lucide="share-2" class="sb-icon"></i><span class="sb-lbl">Карта знаний</span></a>
<a href="/red-book.html" class="sb-link"><i data-lucide="leaf" class="sb-icon"></i><span class="sb-lbl">Красная книга</span></a>
<a href="/classroom" class="sb-link"><i data-lucide="presentation" class="sb-icon"></i><span class="sb-lbl">Онлайн-урок</span></a>
<div class="sb-divider"></div>
<a href="/analytics" class="sb-link sb-teacher-only" style="display:none"><i data-lucide="bar-chart-2" class="sb-icon"></i><span class="sb-lbl">Аналитика</span></a>
<a href="/question-bank" class="sb-link sb-teacher-only" style="display:none"><i data-lucide="database" class="sb-icon"></i><span class="sb-lbl">Банк вопросов</span></a>
<a href="/live-quiz" class="sb-link sb-teacher-only" style="display:none"><i data-lucide="radio" class="sb-icon"></i><span class="sb-lbl">Live-квиз</span></a>
<a href="/gradebook" class="sb-link sb-teacher-only" style="display:none"><i data-lucide="table" class="sb-icon"></i><span class="sb-lbl">Журнал</span></a>
<a href="/admin" class="sb-link" id="sbl-admin" style="display:none"><i data-lucide="settings" class="sb-icon"></i><span class="sb-lbl">Управление</span></a>
</nav>
<div style="padding:4px 2px">
<div id="notif-wrap">
<button class="sb-link" id="notif-btn" onclick="LS.notif.toggle()">
<i data-lucide="bell" class="sb-icon"></i><span class="sb-lbl">Уведомления</span>
<span class="sb-badge" id="notif-badge" style="display:none"></span>
</button>
</div>
</div>
<div class="sb-foot">
<a href="/profile" class="sb-user-row" style="text-decoration:none">
<div class="sb-avatar" id="nav-avatar">?</div>
<div class="sb-user-info">
<div class="sb-user-name" id="nav-user"></div>
<span class="sb-logout" style="pointer-events:none">Мой профиль</span>
</div>
</a>
</div>
</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>
<!-- Color picker + shop -->
<div class="pet-color-row">
<div class="pet-color-lbl">Цвет</div>
<div class="pet-color-picker" id="color-picker"></div>
<button class="pet-btn" onclick="openPetShop()" style="font-size:.62rem;padding:3px 9px;margin-left:2px;display:inline-flex;align-items:center;gap:4px" title="Фоны сцены">
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m2 7 4.41-4.41A2 2 0 0 1 7.83 2h8.34a2 2 0 0 1 1.42.59L22 7"/><path d="M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8"/><path d="M15 22v-4a2 2 0 0 0-2-2h-2a2 2 0 0 0-2 2v4"/><path d="M2 7h20"/><path d="M22 7v3a2 2 0 0 1-2 2a2.7 2.7 0 0 1-1.59-.63.7.7 0 0 0-.82 0A2.7 2.7 0 0 1 16 12a2.7 2.7 0 0 1-1.59-.63.7.7 0 0 0-.82 0A2.7 2.7 0 0 1 12 12a2.7 2.7 0 0 1-1.59-.63.7.7 0 0 0-.82 0A2.7 2.7 0 0 1 8 12a2.7 2.7 0 0 1-1.59-.63.7.7 0 0 0-.82 0A2.7 2.7 0 0 1 4 12a2 2 0 0 1-2-2V7"/></svg>
Фоны
</button>
</div>
<div class="pet-accessories" id="pet-accessories"></div>
<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>
<!-- ── 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="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',
};
const PALETTE_LABELS = {
purple:'Фиолетовый', cyan:'Голубой', gold:'Золотой',
red:'Красный', green:'Зелёный', blue:'Синий',
};
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) {
document.getElementById('nav-avatar').textContent =
(user.name||'LS').split(' ').slice(0,2).map(w=>w[0]?.toUpperCase()||'').join('')||'LS';
document.getElementById('nav-user').textContent = user.name || '—';
if (user.role === 'admin') document.getElementById('sbl-admin').style.display = '';
LS.showBoardIfAllowed();
if (user.role !== 'student') {
document.getElementById('sbl-classes').style.display = '';
}
}
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();
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);
}
}
}
/* ── 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);
}
}
}
/* ── 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)',
};
const BG_NAMES = { default:'Стандарт', space:'Космос', forest:'Лес', aqua:'Океан', sunset:'Закат' };
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
const scene = document.getElementById('pet-scene');
scene.className = scene.className.replace(/\bbg-\S+/g, '') + (id !== 'default' ? ` bg-${id}` : '');
if (_petData) _petData.petBg = id;
applyBgFX(id);
// 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);
}
}
/* ── 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);
// 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;
if (d.petBg && d.petBg !== 'default') scene.classList.add(`bg-${d.petBg}`);
applyWeather(d.mood);
applyBgFX(d.petBg || 'default');
// 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');
// Accessories
const accEl = document.getElementById('pet-accessories');
const ACC = { hat:'Шляпа', glasses:'Очки', crown:'Корона', star:'Звезда' };
accEl.innerHTML = d.accessories.length
? d.accessories.map(a => `<span class="pet-acc-badge">${ACC[a]||a}</span>`).join('') : '';
// 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;
document.getElementById('pet-svg-wrap').innerHTML =
renderPetSVG(_petData.petLevel, _petData.mood, _petData.accessories, colorKey);
document.querySelectorAll('.pet-color-dot').forEach(d =>
d.classList.toggle('active', d.dataset.color === colorKey)
);
}
/* ── Petting ── */
async function petThePet() {
if (_petCooldown) return;
const btn = document.getElementById('btn-pet');
btn.disabled = true;
const res = await LS.api('/api/pet/pet', {method:'POST'}).catch(() => null);
if (!res?.ok) {
if (res?.remaining) startCooldown(res.remaining);
else btn.disabled = false;
return;
}
// Animate pet
const wrap = document.getElementById('pet-svg-wrap');
wrap.classList.add('petting');
setTimeout(() => wrap.classList.remove('petting'), 600);
// Scatter hearts
spawnHearts();
// Update counters
if (_petData) {
_petData.pettingStreak = res.pettingStreak;
_petData.coins = (_petData.coins || 0) + res.coins;
document.getElementById('stat-coins').textContent = _petData.coins;
document.getElementById('stat-petting').textContent = res.pettingStreak + ' дн.';
}
// B4: coin float
floatLabel(`+${res.coins} монет`, '#F9C74F');
// Happy bubble
document.getElementById('pet-bubble').textContent = HAPPY_PHRASES[Math.floor(Math.random()*HAPPY_PHRASES.length)];
startCooldown(60);
}
function startCooldown(secs) {
_petCooldown = true;
const btn = document.getElementById('btn-pet');
btn.disabled = true;
btn.innerHTML = `${SVG_CLOCK} ${secs}с`;
clearInterval(_petCooldownTimer);
_petCooldownTimer = setInterval(() => {
secs--;
if (secs <= 0) {
clearInterval(_petCooldownTimer);
btn.disabled = false;
btn.innerHTML = `${SVG_HAND} Погладить`;
_petCooldown = false;
if (_petData) {
const lines = BUBBLES[_petData.mood] || BUBBLES.neutral;
document.getElementById('pet-bubble').textContent = lines[Math.floor(Math.random()*lines.length)];
}
} else {
btn.innerHTML = `${SVG_CLOCK} ${secs}с`;
}
}, 1000);
}
/* ── Feed mini-game ──────────────────────────────────────────────────── */
const FEED_QUESTIONS = [
{ q: 'Химическая формула воды', a: 'H₂O', opts: ['H₂O', 'CO₂', 'NaCl', 'H₂SO₄'] },
{ q: 'Формула углекислого газа', a: 'CO₂', opts: ['CO₂', 'O₂', 'CH₄', 'N₂O'] },
{ q: 'Молекула кислорода', a: 'O₂', opts: ['O₂', 'O₃', 'H₂O₂', 'OH'] },
{ q: 'Хлорид натрия (поваренная соль)', a: 'NaCl', opts: ['NaCl', 'KCl', 'CaCl₂', 'MgCl₂'] },
{ q: 'Серная кислота', a: 'H₂SO₄', opts: ['H₂SO₄', 'HCl', 'HNO₃', 'H₃PO₄'] },
{ q: 'Аммиак', a: 'NH₃', opts: ['NH₃', 'NO₂', 'N₂H₄', 'NH₄OH'] },
{ q: 'Глюкоза', a: 'C₆H₁₂O₆', opts: ['C₆H₁₂O₆', 'C₁₂H₂₂O₁₁', 'C₅H₁₀O₅', 'C₂H₅OH'] },
{ q: 'Метан (природный газ)', a: 'CH₄', opts: ['CH₄', 'C₂H₆', 'C₃H₈', 'C₄H₁₀'] },
{ q: 'Озон', a: 'O₃', opts: ['O₃', 'O₂', 'O₄', 'OH'] },
{ q: 'Перекись водорода', a: 'H₂O₂', opts: ['H₂O₂', 'H₂O', 'H₂SO₄', 'HOH'] },
{ q: 'Этиловый спирт', a: 'C₂H₅OH', opts: ['C₂H₅OH', 'CH₃OH', 'C₃H₇OH', 'C₄H₉OH'] },
{ q: 'Соляная кислота', a: 'HCl', opts: ['HCl', 'HBr', 'HF', 'HI'] },
{ q: 'Гидроксид натрия (щёлочь)', a: 'NaOH', opts: ['NaOH', 'KOH', 'Ca(OH)₂', 'Mg(OH)₂'] },
{ q: 'АТФ — основа энергии клетки', a: 'C₁₀H₁₆N₅O₁₃P₃', opts: ['C₁₀H₁₆N₅O₁₃P₃', 'C₅H₁₀O₅', 'C₆H₁₂O₆', 'C₃H₇NO₂'] },
{ q: 'Азотная кислота', a: 'HNO₃', opts: ['HNO₃', 'H₂SO₄', 'HCl', 'H₃PO₄'] },
{ q: 'Ацетилсалициловая кислота (аспирин)', a: 'C₉H₈O₄', opts: ['C₉H₈O₄', 'C₇H₆O₃', 'C₁₁H₁₂O₄', 'C₈H₉NO₂'] },
];
let _feedTimerInterval = null, _feedTimerSecs = 0, _feedAnswered = false;
let _feedCooldownTimer = null;
function openFeedGame() {
const btn = document.getElementById('btn-feed');
if (btn.disabled) return;
if (_petData && _petData.feedCooldown > 0) { startFeedCooldown(_petData.feedCooldown); return; }
_feedAnswered = false;
const q = FEED_QUESTIONS[Math.floor(Math.random() * FEED_QUESTIONS.length)];
document.getElementById('feed-q-text').textContent = q.q;
document.getElementById('feed-result').style.display = 'none';
// shuffle opts
const opts = [...q.opts].sort(() => Math.random() - 0.5);
const optsEl = document.getElementById('feed-opts');
optsEl.innerHTML = opts.map(o => `
<button onclick="feedAnswer(this,${JSON.stringify(o).replace(/"/g,'&quot;')},${JSON.stringify(q.a).replace(/"/g,'&quot;')})"
style="padding:10px 14px;border:1.5px solid var(--border-h);border-radius:12px;background:transparent;font-family:'Manrope',sans-serif;font-size:.88rem;font-weight:600;color:var(--text);cursor:pointer;text-align:left;transition:all .2s"
onmouseover="this.style.borderColor='var(--violet)'" onmouseout="this.style.borderColor='var(--border-h)'">
${escHtml(o)}
</button>`).join('');
const overlay = document.getElementById('feed-overlay');
overlay.style.display = 'flex';
// timer: 15 seconds
_feedTimerSecs = 15;
document.getElementById('feed-timer-fill').style.width = '100%';
clearInterval(_feedTimerInterval);
_feedTimerInterval = setInterval(() => {
_feedTimerSecs--;
document.getElementById('feed-timer-fill').style.width = (_feedTimerSecs / 15 * 100) + '%';
if (_feedTimerSecs <= 0) {
clearInterval(_feedTimerInterval);
if (!_feedAnswered) showFeedResult(false, null);
}
}, 1000);
}
function closeFeedGame() {
clearInterval(_feedTimerInterval);
document.getElementById('feed-overlay').style.display = 'none';
}
async function feedAnswer(btn, chosen, correct) {
if (_feedAnswered) return;
_feedAnswered = true;
clearInterval(_feedTimerInterval);
document.getElementById('feed-opts').querySelectorAll('button').forEach(b => b.disabled = true);
const isRight = chosen === correct;
if (isRight) {
btn.style.cssText += ';border-color:#38D95A;background:rgba(56,217,90,.1);color:#38D95A';
const res = await LS.api('/api/pet/feed', {method:'POST'}).catch(() => null);
if (res?.ok) {
if (_petData) { _petData.xp = res.xp; _petData.coins = res.coins; _petData.feedCooldown = 1800; }
showFeedResult(true, res.xpAwarded || 15);
} else if (res?.remaining) {
showFeedResult(null, 0, 'Подожди ' + Math.ceil(res.remaining / 60) + ' мин. — питомец ещё сыт!');
startFeedCooldown(res.remaining);
} else {
showFeedResult(true, 15);
}
} else {
btn.style.cssText += ';border-color:#F94144;background:rgba(249,65,68,.08);color:#F94144';
// Highlight correct
document.getElementById('feed-opts').querySelectorAll('button').forEach(b => {
if (b.textContent.trim() === correct) b.style.cssText += ';border-color:#38D95A;background:rgba(56,217,90,.1);color:#38D95A';
});
showFeedResult(false, null);
}
}
function showFeedResult(correct, xp, customMsg) {
const el = document.getElementById('feed-result');
if (correct === null) {
el.style.cssText = 'display:block;padding:12px 14px;border-radius:12px;background:rgba(155,93,229,.1);border:1.5px solid rgba(155,93,229,.25);font-size:.87rem;font-weight:700;text-align:center;color:#9B5DE5';
el.textContent = customMsg || 'Время вышло!';
} else if (correct) {
el.style.cssText = 'display:block;padding:12px 14px;border-radius:12px;background:rgba(56,217,90,.1);border:1.5px solid rgba(56,217,90,.25);font-size:.87rem;font-weight:700;text-align:center;color:#38D95A';
el.textContent = `Правильно! +${xp} XP — питомец доволен!`;
floatLabel(`+${xp} XP`, '#38D95A');
document.getElementById('pet-bubble').textContent = '😋 Вкуснятина!';
setTimeout(() => { closeFeedGame(); startFeedCooldown(1800); }, 1800);
} else {
el.style.cssText = 'display:block;padding:12px 14px;border-radius:12px;background:rgba(249,65,68,.08);border:1.5px solid rgba(249,65,68,.2);font-size:.87rem;font-weight:700;text-align:center;color:#F94144';
el.textContent = 'Неверно — питомец остался голодным 😢';
setTimeout(closeFeedGame, 1800);
}
}
const SVG_FOOD = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#F9C74F" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 2v7c0 1.1.9 2 2 2h4a2 2 0 0 0 2-2V2"/><path d="M7 2v20"/><path d="M21 15V2v0a5 5 0 0 0-5 5v6c0 1.1.9 2 2 2h3Zm0 0v7"/></svg>`;
function startFeedCooldown(secs) {
const btn = document.getElementById('btn-feed');
btn.disabled = true;
btn.innerHTML = `${SVG_CLOCK} ${Math.ceil(secs / 60)}м`;
clearInterval(_feedCooldownTimer);
_feedCooldownTimer = setInterval(() => {
secs -= 10;
if (secs <= 0) {
clearInterval(_feedCooldownTimer);
btn.disabled = false;
btn.innerHTML = `${SVG_FOOD} Покормить`;
if (_petData) _petData.feedCooldown = 0;
} else {
btn.innerHTML = `${SVG_CLOCK} ${Math.ceil(secs / 60)}м`;
}
}, 10000);
}
/* ── Scatter hearts (petting) ── */
function spawnHearts() {
const scene = document.getElementById('pet-scene');
const hearts = [
'<svg width="1em" height="1em" viewBox="0 0 24 24" fill="#F94144" stroke="none"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/></svg>',
'<svg width="1em" height="1em" viewBox="0 0 24 24" fill="#FF6B9D" stroke="none"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/></svg>',
'<svg width="1em" height="1em" viewBox="0 0 24 24" fill="#F9C74F" stroke="none"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/></svg>',
'<svg width="1em" height="1em" viewBox="0 0 24 24" fill="#9B5DE5" stroke="none"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/></svg>',
'<svg width="1em" height="1em" viewBox="0 0 24 24" fill="#F94144" stroke="none"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/></svg>',
'<svg width="1em" height="1em" viewBox="0 0 24 24" fill="#06D6A0" stroke="none"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/></svg>',
];
for (let i = 0; i < 5; i++) {
setTimeout(() => {
const el = document.createElement('div');
const angle = (i / 5) * Math.PI * 2 + Math.random() * 0.5;
const dist = 38 + Math.random() * 28;
const tx = Math.cos(angle) * dist;
const ty = Math.sin(angle) * dist - 20;
el.style.cssText = `
position:absolute; font-size:${1+Math.random()*.5}rem;
top:50%; left:50%; margin:-10px 0 0 -10px;
pointer-events:none; z-index:10;
--tx:${tx}px; --ty:${ty}px;
animation:floatHeart 1s ease-out forwards;`;
el.innerHTML = hearts[i % hearts.length];
scene.appendChild(el);
setTimeout(() => el.remove(), 1100);
}, i * 110);
}
}
/* ── Evolution burst ── */
function triggerEvoBurst(newLevel) {
const stage = document.getElementById('pet-stage');
const scene = document.getElementById('pet-scene');
stage.classList.add('evo-burst');
setTimeout(() => stage.classList.remove('evo-burst'), 1000);
const sparkles = [
'<svg width="1em" height="1em" viewBox="0 0 24 24" fill="#F9C74F" stroke="none"><path d="m12 3-1.9 5.8a2 2 0 0 1-1.3 1.3L3 12l5.8 1.9a2 2 0 0 1 1.3 1.3L12 21l1.9-5.8a2 2 0 0 1 1.3-1.3L21 12l-5.8-1.9a2 2 0 0 1-1.3-1.3z"/></svg>',
'<svg width="1em" height="1em" viewBox="0 0 24 24" fill="#FFD166" stroke="none"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>',
'<svg width="1em" height="1em" viewBox="0 0 24 24" fill="#9B5DE5" stroke="none"><path d="m12 3-1.9 5.8a2 2 0 0 1-1.3 1.3L3 12l5.8 1.9a2 2 0 0 1 1.3 1.3L12 21l1.9-5.8a2 2 0 0 1 1.3-1.3L21 12l-5.8-1.9a2 2 0 0 1-1.3-1.3z"/></svg>',
'<svg width="1em" height="1em" viewBox="0 0 24 24" fill="#06D6E0" stroke="none"><path d="m12 3-1.9 5.8a2 2 0 0 1-1.3 1.3L3 12l5.8 1.9a2 2 0 0 1 1.3 1.3L12 21l1.9-5.8a2 2 0 0 1 1.3-1.3L21 12l-5.8-1.9a2 2 0 0 1-1.3-1.3z"/></svg>',
'<svg width="1em" height="1em" viewBox="0 0 24 24" fill="#F9C74F" stroke="none"><path d="m12 3-1.9 5.8a2 2 0 0 1-1.3 1.3L3 12l5.8 1.9a2 2 0 0 1 1.3 1.3L12 21l1.9-5.8a2 2 0 0 1 1.3-1.3L21 12l-5.8-1.9a2 2 0 0 1-1.3-1.3z"/></svg>',
'<svg width="1em" height="1em" viewBox="0 0 24 24" fill="#FFD166" stroke="none"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>',
];
for (let i = 0; i < 10; i++) {
setTimeout(() => {
const el = document.createElement('div');
const angle = (i / 10) * Math.PI * 2 + Math.random() * 0.3;
const dist = 55 + Math.random() * 35;
el.style.cssText = `
position:absolute; font-size:${0.9+Math.random()*.5}rem;
top:50%; left:50%; margin:-10px 0 0 -10px;
pointer-events:none; z-index:20;
--tx:${Math.cos(angle)*dist}px; --ty:${Math.sin(angle)*dist}px;
animation:floatHeart 1.3s ease-out forwards;`;
el.innerHTML = sparkles[i % sparkles.length];
scene.appendChild(el);
setTimeout(() => el.remove(), 1400);
}, i * 70);
}
}
/* ── Rename ── */
function toggleRename() {
const form = document.getElementById('rename-form');
if (form.classList.contains('visible')) {
form.classList.remove('visible');
} else {
document.getElementById('rename-input').value = _petData?.petName || '';
form.classList.add('visible');
document.getElementById('rename-input').focus();
}
}
async function saveName() {
const name = document.getElementById('rename-input').value.trim();
if (!name) return;
const res = await LS.api('/api/pet/name', {method:'PATCH', body:{name}}).catch(()=>null);
if (res?.ok) {
document.getElementById('pet-name').textContent = name;
if (_petData) _petData.petName = name;
document.getElementById('rename-form').classList.remove('visible');
}
}
/* ── Helpers ── */
function escHtml(s) { return (s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }
function fmtAgo(dateStr) {
if (!dateStr) return '';
const diff = (Date.now() - new Date(dateStr)) / 1000;
if (diff < 60) return 'только что';
if (diff < 3600) return Math.floor(diff/60) + ' мин.';
if (diff < 86400) return Math.floor(diff/3600) + ' ч.';
const d = Math.floor(diff/86400);
return d === 1 ? 'вчера' : d + ' дн.';
}
function shadeColor(hex, pct) {
const n = parseInt(hex.slice(1), 16);
const r = Math.min(255,Math.max(0,(n>>16)+pct));
const g = Math.min(255,Math.max(0,((n>>8)&0xff)+pct));
const b = Math.min(255,Math.max(0,(n&0xff)+pct));
return `rgb(${r},${g},${b})`;
}
/* ── Pet SVG renderer ── */
function renderPetSVG(level, mood, accessories = [], colorKey = 'purple', streak = 0) {
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 ── */
let accSvg = '';
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('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('crown') || level >= 5) {
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('star') && level < 5) {
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"/>`;
}
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>
</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})"/>
${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>