Загрузка данных
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>BOBITO — Симулятор перекупа телефонов</title>
<style>
*{box-sizing:border-box;margin:0;padding:0;-webkit-tap-highlight-color:transparent;}
html,body{height:100%;overflow-x:hidden;}
body{font-family:'Segoe UI',Roboto,Arial,sans-serif;background:var(--bg);color:var(--text);transition:background .3s,color .3s;}
:root, html[data-theme="dark"]{
--bg:#0c0c12; --bg2:#151520; --card:#1a1a24; --card2:#20202c;
--text:#eceef4; --muted:#9a9aab; --accent:#8b5cf6; --accent2:#a78bfa;
--green:#22c55e; --red:#ef4444; --yellow:#f59e0b; --border:#2a2a38;
--shadow:0 4px 18px rgba(0,0,0,.4);
}
html[data-theme="light"]{
--bg:#f0f0f6; --bg2:#ffffff; --card:#ffffff; --card2:#f4f4fa;
--text:#17171f; --muted:#6c6c7a; --accent:#7c3aed; --accent2:#8b5cf6;
--green:#15803d; --red:#dc2626; --yellow:#b45309; --border:#e2e2ec;
--shadow:0 4px 14px rgba(0,0,0,.1);
}
.hidden{display:none !important;}
.btn{cursor:pointer;border:none;border-radius:12px;padding:11px 18px;font-size:14px;font-weight:600;transition:transform .12s,opacity .12s,box-shadow .12s;font-family:inherit;}
.btn:active{transform:scale(.95);}
.btn-primary{background:linear-gradient(135deg,var(--accent),var(--accent2));color:#fff;box-shadow:0 4px 14px rgba(139,92,246,.35);}
.btn-green{background:var(--green);color:#fff;}
.btn-red{background:var(--red);color:#fff;}
.btn-secondary{background:var(--card2);color:var(--text);border:1px solid var(--border);}
.btn-small{padding:7px 12px;font-size:12.5px;border-radius:9px;}
.btn:disabled{opacity:.45;cursor:not-allowed;transform:none;}
input,select,textarea{font-family:inherit;background:var(--card2);border:1px solid var(--border);color:var(--text);border-radius:10px;padding:10px 12px;font-size:14px;outline:none;width:100%;}
input:focus,select:focus,textarea:focus{border-color:var(--accent);}
/* PHONE FRAME */
.phone-frame{position:relative;width:100%;height:100vh;background:var(--bg);overflow:hidden;}
#loadingScreen,#registerScreen,#app{position:absolute;inset:0;overflow-y:auto;overflow-x:hidden;}
body{background:var(--bg);}
@media(min-width:620px){
body{display:flex;align-items:flex-start;justify-content:center;padding:26px 0;background:#05050a;}
.phone-frame{width:400px;height:840px;border-radius:46px;border:10px solid #111;box-shadow:0 25px 70px rgba(0,0,0,.6);margin:0 auto;}
}
/* LOADING */
#loadingScreen{background:radial-gradient(circle at 50% 30%,#1b1430,#0c0c12 70%);display:flex;flex-direction:column;align-items:center;justify-content:center;z-index:999;}
.logo-wrap{text-align:center;}
.logo-emoji{font-size:64px;animation:floaty 2s ease-in-out infinite;}
@keyframes floaty{0%,100%{transform:translateY(0);}50%{transform:translateY(-14px);}}
.logo-text{font-size:42px;font-weight:800;letter-spacing:4px;background:linear-gradient(135deg,#a78bfa,#8b5cf6,#c4b5fd);-webkit-background-clip:text;background-clip:text;color:transparent;margin-top:10px;}
.logo-sub{color:var(--muted);margin-top:6px;font-size:13px;letter-spacing:2px;}
.loader-bar{width:220px;height:6px;background:#2a2a38;border-radius:4px;margin-top:26px;overflow:hidden;}
.loader-fill{height:100%;width:40%;background:linear-gradient(90deg,#8b5cf6,#a78bfa);border-radius:4px;animation:loading 1.1s ease-in-out infinite;}
@keyframes loading{0%{transform:translateX(-100%);}100%{transform:translateX(350%);}}
/* REGISTER */
#registerScreen{background:var(--bg);display:flex;align-items:center;justify-content:center;z-index:900;padding:20px;}
.reg-box{background:var(--card);border:1px solid var(--border);border-radius:20px;padding:34px 28px;max-width:380px;width:100%;box-shadow:var(--shadow);text-align:center;}
.reg-box h1{font-size:30px;font-weight:800;background:linear-gradient(135deg,#a78bfa,#8b5cf6);-webkit-background-clip:text;background-clip:text;color:transparent;letter-spacing:2px;}
.reg-box p{color:var(--muted);font-size:13px;margin:10px 0 20px;}
.reg-box input{margin-bottom:8px;}
.reg-error{color:var(--red);font-size:12.5px;min-height:16px;margin-bottom:10px;text-align:left;}
.reg-box .btn{width:100%;margin-top:6px;}
/* STATUS BAR + HEADER */
.top-fixed{position:sticky;top:0;z-index:55;background:var(--bg2);}
.status-bar{display:flex;justify-content:space-between;align-items:center;padding:6px 16px;font-size:12px;color:var(--muted);border-bottom:1px solid var(--border);}
.status-icons{display:flex;gap:6px;align-items:center;}
header{padding:10px 16px;border-bottom:1px solid var(--border);}
.hdr-row{display:flex;align-items:center;justify-content:space-between;gap:8px;flex-wrap:wrap;}
.hdr-logo{font-weight:800;font-size:17px;letter-spacing:1px;background:linear-gradient(135deg,#a78bfa,#8b5cf6);-webkit-background-clip:text;background-clip:text;color:transparent;}
.hdr-stats{display:flex;gap:6px;flex-wrap:wrap;}
.stat-chip{background:var(--card);border:1px solid var(--border);border-radius:10px;padding:5px 9px;font-size:11.5px;display:flex;align-items:center;gap:4px;white-space:nowrap;}
.stat-chip b{font-size:12.5px;}
.flash-up{animation:flashGreen .6s;}
.flash-down{animation:flashRed .6s;}
@keyframes flashGreen{0%{color:var(--green);}100%{color:inherit;}}
@keyframes flashRed{0%{color:var(--red);}100%{color:inherit;}}
main{flex:1;padding:14px;max-width:960px;margin:0 auto;width:100%;}
.view{animation:fadeIn .25s ease;}
@keyframes fadeIn{from{opacity:0;transform:translateY(6px);}to{opacity:1;transform:translateY(0);}}
.app-topbar{display:flex;align-items:center;gap:10px;margin-bottom:14px;}
.app-back{background:var(--card);border:1px solid var(--border);color:var(--text);width:36px;height:36px;border-radius:10px;font-size:16px;cursor:pointer;flex-shrink:0;}
.app-topbar h2{font-size:18px;}
/* HOME SCREEN */
.home-balance-card{background:linear-gradient(135deg,#2d1b5c,#7c3aed);border-radius:18px;padding:18px;color:#fff;margin-bottom:18px;box-shadow:0 8px 24px rgba(124,58,237,.3);}
.home-balance-card .lbl{font-size:12px;opacity:.85;}
.home-balance-card .val{font-size:26px;font-weight:800;margin-top:4px;}
.app-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:14px;margin-top:6px;}
@media(max-width:380px){ .app-grid{grid-template-columns:repeat(3,1fr);} }
.app-icon{background:none;border:none;display:flex;flex-direction:column;align-items:center;gap:6px;cursor:pointer;color:var(--text);padding:4px 0;}
.app-icon .ic-box{width:56px;height:56px;border-radius:16px;background:linear-gradient(135deg,var(--card),var(--card2));border:1px solid var(--border);display:flex;align-items:center;justify-content:center;font-size:26px;box-shadow:var(--shadow);transition:transform .15s;}
.app-icon:active .ic-box{transform:scale(.9);}
.app-icon .lbl{font-size:10.5px;color:var(--muted);text-align:center;}
/* FILTER BAR */
.filter-bar{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:14px;}
.filter-bar select, .filter-bar input{width:auto;min-width:120px;flex:1;}
/* GRID / CARDS */
.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(210px,1fr));gap:12px;}
.card{background:var(--card);border:1px solid var(--border);border-radius:16px;padding:14px;box-shadow:var(--shadow);transition:transform .15s;position:relative;overflow:hidden;}
.card:hover{transform:translateY(-3px);}
.card-top{display:flex;justify-content:space-between;align-items:flex-start;gap:6px;}
.phone-emoji{font-size:30px;}
.badge{background:var(--accent);color:#fff;font-size:10px;padding:3px 7px;border-radius:7px;font-weight:700;}
.badge-hot{background:var(--yellow);color:#1a1200;}
.badge-rare{background:#ec4899;}
.cond-tag{font-size:11px;padding:2px 8px;border-radius:7px;background:var(--card2);color:var(--muted);display:inline-block;margin-top:4px;}
.phone-name{font-weight:700;font-size:15px;margin-top:8px;}
.phone-brand{color:var(--muted);font-size:11.5px;}
.price-row{display:flex;justify-content:space-between;margin-top:10px;font-size:13px;}
.price-row span:last-child{font-weight:700;}
.profit-pos{color:var(--green);}
.profit-neg{color:var(--red);}
.card-actions{display:flex;gap:6px;margin-top:12px;flex-wrap:wrap;}
.card-actions .btn{flex:1;min-width:70px;}
.empty-state{text-align:center;color:var(--muted);padding:40px 16px;font-size:14px;}
.empty-state .ic{font-size:40px;display:block;margin-bottom:10px;}
/* SUBTABS */
.subtabs{display:flex;gap:6px;margin-bottom:14px;flex-wrap:wrap;}
.subtab-btn{background:var(--card2);border:1px solid var(--border);color:var(--muted);padding:8px 14px;border-radius:10px;font-size:13px;font-weight:600;cursor:pointer;position:relative;}
.subtab-btn.active{background:var(--accent);color:#fff;border-color:var(--accent);}
.badge-count{background:var(--red);color:#fff;font-size:10px;border-radius:8px;padding:1px 6px;margin-left:5px;}
/* OFFER CARD */
.offer-card{background:var(--card);border:1px solid var(--border);border-radius:16px;padding:14px;margin-bottom:12px;box-shadow:var(--shadow);}
.offer-head{display:flex;gap:10px;align-items:center;}
.avatar{font-size:28px;background:var(--card2);width:44px;height:44px;display:flex;align-items:center;justify-content:center;border-radius:50%;flex-shrink:0;}
.interest-bar{height:6px;background:var(--card2);border-radius:4px;overflow:hidden;margin-top:6px;}
.interest-fill{height:100%;background:linear-gradient(90deg,var(--accent),var(--accent2));}
.offer-row{display:flex;justify-content:space-between;margin-top:10px;font-size:13px;}
.countdown{font-weight:700;color:var(--yellow);}
.countdown.expired{color:var(--red);}
.counter-row{display:flex;gap:6px;margin-top:10px;}
.counter-row input{flex:1;}
/* BANK */
.bank-card{background:linear-gradient(135deg,#2d1b5c,#4c1d95 60%, #7c3aed);border-radius:20px;padding:24px;color:#fff;max-width:380px;margin-bottom:16px;box-shadow:0 8px 24px rgba(124,58,237,.35);position:relative;}
.bank-card .chip{font-size:28px;}
.bank-card .num{font-size:19px;letter-spacing:2px;margin-top:20px;font-weight:600;}
.bank-card .bal{margin-top:10px;font-size:22px;font-weight:800;}
.bank-card .nick{margin-top:14px;font-size:12px;opacity:.85;letter-spacing:1px;text-transform:uppercase;}
.credit-box{background:var(--card);border:1px solid var(--border);border-radius:16px;padding:16px;margin-bottom:14px;}
.progress-bar{height:8px;background:var(--card2);border-radius:5px;overflow:hidden;margin:10px 0;}
.progress-fill{height:100%;background:linear-gradient(90deg,var(--yellow),var(--red));}
.hist-item{display:flex;justify-content:space-between;padding:10px 0;border-bottom:1px solid var(--border);font-size:13px;gap:8px;}
.hist-item:last-child{border-bottom:none;}
.hist-desc .time{color:var(--muted);font-size:11px;display:block;}
.amount-pos{color:var(--green);font-weight:700;}
.amount-neg{color:var(--red);font-weight:700;}
/* ORDERS (PC) */
.order-card{background:var(--card);border:1px solid var(--border);border-radius:16px;padding:14px;margin-bottom:12px;box-shadow:var(--shadow);}
.order-card.inprogress{border-color:var(--accent);}
.req-list{display:flex;flex-wrap:wrap;gap:6px;margin:10px 0;}
.req-chip{background:var(--card2);border:1px solid var(--border);border-radius:8px;padding:4px 8px;font-size:11px;}
/* CASINO */
.casino-card{background:var(--card);border:1px solid var(--border);border-radius:16px;padding:16px;margin-bottom:14px;box-shadow:var(--shadow);}
.casino-card h3{margin-bottom:6px;font-size:16px;}
.reel-row{display:flex;gap:8px;justify-content:center;font-size:34px;margin:12px 0;}
.choice-row{display:flex;gap:8px;flex-wrap:wrap;margin:10px 0;}
.choice-btn{background:var(--card2);border:1px solid var(--border);color:var(--text);padding:8px 14px;border-radius:10px;cursor:pointer;font-weight:700;}
.choice-btn.selected{background:var(--accent);color:#fff;border-color:var(--accent);}
.bet-row{display:flex;gap:8px;margin-top:10px;flex-wrap:wrap;}
.bet-row input{flex:1;min-width:140px;}
.result-text{margin-top:10px;font-weight:700;text-align:center;min-height:20px;font-size:13.5px;}
/* BUILD MODAL */
.build-field{margin-bottom:12px;}
.build-field label{font-size:12.5px;color:var(--muted);display:block;margin-bottom:5px;}
/* SETTINGS */
.settings-block{background:var(--card);border:1px solid var(--border);border-radius:16px;padding:16px;margin-bottom:14px;}
.settings-block h3{font-size:15px;margin-bottom:10px;}
.row-between{display:flex;justify-content:space-between;align-items:center;gap:10px;margin-bottom:10px;}
.switch{position:relative;width:46px;height:26px;background:var(--card2);border:1px solid var(--border);border-radius:14px;cursor:pointer;flex-shrink:0;}
.switch.on{background:var(--accent);}
.switch .dot{position:absolute;top:2px;left:2px;width:20px;height:20px;background:#fff;border-radius:50%;transition:transform .2s;}
.switch.on .dot{transform:translateX(20px);}
.theme-toggle{display:flex;gap:8px;}
.theme-toggle button{flex:1;}
.stat-grid{display:grid;grid-template-columns:1fr 1fr;gap:10px;}
.stat-box{background:var(--card2);border-radius:12px;padding:10px 12px;}
.stat-box .lbl{font-size:11px;color:var(--muted);}
.stat-box .val{font-size:15px;font-weight:700;margin-top:3px;}
/* MODAL */
.modal-overlay{position:fixed;inset:0;background:rgba(0,0,0,.6);display:flex;align-items:center;justify-content:center;z-index:800;padding:16px;}
.modal-box{background:var(--card);border:1px solid var(--border);border-radius:18px;max-width:420px;width:100%;max-height:85vh;overflow-y:auto;box-shadow:var(--shadow);}
.modal-header{display:flex;justify-content:space-between;align-items:center;padding:16px 18px;border-bottom:1px solid var(--border);}
.modal-header h3{font-size:16px;}
.modal-close{background:none;border:none;color:var(--muted);font-size:18px;cursor:pointer;}
.modal-body{padding:16px 18px;font-size:14px;}
.modal-body p{margin-bottom:8px;color:var(--muted);}
.modal-body .field{margin-bottom:12px;}
.modal-body label{font-size:12.5px;color:var(--muted);display:block;margin-bottom:5px;}
.modal-footer{display:flex;gap:8px;padding:14px 18px;border-top:1px solid var(--border);}
.modal-footer .btn{flex:1;}
/* TOAST */
#toastContainer{position:fixed;top:14px;left:50%;transform:translateX(-50%);z-index:999;display:flex;flex-direction:column;gap:8px;width:92%;max-width:380px;}
.toast{background:var(--card);border:1px solid var(--border);border-left:4px solid var(--accent);padding:12px 14px;border-radius:10px;font-size:13.5px;box-shadow:var(--shadow);opacity:0;transform:translateY(-14px);transition:opacity .25s,transform .25s;}
.toast.show{opacity:1;transform:translateY(0);}
.toast-success{border-left-color:var(--green);}
.toast-error{border-left-color:var(--red);}
.toast-info{border-left-color:var(--accent);}
</style>
</head>
<body>
<div class="phone-frame" id="phoneFrame">
<!-- LOADING -->
<div id="loadingScreen">
<div class="logo-wrap">
<div class="logo-emoji">📱</div>
<div class="logo-text">BOBITO</div>
<div class="logo-sub">СИМУЛЯТОР ПЕРЕКУПА</div>
<div class="loader-bar"><div class="loader-fill"></div></div>
</div>
</div>
<!-- REGISTER -->
<div id="registerScreen" class="hidden">
<div class="reg-box">
<h1>BOBITO</h1>
<p>Создай профиль перекупа и начни зарабатывать 💜</p>
<input id="nickInput" maxlength="20" placeholder="Никнейм (2-20 символов)">
<div class="reg-error" id="regError"></div>
<button class="btn btn-primary" onclick="handleRegister()">Начать игру 🚀</button>
</div>
</div>
<!-- APP -->
<div id="app" class="hidden">
<div class="top-fixed">
<div class="status-bar">
<span id="statusTime">00:00</span>
<span class="status-icons">📶 🔋 <span id="statusBattery">100%</span></span>
</div>
<header>
<div class="hdr-row">
<div class="hdr-logo">📱 BOBITO</div>
<div class="hdr-stats">
<div class="stat-chip">👤 <b id="hNick">—</b></div>
<div class="stat-chip">💰 <b id="hBalance">0 ₽</b></div>
<div class="stat-chip">📦 <b id="hCount">0</b></div>
<div class="stat-chip">📈 <b id="hProfit">0 ₽</b></div>
</div>
</div>
</header>
</div>
<main>
<!-- HOME -->
<section id="view-home" class="view">
<div class="home-balance-card">
<div class="lbl">Баланс BOBITO</div>
<div class="val" id="homeBalanceVal">0 ₽</div>
</div>
<div class="app-grid">
<button class="app-icon" onclick="showView('market')"><div class="ic-box">🛒</div><span class="lbl">Перекуп</span></button>
<button class="app-icon" onclick="showView('phones')"><div class="ic-box">📦</div><span class="lbl">Телефоны</span></button>
<button class="app-icon" onclick="showView('bank')"><div class="ic-box">🏦</div><span class="lbl">Банк</span></button>
<button class="app-icon" onclick="openCreditApp()"><div class="ic-box">💳</div><span class="lbl">Кредит</span></button>
<button class="app-icon" onclick="showView('casino')"><div class="ic-box">🎰</div><span class="lbl">Казино</span></button>
<button class="app-icon" onclick="showView('pcbuilder')"><div class="ic-box">🖥️</div><span class="lbl">Сборка ПК</span></button>
<button class="app-icon" onclick="showView('partsmarket')"><div class="ic-box">🔧</div><span class="lbl">Комплектующие</span></button>
<button class="app-icon" onclick="showView('settings')"><div class="ic-box">⚙️</div><span class="lbl">Настройки</span></button>
</div>
</section>
<!-- MARKET -->
<section id="view-market" class="view hidden">
<div class="app-topbar"><button class="app-back" onclick="showView('home')">←</button><h2>🛒 Перекуп</h2></div>
<div class="filter-bar">
<select id="fBrand" onchange="renderMarket()"></select>
<select id="fCond" onchange="renderMarket()"></select>
<select id="fSort" onchange="renderMarket()">
<option value="default">По умолчанию</option>
<option value="price_asc">Цена ↑</option>
<option value="price_desc">Цена ↓</option>
<option value="profit_asc">Прибыль ↑</option>
<option value="profit_desc">Прибыль ↓</option>
</select>
<button class="btn btn-secondary btn-small" onclick="refreshMarket()">🔄 Обновить</button>
</div>
<div class="grid" id="marketGrid"></div>
</section>
<!-- PHONES -->
<section id="view-phones" class="view hidden">
<div class="app-topbar"><button class="app-back" onclick="showView('home')">←</button><h2>📦 Мои телефоны</h2></div>
<div class="subtabs">
<button class="subtab-btn active" data-sub="inv" onclick="switchPhonesSubtab('inv')">Инвентарь</button>
<button class="subtab-btn" data-sub="listings" onclick="switchPhonesSubtab('listings')">Объявления</button>
<button class="subtab-btn" data-sub="offers" onclick="switchPhonesSubtab('offers')">Предложения <span class="badge-count hidden" id="offersBadge">0</span></button>
</div>
<div id="sub-inv" class="subview"><div class="grid" id="invGrid"></div></div>
<div id="sub-listings" class="subview hidden"><div class="grid" id="listingsGrid"></div></div>
<div id="sub-offers" class="subview hidden"><div id="offersList"></div></div>
</section>
<!-- BANK -->
<section id="view-bank" class="view hidden">
<div class="app-topbar"><button class="app-back" onclick="showView('home')">←</button><h2>🏦 Банк</h2></div>
<div class="subtabs">
<button class="subtab-btn active" data-sub="card" onclick="switchBankSubtab('card')">Карта</button>
<button class="subtab-btn" data-sub="credit" onclick="switchBankSubtab('credit')">Кредиты</button>
<button class="subtab-btn" data-sub="hist" onclick="switchBankSubtab('hist')">История</button>
</div>
<div id="bank-card" class="subview">
<div class="bank-card">
<div class="chip">💳</div>
<div class="num" id="cardNum">•••• •••• •••• 0000</div>
<div class="bal" id="cardBal">0 ₽</div>
<div class="nick" id="cardNick">BOBITO HOLDER</div>
</div>
</div>
<div id="bank-credit" class="subview hidden"></div>
<div id="bank-hist" class="subview hidden">
<div class="filter-bar">
<select id="histFilter" onchange="renderHistory()">
<option value="all">Все операции</option>
<option value="buy">Покупки</option>
<option value="sell">Продажи</option>
<option value="credit">Кредиты</option>
<option value="promo">Промокоды</option>
<option value="casino">Казино</option>
<option value="pc">Сборка ПК</option>
<option value="event">События</option>
</select>
</div>
<div class="settings-block" id="historyList"></div>
</div>
</section>
<!-- CASINO -->
<section id="view-casino" class="view hidden">
<div class="app-topbar"><button class="app-back" onclick="showView('home')">←</button><h2>🎰 Казино</h2></div>
<div class="casino-card">
<h3>🎰 Слот-машина</h3>
<div class="reel-row" id="slotReels">❔ ❔ ❔</div>
<div class="bet-row"><input id="slotBet" type="number" min="100" placeholder="Ставка (мин. 100 ₽)"><button class="btn btn-primary" onclick="playSlots()">Крутить</button></div>
<div class="result-text" id="slotResult"></div>
</div>
<div class="casino-card">
<h3>🎲 Кости (угадай число)</h3>
<div class="choice-row" id="diceChoices">
<button class="choice-btn" onclick="selectDiceNum(1,this)">1</button>
<button class="choice-btn" onclick="selectDiceNum(2,this)">2</button>
<button class="choice-btn" onclick="selectDiceNum(3,this)">3</button>
<button class="choice-btn" onclick="selectDiceNum(4,this)">4</button>
<button class="choice-btn" onclick="selectDiceNum(5,this)">5</button>
<button class="choice-btn" onclick="selectDiceNum(6,this)">6</button>
</div>
<div class="bet-row"><input id="diceBet" type="number" min="100" placeholder="Ставка (мин. 100 ₽)"><button class="btn btn-primary" onclick="playDice()">Бросить (x5)</button></div>
<div class="result-text" id="diceResult"></div>
</div>
<div class="casino-card">
<h3>🪙 Монетка</h3>
<div class="choice-row" id="coinChoices">
<button class="choice-btn" onclick="selectCoinSide('heads',this)">Орёл</button>
<button class="choice-btn" onclick="selectCoinSide('tails',this)">Решка</button>
</div>
<div class="bet-row"><input id="coinBet" type="number" min="100" placeholder="Ставка (мин. 100 ₽)"><button class="btn btn-primary" onclick="playCoin()">Подбросить (x1.9)</button></div>
<div class="result-text" id="coinResult"></div>
</div>
</section>
<!-- PC BUILDER -->
<section id="view-pcbuilder" class="view hidden">
<div class="app-topbar"><button class="app-back" onclick="showView('home')">←</button><h2>🖥️ Сборка ПК</h2></div>
<div id="pcOrdersContainer"></div>
</section>
<!-- PARTS MARKET -->
<section id="view-partsmarket" class="view hidden">
<div class="app-topbar"><button class="app-back" onclick="showView('home')">←</button><h2>🔧 Комплектующие</h2></div>
<div class="filter-bar"><select id="partsCategorySelect" onchange="renderPartsMarket()"></select></div>
<div class="grid" id="partsGrid"></div>
</section>
<!-- SETTINGS -->
<section id="view-settings" class="view hidden">
<div class="app-topbar"><button class="app-back" onclick="showView('home')">←</button><h2>⚙️ Настройки</h2></div>
<div class="settings-block">
<h3>👤 Профиль</h3>
<input id="nickChangeInput" placeholder="Новый никнейм">
<div class="reg-error" id="nickChangeError"></div>
<button class="btn btn-primary btn-small" style="margin-top:8px;" onclick="changeNickname()">Сохранить ник</button>
</div>
<div class="settings-block">
<h3>🎨 Тема оформления</h3>
<div class="theme-toggle">
<button class="btn btn-secondary" id="themeDarkBtn" onclick="setTheme('dark')">🌙 Тёмная</button>
<button class="btn btn-secondary" id="themeLightBtn" onclick="setTheme('light')">☀️ Светлая</button>
</div>
</div>
<div class="settings-block">
<h3>🔊 Звук</h3>
<div class="row-between">
<span>Звуковые эффекты</span>
<div class="switch" id="soundSwitch" onclick="toggleSound()"><div class="dot"></div></div>
</div>
</div>
<div class="settings-block">
<h3>🎁 Промокод</h3>
<input id="promoInput" placeholder="Введите промокод">
<button class="btn btn-primary btn-small" style="margin-top:8px;" onclick="handlePromo()">Активировать</button>
</div>
<div class="settings-block">
<h3>📊 Статистика</h3>
<div class="stat-grid" id="statsGrid"></div>
</div>
<div class="settings-block">
<h3>💾 Сохранение</h3>
<div style="display:flex;gap:8px;flex-wrap:wrap;">
<button class="btn btn-secondary btn-small" onclick="exportSave()">⬇️ Экспорт JSON</button>
<button class="btn btn-secondary btn-small" onclick="document.getElementById('importFile').click()">⬆️ Импорт JSON</button>
<input type="file" id="importFile" accept=".json" class="hidden" onchange="importSave(event)">
</div>
</div>
<div class="settings-block">
<h3>⚠️ Опасная зона</h3>
<button class="btn btn-red btn-small" onclick="confirmReset()">Сбросить весь прогресс</button>
</div>
<div style="text-align:center;color:var(--muted);font-size:12px;margin-top:10px;">BOBITO v2.0.0</div>
</section>
</main>
</div>
<div id="toastContainer"></div>
<div id="modalOverlay" class="modal-overlay hidden">
<div class="modal-box">
<div class="modal-header"><h3 id="modalTitle"></h3><button class="modal-close" onclick="closeModal()">✕</button></div>
<div class="modal-body" id="modalBody"></div>
<div class="modal-footer" id="modalFooter"></div>
</div>
</div>
</div>
<script>
/* ================= CATALOG: PHONES ================= */
const CATALOG = [
["Apple","iPhone XR",15000],["Apple","iPhone XS",16000],["Apple","iPhone XS Max",18000],
["Apple","iPhone 11",20000],["Apple","iPhone 11 Pro",25000],["Apple","iPhone 11 Pro Max",28000],
["Apple","iPhone 12",30000],["Apple","iPhone 12 Mini",27000],["Apple","iPhone 12 Pro",35000],["Apple","iPhone 12 Pro Max",40000],
["Apple","iPhone 13",40000],["Apple","iPhone 13 Mini",36000],["Apple","iPhone 13 Pro",50000],["Apple","iPhone 13 Pro Max",55000],
["Apple","iPhone 14",55000],["Apple","iPhone 14 Plus",60000],["Apple","iPhone 14 Pro",70000],["Apple","iPhone 14 Pro Max",80000],
["Apple","iPhone 15",75000],["Apple","iPhone 15 Plus",80000],["Apple","iPhone 15 Pro",95000],["Apple","iPhone 15 Pro Max",105000],
["Apple","iPhone 16",90000],["Apple","iPhone 16 Plus",100000],["Apple","iPhone 16 Pro",115000],["Apple","iPhone 16 Pro Max",130000],
["Samsung","Galaxy A14",12000],["Samsung","Galaxy A24",15000],["Samsung","Galaxy A34",20000],["Samsung","Galaxy A54",25000],["Samsung","Galaxy A55",28000],
["Samsung","Galaxy S21",25000],["Samsung","Galaxy S22",30000],["Samsung","Galaxy S23",40000],["Samsung","Galaxy S23 Ultra",60000],
["Samsung","Galaxy S24",45000],["Samsung","Galaxy S24 Ultra",70000],["Samsung","Galaxy S10",14000],["Samsung","Galaxy Note 20",22000],
["Xiaomi","Redmi 9",8000],["Xiaomi","Redmi 10",10000],["Xiaomi","Redmi Note 11",13000],["Xiaomi","Redmi Note 12",16000],["Xiaomi","Redmi Note 13",19000],
["Xiaomi","Xiaomi 12",25000],["Xiaomi","Xiaomi 13",32000],["Xiaomi","Xiaomi 13T",35000],["Xiaomi","Xiaomi 14",45000],
["POCO","POCO X5",18000],["POCO","POCO X6",22000],["POCO","POCO F5",28000],["POCO","POCO M5",11000],
["Google","Pixel 6",22000],["Google","Pixel 6a",18000],["Google","Pixel 7",28000],["Google","Pixel 7a",24000],["Google","Pixel 8",38000],["Google","Pixel 8 Pro",50000],
["Huawei","P40",20000],["Huawei","P50",30000],["Huawei","Mate 40",28000],["Huawei","Mate 50",35000],["Huawei","Nova 11",19000],
["Honor","Honor 70",22000],["Honor","Honor 90",28000],["Honor","Magic 5",40000],["Honor","Honor X8",12000],
["Realme","Realme 10",14000],["Realme","Realme 11",17000],["Realme","GT Neo",25000],
["OnePlus","OnePlus 10",35000],["OnePlus","OnePlus 11",45000],["OnePlus","Nord 3",20000],
["Nothing","Phone (1)",25000],["Nothing","Phone (2)",35000]
];
const CONDITIONS = [
{name:"Новый", mult:0.95},
{name:"Отличное", mult:0.85},
{name:"Хорошее", mult:0.7},
{name:"Есть царапины", mult:0.55},
{name:"Сильно повреждён", mult:0.38}
];
const BUYER_NAMES=["Артём","Дима_Phone","Lena_K","MaxStore","Ольга","Sergey_82","Nastya","VovaTech","Катя","Igor_Market","Alex_Reseller","Mila","Roman_G","Таня","Denis_X","Юля87"];
const BUYER_AVATARS=["🧑","👨","👩","🧔","👨💼","👩💼","🧑💻","👨🦱","👩🦰","🧓","👱","👨🚀","👩🎤","🧑🎓","👨🔧","👩🔬"];
const PROMO_CODES=["HAHAHA","BOBITO","START"];
const SAVE_KEY="bobito_save_v2";
/* ================= CATALOG: PC PARTS ================= */
const PARTS_CATALOG = {
cpu:{label:"Процессор", icon:"🧠", items:[
{name:"Intel Core i3-10100", power:25, price:5000},
{name:"AMD Ryzen 5 5600", power:40, price:9000},
{name:"Intel Core i5-12400", power:48, price:12000},
{name:"Intel Core i5-13600K", power:65, price:19000},
{name:"AMD Ryzen 7 5800X3D", power:72, price:23000},
{name:"Intel Core i7-13700K", power:82, price:29000},
{name:"AMD Ryzen 7 7800X3D", power:90, price:35000},
{name:"Intel Core i9-13900K", power:97, price:46000}
]},
gpu:{label:"Видеокарта", icon:"🎮", items:[
{name:"GTX 1650", power:20, price:8000},
{name:"GTX 1660 Super", power:30, price:12500},
{name:"RTX 3050", power:38, price:16000},
{name:"RTX 3060", power:50, price:22000},
{name:"RTX 3070", power:63, price:31000},
{name:"RTX 4070", power:78, price:46000},
{name:"RTX 4080", power:90, price:76000},
{name:"RTX 4090", power:100, price:135000}
]},
ram:{label:"Оперативная память", icon:"💾", items:[
{name:"8GB DDR4", power:20, price:2000},
{name:"16GB DDR4", power:40, price:4000},
{name:"16GB DDR5", power:55, price:6000},
{name:"32GB DDR4", power:60, price:7500},
{name:"32GB DDR5", power:75, price:11000},
{name:"64GB DDR5", power:95, price:20000}
]},
storage:{label:"Накопитель", icon:"💽", items:[
{name:"HDD 1TB", power:15, price:2000},
{name:"SSD 256GB", power:30, price:3000},
{name:"SSD 512GB", power:45, price:5000},
{name:"SSD 1TB", power:58, price:7500},
{name:"NVMe 1TB", power:72, price:10500},
{name:"NVMe 2TB", power:90, price:18000}
]},
psu:{label:"Блок питания", icon:"🔌", items:[
{name:"450W", power:20, price:2000},
{name:"550W", power:35, price:3200},
{name:"650W", power:50, price:4500},
{name:"750W", power:65, price:6200},
{name:"850W", power:80, price:8500},
{name:"1000W Platinum", power:95, price:12000}
]},
motherboard:{label:"Материнская плата", icon:"🧩", items:[
{name:"Бюджетная материнка", power:25, price:3500},
{name:"Средняя материнка", power:55, price:7500},
{name:"Топовая материнка", power:85, price:15000}
]},
"case":{label:"Корпус", icon:"🗄️", items:[
{name:"Простой корпус", power:20, price:1800},
{name:"Корпус со стеклом", power:50, price:4000},
{name:"Премиум корпус RGB", power:80, price:8500}
]},
cooler:{label:"Охлаждение", icon:"❄️", items:[
{name:"Боксовый кулер", power:15, price:500},
{name:"Башенный кулер", power:45, price:2500},
{name:"СВО 240мм", power:85, price:7500}
]}
};
/* ================= STATE ================= */
let state=null;
let currentView="home";
let currentPhonesSub="inv";
let currentBankSub="card";
let selectedDiceNum=null;
let selectedCoinSide=null;
function defaultState(){
return {
player:null,
balance:25000,
inventory:[],
listings:[],
offers:[],
market: generateMarket(28),
history:[],
credit:null,
pcInventory:[],
pcOrders:[],
promosUsed:[],
promoBonusClaimed:false,
stats:{bought:0,sold:0,revenue:0,profit:0,bestDeal:0,mostExpensivePurchase:0,negotiations:0,expiredCredits:0,ordersCompleted:0,ordersFailed:0,casinoSpins:0,casinoProfit:0},
settings:{theme:"dark",sound:true},
version:"2.0.0"
};
}
function loadGame(){
try{
const raw = localStorage.getItem(SAVE_KEY);
if(!raw){ state = defaultState(); return; }
const parsed = JSON.parse(raw);
const def = defaultState();
state = Object.assign({}, def, parsed);
state.inventory = Array.isArray(parsed.inventory)?parsed.inventory:[];
state.listings = Array.isArray(parsed.listings)?parsed.listings:[];
state.offers = Array.isArray(parsed.offers)?parsed.offers:[];
state.market = Array.isArray(parsed.market)&&parsed.market.length?parsed.market:generateMarket(28);
state.history = Array.isArray(parsed.history)?parsed.history:[];
state.pcInventory = Array.isArray(parsed.pcInventory)?parsed.pcInventory:[];
state.pcOrders = Array.isArray(parsed.pcOrders)?parsed.pcOrders:[];
state.stats = Object.assign({}, def.stats, parsed.stats||{});
state.settings = Object.assign({}, def.settings, parsed.settings||{});
state.promosUsed = Array.isArray(parsed.promosUsed)?parsed.promosUsed:[];
}catch(e){
console.warn("Save corrupted, creating new state", e);
state = defaultState();
}
}
function save(){
try{ localStorage.setItem(SAVE_KEY, JSON.stringify(state)); }
catch(e){ toast("Ошибка сохранения прогресса","error"); }
}
/* ================= UTILS ================= */
function rand(a,b){return Math.random()*(b-a)+a;}
function randInt(a,b){return Math.floor(rand(a,b+1));}
function choice(arr){return arr[randInt(0,arr.length-1)];}
function money(n){ n=Math.round(Number(n)||0); return n.toLocaleString("ru-RU")+" ₽"; }
function fmtDate(ts){ const d=new Date(ts); return d.toLocaleString("ru-RU",{day:"2-digit",month:"2-digit",year:"numeric",hour:"2-digit",minute:"2-digit"}); }
function fmtCountdown(ms){ if(ms<=0) return "00:00:00"; const s=Math.floor(ms/1000); const h=String(Math.floor(s/3600)).padStart(2,"0"); const m=String(Math.floor((s%3600)/60)).padStart(2,"0"); const sec=String(s%60).padStart(2,"0"); return h+":"+m+":"+sec; }
function esc(s){ return String(s===undefined||s===null?"":s).replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); }
function uid(prefix){ return prefix+Date.now().toString(36)+Math.random().toString(36).slice(2,8); }
function toast(msg,type){
type = type||"info";
const c = document.getElementById("toastContainer");
const el = document.createElement("div");
el.className = "toast toast-"+type;
el.textContent = msg;
c.appendChild(el);
requestAnimationFrame(()=>el.classList.add("show"));
setTimeout(()=>{ el.classList.remove("show"); setTimeout(()=>el.remove(),300); },3200);
}
function flashBalance(positive){
const el=document.getElementById("hBalance");
if(!el) return;
el.classList.remove("flash-up","flash-down");
void el.offsetWidth;
el.classList.add(positive?"flash-up":"flash-down");
}
function playSound(type){
if(!state.settings.sound) return;
try{
const ctx = window.__audioCtx || (window.__audioCtx = new (window.AudioContext||window.webkitAudioContext)());
const o = ctx.createOscillator(); const g = ctx.createGain();
o.connect(g); g.connect(ctx.destination);
const freqs={buy:420,sell:680,deal:860,error:200,money:980};
o.frequency.value = freqs[type]||500;
g.gain.value=0.07;
o.start();
g.gain.exponentialRampToValueAtTime(0.0001, ctx.currentTime+0.25);
o.stop(ctx.currentTime+0.26);
}catch(e){}
}
/* ================= MODAL ================= */
function openModal(title,bodyHTML,buttons){
document.getElementById("modalTitle").textContent=title;
document.getElementById("modalBody").innerHTML=bodyHTML;
const footer=document.getElementById("modalFooter");
footer.innerHTML="";
(buttons||[]).forEach(b=>{
const btn=document.createElement("button");
btn.className="btn "+(b.cls||"btn-secondary");
btn.textContent=b.label;
btn.onclick=b.onClick;
footer.appendChild(btn);
});
document.getElementById("modalOverlay").classList.remove("hidden");
}
function closeModal(){ document.getElementById("modalOverlay").classList.add("hidden"); }
function confirmAction(message,onYes){
openModal("Подтверждение","<p>"+esc(message)+"</p>",[
{label:"Отмена",cls:"btn-secondary",onClick:closeModal},
{label:"Да, продолжить",cls:"btn-red",onClick:function(){ closeModal(); onYes(); }}
]);
}
/* ================= HISTORY ================= */
function addHistory(type,desc,amount){
state.history.unshift({id:uid("h"),type,desc,amount,date:Date.now()});
if(state.history.length>400) state.history.pop();
}
/* ================= MARKET (PHONES) ================= */
function createListing(){
const base = choice(CATALOG);
const cond = choice(CONDITIONS);
const marketValue = Math.max(500, Math.round(base[2]*cond.mult*rand(0.92,1.08)/10)*10);
let dealFactor = rand(0.75,1.1);
const roll = Math.random();
let hot=false;
if(roll<0.14){ dealFactor = rand(0.55,0.75); hot=true; }
else if(roll>0.88){ dealFactor = rand(1.15,1.35); }
const buyPrice = Math.max(500, Math.round(marketValue*dealFactor/10)*10);
return {
id: uid("m"),
brand: base[0], name: base[1], baseValue: base[2],
condition: cond.name, conditionMult: cond.mult,
marketValue, buyPrice, hot, rare:false
};
}
function generateMarket(count){
const arr=[];
for(let i=0;i<count;i++) arr.push(createListing());
return arr;
}
function refreshMarket(){
state.market = generateMarket(28);
save(); renderMarket();
toast("Рынок обновлён 🔄","info");
}
function populateMarketFilters(){
const brandSel=document.getElementById("fBrand");
const condSel=document.getElementById("fCond");
if(brandSel.options.length===0){
brandSel.innerHTML='<option value="all">Все бренды</option>'+[...new Set(CATALOG.map(c=>c[0]))].map(b=>`<option value="${b}">${b}</option>`).join("");
}
if(condSel.options.length===0){
condSel.innerHTML='<option value="all">Любое состояние</option>'+CONDITIONS.map(c=>`<option value="${c.name}">${c.name}</option>`).join("");
}
}
function renderMarket(){
populateMarketFilters();
const brand=document.getElementById("fBrand").value||"all";
const cond=document.getElementById("fCond").value||"all";
const sort=document.getElementById("fSort").value||"default";
let list = state.market.slice();
if(brand!=="all") list = list.filter(l=>l.brand===brand);
if(cond!=="all") list = list.filter(l=>l.condition===cond);
if(sort==="price_asc") list.sort((a,b)=>a.buyPrice-b.buyPrice);
if(sort==="price_desc") list.sort((a,b)=>b.buyPrice-a.buyPrice);
if(sort==="profit_asc") list.sort((a,b)=>(a.marketValue-a.buyPrice)-(b.marketValue-b.buyPrice));
if(sort==="profit_desc") list.sort((a,b)=>(b.marketValue-b.buyPrice)-(a.marketValue-a.buyPrice));
const grid=document.getElementById("marketGrid");
if(list.length===0){
grid.innerHTML = `<div class="empty-state" style="grid-column:1/-1;"><span class="ic">📭</span>Нет подходящих телефонов.<br>Попробуйте изменить фильтры или обновить рынок.</div>`;
return;
}
grid.innerHTML = list.map(item=>{
const profit = item.marketValue - item.buyPrice;
const canAfford = state.balance >= item.buyPrice;
return `<div class="card">
${item.rare?'<span class="badge badge-rare" style="position:absolute;top:10px;right:10px;">✨ РЕДКОСТЬ</span>':(item.hot?'<span class="badge badge-hot" style="position:absolute;top:10px;right:10px;">🔥 ВЫГОДНО</span>':'')}
<div class="card-top"><div class="phone-emoji">📱</div></div>
<div class="phone-name">${esc(item.brand)} ${esc(item.name)}</div>
<div class="phone-brand">${esc(item.brand)}</div>
<div class="cond-tag">${esc(item.condition)}</div>
<div class="price-row"><span>Цена покупки</span><span>${money(item.buyPrice)}</span></div>
<div class="price-row"><span>Рыночная цена</span><span>${money(item.marketValue)}</span></div>
<div class="price-row"><span>Потенциал</span><span class="${profit>=0?'profit-pos':'profit-neg'}">${profit>=0?'+':''}${money(profit)}</span></div>
<div class="card-actions">
<button class="btn btn-primary" ${canAfford?'':'disabled'} onclick="buyPhone('${item.id}')">Купить</button>
</div>
</div>`;
}).join("");
}
function buyPhone(id){
const idx = state.market.findIndex(m=>m.id===id);
if(idx===-1){ toast("Это предложение больше не доступно","error"); return; }
const item = state.market[idx];
if(state.balance < item.buyPrice){
playSound("error");
toast("Недостаточно средств для покупки 😔","error");
return;
}
state.balance -= item.buyPrice;
state.market.splice(idx,1);
const invItem = {
uid: uid("p"),
brand:item.brand, name:item.name, condition:item.condition, conditionMult:item.conditionMult,
baseValue:item.baseValue, buyPrice:item.buyPrice, marketValue:item.marketValue,
boughtAt: Date.now(), listed:false, listingId:null
};
state.inventory.push(invItem);
state.stats.bought++;
state.stats.mostExpensivePurchase = Math.max(state.stats.mostExpensivePurchase, item.buyPrice);
addHistory("buy", `Куплен ${item.brand} ${item.name} (${item.condition})`, -item.buyPrice);
playSound("buy"); flashBalance(false);
toast(`Куплен ${item.brand} ${item.name} за ${money(item.buyPrice)}`,"success");
save(); renderCurrentView(); updateHeader();
}
/* ================= INVENTORY ================= */
function renderInventory(){
const grid=document.getElementById("invGrid");
if(state.inventory.length===0){
grid.innerHTML = `<div class="empty-state" style="grid-column:1/-1;"><span class="ic">📦</span>Инвентарь пуст.<br>Загляните в раздел «Перекуп», чтобы купить первый телефон!</div>`;
return;
}
grid.innerHTML = state.inventory.map(it=>{
const profit = Math.round(it.marketValue - it.buyPrice);
return `<div class="card">
<div class="card-top"><div class="phone-emoji">📱</div>${it.listed?'<span class="badge">НА ПРОДАЖЕ</span>':''}</div>
<div class="phone-name">${esc(it.brand)} ${esc(it.name)}</div>
<div class="cond-tag">${esc(it.condition)}</div>
<div class="price-row"><span>Куплено за</span><span>${money(it.buyPrice)}</span></div>
<div class="price-row"><span>Рыночная цена</span><span>${money(it.marketValue)}</span></div>
<div class="price-row"><span>Прибыль</span><span class="${profit>=0?'profit-pos':'profit-neg'}">${profit>=0?'+':''}${money(profit)}</span></div>
<div class="card-actions">
<button class="btn btn-green" ${it.listed?'disabled':''} onclick="sellInventoryItem('${it.uid}')">Продать</button>
<button class="btn btn-secondary" ${it.listed?'disabled':''} onclick="openListingModal('${it.uid}')">Выставить</button>
<button class="btn btn-secondary" onclick="keepPhone('${it.uid}')">Оставить</button>
<button class="btn btn-secondary" onclick="openDetailsModal('${it.uid}')">Подробнее</button>
</div>
</div>`;
}).join("");
}
function keepPhone(uidVal){
toast("Телефон оставлен в инвентаре 👍","info");
}
function openDetailsModal(uidVal){
const it = state.inventory.find(x=>x.uid===uidVal);
if(!it) return;
const profit = Math.round(it.marketValue-it.buyPrice);
openModal(`${it.brand} ${it.name}`, `
<p><b>Состояние:</b> ${esc(it.condition)}</p>
<p><b>Куплен:</b> ${fmtDate(it.boughtAt)}</p>
<p><b>Цена покупки:</b> ${money(it.buyPrice)}</p>
<p><b>Рыночная цена:</b> ${money(it.marketValue)}</p>
<p><b>Потенциальная прибыль:</b> <span style="color:${profit>=0?'var(--green)':'var(--red)'}">${profit>=0?'+':''}${money(profit)}</span></p>
<p><b>Статус:</b> ${it.listed?'Выставлен на продажу':'В инвентаре'}</p>
`, [{label:"Закрыть",cls:"btn-primary",onClick:closeModal}]);
}
function sellInventoryItem(uidVal){
const idx = state.inventory.findIndex(x=>x.uid===uidVal);
if(idx===-1) return;
const it = state.inventory[idx];
if(it.listed){ toast("Снимите телефон с продажи, чтобы продать напрямую","error"); return; }
confirmAction(`Продать ${it.brand} ${it.name} за ${money(Math.round(it.marketValue))}?`, ()=>{
const sellPrice = Math.max(300, Math.round(it.marketValue*rand(0.95,1.05)/10)*10);
finalizeSale(it, sellPrice, "Продан напрямую на рынке");
});
}
function finalizeSale(it, sellPrice, descPrefix){
const profit = sellPrice - it.buyPrice;
state.balance += sellPrice;
state.inventory = state.inventory.filter(x=>x.uid!==it.uid);
if(it.listed && it.listingId){
state.listings = state.listings.filter(l=>l.id!==it.listingId);
}
state.offers = state.offers.filter(o=>o.itemUid!==it.uid);
state.stats.sold++;
state.stats.revenue += sellPrice;
state.stats.profit += profit;
state.stats.bestDeal = Math.max(state.stats.bestDeal, profit);
addHistory("sell", `${descPrefix}: ${it.brand} ${it.name}`, sellPrice);
playSound("sell"); flashBalance(true);
toast(`Продано за ${money(sellPrice)} (прибыль ${profit>=0?'+':''}${money(profit)})`, profit>=0?"success":"info");
save(); renderCurrentView(); updateHeader();
}
/* ============ LISTINGS ============ */
function openListingModal(uidVal){
const it = state.inventory.find(x=>x.uid===uidVal);
if(!it) return;
openModal(`Выставить: ${it.brand} ${it.name}`, `
<div class="field"><label>Цена продажи (₽)</label><input id="listPriceInput" type="number" min="1" value="${Math.round(it.marketValue)}"></div>
<div class="field"><label>Описание объявления</label><textarea id="listDescInput" rows="3">Состояние: ${esc(it.condition)}. Торг уместен.</textarea></div>
`, [
{label:"Отмена", cls:"btn-secondary", onClick:closeModal},
{label:"Опубликовать", cls:"btn-primary", onClick:function(){ confirmListing(uidVal); }}
]);
}
function confirmListing(uidVal){
const it = state.inventory.find(x=>x.uid===uidVal);
if(!it) return;
const price = Math.floor(Number(document.getElementById("listPriceInput").value));
const desc = document.getElementById("listDescInput").value.trim();
if(isNaN(price) || price<=0){ toast("Введите корректную цену","error"); return; }
const listing = { id:uid("l"), itemUid:it.uid, price, description:desc||"Без описания", createdAt:Date.now() };
state.listings.push(listing);
it.listed=true; it.listingId=listing.id;
closeModal();
addHistory("event", `Выставлено объявление: ${it.brand} ${it.name} за ${money(price)}`, 0);
toast("Объявление опубликовано 📢","success");
save(); renderCurrentView();
}
function renderListings(){
const grid=document.getElementById("listingsGrid");
if(state.listings.length===0){
grid.innerHTML = `<div class="empty-state" style="grid-column:1/-1;"><span class="ic">📢</span>У вас нет активных объявлений.<br>Выставьте телефон из инвентаря на продажу!</div>`;
return;
}
grid.innerHTML = state.listings.map(l=>{
const it = state.inventory.find(x=>x.uid===l.itemUid);
if(!it) return "";
const offers = state.offers.filter(o=>o.listingId===l.id);
return `<div class="card">
<div class="phone-name">${esc(it.brand)} ${esc(it.name)}</div>
<div class="cond-tag">${esc(it.condition)}</div>
<p style="font-size:12.5px;color:var(--muted);margin-top:8px;">${esc(l.description)}</p>
<div class="price-row"><span>Цена объявления</span><span>${money(l.price)}</span></div>
<div class="price-row"><span>Опубликовано</span><span style="font-weight:400;font-size:11.5px;">${fmtDate(l.createdAt)}</span></div>
${offers.length? `<div style="margin-top:8px;font-size:12px;color:var(--accent2);">💬 Есть ${offers.length} предложение(я) — смотрите во вкладке «Предложения»</div>`:''}
<div class="card-actions">
<button class="btn btn-secondary" onclick="editListingPrice('${l.id}')">Изменить цену</button>
<button class="btn btn-red" onclick="removeListing('${l.id}')">Снять</button>
</div>
</div>`;
}).join("");
}
function editListingPrice(listingId){
const l = state.listings.find(x=>x.id===listingId);
if(!l) return;
openModal("Изменить цену объявления", `
<div class="field"><label>Новая цена (₽)</label><input id="editPriceInput" type="number" min="1" value="${l.price}"></div>
`, [
{label:"Отмена", cls:"btn-secondary", onClick:closeModal},
{label:"Сохранить", cls:"btn-primary", onClick:function(){
const p = Math.floor(Number(document.getElementById("editPriceInput").value));
if(isNaN(p)||p<=0){ toast("Некорректная цена","error"); return; }
l.price=p; closeModal(); toast("Цена обновлена","success"); save(); renderCurrentView();
}}
]);
}
function removeListing(listingId){
const l = state.listings.find(x=>x.id===listingId);
if(!l) return;
const it = state.inventory.find(x=>x.uid===l.itemUid);
if(it){ it.listed=false; it.listingId=null; }
state.listings = state.listings.filter(x=>x.id!==listingId);
state.offers = state.offers.filter(o=>o.listingId!==listingId);
toast("Объявление снято","info");
save(); renderCurrentView();
}
/* ============ OFFERS / NEGOTIATION ============ */
function maybeSpawnOffer(){
if(state.inventory.length===0) return false;
if(state.offers.length>=5) return false;
if(Math.random()>0.4) return false;
const candidates = state.inventory.filter(it=>!state.offers.some(o=>o.itemUid===it.uid));
if(candidates.length===0) return false;
const item = choice(candidates);
const interest = randInt(40,95);
let priceBase = item.marketValue;
if(item.listed){
const l = state.listings.find(x=>x.id===item.listingId);
if(l) priceBase = l.price;
}
let factor = rand(0.7,1.05);
if(Math.random()<0.12) factor = rand(1.05,1.3);
const offerPrice = Math.max(300, Math.round(priceBase*factor/10)*10);
const offer = {
id: uid("o"),
itemUid: item.uid,
itemName: item.brand+" "+item.name,
buyerName: choice(BUYER_NAMES),
avatar: choice(BUYER_AVATARS),
offerPrice, interest,
createdAt: Date.now(),
expiresAt: Date.now() + randInt(3,7)*60000,
type: item.listed ? "listing" : "direct",
listingId: item.listingId || null
};
state.offers.push(offer);
return true;
}
function renderOffers(){
const badge=document.getElementById("offersBadge");
if(state.offers.length>0){ badge.classList.remove("hidden"); badge.textContent=state.offers.length; }
else { badge.classList.add("hidden"); }
const container=document.getElementById("offersList");
if(state.offers.length===0){
container.innerHTML = `<div class="empty-state"><span class="ic">💬</span>Пока нет предложений от покупателей.<br>Они появляются периодически, если у вас есть телефоны в инвентаре.</div>`;
return;
}
container.innerHTML = state.offers.map(o=>{
const it = state.inventory.find(x=>x.uid===o.itemUid);
if(!it) return "";
return `<div class="offer-card">
<div class="offer-head">
<div class="avatar">${o.avatar}</div>
<div>
<div style="font-weight:700;">${esc(o.buyerName)}</div>
<div style="font-size:12px;color:var(--muted);">хочет купить: ${esc(o.itemName)}</div>
</div>
</div>
<div class="interest-bar"><div class="interest-fill" style="width:${o.interest}%;"></div></div>
<div class="offer-row"><span>Заинтересованность</span><span>${o.interest}%</span></div>
<div class="offer-row"><span>Предложенная цена</span><span style="font-weight:700;">${money(o.offerPrice)}</span></div>
<div class="offer-row"><span>Истекает через</span><span class="countdown" data-deadline="${o.expiresAt}">${fmtCountdown(o.expiresAt-Date.now())}</span></div>
<div class="card-actions">
<button class="btn btn-green" onclick="acceptOffer('${o.id}')">Принять</button>
<button class="btn btn-red btn-small" onclick="declineOffer('${o.id}')">Отклонить</button>
</div>
<div class="counter-row">
<input type="number" min="1" placeholder="Своя цена" id="counter-${o.id}">
<button class="btn btn-secondary btn-small" onclick="counterOffer('${o.id}')">Торговаться</button>
</div>
</div>`;
}).join("");
}
function acceptOffer(offerId){
const offer = state.offers.find(o=>o.id===offerId);
if(!offer) return;
const it = state.inventory.find(x=>x.uid===offer.itemUid);
if(!it){ state.offers=state.offers.filter(o=>o.id!==offerId); return; }
state.offers = state.offers.filter(o=>o.id!==offerId);
playSound("deal");
finalizeSale(it, offer.offerPrice, `Продано покупателю ${offer.buyerName}`);
}
function declineOffer(offerId){
state.offers = state.offers.filter(o=>o.id!==offerId);
toast("Предложение отклонено","info");
save(); renderCurrentView();
}
function counterOffer(offerId){
const offer = state.offers.find(o=>o.id===offerId);
if(!offer) return;
const input = document.getElementById("counter-"+offerId);
const counterPrice = Math.floor(Number(input.value));
if(isNaN(counterPrice) || counterPrice<=0){ toast("Введите корректную сумму","error"); return; }
const diff = (counterPrice - offer.offerPrice) / offer.offerPrice;
let prob = (offer.interest/100) - diff*1.2;
prob = Math.max(0.05, Math.min(0.95, prob));
const it = state.inventory.find(x=>x.uid===offer.itemUid);
if(Math.random() < prob){
state.offers = state.offers.filter(o=>o.id!==offerId);
state.stats.negotiations++;
playSound("deal");
if(it) finalizeSale(it, counterPrice, `Удачный торг с ${offer.buyerName}`);
} else {
if(Math.random()<0.5){
const newPrice = Math.round(((offer.offerPrice+counterPrice)/2)/10)*10;
offer.offerPrice = newPrice;
offer.interest = Math.max(10, offer.interest-12);
offer.expiresAt = Date.now()+3*60000;
toast(`${offer.buyerName} предлагает встречную цену: ${money(newPrice)}`,"info");
} else {
state.offers = state.offers.filter(o=>o.id!==offerId);
playSound("error");
toast(`${offer.buyerName} отказался от сделки`,"error");
}
save(); renderCurrentView();
}
}
/* ============ BANK / CREDIT ============ */
function cardLast4(nick){
let hash=0;
const s=String(nick||"bobito");
for(let i=0;i<s.length;i++){ hash = (hash*31 + s.charCodeAt(i)) >>> 0; }
return String(hash%10000).padStart(4,"0");
}
function renderBankCard(){
document.getElementById("cardNum").textContent = "•••• •••• •••• "+cardLast4(state.player?state.player.nick:"bobito");
document.getElementById("cardBal").textContent = money(state.balance);
document.getElementById("cardNick").textContent = (state.player?state.player.nick:"BOBITO")+" • BOBITO CARD";
}
function renderCreditTab(){
const el = document.getElementById("bank-credit");
const c = state.credit;
if(c && !c.closed){
const remaining = c.deadline - Date.now();
const total = c.deadline - c.takenAt;
const passed = Math.min(1, Math.max(0,(Date.now()-c.takenAt)/total));
el.innerHTML = `
<div class="credit-box">
<h3 style="margin-bottom:8px;">Активный кредит</h3>
<div class="price-row"><span>Сумма кредита</span><span>${money(c.amount)}</span></div>
<div class="price-row"><span>К погашению (10%)</span><span>${money(c.debt)}</span></div>
<div class="price-row"><span>Осталось времени</span><span class="countdown" data-deadline="${c.deadline}">${fmtCountdown(remaining)}</span></div>
<div class="progress-bar"><div class="progress-fill" style="width:${Math.round(passed*100)}%;"></div></div>
<button class="btn btn-green" style="width:100%;margin-top:6px;" onclick="repayCredit()">Погасить досрочно</button>
<p style="margin-top:10px;font-size:12px;">⚠️ Если не погасить кредит вовремя — ваш баланс будет обнулён!</p>
</div>`;
} else {
el.innerHTML = `
<div class="credit-box">
<h3 style="margin-bottom:8px;">Оформить кредит</h3>
<p style="font-size:12.5px;margin-bottom:10px;">Сумма от 1 000 ₽ до 100 000 ₽. Ставка 10%. Срок погашения — 3 часа реального времени.</p>
<div class="field"><input id="creditAmountInput" type="number" min="1000" max="100000" placeholder="Введите сумму кредита"></div>
<button class="btn btn-primary" style="width:100%;" onclick="handleTakeCredit()">Оформить кредит</button>
</div>`;
}
}
function handleTakeCredit(){
const val = document.getElementById("creditAmountInput").value;
const amount = Math.floor(Number(val));
if(isNaN(amount) || amount<1000 || amount>100000){
toast("Сумма кредита должна быть от 1 000 до 100 000 ₽","error");
return;
}
if(state.credit && !state.credit.closed){
toast("У вас уже есть активный кредит","error");
return;
}
const debt = Math.round(amount*1.1);
state.credit = { amount, debt, takenAt:Date.now(), deadline:Date.now()+3*3600*1000, closed:false, defaulted:false };
state.balance += amount;
addHistory("credit", `Получен кредит на сумму ${money(amount)}`, amount);
playSound("money"); flashBalance(true);
toast(`Кредит на ${money(amount)} зачислен. Погасить нужно ${money(debt)} в течение 3 часов`,"success");
save(); renderCurrentView(); updateHeader();
}
function repayCredit(){
if(!state.credit || state.credit.closed) return;
if(state.balance < state.credit.debt){
toast("Недостаточно средств для погашения кредита","error");
return;
}
confirmAction(`Погасить кредит на сумму ${money(state.credit.debt)}?`, ()=>{
state.balance -= state.credit.debt;
addHistory("credit", "Кредит погашен досрочно", -state.credit.debt);
state.credit.closed = true;
playSound("deal"); flashBalance(false);
toast("Кредит успешно погашен ✅","success");
save(); renderCurrentView(); updateHeader();
});
}
function checkCreditExpiry(){
const c = state.credit;
if(c && !c.closed && !c.defaulted && Date.now() > c.deadline){
state.balance = 0;
c.defaulted = true;
c.closed = true;
state.stats.expiredCredits++;
addHistory("credit", "Кредит просрочен! Баланс обнулён", 0);
playSound("error");
toast("⚠️ Кредит просрочен! Ваш баланс обнулён.","error");
return true;
}
return false;
}
function openCreditApp(){ showView('bank'); switchBankSubtab('credit'); }
/* ============ HISTORY RENDER ============ */
function renderHistory(){
const filter = document.getElementById("histFilter").value;
const list = state.history.filter(h=> filter==="all" || h.type===filter);
const box = document.getElementById("historyList");
if(list.length===0){
box.innerHTML = `<div class="empty-state"><span class="ic">🧾</span>История пуста.</div>`;
return;
}
box.innerHTML = list.slice(0,150).map(h=>{
const cls = h.amount>0?"amount-pos":(h.amount<0?"amount-neg":"");
const sign = h.amount>0?"+":"";
return `<div class="hist-item">
<div class="hist-desc">${esc(h.desc)}<span class="time">${fmtDate(h.date)}</span></div>
<div class="${cls}">${h.amount!==0? sign+money(h.amount) : "—"}</div>
</div>`;
}).join("");
}
/* ============ PROMO ============ */
function handlePromo(){
const input = document.getElementById("promoInput");
const code = input.value.trim().toUpperCase();
if(!code){ toast("Введите промокод","error"); return; }
if(state.promoBonusClaimed){
toast("Бонус по промокодам уже был получен ранее","error");
input.value="";
return;
}
if(PROMO_CODES.includes(code)){
state.balance += 150000;
state.promoBonusClaimed = true;
state.promosUsed.push(code);
addHistory("promo", `Активирован промокод ${code}`, 150000);
playSound("money"); flashBalance(true);
toast("🎉 Промокод активирован! +150 000 ₽","success");
input.value="";
save(); renderCurrentView(); updateHeader();
} else {
playSound("error");
toast("Неверный промокод","error");
}
}
/* ============ SETTINGS ============ */
function changeNickname(){
const input = document.getElementById("nickChangeInput");
const err = document.getElementById("nickChangeError");
const val = input.value.trim();
err.textContent="";
if(!/^[\p{L}0-9_]{2,20}$/u.test(val)){
err.textContent="Ник: 2-20 символов, буквы/цифры/подчёркивание";
return;
}
state.player.nick = val;
input.value="";
save(); updateHeader(); renderCurrentView();
toast("Никнейм успешно изменён","success");
}
function setTheme(theme){
state.settings.theme = theme;
applyTheme();
save();
}
function applyTheme(){
document.documentElement.setAttribute("data-theme", state.settings.theme);
const d = document.getElementById("themeDarkBtn");
const l = document.getElementById("themeLightBtn");
if(d&&l){
d.style.outline = state.settings.theme==="dark" ? "2px solid var(--accent)" : "none";
l.style.outline = state.settings.theme==="light" ? "2px solid var(--accent)" : "none";
}
}
function toggleSound(){
state.settings.sound = !state.settings.sound;
renderSoundSwitch();
save();
if(state.settings.sound) playSound("deal");
}
function renderSoundSwitch(){
const sw = document.getElementById("soundSwitch");
if(sw) sw.classList.toggle("on", state.settings.sound);
}
function renderStats(){
const s = state.stats;
const grid = document.getElementById("statsGrid");
const items = [
["Куплено телефонов", s.bought],
["Продано телефонов", s.sold],
["Общая выручка", money(s.revenue)],
["Общая прибыль", money(s.profit)],
["Лучшая сделка", money(s.bestDeal)],
["Самая дорогая покупка", money(s.mostExpensivePurchase)],
["Успешных торгов", s.negotiations],
["Просроченных кредитов", s.expiredCredits],
["Заказов ПК выполнено", s.ordersCompleted||0],
["Заказов ПК провалено", s.ordersFailed||0],
["Игр в казино", s.casinoSpins||0],
["Прибыль от казино", money(s.casinoProfit||0)]
];
grid.innerHTML = items.map(([lbl,val])=>`<div class="stat-box"><div class="lbl">${lbl}</div><div class="val">${val}</div></div>`).join("");
}
function exportSave(){
try{
const blob = new Blob([JSON.stringify(state,null,2)], {type:"application/json"});
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url; a.download = "bobito_save.json";
document.body.appendChild(a); a.click(); a.remove();
URL.revokeObjectURL(url);
toast("Сохранение экспортировано","success");
}catch(e){ toast("Ошибка при экспорте","error"); }
}
function importSave(ev){
const file = ev.target.files[0];
if(!file) return;
const reader = new FileReader();
reader.onload = function(e){
try{
const data = JSON.parse(e.target.result);
if(!data || typeof data!=="object" || !data.player || typeof data.balance!=="number" || !Array.isArray(data.inventory)){
throw new Error("invalid format");
}
const def = defaultState();
state = Object.assign({}, def, data);
state.inventory = Array.isArray(data.inventory)?data.inventory:[];
state.listings = Array.isArray(data.listings)?data.listings:[];
state.offers = Array.isArray(data.offers)?data.offers:[];
state.market = Array.isArray(data.market)&&data.market.length?data.market:generateMarket(28);
state.history = Array.isArray(data.history)?data.history:[];
state.pcInventory = Array.isArray(data.pcInventory)?data.pcInventory:[];
state.pcOrders = Array.isArray(data.pcOrders)?data.pcOrders:[];
state.stats = Object.assign({}, def.stats, data.stats||{});
state.settings = Object.assign({}, def.settings, data.settings||{});
state.promosUsed = Array.isArray(data.promosUsed)?data.promosUsed:[];
save();
applyTheme();
updateHeader();
renderCurrentView();
toast("Сохранение успешно импортировано","success");
}catch(err){
toast("Файл сохранения повреждён или некорректен","error");
}
};
reader.readAsText(file);
ev.target.value = "";
}
function confirmReset(){
confirmAction("Весь прогресс будет удалён безвозвратно. Продолжить?", ()=>{
localStorage.removeItem(SAVE_KEY);
location.reload();
});
}
/* ============ CASINO ============ */
function renderCasino(){ /* статичный интерфейс, обновление не требуется */ }
function validateBet(inputId,min){
const val = Math.floor(Number(document.getElementById(inputId).value));
if(isNaN(val)||val<min){ toast(`Минимальная ставка ${money(min)}`,'error'); return null; }
if(val>state.balance){ toast('Недостаточно средств для ставки','error'); return null; }
return val;
}
function selectDiceNum(n,btn){
selectedDiceNum=n;
document.querySelectorAll('#diceChoices .choice-btn').forEach(b=>b.classList.remove('selected'));
btn.classList.add('selected');
}
function selectCoinSide(s,btn){
selectedCoinSide=s;
document.querySelectorAll('#coinChoices .choice-btn').forEach(b=>b.classList.remove('selected'));
btn.classList.add('selected');
}
function playSlots(){
const bet = validateBet('slotBet',100);
if(bet===null) return;
state.balance -= bet;
const weighted=["🍒","🍒","🍒","🍋","🍋","🍋","🍇","🍇","🍇","🔔","🔔","⭐","⭐","💎","7️⃣"];
const r=[choice(weighted),choice(weighted),choice(weighted)];
document.getElementById('slotReels').textContent = r.join(' ');
let mult=0;
if(r[0]===r[1] && r[1]===r[2]){ mult = r[0]==='7️⃣'?15:8; }
else if(r[0]===r[1]||r[1]===r[2]||r[0]===r[2]){ mult=1.5; }
const payout = Math.round(bet*mult);
state.balance += payout;
const profit = payout-bet;
state.stats.casinoSpins++; state.stats.casinoProfit += profit;
addHistory('casino', `Казино (слоты): ставка ${money(bet)}`, profit);
const resEl=document.getElementById('slotResult');
if(profit>0){ resEl.textContent=`🎉 Выигрыш +${money(profit)}`; resEl.style.color='var(--green)'; playSound('deal'); }
else { resEl.textContent=`😔 Проигрыш -${money(bet)}`; resEl.style.color='var(--red)'; playSound('error'); }
flashBalance(profit>0);
save(); updateHeader();
}
function playDice(){
if(selectedDiceNum===null){ toast('Выберите число от 1 до 6','error'); return; }
const bet = validateBet('diceBet',100);
if(bet===null) return;
state.balance -= bet;
const roll = randInt(1,6);
const win = roll===selectedDiceNum;
const payout = win?bet*5:0;
state.balance += payout;
const profit = payout-bet;
state.stats.casinoSpins++; state.stats.casinoProfit+=profit;
addHistory('casino', `Казино (кости): выпало ${roll}, ставка ${money(bet)}`, profit);
const resEl=document.getElementById('diceResult');
resEl.textContent = win? `🎲 Выпало ${roll}! Выигрыш +${money(profit)}` : `🎲 Выпало ${roll}. Проигрыш -${money(bet)}`;
resEl.style.color = win?'var(--green)':'var(--red)';
playSound(win?'deal':'error');
flashBalance(win);
save(); updateHeader();
}
function playCoin(){
if(!selectedCoinSide){ toast('Выберите орёл или решку','error'); return; }
const bet = validateBet('coinBet',100);
if(bet===null) return;
state.balance -= bet;
const result = Math.random()<0.5?'heads':'tails';
const win = result===selectedCoinSide;
const payout = win?Math.round(bet*1.9):0;
state.balance += payout;
const profit = payout-bet;
state.stats.casinoSpins++; state.stats.casinoProfit+=profit;
addHistory('casino', `Казино (монетка): выпал(а) ${result==='heads'?'орёл':'решка'}`, profit);
const resEl=document.getElementById('coinResult');
resEl.textContent = win? `🪙 ${result==='heads'?'Орёл':'Решка'}! Выигрыш +${money(profit)}` : `🪙 ${result==='heads'?'Орёл':'Решка'}. Проигрыш -${money(bet)}`;
resEl.style.color = win?'var(--green)':'var(--red)';
playSound(win?'deal':'error');
flashBalance(win);
save(); updateHeader();
}
/* ============ PC PARTS MARKET ============ */
function buyPart(cat, idx){
const catData = PARTS_CATALOG[cat];
if(!catData) return;
const item = catData.items[idx];
if(!item) return;
if(state.balance < item.price){ toast("Недостаточно средств","error"); return; }
state.balance -= item.price;
state.pcInventory.push({uid:uid("part"), category:cat, name:item.name, power:item.power, price:item.price});
addHistory("buy", `Куплена деталь: ${item.name}`, -item.price);
playSound("buy"); flashBalance(false);
toast(`Куплено: ${item.name}`,"success");
save(); renderCurrentView(); updateHeader();
}
function sellPart(uidVal){
const idx = state.pcInventory.findIndex(p=>p.uid===uidVal);
if(idx===-1) return;
const part = state.pcInventory[idx];
const sellPrice = Math.max(100, Math.round(part.price*0.5/10)*10);
state.pcInventory.splice(idx,1);
state.balance += sellPrice;
addHistory("sell", `Продана деталь: ${part.name}`, sellPrice);
playSound("sell"); flashBalance(true);
toast(`Деталь продана за ${money(sellPrice)}`,"info");
save(); renderCurrentView(); updateHeader();
}
function renderPartsMarket(){
const sel = document.getElementById("partsCategorySelect");
if(sel.options.length===0){
sel.innerHTML = Object.keys(PARTS_CATALOG).map(k=>`<option value="${k}">${PARTS_CATALOG[k].icon} ${PARTS_CATALOG[k].label}</option>`).join('') + `<option value="owned">🎒 Мои комплектующие</option>`;
}
const cat = sel.value || Object.keys(PARTS_CATALOG)[0];
const grid = document.getElementById("partsGrid");
if(cat==="owned"){
if(state.pcInventory.length===0){
grid.innerHTML = `<div class="empty-state" style="grid-column:1/-1;"><span class="ic">🎒</span>У вас пока нет комплектующих.<br>Купите детали в категориях выше!</div>`;
} else {
grid.innerHTML = state.pcInventory.map(p=>{
const c = PARTS_CATALOG[p.category];
return `<div class="card">
<div class="card-top"><div class="phone-emoji">${c?c.icon:'🔧'}</div></div>
<div class="phone-name">${esc(p.name)}</div>
<div class="cond-tag">${c?esc(c.label):''} • ⚡ ${p.power}</div>
<div class="price-row"><span>Куплено за</span><span>${money(p.price)}</span></div>
<div class="card-actions"><button class="btn btn-red btn-small" onclick="sellPart('${p.uid}')">Продать (50%)</button></div>
</div>`;
}).join('');
}
return;
}
const catData = PARTS_CATALOG[cat];
grid.innerHTML = catData.items.map((item,idx)=>{
const canAfford = state.balance>=item.price;
return `<div class="card">
<div class="card-top"><div class="phone-emoji">${catData.icon}</div></div>
<div class="phone-name">${esc(item.name)}</div>
<div class="cond-tag">⚡ Мощность: ${item.power}</div>
<div class="price-row"><span>Цена</span><span>${money(item.price)}</span></div>
<div class="card-actions"><button class="btn btn-primary" ${canAfford?'':'disabled'} onclick="buyPart('${cat}',${idx})">Купить</button></div>
</div>`;
}).join('');
}
/* ============ PC ORDERS ============ */
function reqChipsHtml(requirements){
return Object.keys(requirements).map(cat=>{
const c = PARTS_CATALOG[cat];
return `<span class="req-chip">${c.icon} ${c.label}: ${requirements[cat]}+</span>`;
}).join('');
}
function generatePcOrder(){
const base = randInt(15,85);
const requirements={};
Object.keys(PARTS_CATALOG).forEach(cat=>{
requirements[cat] = Math.max(10, Math.min(100, base + randInt(-15,15)));
});
const sumReq = Object.values(requirements).reduce((a,b)=>a+b,0);
let price = Math.round(sumReq*rand(150,220)/10)*10;
price = Math.max(4000, price);
return {
id: uid("ord"),
client: choice(BUYER_NAMES),
avatar: choice(BUYER_AVATARS),
requirements,
price,
status:"available",
createdAt: Date.now()
};
}
function maintainPcOrders(){
const available = state.pcOrders.filter(o=>o.status==="available");
if(available.length<3 && Math.random()<0.6){
state.pcOrders.push(generatePcOrder());
}
}
function acceptPcOrder(orderId){
if(state.pcOrders.some(o=>o.status==="in_progress")){
toast("У вас уже есть заказ в процессе сборки","error");
return;
}
const order = state.pcOrders.find(o=>o.id===orderId);
if(!order) return;
order.status="in_progress";
order.acceptedAt=Date.now();
order.deadline=Date.now()+30*60000;
state.balance += order.price;
addHistory("pc", `Принят заказ на сборку ПК от ${order.client}, предоплата`, order.price);
playSound("money"); flashBalance(true);
toast(`Заказ принят! Получено ${money(order.price)}. Соберите ПК за 30 минут.`,"success");
save(); renderCurrentView(); updateHeader();
}
function openBuildModal(orderId){
const order = state.pcOrders.find(o=>o.id===orderId && o.status==="in_progress");
if(!order) return;
let body="";
Object.keys(order.requirements).forEach(cat=>{
const req = order.requirements[cat];
const owned = state.pcInventory.filter(p=>p.category===cat);
body += `<div class="build-field"><label>${PARTS_CATALOG[cat].icon} ${PARTS_CATALOG[cat].label} (нужно ${req}+)</label>`;
if(owned.length===0){
body += `<div style="font-size:12px;color:var(--red);">Нет деталей этой категории — купите в Комплектующих</div>`;
} else {
body += `<select id="build-${cat}"><option value="">— выбрать —</option>${owned.map(p=>`<option value="${p.uid}">${esc(p.name)} (⚡${p.power})</option>`).join('')}</select>`;
}
body += `</div>`;
});
openModal(`Сборка ПК для ${order.client}`, body, [
{label:"Отмена", cls:"btn-secondary", onClick:closeModal},
{label:"Сдать заказ", cls:"btn-primary", onClick:function(){ submitBuild(orderId); }}
]);
}
function submitBuild(orderId){
const order = state.pcOrders.find(o=>o.id===orderId && o.status==="in_progress");
if(!order) return;
const cats = Object.keys(order.requirements);
const chosen = {};
for(const cat of cats){
const sel = document.getElementById("build-"+cat);
if(!sel || !sel.value){ toast(`Выберите деталь для категории: ${PARTS_CATALOG[cat].label}`,"error"); return; }
chosen[cat]=sel.value;
}
for(const cat of cats){
const part = state.pcInventory.find(p=>p.uid===chosen[cat]);
if(!part || part.power < order.requirements[cat]){
toast(`Деталь в категории "${PARTS_CATALOG[cat].label}" не соответствует требованиям заказчика`,"error");
return;
}
}
cats.forEach(cat=>{
state.pcInventory = state.pcInventory.filter(p=>p.uid!==chosen[cat]);
});
state.pcOrders = state.pcOrders.filter(o=>o.id!==orderId);
state.stats.ordersCompleted = (state.stats.ordersCompleted||0)+1;
addHistory("pc", `Заказ ПК для ${order.client} успешно сдан`, 0);
closeModal();
playSound("deal");
toast("✅ ПК успешно собран и сдан заказчику!","success");
save(); renderCurrentView(); updateHeader();
}
function checkPcOrderExpiry(){
const order = state.pcOrders.find(o=>o.status==="in_progress");
if(order && Date.now()>order.deadline){
const penalty = Math.min(state.balance, order.price);
state.balance -= penalty;
state.pcOrders = state.pcOrders.filter(o=>o.id!==order.id);
state.stats.ordersFailed = (state.stats.ordersFailed||0)+1;
addHistory("pc", `Заказ ПК просрочен! Предоплата списана`, -penalty);
playSound("error");
toast("⏰ Время на сборку ПК истекло! Предоплата списана.","error");
return true;
}
return false;
}
function renderPcBuilder(){
const container = document.getElementById("pcOrdersContainer");
const inProgress = state.pcOrders.find(o=>o.status==="in_progress");
const available = state.pcOrders.filter(o=>o.status==="available");
let html="";
if(inProgress){
const remaining = inProgress.deadline-Date.now();
html += `<div class="order-card inprogress">
<div style="display:flex;align-items:center;gap:8px;"><div class="avatar">${inProgress.avatar}</div><div><b>${esc(inProgress.client)}</b><div style="font-size:11.5px;color:var(--muted);">Заказ в сборке</div></div></div>
<div class="req-list">${reqChipsHtml(inProgress.requirements)}</div>
<div class="price-row"><span>Предоплата получена</span><span>${money(inProgress.price)}</span></div>
<div class="price-row"><span>Осталось времени</span><span class="countdown" data-deadline="${inProgress.deadline}">${fmtCountdown(remaining)}</span></div>
<button class="btn btn-primary" style="width:100%;margin-top:8px;" onclick="openBuildModal('${inProgress.id}')">🛠️ Собрать и сдать</button>
</div>`;
}
html += `<h3 style="margin:14px 0 8px;font-size:14px;color:var(--muted);">Доступные заказы</h3>`;
if(available.length===0){
html += `<div class="empty-state"><span class="ic">⏳</span>Новые заказы скоро появятся.</div>`;
} else {
html += available.map(o=>`
<div class="order-card">
<div style="display:flex;align-items:center;gap:8px;"><div class="avatar">${o.avatar}</div><div><b>${esc(o.client)}</b></div></div>
<div class="req-list">${reqChipsHtml(o.requirements)}</div>
<div class="price-row"><span>Оплата</span><span style="font-weight:700;color:var(--green);">${money(o.price)}</span></div>
<button class="btn btn-green" style="width:100%;margin-top:8px;" ${inProgress?'disabled':''} onclick="acceptPcOrder('${o.id}')">${inProgress?'Сначала завершите текущий заказ':'Принять заказ'}</button>
</div>`).join('');
}
container.innerHTML = html;
}
/* ============ NAV / RENDER DISPATCH ============ */
function showView(name){
document.querySelectorAll(".view").forEach(v=>v.classList.add("hidden"));
document.getElementById("view-"+name).classList.remove("hidden");
currentView = name;
renderCurrentView();
}
function switchPhonesSubtab(name){
currentPhonesSub = name;
document.querySelectorAll('[data-sub]').forEach(b=>{ if(["inv","listings","offers"].includes(b.dataset.sub)) b.classList.toggle("active", b.dataset.sub===name); });
document.getElementById("sub-inv").classList.toggle("hidden", name!=="inv");
document.getElementById("sub-listings").classList.toggle("hidden", name!=="listings");
document.getElementById("sub-offers").classList.toggle("hidden", name!=="offers");
renderCurrentView();
}
function switchBankSubtab(name){
currentBankSub = name;
document.querySelectorAll('[data-sub]').forEach(b=>{ if(["card","credit","hist"].includes(b.dataset.sub)) b.classList.toggle("active", b.dataset.sub===name); });
document.getElementById("bank-card").classList.toggle("hidden", name!=="card");
document.getElementById("bank-credit").classList.toggle("hidden", name!=="credit");
document.getElementById("bank-hist").classList.toggle("hidden", name!=="hist");
renderCurrentView();
}
function renderHome(){
const el = document.getElementById("homeBalanceVal");
if(el) el.textContent = money(state.balance);
}
function renderCurrentView(){
if(currentView==="home") renderHome();
else if(currentView==="market") renderMarket();
else if(currentView==="phones"){
if(currentPhonesSub==="inv") renderInventory();
else if(currentPhonesSub==="listings") renderListings();
else renderOffers();
renderOffersBadgeGlobal();
}
else if(currentView==="bank"){
renderBankCard();
if(currentBankSub==="credit") renderCreditTab();
if(currentBankSub==="hist") renderHistory();
}
else if(currentView==="casino") renderCasino();
else if(currentView==="pcbuilder") renderPcBuilder();
else if(currentView==="partsmarket") renderPartsMarket();
else if(currentView==="settings"){
renderStats();
renderSoundSwitch();
}
}
function renderOffersBadgeGlobal(){
const badge=document.getElementById("offersBadge");
if(!badge) return;
if(state.offers.length>0){ badge.classList.remove("hidden"); badge.textContent=state.offers.length; }
else badge.classList.add("hidden");
}
function updateHeader(){
document.getElementById("hNick").textContent = state.player?state.player.nick:"—";
document.getElementById("hBalance").textContent = money(state.balance);
document.getElementById("hCount").textContent = state.inventory.length;
document.getElementById("hProfit").textContent = money(state.stats.profit);
const homeEl = document.getElementById("homeBalanceVal");
if(homeEl) homeEl.textContent = money(state.balance);
}
/* ============ STATUS BAR ============ */
function updateStatusBar(){
const now = new Date();
const hh = String(now.getHours()).padStart(2,"0");
const mm = String(now.getMinutes()).padStart(2,"0");
const timeEl = document.getElementById("statusTime");
if(timeEl) timeEl.textContent = hh+":"+mm;
const battery = 20 + Math.floor((Date.now()/60000) % 80);
const battEl = document.getElementById("statusBattery");
if(battEl) battEl.textContent = battery+"%";
}
/* ============ GAME LOOP ============ */
function marketFluctuationTick(){
if(state.market.length===0) return;
if(Math.random()<0.25){
const idx = randInt(0, state.market.length-1);
const item = state.market[idx];
const change = rand(-0.15,0.2);
item.marketValue = Math.max(500, Math.round(item.marketValue*(1+change)/10)*10);
item.buyPrice = Math.max(400, Math.round(item.buyPrice*(1+change*0.6)/10)*10);
if(change>0.1){ item.hot=false; }
else if(change<-0.1){ item.hot=true; }
}
if(Math.random()<0.07 && state.market.length<32){
const l = createListing(); l.rare=true; l.hot=true;
state.market.unshift(l);
toast(`✨ На рынке редкая модель: ${l.brand} ${l.name}!`,"info");
}
if(state.market.length<20 && Math.random()<0.5){
state.market.push(createListing());
}
}
function gameTick(){
let changed=false;
if(checkCreditExpiry()) changed=true;
if(checkPcOrderExpiry()) changed=true;
const before = state.offers.length;
state.offers = state.offers.filter(o=>Date.now()<o.expiresAt);
if(state.offers.length!==before) changed=true;
if(maybeSpawnOffer()) changed=true;
maintainPcOrders();
marketFluctuationTick();
changed = true;
if(changed){
save();
renderCurrentView();
updateHeader();
}
}
function updateCountdowns(){
document.querySelectorAll(".countdown").forEach(el=>{
const deadline = Number(el.dataset.deadline);
const diff = deadline - Date.now();
el.textContent = diff>0 ? fmtCountdown(diff) : "00:00:00";
el.classList.toggle("expired", diff<=0);
});
}
/* ============ REGISTRATION ============ */
function handleRegister(){
const input = document.getElementById("nickInput");
const err = document.getElementById("regError");
const val = input.value.trim();
err.textContent = "";
if(!/^[\p{L}0-9_]{2,20}$/u.test(val)){
err.textContent = "Ник должен быть от 2 до 20 символов (буквы, цифры, _)";
return;
}
state.player = { nick: val, createdAt: Date.now() };
save();
document.getElementById("registerScreen").classList.add("hidden");
document.getElementById("app").classList.remove("hidden");
applyTheme();
startApp();
}
/* ============ INIT ============ */
function startApp(){
updateHeader();
renderSoundSwitch();
updateStatusBar();
for(let i=0;i<3;i++){ maintainPcOrders(); }
showView("home");
checkCreditExpiry();
checkPcOrderExpiry();
save();
setInterval(gameTick, 5000);
setInterval(updateCountdowns, 1000);
setInterval(updateStatusBar, 1000);
}
window.addEventListener("DOMContentLoaded", function(){
setTimeout(function(){
loadGame();
document.getElementById("loadingScreen").classList.add("hidden");
if(!state.player || !state.player.nick){
document.getElementById("registerScreen").classList.remove("hidden");
} else {
document.getElementById("app").classList.remove("hidden");
applyTheme();
startApp();
}
}, 900);
});
</script>
</body>
</html>