df808786e71fbf7890b91b0ce35d573530d357b5
[browser-rts.git] / src / repository / city.ts
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';
13
14 export type City = {
15         id: string;
16     owner: string;
17     totalSpace: number;
18     usedSpace: number;
19     gold: number;
20     ore: number;
21     logs: number;
22     bushels: number;
23     population: number;
24     soldiers: number;
25     attackers: number;
26     defenders: number;
27     sp_attackers: number;
28     sp_defenders: number;
29     farms: number;
30     barracks: number;
31     special_attacker_trainer: number;
32     special_defender_trainer: number;
33 }
34
35 export type CityWithLocation = {
36     sector_id: number;
37     location_x: number;
38     location_y: number;
39 } & City;
40
41 export class CityRepository extends Repository<City> {
42     buildQueue: BuildQueueRepository;
43     buildingRepository: BuildingRepository;
44     unitRepository: UnitRepository;
45     unitTrainigQueue: UnitTrainingQueueRepository;
46     armyRepository: ArmyRepository;
47
48     constructor() {
49         super('cities');
50         this.buildingRepository = new BuildingRepository();
51         this.buildQueue = new BuildQueueRepository();
52         this.unitRepository = new UnitRepository();
53         this.unitTrainigQueue = new UnitTrainingQueueRepository();
54         this.armyRepository = new ArmyRepository();
55     }
56
57     async create(accountId: string): Promise<CityWithLocation> {
58         const info: City = {
59             id: uuid(),
60             owner: accountId,
61             totalSpace: 100,
62             usedSpace: 0,
63             gold: 10000,
64             ore: 10000,
65             logs: 10000,
66             bushels: 10000,
67             population: 1000,
68             soldiers: 100,
69             attackers: 0,
70             defenders: 0,
71             sp_attackers: 0,
72             sp_defenders: 0,
73             farms: 0,
74             barracks: 0,
75             special_attacker_trainer: 0,
76             special_defender_trainer: 0,
77         };
78
79         await this.Insert(info);
80
81         // placement can happen randomly
82         const availableSectors = config.AVAILABLE_SECTORS;
83         const sector = _.random(1, availableSectors);
84
85         const location = {
86             sector_id: await this.getAvailableSector(),
87             location_x: random(0, 25),
88             location_y: random(0, 25)
89         }
90
91         await this.db.raw('insert into locations (sector_id, city_id, location_x, location_y) values (?, ?, ?, ?)', [
92             location.sector_id,
93             info.id,
94             location.location_x,
95             location.location_y
96         ]);
97
98         return {
99             ...info,
100             sector_id: location.sector_id,
101             location_x: location.location_x,
102             location_y: location.location_y
103         };
104     }
105
106     async getAvailableSector(): Promise<number> {
107         // figure out which sectors have space (40% fill rate at 25x25);
108         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`);
109         const sample = _.sample(availableSectors.filter(sector => sector.count < 250)) as {count: number, sector_id: number};
110
111         if(!sample) {
112             return _.sortBy(availableSectors, 'sector_id').pop().sector_id+1;
113         }
114         
115         return sample.sector_id;
116     }
117
118     async save(city: Partial<City>) {
119         await this.Save(city, {id: city.id});
120         return city;
121     }
122
123     async findById(cityId: string): Promise<CityWithLocation> {
124         const city = await this.db.raw<CityWithLocation[]>(`select c.*, l.sector_id, l.location_x, l.location_y from cities c
125         join locations l on c.id = l.city_id 
126         where id = ? limit 1`, [cityId]);
127
128         if(!city) {
129             throw new NotFoundError('User has no city', ERROR_CODE.NO_CITY);
130         }
131
132         return city.pop();
133
134     }
135
136     async getUsersCity(owner: string): Promise<CityWithLocation> {
137         const city = await this.db.raw<CityWithLocation[]>(`select c.*, l.sector_id, l.location_x, l.location_y from cities c
138         join locations l on c.id = l.city_id 
139         where owner = ? limit 1`, [owner]);
140
141         if(!city) {
142             throw new NotFoundError('User has no city', ERROR_CODE.NO_CITY);
143         }
144
145         return city.pop();
146     }
147
148     findAllInSector(sector_id: number): Promise<CityWithLocation[]> {
149         return this.db.raw<CityWithLocation[]>(`select c.*, l.sector_id, l.location_x, l.location_y from cities c
150 join locations l on c.id = l.city_id 
151 where l.sector_id = ?`, [sector_id]);
152     }
153
154     async buildBuilding(building: Building, amount: number, city: City): Promise<BuildQueue> {
155         const freeSpace = city.totalSpace - city.usedSpace;
156
157         if(freeSpace < building.land) {
158             throw new InsufficientResourceError('land', building.land, freeSpace);
159         }
160
161         if(city.gold < building.gold) {
162             throw new InsufficientResourceError('gold', building.gold, city.gold);
163         }
164
165         if(city.ore < building.ore) {
166             throw new InsufficientResourceError('ore', building.ore, city.ore);
167         }
168
169         if(city.logs < building.logs) {
170             throw new InsufficientResourceError('logs', building.logs, city.logs);
171         }
172
173         city.usedSpace += (building.land * amount);
174         city.gold -= (building.gold * amount);
175         city.ore -= (building.ore * amount);
176         city.logs -= (building.logs * amount);
177
178         await this.save(city);
179
180         const due = Duration.fromObject({ hours: building.time});
181         const queue = await this.buildQueue.create(
182             city.owner, 
183             DateTime.now().plus({ milliseconds: due.as('milliseconds') }).toMillis(), 
184             building.slug,
185             amount
186         );
187
188         return queue;
189     }
190
191     /**
192      * Returns the distance in seconds
193      * @param city1 
194      * @param city2 
195      */
196     distanceInSeconds(city1: CityWithLocation, city2: CityWithLocation): number {
197         return this.distanceInHours(city1, city2) * 60 * 60;
198     }
199
200     distanceInHours(city1: CityWithLocation, city2: CityWithLocation): number {
201         const dist = Math.sqrt(
202             Math.pow((city2.location_x - city1.location_x), 2) 
203             + 
204             Math.pow((city2.location_y - city1.location_y), 2)
205         );
206
207         return _.round(dist/4, 2);
208
209     }
210
211     async train(unit: Unit, amount: number, city: City): Promise<UnitTrainingQueue> {
212         if(city.gold < unit.gold) {
213             throw new InsufficientResourceError('gold', unit.gold, city.gold);
214         }
215
216         if(city.bushels < unit.bushels) {
217             throw new InsufficientResourceError('bushels', unit.bushels, city.bushels);
218         }
219
220         if(city.population < coalesce(unit.population, 0)) {
221             throw new InsufficientResourceError('population', unit.population, city.population);
222         }
223
224         if(city.soldiers < coalesce(unit.soldiers, 0)) {
225             throw new InsufficientResourceError('soldiers', unit.soldiers, city.soldiers);
226         }
227
228         if(city.attackers < coalesce(unit.attackers, 0)) {
229             throw new InsufficientResourceError('attackers', unit.attackers, city.attackers);
230         }
231
232         if(city.defenders < coalesce(unit.defenders, 0)) {
233             throw new InsufficientResourceError('defenders', unit.defenders, city.defenders);
234         }
235
236         // validate that they have enough of the buildings to support this
237
238         // ok they have everything, lets update their city 
239         // and create the entry in the training queue
240
241         city.gold -= unit.gold * amount;
242         city.bushels -= unit.bushels * amount;
243         city.population -= coalesce(unit.population, 0) * amount;
244         city.soldiers -= coalesce(unit.soldiers, 0) * amount;
245         city.attackers -= coalesce(unit.attackers, 0) * amount;
246         city.defenders -= coalesce(unit.defenders, 0) * amount;
247
248         await this.save(city);
249
250         const due = Duration.fromObject({ hours: unit.time});
251         const queue = await this.unitTrainigQueue.create(
252             city.owner, 
253             DateTime.now().plus({ milliseconds: due.as('milliseconds') }).toMillis(), 
254             unit.slug,
255             amount
256         );
257
258         return queue;
259     }
260
261     async power(checkUnits: {soldiers: number, attackers: number, defenders: number, sp_attackers: number, sp_defenders: number}): Promise<number> {
262         const units = _.keyBy(await this.unitRepository.list(), 'slug');
263         let power = 0;
264
265         _.each(checkUnits, (count, slug) => {
266             try {
267                 power += units[slug].attack * count;
268             }
269             catch(e) {
270             }
271         });
272
273         return power
274     }
275
276     async attack(attacker: CityWithLocation, attacked: CityWithLocation, army: Army): Promise<ArmyQueue> {
277         // validate the user has enough of a military! 
278         if(attacker.soldiers < army.soldiers) {
279             throw new InsufficientResourceError('soldiers', army.soldiers, attacker.soldiers);
280         }
281         if(attacker.attackers < army.attackers) {
282             throw new InsufficientResourceError('attackers', army.attackers, attacker.attackers);
283         }
284         if(attacker.defenders < army.defenders) {
285             throw new InsufficientResourceError('defenders', army.defenders, attacker.defenders);
286         }
287         if(attacker.sp_attackers < army.sp_attackers) {
288             throw new InsufficientResourceError('sp_attackers', army.sp_attackers, attacker.sp_attackers);
289         }
290         if(attacker.sp_defenders < army.sp_defenders) {
291             throw new InsufficientResourceError('sp_defenders', army.sp_defenders, attacker.sp_defenders);
292         }
293
294         // ok, it's a real army lets send it off!
295         attacker.soldiers -= army.soldiers;
296         attacker.attackers -= army.attackers;
297         attacker.defenders -= army.defenders;
298         attacker.sp_attackers -= army.sp_attackers;
299         attacker.sp_defenders -= army.sp_defenders;
300
301         await this.save(attacker);
302
303         return this.armyRepository.create(
304             attacker.owner,
305             attacker,
306             attacked,
307             army,
308             this.distanceInSeconds(attacker, attacked)
309         );
310     }
311
312     async getBuildQueues(owner: string): Promise<BuildQueue[]> {
313         return this.buildQueue.list(owner);
314     }
315
316     async getUnitTrainingQueues(owner: string): Promise<UnitTrainingQueue[]> {
317         return this.unitTrainigQueue.list(owner);
318     }
319 }