364 lines
31 KiB
HTML
364 lines
31 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="ru">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=yes, viewport-fit=cover">
|
||
<title>Апгрейд — CS2 Simulator</title>
|
||
<link rel="stylesheet" href="/static/css/style.css">
|
||
</head>
|
||
<body>
|
||
<nav class="navbar">
|
||
<div class="container">
|
||
<a href="/" class="logo">CS2 <span>Simulator</span></a>
|
||
<button class="mobile-toggle" id="mobileToggle">☰</button>
|
||
<div class="nav-links" id="navLinks">
|
||
{% if user %}
|
||
<a href="/cases" class="nav-link">Кейсы</a>
|
||
<a href="/contracts" class="nav-link">Контракты</a>
|
||
<a href="/upgrade" class="nav-link active">Апгрейд</a>
|
||
<a href="/crash" class="nav-link">Crash</a>
|
||
<a href="/inventory" class="nav-link">Инвентарь</a>
|
||
<a href="/activity" class="nav-link">Лента</a>
|
||
<a href="/achievements" class="nav-link">Достижения</a>
|
||
<a href="/profile" class="nav-link">Профиль</a>
|
||
<span class="user-balance">{{ "%.0f"|format(user.balance) }} ₽</span>
|
||
<button onclick="toggleSafeMode()" class="btn btn-ghost" id="safeModeToggle" title="Режим записи">🎙️</button>
|
||
<button onclick="logout()" class="btn btn-ghost">Выйти</button>
|
||
{% else %}
|
||
<a href="/cases" class="nav-link">Кейсы</a>
|
||
<a href="/contracts" class="nav-link">Контракты</a>
|
||
<a href="/upgrade" class="nav-link active">Апгрейд</a>
|
||
<a href="/activity" class="nav-link">Лента</a>
|
||
<button onclick="toggleSafeMode()" class="btn btn-ghost" id="safeModeToggle" title="Режим записи">🎙️</button>
|
||
<a href="/login" class="btn btn-outline">Войти</a>
|
||
<a href="/register" class="btn btn-primary">Регистрация</a>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
</nav>
|
||
|
||
<main class="section">
|
||
<div class="container">
|
||
<div class="section-header">
|
||
<h1 class="section-title">🆙 Апгрейд</h1>
|
||
<p class="section-subtitle">Рискни предметом ради более дорогого</p>
|
||
</div>
|
||
|
||
<div style="display:grid;grid-template-columns:1fr 320px 1fr;gap:0.75rem;align-items:start;">
|
||
<!-- Inventory -->
|
||
<div class="card" style="padding:0;overflow:hidden;">
|
||
<div style="display:flex;align-items:center;justify-content:space-between;padding:0.6rem 0.75rem;border-bottom:1px solid var(--border);background:rgba(0,0,0,0.1);">
|
||
<span class="display-font text-sm">📦 Инвентарь <span class="text-xs text-dim">{{ inventory_items|length }}</span></span>
|
||
<div class="flex gap-1">
|
||
<input type="text" id="invSearch" class="form-input" placeholder="Поиск..." style="width:auto;min-width:100px;font-size:0.75rem;padding:0.25rem 0.5rem;" oninput="onInvSearch()">
|
||
<button class="btn-icon btn-sm" onclick="setInvSort('default',this)">📋</button>
|
||
<button class="btn-icon btn-sm" onclick="setInvSort('price_asc',this)">💰↑</button>
|
||
<button class="btn-icon btn-sm" onclick="setInvSort('price_desc',this)">💰↓</button>
|
||
</div>
|
||
</div>
|
||
<div style="max-height:400px;overflow-y:auto;padding:0.5rem;" id="inventoryList"></div>
|
||
</div>
|
||
|
||
<!-- Wheel -->
|
||
<div class="card" style="text-align:center;">
|
||
<div style="display:flex;align-items:center;justify-content:center;gap:0.75rem;margin-bottom:1rem;">
|
||
<div style="text-align:center;flex:1;" id="selectedInputDisplay">
|
||
<div style="width:60px;height:60px;margin:0 auto 0.25rem;background:var(--bg-dark);border-radius:var(--radius-sm);display:flex;align-items:center;justify-content:center;border:2px solid var(--border);">
|
||
<img src="/static/placeholder.png" id="displayInputImage" style="max-width:85%;max-height:50px;object-fit:contain;">
|
||
</div>
|
||
<div class="text-xs truncate" id="displayInputName">Не выбрано</div>
|
||
<div class="text-xs text-primary" id="displayInputPrice">—</div>
|
||
</div>
|
||
<div style="font-size:1.5rem;color:var(--text-dim);">➡</div>
|
||
<div style="text-align:center;flex:1;" id="selectedTargetDisplay">
|
||
<div style="width:60px;height:60px;margin:0 auto 0.25rem;background:var(--bg-dark);border-radius:var(--radius-sm);display:flex;align-items:center;justify-content:center;border:2px solid var(--border);">
|
||
<img src="/static/placeholder.png" id="displayTargetImage" style="max-width:85%;max-height:50px;object-fit:contain;">
|
||
</div>
|
||
<div class="text-xs truncate" id="displayTargetName">Не выбрано</div>
|
||
<div class="text-xs text-primary" id="displayTargetPrice">—</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div style="position:relative;width:180px;height:180px;margin:0 auto 1rem;">
|
||
<div style="position:absolute;inset:0;border-radius:50%;border:3px solid var(--border);" class="wheel" id="upgradeWheel" style="--probability:0;">
|
||
<div style="position:absolute;top:-10px;left:50%;transform:translateX(-50%);width:0;height:0;border-left:10px solid transparent;border-right:10px solid transparent;border-top:16px solid var(--primary);z-index:2;" id="pointerContainer"></div>
|
||
<div style="position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);width:80px;height:80px;border-radius:50%;background:var(--bg-dark);display:flex;align-items:center;justify-content:center;border:2px solid var(--border);z-index:1;">
|
||
<span class="display-font text-sm" id="wheelProbability">0%</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div style="display:flex;gap:0.5rem;justify-content:center;">
|
||
{% if user %}
|
||
<button class="btn btn-primary btn-large" onclick="executeUpgrade()" id="upgradeBtn" disabled>🎲 Апгрейд</button>
|
||
<button class="btn-icon" id="quickModeBtn" onclick="toggleQuickMode()" title="Быстрый режим">⚡</button>
|
||
{% else %}
|
||
<div class="flex gap-2" style="align-items:center;">
|
||
<span class="text-dim text-sm">🔒 Войдите, чтобы совершать апгрейды</span>
|
||
<a href="/login" class="btn btn-primary btn-sm">Войти</a>
|
||
<a href="/register" class="btn btn-outline btn-sm">Регистрация</a>
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Target -->
|
||
<div class="card" style="padding:0;overflow:hidden;">
|
||
<div style="padding:0.6rem 0.75rem;border-bottom:1px solid var(--border);background:rgba(0,0,0,0.1);">
|
||
<span class="display-font text-sm">🎯 Цель</span>
|
||
<input type="text" id="targetSearch" class="form-input" placeholder="Поиск..." style="width:100%;margin-top:0.35rem;font-size:0.75rem;padding:0.25rem 0.5rem;" oninput="searchTargetItems()">
|
||
<div class="flex gap-1" style="margin-top:0.35rem;flex-wrap:wrap;">
|
||
<button class="filter-chip active" onclick="setTargetSort('closest',this)">🎯 Ближ.</button>
|
||
<button class="filter-chip" onclick="setTargetSort('price_desc',this)">💎 Дорогие</button>
|
||
<button class="filter-chip" onclick="setTargetSort('price_asc',this)">💰 Дешёвые</button>
|
||
<button class="filter-chip" onclick="setTargetSort('name',this)">📋 Имя</button>
|
||
</div>
|
||
<div class="flex gap-1" style="margin-top:0.35rem;flex-wrap:wrap;display:none;" id="priceFilterBar">
|
||
<span class="text-xs text-dim" style="padding:0.2rem 0;">Шанс</span>
|
||
<button class="filter-chip" onclick="setPriceFilter('probhigh',this)">75%</button>
|
||
<button class="filter-chip" onclick="setPriceFilter('probmid',this)">50%</button>
|
||
<button class="filter-chip" onclick="setPriceFilter('probalow',this)">25%</button>
|
||
<button class="filter-chip" onclick="setPriceFilter('proba10',this)">10%</button>
|
||
<span class="text-xs text-dim" style="padding:0.2rem 0;">×</span>
|
||
<button class="filter-chip" onclick="setPriceFilter('2',this)">2×</button>
|
||
<button class="filter-chip" onclick="setPriceFilter('4',this)">4×</button>
|
||
<button class="filter-chip" onclick="setPriceFilter('8',this)">8×</button>
|
||
<button class="filter-chip" onclick="setPriceFilter('10',this)">10×</button>
|
||
</div>
|
||
</div>
|
||
<div style="max-height:400px;overflow-y:auto;padding:0.5rem;" id="targetList">
|
||
<div class="text-dim text-sm text-center" style="padding:2rem;">Выберите предмет</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- History -->
|
||
<div class="section">
|
||
<h3 class="display-font" style="font-size:0.95rem;margin-bottom:0.75rem;">📜 История <span class="text-dim text-xs" id="historyCount">{{ recent_upgrades|length }}</span></h3>
|
||
<div class="grid grid-auto-sm" id="historyGrid"></div>
|
||
</div>
|
||
</div>
|
||
</main>
|
||
|
||
<script src="/static/js/sounds.js"></script>
|
||
<script src="/static/js/websocket.js"></script>
|
||
<script src="/static/js/safemode.js"></script>
|
||
<script>
|
||
let selectedInput = null, selectedTarget = null, inventoryItems = {{ inventory_items|tojson }}, allTargetItems = [];
|
||
let currentSearchQuery = '', currentSort = 'closest', currentPriceFilter = null, isSpinning = false, quickMode = false, invSearchQuery = '', invSort = 'default';
|
||
|
||
document.addEventListener('DOMContentLoaded', () => { loadInitialTargets(); renderHistory({{ recent_upgrades|tojson }}); renderInventory(); });
|
||
|
||
function renderInventory() {
|
||
let items = [...inventoryItems];
|
||
if (invSearchQuery.length >= 1) { const q = invSearchQuery.toLowerCase(); items = items.filter(i => i.name.toLowerCase().includes(q)); }
|
||
if (invSort === 'price_asc') items.sort((a,b) => a.price_rub - b.price_rub);
|
||
else if (invSort === 'price_desc') items.sort((a,b) => b.price_rub - a.price_rub);
|
||
const container = document.getElementById('inventoryList');
|
||
if (!items.length) { container.innerHTML = '<div class="text-dim text-sm text-center" style="padding:1rem;">Ничего не найдено</div>'; return; }
|
||
container.innerHTML = items.map(item => `<div style="display:flex;align-items:center;gap:0.5rem;padding:0.4rem 0.5rem;border-radius:var(--radius-sm);cursor:pointer;transition:all 0.15s;${selectedInput && selectedInput.inventory_id === item.id ? 'background:var(--primary-subtle);border:1px solid rgba(245,158,11,0.2);' : 'border:1px solid transparent;'}" onclick="selectInputItem(${item.id})" onmouseover="this.style.background='var(--bg-hover)'" onmouseout="this.style.background='${selectedInput && selectedInput.inventory_id === item.id ? 'var(--primary-subtle)' : ''}'">
|
||
<img src="${item.image_url||'/static/placeholder.png'}" onerror="this.src='/static/placeholder.png'" style="width:40px;height:32px;object-fit:contain;flex-shrink:0;">
|
||
<div style="flex:1;min-width:0;"><div class="text-xs truncate" title="${item.name}">${item.name}</div><div class="text-xs text-dim">${item.rarity} <span class="text-primary">${Math.floor(item.price_rub).toLocaleString()} ₽</span></div></div>
|
||
</div>`).join('');
|
||
}
|
||
|
||
async function loadInitialTargets() {
|
||
try { const r = await fetch('/admin/api/items/search?q=&limit=600'); allTargetItems = await r.json(); allTargetItems = allTargetItems.filter((item,index,self) => index === self.findIndex(t => t.id === item.id)); applySortAndFilter(); } catch(e) {}
|
||
}
|
||
|
||
async function loadTargetsForInput(inputPrice) {
|
||
try { const minP = Math.round(inputPrice*1.05), maxP = Math.round(inputPrice*19); const r = await fetch(`/admin/api/items/search?q=&min_price=${minP}&max_price=${maxP}&limit=600`); const items = await r.json(); allTargetItems = items.filter((item,index,self) => index === self.findIndex(t => t.id === item.id)); applySortAndFilter(); } catch(e) { applySortAndFilter(); }
|
||
}
|
||
|
||
function selectInputItem(itemId) {
|
||
if (isSpinning) return;
|
||
const item = inventoryItems.find(i => i.id === itemId); if (!item) return;
|
||
selectedInput = { inventory_id: item.id, item_id: item.item_id, name: item.name, price: item.price_rub, image_url: item.image_url, rarity: item.rarity };
|
||
renderInventory();
|
||
document.getElementById('displayInputImage').src = item.image_url || '/static/placeholder.png';
|
||
document.getElementById('displayInputName').textContent = item.name.substring(0,25);
|
||
document.getElementById('displayInputPrice').textContent = `${Math.floor(item.price_rub).toLocaleString()} ₽`;
|
||
document.getElementById('priceFilterBar').style.display = 'flex';
|
||
currentPriceFilter = null; currentSort = 'closest';
|
||
document.querySelectorAll('.filter-chip').forEach(b => b.classList.remove('active'));
|
||
document.querySelector('.filter-chip[onclick*="closest"]')?.classList.add('active');
|
||
loadTargetsForInput(item.price_rub);
|
||
updateProbability();
|
||
}
|
||
|
||
function applySortAndFilter() {
|
||
if (!allTargetItems.length) return;
|
||
let filtered = [...allTargetItems];
|
||
if (currentSearchQuery.length >= 2) filtered = filtered.filter(item => item.name.toLowerCase().includes(currentSearchQuery.toLowerCase()));
|
||
if (selectedInput) {
|
||
const inputPrice = selectedInput.price;
|
||
filtered = filtered.filter(item => { const p = item.price_rub||100; return p > inputPrice && p <= inputPrice*19; });
|
||
if (currentPriceFilter) {
|
||
filtered = filtered.filter(item => {
|
||
const p = item.price_rub||100, ratio = p/inputPrice;
|
||
switch(currentPriceFilter) {
|
||
case '2': return ratio>=1.8&&ratio<=2.5; case '4': return ratio>=3.5&&ratio<=5; case '8': return ratio>=7&&ratio<=10; case '10': return ratio>=8.5&&ratio<=12;
|
||
case 'probalow': {const prob=(inputPrice/p)*100; return prob>=20&&prob<=30;} case 'probmid': {const prob=(inputPrice/p)*100; return prob>=45&&prob<=55;} case 'probhigh': {const prob=(inputPrice/p)*100; return prob>=70&&prob<=75;} case 'proba10': {const prob=(inputPrice/p)*100; return prob>=8&&prob<=12;}
|
||
default: return true;
|
||
}
|
||
});
|
||
}
|
||
}
|
||
if (currentSort === 'closest' && selectedInput) {
|
||
const ip = selectedInput.price;
|
||
filtered.sort((a,b) => { const da = Math.abs(Math.min(75,(ip/(a.price_rub||100))*100)-75), db = Math.abs(Math.min(75,(ip/(b.price_rub||100))*100)-75); return da!==db?da-db:(a.price_rub||0)-(b.price_rub||0); });
|
||
} else if (currentSort === 'price_asc') filtered.sort((a,b) => (a.price_rub||0)-(b.price_rub||0));
|
||
else if (currentSort === 'price_desc') filtered.sort((a,b) => (b.price_rub||0)-(a.price_rub||0));
|
||
else filtered.sort((a,b) => a.name.localeCompare(b.name));
|
||
displayTargetItems(filtered.slice(0,120));
|
||
}
|
||
|
||
function setTargetSort(sort, btn) { currentSort = sort; document.querySelectorAll('.filter-chip').forEach(b => b.classList.remove('active')); if (btn) btn.classList.add('active'); applySortAndFilter(); }
|
||
function setPriceFilter(mult, btn) { currentPriceFilter = mult === currentPriceFilter ? null : mult; document.querySelectorAll('.filter-chip').forEach(b => b.classList.remove('active')); if (currentPriceFilter && btn) btn.classList.add('active'); applySortAndFilter(); }
|
||
function onInvSearch() { invSearchQuery = document.getElementById('invSearch').value; renderInventory(); }
|
||
function setInvSort(sort, btn) { invSort = sort; document.querySelectorAll('.btn-icon').forEach(b => b.classList.remove('active')); if (btn) btn.classList.add('active'); renderInventory(); }
|
||
function toggleQuickMode() { quickMode = !quickMode; document.getElementById('quickModeBtn').classList.toggle('active', quickMode); }
|
||
|
||
async function searchTargetItems() {
|
||
currentSearchQuery = document.getElementById('targetSearch').value;
|
||
const container = document.getElementById('targetList');
|
||
if (currentSearchQuery.length < 2) { applySortAndFilter(); return; }
|
||
container.innerHTML = '<div class="text-dim text-sm text-center" style="padding:2rem;">🔍 Поиск…</div>';
|
||
try { const r = await fetch(`/admin/api/items/search?q=${encodeURIComponent(currentSearchQuery)}&limit=600`); const items = await r.json(); allTargetItems = items.filter((item,index,self) => index === self.findIndex(t => t.id === item.id)); applySortAndFilter(); } catch(e) { container.innerHTML = '<div class="text-dim text-sm text-center">Ошибка</div>'; }
|
||
}
|
||
|
||
function displayTargetItems(items) {
|
||
const container = document.getElementById('targetList');
|
||
if (!items.length) { container.innerHTML = '<div class="text-dim text-sm text-center" style="padding:2rem;">Ничего не найдено</div>'; return; }
|
||
container.innerHTML = items.map(item => {
|
||
const price = item.price_rub||100, imageUrl = item.image_url||'/static/placeholder.png';
|
||
let disabled = false, disabledReason = '';
|
||
if (selectedInput) { const minP = selectedInput.price*0.5, maxP = selectedInput.price*19; if (price < minP) { disabled = true; disabledReason = 'Слишком дешёвый'; } else if (price > maxP) { disabled = true; disabledReason = 'Слишком дорогой'; } }
|
||
const safeName = item.name.replace(/'/g,"\\'"); const safeUrl = imageUrl.replace(/'/g,"\\'");
|
||
return `<div style="display:flex;align-items:center;gap:0.5rem;padding:0.4rem 0.5rem;border-radius:var(--radius-sm);cursor:pointer;transition:all 0.15s;${disabled ? 'opacity:0.35;cursor:not-allowed;' : ''}border:1px solid transparent;" class="target-item-card ${disabled ? 'disabled' : ''}" onclick="${disabled ? `alert('${disabledReason}')` : `selectTargetItem(${item.id},'${safeName}',${price},'${safeUrl}','${item.rarity||'Unknown'}')`}" onmouseover="${disabled ? '' : 'this.style.background=\'var(--bg-hover)\''}" onmouseout="this.style.background=''" title="${disabled ? disabledReason : ''}">
|
||
<img src="${imageUrl}" onerror="this.src='/static/placeholder.png'" style="width:36px;height:28px;object-fit:contain;flex-shrink:0;">
|
||
<div style="flex:1;min-width:0;"><div class="text-xs truncate" title="${item.name}">${item.name}</div><div class="text-xs text-dim">${item.rarity||''} <span class="text-primary">${price.toLocaleString()} ₽</span></div></div>
|
||
</div>`;
|
||
}).join('');
|
||
}
|
||
|
||
function selectTargetItem(id, name, price, imageUrl, rarity) {
|
||
document.querySelectorAll('.target-item-card').forEach(c => c.style.borderColor = 'transparent'); const card = event.target.closest('.target-item-card'); if (card) card.style.borderColor = 'var(--primary)';
|
||
selectedTarget = { item_id: id, name, price, image_url: imageUrl, rarity };
|
||
document.getElementById('displayTargetImage').src = imageUrl||'/static/placeholder.png';
|
||
document.getElementById('displayTargetName').textContent = name.substring(0,25);
|
||
document.getElementById('displayTargetPrice').textContent = `${price.toLocaleString()} ₽`;
|
||
updateProbability();
|
||
}
|
||
|
||
async function updateProbability() {
|
||
if (!selectedInput || !selectedTarget) return;
|
||
const fd = new FormData(); fd.append('input_inventory_id', selectedInput.inventory_id); fd.append('target_item_id', selectedTarget.item_id);
|
||
try { const r = await fetch('/web/api/upgrade/calculate', { method:'POST', body:fd }); const d = await r.json(); if (d.success) { document.getElementById('wheelProbability').textContent = `${d.adjusted_probability.toFixed(1)}%`; document.getElementById('upgradeWheel').style.setProperty('--probability', d.adjusted_probability); document.getElementById('upgradeBtn').disabled = false; } } catch(e) {}
|
||
}
|
||
|
||
function sparkles(container) {
|
||
for (let i = 0; i < 24; i++) { const s = document.createElement('div'); s.className = 'sparkle'; s.style.cssText = `position:absolute;width:${2+Math.random()*5}px;height:${2+Math.random()*5}px;border-radius:50%;background:${['#f59e0b','#10b981','#ffd700','#fff','#ff6b6b'][Math.floor(Math.random()*5)]};left:${Math.random()*100}%;top:${Math.random()*100}%;pointer-events:none;animation:sparkleFloat 0.8s ease forwards;`; s.style.setProperty('--dx', (Math.random()-0.5)*140+'px'); s.style.setProperty('--dy', -(Math.random()*120+50)+'px'); container.style.position = 'relative'; container.appendChild(s); }
|
||
}
|
||
|
||
async function refreshBalance() {
|
||
try { const r = await fetch('/web/api/user/balance'); const d = await r.json(); if (d.success) document.querySelectorAll('.user-balance').forEach(el => el.textContent = `${Math.floor(d.balance).toLocaleString()} ₽`); } catch(e) {}
|
||
}
|
||
|
||
async function refreshHistory() {
|
||
try { const r = await fetch('/web/api/upgrade/history'); const d = await r.json(); if (d.success) renderHistory(d.history); } catch(e) {}
|
||
}
|
||
|
||
function renderHistory(items) {
|
||
const grid = document.getElementById('historyGrid'), count = document.getElementById('historyCount');
|
||
if (count) count.textContent = items.length;
|
||
if (!items.length) { grid.innerHTML = '<div class="text-dim text-sm text-center" style="grid-column:1/-1;padding:1rem;">История пуста</div>'; return; }
|
||
grid.innerHTML = items.map(u => `<div class="card" style="padding:0.5rem 0.75rem;display:flex;align-items:center;gap:0.5rem;${u.success ? 'border-color:rgba(34,197,94,0.2);' : 'opacity:0.6;'}">
|
||
${u.success ? `<img src="${u.input_image_url||'/static/placeholder.png'}" onerror="this.src='/static/placeholder.png'" style="width:28px;height:22px;object-fit:contain;"><div style="flex:1;min-width:0;"><div class="text-xs truncate">${(u.input_item_name||'').substring(0,12)}… → ${(u.target_item_name||'').substring(0,12)}…</div><div class="text-xs text-dim">${u.probability}% <span style="color:var(--success)">✅</span></div></div><img src="${u.target_image_url||'/static/placeholder.png'}" onerror="this.src='/static/placeholder.png'" style="width:28px;height:22px;object-fit:contain;">`
|
||
: `<img src="${u.target_image_url||'/static/placeholder.png'}" onerror="this.src='/static/placeholder.png'" style="width:28px;height:22px;object-fit:contain;opacity:0.5;"><div style="flex:1;min-width:0;"><div class="text-xs truncate">${(u.input_item_name||'').substring(0,12)}… → ${(u.target_item_name||'').substring(0,12)}…</div><div class="text-xs text-dim">${u.probability}% <span style="color:var(--danger)">❌</span></div></div>`}
|
||
</div>`).join('');
|
||
}
|
||
|
||
async function executeUpgrade() {
|
||
if (!selectedInput || !selectedTarget || isSpinning) return;
|
||
const lockedInputId = selectedInput.inventory_id, lockedTargetId = selectedTarget.item_id;
|
||
const fd = new FormData(); fd.append('input_inventory_id', lockedInputId); fd.append('target_item_id', lockedTargetId); fd.append('quick_mode', quickMode ? 'true' : 'false');
|
||
const btn = document.getElementById('upgradeBtn');
|
||
isSpinning = true; btn.disabled = true; btn.textContent = '🎰 Крутим…';
|
||
const pointer = document.getElementById('pointerContainer');
|
||
pointer.style.transition = 'none'; pointer.style.transform = 'rotate(0deg)'; void pointer.offsetHeight;
|
||
try {
|
||
const r = await fetch('/web/api/upgrade/execute', { method:'POST', body:fd }); const data = await r.json();
|
||
if (data.success) {
|
||
if (typeof handleAchievements === 'function') handleAchievements(data);
|
||
const spinDuration = quickMode ? 4 : 12;
|
||
pointer.style.transition = `transform ${spinDuration}s cubic-bezier(0.4,0,0.15,1.0)`;
|
||
pointer.style.transform = `rotate(${data.final_angle}deg)`;
|
||
|
||
let tickLastDeg = 0, tickCumulative = 0, tickNextThreshold = 15, tickLastTickCumulative = 0, tickControlPlayed = false;
|
||
const tickSlowStart = data.final_angle - 90, tickStartTime = performance.now(), tickMaxDuration = spinDuration*1000+500;
|
||
let tickRAF = null;
|
||
function tickLoop() {
|
||
const elapsed = performance.now() - tickStartTime;
|
||
if (elapsed > tickMaxDuration) return;
|
||
const style = window.getComputedStyle(pointer);
|
||
let deg = 0; const tf = style.transform;
|
||
if (tf && tf !== 'none') {
|
||
const m = tf.match(/matrix\(([^)]+)\)/); if (m) { const v = m[1].split(', ').map(Number); deg = Math.atan2(v[1], v[0]) * (180/Math.PI); }
|
||
else { const m2 = tf.match(/rotate\(([-\d.]+)deg\)/); if (m2) deg = parseFloat(m2[1]); }
|
||
}
|
||
let delta = deg - tickLastDeg; if (delta > 180) delta -= 360; if (delta < -180) delta += 360;
|
||
tickCumulative += delta; tickLastDeg = deg;
|
||
if (tickCumulative >= tickNextThreshold) { SoundManager.wheelTick(); tickLastTickCumulative = tickCumulative; tickNextThreshold += tickCumulative < tickSlowStart ? 15 : 30; }
|
||
const remaining = data.final_angle - tickCumulative, gap = tickCumulative - tickLastTickCumulative;
|
||
if (remaining < 10 && remaining > 0 && gap > 15 && !tickControlPlayed) { SoundManager.wheelTick(); tickControlPlayed = true; }
|
||
tickRAF = requestAnimationFrame(tickLoop);
|
||
}
|
||
tickRAF = requestAnimationFrame(tickLoop);
|
||
|
||
let resultShown = false;
|
||
const onTransitionEnd = () => { pointer.removeEventListener('transitionend', onTransitionEnd); if (tickRAF) cancelAnimationFrame(tickRAF); showResult(); };
|
||
pointer.addEventListener('transitionend', onTransitionEnd);
|
||
|
||
const showResult = () => {
|
||
if (resultShown) return; resultShown = true;
|
||
if (data.upgrade_success && data.received_item) {
|
||
document.getElementById('displayTargetImage').src = data.received_item.image_url||'/static/placeholder.png';
|
||
document.getElementById('displayTargetName').textContent = data.received_item.name.substring(0,25);
|
||
document.getElementById('displayTargetPrice').textContent = `${Math.floor(data.received_item.price).toLocaleString()} ₽`;
|
||
SoundManager.wheelWin(); showNotification('🎉 ' + data.received_item.name, 'success');
|
||
} else {
|
||
SoundManager.wheelLose(); showNotification('😔 Не повезло', 'error');
|
||
}
|
||
refreshBalance();
|
||
setTimeout(() => {
|
||
inventoryItems = inventoryItems.filter(i => i.id !== lockedInputId);
|
||
selectedInput = null; selectedTarget = null;
|
||
document.getElementById('displayInputImage').src = '/static/placeholder.png';
|
||
document.getElementById('displayInputName').textContent = 'Не выбрано';
|
||
document.getElementById('displayInputPrice').textContent = '—';
|
||
document.getElementById('displayTargetImage').src = '/static/placeholder.png';
|
||
document.getElementById('displayTargetName').textContent = 'Не выбрано';
|
||
document.getElementById('displayTargetPrice').textContent = '—';
|
||
document.getElementById('wheelProbability').textContent = '0%';
|
||
document.getElementById('upgradeWheel').style.setProperty('--probability','0');
|
||
btn.disabled = true; btn.textContent = '🎲 Апгрейд'; isSpinning = false;
|
||
refreshHistory();
|
||
pointer.style.transition = 'none'; pointer.style.transform = 'rotate(0deg)';
|
||
}, 2000);
|
||
};
|
||
setTimeout(showResult, spinDuration*1000+300);
|
||
} else { alert(data.error||'Ошибка'); isSpinning = false; btn.disabled = false; btn.textContent = '🎲 Апгрейд'; }
|
||
} catch(e) { alert('Ошибка соединения'); isSpinning = false; btn.disabled = false; btn.textContent = '🎲 Апгрейд'; }
|
||
}
|
||
|
||
function handleAchievements(data) { if (data && data.achievements_unlocked && Array.isArray(data.achievements_unlocked) && data.achievements_unlocked.length > 0) data.achievements_unlocked.forEach(ach => showAchievementPopup(ach.title||ach.name||'Достижение', ach.reward||'')); }
|
||
</script>
|
||
<script>
|
||
function toggleSound() { const e = SoundManager.toggle(); document.getElementById('soundToggle').textContent = e ? '🔊' : '🔇'; }
|
||
document.addEventListener('DOMContentLoaded', () => { const btn = document.getElementById('soundToggle'); if(btn) btn.textContent = localStorage.getItem('sound_enabled') !== 'false' ? '🔊' : '🔇'; });
|
||
</script>
|
||
{% if user %}{% include '_activity_sidebar.html' %}{% endif %}
|
||
</body>
|
||
</html>
|