feat(ui): redesign modern game dashboard and add 2k/4k full-screen fluid scaling

This commit is contained in:
2026-09-17 12:59:30 +00:00
parent 84c28addfe
commit 258b1a9cb1
4 changed files with 357 additions and 34 deletions
+212 -7
View File
@@ -24,8 +24,33 @@ body {
display: flex;
flex-direction: column;
align-items: center;
width: 900px;
max-width: 98vw;
width: min(98vw, calc((98vh - 44px) * 1.5));
max-width: 1920px;
aspect-ratio: 900 / 644;
margin: 0 auto;
position: relative;
}
/* Fullscreen support (F / F11 or Fullscreen button) */
:fullscreen #game-wrapper,
:-webkit-full-screen #game-wrapper {
width: min(100vw, calc(100vh * 1.5));
max-width: 100vw;
height: 100vh;
justify-content: center;
padding: 0;
margin: 0 auto;
}
:fullscreen .top-nav-bar,
:-webkit-full-screen .top-nav-bar {
border-radius: 0;
}
:fullscreen #game-container,
:-webkit-full-screen #game-container {
border-radius: 0;
max-height: calc(100vh - 44px);
}
/* Minimalist Clean Top Navigation Bar */
@@ -138,8 +163,8 @@ body {
#game-container {
position: relative;
width: 100%;
height: 600px;
max-height: calc(95vh - 54px);
height: auto;
max-height: calc(98vh - 44px);
aspect-ratio: 900 / 600;
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.8), 0 0 20px rgba(76, 175, 80, 0.3);
border-radius: 0 0 10px 10px;
@@ -219,16 +244,163 @@ canvas#gameCanvas {
/* Start Screen Specific Card (Spacious & Fits 600px Canvas) */
.start-card {
max-width: 840px;
max-width: 860px;
width: 95%;
max-height: 565px;
padding: 12px 18px;
max-height: 96%;
padding: 14px 20px;
display: flex;
flex-direction: column;
justify-content: space-between;
overflow-y: auto;
}
.modern-dashboard {
background: radial-gradient(circle at 50% 15%, rgba(42, 22, 12, 0.95), rgba(15, 8, 4, 0.98)) !important;
border: 3px solid #ffb74d !important;
box-shadow: 0 15px 45px rgba(0, 0, 0, 0.85), 0 0 30px rgba(255, 183, 77, 0.25) !important;
border-radius: 16px !important;
backdrop-filter: blur(12px);
}
.modern-header {
display: flex;
justify-content: space-between;
align-items: center;
gap: 16px;
margin-bottom: 8px;
border-bottom: 1px solid rgba(255, 183, 77, 0.2);
padding-bottom: 8px;
}
.brand-title-wrap {
text-align: left;
}
.game-logo-text {
font-size: 22px !important;
font-weight: 900;
margin: 0 !important;
letter-spacing: 0.5px;
text-shadow: 0 2px 8px rgba(0,0,0,0.8), 2px 2px 0 #000;
line-height: 1.1;
}
.logo-plant {
color: #aed581;
text-shadow: 0 0 10px rgba(139, 195, 74, 0.6), 2px 2px 0 #000;
}
.logo-vs {
font-size: 16px;
color: #ffca28;
font-style: italic;
margin: 0 2px;
}
.logo-zombie {
color: #e57373;
text-shadow: 0 0 10px rgba(239, 83, 80, 0.6), 2px 2px 0 #000;
}
.game-version-badge {
font-size: 10px;
color: #ffe082;
letter-spacing: 0.8px;
font-weight: bold;
opacity: 0.9;
margin-top: 3px;
}
.start-hero-profile {
display: flex;
align-items: center;
gap: 10px;
background: rgba(0, 0, 0, 0.55);
border: 1.5px solid #8d6e63;
border-radius: 12px;
padding: 6px 12px;
cursor: pointer;
transition: all 0.2s ease;
box-shadow: 0 4px 12px rgba(0,0,0,0.4);
}
.start-hero-profile:hover {
background: rgba(66, 40, 23, 0.7);
border-color: #ffd54f;
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(255, 213, 79, 0.3);
}
.hero-avatar {
font-size: 26px;
line-height: 1;
}
.hero-profile-data {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 2px;
}
.hero-user-line {
display: flex;
align-items: center;
gap: 8px;
}
.hero-username {
font-size: 13px;
font-weight: bold;
color: #fff;
max-width: 140px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.badge-offline {
font-size: 9px;
background: #546e7a;
color: #eceff1;
padding: 1px 6px;
border-radius: 10px;
font-weight: bold;
text-transform: uppercase;
}
.badge-online {
font-size: 9px;
background: #2e7d32;
color: #c8e6c9;
padding: 1px 6px;
border-radius: 10px;
font-weight: bold;
text-transform: uppercase;
box-shadow: 0 0 6px rgba(76, 175, 80, 0.6);
}
.hero-stats-line {
display: flex;
gap: 8px;
font-size: 11px;
color: #ffecb3;
}
.mode-selector-wrapper {
margin-bottom: 6px;
}
.mode-selector-title {
font-size: 11px;
font-weight: bold;
color: #ffe082;
letter-spacing: 1px;
margin-bottom: 4px;
text-transform: uppercase;
opacity: 0.9;
}
.start-header h1 {
font-size: 24px;
margin: 0 0 3px 0;
@@ -263,6 +435,39 @@ canvas#gameCanvas {
font-size: 15px;
}
.btn-play-main {
background: linear-gradient(135deg, #43a047, #2e7d32) !important;
border-color: #81c784 !important;
font-size: 16px !important;
padding: 10px 28px !important;
box-shadow: 0 4px 0 #1b5e20, 0 8px 20px rgba(46, 125, 50, 0.5) !important;
letter-spacing: 0.5px;
}
.btn-play-main:hover {
background: linear-gradient(135deg, #4caf50, #388e3c) !important;
transform: translateY(-2px);
box-shadow: 0 6px 0 #1b5e20, 0 10px 25px rgba(76, 175, 80, 0.6) !important;
}
.btn-campaign {
background: linear-gradient(135deg, #0288d1, #01579b) !important;
border-color: #4fc3f7 !important;
box-shadow: 0 4px 0 #013660, 0 8px 15px rgba(2, 136, 209, 0.4) !important;
}
.btn-campaign:hover {
background: linear-gradient(135deg, #039be5, #0288d1) !important;
}
.btn-inventory {
background: linear-gradient(135deg, #8e24aa, #6a1b9a) !important;
border-color: #ba68c8 !important;
box-shadow: 0 4px 0 #4a148c, 0 8px 15px rgba(142, 36, 170, 0.4) !important;
}
.btn-inventory:hover {
background: linear-gradient(135deg, #9c27b0, #7b1fa2) !important;
}
.start-activities-grid {
display: flex;
gap: 6px;
+46 -21
View File
@@ -27,6 +27,7 @@
</div>
</div>
<div class="nav-right">
<button id="btn-fullscreen" class="nav-btn btn-icon-nav" title="Полноэкранный режим (F11 / F)">⛶ Экран</button>
<button id="btn-speed" class="nav-btn btn-icon-nav" title="Скорость игры (1x / 2x)">⚡ 1x</button>
<button id="btn-pause" class="nav-btn btn-pause btn-icon-nav" title="Пауза / Продолжить (Пробел)">⏸️ Пауза</button>
<button id="btn-mute" class="nav-btn btn-icon-nav" title="Включить / выключить звук">🔊 Звук</button>
@@ -76,47 +77,71 @@
<!-- Main Game Canvas -->
<canvas id="gameCanvas" width="900" height="600"></canvas>
<!-- Start Screen Overlay -->
<!-- Start Screen Overlay / Modern Game Dashboard -->
<div id="start-modal" class="overlay">
<div class="banner-card start-card">
<div class="start-header">
<h1>PLANTS vs. ZOMBIES</h1>
<p>Защитите газон от нашествия зомби! Выберите режим игры и начинайте сражение.</p>
<div class="banner-card start-card modern-dashboard">
<!-- 1. Top Hero Brand Bar with Player Status Widget -->
<div class="start-header modern-header">
<div class="brand-title-wrap">
<h1 class="game-logo-text"><span class="logo-plant">🌱 PLANTS</span> <span class="logo-vs">vs.</span> <span class="logo-zombie">ZOMBIES 🧟</span></h1>
<div class="game-version-badge">2K/4K HD EDITION &bull; 25 УРОВНЕЙ &bull; 5 БИОМОВ &bull; 16 РАСТЕНИЙ</div>
</div>
<!-- Mode Selectors -->
<!-- Integrated Quick Profile Pill -->
<div id="start-hero-profile" class="start-hero-profile" title="Нажмите для входа или просмотра профиля">
<div class="hero-avatar">🌻</div>
<div class="hero-profile-data">
<div class="hero-user-line">
<span id="hero-profile-name" class="hero-username">Гость</span>
<span id="hero-cloud-badge" class="badge-offline">Локально</span>
</div>
<div class="hero-stats-line">
<span title="Монеты">💰 <strong id="hero-stat-coins">150</strong></span>
<span title="Уровень кампании">🗺️ <strong id="hero-stat-level">Ур. 1</strong></span>
<span title="Рекорд очков">🏆 <strong id="hero-stat-score">0</strong></span>
</div>
</div>
</div>
</div>
<!-- 2. Game Mode Selector Chips -->
<div class="mode-selector-wrapper">
<div class="mode-selector-title">ВЫБЕРИТЕ РЕЖИМ ИГРЫ:</div>
<div class="mode-selector">
<button id="btn-mode-adv" class="mode-btn active">🌿 Приключение</button>
<button id="btn-mode-surv" class="mode-btn">⚔️ Выживание</button>
<button id="btn-mode-bowl" class="mode-btn">🎳 Боулинг</button>
<button id="btn-mode-izombie" class="mode-btn">🧠 Я, Зомби</button>
<button id="btn-mode-rogue" class="mode-btn">🎲 Рогалик</button>
<button id="btn-mode-boss" class="mode-btn">👑 Зомбосс</button>
<button id="btn-mode-bossrush" class="mode-btn">⚡ Boss Rush</button>
<button id="btn-mode-adv" class="mode-btn active" title="25 уровней с прогрессией через 5 миров">🌿 Приключение</button>
<button id="btn-mode-surv" class="mode-btn" title="Бесконечный наплыв усиливающихся волн зомби">⚔️ Выживание</button>
<button id="btn-mode-bowl" class="mode-btn" title="Сбивайте зомби катящимися орехами-шарами">🎳 Боулинг</button>
<button id="btn-mode-izombie" class="mode-btn" title="Играйте за зомби и поедайте мозги">🧠 Я, Зомби</button>
<button id="btn-mode-rogue" class="mode-btn" title="Рогалик со случайными реликвиями на каждом этаже">🎲 Рогалик</button>
<button id="btn-mode-boss" class="mode-btn" title="Эпическая битва против гигантского Зомбота 6000 HP">👑 Зомбосс</button>
<button id="btn-mode-bossrush" class="mode-btn" title="Испытание на выносливость против 3 волн боссов">⚡ Boss Rush</button>
</div>
</div>
<div id="start-record" class="start-record-line"></div>
<!-- Hero Action Row -->
<!-- 3. Primary Action Row -->
<div class="start-hero-row">
<button id="btn-start" class="btn btn-hero">▶ ИГРАТЬ</button>
<button id="btn-campaign-select" class="btn btn-campaign btn-hero">🗺️ КАРТА КАМПАНИИ (25 УРОВНЕЙ)</button>
<button id="btn-start" class="btn btn-hero btn-play-main">▶ ИГРАТЬ СЕЙЧАС</button>
<button id="btn-campaign-select" class="btn btn-campaign btn-hero">🗺️ КАРТА КАМПАНИИ</button>
<button id="btn-inventory-start" class="btn btn-inventory btn-hero">🎒 ВЫБОР КОЛОДЫ</button>
</div>
<!-- Secondary Activities Row -->
<!-- 4. Modern Activities Grid Hub -->
<div class="start-activities-grid">
<button id="btn-inventory-start" class="btn btn-inventory btn-act">🎒 Выбор растений (Колода)</button>
<button id="btn-shop" class="btn btn-shop btn-act">🏪 Магазин Дэйва</button>
<button id="btn-workshop-start" class="btn btn-workshop btn-act">🧪 Сад-Мастерская</button>
<button id="btn-workshop-start" class="btn btn-workshop btn-act">🧪 Мастерская</button>
<button id="btn-zengarden-start" class="btn btn-zengarden btn-act">🪴 Дзен-Сад</button>
<button id="btn-almanac-start" class="btn btn-almanac btn-act">📖 Альманах</button>
<button id="btn-achievements-start" class="btn btn-achievements btn-act">🏆 Достижения</button>
<button id="btn-tree-start" class="btn btn-tree btn-act">🌳 Древо Мудрости</button>
<button id="btn-leaderboard-start" class="btn btn-leaderboard btn-act">🏅 Рекорды</button>
<button id="btn-bugreport-start" class="btn btn-bugreport btn-act">🐛 Баг-репорт / Чат</button>
<button id="btn-bugreport-start" class="btn btn-bugreport btn-act">🐛 Чат / Баги</button>
</div>
<div class="footer-info">ai.qpid.pro &bull; 13 растений &bull; 8 видов зомби &bull; 5 биомов: День, Ночь, Бассейн, Туман, Крыша &bull; 25 уровней</div>
<div class="footer-info">
pvz.qpid.pro &bull; 16 видов растений &bull; 8 видов зомби &bull; 5 миров: День, Ночь, Бассейн, Туман, Крыша &bull; 25 этапов
</div>
</div>
</div>
+67 -5
View File
@@ -214,14 +214,34 @@ window.GameEngine = class GameEngine {
const statScore = document.getElementById('profile-stat-score');
const statMaterials = document.getElementById('profile-stat-materials');
// Modern Dashboard Hero Profile elements
const heroName = document.getElementById('hero-profile-name');
const heroCloud = document.getElementById('hero-cloud-badge');
const heroCoins = document.getElementById('hero-stat-coins');
const heroLevel = document.getElementById('hero-stat-level');
const heroScore = document.getElementById('hero-stat-score');
if (window.PvZAuth) {
const user = window.PvZAuth.getCurrentUser();
const isGuest = window.PvZAuth.isGuest();
const uData = window.PvZAuth.getUserData();
const uData = window.PvZAuth.getUserData() || {};
const currentCoins = uData.coins !== undefined ? uData.coins : (this.economy ? this.economy.totalCoins : 0);
const currentLevelNum = uData.level !== undefined ? uData.level : (this.currentLevel || 1);
const currentScoreNum = uData.highScore !== undefined ? uData.highScore : (this.highScore || 0);
if (badgeName) badgeName.textContent = isGuest ? '👤 Гость' : `👤 ${user}`;
if (btnAuth) btnAuth.textContent = isGuest ? 'Войти' : 'Профиль';
if (heroName) heroName.textContent = isGuest ? 'Гость' : user;
if (heroCloud) {
heroCloud.textContent = isGuest ? 'Локально' : 'Облако';
heroCloud.className = isGuest ? 'badge-offline' : 'badge-online';
}
if (heroCoins) heroCoins.textContent = currentCoins.toLocaleString('ru-RU');
if (heroLevel) heroLevel.textContent = `Ур. ${currentLevelNum}`;
if (heroScore) heroScore.textContent = currentScoreNum.toLocaleString('ru-RU');
if (btnLogoutNav) {
if (isGuest) btnLogoutNav.classList.add('hidden');
else btnLogoutNav.classList.remove('hidden');
@@ -238,9 +258,9 @@ window.GameEngine = class GameEngine {
if (registerView) registerView.classList.add('hidden');
if (userTitle) userTitle.textContent = user;
if (statCoins) statCoins.textContent = (uData.coins || (this.economy ? this.economy.totalCoins : 0)).toLocaleString('ru-RU');
if (statLevel) statLevel.textContent = `Уровень ${uData.level || this.currentLevel || 1}`;
if (statScore) statScore.textContent = (uData.highScore || this.highScore || 0).toLocaleString('ru-RU');
if (statCoins) statCoins.textContent = currentCoins.toLocaleString('ru-RU');
if (statLevel) statLevel.textContent = `Уровень ${currentLevelNum}`;
if (statScore) statScore.textContent = currentScoreNum.toLocaleString('ru-RU');
if (statMaterials) {
const mats = uData.materials || (this.workshop ? this.workshop.materials : {});
const totalMats = (mats.bioHumus || 0) + (mats.scrapMetal || 0) + (mats.sunEssence || 0) + (mats.crystals || 0);
@@ -856,7 +876,39 @@ window.GameEngine = class GameEngine {
});
}
// Global Pause Hotkeys (Space, KeyP)
// Fullscreen Toggle
const btnFullscreen = document.getElementById('btn-fullscreen');
const toggleFullscreen = () => {
if (!document.fullscreenElement && !document.webkitFullscreenElement) {
const docEl = document.documentElement;
if (docEl.requestFullscreen) {
docEl.requestFullscreen().catch(() => {});
} else if (docEl.webkitRequestFullscreen) {
docEl.webkitRequestFullscreen();
}
} else {
if (document.exitFullscreen) {
document.exitFullscreen().catch(() => {});
} else if (document.webkitExitFullscreen) {
document.webkitExitFullscreen();
}
}
};
if (btnFullscreen) {
btnFullscreen.addEventListener('click', toggleFullscreen);
}
const updateFullscreenBtn = () => {
if (btnFullscreen) {
const isFs = !!(document.fullscreenElement || document.webkitFullscreenElement);
btnFullscreen.textContent = isFs ? '⛶ Окно' : '⛶ Экран';
}
};
document.addEventListener('fullscreenchange', updateFullscreenBtn);
document.addEventListener('webkitfullscreenchange', updateFullscreenBtn);
// Global Hotkeys (Space, KeyP for Pause; KeyF for Fullscreen)
window.addEventListener('keydown', (e) => {
// Do not intercept hotkeys if user is typing in an input or textarea
if (['INPUT', 'TEXTAREA', 'SELECT'].includes(document.activeElement?.tagName)) return;
@@ -865,6 +917,9 @@ window.GameEngine = class GameEngine {
if (this.isRunning) {
this.togglePause();
}
} else if (e.code === 'KeyF') {
e.preventDefault();
toggleFullscreen();
}
});
@@ -969,6 +1024,13 @@ window.GameEngine = class GameEngine {
});
}
const startHeroProfile = document.getElementById('start-hero-profile');
if (startHeroProfile && btnAuthOpen) {
startHeroProfile.addEventListener('click', () => {
btnAuthOpen.click();
});
}
// Modal Close buttons
if (btnAuthClose && authModal) {
btnAuthClose.addEventListener('click', () => {
+31
View File
@@ -784,6 +784,36 @@ def test_plant_shop_and_deck_selection():
record("GameEngine.renderSeedSelectionUI implemented", "PASS" if "renderSeedSelectionUI(" in game_code else "FAIL")
record("Shop plants nursery population implemented in updateShopUI", "PASS" if "shop-plants-grid" in game_code and "PLANT_CATALOGUE" in game_code else "FAIL")
def test_modern_dashboard_and_fullscreen():
print("\n--- 11c. Modern Dashboard & 2K/4K Fullscreen Responsive ---")
html_path = os.path.join(GAME_DIR, "index.html")
with open(html_path, "r", encoding="utf-8") as f:
html_code = f.read()
css_path = os.path.join(GAME_DIR, "css/style.css")
with open(css_path, "r", encoding="utf-8") as f:
css_code = f.read()
game_js_path = os.path.join(GAME_DIR, "js/game.js")
with open(game_js_path, "r", encoding="utf-8") as f:
game_code = f.read()
dashboard_checks = [
("Fullscreen button present in top nav", 'id="btn-fullscreen"' in html_code),
("Modern dashboard class and structure present", "modern-dashboard" in html_code and "modern-header" in html_code),
("Hero profile widget present on dashboard", 'id="start-hero-profile"' in html_code and 'id="hero-profile-name"' in html_code),
("Hero stat pills (coins, level, score) present", 'id="hero-stat-coins"' in html_code and 'id="hero-stat-level"' in html_code and 'id="hero-stat-score"' in html_code),
("Fluid 2K/4K scaling CSS rules present", "min(98vw" in css_code and "aspect-ratio: 900 / 644" in css_code),
("Fullscreen CSS rules present", ":fullscreen #game-wrapper" in css_code and ":fullscreen #game-container" in css_code),
("Fullscreen API handler and toggle in game.js", "btn-fullscreen" in game_code and "requestFullscreen" in game_code),
("Fullscreen hotkey KeyF supported in game.js", "KeyF" in game_code),
("Hero profile sync in updateProfileUI", "heroName" in game_code and "heroCoins" in game_code and "heroLevel" in game_code),
("Start hero profile click opens auth modal", "startHeroProfile" in game_code and "btnAuthOpen.click()" in game_code),
]
for name, cond in dashboard_checks:
record(name, "PASS" if cond else "FAIL")
def main():
print("==================================================")
print("🌱 PLANTS VS. ZOMBIES — AUTONOMOUS TEST SUITE 🌱")
@@ -801,6 +831,7 @@ def main():
test_campaign_system()
test_feedback_and_layout_system()
test_plant_shop_and_deck_selection()
test_modern_dashboard_and_fullscreen()
test_http_endpoint()
passed = sum(1 for r in TEST_RESULTS if r["status"] == "PASS")