From 4610a3ba3609559826c9118089894594d6ebaf30 Mon Sep 17 00:00:00 2001 From: valb-mig Date: Wed, 25 Mar 2026 18:36:40 -0300 Subject: [PATCH 01/10] feat: (Bundle) add generic immutable bundle base value object --- src/Domain/ValueObjects/Bundle.php | 54 ++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 src/Domain/ValueObjects/Bundle.php diff --git a/src/Domain/ValueObjects/Bundle.php b/src/Domain/ValueObjects/Bundle.php new file mode 100644 index 0000000..5873161 --- /dev/null +++ b/src/Domain/ValueObjects/Bundle.php @@ -0,0 +1,54 @@ + $bundleData + */ + public function __construct( + public readonly array $bundleData = [], + ) {} + + /** + * @param array $bundleData + * @return static + */ + public function add(array $bundleData): static + { + return new static(array_merge($this->bundleData, $bundleData)); + } + + /** + * @param string $key + * @return static + */ + public function remove(string $key): static + { + return new static(array_diff_key($this->bundleData, [$key => true])); + } + + /** + * @param string $key + * @return TData|null + */ + public function get(string $key): mixed + { + return $this->has($key) ? $this->bundleData[$key] : null; + } + + /** + * @param string $key + * @return bool + */ + public function has(string $key): bool + { + return array_key_exists($key, $this->bundleData); + } +} From abe904a8eaae03234fff8a12296e112351f0416c Mon Sep 17 00:00:00 2001 From: valb-mig Date: Wed, 25 Mar 2026 18:36:40 -0300 Subject: [PATCH 02/10] feat: (Character) add entity with Statistics, Attributes and Identity value objects --- src/Domain/Entities/Character.php | 59 +++++++++++++++ .../ValueObjects/Character/Attributes.php | 43 +++++++++++ .../Character/Attributes/GenericAttribute.php | 43 +++++++++++ .../ValueObjects/Character/Identity.php | 18 +++++ .../ValueObjects/Character/Statistics.php | 48 +++++++++++++ .../Character/Statistics/GenericStatistic.php | 68 ++++++++++++++++++ .../Character/Statistics/Health.php | 71 +++++++++++++++++++ 7 files changed, 350 insertions(+) create mode 100644 src/Domain/Entities/Character.php create mode 100644 src/Domain/ValueObjects/Character/Attributes.php create mode 100644 src/Domain/ValueObjects/Character/Attributes/GenericAttribute.php create mode 100644 src/Domain/ValueObjects/Character/Identity.php create mode 100644 src/Domain/ValueObjects/Character/Statistics.php create mode 100644 src/Domain/ValueObjects/Character/Statistics/GenericStatistic.php create mode 100644 src/Domain/ValueObjects/Character/Statistics/Health.php diff --git a/src/Domain/Entities/Character.php b/src/Domain/Entities/Character.php new file mode 100644 index 0000000..328f092 --- /dev/null +++ b/src/Domain/Entities/Character.php @@ -0,0 +1,59 @@ +key()] = $attribute; + } + + parent::__construct($bundleData); + } +} + +abstract class Attribute +{ + /** + * @return string + */ + abstract public function key(): string; + + /** + * @return string + */ + abstract public function description(): string; + + /** + * @return int + */ + abstract public function value(): int; +} diff --git a/src/Domain/ValueObjects/Character/Attributes/GenericAttribute.php b/src/Domain/ValueObjects/Character/Attributes/GenericAttribute.php new file mode 100644 index 0000000..0b1dc49 --- /dev/null +++ b/src/Domain/ValueObjects/Character/Attributes/GenericAttribute.php @@ -0,0 +1,43 @@ +key; + } + + /** + * @return string + */ + #[\Override] + public function description(): string + { + return $this->description; + } + + /** + * @return int + */ + #[\Override] + public function value(): int + { + return $this->value; + } +} diff --git a/src/Domain/ValueObjects/Character/Identity.php b/src/Domain/ValueObjects/Character/Identity.php new file mode 100644 index 0000000..18d5ac7 --- /dev/null +++ b/src/Domain/ValueObjects/Character/Identity.php @@ -0,0 +1,18 @@ + $identityData + */ + public function __construct(array $identityData = []) + { + parent::__construct($identityData); + } +} diff --git a/src/Domain/ValueObjects/Character/Statistics.php b/src/Domain/ValueObjects/Character/Statistics.php new file mode 100644 index 0000000..50c5a83 --- /dev/null +++ b/src/Domain/ValueObjects/Character/Statistics.php @@ -0,0 +1,48 @@ +key()] = $statistic; + } + + parent::__construct($bundleData); + } +} + +abstract class Statistic +{ + /** + * @return string + */ + abstract public function key(): string; + + /** + * @return int + */ + abstract public function current(): int; + + /** + * @param int $value + */ + abstract public function increase(int $value): static; + + /** + * @param int $value + */ + abstract public function decrease(int $value): static; +} diff --git a/src/Domain/ValueObjects/Character/Statistics/GenericStatistic.php b/src/Domain/ValueObjects/Character/Statistics/GenericStatistic.php new file mode 100644 index 0000000..9eff19d --- /dev/null +++ b/src/Domain/ValueObjects/Character/Statistics/GenericStatistic.php @@ -0,0 +1,68 @@ +current = 0; + } + + /** + * @return string + */ + #[\Override] + public function key(): string + { + return $this->key; + } + + /** + * @return int + */ + #[\Override] + public function current(): int + { + return $this->current; + } + + /** + * @param int $value + * @return static + */ + #[\Override] + public function increase(int $value): static + { + if ($this->current >= $this->max) { + return $this; + } + + $clone = clone $this; + $clone->current = $this->current + $value; + return $clone; + } + + /** + * @param int $value + * @return static + */ + #[\Override] + public function decrease(int $value): static + { + if ($this->current <= 0) { + return $this; + } + $clone = clone $this; + $clone->current = $this->current - $value; + return $clone; + } +} diff --git a/src/Domain/ValueObjects/Character/Statistics/Health.php b/src/Domain/ValueObjects/Character/Statistics/Health.php new file mode 100644 index 0000000..b964ca7 --- /dev/null +++ b/src/Domain/ValueObjects/Character/Statistics/Health.php @@ -0,0 +1,71 @@ +current = $max; + } + + /** + * @return string + */ + #[\Override] + public function key(): string + { + return self::KEY; + } + + /** + * @return int + */ + #[\Override] + public function current(): int + { + return $this->current; + } + + /** + * @param int $value + * @return static + */ + #[\Override] + public function increase(int $value): static + { + if ($this->current >= $this->max) { + return $this; + } + $clone = clone $this; + $clone->current = $this->current + $value; + return $clone; + } + + /** + * @param int $value + * @return static + */ + #[\Override] + public function decrease(int $value): static + { + if ($this->current <= 0) { + return $this; + } + $clone = clone $this; + $clone->current = $this->current - $value; + return $clone; + } +} From 02a6474390b2f6e0a0e1d7e4c91b48ace3658841 Mon Sep 17 00:00:00 2001 From: valb-mig Date: Wed, 25 Mar 2026 18:36:40 -0300 Subject: [PATCH 03/10] refactor: (Dice) remove RollDiceAction and move logic into RollDiceUseCase --- .../UseCase/Dice/RollDice/RollDiceUseCase.php | 5 +- src/Domain/Actions/Dice/RollDiceAction.php | 21 ----- .../Actions/Dice/RollDiceActionTest.php | 87 ------------------- 3 files changed, 2 insertions(+), 111 deletions(-) delete mode 100644 src/Domain/Actions/Dice/RollDiceAction.php delete mode 100644 tests/Domain/Actions/Dice/RollDiceActionTest.php diff --git a/src/Application/UseCase/Dice/RollDice/RollDiceUseCase.php b/src/Application/UseCase/Dice/RollDice/RollDiceUseCase.php index a38c5fe..00d054c 100644 --- a/src/Application/UseCase/Dice/RollDice/RollDiceUseCase.php +++ b/src/Application/UseCase/Dice/RollDice/RollDiceUseCase.php @@ -7,7 +7,6 @@ use Eco\Result; use RPGPlayground\Application\UseCase\Dice\RollDice\RollDiceInput; use RPGPlayground\Application\UseCase\Dice\RollDice\RollDiceOutput; -use RPGPlayground\Domain\Actions\Dice\RollDiceAction; use RPGPlayground\Domain\Enums\Roll\RollAttribute; final class RollDiceUseCase @@ -46,11 +45,11 @@ private static function roll(RollDiceInput $input): array $rolls = []; for ($i = 0; $i < $input->multiplier; $i++) { - $rolls[] = RollDiceAction::roll($input->dice); + $rolls[] = $input->dice->roll(); } if ($input->attribute !== null) { - $rolls[] = RollDiceAction::roll($input->dice); + $rolls[] = $input->dice->roll(); } return $rolls; diff --git a/src/Domain/Actions/Dice/RollDiceAction.php b/src/Domain/Actions/Dice/RollDiceAction.php deleted file mode 100644 index 50050a7..0000000 --- a/src/Domain/Actions/Dice/RollDiceAction.php +++ /dev/null @@ -1,21 +0,0 @@ -sides); - } -} diff --git a/tests/Domain/Actions/Dice/RollDiceActionTest.php b/tests/Domain/Actions/Dice/RollDiceActionTest.php deleted file mode 100644 index 5253507..0000000 --- a/tests/Domain/Actions/Dice/RollDiceActionTest.php +++ /dev/null @@ -1,87 +0,0 @@ -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], - ]; - } -} From 847cb8eb09517f61c578e2057d2b95b4fbbb93be Mon Sep 17 00:00:00 2001 From: valb-mig Date: Wed, 25 Mar 2026 18:36:40 -0300 Subject: [PATCH 04/10] chore: update Check, Dice and LogHandler after dice refactor --- src/Core/Handler/LogHandler.php | 2 ++ src/Domain/Entities/Check.php | 4 ++++ src/Domain/ValueObjects/Dice.php | 10 ++++++++++ 3 files changed, 16 insertions(+) diff --git a/src/Core/Handler/LogHandler.php b/src/Core/Handler/LogHandler.php index 234e28a..db0bdc5 100644 --- a/src/Core/Handler/LogHandler.php +++ b/src/Core/Handler/LogHandler.php @@ -1,5 +1,7 @@ sides); + } } From 1a5b55e6413bf88e3d8d566d63e4890c0c6e1cc3 Mon Sep 17 00:00:00 2001 From: valb-mig Date: Wed, 1 Apr 2026 17:40:48 -0300 Subject: [PATCH 05/10] feat(domain): introduce Item and Inventory entities - add Item entity with identity, weight and attributes - add Inventory entity to manage item collections - prepare domain for inventory-driven gameplay --- src/Domain/Entities/Inventory.php | 146 +++++++++++++++++++++ src/Domain/Entities/Item.php | 81 ++++++++++++ src/Domain/Entities/Item/DurableItem.php | 42 ++++++ src/Domain/Entities/Item/InventoryItem.php | 58 ++++++++ 4 files changed, 327 insertions(+) create mode 100644 src/Domain/Entities/Inventory.php create mode 100644 src/Domain/Entities/Item.php create mode 100644 src/Domain/Entities/Item/DurableItem.php create mode 100644 src/Domain/Entities/Item/InventoryItem.php diff --git a/src/Domain/Entities/Inventory.php b/src/Domain/Entities/Inventory.php new file mode 100644 index 0000000..20dd84f --- /dev/null +++ b/src/Domain/Entities/Inventory.php @@ -0,0 +1,146 @@ +checkItemsWeight($items); + } + + /** + * @param InventoryItem[] $items + * @return static + * @throws \InvalidArgumentException + */ + private function checkItemsWeight(array $items): void + { + $totalWeight = 0; + + foreach ($items as $item) { + $totalWeight += $item->getItem()->getWeight(); + } + + if ($totalWeight > $this->maxWeight) { + throw new \InvalidArgumentException('Total weight of items must be less than or equal to max weight'); + } + } + + public function add(InventoryItem $item): void + { + if ($this->has($item->getItem()->getId())) { + throw new \InvalidArgumentException('Item already exists in inventory'); + } + + if ((count($this->items) + $item->getQuantity()) > $this->slots) { + throw new \InvalidArgumentException('Inventory is full'); + } + + // TODO: checks if the inventory will fit the items if the new item is added + $this->checkItemsWeight([...$this->items, $item]); + + $this->items[$item->getItem()->getId()->value] = $item; + } + + /** + * @param Identifier $id + * @return void + */ + public function remove(Identifier $id): void + { + if (!$this->has($id)) { + throw new \InvalidArgumentException('Item not found in inventory'); + } + + unset($this->items[$id->value]); + } + + /** + * @param Identifier $id + * @param int $quantity + * @return self + * @throws \InvalidArgumentException + */ + public function drop(Identifier $id, int $quantity): self + { + if (!$this->has($id)) { + throw new \InvalidArgumentException('Item not found in inventory'); + } + + $item = $this->get($id); + + if ($item->getQuantity() == $quantity) { + $this->remove($id); + return $this; + } + + if ($quantity > $item->getQuantity()) { + throw new \InvalidArgumentException("Not enough quantity of {$item->getItem()->getName()} to drop"); + } + + $item->setQuantity($item->getQuantity() - $quantity); + + $this->items[$id->value] = $item; + + return $this; + } + + /** + * @param Identifier $id + * @return InventoryItem + * @throws \InvalidArgumentException + */ + public function get(Identifier $id): InventoryItem + { + if (!$this->has($id)) { + throw new \InvalidArgumentException('Item not found in inventory'); + } + + return $this->items[$id->value]; + } + + /** + * @param Identifier $id + * @return bool + */ + public function has(Identifier $id): bool + { + if (empty($id)) { + throw new \InvalidArgumentException('Id cannot be empty'); + } + + return array_key_exists($id->value, $this->items); + } + + /** + * @return InventoryItem[] + */ + public function show(): array + { + return $this->items; + } +} diff --git a/src/Domain/Entities/Item.php b/src/Domain/Entities/Item.php new file mode 100644 index 0000000..74ba6f0 --- /dev/null +++ b/src/Domain/Entities/Item.php @@ -0,0 +1,81 @@ +name = $name; + $this->description = $description; + } + + /** + * @return Identifier + */ + public function getId(): Identifier + { + return $this->id; + } + + /** + * @return string + */ + public function getName(): string + { + return $this->name; + } + + /** + * @return string + */ + public function getDescription(): string + { + return $this->description; + } + + /** + * @return float + */ + public function getWeight(): float + { + return $this->weight; + } + + /** + * @return Attributes + */ + public function getAttributes(): Attributes + { + return $this->attributes; + } +} diff --git a/src/Domain/Entities/Item/DurableItem.php b/src/Domain/Entities/Item/DurableItem.php new file mode 100644 index 0000000..196c254 --- /dev/null +++ b/src/Domain/Entities/Item/DurableItem.php @@ -0,0 +1,42 @@ +item; + } + + /** + * @return Durability + */ + public function getDurability(): Durability + { + return $this->durability; + } + + /** + * @param Item $item + * @param Durability $durability + * @return self + */ + public static function fromItem(Item $item, Durability $durability): self + { + return new self($item, $durability); + } +} diff --git a/src/Domain/Entities/Item/InventoryItem.php b/src/Domain/Entities/Item/InventoryItem.php new file mode 100644 index 0000000..295535e --- /dev/null +++ b/src/Domain/Entities/Item/InventoryItem.php @@ -0,0 +1,58 @@ +item; + } + + /** + * @return int + */ + public function getQuantity(): int + { + return $this->quantity; + } + + /** + * @param int $quantity + * @return void + * @throws \InvalidArgumentException + */ + public function setQuantity(int $quantity): void + { + if ($quantity < 1) { + throw new \InvalidArgumentException('Quantity cannot be less than 1'); + } + $this->quantity = $quantity; + } + + /** + * @param Item $item + * @param int $quantity + * @return self + */ + public static function fromItem(Item $item, int $quantity = 1): self + { + return new self($item, $quantity); + } +} From b0d778769ef3c5ec0c8a02ec3ad11e12035c1e3c Mon Sep 17 00:00:00 2001 From: valb-mig Date: Wed, 1 Apr 2026 17:40:59 -0300 Subject: [PATCH 06/10] refactor(attributes): move Attributes to shared value object structure - remove character-specific attributes implementation - introduce generic Attributes value object - improve reusability across domain (items, characters, etc.) --- src/Domain/ValueObjects/{Character => }/Attributes.php | 2 +- .../{Character => }/Attributes/GenericAttribute.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) rename src/Domain/ValueObjects/{Character => }/Attributes.php (93%) rename src/Domain/ValueObjects/{Character => }/Attributes/GenericAttribute.php (83%) diff --git a/src/Domain/ValueObjects/Character/Attributes.php b/src/Domain/ValueObjects/Attributes.php similarity index 93% rename from src/Domain/ValueObjects/Character/Attributes.php rename to src/Domain/ValueObjects/Attributes.php index d400f7c..bc9ba92 100644 --- a/src/Domain/ValueObjects/Character/Attributes.php +++ b/src/Domain/ValueObjects/Attributes.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace RPGPlayground\Domain\ValueObjects\Character; +namespace RPGPlayground\Domain\ValueObjects; use RPGPlayground\Domain\ValueObjects\Bundle; diff --git a/src/Domain/ValueObjects/Character/Attributes/GenericAttribute.php b/src/Domain/ValueObjects/Attributes/GenericAttribute.php similarity index 83% rename from src/Domain/ValueObjects/Character/Attributes/GenericAttribute.php rename to src/Domain/ValueObjects/Attributes/GenericAttribute.php index 0b1dc49..7115a7b 100644 --- a/src/Domain/ValueObjects/Character/Attributes/GenericAttribute.php +++ b/src/Domain/ValueObjects/Attributes/GenericAttribute.php @@ -2,9 +2,9 @@ declare(strict_types=1); -namespace RPGPlayground\Domain\ValueObjects\Character\Attributes; +namespace RPGPlayground\Domain\ValueObjects\Attributes; -use RPGPlayground\Domain\ValueObjects\Character\Attribute; +use RPGPlayground\Domain\ValueObjects\Attribute; final class GenericAttribute extends Attribute { From 8a1d7bee3f076e9e1bf108cb8c7de313edf3f1c4 Mon Sep 17 00:00:00 2001 From: valb-mig Date: Wed, 1 Apr 2026 17:41:04 -0300 Subject: [PATCH 07/10] feat(item): add Durability value object for item state - introduce durability concept for items - enable future support for item degradation and repair mechanics --- src/Domain/ValueObjects/Durability.php | 35 ++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 src/Domain/ValueObjects/Durability.php diff --git a/src/Domain/ValueObjects/Durability.php b/src/Domain/ValueObjects/Durability.php new file mode 100644 index 0000000..6f018df --- /dev/null +++ b/src/Domain/ValueObjects/Durability.php @@ -0,0 +1,35 @@ + $max) { + throw new \InvalidArgumentException('Durability cannot be less than 1 or greater than max'); + } + } + + /** + * @param int $value + * @return self + */ + public function increase(int $value): self + { + return new self(min($this->current + $value, $this->max), $this->max); + } + + /** + * @param int $value + * @return self + */ + public function decrease(int $value): self + { + return new self(max($this->current - $value, 0), $this->max); + } +} From 1c877ca7a8da1f5ea80b3ad2e9fb6bfd45e42ea6 Mon Sep 17 00:00:00 2001 From: valb-mig Date: Wed, 1 Apr 2026 17:41:07 -0300 Subject: [PATCH 08/10] refactor(character): integrate inventory and update structure - update Character entity to support inventory association - align character structure with new domain entities --- src/Domain/Entities/Character.php | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/Domain/Entities/Character.php b/src/Domain/Entities/Character.php index 328f092..9ee04ae 100644 --- a/src/Domain/Entities/Character.php +++ b/src/Domain/Entities/Character.php @@ -5,12 +5,15 @@ namespace RPGPlayground\Domain\Entities; use RPGPlayground\Core\Handler\StrHandler; -use RPGPlayground\Domain\ValueObjects\Character\Attributes; +use RPGPlayground\Domain\Entities\Inventory; +use RPGPlayground\Domain\ValueObjects\Attributes; use RPGPlayground\Domain\ValueObjects\Character\Identity; use RPGPlayground\Domain\ValueObjects\Character\Statistics; class Character { + private Inventory $inventory; + /** * @param string $name * @param string $description @@ -56,4 +59,14 @@ public static function create( return new self($name, $description, $identity, $statistics, $attributes); } + + public function setInventory(Inventory $inventory): void + { + $this->inventory = $inventory; + } + + public function getInventory(): Inventory + { + return $this->inventory; + } } From e696fa8018f4357b980554ed1a2739463f7c2e1d Mon Sep 17 00:00:00 2001 From: valb-mig Date: Wed, 1 Apr 2026 17:41:14 -0300 Subject: [PATCH 09/10] refactor(bundle): improve generic collection handling - adjust bundle structure to support domain collections - prepare for reuse in inventory and attributes --- src/Domain/ValueObjects/Bundle.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Domain/ValueObjects/Bundle.php b/src/Domain/ValueObjects/Bundle.php index 5873161..8652f46 100644 --- a/src/Domain/ValueObjects/Bundle.php +++ b/src/Domain/ValueObjects/Bundle.php @@ -13,7 +13,7 @@ abstract class Bundle * @param array $bundleData */ public function __construct( - public readonly array $bundleData = [], + private array $bundleData = [], ) {} /** @@ -43,6 +43,12 @@ public function get(string $key): mixed return $this->has($key) ? $this->bundleData[$key] : null; } + /** @return array */ + public function show(): array + { + return $this->bundleData; + } + /** * @param string $key * @return bool From 51153ad1d67f928300735422f42b5329f92bdc57 Mon Sep 17 00:00:00 2001 From: valb-mig Date: Wed, 1 Apr 2026 17:41:20 -0300 Subject: [PATCH 10/10] chore(github): update issues configuration - adjust issues.json configuration --- .github/issues.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/issues.json b/.github/issues.json index 32960f8..566c73b 100644 --- a/.github/issues.json +++ b/.github/issues.json @@ -1,2 +1,7 @@ [ + { + "title": "logic to check if the item is durable, because an item that has quantity > 1 cannot be stackable", + "description": "An item that has quantity > 1 cannot be stackable", + "labels": ["bug"] + } ] \ No newline at end of file