56b57d030bd8f5a7b82edd73d76f08dccf74fcdb
[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     credits: number;
20     alloys: number;
21     energy: number;
22     food: number;
23     population: number;
24     soldiers: number;
25     attackers: number;
26     defenders: number;
27     sp_attackers: number;
28     sp_defenders: number;
29     homes: number;
30     farms: number;
31     barracks: number;
32     special_attacker_trainer: number;
33     special_defender_trainer: number;
34     icon: string;
35 }
36
37 export type CityWithLocation = {
38     sector_id: number;
39     location_x: number;
40     location_y: number;
41 } & City;
42
43 export class CityRepository extends Repository<City> {
44     buildQueue: BuildQueueRepository;
45     buildingRepository: BuildingRepository;
46     unitRepository: UnitRepository;
47     unitTrainigQueue: UnitTrainingQueueRepository;
48     armyRepository: ArmyRepository;
49
50     constructor() {
51         super('cities');
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();
57     }
58
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';
61         const info: City = {
62             id: uuid(),
63             owner: accountId,
64             totalSpace: 100,
65             usedSpace: 0,
66             credits: 10000,
67             alloys: 10000,
68             energy: 10000,
69             food: 10000,
70             population: 1000,
71             soldiers: 100,
72             attackers: 0,
73             defenders: 0,
74             sp_attackers: 0,
75             sp_defenders: 0,
76             homes: 20,
77             farms: 0,
78             barracks: 0,
79             special_attacker_trainer: 0,
80             special_defender_trainer: 0,
81             icon
82         };
83
84         await this.Insert(info);
85
86         // placement can happen randomly
87         const availableSectors = config.AVAILABLE_SECTORS;
88         const sector = _.random(1, availableSectors);
89
90         const location = {
91             sector_id: await this.getAvailableSector(),
92             location_x: random(0, 25),
93             location_y: random(0, 25)
94         }
95
96         await this.db.raw('insert into locations (sector_id, city_id, location_x, location_y) values (?, ?, ?, ?)', [
97             location.sector_id,
98             info.id,
99             location.location_x,
100             location.location_y
101         ]);
102
103         return {
104             ...info,
105             sector_id: location.sector_id,
106             location_x: location.location_x,
107             location_y: location.location_y
108         };
109     }
110
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};
115
116         if(!sample) {
117             return _.sortBy(availableSectors, 'sector_id').pop().sector_id+1;
118         }
119         
120         return sample.sector_id;
121     }
122
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'
128       ];
129       
130       const finalData = {};
131
132       fieldsToSave.forEach(field => {
133         if(city.hasOwnProperty(field)) {
134           finalData[field] = city[field];
135         }
136       });
137
138       await this.Save(finalData, {id: city.id});
139       return city;
140     }
141
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]);
146
147         if(!city) {
148             throw new NotFoundError('User has no city', ERROR_CODE.NO_CITY);
149         }
150
151         return city.pop();
152
153     }
154
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]);
159
160         if(!city) {
161             throw new NotFoundError('User has no city', ERROR_CODE.NO_CITY);
162         }
163
164         return city.pop();
165     }
166
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]);
171     }
172
173     async buildBuilding(building: Building, amount: number, city: City): Promise<BuildQueue> {
174         const freeSpace = city.totalSpace - city.usedSpace;
175
176         if(freeSpace < building.land) {
177             throw new InsufficientResourceError('land', building.land, freeSpace);
178         }
179
180         if(city.credits < building.credits) {
181             throw new InsufficientResourceError('credits', building.credits, city.credits);
182         }
183
184         if(city.alloys < building.alloys) {
185             throw new InsufficientResourceError('alloys', building.alloys, city.alloys);
186         }
187
188         if(city.energy < building.energy) {
189             throw new InsufficientResourceError('energy', building.energy, city.energy);
190         }
191
192         city.usedSpace += (building.land * amount);
193         city.credits -= (building.credits * amount);
194         city.alloys -= (building.alloys * amount);
195         city.energy -= (building.energy * amount);
196
197         await this.save(city);
198
199         const due = Duration.fromObject({ hours: building.time});
200         const queue = await this.buildQueue.create(
201             city.owner, 
202             DateTime.now().plus({ milliseconds: due.as('milliseconds') }).toMillis(), 
203             building.slug,
204             amount
205         );
206
207         return queue;
208     }
209
210     /**
211      * Returns the distance in seconds
212      * @param city1 
213      * @param city2 
214      */
215     distanceInSeconds(city1: CityWithLocation, city2: CityWithLocation): number {
216         return this.distanceInHours(city1, city2) * 60 * 60;
217     }
218
219     distanceInHours(city1: CityWithLocation, city2: CityWithLocation): number {
220         const dist = Math.sqrt(
221             Math.pow((city2.location_x - city1.location_x), 2) 
222             + 
223             Math.pow((city2.location_y - city1.location_y), 2)
224         );
225
226         // sectors always add 4 hours
227         const sector_dist = Math.abs(city1.sector_id - city2.sector_id) * 6;
228
229         return _.round(dist/4, 2) + sector_dist;
230
231     }
232
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);
236         }
237
238         if(city.food < unit.food) {
239             throw new InsufficientResourceError('food', unit.food, city.food);
240         }
241
242         if(city.population < coalesce(unit.population, 0)) {
243             throw new InsufficientResourceError('population', unit.population, city.population);
244         }
245
246         if(city.soldiers < coalesce(unit.soldiers, 0)) {
247             throw new InsufficientResourceError('soldiers', unit.soldiers, city.soldiers);
248         }
249
250         if(city.attackers < coalesce(unit.attackers, 0)) {
251             throw new InsufficientResourceError('attackers', unit.attackers, city.attackers);
252         }
253
254         if(city.defenders < coalesce(unit.defenders, 0)) {
255             throw new InsufficientResourceError('defenders', unit.defenders, city.defenders);
256         }
257
258         // validate that they have enough of the buildings to support this
259
260         // ok they have everything, lets update their city 
261         // and create the entry in the training queue
262
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;
269
270         await this.save(city);
271
272         // barracks can drop this by 0.01% for each barrack.
273         const barracksOffset = _.round((this.barracksImprovement(city) * unit.time) + unit.time, 2);
274
275         const due = Duration.fromObject({ hours: barracksOffset});
276
277         const queue = await this.unitTrainigQueue.create(
278             city.owner, 
279             DateTime.now().plus({ milliseconds: due.as('milliseconds') }).toMillis(), 
280             unit.slug,
281             amount
282         );
283
284         return queue;
285     }
286
287     async power(checkUnits: {soldiers: number, attackers: number, defenders: number, sp_attackers: number, sp_defenders: number}): Promise<number> {
288         const units = _.keyBy(await this.unitRepository.list(), 'slug');
289         let power = 0;
290
291         _.each(checkUnits, (count, slug) => {
292             try {
293                 power += units[slug].attack * count;
294             }
295             catch(e) {
296             }
297         });
298
299         return power
300     }
301
302     barracksImprovement(city: City): number {
303       return city.barracks * 0.0001;
304     }
305
306     maxPopulation(city: City): number {
307       return city.homes * 25;
308     }
309
310     async foodProductionPerTick(city: City): Promise<number> {
311       // eventually we should supply the warehouse formula 
312       // to calculate the max amount of food created per tick
313       return _.max([
314         city.population + _.round(city.farms * 50)
315       ])
316     }
317
318     async foodUsagePerTick(city: City): Promise<number> {
319       return (
320         (city.soldiers * 0.5) + 
321         (city.population * 0.25) + 
322         (city.attackers * 0.75) + 
323         (city.attackers * 0.75) + 
324         (city.sp_attackers * 1.3) + 
325         (city.sp_defenders * 1.3)
326       )
327     }
328
329     async energyProductionPerTic(city: City): Promise<number> {
330       return 0;
331     }
332
333     async energyUsagePerTick(city: City): Promise<number> {
334       return 0;
335     }
336
337     async attack(attacker: CityWithLocation, attacked: CityWithLocation, army: Army): Promise<ArmyQueue> {
338         // validate the user has enough of a military! 
339         if(attacker.soldiers < army.soldiers) {
340             throw new InsufficientResourceError('soldiers', army.soldiers, attacker.soldiers);
341         }
342         if(attacker.attackers < army.attackers) {
343             throw new InsufficientResourceError('attackers', army.attackers, attacker.attackers);
344         }
345         if(attacker.defenders < army.defenders) {
346             throw new InsufficientResourceError('defenders', army.defenders, attacker.defenders);
347         }
348         if(attacker.sp_attackers < army.sp_attackers) {
349             throw new InsufficientResourceError('sp_attackers', army.sp_attackers, attacker.sp_attackers);
350         }
351         if(attacker.sp_defenders < army.sp_defenders) {
352             throw new InsufficientResourceError('sp_defenders', army.sp_defenders, attacker.sp_defenders);
353         }
354
355         // ok, it's a real army lets send it off!
356         attacker.soldiers -= army.soldiers;
357         attacker.attackers -= army.attackers;
358         attacker.defenders -= army.defenders;
359         attacker.sp_attackers -= army.sp_attackers;
360         attacker.sp_defenders -= army.sp_defenders;
361
362         await this.save(attacker);
363
364         return this.armyRepository.create(
365             attacker.owner,
366             attacker,
367             attacked,
368             army,
369             this.distanceInSeconds(attacker, attacked)
370         );
371     }
372
373     async getBuildQueues(owner: string): Promise<BuildQueue[]> {
374         return this.buildQueue.list(owner);
375     }
376
377     async getUnitTrainingQueues(owner: string): Promise<UnitTrainingQueue[]> {
378         return this.unitTrainigQueue.list(owner);
379     }
380 }