diff --git a/game/css/style.css b/game/css/style.css index 46c61f9..c3da073 100644 --- a/game/css/style.css +++ b/game/css/style.css @@ -746,6 +746,16 @@ canvas#gameCanvas { width: 95%; max-height: 90vh; overflow-y: auto; + position: relative; +} + +.seed-select-card .seed-select-section-title:first-of-type { + position: sticky; + top: 0; + background: #2a160c; + z-index: 12; + padding-top: 6px; + padding-bottom: 4px; } .seed-select-section-title { @@ -761,16 +771,21 @@ canvas#gameCanvas { } .seed-deck-bar { + position: sticky; + top: 28px; + z-index: 11; display: flex; gap: 8px; padding: 8px 10px; - background: rgba(0, 0, 0, 0.4); + background: rgba(35, 18, 11, 0.96); + backdrop-filter: blur(6px); border: 1.5px solid #81c784; border-radius: 8px; min-height: 76px; align-items: center; flex-wrap: wrap; margin-bottom: 12px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5); } .seed-slot-card { diff --git a/game/js/game.js b/game/js/game.js index 7054386..d49cdeb 100644 --- a/game/js/game.js +++ b/game/js/game.js @@ -448,7 +448,8 @@ window.GameEngine = class GameEngine { // Close any game-phase modals that might have been left open ['game-over-modal', 'victory-modal', 'draft-modal', 'zengarden-modal', 'workshop-modal', 'leaderboard-modal', 'achievement-modal', 'almanac-modal', - 'bugreport-modal', 'campaign-modal', 'tree-modal', 'casino-modal'].forEach(id => { + 'bugreport-modal', 'campaign-modal', 'tree-modal', 'casino-modal', + 'seed-select-modal', 'dave-modal', 'shop-modal'].forEach(id => { const el = document.getElementById(id); if (el) el.classList.add('hidden'); }); @@ -492,6 +493,12 @@ window.GameEngine = class GameEngine { this.currentLevel = level; this.currentMode = mode; + // Ensure all pre-battle and menu overlays are hidden + ['seed-select-modal', 'dave-modal', 'start-modal', 'campaign-modal', 'victory-modal', 'game-over-modal', 'shop-modal'].forEach(id => { + const el = document.getElementById(id); + if (el) el.classList.add('hidden'); + }); + // Pre-set economy mode flags BEFORE reset() so economy.reset() computes // the correct baseSun (avoids stale isConveyor/isIZombie from previous game) this.economy.isConveyor = (mode === 'bowling' || mode === 'boss' || mode === 'bossrush'); @@ -1876,6 +1883,7 @@ window.GameEngine = class GameEngine { } // Animate spin + this.casino.isSpinning = true; [reel1, reel2, reel3].forEach(r => { if (r) r.classList.add('spinning'); }); if (btnSpin) btnSpin.disabled = true; if (resultBanner) { @@ -1884,27 +1892,34 @@ window.GameEngine = class GameEngine { } setTimeout(() => { - if (this.casino) { - this.casino.isSpinning = false; - const res = this.casino.spin(bet); + try { + if (this.casino) { + this.casino.isSpinning = false; + const res = this.casino.spin(bet); + this.casino.isSpinning = false; + + if (res && res.icons) { + if (reel1) reel1.innerHTML = `${res.icons[0]}`; + if (reel2) reel2.innerHTML = `${res.icons[1]}`; + if (reel3) reel3.innerHTML = `${res.icons[2]}`; + } + + if (resultBanner && res) { + resultBanner.textContent = res.message || ''; + if (res.isJackpot) resultBanner.style.color = '#ffd700'; + else if (res.payout > 0) resultBanner.style.color = '#69f0ae'; + else resultBanner.style.color = '#b0bec5'; + } + + this.updateCasinoUI(); + this.updateRecordDisplay(); + } + } catch (e) { + console.error('Casino spin error:', e); + } finally { + if (this.casino) this.casino.isSpinning = false; [reel1, reel2, reel3].forEach(r => { if (r) r.classList.remove('spinning'); }); if (btnSpin) btnSpin.disabled = false; - - if (res.icons) { - if (reel1) reel1.innerHTML = `${res.icons[0]}`; - if (reel2) reel2.innerHTML = `${res.icons[1]}`; - if (reel3) reel3.innerHTML = `${res.icons[2]}`; - } - - if (resultBanner) { - resultBanner.textContent = res.message || ''; - if (res.isJackpot) resultBanner.style.color = '#ffd700'; - else if (res.payout > 0) resultBanner.style.color = '#69f0ae'; - else resultBanner.style.color = '#b0bec5'; - } - - this.updateCasinoUI(); - this.updateRecordDisplay(); } }, 500); }); diff --git a/game/js/systems/auth.js b/game/js/systems/auth.js index c7e10d1..ca5f653 100644 --- a/game/js/systems/auth.js +++ b/game/js/systems/auth.js @@ -117,7 +117,20 @@ window.PvZAuth = (function () { lastVisit: Date.now() }, treeOfWisdom: { height: 0, cheats: {} }, - achievements: {} + achievements: {}, + casino: { + deposit: 0, + depositStartTime: 0, + loan: 0, + spinsTotal: 0, + totalWon: 0, + jackpotsWon: 0 + }, + zombieLab: { + unlockedStage: 1, + levels: { normal: 1, conehead: 1, buckethead: 1, football: 1, dancing: 1 }, + mutations: {} + } }; } @@ -301,7 +314,37 @@ window.PvZAuth = (function () { achievements: { ...(loc.achievements || {}), ...(base.achievements || {}) - } + }, + casino: (base.casino || loc.casino) + ? { + ...def.casino, + ...(loc.casino || {}), + ...(base.casino || {}), + deposit: (base.casino && base.casino.deposit !== undefined) ? Number(base.casino.deposit) : (Number(loc.casino?.deposit) || 0), + depositStartTime: (base.casino && base.casino.depositStartTime) ? Number(base.casino.depositStartTime) : (Number(loc.casino?.depositStartTime) || 0), + loan: (base.casino && base.casino.loan !== undefined) ? Number(base.casino.loan) : (Number(loc.casino?.loan) || 0), + spinsTotal: Math.max(Number(base.casino?.spinsTotal) || 0, Number(loc.casino?.spinsTotal) || 0), + totalWon: Math.max(Number(base.casino?.totalWon) || 0, Number(loc.casino?.totalWon) || 0), + jackpotsWon: Math.max(Number(base.casino?.jackpotsWon) || 0, Number(loc.casino?.jackpotsWon) || 0) + } + : def.casino, + zombieLab: (base.zombieLab || loc.zombieLab) + ? { + ...def.zombieLab, + ...(loc.zombieLab || {}), + ...(base.zombieLab || {}), + unlockedStage: Math.max(Number(base.zombieLab?.unlockedStage) || 1, Number(loc.zombieLab?.unlockedStage) || 1), + levels: { + ...def.zombieLab.levels, + ...(loc.zombieLab?.levels || {}), + ...(base.zombieLab?.levels || {}) + }, + mutations: { + ...(loc.zombieLab?.mutations || {}), + ...(base.zombieLab?.mutations || {}) + } + } + : def.zombieLab }; } diff --git a/game/js/systems/casino.js b/game/js/systems/casino.js index f09450a..61a97e9 100644 --- a/game/js/systems/casino.js +++ b/game/js/systems/casino.js @@ -138,6 +138,7 @@ window.CasinoSystem = class CasinoSystem { } } + this.isSpinning = false; this.saveToAuth(); return { diff --git a/game/js/systems/economy.js b/game/js/systems/economy.js index c43db29..1f865b7 100644 --- a/game/js/systems/economy.js +++ b/game/js/systems/economy.js @@ -130,11 +130,21 @@ window.EconomySystem = class EconomySystem { this.selectedDeck = deckIds.filter(id => this.isPlantUnlocked(id)).slice(0, this.getMaxSlots()); this.saveProfileData(); } - const chosen = this.selectedDeck.length > 0 ? this.selectedDeck : this.defaultUnlocked; + const chosen = (this.selectedDeck && this.selectedDeck.length > 0) ? this.selectedDeck : this.defaultUnlocked; this.cards = chosen.map(id => { const found = this.allCards.find(c => c.id === id); return found ? { ...found, timer: 0 } : null; }).filter(Boolean); + + // Guaranteed fallback: If this.cards is empty, restore default starter cards so top menu never disappears + if (!this.cards || this.cards.length === 0) { + this.selectedDeck = [...this.defaultUnlocked]; + this.cards = this.defaultUnlocked.map(id => { + const found = this.allCards.find(c => c.id === id); + return found ? { ...found, timer: 0 } : null; + }).filter(Boolean); + this.saveProfileData(); + } } reloadProfileData() { @@ -720,6 +730,9 @@ window.EconomySystem = class EconomySystem { } // 2. Draw Cards + if (!this.isConveyor && !this.isIZombie && (!this.cards || this.cards.length === 0)) { + this.setDeckFromSelection(); + } const cardStartX = 105; const cardY = 8; const cardW = 66; diff --git a/scripts/db.py b/scripts/db.py index ffacc70..c2a6f9c 100644 --- a/scripts/db.py +++ b/scripts/db.py @@ -498,6 +498,10 @@ def init_db(): zen_garden TEXT DEFAULT '{}', tree_of_wisdom TEXT DEFAULT '{}', achievements TEXT DEFAULT '{}', + stats TEXT DEFAULT '{}', + casino TEXT DEFAULT '{}', + zombie_lab TEXT DEFAULT '{}', + admin_updated_at TIMESTAMP DEFAULT NULL, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ) @@ -608,6 +612,10 @@ def init_db(): cursor.execute("ALTER TABLE profiles ADD COLUMN stats TEXT DEFAULT '{}'") if "admin_updated_at" not in prof_cols: cursor.execute("ALTER TABLE profiles ADD COLUMN admin_updated_at TIMESTAMP DEFAULT NULL") + if "casino" not in prof_cols: + cursor.execute("ALTER TABLE profiles ADD COLUMN casino TEXT DEFAULT '{}'") + if "zombie_lab" not in prof_cols: + cursor.execute("ALTER TABLE profiles ADD COLUMN zombie_lab TEXT DEFAULT '{}'") conn.commit() conn.close() @@ -1177,6 +1185,19 @@ def get_default_profile_dict(): "plantsPlanted": 0, "levelsCompleted": 0, "coinsEarned": 0 + }, + "casino": { + "deposit": 0, + "depositStartTime": 0, + "loan": 0, + "spinsTotal": 0, + "totalWon": 0, + "jackpotsWon": 0 + }, + "zombieLab": { + "unlockedStage": 1, + "mutations": {}, + "levels": {"normal": 1, "conehead": 1, "buckethead": 1, "football": 1, "dancing": 1} } } @@ -1212,8 +1233,8 @@ def register_user(username, password_hash, initial_data=None, ip_address="", use cursor.execute(""" INSERT INTO profiles ( user_id, coins, level, max_unlocked_level, high_score, theme, - upgrades, materials, plant_levels, perks, zen_garden, tree_of_wisdom, achievements, updated_at - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + upgrades, materials, plant_levels, perks, zen_garden, tree_of_wisdom, achievements, stats, casino, zombie_lab, updated_at + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """, ( user_id, profile_data.get("coins", 150), @@ -1228,6 +1249,9 @@ def register_user(username, password_hash, initial_data=None, ip_address="", use json.dumps(profile_data.get("zenGarden", {})), json.dumps(profile_data.get("treeOfWisdom", {})), json.dumps(profile_data.get("achievements", {})), + json.dumps(profile_data.get("stats", {})), + json.dumps(profile_data.get("casino", {})), + json.dumps(profile_data.get("zombieLab", {})), now_str )) conn.commit() @@ -1319,6 +1343,12 @@ def authenticate_user(username, password_hash, ip_address="", user_agent="", dev "achievements": json.loads(p["achievements"] or "{}"), "stats": json.loads(p["stats"] or "{}") if "stats" in p.keys() and p["stats"] else { "zombiesKilled": 0, "sunProduced": 0, "plantsPlanted": 0, "levelsCompleted": 0, "coinsEarned": 0 + }, + "casino": json.loads(p["casino"] or "{}") if "casino" in p.keys() and p["casino"] else { + "deposit": 0, "depositStartTime": 0, "loan": 0, "spinsTotal": 0, "totalWon": 0, "jackpotsWon": 0 + }, + "zombieLab": json.loads(p["zombie_lab"] or "{}") if "zombie_lab" in p.keys() and p["zombie_lab"] else { + "unlockedStage": 1, "mutations": {}, "levels": {"normal": 1, "conehead": 1, "buckethead": 1, "football": 1, "dancing": 1} } } @@ -1369,6 +1399,12 @@ def get_user_profile(username): "achievements": json.loads(p["achievements"] or "{}"), "stats": json.loads(p["stats"] or "{}") if "stats" in p.keys() and p["stats"] else { "zombiesKilled": 0, "sunProduced": 0, "plantsPlanted": 0, "levelsCompleted": 0, "coinsEarned": 0 + }, + "casino": json.loads(p["casino"] or "{}") if "casino" in p.keys() and p["casino"] else { + "deposit": 0, "depositStartTime": 0, "loan": 0, "spinsTotal": 0, "totalWon": 0, "jackpotsWon": 0 + }, + "zombieLab": json.loads(p["zombie_lab"] or "{}") if "zombie_lab" in p.keys() and p["zombie_lab"] else { + "unlockedStage": 1, "mutations": {}, "levels": {"normal": 1, "conehead": 1, "buckethead": 1, "football": 1, "dancing": 1} } } @@ -1509,29 +1545,56 @@ def save_user_profile(username, data, token=None, is_admin_override=False, ip_ad cur_stats[sk] = max(cur_stats.get(sk, 0), int(sv)) stats_json = json.dumps(cur_stats) + # Casino merge (preserve deposit, spins, won, loan) + cur_casino = json.loads(current["casino"] or "{}") if current and "casino" in current.keys() and current["casino"] else { + "deposit": 0, "depositStartTime": 0, "loan": 0, "spinsTotal": 0, "totalWon": 0, "jackpotsWon": 0 + } + if "casino" in data and isinstance(data["casino"], dict): + for ck, cv in data["casino"].items(): + if ck in ("deposit", "depositStartTime", "loan", "spinsTotal", "totalWon", "jackpotsWon"): + cur_casino[ck] = int(cv) if isinstance(cv, (int, float)) else cv + casino_json = json.dumps(cur_casino) + + # Zombie Lab merge + cur_zombie_lab = json.loads(current["zombie_lab"] or "{}") if current and "zombie_lab" in current.keys() and current["zombie_lab"] else { + "unlockedStage": 1, "mutations": {}, "levels": {"normal": 1, "conehead": 1, "buckethead": 1, "football": 1, "dancing": 1} + } + new_zombie_lab = data.get("zombieLab") or data.get("zombie_lab") + if new_zombie_lab and isinstance(new_zombie_lab, dict): + if "unlockedStage" in new_zombie_lab: + cur_zombie_lab["unlockedStage"] = max(cur_zombie_lab.get("unlockedStage", 1), int(new_zombie_lab["unlockedStage"])) + if "levels" in new_zombie_lab and isinstance(new_zombie_lab["levels"], dict): + cur_zombie_lab.setdefault("levels", {}).update(new_zombie_lab["levels"]) + if "mutations" in new_zombie_lab and isinstance(new_zombie_lab["mutations"], dict): + cur_zombie_lab.setdefault("mutations", {}).update(new_zombie_lab["mutations"]) + zombie_lab_json = json.dumps(cur_zombie_lab) + if current: cursor.execute(""" UPDATE profiles SET coins = ?, level = ?, max_unlocked_level = ?, high_score = ?, theme = ?, upgrades = ?, materials = ?, plant_levels = ?, perks = ?, - zen_garden = ?, tree_of_wisdom = ?, achievements = ?, stats = ?, admin_updated_at = ?, updated_at = ? + zen_garden = ?, tree_of_wisdom = ?, achievements = ?, stats = ?, + casino = ?, zombie_lab = ?, admin_updated_at = ?, updated_at = ? WHERE user_id = ? """, ( coins, level, max_unlocked, high_score, theme, upgrades_json, materials_json, plant_levels_json, perks_json, - zen_garden_json, tree_json, achievements_json, stats_json, admin_updated_val, now_str, + zen_garden_json, tree_json, achievements_json, stats_json, + casino_json, zombie_lab_json, admin_updated_val, now_str, user_id )) else: cursor.execute(""" INSERT INTO profiles ( user_id, coins, level, max_unlocked_level, high_score, theme, - upgrades, materials, plant_levels, perks, zen_garden, tree_of_wisdom, achievements, stats, admin_updated_at, updated_at - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + upgrades, materials, plant_levels, perks, zen_garden, tree_of_wisdom, achievements, stats, casino, zombie_lab, admin_updated_at, updated_at + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """, ( user_id, coins, level, max_unlocked, high_score, theme, upgrades_json, materials_json, plant_levels_json, perks_json, - zen_garden_json, tree_json, achievements_json, stats_json, admin_updated_val, now_str + zen_garden_json, tree_json, achievements_json, stats_json, + casino_json, zombie_lab_json, admin_updated_val, now_str )) conn.commit() @@ -1765,6 +1828,12 @@ def admin_get_user_detail(user_id): "stats": json.loads(p["stats"] or "{}") if "stats" in p.keys() and p["stats"] else { "zombiesKilled": 0, "sunProduced": 0, "plantsPlanted": 0, "levelsCompleted": 0, "coinsEarned": 0 }, + "casino": json.loads(p["casino"] or "{}") if "casino" in p.keys() and p["casino"] else { + "deposit": 0, "depositStartTime": 0, "loan": 0, "spinsTotal": 0, "totalWon": 0, "jackpotsWon": 0 + }, + "zombieLab": json.loads(p["zombie_lab"] or "{}") if "zombie_lab" in p.keys() and p["zombie_lab"] else { + "unlockedStage": 1, "mutations": {}, "levels": {"normal": 1, "conehead": 1, "buckethead": 1, "football": 1, "dancing": 1} + }, "updatedAt": p["updated_at"] } else: @@ -2087,8 +2156,8 @@ def admin_import_db(data): cursor.execute(""" INSERT INTO profiles ( user_id, coins, level, max_unlocked_level, high_score, theme, - upgrades, materials, plant_levels, perks, zen_garden, tree_of_wisdom, achievements, updated_at - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + upgrades, materials, plant_levels, perks, zen_garden, tree_of_wisdom, achievements, stats, casino, zombie_lab, admin_updated_at, updated_at + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """, ( p.get("user_id"), p.get("coins", 150), @@ -2103,6 +2172,10 @@ def admin_import_db(data): p.get("zen_garden", "{}"), p.get("tree_of_wisdom", "{}"), p.get("achievements", "{}"), + p.get("stats", "{}"), + p.get("casino", "{}"), + p.get("zombie_lab", "{}"), + p.get("admin_updated_at"), p.get("updated_at") )) diff --git a/scripts/test_game.py b/scripts/test_game.py index 83d102b..d7748ff 100644 --- a/scripts/test_game.py +++ b/scripts/test_game.py @@ -1320,6 +1320,59 @@ def test_v2_7_features(): record("POST /api/admin/system/cleanup authorized request", "FAIL", str(e)) +def test_gitea_critical_bugfixes(): + """Verify bugfixes for Gitea issues #8, #9, and #10.""" + print("\n--- 19. Gitea Critical Bugfixes (#8, #9, #10) ---") + + # Issue #8: Casino allows multiple spins without freezing + casino_path = os.path.join(GAME_DIR, "js/systems/casino.js") + with open(casino_path, "r", encoding="utf-8") as f: + casino_code = f.read() + game_path = os.path.join(GAME_DIR, "js/game.js") + with open(game_path, "r", encoding="utf-8") as f: + game_code = f.read() + + record("Issue #8: casino.js spin resets isSpinning to false", "PASS" if "this.isSpinning = false;" in casino_code else "FAIL") + record("Issue #8: game.js btnSpin handler releases isSpinning on completion", "PASS" if "this.casino.isSpinning = false" in game_code and "btnSpin.disabled = false" in game_code else "FAIL") + + # Issue #9: Bank deposit persistence in SQLite and Auth + auth_path = os.path.join(GAME_DIR, "js/systems/auth.js") + with open(auth_path, "r", encoding="utf-8") as f: + auth_code = f.read() + record("Issue #9: auth.js merges and stores casino deposit state", "PASS" if "casino:" in auth_code and "deposit:" in auth_code else "FAIL") + record("Issue #9: auth.js merges and stores zombieLab state", "PASS" if "zombieLab:" in auth_code and "unlockedStage" in auth_code else "FAIL") + + try: + conn = db.get_connection() + cursor = conn.cursor() + cursor.execute("PRAGMA table_info(profiles)") + cols = [r["name"] for r in cursor.fetchall()] + conn.close() + record("Issue #9: SQLite profiles table has casino and zombie_lab columns", "PASS" if "casino" in cols and "zombie_lab" in cols else "FAIL") + except Exception as e: + record("Issue #9: SQLite profiles table has casino and zombie_lab columns", "FAIL", str(e)) + + # Check kostyan restored deposit + try: + kostyan_prof = db.get_user_profile("kostyan") + has_deposit = kostyan_prof and isinstance(kostyan_prof.get("casino"), dict) and kostyan_prof["casino"].get("deposit", 0) > 0 + record("Issue #9: Player kostyan deposit restored and retrievable", "PASS" if has_deposit else "FAIL", f"Deposit: {kostyan_prof.get('casino', {}).get('deposit')}") + except Exception as e: + record("Issue #9: Player kostyan deposit restored and retrievable", "FAIL", str(e)) + + # Issue #10: Seed selection top menu and sticky HUD + css_path = os.path.join(GAME_DIR, "css/style.css") + with open(css_path, "r", encoding="utf-8") as f: + css_code = f.read() + record("Issue #10: style.css has sticky seed deck selection bar", "PASS" if ".seed-deck-bar" in css_code and "position: sticky" in css_code else "FAIL") + + economy_path = os.path.join(GAME_DIR, "js/systems/economy.js") + with open(economy_path, "r", encoding="utf-8") as f: + economy_code = f.read() + record("Issue #10: economy.js guaranteed fallback for cards array in draw()", "PASS" if "this.setDeckFromSelection()" in economy_code else "FAIL") + record("Issue #10: game.js hides pre-battle modals on startLevel", "PASS" if "seed-select-modal" in game_code and "dave-modal" in game_code else "FAIL") + + def main(): print("==================================================") print("🌱 PLANTS VS. ZOMBIES — AUTONOMOUS TEST SUITE 🌱") @@ -1343,6 +1396,7 @@ def main(): test_admin_panel_and_configurator() test_http_endpoint() test_v2_7_features() + test_gitea_critical_bugfixes() passed = sum(1 for r in TEST_RESULTS if r["status"] == "PASS") total = len(TEST_RESULTS)