From a9700f00ebabc61699c8cb5a72c4a4b4670c1f44 Mon Sep 17 00:00:00 2001 From: valb-mig Date: Fri, 6 Mar 2026 18:03:04 -0300 Subject: [PATCH 01/18] (wip/feat): Error and Result classes --- index.php | 62 +++++ src/Core/Utils/Error.php | 168 ++++++++++++ src/Core/Utils/Result.php | 331 +++++++++++++++++++++++ src/Domain/ValueObjects/Utils/Result.php | 66 ----- 4 files changed, 561 insertions(+), 66 deletions(-) create mode 100644 index.php create mode 100644 src/Core/Utils/Error.php create mode 100644 src/Core/Utils/Result.php delete mode 100644 src/Domain/ValueObjects/Utils/Result.php diff --git a/index.php b/index.php new file mode 100644 index 0000000..d9ae2cf --- /dev/null +++ b/index.php @@ -0,0 +1,62 @@ + + */ + public static function create(string $name): Result + { + $errors = []; + + if (empty(trim($name))) { + $errors[] = Error::validation('name', 'O nome é obrigatório.'); + } + + if (strlen($name) < 3) { + $errors[] = Error::validation('name', 'O nome deve ter ao menos 3 caracteres.'); + } + + if (!empty($errors)) { + return Result::fail(...$errors); + } + + return Result::ok(new self($name)); + } +} + +class CreateUser +{ + public function execute(CreateUserInput $input): Result + { + $user = new User(id: 1, name: $input->name); + return Result::ok(new CreateUserOutput(id: $user->id, name: $user->name)); + } +} + +class CreateUserOutput +{ + public function __construct( + public readonly int $id, + public readonly string $name, + ) {} +} + +class User +{ + public function __construct( + public readonly int $id, + public readonly string $name, + ) {} +} diff --git a/src/Core/Utils/Error.php b/src/Core/Utils/Error.php new file mode 100644 index 0000000..fdb5957 --- /dev/null +++ b/src/Core/Utils/Error.php @@ -0,0 +1,168 @@ + $entity, + 'id' => $id, + ]); + } + + /** + * Creates a domain / business rule violation error. + * + * Use when an operation is structurally valid but violates a business + * constraint (e.g. discount exceeds the allowed maximum, insufficient + * balance, order already cancelled). + * + * The $code should be a descriptive, domain-specific constant so that + * callers can react to it programmatically: + * + * ```php + * Error::businessRule('DISCOUNT_LIMIT_EXCEEDED', 'Maximum discount is 50%.', ['requested' => 60]) + * Error::businessRule('INSUFFICIENT_BALANCE', 'Not enough credits.') + * ``` + */ + public static function businessRule(string $code, string $message, array $context = []): self + { + return new self(message: $message, code: $code, context: $context); + } + + /** + * Creates an authorization error. + * + * Use when the current user does not have permission to perform the + * requested operation. For missing/invalid credentials, prefer a + * dedicated authentication exception instead. + */ + public static function unauthorized(string $message = 'Access denied.'): self + { + return new self($message, 'UNAUTHORIZED'); + } + + /** + * Creates a conflict error. + * + * Use when the operation cannot proceed because of a conflicting state + * (e.g. duplicate e-mail on registration, optimistic lock mismatch). + * + * ```php + * Error::conflict('An account with this e-mail already exists.', ['email' => $email]) + * ``` + */ + public static function conflict(string $message, array $context = []): self + { + return new self($message, 'CONFLICT', context: $context); + } + + // ------------------------------------------------------------------------- + // Output + // ------------------------------------------------------------------------- + + /** + * Returns a structured array representation of this error. + * + * Null values are filtered out so the output stays clean for + * JSON responses. The $context key is intentionally included only + * when non-empty — avoid exposing it to end users in production. + * + * ```php + * ['code' => 'VALIDATION_ERROR', 'message' => '...', 'field' => 'email'] + * ``` + */ + public function toArray(): array + { + return array_filter([ + 'code' => $this->code, + 'message' => $this->message, + 'field' => $this->field ?: null, + 'context' => $this->context ?: null, + ]); + } + + /** + * Returns a human-readable string representation. + * Prefixes the message with the field name when present. + * + * Example output: + * - `[email] Must be a valid e-mail address. (VALIDATION_ERROR)` + * - `Access denied. (UNAUTHORIZED)` + */ + public function __toString(): string + { + $prefix = $this->field ? "[{$this->field}] " : ''; + + return "{$prefix}{$this->message} ({$this->code})"; + } +} diff --git a/src/Core/Utils/Result.php b/src/Core/Utils/Result.php new file mode 100644 index 0000000..6d25a5b --- /dev/null +++ b/src/Core/Utils/Result.php @@ -0,0 +1,331 @@ +success = $success; + $this->value = $value; + $this->errors = $errors; + } + + // ------------------------------------------------------------------------- + // Factories + // ------------------------------------------------------------------------- + + /** + * Creates a successful Result carrying the given value. + * + * @param T $value + * @return self + */ + public static function ok(mixed $value = null): self + { + return new self(true, $value); + } + + /** + * Creates a failed Result carrying one or more errors. + * Plain strings are automatically wrapped in {@see Error::generic()}. + * + * @param string|Error ...$errors + * @return self + */ + public static function fail(string|Error ...$errors): self + { + $normalized = array_map(fn($e) => $e instanceof Error ? $e : Error::generic($e), $errors); + + return new self(false, null, $normalized); + } + + // ------------------------------------------------------------------------- + // State + // ------------------------------------------------------------------------- + + /** Returns true when the operation succeeded. */ + public function isOk(): bool + { + return $this->success; + } + + /** Returns true when the operation failed. */ + public function isFail(): bool + { + return !$this->success; + } + + // ------------------------------------------------------------------------- + // Value / error access + // ------------------------------------------------------------------------- + + /** + * Returns the successful value. + * + * @throws \LogicException If called on a failed Result. + * @return T + */ + public function getValue(): mixed + { + if ($this->isFail()) { + throw new \LogicException('Cannot get value from a failed Result.'); + } + + return $this->value; + } + + /** + * Returns all errors carried by this Result. + * + * @return Error[] + */ + public function getErrors(): array + { + return $this->errors; + } + + /** + * Returns the first error, or null if there are none. + */ + public function getFirstError(): ?Error + { + return $this->errors[0] ?? null; + } + + /** + * Returns every error message as a plain string array. + * + * @return string[] + */ + public function getErrorMessages(): array + { + return array_map(fn(Error $e) => $e->message, $this->errors); + } + + // ------------------------------------------------------------------------- + // Functional chaining + // ------------------------------------------------------------------------- + + /** + * Transforms the successful value using the given callback. + * If this Result is a failure, it is returned unchanged. + * + * @param callable(T): mixed $fn + * @return self + */ + public function map(callable $fn): self + { + if ($this->isFail()) { + return $this; + } + + return self::ok($fn($this->value)); + } + + /** + * Chains an operation that itself returns a Result. + * Short-circuits on the first failure — subsequent steps are skipped. + * + * Use {@see Result::combine()} instead when steps are independent + * and you want to collect all errors at once. + * + * @param callable(T): Result $fn + * @return self + */ + public function flatMap(callable $fn): self + { + if ($this->isFail()) { + return $this; + } + + return $fn($this->value); + } + + /** + * Executes a side-effect callback when this Result is a failure. + * The Result itself is returned unchanged, making it safe to use inline. + * + * Useful for logging without interrupting the pipeline: + * ```php + * return validate($data) + * ->onFail(fn($errors) => $logger->warning('Validation failed', $errors)) + * ->flatMap(fn($data) => process($data)); + * ``` + * + * @param callable(Error[]): void $fn + * @return self + */ + public function onFail(callable $fn): self + { + if ($this->isFail()) { + $fn($this->errors); + } + + return $this; + } + + // ------------------------------------------------------------------------- + // Unwrap + // ------------------------------------------------------------------------- + + /** + * Returns the value directly, or throws if the Result is a failure. + * + * Use only when you are certain the Result is successful (e.g. right after + * a {@see Result::combine()} check). Calling this on a failure is a + * programming error and will throw a {@see \LogicException}. + * + * @throws \LogicException + * @return T + */ + public function unwrap(): mixed + { + if ($this->isFail()) { + $messages = implode(', ', $this->getErrorMessages()); + throw new \LogicException("Unwrap failed: {$messages}"); + } + + return $this->value; + } + + /** + * Returns the value if successful; otherwise calls the given callback + * with the errors and returns null. + * + * The callback is responsible for deciding what happens on failure + * (respond with HTTP 422, log, throw, exit, etc.). + * + * Always call exit/throw inside the callback if you do not want + * execution to continue with a null value. + * + * ```php + * $input = CreateUserInput::create($data) + * ->unwrapOrHandle(function (array $errors): void { + * http_response_code(422); + * echo json_encode(['errors' => $errors]); + * exit; + * }); + * ``` + * + * @param callable(Error[]): void $onFail + * @return T|null + */ + public function unwrapOrHandle(callable $onFail): mixed + { + if ($this->isFail()) { + $onFail($this->errors); + return null; + } + + return $this->value; + } + + /** + * Returns the value if successful; otherwise returns the given default. + * + * Use when failure has an acceptable fallback and no handling is needed. + * + * ```php + * $displayName = parseName($raw)->unwrapOr('Anonymous'); + * ``` + * + * @param T $default + * @return T + */ + public function unwrapOr(mixed $default): mixed + { + return $this->isOk() ? $this->value : $default; + } + + // ------------------------------------------------------------------------- + // Built-in failure handlers + // ------------------------------------------------------------------------- + + /** + * Returns a ready-made callback for {@see unwrapOrHandle()} that + * throws a RuntimeException with all error messages joined. + * + * Useful when you want to convert a failed Result into an exception + * (e.g. inside a context that already has a global exception handler). + * + * @return callable(Error[]): never + */ + public static function throwOnFail(): callable + { + return function (array $errors): void { + $messages = implode(', ', array_map(fn($e) => $e->message, $errors)); + throw new \RuntimeException($messages); + }; + } + + // ------------------------------------------------------------------------- + // Combining multiple Results + // ------------------------------------------------------------------------- + + /** + * Runs all given Results and collects every error from each failure. + * Returns ok only when all Results succeed. + * + * Unlike {@see flatMap()}, which short-circuits on the first failure, + * combine() always evaluates every Result — making it ideal for form + * validation where you want to show all errors at once. + * + * Note: the returned ok Result carries no value. Build the final object + * only after a successful combine: + * ```php + * $validation = Result::combine( + * self::validateName($name), + * self::validateEmail($email), + * ); + * + * if ($validation->isFail()) { + * return $validation; + * } + * + * return Result::ok(new self($name, $email)); + * ``` + * + * @param Result ...$results + * @return self + */ + public static function combine(Result ...$results): self + { + $allErrors = []; + + foreach ($results as $result) { + if ($result->isFail()) { + $allErrors = array_merge($allErrors, $result->getErrors()); + } + } + + return empty($allErrors) ? self::ok() : new self(false, null, $allErrors); + } +} diff --git a/src/Domain/ValueObjects/Utils/Result.php b/src/Domain/ValueObjects/Utils/Result.php deleted file mode 100644 index 62bafdf..0000000 --- a/src/Domain/ValueObjects/Utils/Result.php +++ /dev/null @@ -1,66 +0,0 @@ - - */ - public static function success(string $message, mixed $data = null): self - { - return new self(self::SUCCESS, $message, $data); - } - - /** - * @template TValue - * @param TValue $data - * @return self - */ - public static function error(string $message, mixed $data = null): self - { - return new self(self::ERROR, $message, $data); - } - - /** - * @return T - */ - public function getData(): mixed - { - return $this->data; - } - - public function getMessage(): string - { - return $this->message; - } - - public function isError(): bool - { - return $this->type === self::ERROR; - } - - public function isSuccess(): bool - { - return $this->type === self::SUCCESS; - } -} From d892a58a94914fa1563be2a84ac4d93f5b8e9e5e Mon Sep 17 00:00:00 2001 From: valb-mig Date: Fri, 6 Mar 2026 18:04:15 -0300 Subject: [PATCH 02/18] (refactor): Changing directories --- src/Application/UseCase/Dice/RollDice/RollDiceUseCase.php | 4 ++-- .../UseCase/Dice/RollDice/RollDiceUseCaseInput.php | 2 +- src/{Infrastructure => Core}/Handler/LogHandler.php | 3 +-- src/{Domain/ValueObjects => Core}/Utils/Identifier.php | 2 +- src/Domain/Actions/Dice/RollDiceAction.php | 2 +- src/Domain/Entities/Session.php | 2 +- src/Domain/ValueObjects/{App => }/Dice.php | 2 +- .../EntryPoints/Console/Dice/RollDiceCommand.php | 4 ++-- .../EntryPoints/Console/Session/StartSessionCommand.php | 2 +- .../UseCase/Dice/RollDice/RollDiceUseCaseTest.php | 2 +- tests/Domain/Entities/SessionTest.php | 2 +- tests/Domain/ValueObjects/{App => }/DiceTest.php | 4 ++-- .../EntryPoints/Console/Session/StartSessionCommandTest.php | 6 ------ 13 files changed, 15 insertions(+), 22 deletions(-) rename src/{Infrastructure => Core}/Handler/LogHandler.php (89%) rename src/{Domain/ValueObjects => Core}/Utils/Identifier.php (82%) rename src/Domain/ValueObjects/{App => }/Dice.php (88%) rename tests/Domain/ValueObjects/{App => }/DiceTest.php (85%) diff --git a/src/Application/UseCase/Dice/RollDice/RollDiceUseCase.php b/src/Application/UseCase/Dice/RollDice/RollDiceUseCase.php index 52e0234..cd17b0b 100644 --- a/src/Application/UseCase/Dice/RollDice/RollDiceUseCase.php +++ b/src/Application/UseCase/Dice/RollDice/RollDiceUseCase.php @@ -5,13 +5,13 @@ namespace RPGPlayground\Application\UseCase\Dice\RollDice; use Psl\Async; +use RPGPlayground\Core\Utils\Result; use RPGPlayground\Domain\Actions\Dice\RollDiceAction; -use RPGPlayground\Domain\ValueObjects\Utils\Result; final class RollDiceUseCase { /** - * @return Result + * @return Result */ public function run(RollDiceUseCaseInput $input): Result { diff --git a/src/Application/UseCase/Dice/RollDice/RollDiceUseCaseInput.php b/src/Application/UseCase/Dice/RollDice/RollDiceUseCaseInput.php index b64d520..1d503ae 100644 --- a/src/Application/UseCase/Dice/RollDice/RollDiceUseCaseInput.php +++ b/src/Application/UseCase/Dice/RollDice/RollDiceUseCaseInput.php @@ -4,7 +4,7 @@ namespace RPGPlayground\Application\UseCase\Dice\RollDice; -use RPGPlayground\Domain\ValueObjects\App\Dice; +use RPGPlayground\Domain\ValueObjects\Dice; final class RollDiceUseCaseInput { diff --git a/src/Infrastructure/Handler/LogHandler.php b/src/Core/Handler/LogHandler.php similarity index 89% rename from src/Infrastructure/Handler/LogHandler.php rename to src/Core/Handler/LogHandler.php index 2ad2679..234e28a 100644 --- a/src/Infrastructure/Handler/LogHandler.php +++ b/src/Core/Handler/LogHandler.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace RPGPlayground\Infrastructure\Handler; +namespace RPGPlayground\Core\Handler; use Monolog\Handler\StreamHandler; use Monolog\Level; @@ -42,7 +42,6 @@ public static function dispatch(Level $level, string $message, array $context = try { self::$instance?->log($level, $message, $context); } catch (\Throwable $e) { - // Handle logging errors gracefully, e.g., by writing to a fallback log file error_log('Logging error: ' . $e->getMessage()); } } diff --git a/src/Domain/ValueObjects/Utils/Identifier.php b/src/Core/Utils/Identifier.php similarity index 82% rename from src/Domain/ValueObjects/Utils/Identifier.php rename to src/Core/Utils/Identifier.php index 8a6fc5d..b270086 100644 --- a/src/Domain/ValueObjects/Utils/Identifier.php +++ b/src/Core/Utils/Identifier.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace RPGPlayground\Domain\ValueObjects\Utils; +namespace RPGPlayground\Core\Utils; final class Identifier { diff --git a/src/Domain/Actions/Dice/RollDiceAction.php b/src/Domain/Actions/Dice/RollDiceAction.php index 93de769..8e0e2c8 100644 --- a/src/Domain/Actions/Dice/RollDiceAction.php +++ b/src/Domain/Actions/Dice/RollDiceAction.php @@ -4,7 +4,7 @@ namespace RPGPlayground\Domain\Actions\Dice; -use RPGPlayground\Domain\ValueObjects\App\Dice; +use RPGPlayground\Domain\ValueObjects\Dice; final class RollDiceAction { diff --git a/src/Domain/Entities/Session.php b/src/Domain/Entities/Session.php index d857f91..3ce33d9 100644 --- a/src/Domain/Entities/Session.php +++ b/src/Domain/Entities/Session.php @@ -4,7 +4,7 @@ namespace RPGPlayground\Domain\Entities; -use RPGPlayground\Domain\ValueObjects\Utils\Identifier; +use RPGPlayground\Core\Utils\Identifier; final class Session { diff --git a/src/Domain/ValueObjects/App/Dice.php b/src/Domain/ValueObjects/Dice.php similarity index 88% rename from src/Domain/ValueObjects/App/Dice.php rename to src/Domain/ValueObjects/Dice.php index 2a9c1f8..4bc9c7d 100644 --- a/src/Domain/ValueObjects/App/Dice.php +++ b/src/Domain/ValueObjects/Dice.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace RPGPlayground\Domain\ValueObjects\App; +namespace RPGPlayground\Domain\ValueObjects; final class Dice { diff --git a/src/Infrastructure/EntryPoints/Console/Dice/RollDiceCommand.php b/src/Infrastructure/EntryPoints/Console/Dice/RollDiceCommand.php index f47f856..3a9142a 100644 --- a/src/Infrastructure/EntryPoints/Console/Dice/RollDiceCommand.php +++ b/src/Infrastructure/EntryPoints/Console/Dice/RollDiceCommand.php @@ -7,8 +7,8 @@ use Monolog\Level; use RPGPlayground\Application\UseCase\Dice\RollDice\RollDiceUseCase; use RPGPlayground\Application\UseCase\Dice\RollDice\RollDiceUseCaseInput; -use RPGPlayground\Domain\ValueObjects\App\Dice; -use RPGPlayground\Infrastructure\Handler\LogHandler; +use RPGPlayground\Core\Handler\LogHandler; +use RPGPlayground\Domain\ValueObjects\Dice; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; diff --git a/src/Infrastructure/EntryPoints/Console/Session/StartSessionCommand.php b/src/Infrastructure/EntryPoints/Console/Session/StartSessionCommand.php index f037548..1475e00 100644 --- a/src/Infrastructure/EntryPoints/Console/Session/StartSessionCommand.php +++ b/src/Infrastructure/EntryPoints/Console/Session/StartSessionCommand.php @@ -7,7 +7,7 @@ use Monolog\Level; use RPGPlayground\Application\UseCase\Session\StartSession\StartSessionUseCase; use RPGPlayground\Application\UseCase\Session\StartSession\StartSessionUseCaseInput; -use RPGPlayground\Infrastructure\Handler\LogHandler; +use RPGPlayground\Core\Handler\LogHandler; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; diff --git a/tests/Application/UseCase/Dice/RollDice/RollDiceUseCaseTest.php b/tests/Application/UseCase/Dice/RollDice/RollDiceUseCaseTest.php index c0281b3..c410934 100644 --- a/tests/Application/UseCase/Dice/RollDice/RollDiceUseCaseTest.php +++ b/tests/Application/UseCase/Dice/RollDice/RollDiceUseCaseTest.php @@ -7,7 +7,7 @@ use PHPUnit\Framework\TestCase; use RPGPlayground\Application\UseCase\Dice\RollDice\RollDiceUseCase; use RPGPlayground\Application\UseCase\Dice\RollDice\RollDiceUseCaseInput; -use RPGPlayground\Domain\ValueObjects\App\Dice; +use RPGPlayground\Domain\ValueObjects\Dice; class RollDiceUseCaseTest extends TestCase { diff --git a/tests/Domain/Entities/SessionTest.php b/tests/Domain/Entities/SessionTest.php index ddc1b94..6afac76 100644 --- a/tests/Domain/Entities/SessionTest.php +++ b/tests/Domain/Entities/SessionTest.php @@ -6,8 +6,8 @@ use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; +use RPGPlayground\Core\Utils\Identifier; use RPGPlayground\Domain\Entities\Session; -use RPGPlayground\Domain\ValueObjects\Utils\Identifier; class SessionTest extends TestCase { diff --git a/tests/Domain/ValueObjects/App/DiceTest.php b/tests/Domain/ValueObjects/DiceTest.php similarity index 85% rename from tests/Domain/ValueObjects/App/DiceTest.php rename to tests/Domain/ValueObjects/DiceTest.php index e388fd1..aa51ceb 100644 --- a/tests/Domain/ValueObjects/App/DiceTest.php +++ b/tests/Domain/ValueObjects/DiceTest.php @@ -2,11 +2,11 @@ declare(strict_types=1); -namespace Tests\Domain\ValueObjects\App; +namespace Tests\Domain\ValueObjects; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; -use RPGPlayground\Domain\ValueObjects\App\Dice; +use RPGPlayground\Domain\ValueObjects\Dice; class DiceTest extends TestCase { diff --git a/tests/Infrastructure/EntryPoints/Console/Session/StartSessionCommandTest.php b/tests/Infrastructure/EntryPoints/Console/Session/StartSessionCommandTest.php index 1ef1a98..695c4c6 100644 --- a/tests/Infrastructure/EntryPoints/Console/Session/StartSessionCommandTest.php +++ b/tests/Infrastructure/EntryPoints/Console/Session/StartSessionCommandTest.php @@ -6,12 +6,6 @@ use PHPUnit\Framework\TestCase; use RPGPlayground\Infrastructure\EntryPoints\Console\Session\StartSessionCommand; -use Symfony\Component\Console\Attribute\AsCommand; -use Symfony\Component\Console\Command\Command; -use Symfony\Component\Console\Input\InputArgument; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Output\OutputInterface; -use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\Console\Tester\CommandTester; final class StartSessionCommandTest extends TestCase From faa6232a395d21f1900ecab44363e604bdb62787 Mon Sep 17 00:00:00 2001 From: valb-mig Date: Fri, 6 Mar 2026 18:04:43 -0300 Subject: [PATCH 03/18] (feat): Custom exceptions --- src/Core/Exceptions/DomainException.php | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 src/Core/Exceptions/DomainException.php diff --git a/src/Core/Exceptions/DomainException.php b/src/Core/Exceptions/DomainException.php new file mode 100644 index 0000000..d96c205 --- /dev/null +++ b/src/Core/Exceptions/DomainException.php @@ -0,0 +1,9 @@ + Date: Fri, 6 Mar 2026 18:05:04 -0300 Subject: [PATCH 04/18] (wip): Test object --- src/Domain/ValueObjects/Test.php | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 src/Domain/ValueObjects/Test.php diff --git a/src/Domain/ValueObjects/Test.php b/src/Domain/ValueObjects/Test.php new file mode 100644 index 0000000..b8c0eb9 --- /dev/null +++ b/src/Domain/ValueObjects/Test.php @@ -0,0 +1,28 @@ + Date: Fri, 6 Mar 2026 18:05:22 -0300 Subject: [PATCH 05/18] (adjust): Updating obsidian --- obsidian/.obsidian/workspace.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/obsidian/.obsidian/workspace.json b/obsidian/.obsidian/workspace.json index c78aaaa..874c8ce 100644 --- a/obsidian/.obsidian/workspace.json +++ b/obsidian/.obsidian/workspace.json @@ -183,8 +183,9 @@ "obsidian-excalidraw-plugin:New drawing": false } }, - "active": "ca2d8013c7331e8e", + "active": "6603145d49dbc37e", "lastOpenFiles": [ + "Excalidraw/Overall.md", "Domain/Dice.md", "Domain/Session.md", "Design.md", @@ -193,7 +194,6 @@ "TODO.md", "Domain/World.md", "Domain/Characters.md", - "Excalidraw/Overall.md", "Domain/NPC.md", "Excalidraw/Drawing 2026-03-02 14.34.45.excalidraw.md", "Sem título", From f29f4357d7408fff0f33bed081044a813cdc7c7d Mon Sep 17 00:00:00 2001 From: valb-mig Date: Wed, 18 Mar 2026 17:26:30 -0300 Subject: [PATCH 06/18] chore: replace internal Result and Error with eco library --- src/Core/Utils/Error.php | 168 ------------------- src/Core/Utils/Result.php | 331 -------------------------------------- 2 files changed, 499 deletions(-) delete mode 100644 src/Core/Utils/Error.php delete mode 100644 src/Core/Utils/Result.php diff --git a/src/Core/Utils/Error.php b/src/Core/Utils/Error.php deleted file mode 100644 index fdb5957..0000000 --- a/src/Core/Utils/Error.php +++ /dev/null @@ -1,168 +0,0 @@ - $entity, - 'id' => $id, - ]); - } - - /** - * Creates a domain / business rule violation error. - * - * Use when an operation is structurally valid but violates a business - * constraint (e.g. discount exceeds the allowed maximum, insufficient - * balance, order already cancelled). - * - * The $code should be a descriptive, domain-specific constant so that - * callers can react to it programmatically: - * - * ```php - * Error::businessRule('DISCOUNT_LIMIT_EXCEEDED', 'Maximum discount is 50%.', ['requested' => 60]) - * Error::businessRule('INSUFFICIENT_BALANCE', 'Not enough credits.') - * ``` - */ - public static function businessRule(string $code, string $message, array $context = []): self - { - return new self(message: $message, code: $code, context: $context); - } - - /** - * Creates an authorization error. - * - * Use when the current user does not have permission to perform the - * requested operation. For missing/invalid credentials, prefer a - * dedicated authentication exception instead. - */ - public static function unauthorized(string $message = 'Access denied.'): self - { - return new self($message, 'UNAUTHORIZED'); - } - - /** - * Creates a conflict error. - * - * Use when the operation cannot proceed because of a conflicting state - * (e.g. duplicate e-mail on registration, optimistic lock mismatch). - * - * ```php - * Error::conflict('An account with this e-mail already exists.', ['email' => $email]) - * ``` - */ - public static function conflict(string $message, array $context = []): self - { - return new self($message, 'CONFLICT', context: $context); - } - - // ------------------------------------------------------------------------- - // Output - // ------------------------------------------------------------------------- - - /** - * Returns a structured array representation of this error. - * - * Null values are filtered out so the output stays clean for - * JSON responses. The $context key is intentionally included only - * when non-empty — avoid exposing it to end users in production. - * - * ```php - * ['code' => 'VALIDATION_ERROR', 'message' => '...', 'field' => 'email'] - * ``` - */ - public function toArray(): array - { - return array_filter([ - 'code' => $this->code, - 'message' => $this->message, - 'field' => $this->field ?: null, - 'context' => $this->context ?: null, - ]); - } - - /** - * Returns a human-readable string representation. - * Prefixes the message with the field name when present. - * - * Example output: - * - `[email] Must be a valid e-mail address. (VALIDATION_ERROR)` - * - `Access denied. (UNAUTHORIZED)` - */ - public function __toString(): string - { - $prefix = $this->field ? "[{$this->field}] " : ''; - - return "{$prefix}{$this->message} ({$this->code})"; - } -} diff --git a/src/Core/Utils/Result.php b/src/Core/Utils/Result.php deleted file mode 100644 index 6d25a5b..0000000 --- a/src/Core/Utils/Result.php +++ /dev/null @@ -1,331 +0,0 @@ -success = $success; - $this->value = $value; - $this->errors = $errors; - } - - // ------------------------------------------------------------------------- - // Factories - // ------------------------------------------------------------------------- - - /** - * Creates a successful Result carrying the given value. - * - * @param T $value - * @return self - */ - public static function ok(mixed $value = null): self - { - return new self(true, $value); - } - - /** - * Creates a failed Result carrying one or more errors. - * Plain strings are automatically wrapped in {@see Error::generic()}. - * - * @param string|Error ...$errors - * @return self - */ - public static function fail(string|Error ...$errors): self - { - $normalized = array_map(fn($e) => $e instanceof Error ? $e : Error::generic($e), $errors); - - return new self(false, null, $normalized); - } - - // ------------------------------------------------------------------------- - // State - // ------------------------------------------------------------------------- - - /** Returns true when the operation succeeded. */ - public function isOk(): bool - { - return $this->success; - } - - /** Returns true when the operation failed. */ - public function isFail(): bool - { - return !$this->success; - } - - // ------------------------------------------------------------------------- - // Value / error access - // ------------------------------------------------------------------------- - - /** - * Returns the successful value. - * - * @throws \LogicException If called on a failed Result. - * @return T - */ - public function getValue(): mixed - { - if ($this->isFail()) { - throw new \LogicException('Cannot get value from a failed Result.'); - } - - return $this->value; - } - - /** - * Returns all errors carried by this Result. - * - * @return Error[] - */ - public function getErrors(): array - { - return $this->errors; - } - - /** - * Returns the first error, or null if there are none. - */ - public function getFirstError(): ?Error - { - return $this->errors[0] ?? null; - } - - /** - * Returns every error message as a plain string array. - * - * @return string[] - */ - public function getErrorMessages(): array - { - return array_map(fn(Error $e) => $e->message, $this->errors); - } - - // ------------------------------------------------------------------------- - // Functional chaining - // ------------------------------------------------------------------------- - - /** - * Transforms the successful value using the given callback. - * If this Result is a failure, it is returned unchanged. - * - * @param callable(T): mixed $fn - * @return self - */ - public function map(callable $fn): self - { - if ($this->isFail()) { - return $this; - } - - return self::ok($fn($this->value)); - } - - /** - * Chains an operation that itself returns a Result. - * Short-circuits on the first failure — subsequent steps are skipped. - * - * Use {@see Result::combine()} instead when steps are independent - * and you want to collect all errors at once. - * - * @param callable(T): Result $fn - * @return self - */ - public function flatMap(callable $fn): self - { - if ($this->isFail()) { - return $this; - } - - return $fn($this->value); - } - - /** - * Executes a side-effect callback when this Result is a failure. - * The Result itself is returned unchanged, making it safe to use inline. - * - * Useful for logging without interrupting the pipeline: - * ```php - * return validate($data) - * ->onFail(fn($errors) => $logger->warning('Validation failed', $errors)) - * ->flatMap(fn($data) => process($data)); - * ``` - * - * @param callable(Error[]): void $fn - * @return self - */ - public function onFail(callable $fn): self - { - if ($this->isFail()) { - $fn($this->errors); - } - - return $this; - } - - // ------------------------------------------------------------------------- - // Unwrap - // ------------------------------------------------------------------------- - - /** - * Returns the value directly, or throws if the Result is a failure. - * - * Use only when you are certain the Result is successful (e.g. right after - * a {@see Result::combine()} check). Calling this on a failure is a - * programming error and will throw a {@see \LogicException}. - * - * @throws \LogicException - * @return T - */ - public function unwrap(): mixed - { - if ($this->isFail()) { - $messages = implode(', ', $this->getErrorMessages()); - throw new \LogicException("Unwrap failed: {$messages}"); - } - - return $this->value; - } - - /** - * Returns the value if successful; otherwise calls the given callback - * with the errors and returns null. - * - * The callback is responsible for deciding what happens on failure - * (respond with HTTP 422, log, throw, exit, etc.). - * - * Always call exit/throw inside the callback if you do not want - * execution to continue with a null value. - * - * ```php - * $input = CreateUserInput::create($data) - * ->unwrapOrHandle(function (array $errors): void { - * http_response_code(422); - * echo json_encode(['errors' => $errors]); - * exit; - * }); - * ``` - * - * @param callable(Error[]): void $onFail - * @return T|null - */ - public function unwrapOrHandle(callable $onFail): mixed - { - if ($this->isFail()) { - $onFail($this->errors); - return null; - } - - return $this->value; - } - - /** - * Returns the value if successful; otherwise returns the given default. - * - * Use when failure has an acceptable fallback and no handling is needed. - * - * ```php - * $displayName = parseName($raw)->unwrapOr('Anonymous'); - * ``` - * - * @param T $default - * @return T - */ - public function unwrapOr(mixed $default): mixed - { - return $this->isOk() ? $this->value : $default; - } - - // ------------------------------------------------------------------------- - // Built-in failure handlers - // ------------------------------------------------------------------------- - - /** - * Returns a ready-made callback for {@see unwrapOrHandle()} that - * throws a RuntimeException with all error messages joined. - * - * Useful when you want to convert a failed Result into an exception - * (e.g. inside a context that already has a global exception handler). - * - * @return callable(Error[]): never - */ - public static function throwOnFail(): callable - { - return function (array $errors): void { - $messages = implode(', ', array_map(fn($e) => $e->message, $errors)); - throw new \RuntimeException($messages); - }; - } - - // ------------------------------------------------------------------------- - // Combining multiple Results - // ------------------------------------------------------------------------- - - /** - * Runs all given Results and collects every error from each failure. - * Returns ok only when all Results succeed. - * - * Unlike {@see flatMap()}, which short-circuits on the first failure, - * combine() always evaluates every Result — making it ideal for form - * validation where you want to show all errors at once. - * - * Note: the returned ok Result carries no value. Build the final object - * only after a successful combine: - * ```php - * $validation = Result::combine( - * self::validateName($name), - * self::validateEmail($email), - * ); - * - * if ($validation->isFail()) { - * return $validation; - * } - * - * return Result::ok(new self($name, $email)); - * ``` - * - * @param Result ...$results - * @return self - */ - public static function combine(Result ...$results): self - { - $allErrors = []; - - foreach ($results as $result) { - if ($result->isFail()) { - $allErrors = array_merge($allErrors, $result->getErrors()); - } - } - - return empty($allErrors) ? self::ok() : new self(false, null, $allErrors); - } -} From 020c0fd4779973b5fc10f91e2c050b53451eedaa Mon Sep 17 00:00:00 2001 From: valb-mig Date: Wed, 18 Mar 2026 17:26:44 -0300 Subject: [PATCH 07/18] refactor: rename UseCase I/O classes to drop UseCase suffix --- ...ceUseCaseOutput.php => RollDiceOutput.php} | 2 +- .../Dice/RollDice/RollDiceUseCaseInput.php | 26 ------------------- ...eCaseOutput.php => StartSessionOutput.php} | 2 +- .../StartSession/StartSessionUseCaseInput.php | 22 ---------------- 4 files changed, 2 insertions(+), 50 deletions(-) rename src/Application/UseCase/Dice/RollDice/{RollDiceUseCaseOutput.php => RollDiceOutput.php} (84%) delete mode 100644 src/Application/UseCase/Dice/RollDice/RollDiceUseCaseInput.php rename src/Application/UseCase/Session/StartSession/{StartSessionUseCaseOutput.php => StartSessionOutput.php} (85%) delete mode 100644 src/Application/UseCase/Session/StartSession/StartSessionUseCaseInput.php diff --git a/src/Application/UseCase/Dice/RollDice/RollDiceUseCaseOutput.php b/src/Application/UseCase/Dice/RollDice/RollDiceOutput.php similarity index 84% rename from src/Application/UseCase/Dice/RollDice/RollDiceUseCaseOutput.php rename to src/Application/UseCase/Dice/RollDice/RollDiceOutput.php index e062262..0df1acc 100644 --- a/src/Application/UseCase/Dice/RollDice/RollDiceUseCaseOutput.php +++ b/src/Application/UseCase/Dice/RollDice/RollDiceOutput.php @@ -4,7 +4,7 @@ namespace RPGPlayground\Application\UseCase\Dice\RollDice; -final class RollDiceUseCaseOutput +final class RollDiceOutput { public function __construct( public readonly int $rollValue, diff --git a/src/Application/UseCase/Dice/RollDice/RollDiceUseCaseInput.php b/src/Application/UseCase/Dice/RollDice/RollDiceUseCaseInput.php deleted file mode 100644 index 1d503ae..0000000 --- a/src/Application/UseCase/Dice/RollDice/RollDiceUseCaseInput.php +++ /dev/null @@ -1,26 +0,0 @@ - $modifiers An array of modifiers to apply to the roll (e.g., +5, -2, x2) - * @param int $multiplier The number of times to roll the dice - * @throws \InvalidArgumentException - */ - public function __construct( - public readonly Dice $dice, - public readonly array $modifiers, - public readonly int $multiplier = 1, - ) { - if ($multiplier < 1) { - throw new \InvalidArgumentException('Invalid multiplier'); - } - } -} diff --git a/src/Application/UseCase/Session/StartSession/StartSessionUseCaseOutput.php b/src/Application/UseCase/Session/StartSession/StartSessionOutput.php similarity index 85% rename from src/Application/UseCase/Session/StartSession/StartSessionUseCaseOutput.php rename to src/Application/UseCase/Session/StartSession/StartSessionOutput.php index f9652b8..bb7e0fb 100644 --- a/src/Application/UseCase/Session/StartSession/StartSessionUseCaseOutput.php +++ b/src/Application/UseCase/Session/StartSession/StartSessionOutput.php @@ -6,7 +6,7 @@ use RPGPlayground\Domain\Entities\Session; -final class StartSessionUseCaseOutput +final class StartSessionOutput { public function __construct( public readonly Session $session, diff --git a/src/Application/UseCase/Session/StartSession/StartSessionUseCaseInput.php b/src/Application/UseCase/Session/StartSession/StartSessionUseCaseInput.php deleted file mode 100644 index ce20884..0000000 --- a/src/Application/UseCase/Session/StartSession/StartSessionUseCaseInput.php +++ /dev/null @@ -1,22 +0,0 @@ -name = htmlspecialchars(trim($name)); - } -} From 737be6df02442f9869f980d136e357a525d6e0c5 Mon Sep 17 00:00:00 2001 From: valb-mig Date: Wed, 18 Mar 2026 17:27:01 -0300 Subject: [PATCH 08/18] feat: add Input value objects with eco Result validation --- .../UseCase/Dice/RollDice/RollDiceInput.php | 44 +++++++++++++++++++ .../StartSession/StartSessionInput.php | 28 ++++++++++++ 2 files changed, 72 insertions(+) create mode 100644 src/Application/UseCase/Dice/RollDice/RollDiceInput.php create mode 100644 src/Application/UseCase/Session/StartSession/StartSessionInput.php diff --git a/src/Application/UseCase/Dice/RollDice/RollDiceInput.php b/src/Application/UseCase/Dice/RollDice/RollDiceInput.php new file mode 100644 index 0000000..3b97804 --- /dev/null +++ b/src/Application/UseCase/Dice/RollDice/RollDiceInput.php @@ -0,0 +1,44 @@ + $modifiers The modifiers to apply to the roll + * @param int $multiplier The number of times to roll the dice + * @throws \InvalidArgumentException + */ + private function __construct( + public readonly Dice $dice, + public readonly array $modifiers, + public readonly int $multiplier = 1, + ) {} + + /** + * @param Dice $dice The dice to roll + * @param array $modifiers The modifiers to apply to the roll + * @param int $multiplier The number of times to roll the dice + * @return Result + * */ + public static function create(Dice $dice, array $modifiers = [], int $multiplier = 1): Result + { + try { + if ($multiplier < 1) { + throw new \InvalidArgumentException('Multiplier must be greater than or equal to 1.'); + } + + return Result::ok(new self($dice, $modifiers, $multiplier)); + } catch (\InvalidArgumentException $e) { + return Result::fail(Error::generic($e->getMessage())); + } + } +} diff --git a/src/Application/UseCase/Session/StartSession/StartSessionInput.php b/src/Application/UseCase/Session/StartSession/StartSessionInput.php new file mode 100644 index 0000000..fa563e6 --- /dev/null +++ b/src/Application/UseCase/Session/StartSession/StartSessionInput.php @@ -0,0 +1,28 @@ + */ + public static function create(string $name): Result + { + /** @var Result */ + return Result::ok($name)->ensure([ + [fn($name) => !empty($name), Error::validation('name', 'Name cannot be empty')], + ])->transform(fn($name): self => new self(StrHandler::sanitize($name))); + } +} From 98ccc222e7b187b9a8ae322566c414a1b92fd515 Mon Sep 17 00:00:00 2001 From: valb-mig Date: Wed, 18 Mar 2026 17:27:19 -0300 Subject: [PATCH 09/18] refactor: remove unnecessary try/catch from use cases --- .../UseCase/Dice/RollDice/RollDiceUseCase.php | 81 +++---------------- .../StartSession/StartSessionUseCase.php | 28 +++---- 2 files changed, 25 insertions(+), 84 deletions(-) diff --git a/src/Application/UseCase/Dice/RollDice/RollDiceUseCase.php b/src/Application/UseCase/Dice/RollDice/RollDiceUseCase.php index cd17b0b..6928434 100644 --- a/src/Application/UseCase/Dice/RollDice/RollDiceUseCase.php +++ b/src/Application/UseCase/Dice/RollDice/RollDiceUseCase.php @@ -4,82 +4,27 @@ namespace RPGPlayground\Application\UseCase\Dice\RollDice; -use Psl\Async; -use RPGPlayground\Core\Utils\Result; +use Eco\Error; +use Eco\Result; +use RPGPlayground\Application\UseCase\Dice\RollDice\RollDiceInput; +use RPGPlayground\Application\UseCase\Dice\RollDice\RollDiceOutput; use RPGPlayground\Domain\Actions\Dice\RollDiceAction; final class RollDiceUseCase { - /** - * @return Result - */ - public function run(RollDiceUseCaseInput $input): Result + /** @return Result */ + public static function handle(RollDiceInput $input): Result { - try { - $dice = $input->dice; - $modifiers = $input->modifiers; - $multiplier = $input->multiplier; + $total = 0; - $rollValue = 0; - - // WIP: Async and enhance performace - // Enhance performance for large multipliers by processing rolls in chunks and using async tasks - // Labels: refactor - // Assignees: valb-mig - $awaitables = []; - foreach ($this->generateChunks($multiplier, 250_000) as $chunkSize) { - $awaitables[] = Async\run(function () use ($chunkSize, $dice): int { - $sum = 0; - for ($i = 0; $i < (int) $chunkSize; $i++) { - $sum += RollDiceAction::roll($dice); - } - return $sum; - }); - } - - $rollValue = array_sum(Async\all($awaitables)); - - foreach ($modifiers as $modifier) { - $symbol = $modifier[0]; - $integer = (int) substr($modifier, 1); - - switch ($symbol) { - case '+': - $rollValue += $integer; - break; - case '-': - $rollValue -= $integer; - break; - case '*': - case 'x': - $rollValue *= $integer; - break; - case '/': - case '÷': - if ($integer !== 0) { - $rollValue /= $integer; - } - break; - default: - throw new \InvalidArgumentException("Invalid modifier: {$modifier}"); - } - } - - $rollValue = (int) ceil($rollValue); - - return Result::success('Success on roll: ' . $rollValue, new RollDiceUseCaseOutput($rollValue)); - } catch (\Exception $e) { - return Result::error($e->getMessage()); + for ($i = 0; $i < $input->multiplier; $i++) { + $total += RollDiceAction::roll($input->dice); } - } - /** @return \Generator */ - private function generateChunks(int $multiplier, int $chunkSize): \Generator - { - $remaining = $multiplier; - while ($remaining > 0) { - yield min($chunkSize, $remaining); - $remaining -= $chunkSize; + foreach ($input->modifiers as $modifier) { + $total = $modifier->apply($total); } + + return Result::ok(new RollDiceOutput($total)); } } diff --git a/src/Application/UseCase/Session/StartSession/StartSessionUseCase.php b/src/Application/UseCase/Session/StartSession/StartSessionUseCase.php index 3fd417b..2cde627 100644 --- a/src/Application/UseCase/Session/StartSession/StartSessionUseCase.php +++ b/src/Application/UseCase/Session/StartSession/StartSessionUseCase.php @@ -4,31 +4,27 @@ namespace RPGPlayground\Application\UseCase\Session\StartSession; -use RPGPlayground\Application\UseCase\Session\StartSession\StartSessionUseCaseOutput; +use Eco\Error; +use Eco\Result; +use RPGPlayground\Application\UseCase\Session\StartSession\StartSessionInput; +use RPGPlayground\Application\UseCase\Session\StartSession\StartSessionOutput; +use RPGPlayground\Core\Utils\Identifier; use RPGPlayground\Domain\Entities\Session; -use RPGPlayground\Domain\ValueObjects\Utils\Identifier; -use RPGPlayground\Domain\ValueObjects\Utils\Result; final class StartSessionUseCase { /** - * @return Result + * @param StartSessionInput $input + * @return Result */ - public function run(StartSessionUseCaseInput $input): Result + public static function handle(StartSessionInput $input): Result { - try { - $resultStartSession = new StartSessionUseCaseOutput(session: new Session( + return Result::ok( + new StartSessionOutput(session: new Session( name: $input->name, identifier: Identifier::generate(), createdAt: new \DateTime(), - )); - - return Result::success( - 'Session (' . $resultStartSession->session->identifier->value . ') started successfully.', - $resultStartSession, - ); - } catch (\InvalidArgumentException $e) { - return Result::error('Failed to start session' . $e->getMessage()); - } + )), + ); } } From f405b130ff793deb62355f15f6a4f2a51f16b2ea Mon Sep 17 00:00:00 2001 From: valb-mig Date: Wed, 18 Mar 2026 17:28:06 -0300 Subject: [PATCH 10/18] feat: add DiceModifier value object with fromString and apply --- src/Domain/ValueObjects/DiceModifier.php | 58 ++++++++++++++++++++++++ src/Domain/ValueObjects/Test.php | 28 ------------ 2 files changed, 58 insertions(+), 28 deletions(-) create mode 100644 src/Domain/ValueObjects/DiceModifier.php delete mode 100644 src/Domain/ValueObjects/Test.php diff --git a/src/Domain/ValueObjects/DiceModifier.php b/src/Domain/ValueObjects/DiceModifier.php new file mode 100644 index 0000000..33b4ad2 --- /dev/null +++ b/src/Domain/ValueObjects/DiceModifier.php @@ -0,0 +1,58 @@ +symbol) { + '+' => $rollValue + $this->value, + '-' => $rollValue - $this->value, + '*', 'x' => $rollValue * $this->value, + '/' => $this->value !== 0 ? $rollValue / $this->value : $rollValue, + }); + } +} diff --git a/src/Domain/ValueObjects/Test.php b/src/Domain/ValueObjects/Test.php deleted file mode 100644 index b8c0eb9..0000000 --- a/src/Domain/ValueObjects/Test.php +++ /dev/null @@ -1,28 +0,0 @@ - Date: Wed, 18 Mar 2026 17:28:31 -0300 Subject: [PATCH 11/18] feat: add StrHandler utility for string sanitization --- src/Core/Handler/StrHandler.php | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 src/Core/Handler/StrHandler.php diff --git a/src/Core/Handler/StrHandler.php b/src/Core/Handler/StrHandler.php new file mode 100644 index 0000000..fdcf263 --- /dev/null +++ b/src/Core/Handler/StrHandler.php @@ -0,0 +1,13 @@ + Date: Wed, 18 Mar 2026 17:28:42 -0300 Subject: [PATCH 12/18] refactor: remove dead try/catch from RollDiceAction --- src/Domain/Actions/Dice/RollDiceAction.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Domain/Actions/Dice/RollDiceAction.php b/src/Domain/Actions/Dice/RollDiceAction.php index 8e0e2c8..566cbcb 100644 --- a/src/Domain/Actions/Dice/RollDiceAction.php +++ b/src/Domain/Actions/Dice/RollDiceAction.php @@ -15,6 +15,6 @@ final class RollDiceAction */ public static function roll(Dice $dice): int { - return (int) ceil(rand(Dice::MINIMUM_VALUE, $dice->sides)); + return random_int(Dice::MINIMUM_VALUE, $dice->sides); } } From c7592db007cd8967148d2fc72e07daafadce1107 Mon Sep 17 00:00:00 2001 From: valb-mig Date: Wed, 18 Mar 2026 17:28:54 -0300 Subject: [PATCH 13/18] chore: remove console entry point commands and tests --- .../Console/Dice/RollDiceCommand.php | 155 ------------------ .../Console/Session/StartSessionCommand.php | 118 ------------- .../Console/Dice/RollDiceCommandTest.php | 82 --------- .../Session/StartSessionCommandTest.php | 42 ----- 4 files changed, 397 deletions(-) delete mode 100644 src/Infrastructure/EntryPoints/Console/Dice/RollDiceCommand.php delete mode 100644 src/Infrastructure/EntryPoints/Console/Session/StartSessionCommand.php delete mode 100644 tests/Infrastructure/EntryPoints/Console/Dice/RollDiceCommandTest.php delete mode 100644 tests/Infrastructure/EntryPoints/Console/Session/StartSessionCommandTest.php diff --git a/src/Infrastructure/EntryPoints/Console/Dice/RollDiceCommand.php b/src/Infrastructure/EntryPoints/Console/Dice/RollDiceCommand.php deleted file mode 100644 index 3a9142a..0000000 --- a/src/Infrastructure/EntryPoints/Console/Dice/RollDiceCommand.php +++ /dev/null @@ -1,155 +0,0 @@ -addArgument('dice_params', InputArgument::OPTIONAL, 'Dice params (e.g., 2d20+5)'); - } catch (\Exception $e) { - // Ignore exceptions during configuration to allow for graceful handling in interact() - } - } - - #[\Override] - protected function interact(InputInterface $input, OutputInterface $output): void - { - try { - $diceParams = (string) $input->getArgument('dice_params'); - - if (!empty($diceParams)) { - return; - } - - $io = new SymfonyStyle($input, $output); - $input->setArgument('dice_params', $io->ask('Enter dice parameters (e.g., 2d20+5)')); - } catch (\Exception) { - // Ignore exceptions during interaction to allow for graceful handling in execute() - } - } - - // @mago-analyse-ignore-start - #[\Override] - protected function initialize(InputInterface $input, OutputInterface $output): void - { - try { - $diceParams = (string) $input->getArgument('dice_params'); - - if (!$diceParams) { - return; - } - - $baseMatches = []; - - if (!preg_match(self::DICE_PATTERN, $diceParams, $baseMatches)) { - return; - } - - $multiplier = (int) ($baseMatches[1] ?? 1); - $sides = (int) ( - $baseMatches[2] ?? throw new \InvalidArgumentException( - 'Invalid dice parameters. Expected format: [multiplier]d[max][modifiers]', - ) - ); - - if ($multiplier < 1) { - return; - } - - if ($sides < Dice::MINIMUM_VALUE) { - return; - } - } catch (\Exception) { - // Ignore exceptions during initialization to allow for interactive input - } - } - - // @mago-analyse-ignore-end - #[\Override] - protected function execute(InputInterface $input, OutputInterface $output): int - { - try { - $io = new SymfonyStyle($input, $output); - $diceParams = (string) $input->getArgument('dice_params'); - $baseMatches = []; - $modifierMatches = []; - - preg_match(self::DICE_PATTERN, $diceParams, $baseMatches); - preg_match_all(self::MODIFIER_PATTERN, $diceParams, $modifierMatches); - - $multiplier = (int) ($baseMatches[1] ?? 1); - $sides = (int) ( - $baseMatches[2] ?? throw new \InvalidArgumentException( - 'Invalid dice parameters. Expected format: [multiplier]d[max][modifiers]', - ) - ); - $modifiers = $modifierMatches[0] ?? []; - - $useCase = new RollDiceUseCase(); - - $resultRollValue = $useCase->run(new RollDiceUseCaseInput( - dice: new Dice($sides), - modifiers: $modifiers, - multiplier: $multiplier, - )); - - if ($resultRollValue->isError()) { - $io->error($resultRollValue->getMessage()); - return Command::FAILURE; - } - - $resultRollValue = $resultRollValue->getData(); - - if (!$resultRollValue) { - $io->error('An unexpected error occurred while rolling the dice.'); - return Command::FAILURE; - } - - $io->definitionList( - ['Entry' => $diceParams], - ['Dices' => "{$multiplier}x d{$sides}"], - ['Modifiers' => count($modifiers) > 0 ? implode(' ', $modifiers) : 'None'], - ); - - $io->block( - messages: 'TOTAL: ' . $resultRollValue->rollValue, - type: 'RESULT', - style: 'fg=black;bg=green;options=bold', - padding: true, - ); - - LogHandler::dispatch(Level::Info, 'Rolled dice', [ - 'dice_params' => $diceParams, - 'roll_value' => $resultRollValue->rollValue, - ]); - - return Command::SUCCESS; - } catch (\InvalidArgumentException $e) { - $io = new SymfonyStyle($input, $output); - $io->error($e->getMessage()); - LogHandler::dispatch(Level::Error, 'Roll dice command', ['exception' => $e->getMessage()]); - return Command::FAILURE; - } - } -} diff --git a/src/Infrastructure/EntryPoints/Console/Session/StartSessionCommand.php b/src/Infrastructure/EntryPoints/Console/Session/StartSessionCommand.php deleted file mode 100644 index 1475e00..0000000 --- a/src/Infrastructure/EntryPoints/Console/Session/StartSessionCommand.php +++ /dev/null @@ -1,118 +0,0 @@ -addArgument('name', InputArgument::OPTIONAL, 'Session name'); - } catch (\Exception) { - // Ignore exceptions during configuration to allow for graceful handling in interact() - } - } - - #[\Override] - protected function interact(InputInterface $input, OutputInterface $output): void - { - try { - $name = (string) $input->getArgument('name'); - - if (!empty($name)) { - return; - } - - $io = new SymfonyStyle($input, $output); - $input->setArgument('name', $io->ask('Enter session name')); - } catch (\Exception) { - // Ignore exceptions during interaction to allow for graceful handling in execute() - } - } - - // @mago-analyse-ignore-start - #[\Override] - protected function initialize(InputInterface $input, OutputInterface $output): void - { - try { - $name = (string) $input->getArgument('name'); - - if (!empty($name)) { - return; - } - } catch (\Exception) { - // Ignore exceptions during initialization to allow for interactive input - } - } - - // @mago-analyse-ignore-end - #[\Override] - protected function execute(InputInterface $input, OutputInterface $output): int - { - try { - $io = new SymfonyStyle($input, $output); - $name = (string) $input->getArgument('name'); - - $useCase = new StartSessionUseCase(); - - $resultStartSession = $useCase->run(new StartSessionUseCaseInput(name: $name)); - - if (!isset($resultStartSession)) { - $io->error('An unexpected error occurred while starting the session.'); - LogHandler::dispatch(Level::Error, 'Start session command', ['error' => 'Result is null']); - return Command::FAILURE; - } - - if ($resultStartSession->isError()) { - $io->error($resultStartSession->getMessage()); - return Command::FAILURE; - } - - $resultStartSession = $resultStartSession->getData(); - - if (!isset($resultStartSession)) { - $io->error('An unexpected error occurred while starting the session.'); - LogHandler::dispatch(Level::Error, 'Start session command', ['error' => 'Result data is null']); - return Command::FAILURE; - } - - $io->definitionList( - ['Identifier' => $resultStartSession->session->identifier->value], - ['Name' => $resultStartSession->session->name], - ['Created At' => $resultStartSession->session->createdAt->format('Y-m-d H:i:s')], - ); - - $io->block( - messages: 'IDENTIFIER: ' . $resultStartSession->session->identifier->value, - type: 'RESULT', - style: 'fg=black;bg=green;options=bold', - padding: true, - ); - - LogHandler::dispatch(Level::Info, 'Started session', [ - 'session_id' => $resultStartSession->session->identifier->value, - ]); - return Command::SUCCESS; - } catch (\InvalidArgumentException $e) { - $io = new SymfonyStyle($input, $output); - $io->error($e->getMessage()); - LogHandler::dispatch(Level::Error, 'Start session command', ['exception' => $e->getMessage()]); - return Command::FAILURE; - } - } -} diff --git a/tests/Infrastructure/EntryPoints/Console/Dice/RollDiceCommandTest.php b/tests/Infrastructure/EntryPoints/Console/Dice/RollDiceCommandTest.php deleted file mode 100644 index 2a06e9f..0000000 --- a/tests/Infrastructure/EntryPoints/Console/Dice/RollDiceCommandTest.php +++ /dev/null @@ -1,82 +0,0 @@ -tester = new CommandTester(new RollDiceCommand()); - } - - public function testRollDiceSuccessfully(): void - { - $this->tester->execute(['dice_params' => '1d20']); - - $this->tester->assertCommandIsSuccessful(); - static::assertStringContainsString('RESULT', $this->tester->getDisplay()); - } - - public function testRollDiceWithMultiplier(): void - { - $this->tester->execute(['dice_params' => '3d6']); - - $this->tester->assertCommandIsSuccessful(); - static::assertStringContainsString('3x d6', $this->tester->getDisplay()); - } - - public function testRollDiceWithPositiveModifier(): void - { - $this->tester->execute(['dice_params' => '2d10+5']); - - $this->tester->assertCommandIsSuccessful(); - static::assertStringContainsString('+5', $this->tester->getDisplay()); - } - - public function testRollDiceWithMultipleModifiers(): void - { - $this->tester->execute(['dice_params' => '2d10+5-3']); - - $this->tester->assertCommandIsSuccessful(); - static::assertStringContainsString('+5 -3', $this->tester->getDisplay()); - } - - public function testRollDiceWithoutMultiplierDefaultsToOne(): void - { - $this->tester->execute(['dice_params' => '1d20']); - - $this->tester->assertCommandIsSuccessful(); - static::assertStringContainsString('1x d20', $this->tester->getDisplay()); - } - - public function testDisplayShowsCorrectEntry(): void - { - $this->tester->execute(['dice_params' => '2d6+3']); - - static::assertStringContainsString('2d6+3', $this->tester->getDisplay()); - } - - public function testDisplayShowsNoneWhenNoModifiers(): void - { - $this->tester->execute(['dice_params' => '1d6']); - - static::assertStringContainsString('None', $this->tester->getDisplay()); - } - - public function testAsksForInputWhenNoArgumentProvided(): void - { - $this->tester->setInputs(['2d6+3']); - $this->tester->execute([]); - - $this->tester->assertCommandIsSuccessful(); - static::assertStringContainsString('2x d6', $this->tester->getDisplay()); - } -} diff --git a/tests/Infrastructure/EntryPoints/Console/Session/StartSessionCommandTest.php b/tests/Infrastructure/EntryPoints/Console/Session/StartSessionCommandTest.php deleted file mode 100644 index 695c4c6..0000000 --- a/tests/Infrastructure/EntryPoints/Console/Session/StartSessionCommandTest.php +++ /dev/null @@ -1,42 +0,0 @@ -tester = new CommandTester(new StartSessionCommand()); - } - - public function testStartSessionSuccessfully(): void - { - $this->tester->execute(['name' => 'Test Session']); - - $this->tester->assertCommandIsSuccessful(); - static::assertStringContainsString('IDENTIFIER', $this->tester->getDisplay()); - static::assertStringContainsString('Name', $this->tester->getDisplay()); - static::assertStringContainsString('Created At', $this->tester->getDisplay()); - } - - public function testStartSessionWithoutNamePromptsForInput(): void - { - $this->tester->setInputs(['Test Session']); - - $this->tester->execute([]); - - $this->tester->assertCommandIsSuccessful(); - static::assertStringContainsString('Enter session name', $this->tester->getDisplay()); - static::assertStringContainsString('IDENTIFIER', $this->tester->getDisplay()); - static::assertStringContainsString('Name', $this->tester->getDisplay()); - static::assertStringContainsString('Created At', $this->tester->getDisplay()); - } -} From 835039b455f718a3c69721bafc4c2a2a0d97542f Mon Sep 17 00:00:00 2001 From: valb-mig Date: Wed, 18 Mar 2026 17:29:07 -0300 Subject: [PATCH 14/18] test: add and update tests for inputs, use cases, actions and value objects --- .../Dice/RollDice/RollDiceInputTest.php | 91 +++++++++++ .../Dice/RollDice/RollDiceUseCaseTest.php | 145 ++++++++++++++---- .../StartSession/StartSessionInputTest.php | 58 +++++++ .../StartSession/StartSessionUseCaseTest.php | 95 ++++++++++++ .../Actions/Dice/RollDiceActionTest.php | 87 +++++++++++ tests/Domain/Entities/SessionTest.php | 53 +++++-- .../Domain/ValueObjects/DiceModifierTest.php | 134 ++++++++++++++++ tests/Domain/ValueObjects/DiceTest.php | 68 ++++++-- 8 files changed, 683 insertions(+), 48 deletions(-) create mode 100644 tests/Application/UseCase/Dice/RollDice/RollDiceInputTest.php create mode 100644 tests/Application/UseCase/Session/StartSession/StartSessionInputTest.php create mode 100644 tests/Application/UseCase/Session/StartSession/StartSessionUseCaseTest.php create mode 100644 tests/Domain/Actions/Dice/RollDiceActionTest.php create mode 100644 tests/Domain/ValueObjects/DiceModifierTest.php diff --git a/tests/Application/UseCase/Dice/RollDice/RollDiceInputTest.php b/tests/Application/UseCase/Dice/RollDice/RollDiceInputTest.php new file mode 100644 index 0000000..fd1830b --- /dev/null +++ b/tests/Application/UseCase/Dice/RollDice/RollDiceInputTest.php @@ -0,0 +1,91 @@ +assertTrue($result->isOk()); + } + + public function test_dice_is_stored_correctly(): void + { + $dice = new Dice(20); + $input = RollDiceInput::create($dice)->unwrap(); + + $this->assertSame($dice, $input->dice); + } + + public function test_modifiers_default_to_empty_array(): void + { + $input = RollDiceInput::create(new Dice(20))->unwrap(); + + $this->assertSame([], $input->modifiers); + } + + public function test_multiplier_defaults_to_one(): void + { + $input = RollDiceInput::create(new Dice(20))->unwrap(); + + $this->assertSame(1, $input->multiplier); + } + + public function test_modifiers_are_stored_correctly(): void + { + $modifiers = [ + DiceModifier::fromString('+5'), + DiceModifier::fromString('-2'), + ]; + + $input = RollDiceInput::create(new Dice(20), $modifiers)->unwrap(); + + $this->assertSame($modifiers, $input->modifiers); + } + + public function test_multiplier_is_stored_correctly(): void + { + $input = RollDiceInput::create(new Dice(20), [], 3)->unwrap(); + + $this->assertSame(3, $input->multiplier); + } + + // ------------------------------------------------------------------------- + // Validation failures + // ------------------------------------------------------------------------- + + public function test_create_fails_with_multiplier_zero(): void + { + $result = RollDiceInput::create(new Dice(20), [], 0); + + $this->assertTrue($result->isFail()); + } + + public function test_create_fails_with_negative_multiplier(): void + { + $result = RollDiceInput::create(new Dice(20), [], -1); + + $this->assertTrue($result->isFail()); + } + + public function test_error_message_on_invalid_multiplier(): void + { + $result = RollDiceInput::create(new Dice(20), [], 0); + $errors = $result->getErrorMessages(); + + $this->assertContains('Multiplier must be greater than or equal to 1.', $errors); + } +} diff --git a/tests/Application/UseCase/Dice/RollDice/RollDiceUseCaseTest.php b/tests/Application/UseCase/Dice/RollDice/RollDiceUseCaseTest.php index c410934..c9d4842 100644 --- a/tests/Application/UseCase/Dice/RollDice/RollDiceUseCaseTest.php +++ b/tests/Application/UseCase/Dice/RollDice/RollDiceUseCaseTest.php @@ -5,52 +5,143 @@ namespace Tests\Application\UseCase\Dice\RollDice; use PHPUnit\Framework\TestCase; +use RPGPlayground\Application\UseCase\Dice\RollDice\RollDiceInput; +use RPGPlayground\Application\UseCase\Dice\RollDice\RollDiceOutput; use RPGPlayground\Application\UseCase\Dice\RollDice\RollDiceUseCase; -use RPGPlayground\Application\UseCase\Dice\RollDice\RollDiceUseCaseInput; use RPGPlayground\Domain\ValueObjects\Dice; +use RPGPlayground\Domain\ValueObjects\DiceModifier; -class RollDiceUseCaseTest extends TestCase +final class RollDiceUseCaseTest extends TestCase { - public function testRollDice(): void + // ------------------------------------------------------------------------- + // Happy path + // ------------------------------------------------------------------------- + + public function test_handle_returns_ok(): void + { + $result = RollDiceUseCase::handle($this->makeInput(new Dice(20))); + + $this->assertTrue($result->isOk()); + } + + public function test_handle_returns_roll_dice_output(): void + { + $output = RollDiceUseCase::handle($this->makeInput(new Dice(20)))->unwrap(); + + $this->assertInstanceOf(RollDiceOutput::class, $output); + } + + public function test_roll_value_is_within_d20_range(): void + { + $output = RollDiceUseCase::handle($this->makeInput(new Dice(20)))->unwrap(); + + $this->assertGreaterThanOrEqual(1, $output->rollValue); + $this->assertLessThanOrEqual(20, $output->rollValue); + } + + public function test_roll_value_is_within_d6_range(): void + { + $output = RollDiceUseCase::handle($this->makeInput(new Dice(6)))->unwrap(); + + $this->assertGreaterThanOrEqual(1, $output->rollValue); + $this->assertLessThanOrEqual(6, $output->rollValue); + } + + // ------------------------------------------------------------------------- + // Multiplier + // ------------------------------------------------------------------------- + + public function test_multiplier_accumulates_rolls(): void + { + // 3x D1 = always 3 (D1 always returns 1) + $input = $this->makeInput(new Dice(1), multiplier: 3); + $output = RollDiceUseCase::handle($input)->unwrap(); + + $this->assertSame(3, $output->rollValue); + } + + public function test_single_multiplier_matches_d1_roll(): void { - $dice = new Dice(20); - $modifiers = ['+5', '-2', '*2', '/2']; - $multiplier = 2; + $input = $this->makeInput(new Dice(1), multiplier: 1); + $output = RollDiceUseCase::handle($input)->unwrap(); + + $this->assertSame(1, $output->rollValue); + } - $rollDice = new RollDiceUseCase(); + // ------------------------------------------------------------------------- + // Modifiers + // ------------------------------------------------------------------------- - $result = $rollDice->run(new RollDiceUseCaseInput($dice, $modifiers, $multiplier)); + public function test_addition_modifier_is_applied(): void + { + // D1 always rolls 1 → +4 = 5 + $input = $this->makeInput(new Dice(1), modifiers: [DiceModifier::fromString('+4')]); + $output = RollDiceUseCase::handle($input)->unwrap(); - static::assertNotNull($result->getData()); - static::assertFalse($result->isError()); - static::assertIsNumeric($result->getData()?->rollValue); + $this->assertSame(5, $output->rollValue); } - public function testRollDiceWithInvalidMultiplier(): void + public function test_subtraction_modifier_is_applied(): void { - $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessage('Invalid multiplier'); + // D1 always rolls 1 → -1 = 0 + $input = $this->makeInput(new Dice(1), modifiers: [DiceModifier::fromString('-1')]); + $output = RollDiceUseCase::handle($input)->unwrap(); - $dice = new Dice(20); - $modifiers = ['+5', '-2', '*2', '/2']; - $multiplier = -1; + $this->assertSame(0, $output->rollValue); + } - $rollDice = new RollDiceUseCase(); + public function test_multiplication_modifier_is_applied(): void + { + // D1 always rolls 1 → x3 = 3 + $input = $this->makeInput(new Dice(1), modifiers: [DiceModifier::fromString('x3')]); + $output = RollDiceUseCase::handle($input)->unwrap(); - $result = $rollDice->run(new RollDiceUseCaseInput($dice, $modifiers, $multiplier)); + $this->assertSame(3, $output->rollValue); } - public function testRollDiceWithInvalidModifier(): void + public function test_division_modifier_is_applied(): void { - $dice = new Dice(20); - $modifiers = ['+5', '-2', '*2', '/2', '%3']; - $multiplier = 2; + // 3x D1 = 3 → /3 = 1 + $input = $this->makeInput(new Dice(1), modifiers: [DiceModifier::fromString('/3')], multiplier: 3); + $output = RollDiceUseCase::handle($input)->unwrap(); - $rollDice = new RollDiceUseCase(); + $this->assertSame(1, $output->rollValue); + } - $result = $rollDice->run(new RollDiceUseCaseInput($dice, $modifiers, $multiplier)); + public function test_multiple_modifiers_are_applied_in_order(): void + { + // D1 = 1 → +9 = 10 → /2 = 5 + $input = $this->makeInput(new Dice(1), modifiers: [ + DiceModifier::fromString('+9'), + DiceModifier::fromString('/2'), + ]); - static::assertTrue($result->isError()); - static::assertStringContainsString('Invalid modifier: %3', $result->getMessage()); + $output = RollDiceUseCase::handle($input)->unwrap(); + + $this->assertSame(5, $output->rollValue); + } + + public function test_no_modifiers_returns_raw_roll(): void + { + // D1 always 1, no modifiers + $input = $this->makeInput(new Dice(1)); + $output = RollDiceUseCase::handle($input)->unwrap(); + + $this->assertSame(1, $output->rollValue); + } + + // ------------------------------------------------------------------------- + // Helpers + // ------------------------------------------------------------------------- + + /** + * @param Dice $dice + * @param array $modifiers + * @param int $multiplier + * @return RollDiceInput + */ + private function makeInput(Dice $dice, array $modifiers = [], int $multiplier = 1): RollDiceInput + { + return RollDiceInput::create($dice, $modifiers, $multiplier)->unwrap(); } } diff --git a/tests/Application/UseCase/Session/StartSession/StartSessionInputTest.php b/tests/Application/UseCase/Session/StartSession/StartSessionInputTest.php new file mode 100644 index 0000000..88b9a91 --- /dev/null +++ b/tests/Application/UseCase/Session/StartSession/StartSessionInputTest.php @@ -0,0 +1,58 @@ +assertTrue($result->isOk()); + } + + public function test_input_name_is_stored_correctly(): void + { + $input = StartSessionInput::create('My Campaign')->unwrap(); + + $this->assertSame('My Campaign', $input->name); + } + + public function test_name_is_sanitized(): void + { + // StrHandler::sanitize should trim whitespace + $input = StartSessionInput::create(' Trimmed Name ')->unwrap(); + + $this->assertSame('Trimmed Name', $input->name); + } + + // ------------------------------------------------------------------------- + // Validation failures + // ------------------------------------------------------------------------- + + public function test_create_fails_with_empty_name(): void + { + $result = StartSessionInput::create(''); + + $this->assertTrue($result->isFail()); + } + + public function test_error_has_validation_type_for_empty_name(): void + { + $result = StartSessionInput::create(''); + $error = $result->getErrors()[0]; + + $this->assertSame('name', $error->field); + $this->assertSame('Name cannot be empty', $error->message); + } +} diff --git a/tests/Application/UseCase/Session/StartSession/StartSessionUseCaseTest.php b/tests/Application/UseCase/Session/StartSession/StartSessionUseCaseTest.php new file mode 100644 index 0000000..a305e2f --- /dev/null +++ b/tests/Application/UseCase/Session/StartSession/StartSessionUseCaseTest.php @@ -0,0 +1,95 @@ +makeInput('My Campaign'); + $result = StartSessionUseCase::handle($input); + + $this->assertTrue($result->isOk()); + } + + public function test_handle_returns_start_session_output(): void + { + $input = $this->makeInput('My Campaign'); + $output = StartSessionUseCase::handle($input)->unwrap(); + + $this->assertInstanceOf(StartSessionOutput::class, $output); + } + + public function test_output_contains_session_entity(): void + { + $input = $this->makeInput('My Campaign'); + $output = StartSessionUseCase::handle($input)->unwrap(); + + $this->assertInstanceOf(Session::class, $output->session); + } + + public function test_session_name_matches_input(): void + { + $input = $this->makeInput('My Campaign'); + $output = StartSessionUseCase::handle($input)->unwrap(); + + $this->assertSame('My Campaign', $output->session->name); + } + + public function test_session_has_identifier(): void + { + $input = $this->makeInput('My Campaign'); + $output = StartSessionUseCase::handle($input)->unwrap(); + + $this->assertNotEmpty($output->session->identifier->value); + } + + public function test_each_session_gets_unique_identifier(): void + { + $input = $this->makeInput('My Campaign'); + + $id1 = StartSessionUseCase::handle($input) + ->unwrap() + ->session + ->identifier + ->value; + $id2 = StartSessionUseCase::handle($input) + ->unwrap() + ->session + ->identifier + ->value; + + $this->assertNotSame($id1, $id2); + } + + public function test_session_created_at_is_set(): void + { + $before = new \DateTime(); + $output = StartSessionUseCase::handle($this->makeInput('My Campaign'))->unwrap(); + $after = new \DateTime(); + + $this->assertGreaterThanOrEqual($before, $output->session->createdAt); + $this->assertLessThanOrEqual($after, $output->session->createdAt); + } + + // ------------------------------------------------------------------------- + // Helpers + // ------------------------------------------------------------------------- + + private function makeInput(string $name): StartSessionInput + { + return StartSessionInput::create($name)->unwrap(); + } +} diff --git a/tests/Domain/Actions/Dice/RollDiceActionTest.php b/tests/Domain/Actions/Dice/RollDiceActionTest.php new file mode 100644 index 0000000..5253507 --- /dev/null +++ b/tests/Domain/Actions/Dice/RollDiceActionTest.php @@ -0,0 +1,87 @@ +assertGreaterThanOrEqual(Dice::MINIMUM_VALUE, $result); + $this->assertLessThanOrEqual(20, $result); + } + + public function test_roll_never_returns_zero(): void + { + $dice = new Dice(6); + + for ($i = 0; $i < 100; $i++) { + $this->assertGreaterThanOrEqual(Dice::MINIMUM_VALUE, RollDiceAction::roll($dice)); + } + } + + public function test_roll_never_exceeds_sides(): void + { + $dice = new Dice(6); + + for ($i = 0; $i < 100; $i++) { + $this->assertLessThanOrEqual(6, RollDiceAction::roll($dice)); + } + } + + // ------------------------------------------------------------------------- + // D1 edge case + // ------------------------------------------------------------------------- + + public function test_d1_always_returns_one(): void + { + $dice = new Dice(1); + + for ($i = 0; $i < 10; $i++) { + $this->assertSame(1, RollDiceAction::roll($dice)); + } + } + + // ------------------------------------------------------------------------- + // Common dice types + // ------------------------------------------------------------------------- + + #[\PHPUnit\Framework\Attributes\DataProvider('commonDiceProvider')] + public function test_roll_stays_within_range_for_common_dice(int $sides): void + { + $dice = new Dice($sides); + $result = RollDiceAction::roll($dice); + + $this->assertGreaterThanOrEqual(Dice::MINIMUM_VALUE, $result); + $this->assertLessThanOrEqual($sides, $result); + } + + /** + * @return array + */ + public static function commonDiceProvider(): array + { + return [ + 'D4' => [4], + 'D6' => [6], + 'D8' => [8], + 'D10' => [10], + 'D12' => [12], + 'D20' => [20], + 'D100' => [100], + ]; + } +} diff --git a/tests/Domain/Entities/SessionTest.php b/tests/Domain/Entities/SessionTest.php index 6afac76..06058f1 100644 --- a/tests/Domain/Entities/SessionTest.php +++ b/tests/Domain/Entities/SessionTest.php @@ -4,28 +4,57 @@ namespace Tests\Domain\Entities; -use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; use RPGPlayground\Core\Utils\Identifier; use RPGPlayground\Domain\Entities\Session; -class SessionTest extends TestCase +final class SessionTest extends TestCase { - #[Test] - public function shouldSucceedWhenCreateSession(): void + // ------------------------------------------------------------------------- + // Happy path + // ------------------------------------------------------------------------- + + public function test_session_is_created_with_valid_data(): void + { + $session = $this->makeSession('My Campaign'); + + $this->assertSame('My Campaign', $session->name); + } + + public function test_session_identifier_is_stored(): void { - $session = new Session(name: 'Test Session', identifier: Identifier::generate(), createdAt: new \DateTime()); + $identifier = Identifier::generate(); + $session = new Session('My Campaign', $identifier, new \DateTime()); - static::assertInstanceOf(Session::class, $session); - static::assertSame('Test Session', $session->name); - static::assertInstanceOf(Identifier::class, $session->identifier); - static::assertInstanceOf(\DateTime::class, $session->createdAt); + $this->assertSame($identifier->value, $session->identifier->value); } - #[Test] - public function shouldFailWhenCreateSessionWithEmptyName(): void + public function test_session_created_at_is_stored(): void + { + $date = new \DateTime('2024-01-01'); + $session = new Session('My Campaign', Identifier::generate(), $date); + + $this->assertSame($date, $session->createdAt); + } + + // ------------------------------------------------------------------------- + // Validation + // ------------------------------------------------------------------------- + + public function test_session_throws_on_empty_name(): void { $this->expectException(\InvalidArgumentException::class); - new Session(name: '', identifier: Identifier::generate(), createdAt: new \DateTime()); + $this->expectExceptionMessage('Session name cannot be empty'); + + new Session('', Identifier::generate(), new \DateTime()); + } + + // ------------------------------------------------------------------------- + // Helpers + // ------------------------------------------------------------------------- + + private function makeSession(string $name): Session + { + return new Session($name, Identifier::generate(), new \DateTime()); } } diff --git a/tests/Domain/ValueObjects/DiceModifierTest.php b/tests/Domain/ValueObjects/DiceModifierTest.php new file mode 100644 index 0000000..5ef5997 --- /dev/null +++ b/tests/Domain/ValueObjects/DiceModifierTest.php @@ -0,0 +1,134 @@ +assertSame($expectedSymbol, $dm->symbol); + $this->assertSame($expectedValue, $dm->value); + } + + /** + * @return array + */ + public static function validModifierProvider(): array + { + return [ + 'plus' => ['+5', '+', 5], + 'minus' => ['-3', '-', 3], + 'multiply star' => ['*2', '*', 2], + 'multiply x' => ['x2', 'x', 2], + 'divide slash' => ['/4', '/', 4], + 'large value' => ['+100', '+', 100], + ]; + } + + // ------------------------------------------------------------------------- + // fromString — validation + // ------------------------------------------------------------------------- + + public function test_from_string_throws_on_invalid_symbol(): void + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid modifier symbol: o'); + + DiceModifier::fromString('o2'); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('invalidSymbolProvider')] + public function test_from_string_throws_for_unknown_symbols(string $modifier): void + { + $this->expectException(\InvalidArgumentException::class); + + DiceModifier::fromString($modifier); + } + + /** + * @return array + */ + public static function invalidSymbolProvider(): array + { + return [ + 'letter o' => ['o2'], + 'hash' => ['#5'], + 'percent' => ['%2'], + 'at sign' => ['@3'], + 'digit' => ['52'], + ]; + } + + // ------------------------------------------------------------------------- + // apply — each operator + // ------------------------------------------------------------------------- + + public function test_apply_addition(): void + { + $result = DiceModifier::fromString('+5')->apply(10); + + $this->assertSame(15, $result); + } + + public function test_apply_subtraction(): void + { + $result = DiceModifier::fromString('-3')->apply(10); + + $this->assertSame(7, $result); + } + + public function test_apply_multiplication_star(): void + { + $result = DiceModifier::fromString('*2')->apply(10); + + $this->assertSame(20, $result); + } + + public function test_apply_multiplication_x(): void + { + $result = DiceModifier::fromString('x2')->apply(10); + + $this->assertSame(20, $result); + } + + public function test_apply_division_slash(): void + { + $result = DiceModifier::fromString('/4')->apply(20); + + $this->assertSame(5, $result); + } + + // ------------------------------------------------------------------------- + // apply — edge cases + // ------------------------------------------------------------------------- + + public function test_apply_division_rounds_up_with_ceil(): void + { + // 10 / 3 = 3.33... → ceil → 4 + $result = DiceModifier::fromString('/3')->apply(10); + + $this->assertSame(4, $result); + } + + public function test_apply_subtraction_can_go_negative(): void + { + $result = DiceModifier::fromString('-15')->apply(10); + + $this->assertSame(-5, $result); + } +} diff --git a/tests/Domain/ValueObjects/DiceTest.php b/tests/Domain/ValueObjects/DiceTest.php index aa51ceb..5e6f934 100644 --- a/tests/Domain/ValueObjects/DiceTest.php +++ b/tests/Domain/ValueObjects/DiceTest.php @@ -2,26 +2,76 @@ declare(strict_types=1); -namespace Tests\Domain\ValueObjects; +namespace RPGPlayground\Tests\Domain\ValueObjects; -use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; use RPGPlayground\Domain\ValueObjects\Dice; -class DiceTest extends TestCase +final class DiceTest extends TestCase { - #[Test] - public function shouldSucceedWhenCreateDice(): void + // ------------------------------------------------------------------------- + // Happy path + // ------------------------------------------------------------------------- + + public function test_dice_is_created_with_valid_sides(): void { $dice = new Dice(20); - static::assertInstanceOf(Dice::class, $dice); - static::assertSame(20, $dice->sides); + + $this->assertSame(20, $dice->sides); + } + + public function test_minimum_value_constant_is_one(): void + { + $this->assertSame(1, Dice::MINIMUM_VALUE); + } + + public function test_d1_is_valid(): void + { + $dice = new Dice(1); + + $this->assertSame(1, $dice->sides); } - #[Test] - public function shouldFailWhenCreateDiceWithZeroSides(): void + #[\PHPUnit\Framework\Attributes\DataProvider('commonDiceProvider')] + public function test_common_dice_are_valid(int $sides): void + { + $dice = new Dice($sides); + + $this->assertSame($sides, $dice->sides); + } + + /** + * @return array + */ + public static function commonDiceProvider(): array + { + return [ + 'D4' => [4], + 'D6' => [6], + 'D8' => [8], + 'D10' => [10], + 'D12' => [12], + 'D20' => [20], + 'D100' => [100], + ]; + } + + // ------------------------------------------------------------------------- + // Validation + // ------------------------------------------------------------------------- + + public function test_throws_on_zero_sides(): void { $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid number of sides for a dice'); + new Dice(0); } + + public function test_throws_on_negative_sides(): void + { + $this->expectException(\InvalidArgumentException::class); + + new Dice(-1); + } } From fa8e27c04699efc19193478940a0afa42515bc33 Mon Sep 17 00:00:00 2001 From: valb-mig Date: Wed, 18 Mar 2026 17:30:12 -0300 Subject: [PATCH 15/18] chore: update deps, tooling config and remove index.php entrypoint --- .github/issues.json | 11 ++-- bin/console | 6 +-- composer.json | 6 ++- composer.lock | 123 +++++++++++++++++++++++++++++++------------- index.php | 62 ---------------------- logs/console.log | 0 mago.toml | 5 +- 7 files changed, 99 insertions(+), 114 deletions(-) delete mode 100644 index.php create mode 100644 logs/console.log diff --git a/.github/issues.json b/.github/issues.json index d256d3c..4fa1a25 100644 --- a/.github/issues.json +++ b/.github/issues.json @@ -1,12 +1,7 @@ [ { - "title": "(refactor): Return types and throwing exceptions", - "body": "Its necessary to update the return types of the use case and its input, as well as to throw exceptions for invalid input. This will improve the robustness and clarity of the code.", - "labels": ["refactor"] - }, - { - "title": "(refactor): Command structure", - "body": "Update the command structure to something more clear and abstract, too many information in one single command can be overwhelming and hard to maintain. Consider breaking it down into smaller, more focused commands or methods.", - "labels": ["refactor"] + "title": "(feat): Advantage dices", + "body": "Implement advantage and disadvantage mechanics for dice rolls.\n\nAdvantage: roll two dice of the same type and keep the highest result.\nDisadvantage: roll two dice of the same type and keep the lowest result.\n\nThis is a common mechanic in tabletop RPG systems such as D&D 5e and should integrate naturally with the existing `RollDiceInput` and `RollDiceUseCase` pipeline.", + "labels": ["enhancement"] } ] \ No newline at end of file diff --git a/bin/console b/bin/console index e456df7..5baeaad 100755 --- a/bin/console +++ b/bin/console @@ -7,9 +7,7 @@ require __DIR__ . '/../bootstrap.php'; use Monolog\Handler\StreamHandler; use Monolog\Logger; -use RPGPlayground\Infrastructure\EntryPoints\Console\Dice\RollDiceCommand; -use RPGPlayground\Infrastructure\EntryPoints\Console\Session\StartSessionCommand; -use RPGPlayground\Infrastructure\Handler\LogHandler; +use RPGPlayground\Core\Handler\LogHandler; use Symfony\Component\Console\Application; LogHandler::bind(new Logger('console')); @@ -18,8 +16,6 @@ LogHandler::stream(new StreamHandler(__DIR__ . '/../logs/console.log')); $application = new Application(); $application->addCommands([ - new RollDiceCommand(), - new StartSessionCommand(), ]); $application->run(); \ No newline at end of file diff --git a/composer.json b/composer.json index 8f519cf..d05eb0a 100644 --- a/composer.json +++ b/composer.json @@ -2,7 +2,8 @@ "name": "valb/php.rpg-playground", "autoload": { "psr-4": { - "RPGPlayground\\": "src/" + "RPGPlayground\\": "src/", + "Tests\\": "tests/" } }, "authors": [ @@ -23,6 +24,7 @@ "symfony/console": "^8.0", "symfony/var-dumper": "^8.0", "monolog/monolog": "^3.10", - "azjezz/psl": "^4.3" + "azjezz/psl": "^4.3", + "valb/eco": "^4.0" } } diff --git a/composer.lock b/composer.lock index cada3fa..0995ba8 100644 --- a/composer.lock +++ b/composer.lock @@ -4,19 +4,19 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "acc9a88802050a8102369e4ad57e4a4b", + "content-hash": "e39ae5ab4314acba626de99cdf0aed8a", "packages": [ { "name": "azjezz/psl", "version": "4.3.0", "source": { "type": "git", - "url": "https://github.com/azjezz/psl.git", + "url": "https://github.com/php-standard-library/php-standard-library.git", "reference": "74c95be0214eb7ea39146ed00ac4eb71b45d787b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/azjezz/psl/zipball/74c95be0214eb7ea39146ed00ac4eb71b45d787b", + "url": "https://api.github.com/repos/php-standard-library/php-standard-library/zipball/74c95be0214eb7ea39146ed00ac4eb71b45d787b", "reference": "74c95be0214eb7ea39146ed00ac4eb71b45d787b", "shasum": "" }, @@ -67,8 +67,8 @@ ], "description": "PHP Standard Library", "support": { - "issues": "https://github.com/azjezz/psl/issues", - "source": "https://github.com/azjezz/psl/tree/4.3.0" + "issues": "https://github.com/php-standard-library/php-standard-library/issues", + "source": "https://github.com/php-standard-library/php-standard-library/tree/4.3.0" }, "funding": [ { @@ -80,6 +80,7 @@ "type": "github" } ], + "abandoned": "php-standard-library/php-standard-library", "time": "2026-02-24T01:58:53+00:00" }, { @@ -362,16 +363,16 @@ }, { "name": "symfony/console", - "version": "v8.0.4", + "version": "v8.0.7", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "ace03c4cf9805080ff40cbeec69fca180c339a3b" + "reference": "15ed9008a4ebe2d6a78e4937f74e0c13ef2e618a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/ace03c4cf9805080ff40cbeec69fca180c339a3b", - "reference": "ace03c4cf9805080ff40cbeec69fca180c339a3b", + "url": "https://api.github.com/repos/symfony/console/zipball/15ed9008a4ebe2d6a78e4937f74e0c13ef2e618a", + "reference": "15ed9008a4ebe2d6a78e4937f74e0c13ef2e618a", "shasum": "" }, "require": { @@ -428,7 +429,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v8.0.4" + "source": "https://github.com/symfony/console/tree/v8.0.7" }, "funding": [ { @@ -448,7 +449,7 @@ "type": "tidelift" } ], - "time": "2026-01-13T13:06:50+00:00" + "time": "2026-03-06T14:06:22+00:00" }, { "name": "symfony/deprecation-contracts", @@ -936,16 +937,16 @@ }, { "name": "symfony/string", - "version": "v8.0.4", + "version": "v8.0.6", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "758b372d6882506821ed666032e43020c4f57194" + "reference": "6c9e1108041b5dce21a9a4984b531c4923aa9ec4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/758b372d6882506821ed666032e43020c4f57194", - "reference": "758b372d6882506821ed666032e43020c4f57194", + "url": "https://api.github.com/repos/symfony/string/zipball/6c9e1108041b5dce21a9a4984b531c4923aa9ec4", + "reference": "6c9e1108041b5dce21a9a4984b531c4923aa9ec4", "shasum": "" }, "require": { @@ -1002,7 +1003,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v8.0.4" + "source": "https://github.com/symfony/string/tree/v8.0.6" }, "funding": [ { @@ -1022,20 +1023,20 @@ "type": "tidelift" } ], - "time": "2026-01-12T12:37:40+00:00" + "time": "2026-02-09T10:14:57+00:00" }, { "name": "symfony/var-dumper", - "version": "v8.0.4", + "version": "v8.0.6", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "326e0406fc315eca57ef5740fa4a280b7a068c82" + "reference": "2e14f7e0bf5ff02c6e63bd31cb8e4855a13d6209" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/326e0406fc315eca57ef5740fa4a280b7a068c82", - "reference": "326e0406fc315eca57ef5740fa4a280b7a068c82", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/2e14f7e0bf5ff02c6e63bd31cb8e4855a13d6209", + "reference": "2e14f7e0bf5ff02c6e63bd31cb8e4855a13d6209", "shasum": "" }, "require": { @@ -1089,7 +1090,7 @@ "dump" ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v8.0.4" + "source": "https://github.com/symfony/var-dumper/tree/v8.0.6" }, "funding": [ { @@ -1109,22 +1110,74 @@ "type": "tidelift" } ], - "time": "2026-01-01T23:07:29+00:00" + "time": "2026-02-15T10:53:29+00:00" + }, + { + "name": "valb/eco", + "version": "v4.0.0", + "source": { + "type": "git", + "url": "https://github.com/valb-mig/php.eco.git", + "reference": "8df3470e00a7fd1e77e35b6424c7bc86e6dfd6e2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/valb-mig/php.eco/zipball/8df3470e00a7fd1e77e35b6424c7bc86e6dfd6e2", + "reference": "8df3470e00a7fd1e77e35b6424c7bc86e6dfd6e2", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpstan/phpstan": "^2.1", + "phpunit/phpunit": "^11.0", + "symfony/var-dumper": "^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Eco\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "valb-mig", + "email": "valb.mig@gmail.com" + } + ], + "description": "A PHP library to handle results, errors and validations.", + "keywords": [ + "error", + "functional", + "railway", + "result", + "validation" + ], + "support": { + "issues": "https://github.com/valb-mig/php.eco/issues", + "source": "https://github.com/valb-mig/php.eco/tree/v4.0.0" + }, + "time": "2026-03-18T19:19:34+00:00" } ], "packages-dev": [ { "name": "carthage-software/mago", - "version": "1.13.2", + "version": "1.15.0", "source": { "type": "git", "url": "https://github.com/carthage-software/mago.git", - "reference": "ed4c07ec6555e2ad2f9890e8c2b1dfb7325cd5a2" + "reference": "4889b696f473d6738a53e17a118a94e134654752" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/carthage-software/mago/zipball/ed4c07ec6555e2ad2f9890e8c2b1dfb7325cd5a2", - "reference": "ed4c07ec6555e2ad2f9890e8c2b1dfb7325cd5a2", + "url": "https://api.github.com/repos/carthage-software/mago/zipball/4889b696f473d6738a53e17a118a94e134654752", + "reference": "4889b696f473d6738a53e17a118a94e134654752", "shasum": "" }, "require": { @@ -1159,7 +1212,7 @@ ], "support": { "issues": "https://github.com/carthage-software/mago/issues", - "source": "https://github.com/carthage-software/mago/tree/1.13.2" + "source": "https://github.com/carthage-software/mago/tree/1.15.0" }, "funding": [ { @@ -1167,7 +1220,7 @@ "type": "github" } ], - "time": "2026-02-27T15:00:23+00:00" + "time": "2026-03-18T10:06:28+00:00" }, { "name": "myclabs/deep-copy", @@ -2205,16 +2258,16 @@ }, { "name": "sebastian/environment", - "version": "9.0.0", + "version": "9.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "bb64d08145b021b67d5f253308a498b73ab0461e" + "reference": "e26e9a944bd9d27b3a38a82fc2093d440951bfbe" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/bb64d08145b021b67d5f253308a498b73ab0461e", - "reference": "bb64d08145b021b67d5f253308a498b73ab0461e", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/e26e9a944bd9d27b3a38a82fc2093d440951bfbe", + "reference": "e26e9a944bd9d27b3a38a82fc2093d440951bfbe", "shasum": "" }, "require": { @@ -2257,7 +2310,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/environment/issues", "security": "https://github.com/sebastianbergmann/environment/security/policy", - "source": "https://github.com/sebastianbergmann/environment/tree/9.0.0" + "source": "https://github.com/sebastianbergmann/environment/tree/9.0.1" }, "funding": [ { @@ -2277,7 +2330,7 @@ "type": "tidelift" } ], - "time": "2026-02-06T04:43:29+00:00" + "time": "2026-03-15T07:13:02+00:00" }, { "name": "sebastian/exporter", diff --git a/index.php b/index.php deleted file mode 100644 index d9ae2cf..0000000 --- a/index.php +++ /dev/null @@ -1,62 +0,0 @@ - - */ - public static function create(string $name): Result - { - $errors = []; - - if (empty(trim($name))) { - $errors[] = Error::validation('name', 'O nome é obrigatório.'); - } - - if (strlen($name) < 3) { - $errors[] = Error::validation('name', 'O nome deve ter ao menos 3 caracteres.'); - } - - if (!empty($errors)) { - return Result::fail(...$errors); - } - - return Result::ok(new self($name)); - } -} - -class CreateUser -{ - public function execute(CreateUserInput $input): Result - { - $user = new User(id: 1, name: $input->name); - return Result::ok(new CreateUserOutput(id: $user->id, name: $user->name)); - } -} - -class CreateUserOutput -{ - public function __construct( - public readonly int $id, - public readonly string $name, - ) {} -} - -class User -{ - public function __construct( - public readonly int $id, - public readonly string $name, - ) {} -} diff --git a/logs/console.log b/logs/console.log new file mode 100644 index 0000000..e69de29 diff --git a/mago.toml b/mago.toml index 69cf494..4f50ad2 100644 --- a/mago.toml +++ b/mago.toml @@ -41,6 +41,7 @@ check-missing-type-hints = true register-super-globals = true excludes = ["obsidian"] ignore = [ - { code = "missing-override-attribute" , in = "tests" }, - { code = "unhandled-thrown-type", in = "tests" } + { code = "missing-override-attribute" , in = "tests" }, + { code = "unhandled-thrown-type", in = "tests" }, + { code = "possibly-undefined-array-index", in = "tests" } ] \ No newline at end of file From dbb9970b269d9eedb32e0fbfb119126fb0b43a90 Mon Sep 17 00:00:00 2001 From: valb-mig Date: Wed, 18 Mar 2026 17:30:25 -0300 Subject: [PATCH 16/18] chore: update obsidian workspace config --- .gitignore | 3 +- obsidian/.obsidian/app.json | 1 - obsidian/.obsidian/appearance.json | 1 - obsidian/.obsidian/community-plugins.json | 3 - obsidian/.obsidian/core-plugins.json | 33 - .../obsidian-excalidraw-plugin/data.json | 820 ------------------ .../obsidian-excalidraw-plugin/main.js | 10 - .../obsidian-excalidraw-plugin/manifest.json | 12 - .../obsidian-excalidraw-plugin/styles.css | 1 - obsidian/.obsidian/workspace.json | 206 ----- obsidian/Design.md | 8 - obsidian/Domain/Dice.md | 1 - obsidian/Domain/World.md | 0 13 files changed, 2 insertions(+), 1097 deletions(-) delete mode 100644 obsidian/.obsidian/app.json delete mode 100644 obsidian/.obsidian/appearance.json delete mode 100644 obsidian/.obsidian/community-plugins.json delete mode 100644 obsidian/.obsidian/core-plugins.json delete mode 100644 obsidian/.obsidian/plugins/obsidian-excalidraw-plugin/data.json delete mode 100644 obsidian/.obsidian/plugins/obsidian-excalidraw-plugin/main.js delete mode 100644 obsidian/.obsidian/plugins/obsidian-excalidraw-plugin/manifest.json delete mode 100644 obsidian/.obsidian/plugins/obsidian-excalidraw-plugin/styles.css delete mode 100644 obsidian/.obsidian/workspace.json delete mode 100644 obsidian/Design.md delete mode 100644 obsidian/Domain/Dice.md delete mode 100644 obsidian/Domain/World.md diff --git a/.gitignore b/.gitignore index b6f89b0..a2f2663 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /vendor/ coverage .phpunit.result.cache -obsidian \ No newline at end of file +obsidian +.obsidian \ No newline at end of file diff --git a/obsidian/.obsidian/app.json b/obsidian/.obsidian/app.json deleted file mode 100644 index 9e26dfe..0000000 --- a/obsidian/.obsidian/app.json +++ /dev/null @@ -1 +0,0 @@ -{} \ No newline at end of file diff --git a/obsidian/.obsidian/appearance.json b/obsidian/.obsidian/appearance.json deleted file mode 100644 index 9e26dfe..0000000 --- a/obsidian/.obsidian/appearance.json +++ /dev/null @@ -1 +0,0 @@ -{} \ No newline at end of file diff --git a/obsidian/.obsidian/community-plugins.json b/obsidian/.obsidian/community-plugins.json deleted file mode 100644 index 8d0c4e1..0000000 --- a/obsidian/.obsidian/community-plugins.json +++ /dev/null @@ -1,3 +0,0 @@ -[ - "obsidian-excalidraw-plugin" -] \ No newline at end of file diff --git a/obsidian/.obsidian/core-plugins.json b/obsidian/.obsidian/core-plugins.json deleted file mode 100644 index 639b90d..0000000 --- a/obsidian/.obsidian/core-plugins.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "file-explorer": true, - "global-search": true, - "switcher": true, - "graph": true, - "backlink": true, - "canvas": true, - "outgoing-link": true, - "tag-pane": true, - "footnotes": false, - "properties": true, - "page-preview": true, - "daily-notes": true, - "templates": true, - "note-composer": true, - "command-palette": true, - "slash-command": false, - "editor-status": true, - "bookmarks": true, - "markdown-importer": false, - "zk-prefixer": false, - "random-note": false, - "outline": true, - "word-count": true, - "slides": false, - "audio-recorder": false, - "workspaces": false, - "file-recovery": true, - "publish": false, - "sync": true, - "bases": true, - "webviewer": false -} \ No newline at end of file diff --git a/obsidian/.obsidian/plugins/obsidian-excalidraw-plugin/data.json b/obsidian/.obsidian/plugins/obsidian-excalidraw-plugin/data.json deleted file mode 100644 index 7887b4d..0000000 --- a/obsidian/.obsidian/plugins/obsidian-excalidraw-plugin/data.json +++ /dev/null @@ -1,820 +0,0 @@ -{ - "copyLinkToElemenetAnchorTo100": false, - "copyFrameLinkByName": false, - "disableDoubleClickTextEditing": false, - "folder": "Excalidraw", - "cropFolder": "", - "annotateFolder": "", - "embedUseExcalidrawFolder": false, - "templateFilePath": "Excalidraw/Template.excalidraw", - "scriptFolderPath": "Excalidraw/Scripts", - "fontAssetsPath": "Excalidraw/CJK Fonts", - "loadChineseFonts": false, - "loadJapaneseFonts": false, - "loadKoreanFonts": false, - "compress": true, - "decompressForMDView": false, - "onceOffCompressFlagReset": true, - "onceOffGPTVersionReset": true, - "autosave": true, - "autosaveIntervalDesktop": 60000, - "autosaveIntervalMobile": 30000, - "drawingFilenamePrefix": "Drawing ", - "drawingEmbedPrefixWithFilename": true, - "drawingFilnameEmbedPostfix": " ", - "drawingFilenameDateTime": "YYYY-MM-DD HH.mm.ss", - "useExcalidrawExtension": true, - "cropSuffix": "", - "cropPrefix": "cropped_", - "annotateSuffix": "", - "annotatePrefix": "annotated_", - "annotatePreserveSize": false, - "previewImageType": "SVGIMG", - "renderingConcurrency": 3, - "allowImageCache": true, - "allowImageCacheInScene": true, - "displayExportedImageIfAvailable": false, - "previewMatchObsidianTheme": false, - "width": "400", - "height": "", - "overrideObsidianFontSize": false, - "dynamicStyling": "colorful", - "isLeftHanded": false, - "desktopUIMode": "tray", - "tabletUIMode": "compact", - "phoneUIMode": "mobile", - "iframeMatchExcalidrawTheme": true, - "matchTheme": false, - "matchThemeAlways": false, - "matchThemeTrigger": false, - "defaultMode": "normal", - "defaultPenMode": "never", - "penModeDoubleTapEraser": true, - "penModeSingleFingerPanning": true, - "penModeCrosshairVisible": true, - "panWithRightMouseButton": false, - "renderImageInMarkdownReadingMode": false, - "renderImageInHoverPreviewForMDNotes": false, - "renderImageInMarkdownToPDF": false, - "allowPinchZoom": false, - "allowWheelZoom": false, - "zoomToFitOnOpen": true, - "zoomToFitOnResize": false, - "zoomToFitMaxLevel": 2, - "zoomStep": 0.05, - "zoomMin": 0.1, - "zoomMax": 30, - "linkPrefix": "📍", - "urlPrefix": "🌐", - "parseTODO": false, - "todo": "☐", - "done": "🗹", - "hoverPreviewWithoutCTRL": false, - "linkOpacity": 1, - "openInAdjacentPane": true, - "showSecondOrderLinks": true, - "focusOnFileTab": true, - "openInMainWorkspace": true, - "showLinkBrackets": false, - "syncElementLinkWithText": false, - "allowCtrlClick": true, - "forceWrap": false, - "pageTransclusionCharLimit": 200, - "wordWrappingDefault": 0, - "removeTransclusionQuoteSigns": true, - "iframelyAllowed": true, - "pngExportScale": 1, - "exportWithTheme": true, - "exportWithBackground": true, - "exportPaddingSVG": 10, - "exportEmbedScene": false, - "keepInSync": false, - "autoexportSVG": false, - "autoexportPNG": false, - "autoExportLightAndDark": false, - "autoexportExcalidraw": false, - "embedType": "excalidraw", - "embedMarkdownCommentLinks": true, - "embedWikiLink": true, - "syncExcalidraw": false, - "experimentalFileType": false, - "experimentalFileTag": "✏️", - "experimentalLivePreview": true, - "fadeOutExcalidrawMarkup": false, - "loadPropertySuggestions": false, - "experimentalEnableFourthFont": false, - "experimantalFourthFont": "Virgil", - "addDummyTextElement": false, - "zoteroCompatibility": false, - "fieldSuggester": true, - "compatibilityMode": false, - "drawingOpenCount": 0, - "library": "deprecated", - "library2": { - "type": "excalidrawlib", - "version": 2, - "source": "https://github.com/zsviczian/obsidian-excalidraw-plugin/releases/tag/2.20.5", - "libraryItems": [] - }, - "imageElementNotice": true, - "mdSVGwidth": 500, - "mdSVGmaxHeight": 800, - "mdFont": "Virgil", - "mdFontColor": "Black", - "mdBorderColor": "Black", - "mdCSS": "", - "scriptEngineSettings": {}, - "previousRelease": "2.20.5", - "showReleaseNotes": true, - "compareManifestToPluginVersion": true, - "showNewVersionNotification": true, - "latexBoilerplate": "\\color{blue}", - "latexPreambleLocation": "preamble.sty", - "taskboneEnabled": false, - "taskboneAPIkey": "", - "pinnedScripts": [], - "sidepanelTabs": [], - "customPens": [ - { - "type": "default", - "freedrawOnly": false, - "strokeColor": "#000000", - "backgroundColor": "transparent", - "fillStyle": "hachure", - "strokeWidth": 0, - "roughness": 0, - "penOptions": { - "highlighter": false, - "constantPressure": false, - "hasOutline": false, - "outlineWidth": 1, - "options": { - "thinning": 0.6, - "smoothing": 0.5, - "streamline": 0.5, - "easing": "easeOutSine", - "start": { - "cap": true, - "taper": 0, - "easing": "linear" - }, - "end": { - "cap": true, - "taper": 0, - "easing": "linear" - } - } - } - }, - { - "type": "highlighter", - "freedrawOnly": true, - "strokeColor": "#FFC47C", - "backgroundColor": "#FFC47C", - "fillStyle": "solid", - "strokeWidth": 2, - "roughness": null, - "penOptions": { - "highlighter": true, - "constantPressure": true, - "hasOutline": true, - "outlineWidth": 4, - "options": { - "thinning": 1, - "smoothing": 0.5, - "streamline": 0.5, - "easing": "linear", - "start": { - "taper": 0, - "cap": true, - "easing": "linear" - }, - "end": { - "taper": 0, - "cap": true, - "easing": "linear" - } - } - } - }, - { - "type": "finetip", - "freedrawOnly": false, - "strokeColor": "#3E6F8D", - "backgroundColor": "transparent", - "fillStyle": "hachure", - "strokeWidth": 0.5, - "roughness": 0, - "penOptions": { - "highlighter": false, - "hasOutline": false, - "outlineWidth": 1, - "constantPressure": true, - "options": { - "smoothing": 0.4, - "thinning": -0.5, - "streamline": 0.4, - "easing": "linear", - "start": { - "taper": 5, - "cap": false, - "easing": "linear" - }, - "end": { - "taper": 5, - "cap": false, - "easing": "linear" - } - } - } - }, - { - "type": "fountain", - "freedrawOnly": false, - "strokeColor": "#000000", - "backgroundColor": "transparent", - "fillStyle": "hachure", - "strokeWidth": 2, - "roughness": 0, - "penOptions": { - "highlighter": false, - "constantPressure": false, - "hasOutline": false, - "outlineWidth": 1, - "options": { - "smoothing": 0.2, - "thinning": 0.6, - "streamline": 0.2, - "easing": "easeInOutSine", - "start": { - "taper": 150, - "cap": true, - "easing": "linear" - }, - "end": { - "taper": 1, - "cap": true, - "easing": "linear" - } - } - } - }, - { - "type": "marker", - "freedrawOnly": true, - "strokeColor": "#B83E3E", - "backgroundColor": "#FF7C7C", - "fillStyle": "dashed", - "strokeWidth": 2, - "roughness": 3, - "penOptions": { - "highlighter": false, - "constantPressure": true, - "hasOutline": true, - "outlineWidth": 4, - "options": { - "thinning": 1, - "smoothing": 0.5, - "streamline": 0.5, - "easing": "linear", - "start": { - "taper": 0, - "cap": true, - "easing": "linear" - }, - "end": { - "taper": 0, - "cap": true, - "easing": "linear" - } - } - } - }, - { - "type": "thick-thin", - "freedrawOnly": true, - "strokeColor": "#CECDCC", - "backgroundColor": "transparent", - "fillStyle": "hachure", - "strokeWidth": 0, - "roughness": null, - "penOptions": { - "highlighter": true, - "constantPressure": true, - "hasOutline": false, - "outlineWidth": 1, - "options": { - "thinning": 1, - "smoothing": 0.5, - "streamline": 0.5, - "easing": "linear", - "start": { - "taper": 0, - "cap": true, - "easing": "linear" - }, - "end": { - "cap": true, - "taper": true, - "easing": "linear" - } - } - } - }, - { - "type": "thin-thick-thin", - "freedrawOnly": true, - "strokeColor": "#CECDCC", - "backgroundColor": "transparent", - "fillStyle": "hachure", - "strokeWidth": 0, - "roughness": null, - "penOptions": { - "highlighter": true, - "constantPressure": true, - "hasOutline": false, - "outlineWidth": 1, - "options": { - "thinning": 1, - "smoothing": 0.5, - "streamline": 0.5, - "easing": "linear", - "start": { - "cap": true, - "taper": true, - "easing": "linear" - }, - "end": { - "cap": true, - "taper": true, - "easing": "linear" - } - } - } - }, - { - "type": "default", - "freedrawOnly": false, - "strokeColor": "#000000", - "backgroundColor": "transparent", - "fillStyle": "hachure", - "strokeWidth": 0, - "roughness": 0, - "penOptions": { - "highlighter": false, - "constantPressure": false, - "hasOutline": false, - "outlineWidth": 1, - "options": { - "thinning": 0.6, - "smoothing": 0.5, - "streamline": 0.5, - "easing": "easeOutSine", - "start": { - "cap": true, - "taper": 0, - "easing": "linear" - }, - "end": { - "cap": true, - "taper": 0, - "easing": "linear" - } - } - } - }, - { - "type": "default", - "freedrawOnly": false, - "strokeColor": "#000000", - "backgroundColor": "transparent", - "fillStyle": "hachure", - "strokeWidth": 0, - "roughness": 0, - "penOptions": { - "highlighter": false, - "constantPressure": false, - "hasOutline": false, - "outlineWidth": 1, - "options": { - "thinning": 0.6, - "smoothing": 0.5, - "streamline": 0.5, - "easing": "easeOutSine", - "start": { - "cap": true, - "taper": 0, - "easing": "linear" - }, - "end": { - "cap": true, - "taper": 0, - "easing": "linear" - } - } - } - }, - { - "type": "default", - "freedrawOnly": false, - "strokeColor": "#000000", - "backgroundColor": "transparent", - "fillStyle": "hachure", - "strokeWidth": 0, - "roughness": 0, - "penOptions": { - "highlighter": false, - "constantPressure": false, - "hasOutline": false, - "outlineWidth": 1, - "options": { - "thinning": 0.6, - "smoothing": 0.5, - "streamline": 0.5, - "easing": "easeOutSine", - "start": { - "cap": true, - "taper": 0, - "easing": "linear" - }, - "end": { - "cap": true, - "taper": 0, - "easing": "linear" - } - } - } - } - ], - "numberOfCustomPens": 0, - "pdfScale": 4, - "pdfBorderBox": true, - "pdfFrame": false, - "pdfGapSize": 20, - "pdfGroupPages": false, - "pdfLockAfterImport": true, - "pdfNumColumns": 1, - "pdfNumRows": 1, - "pdfDirection": "right", - "pdfImportScale": 0.3, - "gridSettings": { - "DYNAMIC_COLOR": true, - "COLOR": "#000000", - "OPACITY": 50, - "GRID_DIRECTION": { - "horizontal": true, - "vertical": true - } - }, - "laserSettings": { - "DECAY_LENGTH": 50, - "DECAY_TIME": 1000, - "COLOR": "#ff0000" - }, - "embeddableMarkdownDefaults": { - "useObsidianDefaults": false, - "backgroundMatchCanvas": false, - "backgroundMatchElement": true, - "backgroundColor": "#fff", - "backgroundOpacity": 60, - "borderMatchElement": true, - "borderColor": "#fff", - "borderOpacity": 0, - "filenameVisible": false - }, - "markdownNodeOneClickEditing": false, - "canvasImmersiveEmbed": true, - "startupScriptPath": "", - "aiEnabled": true, - "openAIAPIToken": "", - "openAIDefaultTextModel": "gpt-5-mini", - "openAIDefaultTextModelMaxTokens": 4096, - "openAIDefaultVisionModel": "gpt-5-mini", - "openAIDefaultImageGenerationModel": "gpt-image-1", - "openAIURL": "https://api.openai.com/v1/chat/completions", - "openAIImageGenerationURL": "https://api.openai.com/v1/images/generations", - "openAIImageEditsURL": "https://api.openai.com/v1/images/edits", - "openAIImageVariationURL": "https://api.openai.com/v1/images/variations", - "modifierKeyConfig": { - "Mac": { - "LocalFileDragAction": { - "defaultAction": "image-import", - "rules": [ - { - "shift": false, - "ctrl_cmd": false, - "alt_opt": false, - "meta_ctrl": false, - "result": "image-import" - }, - { - "shift": true, - "ctrl_cmd": false, - "alt_opt": true, - "meta_ctrl": false, - "result": "link" - }, - { - "shift": true, - "ctrl_cmd": false, - "alt_opt": false, - "meta_ctrl": false, - "result": "image-url" - }, - { - "shift": false, - "ctrl_cmd": false, - "alt_opt": true, - "meta_ctrl": false, - "result": "embeddable" - } - ] - }, - "WebBrowserDragAction": { - "defaultAction": "image-url", - "rules": [ - { - "shift": false, - "ctrl_cmd": false, - "alt_opt": false, - "meta_ctrl": false, - "result": "image-url" - }, - { - "shift": true, - "ctrl_cmd": false, - "alt_opt": true, - "meta_ctrl": false, - "result": "link" - }, - { - "shift": false, - "ctrl_cmd": false, - "alt_opt": true, - "meta_ctrl": false, - "result": "embeddable" - }, - { - "shift": true, - "ctrl_cmd": false, - "alt_opt": false, - "meta_ctrl": false, - "result": "image-import" - } - ] - }, - "InternalDragAction": { - "defaultAction": "link", - "rules": [ - { - "shift": false, - "ctrl_cmd": false, - "alt_opt": false, - "meta_ctrl": false, - "result": "link" - }, - { - "shift": false, - "ctrl_cmd": false, - "alt_opt": false, - "meta_ctrl": true, - "result": "embeddable" - }, - { - "shift": true, - "ctrl_cmd": false, - "alt_opt": false, - "meta_ctrl": false, - "result": "image" - }, - { - "shift": true, - "ctrl_cmd": false, - "alt_opt": false, - "meta_ctrl": true, - "result": "image-fullsize" - } - ] - }, - "LinkClickAction": { - "defaultAction": "new-tab", - "rules": [ - { - "shift": false, - "ctrl_cmd": false, - "alt_opt": false, - "meta_ctrl": false, - "result": "active-pane" - }, - { - "shift": false, - "ctrl_cmd": true, - "alt_opt": false, - "meta_ctrl": false, - "result": "new-tab" - }, - { - "shift": false, - "ctrl_cmd": true, - "alt_opt": true, - "meta_ctrl": false, - "result": "new-pane" - }, - { - "shift": true, - "ctrl_cmd": true, - "alt_opt": true, - "meta_ctrl": false, - "result": "popout-window" - }, - { - "shift": false, - "ctrl_cmd": true, - "alt_opt": false, - "meta_ctrl": true, - "result": "md-properties" - } - ] - } - }, - "Win": { - "LocalFileDragAction": { - "defaultAction": "image-import", - "rules": [ - { - "shift": false, - "ctrl_cmd": false, - "alt_opt": false, - "meta_ctrl": false, - "result": "image-import" - }, - { - "shift": false, - "ctrl_cmd": true, - "alt_opt": false, - "meta_ctrl": false, - "result": "link" - }, - { - "shift": true, - "ctrl_cmd": false, - "alt_opt": false, - "meta_ctrl": false, - "result": "image-url" - }, - { - "shift": true, - "ctrl_cmd": true, - "alt_opt": false, - "meta_ctrl": false, - "result": "embeddable" - } - ] - }, - "WebBrowserDragAction": { - "defaultAction": "image-url", - "rules": [ - { - "shift": false, - "ctrl_cmd": false, - "alt_opt": false, - "meta_ctrl": false, - "result": "image-url" - }, - { - "shift": false, - "ctrl_cmd": true, - "alt_opt": false, - "meta_ctrl": false, - "result": "link" - }, - { - "shift": true, - "ctrl_cmd": true, - "alt_opt": false, - "meta_ctrl": false, - "result": "embeddable" - }, - { - "shift": true, - "ctrl_cmd": false, - "alt_opt": false, - "meta_ctrl": false, - "result": "image-import" - } - ] - }, - "InternalDragAction": { - "defaultAction": "link", - "rules": [ - { - "shift": false, - "ctrl_cmd": false, - "alt_opt": false, - "meta_ctrl": false, - "result": "link" - }, - { - "shift": true, - "ctrl_cmd": true, - "alt_opt": false, - "meta_ctrl": false, - "result": "embeddable" - }, - { - "shift": true, - "ctrl_cmd": false, - "alt_opt": false, - "meta_ctrl": false, - "result": "image" - }, - { - "shift": false, - "ctrl_cmd": true, - "alt_opt": true, - "meta_ctrl": false, - "result": "image-fullsize" - } - ] - }, - "LinkClickAction": { - "defaultAction": "new-tab", - "rules": [ - { - "shift": false, - "ctrl_cmd": false, - "alt_opt": false, - "meta_ctrl": false, - "result": "active-pane" - }, - { - "shift": false, - "ctrl_cmd": true, - "alt_opt": false, - "meta_ctrl": false, - "result": "new-tab" - }, - { - "shift": false, - "ctrl_cmd": true, - "alt_opt": true, - "meta_ctrl": false, - "result": "new-pane" - }, - { - "shift": true, - "ctrl_cmd": true, - "alt_opt": true, - "meta_ctrl": false, - "result": "popout-window" - }, - { - "shift": false, - "ctrl_cmd": true, - "alt_opt": false, - "meta_ctrl": true, - "result": "md-properties" - } - ] - } - } - }, - "slidingPanesSupport": false, - "areaZoomLimit": 1, - "longPressDesktop": 500, - "longPressMobile": 500, - "doubleClickLinkOpenViewMode": true, - "isDebugMode": false, - "rank": "Bronze", - "modifierKeyOverrides": [ - { - "modifiers": [ - "Mod" - ], - "key": "Enter" - }, - { - "modifiers": [ - "Mod" - ], - "key": "k" - }, - { - "modifiers": [ - "Mod" - ], - "key": "G" - } - ], - "showSplashscreen": true, - "pdfSettings": { - "pageSize": "A4", - "pageOrientation": "portrait", - "fitToPage": 1, - "paperColor": "white", - "customPaperColor": "#ffffff", - "alignment": "center", - "margin": "normal" - }, - "disableContextMenu": false -} \ No newline at end of file diff --git a/obsidian/.obsidian/plugins/obsidian-excalidraw-plugin/main.js b/obsidian/.obsidian/plugins/obsidian-excalidraw-plugin/main.js deleted file mode 100644 index f6cc606..0000000 --- a/obsidian/.obsidian/plugins/obsidian-excalidraw-plugin/main.js +++ /dev/null @@ -1,10 +0,0 @@ -"use strict";var obsidian_module=require("obsidian"),view=require("@codemirror/view"),commands=require("@codemirror/commands"),lr=require("@lezer/lr"),language=require("@codemirror/language"),state=require("@codemirror/state");;const INITIAL_TIMESTAMP=Date.now();var LZString=function(){var r=String.fromCharCode,o="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",n="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+-$",e={};function t(r,o){if(!e[r]){e[r]={};for(var n=0;n>>8,n[2*e+1]=s%256}return n},decompressFromUint8Array:function(o){if(null==o)return i.decompress(o);for(var n=new Array(o.length/2),e=0,t=n.length;e>=1}else{for(t=1,e=0;e>=1}0==--l&&(l=Math.pow(2,h),h++),delete u[c]}else for(t=s[c],e=0;e>=1;0==--l&&(l=Math.pow(2,h),h++),s[p]=f++,c=String(a)}if(""!==c){if(Object.prototype.hasOwnProperty.call(u,c)){if(c.charCodeAt(0)<256){for(e=0;e>=1}else{for(t=1,e=0;e>=1}0==--l&&(l=Math.pow(2,h),h++),delete u[c]}else for(t=s[c],e=0;e>=1;0==--l&&(l=Math.pow(2,h),h++)}for(t=2,e=0;e>=1;for(;;){if(m<<=1,v==o-1){d.push(n(m));break}v++}return d.join("")},decompress:function(r){return null==r?"":""==r?null:i._decompress(r.length,32768,function(o){return r.charCodeAt(o)})},_decompress:function(o,n,e){var t,i,s,u,a,p,c,l=[],f=4,h=4,d=3,m="",v=[],g={val:e(0),position:n,index:1};for(t=0;t<3;t+=1)l[t]=t;for(s=0,a=Math.pow(2,2),p=1;p!=a;)u=g.val&g.position,g.position>>=1,0==g.position&&(g.position=n,g.val=e(g.index++)),s|=(u>0?1:0)*p,p<<=1;switch(s){case 0:for(s=0,a=Math.pow(2,8),p=1;p!=a;)u=g.val&g.position,g.position>>=1,0==g.position&&(g.position=n,g.val=e(g.index++)),s|=(u>0?1:0)*p,p<<=1;c=r(s);break;case 1:for(s=0,a=Math.pow(2,16),p=1;p!=a;)u=g.val&g.position,g.position>>=1,0==g.position&&(g.position=n,g.val=e(g.index++)),s|=(u>0?1:0)*p,p<<=1;c=r(s);break;case 2:return""}for(l[3]=c,i=c,v.push(c);;){if(g.index>o)return"";for(s=0,a=Math.pow(2,d),p=1;p!=a;)u=g.val&g.position,g.position>>=1,0==g.position&&(g.position=n,g.val=e(g.index++)),s|=(u>0?1:0)*p,p<<=1;switch(c=s){case 0:for(s=0,a=Math.pow(2,8),p=1;p!=a;)u=g.val&g.position,g.position>>=1,0==g.position&&(g.position=n,g.val=e(g.index++)),s|=(u>0?1:0)*p,p<<=1;l[h++]=r(s),c=h-1,f--;break;case 1:for(s=0,a=Math.pow(2,16),p=1;p!=a;)u=g.val&g.position,g.position>>=1,0==g.position&&(g.position=n,g.val=e(g.index++)),s|=(u>0?1:0)*p,p<<=1;l[h++]=r(s),c=h-1,f--;break;case 2:return v.join("")}if(0==f&&(f=Math.pow(2,d),d++),l[c])m=l[c];else{if(c!==h)return null;m=i+i.charAt(0)}v.push(m),l[h++]=i+m.charAt(0),i=m,0==--f&&(f=Math.pow(2,d),d++)}}};return i}();"function"==typeof define&&define.amd?define(function(){return LZString}):"undefined"!=typeof module&&null!=module?module.exports=LZString:"undefined"!=typeof angular&&null!=angular&&angular.module("LZString",[]).factory("LZString",function(){return LZString}); -let REACT_PACKAGES = `!function(){var e,t;e=this,t=function(e){function M(e){return null!==e&&"object"==typeof e&&"function"==typeof(e=te&&e[te]||e["@@iterator"])?e:null}function t(e,t,n){this.props=e,this.context=t,this.refs=oe,this.updater=n||ne}function n(){}function r(e,t,n){this.props=e,this.context=t,this.refs=oe,this.updater=n||ne}function o(e,t,n){var r,o={},u=null,a=null;if(null!=t)for(r in void 0!==t.ref&&(a=t.ref),void 0!==t.key&&(u=""+t.key),t)ae.call(t,r)&&!ie.hasOwnProperty(r)&&(o[r]=t[r]);var i=arguments.length-2;if(1===i)o.children=n;else if(1>>1,o=e[r];if(!(0>>1;rt)||e&&!q());){var r,o=R.callback;"function"==typeof o?(R.callback=null,P=R.priorityLevel,r=o(R.expirationTime<=t),t=v(),"function"==typeof r?R.callback=r:R===i(C)&&l(C),y(t)):l(C),R=i(C)}var u,a=null!==R||(null!==(u=i(E))&&_(d,u.startTime-t),!1);return a}finally{R=null,P=n,$=!1}}function q(){return!(v()-de")?l.replace("",n.displayName):l}while(1<=u&&0<=i);break}}}finally{xo=!1,Error.prepareStackTrace=t}return(n=n?n.displayName||n.name:"")?Q(n):""}function $(e){switch(e.tag){case 5:return Q(e.type);case 16:return Q("Lazy");case 13:return Q("Suspense");case 19:return Q("SuspenseList");case 0:case 2:case 15:return e=j(e.type,!1);case 11:return e=j(e.type.render,!1);case 1:return e=j(e.type,!0);default:return""}}function q(e){if(null!=e){if("function"==typeof e)return e.displayName||e.name||null;if("string"==typeof e)return e;switch(e){case co:return"Fragment";case so:return"Portal";case po:return"Profiler";case fo:return"StrictMode";case yo:return"Suspense";case vo:return"SuspenseList"}if("object"==typeof e)switch(e.$$typeof){case ho:return(e.displayName||"Context")+".Consumer";case mo:return(e._context.displayName||"Context")+".Provider";case go:var n=e.render;return e=(e=e.displayName)?e:""!==(e=n.displayName||n.name||"")?"ForwardRef("+e+")":"ForwardRef";case bo:return null!==(n=e.displayName||null)?n:q(e.type)||"Memo";case ko:n=e._payload,e=e._init;try{return q(e(n))}catch(e){}}}return null}function K(e){var n=e.type;switch(e.tag){case 24:return"Cache";case 9:return(n.displayName||"Context")+".Consumer";case 10:return(n._context.displayName||"Context")+".Provider";case 18:return"DehydratedFragment";case 11:return e=(e=n.render).displayName||e.name||"",n.displayName||(""!==e?"ForwardRef("+e+")":"ForwardRef");case 7:return"Fragment";case 5:return n;case 4:return"Portal";case 3:return"Root";case 6:return"Text";case 16:return q(n);case 8:return n===fo?"StrictMode":"Mode";case 22:return"Offscreen";case 12:return"Profiler";case 21:return"Scope";case 13:return"Suspense";case 19:return"SuspenseList";case 25:return"TracingMarker";case 1:case 0:case 17:case 2:case 14:case 15:if("function"==typeof n)return n.displayName||n.name||null;if("string"==typeof n)return n}return null}function Y(e){switch(typeof e){case"boolean":case"number":case"string":case"undefined":case"object":return e;default:return""}}function X(e){var n=e.type;return(e=e.nodeName)&&"input"===e.toLowerCase()&&("checkbox"===n||"radio"===n)}function G(e){var n,t,r=X(e)?"checked":"value",l=Object.getOwnPropertyDescriptor(e.constructor.prototype,r),a=""+e[r];if(!e.hasOwnProperty(r)&&void 0!==l&&"function"==typeof l.get&&"function"==typeof l.set)return n=l.get,t=l.set,Object.defineProperty(e,r,{configurable:!0,get:function(){return n.call(this)},set:function(e){a=""+e,t.call(this,e)}}),Object.defineProperty(e,r,{enumerable:l.enumerable}),{getValue:function(){return a},setValue:function(e){a=""+e},stopTracking:function(){e._valueTracker=null,delete e[r]}}}function Z(e){e._valueTracker||(e._valueTracker=G(e))}function J(e){var n,t,r;return!(!e||(n=e._valueTracker)&&(t=n.getValue(),r="",(e=r=e?X(e)?e.checked?"true":"false":e.value:r)===t||(n.setValue(e),0)))}function ee(n){if(void 0===(n=n||("undefined"!=typeof document?document:void 0)))return null;try{return n.activeElement||n.body}catch(e){return n.body}}function ne(e,n){var t=n.checked;return g({},n,{defaultChecked:void 0,defaultValue:void 0,value:void 0,checked:null!=t?t:e._wrapperState.initialChecked})}function te(e,n){var t=null==n.defaultValue?"":n.defaultValue,r=null!=n.checked?n.checked:n.defaultChecked,t=Y(null!=n.value?n.value:t);e._wrapperState={initialChecked:r,initialValue:t,controlled:"checkbox"===n.type||"radio"===n.type?null!=n.checked:null!=n.value}}function re(e,n){null!=(n=n.checked)&&B(e,"checked",n,!1)}function le(e,n){re(e,n);var t=Y(n.value),r=n.type;if(null!=t)"number"===r?(0===t&&""===e.value||e.value!=t)&&(e.value=""+t):e.value!==""+t&&(e.value=""+t);else if("submit"===r||"reset"===r)return void e.removeAttribute("value");n.hasOwnProperty("value")?oe(e,n.type,t):n.hasOwnProperty("defaultValue")&&oe(e,n.type,Y(n.defaultValue)),null==n.checked&&null!=n.defaultChecked&&(e.defaultChecked=!!n.defaultChecked)}function ae(e,n,t){if(n.hasOwnProperty("value")||n.hasOwnProperty("defaultValue")){var r=n.type;if(("submit"===r||"reset"===r)&&null==n.value)return;n=""+e._wrapperState.initialValue,t||n===e.value||(e.value=n),e.defaultValue=n}""!==(t=e.name)&&(e.name=""),e.defaultChecked=!!e._wrapperState.initialChecked,""!==t&&(e.name=t)}function oe(e,n,t){"number"===n&&ee(e.ownerDocument)===e||(null==t?e.defaultValue=""+e._wrapperState.initialValue:e.defaultValue!==""+t&&(e.defaultValue=""+t))}function ue(e,n,t,r){if(e=e.options,n){n={};for(var l=0;l>>=0)?32:31-(iu(e)/su|0)|0}function Re(e){switch(e&-e){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return 4194240&e;case 4194304:case 8388608:case 16777216:case 33554432:case 67108864:return 130023424&e;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 1073741824;default:return e}}function De(e,n){var t=e.pendingLanes;if(0===t)return 0;var r,l=0,a=e.suspendedLanes,o=e.pingedLanes,u=268435455&t;if(0!==u?0!==(r=u&~a)?l=Re(r):0!==(o&=u)&&(l=Re(o)):0!==(u=t&~a)?l=Re(u):0!==o&&(l=Re(o)),0===l)return 0;if(0!==n&&n!==l&&0==(n&a)&&((o=n&-n)<=(a=l&-l)||16===a&&0!=(4194240&o)))return n;if(0!=(4&l)&&(l|=16&t),0!==(n=e.entangledLanes))for(e=e.entanglements,n&=l;0>=r,a-=r,Xi=1<<32-uu(n)+a|t<u?(i=o,o=null):i=o.sibling;var s=y(n,o,t[u],r);if(null===s){null===o&&(o=i);break}f&&o&&null===s.alternate&&d(n,o),e=h(s,e,u),null===a?l=s:a.sibling=s,a=s,o=i}if(u===t.length)p(n,o);else if(null===o)for(;uu?(i=o,o=null):i=o.sibling;var c=y(n,o,s.value,r);if(null===c){null===o&&(o=i);break}f&&o&&null===c.alternate&&d(n,o),e=h(c,e,u),null===a?l=c:a.sibling=c,a=c,o=i}if(s.done)p(n,o);else if(null===o)for(;!s.done;u++,s=t.next())null!==(s=g(n,s.value,r))&&(e=h(s,e,u),null===a?l=s:a.sibling=s,a=s);else{for(o=m(n,o);!s.done;u++,s=t.next())null!==(s=v(o,n,u,s.value,r))&&(f&&null!==s.alternate&&o.delete(null===s.key?u:s.key),e=h(s,e,u),null===a?l=s:a.sibling=s,a=s);f&&o.forEach(function(e){return d(n,e)})}return E&&vt(n,u),l}function w(e,n,t,r){if("object"==typeof(t="object"==typeof t&&null!==t&&t.type===co&&null===t.key?t.props.children:t)&&null!==t){switch(t.$$typeof){case io:e:{for(var l=t.key,a=n;null!==a;){if(a.key===l){if((l=t.type)===co){if(7===a.tag){p(e,a.sibling),(n=o(a,t.props.children)).return=e,e=n;break e}}else if(a.elementType===l||"object"==typeof l&&null!==l&&l.$$typeof===ko&&Mt(l)===a.type){p(e,a.sibling),(n=o(a,t.props)).ref=Lt(e,a,t),n.return=e,e=n;break e}p(e,a);break}d(e,a),a=a.sibling}e=t.type===co?((n=za(t.props.children,e.mode,r,t.key)).return=e,n):((r=Na(t.type,t.key,t.props,null,e.mode,r)).ref=Lt(e,n,t),r.return=e,r)}return u(e);case so:e:{for(a=t.key;null!==n;){if(n.key===a){if(4===n.tag&&n.stateNode.containerInfo===t.containerInfo&&n.stateNode.implementation===t.implementation){p(e,n.sibling),(n=o(n,t.children||[])).return=e,e=n;break e}p(e,n);break}d(e,n),n=n.sibling}(n=La(t,e.mode,r)).return=e,e=n}return u(e);case ko:return w(e,n,(a=t._init)(t._payload),r)}if(Eo(t))return b(e,n,t,r);if(H(t))return k(e,n,t,r);Tt(e,t)}return"string"==typeof t&&""!==t||"number"==typeof t?(t=""+t,(n=null!==n&&6===n.tag?(p(e,n.sibling),o(n,t)):(p(e,n),_a(t,e.mode,r))).return=e,u(e=n)):p(e,n)}return w}function Rt(){os=as=ls=null}function Dt(e,n){n=rs.current,s(rs),e._currentValue=n}function Ot(e,n,t){for(;null!==e;){var r=e.alternate;if((e.childLanes&n)!==n?(e.childLanes|=n,null!==r&&(r.childLanes|=n)):null!==r&&(r.childLanes&n)!==n&&(r.childLanes|=n),e===t)break;e=e.return}}function It(e,n){(os=as=null)!==(e=(ls=e).dependencies)&&null!==e.firstContext&&(0!=(e.lanes&n)&&(_=!0),e.firstContext=null)}function Ut(e){var n=e._currentValue;if(os!==e)if(e={context:e,memoizedValue:n,next:null},null===as){if(null===ls)throw Error(S(308));as=e,ls.dependencies={lanes:0,firstContext:e}}else as=as.next=e;return n}function Vt(e){null===us?us=[e]:us.push(e)}function At(e,n,t,r){var l=n.interleaved;return null===l?(t.next=t,Vt(n)):(t.next=l.next,l.next=t),n.interleaved=t,Wt(e,r)}function Wt(e,n){e.lanes|=n;var t=e.alternate;for(null!==t&&(t.lanes|=n),e=(t=e).return;null!==e;)e.childLanes|=n,null!==(t=e.alternate)&&(t.childLanes|=n),e=(t=e).return;return 3===t.tag?t.stateNode:null}function Bt(e){e.updateQueue={baseState:e.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,interleaved:null,lanes:0},effects:null}}function Ht(e,n){e=e.updateQueue,n.updateQueue===e&&(n.updateQueue={baseState:e.baseState,firstBaseUpdate:e.firstBaseUpdate,lastBaseUpdate:e.lastBaseUpdate,shared:e.shared,effects:e.effects})}function Qt(e,n){return{eventTime:e,lane:n,tag:0,payload:null,callback:null,next:null}}function jt(e,n,t){var r,l=e.updateQueue;return null===l?null:(l=l.shared,(0!=(2&F)?(null===(r=l.pending)?n.next=n:(n.next=r.next,r.next=n),l.pending=n,is):(null===(r=l.interleaved)?(n.next=n,Vt(l)):(n.next=r.next,r.next=n),l.interleaved=n,Wt))(e,t))}function $t(e,n,t){var r;null!==(n=n.updateQueue)&&(n=n.shared,0!=(4194240&t))&&(r=n.lanes,r&=e.pendingLanes,He(e,n.lanes=t|=r))}function qt(e,n){var t=e.updateQueue,r=e.alternate;if(null!==r&&t===(r=r.updateQueue)){var l=null,a=null;if(null!==(t=t.firstBaseUpdate)){do{var o={eventTime:t.eventTime,lane:t.lane,tag:t.tag,payload:t.payload,callback:t.callback,next:null}}while(null===a?l=a=o:a=a.next=o,null!==(t=t.next));null===a?l=a=n:a=a.next=n}else l=a=n;t={baseState:r.baseState,firstBaseUpdate:l,lastBaseUpdate:a,shared:r.shared,effects:r.effects},e.updateQueue=t}else null===(e=t.lastBaseUpdate)?t.firstBaseUpdate=n:e.next=n,t.lastBaseUpdate=n}function Kt(e,n,t,r){var l,a,o=e.updateQueue,u=(ss=!1,o.firstBaseUpdate),i=o.lastBaseUpdate;if(null!==(f=o.shared.pending)&&(o.shared.pending=null,a=(l=f).next,(l.next=null)===i?u=a:i.next=a,i=l,null!==(c=e.alternate))&&(f=(c=c.updateQueue).lastBaseUpdate)!==i&&(null===f?c.firstBaseUpdate=a:f.next=a,c.lastBaseUpdate=l),null!==u){for(var s=o.baseState,i=0,c=a=l=null,f=u;;){var d=f.lane,p=f.eventTime;if((r&d)===d){null!==c&&(c=c.next={eventTime:p,lane:0,tag:f.tag,payload:f.payload,callback:f.callback,next:null});e:{var m=e,h=f,d=n,p=t;switch(h.tag){case 1:if("function"==typeof(m=h.payload)){s=m.call(p,s,d);break e}s=m;break e;case 3:m.flags=-65537&m.flags|128;case 0:if(null==(d="function"==typeof(m=h.payload)?m.call(p,s,d):m))break e;s=g({},s,d);break e;case 2:ss=!0}}null!==f.callback&&0!==f.lane&&(e.flags|=64,null===(d=o.effects)?o.effects=[f]:d.push(f))}else p={eventTime:p,lane:d,tag:f.tag,payload:f.payload,callback:f.callback,next:null},null===c?(a=c=p,l=s):c=c.next=p,i|=d;if(null===(f=f.next)){if(null===(f=o.shared.pending))break;f=(d=f).next,d.next=null,o.lastBaseUpdate=d,o.shared.pending=null}}if(null===c&&(l=s),o.baseState=l,o.firstBaseUpdate=a,o.lastBaseUpdate=c,null!==(n=o.shared.interleaved))for(o=n;i|=o.lane,(o=o.next)!==n;);else null===u&&(o.shared.lanes=0);Qs|=i,e.lanes=i,e.memoizedState=s}}function Yt(e,n,t){if(e=n.effects,(n.effects=null)!==e)for(n=0;n<\\/script>",e=e.removeChild(e.firstChild)):"string"==typeof r.is?e=o.createElement(t,{is:r.is}):(e=o.createElement(t),"select"===t&&(o=e,r.multiple?o.multiple=!0:r.size&&(o.size=r.size))):e=o.createElementNS(e,t),e[Li]=n,e[Ti]=r,Ls(e,n,!1,!1),n.stateNode=e;e:{switch(o=ye(t,r),t){case"dialog":c("cancel",e),c("close",e),a=r;break;case"iframe":case"object":case"embed":c("load",e),a=r;break;case"video":case"audio":for(a=0;aYs&&(n.flags|=128,wl(i,!(r=!0)),n.lanes=4194304)}else{if(!r)if(null!==(e=nr(o))){if(n.flags|=128,r=!0,null!==(t=e.updateQueue)&&(n.updateQueue=t,n.flags|=4),wl(i,!0),null===i.tail&&"hidden"===i.tailMode&&!o.alternate&&!E)return p(n),null}else 2*y()-i.renderingStartTime>Ys&&1073741824!==t&&(n.flags|=128,wl(i,!(r=!0)),n.lanes=4194304);i.isBackwards?(o.sibling=n.child,n.child=o):(null!==(t=i.last)?t.sibling=o:n.child=o,i.last=o)}if(null!==i.tail)return n=i.tail,i.rendering=n,i.tail=n.sibling,i.renderingStartTime=y(),n.sibling=null,t=C.current,f(C,r?1&t|2:1&t),n}return p(n),null;case 22:case 23:return O=Bs.current,s(Bs),r=null!==n.memoizedState,null!==e&&null!==e.memoizedState!==r&&(n.flags|=8192),r&&0!=(1&n.mode)?0!=(1073741824&O)&&(p(n),6&n.subtreeFlags)&&(n.flags|=8192):p(n),null;case 24:case 25:return null}throw Error(S(156,n.tag))}function xl(e,n,t){switch(wt(n),n.tag){case 1:return h(n.type)&&(s(b),s(v)),65536&(e=n.flags)?(n.flags=-65537&e|128,n):null;case 3:return Zt(),s(b),s(v),tr(),0!=(65536&(e=n.flags))&&0==(128&e)?(n.flags=-65537&e|128,n):null;case 5:return er(n),null;case 13:if(s(C),null!==(e=n.memoizedState)&&null!==e.dehydrated){if(null===n.alternate)throw Error(S(340));Pt()}return 65536&(e=n.flags)?(n.flags=-65537&e|128,n):null;case 19:return s(C),null;case 4:return Zt(),null;case 10:return Dt(n.type._context),null;case 22:case 23:return O=Bs.current,s(Bs),null;default:return null}}function El(n,t){var e=n.ref;if(null!==e)if("function"==typeof e)try{e(null)}catch(e){w(n,t,e)}else e.current=null}function Cl(n,t,e){try{e()}catch(e){w(n,t,e)}}function Nl(e,n){if(Ei=Su,Dn(e=Rn())){if("selectionStart"in e)var t={start:e.selectionStart,end:e.selectionEnd};else e:if((a=(t=(t=e.ownerDocument)&&t.defaultView||window).getSelection&&t.getSelection())&&0!==a.rangeCount){var t=a.anchorNode,r=a.anchorOffset,l=a.focusNode,a=a.focusOffset;try{t.nodeType,l.nodeType}catch(e){t=null;break e}var o,u=0,i=-1,s=-1,c=0,f=0,d=e,p=null;n:for(;;){for(;d!==t||0!==r&&3!==d.nodeType||(i=u+r),d!==l||0!==a&&3!==d.nodeType||(s=u+a),3===d.nodeType&&(u+=d.nodeValue.length),null!==(o=d.firstChild);)p=d,d=o;for(;;){if(d===e)break n;if(p===t&&++c===r&&(i=u),p===l&&++f===a&&(s=u),null!==(o=d.nextSibling))break;p=(d=p).parentNode}d=o}t=-1===i||-1===s?null:{start:i,end:s}}else t=null;t=t||{start:0,end:0}}else t=null;for(Su=!(Ci={focusedElem:e,selectionRange:t}),T=n;null!==T;)if(e=(n=T).child,0!=(1028&n.subtreeFlags)&&null!==e)e.return=n,T=e;else for(;null!==T;){n=T;try{var m,h,g,y,v=n.alternate;if(0!=(1024&n.flags))switch(n.tag){case 0:case 11:case 15:break;case 1:null!==v&&(m=v.memoizedProps,h=v.memoizedState,y=(g=n.stateNode).getSnapshotBeforeUpdate(n.elementType===n.type?m:Ar(n.type,m),h),g.__reactInternalSnapshotBeforeUpdate=y);break;case 3:var b=n.stateNode.containerInfo;1===b.nodeType?b.textContent="":9===b.nodeType&&b.documentElement&&b.removeChild(b.documentElement);break;case 5:case 6:case 4:case 17:break;default:throw Error(S(163))}}catch(e){w(n,n.return,e)}if(null!==(e=n.sibling)){e.return=n.return,T=e;break}T=n.return}return v=Os,Os=!1,v}function zl(e,n,t){var r=n.updateQueue;if(null!==(r=null!==r?r.lastEffect:null)){var l,a=r=r.next;do{}while((a.tag&e)===e&&(l=a.destroy,(a.destroy=void 0)!==l)&&Cl(n,t,l),(a=a.next)!==r)}}function Pl(e,n){if(null!==(n=null!==(n=n.updateQueue)?n.lastEffect:null)){var t,r=n=n.next;do{}while((r.tag&e)===e&&(t=r.create,r.destroy=t()),(r=r.next)!==n)}}function _l(e){var n,t=e.ref;null!==t&&(n=e.stateNode,e.tag,e=n,"function"==typeof t?t(e):t.current=e)}function Ll(e){var n=e.alternate;null!==n&&(e.alternate=null,Ll(n)),e.child=null,e.deletions=null,e.sibling=null,5===e.tag&&null!==(n=e.stateNode)&&(delete n[Li],delete n[Ti],delete n[Fi],delete n[Ri],delete n[Di]),e.stateNode=null,e.return=null,e.dependencies=null,e.memoizedProps=null,e.memoizedState=null,e.pendingProps=null,e.stateNode=null,e.updateQueue=null}function Tl(e){return 5===e.tag||3===e.tag||4===e.tag}function Ml(e){e:for(;;){for(;null===e.sibling;){if(null===e.return||Tl(e.return))return null;e=e.return}for(e.sibling.return=e.return,e=e.sibling;5!==e.tag&&6!==e.tag&&18!==e.tag;){if(2&e.flags)continue e;if(null===e.child||4===e.tag)continue e;e=(e.child.return=e).child}if(!(2&e.flags))return e.stateNode}}function Fl(e,n,t){var r=e.tag;if(5===r||6===r)e=e.stateNode,n?(8===t.nodeType?t.parentNode:t).insertBefore(e,n):(8===t.nodeType?(n=t.parentNode).insertBefore(e,t):(n=t).appendChild(e),null==(t=t._reactRootContainer)&&null===n.onclick&&(n.onclick=Jn));else if(4!==r&&null!==(e=e.child))for(Fl(e,n,t),e=e.sibling;null!==e;)Fl(e,n,t),e=e.sibling}function Rl(e,n,t){var r=e.tag;if(5===r||6===r)e=e.stateNode,n?t.insertBefore(e,n):t.appendChild(e);else if(4!==r&&null!==(e=e.child))for(Rl(e,n,t),e=e.sibling;null!==e;)Rl(e,n,t),e=e.sibling}function Dl(e,n,t){for(t=t.child;null!==t;)Ol(e,n,t),t=t.sibling}function Ol(e,n,t){if(ou&&"function"==typeof ou.onCommitFiberUnmount)try{ou.onCommitFiberUnmount(au,t)}catch(e){}switch(t.tag){case 5:L||El(t,n);case 6:var r=M,l=Is;M=null,Dl(e,n,t),Is=l,null!==(M=r)&&(Is?(e=M,t=t.stateNode,(8===e.nodeType?e.parentNode:e).removeChild(t)):M.removeChild(t.stateNode));break;case 18:null!==M&&(Is?(e=M,t=t.stateNode,8===e.nodeType?tt(e.parentNode,t):1===e.nodeType&&tt(e,t),Je(e)):tt(M,t.stateNode));break;case 4:r=M,l=Is,M=t.stateNode.containerInfo,Is=!0,Dl(e,n,t),M=r,Is=l;break;case 0:case 11:case 14:case 15:if(!L&&null!==(r=t.updateQueue)&&null!==(r=r.lastEffect)){l=r=r.next;do{var a=(o=l).destroy,o=o.tag}while(void 0===a||0==(2&o)&&0==(4&o)||Cl(t,n,a),(l=l.next)!==r)}Dl(e,n,t);break;case 1:if(!L&&(El(t,n),"function"==typeof(r=t.stateNode).componentWillUnmount))try{r.props=t.memoizedProps,r.state=t.memoizedState,r.componentWillUnmount()}catch(e){w(t,n,e)}Dl(e,n,t);break;case 21:Dl(e,n,t);break;case 22:1&t.mode?(L=(r=L)||null!==t.memoizedState,Dl(e,n,t),L=r):Dl(e,n,t);break;default:Dl(e,n,t)}}function Il(t){var r,e=t.updateQueue;null!==e&&((t.updateQueue=null)===(r=t.stateNode)&&(r=t.stateNode=new Ds),e.forEach(function(e){var n=ka.bind(null,t,e);r.has(e)||(r.add(e),e.then(n,n))}))}function Ul(e,n,t){if(null!==(t=n.deletions))for(var r=0;r