Files
dodep-simulator/templates/contracts.html
T

364 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>Контракты — CS2 Simulator</title>
<link rel="stylesheet" href="/static/css/style.css">
</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">
{% if user %}
<a href="/cases" class="nav-link">Кейсы</a>
<a href="/contracts" class="nav-link active">Контракты</a>
<a href="/upgrade" class="nav-link">Апгрейд</a>
<a href="/crash" class="nav-link">Crash</a>
<a href="/inventory" class="nav-link">Инвентарь</a>
<a href="/achievements" 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>
{% else %}
<a href="/cases" class="nav-link">Кейсы</a>
<a href="/contracts" class="nav-link active">Контракты</a>
<a href="/upgrade" class="nav-link">Апгрейд</a>
<button onclick="toggleSafeMode()" class="btn btn-ghost" id="safeModeToggle" title="Режим записи">🎙️</button>
<a href="/login" class="btn btn-outline">Войти</a>
<a href="/register" class="btn btn-primary">Регистрация</a>
{% endif %}
</div>
</div>
</nav>
<div class="section">
<div class="container" style="max-width:1100px;">
<div class="section-header" style="margin-bottom:1rem;">
<div>
<h1 class="section-title">🔄 Trade Up Контракт</h1>
<p class="section-subtitle">Выберите 10 предметов одинаковой редкости и типа</p>
</div>
</div>
{% if inventory_items and inventory_items|length >= 10 %}
<div style="display:grid;grid-template-columns:1fr 260px;gap:0.75rem;align-items:start;">
<!-- Inventory -->
<div class="card" style="padding:0;overflow:hidden;">
<div style="display:flex;flex-wrap:wrap;gap:0.35rem;align-items:center;padding:0.6rem 0.75rem;border-bottom:1px solid var(--border);background:rgba(0,0,0,0.1);">
<select id="ctRarityFilter" class="form-select" style="width:auto;min-width:120px;font-size:0.78rem;padding:0.3rem 0.5rem;" onchange="ctRenderInv()">
<option value="">Все редкости</option>
{% set rarities = inventory_items|map(attribute='rarity')|unique|list %}
{% for r in rarities %}
<option value="{{ r }}">{{ r }}</option>
{% endfor %}
</select>
<select id="ctTypeFilter" class="form-select" style="width:auto;min-width:100px;font-size:0.78rem;padding:0.3rem 0.5rem;" onchange="ctRenderInv()">
<option value="">Все типы</option>
{% set types = inventory_items|map(attribute='type')|unique|list %}
{% for t in types %}
<option value="{{ t }}">{{ t }}</option>
{% endfor %}
</select>
<input type="text" id="ctSearch" class="form-input" placeholder="🔍 Поиск..." style="flex:1;min-width:100px;font-size:0.78rem;padding:0.3rem 0.5rem;" oninput="ctRenderInv()">
<select id="ctSort" class="form-select" style="width:auto;font-size:0.78rem;padding:0.3rem 0.5rem;" onchange="ctRenderInv()">
<option value="default">По умолч.</option>
<option value="price_asc">Цена ↑</option>
<option value="price_desc">Цена ↓</option>
<option value="name">Имя</option>
<option value="float">Float</option>
</select>
<span class="text-xs text-dim" id="ctInvCount"></span>
</div>
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(100px,1fr));gap:0.4rem;padding:0.6rem;max-height:520px;overflow-y:auto;" id="ctInvGrid"></div>
</div>
<!-- Selection Panel -->
<div class="card" style="position:sticky;top:80px;min-height:300px;" id="ctSelPanel">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:0.75rem;">
<span class="display-font" style="font-size:0.85rem;">📋 Контракт</span>
<span style="font-size:1.2rem;font-weight:700;font-family:var(--font-display);color:var(--primary);" id="ctSelCount" class="">0/10</span>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:0.35rem;margin-bottom:0.75rem;" id="ctSelGrid">
{% for i in range(10) %}
<div style="background:rgba(0,0,0,0.12);border:1px dashed rgba(255,255,255,0.06);border-radius:8px;padding:0.3rem;min-height:48px;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:0.1rem;transition:all 0.3s;" class="ct-sel-slot" id="ctSlot-{{ i }}" data-idx="{{ i }}">
<span style="font-size:0.5rem;color:var(--text-dim);">{{ i+1 }}</span>
</div>
{% endfor %}
</div>
<div class="text-xs text-dim text-center" style="padding:1rem 0;display:block;" id="ctSelHint">Выберите предмет из инвентаря</div>
<div style="display:flex;flex-direction:column;gap:0.35rem;">
<button class="btn btn-ghost btn-sm" onclick="ctClearAll()" style="color:var(--text-dim);border:1px solid var(--border);">✕ Сбросить</button>
<button class="btn btn-success btn-sm" id="ctSubmitBtn" disabled onclick="ctOpenPaper()">🔄 Заключить контракт</button>
{% if knife_count >= 10 %}
<button class="btn btn-sm" id="ctKnifeBtn" onclick="ctOpenKnife()" style="background:linear-gradient(135deg,#ffd700,#f97316);color:#000;">🔞 Крафт дилдок ×{{ knife_count }}</button>
{% endif %}
</div>
</div>
</div>
{% if not user %}
<div class="empty-state">
<div class="empty-state-icon">🔒</div>
<div class="empty-state-title">Контракты доступны только авторизованным</div>
<div class="empty-state-desc">Войдите в аккаунт, чтобы просматривать инвентарь и заключать контракты</div>
<div class="flex gap-2 justify-center">
<a href="/login" class="btn btn-primary">Войти</a>
<a href="/register" class="btn btn-outline">Регистрация</a>
</div>
</div>
{% endif %}
{% else %}
<div class="empty-state">
{% if user %}
<div style="font-size:2rem;margin-bottom:0.5rem;">📭</div>
<div class="empty-state-title">Недостаточно предметов</div>
<div class="empty-state-desc">Нужно минимум 10 предметов одинаковой редкости и типа</div>
<a href="/cases" class="btn btn-primary">Открыть кейсы</a>
{% else %}
<div style="font-size:2rem;margin-bottom:0.5rem;">🔒</div>
<div class="empty-state-title">Контракты доступны только авторизованным</div>
<div class="empty-state-desc">Войдите в аккаунт, чтобы просматривать инвентарь и заключать контракты</div>
<a href="/login" class="btn btn-primary">Войти</a>
<a href="/register" class="btn btn-outline">Регистрация</a>
{% endif %}
</div>
{% endif %}
</div>
</div>
<!-- Paper Modal -->
<div class="modal-overlay" id="ctPaperModal" style="display:none;">
<div class="modal" style="max-width:500px;">
<div class="modal-header">
<div class="modal-title">📜 TRADE UP CONTRACT</div>
<button class="modal-close" onclick="ctClosePaper()"></button>
</div>
<div class="modal-body">
<p class="text-xs text-dim text-center" style="margin-bottom:1rem;">CS2 Weapon Case — проверка подлинности</p>
<div style="display:grid;grid-template-columns:repeat(5,1fr);gap:0.35rem;margin-bottom:1rem;" id="ctPaperSlots">
{% for i in range(10) %}
<div style="background:rgba(0,0,0,0.12);border:1px dashed rgba(255,255,255,0.06);border-radius:8px;padding:0.35rem;min-height:56px;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:0.1rem;" class="ct-paper-slot" id="ctSlotPaper{{ i }}"><span style="font-size:0.5rem;color:var(--text-dim)">{{ i+1 }}</span></div>
{% endfor %}
</div>
<div style="display:flex;align-items:center;gap:0.5rem;margin-bottom:0.5rem;">
<canvas id="ctSignCanvas" width="400" height="50" style="flex:1;border:1px dashed rgba(255,255,255,0.1);border-radius:6px;cursor:crosshair;background:#0d0e12;height:50px;"></canvas>
<button class="btn btn-ghost btn-sm" onclick="ctClearSign()" style="flex-shrink:0;"></button>
</div>
<div class="display-font text-center" style="font-size:1.2rem;color:var(--success);opacity:0;transition:opacity 0.5s;" id="ctStamp">✔ УТВЕРЖДЕНО</div>
<button class="btn btn-primary btn-block" id="ctSignBtn" onclick="ctSign()" style="margin-top:0.75rem;">✍ ПОДПИСАТЬ</button>
</div>
</div>
</div>
<!-- Result Modal -->
<div class="modal-overlay" id="ctResultModal" style="display:none;">
<div class="modal" style="max-width:380px;">
<div class="modal-body text-center" id="ctResultContent"></div>
</div>
</div>
<script src="/static/js/sounds.js"></script>
<script src="/static/js/websocket.js"></script>
<script src="/static/js/safemode.js"></script>
<script>
const allItems = [{% for item in inventory_items %}{invId:{{ item.inventory_id }},name:{{ item.name|tojson }},rarity:{{ item.rarity|tojson }},type:{{ item.type|tojson }},float:{{ item.float }},image:{{ item.image_url|tojson }},isKnife:{{ 1 if item.is_knife else 0 }}}{% if not loop.last %},{% endif %}{% endfor %}];
const selected = new Map();
let ctMode = '';
function ctRenderInv() {
const rarity = document.getElementById('ctRarityFilter').value;
const type = document.getElementById('ctTypeFilter').value;
const q = document.getElementById('ctSearch').value.toLowerCase();
const sort = document.getElementById('ctSort').value;
let items = allItems.filter(it => {
if (selected.has(it.invId)) return false;
if (rarity && it.rarity !== rarity) return false;
if (type && it.type !== type) return false;
if (q && !it.name.toLowerCase().includes(q)) return false;
if (ctMode === 'knife' && !it.isKnife) return false;
if (ctMode === 'normal') {
const first = selected.values().next().value;
if (first && (it.isKnife || it.rarity !== first.data.rarity || it.type !== first.data.type)) return false;
}
return true;
});
switch (sort) {
case 'price_asc': items.sort((a,b) => (a.float||0)-(b.float||0)); break;
case 'price_desc': items.sort((a,b) => (b.float||0)-(a.float||0)); break;
case 'name': items.sort((a,b) => a.name.localeCompare(b.name)); break;
case 'float': items.sort((a,b) => a.float - b.float); break;
}
document.getElementById('ctInvCount').textContent = items.length;
const grid = document.getElementById('ctInvGrid');
grid.innerHTML = items.map((it,i) => `<div style="background:rgba(0,0,0,0.15);border:2px solid transparent;border-radius:8px;padding:0.4rem 0.35rem;cursor:pointer;transition:all 0.2s;display:flex;flex-direction:column;align-items:center;gap:0.15rem;animation:slideInUp 0.25s ease both;animation-delay:${(i%30)*0.02}s;${it.isKnife?'border-color:rgba(255,215,0,0.12);':''}" onclick="ctPick(${it.invId})" onmouseover="this.style.borderColor='rgba(255,255,255,0.1)'" onmouseout="this.style.borderColor='transparent'">
${it.isKnife ? '<span style="position:absolute;top:2px;left:2px;font-size:0.5rem;padding:0.05rem 0.25rem;background:rgba(255,215,0,0.15);color:#ffd700;border-radius:3px;font-weight:700;">🔪</span>' : ''}
<img src="${it.image||'/static/placeholder.png'}" onerror="this.src='/static/placeholder.png'" loading="lazy" style="width:100%;height:48px;object-fit:contain;">
<div style="font-size:0.55rem;text-align:center;line-height:1.1;overflow:hidden;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;">${it.name}</div>
<div style="font-size:0.5rem;color:var(--text-dim);">★ ${it.float.toFixed(4)}</div>
</div>`).join('');
}
function ctPick(invId) {
const data = allItems.find(it => it.invId === invId);
if (!data) return;
if (selected.size === 0) ctMode = data.isKnife ? 'knife' : 'normal';
if (ctMode === 'knife' && !data.isKnife) return;
if (ctMode === 'normal') {
const first = selected.values().next().value;
const ref = first ? first.data : data;
if (data.isKnife || data.rarity !== ref.rarity || data.type !== ref.type) return;
}
if (selected.size >= 10) return;
selected.set(invId, { data, idx: selected.size });
ctRenderInv(); ctRenderSel();
}
function ctUnpick(invId) {
if (!selected.has(invId)) return;
selected.delete(invId);
let i = 0; for (const [,v] of selected) v.idx = i++;
if (selected.size === 0) ctMode = '';
ctRenderInv(); ctRenderSel();
}
function ctClearAll() { selected.clear(); ctMode = ''; ctRenderInv(); ctRenderSel(); }
function ctRenderSel() {
const count = document.getElementById('ctSelCount');
const hint = document.getElementById('ctSelHint');
const submitBtn = document.getElementById('ctSubmitBtn');
const knifeBtn = document.getElementById('ctKnifeBtn');
count.textContent = selected.size+'/10';
count.style.color = selected.size === 10 ? 'var(--success)' : 'var(--primary)';
if (selected.size === 0) { hint.style.display = 'block'; submitBtn.style.display = 'none'; if (knifeBtn) knifeBtn.style.display = 'none'; }
else { hint.style.display = 'none';
if (ctMode === 'knife') { submitBtn.style.display = 'none'; if (knifeBtn) knifeBtn.style.display = selected.size === 10 ? '' : 'none'; }
else { if (knifeBtn) knifeBtn.style.display = 'none'; submitBtn.style.display = selected.size === 10 ? '' : 'none'; }
}
submitBtn.disabled = selected.size !== 10;
const items = [...selected.entries()].sort((a,b) => a[1].idx - b[1].idx);
for (let i = 0; i < 10; i++) {
const slot = document.getElementById('ctSlot-'+i);
const entry = items[i];
if (entry) {
const [id, v] = entry;
slot.style.borderStyle = 'solid'; slot.style.borderColor = 'rgba(255,255,255,0.1)'; slot.style.background = 'rgba(255,255,255,0.02)';
slot.innerHTML = `<button style="position:absolute;top:1px;right:1px;width:14px;height:14px;border-radius:50%;background:rgba(239,68,68,0.8);color:white;border:none;font-size:0.45rem;cursor:pointer;display:flex;align-items:center;justify-content:center;line-height:1;" onclick="ctUnpick(${id})">✕</button>
<img src="${v.data.image||'/static/placeholder.png'}" onerror="this.src='/static/placeholder.png'" style="width:100%;height:24px;object-fit:contain;">
<div style="font-size:0.45rem;text-align:center;line-height:1.05;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;width:100%;">${v.data.name}</div>`;
} else {
slot.style.borderStyle = 'dashed'; slot.style.borderColor = 'rgba(255,255,255,0.06)'; slot.style.background = '';
slot.innerHTML = `<span style="font-size:0.5rem;color:var(--text-dim);">${i+1}</span>`;
}
}
}
function ctOpenPaper() {
if (selected.size !== 10) return;
const items = [...selected.entries()].sort((a,b) => a[1].idx - b[1].idx).map(([id,v]) => ({invId:id,name:v.data.name,rarity:v.data.rarity,float:v.data.float,image:v.data.image}));
window.__ctItems = items;
const slots = document.getElementById('ctPaperSlots');
slots.innerHTML = '';
for (let i = 0; i < 10; i++) {
const s = document.createElement('div'); s.style.cssText = 'background:rgba(0,0,0,0.12);border:1px dashed rgba(255,255,255,0.06);border-radius:8px;padding:0.35rem;min-height:56px;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:0.1rem;'; s.id = 'ctSlotPaper'+i;
s.innerHTML = '<span style="font-size:0.5rem;color:var(--text-dim)">'+(i+1)+'</span>'; slots.appendChild(s);
}
document.getElementById('ctPaperModal').style.display = 'flex';
document.getElementById('ctStamp').style.opacity = '0';
setTimeout(ctInitSign, 100);
ctAnimateSlots();
}
function ctOpenKnife() { if (selected.size !== 10) return; ctOpenPaper(); }
async function ctAnimateSlots() {
const items = window.__ctItems || [];
for (let i = 0; i < 10; i++) {
const s = document.getElementById('ctSlotPaper'+i);
const item = items[i]; s.style.borderStyle = 'solid'; s.style.borderColor = 'rgba(255,255,255,0.1)'; s.style.background = 'rgba(255,255,255,0.02)';
s.innerHTML = `<img src="${item.image}" onerror="this.src='/static/placeholder.png'" style="width:100%;height:28px;object-fit:contain;"><div style="font-size:0.48rem;text-align:center;line-height:1.1;">${item.name}</div>`;
await new Promise(r => setTimeout(r, 40 + Math.random()*30));
}
await new Promise(r => setTimeout(r, 250));
document.getElementById('ctStamp').style.opacity = '1';
}
let ctSignCtx = null;
function ctInitSign() {
const c = document.getElementById('ctSignCanvas'); const ctx = c.getContext('2d'); ctSignCtx = ctx;
ctx.fillStyle = '#0d0e12'; ctx.fillRect(0,0,c.width,c.height); ctx.strokeStyle = '#d4c5a9'; ctx.lineWidth = 2; ctx.lineCap = 'round'; ctx.lineJoin = 'round';
let drawing = false;
const s = e => { const r=c.getBoundingClientRect(); drawing=true; ctx.beginPath(); ctx.moveTo(e.clientX-r.left, e.clientY-r.top); };
const m = e => { if(!drawing)return; const r=c.getBoundingClientRect(); ctx.lineTo(e.clientX-r.left, e.clientY-r.top); ctx.stroke(); };
const st = () => drawing = false;
c.onmousedown = s; c.onmousemove = m; c.onmouseup = st; c.onmouseleave = st;
c.ontouchstart = e=>{e.preventDefault();const t=e.touches[0];const r=c.getBoundingClientRect();drawing=true;ctx.beginPath();ctx.moveTo(t.clientX-r.left,t.clientY-r.top);};
c.ontouchmove = e=>{e.preventDefault();if(!drawing)return;const t=e.touches[0];const r=c.getBoundingClientRect();ctx.lineTo(t.clientX-r.left,t.clientY-r.top);ctx.stroke();};
c.ontouchend = ()=>drawing=false;
}
function ctClearSign() { const c = document.getElementById('ctSignCanvas'); if(ctSignCtx){ctSignCtx.fillStyle='#0d0e12';ctSignCtx.fillRect(0,0,c.width,c.height);} }
async function ctSign() {
const btn = document.getElementById('ctSignBtn'); btn.disabled = true; btn.textContent = '⏳ ПОДПИСЫВАЕМ...';
SoundManager.contractSubmit();
const items = window.__ctItems || [];
const fd = new FormData(); fd.append('inventory_ids', JSON.stringify(items.map(i=>i.invId)));
try {
const res = await fetch('/web/api/contracts/submit', { method:'POST', body:fd });
const data = await res.json();
if (res.ok && data.success) {
if (typeof handleAchievements === 'function') handleAchievements(data);
SoundManager.contractResult(); ctClosePaper();
setTimeout(() => ctShowResult(data.received_item, data.is_knife_contract), 200);
} else { SoundManager.error(); alert(data.error); ctClosePaper(); }
} catch(_) { SoundManager.error(); alert('Ошибка соединения'); ctClosePaper(); }
btn.disabled = false; btn.textContent = '✍ ПОДПИСАТЬ';
}
function ctShowResult(item, isKnife) {
const g = getRarityColor(item.rarity);
const title = isKnife ? '🔞 КОНТРАКТ ВЫПОЛНЕН' : '🎉 КОНТРАКТ ВЫПОЛНЕН';
document.getElementById('ctResultContent').innerHTML = `
<div style="font-size:2rem;margin-bottom:0.5rem">${isKnife ? '🔞' : '🎉'}</div>
<div class="display-font" style="font-size:1rem;letter-spacing:0.04em;margin-bottom:1rem">${title}</div>
<img src="${item.image_url||'/static/placeholder.png'}" onerror="this.src='/static/placeholder.png'" style="width:120px;height:90px;object-fit:contain;margin-bottom:0.5rem;filter:drop-shadow(0 0 20px ${g})">
<div class="display-font" style="font-size:1rem;">${item.name}</div>
<div style="color:${g};font-weight:600;font-size:0.85rem;margin-bottom:0.3rem;">${item.rarity}</div>
<div class="text-xs text-dim">Float: ${item.float.toFixed(6)}</div>
<div class="text-xs text-dim">Шанс: ${(item.probability||0).toFixed(2)}%</div>
<button class="btn btn-primary" onclick="ctCloseResult()" style="margin-top:1rem;">Продолжить</button>
`;
document.getElementById('ctResultModal').style.display = 'flex';
}
function ctClosePaper() { document.getElementById('ctPaperModal').style.display = 'none'; }
function ctCloseResult() { document.getElementById('ctResultModal').style.display = 'none'; window.location.reload(); }
function handleAchievements(d) {
if(d && d.achievements_unlocked && Array.isArray(d.achievements_unlocked) && d.achievements_unlocked.length)
d.achievements_unlocked.forEach(a => showAchievementPopup(a.title || a.name || 'Достижение', a.reward || ''));
}
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' ? '🔊' : '🔇';
ctRenderInv(); ctRenderSel();
});
document.querySelectorAll('.modal-overlay').forEach(m => {
m.addEventListener('click', e => { if(e.target === m) m.style.display = 'none'; });
});
</script>
{% if user %}{% include '_activity_sidebar.html' %}{% endif %}
</body>
</html>