740 lines
35 KiB
HTML
740 lines
35 KiB
HTML
<!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 Simulator</a>
|
||
<div class="nav-links">
|
||
{% 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="/profile" 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="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>
|
||
{% 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-outline" id="safeModeToggle">🎙️</button>
|
||
<a href="/login" class="btn btn-outline">Войти</a>
|
||
<a href="/register" class="btn btn-primary">Регистрация</a>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
</nav>
|
||
|
||
<style>
|
||
.ct-page { max-width: 1100px; margin: 0 auto; padding: 1rem 1.5rem 3rem; }
|
||
.ct-header { text-align: center; margin-bottom: 1.5rem; }
|
||
.ct-header h1 { font-size: 1.6rem; font-family: 'Russo One', sans-serif; letter-spacing: 0.05em; }
|
||
.ct-header h1 span { color: var(--primary-color); }
|
||
.ct-header p { color: var(--text-secondary); font-size: 0.85rem; margin-top: 0.2rem; }
|
||
|
||
/* ─── Two-panel layout ─── */
|
||
.ct-layout {
|
||
display: grid;
|
||
grid-template-columns: 1fr 280px;
|
||
gap: 1rem;
|
||
align-items: start;
|
||
}
|
||
@media(max-width:800px){
|
||
.ct-layout{grid-template-columns:1fr}
|
||
.ct-sel-panel{order:-1}
|
||
}
|
||
|
||
/* ─── Left: Inventory panel ─── */
|
||
.ct-inv-panel {
|
||
background: rgba(20,21,26,0.5);
|
||
border: 1px solid var(--border-color);
|
||
border-radius: 14px;
|
||
overflow: hidden;
|
||
}
|
||
.ct-inv-toolbar {
|
||
display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center;
|
||
padding: 0.75rem 1rem;
|
||
border-bottom: 1px solid var(--border-color);
|
||
background: rgba(0,0,0,0.15);
|
||
}
|
||
.ct-inv-toolbar select, .ct-inv-toolbar input {
|
||
background: rgba(0,0,0,0.3);
|
||
border: 1px solid var(--border-color);
|
||
border-radius: 6px;
|
||
color: white;
|
||
padding: 0.35rem 0.6rem;
|
||
font-size: 0.78rem;
|
||
outline: none;
|
||
transition: border-color 0.2s;
|
||
}
|
||
.ct-inv-toolbar select:focus, .ct-inv-toolbar input:focus {
|
||
border-color: var(--primary-color);
|
||
}
|
||
.ct-inv-toolbar input {
|
||
flex: 1; min-width: 100px;
|
||
}
|
||
|
||
.ct-inv-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
|
||
gap: 0.5rem;
|
||
padding: 0.75rem;
|
||
max-height: 560px;
|
||
overflow-y: auto;
|
||
}
|
||
.ct-inv-grid::-webkit-scrollbar { width: 4px; }
|
||
.ct-inv-grid::-webkit-scrollbar-thumb { background: var(--border-color); border-radius: 2px; }
|
||
|
||
.ct-card {
|
||
position: relative;
|
||
background: rgba(0,0,0,0.2);
|
||
border: 2px solid transparent;
|
||
border-radius: 8px;
|
||
padding: 0.5rem 0.4rem;
|
||
cursor: pointer;
|
||
transition: all 0.2s cubic-bezier(0.34,1.56,0.64,1);
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 0.2rem;
|
||
user-select: none;
|
||
animation: ctCardIn 0.25s ease both;
|
||
}
|
||
@keyframes ctCardIn { from{opacity:0;transform:translateY(8px) scale(0.95)} to{opacity:1;transform:translateY(0) scale(1)} }
|
||
.ct-card:hover { border-color: rgba(255,255,255,0.1); background: rgba(255,255,255,0.03); }
|
||
.ct-card:active { transform: scale(0.96); }
|
||
.ct-card img {
|
||
width: 100%; height: 56px;
|
||
object-fit: contain; pointer-events: none;
|
||
transition: transform 0.3s;
|
||
}
|
||
.ct-card:hover img { transform: scale(1.06); }
|
||
.ct-card .ct-name {
|
||
font-size: 0.6rem; text-align: center; line-height: 1.1;
|
||
overflow: hidden; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
|
||
}
|
||
.ct-card .ct-float { font-size: 0.55rem; color: var(--text-secondary); }
|
||
|
||
.ct-card.selected {
|
||
border-color: var(--success-color);
|
||
background: rgba(16,185,129,0.08);
|
||
}
|
||
.ct-card.spent {
|
||
opacity: 0.2;
|
||
cursor: not-allowed;
|
||
filter: grayscale(0.8);
|
||
pointer-events: none;
|
||
}
|
||
.ct-card.spent::after {
|
||
content: '✔';
|
||
position: absolute; top: 4px; right: 4px;
|
||
font-size: 0.65rem; color: var(--success-color);
|
||
font-weight: 800;
|
||
}
|
||
.ct-card.blocked {
|
||
opacity: 0.25;
|
||
cursor: not-allowed;
|
||
filter: grayscale(0.8);
|
||
}
|
||
.ct-card.knife-item { border-color: rgba(255,215,0,0.12); }
|
||
.ct-card .ct-knife-badge {
|
||
position: absolute; top: 3px; left: 3px;
|
||
font-size: 0.55rem; padding: 0.05rem 0.25rem;
|
||
background: rgba(255,215,0,0.15); color: #ffd700;
|
||
border-radius: 3px; font-weight: 700;
|
||
}
|
||
|
||
/* ─── Right: Selection panel ─── */
|
||
.ct-sel-panel {
|
||
background: rgba(20,21,26,0.5);
|
||
border: 1px solid var(--border-color);
|
||
border-radius: 14px;
|
||
padding: 1rem;
|
||
position: sticky; top: 80px;
|
||
min-height: 300px;
|
||
}
|
||
.ct-sel-header {
|
||
display: flex; align-items: center; justify-content: space-between;
|
||
margin-bottom: 1rem;
|
||
}
|
||
.ct-sel-header h3 {
|
||
font-family: 'Russo One', sans-serif;
|
||
font-size: 0.9rem;
|
||
letter-spacing: 0.04em;
|
||
}
|
||
.ct-sel-header .ct-sel-count {
|
||
font-size: 1.3rem;
|
||
font-weight: 800;
|
||
font-family: 'Russo One', sans-serif;
|
||
color: var(--primary-color);
|
||
}
|
||
.ct-sel-count.full { color: var(--success-color); }
|
||
|
||
.ct-sel-grid {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 0.4rem;
|
||
margin-bottom: 1rem;
|
||
}
|
||
.ct-sel-slot {
|
||
background: rgba(0,0,0,0.15);
|
||
border: 1px dashed rgba(255,255,255,0.06);
|
||
border-radius: 8px;
|
||
padding: 0.35rem;
|
||
min-height: 52px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 0.1rem;
|
||
transition: all 0.3s;
|
||
position: relative;
|
||
}
|
||
.ct-sel-slot.fill {
|
||
border-color: rgba(255,255,255,0.1);
|
||
border-style: solid;
|
||
background: rgba(255,255,255,0.02);
|
||
}
|
||
.ct-sel-slot img {
|
||
width: 100%; height: 28px;
|
||
object-fit: contain;
|
||
}
|
||
.ct-sel-slot .s-name {
|
||
font-size: 0.48rem; text-align: center; line-height: 1.05;
|
||
overflow: hidden; text-overflow: ellipsis; white-space: nowrap; width: 100%;
|
||
}
|
||
.ct-sel-slot .s-remove {
|
||
position: absolute; top: 1px; right: 1px;
|
||
width: 16px; height: 16px; border-radius: 50%;
|
||
background: rgba(239,68,68,0.8); color: white;
|
||
border: none; font-size: 0.55rem; cursor: pointer;
|
||
display: none; align-items: center; justify-content: center;
|
||
line-height: 1;
|
||
}
|
||
.ct-sel-slot.fill .s-remove { display: flex; }
|
||
.ct-sel-slot .s-remove:hover { background: rgba(239,68,68,1); transform: scale(1.15); }
|
||
.ct-sel-slot .s-num {
|
||
font-size: 0.5rem; color: var(--text-secondary);
|
||
}
|
||
|
||
.ct-sel-actions {
|
||
display: flex; flex-direction: column; gap: 0.4rem;
|
||
}
|
||
.ct-sel-actions button {
|
||
width: 100%; padding: 0.5rem;
|
||
border: none; border-radius: 8px;
|
||
font-family: 'Russo One', sans-serif;
|
||
font-size: 0.75rem;
|
||
letter-spacing: 0.04em;
|
||
cursor: pointer;
|
||
transition: all 0.25s;
|
||
}
|
||
.ct-sel-btn {
|
||
background: linear-gradient(135deg, var(--success-color), #059669);
|
||
color: white;
|
||
}
|
||
.ct-sel-btn:hover:not(:disabled) {
|
||
filter: brightness(1.1);
|
||
box-shadow: 0 4px 20px rgba(16,185,129,0.3);
|
||
}
|
||
.ct-sel-btn:disabled {
|
||
opacity: 0.3; cursor: not-allowed;
|
||
}
|
||
.ct-sel-btn.knife {
|
||
background: linear-gradient(135deg, #ffd700, #f97316);
|
||
}
|
||
.ct-sel-btn.knife:hover:not(:disabled) {
|
||
box-shadow: 0 4px 20px rgba(255,215,0,0.3);
|
||
}
|
||
.ct-sel-clear {
|
||
background: rgba(255,255,255,0.04);
|
||
border: 1px solid var(--border-color) !important;
|
||
color: var(--text-secondary);
|
||
}
|
||
.ct-sel-clear:hover { border-color: var(--danger-color) !important; color: var(--danger-color); }
|
||
|
||
.ct-sel-hint {
|
||
text-align: center;
|
||
font-size: 0.7rem;
|
||
color: var(--text-secondary);
|
||
padding: 1.5rem 0;
|
||
}
|
||
|
||
/* ─── Modals (unchanged) ─── */
|
||
.ct-overlay {
|
||
position: fixed; inset: 0;
|
||
background: rgba(0,0,0,0.7); backdrop-filter: blur(8px);
|
||
z-index: 200; display: none; align-items: center; justify-content: center;
|
||
padding: 1rem; animation: ctOvlIn 0.25s ease;
|
||
}
|
||
.ct-overlay.open { display: flex; }
|
||
@keyframes ctOvlIn { from{opacity:0} to{opacity:1} }
|
||
|
||
.ct-paper {
|
||
background: linear-gradient(145deg, #1c1d24, #14151a);
|
||
border: 1px solid rgba(255,255,255,0.08);
|
||
border-radius: 20px; max-width: 640px; width: 100%; padding: 2rem;
|
||
position: relative; box-shadow: 0 20px 60px rgba(0,0,0,0.5);
|
||
animation: ctPaperIn 0.4s cubic-bezier(0.34,1.56,0.64,1);
|
||
}
|
||
@keyframes ctPaperIn { from{opacity:0;transform:scale(0.92) translateY(20px)} to{opacity:1;transform:scale(1) translateY(0)} }
|
||
|
||
.ct-paper-title { text-align: center; font-family:'Russo One',sans-serif; font-size:1.3rem; letter-spacing:0.08em; }
|
||
.ct-paper-sub { text-align: center; font-size:0.75rem; color:var(--text-secondary); letter-spacing:0.05em; margin-bottom:1.5rem; }
|
||
.ct-paper-line { height:1px; background:linear-gradient(90deg,transparent,rgba(255,255,255,0.08),transparent); margin-bottom:1.25rem; }
|
||
|
||
.ct-paper-grid { display:grid; grid-template-columns:repeat(5,1fr); gap:0.5rem; margin-bottom:1.25rem; }
|
||
.ct-paper-slot {
|
||
background:rgba(0,0,0,0.15); border:1px dashed rgba(255,255,255,0.06);
|
||
border-radius:8px; padding:0.35rem; min-height:60px;
|
||
display:flex; flex-direction:column; align-items:center; justify-content:center;
|
||
gap:0.15rem; transition:all 0.3s;
|
||
}
|
||
.ct-paper-slot.fill { border-color:rgba(255,255,255,0.12); border-style:solid; background:rgba(255,255,255,0.02); }
|
||
.ct-paper-slot img { width:100%; height:30px; object-fit:contain; }
|
||
.ct-paper-slot .s-name { font-size:0.5rem; text-align:center; line-height:1.1; }
|
||
|
||
.ct-sign-area { display:flex; align-items:center; gap:0.75rem; margin-bottom:0.75rem; }
|
||
.ct-sign-canvas { flex:1; border:1px dashed rgba(255,255,255,0.1); border-radius:6px; cursor:crosshair; background:#0d0e12; height:50px; }
|
||
|
||
.ct-stamp { text-align:center; font-family:'Russo One',sans-serif; font-size:1.4rem; letter-spacing:0.15em; color:var(--success-color); opacity:0; transition:opacity 0.5s; }
|
||
.ct-stamp.active { opacity:1; }
|
||
|
||
.ct-sign-btn {
|
||
width:100%; padding:0.7rem; border:none; border-radius:10px;
|
||
background:linear-gradient(135deg,var(--primary-color),#f97316); color:white;
|
||
font-family:'Russo One',sans-serif; font-size:0.85rem; letter-spacing:0.06em;
|
||
cursor:pointer; transition:all 0.25s; margin-top:0.75rem;
|
||
}
|
||
.ct-sign-btn:hover{filter:brightness(1.1);box-shadow:0 4px 24px rgba(245,158,11,0.3)}
|
||
.ct-sign-btn:disabled{opacity:0.5;cursor:not-allowed;filter:none;box-shadow:none}
|
||
|
||
.ct-result { text-align:center; }
|
||
.ct-result-img { width:160px; height:120px; object-fit:contain; margin-bottom:0.75rem; }
|
||
.ct-result-name { font-size:1.15rem; font-weight:700; margin-bottom:0.3rem; }
|
||
.ct-result-rarity { font-size:0.85rem; font-weight:600; margin-bottom:0.4rem; }
|
||
.ct-result-detail { font-size:0.75rem; color:var(--text-secondary); margin-bottom:0.15rem; }
|
||
.ct-result-btn {
|
||
margin-top:1.25rem; padding:0.6rem 1.8rem; border:none; border-radius:8px;
|
||
background:linear-gradient(135deg,var(--primary-color),#f97316); color:white;
|
||
font-family:'Russo One',sans-serif; font-size:0.85rem; cursor:pointer; transition:all 0.25s;
|
||
}
|
||
.ct-result-btn:hover{filter:brightness(1.1);box-shadow:0 4px 24px rgba(245,158,11,0.3)}
|
||
|
||
.ct-empty {
|
||
text-align: center; padding: 3rem 1rem;
|
||
color: var(--text-secondary);
|
||
}
|
||
.ct-empty h3 { margin-bottom: 0.5rem; }
|
||
.ct-empty p { font-size: 0.85rem; margin-bottom: 1rem; }
|
||
</style>
|
||
|
||
<div class="ct-page">
|
||
<div class="ct-header">
|
||
<h1>🔄 <span>Trade Up</span> Контракт</h1>
|
||
<p>Выберите 10 предметов одинаковой редкости и типа</p>
|
||
</div>
|
||
|
||
{% if inventory_items and inventory_items|length >= 10 %}
|
||
<div class="ct-layout">
|
||
<!-- Left: Inventory -->
|
||
<div class="ct-inv-panel">
|
||
<div class="ct-inv-toolbar">
|
||
<select id="ctRarityFilter" 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" 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" placeholder="🔍 Поиск..." oninput="ctRenderInv()">
|
||
<select id="ctSort" 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 style="font-size:0.7rem;color:var(--text-secondary)" id="ctInvCount"></span>
|
||
</div>
|
||
<div class="ct-inv-grid" id="ctInvGrid"></div>
|
||
</div>
|
||
|
||
<!-- Right: Selection -->
|
||
<div class="ct-sel-panel" id="ctSelPanel">
|
||
<div class="ct-sel-header">
|
||
<h3>📋 Контракт</h3>
|
||
<span class="ct-sel-count" id="ctSelCount">0/10</span>
|
||
</div>
|
||
<div class="ct-sel-grid" id="ctSelGrid">
|
||
{% for i in range(10) %}
|
||
<div class="ct-sel-slot" id="ctSlot-{{ i }}" data-idx="{{ i }}">
|
||
<span class="s-num">{{ i+1 }}</span>
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
<div class="ct-sel-hint" id="ctSelHint">Выберите предмет из инвентаря</div>
|
||
<div class="ct-sel-actions">
|
||
<button class="ct-sel-clear" onclick="ctClearAll()">✕ Сбросить</button>
|
||
<button class="ct-sel-btn" id="ctSubmitBtn" disabled onclick="ctOpenPaper()">🔄 Заключить контракт</button>
|
||
{% if knife_count >= 10 %}
|
||
<button class="ct-sel-btn knife" id="ctKnifeBtn" onclick="ctOpenKnife()">🔞 Крафт дилдок ×{{ knife_count }}</button>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{% else %}
|
||
<div class="ct-empty">
|
||
{% if user %}
|
||
<div style="font-size:2rem;margin-bottom:0.5rem">📭</div>
|
||
<h3>Недостаточно предметов</h3>
|
||
<p>Нужно минимум 10 предметов одинаковой редкости и типа</p>
|
||
<a href="/cases" class="btn btn-primary">Открыть кейсы</a>
|
||
{% else %}
|
||
<div style="font-size:2rem;margin-bottom:0.5rem">🔒</div>
|
||
<h3>Контракты доступны только авторизованным</h3>
|
||
<p>Войдите в аккаунт, чтобы просматривать инвентарь и заключать контракты</p>
|
||
<a href="/login" class="btn btn-primary">Войти</a>
|
||
<a href="/register" class="btn btn-outline">Регистрация</a>
|
||
{% endif %}
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
|
||
<!-- Модалка бумаги -->
|
||
<div class="ct-overlay" id="ctPaperModal">
|
||
<div class="ct-paper">
|
||
<div class="ct-paper-title">📜 TRADE UP CONTRACT</div>
|
||
<div class="ct-paper-sub">CS2 Weapon Case — проверка подлинности</div>
|
||
<div class="ct-paper-line"></div>
|
||
<div class="ct-paper-grid" id="ctPaperSlots"></div>
|
||
<div class="ct-paper-line"></div>
|
||
<div class="ct-sign-area">
|
||
<canvas class="ct-sign-canvas" id="ctSignCanvas" width="400" height="50"></canvas>
|
||
<button class="ct-sel-clear" onclick="ctClearSign()" style="padding:0.3rem 0.6rem">✕</button>
|
||
</div>
|
||
<div class="ct-stamp" id="ctStamp">✔ УТВЕРЖДЕНО</div>
|
||
<button class="ct-sign-btn" id="ctSignBtn" onclick="ctSign()">✍ ПОДПИСАТЬ</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Модалка результата -->
|
||
<div class="ct-overlay" id="ctResultModal">
|
||
<div class="ct-paper" style="max-width:420px">
|
||
<div class="ct-result" id="ctResultContent"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<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(); // invId -> {data, idx}
|
||
let ctMode = ''; // '' | 'knife' | 'normal'
|
||
|
||
// ─── Render inventory ──────────────────────────────────────────────────
|
||
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 class="ct-card ${it.isKnife ? 'knife-item' : ''}"
|
||
onclick="ctPick(${it.invId})"
|
||
style="animation-delay:${(i%30)*0.025}s"
|
||
data-inv="${it.invId}">
|
||
${it.isKnife ? '<div class="ct-knife-badge">🔪</div>' : ''}
|
||
<img src="${it.image || '/static/placeholder.png'}" onerror="this.src='/static/placeholder.png'" loading="lazy">
|
||
<div class="ct-name">${it.name}</div>
|
||
<div class="ct-float">★ ${it.float.toFixed(4)}</div>
|
||
</div>
|
||
`).join('');
|
||
}
|
||
|
||
// ─── Pick / Unpick ────────────────────────────────────────────────────
|
||
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 [id, v] of selected) {
|
||
v.idx = i++;
|
||
}
|
||
if (selected.size === 0) ctMode = '';
|
||
ctRenderInv();
|
||
ctRenderSel();
|
||
}
|
||
|
||
function ctClearAll() {
|
||
selected.clear();
|
||
ctMode = '';
|
||
ctRenderInv();
|
||
ctRenderSel();
|
||
}
|
||
|
||
// ─── Render selection panel ───────────────────────────────────────────
|
||
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.classList.toggle('full', selected.size === 10);
|
||
|
||
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;
|
||
|
||
// Fill slots
|
||
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.className = 'ct-sel-slot fill';
|
||
slot.innerHTML = `
|
||
<button class="s-remove" onclick="ctUnpick(${id})">✕</button>
|
||
<img src="${v.data.image || '/static/placeholder.png'}" onerror="this.src='/static/placeholder.png'">
|
||
<div class="s-name">${v.data.name}</div>
|
||
`;
|
||
} else {
|
||
slot.className = 'ct-sel-slot';
|
||
slot.innerHTML = `<span class="s-num">${i+1}</span>`;
|
||
}
|
||
}
|
||
|
||
document.getElementById('ctSubmitBtn').textContent = ctMode === 'knife' ? '🔞 Крафт дилдок' : '🔄 Заключить контракт';
|
||
}
|
||
|
||
// ─── Paper modal ──────────────────────────────────────────────────────
|
||
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.className = 'ct-paper-slot';
|
||
s.id = 'ctSlotPaper' + i;
|
||
s.innerHTML = '<span style="font-size:0.5rem;color:var(--text-secondary)">' + (i+1) + '</span>';
|
||
slots.appendChild(s);
|
||
}
|
||
|
||
document.getElementById('ctPaperModal').classList.add('open');
|
||
document.getElementById('ctStamp').classList.remove('active');
|
||
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.classList.add('fill');
|
||
s.innerHTML = `<img src="${item.image}" onerror="this.src='/static/placeholder.png'">
|
||
<div class="s-name">${item.name}</div>`;
|
||
await new Promise(r => setTimeout(r, 50 + Math.random() * 40));
|
||
}
|
||
await new Promise(r => setTimeout(r, 250));
|
||
document.getElementById('ctStamp').classList.add('active');
|
||
}
|
||
|
||
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(); Notify.error(data.error || 'Ошибка'); ctClosePaper();
|
||
}
|
||
} catch(_) { SoundManager.error(); Notify.error('Ошибка соединения'); ctClosePaper(); }
|
||
btn.disabled = false; btn.textContent = '✍ ПОДПИСАТЬ';
|
||
}
|
||
|
||
function ctShowResult(item, isKnife) {
|
||
const g = getComputedStyle(document.documentElement).getPropertyValue('--rarity-' + (item.rarity||'').toLowerCase().replace(/ /g, '-').split('-')[0]).trim() || '#ffd700';
|
||
const title = isKnife ? '🔞 КОНТРАКТ ВЫПОЛНЕН' : '🎉 КОНТРАКТ ВЫПОЛНЕН';
|
||
document.getElementById('ctResultContent').innerHTML = `
|
||
<div style="--g:${g}">
|
||
<div style="font-size:2rem;margin-bottom:0.5rem">${isKnife ? '🔞' : '🎉'}</div>
|
||
<div style="font-family:'Russo One',sans-serif;font-size:1.1rem;letter-spacing:0.04em;margin-bottom:1rem">${title}</div>
|
||
<img class="ct-result-img" src="${item.image_url || '/static/placeholder.png'}" onerror="this.src='/static/placeholder.png'" style="filter:drop-shadow(0 0 30px var(--g))">
|
||
<div class="ct-result-name">${item.name}</div>
|
||
<div class="ct-result-rarity" style="color:${g}">${item.rarity}</div>
|
||
<div class="ct-result-detail">Float: ${item.float.toFixed(6)}</div>
|
||
<div class="ct-result-detail">Шанс: ${(item.probability || 0).toFixed(2)}%</div>
|
||
<button class="ct-result-btn" onclick="ctCloseResult()">Продолжить</button>
|
||
</div>
|
||
`;
|
||
document.getElementById('ctResultModal').classList.add('open');
|
||
}
|
||
|
||
function ctClosePaper() { document.getElementById('ctPaperModal').classList.remove('open'); }
|
||
function ctCloseResult() { document.getElementById('ctResultModal').classList.remove('open'); 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 e = SoundManager.toggle();
|
||
document.getElementById('soundToggle').textContent = e ? '🔊' : '🔇';
|
||
}
|
||
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
const btn = document.getElementById('soundToggle');
|
||
if(btn) btn.textContent = localStorage.getItem('sound_enabled') !== 'false' ? '🔊' : '🔇';
|
||
ctRenderInv();
|
||
ctRenderSel();
|
||
});
|
||
|
||
document.querySelectorAll('.ct-overlay').forEach(m => {
|
||
m.addEventListener('click', e => { if(e.target === m) m.classList.remove('open'); });
|
||
});
|
||
</script>
|
||
|
||
<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>
|
||
{% if user %}{% include '_activity_sidebar.html' %}{% endif %}
|
||
</body>
|
||
</html>
|