ability to cancel construction and have a portion of the funds returned
[browser-rts.git] / src / repository / city.ts
index 4aaa12778842291b4746efc9da8f156b588e75a1..c9ecb91b0aa358d96c9de6d4487087b328cd2670 100644 (file)
@@ -1,13 +1,14 @@
 import { v4 as uuid } from 'uuid';
 import { ERROR_CODE, InsufficientResourceError, NotFoundError } from '../errors';
 import {Repository} from './base';
+import * as config from '../config';
 import { BuildQueue, BuildQueueRepository } from './build-queue';
 import { DateTime, Duration } from 'luxon';
-import { UnitTrainingQueue, UnitTrainingQueueRepository } from './training-queue';
-import { coalesce } from '../lib/util';
+import { UnitTrainingQueue, UnitTrainingQueueRepository, UnitTrainingQueueWithName } from './training-queue';
+import { coalesce, pluck } from '../lib/util';
 import { Building, BuildingRepository } from './buildings';
 import { Unit, UnitRepository } from './unit';
-import _ from 'lodash';
+import _, { random } from 'lodash';
 import { Army, ArmyQueue, ArmyRepository } from './army';
 
 export type City = {
@@ -15,23 +16,36 @@ export type City = {
     owner: string;
     totalSpace: number;
     usedSpace: number;
-    gold: number;
-    ore: number;
-    logs: number;
-    bushels: number;
+    credits: number;
+    alloys: number;
+    energy: number;
+    food: number;
     population: number;
     soldiers: number;
     attackers: number;
     defenders: number;
     sp_attackers: number;
     sp_defenders: number;
+    homes: number;
     farms: number;
+    warehouses: number;
+    solar_panels: number;
+    accumulators: number;
+    mining_facilities: number;
+    ore_refinery: number;
     barracks: number;
     special_attacker_trainer: number;
     special_defender_trainer: number;
+    icon: string;
+    max_construction_queue: number;
+    max_training_queue: number;
+}
+
+export type CityWithLocation = {
+    sector_id: number;
     location_x: number;
     location_y: number;
-}
+} & City;
 
 export class CityRepository extends Repository<City> {
     buildQueue: BuildQueueRepository;
@@ -49,52 +63,128 @@ export class CityRepository extends Repository<City> {
         this.armyRepository = new ArmyRepository();
     }
 
-    async create(accountId: string): Promise<City> {
+    async create(accountId: string, rebel: boolean = false): Promise<CityWithLocation> {
+      const icon = rebel ? `/colony-ships/rebels/${random(1, 6)}.png` : '/colony-ships/01.png';
         const info: City = {
             id: uuid(),
             owner: accountId,
             totalSpace: 100,
             usedSpace: 0,
-            gold: 10000,
-            ore: 10000,
-            logs: 10000,
-            bushels: 10000,
+            credits: 10000,
+            alloys: 10000,
+            energy: 10000,
+            food: 10000,
             population: 1000,
             soldiers: 100,
             attackers: 0,
             defenders: 0,
             sp_attackers: 0,
             sp_defenders: 0,
-            farms: 0,
+            homes: 20,
+            farms: 5,
+            warehouses: 5,
+            solar_panels: 5,
+            accumulators: 5,
+            mining_facilities: 5,
+            ore_refinery: 5,
             barracks: 0,
             special_attacker_trainer: 0,
             special_defender_trainer: 0,
-            location_x: _.random(0, 100),
-            location_y: _.random(0, 100)
+            max_construction_queue: 2,
+            max_training_queue: 2,
+            icon
         };
 
+        await this.Insert(info);
+
         // placement can happen randomly
+        const availableSectors = config.AVAILABLE_SECTORS;
+        const sector = _.random(1, availableSectors);
 
-        await this.Insert(info);
+        const location = {
+            sector_id: await this.getAvailableSector(),
+            location_x: random(0, 25),
+            location_y: random(0, 25)
+        }
 
-        return info;
+        await this.db.raw('insert into locations (sector_id, city_id, location_x, location_y) values (?, ?, ?, ?)', [
+            location.sector_id,
+            info.id,
+            location.location_x,
+            location.location_y
+        ]);
+
+        return {
+            ...info,
+            sector_id: location.sector_id,
+            location_x: location.location_x,
+            location_y: location.location_y
+        };
     }
 
-    async save(city: City) {
-        await this.Save(city, {id: city.id});
-        return city;
+    async getAvailableSector(): Promise<number> {
+        // figure out which sectors have space (40% fill rate at 25x25);
+        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`);
+        const sample = _.sample(availableSectors.filter(sector => sector.count < 250)) as {count: number, sector_id: number};
+
+        if(!sample) {
+            return _.sortBy(availableSectors, 'sector_id').pop().sector_id+1;
+        }
+        
+        return sample.sector_id;
     }
 
-    async getUsersCity(owner: string): Promise<City> {
-        const city = await this.FindOne({
-            owner
-        });
+    async save(city: Partial<City>) {
+      if(!city.id) {
+        throw new Error('Unknown city to save');
+      }
+      const fieldsToSave = [
+        'totalSpace', 'usedSpace', 'credits', 'alloys', 'energy', 'food',
+        'poulation', 'soldiers', 'attackers', 'defenders', 'sp_attackers', 'sp_defenders',
+        'homes', 'farms', 'barracks', 'special_attacker_trainer', 'special_defender_trainer'
+      ];
+      
+      const finalData = {};
+
+      fieldsToSave.forEach(field => {
+        if(city.hasOwnProperty(field)) {
+          finalData[field] = city[field];
+        }
+      });
+
+      await this.Save(finalData, {id: city.id});
+      return city;
+    }
+
+    async findById(cityId: string): Promise<CityWithLocation> {
+        const city = await this.db.raw<CityWithLocation[]>(`select c.*, l.sector_id, l.location_x, l.location_y from cities c
+        join locations l on c.id = l.city_id 
+        where id = ? limit 1`, [cityId]);
 
         if(!city) {
             throw new NotFoundError('User has no city', ERROR_CODE.NO_CITY);
         }
 
-        return city;
+        return city.pop();
+
+    }
+
+    async getUsersCity(owner: string): Promise<CityWithLocation> {
+        const city = await this.db.raw<CityWithLocation[]>(`select c.*, l.sector_id, l.location_x, l.location_y from cities c
+        join locations l on c.id = l.city_id 
+        where owner = ? limit 1`, [owner]);
+
+        if(!city) {
+            throw new NotFoundError('User has no city', ERROR_CODE.NO_CITY);
+        }
+
+        return city.pop();
+    }
+
+    findAllInSector(sector_id: number): Promise<CityWithLocation[]> {
+        return this.db.raw<CityWithLocation[]>(`select c.*, l.sector_id, l.location_x, l.location_y from cities c
+join locations l on c.id = l.city_id 
+where l.sector_id = ?`, [sector_id]);
     }
 
     async buildBuilding(building: Building, amount: number, city: City): Promise<BuildQueue> {
@@ -104,22 +194,28 @@ export class CityRepository extends Repository<City> {
             throw new InsufficientResourceError('land', building.land, freeSpace);
         }
 
-        if(city.gold < building.gold) {
-            throw new InsufficientResourceError('gold', building.gold, city.gold);
+        if(city.credits < building.credits) {
+            throw new InsufficientResourceError('credits', building.credits, city.credits);
         }
 
-        if(city.ore < building.ore) {
-            throw new InsufficientResourceError('ore', building.ore, city.ore);
+        if(city.alloys < building.alloys) {
+            throw new InsufficientResourceError('alloys', building.alloys, city.alloys);
         }
 
-        if(city.logs < building.logs) {
-            throw new InsufficientResourceError('logs', building.logs, city.logs);
+        if(city.energy < building.energy) {
+            throw new InsufficientResourceError('energy', building.energy, city.energy);
+        }
+
+        // validate that they have enough empty construction queues
+        const concurrentConstruction = await this.buildQueue.list(city.owner);
+        if(concurrentConstruction.length >= city.max_construction_queue) {
+          throw new InsufficientResourceError('Construction queues', concurrentConstruction.length + 1, city.max_construction_queue);
         }
 
         city.usedSpace += (building.land * amount);
-        city.gold -= (building.gold * amount);
-        city.ore -= (building.ore * amount);
-        city.logs -= (building.logs * amount);
+        city.credits -= (building.credits * amount);
+        city.alloys -= (building.alloys * amount);
+        city.energy -= (building.energy * amount);
 
         await this.save(city);
 
@@ -139,28 +235,31 @@ export class CityRepository extends Repository<City> {
      * @param city1 
      * @param city2 
      */
-    distanceInSeconds(city1: City, city2: City): number {
+    distanceInSeconds(city1: CityWithLocation, city2: CityWithLocation): number {
         return this.distanceInHours(city1, city2) * 60 * 60;
     }
 
-    distanceInHours(city1: City, city2: City): number {
+    distanceInHours(city1: CityWithLocation, city2: CityWithLocation): number {
         const dist = Math.sqrt(
             Math.pow((city2.location_x - city1.location_x), 2) 
             + 
             Math.pow((city2.location_y - city1.location_y), 2)
         );
 
-        return _.round(dist/4, 2);
+        // sectors always add 4 hours
+        const sector_dist = Math.abs(city1.sector_id - city2.sector_id) * 6;
+
+        return _.round(dist/4, 2) + sector_dist;
 
     }
 
     async train(unit: Unit, amount: number, city: City): Promise<UnitTrainingQueue> {
-        if(city.gold < unit.gold) {
-            throw new InsufficientResourceError('gold', unit.gold, city.gold);
+        if(city.credits < unit.credits) {
+            throw new InsufficientResourceError('credits', unit.credits, city.credits);
         }
 
-        if(city.bushels < unit.bushels) {
-            throw new InsufficientResourceError('bushels', unit.bushels, city.bushels);
+        if(city.food < unit.food) {
+            throw new InsufficientResourceError('food', unit.food, city.food);
         }
 
         if(city.population < coalesce(unit.population, 0)) {
@@ -179,21 +278,40 @@ export class CityRepository extends Repository<City> {
             throw new InsufficientResourceError('defenders', unit.defenders, city.defenders);
         }
 
-        // validate that they have enough of the buildings to support this
+        // validate that they have enough empty training queues
+        const concurrentTraining = await this.unitTrainigQueue.list(city.owner);
+        if(concurrentTraining.length >= city.max_training_queue) {
+          throw new InsufficientResourceError('Training queues', concurrentTraining.length + 1, city.max_training_queue);
+        }
 
         // ok they have everything, lets update their city 
         // and create the entry in the training queue
 
-        city.gold -= unit.gold * amount;
-        city.bushels -= unit.bushels * amount;
-        city.population -= coalesce(unit.population, 0) * amount;
-        city.soldiers -= coalesce(unit.soldiers, 0) * amount;
-        city.attackers -= coalesce(unit.attackers, 0) * amount;
-        city.defenders -= coalesce(unit.defenders, 0) * amount;
+        city.credits -= unit.credits * amount;
+        city.food -= unit.food * amount;
+        city.population -= unit.population * amount;
+        city.soldiers -= unit.soldiers * amount;
+        city.attackers -= unit.attackers * amount;
+        city.defenders -= unit.defenders * amount;
+
+        console.log(city);
 
         await this.save(city);
 
-        const due = Duration.fromObject({ hours: unit.time});
+        // barracks can drop this by 0.01% for each barrack.
+
+        let additionalOffset = 0;
+        if(unit.slug === 'sp_attackers') {
+          additionalOffset = (this.spAttackerTraininerBoost(city) * unit.time);
+        }
+        else if(unit.slug === 'sp_defenders') {
+          additionalOffset = (this.spDefenderTraininerBoost(city) * unit.time);
+        }
+
+        const barracksOffset = _.round((this.barracksImprovement(city) * unit.time) + unit.time - additionalOffset, 2);
+
+        const due = Duration.fromObject({ hours: barracksOffset });
+
         const queue = await this.unitTrainigQueue.create(
             city.owner, 
             DateTime.now().plus({ milliseconds: due.as('milliseconds') }).toMillis(), 
@@ -219,7 +337,73 @@ export class CityRepository extends Repository<City> {
         return power
     }
 
-    async attack(attacker: City, attacked: City, army: Army): Promise<ArmyQueue> {
+    barracksImprovement(city: City): number {
+      return city.barracks * 0.0001;
+    }
+
+    spAttackerTraininerBoost(city: City): number {
+      return city.special_attacker_trainer * 0.002;
+    }
+
+    spDefenderTraininerBoost(city: City): number {
+      return city.special_defender_trainer * 0.002;
+    }
+
+    maxPopulation(city: City): number {
+      return city.homes * 25;
+    }
+
+    maxFood(city: City): number {
+      return city.warehouses * 250;
+    }
+
+    maxEnergy(city: City): number {
+      return city.accumulators * 150;
+    }
+
+    maxAlloy(city: City): number {
+      return city.ore_refinery * 75;
+    }
+
+    async foodProductionPerTick(city: City): Promise<number> {
+      // eventually we should supply the warehouse formula 
+      // to calculate the max amount of food created per tick
+      return city.farms * 50;
+    }
+
+    async foodUsagePerTick(city: City): Promise<number> {
+      return (
+        (city.soldiers * 0.5) + 
+        (city.population * 0.25) + 
+        (city.attackers * 0.75) + 
+        (city.attackers * 0.75) + 
+        (city.sp_attackers * 1.3) + 
+        (city.sp_defenders * 1.3)
+      )
+    }
+
+    async energyProductionPerTick(city: City): Promise<number> {
+      return city.solar_panels * 125;
+    }
+
+    async energyUsagePerTick(city: City): Promise<number> {
+      const buildings = await this.buildingRepository.list();
+      const buildingsMap = pluck<Building>(buildings, 'slug');
+      const totalEnergy = Math.ceil(_.sum([
+        city.farms * (buildingsMap['farms'].energy * 0.1),
+        city.barracks * (buildingsMap['barracks'].energy * 0.1),
+        city.special_defender_trainer * (buildingsMap['special_defender_trainer'].energy * 0.1),
+        city.special_attacker_trainer * (buildingsMap['special_attacker_trainer'].energy * 0.1),
+        city.homes * (buildingsMap['homes'].energy * 0.1),
+        city.warehouses * (buildingsMap['warehouses'].energy * 0.1),
+        city.solar_panels * (buildingsMap['solar_panels'].energy * 0.1),
+        city.mining_facilities * (buildingsMap['mining_facilities'].energy * 0.1),
+        city.ore_refinery * (buildingsMap['ore_refinery'].energy * 0.1)
+      ]));
+      return totalEnergy;
+    }
+
+    async attack(attacker: CityWithLocation, attacked: CityWithLocation, army: Army): Promise<ArmyQueue> {
         // validate the user has enough of a military! 
         if(attacker.soldiers < army.soldiers) {
             throw new InsufficientResourceError('soldiers', army.soldiers, attacker.soldiers);
@@ -259,7 +443,7 @@ export class CityRepository extends Repository<City> {
         return this.buildQueue.list(owner);
     }
 
-    async getUnitTrainingQueues(owner: string): Promise<UnitTrainingQueue[]> {
+    async getUnitTrainingQueues(owner: string): Promise<UnitTrainingQueueWithName[]> {
         return this.unitTrainigQueue.list(owner);
     }
 }