1 import { v4 as uuid } from 'uuid';
2 import { ERROR_CODE, InsufficientResourceError, NotFoundError } from '../errors';
3 import {Repository} from './base';
4 import * as config from '../config';
5 import { BuildQueue, BuildQueueRepository } from './build-queue';
6 import { DateTime, Duration } from 'luxon';
7 import { UnitTrainingQueue, UnitTrainingQueueRepository, UnitTrainingQueueWithName } from './training-queue';
8 import { coalesce, pluck } from '../lib/util';
9 import { Building, BuildingRepository } from './buildings';
10 import { Unit, UnitRepository } from './unit';
11 import _, { random } from 'lodash';
12 import { Army, ArmyQueue, ArmyRepository } from './army';
34 mining_facilities: number;
37 special_attacker_trainer: number;
38 special_defender_trainer: number;
40 max_construction_queue: number;
41 max_training_queue: number;
44 export type CityWithLocation = {
50 export class CityRepository extends Repository<City> {
51 buildQueue: BuildQueueRepository;
52 buildingRepository: BuildingRepository;
53 unitRepository: UnitRepository;
54 unitTrainigQueue: UnitTrainingQueueRepository;
55 armyRepository: ArmyRepository;
59 this.buildingRepository = new BuildingRepository();
60 this.buildQueue = new BuildQueueRepository();
61 this.unitRepository = new UnitRepository();
62 this.unitTrainigQueue = new UnitTrainingQueueRepository();
63 this.armyRepository = new ArmyRepository();
66 async create(accountId: string, rebel: boolean = false): Promise<CityWithLocation> {
67 const icon = rebel ? `/colony-ships/rebels/${random(1, 6)}.png` : '/colony-ships/01.png';
91 special_attacker_trainer: 0,
92 special_defender_trainer: 0,
93 max_construction_queue: 2,
94 max_training_queue: 2,
98 await this.Insert(info);
100 // placement can happen randomly
101 const availableSectors = config.AVAILABLE_SECTORS;
102 const sector = _.random(1, availableSectors);
105 sector_id: await this.getAvailableSector(),
106 location_x: random(0, 25),
107 location_y: random(0, 25)
110 await this.db.raw('insert into locations (sector_id, city_id, location_x, location_y) values (?, ?, ?, ?)', [
119 sector_id: location.sector_id,
120 location_x: location.location_x,
121 location_y: location.location_y
125 async getAvailableSector(): Promise<number> {
126 // figure out which sectors have space (40% fill rate at 25x25);
127 const availableSectors = await this.db.raw<{count: Number, sector_id: number}[]>(`select count(sector_id) as count, sector_id from locations group by sector_id`);
128 const sample = _.sample(availableSectors.filter(sector => sector.count < 250)) as {count: number, sector_id: number};
131 return _.sortBy(availableSectors, 'sector_id').pop().sector_id+1;
134 return sample.sector_id;
137 async save(city: Partial<City>) {
139 throw new Error('Unknown city to save');
141 const fieldsToSave = [
142 'totalSpace', 'usedSpace', 'credits', 'alloys', 'energy', 'food',
143 'poulation', 'soldiers', 'attackers', 'defenders', 'sp_attackers', 'sp_defenders',
144 'homes', 'farms', 'barracks', 'special_attacker_trainer', 'special_defender_trainer'
147 const finalData = {};
149 fieldsToSave.forEach(field => {
150 if(city.hasOwnProperty(field)) {
151 finalData[field] = city[field];
155 await this.Save(finalData, {id: city.id});
159 async findById(cityId: string): Promise<CityWithLocation> {
160 const city = await this.db.raw<CityWithLocation[]>(`select c.*, l.sector_id, l.location_x, l.location_y from cities c
161 join locations l on c.id = l.city_id
162 where id = ? limit 1`, [cityId]);
165 throw new NotFoundError('User has no city', ERROR_CODE.NO_CITY);
172 async getUsersCity(owner: string): Promise<CityWithLocation> {
173 const city = await this.db.raw<CityWithLocation[]>(`select c.*, l.sector_id, l.location_x, l.location_y from cities c
174 join locations l on c.id = l.city_id
175 where owner = ? limit 1`, [owner]);
178 throw new NotFoundError('User has no city', ERROR_CODE.NO_CITY);
184 findAllInSector(sector_id: number): Promise<CityWithLocation[]> {
185 return this.db.raw<CityWithLocation[]>(`select c.*, l.sector_id, l.location_x, l.location_y from cities c
186 join locations l on c.id = l.city_id
187 where l.sector_id = ?`, [sector_id]);
190 async buildBuilding(building: Building, amount: number, city: City): Promise<BuildQueue> {
191 const freeSpace = city.totalSpace - city.usedSpace;
193 if(freeSpace < building.land) {
194 throw new InsufficientResourceError('land', building.land, freeSpace);
197 if(city.credits < building.credits) {
198 throw new InsufficientResourceError('credits', building.credits, city.credits);
201 if(city.alloys < building.alloys) {
202 throw new InsufficientResourceError('alloys', building.alloys, city.alloys);
205 if(city.energy < building.energy) {
206 throw new InsufficientResourceError('energy', building.energy, city.energy);
209 // validate that they have enough empty construction queues
210 const concurrentConstruction = await this.buildQueue.list(city.owner);
211 if(concurrentConstruction.length >= city.max_construction_queue) {
212 throw new InsufficientResourceError('Construction queues', concurrentConstruction.length + 1, city.max_construction_queue);
215 city.usedSpace += (building.land * amount);
216 city.credits -= (building.credits * amount);
217 city.alloys -= (building.alloys * amount);
218 city.energy -= (building.energy * amount);
220 await this.save(city);
222 const due = Duration.fromObject({ hours: building.time});
223 const queue = await this.buildQueue.create(
225 DateTime.now().plus({ milliseconds: due.as('milliseconds') }).toMillis(),
234 * Returns the distance in seconds
238 distanceInSeconds(city1: CityWithLocation, city2: CityWithLocation): number {
239 return this.distanceInHours(city1, city2) * 60 * 60;
242 distanceInHours(city1: CityWithLocation, city2: CityWithLocation): number {
243 const dist = Math.sqrt(
244 Math.pow((city2.location_x - city1.location_x), 2)
246 Math.pow((city2.location_y - city1.location_y), 2)
249 // sectors always add 4 hours
250 const sector_dist = Math.abs(city1.sector_id - city2.sector_id) * 6;
252 return _.round(dist/4, 2) + sector_dist;
256 async train(unit: Unit, amount: number, city: City): Promise<UnitTrainingQueue> {
257 if(city.credits < unit.credits) {
258 throw new InsufficientResourceError('credits', unit.credits, city.credits);
261 if(city.food < unit.food) {
262 throw new InsufficientResourceError('food', unit.food, city.food);
265 if(city.population < coalesce(unit.population, 0)) {
266 throw new InsufficientResourceError('population', unit.population, city.population);
269 if(city.soldiers < coalesce(unit.soldiers, 0)) {
270 throw new InsufficientResourceError('soldiers', unit.soldiers, city.soldiers);
273 if(city.attackers < coalesce(unit.attackers, 0)) {
274 throw new InsufficientResourceError('attackers', unit.attackers, city.attackers);
277 if(city.defenders < coalesce(unit.defenders, 0)) {
278 throw new InsufficientResourceError('defenders', unit.defenders, city.defenders);
281 // validate that they have enough empty training queues
282 const concurrentTraining = await this.unitTrainigQueue.list(city.owner);
283 if(concurrentTraining.length >= city.max_training_queue) {
284 throw new InsufficientResourceError('Training queues', concurrentTraining.length + 1, city.max_training_queue);
287 // ok they have everything, lets update their city
288 // and create the entry in the training queue
290 city.credits -= unit.credits * amount;
291 city.food -= unit.food * amount;
292 city.population -= unit.population * amount;
293 city.soldiers -= unit.soldiers * amount;
294 city.attackers -= unit.attackers * amount;
295 city.defenders -= unit.defenders * amount;
299 await this.save(city);
301 // barracks can drop this by 0.01% for each barrack.
303 let additionalOffset = 0;
304 if(unit.slug === 'sp_attackers') {
305 additionalOffset = (this.spAttackerTraininerBoost(city) * unit.time);
307 else if(unit.slug === 'sp_defenders') {
308 additionalOffset = (this.spDefenderTraininerBoost(city) * unit.time);
311 const barracksOffset = _.round((this.barracksImprovement(city) * unit.time) + unit.time - additionalOffset, 2);
313 const due = Duration.fromObject({ hours: barracksOffset });
315 const queue = await this.unitTrainigQueue.create(
317 DateTime.now().plus({ milliseconds: due.as('milliseconds') }).toMillis(),
325 async power(checkUnits: {soldiers: number, attackers: number, defenders: number, sp_attackers: number, sp_defenders: number}): Promise<number> {
326 const units = _.keyBy(await this.unitRepository.list(), 'slug');
329 _.each(checkUnits, (count, slug) => {
331 power += units[slug].attack * count;
340 barracksImprovement(city: City): number {
341 return city.barracks * 0.0001;
344 spAttackerTraininerBoost(city: City): number {
345 return city.special_attacker_trainer * 0.002;
348 spDefenderTraininerBoost(city: City): number {
349 return city.special_defender_trainer * 0.002;
352 maxPopulation(city: City): number {
353 return city.homes * 25;
356 maxFood(city: City): number {
357 return city.warehouses * 250;
360 maxEnergy(city: City): number {
361 return city.accumulators * 150;
364 maxAlloy(city: City): number {
365 return city.ore_refinery * 75;
368 async foodProductionPerTick(city: City): Promise<number> {
369 // eventually we should supply the warehouse formula
370 // to calculate the max amount of food created per tick
371 return city.farms * 50;
374 async foodUsagePerTick(city: City): Promise<number> {
376 (city.soldiers * 0.5) +
377 (city.population * 0.25) +
378 (city.attackers * 0.75) +
379 (city.attackers * 0.75) +
380 (city.sp_attackers * 1.3) +
381 (city.sp_defenders * 1.3)
385 async energyProductionPerTick(city: City): Promise<number> {
386 return city.solar_panels * 125;
389 async energyUsagePerTick(city: City): Promise<number> {
390 const buildings = await this.buildingRepository.list();
391 const buildingsMap = pluck<Building>(buildings, 'slug');
392 const totalEnergy = Math.ceil(_.sum([
393 city.farms * (buildingsMap['farms'].energy * 0.1),
394 city.barracks * (buildingsMap['barracks'].energy * 0.1),
395 city.special_defender_trainer * (buildingsMap['special_defender_trainer'].energy * 0.1),
396 city.special_attacker_trainer * (buildingsMap['special_attacker_trainer'].energy * 0.1),
397 city.homes * (buildingsMap['homes'].energy * 0.1),
398 city.warehouses * (buildingsMap['warehouses'].energy * 0.1),
399 city.solar_panels * (buildingsMap['solar_panels'].energy * 0.1),
400 city.mining_facilities * (buildingsMap['mining_facilities'].energy * 0.1),
401 city.ore_refinery * (buildingsMap['ore_refinery'].energy * 0.1)
406 async attack(attacker: CityWithLocation, attacked: CityWithLocation, army: Army): Promise<ArmyQueue> {
407 // validate the user has enough of a military!
408 if(attacker.soldiers < army.soldiers) {
409 throw new InsufficientResourceError('soldiers', army.soldiers, attacker.soldiers);
411 if(attacker.attackers < army.attackers) {
412 throw new InsufficientResourceError('attackers', army.attackers, attacker.attackers);
414 if(attacker.defenders < army.defenders) {
415 throw new InsufficientResourceError('defenders', army.defenders, attacker.defenders);
417 if(attacker.sp_attackers < army.sp_attackers) {
418 throw new InsufficientResourceError('sp_attackers', army.sp_attackers, attacker.sp_attackers);
420 if(attacker.sp_defenders < army.sp_defenders) {
421 throw new InsufficientResourceError('sp_defenders', army.sp_defenders, attacker.sp_defenders);
424 // ok, it's a real army lets send it off!
425 attacker.soldiers -= army.soldiers;
426 attacker.attackers -= army.attackers;
427 attacker.defenders -= army.defenders;
428 attacker.sp_attackers -= army.sp_attackers;
429 attacker.sp_defenders -= army.sp_defenders;
431 await this.save(attacker);
433 return this.armyRepository.create(
438 this.distanceInSeconds(attacker, attacked)
442 async getBuildQueues(owner: string): Promise<BuildQueue[]> {
443 return this.buildQueue.list(owner);
446 async getUnitTrainingQueues(owner: string): Promise<UnitTrainingQueueWithName[]> {
447 return this.unitTrainigQueue.list(owner);