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