201 lines
17 KiB
HTML
201 lines
17 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="ru">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Crash — CS2 Simulator</title>
|
||
<link rel="stylesheet" href="/static/css/style.css">
|
||
<style>
|
||
#crashCanvas { width:100%; height:350px; display:block; border-radius:8px; }
|
||
.crash-multiplier { font-size:4rem; font-weight:800; text-shadow:0 0 40px rgba(34,197,94,0.5); transition:all 0.1s; }
|
||
.crash-multiplier.crashed { color:#ef4444; text-shadow:0 0 40px rgba(239,68,68,0.5); animation:shake 0.5s ease; }
|
||
.crash-multiplier.waiting { font-size:2rem; color:var(--text-dim); text-shadow:none; }
|
||
.crash-multiplier.betting { font-size:3rem; color:var(--primary); text-shadow:0 0 30px rgba(59,130,246,0.3); }
|
||
.quick-bet-btn { padding:0.4rem; background:var(--bg-dark); border:1px solid var(--border); border-radius:var(--radius-sm); color:var(--text); cursor:pointer; font-size:0.78rem; transition:all 0.2s; }
|
||
.quick-bet-btn:hover { border-color:var(--primary); }
|
||
.cashout-slider { width:100%; height:6px; background:var(--bg-dark); border-radius:3px; overflow:hidden; }
|
||
.cashout-slider-fill { height:100%; background:linear-gradient(90deg,var(--success),#f59e0b,#ef4444); border-radius:3px; transition:width 0.1s; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<nav class="navbar">
|
||
<div class="container">
|
||
<a href="/" class="logo">CS2 <span>Simulator</span></a>
|
||
<button class="mobile-toggle" id="mobileToggle">☰</button>
|
||
<div class="nav-links" id="navLinks">
|
||
<a href="/cases" class="nav-link">Кейсы</a>
|
||
<a href="/contracts" class="nav-link">Контракты</a>
|
||
<a href="/upgrade" class="nav-link">Апгрейд</a>
|
||
<a href="/crash" class="nav-link active">Crash</a>
|
||
<a href="/inventory" class="nav-link">Инвентарь</a>
|
||
<a href="/achievements" class="nav-link">Достижения</a>
|
||
<a href="/activity" 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>
|
||
</div>
|
||
</div>
|
||
</nav>
|
||
|
||
<main class="section">
|
||
<div class="container" style="max-width:960px;">
|
||
<div class="card" style="position:relative;overflow:hidden;">
|
||
<canvas id="crashCanvas"></canvas>
|
||
<div style="position:absolute;top:0;left:0;right:0;bottom:0;display:flex;flex-direction:column;align-items:center;justify-content:center;pointer-events:none;">
|
||
<div class="crash-multiplier waiting" id="crashMultiplier">Ожидание...</div>
|
||
<div class="text-dim text-sm" style="margin-top:0.5rem;" id="crashStatus">Следующий раунд скоро начнётся</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="flex gap-2" style="flex-wrap:wrap;padding:0.5rem 0;" id="crashHistory"></div>
|
||
|
||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:0.75rem;">
|
||
<div class="card">
|
||
<h3 class="display-font" style="font-size:0.85rem;margin-bottom:0.75rem;">💰 Сделать ставку</h3>
|
||
<div class="flex gap-2" style="margin-bottom:0.75rem;">
|
||
<input type="number" id="betAmount" value="100" min="1" step="1" class="form-input" style="flex:1;">
|
||
</div>
|
||
<div style="display:grid;grid-template-columns:repeat(4,1fr);gap:0.35rem;margin-bottom:0.75rem;">
|
||
<button class="quick-bet-btn" onclick="setBet(50)">50</button>
|
||
<button class="quick-bet-btn" onclick="setBet(100)">100</button>
|
||
<button class="quick-bet-btn" onclick="setBet(500)">500</button>
|
||
<button class="quick-bet-btn" onclick="setBet(1000)">1K</button>
|
||
</div>
|
||
<button class="btn btn-primary btn-block" id="betBtn" onclick="placeBet()">💰 Сделать ставку</button>
|
||
<button class="btn btn-success btn-block" id="cashoutBtn" onclick="cashOut()" style="display:none;">💰 Забрать</button>
|
||
<div id="myBetDisplay" style="display:none;margin-top:0.5rem;padding:0.4rem;background:var(--bg-dark);border-radius:var(--radius-sm);text-align:center;font-size:0.85rem;"></div>
|
||
<div class="cashout-progress" id="cashoutProgress" style="display:none;margin-top:0.5rem;">
|
||
<div class="cashout-slider"><div class="cashout-slider-fill" id="cashoutSliderFill" style="width:0%"></div></div>
|
||
<div class="flex justify-between text-xs text-dim" style="margin-top:0.2rem;"><span>1.00x</span><span id="cashoutMultLabel">1.00x</span></div>
|
||
</div>
|
||
</div>
|
||
<div class="card">
|
||
<h3 class="display-font" style="font-size:0.85rem;margin-bottom:0.75rem;">👥 Ставки</h3>
|
||
<div id="playersList"><div class="text-dim text-sm text-center" style="padding:1rem;">Ожидание ставок...</div></div>
|
||
</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 canvas = document.getElementById('crashCanvas'), ctx = canvas.getContext('2d');
|
||
const multiplierEl = document.getElementById('crashMultiplier'), statusEl = document.getElementById('crashStatus');
|
||
const betBtn = document.getElementById('betBtn'), cashoutBtn = document.getElementById('cashoutBtn');
|
||
const playersList = document.getElementById('playersList'), historyEl = document.getElementById('crashHistory');
|
||
const betInput = document.getElementById('betAmount'), myBetDisplay = document.getElementById('myBetDisplay');
|
||
const cashoutProgress = document.getElementById('cashoutProgress'), cashoutSliderFill = document.getElementById('cashoutSliderFill'), cashoutMultLabel = document.getElementById('cashoutMultLabel');
|
||
let gameState = null, myActiveBet = null, myCashoutMult = null, lastPhase = null;
|
||
|
||
function resizeCanvas() { const r = canvas.parentElement.getBoundingClientRect(); canvas.width = canvas.clientWidth || r.width-32; canvas.height = canvas.clientHeight || 350; }
|
||
resizeCanvas(); window.addEventListener('resize', resizeCanvas);
|
||
function setBet(a) { betInput.value = a; SoundManager.click(); }
|
||
function expMult(t) { return Math.exp(t*0.08); }
|
||
function timeForMult(m) { return Math.log(m)/0.08; }
|
||
|
||
let graphPoints = [];
|
||
const GRAPH_WINDOW = 15;
|
||
|
||
function drawGraph() {
|
||
const w = canvas.width, h = canvas.height, pad = 40, drawW = w-pad*2, drawH = h-pad*2;
|
||
ctx.clearRect(0,0,w,h);
|
||
if (!gameState || gameState.phase === 'waiting') { ctx.fillStyle = 'rgba(255,255,255,0.08)'; ctx.font = '20px sans-serif'; ctx.textAlign = 'center'; ctx.fillText('💥 Crash', w/2, h/2); return; }
|
||
const phase = gameState.phase, curMult = gameState.current_multiplier||1, crashPoint = gameState.crash_point||100, isCrashed = phase === 'crashed', isRunning = phase === 'running';
|
||
if (isRunning && curMult > 1) { const now = performance.now()/1000; graphPoints.push({t:now, m:curMult}); const cutoff = now - GRAPH_WINDOW; graphPoints = graphPoints.filter(p => p.t >= cutoff); if (graphPoints.length > 500) graphPoints = graphPoints.slice(-500); }
|
||
else if (isCrashed) graphPoints = [];
|
||
let maxY = 2; if (graphPoints.length > 0) maxY = Math.max(...graphPoints.map(p=>p.m)); maxY = Math.max(maxY, curMult+1, crashPoint, 3); maxY = Math.min(maxY, 200); maxY = Math.ceil(maxY);
|
||
ctx.strokeStyle = 'rgba(255,255,255,0.04)'; ctx.lineWidth = 1; ctx.font = '10px sans-serif'; ctx.textAlign = 'right';
|
||
for (let i = 1; i <= 5; i++) { const y = h-pad-(i/5)*drawH; ctx.beginPath(); ctx.moveTo(pad,y); ctx.lineTo(w-pad,y); ctx.stroke(); const multLabel = 1+(maxY-1)*(i/5); ctx.fillStyle = 'rgba(255,255,255,0.12)'; ctx.fillText(`x${multLabel.toFixed(1)}`, pad-6, y+3); }
|
||
if (graphPoints.length < 2) { if (isCrashed) { ctx.fillStyle = 'rgba(239,68,68,0.5)'; ctx.font = '36px sans-serif'; ctx.textAlign = 'center'; ctx.fillText(`💥 x${crashPoint.toFixed(2)}`, w/2, h/2); } return; }
|
||
const color = isCrashed ? '#ef4444' : '#22c55e'; const now = performance.now()/1000, windowStart = now - GRAPH_WINDOW;
|
||
ctx.beginPath(); ctx.strokeStyle = color; ctx.lineWidth = 3; ctx.shadowColor = color; ctx.shadowBlur = 10;
|
||
let first = true;
|
||
for (const p of graphPoints) { const x = pad+((p.t-windowStart)/GRAPH_WINDOW)*drawW, y = h-pad-((p.m-1)/(maxY-1))*drawH; if (first) { ctx.moveTo(x,y); first=false; } else ctx.lineTo(x,y); }
|
||
ctx.stroke(); ctx.shadowBlur = 0;
|
||
if (isRunning && curMult > 1) { const lastPt = graphPoints[graphPoints.length-1]; const x = pad+((lastPt.t-windowStart)/GRAPH_WINDOW)*drawW, y = h-pad-((lastPt.m-1)/(maxY-1))*drawH; ctx.beginPath(); ctx.arc(x,y,5,0,Math.PI*2); ctx.fillStyle = '#22c55e'; ctx.fill(); }
|
||
if (phase === 'betting') { ctx.fillStyle = 'rgba(255,255,255,0.06)'; ctx.font = '40px sans-serif'; ctx.textAlign = 'center'; ctx.fillText(`⏱ ${gameState.timer}`, w/2, h/2+10); }
|
||
}
|
||
|
||
function updateUI() {
|
||
if (!gameState) return;
|
||
const phase = gameState.phase;
|
||
if (phase === 'waiting') { multiplierEl.className = 'crash-multiplier waiting'; multiplierEl.textContent = 'Ожидание...'; statusEl.textContent = 'Следующий раунд скоро начнётся'; }
|
||
else if (phase === 'betting') { multiplierEl.className = 'crash-multiplier betting'; multiplierEl.textContent = `${gameState.timer}`; statusEl.textContent = 'Делайте ставки!'; }
|
||
else if (phase === 'running') { const mult = gameState.current_multiplier; multiplierEl.className = 'crash-multiplier'; multiplierEl.textContent = `x${mult.toFixed(2)}`; statusEl.textContent = '🚀 Взлетаем!'; if (myActiveBet && myCashoutMult === null) { cashoutSliderFill.style.width = `${Math.min(100,((mult-1)/15)*100)}%`; cashoutMultLabel.textContent = `x${mult.toFixed(2)}`; } }
|
||
else if (phase === 'crashed') { multiplierEl.className = 'crash-multiplier crashed'; multiplierEl.textContent = `💥 x${gameState.crash_point.toFixed(2)}`; statusEl.textContent = 'Краш!'; }
|
||
const canBet = phase === 'betting', canCashout = phase === 'running' && myActiveBet !== null && myCashoutMult === null;
|
||
if (canBet) { betBtn.style.display = ''; betBtn.disabled = false; betBtn.textContent = myActiveBet ? `Ставка: ${myActiveBet} ₽` : '💰 Сделать ставку'; betBtn.className = 'btn btn-primary btn-block'; if (myActiveBet) betBtn.className = 'btn btn-outline btn-block'; }
|
||
else betBtn.style.display = canCashout ? 'none' : '';
|
||
cashoutBtn.style.display = canCashout ? '' : 'none';
|
||
if (canCashout) cashoutBtn.textContent = `💰 Забрать (x${gameState.current_multiplier.toFixed(2)})`;
|
||
cashoutProgress.style.display = (phase === 'running' && myActiveBet !== null && myCashoutMult === null) ? 'block' : 'none';
|
||
if (myActiveBet !== null) {
|
||
myBetDisplay.style.display = 'block';
|
||
if (myCashoutMult !== null) myBetDisplay.innerHTML = `✅ Вывел(а) <span style="color:var(--success);font-weight:600;">${(myActiveBet*myCashoutMult).toFixed(0)} ₽</span> на x${myCashoutMult.toFixed(2)}`;
|
||
else if (phase === 'crashed') myBetDisplay.innerHTML = `❌ Проиграно <span style="color:#ef4444;font-weight:600;">${myActiveBet.toFixed(0)} ₽</span>`;
|
||
else if (phase === 'running') myBetDisplay.innerHTML = `Ставка: <span style="color:var(--primary);font-weight:600;">${myActiveBet.toFixed(0)} ₽</span> → ${(myActiveBet*gameState.current_multiplier).toFixed(0)} ₽`;
|
||
else myBetDisplay.innerHTML = `Ставка: <span style="color:var(--primary);font-weight:600;">${myActiveBet.toFixed(0)} ₽</span>`;
|
||
} else myBetDisplay.style.display = 'none';
|
||
if (lastPhase !== phase) {
|
||
if (phase === 'running') SoundManager.crashBet();
|
||
if (phase === 'crashed') { SoundManager.crashCrashed(); myActiveBet = null; myCashoutMult = null; }
|
||
if (phase === 'betting' && (lastPhase === 'crashed' || lastPhase === 'waiting')) { myActiveBet = null; myCashoutMult = null; }
|
||
lastPhase = phase;
|
||
}
|
||
updatePlayers(); updateHistory(); drawGraph();
|
||
}
|
||
|
||
function updatePlayers() {
|
||
if (!gameState) return;
|
||
const bc = gameState.bet_count||0;
|
||
if (bc === 0) { playersList.innerHTML = '<div class="text-dim text-sm text-center" style="padding:1rem;">Пока нет ставок</div>'; return; }
|
||
const cashed = gameState.cashed_out||{};
|
||
let html = `<div class="flex justify-between text-xs" style="font-weight:600;padding:0.3rem 0;border-bottom:1px solid var(--border);"><span>Ставок: ${bc}</span><span>Сумма: ${(gameState.total_bets||0).toFixed(0)} ₽</span><span>Вывели: ${Object.keys(cashed).length}</span></div>`;
|
||
if (gameState.phase === 'running' || gameState.phase === 'crashed') {
|
||
for (const [uid, m] of Object.entries(cashed)) {
|
||
const isMe = myActiveBet !== null && parseInt(uid) === 0;
|
||
html += `<div class="flex justify-between text-xs" style="padding:0.25rem 0;${isMe?'color:var(--success)':''}"><span>${isMe?'Вы':'Игрок #'+uid.slice(-4)}</span><span style="color:var(--success);font-weight:600;">✅ x${m.toFixed(2)}</span></div>`;
|
||
}
|
||
}
|
||
playersList.innerHTML = html;
|
||
}
|
||
|
||
function updateHistory() {
|
||
if (!gameState || !gameState.history) return;
|
||
historyEl.innerHTML = gameState.history.slice().reverse().map(m => `<span style="padding:0.15rem 0.45rem;border-radius:4px;font-size:0.7rem;font-weight:600;${m>2?'background:rgba(34,197,94,0.15);color:var(--success)':'background:rgba(239,68,68,0.15);color:#ef4444'}">x${m.toFixed(2)}</span>`).join('');
|
||
}
|
||
|
||
WS.on('crash_state', (data) => { if (!data.game) return; gameState = data.game; updateUI(); });
|
||
|
||
async function placeBet() {
|
||
const amount = parseInt(betInput.value);
|
||
if (!amount || amount <= 0) { SoundManager.error(); return; }
|
||
const fd = new FormData(); fd.append('amount', amount);
|
||
try {
|
||
const res = await fetch('/web/api/crash/bet', { method:'POST', body:fd });
|
||
const data = await res.json();
|
||
if (data.success) { myActiveBet = data.bet; myCashoutMult = null; SoundManager.crashBet(); document.querySelectorAll('.user-balance').forEach(el => el.textContent = `${Math.floor(data.new_balance)} ₽`); updateUI(); }
|
||
else { SoundManager.error(); alert(data.error); }
|
||
} catch(e) { SoundManager.error(); alert('Ошибка соединения'); }
|
||
}
|
||
|
||
async function cashOut() {
|
||
if (!myActiveBet || myCashoutMult !== null) return;
|
||
try {
|
||
const res = await fetch('/web/api/crash/cashout', { method:'POST' });
|
||
const data = await res.json();
|
||
if (data.success) { myCashoutMult = data.multiplier; SoundManager.crashCashout(myCashoutMult); document.querySelectorAll('.user-balance').forEach(el => el.textContent = `${Math.floor(data.new_balance)} ₽`); updateUI(); }
|
||
else { SoundManager.error(); alert(data.error); }
|
||
} catch(e) { SoundManager.error(); alert('Ошибка соединения'); }
|
||
}
|
||
|
||
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>
|
||
{% include '_activity_sidebar.html' %}
|
||
</body>
|
||
</html>
|