server data-driven now of quests, items, and dungeons + fixed saves
This commit is contained in:
parent
5e1a2e0037
commit
4aeb217e51
@ -1,18 +0,0 @@
|
|||||||
# Game Server Configuration
|
|
||||||
PORT=3002
|
|
||||||
NODE_ENV=production
|
|
||||||
|
|
||||||
# Database Configuration
|
|
||||||
MONGODB_URI=mongodb://localhost:27017/galaxystrikeonline
|
|
||||||
|
|
||||||
# API Server Configuration
|
|
||||||
API_SERVER_URL=http://localhost:3000
|
|
||||||
GAME_SERVER_URL=https://dev.gameserver.galaxystrike.online
|
|
||||||
|
|
||||||
# Optional: Server identification
|
|
||||||
SERVER_NAME=Dev Game Server
|
|
||||||
SERVER_REGION=local
|
|
||||||
MAX_PLAYERS=8
|
|
||||||
|
|
||||||
# Logging
|
|
||||||
LOG_LEVEL=info
|
|
||||||
@ -3,9 +3,12 @@ const logger = require('../utils/logger');
|
|||||||
|
|
||||||
const connectDB = async () => {
|
const connectDB = async () => {
|
||||||
try {
|
try {
|
||||||
|
console.log('[DATABASE] Connecting to MongoDB:', process.env.MONGODB_URI);
|
||||||
const conn = await mongoose.connect(process.env.MONGODB_URI);
|
const conn = await mongoose.connect(process.env.MONGODB_URI);
|
||||||
|
console.log(`[DATABASE] MongoDB Connected: ${conn.connection.host}`);
|
||||||
logger.info(`MongoDB Connected: ${conn.connection.host}`);
|
logger.info(`MongoDB Connected: ${conn.connection.host}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error('[DATABASE] Database connection failed:', error);
|
||||||
logger.error('Database connection failed:', error);
|
logger.error('Database connection failed:', error);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|||||||
128
GameServer/models/PlayerData.js
Normal file
128
GameServer/models/PlayerData.js
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
const mongoose = require('mongoose');
|
||||||
|
|
||||||
|
const playerDataSchema = new mongoose.Schema({
|
||||||
|
userId: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
unique: true
|
||||||
|
},
|
||||||
|
username: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
// Player stats
|
||||||
|
stats: {
|
||||||
|
level: { type: Number, default: 1 },
|
||||||
|
experience: { type: Number, default: 0 },
|
||||||
|
totalExperience: { type: Number, default: 0 },
|
||||||
|
credits: { type: Number, default: 1000 },
|
||||||
|
gems: { type: Number, default: 0 },
|
||||||
|
skillPoints: { type: Number, default: 0 },
|
||||||
|
totalKills: { type: Number, default: 0 },
|
||||||
|
dungeonsCleared: { type: Number, default: 0 },
|
||||||
|
questsCompleted: { type: Number, default: 0 },
|
||||||
|
playTime: { type: Number, default: 0 },
|
||||||
|
lastLogin: { type: Date, default: Date.now }
|
||||||
|
},
|
||||||
|
// Game systems data
|
||||||
|
inventory: {
|
||||||
|
items: [{ type: mongoose.Schema.Types.Mixed }],
|
||||||
|
maxSize: { type: Number, default: 50 }
|
||||||
|
},
|
||||||
|
ship: {
|
||||||
|
type: mongoose.Schema.Types.Mixed,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
base: {
|
||||||
|
type: mongoose.Schema.Types.Mixed,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
quests: {
|
||||||
|
active: [{ type: mongoose.Schema.Types.Mixed }],
|
||||||
|
completed: [{ type: String }]
|
||||||
|
},
|
||||||
|
skills: {
|
||||||
|
type: mongoose.Schema.Types.Mixed,
|
||||||
|
default: {}
|
||||||
|
},
|
||||||
|
idleSystem: {
|
||||||
|
type: mongoose.Schema.Types.Mixed,
|
||||||
|
default: {}
|
||||||
|
},
|
||||||
|
dungeonSystem: {
|
||||||
|
type: mongoose.Schema.Types.Mixed,
|
||||||
|
default: {}
|
||||||
|
},
|
||||||
|
crafting: {
|
||||||
|
type: mongoose.Schema.Types.Mixed,
|
||||||
|
default: {}
|
||||||
|
},
|
||||||
|
// Server-specific data
|
||||||
|
currentServerId: {
|
||||||
|
type: String,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
lastServerJoin: {
|
||||||
|
type: Date,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
totalPlayTime: {
|
||||||
|
type: Number,
|
||||||
|
default: 0
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
timestamps: true
|
||||||
|
});
|
||||||
|
|
||||||
|
// Indexes for performance
|
||||||
|
playerDataSchema.index({ username: 1 });
|
||||||
|
playerDataSchema.index({ 'stats.level': 1 });
|
||||||
|
|
||||||
|
// Note: userId field already has unique: true which creates an index automatically
|
||||||
|
|
||||||
|
// Methods
|
||||||
|
playerDataSchema.methods.addExperience = function(amount) {
|
||||||
|
this.stats.experience += amount;
|
||||||
|
this.stats.playTime += Date.now() - (this.lastLogin || Date.now());
|
||||||
|
this.lastLogin = new Date();
|
||||||
|
return this.stats.experience;
|
||||||
|
};
|
||||||
|
|
||||||
|
playerDataSchema.methods.addCredits = function(amount) {
|
||||||
|
this.stats.credits += amount;
|
||||||
|
return this.stats.credits;
|
||||||
|
};
|
||||||
|
|
||||||
|
playerDataSchema.methods.updatePlayTime = function() {
|
||||||
|
const sessionTime = Date.now() - (this.lastLogin || Date.now());
|
||||||
|
this.stats.playTime += sessionTime;
|
||||||
|
this.totalPlayTime += sessionTime;
|
||||||
|
this.lastLogin = new Date();
|
||||||
|
return sessionTime;
|
||||||
|
};
|
||||||
|
|
||||||
|
playerDataSchema.methods.joinServer = function(serverId) {
|
||||||
|
this.currentServerId = serverId;
|
||||||
|
this.lastServerJoin = new Date();
|
||||||
|
this.lastLogin = new Date();
|
||||||
|
};
|
||||||
|
|
||||||
|
playerDataSchema.methods.leaveServer = function() {
|
||||||
|
console.log('[PLAYER DATA] leaveServer called for:', this.username);
|
||||||
|
|
||||||
|
// Update play time before leaving
|
||||||
|
const sessionTime = Date.now() - (this.lastLogin || Date.now());
|
||||||
|
this.stats.playTime += sessionTime;
|
||||||
|
this.totalPlayTime += sessionTime;
|
||||||
|
this.lastLogin = new Date();
|
||||||
|
|
||||||
|
// Clear server association
|
||||||
|
this.currentServerId = null;
|
||||||
|
this.lastServerJoin = null;
|
||||||
|
|
||||||
|
console.log('[PLAYER DATA] Updated play time:', sessionTime, 'Total play time:', this.totalPlayTime);
|
||||||
|
|
||||||
|
return sessionTime;
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = mongoose.model('PlayerData', playerDataSchema);
|
||||||
@ -263,4 +263,118 @@ router.post('/cleanup', async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Get room types
|
||||||
|
router.get('/room-types', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const roomTypes = dungeonSystem.getRoomTypes();
|
||||||
|
res.json(roomTypes);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: error.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get enemy templates
|
||||||
|
router.get('/enemy-templates', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const enemyTemplates = dungeonSystem.getEnemyTemplates();
|
||||||
|
res.json(enemyTemplates);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: error.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Start dungeon (simplified for client)
|
||||||
|
router.post('/start', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { dungeonId, userId } = req.body;
|
||||||
|
|
||||||
|
const instance = dungeonSystem.createInstance(dungeonId, userId, []);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
instance
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: error.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Process encounter (simplified for client)
|
||||||
|
router.post('/encounter', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { instanceId, userId } = req.body;
|
||||||
|
|
||||||
|
const result = dungeonSystem.startEncounter(instanceId, userId);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
encounter: result
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: error.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Complete dungeon (simplified for client)
|
||||||
|
router.post('/complete', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { instanceId, userId } = req.body;
|
||||||
|
|
||||||
|
const result = dungeonSystem.completeDungeon(instanceId);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
rewards: result
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: error.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get player dungeon status
|
||||||
|
router.get('/status/:userId', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { userId } = req.params;
|
||||||
|
const instance = dungeonSystem.getPlayerInstance(userId);
|
||||||
|
|
||||||
|
if (!instance) {
|
||||||
|
return res.json({
|
||||||
|
success: true,
|
||||||
|
status: {
|
||||||
|
hasActiveDungeon: false,
|
||||||
|
currentInstance: null
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
status: {
|
||||||
|
hasActiveDungeon: true,
|
||||||
|
currentInstance: instance
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: error.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
@ -7,17 +7,42 @@ const express = require('express');
|
|||||||
const http = require('http');
|
const http = require('http');
|
||||||
const socketIo = require('socket.io');
|
const socketIo = require('socket.io');
|
||||||
const cors = require('cors');
|
const cors = require('cors');
|
||||||
const dotenv = require('dotenv');
|
const helmet = require('helmet');
|
||||||
|
const compression = require('compression');
|
||||||
|
const mongoose = require('mongoose');
|
||||||
require('dotenv').config();
|
require('dotenv').config();
|
||||||
|
|
||||||
const logger = require('./utils/logger');
|
const logger = require('./utils/logger');
|
||||||
const connectDB = require('./config/database');
|
const connectDB = require('./config/database');
|
||||||
|
const PlayerData = require('./models/PlayerData');
|
||||||
|
|
||||||
|
// Import server systems for player initialization
|
||||||
|
const QuestSystem = require('./systems/QuestSystem');
|
||||||
|
const SkillSystem = require('./systems/SkillSystem');
|
||||||
|
const DungeonSystem = require('./systems/DungeonSystem');
|
||||||
|
const CraftingSystem = require('./systems/CraftingSystem');
|
||||||
|
const IdleSystem = require('./systems/IdleSystem');
|
||||||
|
const ItemSystem = require('./systems/ItemSystem');
|
||||||
|
|
||||||
|
// Initialize server systems
|
||||||
|
const questSystem = new QuestSystem();
|
||||||
|
const skillSystem = new SkillSystem();
|
||||||
|
const dungeonSystem = new DungeonSystem();
|
||||||
|
const craftingSystem = new CraftingSystem();
|
||||||
|
const idleSystem = new IdleSystem();
|
||||||
|
const itemSystem = new ItemSystem();
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
const server = http.createServer(app);
|
const server = http.createServer(app);
|
||||||
const io = socketIo(server, {
|
const io = socketIo(server, {
|
||||||
cors: {
|
cors: {
|
||||||
origin: ["http://localhost:3000", "http://127.0.0.1:3000", "file://"],
|
origin: [
|
||||||
|
"http://localhost:3000",
|
||||||
|
"http://127.0.0.1:3000",
|
||||||
|
"file://",
|
||||||
|
"https://dev.galaxystrike.online",
|
||||||
|
"https://galaxystrike.online"
|
||||||
|
],
|
||||||
methods: ["GET", "POST"],
|
methods: ["GET", "POST"],
|
||||||
credentials: true
|
credentials: true
|
||||||
}
|
}
|
||||||
@ -27,9 +52,20 @@ const io = socketIo(server, {
|
|||||||
const gameInstances = new Map();
|
const gameInstances = new Map();
|
||||||
const connectedClients = new Map();
|
const connectedClients = new Map();
|
||||||
|
|
||||||
|
// Server-side shop item lookup using ItemSystem
|
||||||
|
function findShopItem(itemId) {
|
||||||
|
return itemSystem.getItem(itemId);
|
||||||
|
}
|
||||||
|
|
||||||
// Middleware
|
// Middleware
|
||||||
app.use(cors({
|
app.use(cors({
|
||||||
origin: ["http://localhost:3000", "http://127.0.0.1:3000", "file://"],
|
origin: [
|
||||||
|
"http://localhost:3000",
|
||||||
|
"http://127.0.0.1:3000",
|
||||||
|
"file://",
|
||||||
|
"https://dev.galaxystrike.online",
|
||||||
|
"https://galaxystrike.online"
|
||||||
|
],
|
||||||
credentials: true
|
credentials: true
|
||||||
}));
|
}));
|
||||||
app.use(express.json({ limit: '10mb' }));
|
app.use(express.json({ limit: '10mb' }));
|
||||||
@ -57,6 +93,69 @@ app.get('/api/ssc/version', (req, res) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Shop API endpoints
|
||||||
|
app.get('/api/shop/items', (req, res) => {
|
||||||
|
try {
|
||||||
|
const shopItems = itemSystem.getShopItems();
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
items: shopItems,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[GAME SERVER] Error fetching shop items:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Failed to fetch shop items'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/api/shop/items/:category', (req, res) => {
|
||||||
|
try {
|
||||||
|
const { category } = req.params;
|
||||||
|
const items = itemSystem.getItemsByCategory(category);
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
category: category,
|
||||||
|
items: items,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[GAME SERVER] Error fetching shop category:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Failed to fetch shop category'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/api/items/:itemId', (req, res) => {
|
||||||
|
try {
|
||||||
|
const { itemId } = req.params;
|
||||||
|
const item = itemSystem.getItem(itemId);
|
||||||
|
|
||||||
|
if (!item) {
|
||||||
|
return res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Item not found'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
item: item
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[GAME SERVER] Error fetching item:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Failed to fetch item'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Game data endpoints (similar to LocalServer)
|
// Game data endpoints (similar to LocalServer)
|
||||||
app.post('/api/game/player/:id/save', (req, res) => {
|
app.post('/api/game/player/:id/save', (req, res) => {
|
||||||
const playerId = req.params.id;
|
const playerId = req.params.id;
|
||||||
@ -117,36 +216,146 @@ io.on('connection', (socket) => {
|
|||||||
instanceId: null
|
instanceId: null
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log('[GAME SERVER] Waiting for authentication from:', socket.id);
|
||||||
|
|
||||||
// Update player count on API server when new player connects
|
// Update player count on API server when new player connects
|
||||||
updatePlayerCountOnAPI();
|
updatePlayerCountOnAPI();
|
||||||
|
|
||||||
|
// Add timeout for authentication
|
||||||
|
const authTimeout = setTimeout(() => {
|
||||||
|
const clientData = connectedClients.get(socket.id);
|
||||||
|
if (clientData && !clientData.userId) {
|
||||||
|
console.log('[GAME SERVER] Authentication timeout for:', socket.id);
|
||||||
|
socket.emit('authenticated', {
|
||||||
|
success: false,
|
||||||
|
error: 'Authentication timeout'
|
||||||
|
});
|
||||||
|
socket.disconnect();
|
||||||
|
}
|
||||||
|
}, 10000); // 10 seconds timeout
|
||||||
|
|
||||||
|
// Clear timeout when authenticated
|
||||||
|
socket.on('authenticated', () => {
|
||||||
|
clearTimeout(authTimeout);
|
||||||
|
});
|
||||||
|
|
||||||
// Authentication (similar to LocalServer)
|
// Authentication (similar to LocalServer)
|
||||||
socket.on('authenticate', (data) => {
|
socket.on('authenticate', async (data) => {
|
||||||
console.log('[GAME SERVER] Authenticating client:', socket.id, data);
|
console.log('[GAME SERVER] Authenticating client:', socket.id, data);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Check database connection first
|
||||||
|
if (mongoose.connection.readyState !== 1) {
|
||||||
|
console.error('[GAME SERVER] Database not connected, authentication failed');
|
||||||
|
socket.emit('authenticated', {
|
||||||
|
success: false,
|
||||||
|
error: 'Database not available'
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[GAME SERVER] Database is connected, proceeding with authentication');
|
||||||
|
|
||||||
|
// Load player data from database
|
||||||
|
const playerData = await loadPlayerData(data.userId || socket.id, data.username || 'Game Player');
|
||||||
|
|
||||||
|
if (playerData) {
|
||||||
|
console.log('[GAME SERVER] Player data loaded successfully:', playerData.username);
|
||||||
|
|
||||||
|
// Store player data in client connection
|
||||||
|
const clientData = connectedClients.get(socket.id);
|
||||||
|
if (clientData) {
|
||||||
|
clientData.playerData = playerData;
|
||||||
|
clientData.userId = playerData.userId;
|
||||||
|
clientData.username = playerData.username;
|
||||||
|
} else {
|
||||||
|
console.error('[GAME SERVER] No client data found for socket:', socket.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Join server
|
||||||
|
playerData.joinServer(`devgame-server-${PORT}`);
|
||||||
|
|
||||||
|
// Update last login time
|
||||||
|
if (!playerData.stats) playerData.stats = {};
|
||||||
|
playerData.stats.lastLogin = new Date().toISOString();
|
||||||
|
|
||||||
|
// Ensure all required fields exist (for existing players)
|
||||||
|
if (playerData.stats.totalExperience === undefined) playerData.stats.totalExperience = playerData.stats.experience || 0;
|
||||||
|
if (playerData.stats.gems === undefined || playerData.stats.gems === 0) playerData.stats.gems = 50; // Restore gems if wiped
|
||||||
|
if (playerData.stats.skillPoints === undefined) playerData.stats.skillPoints = 0;
|
||||||
|
if (playerData.stats.totalKills === undefined) playerData.stats.totalKills = 0;
|
||||||
|
if (playerData.stats.questsCompleted === undefined) playerData.stats.questsCompleted = 0;
|
||||||
|
|
||||||
|
await savePlayerData(playerData.userId, playerData);
|
||||||
|
|
||||||
// In production, validate with API server
|
// In production, validate with API server
|
||||||
socket.emit('authenticated', {
|
socket.emit('authenticated', {
|
||||||
success: true,
|
success: true,
|
||||||
user: {
|
user: {
|
||||||
id: data.userId || socket.id,
|
id: playerData.userId,
|
||||||
username: data.username || 'Game Player',
|
username: playerData.username,
|
||||||
token: 'game-token-' + Date.now()
|
token: 'game-token-' + Date.now()
|
||||||
|
},
|
||||||
|
playerData: {
|
||||||
|
...playerData.toObject(),
|
||||||
|
serverTimestamp: Date.now(), // Add server timestamp for time synchronization
|
||||||
|
serverTimezone: 'UTC' // Server operates in UTC timezone
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log(`[GAME SERVER] ${playerData.username} authenticated with Level ${playerData.stats.level}`);
|
||||||
|
} else {
|
||||||
|
console.error('[GAME SERVER] Failed to load player data');
|
||||||
|
socket.emit('authenticated', {
|
||||||
|
success: false,
|
||||||
|
error: 'Failed to load player data'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[GAME SERVER] Authentication error:', error);
|
||||||
|
console.error('[GAME INITIALIZER] Error stack:', error.stack);
|
||||||
|
socket.emit('authenticated', {
|
||||||
|
success: false,
|
||||||
|
error: 'Authentication failed: ' + error.message
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Game data events (similar to LocalServer)
|
// Game data events (similar to LocalServer)
|
||||||
socket.on('saveGameData', (data) => {
|
socket.on('saveGameData', async (data) => {
|
||||||
console.log('[GAME SERVER] Saving game data for:', socket.id);
|
console.log('[GAME SERVER] Saving game data for:', socket.id);
|
||||||
|
|
||||||
if (connectedClients.has(socket.id)) {
|
const clientData = connectedClients.get(socket.id);
|
||||||
connectedClients.get(socket.id).playerData = data;
|
if (clientData && clientData.userId) {
|
||||||
|
try {
|
||||||
|
// Validate the data before saving
|
||||||
|
if (!data || typeof data !== 'object') {
|
||||||
|
console.warn('[GAME SERVER] Invalid game data received, skipping save');
|
||||||
|
socket.emit('gameDataSaved', { success: false, error: 'Invalid game data' });
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update player data with new game data
|
||||||
|
const updatedPlayerData = { ...clientData.playerData, ...data };
|
||||||
|
clientData.playerData = updatedPlayerData;
|
||||||
|
|
||||||
|
// Save to database
|
||||||
|
const success = await savePlayerData(clientData.userId, updatedPlayerData);
|
||||||
|
|
||||||
socket.emit('gameDataSaved', {
|
socket.emit('gameDataSaved', {
|
||||||
success: true,
|
success: success,
|
||||||
timestamp: Date.now()
|
message: success ? 'Game saved to server!' : 'Failed to save to server'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log(`[GAME SERVER] Saved game data for ${clientData.username}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[GAME SERVER] Error saving game data:', error);
|
||||||
|
socket.emit('gameDataSaved', { success: false, error: 'Failed to save data' });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn('[GAME SERVER] No client data or user ID found for saveGameData');
|
||||||
|
socket.emit('gameDataSaved', { success: false, error: 'Player not authenticated' });
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('loadGameData', (data) => {
|
socket.on('loadGameData', (data) => {
|
||||||
@ -160,6 +369,61 @@ io.on('connection', (socket) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Shop and item system events
|
||||||
|
socket.on('getShopItems', (data) => {
|
||||||
|
console.log('[GAME SERVER] Sending shop items to:', socket.id);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const shopItems = itemSystem.getShopItems();
|
||||||
|
socket.emit('shopItemsReceived', {
|
||||||
|
success: true,
|
||||||
|
items: shopItems,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[GAME SERVER] Error sending shop items:', error);
|
||||||
|
socket.emit('shopItemsReceived', {
|
||||||
|
success: false,
|
||||||
|
error: 'Failed to load shop items'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('getItemDetails', (data) => {
|
||||||
|
const { itemId } = data;
|
||||||
|
|
||||||
|
if (!itemId) {
|
||||||
|
socket.emit('itemDetailsReceived', {
|
||||||
|
success: false,
|
||||||
|
error: 'Item ID required'
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const item = itemSystem.getItem(itemId);
|
||||||
|
|
||||||
|
if (!item) {
|
||||||
|
socket.emit('itemDetailsReceived', {
|
||||||
|
success: false,
|
||||||
|
error: 'Item not found'
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
socket.emit('itemDetailsReceived', {
|
||||||
|
success: true,
|
||||||
|
item: item
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[GAME SERVER] Error getting item details:', error);
|
||||||
|
socket.emit('itemDetailsReceived', {
|
||||||
|
success: false,
|
||||||
|
error: 'Failed to get item details'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Game-specific events
|
// Game-specific events
|
||||||
socket.on('joinGameInstance', (data) => {
|
socket.on('joinGameInstance', (data) => {
|
||||||
const { instanceId } = data;
|
const { instanceId } = data;
|
||||||
@ -226,11 +490,359 @@ io.on('connection', (socket) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('disconnect', () => {
|
// Idle rewards events
|
||||||
|
socket.on('claimOfflineRewards', async (data) => {
|
||||||
|
console.log('[GAME SERVER] Claiming offline rewards for:', socket.id);
|
||||||
|
|
||||||
|
const clientData = connectedClients.get(socket.id);
|
||||||
|
if (!clientData || !clientData.userId) {
|
||||||
|
socket.emit('offlineRewardsClaimed', { success: false, error: 'Not authenticated' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const playerData = await loadPlayerData(clientData.userId);
|
||||||
|
const offlineRewards = idleSystem.calculateOfflineRewards(clientData.userId);
|
||||||
|
|
||||||
|
if (offlineRewards.offlineTime > 0 && offlineRewards.rewards.credits > 0) {
|
||||||
|
// Apply rewards to player
|
||||||
|
playerData.stats.credits = (playerData.stats.credits || 0) + offlineRewards.rewards.credits;
|
||||||
|
playerData.stats.experience = (playerData.stats.experience || 0) + offlineRewards.rewards.experience;
|
||||||
|
|
||||||
|
// Update idle system data
|
||||||
|
if (!playerData.idleSystem) playerData.idleSystem = {};
|
||||||
|
playerData.idleSystem.lastActive = new Date().toISOString();
|
||||||
|
playerData.idleSystem.totalOfflineTime += offlineRewards.offlineTime;
|
||||||
|
playerData.idleSystem.totalIdleCredits += offlineRewards.rewards.credits;
|
||||||
|
|
||||||
|
await savePlayerData(clientData.userId, playerData);
|
||||||
|
|
||||||
|
socket.emit('offlineRewardsClaimed', {
|
||||||
|
success: true,
|
||||||
|
rewards: offlineRewards.rewards,
|
||||||
|
offlineTime: offlineRewards.offlineTime
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`[GAME SERVER] Offline rewards claimed for ${clientData.username}: ${offlineRewards.rewards.credits} credits`);
|
||||||
|
} else {
|
||||||
|
socket.emit('offlineRewardsClaimed', {
|
||||||
|
success: true,
|
||||||
|
rewards: { credits: 0, experience: 0, energy: 0 },
|
||||||
|
offlineTime: 0
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[GAME SERVER] Error claiming offline rewards:', error);
|
||||||
|
socket.emit('offlineRewardsClaimed', { success: false, error: 'Failed to claim rewards' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Shop purchase events
|
||||||
|
socket.on('purchaseItem', async (data) => {
|
||||||
|
console.log('[GAME SERVER] Processing purchase request:', socket.id, data);
|
||||||
|
|
||||||
|
const clientData = connectedClients.get(socket.id);
|
||||||
|
if (!clientData || !clientData.userId) {
|
||||||
|
console.log('[GAME SERVER] Purchase failed - not authenticated');
|
||||||
|
socket.emit('purchaseCompleted', { success: false, error: 'Not authenticated' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { itemId, quantity = 1 } = data;
|
||||||
|
|
||||||
|
console.log('[GAME SERVER] Purchase details:', {
|
||||||
|
itemId: itemId,
|
||||||
|
quantity: quantity,
|
||||||
|
userId: clientData.userId,
|
||||||
|
username: clientData.username
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!itemId) {
|
||||||
|
console.log('[GAME SERVER] Purchase failed - no item ID');
|
||||||
|
socket.emit('purchaseCompleted', { success: false, error: 'Item ID required' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load current player data
|
||||||
|
const playerData = await loadPlayerData(clientData.userId);
|
||||||
|
console.log('[GAME SERVER] Player data loaded:', {
|
||||||
|
username: playerData.username,
|
||||||
|
credits: playerData.stats.credits,
|
||||||
|
gems: playerData.stats.gems
|
||||||
|
});
|
||||||
|
|
||||||
|
// Find the item in shop (server-side validation)
|
||||||
|
const item = findShopItem(itemId);
|
||||||
|
if (!item) {
|
||||||
|
console.log('[GAME SERVER] Purchase failed - item not found:', itemId);
|
||||||
|
socket.emit('purchaseCompleted', { success: false, error: 'Item not found in shop' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[GAME SERVER] Item found:', {
|
||||||
|
id: item.id,
|
||||||
|
name: item.name,
|
||||||
|
type: item.type,
|
||||||
|
price: item.price,
|
||||||
|
currency: item.currency
|
||||||
|
});
|
||||||
|
|
||||||
|
// Calculate total cost
|
||||||
|
const totalCost = item.price * quantity;
|
||||||
|
const currency = item.currency;
|
||||||
|
|
||||||
|
console.log('[GAME SERVER] Cost calculation:', {
|
||||||
|
unitPrice: item.price,
|
||||||
|
quantity: quantity,
|
||||||
|
totalCost: totalCost,
|
||||||
|
currency: currency
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check if player can afford
|
||||||
|
if (currency === 'credits' && (playerData.stats.credits || 0) < totalCost) {
|
||||||
|
console.log('[GAME SERVER] Purchase failed - insufficient credits:', {
|
||||||
|
required: totalCost,
|
||||||
|
current: playerData.stats.credits || 0
|
||||||
|
});
|
||||||
|
socket.emit('purchaseCompleted', { success: false, error: 'Not enough credits' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currency === 'gems' && (playerData.stats.gems || 0) < totalCost) {
|
||||||
|
console.log('[GAME SERVER] Purchase failed - insufficient gems:', {
|
||||||
|
required: totalCost,
|
||||||
|
current: playerData.stats.gems || 0
|
||||||
|
});
|
||||||
|
socket.emit('purchaseCompleted', { success: false, error: 'Not enough gems' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if already owns this cosmetic
|
||||||
|
if (item.type === 'cosmetic' && playerData.ownedCosmetics && playerData.ownedCosmetics.includes(item.id)) {
|
||||||
|
console.log('[GAME SERVER] Purchase failed - already owns cosmetic:', item.id);
|
||||||
|
socket.emit('purchaseCompleted', { success: false, error: 'You already own this cosmetic' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process payment
|
||||||
|
console.log('[GAME SERVER] Processing payment...');
|
||||||
|
if (currency === 'credits') {
|
||||||
|
playerData.stats.credits = (playerData.stats.credits || 0) - totalCost;
|
||||||
|
} else if (currency === 'gems') {
|
||||||
|
playerData.stats.gems = (playerData.stats.gems || 0) - totalCost;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[GAME SERVER] Payment processed, new balance:', {
|
||||||
|
credits: playerData.stats.credits,
|
||||||
|
gems: playerData.stats.gems
|
||||||
|
});
|
||||||
|
|
||||||
|
// Give item based on type
|
||||||
|
console.log('[GAME SERVER] Adding item to inventory, type:', item.type);
|
||||||
|
switch (item.type) {
|
||||||
|
case 'ship':
|
||||||
|
if (!playerData.ownedShips) playerData.ownedShips = [];
|
||||||
|
if (!playerData.ownedShips.includes(item.id)) {
|
||||||
|
playerData.ownedShips.push(item.id);
|
||||||
|
console.log('[GAME SERVER] Ship added to owned ships:', item.id);
|
||||||
|
} else {
|
||||||
|
console.log('[GAME SERVER] Ship already owned:', item.id);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'cosmetic':
|
||||||
|
if (!playerData.ownedCosmetics) playerData.ownedCosmetics = [];
|
||||||
|
if (!playerData.ownedCosmetics.includes(item.id)) {
|
||||||
|
playerData.ownedCosmetics.push(item.id);
|
||||||
|
console.log('[GAME SERVER] Cosmetic added to owned cosmetics:', item.id);
|
||||||
|
} else {
|
||||||
|
console.log('[GAME SERVER] Cosmetic already owned:', item.id);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'consumable':
|
||||||
|
if (!playerData.inventory) playerData.inventory = { items: [] };
|
||||||
|
playerData.inventory.items.push({
|
||||||
|
id: item.id,
|
||||||
|
name: item.name,
|
||||||
|
type: item.type,
|
||||||
|
quantity: quantity,
|
||||||
|
acquired: new Date().toISOString()
|
||||||
|
});
|
||||||
|
console.log('[GAME SERVER] Consumable added to inventory:', item.name);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'material':
|
||||||
|
if (!playerData.inventory) playerData.inventory = { items: [] };
|
||||||
|
playerData.inventory.items.push({
|
||||||
|
id: item.id,
|
||||||
|
name: item.name,
|
||||||
|
type: item.type,
|
||||||
|
quantity: quantity,
|
||||||
|
acquired: new Date().toISOString()
|
||||||
|
});
|
||||||
|
console.log('[GAME SERVER] Material added to inventory:', item.name);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
console.warn(`[GAME SERVER] Unknown item type: ${item.type}`);
|
||||||
|
socket.emit('purchaseCompleted', { success: false, error: 'Unknown item type' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save updated player data
|
||||||
|
console.log('[GAME SERVER] Saving player data...');
|
||||||
|
await savePlayerData(clientData.userId, playerData);
|
||||||
|
|
||||||
|
// Update client data with new values
|
||||||
|
clientData.playerData = playerData;
|
||||||
|
|
||||||
|
// Send success response
|
||||||
|
const response = {
|
||||||
|
success: true,
|
||||||
|
item: item,
|
||||||
|
quantity: quantity,
|
||||||
|
totalCost: totalCost,
|
||||||
|
currency: currency,
|
||||||
|
newBalance: currency === 'credits' ? playerData.stats.credits : playerData.stats.gems
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('[GAME SERVER] Sending success response:', response);
|
||||||
|
socket.emit('purchaseCompleted', response);
|
||||||
|
|
||||||
|
console.log(`[GAME SERVER] Purchase completed for ${clientData.username}: ${item.name} x${quantity} for ${totalCost} ${currency}`);
|
||||||
|
|
||||||
|
// Broadcast economy update to client
|
||||||
|
broadcastEconomyUpdate(socket.id);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[GAME SERVER] Error processing purchase:', error);
|
||||||
|
socket.emit('purchaseCompleted', { success: false, error: 'Purchase failed: ' + error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Dungeon System Packet Handlers
|
||||||
|
socket.on('get_dungeons', () => {
|
||||||
|
console.log('[GAME SERVER] Sending dungeons data to:', socket.id);
|
||||||
|
const dungeons = dungeonSystem.getDungeonsGroupedByDifficulty();
|
||||||
|
socket.emit('dungeons_data', { dungeons });
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('get_room_types', () => {
|
||||||
|
console.log('[GAME SERVER] Sending room types to:', socket.id);
|
||||||
|
const roomTypes = dungeonSystem.getRoomTypes();
|
||||||
|
socket.emit('room_types_data', roomTypes);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('get_enemy_templates', () => {
|
||||||
|
console.log('[GAME SERVER] Sending enemy templates to:', socket.id);
|
||||||
|
const enemyTemplates = dungeonSystem.getEnemyTemplates();
|
||||||
|
socket.emit('enemy_templates_data', enemyTemplates);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Economy System Packet Handlers
|
||||||
|
socket.on('get_economy_data', () => {
|
||||||
|
console.log('[GAME SERVER] Sending economy data to:', socket.id);
|
||||||
|
const clientData = connectedClients.get(socket.id);
|
||||||
|
if (clientData && clientData.playerData) {
|
||||||
|
const economyData = {
|
||||||
|
credits: clientData.playerData.stats.credits || 0,
|
||||||
|
gems: clientData.playerData.stats.gems || 0
|
||||||
|
};
|
||||||
|
socket.emit('economy_data', economyData);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Function to broadcast economy updates to specific client
|
||||||
|
function broadcastEconomyUpdate(socketId) {
|
||||||
|
const clientData = connectedClients.get(socketId);
|
||||||
|
if (clientData && clientData.playerData) {
|
||||||
|
const economyData = {
|
||||||
|
credits: clientData.playerData.stats.credits || 0,
|
||||||
|
gems: clientData.playerData.stats.gems || 0
|
||||||
|
};
|
||||||
|
io.to(socketId).emit('economy_data', economyData);
|
||||||
|
console.log('[GAME SERVER] Broadcasted economy update to:', socketId, economyData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
socket.on('start_dungeon', (data) => {
|
||||||
|
console.log('[GAME SERVER] Starting dungeon for:', socket.id, data);
|
||||||
|
try {
|
||||||
|
const { dungeonId, userId } = data;
|
||||||
|
const instance = dungeonSystem.createInstance(dungeonId, userId, []);
|
||||||
|
socket.emit('dungeon_started', { instance });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[GAME SERVER] Error starting dungeon:', error);
|
||||||
|
socket.emit('dungeon_started', { success: false, error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('process_encounter', (data) => {
|
||||||
|
console.log('[GAME SERVER] Processing encounter for:', socket.id, data);
|
||||||
|
try {
|
||||||
|
const { instanceId, userId } = data;
|
||||||
|
const encounter = dungeonSystem.startEncounter(instanceId, userId);
|
||||||
|
socket.emit('encounter_data', { encounter });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[GAME SERVER] Error processing encounter:', error);
|
||||||
|
socket.emit('encounter_data', { success: false, error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('complete_dungeon', (data) => {
|
||||||
|
console.log('[GAME SERVER] Completing dungeon for:', socket.id, data);
|
||||||
|
try {
|
||||||
|
const { instanceId, userId } = data;
|
||||||
|
const result = dungeonSystem.completeDungeon(instanceId);
|
||||||
|
socket.emit('dungeon_completed', { rewards: result });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[GAME SERVER] Error completing dungeon:', error);
|
||||||
|
socket.emit('dungeon_completed', { success: false, error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('get_dungeon_status', (data) => {
|
||||||
|
console.log('[GAME SERVER] Getting dungeon status for:', socket.id, data);
|
||||||
|
try {
|
||||||
|
const { userId } = data;
|
||||||
|
const instance = dungeonSystem.getPlayerInstance(userId);
|
||||||
|
if (instance) {
|
||||||
|
socket.emit('dungeon_status', {
|
||||||
|
hasActiveDungeon: true,
|
||||||
|
currentInstance: instance
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
socket.emit('dungeon_status', {
|
||||||
|
hasActiveDungeon: false,
|
||||||
|
currentInstance: null
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[GAME SERVER] Error getting dungeon status:', error);
|
||||||
|
socket.emit('dungeon_status', { success: false, error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('disconnect', async () => {
|
||||||
console.log('[GAME SERVER] Client disconnected:', socket.id);
|
console.log('[GAME SERVER] Client disconnected:', socket.id);
|
||||||
|
|
||||||
const clientData = connectedClients.get(socket.id);
|
const clientData = connectedClients.get(socket.id);
|
||||||
if (clientData && clientData.instanceId) {
|
if (clientData) {
|
||||||
|
// Save player data before disconnect
|
||||||
|
if (clientData.playerData && clientData.userId) {
|
||||||
|
try {
|
||||||
|
clientData.playerData.leaveServer();
|
||||||
|
await savePlayerData(clientData.userId, clientData.playerData);
|
||||||
|
console.log(`[GAME SERVER] Saved data for ${clientData.username} on disconnect`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[GAME SERVER] Error saving data on disconnect:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle game instance cleanup
|
||||||
|
if (clientData.instanceId) {
|
||||||
const instance = gameInstances.get(clientData.instanceId);
|
const instance = gameInstances.get(clientData.instanceId);
|
||||||
if (instance) {
|
if (instance) {
|
||||||
instance.players.delete(socket.id);
|
instance.players.delete(socket.id);
|
||||||
@ -247,6 +859,7 @@ io.on('connection', (socket) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
connectedClients.delete(socket.id);
|
connectedClients.delete(socket.id);
|
||||||
|
|
||||||
@ -255,6 +868,214 @@ io.on('connection', (socket) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Load player data from database
|
||||||
|
async function loadPlayerData(userId, username) {
|
||||||
|
try {
|
||||||
|
// Check if database is connected
|
||||||
|
if (mongoose.connection.readyState !== 1) {
|
||||||
|
console.error('[GAME SERVER] Database not connected, cannot load player data');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let playerData = await PlayerData.findOne({ userId });
|
||||||
|
|
||||||
|
if (!playerData) {
|
||||||
|
// Create new player data with initialized systems
|
||||||
|
console.log(`[GAME SERVER] Creating new player data for ${username} with system initialization`);
|
||||||
|
|
||||||
|
// Initialize player data in all systems
|
||||||
|
const questData = questSystem.initializePlayerData(userId);
|
||||||
|
const skillData = skillSystem.initializePlayerData(userId);
|
||||||
|
const craftingData = craftingSystem.initializePlayerData(userId);
|
||||||
|
|
||||||
|
// For dungeons, initialize with available dungeons
|
||||||
|
const availableDungeons = dungeonSystem.getAllDungeons();
|
||||||
|
const dungeonData = {
|
||||||
|
availableDungeons: availableDungeons.map(d => ({
|
||||||
|
id: d.id,
|
||||||
|
name: d.name,
|
||||||
|
difficulty: d.difficulty,
|
||||||
|
minLevel: d.minLevel || 1,
|
||||||
|
maxPlayers: d.maxPlayers || 4,
|
||||||
|
description: d.description
|
||||||
|
})),
|
||||||
|
completedDungeons: [],
|
||||||
|
currentInstance: null,
|
||||||
|
dungeonProgress: {}
|
||||||
|
};
|
||||||
|
|
||||||
|
playerData = new PlayerData({
|
||||||
|
userId,
|
||||||
|
username,
|
||||||
|
stats: {
|
||||||
|
level: 1,
|
||||||
|
experience: 0,
|
||||||
|
totalExperience: 0,
|
||||||
|
credits: 1000,
|
||||||
|
gems: 50, // Give some starting gems
|
||||||
|
skillPoints: 0,
|
||||||
|
totalKills: 0,
|
||||||
|
dungeonsCleared: 0,
|
||||||
|
questsCompleted: 0,
|
||||||
|
playTime: 0,
|
||||||
|
lastLogin: new Date()
|
||||||
|
},
|
||||||
|
// Initialize with system data
|
||||||
|
quests: {
|
||||||
|
active: Array.from(questData.activeQuests.values()),
|
||||||
|
completed: Array.from(questData.completedQuests.keys())
|
||||||
|
},
|
||||||
|
skills: skillData,
|
||||||
|
dungeonSystem: dungeonData,
|
||||||
|
crafting: craftingData
|
||||||
|
});
|
||||||
|
await playerData.save();
|
||||||
|
console.log(`[GAME SERVER] Created new player data for ${username} with ${questData.activeQuests.size} active quests`);
|
||||||
|
} else {
|
||||||
|
// Auto-migration for existing players - check for missing data
|
||||||
|
console.log(`[GAME SERVER] Loading existing player data for ${username} (Level ${playerData.stats.level})`);
|
||||||
|
|
||||||
|
let migrated = false;
|
||||||
|
let migrationLog = [];
|
||||||
|
|
||||||
|
// Always categorize quests to ensure proper format
|
||||||
|
console.log(`[GAME SERVER] Categorizing quests for ${username}`);
|
||||||
|
console.log(`[GAME SERVER] Current playerData.quests before categorization:`, playerData.quests);
|
||||||
|
|
||||||
|
const questData = questSystem.getPlayerData(userId);
|
||||||
|
console.log(`[GAME SERVER] QuestSystem data:`, {
|
||||||
|
activeQuestsCount: questData.activeQuests.size,
|
||||||
|
completedQuestsCount: questData.completedQuests.size,
|
||||||
|
activeQuests: Array.from(questData.activeQuests.values())
|
||||||
|
});
|
||||||
|
|
||||||
|
// Categorize quests by type for client
|
||||||
|
const activeQuests = Array.from(questData.activeQuests.values());
|
||||||
|
const mainQuests = activeQuests.filter(quest => quest.type === 'main');
|
||||||
|
const dailyQuests = activeQuests.filter(quest => quest.type === 'daily');
|
||||||
|
const weeklyQuests = activeQuests.filter(quest => quest.type === 'weekly');
|
||||||
|
const tutorialQuests = activeQuests.filter(quest => quest.type === 'tutorial');
|
||||||
|
|
||||||
|
playerData.quests = {
|
||||||
|
main: mainQuests,
|
||||||
|
daily: dailyQuests,
|
||||||
|
weekly: weeklyQuests,
|
||||||
|
tutorial: tutorialQuests,
|
||||||
|
active: activeQuests,
|
||||||
|
completed: Array.from(questData.completedQuests.keys())
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log(`[GAME SERVER] Quest categorization complete:`, {
|
||||||
|
main: mainQuests.length,
|
||||||
|
daily: dailyQuests.length,
|
||||||
|
weekly: weeklyQuests.length,
|
||||||
|
tutorial: tutorialQuests.length,
|
||||||
|
total: activeQuests.length
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`[GAME SERVER] Final playerData.quests after categorization:`, playerData.quests);
|
||||||
|
|
||||||
|
// IMPORTANT: Save the updated quest data to database
|
||||||
|
try {
|
||||||
|
await savePlayerData(userId, playerData);
|
||||||
|
console.log(`[GAME SERVER] Saved categorized quest data for ${username}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[GAME SERVER] Failed to save quest data for ${username}:`, error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Migrate skills if missing or empty
|
||||||
|
if (!playerData.skills || Object.keys(playerData.skills).length === 0) {
|
||||||
|
console.log(`[GAME SERVER] Migrating skills for ${username}`);
|
||||||
|
const skillData = skillSystem.initializePlayerData(userId);
|
||||||
|
playerData.skills = skillData;
|
||||||
|
migrated = true;
|
||||||
|
migrationLog.push('Added skill data');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Migrate dungeons if missing or empty
|
||||||
|
if (!playerData.dungeonSystem || !playerData.dungeonSystem.availableDungeons ||
|
||||||
|
(playerData.dungeonSystem.availableDungeons && playerData.dungeonSystem.availableDungeons.length === 0)) {
|
||||||
|
console.log(`[GAME SERVER] Migrating dungeons for ${username}`);
|
||||||
|
const availableDungeons = dungeonSystem.getAllDungeons();
|
||||||
|
playerData.dungeonSystem = {
|
||||||
|
availableDungeons: availableDungeons.map(d => ({
|
||||||
|
id: d.id,
|
||||||
|
name: d.name,
|
||||||
|
difficulty: d.difficulty,
|
||||||
|
minLevel: d.minLevel || 1,
|
||||||
|
maxPlayers: d.maxPlayers || 4,
|
||||||
|
description: d.description
|
||||||
|
})),
|
||||||
|
completedDungeons: [],
|
||||||
|
currentInstance: null,
|
||||||
|
dungeonProgress: {}
|
||||||
|
};
|
||||||
|
migrated = true;
|
||||||
|
migrationLog.push(`Added ${availableDungeons.length} available dungeons`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Migrate crafting if missing or empty (this field might not exist at all)
|
||||||
|
if (!playerData.crafting || Object.keys(playerData.crafting).length === 0) {
|
||||||
|
console.log(`[GAME SERVER] Migrating crafting for ${username}`);
|
||||||
|
const craftingData = craftingSystem.initializePlayerData(userId);
|
||||||
|
playerData.crafting = craftingData;
|
||||||
|
migrated = true;
|
||||||
|
migrationLog.push('Added crafting data');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize idle system data
|
||||||
|
const idleData = idleSystem.initializePlayerData(userId);
|
||||||
|
if (!playerData.idleSystem) {
|
||||||
|
playerData.idleSystem = {
|
||||||
|
lastActive: new Date().toISOString(),
|
||||||
|
totalOfflineTime: 0,
|
||||||
|
totalIdleCredits: 0
|
||||||
|
};
|
||||||
|
migrated = true;
|
||||||
|
migrationLog.push('Added idle system data');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save if migration occurred
|
||||||
|
if (migrated) {
|
||||||
|
await playerData.save();
|
||||||
|
console.log(`[GAME SERVER] Migration completed for ${username}: ${migrationLog.join(', ')}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update existing player login info
|
||||||
|
playerData.username = username;
|
||||||
|
playerData.lastLogin = new Date();
|
||||||
|
await playerData.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
return playerData;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[GAME SERVER] Error loading player data:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save player data to database
|
||||||
|
async function savePlayerData(userId, playerData) {
|
||||||
|
try {
|
||||||
|
// Check if database is connected
|
||||||
|
if (mongoose.connection.readyState !== 1) {
|
||||||
|
console.error('[GAME SERVER] Database not connected, cannot save player data');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
await PlayerData.findOneAndUpdate(
|
||||||
|
{ userId },
|
||||||
|
playerData,
|
||||||
|
{ upsert: true, new: true }
|
||||||
|
);
|
||||||
|
console.log(`[GAME SERVER] Saved player data for ${playerData.username}`);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[GAME SERVER] Error saving player data:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Update player count on API server
|
// Update player count on API server
|
||||||
async function updatePlayerCountOnAPI() {
|
async function updatePlayerCountOnAPI() {
|
||||||
try {
|
try {
|
||||||
@ -290,9 +1111,13 @@ const PORT = process.env.PORT || 3002;
|
|||||||
|
|
||||||
async function startServer() {
|
async function startServer() {
|
||||||
try {
|
try {
|
||||||
|
console.log('[GAME SERVER] Attempting to connect to database...');
|
||||||
|
|
||||||
// Connect to database
|
// Connect to database
|
||||||
await connectDB();
|
await connectDB();
|
||||||
|
|
||||||
|
console.log('[GAME SERVER] Database connection established');
|
||||||
|
|
||||||
server.listen(PORT, async () => {
|
server.listen(PORT, async () => {
|
||||||
console.log(`[GAME SERVER] Game server running on port ${PORT}`);
|
console.log(`[GAME SERVER] Game server running on port ${PORT}`);
|
||||||
console.log(`[GAME SERVER] Socket.IO ready for multiplayer connections`);
|
console.log(`[GAME SERVER] Socket.IO ready for multiplayer connections`);
|
||||||
@ -302,6 +1127,43 @@ async function startServer() {
|
|||||||
|
|
||||||
// Start periodic player count updates
|
// Start periodic player count updates
|
||||||
setInterval(updatePlayerCountOnAPI, 30000); // Update every 30 seconds
|
setInterval(updatePlayerCountOnAPI, 30000); // Update every 30 seconds
|
||||||
|
|
||||||
|
// Start online idle rewards generation (every 10 seconds)
|
||||||
|
setInterval(async () => {
|
||||||
|
for (const [socketId, clientData] of connectedClients.entries()) {
|
||||||
|
if (clientData.userId && clientData.playerData) {
|
||||||
|
try {
|
||||||
|
// Update playTime for active players
|
||||||
|
const sessionTime = clientData.playerData.updatePlayTime();
|
||||||
|
console.log(`[GAME SERVER] Updated playTime for ${clientData.username}: +${sessionTime}ms, Total: ${clientData.playerData.stats.playTime}ms`);
|
||||||
|
|
||||||
|
// Send playTime update to client
|
||||||
|
io.to(socketId).emit('playTimeUpdated', {
|
||||||
|
playTime: clientData.playerData.stats.playTime,
|
||||||
|
sessionTime: sessionTime
|
||||||
|
});
|
||||||
|
|
||||||
|
const onlineRewards = idleSystem.generateOnlineIdleRewards(clientData.userId, 10000); // 10 seconds
|
||||||
|
|
||||||
|
if (onlineRewards.credits > 0) {
|
||||||
|
// Update player data with online rewards
|
||||||
|
clientData.playerData.stats.credits = (clientData.playerData.stats.credits || 0) + onlineRewards.credits;
|
||||||
|
clientData.playerData.stats.experience = (clientData.playerData.stats.experience || 0) + onlineRewards.experience;
|
||||||
|
|
||||||
|
// Send update to client
|
||||||
|
io.to(socketId).emit('onlineIdleRewards', {
|
||||||
|
credits: onlineRewards.credits,
|
||||||
|
experience: onlineRewards.experience,
|
||||||
|
newBalance: clientData.playerData.stats.credits,
|
||||||
|
playTime: clientData.playerData.stats.playTime
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[GAME SERVER] Error generating online idle rewards for ${socketId}:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 10000); // Every 10 seconds
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[GAME SERVER] Failed to start server:', error);
|
console.error('[GAME SERVER] Failed to start server:', error);
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -9,12 +9,20 @@ class IdleSystem {
|
|||||||
this.maxOfflineTime = 7 * 24 * 60 * 60 * 1000; // 7 days in milliseconds
|
this.maxOfflineTime = 7 * 24 * 60 * 60 * 1000; // 7 days in milliseconds
|
||||||
this.playerLastActive = new Map(); // userId -> last active timestamp
|
this.playerLastActive = new Map(); // userId -> last active timestamp
|
||||||
this.playerProductionRates = new Map(); // userId -> production rates
|
this.playerProductionRates = new Map(); // userId -> production rates
|
||||||
|
this.playerAchievements = new Map(); // userId -> achievements
|
||||||
|
|
||||||
// Default production rates
|
// Default production rates
|
||||||
this.defaultProductionRates = {
|
this.defaultProductionRates = {
|
||||||
credits: 10, // credits per second (increased for better gameplay)
|
credits: 0.1, // 1 credit every 10 seconds (0.1 per second)
|
||||||
experience: 1, // experience per second (increased for better progression)
|
experience: 0, // no auto experience - only from dungeons
|
||||||
energy: 0.5 // energy regeneration per second
|
energy: 1/300 // 1 energy every 5 minutes (1/300 per second)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Offline rates (different from online rates)
|
||||||
|
this.offlineProductionRates = {
|
||||||
|
credits: 1/60, // 1 credit every 1 minute (1/60 per second)
|
||||||
|
experience: 0, // no experience offline - only from dungeons
|
||||||
|
energy: 1/300 // 1 energy every 5 minutes (same as online)
|
||||||
};
|
};
|
||||||
|
|
||||||
// Idle rewards
|
// Idle rewards
|
||||||
@ -90,12 +98,13 @@ class IdleSystem {
|
|||||||
const productionRates = this.playerProductionRates.get(userId);
|
const productionRates = this.playerProductionRates.get(userId);
|
||||||
const achievements = this.playerAchievements.get(userId);
|
const achievements = this.playerAchievements.get(userId);
|
||||||
|
|
||||||
// Calculate offline rewards
|
// Calculate offline rewards using offline rates
|
||||||
const offlineSeconds = Math.floor(cappedOfflineTime / 1000);
|
const offlineSeconds = Math.floor(cappedOfflineTime / 1000);
|
||||||
|
const offlineRates = this.offlineProductionRates;
|
||||||
const rewards = {
|
const rewards = {
|
||||||
credits: Math.floor(productionRates.credits * offlineSeconds),
|
credits: Math.floor(offlineRates.credits * offlineSeconds),
|
||||||
experience: Math.floor(productionRates.experience * offlineSeconds),
|
experience: Math.floor(offlineRates.experience * offlineSeconds),
|
||||||
energy: Math.floor(productionRates.energy * offlineSeconds),
|
energy: Math.floor(offlineRates.energy * offlineSeconds),
|
||||||
items: []
|
items: []
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -239,6 +248,27 @@ class IdleSystem {
|
|||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Generate online idle rewards (called periodically while player is online)
|
||||||
|
generateOnlineIdleRewards(userId, deltaTimeMs) {
|
||||||
|
const productionRates = this.playerProductionRates.get(userId);
|
||||||
|
if (!productionRates) {
|
||||||
|
return {
|
||||||
|
credits: 0,
|
||||||
|
experience: 0,
|
||||||
|
energy: 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const deltaTimeSeconds = deltaTimeMs / 1000;
|
||||||
|
const rewards = {
|
||||||
|
credits: Math.floor(productionRates.credits * deltaTimeSeconds),
|
||||||
|
experience: Math.floor(productionRates.experience * deltaTimeSeconds),
|
||||||
|
energy: Math.floor(productionRates.energy * deltaTimeSeconds)
|
||||||
|
};
|
||||||
|
|
||||||
|
return rewards;
|
||||||
|
}
|
||||||
|
|
||||||
// Reset idle data for a user
|
// Reset idle data for a user
|
||||||
resetPlayerData(userId) {
|
resetPlayerData(userId) {
|
||||||
this.playerLastActive.delete(userId);
|
this.playerLastActive.delete(userId);
|
||||||
|
|||||||
618
GameServer/systems/ItemSystem.js
Normal file
618
GameServer/systems/ItemSystem.js
Normal file
@ -0,0 +1,618 @@
|
|||||||
|
/**
|
||||||
|
* Galaxy Strike Online - Item System
|
||||||
|
* Centralized item management for shop, inventory, dungeons, and all game systems
|
||||||
|
*/
|
||||||
|
|
||||||
|
class ItemSystem {
|
||||||
|
constructor() {
|
||||||
|
// Master item catalog - single source of truth
|
||||||
|
this.itemCatalog = {
|
||||||
|
// Ships
|
||||||
|
ships: [
|
||||||
|
{
|
||||||
|
id: 'starter_cruiser_common',
|
||||||
|
name: 'Starter Cruiser',
|
||||||
|
type: 'ship',
|
||||||
|
rarity: 'common',
|
||||||
|
price: 5000,
|
||||||
|
currency: 'credits',
|
||||||
|
description: 'Reliable starter cruiser for new pilots',
|
||||||
|
texture: 'assets/textures/ships/starter_cruiser.png',
|
||||||
|
stats: { attack: 15, speed: 10, defense: 12, hull: 100 },
|
||||||
|
categories: ['shop', 'dungeon_reward'],
|
||||||
|
requirements: { level: 1 },
|
||||||
|
stackable: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'starter_cruiser_uncommon',
|
||||||
|
name: 'Starter Cruiser II',
|
||||||
|
type: 'ship',
|
||||||
|
rarity: 'uncommon',
|
||||||
|
price: 12000,
|
||||||
|
currency: 'credits',
|
||||||
|
description: 'Upgraded starter cruiser with enhanced systems',
|
||||||
|
texture: 'assets/textures/ships/starter_cruiser.png',
|
||||||
|
stats: { attack: 18, speed: 12, defense: 15, hull: 120 },
|
||||||
|
categories: ['shop', 'dungeon_reward'],
|
||||||
|
requirements: { level: 5 },
|
||||||
|
stackable: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'starter_cruiser_rare',
|
||||||
|
name: 'Starter Cruiser III',
|
||||||
|
type: 'ship',
|
||||||
|
rarity: 'rare',
|
||||||
|
price: 25000,
|
||||||
|
currency: 'credits',
|
||||||
|
description: 'Elite starter cruiser with maximum upgrades',
|
||||||
|
texture: 'assets/textures/ships/starter_cruiser.png',
|
||||||
|
stats: { attack: 22, speed: 15, defense: 18, hull: 150 },
|
||||||
|
categories: ['shop', 'dungeon_reward'],
|
||||||
|
requirements: { level: 10 },
|
||||||
|
stackable: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'interceptor_common',
|
||||||
|
name: 'Interceptor',
|
||||||
|
type: 'ship',
|
||||||
|
rarity: 'common',
|
||||||
|
price: 8000,
|
||||||
|
currency: 'credits',
|
||||||
|
description: 'Fast attack ship for hit-and-run tactics',
|
||||||
|
texture: 'assets/textures/ships/interceptor.png',
|
||||||
|
stats: { attack: 12, speed: 18, defense: 8, hull: 80 },
|
||||||
|
categories: ['shop', 'dungeon_reward'],
|
||||||
|
requirements: { level: 3 },
|
||||||
|
stackable: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'interceptor_uncommon',
|
||||||
|
name: 'Interceptor II',
|
||||||
|
type: 'ship',
|
||||||
|
rarity: 'uncommon',
|
||||||
|
price: 18000,
|
||||||
|
currency: 'credits',
|
||||||
|
description: 'Enhanced interceptor with improved weapons',
|
||||||
|
texture: 'assets/textures/ships/interceptor.png',
|
||||||
|
stats: { attack: 15, speed: 22, defense: 10, hull: 95 },
|
||||||
|
categories: ['shop', 'dungeon_reward'],
|
||||||
|
requirements: { level: 7 },
|
||||||
|
stackable: false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
// Materials
|
||||||
|
materials: [
|
||||||
|
{
|
||||||
|
id: 'steel_plating',
|
||||||
|
name: 'Steel Plating',
|
||||||
|
type: 'material',
|
||||||
|
rarity: 'common',
|
||||||
|
price: 100,
|
||||||
|
currency: 'credits',
|
||||||
|
description: 'Basic armor material for ship upgrades',
|
||||||
|
categories: ['shop', 'dungeon_loot', 'crafting'],
|
||||||
|
requirements: { level: 1 },
|
||||||
|
stackable: true,
|
||||||
|
maxStack: 99,
|
||||||
|
effects: { defense: 5 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'energy_core',
|
||||||
|
name: 'Energy Core',
|
||||||
|
type: 'material',
|
||||||
|
rarity: 'uncommon',
|
||||||
|
price: 250,
|
||||||
|
currency: 'credits',
|
||||||
|
description: 'Power source for advanced upgrades',
|
||||||
|
categories: ['shop', 'dungeon_loot', 'crafting'],
|
||||||
|
requirements: { level: 5 },
|
||||||
|
stackable: true,
|
||||||
|
maxStack: 50,
|
||||||
|
effects: { energy: 10 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'quantum_fuel',
|
||||||
|
name: 'Quantum Fuel',
|
||||||
|
type: 'material',
|
||||||
|
rarity: 'rare',
|
||||||
|
price: 500,
|
||||||
|
currency: 'credits',
|
||||||
|
description: 'Advanced fuel for long-range travel',
|
||||||
|
categories: ['shop', 'dungeon_loot', 'crafting'],
|
||||||
|
requirements: { level: 10 },
|
||||||
|
stackable: true,
|
||||||
|
maxStack: 25,
|
||||||
|
effects: { speed: 15 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'dark_matter',
|
||||||
|
name: 'Dark Matter',
|
||||||
|
type: 'material',
|
||||||
|
rarity: 'legendary',
|
||||||
|
price: 2000,
|
||||||
|
currency: 'gems',
|
||||||
|
description: 'Exotic matter for ultimate upgrades',
|
||||||
|
categories: ['shop', 'dungeon_loot', 'crafting'],
|
||||||
|
requirements: { level: 15 },
|
||||||
|
stackable: true,
|
||||||
|
maxStack: 10,
|
||||||
|
effects: { attack: 20, defense: 20 }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
// Consumables
|
||||||
|
consumables: [
|
||||||
|
{
|
||||||
|
id: 'repair_kit',
|
||||||
|
name: 'Repair Kit',
|
||||||
|
type: 'consumable',
|
||||||
|
rarity: 'common',
|
||||||
|
price: 50,
|
||||||
|
currency: 'credits',
|
||||||
|
description: 'Instantly repairs ship damage',
|
||||||
|
categories: ['shop', 'dungeon_loot'],
|
||||||
|
requirements: { level: 1 },
|
||||||
|
stackable: true,
|
||||||
|
maxStack: 20,
|
||||||
|
effects: { hull_repair: 50 },
|
||||||
|
consumable: true,
|
||||||
|
cooldown: 30000 // 30 seconds
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'energy_boost',
|
||||||
|
name: 'Energy Boost',
|
||||||
|
type: 'consumable',
|
||||||
|
rarity: 'common',
|
||||||
|
price: 75,
|
||||||
|
currency: 'credits',
|
||||||
|
description: 'Temporary energy increase',
|
||||||
|
categories: ['shop', 'dungeon_loot'],
|
||||||
|
requirements: { level: 1 },
|
||||||
|
stackable: true,
|
||||||
|
maxStack: 15,
|
||||||
|
effects: { energy_boost: 25, duration: 60000 }, // 1 minute
|
||||||
|
consumable: true,
|
||||||
|
cooldown: 45000 // 45 seconds
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'shield_booster',
|
||||||
|
name: 'Shield Booster',
|
||||||
|
type: 'consumable',
|
||||||
|
rarity: 'uncommon',
|
||||||
|
price: 150,
|
||||||
|
currency: 'credits',
|
||||||
|
description: 'Temporary shield enhancement',
|
||||||
|
categories: ['shop', 'dungeon_loot'],
|
||||||
|
requirements: { level: 5 },
|
||||||
|
stackable: true,
|
||||||
|
maxStack: 10,
|
||||||
|
effects: { shield_boost: 50, duration: 90000 }, // 1.5 minutes
|
||||||
|
consumable: true,
|
||||||
|
cooldown: 60000 // 1 minute
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'damage_amplifier',
|
||||||
|
name: 'Damage Amplifier',
|
||||||
|
type: 'consumable',
|
||||||
|
rarity: 'rare',
|
||||||
|
price: 300,
|
||||||
|
currency: 'credits',
|
||||||
|
description: 'Increases damage output temporarily',
|
||||||
|
categories: ['shop', 'dungeon_loot'],
|
||||||
|
requirements: { level: 8 },
|
||||||
|
stackable: true,
|
||||||
|
maxStack: 5,
|
||||||
|
effects: { damage_multiplier: 1.5, duration: 45000 }, // 45 seconds
|
||||||
|
consumable: true,
|
||||||
|
cooldown: 120000 // 2 minutes
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
// Cosmetics
|
||||||
|
cosmetics: [
|
||||||
|
{
|
||||||
|
id: 'red_paint',
|
||||||
|
name: 'Red Paint Job',
|
||||||
|
type: 'cosmetic',
|
||||||
|
rarity: 'common',
|
||||||
|
price: 500,
|
||||||
|
currency: 'credits',
|
||||||
|
description: 'Red color scheme for your ship',
|
||||||
|
categories: ['shop'],
|
||||||
|
requirements: { level: 1 },
|
||||||
|
stackable: false,
|
||||||
|
cosmetic: true,
|
||||||
|
slot: 'paint'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'blue_paint',
|
||||||
|
name: 'Blue Paint Job',
|
||||||
|
type: 'cosmetic',
|
||||||
|
rarity: 'common',
|
||||||
|
price: 500,
|
||||||
|
currency: 'credits',
|
||||||
|
description: 'Blue color scheme for your ship',
|
||||||
|
categories: ['shop'],
|
||||||
|
requirements: { level: 1 },
|
||||||
|
stackable: false,
|
||||||
|
cosmetic: true,
|
||||||
|
slot: 'paint'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'gold_trim',
|
||||||
|
name: 'Gold Trim',
|
||||||
|
type: 'cosmetic',
|
||||||
|
rarity: 'uncommon',
|
||||||
|
price: 1000,
|
||||||
|
currency: 'credits',
|
||||||
|
description: 'Gold accent trim for your ship',
|
||||||
|
categories: ['shop'],
|
||||||
|
requirements: { level: 5 },
|
||||||
|
stackable: false,
|
||||||
|
cosmetic: true,
|
||||||
|
slot: 'trim'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'rainbow_effect',
|
||||||
|
name: 'Rainbow Engine Effect',
|
||||||
|
type: 'cosmetic',
|
||||||
|
rarity: 'rare',
|
||||||
|
price: 2500,
|
||||||
|
currency: 'credits',
|
||||||
|
description: 'Colorful engine trail effect',
|
||||||
|
categories: ['shop'],
|
||||||
|
requirements: { level: 10 },
|
||||||
|
stackable: false,
|
||||||
|
cosmetic: true,
|
||||||
|
slot: 'engine'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
// Weapons
|
||||||
|
weapons: [
|
||||||
|
{
|
||||||
|
id: 'laser_cannon_common',
|
||||||
|
name: 'Laser Cannon',
|
||||||
|
type: 'weapon',
|
||||||
|
rarity: 'common',
|
||||||
|
price: 1000,
|
||||||
|
currency: 'credits',
|
||||||
|
description: 'Basic laser weapon for beginners',
|
||||||
|
categories: ['shop', 'dungeon_loot'],
|
||||||
|
requirements: { level: 1 },
|
||||||
|
stackable: false,
|
||||||
|
stats: { attack: 10, range: 100, fire_rate: 2 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'plasma_rifle_uncommon',
|
||||||
|
name: 'Plasma Rifle',
|
||||||
|
type: 'weapon',
|
||||||
|
rarity: 'uncommon',
|
||||||
|
price: 2500,
|
||||||
|
currency: 'credits',
|
||||||
|
description: 'Plasma-based weapon with increased damage',
|
||||||
|
categories: ['shop', 'dungeon_loot'],
|
||||||
|
requirements: { level: 5 },
|
||||||
|
stackable: false,
|
||||||
|
stats: { attack: 18, range: 120, fire_rate: 1.5 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'quantum_blaster_rare',
|
||||||
|
name: 'Quantum Blaster',
|
||||||
|
type: 'weapon',
|
||||||
|
rarity: 'rare',
|
||||||
|
price: 6000,
|
||||||
|
currency: 'credits',
|
||||||
|
description: 'Advanced quantum weapon with high damage output',
|
||||||
|
categories: ['shop', 'dungeon_loot'],
|
||||||
|
requirements: { level: 10 },
|
||||||
|
stackable: false,
|
||||||
|
stats: { attack: 30, range: 150, fire_rate: 1 }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
// Armors
|
||||||
|
armors: [
|
||||||
|
{
|
||||||
|
id: 'basic_shield_common',
|
||||||
|
name: 'Basic Shield Generator',
|
||||||
|
type: 'armor',
|
||||||
|
rarity: 'common',
|
||||||
|
price: 800,
|
||||||
|
currency: 'credits',
|
||||||
|
description: 'Basic shield protection for beginners',
|
||||||
|
categories: ['shop', 'dungeon_loot'],
|
||||||
|
requirements: { level: 1 },
|
||||||
|
stackable: false,
|
||||||
|
stats: { defense: 8, shield_capacity: 50, recharge_rate: 5 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'energy_armor_uncommon',
|
||||||
|
name: 'Energy Armor',
|
||||||
|
type: 'armor',
|
||||||
|
rarity: 'uncommon',
|
||||||
|
price: 2000,
|
||||||
|
currency: 'credits',
|
||||||
|
description: 'Energy-based armor with enhanced protection',
|
||||||
|
categories: ['shop', 'dungeon_loot'],
|
||||||
|
requirements: { level: 5 },
|
||||||
|
stackable: false,
|
||||||
|
stats: { defense: 15, shield_capacity: 100, recharge_rate: 8 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'quantum_deflector_rare',
|
||||||
|
name: 'Quantum Deflector',
|
||||||
|
type: 'armor',
|
||||||
|
rarity: 'rare',
|
||||||
|
price: 5000,
|
||||||
|
currency: 'credits',
|
||||||
|
description: 'Advanced quantum armor with maximum protection',
|
||||||
|
categories: ['shop', 'dungeon_loot'],
|
||||||
|
requirements: { level: 10 },
|
||||||
|
stackable: false,
|
||||||
|
stats: { defense: 25, shield_capacity: 200, recharge_rate: 12 }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
// Dungeon-specific rewards
|
||||||
|
dungeon_rewards: [
|
||||||
|
{
|
||||||
|
id: 'dungeon_key_basic',
|
||||||
|
name: 'Basic Dungeon Key',
|
||||||
|
type: 'key',
|
||||||
|
rarity: 'common',
|
||||||
|
price: 200,
|
||||||
|
currency: 'credits',
|
||||||
|
description: 'Key to enter basic dungeons',
|
||||||
|
categories: ['shop', 'dungeon_loot'],
|
||||||
|
requirements: { level: 1 },
|
||||||
|
stackable: true,
|
||||||
|
maxStack: 10,
|
||||||
|
consumable: true,
|
||||||
|
dungeon_access: ['basic_dungeon', 'mines']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'dungeon_key_advanced',
|
||||||
|
name: 'Advanced Dungeon Key',
|
||||||
|
type: 'key',
|
||||||
|
rarity: 'uncommon',
|
||||||
|
price: 800,
|
||||||
|
currency: 'credits',
|
||||||
|
description: 'Key to enter advanced dungeons',
|
||||||
|
categories: ['shop', 'dungeon_loot'],
|
||||||
|
requirements: { level: 8 },
|
||||||
|
stackable: true,
|
||||||
|
maxStack: 5,
|
||||||
|
consumable: true,
|
||||||
|
dungeon_access: ['advanced_dungeon', 'fortress']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'treasure_chest',
|
||||||
|
name: 'Treasure Chest',
|
||||||
|
type: 'container',
|
||||||
|
rarity: 'rare',
|
||||||
|
price: 0,
|
||||||
|
currency: 'credits',
|
||||||
|
description: 'Contains random valuable items',
|
||||||
|
categories: ['dungeon_loot'],
|
||||||
|
requirements: { level: 1 },
|
||||||
|
stackable: true,
|
||||||
|
maxStack: 5,
|
||||||
|
consumable: true,
|
||||||
|
loot_table: 'treasure_chest_common'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initialize item lookup maps for performance
|
||||||
|
this.itemLookup = new Map();
|
||||||
|
this.categoryLookup = new Map();
|
||||||
|
this.rarityLookup = new Map();
|
||||||
|
|
||||||
|
this.buildLookupMaps();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build lookup maps for efficient item retrieval
|
||||||
|
*/
|
||||||
|
buildLookupMaps() {
|
||||||
|
// Build ID lookup
|
||||||
|
for (const [category, items] of Object.entries(this.itemCatalog)) {
|
||||||
|
for (const item of items) {
|
||||||
|
this.itemLookup.set(item.id, item);
|
||||||
|
|
||||||
|
// Build category lookup
|
||||||
|
if (!this.categoryLookup.has(category)) {
|
||||||
|
this.categoryLookup.set(category, []);
|
||||||
|
}
|
||||||
|
this.categoryLookup.get(category).push(item);
|
||||||
|
|
||||||
|
// Build rarity lookup
|
||||||
|
if (!this.rarityLookup.has(item.rarity)) {
|
||||||
|
this.rarityLookup.set(item.rarity, []);
|
||||||
|
}
|
||||||
|
this.rarityLookup.get(item.rarity).push(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get item by ID
|
||||||
|
*/
|
||||||
|
getItem(itemId) {
|
||||||
|
return this.itemLookup.get(itemId) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all items in a category
|
||||||
|
*/
|
||||||
|
getItemsByCategory(category) {
|
||||||
|
return this.categoryLookup.get(category) || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get items by rarity
|
||||||
|
*/
|
||||||
|
getItemsByRarity(rarity) {
|
||||||
|
return this.rarityLookup.get(rarity) || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get items available for specific context (shop, dungeon, etc.)
|
||||||
|
*/
|
||||||
|
getItemsByContext(context) {
|
||||||
|
const results = [];
|
||||||
|
|
||||||
|
for (const item of this.itemLookup.values()) {
|
||||||
|
if (item.categories && item.categories.includes(context)) {
|
||||||
|
results.push(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get shop items (items available in shop)
|
||||||
|
*/
|
||||||
|
getShopItems() {
|
||||||
|
return this.getItemsByContext('shop');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get dungeon loot items
|
||||||
|
*/
|
||||||
|
getDungeonLootItems() {
|
||||||
|
return this.getItemsByContext('dungeon_loot');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get dungeon reward items
|
||||||
|
*/
|
||||||
|
getDungeonRewardItems() {
|
||||||
|
return this.getItemsByContext('dungeon_reward');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get crafting materials
|
||||||
|
*/
|
||||||
|
getCraftingMaterials() {
|
||||||
|
return this.getItemsByContext('crafting');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if player meets item requirements
|
||||||
|
*/
|
||||||
|
canPlayerUseItem(playerData, item) {
|
||||||
|
if (!item.requirements) return true;
|
||||||
|
|
||||||
|
// Check level requirement
|
||||||
|
if (item.requirements.level && playerData.stats.level < item.requirements.level) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add other requirement checks here (skills, quests, etc.)
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get filtered items for player (based on requirements)
|
||||||
|
*/
|
||||||
|
getAvailableItemsForPlayer(playerData, context = null) {
|
||||||
|
let items = context ? this.getItemsByContext(context) : Array.from(this.itemLookup.values());
|
||||||
|
|
||||||
|
return items.filter(item => this.canPlayerUseItem(playerData, item));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate random loot based on rarity weights
|
||||||
|
*/
|
||||||
|
generateLoot(rarityWeights = null, context = null) {
|
||||||
|
const defaultWeights = {
|
||||||
|
common: 50,
|
||||||
|
uncommon: 30,
|
||||||
|
rare: 15,
|
||||||
|
legendary: 5
|
||||||
|
};
|
||||||
|
|
||||||
|
const weights = rarityWeights || defaultWeights;
|
||||||
|
const availableItems = context ? this.getItemsByContext(context) : Array.from(this.itemLookup.values());
|
||||||
|
|
||||||
|
// Filter by rarity weights
|
||||||
|
const weightedItems = [];
|
||||||
|
for (const item of availableItems) {
|
||||||
|
const weight = weights[item.rarity] || 0;
|
||||||
|
for (let i = 0; i < weight; i++) {
|
||||||
|
weightedItems.push(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (weightedItems.length === 0) return null;
|
||||||
|
|
||||||
|
// Random selection
|
||||||
|
const randomIndex = Math.floor(Math.random() * weightedItems.length);
|
||||||
|
return weightedItems[randomIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get item statistics
|
||||||
|
*/
|
||||||
|
getItemStats() {
|
||||||
|
const stats = {
|
||||||
|
totalItems: this.itemLookup.size,
|
||||||
|
byCategory: {},
|
||||||
|
byRarity: {},
|
||||||
|
byType: {}
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const item of this.itemLookup.values()) {
|
||||||
|
// Count by category
|
||||||
|
for (const category of item.categories || []) {
|
||||||
|
stats.byCategory[category] = (stats.byCategory[category] || 0) + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Count by rarity
|
||||||
|
stats.byRarity[item.rarity] = (stats.byRarity[item.rarity] || 0) + 1;
|
||||||
|
|
||||||
|
// Count by type
|
||||||
|
stats.byType[item.type] = (stats.byType[item.type] || 0) + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return stats;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate item data structure
|
||||||
|
*/
|
||||||
|
validateItem(item) {
|
||||||
|
const required = ['id', 'name', 'type', 'rarity'];
|
||||||
|
const missing = required.filter(field => !item[field]);
|
||||||
|
|
||||||
|
if (missing.length > 0) {
|
||||||
|
return { valid: false, errors: [`Missing required fields: ${missing.join(', ')}`] };
|
||||||
|
}
|
||||||
|
|
||||||
|
const errors = [];
|
||||||
|
|
||||||
|
// Validate type
|
||||||
|
if (!['ship', 'material', 'consumable', 'cosmetic', 'key', 'container'].includes(item.type)) {
|
||||||
|
errors.push(`Invalid item type: ${item.type}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate rarity
|
||||||
|
if (!['common', 'uncommon', 'rare', 'legendary'].includes(item.rarity)) {
|
||||||
|
errors.push(`Invalid rarity: ${item.rarity}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate stackable items
|
||||||
|
if (item.stackable && (!item.maxStack || item.maxStack < 1)) {
|
||||||
|
errors.push('Stackable items must have maxStack >= 1');
|
||||||
|
}
|
||||||
|
|
||||||
|
return { valid: errors.length === 0, errors };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = ItemSystem;
|
||||||
@ -30,6 +30,176 @@ class QuestSystem {
|
|||||||
timeLimit: null
|
timeLimit: null
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Main Story quests
|
||||||
|
this.addQuest('main_story_beginning', {
|
||||||
|
name: 'The Beginning',
|
||||||
|
description: 'Start your journey as a space pilot',
|
||||||
|
type: 'main',
|
||||||
|
difficulty: 'easy',
|
||||||
|
requirements: {
|
||||||
|
level: 1,
|
||||||
|
battlesWon: 1
|
||||||
|
},
|
||||||
|
rewards: {
|
||||||
|
experience: 200,
|
||||||
|
credits: 1000,
|
||||||
|
skillPoints: 2
|
||||||
|
},
|
||||||
|
prerequisites: [],
|
||||||
|
repeatable: false,
|
||||||
|
timeLimit: null
|
||||||
|
});
|
||||||
|
|
||||||
|
this.addQuest('main_story_first_dungeon', {
|
||||||
|
name: 'First Dungeon',
|
||||||
|
description: 'Complete your first dungeon run',
|
||||||
|
type: 'main',
|
||||||
|
difficulty: 'medium',
|
||||||
|
requirements: {
|
||||||
|
level: 5,
|
||||||
|
dungeonsCleared: 1
|
||||||
|
},
|
||||||
|
rewards: {
|
||||||
|
experience: 500,
|
||||||
|
credits: 2000,
|
||||||
|
items: ['rare_weapon']
|
||||||
|
},
|
||||||
|
prerequisites: ['main_story_beginning'],
|
||||||
|
repeatable: false,
|
||||||
|
timeLimit: null
|
||||||
|
});
|
||||||
|
|
||||||
|
this.addQuest('main_story_space_exploration', {
|
||||||
|
name: 'Space Explorer',
|
||||||
|
description: 'Explore 10 different sectors',
|
||||||
|
type: 'main',
|
||||||
|
difficulty: 'medium',
|
||||||
|
requirements: {
|
||||||
|
level: 10,
|
||||||
|
sectorsExplored: 10
|
||||||
|
},
|
||||||
|
rewards: {
|
||||||
|
experience: 1000,
|
||||||
|
credits: 5000,
|
||||||
|
items: ['explorer_badge']
|
||||||
|
},
|
||||||
|
prerequisites: ['main_story_first_dungeon'],
|
||||||
|
repeatable: false,
|
||||||
|
timeLimit: null
|
||||||
|
});
|
||||||
|
|
||||||
|
// Daily quests
|
||||||
|
this.addQuest('daily_battles', {
|
||||||
|
name: 'Daily Battles',
|
||||||
|
description: 'Win 5 battles today',
|
||||||
|
type: 'daily',
|
||||||
|
difficulty: 'easy',
|
||||||
|
requirements: {
|
||||||
|
battlesWon: 5
|
||||||
|
},
|
||||||
|
rewards: {
|
||||||
|
experience: 150,
|
||||||
|
credits: 750,
|
||||||
|
gems: 5
|
||||||
|
},
|
||||||
|
prerequisites: [],
|
||||||
|
repeatable: true,
|
||||||
|
timeLimit: 24 * 60 * 60 * 1000 // 24 hours
|
||||||
|
});
|
||||||
|
|
||||||
|
this.addQuest('daily_exploration', {
|
||||||
|
name: 'Daily Exploration',
|
||||||
|
description: 'Explore 3 new sectors today',
|
||||||
|
type: 'daily',
|
||||||
|
difficulty: 'easy',
|
||||||
|
requirements: {
|
||||||
|
sectorsExplored: 3
|
||||||
|
},
|
||||||
|
rewards: {
|
||||||
|
experience: 100,
|
||||||
|
credits: 500,
|
||||||
|
gems: 3
|
||||||
|
},
|
||||||
|
prerequisites: [],
|
||||||
|
repeatable: true,
|
||||||
|
timeLimit: 24 * 60 * 60 * 1000 // 24 hours
|
||||||
|
});
|
||||||
|
|
||||||
|
this.addQuest('daily_resources', {
|
||||||
|
name: 'Daily Resource Collection',
|
||||||
|
description: 'Collect 1000 resources today',
|
||||||
|
type: 'daily',
|
||||||
|
difficulty: 'medium',
|
||||||
|
requirements: {
|
||||||
|
resourcesCollected: 1000
|
||||||
|
},
|
||||||
|
rewards: {
|
||||||
|
experience: 200,
|
||||||
|
credits: 1000,
|
||||||
|
gems: 8
|
||||||
|
},
|
||||||
|
prerequisites: [],
|
||||||
|
repeatable: true,
|
||||||
|
timeLimit: 24 * 60 * 60 * 1000 // 24 hours
|
||||||
|
});
|
||||||
|
|
||||||
|
// Weekly quests
|
||||||
|
this.addQuest('weekly_champion', {
|
||||||
|
name: 'Weekly Champion',
|
||||||
|
description: 'Win 50 battles this week',
|
||||||
|
type: 'weekly',
|
||||||
|
difficulty: 'hard',
|
||||||
|
requirements: {
|
||||||
|
battlesWon: 50
|
||||||
|
},
|
||||||
|
rewards: {
|
||||||
|
experience: 2000,
|
||||||
|
credits: 10000,
|
||||||
|
gems: 50,
|
||||||
|
items: ['champion_title']
|
||||||
|
},
|
||||||
|
prerequisites: [],
|
||||||
|
repeatable: true,
|
||||||
|
timeLimit: 7 * 24 * 60 * 60 * 1000 // 7 days
|
||||||
|
});
|
||||||
|
|
||||||
|
this.addQuest('weekly_dungeon_master', {
|
||||||
|
name: 'Weekly Dungeon Master',
|
||||||
|
description: 'Complete 10 dungeons this week',
|
||||||
|
type: 'weekly',
|
||||||
|
difficulty: 'hard',
|
||||||
|
requirements: {
|
||||||
|
dungeonsCleared: 10
|
||||||
|
},
|
||||||
|
rewards: {
|
||||||
|
experience: 3000,
|
||||||
|
credits: 15000,
|
||||||
|
gems: 75,
|
||||||
|
items: ['dungeon_master_cape']
|
||||||
|
},
|
||||||
|
prerequisites: [],
|
||||||
|
repeatable: true,
|
||||||
|
timeLimit: 7 * 24 * 60 * 60 * 1000 // 7 days
|
||||||
|
});
|
||||||
|
|
||||||
|
this.addQuest('weekly_wealth_collector', {
|
||||||
|
name: 'Weekly Wealth Collector',
|
||||||
|
description: 'Earn 10000 credits this week',
|
||||||
|
type: 'weekly',
|
||||||
|
difficulty: 'medium',
|
||||||
|
requirements: {
|
||||||
|
creditsEarned: 10000
|
||||||
|
},
|
||||||
|
rewards: {
|
||||||
|
experience: 1500,
|
||||||
|
credits: 5000,
|
||||||
|
gems: 25
|
||||||
|
},
|
||||||
|
prerequisites: [],
|
||||||
|
repeatable: true,
|
||||||
|
timeLimit: 7 * 24 * 60 * 60 * 1000 // 7 days
|
||||||
|
});
|
||||||
|
|
||||||
// Combat quests
|
// Combat quests
|
||||||
this.addQuest('warrior_path', {
|
this.addQuest('warrior_path', {
|
||||||
name: 'Warrior Path',
|
name: 'Warrior Path',
|
||||||
@ -152,18 +322,89 @@ class QuestSystem {
|
|||||||
|
|
||||||
initializePlayerData(userId) {
|
initializePlayerData(userId) {
|
||||||
if (!this.playerQuests.has(userId)) {
|
if (!this.playerQuests.has(userId)) {
|
||||||
this.playerQuests.set(userId, {
|
const playerData = {
|
||||||
activeQuests: new Map(),
|
activeQuests: new Map(),
|
||||||
completedQuests: new Map(),
|
completedQuests: new Map(),
|
||||||
questHistory: [],
|
questHistory: [],
|
||||||
totalQuestsCompleted: 0,
|
totalQuestsCompleted: 0,
|
||||||
dailyQuestsCompleted: 0,
|
dailyQuestsCompleted: 0,
|
||||||
weeklyQuestsCompleted: 0
|
weeklyQuestsCompleted: 0
|
||||||
});
|
};
|
||||||
|
|
||||||
|
// Assign starting quests to new players
|
||||||
|
this.assignStartingQuests(userId, playerData);
|
||||||
|
|
||||||
|
this.playerQuests.set(userId, playerData);
|
||||||
}
|
}
|
||||||
return this.playerQuests.get(userId);
|
return this.playerQuests.get(userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
assignStartingQuests(userId, playerData) {
|
||||||
|
console.log(`[QUEST SYSTEM] Assigning starting quests to player ${userId}`);
|
||||||
|
|
||||||
|
// Assign main story quests
|
||||||
|
const mainStoryQuests = ['main_story_beginning', 'main_story_first_dungeon'];
|
||||||
|
mainStoryQuests.forEach(questId => {
|
||||||
|
const quest = this.quests.get(questId);
|
||||||
|
if (quest && !playerData.activeQuests.has(questId) && !playerData.completedQuests.has(questId)) {
|
||||||
|
playerData.activeQuests.set(questId, {
|
||||||
|
...quest,
|
||||||
|
progress: 0,
|
||||||
|
startedAt: Date.now()
|
||||||
|
});
|
||||||
|
console.log(`[QUEST SYSTEM] Assigned main story quest: ${quest.name}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Assign daily quests
|
||||||
|
this.generateDailyQuests(userId, playerData);
|
||||||
|
|
||||||
|
// Assign weekly quests
|
||||||
|
this.generateWeeklyQuests(userId, playerData);
|
||||||
|
|
||||||
|
console.log(`[QUEST SYSTEM] Player now has ${playerData.activeQuests.size} active quests`);
|
||||||
|
}
|
||||||
|
|
||||||
|
generateDailyQuests(userId, playerData) {
|
||||||
|
console.log(`[QUEST SYSTEM] Generating daily quests for player ${userId}`);
|
||||||
|
|
||||||
|
const dailyQuestTemplates = ['daily_battles', 'daily_exploration', 'daily_resources'];
|
||||||
|
|
||||||
|
dailyQuestTemplates.forEach(questId => {
|
||||||
|
const quest = this.quests.get(questId);
|
||||||
|
if (quest) {
|
||||||
|
playerData.activeQuests.set(questId, {
|
||||||
|
...quest,
|
||||||
|
progress: 0,
|
||||||
|
startedAt: Date.now(),
|
||||||
|
type: 'daily',
|
||||||
|
resetTime: Date.now() + (24 * 60 * 60 * 1000) // 24 hours from now
|
||||||
|
});
|
||||||
|
console.log(`[QUEST SYSTEM] Assigned daily quest: ${quest.name}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
generateWeeklyQuests(userId, playerData) {
|
||||||
|
console.log(`[QUEST SYSTEM] Generating weekly quests for player ${userId}`);
|
||||||
|
|
||||||
|
const weeklyQuestTemplates = ['weekly_champion', 'weekly_dungeon_master', 'weekly_wealth_collector'];
|
||||||
|
|
||||||
|
weeklyQuestTemplates.forEach(questId => {
|
||||||
|
const quest = this.quests.get(questId);
|
||||||
|
if (quest) {
|
||||||
|
playerData.activeQuests.set(questId, {
|
||||||
|
...quest,
|
||||||
|
progress: 0,
|
||||||
|
startedAt: Date.now(),
|
||||||
|
type: 'weekly',
|
||||||
|
resetTime: Date.now() + (7 * 24 * 60 * 60 * 1000) // 7 days from now
|
||||||
|
});
|
||||||
|
console.log(`[QUEST SYSTEM] Assigned weekly quest: ${quest.name}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
getPlayerData(userId) {
|
getPlayerData(userId) {
|
||||||
return this.playerQuests.get(userId) || this.initializePlayerData(userId);
|
return this.playerQuests.get(userId) || this.initializePlayerData(userId);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user