first commit

This commit is contained in:
jakciehan
2026-04-10 22:59:39 +08:00
commit cc2e7b9bb0
89 changed files with 23631 additions and 0 deletions
+275
View File
@@ -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;