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
+155
View File
@@ -0,0 +1,155 @@
// ─── Shared CS2 Simulator Utilities ─────────────────────────────────────────
// Logout
async function logout() {
try {
await fetch('/web/api/auth/logout', { method: 'POST' });
} catch (e) {}
window.location.href = '/';
}
// Refresh balance display
async function refreshBalance() {
try {
const res = await fetch('/web/api/user/balance');
const data = await res.json();
if (data.success) {
document.querySelectorAll('.user-balance').forEach(el => {
el.textContent = `${Math.floor(data.balance)}`;
});
}
} catch (e) {}
}
// Rarity colors
function getRarityColor(rarity) {
const colors = {
'Consumer Grade': '#b0b0b0',
'Industrial Grade': '#5e98d9',
'Mil-Spec': '#4b69ff',
'Mil-Spec Grade': '#4b69ff',
'Restricted': '#8847ff',
'Classified': '#d32ce6',
'Covert': '#eb4b4b',
'Rare Special Item': '#ffd700',
'Extraordinary': '#ffd700',
};
return colors[rarity] || '#b0b0b0';
}
// Format number with spaces
function formatNumber(n) {
return n.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ' ');
}
// Debounce
function debounce(fn, ms = 300) {
let timer;
return function (...args) {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), ms);
};
}
// Escape HTML
function escapeHtml(str) {
const div = document.createElement('div');
div.textContent = str;
return div.innerHTML;
}
// Show notification
function showNotification(message, type = 'info', duration = 3000) {
const existing = document.querySelector('.notification-toast');
if (existing) existing.remove();
const colors = {
success: '#22c55e',
error: '#ef4444',
info: '#3b82f6',
warning: '#f59e0b',
};
const toast = document.createElement('div');
toast.className = 'notification-toast';
toast.style.cssText = `
position: fixed; top: 20px; right: 20px; z-index: 99999;
background: #1a1a2e; border: 1px solid ${colors[type] || colors.info};
color: white; padding: 12px 20px; border-radius: 8px;
font-size: 14px; box-shadow: 0 10px 40px rgba(0,0,0,0.5);
animation: slideInRight 0.3s ease; max-width: 400px;
`;
toast.textContent = message;
document.body.appendChild(toast);
setTimeout(() => {
toast.style.animation = 'slideOutRight 0.3s ease forwards';
setTimeout(() => toast.remove(), 300);
}, duration);
}
// Inject notification styles once
if (!document.getElementById('notificationStyles')) {
const style = document.createElement('style');
style.id = 'notificationStyles';
style.textContent = `
@keyframes slideInRight {
from { transform: translateX(120%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
@keyframes slideOutRight {
from { transform: translateX(0); opacity: 1; }
to { transform: translateX(120%); opacity: 0; }
}
`;
document.head.appendChild(style);
}
// Show achievement popup (global function used by templates)
function showAchievementPopup(title, reward) {
let popup = document.getElementById('achievementGlobalPopup');
if (!popup) {
popup = document.createElement('div');
popup.id = 'achievementGlobalPopup';
popup.style.cssText = `
position: fixed; top: 20px; left: 50%; transform: translateX(-50%);
z-index: 99999; background: linear-gradient(135deg, #1a3a1a, #0d260d);
border: 1px solid #22c55e; border-radius: 12px; padding: 1rem 1.5rem;
box-shadow: 0 10px 40px rgba(0,0,0,0.5);
animation: slideInDown 0.5s ease; max-width: 400px; width: 90%;
text-align: center;
`;
popup.innerHTML = `
<div style="font-size:0.85rem;color:#94a3b8;margin-bottom:0.25rem;">🏆 Новое достижение!</div>
<div style="font-size:1.1rem;font-weight:600;" id="globalPopupTitle"></div>
<div style="color:#22c55e;font-size:0.9rem;margin-top:0.25rem;" id="globalPopupReward"></div>
`;
document.body.appendChild(popup);
if (!document.getElementById('slideInDownStyle')) {
const s = document.createElement('style');
s.id = 'slideInDownStyle';
s.textContent = `
@keyframes slideInDown {
from { transform: translateX(-50%) translateY(-120%); opacity: 0; }
to { transform: translateX(-50%) translateY(0); opacity: 1; }
}
`;
document.head.appendChild(s);
}
}
document.getElementById('globalPopupTitle').textContent = title;
document.getElementById('globalPopupReward').textContent = reward ? `+${reward}` : '';
popup.style.display = 'block';
if (window.SoundManager) SoundManager.achievement();
setTimeout(() => {
popup.style.animation = 'slideInDown 0.5s ease reverse forwards';
setTimeout(() => {
popup.style.display = 'none';
popup.style.animation = 'slideInDown 0.5s ease';
}, 500);
}, 4000);
}
+347
View File
@@ -0,0 +1,347 @@
// ─── Safe Mode (Recording Mode for Streamers/Bloggers) ─────────────────────
// Replaces gambling terms with YouTube-safe synonyms
const SafeMode = (() => {
const STORAGE_KEY = 'safe_mode';
const DICT = [
// Currency
[/₽(?=\s*\d)/g, 'тугриков'],
[/(\d+)\s*₽/g, (_, n) => {
const num = parseInt(n);
const last = num % 10;
const lastTwo = num % 100;
if (lastTwo >= 11 && lastTwo <= 19) return n + ' тугриков';
if (last === 1) return n + ' тугрик';
if (last >= 2 && last <= 4) return n + ' тугрика';
return n + ' тугриков';
}],
[/₽(?!\d)/g, 'тугриков'],
// Cases
[/\b(открыть|открывай|открыл)\s+кейс/gi, (m) => m.replace(/кейс/i, 'бокс')],
[/\bкейс[а-я]*\b/gi, (m) => {
const lower = m.toLowerCase();
if (lower === 'кейс') return 'бокс';
if (lower === 'кейса') return 'бокса';
if (lower === 'кейсу') return 'боксу';
if (lower === 'кейсом') return 'боксом';
if (lower === 'кейсе') return 'боксе';
if (lower === 'кейсы') return 'боксы';
if (lower === 'кейсов') return 'боксов';
if (lower === 'кейсам') return 'боксам';
if (lower === 'кейсами') return 'боксами';
if (lower === 'кейсах') return 'боксах';
return m;
}],
// Case (English)
[/\bcase\b/gi, 'box'],
// Skins
[/\bскин[а-я]*\b/gi, (m) => {
const lower = m.toLowerCase();
if (lower === 'скин') return 'облик';
if (lower === 'скина') return 'облика';
if (lower === 'скину') return 'облику';
if (lower === 'скином') return 'обликом';
if (lower === 'скине') return 'облике';
if (lower === 'скины' || lower === 'скин') return 'облики';
if (lower === 'скинов') return 'обликов';
if (lower === 'скинам') return 'обликам';
if (lower === 'скинами') return 'обликами';
if (lower === 'скинах') return 'обликах';
return m;
}],
// Contract
[/\bконтракт[а-я]*\b/gi, (m) => {
const lower = m.toLowerCase();
if (lower === 'контракт') return 'сделка';
if (lower === 'контракта') return 'сделки';
if (lower === 'контракту') return 'сделке';
if (lower === 'контрактом') return 'сделкой';
if (lower === 'контракте') return 'сделке';
if (lower === 'контракты' || lower === 'контракт') return 'сделки';
if (lower === 'контрактов') return 'сделок';
if (lower === 'контрактам') return 'сделкам';
if (lower === 'контрактами') return 'сделками';
if (lower === 'контрактах') return 'сделках';
if (lower === 'контрактная' || lower === 'контрактный' || lower === 'контрактное') return 'договорная';
return m;
}],
// Upgrade
[/\bапгрейд[а-я]*\b/gi, (m) => {
if (m.toLowerCase() === 'апгрейд') return 'улучшение';
if (m.toLowerCase() === 'апгрейда') return 'улучшения';
if (m.toLowerCase() === 'апгрейду') return 'улучшению';
if (m.toLowerCase() === 'апгрейдом') return 'улучшением';
if (m.toLowerCase() === 'апгрейде') return 'улучшении';
if (m.toLowerCase() === 'апгрейды') return 'улучшения';
if (m.toLowerCase() === 'апгрейдов') return 'улучшений';
return m;
}],
[/\bupgrade\b/gi, 'upgrade'],
// Float
[/\bfloat\b/gi, 'состояние'],
// Rarity
[/\bредкост[ьи][а-я]*\b/gi, (m) => {
const lower = m.toLowerCase();
if (lower === 'редкость') return 'класс';
if (lower === 'редкости') return 'классы';
if (lower === 'редкостью') return 'классом';
if (lower === 'редкостей') return 'классов';
if (lower === 'редкостям') return 'классам';
return m;
}],
// Chance
[/\bшанс[а-я]*\b/gi, (m) => {
const lower = m.toLowerCase();
if (lower === 'шанс') return 'вероятность';
if (lower === 'шанса') return 'вероятности';
if (lower === 'шансу') return 'вероятности';
if (lower === 'шансом') return 'вероятностью';
if (lower === 'шансе') return 'вероятности';
if (lower === 'шансы') return 'вероятности';
if (lower === 'шансов') return 'вероятностей';
return m;
}],
// Bet
[/\bставк[а-я]*\b/gi, (m) => {
const lower = m.toLowerCase();
if (lower === 'ставка') return 'вход';
if (lower === 'ставки') return 'входы';
if (lower === 'ставку') return 'вход';
if (lower === 'ставкой') return 'входом';
if (lower === 'ставок') return 'входов';
if (lower === 'ставкам') return 'входам';
return m;
}],
// Win / Lose
[/\bвыигрыш[а-я]*\b/gi, (m) => {
if (m.toLowerCase() === 'выигрыш' || m.toLowerCase() === 'выигрыша') return 'приз';
if (m.toLowerCase() === 'выигрышу') return 'призу';
if (m.toLowerCase() === 'выигрышем') return 'призом';
if (m.toLowerCase() === 'выигрыше') return 'призе';
return m;
}],
[/\b(выиграл|выиграла|выиграли)\b/gi, (m) => {
if (m.toLowerCase() === 'выиграл' || m.toLowerCase() === 'выиграла') return 'получил(a)';
if (m.toLowerCase() === 'выиграли') return 'получили';
return m;
}],
// Lose
[/\bпроиграл[а-я]*\b/gi, 'не получилось'],
// Crash game
[/\b(краш|crash)\b/gi, 'риск-игра'],
// Open case
[/\bоткры[тв][а-я]+\s+кейс/gi, (m) => m.replace(/кейс/i, 'бокс')],
// Wear
[/\bизнос\b/gi, 'состояние'],
// Probability display
[/\bвероятность\b/gi, 'шанс'],
// Quick mode tooltips
[/\bбыстрый режим\b/gi, 'турбо-режим'],
// Balance
[/\bбаланс\b/gi, 'счёт'],
// Sell
[/\bпрода[жв][а-я]*\b/gi, (m) => {
const lower = m.toLowerCase();
if (lower === 'продажа' || lower === 'продать') return 'обменять';
if (lower === 'продал') return 'обменял';
if (lower === 'продайте') return 'обменяйте';
return m;
}],
// Inventory
[/\bинвентар[ьи][а-я]*\b/gi, (m) => {
const lower = m.toLowerCase();
if (lower === 'инвентарь') return 'коллекция';
if (lower === 'инвентаря') return 'коллекции';
if (lower === 'инвентаре') return 'коллекции';
if (lower === 'инвентарём') return 'коллекцией';
return m;
}],
// Trade up
[/\btrade.?up\b/gi, 'treyd-ap'],
// Premium
[/\bпремиум\b/gi, 'эксклюзив'],
// Rare names in English
[/\bcovert\b/gi, 'covert'],
[/\bclassified\b/gi, 'classified'],
[/\bconsumers?\b/gi, 'consumer'],
[/\bindustrials?\b/gi, 'industrial'],
[/\bmil-spec\b/gi, 'mil-spec'],
[/\brestricted\b/gi, 'restricted'],
];
let observer = null;
let active = false;
function isActive() {
return localStorage.getItem(STORAGE_KEY) === 'true';
}
function setActive(val) {
localStorage.setItem(STORAGE_KEY, val ? 'true' : 'false');
active = val;
}
function replaceText(node) {
if (node.nodeType === Node.TEXT_NODE) {
let text = node.textContent;
let changed = false;
for (const [pattern, replacement] of DICT) {
const newText = text.replace(pattern, replacement);
if (newText !== text) {
text = newText;
changed = true;
}
}
if (changed) {
node.textContent = text;
}
} else if (node.nodeType === Node.ELEMENT_NODE) {
// Don't process script, style, input, textarea, canvas elements
const tag = node.tagName.toLowerCase();
if (tag === 'script' || tag === 'style' || tag === 'input' ||
tag === 'textarea' || tag === 'canvas' || tag === 'svg' ||
tag === 'code' || tag === 'pre') {
return;
}
// Process title and alt attributes
if (node.hasAttribute('title')) {
let t = node.getAttribute('title');
for (const [pattern, replacement] of DICT) {
t = t.replace(pattern, replacement);
}
node.setAttribute('title', t);
}
if (node.hasAttribute('alt')) {
let a = node.getAttribute('alt');
for (const [pattern, replacement] of DICT) {
a = a.replace(pattern, replacement);
}
node.setAttribute('alt', a);
}
// Process children
for (let i = 0; i < node.childNodes.length; i++) {
replaceText(node.childNodes[i]);
}
}
}
function apply() {
replaceText(document.body);
// Update document title
if (document.title) {
let t = document.title;
for (const [pattern, replacement] of DICT) {
t = t.replace(pattern, replacement);
}
document.title = t;
}
// Update safe mode button appearance
const btn = document.getElementById('safeModeToggle');
if (btn) {
btn.textContent = active ? '🛡️' : '🎙️';
btn.title = active ? 'Режим записи активен' : 'Режим записи';
btn.style.borderColor = active ? 'var(--success-color)' : '';
}
}
function startObserver() {
if (observer) observer.disconnect();
observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
for (const node of mutation.addedNodes) {
if (node.nodeType === Node.ELEMENT_NODE) {
replaceText(node);
} else if (node.nodeType === Node.TEXT_NODE) {
replaceText(node);
}
}
}
});
observer.observe(document.body, {
childList: true,
subtree: true,
characterData: true
});
}
function toggle() {
active = !isActive();
setActive(active);
if (active) {
apply();
startObserver();
} else {
if (observer) {
observer.disconnect();
observer = null;
}
// Reload to restore original text
location.reload();
}
return active;
}
function init() {
active = isActive();
if (active) {
// Wait for DOM to be ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
apply();
startObserver();
});
} else {
apply();
startObserver();
}
}
// Set initial button state
const btn = document.getElementById('safeModeToggle');
if (btn) {
btn.textContent = active ? '🛡️' : '🎙️';
btn.title = active ? 'Режим записи активен' : 'Режим записи';
if (active) btn.style.borderColor = 'var(--success-color)';
}
}
return {
init,
toggle,
isActive
};
})();
// Global toggle function for onclick in navbars
function toggleSafeMode() {
SafeMode.toggle();
}
// Auto-init
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => SafeMode.init());
} else {
SafeMode.init();
}
+185
View File
@@ -0,0 +1,185 @@
// Sound effects system using Web Audio API - no external files needed
const SoundManager = {
_ctx: null,
_enabled: true,
_volume: 0.3,
init() {
try {
this._ctx = new (window.AudioContext || window.webkitAudioContext)();
} catch (e) {
console.warn('Web Audio API not supported');
this._enabled = false;
}
const saved = localStorage.getItem('sound_enabled');
if (saved !== null) this._enabled = saved === 'true';
},
setEnabled(on) {
this._enabled = on;
localStorage.setItem('sound_enabled', on);
},
toggle() {
this.setEnabled(!this._enabled);
return this._enabled;
},
_play(freq, duration, type = 'sine', volume = 1) {
if (!this._enabled || !this._ctx) return;
if (this._ctx.state === 'suspended') this._ctx.resume();
const osc = this._ctx.createOscillator();
const gain = this._ctx.createGain();
osc.type = type;
osc.frequency.setValueAtTime(freq, this._ctx.currentTime);
gain.gain.setValueAtTime(this._volume * volume, this._ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, this._ctx.currentTime + duration);
osc.connect(gain);
gain.connect(this._ctx.destination);
osc.start();
osc.stop(this._ctx.currentTime + duration);
},
_noise(duration, volume = 1) {
if (!this._enabled || !this._ctx) return;
if (this._ctx.state === 'suspended') this._ctx.resume();
const bufferSize = this._ctx.sampleRate * duration;
const buffer = this._ctx.createBuffer(1, bufferSize, this._ctx.sampleRate);
const data = buffer.getChannelData(0);
for (let i = 0; i < bufferSize; i++) {
data[i] = Math.random() * 2 - 1;
}
const source = this._ctx.createBufferSource();
source.buffer = buffer;
const gain = this._ctx.createGain();
gain.gain.setValueAtTime(this._volume * volume, this._ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, this._ctx.currentTime + duration);
source.connect(gain);
gain.connect(this._ctx.destination);
source.start();
},
caseOpen() {
// Descending sweep with click
this._play(800, 0.1, 'square', 0.3);
setTimeout(() => this._play(400, 0.15, 'sawtooth', 0.2), 80);
setTimeout(() => this._play(200, 0.2, 'sine', 0.15), 180);
this._noise(0.05, 0.4);
},
caseRare() {
// Ascending bright chime for rare items
this._play(523, 0.15, 'sine', 0.3);
setTimeout(() => this._play(659, 0.15, 'sine', 0.3), 100);
setTimeout(() => this._play(784, 0.15, 'sine', 0.3), 200);
setTimeout(() => this._play(1047, 0.3, 'sine', 0.4), 300);
},
caseCovert() {
// Epic sound for covert/knife
this._play(392, 0.2, 'sawtooth', 0.3);
setTimeout(() => this._play(523, 0.2, 'sawtooth', 0.3), 150);
setTimeout(() => this._play(659, 0.2, 'sawtooth', 0.3), 300);
setTimeout(() => this._play(784, 0.4, 'sine', 0.5), 450);
setTimeout(() => this._play(1047, 0.6, 'sine', 0.6), 600);
},
slotSpin() {
// Ratcheting sound for slot reels
for (let i = 0; i < 8; i++) {
setTimeout(() => {
this._play(300 + Math.random() * 400, 0.05, 'square', 0.15);
}, i * 80);
}
},
slotMatch() {
// Win jingle
this._play(523, 0.1, 'sine', 0.3);
setTimeout(() => this._play(659, 0.1, 'sine', 0.3), 100);
setTimeout(() => this._play(784, 0.1, 'sine', 0.3), 200);
setTimeout(() => this._play(1047, 0.4, 'sine', 0.5), 300);
},
contractSubmit() {
// Mechanical sound
this._play(150, 0.3, 'sawtooth', 0.2);
setTimeout(() => this._play(200, 0.2, 'square', 0.2), 200);
setTimeout(() => this._play(250, 0.1, 'square', 0.15), 350);
},
contractResult() {
// Rising success
this._play(400, 0.15, 'sine', 0.3);
setTimeout(() => this._play(600, 0.15, 'sine', 0.3), 120);
setTimeout(() => this._play(800, 0.3, 'sine', 0.4), 240);
},
wheelSpin() {
// Ticking as wheel rotates
this._play(200, 0.03, 'square', 0.1);
},
wheelWin() {
// Celebration
this._play(523, 0.15, 'sine', 0.3);
setTimeout(() => this._play(659, 0.15, 'sine', 0.3), 120);
setTimeout(() => this._play(784, 0.2, 'sine', 0.35), 240);
setTimeout(() => this._play(1047, 0.5, 'sine', 0.5), 360);
},
wheelLose() {
// Sad trombone
this._play(400, 0.2, 'sine', 0.2);
setTimeout(() => this._play(350, 0.2, 'sine', 0.2), 150);
setTimeout(() => this._play(300, 0.3, 'sine', 0.15), 300);
},
achievement() {
// Achievement unlock
this._play(659, 0.1, 'sine', 0.3);
setTimeout(() => this._play(523, 0.1, 'sine', 0.3), 100);
setTimeout(() => this._play(784, 0.1, 'sine', 0.3), 200);
setTimeout(() => this._play(659, 0.1, 'sine', 0.3), 300);
setTimeout(() => this._play(1047, 0.4, 'sine', 0.5), 400);
},
crashBet() {
this._play(300, 0.1, 'square', 0.15);
},
crashTick() {
this._play(500 + Math.random() * 500, 0.03, 'sine', 0.08);
},
crashCrashed() {
// Explosion
this._noise(0.3, 0.6);
this._play(80, 0.5, 'sawtooth', 0.4);
setTimeout(() => this._play(50, 0.8, 'sine', 0.3), 100);
},
crashCashout() {
// Cashout success
this._play(600, 0.1, 'sine', 0.3);
setTimeout(() => this._play(800, 0.1, 'sine', 0.3), 80);
setTimeout(() => this._play(1000, 0.2, 'sine', 0.4), 160);
},
click() {
this._play(800, 0.03, 'sine', 0.1);
},
error() {
this._play(200, 0.15, 'square', 0.2);
},
balanceUpdate() {
this._play(1000, 0.05, 'sine', 0.1);
}
};
// Auto-init
document.addEventListener('DOMContentLoaded', () => SoundManager.init());
+93
View File
@@ -0,0 +1,93 @@
// WebSocket client for real-time features
const WS = {
_ws: null,
_reconnectTimer: null,
_listeners: {},
_connected: false,
connect() {
if (this._ws && this._ws.readyState === WebSocket.OPEN) return;
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const url = `${protocol}//${window.location.host}/ws`;
try {
this._ws = new WebSocket(url);
} catch (e) {
console.warn('WS connection failed, retrying in 5s');
this._scheduleReconnect();
return;
}
this._ws.onopen = () => {
this._connected = true;
this._emit('connected');
};
this._ws.onclose = () => {
this._connected = false;
this._emit('disconnected');
this._scheduleReconnect();
};
this._ws.onerror = () => {
this._emit('error');
};
this._ws.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
this._emit(data.type, data);
} catch (e) {
// ignore non-JSON messages
}
};
// Keepalive ping every 30s
this._pingInterval = setInterval(() => {
if (this._ws && this._ws.readyState === WebSocket.OPEN) {
this._ws.send(JSON.stringify({ type: 'ping' }));
}
}, 30000);
},
disconnect() {
if (this._pingInterval) clearInterval(this._pingInterval);
if (this._reconnectTimer) clearTimeout(this._reconnectTimer);
if (this._ws) {
this._ws.onclose = null;
this._ws.close();
this._ws = null;
}
this._connected = false;
},
_scheduleReconnect() {
if (this._reconnectTimer) return;
this._reconnectTimer = setTimeout(() => {
this._reconnectTimer = null;
this.connect();
}, 5000);
},
on(event, callback) {
if (!this._listeners[event]) this._listeners[event] = [];
this._listeners[event].push(callback);
return () => {
this._listeners[event] = this._listeners[event].filter(cb => cb !== callback);
};
},
_emit(event, data) {
(this._listeners[event] || []).forEach(cb => cb(data));
// Also emit all for catch-all listeners
(this._listeners['*'] || []).forEach(cb => cb(event, data));
},
isConnected() {
return this._connected;
}
};
// Auto-connect on page load
document.addEventListener('DOMContentLoaded', () => WS.connect());