211 lines
8.5 KiB
JavaScript
211 lines
8.5 KiB
JavaScript
/**
|
|
* Galaxy Strike Online — Server Item System
|
|
* All item/shop data comes from ContentLoader (data/gso/items/).
|
|
* No hardcoded item data lives here.
|
|
*/
|
|
|
|
class ItemSystem {
|
|
constructor(contentLoader = null) {
|
|
this.serverUrl = process.env.SERVER_URL || 'http://localhost:3002';
|
|
this._loader = contentLoader;
|
|
|
|
if (contentLoader) {
|
|
console.log(`[ITEM SYSTEM] ${contentLoader.items.size} items available from ContentLoader`);
|
|
} else {
|
|
console.warn('[ITEM SYSTEM] No ContentLoader provided — item catalog is empty');
|
|
}
|
|
}
|
|
|
|
setServerUrl(url) { this.serverUrl = url; }
|
|
|
|
// ── Catalog access ─────────────────────────────────────────────────────
|
|
|
|
getAllItems() { return this._loader ? this._loader.getAllItems() : []; }
|
|
getItem(id) { return this._loader ? this._loader.getItem(id) : null; }
|
|
getItemsByType(type) { return this._loader ? this._loader.getItemsByType(type) : []; }
|
|
|
|
/**
|
|
* Returns all items that include 'shop' in their categories array.
|
|
* Grouped by item type: { ship: [...], weapon: [...], ... }
|
|
*/
|
|
getShopItemsByCategory() {
|
|
const out = {};
|
|
for (const item of this.getAllItems()) {
|
|
if (!Array.isArray(item.categories) || !item.categories.includes('shop')) continue;
|
|
const t = item.type || 'misc';
|
|
if (!out[t]) out[t] = [];
|
|
out[t].push(this._normaliseForClient(item));
|
|
}
|
|
return out;
|
|
}
|
|
|
|
/**
|
|
* Flat array of all shop items.
|
|
* Called as getRandomShopItems() by legacy server.js code — returns the same thing,
|
|
* just grouped (matching the expected { category: [items] } shape).
|
|
*/
|
|
getRandomShopItems() {
|
|
return this.getShopItemsByCategory();
|
|
}
|
|
|
|
/**
|
|
* All shop items for a single type/category.
|
|
* Called as getRandomItemsByCategory(category) by legacy server.js code.
|
|
*/
|
|
getRandomItemsByCategory(category) {
|
|
return this.getAllItems()
|
|
.filter(i =>
|
|
Array.isArray(i.categories) && i.categories.includes('shop') &&
|
|
i.type === category
|
|
)
|
|
.map(i => this._normaliseForClient(i));
|
|
}
|
|
|
|
// ── Shop helpers ───────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Find an item that is available in the shop.
|
|
* Returns null if the item doesn't exist or isn't in the shop.
|
|
*/
|
|
findShopItem(itemId) {
|
|
const item = this.getItem(itemId);
|
|
if (!item) return null;
|
|
if (!Array.isArray(item.categories) || !item.categories.includes('shop')) return null;
|
|
return item;
|
|
}
|
|
|
|
/**
|
|
* Validate a purchase against player wallet.
|
|
* Returns { valid: true } or { valid: false, error: '...' }
|
|
*/
|
|
validatePurchase(item, playerStats, quantity = 1) {
|
|
const currency = item.currency || 'credits';
|
|
const totalCost = (item.price || 0) * quantity;
|
|
|
|
if (currency === 'credits' && (playerStats.credits || 0) < totalCost) {
|
|
return { valid: false, error: 'Not enough credits' };
|
|
}
|
|
if (currency === 'gems' && (playerStats.gems || 0) < totalCost) {
|
|
return { valid: false, error: 'Not enough gems' };
|
|
}
|
|
return { valid: true, totalCost, currency };
|
|
}
|
|
|
|
/**
|
|
* Apply a purchase to playerData (mutates directly, like the original server did).
|
|
* Returns the purchase summary for the response payload.
|
|
*/
|
|
applyPurchase(item, playerData, quantity = 1) {
|
|
const currency = item.currency || 'credits';
|
|
const totalCost = (item.price || 0) * quantity;
|
|
|
|
// Deduct currency
|
|
if (currency === 'credits') playerData.stats.credits = (playerData.stats.credits || 0) - totalCost;
|
|
if (currency === 'gems') playerData.stats.gems = (playerData.stats.gems || 0) - totalCost;
|
|
|
|
// Grant item
|
|
switch (item.type) {
|
|
case 'decoration':
|
|
if (!playerData.starbase) playerData.starbase = { wallpaper: null, ownedWallpapers: [], unlockedRooms: [] };
|
|
if (item.subtype === 'wallpaper') {
|
|
if (!playerData.starbase.ownedWallpapers) playerData.starbase.ownedWallpapers = [];
|
|
if (!playerData.starbase.ownedWallpapers.includes(item.id)) {
|
|
playerData.starbase.ownedWallpapers.push(item.id);
|
|
}
|
|
} else if (item.subtype === 'room_unlock') {
|
|
if (!playerData.starbase.unlockedRooms) playerData.starbase.unlockedRooms = [];
|
|
if (item.roomId && !playerData.starbase.unlockedRooms.includes(item.roomId)) {
|
|
playerData.starbase.unlockedRooms.push(item.roomId);
|
|
}
|
|
}
|
|
break;
|
|
|
|
case 'ship':
|
|
if (!playerData.ownedShips) playerData.ownedShips = [];
|
|
if (!playerData.ownedShips.includes(item.id)) playerData.ownedShips.push(item.id);
|
|
break;
|
|
|
|
case 'cosmetic':
|
|
if (!playerData.ownedCosmetics) playerData.ownedCosmetics = [];
|
|
if (!playerData.ownedCosmetics.includes(item.id)) playerData.ownedCosmetics.push(item.id);
|
|
break;
|
|
|
|
case 'weapon':
|
|
case 'armour':
|
|
case 'consumable':
|
|
case 'material':
|
|
case 'hullPlating':
|
|
default:
|
|
if (!playerData.inventory) playerData.inventory = { items: [] };
|
|
if (!playerData.inventory.items) playerData.inventory.items = [];
|
|
// Stack consumables/materials; unique otherwise
|
|
const existing = playerData.inventory.items.find(i => i.id === item.id && item.stackable);
|
|
if (existing) {
|
|
existing.quantity = (existing.quantity || 1) + quantity;
|
|
} else {
|
|
playerData.inventory.items.push({
|
|
id: item.id,
|
|
name: item.name,
|
|
type: item.type,
|
|
quantity: quantity,
|
|
acquired: new Date().toISOString()
|
|
});
|
|
}
|
|
break;
|
|
}
|
|
|
|
return {
|
|
totalCost,
|
|
currency,
|
|
newBalance: currency === 'credits' ? playerData.stats.credits : playerData.stats.gems
|
|
};
|
|
}
|
|
|
|
// ── Packet builder helpers ─────────────────────────────────────────────
|
|
|
|
/** Builds the full shopItemsReceived payload */
|
|
buildShopResponse() {
|
|
return {
|
|
success: true,
|
|
shopItems: this.getShopItemsByCategory(),
|
|
timestamp: new Date().toISOString()
|
|
};
|
|
}
|
|
|
|
/** Builds the itemDetailsReceived payload */
|
|
buildItemDetailResponse(itemId) {
|
|
const item = this.getItem(itemId);
|
|
if (!item) return { success: false, error: `Item '${itemId}' not found` };
|
|
return { success: true, item: this._normaliseForClient(item) };
|
|
}
|
|
|
|
// ── Internal normalisation ─────────────────────────────────────────────
|
|
|
|
/**
|
|
* Normalise an item object into the flat shape the client expects.
|
|
* Handles both bare-item format (what ContentLoader returns after unwrapping templates)
|
|
* and the raw wrapped format { templates: { <type>: {...} } }.
|
|
*/
|
|
_normaliseForClient(item) {
|
|
return {
|
|
id: item.id,
|
|
name: item.name || item.id,
|
|
type: item.type || 'misc',
|
|
rarity: item.rarity || 'common',
|
|
price: item.price || 0,
|
|
currency: item.currency || 'credits',
|
|
description: item.description || '',
|
|
texture: item.texture || '',
|
|
texturePath: item.texture || '',
|
|
stats: item.stats || {},
|
|
categories: item.categories || [],
|
|
requirements:item.requirements || {},
|
|
stackable: item.stackable ?? false,
|
|
maxStack: item.maxStack || 1,
|
|
meta: item.meta || {}
|
|
};
|
|
}
|
|
}
|
|
|
|
module.exports = ItemSystem;
|