From fa00b957378a393f8edbfc98ef111d35d18ecb09 Mon Sep 17 00:00:00 2001 From: Daniel Weipert Date: Sun, 24 Sep 2023 13:40:25 +0200 Subject: initial commit --- src/App.php | 32 ++++++++ src/Controller/Building.php | 49 +++++++++++ src/Controller/Village.php | 54 +++++++++++++ src/DB.php | 78 ++++++++++++++++++ src/EventRunner.php | 86 ++++++++++++++++++++ src/Model.php | 19 +++++ src/Model/Building.php | 102 +++++++++++++++++++++++ src/Model/Building/ClayPit.php | 16 ++++ src/Model/Building/Farm.php | 31 +++++++ src/Model/Building/IronMine.php | 16 ++++ src/Model/Building/ResourceGenerator.php | 22 +++++ src/Model/Building/Storage.php | 28 +++++++ src/Model/Building/TownHall.php | 17 ++++ src/Model/Building/WoodCutter.php | 16 ++++ src/Model/Event.php | 32 ++++++++ src/Model/Event/TrainUnits.php | 27 +++++++ src/Model/Event/UpgradeBuilding.php | 27 +++++++ src/Model/Unit.php | 77 ++++++++++++++++++ src/Model/Unit/Farmer.php | 15 ++++ src/Model/Unit/Miner.php | 15 ++++ src/Model/Unit/PitWorker.php | 15 ++++ src/Model/Unit/WoodCutter.php | 15 ++++ src/Model/User.php | 7 ++ src/Model/Village.php | 135 +++++++++++++++++++++++++++++++ src/Model/Village/StorageConfig.php | 18 +++++ src/Router.php | 80 ++++++++++++++++++ src/Support/ResourceType.php | 11 +++ src/Support/RouteLoader.php | 24 ++++++ src/Support/UnitType.php | 11 +++ src/View.php | 37 +++++++++ 30 files changed, 1112 insertions(+) create mode 100644 src/App.php create mode 100644 src/Controller/Building.php create mode 100644 src/Controller/Village.php create mode 100644 src/DB.php create mode 100644 src/EventRunner.php create mode 100644 src/Model.php create mode 100644 src/Model/Building.php create mode 100644 src/Model/Building/ClayPit.php create mode 100644 src/Model/Building/Farm.php create mode 100644 src/Model/Building/IronMine.php create mode 100644 src/Model/Building/ResourceGenerator.php create mode 100644 src/Model/Building/Storage.php create mode 100644 src/Model/Building/TownHall.php create mode 100644 src/Model/Building/WoodCutter.php create mode 100644 src/Model/Event.php create mode 100644 src/Model/Event/TrainUnits.php create mode 100644 src/Model/Event/UpgradeBuilding.php create mode 100644 src/Model/Unit.php create mode 100644 src/Model/Unit/Farmer.php create mode 100644 src/Model/Unit/Miner.php create mode 100644 src/Model/Unit/PitWorker.php create mode 100644 src/Model/Unit/WoodCutter.php create mode 100644 src/Model/User.php create mode 100644 src/Model/Village.php create mode 100644 src/Model/Village/StorageConfig.php create mode 100644 src/Router.php create mode 100644 src/Support/ResourceType.php create mode 100644 src/Support/RouteLoader.php create mode 100644 src/Support/UnitType.php create mode 100644 src/View.php (limited to 'src') diff --git a/src/App.php b/src/App.php new file mode 100644 index 0000000..f1fa97e --- /dev/null +++ b/src/App.php @@ -0,0 +1,32 @@ +send(); + } +} diff --git a/src/Controller/Building.php b/src/Controller/Building.php new file mode 100644 index 0000000..d8fe656 --- /dev/null +++ b/src/Controller/Building.php @@ -0,0 +1,49 @@ +get('x'), $request->get('y')); + $building = Model::getByVillage($village->id, $request->get('type')); + + // resources + foreach ($building->getResourceRequirements() as $resourceType => $resourceValue) { + $village->{$resourceType} -= $resourceValue; + } + $village->updateResources(); + + // event + $event = new Event(); + $event->type = 'UpgradeBuilding'; + $event->time = (new \DateTime())->add(\DateInterval::createFromDateString($building->getBuildTime() . ' seconds')); + $event->payload = json_encode([ + 'id' => $building->id, + ]); + + DB::query( + 'insert into events (type, time, payload, village_id) VALUES (:type, :time, :payload, :id)', + ['type' => $event->type, 'time' => $event->time->format('c'), 'payload' => $event->payload, 'id' => $village->id] + ); + + return new RedirectResponse( + Router::generate( + 'village.show', + ['x' => $request->get('x'), 'y' => $request->get('y')] + ) + ); + } +} diff --git a/src/Controller/Village.php b/src/Controller/Village.php new file mode 100644 index 0000000..3854d29 --- /dev/null +++ b/src/Controller/Village.php @@ -0,0 +1,54 @@ + $villages, + ])); + } + + #[Route(path: '/village/{x}/{y}', methods: ['GET'])] + public function show(Request $request): Response + { + $village = Model::getByCoordinates($request->get('x'), $request->get('y')); + + $results = DB::query( + <<'id')::bigint + where events.village_id=:id and events.type=:type + SQL, ['id' => $village->id, 'type' => 'UpgradeBuilding'] + )->fetchAll(); + + $events = []; + foreach ($results as $row) { + $events[$row['type']][] = [ + 'event' => DB::convertToModel(UpgradeBuilding::class, $row), + 'data' => [ + 'building' => $row['building'], + ], + ]; + } + + return new Response(View::render('village.twig', [ + 'village' => $village, + 'events' => $events, + ])); + } +} diff --git a/src/DB.php b/src/DB.php new file mode 100644 index 0000000..4e34d32 --- /dev/null +++ b/src/DB.php @@ -0,0 +1,78 @@ +prepare($query); + $statement->execute($params); + + return $statement; + } + + /** + * @param string $class + * @param string $query + * @param array $params + * + * @return array + */ + public static function fetch(string $class, string $query, array $params = []): array + { + $rows = DB::query($query, $params)->fetchAll(\PDO::FETCH_ASSOC); + + $results = []; + foreach ($rows as $row) { + $results[] = DB::convertToModel($class, $row); + } + + return $results; + } + + /** + * @param string $class + * @param array $row + * + * @return object + */ + public static function convertToModel(string $class, array $row): object + { + $object = new $class(); + + foreach ($row as $columnKey => $columnValue) { + $objectKey = explode('_', $columnKey); + $objectKey = $objectKey[0] . implode('', array_map('ucwords', array_slice($objectKey, 1))); + + if (property_exists($object, $objectKey)) { + $propertyType = (new \ReflectionProperty($object, $objectKey))->getType(); + + if (class_exists($propertyType->getName())) { + $object->$objectKey = new ($propertyType->getName())($columnValue); + } else { + $object->$objectKey = $columnValue; + } + } + } + + return $object; + } +} diff --git a/src/EventRunner.php b/src/EventRunner.php new file mode 100644 index 0000000..d2f1589 --- /dev/null +++ b/src/EventRunner.php @@ -0,0 +1,86 @@ +fetchAll(); + + foreach ($results as $row) { + /**@var Event $event*/ + $event = DB::convertToModel(Event::resolveType($row['type']), $row); + $event(); + + DB::query( + 'delete from events where id=:id', + ['id' => $event->id] + ); + } + + + // Resources + + $lastTick = json_decode(DB::query('select value from system where key=:key', ['key' => 'last_resource_tick'])->fetchColumn()); + if ($lastTick) { + $lastTick = new \DateTime($lastTick); + } else { + $lastTick = (new \DateTime())->modify('- 1 min'); + } + + $diff = (new \DateTime())->diff($lastTick); + $tickMultiplier = $diff->i; + + if ($tickMultiplier > 0) { + $villages = DB::fetch(Village::class, 'select id,wood,clay,iron,food from villages'); + foreach ($villages as $village) { + /**@var Village $village*/ + + /**@var array $resourceGenerators*/ + $resourceGenerators = []; + + /**@var Storage $storage*/ + $storage = null; + + /**@var Building[] $buildings*/ + $buildings = DB::fetch(ResourceGenerator::class, 'select level,type,village_id from village_buildings where village_id=:id', ['id' => $village->id]); + foreach ($buildings as $building) { + if ($building->type == 'Storage') { + $storage = $building->cast(); + } + else if (in_array($building->type, ['WoodCutter', 'ClayPit', 'IronMine', 'Farm'])) { + $resourceGenerators[] = $building->cast(); + } + } + + $resources = []; + foreach ($resourceGenerators as $generator) { + $village->{$generator->resourceType} = min( + $village->{$generator->resourceType} + ($generator->getResourceIncrementor() * $tickMultiplier), + $storage->getResourceCapacity($generator->resourceType) + ); + } + + DB::query( + 'update villages set wood=:wood, clay=:clay, iron=:iron, food=:food where id=:id', + ['wood' => $village->wood, 'clay' => $village->clay, 'iron' => $village->iron, 'food' => $village->food, 'id' => $village->id] + ); + } + + DB::query('delete from system where key=:key', ['key' => 'last_resource_tick']); + $value = (new \DateTime((new \DateTime())->format('Y-m-d H:i')))->format('c'); + DB::query('insert into system (key,value) VALUES (:key,:value)', ['key' => 'last_resource_tick', 'value' => json_encode($value)]); + } + } +} diff --git a/src/Model.php b/src/Model.php new file mode 100644 index 0000000..5f45ed4 --- /dev/null +++ b/src/Model.php @@ -0,0 +1,19 @@ + $_) { + if (! empty($original->$property) && empty($object->$property)) { + $object->$property = $original->$property; + } + } + + return $object; + } +} diff --git a/src/Model/Building.php b/src/Model/Building.php new file mode 100644 index 0000000..eb166f9 --- /dev/null +++ b/src/Model/Building.php @@ -0,0 +1,102 @@ + $id]); + + return isset($results[0]) ? $results[0]->cast() : null; + } + + public static function getByVillage(int $villageId, string $buildingType): ?Building + { + $results = DB::fetch(Building::class, 'select * from village_buildings where village_id=:id and type=:type', ['id' => $villageId, 'type' => $buildingType]); + + return isset($results[0]) ? $results[0]->cast() : null; + } + + public static function getByVillageCoordinates(int $x, int $y, string $buildingType): ?Building + { + $results = DB::fetch( + Building::class, + << $x, 'y' => $y, 'type' => $buildingType] + ); + + return isset($results[0]) ? $results[0]->cast() : null; + } + + + public function getBuildTime(): int + { + $townHall = Village::getBuilding($this->villageId, 'TownHall'); + + $nextLevel = $this->level + 1; + + return intval($nextLevel * ($nextLevel / $townHall->level) * $_ENV['BASE_BUILDING_BUILD_TIME_FACTOR'] * $this->buildTimeFactor); + } + + /** + * @return array + */ + public function getResourceRequirements(): array + { + return $this->getResourceRequirementsForLevel($this->level); + } + + /** + * @return array + */ + public function getResourceRequirementsForLevel(int $level): array + { + $level += 1; + + return array_map( + fn ($resourceRequirement) => ceil(log($level * 2) * $resourceRequirement * 64 * $level), + $this->resourceRequirements + ); + } + + + /* OOP */ + + public function cast(): Building + { + $class = Building::resolveType($this->type); + + return Model::castToType($this, Building::resolveType($this->type)); + } + + public static function resolveType(string $type): string + { + return __NAMESPACE__ . '\\Building\\' . $type; + } +} diff --git a/src/Model/Building/ClayPit.php b/src/Model/Building/ClayPit.php new file mode 100644 index 0000000..8127818 --- /dev/null +++ b/src/Model/Building/ClayPit.php @@ -0,0 +1,16 @@ + 1.0, + ]; + + public string $resourceType = 'clay'; +} diff --git a/src/Model/Building/Farm.php b/src/Model/Building/Farm.php new file mode 100644 index 0000000..aaa58b5 --- /dev/null +++ b/src/Model/Building/Farm.php @@ -0,0 +1,31 @@ + 1.0, + ]; + + public string $resourceType = 'food'; + + public function getResourceIncrementor(): int + { + $populationDemand = array_reduce( + Village::getUnits($this->villageId, Village::FETCH_UNIT_RESIDENCE), + function ($carry, Unit $unit) { + return $carry + $unit->getPopulationDemand(); + } + ); + + return parent::getResourceIncrementor() - $populationDemand; + } +} diff --git a/src/Model/Building/IronMine.php b/src/Model/Building/IronMine.php new file mode 100644 index 0000000..4bf5cc6 --- /dev/null +++ b/src/Model/Building/IronMine.php @@ -0,0 +1,16 @@ + 1.0, + ]; + + public string $resourceType = 'iron'; +} diff --git a/src/Model/Building/ResourceGenerator.php b/src/Model/Building/ResourceGenerator.php new file mode 100644 index 0000000..5f1a6bb --- /dev/null +++ b/src/Model/Building/ResourceGenerator.php @@ -0,0 +1,22 @@ +unitType, $this->villageId); + + return (int)ceil( + log( + ($this->level * $amountResiding) + 1 + ) * $_ENV['BASE_RESOURCE_GENERATION_FACTOR'] + ); + } +} diff --git a/src/Model/Building/Storage.php b/src/Model/Building/Storage.php new file mode 100644 index 0000000..fde4c4e --- /dev/null +++ b/src/Model/Building/Storage.php @@ -0,0 +1,28 @@ + 1.0, + ]; + + public function getCapacity(): int + { + return $this->level * 2560; + } + + public function getResourceCapacity(string $resourceType): int + { + $p = Village::getStorageConfig($this->villageId)->$resourceType / 100; + + return ceil($this->getCapacity() * $p); + } +} diff --git a/src/Model/Building/TownHall.php b/src/Model/Building/TownHall.php new file mode 100644 index 0000000..608f083 --- /dev/null +++ b/src/Model/Building/TownHall.php @@ -0,0 +1,17 @@ + 1.0, + 'clay' => 1.0, + 'iron' => 1.0, + ]; +} diff --git a/src/Model/Building/WoodCutter.php b/src/Model/Building/WoodCutter.php new file mode 100644 index 0000000..86bde9b --- /dev/null +++ b/src/Model/Building/WoodCutter.php @@ -0,0 +1,16 @@ + 1.0, + ]; + + public string $resourceType = 'wood'; +} diff --git a/src/Model/Event.php b/src/Model/Event.php new file mode 100644 index 0000000..aa235f9 --- /dev/null +++ b/src/Model/Event.php @@ -0,0 +1,32 @@ +type); + $object = new $class(); + + return Model::castToType($this, Event::resolveType($this->type)); + } + + public static function resolveType(string $type): string + { + return __NAMESPACE__ . '\\Event\\' . $type; + } +} diff --git a/src/Model/Event/TrainUnits.php b/src/Model/Event/TrainUnits.php new file mode 100644 index 0000000..0c7e0de --- /dev/null +++ b/src/Model/Event/TrainUnits.php @@ -0,0 +1,27 @@ +payload, true); + + DB::query( + << $payload['amount'], 'type' => $payload['type'], 'id' => $this->villageId] + ); + } +} diff --git a/src/Model/Event/UpgradeBuilding.php b/src/Model/Event/UpgradeBuilding.php new file mode 100644 index 0000000..c014cfe --- /dev/null +++ b/src/Model/Event/UpgradeBuilding.php @@ -0,0 +1,27 @@ +payload, true); + + DB::query( + 'update village_buildings set level=level+1 where id=:id', + ['id' => $payload['id']] + ); + + DB::query( + 'delete from events where id=:id', + ['id' => $this->id] + ); + } +} diff --git a/src/Model/Unit.php b/src/Model/Unit.php new file mode 100644 index 0000000..a0d1a35 --- /dev/null +++ b/src/Model/Unit.php @@ -0,0 +1,77 @@ +getBuilding()->level ?: 1)) * $amount); + } + + public function getPopulationDemand(): int + { + return $this->getPopulationDemandForAmount($this->amount); + } + + public function getPopulationDemandForAmount(int $amount): int + { + return $amount * $this->populationDemandFactor; + } + + + /* Relations */ + + public function getBuilding(): ?Building + { + return Village::getBuilding($this->homeVillageId, $this->buildingType); + } + + public function cast(): Unit + { + $class = Unit::resolveType($this->type); + + return Model::castToType($this, Unit::resolveType($this->type)); + } + + public static function resolveType(string $type): string + { + return __NAMESPACE__ . '\\Unit\\' . $type; + } + + + /* Static */ + + public static function getAmountResiding(string $unitType, int $villageId): int + { + $statement = DB::query( + 'select SUM(amount) from village_units where type=:type and residence_village_id=:id', + ['type' => $unitType, 'id' => $villageId] + ); + $result = $statement->fetch()['sum']; + + return intval($result); + } + +} diff --git a/src/Model/Unit/Farmer.php b/src/Model/Unit/Farmer.php new file mode 100644 index 0000000..de37802 --- /dev/null +++ b/src/Model/Unit/Farmer.php @@ -0,0 +1,15 @@ + 1.0, + ]; +} diff --git a/src/Model/Unit/Miner.php b/src/Model/Unit/Miner.php new file mode 100644 index 0000000..ae6c00a --- /dev/null +++ b/src/Model/Unit/Miner.php @@ -0,0 +1,15 @@ + 1.0, + ]; +} diff --git a/src/Model/Unit/PitWorker.php b/src/Model/Unit/PitWorker.php new file mode 100644 index 0000000..4f873b4 --- /dev/null +++ b/src/Model/Unit/PitWorker.php @@ -0,0 +1,15 @@ + 1.0, + ]; +} diff --git a/src/Model/Unit/WoodCutter.php b/src/Model/Unit/WoodCutter.php new file mode 100644 index 0000000..17923ca --- /dev/null +++ b/src/Model/Unit/WoodCutter.php @@ -0,0 +1,15 @@ + 1.0, + ]; +} diff --git a/src/Model/User.php b/src/Model/User.php new file mode 100644 index 0000000..fcb1869 --- /dev/null +++ b/src/Model/User.php @@ -0,0 +1,7 @@ +level >= $building->maxLevel) { + return false; + } + + $resourceRequirements = $building->getResourceRequirements(); + foreach ($resourceRequirements as $resourceType => $requirement) { + if ($village->$resourceType < $requirement) { + return false; + } + } + + return true; + } + + /* DB - Actions */ + + public static function get(int $id): ?Village + { + return DB::fetch(Village::class, 'select * from villages where id=:id', ['id' => $id])[0] ?? null; + } + + public static function getByCoordinates(int $x, int $y): ?Village + { + return DB::fetch(Village::class, 'select * from villages where x=:x and y=:y', ['x' => $x, 'y' => $y])[0] ?? null; + } + + public function updateResources(): mixed + { + return DB::query( + 'update villages set wood=:wood,clay=:clay,iron=:iron,food=:food where id=:id', + ['wood' => $this->wood, 'clay' => $this->clay, 'iron' => $this->iron, 'food' => $this->food, 'id' => $this->id] + ); + } + + /* DB - Relations */ + + public static function getBuildings(int $villageId): array + { + $buildings = DB::fetch(Building::class, 'select * from village_buildings where village_id=:id', ['id' => $villageId]); + + return array_map(function (Building $building) { + return $building->cast(); + }, $buildings); + } + + public static function getBuilding(int $villageId, string $buildingType): ?Building + { + $results = DB::fetch( + Building::resolveType($buildingType), + 'select * from village_buildings where village_id=:id and type=:type', + ['id' => $villageId, 'type' => $buildingType] + ); + + return isset($results[0]) ? $results[0]->cast() : null; + } + + public static function getStorage(int $villageId): ?Storage + { + return Village::getBuilding($villageId, 'Storage'); + } + + public static function getStorageConfig(int $villageId): ?StorageConfig + { + $results = DB::fetch( + StorageConfig::class, + 'select * from village_storage_config where village_id=:id', + ['id' => $villageId] + ); + + return $results[0] ?? null; + } + + public const FETCH_UNIT_HOME_AT_HOME = 1; + public const FETCH_UNIT_HOME_AT_SUPPORT = 2; + public const FETCH_UNIT_SUPPORT_AT_HOME = 3; + public const FETCH_UNIT_RESIDENCE = 4; + + public static function getUnit(string $unitType, int $flag): ?Unit + { + } + + /** + * @param int $flag + * + * @return array + */ + public static function getUnits(int $villageId, $flag = Village::FETCH_UNIT_ALL): array + { + if ($flag == Village::FETCH_UNIT_HOME_AT_HOME) { + $units = DB::fetch(Unit::class, 'select * from village_units where home_village_id=:id and residence_village_id=:id', ['id' => $villageId]); + } + else if ($flag == Village::FETCH_UNIT_HOME_AT_SUPPORT) { + $units = DB::fetch(Unit::class, 'select * from village_units where home_village_id=:id and residence_village_id!=:id', ['id' => $villageId]); + } + else if ($flag == Village::FETCH_UNIT_SUPPORT_AT_HOME) { + $units = DB::fetch(Unit::class, 'select * from village_units where home_village_id!=:id and residence_village_id=:id', ['id' => $villageId]); + } + else if ($flag == Village::FETCH_UNIT_RESIDENCE) { + $units = DB::fetch(Unit::class, 'select * from village_units where residence_village_id=:id', ['id' => $villageId]); + } + + return array_map(function (Unit $unit) { + return $unit->cast(); + }, $units); + } +} diff --git a/src/Model/Village/StorageConfig.php b/src/Model/Village/StorageConfig.php new file mode 100644 index 0000000..272eaf5 --- /dev/null +++ b/src/Model/Village/StorageConfig.php @@ -0,0 +1,18 @@ +fromRequest($request); + + self::$routes = new RouteCollection(); + $loader = new AnnotationFileLoader(new FileLocator(), new RouteLoader()); + $iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator(__DIR__ . '/Controller')); + foreach ($iterator as $file) { + /**@var \SplFileInfo $file*/ + if (in_array($file->getFilename(), ['.', '..'])) continue; + + $collection = $loader->load($file->getPathname(), 'attribute'); + self::$routes->addCollection($collection); + } + } + + public static function execute(): Response + { + try { + $matcher = new UrlMatcher(self::$routes, self::$context); + $match = $matcher->matchRequest(self::$request); + + foreach ($match as $key => $value) { + if (str_starts_with($key, '_')) continue; + + self::$request->query->set($key, $value); + } + + /**@var \ReflectionClass $class*/ + $class = $match['_']['class']; + /**@var \ReflectionMethod $method*/ + $method = $match['_']['method']; + + return ($class->newInstance())->{$method->getName()}(self::$request); + } catch (ResourceNotFoundException $exception) { + return new Response('404', 404); + } catch (MethodNotAllowedException $exception) { + return new Response('403', 403); + } catch (\Exception $exception) { + return new Response('500: ' . $exception->getMessage(), 500); + } + } + + /** + * @param string $name + * @param array $parameters + * @param int $referenceType + */ + public static function generate(string $name, array $parameters = [], int $referenceType = 1): string + { + $generator = new UrlGenerator(self::$routes, self::$context); + + return $generator->generate($name, $parameters, $referenceType); + } +} diff --git a/src/Support/ResourceType.php b/src/Support/ResourceType.php new file mode 100644 index 0000000..96c0f2c --- /dev/null +++ b/src/Support/ResourceType.php @@ -0,0 +1,11 @@ +setDefault('_', compact('class', 'method', 'annotation')); + } + + protected function getDefaultRouteName(\ReflectionClass $class, \ReflectionMethod $method) + { + $name = parent::getDefaultRouteName($class, $method); + + return str_replace( + '_', + '.', + str_replace('app_controller_', '', $name) + ); + } +} diff --git a/src/Support/UnitType.php b/src/Support/UnitType.php new file mode 100644 index 0000000..5a53f24 --- /dev/null +++ b/src/Support/UnitType.php @@ -0,0 +1,11 @@ + $_ENV['APP_ENV'] === 'development', + ]); + + self::$twig->addExtension(new DebugExtension()); + // self::$twig->addExtension(new IntlExtension()); + + self::$twig->addFilter(new TwigFilter('buildTime', function ($buildTime) { + return @sprintf('%02d:%02d:%02d', $buildTime / 3600, ($buildTime / 60) % 60, $buildTime % 60); + })); + } + + /** + * @param string $name + * @param array $context + */ + public static function render(string $name, array $context = []): string + { + return self::$twig->render($name, $context); + } +} -- cgit v1.2.3