diff options
author | Daniel Weipert <git@mail.dweipert.de> | 2024-01-02 20:42:01 +0100 |
---|---|---|
committer | Daniel Weipert <git@mail.dweipert.de> | 2024-01-05 12:33:59 +0100 |
commit | b21316248572cb27ed1f504529ad6680a473022e (patch) | |
tree | f8a2f81258cae3b1d2429fb7df5a3287954b683a | |
parent | f621d95f89ded05a2e916c5ee363bfe75ea37482 (diff) |
gemini
-rw-r--r-- | .env.example | 1 | ||||
-rw-r--r-- | .gitignore | 3 | ||||
-rw-r--r-- | Justfile | 2 | ||||
-rw-r--r-- | Readme.md | 16 | ||||
-rw-r--r-- | bin/db.php | 17 | ||||
-rwxr-xr-x | bin/gemini.sh | 11 | ||||
-rw-r--r-- | composer.json | 8 | ||||
-rw-r--r-- | composer.lock | 121 | ||||
-rw-r--r-- | docker-compose.yml | 25 | ||||
-rw-r--r-- | shell.nix | 10 | ||||
-rw-r--r-- | src/App.php | 32 | ||||
-rw-r--r-- | src/DB.php | 3 | ||||
-rw-r--r-- | src/EventRunner.php | 2 | ||||
-rw-r--r-- | src/View.php | 12 | ||||
-rw-r--r-- | src/gemini/Controller/Building.php | 44 | ||||
-rw-r--r-- | src/gemini/Controller/Unit.php | 60 | ||||
-rw-r--r-- | src/gemini/Controller/User.php | 118 | ||||
-rw-r--r-- | src/gemini/Controller/Village.php | 167 | ||||
-rw-r--r-- | src/gemini/Gemini.php | 120 | ||||
-rw-r--r-- | src/http/Controller/Building.php (renamed from src/Controller/Building.php) | 4 | ||||
-rw-r--r-- | src/http/Controller/Event.php (renamed from src/Controller/Event.php) | 4 | ||||
-rw-r--r-- | src/http/Controller/Login.php (renamed from src/Controller/Login.php) | 6 | ||||
-rw-r--r-- | src/http/Controller/Map.php (renamed from src/Controller/Map.php) | 2 | ||||
-rw-r--r-- | src/http/Controller/Unit.php (renamed from src/Controller/Unit.php) | 4 | ||||
-rw-r--r-- | src/http/Controller/Village.php (renamed from src/Controller/Village.php) | 4 | ||||
-rw-r--r-- | src/http/Http.php | 37 | ||||
-rw-r--r-- | src/http/Router.php (renamed from src/Router.php) | 4 | ||||
-rw-r--r-- | src/http/Support/RouteLoader.php (renamed from src/Support/RouteLoader.php) | 2 | ||||
-rw-r--r-- | views/gemini/error.twig | 3 | ||||
-rw-r--r-- | views/gemini/storage.twig | 16 | ||||
-rw-r--r-- | views/gemini/village.twig | 136 | ||||
-rw-r--r-- | views/gemini/villages.twig | 3 | ||||
-rw-r--r-- | views/http/base.twig (renamed from views/base.twig) | 0 | ||||
-rw-r--r-- | views/http/components/timer.twig (renamed from views/components/timer.twig) | 0 | ||||
-rw-r--r-- | views/http/error.twig (renamed from views/error.twig) | 0 | ||||
-rw-r--r-- | views/http/login.twig (renamed from views/login.twig) | 2 | ||||
-rw-r--r-- | views/http/map.twig (renamed from views/map.twig) | 0 | ||||
-rw-r--r-- | views/http/root.twig (renamed from views/root.twig) | 0 | ||||
-rw-r--r-- | views/http/village.twig (renamed from views/village.twig) | 0 | ||||
-rw-r--r-- | views/http/villages.twig (renamed from views/villages.twig) | 0 |
40 files changed, 916 insertions, 83 deletions
diff --git a/.env.example b/.env.example index 6cd359f..9281323 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,5 @@ APP_ENV= +APP_HOST= DB_NAME= DB_USER= @@ -2,3 +2,6 @@ config.yml /vendor/ + +/cert.pem +/key.rsa diff --git a/Justfile b/Justfile new file mode 100644 index 0000000..2207542 --- /dev/null +++ b/Justfile @@ -0,0 +1,2 @@ +gemini: + find $(pwd) -type f -name '*.php' -o -name '*.twig' | GEMINI=true DB_HOST=localhost entr -r php public/index.php @@ -3,3 +3,19 @@ https://openmoji.org https://game-icons.net https://lospec.com/palette-list/poisson-23 + + +## Development + +http +``` +docker compose up +``` + +gemini +``` +docker compose up # = DB + +openssl req -x509 -newkey rsa:4096 -keyout key.rsa -out cert.pem -days 3650 -nodes -subj "/CN=localhost" +find $(pwd) -type f -name '*.php' -o -name '*.twig' | GEMINI_HOSTNAME=localhost DB_HOST=localhost entr -r php public/index.php +``` @@ -196,6 +196,23 @@ DB::query(<<<SQL SQL); DB::query(<<<SQL + create table if not exists "users_gemini" ( + "id" bigserial primary key, + + "certificate" character varying(255) not null, + + "user_id" bigint not null, + constraint "relation_user" + foreign key ("user_id") references users("id") on delete cascade, + + "created_at" timestamp(0) not null default current_timestamp, + "updated_at" timestamp(0) not null default current_timestamp, + + unique ("certificate") + ); +SQL); + +DB::query(<<<SQL create table if not exists "user_settings" ( "id" bigserial primary key, diff --git a/bin/gemini.sh b/bin/gemini.sh new file mode 100755 index 0000000..5c2a845 --- /dev/null +++ b/bin/gemini.sh @@ -0,0 +1,11 @@ +#!/bin/sh + +if ! type entr > /dev/null; then + apk add entr +fi + +openssl req -x509 -newkey rsa:4096 -keyout key.rsa -out cert.pem -days 3650 -nodes -subj "/CN=localhost" + +# php $(pwd)/public/index.php +#find $(pwd) -type f -name '*.php' -o -name '*.twig' +find $(pwd) -type f \( -name "*.php" -o -name "*.twig" \) -not \( -path "*/vendor/*" \) | entr -rn php $(pwd)/public/index.php diff --git a/composer.json b/composer.json index c86117f..dcdb864 100644 --- a/composer.json +++ b/composer.json @@ -8,12 +8,20 @@ ], "require": { "php": "^8.0", + "dweipert/gemini-foundation": "dev-main", "symfony/config": "^6.3", "symfony/dotenv": "^6.3", "symfony/http-foundation": "^6.3", "symfony/routing": "^6.3", "twig/twig": "^3.7" }, + "repositories": [ + { + "type": "vcs", + "url": "https://git.dweipert.de/gemini-foundation" + } + ], + "minimum-stability": "dev", "autoload": { "psr-4": { "App\\": "src/" diff --git a/composer.lock b/composer.lock index 8bc246d..2524e07 100644 --- a/composer.lock +++ b/composer.lock @@ -4,11 +4,37 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "ee17ea11ec110fee294c21a2ef86e5cc", + "content-hash": "ccb607ba0346d76a58c48da66d90212d", "packages": [ { + "name": "dweipert/gemini-foundation", + "version": "dev-main", + "source": { + "type": "git", + "url": "https://git.dweipert.de/gemini-foundation", + "reference": "3776ab80f23c07943d2228f2975e515c494f930a" + }, + "default-branch": true, + "type": "library", + "autoload": { + "psr-4": { + "GeminiFoundation\\": "src/" + }, + "files": [ + "src/functions.php" + ] + }, + "authors": [ + { + "name": "Daniel Weipert", + "email": "code@drogueronin.de" + } + ], + "time": "2024-01-02T19:39:07+00:00" + }, + { "name": "symfony/config", - "version": "v6.4.0", + "version": "6.4.x-dev", "source": { "type": "git", "url": "https://github.com/symfony/config.git", @@ -63,7 +89,7 @@ "description": "Helps you find, load, combine, autofill and validate configuration values of any kind", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/config/tree/v6.4.0" + "source": "https://github.com/symfony/config/tree/6.4" }, "funding": [ { @@ -83,7 +109,7 @@ }, { "name": "symfony/deprecation-contracts", - "version": "v3.4.0", + "version": "dev-main", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", @@ -98,6 +124,7 @@ "require": { "php": ">=8.1" }, + "default-branch": true, "type": "library", "extra": { "branch-alias": { @@ -150,16 +177,16 @@ }, { "name": "symfony/dotenv", - "version": "v6.4.0", + "version": "6.4.x-dev", "source": { "type": "git", "url": "https://github.com/symfony/dotenv.git", - "reference": "d0d584a91422ddaa2c94317200d4c4e5b935555f" + "reference": "835f8d2d1022934ac038519de40b88158798c96f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dotenv/zipball/d0d584a91422ddaa2c94317200d4c4e5b935555f", - "reference": "d0d584a91422ddaa2c94317200d4c4e5b935555f", + "url": "https://api.github.com/repos/symfony/dotenv/zipball/835f8d2d1022934ac038519de40b88158798c96f", + "reference": "835f8d2d1022934ac038519de40b88158798c96f", "shasum": "" }, "require": { @@ -204,7 +231,7 @@ "environment" ], "support": { - "source": "https://github.com/symfony/dotenv/tree/v6.4.0" + "source": "https://github.com/symfony/dotenv/tree/6.4" }, "funding": [ { @@ -220,20 +247,20 @@ "type": "tidelift" } ], - "time": "2023-10-26T18:19:48+00:00" + "time": "2023-12-28T19:16:56+00:00" }, { "name": "symfony/filesystem", - "version": "v7.0.0", + "version": "7.1.x-dev", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "7da8ea2362a283771478c5f7729cfcb43a76b8b7" + "reference": "2d3a1adb578bd4e783b5f1cbfc4e2c12ae05b466" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/7da8ea2362a283771478c5f7729cfcb43a76b8b7", - "reference": "7da8ea2362a283771478c5f7729cfcb43a76b8b7", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/2d3a1adb578bd4e783b5f1cbfc4e2c12ae05b466", + "reference": "2d3a1adb578bd4e783b5f1cbfc4e2c12ae05b466", "shasum": "" }, "require": { @@ -267,7 +294,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v7.0.0" + "source": "https://github.com/symfony/filesystem/tree/7.1" }, "funding": [ { @@ -283,20 +310,20 @@ "type": "tidelift" } ], - "time": "2023-07-27T06:33:22+00:00" + "time": "2023-12-19T09:30:49+00:00" }, { "name": "symfony/http-foundation", - "version": "v6.4.0", + "version": "6.4.x-dev", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "44a6d39a9cc11e154547d882d5aac1e014440771" + "reference": "172d807f9ef3fc3fbed8377cc57c20d389269271" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/44a6d39a9cc11e154547d882d5aac1e014440771", - "reference": "44a6d39a9cc11e154547d882d5aac1e014440771", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/172d807f9ef3fc3fbed8377cc57c20d389269271", + "reference": "172d807f9ef3fc3fbed8377cc57c20d389269271", "shasum": "" }, "require": { @@ -344,7 +371,7 @@ "description": "Defines an object-oriented layer for the HTTP specification", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-foundation/tree/v6.4.0" + "source": "https://github.com/symfony/http-foundation/tree/6.4" }, "funding": [ { @@ -360,11 +387,11 @@ "type": "tidelift" } ], - "time": "2023-11-20T16:41:16+00:00" + "time": "2023-12-27T22:16:42+00:00" }, { "name": "symfony/polyfill-ctype", - "version": "v1.28.0", + "version": "1.x-dev", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", @@ -385,6 +412,7 @@ "suggest": { "ext-ctype": "For best performance" }, + "default-branch": true, "type": "library", "extra": { "branch-alias": { @@ -446,7 +474,7 @@ }, { "name": "symfony/polyfill-mbstring", - "version": "v1.28.0", + "version": "1.x-dev", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", @@ -467,6 +495,7 @@ "suggest": { "ext-mbstring": "For best performance" }, + "default-branch": true, "type": "library", "extra": { "branch-alias": { @@ -529,7 +558,7 @@ }, { "name": "symfony/polyfill-php80", - "version": "v1.28.0", + "version": "1.x-dev", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php80.git", @@ -544,6 +573,7 @@ "require": { "php": ">=7.1" }, + "default-branch": true, "type": "library", "extra": { "branch-alias": { @@ -612,7 +642,7 @@ }, { "name": "symfony/polyfill-php83", - "version": "v1.28.0", + "version": "1.x-dev", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php83.git", @@ -628,6 +658,7 @@ "php": ">=7.1", "symfony/polyfill-php80": "^1.14" }, + "default-branch": true, "type": "library", "extra": { "branch-alias": { @@ -692,16 +723,16 @@ }, { "name": "symfony/routing", - "version": "v6.4.1", + "version": "6.4.x-dev", "source": { "type": "git", "url": "https://github.com/symfony/routing.git", - "reference": "0c95c164fdba18b12523b75e64199ca3503e6d40" + "reference": "98eab13a07fddc85766f1756129c69f207ffbc21" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/routing/zipball/0c95c164fdba18b12523b75e64199ca3503e6d40", - "reference": "0c95c164fdba18b12523b75e64199ca3503e6d40", + "url": "https://api.github.com/repos/symfony/routing/zipball/98eab13a07fddc85766f1756129c69f207ffbc21", + "reference": "98eab13a07fddc85766f1756129c69f207ffbc21", "shasum": "" }, "require": { @@ -755,7 +786,7 @@ "url" ], "support": { - "source": "https://github.com/symfony/routing/tree/v6.4.1" + "source": "https://github.com/symfony/routing/tree/6.4" }, "funding": [ { @@ -771,24 +802,25 @@ "type": "tidelift" } ], - "time": "2023-12-01T14:54:37+00:00" + "time": "2023-12-29T15:34:34+00:00" }, { "name": "twig/twig", - "version": "v3.8.0", + "version": "3.x-dev", "source": { "type": "git", "url": "https://github.com/twigphp/Twig.git", - "reference": "9d15f0ac07f44dc4217883ec6ae02fd555c6f71d" + "reference": "b4c3c1c448583e9ffa24933675c01f4be3435b41" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/Twig/zipball/9d15f0ac07f44dc4217883ec6ae02fd555c6f71d", - "reference": "9d15f0ac07f44dc4217883ec6ae02fd555c6f71d", + "url": "https://api.github.com/repos/twigphp/Twig/zipball/b4c3c1c448583e9ffa24933675c01f4be3435b41", + "reference": "b4c3c1c448583e9ffa24933675c01f4be3435b41", "shasum": "" }, "require": { "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-ctype": "^1.8", "symfony/polyfill-mbstring": "^1.3", "symfony/polyfill-php80": "^1.22" @@ -797,8 +829,15 @@ "psr/container": "^1.0|^2.0", "symfony/phpunit-bridge": "^5.4.9|^6.3|^7.0" }, + "default-branch": true, "type": "library", "autoload": { + "files": [ + "src/Resources/core.php", + "src/Resources/debug.php", + "src/Resources/escaper.php", + "src/Resources/string_loader.php" + ], "psr-4": { "Twig\\": "src/" } @@ -831,7 +870,7 @@ ], "support": { "issues": "https://github.com/twigphp/Twig/issues", - "source": "https://github.com/twigphp/Twig/tree/v3.8.0" + "source": "https://github.com/twigphp/Twig/tree/3.x" }, "funding": [ { @@ -843,13 +882,15 @@ "type": "tidelift" } ], - "time": "2023-11-21T18:54:41+00:00" + "time": "2024-01-01T14:44:03+00:00" } ], "packages-dev": [], "aliases": [], - "minimum-stability": "stable", - "stability-flags": [], + "minimum-stability": "dev", + "stability-flags": { + "dweipert/gemini-foundation": 20 + }, "prefer-stable": false, "prefer-lowest": false, "platform": { diff --git a/docker-compose.yml b/docker-compose.yml index 6d5f1c1..154d35f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,15 +1,11 @@ version: "3" services: - app: - build: - context: docker/php - volumes: - - "./:/var/www/html" - db: image: postgres restart: unless-stopped + ports: + - "5432:5432" environment: - "POSTGRES_DB=${DB_NAME}" - "POSTGRES_USER=${DB_USER}" @@ -17,6 +13,12 @@ services: volumes: - "db:/var/lib/postgresql/data" + app: + build: + context: docker/php + volumes: + - "./:/var/www/html" + web: build: context: docker/nginx @@ -25,6 +27,17 @@ services: volumes: - "./:/var/www/html" + # gemini: + # build: + # context: docker/php + # ports: + # - "1965:1965" + # environment: + # - "GEMINI_HOSTNAME=0.0.0.0" + # volumes: + # - "./:/var/www/html" + # command: "./bin/gemini.sh" + adminer: image: adminer ports: diff --git a/shell.nix b/shell.nix new file mode 100644 index 0000000..358ee54 --- /dev/null +++ b/shell.nix @@ -0,0 +1,10 @@ +with (import <nixpkgs> {}); +mkShell { + buildInputs = [ + php + phpPackages.composer + nodejs + just + entr + ]; +} diff --git a/src/App.php b/src/App.php index 7524d38..9516a06 100644 --- a/src/App.php +++ b/src/App.php @@ -2,34 +2,32 @@ namespace App; -use Symfony\Component\HttpFoundation\Request; +use App\gemini\Gemini; +use App\http\Http; class App { + private $appRunner; + public function __construct() { if ($_ENV['APP_ENV'] === 'development') { error_reporting(E_ALL); } - // Session - session_start(); - - // DB - DB::init(); - - // Router - Router::init(Request::createFromGlobals()); - - // View - View::init(); - - // Events - new EventRunner(); + if (isset($_ENV['GEMINI'])) { + $this->appRunner = new Gemini([ + 'file' => dirname(__DIR__) . '/cert.pem', + 'key' => dirname(__DIR__) . '/key.rsa', + 'passphrase' => '', + ], $_ENV['APP_HOST']); + } + else { + $this->appRunner = new Http(); + } } public function run(): void { - $response = Router::execute(); - $response->send(); + $this->appRunner->run(); } } @@ -9,11 +9,12 @@ class DB { { $driver = $_ENV['DB_DRIVER'] ?? 'pgsql'; $host = $_ENV['DB_HOST'] ?? 'db'; + $port = $_ENV['DB_PORT'] ?? 5432; $dbname = $_ENV['DB_NAME']; $user = $_ENV['DB_USER']; $password = $_ENV['DB_PASSWORD']; - self::$connection = new \PDO("pgsql:host=$host;dbname=$dbname", $user, $password); + self::$connection = new \PDO("pgsql:host=$host;port=$port;dbname=$dbname", $user, $password); } /** diff --git a/src/EventRunner.php b/src/EventRunner.php index 7dd773d..2f2fd8b 100644 --- a/src/EventRunner.php +++ b/src/EventRunner.php @@ -82,7 +82,7 @@ class EventRunner } $diff = (new \DateTime())->diff($lastTick); - $tickMultiplier = $diff->i; + $tickMultiplier = $diff->i + 60*$diff->h + 60*24*$diff->d; if ($tickMultiplier > 0) { $villages = DB::fetch(Village::class, 'select id,wood,clay,iron,food from villages'); diff --git a/src/View.php b/src/View.php index 7862b12..1c29a67 100644 --- a/src/View.php +++ b/src/View.php @@ -9,11 +9,14 @@ use Twig\TwigFilter; class View { - private static Environment $twig; + public static Environment $twig; public static function init(): void { - $loader = new FilesystemLoader(dirname(__DIR__) . '/views'); + $loader = new FilesystemLoader( + dirname(__DIR__) . '/views/' . + (isset($_ENV['GEMINI']) ? 'gemini' : 'http') + ); self::$twig = new Environment($loader, [ 'debug' => $_ENV['APP_ENV'] === 'development', ]); @@ -24,8 +27,11 @@ class View self::$twig->addFilter(new TwigFilter('buildTime', function ($buildTime) { return @sprintf('%02d:%02d:%02d', $buildTime / 3600, ($buildTime / 60) % 60, $buildTime % 60); })); + } - self::$twig->addGlobal('session', $_SESSION); + public static function addGlobal(string $name, mixed $value): void + { + self::$twig->addGlobal($name, $value); } /** 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(); + } +} diff --git a/src/Controller/Building.php b/src/http/Controller/Building.php index e141113..4a59f0e 100644 --- a/src/Controller/Building.php +++ b/src/http/Controller/Building.php @@ -1,12 +1,12 @@ <?php -namespace App\Controller; +namespace App\http\Controller; use App\Model\Building as Model; use App\Model\Event; use App\Model\Event\UpgradeBuilding; use App\Model\Village; -use App\Router; +use App\http\Router; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; diff --git a/src/Controller/Event.php b/src/http/Controller/Event.php index 1fd304d..070d449 100644 --- a/src/Controller/Event.php +++ b/src/http/Controller/Event.php @@ -1,12 +1,12 @@ <?php -namespace App\Controller; +namespace App\http\Controller; use App\DB; use App\Model\Event as Model; use App\Model\Event\SendUnits; use App\Model\Village; -use App\Router; +use App\http\Router; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; diff --git a/src/Controller/Login.php b/src/http/Controller/Login.php index 0f360ae..8c04d85 100644 --- a/src/Controller/Login.php +++ b/src/http/Controller/Login.php @@ -1,6 +1,6 @@ <?php -namespace App\Controller; +namespace App\http\Controller; use App\DB; use App\View; @@ -21,11 +21,13 @@ class Login public function login(Request $request): Response { $email = $request->get('email'); - $user = DB::query('select id,username,password from users where email=:email', ['email' => $email])->fetch(); + $user = DB::query('select id,username,password from users where email=:email or username=:email', ['email' => $email])->fetch(); if (empty($user)) { $password = password_hash($request->get('password'), PASSWORD_DEFAULT); DB::query('insert into users (username, password, email) values (:username, :password, :email)', ['username' => $email, 'password' => $password, 'email' => $email]); + + // TODO: also insert new village at random free coordinates } else { $password = $user['password']; } diff --git a/src/Controller/Map.php b/src/http/Controller/Map.php index 6967470..69d23e1 100644 --- a/src/Controller/Map.php +++ b/src/http/Controller/Map.php @@ -1,6 +1,6 @@ <?php -namespace App\Controller; +namespace App\http\Controller; use App\DB; use App\View; diff --git a/src/Controller/Unit.php b/src/http/Controller/Unit.php index 0508249..c314cda 100644 --- a/src/Controller/Unit.php +++ b/src/http/Controller/Unit.php @@ -1,6 +1,6 @@ <?php -namespace App\Controller; +namespace App\http\Controller; use App\DB; use App\Model\Event\SendUnits; @@ -8,7 +8,7 @@ use App\Model\Event\TrainUnits; use App\Model\Unit as Model; use App\Model\Event; use App\Model\Village; -use App\Router; +use App\http\Router; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; diff --git a/src/Controller/Village.php b/src/http/Controller/Village.php index c678779..cd38442 100644 --- a/src/Controller/Village.php +++ b/src/http/Controller/Village.php @@ -1,6 +1,6 @@ <?php -namespace App\Controller; +namespace App\http\Controller; use App\DB; use App\Guard; @@ -8,7 +8,7 @@ use App\Model\Event\SendUnits; use App\Model\Event\TrainUnits; use App\Model\Event\UpgradeBuilding; use App\Model\Village as Model; -use App\Router; +use App\http\Router; use App\View; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; diff --git a/src/http/Http.php b/src/http/Http.php new file mode 100644 index 0000000..1867eb4 --- /dev/null +++ b/src/http/Http.php @@ -0,0 +1,37 @@ +<?php + +namespace App\http; + +use App\DB; +use App\EventRunner; +use App\http\Router; +use App\View; +use Symfony\Component\HttpFoundation\Request; + +class Http +{ + public function __construct() + { + // Session + session_start(); + + // DB + DB::init(); + + // Router + Router::init(Request::createFromGlobals()); + + // View + View::init(); + View::addGlobal('session', $_SESSION); + + // Events + new EventRunner(); + } + + public function run(): void + { + $response = Router::execute(); + $response->send(); + } +} diff --git a/src/Router.php b/src/http/Router.php index 8b24000..db75f81 100644 --- a/src/Router.php +++ b/src/http/Router.php @@ -1,8 +1,8 @@ <?php -namespace App; +namespace App\http; -use App\Support\RouteLoader; +use App\http\Support\RouteLoader; use Symfony\Component\Config\FileLocator; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; diff --git a/src/Support/RouteLoader.php b/src/http/Support/RouteLoader.php index ba124c5..b0e74cb 100644 --- a/src/Support/RouteLoader.php +++ b/src/http/Support/RouteLoader.php @@ -1,6 +1,6 @@ <?php -namespace App\Support; +namespace App\http\Support; use Symfony\Component\Routing\Loader\AnnotationClassLoader; use Symfony\Component\Routing\Route; diff --git a/views/gemini/error.twig b/views/gemini/error.twig new file mode 100644 index 0000000..67aff6a --- /dev/null +++ b/views/gemini/error.twig @@ -0,0 +1,3 @@ +=> /villages Overview + +Error: {{ message }} diff --git a/views/gemini/storage.twig b/views/gemini/storage.twig new file mode 100644 index 0000000..c9ecd4f --- /dev/null +++ b/views/gemini/storage.twig @@ -0,0 +1,16 @@ +=> /village/{{ village.x }}/{{ village.y }} Back + +# Configure Storage + +=> /village/{{ village.x }}/{{ village.y }}/storage/config/wood {{ village.getStorageConfig(village.id).wood }}% Wood +=> /village/{{ village.x }}/{{ village.y }}/storage/config/clay {{ village.getStorageConfig(village.id).clay }}% Clay +=> /village/{{ village.x }}/{{ village.y }}/storage/config/iron {{ village.getStorageConfig(village.id).iron }}% Iron +=> /village/{{ village.x }}/{{ village.y }}/storage/config/food {{ village.getStorageConfig(village.id).food }}% Food + +# Resources + +Wood: {{ village.wood }} / {{ village.getStorage(village.id).getResourceCapacity('wood') }} - Increment: {{ village.getBuilding(village.id, 'WoodCutter').getResourceIncrementor() }} +Clay: {{ village.clay }} / {{ village.getStorage(village.id).getResourceCapacity('clay') }} - Increment: {{ village.getBuilding(village.id, 'ClayPit').getResourceIncrementor() }} +Iron: {{ village.iron }} / {{ village.getStorage(village.id).getResourceCapacity('iron') }} - Increment: {{ village.getBuilding(village.id, 'IronMine').getResourceIncrementor() }} +Food: {{ village.food }} / {{ village.getStorage(village.id).getResourceCapacity('food') }} - Increment: {{ village.getBuilding(village.id, 'Farm').getResourceIncrementor() }} +Storage Capacity: {{ village.getStorage(village.id).getCapacity() }} diff --git a/views/gemini/village.twig b/views/gemini/village.twig new file mode 100644 index 0000000..199f53a --- /dev/null +++ b/views/gemini/village.twig @@ -0,0 +1,136 @@ +=> /villages Overview + + +# {{ village.name }} - {{ village.x }} x {{ village.y }} +=> /map/{{ village.x }}/{{ village.y }} Map - {{ village.x }} x {{ village.y }} +Satisfaction: {{ village.satisfaction }} + + +# Resources + +Wood: {{ village.wood }} / {{ village.getStorage(village.id).getResourceCapacity('wood') }} - Increment: {{ village.getBuilding(village.id, 'WoodCutter').getResourceIncrementor() }} +Clay: {{ village.clay }} / {{ village.getStorage(village.id).getResourceCapacity('clay') }} - Increment: {{ village.getBuilding(village.id, 'ClayPit').getResourceIncrementor() }} +Iron: {{ village.iron }} / {{ village.getStorage(village.id).getResourceCapacity('iron') }} - Increment: {{ village.getBuilding(village.id, 'IronMine').getResourceIncrementor() }} +Food: {{ village.food }} / {{ village.getStorage(village.id).getResourceCapacity('food') }} - Increment: {{ village.getBuilding(village.id, 'Farm').getResourceIncrementor() }} +Storage Capacity: {{ village.getStorage(village.id).getCapacity() }} +=> /village/{{ village.x }}/{{ village.y }}/storage/config Configure Storage + + +# Events + +{% if events['UpgradeBuilding'] %} +## Upgrade Building +{% for typeEvents in events['UpgradeBuilding'] %} +{% for event in typeEvents %} +### {{ event.type }} +Finished: {{ event.event.time | date('c') }} +=> /village/{{ village.x }}/{{ village.y }}/building/UpgradeBuilding/build/cancel Cancel +{% endfor %} +{% endfor %} +{% endif %} + +{% if events['TrainUnits'] %} +## Train Units +{% for event in events['TrainUnits'] %} +### {{ event.type }} +Amount: {{ event.amount }} +Finished: {{ event.event.time | date('c') }} +=> /village/{{ village.x }}/{{ village.y }}/unit/train/cancel Cancel +{% endfor %} +{% endif %} + +{% if events['SendUnits'] %} +## Send Units +{% for event in events['SendUnits'] %} +### {{ event.unit }} - {{ event.type }} +Amount: {{ event.amount }} +Source: {{ village.get(event.source).name }} +Destination: {{ village.get(event.destination).name }} +Finished: {{ event.event.time | date('c') }} +{% if event.isCanceled %} +Canceled +{% else %} +{% if event.event.villageId == village.id %} +=> /event/{{ event.event.id }}/cancel Cancel +{% endif %} +{% endif %} +{% endfor %} +{% endif %} + + +# Buildings + +{% for building in buildings %} +## {{ building.type }} +Level: {{ building.level | default(0) }} +Build Time: {{ building.getBuildTimeForLevel(building.getEffectiveLevel() + 1) | buildTime }} +Resources: +* Wood: {{ building.getResourceRequirementsForLevel(building.getEffectiveLevel() + 1)['wood'] }} +* Clay: {{ building.getResourceRequirementsForLevel(building.getEffectiveLevel() + 1)['clay'] }} +* Iron: {{ building.getResourceRequirementsForLevel(building.getEffectiveLevel() + 1)['iron'] }} +=> /village/{{ village.x }}/{{ village.y }}/building/{{ building.type }}/manage Manage +{% if village.canBuild(village, building) %} +=> /village/{{ village.x }}/{{ village.y }}/building/{{ building.type }}/level-up Level up +{% endif %} +{% endfor %} + + +# Units At Home + +{% for unit in village.getUnits(village.id, 1, 3) %} +## {{ unit.type }} +Amount: {{ unit.amount }} +{% if not unit.isTraveling %} +Build Time: {{ unit.getBuildTime(1) | buildTime }} +Resources: +* Wood: {{ unit.getResourceRequirements(unit, 1)['wood'] }} +* Clay: {{ unit.getResourceRequirements(unit, 1)['clay'] }} +* Iron: {{ unit.getResourceRequirements(unit, 1)['iron'] }} +* Food: {{ unit.getResourceRequirements(unit, 1)['food'] ?? 0 }} +{% if village.canTrain(village, unit, 1) %} +=> /village/{{ village.x }}/{{ village.y }}/unit/{{ unit.type }}/create Train +{% endif %} +{% else %} +~traveling~ +{% endif %} +{% endfor %} + + +# Units Supporting + +{% for unit in village.getUnits(village.id, 2) | merge(village.getUnits(village.id, 3)) %} +## {{ unit.type }} +Amount: {{ unit.amount }} +Origin: {{ village.get(unit.homeVillageId).name }} +Location: {{ not unit.isTraveling ? village.get(unit.residenceVillageId).name : '~traveling~' }} +Travel Time: {{ unit.getTravelTime(unit, village.getDistance(unit.getHomeVillage().x, unit.getHomeVillage().x, unit.getResidenceVillage().x, unit.getResidenceVillage().y)) | buildTime }} +{% if not unit.isTraveling %} +{% if unit.homeVillageId == village.id %} +=> /village/{{ village.x }}/{{ village.y }}/unit/{{ unit.type }}/location/{{ unit.getResidenceVillage().x }}/{{ unit.getResidenceVillage().y }}/recall Recall Home +{% else %} +=> /village/{{ village.x }}/{{ village.y }}/unit/{{ unit.type }}/location/{{ unit.getHomeVillage().x }}/{{ unit.getHomeVillage().y }}/send-back Send Back +{% endif %} +{% endif %} +{% endfor %} + + +# Send Units + +TODO +* list possible units +* list possible villages +* send INPUT request for amount + + +# Send Resources + +TODO +* list possible resource types +* list possible villages +* send INPUT request for amount + + +Logged in as {{ session.user.username }} +=> /logout Logout + +Server Time: {{ 'now' | date('c') }} diff --git a/views/gemini/villages.twig b/views/gemini/villages.twig new file mode 100644 index 0000000..1c8eb82 --- /dev/null +++ b/views/gemini/villages.twig @@ -0,0 +1,3 @@ +{% for village in villages %} +=> /village/{{ village.x }}/{{ village.y }} {{ village.name }} +{% endfor %} diff --git a/views/base.twig b/views/http/base.twig index 15ddafd..15ddafd 100644 --- a/views/base.twig +++ b/views/http/base.twig diff --git a/views/components/timer.twig b/views/http/components/timer.twig index ccb31a7..ccb31a7 100644 --- a/views/components/timer.twig +++ b/views/http/components/timer.twig diff --git a/views/error.twig b/views/http/error.twig index f9d70cd..f9d70cd 100644 --- a/views/error.twig +++ b/views/http/error.twig diff --git a/views/login.twig b/views/http/login.twig index 095f53e..723d5d2 100644 --- a/views/login.twig +++ b/views/http/login.twig @@ -5,7 +5,7 @@ <form action="/login" method="post"> <label> E-Mail: - <input type="email" name="email"> + <input type="text" name="email" placeholder="or Username"> </label> <label> Password: diff --git a/views/map.twig b/views/http/map.twig index 29f0294..29f0294 100644 --- a/views/map.twig +++ b/views/http/map.twig diff --git a/views/root.twig b/views/http/root.twig index 35399c0..35399c0 100644 --- a/views/root.twig +++ b/views/http/root.twig diff --git a/views/village.twig b/views/http/village.twig index c72018c..c72018c 100644 --- a/views/village.twig +++ b/views/http/village.twig diff --git a/views/villages.twig b/views/http/villages.twig index bdb87be..bdb87be 100644 --- a/views/villages.twig +++ b/views/http/villages.twig |