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