diff --git a/docs/Serialization/Scenes.md b/docs/Serialization/Scenes.md new file mode 100644 index 0000000..49c1d0f --- /dev/null +++ b/docs/Serialization/Scenes.md @@ -0,0 +1,316 @@ +# Sendama Scene Files (`*.scene.php`) + +Sendama scenes are **PHP files that return an associative array** describing a scene’s metadata and its object hierarchy. The engine loads the file (for example `level01.scene.php`), then builds a `Scene` by creating an **anonymous class that extends ****AbstractScene**, and finally constructs the hierarchy based on the array returned by the file. + +This document explains the expected structure of a Sendama scene file using a real-world example. + +--- + +## File naming and conventions + +Scene files follow this naming convention: + +``` +.scene.php +``` + +Example: + +``` +level01.scene.php +``` + +Because scene files are plain PHP, you may: + +* Import classes with `use` +* Reference constants +* Use enums (for your own game logic, if desired) +* Perform calculations +* Build strings dynamically + +The **only hard requirement** is that the file returns an array matching the scene schema. + +--- + +## Top-level structure + +A scene file must return an array with the following top-level keys: + +```php +return [ + "width" => 80, + "height" => 24, + "environmentTileMapPath" => "Maps/level", + "hierarchy" => [ + // GameObjects and UIElements + ], +]; +``` + +### `width` + +Defines the logical width of the scene. + +* Type: `int` +* Typically set using engine constants + +Example: + +```php +"width" => 80 +``` + +### `height` + +Defines the logical height of the scene. + +* Type: `int` + +Example: + +```php +"height" => 24 +``` + +### `environmentTileMapPath` + +Specifies the base path to the environment or tilemap used by the scene. + +* Type: `string` +* Interpreted by the game’s asset loading system + +Example: + +```php +"environmentTileMapPath" => "Maps/level" +``` + +### `hierarchy` + +Defines the objects that exist in the scene at load time. Each entry represents an object to be instantiated. + +* Type: `array>` + +--- + +## Scene hierarchy + +Each entry in the `hierarchy` array describes **one object in the scene**. At load time, each entry must resolve to either: + +* a `GameObject`, or +* a UI element that extends the `UIElement` abstract class (for example `Label`, `Text`, etc.) + +The `type` field determines which class is instantiated, while the remaining fields configure the object. + +### Common GameObject and UIElement fields + +#### `type` + +Fully-qualified class name of the object to create. + +* Type: `class-string` + +Example: + +```php +"type" => GameObject::class +``` + +#### `name` + +Human-readable identifier for debugging and tooling. + +* Type: `string` + +Example: + +```php +"name" => "Player" +``` + +#### `tag` + +Logical grouping identifier used for lookups, filtering, or gameplay logic. Tags are plain strings and are not enforced by the engine. + +* Type: `string` +* Tags are user-defined and not provided by the Sendama Engine + +Example: + +```php +"tag" => "player" +``` + +#### `position`, `rotation`, `scale` + +Defines transform-related properties for the object. + +* Type: `array{x:int|float, y:int|float}` + +Example: + +```php +"position" => ["x" => 4, "y" => 12], +"rotation" => ["x" => 0, "y" => 0], +"scale" => ["x" => 1, "y" => 1], +``` + +Even if rotation or scale are not heavily used, they are part of the standard scene schema. + +--- + +## Game objects and components + +### `GameObject` definitions + +A typical `GameObject` definition looks like this: + +```php +[ + "type" => GameObject::class, + "name" => "Player", + "tag" => "player", + "position" => ["x" => 4, "y" => 12], + "rotation" => ["x" => 0, "y" => 0], + "scale" => ["x" => 1, "y" => 1], + "sprite" => [ /* optional */ ], + "components" => [ /* behaviours */ ], +] +``` + +### `components` + +Components attach behaviour to a game object. Each component entry specifies a class to instantiate and may optionally define property values that are applied during scene initialization. + +* Type: `array}>` + +Basic example: + +```php +"components" => [ + [ "class" => PlayerController::class ], + [ "class" => Gun::class ], +] +``` + +#### `properties` + +The `properties` key allows you to set default values for component properties at scene load time. + +* Applies to: + + * public properties, and + * protected or private properties marked with the `#[SerializeField]` attribute + +Example: + +```php +"components" => [ + [ + "class" => Gun::class, + "properties" => [ + "fireRate" => 0.25, + "ammo" => 30, + ] + ] +] +``` + +Property assignment occurs after the component is constructed and before the scene begins execution. + +This design keeps scene files declarative while encapsulating behaviour inside components. + +--- + +## Sprites and textures + +### `sprite` + +The `sprite` block describes how an object is rendered in the terminal. + +```php +"sprite" => [ + "texture" => [ + "path" => "Textures/player", + "position" => ["x" => 0, "y" => 0], + "size" => ["x" => 1, "y" => 5], + ] +] +``` + +#### `texture.path` + +Path to the texture asset. + +* Type: `string` + +#### `texture.position` + +Top-left coordinate of the texture region. + +* Type: `array{x:int, y:int}` + +#### `texture.size` + +Width and height of the texture region. + +* Type: `array{x:int, y:int}` + +--- + +## UI elements (example: `Label`) + +UI elements are declared directly in the scene hierarchy and must extend the `UIElement` abstract class. They may expose a different set of properties than `GameObject`. + +Example: + +```php +[ + "type" => Label::class, + "name" => "Score", + "tag" => "ui", + "position" => ["x" => 4, "y" => 22], + "size" => ["x" => 10, "y" => 1], + "text" => "Score: " . str_pad('0', 3, '0', STR_PAD_LEFT), +] +``` + +### `size` + +Defines the bounding area of the UI element. + +* Type: `array{x:int, y:int}` + +### `text` + +Text content of the label. + +* Type: `string` + +Because this is PHP, text may be dynamically generated or formatted. + +--- + +## Scene loading lifecycle + +When a scene is loaded, the engine: + +1. Includes the `*.scene.php` file +2. Reads the returned array +3. Creates an anonymous class extending `AbstractScene` +4. Applies scene-level metadata (`width`, `height`, etc.) +5. Iterates over `hierarchy` and instantiates each GameObject or UIElement +6. Attaches components and applies configuration + +The scene file is therefore the **authoritative description** of the scene’s initial state. + +--- + +## Authoring guidelines + +* Keep scene files declarative; put logic in components +* Use tags consistently for querying and grouping +* Use PHP expressions sparingly but intentionally +* Treat scene files as data, not scripts + +This format allows Sendama scenes to remain readable, expressive, and version-control friendly while still leveraging the full power of PHP. diff --git a/src/Core/GameObject.php b/src/Core/GameObject.php index 75ffcb1..0588b09 100644 --- a/src/Core/GameObject.php +++ b/src/Core/GameObject.php @@ -94,6 +94,8 @@ public static function instantiate(GameObject $original, ?Vector2 $position = nu $clone->transform->setParent($parent); } + SceneManager::getInstance()->getActiveScene()->add($clone); + return $clone; } @@ -601,14 +603,14 @@ public function getUIElements(?string $uiElementClass = null): array /** * @inheritDoc */ - public function setSpriteFromTexture(Texture2D|array|string $texture, Vector2 $position, Vector2 $size): void + public function setSpriteFromTexture(Texture|array|string $texture, Vector2 $position, Vector2 $size): void { if (is_array($texture)) { - $texture = new Texture2D($texture['path'], $texture['width'] ?? -1, $texture['height'] ?? -1); + $texture = new Texture($texture['path'], $texture['width'] ?? -1, $texture['height'] ?? -1); } if (is_string($texture)) { - $texture = new Texture2D($texture); + $texture = new Texture($texture); } $this->setSprite(new Sprite($texture, new Rect($position, $size))); diff --git a/src/Core/Interfaces/GameObjectInterface.php b/src/Core/Interfaces/GameObjectInterface.php index 7c69845..a93b77c 100644 --- a/src/Core/Interfaces/GameObjectInterface.php +++ b/src/Core/Interfaces/GameObjectInterface.php @@ -5,7 +5,7 @@ use Sendama\Engine\Core\Component; use Sendama\Engine\Core\Scenes\Interfaces\SceneInterface; use Sendama\Engine\Core\Sprite; -use Sendama\Engine\Core\Texture2D; +use Sendama\Engine\Core\Texture; use Sendama\Engine\Core\Vector2; use Sendama\Engine\UI\Interfaces\UIElementInterface; @@ -26,12 +26,12 @@ public function getScene(): SceneInterface; /** * Sets the sprite of the game object from a texture. * - * @param Texture2D|array{path: string, width: ?int, height: ?int}|string $texture The path to the sprite texture. + * @param Texture|array{path: string, width: ?int, height: ?int}|string $texture The path to the sprite texture. * @param Vector2 $position The position of the sprite * @param Vector2 $size * @return void */ - public function setSpriteFromTexture(Texture2D|array|string $texture, Vector2 $position, Vector2 $size): void; + public function setSpriteFromTexture(Texture|array|string $texture, Vector2 $position, Vector2 $size): void; /** * Sets the sprite of the game object diff --git a/src/Core/Scenes/AbstractScene.php b/src/Core/Scenes/AbstractScene.php index 68b95bf..6ffe718 100644 --- a/src/Core/Scenes/AbstractScene.php +++ b/src/Core/Scenes/AbstractScene.php @@ -88,7 +88,7 @@ public final function __construct(protected string $name, protected ?object $sce $this->awake(); if ($this->environmentTileMapPath) { - $this->loadEnvironmentTileMapData(); + $this->loadEnvironmentTileMapData($this->environmentTileMapPath); } } diff --git a/src/Core/Scenes/ExampleScene.php b/src/Core/Scenes/ExampleScene.php index c1ecfc0..62d033a 100644 --- a/src/Core/Scenes/ExampleScene.php +++ b/src/Core/Scenes/ExampleScene.php @@ -6,7 +6,7 @@ use Sendama\Engine\Core\Behaviours\SimpleQuitListener; use Sendama\Engine\Core\GameObject; use Sendama\Engine\Core\Sprite; -use Sendama\Engine\Core\Texture2D; +use Sendama\Engine\Core\Texture; use Sendama\Engine\Core\Vector2; /** @@ -31,7 +31,7 @@ public function awake(): void $levelManager->addComponent(SimpleQuitListener::class); # Set up the player - $playerTexture = new Texture2D('Textures/player.texture'); + $playerTexture = new Texture('Textures/player.texture'); $player->setSpriteFromTexture($playerTexture, Vector2::zero(), Vector2::one()); /** * @var CharacterMovement $characterMovement diff --git a/src/Core/Scenes/SceneManager.php b/src/Core/Scenes/SceneManager.php index 7fcc539..ea48bc0 100644 --- a/src/Core/Scenes/SceneManager.php +++ b/src/Core/Scenes/SceneManager.php @@ -12,7 +12,7 @@ use Sendama\Engine\Core\Interfaces\SingletonInterface; use Sendama\Engine\Core\Scenes\Interfaces\SceneInterface; use Sendama\Engine\Core\Scenes\Interfaces\SceneNodeInterface; -use Sendama\Engine\Core\Texture2D; +use Sendama\Engine\Core\Texture; use Sendama\Engine\Core\Vector2; use Sendama\Engine\Debug\Debug; use Sendama\Engine\Events\Enumerations\SceneEventType; @@ -23,7 +23,8 @@ use Sendama\Engine\Exceptions\Scenes\SceneNotFoundException; use Sendama\Engine\Physics\Interfaces\ColliderInterface; use Sendama\Engine\Physics\Physics; -use Sendama\Engine\Util\Path; +use Sendama\Engine\UI\Label\Label; +use Sendama\Engine\UI\Text\Text; use function dispatchEvent; /** @@ -254,6 +255,9 @@ public function suspend(): void $this->activeSceneNode?->getScene()->suspend(); } + /** + * Updates the physics of the active scene. + */ public function updatePhysics(): void { if ($this->activeSceneNode) { @@ -351,65 +355,107 @@ public function awake(): void $sceneMetadata = $this->sceneMetadata; if (isset($sceneMetadata->environmentTileMapPath)) { - Debug::log("Setting environment tile map path to: " . $sceneMetadata->environmentTileMapPath); $this->environmentTileMapPath = $sceneMetadata->environmentTileMapPath; } // Build hierarchy if (isset($sceneMetadata->hierarchy)) { - Debug::info("1. Building scene hierarchy from metadata"); - foreach ($sceneMetadata->hierarchy as $index => $rootGameObject) { - $position = new Vector2(); - if (isset($rootGameObject->position)) { - $position = Vector2::fromArray((array)$rootGameObject->position); + foreach ($sceneMetadata->hierarchy as $index => $item) { + if (!isset($item->type)) { + Debug::warn("The 'type' property is not supported in scene hierarchy items. Item: " . ($item->name ?? "Unnamed GameObject") . " - $index"); + continue; } - $rotation = new Vector2(); - if (isset($rootGameObject->rotation)) { - $rotation = Vector2::fromArray((array)$rootGameObject->rotation); - } + $itemName = $item?->name . " - $index" ?? throw new SceneManagementException("Invalid game object name"); - $scale = new Vector2(); - if (isset($rootGameObject->scale)) { - $scale = Vector2::fromArray((array)$rootGameObject->scale); + $position = new Vector2(); + if (isset($item->position)) { + $position = Vector2::fromArray((array)$item->position); } - $gameObject = new GameObject( - $rootGameObject?->name . " - $index" ?? throw new SceneManagementException("Invalid game object name"), - $rootGameObject?->tag, - $position, - $rotation, - $scale - ); - - if (isset($rootGameObject->sprite)) { - if (!isset($rootGameObject->sprite->texture)) { - throw new SceneManagementException("Sprite texture not defined for game object: " . $gameObject->getName()); - } - - $spriteTextureMetadata = $rootGameObject->sprite->texture; - $spriteTexture = new Texture2D($spriteTextureMetadata->path ?? throw new SceneManagementException("Invalid sprite texture path")); - $spritePosition = new Vector2(); - if (isset($spriteTextureMetadata->position)) { - $spritePosition = Vector2::fromArray((array)$spriteTextureMetadata->position); - } - $spriteSize = new Vector2(); - if (isset($spriteTextureMetadata->size)) { - $spriteSize = Vector2::fromArray((array)$spriteTextureMetadata->size); - } - - $gameObject->setSpriteFromTexture($spriteTexture, $spritePosition, $spriteSize); + $size = new Vector2(); + if (isset($item->size)) { + $size = Vector2::fromArray((array)$item->size); } - if (isset($rootGameObject->components)) { - foreach ($rootGameObject->components as $componentMetadata) { - if (!isset($componentMetadata->class)) { - throw new SceneManagementException("Component class not defined for game object: " . $gameObject->getName()); + $gameObject = null; + + switch ($item->type) { + case GameObject::class: + $rotation = new Vector2(); + if (isset($item->rotation)) { + $rotation = Vector2::fromArray((array)$item->rotation); + } + + $scale = new Vector2(); + if (isset($item->scale)) { + $scale = Vector2::fromArray((array)$item->scale); } - $componentClass = $componentMetadata->class; - $gameObject->addComponent($componentClass); - } + $gameObject = new GameObject( + $itemName, + $item?->tag, + $position, + $rotation, + $scale + ); + + if (isset($item->sprite)) { + if (!isset($item->sprite->texture)) { + throw new SceneManagementException("Sprite texture not defined for game object: " . $gameObject->getName()); + } + + $spriteTextureMetadata = $item->sprite->texture; + $spriteTexture = new Texture($spriteTextureMetadata->path ?? throw new SceneManagementException("Invalid sprite texture path")); + $spritePosition = new Vector2(); + if (isset($spriteTextureMetadata->position)) { + $spritePosition = Vector2::fromArray((array)$spriteTextureMetadata->position); + } + $spriteSize = new Vector2(); + if (isset($spriteTextureMetadata->size)) { + $spriteSize = Vector2::fromArray((array)$spriteTextureMetadata->size); + } + + $gameObject->setSpriteFromTexture($spriteTexture, $spritePosition, $spriteSize); + } + + if (isset($item->components)) { + foreach ($item->components as $componentMetadata) { + if (!isset($componentMetadata->class)) { + throw new SceneManagementException("Component class not defined for game object: " . $gameObject->getName()); + } + + $componentClass = $componentMetadata->class; + $component = $gameObject->addComponent($componentClass); + + if (isset($componentMetadata->proerties)) { + foreach ($componentMetadata->proerties as $key => $value) { + if (!property_exists($component, $key)) { + Debug::warn("Property '$key' does not exist on component of type: " . $componentClass); + continue; + } + + $component->$key = $value; + } + } + } + } + + break; + + default: + $gameObject = match($item->type) { + Label::class => new Label($this, $itemName, $position, $size), + Text::class => new Text($this, $itemName, $position, $size) + }; + + if (isset($item->text)) { + if (!method_exists($gameObject, 'setText')) { + throw new SceneManagementException("The 'text' property is not supported for game object of type: " . $item->type); + } + + $gameObject->setText($item->text); + } } $this->add($gameObject); diff --git a/src/Core/Scenes/TitleScene.php b/src/Core/Scenes/TitleScene.php index dcf1e30..ed16b5a 100644 --- a/src/Core/Scenes/TitleScene.php +++ b/src/Core/Scenes/TitleScene.php @@ -20,216 +20,235 @@ */ class TitleScene extends AbstractScene { - /** - * @var Menu $menu - */ - protected Menu $menu; - /** - * @var Text $titleText - */ - protected Text $titleText; - /** - * @var int|null - */ - protected ?int $screenWidth = null; - /** - * @var int|null - */ - protected ?int $screenHeight = null; - /** - * The width of the menu. - * - * @var int $menuWidth - */ - protected int $menuWidth = 20; - /** - * The height of the menu. - * - * @var int $menuHeight - */ - protected int $menuHeight = 8; - /** - * The scene manager. - * - * @var SceneManager $sceneManager - */ - protected SceneManager $sceneManager; - /** - * The title of the game. - * - * @var string $title - */ - protected string $title = ''; - /** - * The left margin of the title. - * - * @var int $titleLeftMargin - */ - protected int $titleLeftMargin = 4; - /** - * The top margin of the title. - * - * @var int $titleTopMargin - */ - protected int $titleTopMargin = 4; - - /** - * @inheritDoc - * @throws Exception - */ - public function awake(): void - { - $this->sceneManager = SceneManager::getInstance(); - $gameName = getGameName() ?? $this->name; - if (!$this->title) { - $this->title = $gameName; + const int TOP_MARGIN_OFFSET = 4; + /** + * @var Menu $menu + */ + protected Menu $menu; + /** + * @var Text $titleText + */ + protected Text $titleText; + /** + * @var int|null + */ + protected ?int $screenWidth = null; + /** + * @var int|null + */ + protected ?int $screenHeight = null; + /** + * The width of the menu. + * + * @var int $menuWidth + */ + protected int $menuWidth = 20; + /** + * The height of the menu. + * + * @var int $menuHeight + */ + protected int $menuHeight = 8; + /** + * The scene manager. + * + * @var SceneManager $sceneManager + */ + protected SceneManager $sceneManager; + /** + * The title of the game. + * + * @var string $title + */ + protected string $title = ''; + /** + * The left margin of the title. + * + * @var int $titleLeftMargin + */ + protected int $titleLeftMargin = 4; + /** + * The top margin of the title. + * + * @var int $titleTopMargin + */ + protected int $titleTopMargin = 4; + + /** + * @inheritDoc + * @throws Exception + */ + public function awake(): void + { + $this->sceneManager = SceneManager::getInstance(); + $gameName = getGameName() ?? $this->name; + + $this->titleText = new Text(scene: $this, name: $gameName, position: new Vector2(0, $this->titleTopMargin), size: new Vector2(DEFAULT_SCREEN_WIDTH, 5)); + $this->titleText->setFontName(FontName::BIG->value); + $this->setTitleText($gameName); + + if (is_array($gameName)) { + $gameName = $_ENV['GAME_NAME'] ?? $this->name; + } + + $this->menu = new Menu(title: $gameName, description: 'q:quit', dimensions: new Rect(new Vector2($this->getMenuLeftMargin(), $this->getMenuTopMargin()), new Vector2($this->menuWidth, $this->menuHeight)), cancelKey: [KeyCode::Q, KeyCode::q], onCancel: fn() => quitGame()); + $this->menu->addItem(new MenuItem(label: 'New Game', description: 'Start a new game', icon: '🎮', callback: function () { + loadScene(1); + })); + $this->menu->addItem(new MenuItem(label: 'Quit', description: 'Quit the game', icon: '🚪', callback: function () { + quitGame(); + })); + + $this->add($this->titleText); + $this->add($this->menu); } - $this->titleText = new Text(scene: $this, name: $gameName, position: new Vector2(0, 4), size: new Vector2(DEFAULT_SCREEN_WIDTH, 5)); - $this->titleText->setFontName(FontName::BIG->value); - $this->titleText->setText($gameName); - $this->titleLeftMargin = round(($this->sceneManager->getSettings('screen_width') / 2) - ($this->titleText->getWidth() / 2)); - $this->titleTopMargin = 4; - $this->titleText->setPosition(new Vector2(round($this->titleLeftMargin), round($this->titleTopMargin))); + /** + * Returns the left margin of the menu based on the screen width and the menu width. + * This is used to center the menu on the screen. + * + * @return int The left margin of the menu. + * @throws Exception + */ + private function getMenuLeftMargin(): int + { + $screenWidth = $this->screenWidth ?? get_screen_width(); + return (int)round($screenWidth / 2) - (int)round($this->menuWidth / 2); + } + + /** + * @return int + */ + private function getMenuTopMargin(): int + { + return ($this->titleTopMargin + $this->titleText->getHeight() + 1); + } + + /** + * @param string $text + * @return $this + * @throws Exception + */ + public function setTitleText(string $text): self + { + $this->titleText->setText($text); + $this->titleLeftMargin = round((get_screen_width() / 2) - ($this->titleText->getWidth() / 2)); + $this->titleTopMargin = self::TOP_MARGIN_OFFSET; + $this->titleText->setPosition(new Vector2(round($this->titleLeftMargin), round($this->titleTopMargin))); + + return $this; + } + + /** + * Sets the font name of the title text. + * + * @param FontName|string $fontName The font name of the title text. + * @return $this + * @throws Exception + */ + public function setTitleFont(FontName|string $fontName): self + { + $this->titleText->setFontName($fontName instanceof FontName ? $fontName->value : $fontName); + return $this; + } + + /** + * Returns the title of the menu. + * + * @return string The title of the menu. + */ + public function getMenuTitle(): string + { + return $this->menu->getTitle(); + } - if (is_array($gameName)) { - $gameName = $_ENV['GAME_NAME'] ?? $this->name; + public function getTitle(): string + { + return $this->titleText->getText(); } - $this->menu = new Menu(title: $gameName, description: 'q:quit', dimensions: new Rect(new Vector2($this->getMenuLeftMargin(), $this->getMenuTopMargin()), new Vector2($this->menuWidth, $this->menuHeight)), cancelKey: [KeyCode::Q, KeyCode::q], onCancel: fn() => quitGame()); - $this->menu->addItem(new MenuItem(label: 'New Game', description: 'Start a new game', icon: '🎮', callback: function () { - loadScene(1); - })); - $this->menu->addItem(new MenuItem(label: 'Quit', description: 'Quit the game', icon: '🚪', callback: function () { - quitGame(); - })); - - $this->add($this->titleText); - $this->add($this->menu); - } - - /** - * @return int - */ - private function getMenuLeftMargin(): int - { - $screenWidth = $this->screenWidth ?? $this->sceneManager->getSettings('screen_width'); - return ($screenWidth / 2) - ($this->menuWidth / 2); - } - - /** - * @return int - */ - private function getMenuTopMargin(): int - { - return ($this->titleTopMargin + $this->titleText->getHeight() + 1); - } - - /** - * Returns the title of the game. - * - * @return string The title of the game. - */ - public function getTitle(): string - { - return $this->title; - } - - /** - * Set the title of the game. - * - * @param string $title The title of the game. - */ - public function setTitle(string $title): void - { - $this->title = $title; - } - - /** - * Sets the default border pack that will be used to render the menu borders. - * - * @param BorderPackInterface $borderPack The border pack to use. - * @return $this - */ - public function setBorderPack(BorderPackInterface $borderPack): self - { - $this->menu->setBorderPack($borderPack); - return $this; - } - - /** - * Sets the index of the new game scene. - * - * @param int $newGameSceneIndex The index of the new game scene. - * @return TitleScene $this - */ - public function setNewGameSceneIndex(int $newGameSceneIndex): self - { - $this->menu->getItemByIndex(0)->setCallback(function () use ($newGameSceneIndex) { - loadScene(max($newGameSceneIndex, 1)); - }); - - return $this; - } - - /** - * Sets the index of the new game scene by the scene name. - * - * @param string $newGameSceneName The name of the new game scene. - * @return TitleScene $this - */ - public function setNewGameSceneIndexBySceneName(string $newGameSceneName): self - { - $this->menu->getItemByIndex(0)->setCallback(function () use ($newGameSceneName) { - loadScene($newGameSceneName); - }); - - return $this; - } - - /** - * Sets the screen dimensions. - * - * @param int|null $width The width of the screen. - * @param int|null $height The height of the screen. - * @return void - */ - public function setScreenDimensions(?int $width = null, ?int $height = null): void - { - $this->screenWidth = $width; - $this->screenHeight = $height; - } - - /** - * Adds menu items to the menu. - * - * @param MenuItemInterface ...$item The menu items to add. - * @return $this - */ - public function addMenuItems(MenuItemInterface ...$item): self - { - $lastItemIndex = $this->menu->getItems()->count() - 1; - $quitItem = $this->menu->getItemByIndex($lastItemIndex); - $this->menu->removeItemByIndex($lastItemIndex); - - foreach ($item as $menuItem) { - $this->menu->addItem($menuItem); + /** + * Set the title of the menu. + * @param string $title The title of the menu. + * @return void + */ + public function setMenuTitle(string $title): void + { + $this->menu->setTitle($title); } - $this->menu->addItem($quitItem); - return $this; - } - - /** - * Sets the font name of the title text. - * - * @param FontName|string $fontName The font name of the title text. - * @return $this - * @throws Exception - */ - public function setTitleFont(FontName|string $fontName): self - { - $this->titleText->setFontName($fontName instanceof FontName ? $fontName->value : $fontName); - return $this; - } + /** + * Sets the default border pack that will be used to render the menu borders. + * + * @param BorderPackInterface $borderPack The border pack to use. + * @return $this + */ + public function setBorderPack(BorderPackInterface $borderPack): self + { + $this->menu->setBorderPack($borderPack); + return $this; + } + + /** + * Sets the index of the new game scene. + * + * @param int $newGameSceneIndex The index of the new game scene. + * @return TitleScene $this + */ + public function setNewGameSceneIndex(int $newGameSceneIndex): self + { + $this->menu->getItemByIndex(0)->setCallback(function () use ($newGameSceneIndex) { + loadScene(max($newGameSceneIndex, 1)); + }); + + return $this; + } + + /** + * Sets the index of the new game scene by the scene name. + * + * @param string $newGameSceneName The name of the new game scene. + * @return TitleScene $this + */ + public function setNewGameSceneIndexBySceneName(string $newGameSceneName): self + { + $this->menu->getItemByIndex(0)->setCallback(function () use ($newGameSceneName) { + loadScene($newGameSceneName); + }); + + return $this; + } + + /** + * Sets the screen dimensions. + * + * @param int|null $width The width of the screen. + * @param int|null $height The height of the screen. + * @return void + */ + public function setScreenDimensions(?int $width = null, ?int $height = null): void + { + $this->screenWidth = $width; + $this->screenHeight = $height; + } + + /** + * Adds menu items to the menu. + * + * @param MenuItemInterface ...$item The menu items to add. + * @return $this + */ + public function addMenuItems(MenuItemInterface ...$item): self + { + $lastItemIndex = $this->menu->getItems()->count() - 1; + $quitItem = $this->menu->getItemByIndex($lastItemIndex); + $this->menu->removeItemByIndex($lastItemIndex); + + foreach ($item as $menuItem) { + $this->menu->addItem($menuItem); + } + + $this->menu->addItem($quitItem); + return $this; + } } \ No newline at end of file diff --git a/src/Core/Sprite.php b/src/Core/Sprite.php index 10430a1..d594794 100644 --- a/src/Core/Sprite.php +++ b/src/Core/Sprite.php @@ -17,11 +17,11 @@ class Sprite implements Serializable /** * Sprite constructor. Creates a new sprite. * - * @param Texture2D $texture The texture of the sprite. + * @param Texture $texture The texture of the sprite. * @param Rect|array{x: int, y: int, width: int, height: int} $rect The rectangle of the sprite. * @param Vector2 $pivot The pivot of the sprite. */ - public function __construct(protected Texture2D $texture, Rect|array $rect, protected Vector2 $pivot = new Vector2(0, 0)) + public function __construct(protected Texture $texture, Rect|array $rect, protected Vector2 $pivot = new Vector2(0, 0)) { $this->rect = is_array($rect) ? Rect::fromArray($rect) : $rect; } @@ -29,9 +29,9 @@ public function __construct(protected Texture2D $texture, Rect|array $rect, prot /** * Returns the texture of the sprite. * - * @return Texture2D The texture of the sprite. + * @return Texture The texture of the sprite. */ - public function getTexture(): Texture2D + public function getTexture(): Texture { return $this->texture; } @@ -39,10 +39,10 @@ public function getTexture(): Texture2D /** * Sets the texture of the sprite. * - * @param Texture2D $texture The texture to set. + * @param Texture $texture The texture to set. * @return void */ - public function setTexture(Texture2D $texture): void + public function setTexture(Texture $texture): void { $this->texture = $texture; } @@ -151,7 +151,7 @@ public function unserialize(string $data): void } /** - * @return array{texture: Texture2D, rect: Rect, pivot: Vector2} + * @return array{texture: Texture, rect: Rect, pivot: Vector2} */ public function __serialize(): array { @@ -159,7 +159,7 @@ public function __serialize(): array } /** - * @param array{texture: Texture2D, rect: Rect, pivot: Vector2} $data + * @param array{texture: Texture, rect: Rect, pivot: Vector2} $data */ public function __unserialize(array $data): void { diff --git a/src/Core/Texture2D.php b/src/Core/Texture.php similarity index 98% rename from src/Core/Texture2D.php rename to src/Core/Texture.php index b95334e..79bdae2 100644 --- a/src/Core/Texture2D.php +++ b/src/Core/Texture.php @@ -13,7 +13,7 @@ * * @package Sendama\Engine\Core */ -class Texture2D implements Stringable +class Texture implements Stringable { use DimensionTrait; @@ -31,7 +31,7 @@ class Texture2D implements Stringable private string $path; /** - * Creates a new instance of the Texture2D class. + * Creates a new instance of the Texture class. * * @param string $path The path to the image file. * @param int $width The width of the texture. diff --git a/src/Game.php b/src/Game.php index fc45ee2..d57ebb5 100644 --- a/src/Game.php +++ b/src/Game.php @@ -747,11 +747,12 @@ public function addScenes(SceneInterface ...$scenes): self /** * @param string ...$paths * @return $this + * @throws SceneNotFoundException */ public function loadScenes(string ...$paths): self { foreach ($paths as $path) { - $canonicalPath = Path::join(Path::getCurrentWorkingDirectory(), $path); + $canonicalPath = Path::join(Path::getWorkingDirectoryAssetsPath(), $path); $this->sceneManager->loadSceneFromFile($canonicalPath); } return $this; diff --git a/src/IO/Console/Console.php b/src/IO/Console/Console.php index 1885389..403e31c 100644 --- a/src/IO/Console/Console.php +++ b/src/IO/Console/Console.php @@ -205,8 +205,8 @@ public static function setSize(int $width, int $height): void */ public static function getSize(): Rect { - $width = passthru("tput cols") ?: throw new Exception('Failed to get terminal width.'); - $height = passthru("tput lines") ?: throw new Exception('Failed to get terminal height.'); + $width = (int)trim(shell_exec("tput cols")) ?: throw new Exception('Failed to get terminal width.'); + $height = (int)trim(shell_exec("tput lines")) ?: throw new Exception('Failed to get terminal height.'); return new Rect(new Vector2(1, 1), new Vector2($width, $height)); } diff --git a/src/Metadata/ComponentMetadata.php b/src/Metadata/ComponentMetadata.php new file mode 100644 index 0000000..0812b8e --- /dev/null +++ b/src/Metadata/ComponentMetadata.php @@ -0,0 +1,43 @@ +class = $data['class'] ?? ''; + $instance->properties = $data['properties'] ?? new \stdClass(); + return $instance; + } + + /** + * Converts the instance to an array. + * + * @return array{class: string, properties: null|object} The array representation of the instance. + */ + public function toArray(): array + { + return [ + 'class' => $this->class, + 'properties' => $this->properties, + ]; + } +} \ No newline at end of file diff --git a/src/Metadata/GameObjectMetadata.php b/src/Metadata/GameObjectMetadata.php new file mode 100644 index 0000000..dc85b0a --- /dev/null +++ b/src/Metadata/GameObjectMetadata.php @@ -0,0 +1,63 @@ +name = $data['name'] ?? ''; + $instance->tag = $data['tag'] ?? ''; + $instance->position = Vector2Metadata::fromArray($data['position'] ?? ['x' => 0, 'y' => 0]); + $instance->rotation = Vector2Metadata::fromArray($data['rotation'] ?? ['x' => 0, 'y' => 0]); + $instance->scale = Vector2Metadata::fromArray($data['scale'] ?? ['x' => 1, 'y' => 1]); + $instance->sprite = SpriteMetadata::fromArray($data['sprite'] ?? []); + $instance->components = array_map( + fn($componentData) => ComponentMetadata::fromArray($componentData), + $data['components'] ?? [] + ); + + return $instance; + } + + public function toArray(): array + { + return [ + "type" => GameObject::class, + "name" => $this->name, + "tag" => $this->tag, + "position" => $this->position->toArray(), + "rotation" => $this->rotation->toArray(), + "scale" => $this->scale->toArray(), + "sprite" => [ + "texture" => [ + "path" => "Textures/player", + "position" => ["x" => 0, "y" => 0], + "size" => ["x" => 1, "y" => 5] + ] + ], + "components" => array_map(fn($component) => $component->toArray(), $this->components) + ]; + } +} \ No newline at end of file diff --git a/src/Metadata/Interfaces/SceneObjectMetadataInterface.php b/src/Metadata/Interfaces/SceneObjectMetadataInterface.php new file mode 100644 index 0000000..8d23719 --- /dev/null +++ b/src/Metadata/Interfaces/SceneObjectMetadataInterface.php @@ -0,0 +1,15 @@ +} $data The sprite metadata as an array. + * @return self The created SpriteMetadata instance. + * @throws InvalidArgumentException If required keys are missing or invalid. + */ + public static function fromArray(array $data): self + { + $metadata = new self(); + + // Validate that the required 'texture' key exists in the data array + if (!isset($data['texture']) || !is_array($data['texture'])) { + throw new InvalidArgumentException("The 'texture' key is required and must be an array."); + } + + $metadata->texture = TextureMetadata::fromArray($data['texture']); + return $metadata; + } + + /** + * @return array{texture: array} The sprite metadata as an array. + */ + public function toArray(): array + { + return [ + 'texture' => $this->texture->toArray(), + ]; + } +} \ No newline at end of file diff --git a/src/Metadata/TextureMetadata.php b/src/Metadata/TextureMetadata.php new file mode 100644 index 0000000..82dbf64 --- /dev/null +++ b/src/Metadata/TextureMetadata.php @@ -0,0 +1,54 @@ +path = $data['path']; + $metadata->position = Vector2Metadata::fromArray($data['position']); + $metadata->size = Vector2Metadata::fromArray($data['size']); + return $metadata; + } + + /** + * Convert TextureMetadata to an associative array. + * + * @return array{path: string, position: array, size: array} + */ + public function toArray(): array + { + return [ + 'path' => $this->path, + 'position' => $this->position->toArray(), + 'size' => $this->size->toArray(), + ]; + } +} \ No newline at end of file diff --git a/src/Metadata/UIElementMetadata.php b/src/Metadata/UIElementMetadata.php new file mode 100644 index 0000000..51719d9 --- /dev/null +++ b/src/Metadata/UIElementMetadata.php @@ -0,0 +1,43 @@ +name = $data['name'] ?? throw new InvalidArgumentException('Name is required for UIElementMetadata'); + $metadata->position = Vector2Metadata::fromArray($data['position'] ?? []); + $metadata->size = Vector2Metadata::fromArray($data['size'] ?? []); + return $metadata; + } + + /** + * Converts the UIElementMetadata instance to an array. + * + * @return array{name: string, position: object, size: object} The array representation of the UIElementMetadata instance. + */ + public function toArray(): array + { + return [ + 'name' => $this->name, + 'position' => $this->position->toArray(), + 'size' => $this->size->toArray(), + ]; + } +} \ No newline at end of file diff --git a/src/Metadata/Vector2Metadata.php b/src/Metadata/Vector2Metadata.php new file mode 100644 index 0000000..ea95294 --- /dev/null +++ b/src/Metadata/Vector2Metadata.php @@ -0,0 +1,42 @@ +x = $data['x'] ?? 0; + $instance->y = $data['y'] ?? 0; + + return $instance; + } + + /** + * Converts the Vector2Metadata instance to an associative array. + * + * @return array{x: int, y: int} Associative array with keys 'x' and 'y'. + */ + public function toArray(): array + { + return [ + 'x' => $this->x, + 'y' => $this->y, + ]; + } +} \ No newline at end of file diff --git a/src/Physics/CharacterController.php b/src/Physics/CharacterController.php index 52470ac..f539f95 100644 --- a/src/Physics/CharacterController.php +++ b/src/Physics/CharacterController.php @@ -10,6 +10,7 @@ use Sendama\Engine\Events\Interfaces\ObserverInterface; use Sendama\Engine\Events\Interfaces\StaticObserverInterface; use Sendama\Engine\Physics\Interfaces\CollisionInterface; +use Sendama\Engine\Physics\Strategies\SimpleCollisionDetectionStrategy; /** * The class CharacterController. It allows you to do movement constrained by collisions without having to deal with a @@ -23,151 +24,145 @@ */ class CharacterController extends Collider implements ObservableInterface { - /** - * The observers. - * - * @var ItemList - */ - protected ItemList $observers; - - /** - * @var ItemList - */ - protected ItemList $staticObservers; - - /** - * @var array> The previous collisions. - */ - private array $previousCollisions = []; - - public function onStart(): void - { - /** @var ItemList $observers */ - $observers = new ItemList(ObserverInterface::class); - $this->observers = $observers; - - /** @var ItemList $staticObservers */ - $staticObservers = new ItemList(StaticObserverInterface::class); - $this->staticObservers = $staticObservers; - } - - /** - * Moves the character. - * - * @param Vector2 $motion The motion. - * @return void - */ - public function move(Vector2 $motion): void - { - $collisions = $this->physics?->checkCollisions($this, $motion); - $canMove = true; - - // If there are collisions, resolve them. - foreach ($collisions ?? [] as $collision) { - if ($collision->getContact(0)?->getSeparation()) { - $this->resolveCollision($collision); - } + /** + * The observers. + * + * @var ItemList + */ + protected ItemList $observers; + + /** + * @var ItemList + */ + protected ItemList $staticObservers; + + /** + * @var array> The previous collisions. + */ + private array $previousCollisions = []; + + public function onStart(): void + { + $this->collisionDetectionStrategy = new SimpleCollisionDetectionStrategy($this); + + /** @var ItemList $observers */ + $observers = new ItemList(ObserverInterface::class); + $this->observers = $observers; + + /** @var ItemList $staticObservers */ + $staticObservers = new ItemList(StaticObserverInterface::class); + $this->staticObservers = $staticObservers; } - // If there are no collisions, move the character. - $this->getTransform()->translate($motion); - } - - /** - * Resolves the collision. - * - * @param CollisionInterface $collision The collision. - * @return void - */ - private function resolveCollision(CollisionInterface $collision): void - { - // TODO: Implement collision resolution. - $methodName = match (true) { - $this->previousCollisionsIncludes($collision) => "onCollisionStay", - default => "onCollisionEnter" - }; - - $collision - ->getContact(0) - ?->getThisCollider() - ->getGameObject() - ->broadcast($methodName, ['collision' => $collision]); - $collision - ->getContact(0) - ?->getOtherCollider() - ->getGameObject() - ->broadcast($methodName, ['collision' => $collision]); - - Debug::log("Collision for {$collision->getGameObject()->getName()} at " . $collision->getContact(0)?->getPoint()); - } - - /** - * @inheritDoc - */ - public function addObservers(string|StaticObserverInterface|ObserverInterface ...$observers): void - { - foreach ($observers as $observer) { - if (is_object($observer)) { - if (get_class($observer) === ObserverInterface::class) { - $this->observers->add($observer); + /** + * Moves the character. + * + * @param Vector2 $motion The motion. + * @return void + */ + public function move(Vector2 $motion): void + { + $collisions = $this->physics?->checkCollisions($this, $motion); + $canMove = true; + + // If there are collisions, resolve them. + foreach ($collisions ?? [] as $collision) { + if ($collision->getContact(0)?->getSeparation()) { + $this->resolveCollision($collision); + } } - if (get_class($observer) === StaticObserverInterface::class) { - $this->staticObservers->add($observer); - } - } + // If there are no collisions, move the character. + $this->getTransform()->translate($motion); + } + + /** + * Resolves the collision. + * + * @param CollisionInterface $collision The collision. + * @return void + */ + private function resolveCollision(CollisionInterface $collision): void + { + // TODO: Implement collision resolution. + $methodName = match (true) { + $this->previousCollisionsIncludes($collision) => "onCollisionStay", + default => "onCollisionEnter" + }; + + $collision->getContact(0)?->getThisCollider()->getGameObject()->broadcast($methodName, ['collision' => $collision]); + $collision->getContact(0)?->getOtherCollider()->getGameObject()->broadcast($methodName, ['collision' => $collision]); + + Debug::log("Collision for {$collision->getGameObject()->getName()} at " . $collision->getContact(0)?->getPoint()); } - } - - /** - * @inheritDoc - */ - public function removeObservers(string|StaticObserverInterface|ObserverInterface|null ...$observers): void - { - foreach ($observers as $observer) { - if (is_object($observer)) { - if (get_class($observer) === ObserverInterface::class) { - $this->observers->remove($observer); - } - if (get_class($observer) === StaticObserverInterface::class) { - $this->staticObservers->remove($observer); + /** + * Checks if the previous collisions includes the collision. + * + * @param CollisionInterface $collision The collision. + * @return bool + */ + private function previousCollisionsIncludes(CollisionInterface $collision): bool + { + foreach ($this->previousCollisions as $previousCollision) { + if ($previousCollision->getContact(0)?->getOtherCollider() === $collision->getContact(0)?->getOtherCollider()) { + return true; + } } - } - } - } - - /** - * @inheritDoc - */ - public function notify(EventInterface $event): void - { - /** @var ObserverInterface $observer */ - foreach ($this->observers as $observer) { - $observer->onNotify($this, $event); + return false; } - /** @var StaticObserverInterface $staticObserver */ - foreach ($this->staticObservers as $staticObserver) { - $staticObserver::onNotify($this, $event); + /** + * @inheritDoc + */ + public function addObservers(string|StaticObserverInterface|ObserverInterface ...$observers): void + { + foreach ($observers as $observer) { + if (is_object($observer)) { + if (get_class($observer) === ObserverInterface::class) { + $this->observers->add($observer); + } + + if (get_class($observer) === StaticObserverInterface::class) { + $this->staticObservers->add($observer); + } + } + } } - } - - /** - * Checks if the previous collisions includes the collision. - * - * @param CollisionInterface $collision The collision. - * @return bool - */ - private function previousCollisionsIncludes(CollisionInterface $collision): bool - { - foreach ($this->previousCollisions as $previousCollision) { - if ($previousCollision->getContact(0)?->getOtherCollider() === $collision->getContact(0)?->getOtherCollider()) { - return true; - } + + /** + * @inheritDoc + */ + public function removeObservers(string|StaticObserverInterface|ObserverInterface|null ...$observers): void + { + foreach ($observers as $observer) { + if (is_object($observer)) { + if (get_class($observer) === ObserverInterface::class) { + $this->observers->remove($observer); + } + + if (get_class($observer) === StaticObserverInterface::class) { + $this->staticObservers->remove($observer); + } + } + + } } - return false; - } + /** + * @inheritDoc + */ + public function notify(EventInterface $event): void + { + /** @var ObserverInterface $observer */ + foreach ($this->observers as $observer) { + $observer->onNotify($this, $event); + } + + /** @var StaticObserverInterface $staticObserver */ + foreach ($this->staticObservers as $staticObserver) { + $staticObserver::onNotify($this, $event); + } + } } \ No newline at end of file diff --git a/src/Physics/Physics.php b/src/Physics/Physics.php index 9052bc9..5148332 100644 --- a/src/Physics/Physics.php +++ b/src/Physics/Physics.php @@ -19,225 +19,219 @@ */ final class Physics implements SingletonInterface, SimulatorInterface { - /** - * @var self|null - */ - protected static ?self $instance = null; - /** - * @var float The gravity applied to all rigid bodies in the scene. - */ - protected float $gravity = 9.81; - /** - * @var ItemList> The colliders in the physics engine. - */ - protected ItemList $colliders; - /** - * @var Grid The static collision map. This is used for detecting collisions with static objects in the scene. - */ - protected Grid $staticCollisionMap; - /** - * @var Grid The world. This is used for detecting collisions with the world bounds. - */ - protected Grid $world; - /** - * @var int The width of the world. This is used for detecting collisions with the world bounds. - */ - protected int $worldWidth; - /** - * @var int The height of the world. This is used for detecting collisions with the world bounds. - */ - protected int $worldHeight; - /** - * @var array> The collisions in the physics engine. - */ - protected array $collisions = []; - - /** - * Physics constructor. - */ - private function __construct() - { - // This is a private constructor to prevent users from creating a new instance of the Physics class. - $this->worldWidth = MAX_SCREEN_WIDTH * 3; - $this->worldHeight = MAX_SCREEN_WIDTH * 3; - - $this->init(); - } - - /** - * @inheritDoc - * - * @return self - */ - public static function getInstance(): self - { - if (self::$instance === null) { - self::$instance = new self(); + /** + * @var self|null + */ + protected static ?self $instance = null; + /** + * @var float The gravity applied to all rigid bodies in the scene. + */ + protected float $gravity = 9.81; + /** + * @var ItemList> The colliders in the physics engine. + */ + protected ItemList $colliders; + /** + * @var Grid The static collision map. This is used for detecting collisions with static objects in the scene. + */ + protected Grid $staticCollisionMap; + /** + * @var Grid The world. This is used for detecting collisions with the world bounds. + */ + protected Grid $world; + /** + * @var int The width of the world. This is used for detecting collisions with the world bounds. + */ + protected int $worldWidth; + /** + * @var int The height of the world. This is used for detecting collisions with the world bounds. + */ + protected int $worldHeight; + /** + * @var array> The collisions in the physics engine. + */ + protected array $collisions = []; + + /** + * Physics constructor. + */ + private function __construct() + { + // This is a private constructor to prevent users from creating a new instance of the Physics class. + $this->worldWidth = MAX_SCREEN_WIDTH * 3; + $this->worldHeight = MAX_SCREEN_WIDTH * 3; + + $this->init(); } - return self::$instance; - } - - /** - * Simulates the physics in the Scene. - */ - public function simulate(): void - { - // This method will be called once per frame to simulate the physics in the scene. - # Get a clean slate of the physics world - $this->clearWorld(); - - # Update the physics world - $this->updateWorld(); - - # Record the collisions - $this->recordCollisions(); - - # Dispatch the collisions - $this->dispatchCollisions(); - } - - /** - * Adds a collider to the physics engine. - * - * @param ColliderInterface $collider The collider to add. - */ - public function addCollider(ColliderInterface $collider): void - { - $this->colliders->add($collider); - } - - /** - * Removes a collider from the physics engine. - * - * @param ColliderInterface $collider The collider to remove. - * @return void - */ - public function removeCollider(ColliderInterface $collider): void - { - if (! $this->colliders->remove($collider) ) { - Debug::warn("Failed to remove collider from physics engine."); + /** + * Initializes the physics engine. + * + * @return void + */ + public function init(): void + { + /** @var ItemList> $colliders */ + $colliders = new ItemList(ColliderInterface::class); + + $this->colliders = $colliders; + + $this->clearWorld(); } - } - - /** - * Checks for collisions between the given collider and all other colliders in the physics engine. - * - * @param ColliderInterface $collider The collider to check for collisions. - * @param Vector2 $motion The motion of the collider. - * @return array> The collisions found. - */ - public function checkCollisions(ColliderInterface $collider, Vector2 $motion): array - { - $collisions = []; - - foreach ($this->colliders as $otherCollider) { - if ($collider->isTouching($otherCollider)) { - $collisions[] = new Collision($otherCollider, [ - new ContactPoint( - Vector2::sum($collider->getTransform()->getPosition(), $motion), - $collider, - $otherCollider - ) - ]); - } + + /** + * @return void + */ + protected function clearWorld(): void + { + $this->world = new Grid($this->worldWidth, $this->worldHeight); + $this->staticCollisionMap = new Grid($this->worldWidth, $this->worldWidth); } - return $collisions; - } - - /** - * Loads the static collision map. - * - * @param Grid $grid The grid to load. - * @return void - */ - public function loadStaticCollisionMap(Grid $grid): void - { - $this->staticCollisionMap = $grid; - } - - /** - * Checks if the given position is touching a static object. - * - * @param Vector2 $position The position to check. - * @return bool Whether the given position is touching a static object or not. - */ - public function isTouchingStaticObject(Vector2 $position): bool - { - [$x, $y] = [$position->getX(), $position->getY()]; - - return $this->staticCollisionMap->get($x, $y) === 1; - } - - /** - * Checks if the given position is touching a dynamic object. - * - * @param Vector2 $position The position to check. - * @return bool Whether the given position is touching a dynamic object or not. - */ - public function isTouchingDynamicObject(Vector2 $position): bool - { - foreach ($this->colliders as $collider) { - if ($collider->getTransform()->getPosition() === $position) { - return true; - } + /** + * @inheritDoc + * + * @return self + */ + public static function getInstance(): self + { + if (self::$instance === null) { + self::$instance = new self(); + } + + return self::$instance; } - return false; - } - - /** - * @return void - */ - protected function clearWorld(): void - { - $this->world = new Grid($this->worldWidth, $this->worldHeight); - $this->staticCollisionMap = new Grid($this->worldWidth, $this->worldWidth); - } - - /** - * Updates the physics world. - * - * @return void - */ - protected function updateWorld(): void - { - // TODO: Implement updateWorld() method. - } - - /** - * Records the collisions in the physics engine. - * - * @return void - */ - protected function recordCollisions(): void - { - // TODO: Implement recordCollisions() method. - } - - /** - * Dispatches the collisions to the colliders. - * - * @return void - */ - protected function dispatchCollisions(): void - { - // TODO: Implement dispatchCollisions() method. - } - - /** - * Initializes the physics engine. - * - * @return void - */ - public function init(): void - { - /** @var ItemList> $colliders */ - $colliders = new ItemList(ColliderInterface::class); - - $this->colliders = $colliders; - - $this->clearWorld(); - } + /** + * Simulates the physics in the Scene. + */ + public function simulate(): void + { + // This method will be called once per frame to simulate the physics in the scene. + # Get a clean slate of the physics world + $this->clearWorld(); + + # Update the physics world + $this->updateWorld(); + + # Record the collisions + $this->recordCollisions(); + + # Dispatch the collisions + $this->dispatchCollisions(); + } + + /** + * Updates the physics world. + * + * @return void + */ + protected function updateWorld(): void + { + // TODO: Implement updateWorld() method. + } + + /** + * Records the collisions in the physics engine. + * + * @return void + */ + protected function recordCollisions(): void + { + // TODO: Implement recordCollisions() method. + } + + /** + * Dispatches the collisions to the colliders. + * + * @return void + */ + protected function dispatchCollisions(): void + { + // TODO: Implement dispatchCollisions() method. + } + + /** + * Adds a collider to the physics engine. + * + * @param ColliderInterface $collider The collider to add. + */ + public function addCollider(ColliderInterface $collider): void + { + $this->colliders->add($collider); + } + + /** + * Removes a collider from the physics engine. + * + * @param ColliderInterface $collider The collider to remove. + * @return void + */ + public function removeCollider(ColliderInterface $collider): void + { + if (!$this->colliders->remove($collider)) { + Debug::warn("Failed to remove collider from physics engine."); + } + } + + /** + * Checks for collisions between the given collider and all other colliders in the physics engine. + * + * @param ColliderInterface $collider The collider to check for collisions. + * @param Vector2 $motion The motion of the collider. + * @return array> The collisions found. + */ + public function checkCollisions(ColliderInterface $collider, Vector2 $motion): array + { + $collisions = []; + + foreach ($this->colliders as $otherCollider) { + if ($collider->isTouching($otherCollider)) { + $collisions[] = new Collision($otherCollider, [new ContactPoint(Vector2::sum($collider->getTransform()->getPosition(), $motion), $collider, $otherCollider)]); + } + } + + return $collisions; + } + + /** + * Loads the static collision map. + * + * @param Grid $grid The grid to load. + * @return void + */ + public function loadStaticCollisionMap(Grid $grid): void + { + $this->staticCollisionMap = $grid; + } + + /** + * Checks if the given position is touching a static object. + * + * @param Vector2 $position The position to check. + * @return bool Whether the given position is touching a static object or not. + */ + public function isTouchingStaticObject(Vector2 $position): bool + { + [$x, $y] = [$position->getX(), $position->getY()]; + + return $this->staticCollisionMap->get($x, $y) === 1; + } + + /** + * Checks if the given position is touching a dynamic object. + * + * @param Vector2 $position The position to check. + * @return bool Whether the given position is touching a dynamic object or not. + */ + public function isTouchingDynamicObject(Vector2 $position): bool + { + foreach ($this->colliders as $collider) { + if ($collider->getTransform()->getPosition() === $position) { + return true; + } + } + + return false; + } } \ No newline at end of file diff --git a/src/UI/Menus/Menu.php b/src/UI/Menus/Menu.php index 63bc7eb..085541d 100644 --- a/src/UI/Menus/Menu.php +++ b/src/UI/Menus/Menu.php @@ -22,6 +22,7 @@ use Sendama\Engine\UI\Menus\Interfaces\MenuInterface; use Sendama\Engine\UI\Menus\Interfaces\MenuItemInterface; use Sendama\Engine\UI\Windows\BorderPack; +use Sendama\Engine\UI\Windows\Interfaces\BorderPackInterface; use Sendama\Engine\UI\Windows\Window; /** @@ -94,7 +95,7 @@ class Menu implements MenuInterface * @param bool $canNavigate Whether the menu can navigate or not. */ public function __construct( - protected string $title, + string $title, protected string $description = '', protected Rect $dimensions = new Rect( new Vector2(0, 0), @@ -114,7 +115,7 @@ public function __construct( $this->observers = new ItemList(ObserverInterface::class); $this->staticObservers = new ItemList(StaticObserverInterface::class); - $this->window = new Window($this->title, $this->description, $this->dimensions->getPosition(), $this->dimensions->getWidth(), $this->dimensions->getHeight(), $borderPack); + $this->window = new Window($title, $this->description, $this->dimensions->getPosition(), $this->dimensions->getWidth(), $this->dimensions->getHeight(), $borderPack); $this->awake(); } @@ -138,10 +139,10 @@ public function awake(): void /** * Sets the border of the menu. * - * @param BorderPack $borderPack The border pack. + * @param BorderPackInterface $borderPack The border pack. * @return void */ - public function setBorderPack(BorderPack $borderPack): void + public function setBorderPack(BorderPackInterface $borderPack): void { $this->window->setBorderPack($borderPack); } @@ -436,7 +437,7 @@ public function getName(): string */ public function getTitle(): string { - return $this->title; + return $this->window->getTitle(); } /** @@ -613,7 +614,7 @@ public function setActiveColor(Color $color): void */ public function getArgs(): array { - return ['title' => $this->title, 'description' => $this->description, 'dimensions' => $this->dimensions, 'items' => $this->items, 'cursor' => $this->cursor, 'active_color' => $this->activeColor,]; + return ['title' => $this->getTitle(), 'description' => $this->description, 'dimensions' => $this->dimensions, 'items' => $this->items, 'cursor' => $this->cursor, 'active_color' => $this->activeColor,]; } /** @@ -629,7 +630,7 @@ public function setName(string $name): void */ public function setTitle(string $title): void { - $this->title = $title; + $this->window->setTitle($title); } /** diff --git a/tests/Unit/Core/SpriteTest.php b/tests/Unit/Core/SpriteTest.php index 4d49427..b8f22a3 100644 --- a/tests/Unit/Core/SpriteTest.php +++ b/tests/Unit/Core/SpriteTest.php @@ -1,7 +1,5 @@ workingDirectory = dirname(__DIR__, 2); $this->texturePath = Path::join($this->workingDirectory, 'Mocks/Textures/test.texture'); }); it ('can be created', function () { - $texture = new Texture2D($this->texturePath); - expect($texture)->toBeInstanceOf(Texture2D::class); + $texture = new Texture($this->texturePath); + expect($texture)->toBeInstanceOf(Texture::class); }); it('can manipulate the texture dimensions', function() { - $texture = new Texture2D($this->texturePath); + $texture = new Texture($this->texturePath); $width = 32; $height = 32; @@ -30,7 +30,7 @@ }); it('can control the texture coloer', function() { - $texture = new Texture2D($this->texturePath); + $texture = new Texture($this->texturePath); $color = Color::RED; $texture->setColor($color); @@ -40,7 +40,7 @@ }); it('can manipulate texture pixels', function() { - $texture = new Texture2D($this->texturePath); + $texture = new Texture($this->texturePath); $x = 1; $y = 0; $expectedPixel = '<'; @@ -57,7 +57,7 @@ }); it('can be converted to a string', function() { - $texture = new Texture2D($this->texturePath); + $texture = new Texture($this->texturePath); $expectedString = file_get_contents($this->texturePath); expect(strval($texture))