summaryrefslogtreecommitdiff
path: root/src/gemini
diff options
context:
space:
mode:
Diffstat (limited to 'src/gemini')
-rw-r--r--src/gemini/Controller/Building.php44
-rw-r--r--src/gemini/Controller/Unit.php60
-rw-r--r--src/gemini/Controller/User.php118
-rw-r--r--src/gemini/Controller/Village.php167
-rw-r--r--src/gemini/Gemini.php120
5 files changed, 509 insertions, 0 deletions
diff --git a/src/gemini/Controller/Building.php b/src/gemini/Controller/Building.php
new file mode 100644
index 0000000..9827d00
--- /dev/null
+++ b/src/gemini/Controller/Building.php
@@ -0,0 +1,44 @@
+<?php
+
+namespace App\gemini\Controller;
+
+use App\Model\Building as Model;
+use App\Model\Event;
+use App\Model\Event\UpgradeBuilding;
+use App\Model\Village;
+use GeminiFoundation\Request;
+use GeminiFoundation\Response;
+use GeminiFoundation\Status;
+
+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')) ?? Model::getEmpty($village->id, $request->get('type'));
+
+ // resources
+ foreach ($building->getResourceRequirements() as $resourceType => $resourceValue) {
+ $village->{$resourceType} -= $resourceValue;
+ }
+ $village->updateResources();
+
+ // event
+ $event = new Event();
+ $event->time = (new \DateTime())->add(\DateInterval::createFromDateString(
+ $building->getBuildTimeForLevel($building->getEffectiveLevel() + 1) . ' seconds'
+ ));
+ $event->villageId = $building->villageId;
+ $upgradeBuildingEvent = new UpgradeBuilding();
+ $upgradeBuildingEvent->event = $event;
+ $upgradeBuildingEvent->type = $building->type;
+ $upgradeBuildingEvent->dbInsert();
+
+
+ return new Response(
+ statusCode: Status::REDIRECT_TEMPORARY, # correct response code?
+ meta: "/village/{$village->x}/{$village->y}"
+ );
+ }
+}
diff --git a/src/gemini/Controller/Unit.php b/src/gemini/Controller/Unit.php
new file mode 100644
index 0000000..c04079a
--- /dev/null
+++ b/src/gemini/Controller/Unit.php
@@ -0,0 +1,60 @@
+<?php
+
+namespace App\gemini\Controller;
+
+use App\Model\Event;
+use App\Model\Event\TrainUnits;
+use App\Model\Unit as Model;
+use App\Model\Village;
+use GeminiFoundation\Request;
+use GeminiFoundation\Response;
+use GeminiFoundation\Status;
+
+class Unit
+{
+ // #[Route(path: '/village/{x}/{y}/unit/{type}/create', methods: ['POST'])]
+ public function train(Request $request): Response
+ {
+ if (empty($request->get('input'))) {
+ return new Response(statusCode: Status::INPUT, meta: 'Amount');
+ }
+
+ $village = Village::getByCoordinates($request->get('x'), $request->get('y'));
+
+ /**@var Model $unit*/
+ $unit = new (Model::resolveType($request->get('type')))();
+ $unit->type = $request->get('type');
+ $unit->homeVillageId = $village->id;
+
+ $amount = intval($request->get('input'));
+
+ if (! Village::canTrain($village, $unit, $amount)) {
+ return new Response(
+ statusCode: Status::REDIRECT_TEMPORARY,
+ meta: "/village/{$village->x}/{$village->y}"
+ );
+ }
+
+ // resources
+ foreach (Model::getResourceRequirements($unit, $amount) as $resourceType => $resourceValue) {
+ $village->{$resourceType} -= $resourceValue;
+ }
+ $village->updateResources();
+
+ // event
+ $event = new Event();
+ $event->time = (new \DateTime())->add(\DateInterval::createFromDateString($unit->getBuildTime($amount) . ' seconds'));
+ $event->villageId = $village->id;
+ $trainUnitsEvent = new TrainUnits();
+ $trainUnitsEvent->event = $event;
+ $trainUnitsEvent->type = $request->get('type');
+ $trainUnitsEvent->amount = $amount;
+ $trainUnitsEvent->dbInsert();
+
+
+ return new Response(
+ statusCode: Status::REDIRECT_TEMPORARY,
+ meta: "/village/{$village->x}/{$village->y}"
+ );
+ }
+}
diff --git a/src/gemini/Controller/User.php b/src/gemini/Controller/User.php
new file mode 100644
index 0000000..a870b88
--- /dev/null
+++ b/src/gemini/Controller/User.php
@@ -0,0 +1,118 @@
+<?php
+
+namespace App\gemini\Controller;
+
+use App\DB;
+use GeminiFoundation\Request;
+
+class User
+{
+ public function get(Request $request): array|bool
+ {
+ if ($request->getClientCertificate() === null) {
+ return false;
+ }
+
+ return DB::query(
+ <<<SQL
+ select users.id, username, email from users
+ join users_gemini on users.id = users_gemini.user_id
+ where users_gemini.certificate=:fingerprint
+ SQL,
+ ['fingerprint' => $request->getClientCertificate()->getFingerprint()]
+ )->fetch();
+ }
+
+ public function create(Request $request): array|bool
+ {
+ DB::query(
+ 'insert into users (username, password, email) values (:username, :fingerprint, :email)',
+ [
+ 'username' => md5($request->getClientCertificate()->getFingerprint()),
+ 'fingerprint' => $request->getClientCertificate()->getFingerprint(),
+ 'email' => '(no email)',
+ ]
+ );
+ $userId = DB::query('select id from users where password=:password', ['password' => $request->getClientCertificate()->getFingerprint()])->fetchColumn();
+
+ DB::query(
+ 'insert into users_gemini (certificate, user_id) values (:fingerprint, :userId)',
+ ['fingerprint' => $request->getClientCertificate()->getFingerprint(), 'userId' => $userId]
+ );
+
+ // also insert new village at random free coordinates
+ DB::query(
+ 'insert into villages (name, x, y, wood, clay, iron, food, satisfaction) values (:name, :x, :y, :wood, :clay, :iron, :food, :satisfaction)',
+ [
+ 'name' => substr(md5(rand()), 0, 6),
+ 'x' => rand(0, 100),
+ 'y' => rand(0, 100),
+ 'wood' => 500,
+ 'clay' => 500,
+ 'iron' => 500,
+ 'food' => 500,
+ 'satisfaction' => 100,
+ ]
+ );
+ $villageId = DB::query('select id from villages order by id desc limit 1')->fetchColumn();
+
+ DB::query(
+ 'insert into user_villages (user_id, village_id) values (:userId, :villageId)',
+ ['userId' => $userId, 'villageId' => $villageId]
+ );
+
+ // insert base buildings
+ DB::query(
+ 'insert into village_buildings (level, type, village_id) values (:level, :type, :villageId)',
+ ['level' => 1, 'type' => 'TownHall', 'villageId' => $villageId]
+ );
+
+ DB::query(
+ 'insert into village_buildings (level, type, village_id) values (:level, :type, :villageId)',
+ ['level' => 1, 'type' => 'Storage', 'villageId' => $villageId]
+ );
+ DB::query(
+ 'insert into village_storage_config (wood, clay, iron, food, village_id) values (:wood, :clay, :iron, :food, :villageId)',
+ ['wood' => 25, 'clay' => 25, 'iron' => 25, 'food' => 25, 'villageId' => $villageId]
+ );
+
+ DB::query(
+ 'insert into village_buildings (level, type, village_id) values (:level, :type, :villageId)',
+ ['level' => 1, 'type' => 'WoodCutter', 'villageId' => $villageId]
+ );
+ DB::query(
+ 'insert into village_units (amount, type, is_traveling, home_village_id, residence_village_id) values (:amount, :type, false, :villageId, :villageId)',
+ ['amount' => 1, 'type' => 'WoodCutter', 'villageId' => $villageId]
+ );
+
+ DB::query(
+ 'insert into village_buildings (level, type, village_id) values (:level, :type, :villageId)',
+ ['level' => 1, 'type' => 'ClayPit', 'villageId' => $villageId]
+ );
+ DB::query(
+ 'insert into village_units (amount, type, is_traveling, home_village_id, residence_village_id) values (:amount, :type, false, :villageId, :villageId)',
+ ['amount' => 1, 'type' => 'PitWorker', 'villageId' => $villageId]
+ );
+
+ DB::query(
+ 'insert into village_buildings (level, type, village_id) values (:level, :type, :villageId)',
+ ['level' => 1, 'type' => 'IronMine', 'villageId' => $villageId]
+ );
+ DB::query(
+ 'insert into village_units (amount, type, is_traveling, home_village_id, residence_village_id) values (:amount, :type, false, :villageId, :villageId)',
+ ['amount' => 1, 'type' => 'Miner', 'villageId' => $villageId]
+ );
+
+ DB::query(
+ 'insert into village_buildings (level, type, village_id) values (:level, :type, :villageId)',
+ ['level' => 1, 'type' => 'Farm', 'villageId' => $villageId]
+ );
+ DB::query(
+ 'insert into village_units (amount, type, is_traveling, home_village_id, residence_village_id) values (:amount, :type, false, :villageId, :villageId)',
+ ['amount' => 1, 'type' => 'Farmer', 'villageId' => $villageId]
+ );
+
+
+ return $this->get($request);
+ }
+}
diff --git a/src/gemini/Controller/Village.php b/src/gemini/Controller/Village.php
new file mode 100644
index 0000000..9b27561
--- /dev/null
+++ b/src/gemini/Controller/Village.php
@@ -0,0 +1,167 @@
+<?php
+
+namespace App\gemini\Controller;
+
+use App\DB;
+use App\Guard;
+use App\Model\Event\SendUnits;
+use App\Model\Event\TrainUnits;
+use App\Model\Event\UpgradeBuilding;
+use App\Model\Village as Model;
+use App\View;
+use GeminiFoundation\Request;
+use GeminiFoundation\Response;
+use GeminiFoundation\Status;
+
+class Village
+{
+ // #[Route(path: '/villages', methods: ['GET'])]
+ public function list(): Response
+ {
+ $villages = DB::fetch(
+ Model::class,
+ <<<SQL
+ select * from villages
+ join user_villages on villages.id = user_villages.village_id
+ where user_villages.user_id=:id
+ SQL,
+ ['id' => $_SESSION['user']['id']]
+ );
+
+
+ return new Response(body: 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'));
+
+ if (! Guard::ownsVillage($village->id)) {
+ return new Response(body: View::render('error.twig', ['message' => 'Insufficient permission']));
+ }
+
+ $events = [];
+
+ $eventsBuilding = DB::query(
+ <<<SQL
+ select * from events_upgrade_building as event
+ left join events on event.event_id = events.id
+ where events.village_id=:id
+ SQL, ['id' => $village->id]
+ )->fetchAll();
+
+ foreach ($eventsBuilding as $row) {
+ $events['UpgradeBuilding'][$row['type']][] = DB::convertToModel(UpgradeBuilding::class, $row);
+ }
+
+ $eventsUnits = DB::query(
+ <<<SQL
+ select * from events_train_units as event
+ left join events on event.event_id = events.id
+ where village_id=:id
+ SQL, ['id' => $village->id]
+ )->fetchAll();
+
+ foreach ($eventsUnits as $row) {
+ $events['TrainUnits'][] = DB::convertToModel(TrainUnits::class, $row);
+ }
+
+ $eventsUnitsSendOwn = DB::query(
+ <<<SQL
+ select * from events_send_units as event
+ left join events on event.event_id = events.id
+ where village_id=:id
+ SQL, ['id' => $village->id]
+ )->fetchAll();
+
+ $eventsUnitsSendOther = DB::query(
+ <<<SQL
+ select * from events_send_units as event
+ left join events on event.event_id = events.id
+ where (destination=:id or source=:id) and village_id!=:id and is_canceled=false
+ SQL, ['id' => $village->id]
+ )->fetchAll();
+
+ foreach ([...$eventsUnitsSendOwn, ...$eventsUnitsSendOther] as $row) {
+ $events['SendUnits'][] = DB::convertToModel(SendUnits::class, $row);;
+ }
+
+ $buildings = [];
+ foreach (Model::getBuildings($village->id, true) as $building) {
+ $buildings[$building->type] = $building;
+ }
+
+
+ return new Response(body: View::render('village.twig', [
+ 'village' => $village,
+ 'events' => $events,
+ 'buildings' => $buildings,
+ 'villages' => DB::fetch(Model::class, "select * from villages where id!=:id", ['id' => $village->id]),
+ ]));
+ }
+
+ // #[Route(path: '/village/{x}/{y}/storage/config', methods: ['POST'])]
+ public function storageConfig(Request $request): Response
+ {
+ $village = Model::getByCoordinates($request->get('x'), $request->get('y'));
+ $type = $request->get('type');
+
+ if (empty($type)) {
+ return new Response(body: View::render('storage.twig', [
+ 'village' => $village,
+ ]));
+ }
+
+ if (empty($request->get('input'))) {
+ return new Response(statusCode: Status::INPUT, meta: "$type percent?");
+ }
+
+ $input = intval($request->get('input'));
+
+ // calculate to max 100%
+ $allTypes = ['wood', 'clay', 'iron', 'food'];
+ $allOtherTypes = array_diff($allTypes, [$type]);
+
+ $storageConfig = $village->getStorageConfig($village->id);
+
+ $values = [];
+ foreach ($allTypes as $resourceType) {
+ $values[$resourceType] = $storageConfig->$resourceType;
+ }
+ $values[$type] = $input;
+
+ $total = 0;
+ foreach ($values as $value) {
+ $total += $value;
+ }
+
+ foreach ($values as $resourceType => $value) {
+ $values[$resourceType] = round(($value / $total) * 100);
+ }
+
+ if ($values[$type] !== $input) {
+ $values[$type] = $input;
+ }
+
+ $newTotal = array_sum($values);
+ $values['food'] += (100 - $newTotal);
+
+ DB::query(
+ <<<SQL
+ update village_storage_config
+ set wood=:wood, clay=:clay, iron=:iron, food=:food
+ where village_id=:id
+ SQL,
+ ['wood' => $values['wood'], 'clay' => $values['clay'], 'iron' => $values['iron'], 'food' => $values['food'], 'id' => $village->id]
+ );
+
+
+ return new Response(
+ statusCode: Status::REDIRECT_TEMPORARY,
+ meta: "/village/{$village->x}/{$village->y}/storage/config"
+ );
+ }
+}
diff --git a/src/gemini/Gemini.php b/src/gemini/Gemini.php
new file mode 100644
index 0000000..41b8716
--- /dev/null
+++ b/src/gemini/Gemini.php
@@ -0,0 +1,120 @@
+<?php
+
+namespace App\gemini;
+
+use App\DB;
+use App\EventRunner;
+use App\View;
+use App\gemini\Controller\Building;
+use App\gemini\Controller\Unit;
+use App\gemini\Controller\User;
+use App\gemini\Controller\Village;
+use GeminiFoundation\Request;
+use GeminiFoundation\Response;
+use GeminiFoundation\Server;
+use GeminiFoundation\Status;
+
+class Gemini
+{
+ private array $certificate;
+ private string $hostname;
+
+ public function __construct(array $certificate, string $hostname)
+ {
+ $this->certificate = $certificate;
+ $this->hostname = $hostname;
+
+ global $_SESSION;
+ $_SESSION = [];
+
+ DB::init();
+
+ View::init();
+ View::addGlobal('session', $_SESSION);
+ }
+
+ public function run(): void
+ {
+ $server = new Server($this->certificate, $this->hostname, [
+ 'client_certificate_support_workaround' => true,
+ ]);
+
+ $server->onRequest(function (Response $response, Request $request) {
+ new EventRunner();
+
+ // auth
+ if ($request->getClientCertificate() === null) {
+ return new Response(
+ statusCode: Status::CLIENT_CERTIFICATE_REQUIRED,
+ meta: 'Attach a client certificate to log in'
+ );
+ }
+
+ $userController = new User();
+ $user = $userController->get($request);
+ if (empty($user)) {
+ $user = $userController->create($request);
+ }
+
+ global $_SESSION;
+ $_SESSION['user'] = [
+ 'id' => $user['id'],
+ 'username' => $user['username'],
+ ];
+ View::addGlobal('session', $_SESSION);
+
+
+ // routes
+ if ($request->getPath() == '/villages') {
+ $villageController = new Village();
+ $response = $villageController->list($request);
+ }
+
+ else if (preg_match('@village/(\d+)/(\d+)/storage/config/?(\w+)?@', $request->getPath(), $routeMatch)) {
+ $request
+ ->set('x', $routeMatch[1])
+ ->set('y', $routeMatch[2]);
+
+ if (isset($routeMatch[3])) {
+ $request->set('type', $routeMatch[3]);
+ }
+
+ $villageController = new Village();
+ $response = $villageController->storageConfig($request);
+ }
+
+ else if (preg_match('@village/(\d+)/(\d+)/building/(\w+)/level-up@', $request->getPath(), $routeMatch)) {
+ $request
+ ->set('x', $routeMatch[1])
+ ->set('y', $routeMatch[2])
+ ->set('type', $routeMatch[3]);
+
+ $buildingController = new Building();
+ $response = $buildingController->levelUp($request);
+ }
+
+ else if (preg_match('@village/(\d+)/(\d+)/unit/(\w+)/create@', $request->getPath(), $routeMatch)) {
+ $request
+ ->set('x', $routeMatch[1])
+ ->set('y', $routeMatch[2])
+ ->set('type', $routeMatch[3]);
+
+ $unitController = new Unit();
+ $response = $unitController->train($request);
+ }
+
+ else if (preg_match('@village/(\d+)/(\d+)@', $request->getPath(), $routeMatch)) {
+ $request
+ ->set('x', $routeMatch[1])
+ ->set('y', $routeMatch[2]);
+
+ $villageController = new Village();
+ $response = $villageController->show($request);
+ }
+
+ return $response;
+ });
+
+ $server->listen();
+ }
+}