chore: initial commit — CS2 Simulator

This commit is contained in:
2026-07-05 10:40:00 +00:00
commit c268557997
40 changed files with 824236 additions and 0 deletions
+518
View File
@@ -0,0 +1,518 @@
<!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>
.crash-layout { max-width: 960px; margin: 0 auto; display: flex; flex-direction: column; gap: 1rem; }
.crash-canvas-container {
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: 12px;
padding: 1rem;
position: relative;
overflow: hidden;
}
#crashCanvas { width: 100%; height: 350px; display: block; border-radius: 8px; }
.crash-overlay {
position: absolute; top: 0; left: 0; right: 0; bottom: 0;
display: flex; flex-direction: column; align-items: center; justify-content: center;
pointer-events: none;
}
.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: crashShake 0.5s ease; }
.crash-multiplier.waiting { font-size: 2rem; color: var(--text-secondary); text-shadow: none; }
.crash-multiplier.betting { font-size: 3rem; color: var(--primary-color); text-shadow: 0 0 30px rgba(59,130,246,0.3); }
@keyframes crashShake {
0%,100%{transform:translateX(0)}10%{transform:translateX(-10px)rotate(-2deg)}30%{transform:translateX(10px)rotate(2deg)}50%{transform:translateX(-8px)rotate(-1deg)}70%{transform:translateX(8px)rotate(1deg)}90%{transform:translateX(-4px)}
}
.crash-status-text { font-size: 1rem; color: var(--text-secondary); margin-top: 0.5rem; }
.crash-controls { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; }
.crash-bet-panel { background: var(--bg-card); border: 1px solid var(--border-color); border-radius: 12px; padding: 1.5rem; }
.crash-players-panel { background: var(--bg-card); border: 1px solid var(--border-color); border-radius: 12px; padding: 1.5rem; max-height: 300px; overflow-y: auto; }
.crash-bet-input { display: flex; gap: 0.5rem; margin-bottom: 1rem; }
.crash-bet-input input { flex: 1; padding: 0.75rem; background: var(--bg-dark); border: 1px solid var(--border-color); border-radius: 8px; color: var(--text-color); font-size: 1.1rem; }
.quick-bet-btns { display: grid; grid-template-columns: repeat(4,1fr); gap: 0.5rem; margin-bottom: 1rem; }
.quick-bet-btn { padding: 0.5rem; background: var(--bg-dark); border: 1px solid var(--border-color); border-radius: 6px; color: var(--text-color); cursor: pointer; font-size: 0.85rem; transition: all 0.2s; }
.quick-bet-btn:hover { border-color: var(--primary-color); }
.crash-btn { width: 100%; padding: 0.75rem; border: none; border-radius: 8px; font-size: 1rem; font-weight: 600; cursor: pointer; transition: all 0.2s; }
.crash-btn.bet { background: var(--primary-color); color: white; }
.crash-btn.bet:hover { filter: brightness(1.1); }
.crash-btn.bet.placed { background: #6366f1; }
.crash-btn.cashout { background: var(--success-color); color: white; }
.crash-btn.cashout:hover { filter: brightness(1.1); }
.crash-btn:disabled { opacity: 0.5; cursor: not-allowed; }
.crash-btn.hidden { display: none; }
.player-row { display: flex; justify-content: space-between; align-items: center; padding: 0.4rem 0; border-bottom: 1px solid var(--border-color); font-size: 0.85rem; }
.player-row:last-child { border-bottom: none; }
.player-row .payout { color: var(--success-color); font-weight: 600; }
.player-row .bet-amount { color: var(--text-secondary); }
.crash-history { display: flex; gap: 0.4rem; flex-wrap: wrap; padding: 0.5rem 0; }
.crash-history-item { padding: 0.2rem 0.5rem; border-radius: 4px; font-size: 0.75rem; font-weight: 600; }
.crash-history-item.win { background: rgba(34,197,94,0.15); color: var(--success-color); }
.crash-history-item.loss { background: rgba(239,68,68,0.15); color: #ef4444; }
.cashout-progress { margin-top: 0.75rem; display: none; }
.cashout-progress.active { display: block; }
.cashout-slider { width: 100%; height: 6px; background: var(--bg-dark); border-radius: 3px; position: relative; overflow: hidden; }
.cashout-slider-fill { height: 100%; background: linear-gradient(90deg, var(--success-color), #f59e0b, #ef4444); border-radius: 3px; transition: width 0.1s; }
.cashout-label { display: flex; justify-content: space-between; font-size: 0.75rem; color: var(--text-secondary); margin-top: 0.25rem; }
.crash-my-bet { margin-top: 0.5rem; padding: 0.5rem; background: var(--bg-dark); border-radius: 6px; text-align: center; font-size: 0.85rem; }
.crash-my-bet .bet-highlight { color: var(--primary-color); font-weight: 600; }
.crash-my-bet .win-highlight { color: var(--success-color); font-weight: 600; }
.crash-my-bet .lose-highlight { color: #ef4444; font-weight: 600; }
</style>
</head>
<body>
<nav class="navbar">
<div class="container">
<a href="/" class="logo">🎮 CS2 Simulator</a>
<div class="nav-links">
<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="toggleSound()" class="btn btn-outline" id="soundToggle">🔊</button>
<button onclick="toggleSafeMode()" class="btn btn-outline" id="safeModeToggle">🎙️</button>
<button onclick="logout()" class="btn btn-outline">Выйти</button>
</div>
</div>
</nav>
<main class="container" style="padding: 2rem 1rem;">
<div class="crash-layout">
<div class="crash-canvas-container">
<canvas id="crashCanvas"></canvas>
<div class="crash-overlay">
<div class="crash-multiplier waiting" id="crashMultiplier">Ожидание...</div>
<div class="crash-status-text" id="crashStatus">Следующий раунд скоро начнётся</div>
</div>
</div>
<div class="crash-history" id="crashHistory"></div>
<div class="crash-controls">
<div class="crash-bet-panel">
<h3>💰 Сделать ставку</h3>
<div class="crash-bet-input">
<input type="number" id="betAmount" value="100" min="1" step="1">
</div>
<div class="quick-bet-btns">
<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="crash-btn bet" id="betBtn" onclick="placeBet()">Сделать ставку</button>
<button class="crash-btn cashout hidden" id="cashoutBtn" onclick="cashOut()">💰 Забрать</button>
<div id="myBetDisplay" class="crash-my-bet" style="display:none;"></div>
<div class="cashout-progress" id="cashoutProgress">
<div class="cashout-slider">
<div class="cashout-slider-fill" id="cashoutSliderFill" style="width:0%"></div>
</div>
<div class="cashout-label">
<span>1.00x</span>
<span id="cashoutMultLabel">1.00x</span>
</div>
</div>
</div>
<div class="crash-players-panel">
<h3>👥 Ставки</h3>
<div id="playersList">
<div style="color: var(--text-secondary); text-align: center; 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');
const ctx = canvas.getContext('2d');
const multiplierEl = document.getElementById('crashMultiplier');
const statusEl = document.getElementById('crashStatus');
const betBtn = document.getElementById('betBtn');
const cashoutBtn = document.getElementById('cashoutBtn');
const playersList = document.getElementById('playersList');
const historyEl = document.getElementById('crashHistory');
const betInput = document.getElementById('betAmount');
const myBetDisplay = document.getElementById('myBetDisplay');
const cashoutProgress = document.getElementById('cashoutProgress');
const cashoutSliderFill = document.getElementById('cashoutSliderFill');
const cashoutMultLabel = document.getElementById('cashoutMultLabel');
let gameState = null;
let myActiveBet = null; // текущая ставка в этом раунде
let myCashoutMult = null; // на каком множителе вывел (null = не вывел)
let lastPhase = null;
function resizeCanvas() {
const rect = canvas.parentElement.getBoundingClientRect();
canvas.width = canvas.clientWidth || rect.width - 32;
canvas.height = canvas.clientHeight || 350;
}
resizeCanvas();
window.addEventListener('resize', resizeCanvas);
function setBet(amount) { betInput.value = amount; 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;
const 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;
const curMult = gameState.current_multiplier || 1;
const crashPoint = gameState.crash_point || 100;
const isCrashed = phase === 'crashed';
const 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 = [];
}
// Определяем диапазон мультипликаторов для оси Y
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;
const gridLines = 5;
ctx.font = '10px sans-serif';
ctx.textAlign = 'right';
for (let i = 1; i <= gridLines; i++) {
const y = h - pad - (i / gridLines) * drawH;
ctx.beginPath(); ctx.moveTo(pad, y); ctx.lineTo(w - pad, y); ctx.stroke();
const multLabel = 1 + (maxY - 1) * (i / gridLines);
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;
const 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;
const 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;
const 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();
ctx.shadowColor = '#22c55e';
ctx.shadowBlur = 20;
ctx.beginPath();
ctx.arc(x, y, 8, 0, Math.PI * 2);
ctx.strokeStyle = 'rgba(34,197,94,0.4)';
ctx.lineWidth = 2;
ctx.stroke();
ctx.shadowBlur = 0;
}
// Беттинг фаза — таймер
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) {
const prog = Math.min(100, ((mult - 1) / 15) * 100);
cashoutSliderFill.style.width = `${prog}%`;
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';
const canCashout = phase === 'running' && myActiveBet !== null && myCashoutMult === null;
betBtn.classList.toggle('hidden', !canBet && !(phase === 'betting' && myActiveBet));
if (canBet) {
betBtn.disabled = false;
betBtn.textContent = myActiveBet ? `Ставка: ${myActiveBet}` : '💰 Сделать ставку';
betBtn.className = 'crash-btn bet' + (myActiveBet ? ' placed' : '');
}
cashoutBtn.classList.toggle('hidden', !canCashout);
if (canCashout) {
cashoutBtn.disabled = false;
cashoutBtn.textContent = `💰 Забрать (x${gameState.current_multiplier.toFixed(2)})`;
}
cashoutProgress.classList.toggle('active', phase === 'running' && myActiveBet !== null && myCashoutMult === null);
// Статус моей ставки
if (myActiveBet !== null) {
if (myCashoutMult !== null) {
const payout = myActiveBet * myCashoutMult;
myBetDisplay.style.display = 'block';
myBetDisplay.innerHTML = `
✅ Вывел(а) <span class="win-highlight">${payout.toFixed(0)} ₽</span>
на x${myCashoutMult.toFixed(2)}
`;
} else if (phase === 'crashed') {
myBetDisplay.style.display = 'block';
myBetDisplay.innerHTML = `
❌ Проиграно <span class="lose-highlight">${myActiveBet.toFixed(0)} ₽</span>
`;
} else if (phase === 'running') {
const potential = myActiveBet * gameState.current_multiplier;
myBetDisplay.style.display = 'block';
myBetDisplay.innerHTML = `
Ставка: <span class="bet-highlight">${myActiveBet.toFixed(0)} ₽</span>
${potential.toFixed(0)}
`;
} else {
myBetDisplay.style.display = 'block';
myBetDisplay.innerHTML = `
Ставка: <span class="bet-highlight">${myActiveBet.toFixed(0)} ₽</span>
`;
}
} else {
myBetDisplay.style.display = 'none';
}
// Звуки на смену фазы
if (lastPhase !== phase) {
if (phase === 'running') SoundManager.crashBet();
if (phase === 'crashed') SoundManager.crashCrashed();
lastPhase = phase;
}
updatePlayers();
updateHistory();
drawGraph();
}
function updatePlayers() {
if (!gameState) return;
const bc = gameState.bet_count || 0;
if (bc === 0) {
playersList.innerHTML = '<div style="color:var(--text-secondary);text-align:center;padding:1rem;">Пока нет ставок</div>';
return;
}
const cashed = gameState.cashed_out || {};
const cashCount = Object.keys(cashed).length;
let html = `<div class="player-row" style="font-weight:600;border-bottom:2px solid var(--border-color);">
<span>Ставок: ${bc}</span>
<span>Сумма: ${(gameState.total_bets || 0).toFixed(0)} ₽</span>
<span>Вывели: ${cashCount}</span>
</div>`;
if (gameState.phase === 'running' || gameState.phase === 'crashed') {
const mult = gameState.current_multiplier || gameState.crash_point;
for (const [uid, m] of Object.entries(cashed)) {
const isMe = myActiveBet !== null && parseInt(uid) === 0;
html += `<div class="player-row" style="${isMe ? 'color:var(--success-color)' : ''}">
<span>${isMe ? 'Вы' : 'Игрок #' + uid.slice(-4)}</span>
<span class="payout">✅ x${m.toFixed(2)}</span>
</div>`;
}
}
playersList.innerHTML = html;
}
function updateHistory() {
if (!gameState || !gameState.history) return;
historyEl.innerHTML = gameState.history.slice().reverse().map(m => {
const isWin = m > 2;
return `<span class="crash-history-item ${isWin ? 'win' : 'loss'}">x${m.toFixed(2)}</span>`;
}).join('');
}
// WebSocket events
WS.on('crash_state', (data) => {
if (!data.game) return;
gameState = data.game;
// При краше сбрасываем мою ставку т.к. раунд закончился
if (gameState.phase === 'crashed' && lastPhase === 'running') {
// Если не вывел и краш — ставка проиграна, но стейт остается
}
if (gameState.phase === 'waiting' || gameState.phase === 'betting') {
// Новый раунд — если у меня была ставка, её уже нет (проиграна/выведена)
if (lastPhase === 'crashed' || lastPhase === 'running') {
if (myCashoutMult === null && myActiveBet !== null && gameState.phase === 'betting') {
// Ставка проиграна в прошлом раунде
}
}
if (gameState.phase === 'betting') {
// Если начался новый раунд betting и мы не обнулились —
// это новый раунд. Старая ставка аннулируется.
if (lastPhase === 'crashed' || lastPhase === 'waiting') {
myActiveBet = null;
myCashoutMult = null;
}
}
}
updateUI();
});
async function placeBet() {
const amount = parseInt(betInput.value);
if (!amount || amount <= 0) {
SoundManager.error();
return;
}
const formData = new FormData();
formData.append('amount', amount);
try {
const res = await fetch('/web/api/crash/bet', { method: 'POST', body: formData });
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();
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 logout() {
await fetch('/web/api/auth/logout', { method: 'POST' });
window.location.href = '/';
}
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>
{% include '_activity_sidebar.html' %}
</body>
</html>