first commit
This commit is contained in:
@@ -0,0 +1,275 @@
|
||||
/**
|
||||
* 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<string>} 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<object>}
|
||||
*/
|
||||
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;
|
||||
Reference in New Issue
Block a user