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';
31 special_attacker_trainer: number;
32 special_defender_trainer: number;
36 export type CityWithLocation = {
42 export class CityRepository extends Repository<City> {
43 buildQueue: BuildQueueRepository;
44 buildingRepository: BuildingRepository;
45 unitRepository: UnitRepository;
46 unitTrainigQueue: UnitTrainingQueueRepository;
47 armyRepository: ArmyRepository;
51 this.buildingRepository = new BuildingRepository();
52 this.buildQueue = new BuildQueueRepository();
53 this.unitRepository = new UnitRepository();
54 this.unitTrainigQueue = new UnitTrainingQueueRepository();
55 this.armyRepository = new ArmyRepository();
58 async create(accountId: string, rebel: boolean = false): Promise<CityWithLocation> {
59 const icon = rebel ? `/colony-ships/rebels/${random(1, 6)}.png` : '/colony-ships/01.png';
77 special_attacker_trainer: 0,
78 special_defender_trainer: 0,
82 await this.Insert(info);
84 // placement can happen randomly
85 const availableSectors = config.AVAILABLE_SECTORS;
86 const sector = _.random(1, availableSectors);
89 sector_id: await this.getAvailableSector(),
90 location_x: random(0, 25),
91 location_y: random(0, 25)
94 await this.db.raw('insert into locations (sector_id, city_id, location_x, location_y) values (?, ?, ?, ?)', [
103 sector_id: location.sector_id,
104 location_x: location.location_x,
105 location_y: location.location_y
109 async getAvailableSector(): Promise<number> {
110 // figure out which sectors have space (40% fill rate at 25x25);
111 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`);
112 const sample = _.sample(availableSectors.filter(sector => sector.count < 250)) as {count: number, sector_id: number};
115 return _.sortBy(availableSectors, 'sector_id').pop().sector_id+1;
118 return sample.sector_id;
121 async save(city: Partial<City>) {
122 await this.Save(city, {id: city.id});
126 async findById(cityId: string): Promise<CityWithLocation> {
127 const city = await this.db.raw<CityWithLocation[]>(`select c.*, l.sector_id, l.location_x, l.location_y from cities c
128 join locations l on c.id = l.city_id
129 where id = ? limit 1`, [cityId]);
132 throw new NotFoundError('User has no city', ERROR_CODE.NO_CITY);
139 async getUsersCity(owner: string): Promise<CityWithLocation> {
140 const city = await this.db.raw<CityWithLocation[]>(`select c.*, l.sector_id, l.location_x, l.location_y from cities c
141 join locations l on c.id = l.city_id
142 where owner = ? limit 1`, [owner]);
145 throw new NotFoundError('User has no city', ERROR_CODE.NO_CITY);
151 findAllInSector(sector_id: number): Promise<CityWithLocation[]> {
152 return this.db.raw<CityWithLocation[]>(`select c.*, l.sector_id, l.location_x, l.location_y from cities c
153 join locations l on c.id = l.city_id
154 where l.sector_id = ?`, [sector_id]);
157 async buildBuilding(building: Building, amount: number, city: City): Promise<BuildQueue> {
158 const freeSpace = city.totalSpace - city.usedSpace;
160 if(freeSpace < building.land) {
161 throw new InsufficientResourceError('land', building.land, freeSpace);
164 if(city.gold < building.gold) {
165 throw new InsufficientResourceError('gold', building.gold, city.gold);
168 if(city.ore < building.ore) {
169 throw new InsufficientResourceError('ore', building.ore, city.ore);
172 if(city.logs < building.logs) {
173 throw new InsufficientResourceError('logs', building.logs, city.logs);
176 city.usedSpace += (building.land * amount);
177 city.gold -= (building.gold * amount);
178 city.ore -= (building.ore * amount);
179 city.logs -= (building.logs * amount);
181 await this.save(city);
183 const due = Duration.fromObject({ hours: building.time});
184 const queue = await this.buildQueue.create(
186 DateTime.now().plus({ milliseconds: due.as('milliseconds') }).toMillis(),
195 * Returns the distance in seconds
199 distanceInSeconds(city1: CityWithLocation, city2: CityWithLocation): number {
200 return this.distanceInHours(city1, city2) * 60 * 60;
203 distanceInHours(city1: CityWithLocation, city2: CityWithLocation): number {
204 const dist = Math.sqrt(
205 Math.pow((city2.location_x - city1.location_x), 2)
207 Math.pow((city2.location_y - city1.location_y), 2)
210 // sectors always add 4 hours
211 const sector_dist = Math.abs(city1.sector_id - city2.sector_id) * 6;
213 return _.round(dist/4, 2) + sector_dist;
217 async train(unit: Unit, amount: number, city: City): Promise<UnitTrainingQueue> {
218 if(city.gold < unit.gold) {
219 throw new InsufficientResourceError('gold', unit.gold, city.gold);
222 if(city.bushels < unit.bushels) {
223 throw new InsufficientResourceError('bushels', unit.bushels, city.bushels);
226 if(city.population < coalesce(unit.population, 0)) {
227 throw new InsufficientResourceError('population', unit.population, city.population);
230 if(city.soldiers < coalesce(unit.soldiers, 0)) {
231 throw new InsufficientResourceError('soldiers', unit.soldiers, city.soldiers);
234 if(city.attackers < coalesce(unit.attackers, 0)) {
235 throw new InsufficientResourceError('attackers', unit.attackers, city.attackers);
238 if(city.defenders < coalesce(unit.defenders, 0)) {
239 throw new InsufficientResourceError('defenders', unit.defenders, city.defenders);
242 // validate that they have enough of the buildings to support this
244 // ok they have everything, lets update their city
245 // and create the entry in the training queue
247 city.gold -= unit.gold * amount;
248 city.bushels -= unit.bushels * amount;
249 city.population -= coalesce(unit.population, 0) * amount;
250 city.soldiers -= coalesce(unit.soldiers, 0) * amount;
251 city.attackers -= coalesce(unit.attackers, 0) * amount;
252 city.defenders -= coalesce(unit.defenders, 0) * amount;
254 await this.save(city);
256 const due = Duration.fromObject({ hours: unit.time});
257 const queue = await this.unitTrainigQueue.create(
259 DateTime.now().plus({ milliseconds: due.as('milliseconds') }).toMillis(),
267 async power(checkUnits: {soldiers: number, attackers: number, defenders: number, sp_attackers: number, sp_defenders: number}): Promise<number> {
268 const units = _.keyBy(await this.unitRepository.list(), 'slug');
271 _.each(checkUnits, (count, slug) => {
273 power += units[slug].attack * count;
282 async attack(attacker: CityWithLocation, attacked: CityWithLocation, army: Army): Promise<ArmyQueue> {
283 // validate the user has enough of a military!
284 if(attacker.soldiers < army.soldiers) {
285 throw new InsufficientResourceError('soldiers', army.soldiers, attacker.soldiers);
287 if(attacker.attackers < army.attackers) {
288 throw new InsufficientResourceError('attackers', army.attackers, attacker.attackers);
290 if(attacker.defenders < army.defenders) {
291 throw new InsufficientResourceError('defenders', army.defenders, attacker.defenders);
293 if(attacker.sp_attackers < army.sp_attackers) {
294 throw new InsufficientResourceError('sp_attackers', army.sp_attackers, attacker.sp_attackers);
296 if(attacker.sp_defenders < army.sp_defenders) {
297 throw new InsufficientResourceError('sp_defenders', army.sp_defenders, attacker.sp_defenders);
300 // ok, it's a real army lets send it off!
301 attacker.soldiers -= army.soldiers;
302 attacker.attackers -= army.attackers;
303 attacker.defenders -= army.defenders;
304 attacker.sp_attackers -= army.sp_attackers;
305 attacker.sp_defenders -= army.sp_defenders;
307 await this.save(attacker);
309 return this.armyRepository.create(
314 this.distanceInSeconds(attacker, attacked)
318 async getBuildQueues(owner: string): Promise<BuildQueue[]> {
319 return this.buildQueue.list(owner);
322 async getUnitTrainingQueues(owner: string): Promise<UnitTrainingQueue[]> {
323 return this.unitTrainigQueue.list(owner);