Files
dodep-simulator/templates/crash.html
T
sasheg 47c7f17c90 RPU v2, promo/bank система, админка, оптимизации
— RPU v2: бюджет удачи, потолок (ceiling), комбек после
  проигрышей, hot/cold анализ, интеграция с promo-фазами
— Bank API: mock-банк для карты (депозит/вывод с балансом 1000₽)
— Promo-сервис: автогенерация промокодов каждый час, типы
  (card_to_site, luck_boost, ceiling_boost, free_case, reset_streak)
— Promo-фазы: LUCKY/NEUTRAL/UNLUCKY, переключение каждые 1-6ч
— Admin: история транзакций (user_history), управление промокодами,
  управление promo-фазами, API фильтров/коллекций, RPU info
— Database: WAL mode, новые поля RPU v2, TransactionLog,
  карточный баланс, статистики (streaks, ceiling, budget)
— Frontend: /deposit, /promo/activate, /withdraw, /card-balance,
  /online-count, /rpu-info, lifespan вместо startup event
— UI: user_history.html, улучшения профиля, кейсов, навигации
— Cases: обновление cases.json (1892 строки новых данных)
— Achievements: кэш коллекций, оптимизация is_custom проверок
— Sounds: дополнительные звуковые эффекты
— Upgrade: интеграция с RPU (множители, streaks)
— .gitignore: исключены .db, .bak, __pycache__
2026-07-25 13:11:08 +00:00

504 lines
25 KiB
HTML
Raw 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">
<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>
{% include "_nav.html" %}
<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/notifications.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();
const el = document.getElementById('siteBalanceDisplay');
if (el) {
el.innerHTML = Math.floor(data.new_balance).toLocaleString('ru') + ' ₽ <span style="font-size:0.6rem;opacity:0.5;margin-left:0.25rem">▼ депозит</span>';
}
updateUI();
} else {
SoundManager.error();
Notify.error(data.error);
}
} catch (e) {
SoundManager.error();
Notify.error('Ошибка соединения');
}
}
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();
const el = document.getElementById('siteBalanceDisplay');
if (el) {
el.innerHTML = Math.floor(data.new_balance).toLocaleString('ru') + ' ₽ <span style="font-size:0.6rem;opacity:0.5;margin-left:0.25rem">▼ депозит</span>';
}
updateUI();
} else {
SoundManager.error();
Notify.error(data.error);
}
} catch (e) {
SoundManager.error();
Notify.error('Ошибка соединения');
}
}
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>