Files
dodep-simulator/templates/upgrade.html
T

364 lines
31 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, 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>