initial commit
[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 { BuildQueue, BuildQueueRepository } from './build-queue';
5 import { DateTime, Duration } from 'luxon';
6 import { UnitTrainingQueue, UnitTrainingQueueRepository } from './training-queue';
7 import { coalesce } from '../lib/util';
8 import { Building, BuildingRepository } from './buildings';
9 import { Unit, UnitRepository } from './unit';
10 import _ from 'lodash';
11 import { Army, ArmyQueue, ArmyRepository } from './army';
12
13 export type City = {
14         id: string;
15     owner: string;
16     totalSpace: number;
17     usedSpace: number;
18     gold: number;
19     ore: number;
20     logs: number;
21     bushels: number;
22     population: number;
23     soldiers: number;
24     attackers: number;
25     defenders: number;
26     sp_attackers: number;
27     sp_defenders: number;
28     farms: number;
29     barracks: number;
30     special_attacker_trainer: number;
31     special_defender_trainer: number;
32     location_x: number;
33     location_y: number;
34 }
35
36 export class CityRepository extends Repository<City> {
37     buildQueue: BuildQueueRepository;
38     buildingRepository: BuildingRepository;
39     unitRepository: UnitRepository;
40     unitTrainigQueue: UnitTrainingQueueRepository;
41     armyRepository: ArmyRepository;
42
43     constructor() {
44         super('cities');
45         this.buildingRepository = new BuildingRepository();
46         this.buildQueue = new BuildQueueRepository();
47         this.unitRepository = new UnitRepository();
48         this.unitTrainigQueue = new UnitTrainingQueueRepository();
49         this.armyRepository = new ArmyRepository();
50     }
51
52     async create(accountId: string): Promise<City> {
53         const info: City = {
54             id: uuid(),
55             owner: accountId,
56             totalSpace: 100,
57             usedSpace: 0,
58             gold: 10000,
59             ore: 10000,
60             logs: 10000,
61             bushels: 10000,
62             population: 1000,
63             soldiers: 100,
64             attackers: 0,
65             defenders: 0,
66             sp_attackers: 0,
67             sp_defenders: 0,
68             farms: 0,
69             barracks: 0,
70             special_attacker_trainer: 0,
71             special_defender_trainer: 0,
72             location_x: _.random(0, 100),
73             location_y: _.random(0, 100)
74         };
75
76         // placement can happen randomly
77
78         await this.Insert(info);
79
80         return info;
81     }
82
83     async save(city: City) {
84         await this.Save(city, {id: city.id});
85         return city;
86     }
87
88     async getUsersCity(owner: string): Promise<City> {
89         const city = await this.FindOne({
90             owner
91         });
92
93         if(!city) {
94             throw new NotFoundError('User has no city', ERROR_CODE.NO_CITY);
95         }
96
97         return city;
98     }
99
100     async buildBuilding(building: Building, amount: number, city: City): Promise<BuildQueue> {
101         const freeSpace = city.totalSpace - city.usedSpace;
102
103         if(freeSpace < building.land) {
104             throw new InsufficientResourceError('land', building.land, freeSpace);
105         }
106
107         if(city.gold < building.gold) {
108             throw new InsufficientResourceError('gold', building.gold, city.gold);
109         }
110
111         if(city.ore < building.ore) {
112             throw new InsufficientResourceError('ore', building.ore, city.ore);
113         }
114
115         if(city.logs < building.logs) {
116             throw new InsufficientResourceError('logs', building.logs, city.logs);
117         }
118
119         city.usedSpace += (building.land * amount);
120         city.gold -= (building.gold * amount);
121         city.ore -= (building.ore * amount);
122         city.logs -= (building.logs * amount);
123
124         await this.save(city);
125
126         const due = Duration.fromObject({ hours: building.time});
127         const queue = await this.buildQueue.create(
128             city.owner, 
129             DateTime.now().plus({ milliseconds: due.as('milliseconds') }).toMillis(), 
130             building.slug,
131             amount
132         );
133
134         return queue;
135     }
136
137     /**
138      * Returns the distance in seconds
139      * @param city1 
140      * @param city2 
141      */
142     distanceInSeconds(city1: City, city2: City): number {
143         return this.distanceInHours(city1, city2) * 60 * 60;
144     }
145
146     distanceInHours(city1: City, city2: City): number {
147         const dist = Math.sqrt(
148             Math.pow((city2.location_x - city1.location_x), 2) 
149             + 
150             Math.pow((city2.location_y - city1.location_y), 2)
151         );
152
153         return _.round(dist/4, 2);
154
155     }
156
157     async train(unit: Unit, amount: number, city: City): Promise<UnitTrainingQueue> {
158         if(city.gold < unit.gold) {
159             throw new InsufficientResourceError('gold', unit.gold, city.gold);
160         }
161
162         if(city.bushels < unit.bushels) {
163             throw new InsufficientResourceError('bushels', unit.bushels, city.bushels);
164         }
165
166         if(city.population < coalesce(unit.population, 0)) {
167             throw new InsufficientResourceError('population', unit.population, city.population);
168         }
169
170         if(city.soldiers < coalesce(unit.soldiers, 0)) {
171             throw new InsufficientResourceError('soldiers', unit.soldiers, city.soldiers);
172         }
173
174         if(city.attackers < coalesce(unit.attackers, 0)) {
175             throw new InsufficientResourceError('attackers', unit.attackers, city.attackers);
176         }
177
178         if(city.defenders < coalesce(unit.defenders, 0)) {
179             throw new InsufficientResourceError('defenders', unit.defenders, city.defenders);
180         }
181
182         // validate that they have enough of the buildings to support this
183
184         // ok they have everything, lets update their city 
185         // and create the entry in the training queue
186
187         city.gold -= unit.gold * amount;
188         city.bushels -= unit.bushels * amount;
189         city.population -= coalesce(unit.population, 0) * amount;
190         city.soldiers -= coalesce(unit.soldiers, 0) * amount;
191         city.attackers -= coalesce(unit.attackers, 0) * amount;
192         city.defenders -= coalesce(unit.defenders, 0) * amount;
193
194         await this.save(city);
195
196         const due = Duration.fromObject({ hours: unit.time});
197         const queue = await this.unitTrainigQueue.create(
198             city.owner, 
199             DateTime.now().plus({ milliseconds: due.as('milliseconds') }).toMillis(), 
200             unit.slug,
201             amount
202         );
203
204         return queue;
205     }
206
207     async power(checkUnits: {soldiers: number, attackers: number, defenders: number, sp_attackers: number, sp_defenders: number}): Promise<number> {
208         const units = _.keyBy(await this.unitRepository.list(), 'slug');
209         let power = 0;
210
211         _.each(checkUnits, (count, slug) => {
212             try {
213                 power += units[slug].attack * count;
214             }
215             catch(e) {
216             }
217         });
218
219         return power
220     }
221
222     async attack(attacker: City, attacked: City, army: Army): Promise<ArmyQueue> {
223         // validate the user has enough of a military! 
224         if(attacker.soldiers < army.soldiers) {
225             throw new InsufficientResourceError('soldiers', army.soldiers, attacker.soldiers);
226         }
227         if(attacker.attackers < army.attackers) {
228             throw new InsufficientResourceError('attackers', army.attackers, attacker.attackers);
229         }
230         if(attacker.defenders < army.defenders) {
231             throw new InsufficientResourceError('defenders', army.defenders, attacker.defenders);
232         }
233         if(attacker.sp_attackers < army.sp_attackers) {
234             throw new InsufficientResourceError('sp_attackers', army.sp_attackers, attacker.sp_attackers);
235         }
236         if(attacker.sp_defenders < army.sp_defenders) {
237             throw new InsufficientResourceError('sp_defenders', army.sp_defenders, attacker.sp_defenders);
238         }
239
240         // ok, it's a real army lets send it off!
241         attacker.soldiers -= army.soldiers;
242         attacker.attackers -= army.attackers;
243         attacker.defenders -= army.defenders;
244         attacker.sp_attackers -= army.sp_attackers;
245         attacker.sp_defenders -= army.sp_defenders;
246
247         await this.save(attacker);
248
249         return this.armyRepository.create(
250             attacker.owner,
251             attacker,
252             attacked,
253             army,
254             this.distanceInSeconds(attacker, attacked)
255         );
256     }
257
258     async getBuildQueues(owner: string): Promise<BuildQueue[]> {
259         return this.buildQueue.list(owner);
260     }
261
262     async getUnitTrainingQueues(owner: string): Promise<UnitTrainingQueue[]> {
263         return this.unitTrainigQueue.list(owner);
264     }
265 }