178 lines
7.8 KiB
JavaScript
178 lines
7.8 KiB
JavaScript
/**
|
||
* 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 3–24 characters');
|
||
if (!tag || tag.length < 2 || tag.length > 4) throw new Error('Alliance tag must be 2–4 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 };
|