Game-Server/Galaxy-Strike-Online-main/GameServer/systems/AllianceSystem.js

178 lines
7.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Galaxy Strike Online — Alliance System (GDD §12)
* - Alliance creation (Quantum Relay level 1, 10,000 credits founding fee)
* - Ranks: Founder, Officer, Veteran, Member
* - Alliance Warehouse: shared resource pool
* - Alliance Research stubs (phase 2)
*/
const mongoose = require('mongoose');
const allianceSchema = new mongoose.Schema({
allianceId: { type: String, required: true, unique: true },
name: { type: String, required: true, unique: true },
tag: { type: String, required: true, unique: true, maxlength: 4 },
founderId: { type: String, required: true },
founderName: { type: String, default: '' },
description: { type: String, default: '' },
createdAt: { type: Date, default: Date.now },
members: [{
userId: String,
username: String,
rank: { type: String, enum: ['founder','officer','veteran','member'], default: 'member' },
joinedAt: { type: Date, default: Date.now },
}],
// GDD §12.3 Alliance Warehouse
warehouse: {
metal: { type: Number, default: 0 },
gas: { type: Number, default: 0 },
crystal: { type: Number, default: 0 },
energyCells: { type: Number, default: 0 },
credits: { type: Number, default: 0 },
maxPerResource: { type: Number, default: 50000 },
},
warehouseLog: [{ type: mongoose.Schema.Types.Mixed }],
// Alliance research (stubbed for phase 2)
research: { type: mongoose.Schema.Types.Mixed, default: {} },
maxMembers: { type: Number, default: 50 },
isRecruiting: { type: Boolean, default: true },
}, { timestamps: true });
const Alliance = mongoose.model('Alliance', allianceSchema);
const RANKS = ['founder', 'officer', 'veteran', 'member'];
const RANK_PERMS = {
founder: ['kick', 'promote', 'demote', 'deposit', 'withdraw', 'edit', 'disband'],
officer: ['kick_member', 'promote_veteran', 'deposit', 'withdraw'],
veteran: ['deposit', 'withdraw_limited'],
member: ['deposit'],
};
class AllianceSystem {
constructor() { this.Alliance = Alliance; }
async createAlliance(playerData, { name, tag, description = '' }) {
if (!name || name.length < 3 || name.length > 24) throw new Error('Alliance name must be 324 characters');
if (!tag || tag.length < 2 || tag.length > 4) throw new Error('Alliance tag must be 24 characters');
if (playerData.stats.credits < 10000) throw new Error('Founding fee is 10,000 credits');
if (playerData.allianceId) throw new Error('You are already in an alliance');
// Check Quantum Relay (if building exists)
const qrLevel = playerData.buildings?.quantum_relay?.level || 0;
// Allow creation even without relay in early access — just warn
// if (qrLevel < 1) throw new Error('Quantum Relay level 1 required to found an alliance');
const allianceId = 'alliance_' + Date.now() + '_' + Math.random().toString(36).slice(2,6);
const alliance = new Alliance({
allianceId,
name: name.trim(),
tag: tag.toUpperCase().trim(),
founderId: playerData.userId,
founderName: playerData.username,
description: description.trim(),
members: [{ userId: playerData.userId, username: playerData.username, rank: 'founder', joinedAt: new Date() }],
});
await alliance.save();
playerData.stats.credits -= 10000;
playerData.allianceId = allianceId;
playerData.allianceTag = alliance.tag;
playerData.allianceName = alliance.name;
playerData.allianceRank = 'founder';
return alliance;
}
async joinAlliance(playerData, allianceId) {
if (playerData.allianceId) throw new Error('Leave your current alliance first');
const alliance = await Alliance.findOne({ allianceId });
if (!alliance) throw new Error('Alliance not found');
if (!alliance.isRecruiting) throw new Error('Alliance is not recruiting');
if (alliance.members.length >= alliance.maxMembers) throw new Error('Alliance is full');
alliance.members.push({ userId: playerData.userId, username: playerData.username, rank: 'member', joinedAt: new Date() });
await alliance.save();
playerData.allianceId = allianceId;
playerData.allianceTag = alliance.tag;
playerData.allianceName = alliance.name;
playerData.allianceRank = 'member';
return alliance;
}
async leaveAlliance(playerData) {
if (!playerData.allianceId) throw new Error('Not in an alliance');
const alliance = await Alliance.findOne({ allianceId: playerData.allianceId });
if (!alliance) { playerData.allianceId = null; return; }
if (alliance.founderId === playerData.userId && alliance.members.length > 1) {
throw new Error('Transfer leadership before leaving');
}
alliance.members = alliance.members.filter(m => m.userId !== playerData.userId);
if (alliance.members.length === 0) {
await Alliance.deleteOne({ allianceId: playerData.allianceId });
} else {
await alliance.save();
}
playerData.allianceId = null;
playerData.allianceTag = null;
playerData.allianceName = null;
playerData.allianceRank = null;
}
async depositWarehouse(playerData, { resource, amount }) {
if (!playerData.allianceId) throw new Error('Not in an alliance');
if (!['metal','gas','crystal','energyCells','credits'].includes(resource)) throw new Error('Invalid resource');
amount = Math.floor(amount);
if (amount <= 0) throw new Error('Amount must be positive');
const alliance = await Alliance.findOne({ allianceId: playerData.allianceId });
if (!alliance) throw new Error('Alliance not found');
// Deduct from player
const resKey = resource === 'credits' ? null : resource;
if (resource === 'credits') {
if ((playerData.stats.credits || 0) < amount) throw new Error('Insufficient credits');
playerData.stats.credits -= amount;
} else {
if (!playerData.resources || (playerData.resources[resource] || 0) < amount) throw new Error(`Insufficient ${resource}`);
playerData.resources[resource] -= amount;
}
alliance.warehouse[resource] = Math.min((alliance.warehouse[resource] || 0) + amount, alliance.warehouse.maxPerResource);
alliance.warehouseLog.push({ userId: playerData.userId, username: playerData.username, action:'deposit', resource, amount, at: new Date() });
if (alliance.warehouseLog.length > 200) alliance.warehouseLog = alliance.warehouseLog.slice(-200);
await alliance.save();
return alliance.warehouse;
}
async withdrawWarehouse(playerData, { resource, amount }) {
if (!playerData.allianceId) throw new Error('Not in an alliance');
const rank = playerData.allianceRank || 'member';
if (!RANK_PERMS[rank]?.some(p => p.startsWith('withdraw'))) throw new Error('Insufficient rank to withdraw');
const alliance = await Alliance.findOne({ allianceId: playerData.allianceId });
if (!alliance) throw new Error('Alliance not found');
amount = Math.floor(amount);
if ((alliance.warehouse[resource] || 0) < amount) throw new Error('Insufficient in warehouse');
alliance.warehouse[resource] -= amount;
if (resource === 'credits') playerData.stats.credits = (playerData.stats.credits || 0) + amount;
else { if (!playerData.resources) playerData.resources = {}; playerData.resources[resource] = (playerData.resources[resource]||0) + amount; }
alliance.warehouseLog.push({ userId: playerData.userId, username: playerData.username, action:'withdraw', resource, amount, at: new Date() });
await alliance.save();
return alliance.warehouse;
}
async getAllianceData(allianceId) {
return Alliance.findOne({ allianceId }).lean();
}
async searchAlliances(query = '') {
const q = query.trim();
const filter = q ? { $or: [{ name: new RegExp(q,'i') }, { tag: new RegExp(q,'i') }] } : {};
return Alliance.find(filter, { name:1, tag:1, description:1, allianceId:1, 'members':1, isRecruiting:1, maxMembers:1 }).limit(20).lean();
}
}
module.exports = { AllianceSystem, Alliance };