[html]<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<style>
@import url('https://fonts.googleapis.com/css2?family=Oswald:wght@400;700&family=Roboto+Condensed:wght@400;700&display=swap');
*{margin:0;padding:0;box-sizing:border-box}
body{background:#0a0a0c;background-image:radial-gradient(circle at center,#1a1a2e 0%,#0a0a0c 100%);color:#e0e0e0;font-family:'Roboto Condensed',sans-serif;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:20px}
.game-container{display:grid;grid-template-columns:300px 1fr;gap:25px;max-width:1100px;width:100%;background:rgba(0,0,0,0.6);padding:30px;border-radius:20px;border:1px solid rgba(255,255,255,0.05);box-shadow:0 20px 50px rgba(0,0,0,0.5);backdrop-filter:blur(10px)}
.sidebar{display:flex;flex-direction:column;gap:20px}
.panel{background:rgba(255,255,255,0.03);border-left:3px solid #ff4655;padding:15px;border-radius:4px}
.panel-h{font-family:'Oswald',sans-serif;font-size:14px;letter-spacing:2px;color:#ff4655;margin-bottom:12px;text-transform:uppercase}
.stat-row{display:flex;justify-content:space-between;padding:6px 0;border-bottom:1px solid rgba(255,255,255,0.05);font-size:14px;gap:10px}
.record-info{display:flex;flex-direction:column;gap:4px;text-align:right}
.record-player{color:#aaa;font-size:12px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:180px}
.log-container{font-size:13px;height:200px;overflow-y:auto;scrollbar-width:thin;scrollbar-color:#ff4655 transparent}
.log-entry{padding:4px 0;color:#aaa;border-bottom:1px solid rgba(255,255,255,0.02)}
.log-entry b{color:#ff4655}
.main-content{display:flex;flex-direction:column;gap:20px}
.boss-frame{position:relative;border-radius:12px;overflow:hidden;box-shadow:0 0 30px rgba(255,70,85,0.2);border:1px solid rgba(255,70,85,0.3);transition:transform 0.1s}
.boss-img{width:100%;height:450px;object-fit:cover;display:block}
.boss-overlay{position:absolute;bottom:0;left:0;right:0;padding:40px 20px 20px;background:linear-gradient(to top,rgba(0,0,0,0.9) 0%,transparent 100%)}
.boss-info{display:flex;justify-content:space-between;align-items:flex-end;margin-bottom:8px;gap:10px}
.boss-name{font-family:'Oswald',sans-serif;font-size:32px;font-weight:700;color:#fff}
.hp-text{font-family:'Oswald',sans-serif;color:#ff4655;font-size:18px}
.hp-bar-bg{height:12px;background:rgba(255,255,255,0.1);border-radius:6px;overflow:hidden}
.hp-bar-fill{height:100%;background:linear-gradient(90deg,#ff4655,#ff8a71);box-shadow:0 0 15px #ff4655;width:100%;transition:width 0.4s ease-out}
.controls{display:grid;grid-template-columns:1fr 200px;gap:15px}
.game-input{width:100%;height:60px;background:rgba(255,255,255,0.05);border:1px solid rgba(255,255,255,0.1);border-radius:8px;padding:0 20px;color:#fff;font-size:16px;outline:none}
.btn-attack{height:60px;background:#ff4655;color:#fff;border:none;border-radius:8px;font-family:'Oswald',sans-serif;font-size:20px;font-weight:700;cursor:pointer;text-transform:uppercase;transition:0.2s}
.btn-attack:hover:not(:disabled){background:#ff5e6a;transform:translateY(-2px)}
.btn-attack:disabled{background:#333;cursor:not-allowed}
.damage-popup{text-align:center;font-family:'Oswald',sans-serif;font-size:24px;color:#ff4655;height:30px;margin-top:10px}
</style>
</head>
<body>
<div class="game-container">
<aside class="sidebar">
<div class="panel">
<div class="panel-h">Лидеры</div>
<div id="topList"></div>
</div>
<div class="panel">
<div class="panel-h">Рекорды</div>
<div class="stat-row">
<span>Макс. урон</span>
<div class="record-info">
<b id="maxDmg" style="color:#ff4655">-</b>
<div id="maxPlayer" class="record-player">-</div>
</div>
</div>
<div class="stat-row">
<span>Мин. урон</span>
<div class="record-info">
<b id="minDmg" style="color:#fff">-</b>
<div id="minPlayer" class="record-player">-</div>
</div>
</div>
</div>
<div class="panel" style="flex-grow:1;">
<div class="panel-h">Журнал боя</div>
<div id="logs" class="log-container"></div>
</div>
</aside>
<main class="main-content">
<div class="boss-frame" id="bossFrame">
<img src="https://upforme.ru/uploads/001c/84/76/2/433839.jpg" class="boss-img" alt="Boss">
<div class="boss-overlay">
<div class="boss-info">
<div class="boss-name">собака сутулая</div>
<div id="hpText" class="hp-text">... / ...</div>
</div>
<div class="hp-bar-bg">
<div id="hpBar" class="hp-bar-fill"></div>
</div>
</div>
</div>
<div class="controls">
<input type="text" id="msg" class="game-input" placeholder="Введите боевой клич...">
<button id="atkBtn" class="btn-attack">АТАКОВАТЬ</button>
</div>
<div id="dmgRes" class="damage-popup"></div>
</main>
</div>
<script>
const send = (action, data = {}) => {
const requestId = Math.random().toString(16).slice(2);
window.parent.postMessage({ _monsterGame: true, type: "gameRequest", requestId, action, ...data }, "*");
return new Promise(resolve => {
const handler = (e) => {
if (e.data?._monsterGame && e.data.type === "gameResponse" && e.data.requestId === requestId) {
window.removeEventListener("message", handler);
resolve(e.data.data);
}
};
window.addEventListener("message", handler);
});
};
// ✅ кэш снапшота в текущей вкладке (ускоряет первый рендер)
const SNAP_CACHE_KEY = "monster_snapshot_cache_v1";
function applySnapshotToUI(snap) {
if (!snap || !snap.game || !snap.game.boss) return;
const game = snap.game;
const top = snap.top || [];
const logs = snap.logs || [];
const rec = snap.records || { maxVal:null, maxPlayers:[], minVal:null, minPlayers:[] };
const currentHp = game.boss.currentHealth;
const maxHp = game.boss.maxHealth;
const hpPct = Math.max(0, (currentHp / maxHp) * 100);
document.getElementById('hpBar').style.width = hpPct + '%';
document.getElementById('hpText').textContent = `${currentHp} / ${maxHp}`;
const isOver = (currentHp <= 0) || !!game.boss.isDefeated;
document.getElementById('atkBtn').disabled = isOver;
const cd = snap.cooldownLeft || 0;
const btn = document.getElementById('atkBtn');
btn.disabled = isOver || cd > 0;
if (!isOver && cd > 0) {
const mins = Math.ceil(cd / 60000);
btn.textContent = `ЖДАТЬ ${mins} МИН`;
} else {
btn.textContent = 'АТАКОВАТЬ';
}
document.getElementById('topList').innerHTML =
top.slice(0,5).map(p => `<div class="stat-row"><span>${p.nickname}</span><b>${p.totalDamage}</b></div>`).join('')
|| 'Нет данных';
// ✅ корректный рендер логов: атаки отдельно, системные строки отдельно
document.getElementById('logs').innerHTML = logs.slice(-20).reverse().map(l => {
const m = l.match(/^(.+?) нанес (\d+) урона$/);
if (m) return `<div class="log-entry"><b>${m[1]}</b> нанес ${m[2]} урона</div>`;
return `<div class="log-entry">${l}</div>`;
}).join('');
if (rec.maxVal != null) {
document.getElementById('maxDmg').textContent = String(rec.maxVal);
document.getElementById('maxPlayer').textContent = (rec.maxPlayers || []).join(', ') || '-';
} else {
document.getElementById('maxDmg').textContent = '-';
document.getElementById('maxPlayer').textContent = '-';
}
if (rec.minVal != null) {
document.getElementById('minDmg').textContent = String(rec.minVal);
document.getElementById('minPlayer').textContent = (rec.minPlayers || []).join(', ') || '-';
} else {
document.getElementById('minDmg').textContent = '-';
document.getElementById('minPlayer').textContent = '-';
}
}
// ✅ получаем снапшот, сохраняем в кэш и рисуем
async function updateUI() {
try {
const snap = await send("getSnapshot");
sessionStorage.setItem(SNAP_CACHE_KEY, JSON.stringify(snap));
applySnapshotToUI(snap);
} catch (e) {
console.error("UI Update Error:", e);
}
}
document.getElementById('atkBtn').onclick = async () => {
const msgInput = document.getElementById('msg');
const btn = document.getElementById('atkBtn');
btn.disabled = true;
const res = await send("processAttack", { attackData: { message: msgInput.value } });
if (res && res.cooldown) {
const mins = Math.ceil(res.cooldownLeft / 60000);
document.getElementById('dmgRes').textContent = `Повторная атака через ${mins} мин.`;
setTimeout(() => document.getElementById('dmgRes').textContent = '', 3000);
await updateUI();
return;
}
if (res && res.gameOver) {
document.getElementById('dmgRes').textContent = 'БОЙ ЗАВЕРШЕН!';
await updateUI();
return;
}
if (res && res.success) {
document.getElementById('dmgRes').textContent = `УДАР: -${res.damage} HP!`;
msgInput.value = '';
const frame = document.getElementById('bossFrame');
frame.style.transform = 'translate(4px, 4px) rotate(1deg)';
setTimeout(() => frame.style.transform = 'none', 100);
// небольшая задержка, чтобы UI точно считал сохранённое
setTimeout(updateUI, 250);
}
};
// ✅ мгновенная отрисовка из кэша (если он есть), потом подтягиваем актуальное
try {
const cached = sessionStorage.getItem(SNAP_CACHE_KEY);
if (cached) applySnapshotToUI(JSON.parse(cached));
} catch(e) {}
updateUI();
setInterval(updateUI, 15000);
</script>
</body>
</html>[/html]
[hideprofile]