Upíří internáty - Základna
?
QR Kód s aktuální adresou
Fullscreen
Upíří internáty
Aktivovat základnu
Tímto se tablet odemkne a povolí zvuková upozornění.
Internát:
Noc je klidná... zatím.
Až nastane čas, základna vás upozorní.
Lov začal!
Internát:
Přelosovat (-15 krve)
05:00
Upravit obsah stránky
{% extends "templates/base.html" %} {% set game_name = "Upíří internáty - Základna" %} {% block content %} <button id="goFullscreen" class="btn btn-outline-danger bg-dark fw-bold shadow-lg" style="position:fixed; top:10px; right:10px; z-index:2000;"> <i class="fa-solid fa-expand"></i> Fullscreen </button> <div id="flashOverlay"></div> <div id="gameView" class="full-screen-app"> <div class="container py-4 h-100 d-flex flex-column"> <div id="loginScreen" class="text-center my-auto"> <h1 class="vampire-header mb-5"><i class="fa-solid fa-bat"></i> Upíří internáty <i class="fa-solid fa-bat"></i></h1> <div class="row justify-content-center"> <div class="col-md-6 col-lg-5"> <input type="text" id="playerName" class="form-control form-control-lg bg-dark text-white border-danger text-center mb-3 fs-4" placeholder="Zadejte název internátu..."> <button class="btn btn-blood btn-lg w-100 fs-3 fw-bold" onclick="login()"><i class="fa-solid fa-moon"></i> Aktivovat základnu</button> <small class="text-light mt-3 d-block fs-5">Tímto se tablet odemkne a povolí zvuková upozornění.</small> </div> </div> </div> <div id="idleScreen" class="text-center my-auto" style="display: none;"> <h3 class="text-warning mb-2 fs-2">Internát: <span id="displayTeamNameIdle" class="fw-bold text-white"></span></h3> <i class="fa-solid fa-moon fa-5x text-light mb-4 mt-4" style="text-shadow: 0 0 15px #fff;"></i> <h2 class="text-white display-4 fw-bold">Noc je klidná... zatím.</h2> <p class="text-light fs-4 mt-3">Až nastane čas, základna vás upozorní.</p> </div> <div id="activeScreen" style="display: none;"> <div class="d-flex justify-content-between align-items-center mb-2"> <div> <h3 class="vampire-header m-0 fs-2"><i class="fa-solid fa-skull"></i> Lov začal!</h3> <h4 class="text-warning mt-1">Internát: <span id="displayTeamNameActive" class="text-white"></span></h4> </div> <button class="btn btn-warning btn-lg fw-bold shadow" onclick="rerollTasks()"><i class="fa-solid fa-dice"></i> Přelosovat (-15 krve)</button> </div> <div class="text-center mb-4"> <div id="countdownDisplay" class="text-danger">05:00</div> </div> <div id="tasksContainer" class="row g-3 justify-content-center"> </div> </div> </div> </div> <style> .full-screen-app { position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background-color: #121212; color: #ffffff; overflow-y: auto; overflow-x: hidden; z-index: 1000; } .vampire-header { color: #dc3545; text-transform: uppercase; letter-spacing: 2px; text-shadow: 2px 2px 4px #000; } .card { background-color: #1e1e1e; border: 1px solid #dc3545; box-shadow: 0 4px 15px rgba(220, 53, 69, 0.3); } .btn-blood { background-color: #8b0000; color: white; border: none; transition: 0.2s; } .btn-blood:hover:not(:disabled) { background-color: #a50000; color: white; transform: scale(1.05); } .task-heavy { border-color: #ffc107; border-width: 3px; } .task-medium { border-color: #fd7e14; border-width: 2px; } .task-light { border-color: #dc3545; } #countdownDisplay { font-size: 4.5rem; font-weight: bold; font-variant-numeric: tabular-nums; text-shadow: 0 0 20px #dc3545; line-height: 1; } .points-badge { font-size: 1.5rem; } #flashOverlay { display: none; position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background-color: rgba(255, 0, 0, 0.6); z-index: 9999; pointer-events: none; } </style> <script> const fsBtn = document.getElementById('goFullscreen'); fsBtn.addEventListener('click', () => { const el = document.documentElement; if(el.requestFullscreen) el.requestFullscreen(); else if(el.webkitRequestFullscreen) el.webkitRequestFullscreen(); else if(el.msRequestFullscreen) el.msRequestFullscreen(); }); document.addEventListener('fullscreenchange', () => { if (!document.fullscreenElement) fsBtn.style.display = 'block'; else fsBtn.style.display = 'none'; }); document.addEventListener('webkitfullscreenchange', () => { if (!document.webkitFullscreenElement) fsBtn.style.display = 'block'; else fsBtn.style.display = 'none'; }); forrestHubLib = ForrestHubLib.getInstance(); const ARRAYS = { LEADERBOARD: 'vampire_leaderboard_v3' }; let playerName = ""; let audioCtx = null; let gameState = { status: 'idle', waveId: null, endTime: 0, durationMs: 300000 }; let pools = { heavy: [], medium: [], light: [] }; let localTasks = []; let timerInterval = null; document.addEventListener('DOMContentLoaded', async () => { playerName = localStorage.getItem('vampire_playerName') || ""; if (playerName) { document.getElementById('playerName').value = playerName; } setInterval(syncState, 1000); }); async function syncState() { if (!playerName) return; try { const poolsData = await forrestHubLib.dbVarGetKey('vampire_pools'); if (poolsData) pools = JSON.parse(poolsData); const stateData = await forrestHubLib.dbVarGetKey('vampire_state'); if (stateData) { const newState = JSON.parse(stateData); if (newState.status === 'active' && newState.waveId !== gameState.waveId) { gameState = newState; startNewWave(); } else if (newState.status === 'idle' && gameState.status === 'active') { gameState = newState; endWave(); } } } catch (e) {} } function login() { playerName = document.getElementById('playerName').value.trim(); if (!playerName) return forrestHubLib.uiShowAlert('warning', 'Zadej název internátu!'); localStorage.setItem('vampire_playerName', playerName); document.getElementById('displayTeamNameIdle').innerText = playerName; document.getElementById('displayTeamNameActive').innerText = playerName; audioCtx = new (window.AudioContext || window.webkitAudioContext)(); document.getElementById('loginScreen').style.display = 'none'; const savedWave = localStorage.getItem('vampire_localWaveId'); if (savedWave && gameState.status === 'active' && savedWave === gameState.waveId) { localTasks = JSON.parse(localStorage.getItem('vampire_localTasks')); renderTasks(); switchScreen('active'); } else { switchScreen('idle'); } syncState(); } function switchScreen(screen) { document.getElementById('idleScreen').style.display = screen === 'idle' ? 'block' : 'none'; document.getElementById('activeScreen').style.display = screen === 'active' ? 'block' : 'none'; } function playAlarm() { if (audioCtx) { for(let i=0; i<4; i++) { setTimeout(() => { const osc = audioCtx.createOscillator(); osc.type = 'square'; osc.frequency.setValueAtTime(500 + (i*50), audioCtx.currentTime); osc.connect(audioCtx.destination); osc.start(); osc.stop(audioCtx.currentTime + 0.2); }, i * 350); } } const overlay = document.getElementById('flashOverlay'); overlay.style.display = 'block'; let flashes = 0; const flashInt = setInterval(() => { overlay.style.backgroundColor = overlay.style.backgroundColor === 'rgba(255, 0, 0, 0.6)' ? 'transparent' : 'rgba(255, 0, 0, 0.6)'; flashes++; if(flashes > 12) { clearInterval(flashInt); overlay.style.display = 'none'; } }, 250); } // --- NOVÁ LOGIKA PRO NEOPAKOVÁNÍ ÚKOLŮ --- function getRandomTask(type) { const pool = pools[type] || []; if (pool.length === 0) return `(Došla zásoba pro kategorii ${type})`; // Načtení už použitých úkolů pro tento tablet const storageKey = `vampire_used_${type}`; let usedTasks = JSON.parse(localStorage.getItem(storageKey)) || []; // Zjištění dostupných úkolů (ty, co ještě nejsou v usedTasks a co zrovna teď nesvítí na obrazovce) // localTasks obsahuje aktuálně zobrazené, ať nelosujeme 2x to stejné do jednoho kola const currentDisplayed = localTasks ? localTasks.map(t => t.text) : []; let availableTasks = pool.filter(task => !usedTasks.includes(task) && !currentDisplayed.includes(task)); // Pokud došly úkoly, vymažeme paměť a jedeme od znova if (availableTasks.length === 0) { console.log(`Zásoba úkolů '${type}' vyčerpána, resetuji historii.`); usedTasks = []; availableTasks = pool.filter(task => !currentDisplayed.includes(task)); // Pojistka, pokud by v poolu byl celkově jen 1 úkol if(availableTasks.length === 0) availableTasks = pool; } // Vybereme náhodný z dostupných const randomIndex = Math.floor(Math.random() * availableTasks.length); const selectedTask = availableTasks[randomIndex]; // Uložíme ho jako použitý usedTasks.push(selectedTask); localStorage.setItem(storageKey, JSON.stringify(usedTasks)); return selectedTask; } function startNewWave() { playAlarm(); generateTasks(); localStorage.setItem('vampire_localWaveId', gameState.waveId); switchScreen('active'); if (timerInterval) clearInterval(timerInterval); timerInterval = setInterval(updateTimerAndPoints, 200); } function endWave() { switchScreen('idle'); if (timerInterval) clearInterval(timerInterval); localStorage.removeItem('vampire_localWaveId'); localTasks = []; // Vyčistíme lokální po skončení vlny } function generateTasks() { localTasks = []; // Reset před plněním localTasks.push({ id: Date.now()+1, type: 'heavy', text: getRandomTask('heavy'), maxPts: 20 }); localTasks.push({ id: Date.now()+2, type: 'medium', text: getRandomTask('medium'), maxPts: 10 }); localTasks.push({ id: Date.now()+3, type: 'light', text: getRandomTask('light'), maxPts: 5 }); localTasks.push({ id: Date.now()+4, type: 'light', text: getRandomTask('light'), maxPts: 5 }); localTasks.push({ id: Date.now()+5, type: 'light', text: getRandomTask('light'), maxPts: 5 }); saveLocalTasks(); renderTasks(); } function saveLocalTasks() { localStorage.setItem('vampire_localTasks', JSON.stringify(localTasks)); } function rerollTasks() { if (confirm('Opravdu zaplatit 15 krve za nové úkoly? Odpočet se nezastaví!')) { payPoints(-15); generateTasks(); forrestHubLib.uiShowAlert('warning', 'Úkoly přelosovány! Krev prolita.'); } } function updateTimerAndPoints() { const now = Date.now(); const timeLeftMs = Math.max(0, gameState.endTime - now); const mins = Math.floor(timeLeftMs / 60000).toString().padStart(2, '0'); const secs = Math.floor((timeLeftMs % 60000) / 1000).toString().padStart(2, '0'); document.getElementById('countdownDisplay').innerText = `${mins}:${secs}`; const ratio = timeLeftMs / gameState.durationMs; document.querySelectorAll('.task-card').forEach(el => { const maxPts = parseInt(el.dataset.max); const currentPts = Math.max(1, Math.ceil(ratio * maxPts)); el.querySelector('.points-val').innerText = currentPts; }); if (timeLeftMs <= 0) endWave(); } function renderTasks() { const container = document.getElementById('tasksContainer'); container.innerHTML = ''; localTasks.forEach((task, index) => { let borderClass = task.type === 'heavy' ? 'task-heavy' : (task.type === 'medium' ? 'task-medium' : 'task-light'); let icon = task.type === 'heavy' ? '<i class="fa-solid fa-users text-warning"></i> TÝMOVÝ' : (task.type === 'medium' ? '<i class="fa-solid fa-user-ninja" style="color:#fd7e14"></i> STŘEDNÍ' : '<i class="fa-solid fa-bolt text-danger"></i> RYCHLÝ'); container.innerHTML += ` <div class="col-md-6 col-lg-4"> <div class="card h-100 text-center task-card ${borderClass}" data-max="${task.maxPts}"> <div class="card-header d-flex justify-content-between align-items-center"> <span class="text-white fw-bold fs-5">${icon}</span> <span class="badge bg-danger points-badge"><span class="points-val">${task.maxPts}</span> b</span> </div> <div class="card-body d-flex flex-column justify-content-between p-3"> <h4 class="card-text mb-4 text-white">${task.text}</h4> <button class="btn btn-blood btn-lg w-100 shadow fs-4 fw-bold" onclick="completeTask(${index})"> Splněno! </button> </div> </div> </div> `; }); updateTimerAndPoints(); } async function completeTask(index) { const task = localTasks[index]; const ratio = Math.max(0, gameState.endTime - Date.now()) / gameState.durationMs; const pointsEarned = Math.max(1, Math.ceil(ratio * task.maxPts)); await payPoints(pointsEarned); forrestHubLib.uiShowAlert('success', `Získáno ${pointsEarned} kapek krve!`); // Splněný úkol se hned nahradí novým lehkým úkolem localTasks[index] = { id: Date.now(), type: 'light', text: getRandomTask('light'), maxPts: 5 }; saveLocalTasks(); renderTasks(); } async function payPoints(amount) { try { const leaderboard = await forrestHubLib.dbArrayFetchAllRecords(ARRAYS.LEADERBOARD) || {}; let playerId = null; let currentPoints = 0; for (const [key, val] of Object.entries(leaderboard)) { if (val.name === playerName) { playerId = key; currentPoints = parseInt(val.points); break; } } if (playerId) { await forrestHubLib.dbArrayUpdateRecord(ARRAYS.LEADERBOARD, playerId, { name: playerName, points: currentPoints + amount }); } else { await forrestHubLib.dbArrayAddRecord(ARRAYS.LEADERBOARD, { name: playerName, points: amount }); } } catch (e) { console.error(e); } } </script> {% endblock %}