7b8818510e9b45761473d6da7411c27735ce4299
[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     gold: number;
20     ore: number;
21     logs: number;
22     bushels: number;
23     population: number;
24     soldiers: number;
25     attackers: number;
26     defenders: number;
27     sp_attackers: number;
28     sp_defenders: number;
29     farms: number;
30     barracks: number;
31     special_attacker_trainer: number;
32     special_defender_trainer: number;
33     icon: string;
34 }
35
36 export type CityWithLocation = {
37     sector_id: number;
38     location_x: number;
39     location_y: number;
40 } & City;
41
42 export class CityRepository extends Repository<City> {
43     buildQueue: BuildQueueRepository;
44     buildingRepository: BuildingRepository;
45     unitRepository: UnitRepository;
46     unitTrainigQueue: UnitTrainingQueueRepository;
47     armyRepository: ArmyRepository;
48
49     constructor() {
50         super('cities');
51         this.buildingRepository = new BuildingRepository();
52         this.buildQueue = new BuildQueueRepository();
53         this.unitRepository = new UnitRepository();
54         this.unitTrainigQueue = new UnitTrainingQueueRepository();
55         this.armyRepository = new ArmyRepository();
56     }
57
58     async create(accountId: string, rebel: boolean = false): Promise<CityWithLocation> {
59       const icon = rebel ? `/colony-ships/rebels/${random(1, 6)}.png` : '/colony-ships/01.png';
60         const info: City = {
61             id: uuid(),
62             owner: accountId,
63             totalSpace: 100,
64             usedSpace: 0,
65             gold: 10000,
66             ore: 10000,
67             logs: 10000,
68             bushels: 10000,
69             population: 1000,
70             soldiers: 100,
71             attackers: 0,
72             defenders: 0,
73             sp_attackers: 0,
74             sp_defenders: 0,
75             farms: 0,
76             barracks: 0,
77             special_attacker_trainer: 0,
78             special_defender_trainer: 0,
79             icon
80         };
81
82         await this.Insert(info);
83
84         // placement can happen randomly
85         const availableSectors = config.AVAILABLE_SECTORS;
86         const sector = _.random(1, availableSectors);
87
88         const location = {
89             sector_id: await this.getAvailableSector(),
90             location_x: random(0, 25),
91             location_y: random(0, 25)
92         }
93
94         await this.db.raw('insert into locations (sector_id, city_id, location_x, location_y) values (?, ?, ?, ?)', [
95             location.sector_id,
96             info.id,
97             location.location_x,
98             location.location_y
99         ]);
100
101         return {
102             ...info,
103             sector_id: location.sector_id,
104             location_x: location.location_x,
105             location_y: location.location_y
106         };
107     }
108
109     async getAvailableSector(): Promise<number> {
110         // figure out which sectors have space (40% fill rate at 25x25);
111         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`);
112         const sample = _.sample(availableSectors.filter(sector => sector.count < 250)) as {count: number, sector_id: number};
113
114         if(!sample) {
115             return _.sortBy(availableSectors, 'sector_id').pop().sector_id+1;
116         }
117         
118         return sample.sector_id;
119     }
120
121     async save(city: Partial<City>) {
122         await this.Save(city, {id: city.id});
123         return city;
124     }
125
126     async findById(cityId: string): Promise<CityWithLocation> {
127         const city = await this.db.raw<CityWithLocation[]>(`select c.*, l.sector_id, l.location_x, l.location_y from cities c
128         join locations l on c.id = l.city_id 
129         where id = ? limit 1`, [cityId]);
130
131         if(!city) {
132             throw new NotFoundError('User has no city', ERROR_CODE.NO_CITY);
133         }
134
135         return city.pop();
136
137     }
138
139     async getUsersCity(owner: string): Promise<CityWithLocation> {
140         const city = await this.db.raw<CityWithLocation[]>(`select c.*, l.sector_id, l.location_x, l.location_y from cities c
141         join locations l on c.id = l.city_id 
142         where owner = ? limit 1`, [owner]);
143
144         if(!city) {
145             throw new NotFoundError('User has no city', ERROR_CODE.NO_CITY);
146         }
147
148         return city.pop();
149     }
150
151     findAllInSector(sector_id: number): Promise<CityWithLocation[]> {
152         return this.db.raw<CityWithLocation[]>(`select c.*, l.sector_id, l.location_x, l.location_y from cities c
153 join locations l on c.id = l.city_id 
154 where l.sector_id = ?`, [sector_id]);
155     }
156
157     async buildBuilding(building: Building, amount: number, city: City): Promise<BuildQueue> {
158         const freeSpace = city.totalSpace - city.usedSpace;
159
160         if(freeSpace < building.land) {
161             throw new InsufficientResourceError('land', building.land, freeSpace);
162         }
163
164         if(city.gold < building.gold) {
165             throw new InsufficientResourceError('gold', building.gold, city.gold);
166         }
167
168         if(city.ore < building.ore) {
169             throw new InsufficientResourceError('ore', building.ore, city.ore);
170         }
171
172         if(city.logs < building.logs) {
173             throw new InsufficientResourceError('logs', building.logs, city.logs);
174         }
175
176         city.usedSpace += (building.land * amount);
177         city.gold -= (building.gold * amount);
178         city.ore -= (building.ore * amount);
179         city.logs -= (building.logs * amount);
180
181         await this.save(city);
182
183         const due = Duration.fromObject({ hours: building.time});
184         const queue = await this.buildQueue.create(
185             city.owner, 
186             DateTime.now().plus({ milliseconds: due.as('milliseconds') }).toMillis(), 
187             building.slug,
188             amount
189         );
190
191         return queue;
192     }
193
194     /**
195      * Returns the distance in seconds
196      * @param city1 
197      * @param city2 
198      */
199     distanceInSeconds(city1: CityWithLocation, city2: CityWithLocation): number {
200         return this.distanceInHours(city1, city2) * 60 * 60;
201     }
202
203     distanceInHours(city1: CityWithLocation, city2: CityWithLocation): number {
204         const dist = Math.sqrt(
205             Math.pow((city2.location_x - city1.location_x), 2) 
206             + 
207             Math.pow((city2.location_y - city1.location_y), 2)
208         );
209
210         // sectors always add 4 hours
211         const sector_dist = Math.abs(city1.sector_id - city2.sector_id) * 6;
212
213         return _.round(dist/4, 2) + sector_dist;
214
215     }
216
217     async train(unit: Unit, amount: number, city: City): Promise<UnitTrainingQueue> {
218         if(city.gold < unit.gold) {
219             throw new InsufficientResourceError('gold', unit.gold, city.gold);
220         }
221
222         if(city.bushels < unit.bushels) {
223             throw new InsufficientResourceError('bushels', unit.bushels, city.bushels);
224         }
225
226         if(city.population < coalesce(unit.population, 0)) {
227             throw new InsufficientResourceError('population', unit.population, city.population);
228         }
229
230         if(city.soldiers < coalesce(unit.soldiers, 0)) {
231             throw new InsufficientResourceError('soldiers', unit.soldiers, city.soldiers);
232         }
233
234         if(city.attackers < coalesce(unit.attackers, 0)) {
235             throw new InsufficientResourceError('attackers', unit.attackers, city.attackers);
236         }
237
238         if(city.defenders < coalesce(unit.defenders, 0)) {
239             throw new InsufficientResourceError('defenders', unit.defenders, city.defenders);
240         }
241
242         // validate that they have enough of the buildings to support this
243
244         // ok they have everything, lets update their city 
245         // and create the entry in the training queue
246
247         city.gold -= unit.gold * amount;
248         city.bushels -= unit.bushels * amount;
249         city.population -= coalesce(unit.population, 0) * amount;
250         city.soldiers -= coalesce(unit.soldiers, 0) * amount;
251         city.attackers -= coalesce(unit.attackers, 0) * amount;
252         city.defenders -= coalesce(unit.defenders, 0) * amount;
253
254         await this.save(city);
255
256         const due = Duration.fromObject({ hours: unit.time});
257         const queue = await this.unitTrainigQueue.create(
258             city.owner, 
259             DateTime.now().plus({ milliseconds: due.as('milliseconds') }).toMillis(), 
260             unit.slug,
261             amount
262         );
263
264         return queue;
265     }
266
267     async power(checkUnits: {soldiers: number, attackers: number, defenders: number, sp_attackers: number, sp_defenders: number}): Promise<number> {
268         const units = _.keyBy(await this.unitRepository.list(), 'slug');
269         let power = 0;
270
271         _.each(checkUnits, (count, slug) => {
272             try {
273                 power += units[slug].attack * count;
274             }
275             catch(e) {
276             }
277         });
278
279         return power
280     }
281
282     async attack(attacker: CityWithLocation, attacked: CityWithLocation, army: Army): Promise<ArmyQueue> {
283         // validate the user has enough of a military! 
284         if(attacker.soldiers < army.soldiers) {
285             throw new InsufficientResourceError('soldiers', army.soldiers, attacker.soldiers);
286         }
287         if(attacker.attackers < army.attackers) {
288             throw new InsufficientResourceError('attackers', army.attackers, attacker.attackers);
289         }
290         if(attacker.defenders < army.defenders) {
291             throw new InsufficientResourceError('defenders', army.defenders, attacker.defenders);
292         }
293         if(attacker.sp_attackers < army.sp_attackers) {
294             throw new InsufficientResourceError('sp_attackers', army.sp_attackers, attacker.sp_attackers);
295         }
296         if(attacker.sp_defenders < army.sp_defenders) {
297             throw new InsufficientResourceError('sp_defenders', army.sp_defenders, attacker.sp_defenders);
298         }
299
300         // ok, it's a real army lets send it off!
301         attacker.soldiers -= army.soldiers;
302         attacker.attackers -= army.attackers;
303         attacker.defenders -= army.defenders;
304         attacker.sp_attackers -= army.sp_attackers;
305         attacker.sp_defenders -= army.sp_defenders;
306
307         await this.save(attacker);
308
309         return this.armyRepository.create(
310             attacker.owner,
311             attacker,
312             attacked,
313             army,
314             this.distanceInSeconds(attacker, attacked)
315         );
316     }
317
318     async getBuildQueues(owner: string): Promise<BuildQueue[]> {
319         return this.buildQueue.list(owner);
320     }
321
322     async getUnitTrainingQueues(owner: string): Promise<UnitTrainingQueue[]> {
323         return this.unitTrainigQueue.list(owner);
324     }
325 }