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 } from './training-queue';
8 import { coalesce } 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';
32 special_attacker_trainer: number;
33 special_defender_trainer: number;
37 export type CityWithLocation = {
43 export class CityRepository extends Repository<City> {
44 buildQueue: BuildQueueRepository;
45 buildingRepository: BuildingRepository;
46 unitRepository: UnitRepository;
47 unitTrainigQueue: UnitTrainingQueueRepository;
48 armyRepository: ArmyRepository;
52 this.buildingRepository = new BuildingRepository();
53 this.buildQueue = new BuildQueueRepository();
54 this.unitRepository = new UnitRepository();
55 this.unitTrainigQueue = new UnitTrainingQueueRepository();
56 this.armyRepository = new ArmyRepository();
59 async create(accountId: string, rebel: boolean = false): Promise<CityWithLocation> {
60 const icon = rebel ? `/colony-ships/rebels/${random(1, 6)}.png` : '/colony-ships/01.png';
79 special_attacker_trainer: 0,
80 special_defender_trainer: 0,
84 await this.Insert(info);
86 // placement can happen randomly
87 const availableSectors = config.AVAILABLE_SECTORS;
88 const sector = _.random(1, availableSectors);
91 sector_id: await this.getAvailableSector(),
92 location_x: random(0, 25),
93 location_y: random(0, 25)
96 await this.db.raw('insert into locations (sector_id, city_id, location_x, location_y) values (?, ?, ?, ?)', [
105 sector_id: location.sector_id,
106 location_x: location.location_x,
107 location_y: location.location_y
111 async getAvailableSector(): Promise<number> {
112 // figure out which sectors have space (40% fill rate at 25x25);
113 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`);
114 const sample = _.sample(availableSectors.filter(sector => sector.count < 250)) as {count: number, sector_id: number};
117 return _.sortBy(availableSectors, 'sector_id').pop().sector_id+1;
120 return sample.sector_id;
123 async save(city: City) {
124 const fieldsToSave = [
125 'totalSpace', 'usedSpace', 'credits', 'alloys', 'energy', 'food',
126 'poulation', 'soldiers', 'attackers', 'defenders', 'sp_attackers', 'sp_defenders',
127 'homes', 'farms', 'barracks', 'special_attacker_trainer', 'special_defender_trainer'
130 const finalData = {};
132 fieldsToSave.forEach(field => {
133 if(city.hasOwnProperty(field)) {
134 finalData[field] = city[field];
138 await this.Save(finalData, {id: city.id});
142 async findById(cityId: string): Promise<CityWithLocation> {
143 const city = await this.db.raw<CityWithLocation[]>(`select c.*, l.sector_id, l.location_x, l.location_y from cities c
144 join locations l on c.id = l.city_id
145 where id = ? limit 1`, [cityId]);
148 throw new NotFoundError('User has no city', ERROR_CODE.NO_CITY);
155 async getUsersCity(owner: string): Promise<CityWithLocation> {
156 const city = await this.db.raw<CityWithLocation[]>(`select c.*, l.sector_id, l.location_x, l.location_y from cities c
157 join locations l on c.id = l.city_id
158 where owner = ? limit 1`, [owner]);
161 throw new NotFoundError('User has no city', ERROR_CODE.NO_CITY);
167 findAllInSector(sector_id: number): Promise<CityWithLocation[]> {
168 return this.db.raw<CityWithLocation[]>(`select c.*, l.sector_id, l.location_x, l.location_y from cities c
169 join locations l on c.id = l.city_id
170 where l.sector_id = ?`, [sector_id]);
173 async buildBuilding(building: Building, amount: number, city: City): Promise<BuildQueue> {
174 const freeSpace = city.totalSpace - city.usedSpace;
176 if(freeSpace < building.land) {
177 throw new InsufficientResourceError('land', building.land, freeSpace);
180 if(city.credits < building.credits) {
181 throw new InsufficientResourceError('credits', building.credits, city.credits);
184 if(city.alloys < building.alloys) {
185 throw new InsufficientResourceError('alloys', building.alloys, city.alloys);
188 if(city.energy < building.energy) {
189 throw new InsufficientResourceError('energy', building.energy, city.energy);
192 city.usedSpace += (building.land * amount);
193 city.credits -= (building.credits * amount);
194 city.alloys -= (building.alloys * amount);
195 city.energy -= (building.energy * amount);
197 await this.save(city);
199 const due = Duration.fromObject({ hours: building.time});
200 const queue = await this.buildQueue.create(
202 DateTime.now().plus({ milliseconds: due.as('milliseconds') }).toMillis(),
211 * Returns the distance in seconds
215 distanceInSeconds(city1: CityWithLocation, city2: CityWithLocation): number {
216 return this.distanceInHours(city1, city2) * 60 * 60;
219 distanceInHours(city1: CityWithLocation, city2: CityWithLocation): number {
220 const dist = Math.sqrt(
221 Math.pow((city2.location_x - city1.location_x), 2)
223 Math.pow((city2.location_y - city1.location_y), 2)
226 // sectors always add 4 hours
227 const sector_dist = Math.abs(city1.sector_id - city2.sector_id) * 6;
229 return _.round(dist/4, 2) + sector_dist;
233 async train(unit: Unit, amount: number, city: City): Promise<UnitTrainingQueue> {
234 if(city.credits < unit.credits) {
235 throw new InsufficientResourceError('credits', unit.credits, city.credits);
238 if(city.food < unit.food) {
239 throw new InsufficientResourceError('food', unit.food, city.food);
242 if(city.population < coalesce(unit.population, 0)) {
243 throw new InsufficientResourceError('population', unit.population, city.population);
246 if(city.soldiers < coalesce(unit.soldiers, 0)) {
247 throw new InsufficientResourceError('soldiers', unit.soldiers, city.soldiers);
250 if(city.attackers < coalesce(unit.attackers, 0)) {
251 throw new InsufficientResourceError('attackers', unit.attackers, city.attackers);
254 if(city.defenders < coalesce(unit.defenders, 0)) {
255 throw new InsufficientResourceError('defenders', unit.defenders, city.defenders);
258 // validate that they have enough of the buildings to support this
260 // ok they have everything, lets update their city
261 // and create the entry in the training queue
263 city.credits -= unit.credits * amount;
264 city.food -= unit.food * amount;
265 city.population -= coalesce(unit.population, 0) * amount;
266 city.soldiers -= coalesce(unit.soldiers, 0) * amount;
267 city.attackers -= coalesce(unit.attackers, 0) * amount;
268 city.defenders -= coalesce(unit.defenders, 0) * amount;
270 await this.save(city);
272 const due = Duration.fromObject({ hours: unit.time});
273 const queue = await this.unitTrainigQueue.create(
275 DateTime.now().plus({ milliseconds: due.as('milliseconds') }).toMillis(),
283 async power(checkUnits: {soldiers: number, attackers: number, defenders: number, sp_attackers: number, sp_defenders: number}): Promise<number> {
284 const units = _.keyBy(await this.unitRepository.list(), 'slug');
287 _.each(checkUnits, (count, slug) => {
289 power += units[slug].attack * count;
298 maxPopulation(city: City): number {
299 return city.homes * 25;
302 async foodProductionPerTick(city: City): Promise<number> {
303 // eventually we should supply the warehouse formula
304 // to calculate the max amount of food created per tick
306 city.population + _.round(city.farms * 50)
310 async foodUsagePerTick(city: City): Promise<number> {
312 (city.soldiers * 0.5) +
313 (city.population * 0.25) +
314 (city.attackers * 0.75) +
315 (city.attackers * 0.75) +
316 (city.sp_attackers * 1.3) +
317 (city.sp_defenders * 1.3)
321 async energyProductionPerTic(city: City): Promise<number> {
325 async energyUsagePerTick(city: City): Promise<number> {
329 async attack(attacker: CityWithLocation, attacked: CityWithLocation, army: Army): Promise<ArmyQueue> {
330 // validate the user has enough of a military!
331 if(attacker.soldiers < army.soldiers) {
332 throw new InsufficientResourceError('soldiers', army.soldiers, attacker.soldiers);
334 if(attacker.attackers < army.attackers) {
335 throw new InsufficientResourceError('attackers', army.attackers, attacker.attackers);
337 if(attacker.defenders < army.defenders) {
338 throw new InsufficientResourceError('defenders', army.defenders, attacker.defenders);
340 if(attacker.sp_attackers < army.sp_attackers) {
341 throw new InsufficientResourceError('sp_attackers', army.sp_attackers, attacker.sp_attackers);
343 if(attacker.sp_defenders < army.sp_defenders) {
344 throw new InsufficientResourceError('sp_defenders', army.sp_defenders, attacker.sp_defenders);
347 // ok, it's a real army lets send it off!
348 attacker.soldiers -= army.soldiers;
349 attacker.attackers -= army.attackers;
350 attacker.defenders -= army.defenders;
351 attacker.sp_attackers -= army.sp_attackers;
352 attacker.sp_defenders -= army.sp_defenders;
354 await this.save(attacker);
356 return this.armyRepository.create(
361 this.distanceInSeconds(attacker, attacked)
365 async getBuildQueues(owner: string): Promise<BuildQueue[]> {
366 return this.buildQueue.list(owner);
369 async getUnitTrainingQueues(owner: string): Promise<UnitTrainingQueue[]> {
370 return this.unitTrainigQueue.list(owner);