/** * SkinManager.js * Manages tank skin purchases, equipping, and persistence. * Skins are cosmetic-only color schemes purchased with gold. */ /** * DEV MODE: Set to true to unlock all skins for testing. * ⚠️ MUST be set to false before publishing! */ const DEV_UNLOCK_ALL = false; /** Skin definitions with id, name, cost, and color scheme. */ const SKINS = { default: { id: 'default', nameKey: 'skin.default', cost: 0, colors: null, // uses default tank color preview: '#FFD700', }, arctic: { id: 'arctic', nameKey: 'skin.arctic', cost: 500, colors: { body: '#B0E0E6', turret: '#5F9EA0', track: '#2F4F4F' }, preview: '#B0E0E6', }, inferno: { id: 'inferno', nameKey: 'skin.inferno', cost: 800, colors: { body: '#FF4500', turret: '#8B0000', track: '#2F0000' }, preview: '#FF4500', }, phantom: { id: 'phantom', nameKey: 'skin.phantom', cost: 1200, colors: { body: '#9370DB', turret: '#4B0082', track: '#1C0033' }, preview: '#9370DB', }, jungle: { id: 'jungle', nameKey: 'skin.jungle', cost: 1000, colors: { body: '#3CB371', turret: '#006400', track: '#002200' }, preview: '#3CB371', }, neon: { id: 'neon', nameKey: 'skin.neon', cost: 2000, colors: { body: '#FF1493', turret: '#FF6EC7', track: '#C71585' }, preview: '#FF1493', }, nebula: { id: 'nebula', nameKey: 'skin.nebula', cost: 3000, colors: { body: '#6A0DAD', turret: '#FF00FF', track: '#3D0066' }, preview: '#6A0DAD', }, royal: { id: 'royal', nameKey: 'skin.royal', cost: 5000, colors: { body: '#FFD700', turret: '#DAA520', track: '#B8860B' }, preview: '#FFD700', }, sakura: { id: 'sakura', nameKey: 'skin.sakura', cost: 3500, colors: { body: '#FFB7C5', turret: '#FF69B4', track: '#C44D78' }, preview: '#FFB7C5', }, thunder: { id: 'thunder', nameKey: 'skin.thunder', cost: 4000, colors: { body: '#1E90FF', turret: '#00BFFF', track: '#0A5E9C' }, preview: '#1E90FF', }, diamond: { id: 'diamond', nameKey: 'skin.diamond', cost: 8000, colors: { body: '#E0F7FF', turret: '#7DF9FF', track: '#5B8FA8' }, preview: '#7DF9FF', }, }; /** Ordered list of skin IDs for display. */ const SKIN_ORDER = ['default', 'arctic', 'inferno', 'jungle', 'phantom', 'neon', 'nebula', 'royal', 'sakura', 'thunder', 'diamond']; class SkinManager { constructor() { /** @type {Set} Unlocked skin IDs. */ this._unlocked = new Set(['default']); /** @type {string} Currently equipped skin ID. */ this._equipped = 'default'; this._load(); } // ============================================================ // Persistence // ============================================================ /** @private */ _load() { try { if (GameGlobal && GameGlobal.storageManager) { const data = GameGlobal.storageManager.get('skins', null); if (data) { this._unlocked = new Set(data.unlocked || ['default']); this._equipped = data.equipped || 'default'; // Ensure default is always unlocked this._unlocked.add('default'); } } } catch (e) { console.warn('[SkinManager] Failed to load skin data:', e); } } /** @private */ _save() { try { if (GameGlobal && GameGlobal.storageManager) { GameGlobal.storageManager.set('skins', { unlocked: Array.from(this._unlocked), equipped: this._equipped, }); } } catch (e) { console.warn('[SkinManager] Failed to save skin data:', e); } } // ============================================================ // Queries // ============================================================ /** * Get all skin definitions in display order. * @returns {Array} */ getAllSkins() { return SKIN_ORDER.map(id => SKINS[id]); } /** * Check if a skin is unlocked. * @param {string} skinId * @returns {boolean} */ isUnlocked(skinId) { if (DEV_UNLOCK_ALL) return true; return this._unlocked.has(skinId); } /** * Get the currently equipped skin ID. * @returns {string} */ getEquippedSkinId() { return this._equipped; } /** * Get the color scheme for the currently equipped skin. * @returns {object|null} { body, turret, track } or null for default. */ getCurrentSkinColors() { const skin = SKINS[this._equipped]; if (!skin) return null; return skin.colors; // null for default skin } /** * Get skin definition by ID. * @param {string} skinId * @returns {object|null} */ getSkin(skinId) { return SKINS[skinId] || null; } // ============================================================ // Actions // ============================================================ /** * Purchase a skin with gold. * @param {string} skinId * @returns {{ success: boolean, error?: string }} */ purchaseSkin(skinId) { const skin = SKINS[skinId]; if (!skin) { return { success: false, error: 'Invalid skin' }; } if (this._unlocked.has(skinId)) { return { success: false, error: 'Already unlocked' }; } // Dev mode: unlock for free without spending gold if (DEV_UNLOCK_ALL) { this._unlocked.add(skinId); this._save(); console.log(`[SkinManager][DEV] Free unlock skin: ${skinId}`); try { if (GameGlobal.eventBus) { GameGlobal.eventBus.emit('skin:purchased', { id: skinId, cost: 0 }); } } catch (e) {} return { success: true }; } const cm = GameGlobal.currencyManager; if (!cm || !cm.hasGold(skin.cost)) { return { success: false, error: 'Insufficient gold' }; } const spent = cm.spendGold(skin.cost); if (!spent) { return { success: false, error: 'Insufficient gold' }; } this._unlocked.add(skinId); this._save(); console.log(`[SkinManager] Purchased skin: ${skinId} for ${skin.cost} gold`); // Emit event try { if (GameGlobal.eventBus) { GameGlobal.eventBus.emit('skin:purchased', { id: skinId, cost: skin.cost }); } } catch (e) {} return { success: true }; } /** * Equip an unlocked skin. * @param {string} skinId * @returns {{ success: boolean, error?: string }} */ equipSkin(skinId) { if (!SKINS[skinId]) { return { success: false, error: 'Invalid skin' }; } if (!DEV_UNLOCK_ALL && !this._unlocked.has(skinId)) { return { success: false, error: 'Not unlocked' }; } this._equipped = skinId; this._save(); console.log(`[SkinManager] Equipped skin: ${skinId}`); // Emit event try { if (GameGlobal.eventBus) { GameGlobal.eventBus.emit('skin:equipped', { id: skinId }); } } catch (e) {} return { success: true }; } // ============================================================ // Cloud Sync // ============================================================ /** * Get skin data for cloud sync. * @returns {object} */ getCloudSyncData() { return { unlocked: Array.from(this._unlocked), equipped: this._equipped, }; } /** * Restore skin data from cloud (merge: keep all unlocked). * @param {object} cloudData */ restoreFromCloud(cloudData) { if (!cloudData) return; if (cloudData.unlocked) { for (const id of cloudData.unlocked) { if (SKINS[id]) { this._unlocked.add(id); } } } if (cloudData.equipped && SKINS[cloudData.equipped] && this._unlocked.has(cloudData.equipped)) { this._equipped = cloudData.equipped; } this._save(); } } module.exports = SkinManager;