Revert "UI redesign: CS2 gambling aesthetic, glassmorphism, responsive layout, enhanced sounds, admin panel overhaul"
This reverts commit 4b0fc594b4.
This commit is contained in:
+640
-179
@@ -6,30 +6,30 @@
|
||||
<title>Апгрейд — CS2 Simulator</title>
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<body class="upgrade-page">
|
||||
<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">
|
||||
<a href="/" class="logo">🎮 CS2 Simulator</a>
|
||||
<div class="nav-links">
|
||||
{% 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="toggleSafeMode()" class="btn btn-ghost" id="safeModeToggle" title="Режим записи">🎙️</button>
|
||||
<button onclick="logout()" class="btn btn-ghost">Выйти</button>
|
||||
<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>
|
||||
{% 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="/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="/login" class="btn btn-outline">Войти</a>
|
||||
<a href="/register" class="btn btn-primary">Регистрация</a>
|
||||
{% endif %}
|
||||
@@ -37,303 +37,700 @@
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main class="section">
|
||||
<main class="upgrade-container">
|
||||
<div class="container">
|
||||
<div class="section-header">
|
||||
<h1 class="section-title">🆙 Апгрейд</h1>
|
||||
<p class="section-subtitle">Рискни предметом ради более дорогого</p>
|
||||
<div class="upgrade-header">
|
||||
<h1>🆙 Апгрейд</h1>
|
||||
<p>Рискни предметом ради более дорогого</p>
|
||||
</div>
|
||||
|
||||
<div style="display:grid;grid-template-columns:1fr 320px 1fr;gap:0.75rem;align-items:start;">
|
||||
<div class="upgrade-layout">
|
||||
<!-- 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 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 style="max-height:400px;overflow-y:auto;padding:0.5rem;" id="inventoryList"></div>
|
||||
<div class="inventory-list" 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 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="text-xs truncate" id="displayInputName">Не выбрано</div>
|
||||
<div class="text-xs text-primary" id="displayInputPrice">—</div>
|
||||
<div class="selected-item-mini-name" id="displayInputName">Не выбрано</div>
|
||||
<div class="selected-item-mini-price" 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 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="text-xs truncate" id="displayTargetName">Не выбрано</div>
|
||||
<div class="text-xs text-primary" id="displayTargetPrice">—</div>
|
||||
<div class="selected-item-mini-name" id="displayTargetName">Не выбрано</div>
|
||||
<div class="selected-item-mini-price" 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 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 style="display:flex;gap:0.5rem;justify-content:center;">
|
||||
<div class="upgrade-actions">
|
||||
{% 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>
|
||||
<button class="upgrade-btn" onclick="executeUpgrade()" id="upgradeBtn" disabled>
|
||||
🎲 Апгрейд
|
||||
</button>
|
||||
<button class="quick-btn" 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 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 id="toastContainer" class="toast-container"></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 class="target-panel">
|
||||
<div class="target-header">
|
||||
<div class="panel-title" style="margin-bottom: 0.4rem;">
|
||||
<span>🎯 Цель</span>
|
||||
</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>
|
||||
<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 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 class="target-list" id="targetList">
|
||||
<div class="empty-state">Выберите предмет</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 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 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';
|
||||
// === Фразы для побед/поражений ===
|
||||
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);
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => { loadInitialTargets(); renderHistory({{ recent_upgrades|tojson }}); renderInventory(); });
|
||||
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);
|
||||
|
||||
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('');
|
||||
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 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) {}
|
||||
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), 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(); }
|
||||
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 };
|
||||
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; currentSort = 'closest';
|
||||
document.querySelectorAll('.filter-chip').forEach(b => b.classList.remove('active'));
|
||||
document.querySelector('.filter-chip[onclick*="closest"]')?.classList.add('active');
|
||||
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 (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; });
|
||||
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 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;}
|
||||
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 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));
|
||||
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('.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); }
|
||||
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() {
|
||||
currentSearchQuery = document.getElementById('targetSearch').value;
|
||||
const query = document.getElementById('targetSearch').value;
|
||||
currentSearchQuery = query;
|
||||
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>'; }
|
||||
|
||||
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) { container.innerHTML = '<div class="text-dim text-sm text-center" style="padding:2rem;">Ничего не найдено</div>'; return; }
|
||||
|
||||
if (items.length === 0) {
|
||||
container.innerHTML = '<div class="empty-state">Ничего не найдено</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>`;
|
||||
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) {
|
||||
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);
|
||||
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.getElementById('displayTargetPrice').textContent = `${price.toLocaleString()} ₽`;
|
||||
document.getElementById('selectedTargetDisplay').classList.add('selected');
|
||||
|
||||
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) {}
|
||||
|
||||
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.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); }
|
||||
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) 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'), count = document.getElementById('historyCount');
|
||||
const grid = document.getElementById('historyGrid');
|
||||
const 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('');
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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');
|
||||
|
||||
// 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 = '🎰 Крутим…';
|
||||
|
||||
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 r = await fetch('/web/api/upgrade/execute', { method:'POST', body:fd }); const data = await r.json();
|
||||
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.15,1.0)`;
|
||||
pointer.style.transition = `transform ${spinDuration}s cubic-bezier(0.4, 0.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;
|
||||
|
||||
// Тики колеса: быстрая фаза каждые 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 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]); }
|
||||
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;
|
||||
}
|
||||
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()} ₽`;
|
||||
SoundManager.wheelWin(); showNotification('🎉 ' + data.received_item.name, 'success');
|
||||
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();
|
||||
} else {
|
||||
SoundManager.wheelLose(); showNotification('😔 Не повезло', 'error');
|
||||
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();
|
||||
}
|
||||
|
||||
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;
|
||||
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 = '—';
|
||||
@@ -341,23 +738,87 @@
|
||||
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;
|
||||
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)';
|
||||
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 = '🎲 Апгрейд'; }
|
||||
|
||||
// Запасной таймер на случай если 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 = '🎲 Апгрейд';
|
||||
}
|
||||
}
|
||||
|
||||
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||'')); }
|
||||
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);
|
||||
});
|
||||
}
|
||||
}
|
||||
</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' ? '🔊' : '🔇'; });
|
||||
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>
|
||||
</html>
|
||||
Reference in New Issue
Block a user