feat: Add Pause system, User Profile Dashboard, and shift grid with neighbour fence
- Implement in-game pause with Space/KeyP hotkeys and pause banner overlay - Split Auth modal into clean Profile Dashboard (stats, coins, cloud save status) and Guest Login - Shift lawn startY to 108 to make top-row zombie HP bars 100% visible - Render decorative wooden neighbour fence above top row
This commit is contained in:
1 parent
82654d8f64
commit
2cd343e8ef
4 files changed
+316
-30
No files matched your search
+91
-3
@@ -450,10 +450,98 @@ canvas#gameCanvas {
|
|||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Auth Modal Styles */
|
/* Auth Modal & Profile Dashboard Styles */
|
||||||
.auth-card {
|
.auth-card {
|
||||||
max-width: 420px;
|
max-width: 440px;
|
||||||
width: 90%;
|
width: 92%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-view.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-header-card {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 14px;
|
||||||
|
background: rgba(0, 0, 0, 0.4);
|
||||||
|
border: 1.5px solid #81c784;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 10px 14px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-avatar-icon {
|
||||||
|
font-size: 32px;
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: 2px solid #81c784;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-info-text {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-username {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #ffeb3b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-status-badge {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #a5d6a7;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-stats-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-stat-box {
|
||||||
|
background: rgba(0, 0, 0, 0.35);
|
||||||
|
border: 1px solid #5d4037;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-stat-box .stat-label {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #bcaaa4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-stat-box .stat-value {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #fff;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-pause {
|
||||||
|
background: #37474f;
|
||||||
|
color: #eceff1;
|
||||||
|
border-color: #78909c;
|
||||||
|
}
|
||||||
|
.btn-pause:hover {
|
||||||
|
background: #455a64;
|
||||||
|
border-color: #b0bec5;
|
||||||
|
}
|
||||||
|
.btn-pause.active {
|
||||||
|
background: #d84315 !important;
|
||||||
|
border-color: #ffab91 !important;
|
||||||
|
color: #fff !important;
|
||||||
|
box-shadow: 0 0 10px rgba(255, 87, 34, 0.6) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.auth-input {
|
.auth-input {
|
||||||
|
|||||||
+46
-9
@@ -39,6 +39,7 @@
|
|||||||
<div class="nav-right">
|
<div class="nav-right">
|
||||||
<button id="btn-theme" class="nav-btn" title="Смена темы оформления (Классика, Киберпанк, Ретро, Зима)">🎨 Классика</button>
|
<button id="btn-theme" class="nav-btn" title="Смена темы оформления (Классика, Киберпанк, Ретро, Зима)">🎨 Классика</button>
|
||||||
<button id="btn-speed" class="nav-btn" title="Ускорить игру">⚡ 1x</button>
|
<button id="btn-speed" class="nav-btn" title="Ускорить игру">⚡ 1x</button>
|
||||||
|
<button id="btn-pause" class="nav-btn btn-pause" title="Пауза / Продолжить (Пробел)">⏸️ Пауза</button>
|
||||||
<button id="btn-music" class="nav-btn" title="Включить/выключить музыку">🎵 Музыка</button>
|
<button id="btn-music" class="nav-btn" title="Включить/выключить музыку">🎵 Музыка</button>
|
||||||
<button id="btn-mute" class="nav-btn" title="Включить/выключить звук">🔊 Звук</button>
|
<button id="btn-mute" class="nav-btn" title="Включить/выключить звук">🔊 Звук</button>
|
||||||
<button id="btn-mobile-drawer" class="nav-btn btn-drawer-toggle" title="Открыть меню разделов">☰ Разделы</button>
|
<button id="btn-mobile-drawer" class="nav-btn btn-drawer-toggle" title="Открыть меню разделов">☰ Разделы</button>
|
||||||
@@ -330,27 +331,63 @@
|
|||||||
<!-- Authentication & Profile Modal -->
|
<!-- Authentication & Profile Modal -->
|
||||||
<div id="auth-modal" class="overlay hidden">
|
<div id="auth-modal" class="overlay hidden">
|
||||||
<div class="banner-card auth-card">
|
<div class="banner-card auth-card">
|
||||||
<h1 style="color: #ffeb3b; font-size: 26px;">ПРОФИЛЬ ИГРОКА</h1>
|
<!-- 1. Logged-in User Profile Dashboard View -->
|
||||||
<div id="auth-current-status" style="font-size: 14px; margin-bottom: 10px; color: #a5d6a7;">
|
<div id="auth-logged-in-view" class="auth-view hidden">
|
||||||
Текущий режим: <strong>Гость</strong>
|
<h1 style="color: #81c784; font-size: 24px; margin-bottom: 6px;">👤 ЛИЧНЫЙ ПРОФИЛЬ</h1>
|
||||||
|
<div class="profile-header-card">
|
||||||
|
<div class="profile-avatar-icon">🌻</div>
|
||||||
|
<div class="profile-info-text">
|
||||||
|
<div id="profile-user-title" class="profile-username">kopri</div>
|
||||||
|
<div class="profile-status-badge">🟢 Облачные сохранения активны</div>
|
||||||
</div>
|
</div>
|
||||||
<p id="auth-modal-desc" style="font-size: 13px; margin-bottom: 14px; color: #d7ccc8;">
|
</div>
|
||||||
Войдите под логином и паролем для сохранения монет и прогресса, либо играйте как Гость (без сохранения).
|
|
||||||
|
<!-- Profile Game Stats -->
|
||||||
|
<div class="profile-stats-grid">
|
||||||
|
<div class="profile-stat-box">
|
||||||
|
<span class="stat-label">💰 Монеты</span>
|
||||||
|
<span id="profile-stat-coins" class="stat-value">5,000</span>
|
||||||
|
</div>
|
||||||
|
<div class="profile-stat-box">
|
||||||
|
<span class="stat-label">🗺️ Кампания</span>
|
||||||
|
<span id="profile-stat-level" class="stat-value">Уровень 15</span>
|
||||||
|
</div>
|
||||||
|
<div class="profile-stat-box">
|
||||||
|
<span class="stat-label">🏆 Рекорд очков</span>
|
||||||
|
<span id="profile-stat-score" class="stat-value">12,500</span>
|
||||||
|
</div>
|
||||||
|
<div class="profile-stat-box">
|
||||||
|
<span class="stat-label">🧪 Материалы</span>
|
||||||
|
<span id="profile-stat-materials" class="stat-value">265 шт.</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display: flex; gap: 10px; justify-content: center; margin-top: 14px;">
|
||||||
|
<button type="button" id="btn-auth-logout" class="btn btn-danger">🚪 Выйти из аккаунта</button>
|
||||||
|
<button type="button" id="btn-auth-close-profile" class="btn btn-secondary">Закрыть</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 2. Guest Login / Registration View -->
|
||||||
|
<div id="auth-guest-view" class="auth-view">
|
||||||
|
<h1 style="color: #ffeb3b; font-size: 24px; margin-bottom: 4px;">ВХОД В ПРОФИЛЬ</h1>
|
||||||
|
<p style="font-size: 13px; margin-bottom: 12px; color: #d7ccc8;">
|
||||||
|
Войдите или зарегистрируйтесь, чтобы сохранять монеты, крафт и уровень кампании на сервере.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<form id="auth-form" onsubmit="return false;" style="display: flex; flex-direction: column; gap: 10px; margin-bottom: 14px;">
|
<form id="auth-form" onsubmit="return false;" style="display: flex; flex-direction: column; gap: 8px; margin-bottom: 12px;">
|
||||||
<input type="text" id="auth-username" class="auth-input" placeholder="Имя пользователя (логин)" maxlength="20" autocomplete="username">
|
<input type="text" id="auth-username" class="auth-input" placeholder="Имя пользователя (логин)" maxlength="20" autocomplete="username">
|
||||||
<input type="password" id="auth-password" class="auth-input" placeholder="Пароль" maxlength="30" autocomplete="current-password">
|
<input type="password" id="auth-password" class="auth-input" placeholder="Пароль" maxlength="30" autocomplete="current-password">
|
||||||
<div id="auth-error" class="auth-error hidden"></div>
|
<div id="auth-error" class="auth-error hidden"></div>
|
||||||
|
|
||||||
<div style="display: flex; gap: 10px; justify-content: center; flex-wrap: wrap; margin-top: 6px;">
|
<div style="display: flex; gap: 8px; justify-content: center; flex-wrap: wrap; margin-top: 6px;">
|
||||||
<button type="button" id="btn-auth-submit" class="btn btn-auth-submit">Войти / Регистрация</button>
|
<button type="button" id="btn-auth-submit" class="btn btn-auth-submit">Войти / Регистрация</button>
|
||||||
<button type="button" id="btn-auth-guest" class="btn btn-secondary">Играть как Гость</button>
|
<button type="button" id="btn-auth-guest" class="btn btn-secondary">Играть как Гость</button>
|
||||||
<button type="button" id="btn-auth-logout" class="btn btn-danger hidden">🚪 Выйти из профиля</button>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<button id="btn-auth-close" class="btn btn-secondary" style="font-size: 14px; padding: 6px 22px;">Закрыть</button>
|
<button id="btn-auth-close" class="btn btn-secondary" style="font-size: 13px; padding: 6px 20px;">Закрыть</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
+100
-16
@@ -63,6 +63,7 @@ window.GameEngine = class GameEngine {
|
|||||||
this.isGameOver = false;
|
this.isGameOver = false;
|
||||||
this.isVictory = false;
|
this.isVictory = false;
|
||||||
this.gameSpeed = 1.0;
|
this.gameSpeed = 1.0;
|
||||||
|
this.isPaused = false;
|
||||||
this.lastTime = 0;
|
this.lastTime = 0;
|
||||||
this.hoverCell = null;
|
this.hoverCell = null;
|
||||||
this.loopStarted = false;
|
this.loopStarted = false;
|
||||||
@@ -187,35 +188,62 @@ window.GameEngine = class GameEngine {
|
|||||||
if (window.PvZAudio) window.PvZAudio.playCoin();
|
if (window.PvZAudio) window.PvZAudio.playCoin();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
togglePause() {
|
||||||
|
if (!this.isRunning) return;
|
||||||
|
this.isPaused = !this.isPaused;
|
||||||
|
const btnPause = document.getElementById('btn-pause');
|
||||||
|
if (btnPause) {
|
||||||
|
btnPause.textContent = this.isPaused ? '▶️ Играть' : '⏸️ Пауза';
|
||||||
|
if (this.isPaused) btnPause.classList.add('active');
|
||||||
|
else btnPause.classList.remove('active');
|
||||||
|
}
|
||||||
|
this.triggerHaptic(15);
|
||||||
|
if (window.PvZAudio) window.PvZAudio.playButtonClick();
|
||||||
|
}
|
||||||
|
|
||||||
updateProfileUI() {
|
updateProfileUI() {
|
||||||
const badgeName = document.getElementById('badge-user-name');
|
const badgeName = document.getElementById('badge-user-name');
|
||||||
const btnAuth = document.getElementById('btn-auth-open');
|
const btnAuth = document.getElementById('btn-auth-open');
|
||||||
const btnLogoutNav = document.getElementById('btn-auth-logout-nav');
|
const btnLogoutNav = document.getElementById('btn-auth-logout-nav');
|
||||||
const authStatus = document.getElementById('auth-current-status');
|
const loggedInView = document.getElementById('auth-logged-in-view');
|
||||||
const btnLogoutModal = document.getElementById('btn-auth-logout');
|
const guestView = document.getElementById('auth-guest-view');
|
||||||
const btnAuthGuest = document.getElementById('btn-auth-guest');
|
const userTitle = document.getElementById('profile-user-title');
|
||||||
|
const statCoins = document.getElementById('profile-stat-coins');
|
||||||
|
const statLevel = document.getElementById('profile-stat-level');
|
||||||
|
const statScore = document.getElementById('profile-stat-score');
|
||||||
|
const statMaterials = document.getElementById('profile-stat-materials');
|
||||||
|
|
||||||
if (window.PvZAuth) {
|
if (window.PvZAuth) {
|
||||||
const user = window.PvZAuth.getCurrentUser();
|
const user = window.PvZAuth.getCurrentUser();
|
||||||
const isGuest = window.PvZAuth.isGuest();
|
const isGuest = window.PvZAuth.isGuest();
|
||||||
|
const uData = window.PvZAuth.getUserData();
|
||||||
|
|
||||||
if (badgeName) badgeName.textContent = isGuest ? '👤 Гость' : `👤 ${user}`;
|
if (badgeName) badgeName.textContent = isGuest ? '👤 Гость' : `👤 ${user}`;
|
||||||
if (btnAuth) btnAuth.textContent = isGuest ? 'Войти' : 'Сменить';
|
if (btnAuth) btnAuth.textContent = isGuest ? 'Войти' : 'Профиль';
|
||||||
|
|
||||||
if (btnLogoutNav) {
|
if (btnLogoutNav) {
|
||||||
if (isGuest) btnLogoutNav.classList.add('hidden');
|
if (isGuest) btnLogoutNav.classList.add('hidden');
|
||||||
else btnLogoutNav.classList.remove('hidden');
|
else btnLogoutNav.classList.remove('hidden');
|
||||||
}
|
}
|
||||||
if (authStatus) {
|
|
||||||
authStatus.innerHTML = isGuest
|
if (loggedInView && guestView) {
|
||||||
? 'Текущий режим: <strong>Гость</strong>'
|
if (isGuest) {
|
||||||
: `Вы вошли как: <strong>${user}</strong>`;
|
loggedInView.classList.add('hidden');
|
||||||
|
guestView.classList.remove('hidden');
|
||||||
|
} else {
|
||||||
|
loggedInView.classList.remove('hidden');
|
||||||
|
guestView.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 (statMaterials) {
|
||||||
|
const mats = uData.materials || (this.workshop ? this.workshop.materials : {});
|
||||||
|
const totalMats = (mats.bioHumus || 0) + (mats.scrapMetal || 0) + (mats.sunEssence || 0) + (mats.crystals || 0);
|
||||||
|
statMaterials.textContent = `${totalMats} шт.`;
|
||||||
}
|
}
|
||||||
if (btnLogoutModal) {
|
|
||||||
if (isGuest) btnLogoutModal.classList.add('hidden');
|
|
||||||
else btnLogoutModal.classList.remove('hidden');
|
|
||||||
}
|
}
|
||||||
if (btnAuthGuest) {
|
|
||||||
if (isGuest) btnAuthGuest.classList.remove('hidden');
|
|
||||||
else btnAuthGuest.classList.add('hidden');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -775,6 +803,26 @@ window.GameEngine = class GameEngine {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Pause Button
|
||||||
|
const btnPause = document.getElementById('btn-pause');
|
||||||
|
if (btnPause) {
|
||||||
|
btnPause.addEventListener('click', () => {
|
||||||
|
this.togglePause();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Global Pause Hotkeys (Space, KeyP)
|
||||||
|
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;
|
||||||
|
if (e.code === 'Space' || e.code === 'KeyP') {
|
||||||
|
e.preventDefault();
|
||||||
|
if (this.isRunning) {
|
||||||
|
this.togglePause();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Music
|
// Music
|
||||||
const btnMusic = document.getElementById('btn-music');
|
const btnMusic = document.getElementById('btn-music');
|
||||||
if (btnMusic) {
|
if (btnMusic) {
|
||||||
@@ -813,6 +861,7 @@ window.GameEngine = class GameEngine {
|
|||||||
const authModal = document.getElementById('auth-modal');
|
const authModal = document.getElementById('auth-modal');
|
||||||
const btnAuthOpen = document.getElementById('btn-auth-open');
|
const btnAuthOpen = document.getElementById('btn-auth-open');
|
||||||
const btnAuthClose = document.getElementById('btn-auth-close');
|
const btnAuthClose = document.getElementById('btn-auth-close');
|
||||||
|
const btnAuthCloseProfile = document.getElementById('btn-auth-close-profile');
|
||||||
const btnAuthSubmit = document.getElementById('btn-auth-submit');
|
const btnAuthSubmit = document.getElementById('btn-auth-submit');
|
||||||
const btnAuthGuest = document.getElementById('btn-auth-guest');
|
const btnAuthGuest = document.getElementById('btn-auth-guest');
|
||||||
const btnAuthLogout = document.getElementById('btn-auth-logout');
|
const btnAuthLogout = document.getElementById('btn-auth-logout');
|
||||||
@@ -848,7 +897,7 @@ window.GameEngine = class GameEngine {
|
|||||||
if (authError) authError.classList.add('hidden');
|
if (authError) authError.classList.add('hidden');
|
||||||
this.updateProfileUI();
|
this.updateProfileUI();
|
||||||
authModal.classList.remove('hidden');
|
authModal.classList.remove('hidden');
|
||||||
if (authUser) authUser.focus();
|
if (authUser && window.PvZAuth && window.PvZAuth.isGuest()) authUser.focus();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (btnAuthClose && authModal) {
|
if (btnAuthClose && authModal) {
|
||||||
@@ -856,6 +905,11 @@ window.GameEngine = class GameEngine {
|
|||||||
authModal.classList.add('hidden');
|
authModal.classList.add('hidden');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (btnAuthCloseProfile && authModal) {
|
||||||
|
btnAuthCloseProfile.addEventListener('click', () => {
|
||||||
|
authModal.classList.add('hidden');
|
||||||
|
});
|
||||||
|
}
|
||||||
if (btnAuthGuest && authModal) {
|
if (btnAuthGuest && authModal) {
|
||||||
btnAuthGuest.addEventListener('click', () => {
|
btnAuthGuest.addEventListener('click', () => {
|
||||||
handleLogout();
|
handleLogout();
|
||||||
@@ -2300,7 +2354,7 @@ window.GameEngine = class GameEngine {
|
|||||||
this.lastTime = timestamp;
|
this.lastTime = timestamp;
|
||||||
const dt = rawDt * this.gameSpeed;
|
const dt = rawDt * this.gameSpeed;
|
||||||
|
|
||||||
if (this.isRunning) {
|
if (this.isRunning && !this.isPaused) {
|
||||||
this.update(dt);
|
this.update(dt);
|
||||||
}
|
}
|
||||||
this.render();
|
this.render();
|
||||||
@@ -2466,6 +2520,36 @@ window.GameEngine = class GameEngine {
|
|||||||
: (this.currentMode === 'roguelike' ? `Рогалик: Этаж ${this.roguelike ? this.roguelike.currentFloor : 1}` : `Уровень ${this.currentLevel}`)));
|
: (this.currentMode === 'roguelike' ? `Рогалик: Этаж ${this.roguelike ? this.roguelike.currentFloor : 1}` : `Уровень ${this.currentLevel}`)));
|
||||||
this.ctx.fillText(`${modeLabel} | Счёт: ${this.score}`, 880, 78);
|
this.ctx.fillText(`${modeLabel} | Счёт: ${this.score}`, 880, 78);
|
||||||
this.ctx.restore();
|
this.ctx.restore();
|
||||||
|
|
||||||
|
// 11. Pause Banner Overlay
|
||||||
|
if (this.isPaused) {
|
||||||
|
this.ctx.save();
|
||||||
|
this.ctx.fillStyle = 'rgba(0, 0, 0, 0.65)';
|
||||||
|
this.ctx.fillRect(0, 0, this.logicalWidth, this.logicalHeight);
|
||||||
|
|
||||||
|
// Center Modal Card
|
||||||
|
this.ctx.fillStyle = '#2a160c';
|
||||||
|
this.ctx.strokeStyle = '#e09f3e';
|
||||||
|
this.ctx.lineWidth = 4;
|
||||||
|
this.ctx.beginPath();
|
||||||
|
this.ctx.roundRect(260, 210, 380, 180, 16);
|
||||||
|
this.ctx.fill();
|
||||||
|
this.ctx.stroke();
|
||||||
|
|
||||||
|
this.ctx.fillStyle = '#ffeb3b';
|
||||||
|
this.ctx.font = 'bold 28px sans-serif';
|
||||||
|
this.ctx.textAlign = 'center';
|
||||||
|
this.ctx.fillText('⏸️ ИГРА НА ПАУЗЕ', this.logicalWidth / 2, 270);
|
||||||
|
|
||||||
|
this.ctx.fillStyle = '#e0e0e0';
|
||||||
|
this.ctx.font = '14px sans-serif';
|
||||||
|
this.ctx.fillText('Все растения и зомби остановлены.', this.logicalWidth / 2, 308);
|
||||||
|
|
||||||
|
this.ctx.fillStyle = '#81c784';
|
||||||
|
this.ctx.font = 'bold 15px sans-serif';
|
||||||
|
this.ctx.fillText('Нажмите Пробел или кнопку «▶️ Играть»', this.logicalWidth / 2, 345);
|
||||||
|
this.ctx.restore();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
drawRake(ctx, x, y) {
|
drawRake(ctx, x, y) {
|
||||||
|
|||||||
+79
-2
@@ -4,7 +4,7 @@ window.GridSystem = class GridSystem {
|
|||||||
this.biome = 'day'; // 'day', 'night', 'pool'
|
this.biome = 'day'; // 'day', 'night', 'pool'
|
||||||
this.cols = 9;
|
this.cols = 9;
|
||||||
this.startX = 120;
|
this.startX = 120;
|
||||||
this.startY = 90;
|
this.startY = 108; // Shifted down so zombie HP bars in top row are fully visible under HUD
|
||||||
this.cellW = 80;
|
this.cellW = 80;
|
||||||
|
|
||||||
this.graves = [];
|
this.graves = [];
|
||||||
@@ -15,7 +15,7 @@ window.GridSystem = class GridSystem {
|
|||||||
initBiome(biome = 'day') {
|
initBiome(biome = 'day') {
|
||||||
this.biome = biome;
|
this.biome = biome;
|
||||||
this.rows = (biome === 'pool' || biome === 'fog') ? 6 : 5;
|
this.rows = (biome === 'pool' || biome === 'fog') ? 6 : 5;
|
||||||
this.cellH = (biome === 'pool' || biome === 'fog') ? 83 : 100;
|
this.cellH = (biome === 'pool' || biome === 'fog') ? 80 : 96;
|
||||||
|
|
||||||
this.grid = [];
|
this.grid = [];
|
||||||
for (let r = 0; r < this.rows; r++) {
|
for (let r = 0; r < this.rows; r++) {
|
||||||
@@ -246,6 +246,9 @@ window.GridSystem = class GridSystem {
|
|||||||
ctx.fillRect(rightEdge, this.startY, 900 - rightEdge, this.rows * this.cellH);
|
ctx.fillRect(rightEdge, this.startY, 900 - rightEdge, this.rows * this.cellH);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 2b. Decorative Neighbour Fence along top edge (y: 86..108)
|
||||||
|
this.drawNeighbourFence(ctx);
|
||||||
|
|
||||||
// 4. Draw Gravestones in Night Mode
|
// 4. Draw Gravestones in Night Mode
|
||||||
for (const grave of this.graves) {
|
for (const grave of this.graves) {
|
||||||
const bounds = this.getCellBounds(grave.col, grave.row);
|
const bounds = this.getCellBounds(grave.col, grave.row);
|
||||||
@@ -425,4 +428,78 @@ window.GridSystem = class GridSystem {
|
|||||||
|
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
drawNeighbourFence(ctx) {
|
||||||
|
if (this.biome === 'roof') return; // Roof has shingles/gutters instead
|
||||||
|
|
||||||
|
const fenceY = 88;
|
||||||
|
const fenceH = 20;
|
||||||
|
|
||||||
|
ctx.save();
|
||||||
|
|
||||||
|
// 1. Shadow under fence on the lawn
|
||||||
|
ctx.fillStyle = 'rgba(0, 0, 0, 0.28)';
|
||||||
|
ctx.fillRect(0, fenceY + fenceH - 2, 900, 4);
|
||||||
|
|
||||||
|
// 2. Dirt/Base Strip
|
||||||
|
ctx.fillStyle = (this.biome === 'night' || this.biome === 'fog') ? '#1a0f07' : '#3e2723';
|
||||||
|
ctx.fillRect(0, fenceY + fenceH - 4, 900, 4);
|
||||||
|
|
||||||
|
// 3. Horizontal Support Rails
|
||||||
|
const isNight = this.biome === 'night' || this.biome === 'fog';
|
||||||
|
const isWinter = this.theme === 'winter';
|
||||||
|
const isCyber = this.theme === 'cyberpunk';
|
||||||
|
|
||||||
|
let railColor = isNight ? '#4e342e' : '#8d6e63';
|
||||||
|
let picketColor = isNight ? '#5d4037' : '#d7ccc8';
|
||||||
|
let picketTopColor = isNight ? '#795548' : '#efebe9';
|
||||||
|
|
||||||
|
if (isWinter) {
|
||||||
|
picketColor = '#eceff1';
|
||||||
|
picketTopColor = '#ffffff';
|
||||||
|
railColor = '#b0bec5';
|
||||||
|
} else if (isCyber) {
|
||||||
|
picketColor = '#21083a';
|
||||||
|
picketTopColor = '#00e5ff';
|
||||||
|
railColor = '#e91e63';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Top & Bottom Horizontal Rails
|
||||||
|
ctx.fillStyle = railColor;
|
||||||
|
ctx.fillRect(0, fenceY + 4, 900, 3);
|
||||||
|
ctx.fillRect(0, fenceY + 12, 900, 3);
|
||||||
|
|
||||||
|
// 4. Vertical Pickets (Wooden Fence Slats)
|
||||||
|
const picketWidth = 8;
|
||||||
|
const picketGap = 16;
|
||||||
|
for (let x = 4; x < 900; x += picketGap) {
|
||||||
|
// Picket Body
|
||||||
|
ctx.fillStyle = picketColor;
|
||||||
|
ctx.fillRect(x, fenceY + 2, picketWidth, fenceH - 4);
|
||||||
|
|
||||||
|
// Pointy Picket Cap
|
||||||
|
ctx.fillStyle = picketTopColor;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(x, fenceY + 2);
|
||||||
|
ctx.lineTo(x + picketWidth / 2, fenceY - 2);
|
||||||
|
ctx.lineTo(x + picketWidth, fenceY + 2);
|
||||||
|
ctx.closePath();
|
||||||
|
ctx.fill();
|
||||||
|
|
||||||
|
// Nail / Wood detail
|
||||||
|
ctx.fillStyle = 'rgba(0, 0, 0, 0.4)';
|
||||||
|
ctx.fillRect(x + 3, fenceY + 5, 2, 2);
|
||||||
|
ctx.fillRect(x + 3, fenceY + 13, 2, 2);
|
||||||
|
|
||||||
|
// Winter snow cap
|
||||||
|
if (isWinter) {
|
||||||
|
ctx.fillStyle = '#ffffff';
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(x + picketWidth / 2, fenceY - 2, 3, 0, Math.PI * 2);
|
||||||
|
ctx.fill();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
Reference in new issue
Block a user