Created
August 31, 2025 13:39
-
-
Save postpersonality/2e20f609db2806107296490928a9ea51 to your computer and use it in GitHub Desktop.
Игра "От лица первого" (Суп)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!doctype html> | |
| <html lang="ru"> | |
| <head> | |
| <meta charset="utf-8" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1" /> | |
| <title>Soup 4X — развей цивилизацию до того, как тебя сольют</title> | |
| <style> | |
| :root { | |
| --bg:#0b1020; /* темный фон как у холодильника ночью */ | |
| --card:#111833; | |
| --accent:#7dd3fc; /* спокойный голубой */ | |
| --accent-2:#a7f3d0; /* мятный для успехов */ | |
| --warn:#fca5a5; /* розоватый для рисков */ | |
| --text:#e5e7eb; | |
| --muted:#9ca3af; | |
| } | |
| * { box-sizing: border-box; } | |
| body { | |
| margin:0; font: 16px/1.35 system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; | |
| color:var(--text); background:linear-gradient(180deg, #0b1020, #0b1228 40%, #0b1020); | |
| } | |
| .wrap { max-width: 900px; margin: 0 auto; padding: 16px; } | |
| h1 { font-size: 22px; margin: 12px 0 8px; } | |
| p.lead { color:var(--muted); margin: 0 0 14px; } | |
| .row { display:grid; grid-template-columns: 1fr; gap: 12px; } | |
| @media (min-width: 760px){ .row { grid-template-columns: 1.2fr .8fr; } }.card { background: rgba(17,24,51,0.85); backdrop-filter: blur(6px); | |
| border:1px solid rgba(125,211,252,0.12); border-radius:16px; padding:12px 12px 10px; box-shadow: 0 10px 30px rgba(0,0,0,0.35); } | |
| .section-title { margin:0 0 8px; font-weight:700; letter-spacing:0.2px; color:var(--accent); } | |
| .stat { margin-bottom:10px; } | |
| .stat .top { display:flex; justify-content:space-between; align-items:baseline; font-size: 14px; color:var(--muted); } | |
| .stat .val { color:var(--text); font-weight:600; } | |
| .bar { height: 8px; background:#0e1428; border-radius:999px; overflow:hidden; border:1px solid rgba(125,211,252,0.12); } | |
| .bar > i { display:block; height:100%; width:0%; background: linear-gradient(90deg, var(--accent), #60a5fa); } | |
| .bar.warn > i { background: linear-gradient(90deg, #fda4af, #fb7185); } | |
| .bar.good > i { background: linear-gradient(90deg, #a7f3d0, #34d399); } | |
| .btns { display:grid; grid-template-columns: 1fr 1fr; gap: 8px; } | |
| @media (min-width: 760px){ .btns { grid-template-columns: repeat(3, 1fr); } } | |
| button { appearance:none; border:none; border-radius:14px; padding:12px 10px; font-weight:600; cursor:pointer; | |
| color:#0b1020; background: linear-gradient(180deg, #dbeafe, #bae6fd); | |
| box-shadow: 0 4px 12px rgba(13,148,136,0.15), inset 0 0 0 1px rgba(0,0,0,0.05); | |
| } | |
| button.small { padding:8px 10px; font-size:14px; } | |
| button:hover { filter:brightness(1.02); } | |
| button:disabled { opacity:.55; cursor:not-allowed; } | |
| .btn-warn { background: linear-gradient(180deg, #fecaca, #fda4af); } | |
| .btn-good { background: linear-gradient(180deg, #bbf7d0, #86efac); } | |
| .log { height: 260px; overflow:auto; padding:8px; border-radius:12px; background:#0a0f22; border:1px solid rgba(125,211,252,0.1); } | |
| .log p { margin:6px 0; font-size:14px; color:#cbd5e1; } | |
| .log .t { color:#64748b; } | |
| .muted { color:var(--muted); font-size:13px; } | |
| .pill { display:inline-block; padding:4px 8px; border-radius:999px; background:#0c132a; border:1px solid rgba(125,211,252,0.15); color:#9ca3af; font-size:12px; margin:0 6px 6px 0; } | |
| .footer { margin-top: 12px; color:var(--muted); font-size: 13px; } | |
| .topbar { display:flex; align-items:center; gap:8px; } | |
| .grow { flex: 1; } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="wrap"> | |
| <div class="topbar"> | |
| <h1>Soup 4X. Стратегия от лица первого</h1> | |
| <div class="grow"></div> | |
| <button id="pauseBtn" class="small">Пауза</button> | |
| <button id="restartBtn" class="small">Заново</button> | |
| </div> | |
| <p class="lead">Вы — суп в холодильнике. Ваша задача — успеть взрастить разумную цивилизацию микроорганизмов до того, как хозяин вас обнаружит и без сантиментов сольет в унитаз. Игра минималистичная: никаких спецэффектов, только холодный расчет.</p><div class="row"> | |
| <div class="card"> | |
| <h2 class="section-title">Показатели колонии</h2> | |
| <div id="stats"></div> | |
| <h2 class="section-title" style="margin-top:12px">Действия</h2> | |
| <div class="btns"> | |
| <button id="btnGrow" title="Увеличить численность колонии. Цена 15 энергии.">Деление +5 бактерий<br><span class="muted">Цена 15</span></button> | |
| <button id="btnResearch" title="Продвинуться к разуму. Цена 30 энергии.">Исследования<br><span class="muted">Цена 30</span></button> | |
| <button id="btnBiofilm" title="Биопленка снижает заметность и ускоряет метаболизм. Цена 20 энергии.">Построить биопленку<br><span class="muted">Цена 20</span></button> | |
| <button id="btnDeodor" class="btn-good" title="Сбросить запах. Цена 20 энергии.">Дезодорация<br><span class="muted">Цена 20</span></button> | |
| <button id="btnCamouflage" class="btn-good" title="Понизить видимость плесени. Цена 20 энергии.">Камуфляж<br><span class="muted">Цена 20</span></button> | |
| <button id="btnDormant" class="btn-warn" title="Уснуть на 8 сек. Рост останавливается, риск падает. Цена 10 энергии.">Гибернация 8 сек<br><span class="muted">Цена 10</span></button> | |
| </div> | |
| <h2 class="section-title" style="margin-top:12px">Улучшения</h2> | |
| <div id="upgrades" class="btns"></div> | |
| </div> | |
| <div class="card"> | |
| <h2 class="section-title">События</h2> | |
| <div class="log" id="log"></div> | |
| <div style="margin-top:10px;"> | |
| <span class="pill" id="pillTemp">t 4 C</span> | |
| <span class="pill" id="pillDoor">дверь закрыта</span> | |
| <span class="pill" id="pillMode">нормальный режим</span> | |
| </div> | |
| <p class="footer">Победа: интеллект 100. Поражение: обнаружение во время открытия двери или уборки. Очевидные решения иногда приводят к совпадениям с санитарными службами, так что риски рассчитывайте.</p> | |
| </div> | |
| </div> | |
| </div> <script> | |
| const el = sel => document.querySelector(sel); | |
| const statsBox = el('#stats'); | |
| const logBox = el('#log'); | |
| const pillTemp = el('#pillTemp'); | |
| const pillDoor = el('#pillDoor'); | |
| const pillMode = el('#pillMode'); | |
| const buttons = { | |
| grow: el('#btnGrow'), | |
| research: el('#btnResearch'), | |
| biofilm: el('#btnBiofilm'), | |
| deodor: el('#btnDeodor'), | |
| camo: el('#btnCamouflage'), | |
| dormant: el('#btnDormant'), | |
| pause: el('#pauseBtn'), | |
| restart: el('#restartBtn') | |
| }; | |
| const state = { | |
| t: 0, | |
| energy: 30, | |
| population: 8, | |
| tech: 0, | |
| smell: 0, | |
| visibility: 0, | |
| biofilm: 0, | |
| temp: 4, | |
| warmTicks: 0, | |
| dormantTicks: 0, | |
| nextDoor: rndInt(10,24), | |
| cleaningAt: rndInt(160,260), | |
| cleaningDone: false, | |
| paused: false, | |
| over: false, | |
| win: false, | |
| doorOpen: false, | |
| upgrades: { | |
| cold:false, | |
| neural:false, | |
| odorControl:0, // уровни 0..2 | |
| camouflage:0 // уровни 0..2 | |
| } | |
| }; | |
| function rndInt(a,b){ return Math.floor(Math.random()*(b-a+1))+a; } | |
| function clamp(v, lo, hi){ return Math.max(lo, Math.min(hi, v)); } | |
| function log(msg){ | |
| const p = document.createElement('p'); | |
| const ts = new Date().toLocaleTimeString(); | |
| p.innerHTML = `<span class="t">[${ts}]</span> ${msg}`; | |
| logBox.appendChild(p); | |
| logBox.scrollTop = logBox.scrollHeight; | |
| } | |
| function stat(name, value, max, hint, barClass){ | |
| const pct = clamp((value/max)*100, 0, 100); | |
| return ` | |
| <div class="stat"> | |
| <div class="top"><span>${name}</span><span class="val">${value.toFixed ? value.toFixed(1) : value}</span></div> | |
| <div class="bar ${barClass||''}"><i style="width:${pct}%"></i></div> | |
| <div class="muted">${hint||''}</div> | |
| </div>`; | |
| } | |
| function riskNow(){ | |
| // Базовый риск от запаха и видимости, усиленный численностью | |
| const base = 0.6*state.smell + 0.4*state.visibility + state.population*0.6; | |
| const bioShield = state.biofilm * 0.35; // биопленка частично экранирует | |
| const uShield = state.upgrades.camouflage*6 + state.upgrades.odorControl*5; | |
| return clamp((base - bioShield - uShield)/1.2, 0, 100); | |
| } | |
| function updateStats(){ | |
| const risk = riskNow(); | |
| statsBox.innerHTML = [ | |
| stat('Энергия', state.energy, 200, 'Тратится на действия.'), | |
| stat('Население', state.population, 200, 'Чем больше, тем быстрее рост, но выше риск.'), | |
| stat('Интеллект', state.tech, 100, 'Достигните 100 для победы.', 'good'), | |
| stat('Запах', state.smell, 100, 'Слишком высокий привлекает внимание.', 'warn'), | |
| stat('Видимость', state.visibility, 100, 'Пятна плесени на поверхности.', 'warn'), | |
| stat('Биопленка', state.biofilm, 100, 'Скрывает и стабилизирует колонию.'), | |
| stat('Риск обнаружения', risk, 100, 'Рассчитывается из запаха, видимости и численности.', 'warn') | |
| ].join(''); | |
| pillTemp.textContent = `t ${state.temp} C`; | |
| pillDoor.textContent = state.doorOpen ? 'дверь открыта' : 'дверь закрыта'; | |
| pillMode.textContent = state.dormantTicks>0 ? 'гибернация' : (state.warmTicks>0 ? 'теплый сквозняк' : 'нормальный режим'); | |
| // доступность кнопок | |
| buttons.grow.disabled = state.energy < 15 || state.dormantTicks>0 || state.over; | |
| buttons.research.disabled = state.energy < 30 || state.dormantTicks>0 || state.over; | |
| buttons.biofilm.disabled = state.energy < 20 || state.dormantTicks>0 || state.over; | |
| buttons.deodor.disabled = state.energy < 20 || state.over; | |
| buttons.camo.disabled = state.energy < 20 || state.over; | |
| buttons.dormant.disabled = state.energy < 10 || state.dormantTicks>0 || state.over; | |
| buttons.pause.textContent = state.paused ? 'Продолжить' : 'Пауза'; | |
| renderUpgrades(); | |
| } | |
| function renderUpgrades(){ | |
| const box = el('#upgrades'); | |
| box.innerHTML = ''; | |
| const addBtn = (id, title, price, enableIf, onClick) => { | |
| if (!enableIf) return; | |
| const b = document.createElement('button'); | |
| b.innerHTML = `${title}<br><span class="muted">Цена ${price}</span>`; | |
| b.disabled = state.energy < price || state.over; | |
| b.addEventListener('click', ()=>{ if (state.energy >= price){ state.energy -= price; onClick(); updateStats(); } }); | |
| box.appendChild(b); | |
| }; | |
| addBtn('u_cold', 'Холодоустойчивость', 35, state.tech>=20 && !state.upgrades.cold, ()=>{ | |
| state.upgrades.cold = true; log('Колония адаптировалась к низкой температуре. Метаболизм стал устойчивее.'); | |
| }); | |
| addBtn('u_neural', 'Нейронная плесень', 50, state.tech>=45 && !state.upgrades.neural, ()=>{ | |
| state.upgrades.neural = true; log('Строится простейшая нейросеть из гиф и биопленки. Исследования ускорены.'); | |
| }); | |
| addBtn('u_odor', 'Антизапах ур.1', 40, state.tech>=70 && state.upgrades.odorControl===0, ()=>{ | |
| state.upgrades.odorControl = 1; log('Вы научились нейтрализовать летучие соединения. Запах растет медленнее.'); | |
| }); | |
| addBtn('u_odor2', 'Антизапах ур.2', 55, state.tech>=85 && state.upgrades.odorControl===1, ()=>{ | |
| state.upgrades.odorControl = 2; log('Антизапах максимального уровня. Теперь пахнет почти как вчера.'); | |
| }); | |
| addBtn('u_cam', 'Камуфляж ур.1', 40, state.tech>=80 && state.upgrades.camouflage===0, ()=>{ | |
| state.upgrades.camouflage = 1; log('Пигменты имитируют поверхность супа. Видимость падает.'); | |
| }); | |
| addBtn('u_cam2', 'Камуфляж ур.2', 55, state.tech>=92 && state.upgrades.camouflage===1, ()=>{ | |
| state.upgrades.camouflage = 2; log('Камуфляж совершенен. Пятна сливаются с окружением.'); | |
| }); | |
| } | |
| // Действия | |
| buttons.grow.addEventListener('click', ()=>{ | |
| if (state.energy < 15) return; state.energy -= 15; state.population += 5; log('Массовое деление: +5 к населению.'); updateStats(); | |
| }); | |
| buttons.research.addEventListener('click', ()=>{ | |
| if (state.energy < 30) return; state.energy -= 30; let inc = 8; | |
| let mult = 1 + (state.upgrades.neural?0.5:0) + Math.min(0.5, state.biofilm*0.006); | |
| state.tech += inc * mult; state.tech = Math.min(state.tech, 100); | |
| log(`Проведены исследования. Интеллект +${(inc*mult).toFixed(1)}.`); | |
| updateStats(); | |
| }); | |
| buttons.biofilm.addEventListener('click', ()=>{ | |
| if (state.energy < 20) return; state.energy -= 20; state.biofilm = clamp(state.biofilm + 6, 0, 100); | |
| log('Биопленка наращена. Колония чувствует себя защищенно.'); updateStats(); | |
| }); | |
| buttons.deodor.addEventListener('click', ()=>{ | |
| if (state.energy < 20) return; state.energy -= 20; state.smell = clamp(state.smell - 15, 0, 100); | |
| log('Запах частично нейтрализован.'); updateStats(); | |
| }); | |
| buttons.camo.addEventListener('click', ()=>{ | |
| if (state.energy < 20) return; state.energy -= 20; state.visibility = clamp(state.visibility - 15, 0, 100); | |
| log('Видимость плесени уменьшена за счет пигментов.'); updateStats(); | |
| }); | |
| buttons.dormant.addEventListener('click', ()=>{ | |
| if (state.energy < 10) return; state.energy -= 10; state.dormantTicks = 8; log('Колония ушла в гибернацию. Пожалуйста, не беспокоить.'); updateStats(); | |
| }); | |
| buttons.pause.addEventListener('click', ()=>{ state.paused = !state.paused; updateStats(); }); | |
| buttons.restart.addEventListener('click', ()=>{ window.location.reload(); }); | |
| function openDoor(checkStrength){ | |
| state.doorOpen = true; pillDoor.textContent = 'дверь открыта'; | |
| state.warmTicks = 6; // около 6 секунд теплого воздуха | |
| log('Дверца холодильника открылась. Мимо прошел человек.'); | |
| // Проверка обнаружения | |
| const risk = riskNow(); | |
| let chance = clamp(risk/100, 0, 0.95); | |
| // если уборка, шанс сильнее | |
| if (checkStrength === 'cleaning') chance = clamp(risk/70, 0, 0.98); | |
| // если сильно пахнет, небольшой бонус к обнаружению | |
| if (state.smell > 60) chance += 0.08; | |
| if (Math.random() < chance){ | |
| gameOver(false, 'Вас заметили при открытии холодильника. Судьба супа предсказуема.'); | |
| return; | |
| } else { | |
| log('Пахнуло, но выжили незамеченными.'); | |
| } | |
| } | |
| function closeDoor(){ state.doorOpen = false; pillDoor.textContent = 'дверь закрыта'; } | |
| function gameOver(win, reason){ | |
| state.over = true; state.win = !!win; state.paused = true; | |
| if (win){ log('Цивилизация достигла самосознания. Вы перехватили управление внешним миром через пищевые цепочки. Победа.'); alert('Победа! Интеллект достиг нужного уровня.'); } | |
| else { log('<b>' + reason + '</b>'); alert('Поражение: ' + reason); } | |
| updateStats(); | |
| } | |
| function tick(){ | |
| if (state.paused || state.over) return; | |
| state.t++; | |
| // Температура | |
| if (state.warmTicks>0){ state.temp = 9; state.warmTicks--; if (state.warmTicks===0) closeDoor(); } else { state.temp = 4; } | |
| // Метаболизм и рост | |
| if (state.dormantTicks>0){ | |
| state.dormantTicks--; // в гибернации почти все стоит | |
| state.energy += 0.2; // что-то там проходит по цепочке | |
| state.smell = clamp(state.smell - 0.25, 0, 100); | |
| state.visibility = clamp(state.visibility - 0.1, 0, 100); | |
| } else { | |
| // Энергия образуется из остатков супа и деятельности колонии | |
| let prod = 1 + state.population*0.12 + state.biofilm*0.03; | |
| if (state.temp>7) prod *= 1.45; // теплее — быстрее метаболизм | |
| if (state.upgrades.cold) prod *= 1.08; | |
| state.energy += prod; | |
| // Побочные эффекты | |
| const smellInc = Math.max(0, state.population*0.03 - state.biofilm*0.006 - state.upgrades.odorControl*0.02); | |
| const visInc = Math.max(0, state.population*0.02 - state.biofilm*0.008 - state.upgrades.camouflage*0.02); | |
| state.smell = clamp(state.smell + smellInc, 0, 100); | |
| state.visibility = clamp(state.visibility + visInc, 0, 100); | |
| } | |
| // Случайные небольшие находки | |
| if (Math.random() < 0.02){ state.energy += 6; log('Колония доела упавший на дно кусочек мяса. Энергия +6.'); } | |
| // События: открытие двери | |
| if (state.t >= state.nextDoor){ | |
| openDoor('normal'); | |
| state.nextDoor = state.t + rndInt(12, 24); | |
| } | |
| // Уборка холодильника | |
| if (!state.cleaningDone && state.t >= state.cleaningAt){ | |
| log('Сегодня генеральная уборка. Кто-то идет с губкой и отвагой.'); | |
| openDoor('cleaning'); | |
| state.cleaningDone = true; | |
| // Возможна следующая уборка еще через несколько минут, если выжили | |
| if (!state.over) state.cleaningAt = state.t + rndInt(180, 260), state.cleaningDone=false; | |
| } | |
| // Победа | |
| if (!state.over && state.tech >= 100){ gameOver(true, ''); } | |
| updateStats(); | |
| } | |
| function start(){ | |
| log('Вы просыпаетесь в темном холодильнике. Суп еще помнит былую температуру.'); | |
| updateStats(); | |
| setInterval(tick, 1000); | |
| } | |
| start(); | |
| </script></body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment