UI redesign: CS2 gambling aesthetic, glassmorphism, responsive layout, enhanced sounds, admin panel overhaul

This commit is contained in:
root
2026-07-05 14:22:49 +00:00
parent 01c6573f5f
commit 4b0fc594b4
21 changed files with 2750 additions and 9203 deletions
+179 -640
View File
@@ -6,30 +6,30 @@
<title>Апгрейд — CS2 Simulator</title>
<link rel="stylesheet" href="/static/css/style.css">
</head>
<body class="upgrade-page">
<body>
<nav class="navbar">
<div class="container">
<a href="/" class="logo">🎮 CS2 Simulator</a>
<div class="nav-links">
<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="/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="/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="toggleSound()" class="btn btn-outline" id="soundToggle" title="Звук">🔊</button>
<button onclick="toggleSafeMode()" class="btn btn-outline" id="safeModeToggle" title="Режим записи">🎙️</button>
<button onclick="logout()" class="btn btn-outline">Выйти</button>
<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-outline" id="safeModeToggle" title="Режим записи">🎙️</button>
<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 %}
@@ -37,700 +37,303 @@
</div>
</nav>
<main class="upgrade-container">
<main class="section">
<div class="container">
<div class="upgrade-header">
<h1>🆙 Апгрейд</h1>
<p>Рискни предметом ради более дорогого</p>
<div class="section-header">
<h1 class="section-title">🆙 Апгрейд</h1>
<p class="section-subtitle">Рискни предметом ради более дорогого</p>
</div>
<div class="upgrade-layout">
<div style="display:grid;grid-template-columns:1fr 320px 1fr;gap:0.75rem;align-items:start;">
<!-- Inventory -->
<div class="inventory-panel">
<div class="panel-header">
<div class="panel-title">
<span>📦 Инвентарь</span>
<span style="font-size: 0.65rem; color: #6b6b7a;">{{ inventory_items|length }}</span>
</div>
<div class="inv-filter-bar">
<input type="text" class="inv-search-input" id="invSearch" placeholder="Поиск..." oninput="onInvSearch()">
<div class="inv-sort-group">
<button class="inv-sort-btn" onclick="setInvSort('default', this)">📋</button>
<button class="inv-sort-btn" onclick="setInvSort('price_asc', this)">💰↑</button>
<button class="inv-sort-btn" onclick="setInvSort('price_desc', this)">💰↓</button>
</div>
<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 class="inventory-list" id="inventoryList"></div>
<div style="max-height:400px;overflow-y:auto;padding:0.5rem;" id="inventoryList"></div>
</div>
<!-- Wheel -->
<div class="wheel-panel" style="position: relative;">
<div class="selected-items-display">
<div class="selected-item-mini" id="selectedInputDisplay">
<div class="item-frame">
<img src="/static/placeholder.png" id="displayInputImage">
<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="selected-item-mini-name" id="displayInputName">Не выбрано</div>
<div class="selected-item-mini-price" id="displayInputPrice"></div>
<div class="text-xs truncate" id="displayInputName">Не выбрано</div>
<div class="text-xs text-primary" id="displayInputPrice"></div>
</div>
<div class="arrow-icon"></div>
<div class="selected-item-mini" id="selectedTargetDisplay">
<div class="item-frame">
<img src="/static/placeholder.png" id="displayTargetImage">
<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="selected-item-mini-name" id="displayTargetName">Не выбрано</div>
<div class="selected-item-mini-price" id="displayTargetPrice"></div>
<div class="text-xs truncate" id="displayTargetName">Не выбрано</div>
<div class="text-xs text-primary" id="displayTargetPrice"></div>
</div>
</div>
<div class="wheel-container">
<div class="wheel-outer-ring"></div>
<div class="wheel" id="upgradeWheel" style="--probability: 0;"></div>
<div class="wheel-ticks"></div>
<div class="wheel-pointer-container" id="pointerContainer">
<div class="wheel-pointer"></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 class="wheel-center" id="wheelProbability">0%</div>
</div>
<div class="upgrade-actions">
<div style="display:flex;gap:0.5rem;justify-content:center;">
{% if user %}
<button class="upgrade-btn" onclick="executeUpgrade()" id="upgradeBtn" disabled>
🎲 Апгрейд
</button>
<button class="quick-btn" id="quickModeBtn" onclick="toggleQuickMode()" title="Быстрый режим"></button>
<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 style="display:flex;align-items:center;gap:1rem;width:100%;justify-content:center;padding:1rem;">
<span style="color:var(--text-secondary);">🔒 Войдите, чтобы совершать апгрейды</span>
<a href="/login" class="btn btn-primary">Войти</a>
<a href="/register" class="btn btn-outline">Регистрация</a>
<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 id="toastContainer" class="toast-container"></div>
</div>
<!-- Target -->
<div class="target-panel">
<div class="target-header">
<div class="panel-title" style="margin-bottom: 0.4rem;">
<span>🎯 Цель</span>
<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>
<input type="text" class="target-search-input" id="targetSearch" placeholder="Поиск..." oninput="searchTargetItems()">
<div class="sort-controls">
<button class="sort-btn" onclick="setTargetSort('closest', this)">🎯 Ближайшие</button>
<button class="sort-btn" onclick="setTargetSort('price_desc', this)">💎 Дорогие</button>
<button class="sort-btn" onclick="setTargetSort('price_asc', this)">💰 Дешёвые</button>
<button class="sort-btn" onclick="setTargetSort('name', this)">📋 Имя</button>
</div>
<div class="price-filter-bar" id="priceFilterBar" style="display: none;">
<span style="font-size:0.55rem;color:#4a4a5a;text-transform:uppercase;letter-spacing:0.04em;padding:0.2rem 0;margin-right:0.15rem;">Шанс</span>
<button class="pf-btn" onclick="setPriceFilter('probhigh', this)">75%</button>
<button class="pf-btn" onclick="setPriceFilter('probmid', this)">50%</button>
<button class="pf-btn" onclick="setPriceFilter('probalow', this)">25%</button>
<button class="pf-btn" onclick="setPriceFilter('proba10', this)">10%</button>
<span style="font-size:0.55rem;color:#4a4a5a;text-transform:uppercase;letter-spacing:0.04em;padding:0.2rem 0;margin:0 0.15rem;">×</span>
<button class="pf-btn" onclick="setPriceFilter('2', this)">2×</button>
<button class="pf-btn" onclick="setPriceFilter('4', this)">4×</button>
<button class="pf-btn" onclick="setPriceFilter('8', this)">8×</button>
<button class="pf-btn" onclick="setPriceFilter('10', this)">10×</button>
<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 class="target-list" id="targetList">
<div class="empty-state">Выберите предмет</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="history-section">
<h3 class="history-title">📜 История <span class="history-count" id="historyCount">{{ recent_upgrades|length }}</span></h3>
<div class="history-grid" id="historyGrid"></div>
<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>
// === Фразы для побед/поражений ===
const PHRASE_PARTS = {
win: {
pre: ['🎉','🔥','⭐','🏆','💎','✨','🎯','👑','💫','🌟','🎰','💪','🥇','🎊','🏅'],
verb: ['Победа','Джекпот','Успех','Красава','В дамках','Есть контакт','Повезло','Забрал','Выбил','Сорвал куш','Всё пучком','Лут','Топчик','Козырь','Фартануло'],
noun: ['! 🔥','! 💎','! 🏆','! 👑','! ✨','! 💪','!','!','!'],
},
lose: {
pre: ['😔','💔','😢','💀','😭','😤','😩','💸','😞','😓','🤡','💀','😅','🙃','🤷'],
verb: ['Не повезло','Облом','Мимо','Пролёт','Не выпал','Не срослось','Пусто','Мимо кассы','В следующий раз','Не фортануло','Повезёт потом','Ноль','Минус','Увы','Печаль'],
noun: [' 😔',' 💔','...',' 😢',' 😭',' 💸','...'],
}
};
let usedPhraseKeys = new Set();
function getRandomPhrase(type) {
const parts = PHRASE_PARTS[type];
for (let attempt = 0; attempt < 100; attempt++) {
const pre = parts.pre[Math.floor(Math.random() * parts.pre.length)];
const verb = parts.verb[Math.floor(Math.random() * parts.verb.length)];
const noun = parts.noun[Math.floor(Math.random() * parts.noun.length)];
const key = pre + '|' + verb + '|' + noun;
if (!usedPhraseKeys.has(key)) {
usedPhraseKeys.add(key);
return pre + ' ' + verb + noun;
}
}
usedPhraseKeys.clear();
return getRandomPhrase(type);
}
let selectedInput = null, selectedTarget = null, inventoryItems = {{ inventory_items|tojson }}, allTargetItems = [];
let currentSearchQuery = '', currentSort = 'closest', currentPriceFilter = null, isSpinning = false, quickMode = false, invSearchQuery = '', invSort = 'default';
let selectedInput = null;
let selectedTarget = null;
let inventoryItems = {{ inventory_items|tojson }};
let allTargetItems = [];
let currentSearchQuery = '';
let currentSort = 'closest';
let currentPriceFilter = null;
let isSpinning = false;
let quickMode = false;
let invSearchQuery = '';
let invSort = 'default';
document.addEventListener('DOMContentLoaded', () => {
loadInitialTargets();
renderHistory({{ recent_upgrades|tojson }});
renderInventory();
});
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);
}
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 === 0) {
container.innerHTML = '<div class="empty-state" style="padding:1rem;text-align:center;color:var(--text-secondary)">Ничего не найдено</div>';
return;
}
container.innerHTML = items.map(item => `
<div class="inventory-item-card ${selectedInput && selectedInput.inventory_id === item.id ? 'selected' : ''}"
onclick="selectInputItem(${item.id})" data-id="${item.id}" data-price="${item.price_rub}">
<img src="${item.image_url || '/static/placeholder.png'}" onerror="this.src='/static/placeholder.png'">
<div class="inventory-item-info">
<div class="inventory-item-name" title="${item.name}">${item.name}</div>
<div class="inventory-item-meta">
<span>${item.rarity}</span>
<span class="inventory-item-price">${Math.floor(item.price_rub).toLocaleString()} ₽</span>
</div>
</div>
</div>
`).join('');
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 response = await fetch(`/admin/api/items/search?q=&limit=600`);
allTargetItems = await response.json();
allTargetItems = allTargetItems.filter((item, index, self) =>
index === self.findIndex(t => t.id === item.id)
);
currentSort = 'closest';
document.querySelectorAll('.sort-btn').forEach(b => b.classList.remove('active'));
document.querySelector('.sort-btn[onclick*="closest"]')?.classList.add('active');
applySortAndFilter();
} catch (e) {}
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);
const maxP = Math.round(inputPrice * 19);
const resp = await fetch(`/admin/api/items/search?q=&min_price=${minP}&max_price=${maxP}&limit=600`);
const items = await resp.json();
allTargetItems = items.filter((item, index, self) =>
index === self.findIndex(t => t.id === item.id)
);
currentSort = 'closest';
document.querySelectorAll('.sort-btn').forEach(b => b.classList.remove('active'));
document.querySelector('.sort-btn[onclick*="closest"]')?.classList.add('active');
applySortAndFilter();
} catch (e) {
console.error('Failed to load targets:', e);
applySortAndFilter();
}
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
};
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('displayInputName').textContent = item.name.substring(0,25);
document.getElementById('displayInputPrice').textContent = `${Math.floor(item.price_rub).toLocaleString()}`;
document.getElementById('selectedInputDisplay').classList.add('selected');
document.getElementById('priceFilterBar').style.display = 'flex';
currentPriceFilter = null;
document.querySelectorAll('.pf-btn').forEach(b => b.classList.remove('active'));
currentSort = 'closest';
document.querySelectorAll('.sort-btn').forEach(b => b.classList.remove('active'));
document.querySelector('.sort-btn[onclick*="closest"]')?.classList.add('active');
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 (currentSearchQuery.length >= 2) filtered = filtered.filter(item => item.name.toLowerCase().includes(currentSearchQuery.toLowerCase()));
if (selectedInput) {
const inputPrice = selectedInput.price;
filtered = filtered.filter(item => {
const price = item.price_rub || 100;
if (price <= inputPrice) return false;
const maxPrice = inputPrice * 19;
return price <= maxPrice;
});
filtered = filtered.filter(item => { const p = item.price_rub||100; return p > inputPrice && p <= inputPrice*19; });
if (currentPriceFilter) {
filtered = filtered.filter(item => {
const price = item.price_rub || 100;
const ratio = price / inputPrice;
const prob = (inputPrice / price) * 100;
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': return prob >= 20 && prob <= 30;
case 'probmid': return prob >= 45 && prob <= 55;
case 'probhigh': return prob >= 70 && prob <= 75;
case 'proba10': return prob >= 8 && prob <= 12;
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 inputPrice = selectedInput.price;
filtered.sort((a, b) => {
const pa = (inputPrice / (a.price_rub || 100)) * 100;
const pb = (inputPrice / (b.price_rub || 100)) * 100;
const da = Math.abs(Math.min(75, pa) - 75);
const db = Math.abs(Math.min(75, pb) - 75);
if (da !== db) return da - db;
return (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));
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('.sort-btn').forEach(b => b.classList.remove('active'));
if (btn) btn.classList.add('active');
applySortAndFilter();
}
function setPriceFilter(mult, btn) {
currentPriceFilter = mult === currentPriceFilter ? null : mult;
document.querySelectorAll('.pf-btn').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('.inv-sort-btn').forEach(b => b.classList.remove('active'));
if (btn) btn.classList.add('active');
renderInventory();
}
function toggleQuickMode() {
quickMode = !quickMode;
document.getElementById('quickModeBtn').classList.toggle('active', quickMode);
}
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() {
const query = document.getElementById('targetSearch').value;
currentSearchQuery = query;
currentSearchQuery = document.getElementById('targetSearch').value;
const container = document.getElementById('targetList');
if (query.length < 2) {
applySortAndFilter();
return;
}
container.innerHTML = '<div class="empty-state">🔍 Поиск…</div>';
try {
const response = await fetch(`/admin/api/items/search?q=${encodeURIComponent(query)}&limit=600`);
const items = await response.json();
allTargetItems = items.filter((item, index, self) =>
index === self.findIndex(t => t.id === item.id)
);
applySortAndFilter();
} catch (e) {
container.innerHTML = '<div class="empty-state">Ошибка</div>';
}
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 === 0) {
container.innerHTML = '<div class="empty-state">Ничего не найдено</div>';
return;
}
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;
const imageUrl = item.image_url || '/static/placeholder.png';
let isDisabled = false;
let disabledReason = '';
if (selectedInput) {
const minPrice = selectedInput.price * 0.5;
const maxPrice = selectedInput.price * 19;
if (price < minPrice) {
isDisabled = true;
disabledReason = 'Слишком дешёвый';
} else if (price > maxPrice) {
isDisabled = true;
disabledReason = 'Слишком дорогой';
}
}
const safeName = item.name.replace(/'/g, "\\'");
const safeUrl = imageUrl.replace(/'/g, "\\'");
return `
<div class="target-item-card ${isDisabled ? 'disabled' : ''}"
onclick="selectTargetItem(${item.id}, '${safeName}', ${price}, '${safeUrl}', '${item.rarity || 'Unknown'}')"
data-price="${price}"
title="${isDisabled ? disabledReason : ''}">
<img src="${imageUrl}" onerror="this.src='/static/placeholder.png'">
<div class="target-item-info">
<div class="target-item-name" title="${item.name}">${item.name}</div>
<div class="target-item-meta">
<span>${item.rarity || ''}</span>
<span class="target-item-price">${price.toLocaleString()} ₽</span>
</div>
</div>
</div>
`;
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('');
if (selectedTarget) {
document.querySelectorAll('.target-item-card').forEach(card => {
if (card.onclick && card.onclick.toString().includes(`selectTargetItem(${selectedTarget.item_id}`)) {
card.classList.add('selected');
}
});
}
}
function selectTargetItem(id, name, price, imageUrl, rarity) {
if (isSpinning) return;
const card = event.target.closest('.target-item-card');
if (card.classList.contains('disabled')) {
alert('Этот предмет не подходит!');
return;
}
document.querySelectorAll('.target-item-card').forEach(c => c.classList.remove('selected'));
card.classList.add('selected');
selectedTarget = {
item_id: id,
name: name,
price: price,
image_url: imageUrl,
rarity: rarity
};
document.getElementById('displayTargetImage').src = imageUrl || '/static/placeholder.png';
document.getElementById('displayTargetName').textContent = name.substring(0, 25);
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()}`;
document.getElementById('selectedTargetDisplay').classList.add('selected');
updateProbability();
}
async function updateProbability() {
if (!selectedInput || !selectedTarget) return;
const formData = new FormData();
formData.append('input_inventory_id', selectedInput.inventory_id);
formData.append('target_item_id', selectedTarget.item_id);
try {
const response = await fetch('/web/api/upgrade/calculate', {
method: 'POST',
body: formData
});
const data = await response.json();
if (data.success) {
const prob = data.adjusted_probability.toFixed(1);
document.getElementById('wheelProbability').textContent = `${prob}%`;
const wheel = document.getElementById('upgradeWheel');
wheel.style.setProperty('--probability', prob);
document.getElementById('upgradeBtn').disabled = false;
}
} catch (e) {}
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.left = Math.random() * 100 + '%';
s.style.top = Math.random() * 100 + '%';
s.style.setProperty('--dx', (Math.random() - 0.5) * 140 + 'px');
s.style.setProperty('--dy', -(Math.random() * 120 + 50) + 'px');
s.style.animationDelay = Math.random() * 0.6 + 's';
s.style.width = (2 + Math.random() * 5) + 'px';
s.style.height = s.style.width;
s.style.background = ['#f59e0b', '#10b981', '#ffd700', '#fff', '#ff6b6b'][Math.floor(Math.random() * 5)];
container.appendChild(s);
}
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) {}
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) {}
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');
const count = document.getElementById('historyCount');
const grid = document.getElementById('historyGrid'), count = document.getElementById('historyCount');
if (count) count.textContent = items.length;
if (!items.length) {
grid.innerHTML = '<div class="empty-state" style="grid-column:1/-1;text-align:center;padding:1rem;color:var(--text-secondary)">История пуста</div>';
return;
}
grid.innerHTML = items.map(u => `
<div class="history-card ${u.success ? 'success' : 'fail'}">
${u.success
? `<img src="${u.input_image_url || '/static/placeholder.png'}" onerror="this.src='/static/placeholder.png'" loading="lazy">
<div class="history-info">
<div class="history-names">${(u.input_item_name || '').substring(0, 12)}… → ${(u.target_item_name || '').substring(0, 12)}…</div>
<div class="history-meta">
<span>${u.probability}%</span>
<span class="history-result success">✅</span>
</div>
</div>
<img src="${u.target_image_url || '/static/placeholder.png'}" onerror="this.src='/static/placeholder.png'" loading="lazy" style="width:32px;height:28px;">`
: `<img src="${u.target_image_url || '/static/placeholder.png'}" onerror="this.src='/static/placeholder.png'" loading="lazy" style="opacity:0.5;">
<div class="history-info">
<div class="history-names">${(u.input_item_name || '').substring(0, 12)}… → ${(u.target_item_name || '').substring(0, 12)}…</div>
<div class="history-meta">
<span>${u.probability}%</span>
<span class="history-result fail">❌</span>
</div>
</div>`
}
</div>
`).join('');
}
function showToast(message, type) {
const container = document.getElementById('toastContainer');
const toast = document.createElement('div');
toast.className = `toast ${type}`;
toast.textContent = message;
container.appendChild(toast);
requestAnimationFrame(() => toast.classList.add('show'));
setTimeout(() => {
toast.classList.remove('show');
setTimeout(() => toast.remove(), 300);
}, 3000);
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;
// Lock the chosen items at spin start
const lockedInputId = selectedInput.inventory_id;
const lockedTargetId = selectedTarget.item_id;
const lockedPrice = selectedInput.price;
const formData = new FormData();
formData.append('input_inventory_id', lockedInputId);
formData.append('target_item_id', lockedTargetId);
formData.append('quick_mode', quickMode ? 'true' : 'false');
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 = '🎰 Крутим…';
isSpinning = true; btn.disabled = true; btn.textContent = '🎰 Крутим…';
const pointer = document.getElementById('pointerContainer');
pointer.style.transition = 'none';
pointer.style.transform = 'rotate(0deg)';
void pointer.offsetHeight;
pointer.style.transition = 'none'; pointer.style.transform = 'rotate(0deg)'; void pointer.offsetHeight;
try {
const response = await fetch('/web/api/upgrade/execute', {
method: 'POST',
body: formData
});
const data = await response.json();
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, 0.15, 1.0)`;
pointer.style.transition = `transform ${spinDuration}s cubic-bezier(0.4,0,0.15,1.0)`;
pointer.style.transform = `rotate(${data.final_angle}deg)`;
// Тики колеса: быстрая фаза каждые 15°, медленная (последние 90°) — 30°
let tickLastDeg = 0, tickCumulative = 0, tickNextThreshold = 15;
let tickLastTickCumulative = 0;
let tickControlPlayed = false;
const tickSlowStart = data.final_angle - 90;
const tickStartTime = performance.now();
const tickMaxDuration = spinDuration * 1000 + 500;
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;
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.wheelSpin();
tickLastTickCumulative = tickCumulative;
tickNextThreshold += tickCumulative < tickSlowStart ? 15 : 30;
}
// Контрольный тик: если до финала <10°, а последний тик был >15° назад
const remaining = data.final_angle - tickCumulative;
const gap = tickCumulative - tickLastTickCumulative;
if (remaining < 10 && remaining > 0 && gap > 15 && !tickControlPlayed) {
SoundManager.wheelSpin();
tickControlPlayed = true;
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();
};
const onTransitionEnd = () => { pointer.removeEventListener('transitionend', onTransitionEnd); if (tickRAF) cancelAnimationFrame(tickRAF); showResult(); };
pointer.addEventListener('transitionend', onTransitionEnd);
const showResult = () => {
if (resultShown) return;
resultShown = true;
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('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()}`;
document.getElementById('selectedTargetDisplay').style.borderColor = '#10b981';
document.getElementById('selectedTargetDisplay').style.boxShadow = '0 0 20px rgba(16,185,129,0.3)';
showToast(`${getRandomPhrase('win')} ${data.received_item.name}`, 'success');
SoundManager.wheelWin();
SoundManager.wheelWin(); showNotification('🎉 ' + data.received_item.name, 'success');
} else {
document.getElementById('selectedInputDisplay').style.borderColor = '#ef4444';
document.getElementById('selectedInputDisplay').style.boxShadow = '0 0 20px rgba(239,68,68,0.3)';
showToast(getRandomPhrase('lose'), 'fail');
SoundManager.wheelLose();
SoundManager.wheelLose(); showNotification('😔 Не повезло', 'error');
}
refreshBalance();
setTimeout(() => {
document.getElementById('selectedInputDisplay').style.borderColor = '';
document.getElementById('selectedInputDisplay').style.boxShadow = '';
document.getElementById('selectedTargetDisplay').style.borderColor = '';
document.getElementById('selectedTargetDisplay').style.boxShadow = '';
inventoryItems = inventoryItems.filter(i => i.id !== lockedInputId);
selectedInput = null;
selectedTarget = null;
document.getElementById('selectedInputDisplay').classList.remove('selected');
document.getElementById('selectedTargetDisplay').classList.remove('selected');
selectedInput = null; selectedTarget = null;
document.getElementById('displayInputImage').src = '/static/placeholder.png';
document.getElementById('displayInputName').textContent = 'Не выбрано';
document.getElementById('displayInputPrice').textContent = '—';
@@ -738,87 +341,23 @@
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;
refreshInventoryAjax();
document.getElementById('upgradeWheel').style.setProperty('--probability','0');
btn.disabled = true; btn.textContent = '🎲 Апгрейд'; isSpinning = false;
refreshHistory();
pointer.style.transition = 'none';
pointer.style.transform = 'rotate(0deg)';
pointer.style.transition = 'none'; pointer.style.transform = 'rotate(0deg)';
}, 2000);
};
// Запасной таймер на случай если transitionend не сработает
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 = '🎲 Апгрейд';
}
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 = '🎲 Апгрейд'; }
}
async function refreshInventoryAjax() {
try {
const resp = await fetch('/web/api/inventory/items');
const serverItems = await resp.json();
// Enrich with price/image from server data
inventoryItems = serverItems.map(item => {
const img = item.image_url || (item.item_id ? `/static/placeholder.png` : '/static/placeholder.png');
return {
id: item.id,
item_id: item.item_id,
name: item.name,
rarity: item.rarity,
price_rub: item.price || item.price_rub || 100,
image_url: img,
wear: item.wear || '',
float: item.float || item.float_value || 0
};
});
renderInventory();
} catch (e) {
console.error('Failed to refresh inventory:', e);
renderInventory();
}
}
async function logout() {
await fetch('/web/api/auth/logout', { method: 'POST' });
window.location.href = '/';
}
</script>
<script src="/static/js/sounds.js"></script>
<script src="/static/js/websocket.js"></script>
<script src="/static/js/safemode.js"></script>
<script>
function handleAchievements(data) {
if (data && data.achievements_unlocked && Array.isArray(data.achievements_unlocked) && data.achievements_unlocked.length > 0) {
data.achievements_unlocked.forEach(ach => {
const title = ach.title || ach.name || 'Достижение';
const reward = ach.reward || '';
showAchievementPopup(title, reward);
});
}
}
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 enabled = SoundManager.toggle();
document.getElementById('soundToggle').textContent = enabled ? '🔊' : '🔇';
}
document.addEventListener('DOMContentLoaded', () => {
const btn = document.getElementById('soundToggle');
if (btn) btn.textContent = localStorage.getItem('sound_enabled') !== 'false' ? '🔊' : '🔇';
});
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>
</html>