summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorDaniel Weipert <code@drogueronin.de>2023-09-24 13:40:25 +0200
committerDaniel Weipert <code@drogueronin.de>2023-09-24 13:40:25 +0200
commitfa00b957378a393f8edbfc98ef111d35d18ecb09 (patch)
tree654e7dc5414f7f2795dbe996d3e1570793a5b1b8 /src
initial commit
Diffstat (limited to 'src')
-rw-r--r--src/App.php32
-rw-r--r--src/Controller/Building.php49
-rw-r--r--src/Controller/Village.php54
-rw-r--r--src/DB.php78
-rw-r--r--src/EventRunner.php86
-rw-r--r--src/Model.php19
-rw-r--r--src/Model/Building.php102
-rw-r--r--src/Model/Building/ClayPit.php16
-rw-r--r--src/Model/Building/Farm.php31
-rw-r--r--src/Model/Building/IronMine.php16
-rw-r--r--src/Model/Building/ResourceGenerator.php22
-rw-r--r--src/Model/Building/Storage.php28
-rw-r--r--src/Model/Building/TownHall.php17
-rw-r--r--src/Model/Building/WoodCutter.php16
-rw-r--r--src/Model/Event.php32
-rw-r--r--src/Model/Event/TrainUnits.php27
-rw-r--r--src/Model/Event/UpgradeBuilding.php27
-rw-r--r--src/Model/Unit.php77
-rw-r--r--src/Model/Unit/Farmer.php15
-rw-r--r--src/Model/Unit/Miner.php15
-rw-r--r--src/Model/Unit/PitWorker.php15
-rw-r--r--src/Model/Unit/WoodCutter.php15
-rw-r--r--src/Model/User.php7
-rw-r--r--src/Model/Village.php135
-rw-r--r--src/Model/Village/StorageConfig.php18
-rw-r--r--src/Router.php80
-rw-r--r--src/Support/ResourceType.php11
-rw-r--r--src/Support/RouteLoader.php24
-rw-r--r--src/Support/UnitType.php11
-rw-r--r--src/View.php37
30 files changed, 1112 insertions, 0 deletions
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 @@
+<?php
+
+namespace App;
+
+use Symfony\Component\HttpFoundation\Request;
+
+class App
+{
+ public function __construct() {
+ if ($_ENV['APP_ENV'] === 'development') {
+ error_reporting(E_ALL);
+ }
+
+ // DB
+ DB::init();
+
+ // Router
+ Router::init(Request::createFromGlobals());
+
+ // View
+ View::init();
+
+ // Events
+ new EventRunner();
+ }
+
+ public function run(): void
+ {
+ $response = Router::execute();
+ $response->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 @@
+<?php
+
+namespace App\Controller;
+
+use App\DB;
+use App\Model\Building as Model;
+use App\Model\Event;
+use App\Model\Village;
+use App\Router;
+use Symfony\Component\HttpFoundation\RedirectResponse;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+use Symfony\Component\Routing\Annotation\Route;
+
+class Building
+{
+ #[Route(path: '/village/{x}/{y}/building/{type}/level-up', methods: ['POST'])]
+ public function levelUp(Request $request): Response
+ {
+ $village = Village::getByCoordinates($request->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 @@
+<?php
+
+namespace App\Controller;
+
+use App\DB;
+use App\Model\Event\UpgradeBuilding;
+use App\Model\Village as Model;
+use App\View;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+use Symfony\Component\Routing\Annotation\Route;
+
+class Village
+{
+ #[Route(path: '/villages', methods: ['GET'])]
+ public function list(): Response
+ {
+ $villages = DB::fetch(Model::class, "select * from villages");
+
+ return new Response(View::render('villages.twig', [
+ 'villages' => $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(
+ <<<SQL
+ select events.*, village_buildings.type as building from events
+ join village_buildings
+ on village_buildings.id=(events.payload->'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 @@
+<?php
+
+namespace App;
+
+class DB {
+ private static \PDO $connection;
+
+ public static function init(): void
+ {
+ $driver = $_ENV['DB_DRIVER'] ?? 'pgsql';
+ $host = $_ENV['DB_HOST'] ?? 'db';
+ $dbname = $_ENV['DB_NAME'];
+ $user = $_ENV['DB_USER'];
+ $password = $_ENV['DB_PASSWORD'];
+
+ self::$connection = new \PDO("pgsql:host=$host;dbname=$dbname", $user, $password);
+ }
+
+ /**
+ * @param string $query
+ * @param array $params
+ */
+ public static function query(string $query, array $params = []): \PDOStatement|false
+ {
+ /**@var \PDOStatement $statement*/
+ $statement = self::$connection->prepare($query);
+ $statement->execute($params);
+
+ return $statement;
+ }
+
+ /**
+ * @param string $class
+ * @param string $query
+ * @param array $params
+ *
+ * @return array<object>
+ */
+ 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 @@
+<?php
+
+namespace App;
+
+use App\Model\Building;
+use App\Model\Building\ClayPit;
+use App\Model\Building\Farm;
+use App\Model\Building\IronMine;
+use App\Model\Building\ResourceGenerator;
+use App\Model\Building\Storage;
+use App\Model\Building\WoodCutter;
+use App\Model\Event;
+use App\Model\Village;
+
+class EventRunner
+{
+ public function __construct()
+ {
+ $results = DB::query('select * from events where time < now()')->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<int, WoodCutter|ClayPit|IronMine|Farm> $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 @@
+<?php
+
+namespace App;
+
+class Model
+{
+ public static function castToType(object $original, string $cast): object
+ {
+ $object = new $cast();
+
+ foreach (get_class_vars(get_class($original)) as $property => $_) {
+ 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 @@
+<?php
+
+namespace App\Model;
+
+use App\DB;
+use App\Model;
+
+class Building
+{
+ public int $id;
+
+ public int $level;
+ public string $type;
+ public int $villageId;
+
+ public \DateTime $createdAt;
+ public \DateTime $updatedAt;
+
+ public string $unitType;
+ public int $buildTimeFactor;
+ public array $resourceRequirements;
+ public array $buildingRequirements;
+ public array $techRequirements;
+ public int $maxLevel;
+
+
+ public static function get(int $id): ?Building
+ {
+ $results = DB::fetch(Building::class, 'select * from village_buildings where id=:id', ['id' => $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,
+ <<<SQL
+ select village_buildings.*
+ from village_buildings
+ join villages on (villages.x=:x and villages.y=:y)
+ where village_buildings.village_id=villages.id and type=:type
+ SQL,
+ ['x' => $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<string, int>
+ */
+ public function getResourceRequirements(): array
+ {
+ return $this->getResourceRequirementsForLevel($this->level);
+ }
+
+ /**
+ * @return array<string, int>
+ */
+ 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 @@
+<?php
+
+namespace App\Model\Building;
+
+class ClayPit extends ResourceGenerator
+{
+ public string $unitType = 'PitWorker';
+ public int $buildTimeFactor = 1;
+ public int $maxLevel = 25;
+
+ public array $resourceRequirements = [
+ 'wood' => 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 @@
+<?php
+
+namespace App\Model\Building;
+
+use App\Model\Unit;
+use App\Model\Village;
+
+class Farm extends ResourceGenerator
+{
+ public string $unitType = 'Farmer';
+ public int $buildTimeFactor = 1;
+ public int $maxLevel = 25;
+
+ public array $resourceRequirements = [
+ 'wood' => 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 @@
+<?php
+
+namespace App\Model\Building;
+
+class IronMine extends ResourceGenerator
+{
+ public string $unitType = 'Miner';
+ public int $buildTimeFactor = 1;
+ public int $maxLevel = 25;
+
+ public array $resourceRequirements = [
+ 'wood' => 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 @@
+<?php
+
+namespace App\Model\Building;
+
+use App\Model\Building;
+use App\Model\Unit;
+
+class ResourceGenerator extends Building
+{
+ public string $resourceType;
+
+ public function getResourceIncrementor(): int
+ {
+ $amountResiding = Unit::getAmountResiding($this->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 @@
+<?php
+
+namespace App\Model\Building;
+
+use App\Model\Building;
+use App\Model\Village;
+
+class Storage extends Building
+{
+ public int $buildTimeFactor = 1;
+ public int $maxLevel = 25;
+
+ public array $resourceRequirements = [
+ 'wood' => 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 @@
+<?php
+
+namespace App\Model\Building;
+
+use App\Model\Building;
+
+class TownHall extends Building
+{
+ public int $buildTimeFactor = 1;
+ public int $maxLevel = 25;
+
+ public array $resourceRequirements = [
+ 'wood' => 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 @@
+<?php
+
+namespace App\Model\Building;
+
+class WoodCutter extends ResourceGenerator
+{
+ public string $unitType = 'WoodCutter';
+ public int $buildTimeFactor = 1;
+ public int $maxLevel = 25;
+
+ public array $resourceRequirements = [
+ 'wood' => 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 @@
+<?php
+
+namespace App\Model;
+
+use App\Model;
+
+class Event
+{
+ public int $id;
+
+ public string $type;
+ public \DateTime $time;
+ public string $payload;
+
+ public int $villageId;
+
+
+ /* OOP */
+
+ public function cast(): Event
+ {
+ $class = Event::resolveType($this->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 @@
+<?php
+
+namespace App\Model\Event;
+
+use App\DB;
+use App\Model\Event;
+
+class TrainUnits extends Event
+{
+ /**
+ * @return void
+ */
+ public function __invoke(): void
+ {
+ $payload = json_decode($this->payload, true);
+
+ DB::query(
+ <<<SQL
+ insert into village_units (amount, type, is_traveling, home_village_id, residence_village_id)
+ values (:amount, :type, false, :id, :id)
+ on conflict (type, home_village_id, residence_village_id)
+ do update set amount = excluded.amount+:amount
+ SQL,
+ ['amount' => $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 @@
+<?php
+
+namespace App\Model\Event;
+
+use App\DB;
+use App\Model\Event;
+
+class UpgradeBuilding extends Event
+{
+ /**
+ * @return void
+ */
+ public function __invoke(): void
+ {
+ $payload = json_decode($this->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 @@
+<?php
+
+namespace App\Model;
+
+use App\DB;
+use App\Model;
+
+class Unit
+{
+ public int $id;
+
+ public int $amount;
+ public string $type;
+ public bool $isTraveling;
+
+ public int $homeVillageId;
+ public int $residenceVillageId;
+
+ public string $createdAt;
+ public string $updatedAt;
+
+ public string $buildingType;
+ public int $travelTime;
+ public int $populationDemandFactor;
+ public array $resourceRequirements = [];
+
+
+ public function getBuildTime(int $amount): int
+ {
+ return intval(($_ENV['BASE_UNIT_BUILD_TIME_FACTOR'] / ($this->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 @@
+<?php
+
+namespace App\Model\Unit;
+
+use App\Model\Unit;
+
+class Farmer extends Unit
+{
+ public string $buildingType = 'Farm';
+ public int $travelTime = 1;
+ public int $populationDemandFactor = 1;
+ public array $resourceRequirements = [
+ 'wood' => 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 @@
+<?php
+
+namespace App\Model\Unit;
+
+use App\Model\Unit;
+
+class Miner extends Unit
+{
+ public string $buildingType = 'IronMine';
+ public int $travelTime = 1;
+ public int $populationDemandFactor = 1;
+ public array $resourceRequirements = [
+ 'wood' => 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 @@
+<?php
+
+namespace App\Model\Unit;
+
+use App\Model\Unit;
+
+class PitWorker extends Unit
+{
+ public string $buildingType = 'ClayPit';
+ public int $travelTime = 1;
+ public int $populationDemandFactor = 1;
+ public array $resourceRequirements = [
+ 'wood' => 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 @@
+<?php
+
+namespace App\Model\Unit;
+
+use App\Model\Unit;
+
+class WoodCutter extends Unit
+{
+ public string $buildingType = 'WoodCutter';
+ public int $travelTime = 1;
+ public int $populationDemandFactor = 1;
+ public array $resourceRequirements = [
+ 'wood' => 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 @@
+<?php
+
+namespace App\Model;
+
+class User
+{
+}
diff --git a/src/Model/Village.php b/src/Model/Village.php
new file mode 100644
index 0000000..cd1c749
--- /dev/null
+++ b/src/Model/Village.php
@@ -0,0 +1,135 @@
+<?php
+
+namespace App\Model;
+
+use App\DB;
+use App\Model\Building\Storage;
+use App\Model\Village\StorageConfig;
+
+class Village
+{
+ public int $id;
+
+ public string $name;
+
+ public int $x;
+ public int $y;
+
+ public int $wood;
+ public int $clay;
+ public int $iron;
+ public int $food;
+
+ public int $satisfaction;
+
+ public string $createdAt;
+ public string $updatedAt;
+
+ public static function canBuild(Village $village, Building $building): bool
+ {
+ if ($building->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<int, Unit>
+ */
+ 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 @@
+<?php
+
+namespace App\Model\Village;
+
+class StorageConfig
+{
+ public int $id;
+
+ public int $wood;
+ public int $clay;
+ public int $iron;
+ public int $food;
+
+ public int $villageId;
+
+ public string $createdAt;
+ public string $updatedAt;
+}
diff --git a/src/Router.php b/src/Router.php
new file mode 100644
index 0000000..8b24000
--- /dev/null
+++ b/src/Router.php
@@ -0,0 +1,80 @@
+<?php
+
+namespace App;
+
+use App\Support\RouteLoader;
+use Symfony\Component\Config\FileLocator;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+use Symfony\Component\Routing\Exception\MethodNotAllowedException;
+use Symfony\Component\Routing\Exception\ResourceNotFoundException;
+use Symfony\Component\Routing\Generator\UrlGenerator;
+use Symfony\Component\Routing\Loader\AnnotationFileLoader;
+use Symfony\Component\Routing\Matcher\UrlMatcher;
+use Symfony\Component\Routing\RequestContext;
+use Symfony\Component\Routing\RouteCollection;
+
+class Router
+{
+ public static Request $request;
+ public static RequestContext $context;
+ public static RouteCollection $routes;
+
+ public static function init(Request $request): void
+ {
+ self::$request = $request;
+
+ self::$context = new RequestContext();
+ self::$context->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 @@
+<?php
+
+namespace App\Support;
+
+enum ResourceType: string
+{
+ case Wood = 'Wood';
+ case Clay = 'Clay';
+ case Iron = 'Iron';
+ case Food = 'Food';
+}
diff --git a/src/Support/RouteLoader.php b/src/Support/RouteLoader.php
new file mode 100644
index 0000000..ba124c5
--- /dev/null
+++ b/src/Support/RouteLoader.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace App\Support;
+
+use Symfony\Component\Routing\Loader\AnnotationClassLoader;
+use Symfony\Component\Routing\Route;
+
+class RouteLoader extends AnnotationClassLoader
+{
+ protected function configureRoute(Route $route, \ReflectionClass $class, \ReflectionMethod $method, object $annotation) {
+ $route->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 @@
+<?php
+
+namespace App\Support;
+
+enum UnitType: string
+{
+ case WoodCutter = 'WoodCutter';
+ case PitWorker = 'PitWorker';
+ case Miner = 'Miner';
+ case Farmer = 'Farmer';
+}
diff --git a/src/View.php b/src/View.php
new file mode 100644
index 0000000..5f9ca34
--- /dev/null
+++ b/src/View.php
@@ -0,0 +1,37 @@
+<?php
+
+namespace App;
+
+use Twig\Environment;
+use Twig\Extension\DebugExtension;
+use Twig\Loader\FilesystemLoader;
+use Twig\TwigFilter;
+
+class View
+{
+ private static Environment $twig;
+
+ public static function init(): void
+ {
+ $loader = new FilesystemLoader(dirname(__DIR__) . '/views');
+ self::$twig = new Environment($loader, [
+ 'debug' => $_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);
+ }
+}