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