chore: initial commit — CS2 Simulator
This commit is contained in:
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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());
|
||||
@@ -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());
|
||||
Reference in New Issue
Block a user