This repository has been archived on 2026-05-04. You can view files and clone it, but cannot push or open issues or pull requests.
Galaxy-Strike-Online/GameServer/systems/ItemSystem.js

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;