feat: Initial commit Plants vs Zombies Web Edition v2.4
- 5 Biomes: Day, Night, Pool, Fog, Roof (25 Campaign levels) - Minigames: Wall-nut Bowling, I Zombie, Roguelike, Boss Rush & Dr. Zomboss - Systems: Zen Garden, Workshop Crafting, Almanac, Tree of Wisdom, Achievements - Backend: REST API + SQLite Accounts & Save Synchronization - Infrastructure: Nginx HTTPS & Docker deployment
This commit is contained in:
commit
6457a8904e
50 files changed
+14486
No files matched your search
@@ -0,0 +1,208 @@
|
||||
// Bug Reporting & Community Feedback Chat System
|
||||
window.PvZFeedback = (function() {
|
||||
const STORAGE_KEY = 'pvz_cached_bugs';
|
||||
let cachedReports = [];
|
||||
let currentFilter = 'all';
|
||||
|
||||
function getLocalFallback() {
|
||||
try {
|
||||
const saved = localStorage.getItem(STORAGE_KEY);
|
||||
if (saved) return JSON.parse(saved);
|
||||
} catch (e) {}
|
||||
return [
|
||||
{
|
||||
id: 1,
|
||||
author: "Player_Kopri",
|
||||
category: "bug",
|
||||
text: "Зомби после поедания растения не шел дальше, а оставался стоять на месте.",
|
||||
status: "исправлено",
|
||||
timestamp: "2026-09-16 09:30",
|
||||
adminReply: "Исправлено в v1.6: сброшен флаг isEating и таймер поедания."
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
author: "DaveFan",
|
||||
category: "bug",
|
||||
text: "Режим 'Я, Зомби' не работал: сразу после старта выводилось сообщение 'победа'.",
|
||||
status: "исправлено",
|
||||
timestamp: "2026-09-16 09:45",
|
||||
adminReply: "Исправлено в v1.6: устранено состояние гонки при инициализации газона."
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
author: "ZombossHater",
|
||||
category: "visual",
|
||||
text: "Элементы главного меню и кнопки вываливаются за границы экрана на небольших дисплеях.",
|
||||
status: "исправлено",
|
||||
timestamp: "2026-09-16 10:50",
|
||||
adminReply: "Исправлено в v2.2: адаптивная панель навигации, компактные карточки и вертикальный скролл."
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
return {
|
||||
init: function() {
|
||||
cachedReports = getLocalFallback();
|
||||
this.fetchReports();
|
||||
},
|
||||
|
||||
getReports: function() {
|
||||
return cachedReports;
|
||||
},
|
||||
|
||||
fetchReports: async function(onDone) {
|
||||
try {
|
||||
const resp = await fetch('/api/bugs', { cache: 'no-cache' });
|
||||
if (resp.ok) {
|
||||
const data = await resp.json();
|
||||
if (Array.isArray(data)) {
|
||||
cachedReports = data;
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
|
||||
} catch (e) {}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('Could not fetch online bugs, using local cache:', err);
|
||||
if (cachedReports.length === 0) {
|
||||
cachedReports = getLocalFallback();
|
||||
}
|
||||
}
|
||||
if (typeof onDone === 'function') onDone(cachedReports);
|
||||
this.renderFeed(currentFilter);
|
||||
},
|
||||
|
||||
sendReport: async function(author, category, text, onResult) {
|
||||
const payload = {
|
||||
author: (author || 'Гость').trim(),
|
||||
category: category || 'bug',
|
||||
text: (text || '').trim(),
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
|
||||
if (!payload.text) {
|
||||
if (onResult) onResult({ success: false, message: 'Введите описание проблемы' });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const resp = await fetch('/api/bugs', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
|
||||
if (resp.ok) {
|
||||
const res = await resp.json();
|
||||
await this.fetchReports();
|
||||
if (onResult) onResult({ success: true, report: res.report });
|
||||
return;
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('Failed to POST to server, storing locally:', err);
|
||||
}
|
||||
|
||||
// Offline fallback: save into local list
|
||||
const newId = (cachedReports.reduce((m, r) => Math.max(m, r.id || 0), 0) || 0) + 1;
|
||||
const localReport = {
|
||||
id: newId,
|
||||
author: payload.author,
|
||||
category: payload.category,
|
||||
text: payload.text,
|
||||
status: 'новый',
|
||||
timestamp: new Date().toLocaleString('ru-RU', { hour: '2-digit', minute: '2-digit', day: '2-digit', month: '2-digit' }),
|
||||
adminReply: null
|
||||
};
|
||||
cachedReports.push(localReport);
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(cachedReports));
|
||||
} catch (e) {}
|
||||
this.renderFeed(currentFilter);
|
||||
if (onResult) onResult({ success: true, report: localReport, offline: true });
|
||||
},
|
||||
|
||||
setFilter: function(filter) {
|
||||
currentFilter = filter;
|
||||
this.renderFeed(filter);
|
||||
},
|
||||
|
||||
renderFeed: function(filter = currentFilter) {
|
||||
currentFilter = filter;
|
||||
const feedEl = document.getElementById('bugreport-feed');
|
||||
const countBadge = document.getElementById('bug-count-badge');
|
||||
if (countBadge) {
|
||||
const total = cachedReports.length;
|
||||
const word = total % 10 === 1 && total % 100 !== 11 ? 'репорт' : (total % 10 >= 2 && total % 10 <= 4 && (total % 100 < 10 || total % 100 >= 20) ? 'репорта' : 'репортов');
|
||||
countBadge.textContent = `${total} ${word}`;
|
||||
}
|
||||
if (!feedEl) return;
|
||||
|
||||
let list = [...cachedReports];
|
||||
if (filter === 'bug') list = list.filter(r => r.category === 'bug');
|
||||
else if (filter === 'feature') list = list.filter(r => r.category === 'feature');
|
||||
else if (filter === 'visual') list = list.filter(r => r.category === 'visual');
|
||||
else if (filter === 'balance') list = list.filter(r => r.category === 'balance');
|
||||
else if (filter === 'resolved') list = list.filter(r => r.status === 'исправлено');
|
||||
|
||||
if (list.length === 0) {
|
||||
feedEl.innerHTML = '<div class="bug-empty">Сообщений в этой категории пока нет. Напишите первое! 💬</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Show newest first
|
||||
list.sort((a, b) => (b.id || 0) - (a.id || 0));
|
||||
|
||||
const categoryLabels = {
|
||||
bug: { text: '🔴 Баг', cls: 'cat-bug' },
|
||||
feature: { text: '💡 Идея', cls: 'cat-feature' },
|
||||
visual: { text: '🎨 Графика/UI', cls: 'cat-visual' },
|
||||
balance: { text: '⚖️ Баланс', cls: 'cat-balance' }
|
||||
};
|
||||
|
||||
const statusLabels = {
|
||||
'новый': { text: '🔴 Новый', cls: 'status-new' },
|
||||
'в работе': { text: '🟡 В работе', cls: 'status-progress' },
|
||||
'исправлено': { text: '✅ Исправлено', cls: 'status-done' }
|
||||
};
|
||||
|
||||
let html = '';
|
||||
list.forEach(r => {
|
||||
const cat = categoryLabels[r.category] || categoryLabels.bug;
|
||||
const st = statusLabels[r.status] || statusLabels['новый'];
|
||||
const replyHtml = r.adminReply
|
||||
? `<div class="bug-reply">
|
||||
<span class="reply-tag">🛠️ Ответ разработчика:</span>
|
||||
<div class="reply-text">${this.escapeHtml(r.adminReply)}</div>
|
||||
</div>`
|
||||
: '';
|
||||
|
||||
html += `
|
||||
<div class="bug-card ${r.status === 'исправлено' ? 'is-fixed' : ''}">
|
||||
<div class="bug-card-top">
|
||||
<div class="bug-author">👤 <strong>${this.escapeHtml(r.author || 'Гость')}</strong></div>
|
||||
<div class="bug-meta">
|
||||
<span class="bug-category ${cat.cls}">${cat.text}</span>
|
||||
<span class="bug-status ${st.cls}">${st.text}</span>
|
||||
<span class="bug-time">🕒 ${this.escapeHtml(r.timestamp || '')}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bug-text">${this.escapeHtml(r.text)}</div>
|
||||
${replyHtml}
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
feedEl.innerHTML = html;
|
||||
},
|
||||
|
||||
escapeHtml: function(str) {
|
||||
if (!str) return '';
|
||||
return String(str)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
};
|
||||
})();
|
||||
Reference in new issue
Block a user