diff --git a/backend/src/controllers/petController.js b/backend/src/controllers/petController.js index 8cb3b63..171a416 100644 --- a/backend/src/controllers/petController.js +++ b/backend/src/controllers/petController.js @@ -37,6 +37,9 @@ const BG_SHOP = [ { id:'forest', name:'Лес', price:50, desc:'Таинственный лес' }, { id:'aqua', name:'Океан', price:75, desc:'Морская глубина' }, { id:'sunset', name:'Закат', price:75, desc:'Пурпурный закат' }, + { id:'aurora', name:'Сияние', price:100, desc:'Северное сияние' }, + { id:'candy', name:'Леденец', price:100, desc:'Сладкие облака' }, + { id:'sakura', name:'Сакура', price:125, desc:'Цветущая вишня' }, ]; function _parseOwned(raw) { @@ -67,16 +70,35 @@ function _mood(streak, daysSinceLogin) { /* ── Гардероб: каталог аксессуаров (разблокировка по порогам/бесплатно) ── slot — на каждый слот можно надеть только один предмет. */ const ACCESSORY_CATALOG = [ + // голова { id: 'grad', name: 'Шапочка выпускника', slot: 'head', unlock: { type: 'free' } }, + { id: 'party', name: 'Праздничный колпак', slot: 'head', unlock: { type: 'free' } }, { id: 'hat', name: 'Цилиндр', slot: 'head', unlock: { type: 'streak', value: 7 } }, { id: 'crown', name: 'Корона', slot: 'head', unlock: { type: 'xp', value: 5000 } }, + // лицо { id: 'glasses', name: 'Очки', slot: 'face', unlock: { type: 'level', value: 5 } }, + { id: 'sunglasses', name: 'Тёмные очки', slot: 'face', unlock: { type: 'free' } }, + // шея { id: 'bowtie', name: 'Бабочка', slot: 'neck', unlock: { type: 'free' } }, + { id: 'scarf', name: 'Шарф', slot: 'neck', unlock: { type: 'free' } }, + // уши { id: 'headphones', name: 'Наушники', slot: 'ears', unlock: { type: 'free' } }, + // акцент + { id: 'flower', name: 'Цветок', slot: 'accent', unlock: { type: 'free' } }, { id: 'star', name: 'Звезда', slot: 'accent', unlock: { type: 'level', value: 10 } }, ]; const ACC_BY_ID = new Map(ACCESSORY_CATALOG.map(a => [a.id, a])); +// Узоры тела (новая ось кастомизации). Все бесплатны. +const PET_PATTERNS = [ + { id: 'none', name: 'Без узора' }, + { id: 'spots', name: 'Пятнышки' }, + { id: 'stripes', name: 'Полоски' }, + { id: 'gradient', name: 'Градиент' }, + { id: 'galaxy', name: 'Галактика' }, +]; +const PATTERN_IDS = new Set(PET_PATTERNS.map(p => p.id)); + function _accUnlocked(u, a) { const k = a.unlock; if (k.type === 'free') return true; @@ -130,7 +152,7 @@ function getPet(req, res) { const user = db.prepare( `SELECT xp, level, streak_current, streak_best, streak_date, coins, pet_name, last_login, pet_color, pet_last_petted, pet_petting_streak, - pet_bg, pet_bg_owned, pet_last_fed, pet_equipped + pet_bg, pet_bg_owned, pet_last_fed, pet_equipped, pet_pattern FROM users WHERE id = ?` ).get(req.user.id); @@ -205,6 +227,8 @@ function getPet(req, res) { petName: user.pet_name || 'Квантик', petLevel: petLvl, petColor: user.pet_color || 'purple', + petPattern: user.pet_pattern || 'none', + patterns: PET_PATTERNS, mood, daysSinceLogin: daysSince, accessories, @@ -278,6 +302,14 @@ function updateColor(req, res) { res.json({ ok: true, color }); } +/* ── PATCH /api/pet/pattern — узор тела ───────────────────────────────── */ +function updatePattern(req, res) { + const pattern = req.body && req.body.pattern; + if (!PATTERN_IDS.has(pattern)) return res.status(400).json({ error: 'invalid pattern' }); + db.prepare('UPDATE users SET pet_pattern = ? WHERE id = ?').run(pattern, req.user.id); + res.json({ ok: true, pattern }); +} + /* ── PATCH /api/pet/equip — надеть/снять аксессуары (гардероб) ─────────── */ function equipAccessories(req, res) { const list = req.body && req.body.equipped; @@ -376,4 +408,4 @@ function feedPet(req, res) { res.json({ ok: true, xpAwarded: 15, xp: updated.xp, coins: updated.coins }); } -module.exports = { getPet, renamePet, petAction, updateColor, starCatch, getShop, buyBg, setBg, feedPet, equipAccessories }; +module.exports = { getPet, renamePet, petAction, updateColor, starCatch, getShop, buyBg, setBg, feedPet, equipAccessories, updatePattern }; diff --git a/backend/src/db/migrations/066_pet_pattern.sql b/backend/src/db/migrations/066_pet_pattern.sql new file mode 100644 index 0000000..f9dac57 --- /dev/null +++ b/backend/src/db/migrations/066_pet_pattern.sql @@ -0,0 +1,2 @@ +-- Узор тела питомца (none/spots/stripes/gradient/galaxy). NULL = none. +ALTER TABLE users ADD COLUMN pet_pattern TEXT; diff --git a/backend/src/routes/pet.js b/backend/src/routes/pet.js index 4f9a1c8..c6ece1d 100644 --- a/backend/src/routes/pet.js +++ b/backend/src/routes/pet.js @@ -7,6 +7,7 @@ router.patch('/name', authMiddleware, c.renamePet); router.post('/pet', authMiddleware, c.petAction); router.patch('/color', authMiddleware, c.updateColor); router.patch('/equip', authMiddleware, c.equipAccessories); +router.patch('/pattern', authMiddleware, c.updatePattern); router.post('/star', authMiddleware, c.starCatch); router.get('/shop', authMiddleware, c.getShop); router.post('/shop/buy', authMiddleware, c.buyBg); diff --git a/frontend/js/pet-sprite.js b/frontend/js/pet-sprite.js index b1627af..7264c49 100644 --- a/frontend/js/pet-sprite.js +++ b/frontend/js/pet-sprite.js @@ -18,7 +18,7 @@ return `rgb(${r},${g},${b})`; } - function renderPetSVG(level, mood, accessories = [], colorKey = 'purple', streak = 0) { + function renderPetSVG(level, mood, accessories = [], colorKey = 'purple', streak = 0, pattern = 'none') { const col = PET_PALETTES[colorKey] || '#9B5DE5'; const dark = shadeColor(col, -45); const light = shadeColor(col, 52); @@ -143,6 +143,26 @@ /* ── Accessories (гардероб: строго по equipped-списку, без авто по уровню) ── */ let accSvg = ''; + if (accessories.includes('party')) { + accSvg += ` + + + `; + } + if (accessories.includes('sunglasses')) { + accSvg += ` + + + + `; + } + if (accessories.includes('scarf')) { + accSvg += ` + `; + } + if (accessories.includes('flower')) { + accSvg += ``; + } if (accessories.includes('headphones')) { accSvg += ` @@ -289,6 +309,17 @@ shimmer = ``; } + // Узор тела (клипуется по силуэту) + let patternSvg = ''; + if (pattern && pattern !== 'none') { + let pin = ''; + if (pattern === 'spots') pin = ``; + else if (pattern === 'stripes') pin = `${[-10,8,26,44,62,80,98].map(x => ``).join('')}`; + else if (pattern === 'galaxy') pin = ``; + else if (pattern === 'gradient') pin = ``; + patternSvg = `${pin}`; + } + return ` @@ -304,6 +335,10 @@ + + + + ${aura} @@ -314,6 +349,7 @@ ${antennae} ${paws} + ${patternSvg} ${shimmer} ${cheeks}${eyebrows}${eyeGroups}${nose}${mouth}${extras} diff --git a/frontend/pet.html b/frontend/pet.html index 7ded3d0..b54c0ed 100644 --- a/frontend/pet.html +++ b/frontend/pet.html @@ -286,6 +286,9 @@ .pet-scene.bg-forest { background:radial-gradient(ellipse at 50% 100%,rgba(56,217,90,.8) 0%,transparent 50%),radial-gradient(ellipse at 15% 60%,rgba(10,160,40,.5) 0%,transparent 40%),radial-gradient(ellipse at 85% 35%,rgba(80,200,60,.35) 0%,transparent 35%),linear-gradient(170deg,#010701,#041203,#082007) !important; } .pet-scene.bg-aqua { background:radial-gradient(ellipse at 50% 100%,rgba(6,214,224,.8) 0%,transparent 50%),radial-gradient(ellipse at 80% 25%,rgba(6,170,220,.5) 0%,transparent 40%),radial-gradient(ellipse at 20% 50%,rgba(0,100,180,.4) 0%,transparent 35%),linear-gradient(170deg,#010810,#021422,#03203a) !important; } .pet-scene.bg-sunset { background:radial-gradient(ellipse at 50% 95%,rgba(249,100,20,.9) 0%,transparent 50%),radial-gradient(ellipse at 30% 50%,rgba(249,160,20,.55) 0%,transparent 40%),radial-gradient(ellipse at 75% 30%,rgba(200,50,10,.45) 0%,transparent 35%),linear-gradient(170deg,#100201,#250602,#1e0404) !important; } + .pet-scene.bg-aurora { background:radial-gradient(ellipse at 30% 25%,rgba(56,217,140,.6) 0%,transparent 42%),radial-gradient(ellipse at 70% 30%,rgba(120,90,255,.55) 0%,transparent 44%),radial-gradient(ellipse at 50% 92%,rgba(6,214,180,.4) 0%,transparent 52%),linear-gradient(170deg,#02040f,#04122a,#0a0426) !important; } + .pet-scene.bg-candy { background:radial-gradient(ellipse at 30% 25%,rgba(255,170,210,.7) 0%,transparent 46%),radial-gradient(ellipse at 75% 70%,rgba(150,200,255,.6) 0%,transparent 46%),linear-gradient(170deg,#3a2540,#52324f,#3a2848) !important; } + .pet-scene.bg-sakura { background:radial-gradient(ellipse at 50% 100%,rgba(255,140,190,.7) 0%,transparent 52%),radial-gradient(ellipse at 25% 30%,rgba(255,180,210,.45) 0%,transparent 42%),linear-gradient(170deg,#1a0a14,#2e1226,#241026) !important; } /* B2 — BgFX particle container + keyframes */ .pet-bgfx { position:absolute; inset:0; pointer-events:none; overflow:hidden; border-radius:50%; z-index:1; } @@ -293,6 +296,7 @@ @keyframes bgFirefly { 0%,100%{opacity:.08;transform:translate(0,0)} 50%{opacity:1;transform:translate(var(--fx,3px),var(--fy,-4px))} } @keyframes bgBubble { 0%{transform:translateY(0) scale(1);opacity:.75} 100%{transform:translateY(-140px) scale(1.5);opacity:0} } @keyframes bgEmber { 0%{transform:translate(0,0);opacity:1} 60%{opacity:.65} 100%{transform:translate(var(--ex,5px),-130px);opacity:0} } + @keyframes bgPetal { 0%{transform:translate(0,0) rotate(0);opacity:.9} 100%{transform:translate(var(--px,12px),190px) rotate(220deg);opacity:0} } /* B2 — Shop modal */ .pet-shop-overlay { position:fixed;inset:0;background:rgba(0,0,0,.65);z-index:1000;display:flex;align-items:center;justify-content:center;backdrop-filter:blur(4px); } @@ -328,6 +332,30 @@ .pc-bg-grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(118px,1fr)); gap:10px; } .pet-evo-legend { font-size:.6rem; color:var(--text-3); margin-top:7px; line-height:1.45; text-align:center; cursor:help; max-width:230px; } + /* Гардероб: панель действий + зоны */ + .wr-bar { display:flex; align-items:center; justify-content:space-between; gap:8px; margin-bottom:12px; flex-wrap:wrap; } + .wr-count { font-size:.74rem; color:var(--text-2); font-weight:700; } + .wr-count b { color:var(--violet); } + .wr-actions { display:flex; gap:7px; flex-wrap:wrap; } + .wr-btn { padding:5px 12px; border-radius:99px; border:1.5px solid var(--border-h); background:transparent; + color:var(--text-2); font:700 .72rem 'Manrope',sans-serif; cursor:pointer; transition:all .15s; } + .wr-btn:hover { border-color:var(--violet); color:var(--text); } + .wr-zone { margin-bottom:11px; } + .wr-zone-lbl { font-size:.6rem; font-weight:800; color:var(--text-3); text-transform:uppercase; letter-spacing:.06em; margin-bottom:6px; } + .wr-chips { display:flex; flex-wrap:wrap; gap:7px; } + /* Узор: свотчи */ + .pc-pattern-grid { display:flex; flex-wrap:wrap; gap:8px; } + .pc-swatch { display:inline-flex; align-items:center; gap:7px; padding:6px 12px 6px 6px; border-radius:99px; + border:1.5px solid var(--border-h); background:transparent; color:var(--text-2); + font:700 .76rem 'Manrope',sans-serif; cursor:pointer; transition:all .15s; } + .pc-swatch:hover { border-color:var(--violet); color:var(--text); } + .pc-swatch.active { border-color:var(--violet); background:rgba(155,93,229,.18); color:#fff; } + .pc-swatch-dot { width:24px; height:24px; border-radius:50%; flex-shrink:0; border:1.5px solid rgba(255,255,255,.18); } + .pc-swatch-dot.pat-none { background:#9B5DE5; } + .pc-swatch-dot.pat-spots { background:radial-gradient(circle at 32% 32%,#5a2da0 2.2px,transparent 2.4px),radial-gradient(circle at 70% 66%,#5a2da0 2.2px,transparent 2.4px),#9B5DE5; } + .pc-swatch-dot.pat-stripes { background:repeating-linear-gradient(45deg,#9B5DE5 0 4px,#5a2da0 4px 7px); } + .pc-swatch-dot.pat-gradient{ background:linear-gradient(180deg,#c9a6ec,#5a2da0); } + .pc-swatch-dot.pat-galaxy { background:radial-gradient(circle at 62% 40%,#1a0a3a 55%,#9B5DE5); } @media(max-width:768px) { .pet-hero { grid-template-columns:1fr; } @@ -483,9 +511,10 @@
Кастомизация
- - - + + + +
Нажми, чтобы надеть или снять. Заблокированные открываются за достижения. По одному предмету на зону (голова / лицо / шея / уши / акцент).
@@ -495,6 +524,10 @@
Основной цвет питомца.
+