Files
tankwar_proj/js/managers/SkinManager.js
T
2026-05-02 13:50:52 +08:00

317 lines
7.6 KiB
JavaScript

/**
* 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<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) {
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;