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