/** * SkinManager.js * Manages tank skin purchases, equipping, and persistence. * Skins are cosmetic-only color schemes purchased with gold. */ /** 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: '#00FF7F', turret: '#00CED1', track: '#008B8B' }, preview: '#00FF7F', }, shadow: { id: 'shadow', nameKey: 'skin.shadow', cost: 3000, colors: { body: '#2C2C2C', turret: '#1A1A1A', track: '#0D0D0D' }, preview: '#2C2C2C', }, royal: { id: 'royal', nameKey: 'skin.royal', cost: 5000, colors: { body: '#FFD700', turret: '#DAA520', track: '#B8860B' }, preview: '#FFD700', }, }; /** Ordered list of skin IDs for display. */ const SKIN_ORDER = ['default', 'arctic', 'inferno', 'jungle', 'phantom', 'neon', 'shadow', 'royal']; 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) { 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' }; } 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 (!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;