Files

790 lines
40 KiB
HTML
Raw Permalink 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.min.css">
</head>
<body class="upgrade-page">
{% include "_nav.html" %}
<main class="upgrade-container">
<div class="container">
<div class="upgrade-header">
<h1>🆙 Апгрейд</h1>
<p>Рискни предметом ради более дорогого</p>
</div>
<div class="upgrade-layout">
<!-- 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>
</div>
<div class="inventory-list" 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>
<div class="selected-item-mini-name" id="displayInputName">Не выбрано</div>
<div class="selected-item-mini-price" 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>
<div class="selected-item-mini-name" id="displayTargetName">Не выбрано</div>
<div class="selected-item-mini-price" 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>
<div class="wheel-center" id="wheelProbability">0%</div>
</div>
<div class="upgrade-actions">
{% if user %}
<button class="upgrade-btn" onclick="executeUpgrade()" id="upgradeBtn" disabled>
🎲 Апгрейд
</button>
<button class="quick-btn" 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>
{% endif %}
</div>
</div>
<!-- Target -->
<div class="target-panel">
<div class="target-header">
<div class="panel-title" style="margin-bottom: 0.4rem;">
<span>🎯 Цель</span>
</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>
</div>
<div class="target-list" id="targetList">
<div class="empty-state">Выберите предмет</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>
</div>
</main>
<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;
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();
});
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 === 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('');
}
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) {}
}
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();
}
}
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('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');
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 price = item.price_rub || 100;
if (price <= inputPrice) return false;
const maxPrice = inputPrice * 19;
return price <= maxPrice;
});
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;
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));
}
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);
}
async function searchTargetItems() {
const query = document.getElementById('targetSearch').value;
currentSearchQuery = query;
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>';
}
}
function displayTargetItems(items) {
const container = document.getElementById('targetList');
if (items.length === 0) {
container.innerHTML = '<div class="empty-state">Ничего не найдено</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>
`;
}).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')) {
Notify.error('Этот предмет не подходит!');
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.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) {}
}
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);
}
}
async function refreshBalance() {
try {
const r = await fetch('/web/api/user/balance');
const d = await r.json();
if (d.success) {
const el = document.getElementById('siteBalanceDisplay');
if (el) {
el.innerHTML = Math.floor(d.balance).toLocaleString('ru') + ' ₽ <span style="font-size:0.6rem;opacity:0.5;margin-left:0.25rem">▼ депозит</span>';
}
}
} 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');
const 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('');
}
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 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 response = await fetch('/web/api/upgrade/execute', {
method: 'POST',
body: formData
});
const data = await response.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.transform = `rotate(${data.final_angle}deg)`;
// Тики колеса: быстрая фаза каждые 15°, медленная (последние 90°) — 30°
let tickLastDeg = 0, tickCumulative = 0, tickNextThreshold = 15;
let tickLastTickCumulative = 0;
let tickControlPlayed = false;
let lastTickSoundTime = 0;
const TICK_SOUND_COOLDOWN = 60;
const tickSlowStart = data.final_angle - 90;
const tickStartTime = performance.now();
const 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;
const now = performance.now();
if (tickCumulative >= tickNextThreshold && now - lastTickSoundTime >= TICK_SOUND_COOLDOWN) {
SoundManager.wheelSpin();
lastTickSoundTime = now;
tickLastTickCumulative = tickCumulative;
tickNextThreshold += tickCumulative < tickSlowStart ? 15 : 30;
}
const remaining = data.final_angle - tickCumulative;
const gap = tickCumulative - tickLastTickCumulative;
if (remaining < 10 && remaining > 0 && gap > 15 && !tickControlPlayed && now - lastTickSoundTime >= TICK_SOUND_COOLDOWN) {
SoundManager.wheelSpin();
lastTickSoundTime = now;
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;
SoundManager.stopAll();
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()}`;
document.getElementById('selectedTargetDisplay').style.borderColor = '#10b981';
document.getElementById('selectedTargetDisplay').style.boxShadow = '0 0 20px rgba(16,185,129,0.3)';
Notify.success(`${getRandomPhrase('win')} ${data.received_item.name}`);
SoundManager.wheelWin();
} else {
document.getElementById('selectedInputDisplay').style.borderColor = '#ef4444';
document.getElementById('selectedInputDisplay').style.boxShadow = '0 0 20px rgba(239,68,68,0.3)';
Notify.error(getRandomPhrase('lose'));
SoundManager.wheelLose();
}
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');
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;
refreshInventoryAjax();
refreshHistory();
pointer.style.transition = 'none';
pointer.style.transform = 'rotate(0deg)';
}, 2000);
};
// Запасной таймер на случай если transitionend не сработает
setTimeout(showResult, spinDuration * 1000 + 300);
} else {
Notify.error(data.error || 'Ошибка');
isSpinning = false;
btn.disabled = false;
btn.textContent = '🎲 Апгрейд';
}
} catch (e) {
Notify.error('Ошибка соединения');
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 || 0,
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.min.js"></script>
<script src="/static/js/websocket.min.js"></script>
<script src="/static/js/notifications.min.js"></script>
<script src="/static/js/safemode.js"></script>
<script src="/static/js/app.min.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);
});
}
}
</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' ? '🔊' : '🔇';
});
</script>
{% if user %}{% include '_activity_sidebar.html' %}{% endif %}
</body>
</html>