diff --git a/assets/schema/gameobject.schema.json b/assets/schema/gameobject.schema.json new file mode 100644 index 0000000..83170d0 --- /dev/null +++ b/assets/schema/gameobject.schema.json @@ -0,0 +1,48 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "tag": { + "type": "string" + }, + "position": { + "$ref": "vector2.schema.json" + }, + "rotation": { + "$ref": "vector2.schema.json" + }, + "scale": { + "$ref": "vector2.schema.json" + }, + "components": { + "type": "array", + "items": { + "type": "object", + "properties": { + "type": { + "type": "string" + }, + "properties": { + "type": "object", + "additionalProperties": true, + "description": "A dictionary of component-specific properties.", + "example": { + "health": 100, + "speed": 5.0, + "isActive": true + } + } + } + } + }, + "children": { + "type": "array", + "items": { + "$ref": "gameobject.schema.json" + } + } + } +} \ No newline at end of file diff --git a/assets/schema/prefab.schema.json b/assets/schema/prefab.schema.json new file mode 100644 index 0000000..0538291 --- /dev/null +++ b/assets/schema/prefab.schema.json @@ -0,0 +1,24 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "array", + "items" : { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "fruit": { + "type": "string" + }, + "wikipedia": { + "type": "string" + }, + "color": { + "type": "string" + }, + "rating": { + "type": "number" + } + } + } +} \ No newline at end of file diff --git a/assets/schema/scene.schema.json b/assets/schema/scene.schema.json new file mode 100644 index 0000000..a410821 --- /dev/null +++ b/assets/schema/scene.schema.json @@ -0,0 +1,21 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "width": { + "type": "number" + }, + "height": { + "type": "number" + }, + "hierarchy": { + "type": "array", + "items": { + "$ref": "gameobject.schema.json" + } + } + } +} \ No newline at end of file diff --git a/assets/schema/vector2.schema.json b/assets/schema/vector2.schema.json new file mode 100644 index 0000000..9959d3e --- /dev/null +++ b/assets/schema/vector2.schema.json @@ -0,0 +1,13 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "name": "Vector2", + "type": "object", + "properties": { + "x": { + "type": "number" + }, + "y": { + "type": "number" + } + } +} \ No newline at end of file diff --git a/composer.json b/composer.json index d9f5898..2e19187 100644 --- a/composer.json +++ b/composer.json @@ -30,7 +30,9 @@ "amasiye/figlet": "^1.2", "vlucas/phpdotenv": "^5.6", "league/climate": "^3.8", - "ext-pcntl": "*" + "ext-pcntl": "*", + "swaggest/json-schema": "^0.12.43", + "symfony/polyfill-php83": "^1.33" }, "config": { "allow-plugins": { diff --git a/composer.lock b/composer.lock index c9deb01..05a6882 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "d321fecddb0ff981b106c127d06a3533", + "content-hash": "e355cc5c432fb93dcff70e25bb7e81a4", "packages": [ { "name": "amasiye/figlet", @@ -239,6 +239,54 @@ }, "time": "2024-11-18T09:09:55+00:00" }, + { + "name": "phplang/scope-exit", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/phplang/scope-exit.git", + "reference": "239b73abe89f9414aa85a7ca075ec9445629192b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phplang/scope-exit/zipball/239b73abe89f9414aa85a7ca075ec9445629192b", + "reference": "239b73abe89f9414aa85a7ca075ec9445629192b", + "shasum": "" + }, + "require-dev": { + "phpunit/phpunit": "*" + }, + "type": "library", + "autoload": { + "psr-4": { + "PhpLang\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD" + ], + "authors": [ + { + "name": "Sara Golemon", + "email": "pollita@php.net", + "homepage": "https://twitter.com/SaraMG", + "role": "Developer" + } + ], + "description": "Emulation of SCOPE_EXIT construct from C++", + "homepage": "https://github.com/phplang/scope-exit", + "keywords": [ + "cleanup", + "exit", + "scope" + ], + "support": { + "issues": "https://github.com/phplang/scope-exit/issues", + "source": "https://github.com/phplang/scope-exit/tree/master" + }, + "time": "2016-09-17T00:15:18+00:00" + }, { "name": "phpoption/phpoption", "version": "1.9.5", @@ -472,6 +520,101 @@ }, "time": "2020-12-15T21:32:01+00:00" }, + { + "name": "swaggest/json-diff", + "version": "v3.12.1", + "source": { + "type": "git", + "url": "https://github.com/swaggest/json-diff.git", + "reference": "7ebc4eab95bcc73916433964c266588d09b35052" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/swaggest/json-diff/zipball/7ebc4eab95bcc73916433964c266588d09b35052", + "reference": "7ebc4eab95bcc73916433964c266588d09b35052", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": ">=7.1" + }, + "require-dev": { + "phperf/phpunit": "4.8.37" + }, + "type": "library", + "autoload": { + "psr-4": { + "Swaggest\\JsonDiff\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Viacheslav Poturaev", + "email": "vearutop@gmail.com" + } + ], + "description": "JSON diff/rearrange/patch/pointer library for PHP", + "support": { + "issues": "https://github.com/swaggest/json-diff/issues", + "source": "https://github.com/swaggest/json-diff/tree/v3.12.1" + }, + "time": "2025-03-10T08:22:10+00:00" + }, + { + "name": "swaggest/json-schema", + "version": "v0.12.43", + "source": { + "type": "git", + "url": "https://github.com/swaggest/php-json-schema.git", + "reference": "1f3a77a382c5d273a0f1fe34be3b8af4060a88cd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/swaggest/php-json-schema/zipball/1f3a77a382c5d273a0f1fe34be3b8af4060a88cd", + "reference": "1f3a77a382c5d273a0f1fe34be3b8af4060a88cd", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": ">=7.1", + "phplang/scope-exit": "^1.0", + "swaggest/json-diff": "^3.8.2", + "symfony/polyfill-mbstring": "^1.19" + }, + "require-dev": { + "phperf/phpunit": "4.8.37" + }, + "suggest": { + "ext-mbstring": "For better performance" + }, + "type": "library", + "autoload": { + "psr-4": { + "Swaggest\\JsonSchema\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Viacheslav Poturaev", + "email": "vearutop@gmail.com" + } + ], + "description": "High definition PHP structures with JSON-schema based validation", + "support": { + "email": "vearutop@gmail.com", + "issues": "https://github.com/swaggest/php-json-schema/issues", + "source": "https://github.com/swaggest/php-json-schema/tree/v0.12.43" + }, + "time": "2024-12-22T21:18:27+00:00" + }, { "name": "symfony/console", "version": "v7.4.4", @@ -1056,6 +1199,86 @@ ], "time": "2025-01-02T08:10:11+00:00" }, + { + "name": "symfony/polyfill-php83", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php83.git", + "reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/17f6f9a6b1735c0f163024d959f700cfbc5155e5", + "reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php83\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.3+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php83/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-07-08T02:45:35+00:00" + }, { "name": "symfony/service-contracts", "version": "v3.6.1", diff --git a/examples/collector/assets/splash.texture b/examples/collector/assets/splash.texture index dbd9233..6786613 100644 --- a/examples/collector/assets/splash.texture +++ b/examples/collector/assets/splash.texture @@ -1,16 +1,8 @@ - ______ __ __ ___ -| || | | / _] -| || | | / [_ -|_| |_|| _ || _] - | | | | || [_ - | | | | || | - |__| |__|__||_____| - - __ ___ _ _ ___ __ ______ ___ ____ - / ] / \ | | | | / _] / ] | / \ | \ - / / | || | | | / [_ / /| || || D ) - / / | O || |___ | |___ | _]/ / |_| |_|| O || / -/ \_ | || || || [_/ \_ | | | || \ -\ || || || || \ | | | | || . \ - \____| \___/ |_____||_____||_____|\____| |__| \___/ |__|\_| - +:'######:::'#######::'##:::::::'##:::::::'########::'######::'########::'#######::'########:: +'##... ##:'##.... ##: ##::::::: ##::::::: ##.....::'##... ##:... ##..::'##.... ##: ##.... ##: + ##:::..:: ##:::: ##: ##::::::: ##::::::: ##::::::: ##:::..::::: ##:::: ##:::: ##: ##:::: ##: + ##::::::: ##:::: ##: ##::::::: ##::::::: ######::: ##:::::::::: ##:::: ##:::: ##: ########:: + ##::::::: ##:::: ##: ##::::::: ##::::::: ##...:::: ##:::::::::: ##:::: ##:::: ##: ##.. ##::: + ##::: ##: ##:::: ##: ##::::::: ##::::::: ##::::::: ##::: ##:::: ##:::: ##:::: ##: ##::. ##:: +. ######::. #######:: ########: ########: ########:. ######::::: ##::::. #######:: ##:::. ##: +:......::::.......:::........::........::........:::......::::::..::::::.......:::..:::::..:: diff --git a/examples/pong/.gitignore b/examples/pong/.gitignore new file mode 100644 index 0000000..1f0c0e6 --- /dev/null +++ b/examples/pong/.gitignore @@ -0,0 +1,7 @@ +/vendor/ +/.idea/ +/.vscode/ +.env +*.log +logs/* +*.sedata \ No newline at end of file diff --git a/examples/pong/assets/splash.texture b/examples/pong/assets/splash.texture new file mode 100644 index 0000000..4021861 --- /dev/null +++ b/examples/pong/assets/splash.texture @@ -0,0 +1,8 @@ +d8888b. .d88b. d8b db d888b +88 `8D .8P Y8. 888o 88 88' Y8b +88oodD' 88 88 88V8o 88 88 +88~~~ 88 88 88 V8o88 88 ooo +88 `8b d8' 88 V888 88. ~8~ +88 `Y88P' VP V8P Y888P + + diff --git a/examples/pong/composer.json b/examples/pong/composer.json new file mode 100644 index 0000000..7c5e334 --- /dev/null +++ b/examples/pong/composer.json @@ -0,0 +1,33 @@ +{ + "name": "sendama-engine/pong", + "version": "1.0.0", + "description": "A new 2D ASCII terminal game.", + "type": "project", + "require": { + "php": "^8.3", + "sendamaphp/engine": "*" + }, + "require-dev": { + "phpstan/phpstan": "^1.10", + "pestphp/pest": "^4.3" + }, + "autoload": { + "psr-4": { + "Sendama\\Pong\\": "assets/" + } + }, + "repositories": [ + { + "type": "path", + "url": "../", + "options": { + "symlink": true + } + } + ], + "config": { + "allow-plugins": { + "pestphp/pest-plugin": true + } + } +} \ No newline at end of file diff --git a/examples/pong/composer.lock b/examples/pong/composer.lock new file mode 100644 index 0000000..0a940b8 --- /dev/null +++ b/examples/pong/composer.lock @@ -0,0 +1,4563 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "ef457958aba18fc5f67701369aca652f", + "packages": [ + { + "name": "amasiye/figlet", + "version": "1.2.0", + "source": { + "type": "git", + "url": "https://github.com/amasiye/figlet.git", + "reference": "5a420aa817e960da37fd0a7043abf78c213d63e7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amasiye/figlet/zipball/5a420aa817e960da37fd0a7043abf78c213d63e7", + "reference": "5a420aa817e960da37fd0a7043abf78c213d63e7", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/console": "^7.0" + }, + "require-dev": { + "pestphp/pest": "^2.34" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "psr-4": { + "Amasiye\\Figlet\\": "src/Figlet" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Povilas Susinskas", + "email": "povilassusinskas@gmail.com" + }, + { + "name": "Andrew Masiye", + "email": "amasiye313@gmail.com" + } + ], + "description": "Figlet text generator - PHP", + "keywords": [ + "ascii", + "figlet", + "github", + "php", + "text" + ], + "support": { + "source": "https://github.com/amasiye/figlet/tree/1.2.0" + }, + "time": "2024-05-20T14:35:35+00:00" + }, + { + "name": "assegaiphp/collections", + "version": "0.3.4", + "source": { + "type": "git", + "url": "https://github.com/assegaiphp/collections.git", + "reference": "eec7ee56dd024b3f4d9ea11eb3b200c009c6ceda" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/assegaiphp/collections/zipball/eec7ee56dd024b3f4d9ea11eb3b200c009c6ceda", + "reference": "eec7ee56dd024b3f4d9ea11eb3b200c009c6ceda", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "pestphp/pest": "^2.6" + }, + "type": "library", + "autoload": { + "files": [ + "src/Util/Functions.php" + ], + "psr-4": { + "Assegai\\Collections\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Andrew Masiye", + "email": "andrew.masiye@assegaiphp.com" + } + ], + "description": "The assegaiphp/collections package is a powerful tool for creating and managing groups of related objects in your AssegaiPHP projects. With this library, you can easily organize and manipulate data in a variety of ways, such as arrays, lists, sets, and maps. The package offers a wide range of methods for adding, removing, and manipulating items, as well as sorting, searching, and filtering your collections. It is fully compatible with PHP 7.x and above and is designed to be lightweight and easy to use. Whether you're building a web application, a CLI tool, or a standalone script, assegaiphp/collections is the perfect choice for working with data. With its simple and intuitive interface, you can easily start working with collections in your project right away.", + "support": { + "issues": "https://github.com/assegaiphp/collections/issues", + "source": "https://github.com/assegaiphp/collections/tree/0.3.4" + }, + "time": "2025-03-08T22:03:19+00:00" + }, + { + "name": "graham-campbell/result-type", + "version": "v1.1.4", + "source": { + "type": "git", + "url": "https://github.com/GrahamCampbell/Result-Type.git", + "reference": "e01f4a821471308ba86aa202fed6698b6b695e3b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/e01f4a821471308ba86aa202fed6698b6b695e3b", + "reference": "e01f4a821471308ba86aa202fed6698b6b695e3b", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "phpoption/phpoption": "^1.9.5" + }, + "require-dev": { + "phpunit/phpunit": "^8.5.41 || ^9.6.22 || ^10.5.45 || ^11.5.7" + }, + "type": "library", + "autoload": { + "psr-4": { + "GrahamCampbell\\ResultType\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + } + ], + "description": "An Implementation Of The Result Type", + "keywords": [ + "Graham Campbell", + "GrahamCampbell", + "Result Type", + "Result-Type", + "result" + ], + "support": { + "issues": "https://github.com/GrahamCampbell/Result-Type/issues", + "source": "https://github.com/GrahamCampbell/Result-Type/tree/v1.1.4" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/graham-campbell/result-type", + "type": "tidelift" + } + ], + "time": "2025-12-27T19:43:20+00:00" + }, + { + "name": "league/climate", + "version": "3.10.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/climate.git", + "reference": "237f70e1032b16d32ff3f65dcda68706911e1c74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/climate/zipball/237f70e1032b16d32ff3f65dcda68706911e1c74", + "reference": "237f70e1032b16d32ff3f65dcda68706911e1c74", + "shasum": "" + }, + "require": { + "php": "^7.3 || ^8.0", + "psr/log": "^1.0 || ^2.0 || ^3.0", + "seld/cli-prompt": "^1.0" + }, + "require-dev": { + "mikey179/vfsstream": "^1.6.12", + "mockery/mockery": "^1.6.12", + "phpunit/phpunit": "^9.5.10", + "squizlabs/php_codesniffer": "^3.10" + }, + "suggest": { + "ext-mbstring": "If ext-mbstring is not available you MUST install symfony/polyfill-mbstring" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\CLImate\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Joe Tannenbaum", + "email": "hey@joe.codes", + "homepage": "http://joe.codes/", + "role": "Developer" + }, + { + "name": "Craig Duncan", + "email": "git@duncanc.co.uk", + "homepage": "https://github.com/duncan3dc", + "role": "Developer" + } + ], + "description": "PHP's best friend for the terminal. CLImate allows you to easily output colored text, special formats, and more.", + "keywords": [ + "cli", + "colors", + "command", + "php", + "terminal" + ], + "support": { + "issues": "https://github.com/thephpleague/climate/issues", + "source": "https://github.com/thephpleague/climate/tree/3.10.0" + }, + "time": "2024-11-18T09:09:55+00:00" + }, + { + "name": "phpoption/phpoption", + "version": "1.9.5", + "source": { + "type": "git", + "url": "https://github.com/schmittjoh/php-option.git", + "reference": "75365b91986c2405cf5e1e012c5595cd487a98be" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/75365b91986c2405cf5e1e012c5595cd487a98be", + "reference": "75365b91986c2405cf5e1e012c5595cd487a98be", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.44 || ^9.6.25 || ^10.5.53 || ^11.5.34" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + }, + "branch-alias": { + "dev-master": "1.9-dev" + } + }, + "autoload": { + "psr-4": { + "PhpOption\\": "src/PhpOption/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Johannes M. Schmitt", + "email": "schmittjoh@gmail.com", + "homepage": "https://github.com/schmittjoh" + }, + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + } + ], + "description": "Option Type for PHP", + "keywords": [ + "language", + "option", + "php", + "type" + ], + "support": { + "issues": "https://github.com/schmittjoh/php-option/issues", + "source": "https://github.com/schmittjoh/php-option/tree/1.9.5" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpoption/phpoption", + "type": "tidelift" + } + ], + "time": "2025-12-27T19:41:33+00:00" + }, + { + "name": "psr/container", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "shasum": "" + }, + "require": { + "php": ">=7.4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/2.0.2" + }, + "time": "2021-11-05T16:47:00+00:00" + }, + { + "name": "psr/log", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/3.0.2" + }, + "time": "2024-09-11T13:17:53+00:00" + }, + { + "name": "seld/cli-prompt", + "version": "1.0.4", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/cli-prompt.git", + "reference": "b8dfcf02094b8c03b40322c229493bb2884423c5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/cli-prompt/zipball/b8dfcf02094b8c03b40322c229493bb2884423c5", + "reference": "b8dfcf02094b8c03b40322c229493bb2884423c5", + "shasum": "" + }, + "require": { + "php": ">=5.3" + }, + "require-dev": { + "phpstan/phpstan": "^0.12.63" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Seld\\CliPrompt\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be" + } + ], + "description": "Allows you to prompt for user input on the command line, and optionally hide the characters they type", + "keywords": [ + "cli", + "console", + "hidden", + "input", + "prompt" + ], + "support": { + "issues": "https://github.com/Seldaek/cli-prompt/issues", + "source": "https://github.com/Seldaek/cli-prompt/tree/1.0.4" + }, + "time": "2020-12-15T21:32:01+00:00" + }, + { + "name": "sendamaphp/engine", + "version": "0.4.7", + "source": { + "type": "git", + "url": "https://github.com/sendamaphp/engine.git", + "reference": "0a14468e0bf956a880bc9a16e4aa571ccd84208a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sendamaphp/engine/zipball/0a14468e0bf956a880bc9a16e4aa571ccd84208a", + "reference": "0a14468e0bf956a880bc9a16e4aa571ccd84208a", + "shasum": "" + }, + "require": { + "amasiye/figlet": "^1.2", + "assegaiphp/collections": "^0.3.2", + "ext-pcntl": "*", + "league/climate": "^3.8", + "php": "^8.3", + "vlucas/phpdotenv": "^5.6" + }, + "require-dev": { + "pestphp/pest": "^4.3", + "phpstan/phpstan": "^1.10" + }, + "type": "library", + "autoload": { + "files": [ + "src/Util/Constants.php", + "src/Util/Functions.php" + ], + "psr-4": { + "Sendama\\Engine\\": "src/", + "Sendama\\Engine\\Mocks\\": "tests/Mocks/", + "Sendama\\Examples\\Blasters\\": "examples/blasters/assets/", + "Sendama\\Examples\\Collector\\": "examples/collector/assets/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Andrew Masiye", + "email": "amasiye313@gmail.com" + } + ], + "description": "A simple game engine for making terminal/console games. Lovingly written in pure PHP.", + "support": { + "issues": "https://github.com/sendamaphp/engine/issues", + "source": "https://github.com/sendamaphp/engine/tree/0.4.7" + }, + "time": "2026-02-04T02:19:17+00:00" + }, + { + "name": "symfony/console", + "version": "v7.4.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "41e38717ac1dd7a46b6bda7d6a82af2d98a78894" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/41e38717ac1dd7a46b6bda7d6a82af2d98a78894", + "reference": "41e38717ac1dd7a46b6bda7d6a82af2d98a78894", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/string": "^7.2|^8.0" + }, + "conflict": { + "symfony/dependency-injection": "<6.4", + "symfony/dotenv": "<6.4", + "symfony/event-dispatcher": "<6.4", + "symfony/lock": "<6.4", + "symfony/process": "<6.4" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/event-dispatcher": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/lock": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/stopwatch": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Eases the creation of beautiful and testable command line interfaces", + "homepage": "https://symfony.com", + "keywords": [ + "cli", + "command-line", + "console", + "terminal" + ], + "support": { + "source": "https://github.com/symfony/console/tree/v7.4.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-01-13T11:36:38+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:21:43+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "provide": { + "ext-ctype": "*" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/380872130d3a5dd3ace2f4010d95125fde5d5c70", + "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's grapheme_* functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "grapheme", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-06-27T09:58:17+00:00" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "3833d7255cc303546435cb650316bff708a1c75c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c", + "reference": "3833d7255cc303546435cb650316bff708a1c75c", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's Normalizer class and related functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493", + "shasum": "" + }, + "require": { + "ext-iconv": "*", + "php": ">=7.2" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-12-23T08:48:59+00:00" + }, + { + "name": "symfony/polyfill-php80", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/0cc9dd0f17f61d8131e7df6b84bd344899fe2608", + "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php80\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php80/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-01-02T08:10:11+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v3.6.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/45112560a3ba2d715666a509a0bc9521d10b6c43", + "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/v3.6.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-07-15T11:30:57+00:00" + }, + { + "name": "symfony/string", + "version": "v8.0.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/string.git", + "reference": "758b372d6882506821ed666032e43020c4f57194" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/string/zipball/758b372d6882506821ed666032e43020c4f57194", + "reference": "758b372d6882506821ed666032e43020c4f57194", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "symfony/polyfill-ctype": "^1.8", + "symfony/polyfill-intl-grapheme": "^1.33", + "symfony/polyfill-intl-normalizer": "^1.0", + "symfony/polyfill-mbstring": "^1.0" + }, + "conflict": { + "symfony/translation-contracts": "<2.5" + }, + "require-dev": { + "symfony/emoji": "^7.4|^8.0", + "symfony/http-client": "^7.4|^8.0", + "symfony/intl": "^7.4|^8.0", + "symfony/translation-contracts": "^2.5|^3.0", + "symfony/var-exporter": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\String\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", + "homepage": "https://symfony.com", + "keywords": [ + "grapheme", + "i18n", + "string", + "unicode", + "utf-8", + "utf8" + ], + "support": { + "source": "https://github.com/symfony/string/tree/v8.0.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-01-12T12:37:40+00:00" + }, + { + "name": "vlucas/phpdotenv", + "version": "v5.6.3", + "source": { + "type": "git", + "url": "https://github.com/vlucas/phpdotenv.git", + "reference": "955e7815d677a3eaa7075231212f2110983adecc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/955e7815d677a3eaa7075231212f2110983adecc", + "reference": "955e7815d677a3eaa7075231212f2110983adecc", + "shasum": "" + }, + "require": { + "ext-pcre": "*", + "graham-campbell/result-type": "^1.1.4", + "php": "^7.2.5 || ^8.0", + "phpoption/phpoption": "^1.9.5", + "symfony/polyfill-ctype": "^1.26", + "symfony/polyfill-mbstring": "^1.26", + "symfony/polyfill-php80": "^1.26" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "ext-filter": "*", + "phpunit/phpunit": "^8.5.34 || ^9.6.13 || ^10.4.2" + }, + "suggest": { + "ext-filter": "Required to use the boolean validator." + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + }, + "branch-alias": { + "dev-master": "5.6-dev" + } + }, + "autoload": { + "psr-4": { + "Dotenv\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Vance Lucas", + "email": "vance@vancelucas.com", + "homepage": "https://github.com/vlucas" + } + ], + "description": "Loads environment variables from `.env` to `getenv()`, `$_ENV` and `$_SERVER` automagically.", + "keywords": [ + "dotenv", + "env", + "environment" + ], + "support": { + "issues": "https://github.com/vlucas/phpdotenv/issues", + "source": "https://github.com/vlucas/phpdotenv/tree/v5.6.3" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/vlucas/phpdotenv", + "type": "tidelift" + } + ], + "time": "2025-12-27T19:49:13+00:00" + } + ], + "packages-dev": [ + { + "name": "brianium/paratest", + "version": "v7.16.1", + "source": { + "type": "git", + "url": "https://github.com/paratestphp/paratest.git", + "reference": "f0fdfd8e654e0d38bc2ba756a6cabe7be287390b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paratestphp/paratest/zipball/f0fdfd8e654e0d38bc2ba756a6cabe7be287390b", + "reference": "f0fdfd8e654e0d38bc2ba756a6cabe7be287390b", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-pcre": "*", + "ext-reflection": "*", + "ext-simplexml": "*", + "fidry/cpu-core-counter": "^1.3.0", + "jean85/pretty-package-versions": "^2.1.1", + "php": "~8.3.0 || ~8.4.0 || ~8.5.0", + "phpunit/php-code-coverage": "^12.5.2", + "phpunit/php-file-iterator": "^6", + "phpunit/php-timer": "^8", + "phpunit/phpunit": "^12.5.4", + "sebastian/environment": "^8.0.3", + "symfony/console": "^7.3.4 || ^8.0.0", + "symfony/process": "^7.3.4 || ^8.0.0" + }, + "require-dev": { + "doctrine/coding-standard": "^14.0.0", + "ext-pcntl": "*", + "ext-pcov": "*", + "ext-posix": "*", + "phpstan/phpstan": "^2.1.33", + "phpstan/phpstan-deprecation-rules": "^2.0.3", + "phpstan/phpstan-phpunit": "^2.0.11", + "phpstan/phpstan-strict-rules": "^2.0.7", + "symfony/filesystem": "^7.3.2 || ^8.0.0" + }, + "bin": [ + "bin/paratest", + "bin/paratest_for_phpstorm" + ], + "type": "library", + "autoload": { + "psr-4": { + "ParaTest\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Brian Scaturro", + "email": "scaturrob@gmail.com", + "role": "Developer" + }, + { + "name": "Filippo Tessarotto", + "email": "zoeslam@gmail.com", + "role": "Developer" + } + ], + "description": "Parallel testing for PHP", + "homepage": "https://github.com/paratestphp/paratest", + "keywords": [ + "concurrent", + "parallel", + "phpunit", + "testing" + ], + "support": { + "issues": "https://github.com/paratestphp/paratest/issues", + "source": "https://github.com/paratestphp/paratest/tree/v7.16.1" + }, + "funding": [ + { + "url": "https://github.com/sponsors/Slamdunk", + "type": "github" + }, + { + "url": "https://paypal.me/filippotessarotto", + "type": "paypal" + } + ], + "time": "2026-01-08T07:23:06+00:00" + }, + { + "name": "doctrine/deprecations", + "version": "1.1.5", + "source": { + "type": "git", + "url": "https://github.com/doctrine/deprecations.git", + "reference": "459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/deprecations/zipball/459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38", + "reference": "459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "phpunit/phpunit": "<=7.5 || >=13" + }, + "require-dev": { + "doctrine/coding-standard": "^9 || ^12 || ^13", + "phpstan/phpstan": "1.4.10 || 2.1.11", + "phpstan/phpstan-phpunit": "^1.0 || ^2", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6 || ^10.5 || ^11.5 || ^12", + "psr/log": "^1 || ^2 || ^3" + }, + "suggest": { + "psr/log": "Allows logging deprecations via PSR-3 logger implementation" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Deprecations\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A small layer on top of trigger_error(E_USER_DEPRECATED) or PSR-3 logging with options to disable all deprecations or selectively for packages.", + "homepage": "https://www.doctrine-project.org/", + "support": { + "issues": "https://github.com/doctrine/deprecations/issues", + "source": "https://github.com/doctrine/deprecations/tree/1.1.5" + }, + "time": "2025-04-07T20:06:18+00:00" + }, + { + "name": "fidry/cpu-core-counter", + "version": "1.3.0", + "source": { + "type": "git", + "url": "https://github.com/theofidry/cpu-core-counter.git", + "reference": "db9508f7b1474469d9d3c53b86f817e344732678" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/db9508f7b1474469d9d3c53b86f817e344732678", + "reference": "db9508f7b1474469d9d3c53b86f817e344732678", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "fidry/makefile": "^0.2.0", + "fidry/php-cs-fixer-config": "^1.1.2", + "phpstan/extension-installer": "^1.2.0", + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-deprecation-rules": "^2.0.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^8.5.31 || ^9.5.26", + "webmozarts/strict-phpunit": "^7.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Fidry\\CpuCoreCounter\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Théo FIDRY", + "email": "theo.fidry@gmail.com" + } + ], + "description": "Tiny utility to get the number of CPU cores.", + "keywords": [ + "CPU", + "core" + ], + "support": { + "issues": "https://github.com/theofidry/cpu-core-counter/issues", + "source": "https://github.com/theofidry/cpu-core-counter/tree/1.3.0" + }, + "funding": [ + { + "url": "https://github.com/theofidry", + "type": "github" + } + ], + "time": "2025-08-14T07:29:31+00:00" + }, + { + "name": "filp/whoops", + "version": "2.18.4", + "source": { + "type": "git", + "url": "https://github.com/filp/whoops.git", + "reference": "d2102955e48b9fd9ab24280a7ad12ed552752c4d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/filp/whoops/zipball/d2102955e48b9fd9ab24280a7ad12ed552752c4d", + "reference": "d2102955e48b9fd9ab24280a7ad12ed552752c4d", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0", + "psr/log": "^1.0.1 || ^2.0 || ^3.0" + }, + "require-dev": { + "mockery/mockery": "^1.0", + "phpunit/phpunit": "^7.5.20 || ^8.5.8 || ^9.3.3", + "symfony/var-dumper": "^4.0 || ^5.0" + }, + "suggest": { + "symfony/var-dumper": "Pretty print complex values better with var-dumper available", + "whoops/soap": "Formats errors as SOAP responses" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.7-dev" + } + }, + "autoload": { + "psr-4": { + "Whoops\\": "src/Whoops/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Filipe Dobreira", + "homepage": "https://github.com/filp", + "role": "Developer" + } + ], + "description": "php error handling for cool kids", + "homepage": "https://filp.github.io/whoops/", + "keywords": [ + "error", + "exception", + "handling", + "library", + "throwable", + "whoops" + ], + "support": { + "issues": "https://github.com/filp/whoops/issues", + "source": "https://github.com/filp/whoops/tree/2.18.4" + }, + "funding": [ + { + "url": "https://github.com/denis-sokolov", + "type": "github" + } + ], + "time": "2025-08-08T12:00:00+00:00" + }, + { + "name": "jean85/pretty-package-versions", + "version": "2.1.1", + "source": { + "type": "git", + "url": "https://github.com/Jean85/pretty-package-versions.git", + "reference": "4d7aa5dab42e2a76d99559706022885de0e18e1a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Jean85/pretty-package-versions/zipball/4d7aa5dab42e2a76d99559706022885de0e18e1a", + "reference": "4d7aa5dab42e2a76d99559706022885de0e18e1a", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2.1.0", + "php": "^7.4|^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.2", + "jean85/composer-provided-replaced-stub-package": "^1.0", + "phpstan/phpstan": "^2.0", + "phpunit/phpunit": "^7.5|^8.5|^9.6", + "rector/rector": "^2.0", + "vimeo/psalm": "^4.3 || ^5.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Jean85\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Alessandro Lai", + "email": "alessandro.lai85@gmail.com" + } + ], + "description": "A library to get pretty versions strings of installed dependencies", + "keywords": [ + "composer", + "package", + "release", + "versions" + ], + "support": { + "issues": "https://github.com/Jean85/pretty-package-versions/issues", + "source": "https://github.com/Jean85/pretty-package-versions/tree/2.1.1" + }, + "time": "2025-03-19T14:43:43+00:00" + }, + { + "name": "myclabs/deep-copy", + "version": "1.13.4", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3 <3.2.2" + }, + "require-dev": { + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpspec/prophecy": "^1.10", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" + }, + "type": "library", + "autoload": { + "files": [ + "src/DeepCopy/deep_copy.php" + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2025-08-01T08:46:24+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v5.7.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-json": "*", + "ext-tokenizer": "*", + "php": ">=7.4" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v5.7.0" + }, + "time": "2025-12-06T11:56:16+00:00" + }, + { + "name": "nunomaduro/collision", + "version": "v8.8.3", + "source": { + "type": "git", + "url": "https://github.com/nunomaduro/collision.git", + "reference": "1dc9e88d105699d0fee8bb18890f41b274f6b4c4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nunomaduro/collision/zipball/1dc9e88d105699d0fee8bb18890f41b274f6b4c4", + "reference": "1dc9e88d105699d0fee8bb18890f41b274f6b4c4", + "shasum": "" + }, + "require": { + "filp/whoops": "^2.18.1", + "nunomaduro/termwind": "^2.3.1", + "php": "^8.2.0", + "symfony/console": "^7.3.0" + }, + "conflict": { + "laravel/framework": "<11.44.2 || >=13.0.0", + "phpunit/phpunit": "<11.5.15 || >=13.0.0" + }, + "require-dev": { + "brianium/paratest": "^7.8.3", + "larastan/larastan": "^3.4.2", + "laravel/framework": "^11.44.2 || ^12.18", + "laravel/pint": "^1.22.1", + "laravel/sail": "^1.43.1", + "laravel/sanctum": "^4.1.1", + "laravel/tinker": "^2.10.1", + "orchestra/testbench-core": "^9.12.0 || ^10.4", + "pestphp/pest": "^3.8.2 || ^4.0.0", + "sebastian/environment": "^7.2.1 || ^8.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "NunoMaduro\\Collision\\Adapters\\Laravel\\CollisionServiceProvider" + ] + }, + "branch-alias": { + "dev-8.x": "8.x-dev" + } + }, + "autoload": { + "files": [ + "./src/Adapters/Phpunit/Autoload.php" + ], + "psr-4": { + "NunoMaduro\\Collision\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "Cli error handling for console/command-line PHP applications.", + "keywords": [ + "artisan", + "cli", + "command-line", + "console", + "dev", + "error", + "handling", + "laravel", + "laravel-zero", + "php", + "symfony" + ], + "support": { + "issues": "https://github.com/nunomaduro/collision/issues", + "source": "https://github.com/nunomaduro/collision" + }, + "funding": [ + { + "url": "https://www.paypal.com/paypalme/enunomaduro", + "type": "custom" + }, + { + "url": "https://github.com/nunomaduro", + "type": "github" + }, + { + "url": "https://www.patreon.com/nunomaduro", + "type": "patreon" + } + ], + "time": "2025-11-20T02:55:25+00:00" + }, + { + "name": "nunomaduro/termwind", + "version": "v2.3.3", + "source": { + "type": "git", + "url": "https://github.com/nunomaduro/termwind.git", + "reference": "6fb2a640ff502caace8e05fd7be3b503a7e1c017" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nunomaduro/termwind/zipball/6fb2a640ff502caace8e05fd7be3b503a7e1c017", + "reference": "6fb2a640ff502caace8e05fd7be3b503a7e1c017", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": "^8.2", + "symfony/console": "^7.3.6" + }, + "require-dev": { + "illuminate/console": "^11.46.1", + "laravel/pint": "^1.25.1", + "mockery/mockery": "^1.6.12", + "pestphp/pest": "^2.36.0 || ^3.8.4 || ^4.1.3", + "phpstan/phpstan": "^1.12.32", + "phpstan/phpstan-strict-rules": "^1.6.2", + "symfony/var-dumper": "^7.3.5", + "thecodingmachine/phpstan-strict-rules": "^1.0.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Termwind\\Laravel\\TermwindServiceProvider" + ] + }, + "branch-alias": { + "dev-2.x": "2.x-dev" + } + }, + "autoload": { + "files": [ + "src/Functions.php" + ], + "psr-4": { + "Termwind\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "Its like Tailwind CSS, but for the console.", + "keywords": [ + "cli", + "console", + "css", + "package", + "php", + "style" + ], + "support": { + "issues": "https://github.com/nunomaduro/termwind/issues", + "source": "https://github.com/nunomaduro/termwind/tree/v2.3.3" + }, + "funding": [ + { + "url": "https://www.paypal.com/paypalme/enunomaduro", + "type": "custom" + }, + { + "url": "https://github.com/nunomaduro", + "type": "github" + }, + { + "url": "https://github.com/xiCO2k", + "type": "github" + } + ], + "time": "2025-11-20T02:34:59+00:00" + }, + { + "name": "pestphp/pest", + "version": "v4.3.2", + "source": { + "type": "git", + "url": "https://github.com/pestphp/pest.git", + "reference": "3a4329ddc7a2b67c19fca8342a668b39be3ae398" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pestphp/pest/zipball/3a4329ddc7a2b67c19fca8342a668b39be3ae398", + "reference": "3a4329ddc7a2b67c19fca8342a668b39be3ae398", + "shasum": "" + }, + "require": { + "brianium/paratest": "^7.16.1", + "nunomaduro/collision": "^8.8.3", + "nunomaduro/termwind": "^2.3.3", + "pestphp/pest-plugin": "^4.0.0", + "pestphp/pest-plugin-arch": "^4.0.0", + "pestphp/pest-plugin-mutate": "^4.0.1", + "pestphp/pest-plugin-profanity": "^4.2.1", + "php": "^8.3.0", + "phpunit/phpunit": "^12.5.8", + "symfony/process": "^7.4.4|^8.0.0" + }, + "conflict": { + "filp/whoops": "<2.18.3", + "phpunit/phpunit": ">12.5.8", + "sebastian/exporter": "<7.0.0", + "webmozart/assert": "<1.11.0" + }, + "require-dev": { + "pestphp/pest-dev-tools": "^4.0.0", + "pestphp/pest-plugin-browser": "^4.2.1", + "pestphp/pest-plugin-type-coverage": "^4.0.3", + "psy/psysh": "^0.12.18" + }, + "bin": [ + "bin/pest" + ], + "type": "library", + "extra": { + "pest": { + "plugins": [ + "Pest\\Mutate\\Plugins\\Mutate", + "Pest\\Plugins\\Configuration", + "Pest\\Plugins\\Bail", + "Pest\\Plugins\\Cache", + "Pest\\Plugins\\Coverage", + "Pest\\Plugins\\Init", + "Pest\\Plugins\\Environment", + "Pest\\Plugins\\Help", + "Pest\\Plugins\\Memory", + "Pest\\Plugins\\Only", + "Pest\\Plugins\\Printer", + "Pest\\Plugins\\ProcessIsolation", + "Pest\\Plugins\\Profile", + "Pest\\Plugins\\Retry", + "Pest\\Plugins\\Snapshot", + "Pest\\Plugins\\Verbose", + "Pest\\Plugins\\Version", + "Pest\\Plugins\\Shard", + "Pest\\Plugins\\Parallel" + ] + }, + "phpstan": { + "includes": [ + "extension.neon" + ] + } + }, + "autoload": { + "files": [ + "src/Functions.php", + "src/Pest.php" + ], + "psr-4": { + "Pest\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "The elegant PHP Testing Framework.", + "keywords": [ + "framework", + "pest", + "php", + "test", + "testing", + "unit" + ], + "support": { + "issues": "https://github.com/pestphp/pest/issues", + "source": "https://github.com/pestphp/pest/tree/v4.3.2" + }, + "funding": [ + { + "url": "https://www.paypal.com/paypalme/enunomaduro", + "type": "custom" + }, + { + "url": "https://github.com/nunomaduro", + "type": "github" + } + ], + "time": "2026-01-28T01:01:19+00:00" + }, + { + "name": "pestphp/pest-plugin", + "version": "v4.0.0", + "source": { + "type": "git", + "url": "https://github.com/pestphp/pest-plugin.git", + "reference": "9d4b93d7f73d3f9c3189bb22c220fef271cdf568" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pestphp/pest-plugin/zipball/9d4b93d7f73d3f9c3189bb22c220fef271cdf568", + "reference": "9d4b93d7f73d3f9c3189bb22c220fef271cdf568", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^2.0.0", + "composer-runtime-api": "^2.2.2", + "php": "^8.3" + }, + "conflict": { + "pestphp/pest": "<4.0.0" + }, + "require-dev": { + "composer/composer": "^2.8.10", + "pestphp/pest": "^4.0.0", + "pestphp/pest-dev-tools": "^4.0.0" + }, + "type": "composer-plugin", + "extra": { + "class": "Pest\\Plugin\\Manager" + }, + "autoload": { + "psr-4": { + "Pest\\Plugin\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "The Pest plugin manager", + "keywords": [ + "framework", + "manager", + "pest", + "php", + "plugin", + "test", + "testing", + "unit" + ], + "support": { + "source": "https://github.com/pestphp/pest-plugin/tree/v4.0.0" + }, + "funding": [ + { + "url": "https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=66BYDWAT92N6L", + "type": "custom" + }, + { + "url": "https://github.com/nunomaduro", + "type": "github" + }, + { + "url": "https://www.patreon.com/nunomaduro", + "type": "patreon" + } + ], + "time": "2025-08-20T12:35:58+00:00" + }, + { + "name": "pestphp/pest-plugin-arch", + "version": "v4.0.0", + "source": { + "type": "git", + "url": "https://github.com/pestphp/pest-plugin-arch.git", + "reference": "25bb17e37920ccc35cbbcda3b00d596aadf3e58d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pestphp/pest-plugin-arch/zipball/25bb17e37920ccc35cbbcda3b00d596aadf3e58d", + "reference": "25bb17e37920ccc35cbbcda3b00d596aadf3e58d", + "shasum": "" + }, + "require": { + "pestphp/pest-plugin": "^4.0.0", + "php": "^8.3", + "ta-tikoma/phpunit-architecture-test": "^0.8.5" + }, + "require-dev": { + "pestphp/pest": "^4.0.0", + "pestphp/pest-dev-tools": "^4.0.0" + }, + "type": "library", + "extra": { + "pest": { + "plugins": [ + "Pest\\Arch\\Plugin" + ] + } + }, + "autoload": { + "files": [ + "src/Autoload.php" + ], + "psr-4": { + "Pest\\Arch\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "The Arch plugin for Pest PHP.", + "keywords": [ + "arch", + "architecture", + "framework", + "pest", + "php", + "plugin", + "test", + "testing", + "unit" + ], + "support": { + "source": "https://github.com/pestphp/pest-plugin-arch/tree/v4.0.0" + }, + "funding": [ + { + "url": "https://www.paypal.com/paypalme/enunomaduro", + "type": "custom" + }, + { + "url": "https://github.com/nunomaduro", + "type": "github" + } + ], + "time": "2025-08-20T13:10:51+00:00" + }, + { + "name": "pestphp/pest-plugin-mutate", + "version": "v4.0.1", + "source": { + "type": "git", + "url": "https://github.com/pestphp/pest-plugin-mutate.git", + "reference": "d9b32b60b2385e1688a68cc227594738ec26d96c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pestphp/pest-plugin-mutate/zipball/d9b32b60b2385e1688a68cc227594738ec26d96c", + "reference": "d9b32b60b2385e1688a68cc227594738ec26d96c", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^5.6.1", + "pestphp/pest-plugin": "^4.0.0", + "php": "^8.3", + "psr/simple-cache": "^3.0.0" + }, + "require-dev": { + "pestphp/pest": "^4.0.0", + "pestphp/pest-dev-tools": "^4.0.0", + "pestphp/pest-plugin-type-coverage": "^4.0.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Pest\\Mutate\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + }, + { + "name": "Sandro Gehri", + "email": "sandrogehri@gmail.com" + } + ], + "description": "Mutates your code to find untested cases", + "keywords": [ + "framework", + "mutate", + "mutation", + "pest", + "php", + "plugin", + "test", + "testing", + "unit" + ], + "support": { + "source": "https://github.com/pestphp/pest-plugin-mutate/tree/v4.0.1" + }, + "funding": [ + { + "url": "https://www.paypal.com/paypalme/enunomaduro", + "type": "custom" + }, + { + "url": "https://github.com/gehrisandro", + "type": "github" + }, + { + "url": "https://github.com/nunomaduro", + "type": "github" + } + ], + "time": "2025-08-21T20:19:25+00:00" + }, + { + "name": "pestphp/pest-plugin-profanity", + "version": "v4.2.1", + "source": { + "type": "git", + "url": "https://github.com/pestphp/pest-plugin-profanity.git", + "reference": "343cfa6f3564b7e35df0ebb77b7fa97039f72b27" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pestphp/pest-plugin-profanity/zipball/343cfa6f3564b7e35df0ebb77b7fa97039f72b27", + "reference": "343cfa6f3564b7e35df0ebb77b7fa97039f72b27", + "shasum": "" + }, + "require": { + "pestphp/pest-plugin": "^4.0.0", + "php": "^8.3" + }, + "require-dev": { + "faissaloux/pest-plugin-inside": "^1.9", + "pestphp/pest": "^4.0.0", + "pestphp/pest-dev-tools": "^4.0.0" + }, + "type": "library", + "extra": { + "pest": { + "plugins": [ + "Pest\\Profanity\\Plugin" + ] + } + }, + "autoload": { + "psr-4": { + "Pest\\Profanity\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "The Pest Profanity Plugin", + "keywords": [ + "framework", + "pest", + "php", + "plugin", + "profanity", + "test", + "testing", + "unit" + ], + "support": { + "source": "https://github.com/pestphp/pest-plugin-profanity/tree/v4.2.1" + }, + "time": "2025-12-08T00:13:17+00:00" + }, + { + "name": "phar-io/manifest", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "54750ef60c58e43759730615a392c31c80e23176" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", + "reference": "54750ef60c58e43759730615a392c31c80e23176", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-phar": "*", + "ext-xmlwriter": "*", + "phar-io/version": "^3.0.1", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:33:53+00:00" + }, + { + "name": "phar-io/version", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/3.2.1" + }, + "time": "2022-02-21T01:04:05+00:00" + }, + { + "name": "phpdocumentor/reflection-common", + "version": "2.2.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionCommon.git", + "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/1d01c49d4ed62f25aa84a747ad35d5a16924662b", + "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-2.x": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jaap van Otterdijk", + "email": "opensource@ijaap.nl" + } + ], + "description": "Common reflection classes used by phpdocumentor to reflect the code structure", + "homepage": "http://www.phpdoc.org", + "keywords": [ + "FQSEN", + "phpDocumentor", + "phpdoc", + "reflection", + "static analysis" + ], + "support": { + "issues": "https://github.com/phpDocumentor/ReflectionCommon/issues", + "source": "https://github.com/phpDocumentor/ReflectionCommon/tree/2.x" + }, + "time": "2020-06-27T09:03:43+00:00" + }, + { + "name": "phpdocumentor/reflection-docblock", + "version": "6.0.1", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", + "reference": "2f5cbed597cb261d1ea458f3da3a9ad32e670b1e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/2f5cbed597cb261d1ea458f3da3a9ad32e670b1e", + "reference": "2f5cbed597cb261d1ea458f3da3a9ad32e670b1e", + "shasum": "" + }, + "require": { + "doctrine/deprecations": "^1.1", + "ext-filter": "*", + "php": "^7.4 || ^8.0", + "phpdocumentor/reflection-common": "^2.2", + "phpdocumentor/type-resolver": "^2.0", + "phpstan/phpdoc-parser": "^2.0", + "webmozart/assert": "^1.9.1 || ^2" + }, + "require-dev": { + "mockery/mockery": "~1.3.5 || ~1.6.0", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-mockery": "^1.1", + "phpstan/phpstan-webmozart-assert": "^1.2", + "phpunit/phpunit": "^9.5", + "psalm/phar": "^5.26", + "shipmonk/dead-code-detector": "^0.5.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + }, + { + "name": "Jaap van Otterdijk", + "email": "opensource@ijaap.nl" + } + ], + "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", + "support": { + "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", + "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/6.0.1" + }, + "time": "2026-01-20T15:30:42+00:00" + }, + { + "name": "phpdocumentor/type-resolver", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/TypeResolver.git", + "reference": "327a05bbee54120d4786a0dc67aad30226ad4cf9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/327a05bbee54120d4786a0dc67aad30226ad4cf9", + "reference": "327a05bbee54120d4786a0dc67aad30226ad4cf9", + "shasum": "" + }, + "require": { + "doctrine/deprecations": "^1.0", + "php": "^7.4 || ^8.0", + "phpdocumentor/reflection-common": "^2.0", + "phpstan/phpdoc-parser": "^2.0" + }, + "require-dev": { + "ext-tokenizer": "*", + "phpbench/phpbench": "^1.2", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^9.5", + "psalm/phar": "^4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-1.x": "1.x-dev", + "dev-2.x": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + } + ], + "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", + "support": { + "issues": "https://github.com/phpDocumentor/TypeResolver/issues", + "source": "https://github.com/phpDocumentor/TypeResolver/tree/2.0.0" + }, + "time": "2026-01-06T21:53:42+00:00" + }, + { + "name": "phpstan/phpdoc-parser", + "version": "2.3.2", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpdoc-parser.git", + "reference": "a004701b11273a26cd7955a61d67a7f1e525a45a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/a004701b11273a26cd7955a61d67a7f1e525a45a", + "reference": "a004701b11273a26cd7955a61d67a7f1e525a45a", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "doctrine/annotations": "^2.0", + "nikic/php-parser": "^5.3.0", + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^9.6", + "symfony/process": "^5.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "PHPStan\\PhpDocParser\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPDoc parser with support for nullable, intersection and generic types", + "support": { + "issues": "https://github.com/phpstan/phpdoc-parser/issues", + "source": "https://github.com/phpstan/phpdoc-parser/tree/2.3.2" + }, + "time": "2026-01-25T14:56:51+00:00" + }, + { + "name": "phpstan/phpstan", + "version": "1.12.32", + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/2770dcdf5078d0b0d53f94317e06affe88419aa8", + "reference": "2770dcdf5078d0b0d53f94317e06affe88419aa8", + "shasum": "" + }, + "require": { + "php": "^7.2|^8.0" + }, + "conflict": { + "phpstan/phpstan-shim": "*" + }, + "bin": [ + "phpstan", + "phpstan.phar" + ], + "type": "library", + "autoload": { + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPStan - PHP Static Analysis Tool", + "keywords": [ + "dev", + "static analysis" + ], + "support": { + "docs": "https://phpstan.org/user-guide/getting-started", + "forum": "https://github.com/phpstan/phpstan/discussions", + "issues": "https://github.com/phpstan/phpstan/issues", + "security": "https://github.com/phpstan/phpstan/security/policy", + "source": "https://github.com/phpstan/phpstan-src" + }, + "funding": [ + { + "url": "https://github.com/ondrejmirtes", + "type": "github" + }, + { + "url": "https://github.com/phpstan", + "type": "github" + } + ], + "time": "2025-09-30T10:16:31+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "12.5.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "4a9739b51cbcb355f6e95659612f92e282a7077b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/4a9739b51cbcb355f6e95659612f92e282a7077b", + "reference": "4a9739b51cbcb355f6e95659612f92e282a7077b", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-xmlwriter": "*", + "nikic/php-parser": "^5.7.0", + "php": ">=8.3", + "phpunit/php-file-iterator": "^6.0", + "phpunit/php-text-template": "^5.0", + "sebastian/complexity": "^5.0", + "sebastian/environment": "^8.0.3", + "sebastian/lines-of-code": "^4.0", + "sebastian/version": "^6.0", + "theseer/tokenizer": "^2.0.1" + }, + "require-dev": { + "phpunit/phpunit": "^12.5.1" + }, + "suggest": { + "ext-pcov": "PHP extension that provides line coverage", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "12.5.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", + "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/12.5.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-code-coverage", + "type": "tidelift" + } + ], + "time": "2025-12-24T07:03:04+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "6.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "3d1cd096ef6bea4bf2762ba586e35dbd317cbfd5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/3d1cd096ef6bea4bf2762ba586e35dbd317cbfd5", + "reference": "3d1cd096ef6bea4bf2762ba586e35dbd317cbfd5", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", + "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/6.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-file-iterator", + "type": "tidelift" + } + ], + "time": "2026-02-02T14:04:18+00:00" + }, + { + "name": "phpunit/php-invoker", + "version": "6.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "12b54e689b07a25a9b41e57736dfab6ec9ae5406" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/12b54e689b07a25a9b41e57736dfab6ec9ae5406", + "reference": "12b54e689b07a25a9b41e57736dfab6ec9ae5406", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "^12.0" + }, + "suggest": { + "ext-pcntl": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Invoke callables with a timeout", + "homepage": "https://github.com/sebastianbergmann/php-invoker/", + "keywords": [ + "process" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-invoker/issues", + "security": "https://github.com/sebastianbergmann/php-invoker/security/policy", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/6.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:58:58+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "5.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "e1367a453f0eda562eedb4f659e13aa900d66c53" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/e1367a453f0eda562eedb4f659e13aa900d66c53", + "reference": "e1367a453f0eda562eedb4f659e13aa900d66c53", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "security": "https://github.com/sebastianbergmann/php-text-template/security/policy", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/5.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:59:16+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "8.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "f258ce36aa457f3aa3339f9ed4c81fc66dc8c2cc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/f258ce36aa457f3aa3339f9ed4c81fc66dc8c2cc", + "reference": "f258ce36aa457f3aa3339f9ed4c81fc66dc8c2cc", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "8.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "security": "https://github.com/sebastianbergmann/php-timer/security/policy", + "source": "https://github.com/sebastianbergmann/php-timer/tree/8.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:59:38+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "12.5.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "37ddb96c14bfee10304825edbb7e66d341ec6889" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/37ddb96c14bfee10304825edbb7e66d341ec6889", + "reference": "37ddb96c14bfee10304825edbb7e66d341ec6889", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.13.4", + "phar-io/manifest": "^2.0.4", + "phar-io/version": "^3.2.1", + "php": ">=8.3", + "phpunit/php-code-coverage": "^12.5.2", + "phpunit/php-file-iterator": "^6.0.0", + "phpunit/php-invoker": "^6.0.0", + "phpunit/php-text-template": "^5.0.0", + "phpunit/php-timer": "^8.0.0", + "sebastian/cli-parser": "^4.2.0", + "sebastian/comparator": "^7.1.4", + "sebastian/diff": "^7.0.0", + "sebastian/environment": "^8.0.3", + "sebastian/exporter": "^7.0.2", + "sebastian/global-state": "^8.0.2", + "sebastian/object-enumerator": "^7.0.0", + "sebastian/type": "^6.0.3", + "sebastian/version": "^6.0.0", + "staabm/side-effects-detector": "^1.0.5" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "12.5-dev" + } + }, + "autoload": { + "files": [ + "src/Framework/Assert/Functions.php" + ], + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "security": "https://github.com/sebastianbergmann/phpunit/security/policy", + "source": "https://github.com/sebastianbergmann/phpunit/tree/12.5.8" + }, + "funding": [ + { + "url": "https://phpunit.de/sponsors.html", + "type": "custom" + }, + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", + "type": "tidelift" + } + ], + "time": "2026-01-27T06:12:29+00:00" + }, + { + "name": "psr/simple-cache", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/simple-cache.git", + "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/simple-cache/zipball/764e0b3939f5ca87cb904f570ef9be2d78a07865", + "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\SimpleCache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interfaces for simple caching", + "keywords": [ + "cache", + "caching", + "psr", + "psr-16", + "simple-cache" + ], + "support": { + "source": "https://github.com/php-fig/simple-cache/tree/3.0.0" + }, + "time": "2021-10-29T13:26:27+00:00" + }, + { + "name": "sebastian/cli-parser", + "version": "4.2.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "90f41072d220e5c40df6e8635f5dafba2d9d4d04" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/90f41072d220e5c40df6e8635f5dafba2d9d4d04", + "reference": "90f41072d220e5c40df6e8635f5dafba2d9d4d04", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for parsing CLI options", + "homepage": "https://github.com/sebastianbergmann/cli-parser", + "support": { + "issues": "https://github.com/sebastianbergmann/cli-parser/issues", + "security": "https://github.com/sebastianbergmann/cli-parser/security/policy", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/4.2.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/cli-parser", + "type": "tidelift" + } + ], + "time": "2025-09-14T09:36:45+00:00" + }, + { + "name": "sebastian/comparator", + "version": "7.1.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "6a7de5df2e094f9a80b40a522391a7e6022df5f6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/6a7de5df2e094f9a80b40a522391a7e6022df5f6", + "reference": "6a7de5df2e094f9a80b40a522391a7e6022df5f6", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-mbstring": "*", + "php": ">=8.3", + "sebastian/diff": "^7.0", + "sebastian/exporter": "^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^12.2" + }, + "suggest": { + "ext-bcmath": "For comparing BcMath\\Number objects" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "security": "https://github.com/sebastianbergmann/comparator/security/policy", + "source": "https://github.com/sebastianbergmann/comparator/tree/7.1.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/comparator", + "type": "tidelift" + } + ], + "time": "2026-01-24T09:28:48+00:00" + }, + { + "name": "sebastian/complexity", + "version": "5.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "bad4316aba5303d0221f43f8cee37eb58d384bbb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/bad4316aba5303d0221f43f8cee37eb58d384bbb", + "reference": "bad4316aba5303d0221f43f8cee37eb58d384bbb", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^5.0", + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for calculating the complexity of PHP code units", + "homepage": "https://github.com/sebastianbergmann/complexity", + "support": { + "issues": "https://github.com/sebastianbergmann/complexity/issues", + "security": "https://github.com/sebastianbergmann/complexity/security/policy", + "source": "https://github.com/sebastianbergmann/complexity/tree/5.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:55:25+00:00" + }, + { + "name": "sebastian/diff", + "version": "7.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "7ab1ea946c012266ca32390913653d844ecd085f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/7ab1ea946c012266ca32390913653d844ecd085f", + "reference": "7ab1ea946c012266ca32390913653d844ecd085f", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0", + "symfony/process": "^7.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "security": "https://github.com/sebastianbergmann/diff/security/policy", + "source": "https://github.com/sebastianbergmann/diff/tree/7.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:55:46+00:00" + }, + { + "name": "sebastian/environment", + "version": "8.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "24a711b5c916efc6d6e62aa65aa2ec98fef77f68" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/24a711b5c916efc6d6e62aa65aa2ec98fef77f68", + "reference": "24a711b5c916efc6d6e62aa65aa2ec98fef77f68", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "8.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "https://github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "security": "https://github.com/sebastianbergmann/environment/security/policy", + "source": "https://github.com/sebastianbergmann/environment/tree/8.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/environment", + "type": "tidelift" + } + ], + "time": "2025-08-12T14:11:56+00:00" + }, + { + "name": "sebastian/exporter", + "version": "7.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "016951ae10980765e4e7aee491eb288c64e505b7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/016951ae10980765e4e7aee491eb288c64e505b7", + "reference": "016951ae10980765e4e7aee491eb288c64e505b7", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": ">=8.3", + "sebastian/recursion-context": "^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "https://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "security": "https://github.com/sebastianbergmann/exporter/security/policy", + "source": "https://github.com/sebastianbergmann/exporter/tree/7.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/exporter", + "type": "tidelift" + } + ], + "time": "2025-09-24T06:16:11+00:00" + }, + { + "name": "sebastian/global-state", + "version": "8.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "ef1377171613d09edd25b7816f05be8313f9115d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/ef1377171613d09edd25b7816f05be8313f9115d", + "reference": "ef1377171613d09edd25b7816f05be8313f9115d", + "shasum": "" + }, + "require": { + "php": ">=8.3", + "sebastian/object-reflector": "^5.0", + "sebastian/recursion-context": "^7.0" + }, + "require-dev": { + "ext-dom": "*", + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "8.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "https://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "security": "https://github.com/sebastianbergmann/global-state/security/policy", + "source": "https://github.com/sebastianbergmann/global-state/tree/8.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/global-state", + "type": "tidelift" + } + ], + "time": "2025-08-29T11:29:25+00:00" + }, + { + "name": "sebastian/lines-of-code", + "version": "4.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "97ffee3bcfb5805568d6af7f0f893678fc076d2f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/97ffee3bcfb5805568d6af7f0f893678fc076d2f", + "reference": "97ffee3bcfb5805568d6af7f0f893678fc076d2f", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^5.0", + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for counting the lines of code in PHP source code", + "homepage": "https://github.com/sebastianbergmann/lines-of-code", + "support": { + "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", + "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/4.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:57:28+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "7.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "1effe8e9b8e068e9ae228e542d5d11b5d16db894" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/1effe8e9b8e068e9ae228e542d5d11b5d16db894", + "reference": "1effe8e9b8e068e9ae228e542d5d11b5d16db894", + "shasum": "" + }, + "require": { + "php": ">=8.3", + "sebastian/object-reflector": "^5.0", + "sebastian/recursion-context": "^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", + "security": "https://github.com/sebastianbergmann/object-enumerator/security/policy", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/7.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:57:48+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "5.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "4bfa827c969c98be1e527abd576533293c634f6a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/4bfa827c969c98be1e527abd576533293c634f6a", + "reference": "4bfa827c969c98be1e527abd576533293c634f6a", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-reflector/issues", + "security": "https://github.com/sebastianbergmann/object-reflector/security/policy", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/5.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:58:17+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "7.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "0b01998a7d5b1f122911a66bebcb8d46f0c82d8c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/0b01998a7d5b1f122911a66bebcb8d46f0c82d8c", + "reference": "0b01998a7d5b1f122911a66bebcb8d46f0c82d8c", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "https://github.com/sebastianbergmann/recursion-context", + "support": { + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "security": "https://github.com/sebastianbergmann/recursion-context/security/policy", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/7.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/recursion-context", + "type": "tidelift" + } + ], + "time": "2025-08-13T04:44:59+00:00" + }, + { + "name": "sebastian/type", + "version": "6.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "e549163b9760b8f71f191651d22acf32d56d6d4d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/e549163b9760b8f71f191651d22acf32d56d6d4d", + "reference": "e549163b9760b8f71f191651d22acf32d56d6d4d", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", + "support": { + "issues": "https://github.com/sebastianbergmann/type/issues", + "security": "https://github.com/sebastianbergmann/type/security/policy", + "source": "https://github.com/sebastianbergmann/type/tree/6.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/type", + "type": "tidelift" + } + ], + "time": "2025-08-09T06:57:12+00:00" + }, + { + "name": "sebastian/version", + "version": "6.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "3e6ccf7657d4f0a59200564b08cead899313b53c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/3e6ccf7657d4f0a59200564b08cead899313b53c", + "reference": "3e6ccf7657d4f0a59200564b08cead899313b53c", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "support": { + "issues": "https://github.com/sebastianbergmann/version/issues", + "security": "https://github.com/sebastianbergmann/version/security/policy", + "source": "https://github.com/sebastianbergmann/version/tree/6.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T05:00:38+00:00" + }, + { + "name": "staabm/side-effects-detector", + "version": "1.0.5", + "source": { + "type": "git", + "url": "https://github.com/staabm/side-effects-detector.git", + "reference": "d8334211a140ce329c13726d4a715adbddd0a163" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/staabm/side-effects-detector/zipball/d8334211a140ce329c13726d4a715adbddd0a163", + "reference": "d8334211a140ce329c13726d4a715adbddd0a163", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^1.12.6", + "phpunit/phpunit": "^9.6.21", + "symfony/var-dumper": "^5.4.43", + "tomasvotruba/type-coverage": "1.0.0", + "tomasvotruba/unused-public": "1.0.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "lib/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A static analysis tool to detect side effects in PHP code", + "keywords": [ + "static analysis" + ], + "support": { + "issues": "https://github.com/staabm/side-effects-detector/issues", + "source": "https://github.com/staabm/side-effects-detector/tree/1.0.5" + }, + "funding": [ + { + "url": "https://github.com/staabm", + "type": "github" + } + ], + "time": "2024-10-20T05:08:20+00:00" + }, + { + "name": "symfony/finder", + "version": "v8.0.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/finder.git", + "reference": "8bd576e97c67d45941365bf824e18dc8538e6eb0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/finder/zipball/8bd576e97c67d45941365bf824e18dc8538e6eb0", + "reference": "8bd576e97c67d45941365bf824e18dc8538e6eb0", + "shasum": "" + }, + "require": { + "php": ">=8.4" + }, + "require-dev": { + "symfony/filesystem": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Finder\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Finds files and directories via an intuitive fluent interface", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/finder/tree/v8.0.5" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-01-26T15:08:38+00:00" + }, + { + "name": "symfony/process", + "version": "v8.0.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "b5f3aa6762e33fd95efbaa2ec4f4bc9fdd16d674" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/b5f3aa6762e33fd95efbaa2ec4f4bc9fdd16d674", + "reference": "b5f3aa6762e33fd95efbaa2ec4f4bc9fdd16d674", + "shasum": "" + }, + "require": { + "php": ">=8.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Process\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Executes commands in sub-processes", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/process/tree/v8.0.5" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-01-26T15:08:38+00:00" + }, + { + "name": "ta-tikoma/phpunit-architecture-test", + "version": "0.8.6", + "source": { + "type": "git", + "url": "https://github.com/ta-tikoma/phpunit-architecture-test.git", + "reference": "ad48430b92901fd7d003fdaf2d7b139f96c0906e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ta-tikoma/phpunit-architecture-test/zipball/ad48430b92901fd7d003fdaf2d7b139f96c0906e", + "reference": "ad48430b92901fd7d003fdaf2d7b139f96c0906e", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.18.0 || ^5.0.0", + "php": "^8.1.0", + "phpdocumentor/reflection-docblock": "^5.3.0 || ^6.0.0", + "phpunit/phpunit": "^10.5.5 || ^11.0.0 || ^12.0.0", + "symfony/finder": "^6.4.0 || ^7.0.0 || ^8.0.0" + }, + "require-dev": { + "laravel/pint": "^1.13.7", + "phpstan/phpstan": "^1.10.52" + }, + "type": "library", + "autoload": { + "psr-4": { + "PHPUnit\\Architecture\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ni Shi", + "email": "futik0ma011@gmail.com" + }, + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "Methods for testing application architecture", + "keywords": [ + "architecture", + "phpunit", + "stucture", + "test", + "testing" + ], + "support": { + "issues": "https://github.com/ta-tikoma/phpunit-architecture-test/issues", + "source": "https://github.com/ta-tikoma/phpunit-architecture-test/tree/0.8.6" + }, + "time": "2026-01-30T07:16:00+00:00" + }, + { + "name": "theseer/tokenizer", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "7989e43bf381af0eac72e4f0ca5bcbfa81658be4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/7989e43bf381af0eac72e4f0ca5bcbfa81658be4", + "reference": "7989e43bf381af0eac72e4f0ca5bcbfa81658be4", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^8.1" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "support": { + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/2.0.1" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2025-12-08T11:19:18+00:00" + }, + { + "name": "webmozart/assert", + "version": "2.1.2", + "source": { + "type": "git", + "url": "https://github.com/webmozarts/assert.git", + "reference": "ce6a2f100c404b2d32a1dd1270f9b59ad4f57649" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webmozarts/assert/zipball/ce6a2f100c404b2d32a1dd1270f9b59ad4f57649", + "reference": "ce6a2f100c404b2d32a1dd1270f9b59ad4f57649", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-date": "*", + "ext-filter": "*", + "php": "^8.2" + }, + "suggest": { + "ext-intl": "", + "ext-simplexml": "", + "ext-spl": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-feature/2-0": "2.0-dev" + } + }, + "autoload": { + "psr-4": { + "Webmozart\\Assert\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + }, + { + "name": "Woody Gilk", + "email": "woody.gilk@gmail.com" + } + ], + "description": "Assertions to validate method input/output with nice error messages.", + "keywords": [ + "assert", + "check", + "validate" + ], + "support": { + "issues": "https://github.com/webmozarts/assert/issues", + "source": "https://github.com/webmozarts/assert/tree/2.1.2" + }, + "time": "2026-01-13T14:02:24+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": {}, + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": "^8.3" + }, + "platform-dev": {}, + "plugin-api-version": "2.9.0" +} diff --git a/src/Core/GameObject.php b/src/Core/GameObject.php index ebca938..75ffcb1 100644 --- a/src/Core/GameObject.php +++ b/src/Core/GameObject.php @@ -19,614 +19,614 @@ */ class GameObject implements GameObjectInterface { - /** - * @var bool $active Whether the game object is active or not. - */ - protected bool $active = true; - /** - * @var string $hash The hash of the game object. - */ - protected string $hash = ''; - /** - * @var ComponentInterface[] $components The components attached to the game object. - */ - protected array $components = []; - /** - * @var UIElementInterface[] $uiElements The UI elements attached to the game object. - */ - protected array $uiElements = []; - /** - * @var Transform $transform The transform of the game object. - */ - protected Transform $transform; - /** - * @var Renderer $renderer The renderer for the game object. - */ - protected Renderer $renderer; - - /** - * GameObject constructor. - * - * @param string $name The name of the game object. - * @param string|null $tag The tag of the game object. - * @param Vector2 $position The position of the game object. - * @param Vector2 $rotation The rotation of the game object. - * @param Vector2 $scale The scale of the game object. - * @param Sprite|null $sprite The sprite of the game object. - */ - public function __construct(protected string $name, protected ?string $tag = null, protected Vector2 $position = new Vector2(), protected Vector2 $rotation = new Vector2(), protected Vector2 $scale = new Vector2(), protected ?Sprite $sprite = null) - { - $this->hash = md5(__CLASS__) . '-' . uniqid($this->name, true); - $this->transform = new Transform($this, $position, $scale, $rotation); - $this->renderer = new Renderer($this, $sprite); - - $this->components[] = $this->transform; - $this->components[] = $this->renderer; - } - - /** - * Clones the original game object and returns the clone. - * - * @param GameObject $original The original game object to clone. - * @param Vector2|null $position The position of the clone. - * @param Vector2|null $rotation The rotation of the clone. - * @param Vector2|null $scale The scale of the clone. - * @param Transform|null $parent The parent of the clone. - * @return GameObject The clone of the original game object. - */ - public static function instantiate(GameObject $original, ?Vector2 $position = null, ?Vector2 $rotation = null, ?Vector2 $scale = null, ?Transform $parent = null): GameObject - { - $clone = clone $original; - - if ($position) { - $clone->transform->setPosition($position); - } - - if ($rotation) { - $clone->transform->setRotation($rotation); - } - - if ($scale) { - $clone->transform->setScale($scale); - } - - if ($parent) { - $clone->transform->setParent($parent); - } - - return $clone; - } - - /** - * Destroys the game object after the specified delay. This removes the game object from the scene. - * - * @param GameObject $gameObject The game object to destroy. - * @param float $delay The delay before destroying the game object. - * @return void - */ - public static function destroy(GameObject $gameObject, float $delay = 0.0): void - { - if ($activeScene = SceneManager::getInstance()->getActiveScene()) { - // Wait for the delay before destroying the game object. - - $activeScene->remove($gameObject); - unset($gameObject); - } - } - - /** - * @inheritDoc - */ - public static function pool(GameObjectInterface $gameObject, int $size): array - { - $pool = []; - - for ($i = 0; $i < $size; ++$i) { - $pool[] = clone $gameObject; - } - - return $pool; - } - - /** - * @inheritDoc - */ - public static function find(string $gameObjectName): ?GameObjectInterface - { - if ($activeScene = SceneManager::getInstance()->getActiveScene()) { - foreach ($activeScene->getRootGameObjects() as $gameObject) { - if ($gameObject->getName() === $gameObjectName) { - return $gameObject; - } - } - } - - return null; - } - - /** - * Returns the name of the game object. - * - * @return string The name of the game object. - */ - public function getName(): string - { - return $this->name; - } - - /** - * @inheritDoc - */ - public static function findWithTag(string $gameObjectTag): ?GameObjectInterface - { - if ($activeScene = SceneManager::getInstance()->getActiveScene()) { - foreach ($activeScene->getRootGameObjects() as $gameObject) { - if ($gameObject->getTag() === $gameObjectTag) { - return $gameObject; - } - } - } - - return null; - } - - /** - * Returns the tag of the game object. - * - * @return string The tag of the game object. - */ - public function getTag(): string - { - return $this->tag ?? ''; - } - - /** - * @inheritDoc - */ - public static function findAll(string $gameObjectName): array - { - $gameObjects = []; - - if ($activeScene = SceneManager::getInstance()->getActiveScene()) { - foreach ($activeScene->getRootGameObjects() as $gameObject) { - if ($gameObject->getName() === $gameObjectName) { - $gameObjects[] = $gameObject; - } - } - } - - return $gameObjects; - } - - /** - * @inheritDoc - */ - public static function findAllWithTag(string $gameObjectTag): array - { - $gameObjects = []; - - if ($activeScene = SceneManager::getInstance()->getActiveScene()) { - foreach ($activeScene->getRootGameObjects() as $gameObject) { - if ($gameObject->getTag() === $gameObjectTag) { - $gameObjects[] = $gameObject; - } - } - } - - return $gameObjects; - } - - /** - * @inheritDoc - */ - public function getScene(): SceneInterface - { - return SceneManager::getInstance()->getActiveScene(); - } - - /** - * @return void - */ - public function __clone(): void - { - $this->hash = md5(__CLASS__) . '-' . uniqid($this->name, true); - - $this->transform = clone $this->transform; - $this->renderer = clone $this->renderer; - - if ($this->sprite) { - $this->sprite = clone $this->sprite; - } - } - - /** - * Returns the transform of the game object. - * - * @return Transform The transform of the game object. - */ - public function getTransform(): Transform - { - return $this->transform; - } - - /** - * @inheritDoc - */ - public function greaterThan(CanCompare $other): bool - { - return $this->compareTo($other) > 0; - } - - /** - * @inheritDoc - */ - public function compareTo(CanCompare $other): int - { - if (!$other instanceof GameObject) { - throw new InvalidArgumentException('Cannot compare a game object with a non-game object.'); - } - - return strcmp($this->getHash(), $other->getHash()); - } - - /** - * @inheritDoc - */ - public function getHash(): string - { - return $this->hash; - } - - /** - * @inheritDoc - */ - public function greaterThanOrEqual(CanCompare $other): bool - { - return $this->compareTo($other) >= 0; - } - - /** - * @inheritDoc - */ - public function lessThan(CanCompare $other): bool - { - return $this->compareTo($other) < 0; - } - - /** - * @inheritDoc - */ - public function lessThanOrEqual(CanCompare $other): bool - { - return $this->compareTo($other) <= 0; - } - - /** - * @inheritDoc - */ - public function notEquals(CanEquate $equatable): bool - { - return !$this->equals($equatable); - } - - /** - * @inheritDoc - */ - public function equals(CanEquate $equatable): bool - { - return $this->getHash() === $equatable->getHash(); - } - - /** - * @inheritDoc - */ - public function render(): void - { - if ($this->isActive() && $this->renderer->isEnabled()) { - $this->renderer->render(); - } - } - - /** - * @inheritDoc - */ - public function isActive(): bool - { - return $this->active; - } - - /** - * @inheritDoc - */ - public function renderAt(?int $x = null, ?int $y = null): void - { - if ($this->isActive() && $this->renderer->isEnabled()) { - $this->renderer->renderAt($x, $y); - } - } - - /** - * @inheritDoc - */ - public function erase(): void - { - if ($this->isActive() && $this->renderer->isEnabled()) { - $this->renderer->erase(); - } - } - - /** - * @inheritDoc - */ - public function eraseAt(?int $x = null, ?int $y = null): void - { - if ($this->isActive() && $this->renderer->isEnabled()) { - $this->renderer->eraseAt($x, $y); - } - } - - /** - * @inheritDoc - */ - public function resume(): void - { - if ($this->isActive()) { - foreach ($this->components as $component) { - if ($component->isEnabled()) { - $component->resume(); - } - } - } - } - - /** - * @inheritDoc - */ - public function suspend(): void - { - if ($this->isActive()) { - foreach ($this->components as $component) { - if ($component->isEnabled()) { - $component->suspend(); - } - } - } - } - - /** - * @inheritDoc - */ - public function start(): void - { - if ($this->isActive()) { - foreach ($this->components as $component) { - if ($component->isEnabled()) { - $component->start(); - } - } - } - } - - /** - * @inheritDoc - */ - public function stop(): void - { - if ($this->isActive()) { - foreach ($this->components as $component) { - if ($component->isEnabled()) { - $component->stop(); - } - } - } - } - - /** - * @inheritDoc - */ - public function fixedUpdate(): void - { - if ($this->isActive()) { - foreach ($this->components as $component) { - if ($component->isEnabled()) { - $component->fixedUpdate(); - } - } - } - } - - /** - * @inheritDoc - */ - public function update(): void - { - if ($this->isActive()) { - foreach ($this->components as $component) { - if ($component->isEnabled()) { - $component->update(); - } - } - } - } - - /** - * @inheritDoc - */ - public function activate(): void - { - $this->active = true; - $this->getRenderer()->render(); - } - - /** - * @inheritDoc - */ - public function deactivate(): void - { - $this->active = false; - $this->getRenderer()->erase(); - } - - /** - * Calls the method named $methodName on every component in this game object and its children. - * - * @param string $methodName The name of the method to call. - * @param array $args The arguments to pass to the method. - * @return void - */ - public function broadcast(string $methodName, array $args = []): void - { - foreach ($this->components as $component) { - if (method_exists($component, $methodName)) { - $component->$methodName(...$args); - } - } - } - - /** - * @inheritDoc - */ - public function addComponent(string $componentType): Component - { - if (!class_exists($componentType)) { - throw new InvalidArgumentException('The component type ' . $componentType . ' does not exist.'); - } - - if (!is_subclass_of($componentType, Component::class)) { - throw new InvalidArgumentException('The component type ' . $componentType . ' is not a subclass of ' . Component::class); - } - - $component = new $componentType($this); - $this->components[] = $component; - return $component; - } - - /** - * Returns the number of components attached to the game object. - * - * @return int The number of components attached to the game object. - */ - public function getComponentCount(): int - { - return count($this->components); - } - - /** - * Gets the index of the component specified on the specified GameObject. - * - * @param Component $component The component to find. - * @return int The index of the component, or -1 if the component is not found. - */ - public function getComponentIndex(Component $component): int - { - foreach ($this->components as $index => $gameObjectComponent) { - if ($component->equals($gameObjectComponent)) { - return $index; - } - } - - return -1; - } - - /** - * @inheritDoc - */ - public function getComponent(string $componentClass): ?ComponentInterface - { - if (!class_exists($componentClass) && !interface_exists($componentClass)) { - throw new InvalidArgumentException('The component type ' . $componentClass . ' does not exist.'); - } - - foreach ($this->components as $component) { - if ($component instanceof $componentClass) { + /** + * @var bool $active Whether the game object is active or not. + */ + protected bool $active = true; + /** + * @var string $hash The hash of the game object. + */ + protected string $hash = ''; + /** + * @var ComponentInterface[] $components The components attached to the game object. + */ + protected array $components = []; + /** + * @var UIElementInterface[] $uiElements The UI elements attached to the game object. + */ + protected array $uiElements = []; + /** + * @var Transform $transform The transform of the game object. + */ + protected Transform $transform; + /** + * @var Renderer $renderer The renderer for the game object. + */ + protected Renderer $renderer; + + /** + * GameObject constructor. + * + * @param string $name The name of the game object. + * @param string|null $tag The tag of the game object. + * @param Vector2 $position The position of the game object. + * @param Vector2 $rotation The rotation of the game object. + * @param Vector2 $scale The scale of the game object. + * @param Sprite|null $sprite The sprite of the game object. + */ + public function __construct(protected string $name, protected ?string $tag = null, protected Vector2 $position = new Vector2(), protected Vector2 $rotation = new Vector2(), protected Vector2 $scale = new Vector2(), protected ?Sprite $sprite = null) + { + $this->hash = md5(__CLASS__) . '-' . uniqid($this->name, true); + $this->transform = new Transform($this, $position, $scale, $rotation); + $this->renderer = new Renderer($this, $sprite); + + $this->components[] = $this->transform; + $this->components[] = $this->renderer; + } + + /** + * Clones the original game object and returns the clone. + * + * @param GameObject $original The original game object to clone. + * @param Vector2|null $position The position of the clone. + * @param Vector2|null $rotation The rotation of the clone. + * @param Vector2|null $scale The scale of the clone. + * @param Transform|null $parent The parent of the clone. + * @return GameObject The clone of the original game object. + */ + public static function instantiate(GameObject $original, ?Vector2 $position = null, ?Vector2 $rotation = null, ?Vector2 $scale = null, ?Transform $parent = null): GameObject + { + $clone = clone $original; + + if ($position) { + $clone->transform->setPosition($position); + } + + if ($rotation) { + $clone->transform->setRotation($rotation); + } + + if ($scale) { + $clone->transform->setScale($scale); + } + + if ($parent) { + $clone->transform->setParent($parent); + } + + return $clone; + } + + /** + * Destroys the game object after the specified delay. This removes the game object from the scene. + * + * @param GameObject $gameObject The game object to destroy. + * @param float $delay The delay before destroying the game object. + * @return void + */ + public static function destroy(GameObject $gameObject, float $delay = 0.0): void + { + if ($activeScene = SceneManager::getInstance()->getActiveScene()) { + // Wait for the delay before destroying the game object. + + $activeScene->remove($gameObject); + unset($gameObject); + } + } + + /** + * @inheritDoc + */ + public static function pool(GameObjectInterface $gameObject, int $size): array + { + $pool = []; + + for ($i = 0; $i < $size; ++$i) { + $pool[] = clone $gameObject; + } + + return $pool; + } + + /** + * @inheritDoc + */ + public static function find(string $gameObjectName): ?GameObjectInterface + { + if ($activeScene = SceneManager::getInstance()->getActiveScene()) { + foreach ($activeScene->getRootGameObjects() as $gameObject) { + if ($gameObject->getName() === $gameObjectName) { + return $gameObject; + } + } + } + + return null; + } + + /** + * Returns the name of the game object. + * + * @return string The name of the game object. + */ + public function getName(): string + { + return $this->name; + } + + /** + * @inheritDoc + */ + public static function findWithTag(string $gameObjectTag): ?GameObjectInterface + { + if ($activeScene = SceneManager::getInstance()->getActiveScene()) { + foreach ($activeScene->getRootGameObjects() as $gameObject) { + if ($gameObject->getTag() === $gameObjectTag) { + return $gameObject; + } + } + } + + return null; + } + + /** + * Returns the tag of the game object. + * + * @return string The tag of the game object. + */ + public function getTag(): string + { + return $this->tag ?? ''; + } + + /** + * @inheritDoc + */ + public static function findAll(string $gameObjectName): array + { + $gameObjects = []; + + if ($activeScene = SceneManager::getInstance()->getActiveScene()) { + foreach ($activeScene->getRootGameObjects() as $gameObject) { + if ($gameObject->getName() === $gameObjectName) { + $gameObjects[] = $gameObject; + } + } + } + + return $gameObjects; + } + + /** + * @inheritDoc + */ + public static function findAllWithTag(string $gameObjectTag): array + { + $gameObjects = []; + + if ($activeScene = SceneManager::getInstance()->getActiveScene()) { + foreach ($activeScene->getRootGameObjects() as $gameObject) { + if ($gameObject->getTag() === $gameObjectTag) { + $gameObjects[] = $gameObject; + } + } + } + + return $gameObjects; + } + + /** + * @inheritDoc + */ + public function getScene(): SceneInterface + { + return SceneManager::getInstance()->getActiveScene(); + } + + /** + * @return void + */ + public function __clone(): void + { + $this->hash = md5(__CLASS__) . '-' . uniqid($this->name, true); + + $this->transform = clone $this->transform; + $this->renderer = clone $this->renderer; + + if ($this->sprite) { + $this->sprite = clone $this->sprite; + } + } + + /** + * Returns the transform of the game object. + * + * @return Transform The transform of the game object. + */ + public function getTransform(): Transform + { + return $this->transform; + } + + /** + * @inheritDoc + */ + public function greaterThan(CanCompare $other): bool + { + return $this->compareTo($other) > 0; + } + + /** + * @inheritDoc + */ + public function compareTo(CanCompare $other): int + { + if (!$other instanceof GameObject) { + throw new InvalidArgumentException('Cannot compare a game object with a non-game object.'); + } + + return strcmp($this->getHash(), $other->getHash()); + } + + /** + * @inheritDoc + */ + public function getHash(): string + { + return $this->hash; + } + + /** + * @inheritDoc + */ + public function greaterThanOrEqual(CanCompare $other): bool + { + return $this->compareTo($other) >= 0; + } + + /** + * @inheritDoc + */ + public function lessThan(CanCompare $other): bool + { + return $this->compareTo($other) < 0; + } + + /** + * @inheritDoc + */ + public function lessThanOrEqual(CanCompare $other): bool + { + return $this->compareTo($other) <= 0; + } + + /** + * @inheritDoc + */ + public function notEquals(CanEquate $equatable): bool + { + return !$this->equals($equatable); + } + + /** + * @inheritDoc + */ + public function equals(CanEquate $equatable): bool + { + return $this->getHash() === $equatable->getHash(); + } + + /** + * @inheritDoc + */ + public function renderAt(?int $x = null, ?int $y = null): void + { + if ($this->isActive() && $this->renderer->isEnabled()) { + $this->renderer->renderAt($x, $y); + } + } + + /** + * @inheritDoc + */ + public function isActive(): bool + { + return $this->active; + } + + /** + * @inheritDoc + */ + public function eraseAt(?int $x = null, ?int $y = null): void + { + if ($this->isActive() && $this->renderer->isEnabled()) { + $this->renderer->eraseAt($x, $y); + } + } + + /** + * @inheritDoc + */ + public function resume(): void + { + if ($this->isActive()) { + foreach ($this->components as $component) { + if ($component->isEnabled()) { + $component->resume(); + } + } + } + } + + /** + * @inheritDoc + */ + public function suspend(): void + { + if ($this->isActive()) { + foreach ($this->components as $component) { + if ($component->isEnabled()) { + $component->suspend(); + } + } + } + } + + /** + * @inheritDoc + */ + public function start(): void + { + if ($this->isActive()) { + foreach ($this->components as $component) { + if ($component->isEnabled()) { + $component->start(); + } + } + } + } + + /** + * @inheritDoc + */ + public function stop(): void + { + if ($this->isActive()) { + foreach ($this->components as $component) { + if ($component->isEnabled()) { + $component->stop(); + } + } + } + } + + /** + * @inheritDoc + */ + public function fixedUpdate(): void + { + if ($this->isActive()) { + foreach ($this->components as $component) { + if ($component->isEnabled()) { + $component->fixedUpdate(); + } + } + } + } + + /** + * @inheritDoc + */ + public function update(): void + { + if ($this->isActive()) { + foreach ($this->components as $component) { + if ($component->isEnabled()) { + $component->update(); + } + } + } + } + + /** + * @inheritDoc + */ + public function activate(): void + { + $this->active = true; + $this->getRenderer()->render(); + } + + /** + * @inheritDoc + */ + public function render(): void + { + if ($this->isActive() && $this->renderer->isEnabled()) { + $this->renderer->render(); + } + } + + /** + * Returns the renderer for the game object. + * + * @return Renderer The renderer for the game object. + */ + public function getRenderer(): Renderer + { + return $this->renderer; + } + + /** + * @inheritDoc + */ + public function deactivate(): void + { + $this->active = false; + $this->getRenderer()->erase(); + } + + /** + * @inheritDoc + */ + public function erase(): void + { + if ($this->isActive() && $this->renderer->isEnabled()) { + $this->renderer->erase(); + } + } + + /** + * Calls the method named $methodName on every component in this game object and its children. + * + * @param string $methodName The name of the method to call. + * @param array $args The arguments to pass to the method. + * @return void + */ + public function broadcast(string $methodName, array $args = []): void + { + foreach ($this->components as $component) { + if (method_exists($component, $methodName)) { + $component->$methodName(...$args); + } + } + } + + /** + * @inheritDoc + */ + public function addComponent(string $componentType): Component + { + if (!class_exists($componentType)) { + throw new InvalidArgumentException('The component type ' . $componentType . ' does not exist.'); + } + + if (!is_subclass_of($componentType, Component::class)) { + throw new InvalidArgumentException('The component type ' . $componentType . ' is not a subclass of ' . Component::class); + } + + $component = new $componentType($this); + $this->components[] = $component; return $component; - } - } - - return null; - } - - /** - * @inheritDoc - */ - public function getComponents(?string $componentClass = null): array - { - if ($componentClass) { - return array_filter($this->components, fn(ComponentInterface $component) => $component instanceof $componentClass); - } - - return $this->components; - } - - /** - * @inheritDoc - */ - public function getUIElement(string $uiElementClass): ?UIElementInterface - { - if (!class_exists($uiElementClass) && !interface_exists($uiElementClass)) { - throw new InvalidArgumentException('The ui element type ' . $uiElementClass . ' does not exist.'); - } - - foreach ($this->uiElements as $uiElement) { - if ($uiElement instanceof $uiElementClass) { - return $uiElement; - } - } - - return null; - } - - /** - * @inheritDoc - */ - public function getUIElements(?string $uiElementClass = null): array - { - if ($uiElementClass) { - return array_filter($this->uiElements, fn(UIElementInterface $uiElement) => $uiElement instanceof $uiElementClass); - } - - return $this->uiElements; - } - - /** - * @inheritDoc - */ - public function setSpriteFromTexture(Texture2D|array|string $texture, Vector2 $position, Vector2 $size): void - { - if (is_array($texture)) { - $texture = new Texture2D($texture['path'], $texture['width'] ?? -1, $texture['height'] ?? -1); - } - - if (is_string($texture)) { - $texture = new Texture2D($texture); - } - - $this->setSprite(new Sprite($texture, new Rect($position, $size))); - } - - /** - * @inheritDoc - */ - public function setSprite(Sprite $sprite): void - { - $this->getRenderer()->setSprite($sprite); - } - - /** - * Returns the renderer for the game object. - * - * @return Renderer The renderer for the game object. - */ - public function getRenderer(): Renderer - { - return $this->renderer; - } - - /** - * @inheritDoc - */ - public function getSprite(): Sprite - { - return $this->getRenderer()->getSprite(); - } + } + + /** + * Returns the number of components attached to the game object. + * + * @return int The number of components attached to the game object. + */ + public function getComponentCount(): int + { + return count($this->components); + } + + /** + * Gets the index of the component specified on the specified GameObject. + * + * @param Component $component The component to find. + * @return int The index of the component, or -1 if the component is not found. + */ + public function getComponentIndex(Component $component): int + { + foreach ($this->components as $index => $gameObjectComponent) { + if ($component->equals($gameObjectComponent)) { + return $index; + } + } + + return -1; + } + + /** + * @inheritDoc + */ + public function getComponent(string $componentClass): ?ComponentInterface + { + if (!class_exists($componentClass) && !interface_exists($componentClass)) { + throw new InvalidArgumentException('The component type ' . $componentClass . ' does not exist.'); + } + + foreach ($this->components as $component) { + if ($component instanceof $componentClass) { + return $component; + } + } + + return null; + } + + /** + * @inheritDoc + */ + public function getComponents(?string $componentClass = null): array + { + if ($componentClass) { + return array_filter($this->components, fn(ComponentInterface $component) => $component instanceof $componentClass); + } + + return $this->components; + } + + /** + * @inheritDoc + */ + public function getUIElement(string $uiElementClass): ?UIElementInterface + { + if (!class_exists($uiElementClass) && !interface_exists($uiElementClass)) { + throw new InvalidArgumentException('The ui element type ' . $uiElementClass . ' does not exist.'); + } + + foreach ($this->uiElements as $uiElement) { + if ($uiElement instanceof $uiElementClass) { + return $uiElement; + } + } + + return null; + } + + /** + * @inheritDoc + */ + public function getUIElements(?string $uiElementClass = null): array + { + if ($uiElementClass) { + return array_filter($this->uiElements, fn(UIElementInterface $uiElement) => $uiElement instanceof $uiElementClass); + } + + return $this->uiElements; + } + + /** + * @inheritDoc + */ + public function setSpriteFromTexture(Texture2D|array|string $texture, Vector2 $position, Vector2 $size): void + { + if (is_array($texture)) { + $texture = new Texture2D($texture['path'], $texture['width'] ?? -1, $texture['height'] ?? -1); + } + + if (is_string($texture)) { + $texture = new Texture2D($texture); + } + + $this->setSprite(new Sprite($texture, new Rect($position, $size))); + } + + /** + * @inheritDoc + */ + public function setSprite(Sprite $sprite): void + { + $this->getRenderer()->setSprite($sprite); + } + + /** + * @inheritDoc + */ + public function getSprite(): Sprite + { + return $this->getRenderer()->getSprite(); + } } \ No newline at end of file diff --git a/src/Core/Scenes/AbstractScene.php b/src/Core/Scenes/AbstractScene.php index 4c3241d..68b95bf 100644 --- a/src/Core/Scenes/AbstractScene.php +++ b/src/Core/Scenes/AbstractScene.php @@ -23,543 +23,544 @@ */ abstract class AbstractScene implements SceneInterface { - /** - * @const string MAP_FILE_EXTENSION - */ - const string MAP_FILE_EXTENSION = '.tmap'; - /** - * @var array $settings - */ - protected array $settings = []; - - /** - * @var array $rootGameObjects - */ - public array $rootGameObjects = []; - - /** - * @var array $uiElements - */ - public array $uiElements = []; - - /** - * @var Physics - */ - protected Physics $physics; - - /** - * @var Grid $worldsSpace - */ - protected Grid $worldsSpace; - - /** - * @var Grid $collisionWorldSpace - */ - protected Grid $collisionWorldSpace; - - /** - * @var CameraInterface $camera - */ - protected CameraInterface $camera; - /** - * @var string $environmentTileMapPath - */ - protected string $environmentTileMapPath = ''; - /** - * @var string $environmentTileMapPath - */ - protected string $environmentTileMapData = ''; - - /** - * @var bool $started - */ - protected bool $started = false; - - /** - * Constructs a scene. - * - * @param string $name The name of the scene. - * @throws FileNotFoundException - */ - public final function __construct(protected string $name) - { - $this->worldsSpace = new Grid(); - $this->collisionWorldSpace = new Grid(); - $this->physics = Physics::getInstance(); - $this->camera = new Camera($this); - - $this->awake(); - - if ($this->environmentTileMapPath) { - $this->loadEnvironmentTileMapData(); - } - } - - /** - * @inheritDoc - */ - public function load(): void - { - // Do nothing. This method is meant to be overridden. - } - - /** - * @inheritDoc - */ - public function unload(): void - { - // Do nothing. This method is meant to be overridden. - } - - /** - * @inheritDoc - */ - public function getName(): string - { - return $this->name; - } - - /** - * @inheritDoc - */ - public final function renderAt(?int $x = null, ?int $y = null): void - { - $this->camera->renderAt($x, $y); - } - - /** - * @inheritDoc - */ - public final function eraseAt(?int $x = null, ?int $y = null): void - { - // Do nothing. - } - - /** - * @inheritDoc - */ - public final function loadSceneSettings(?array $settings = null): self - { - foreach ($settings as $key => $value) { - $this->settings[$key] = $value; - } - - if (isset($this->settings['screen_width']) && isset($this->settings['screen_height'])) { - $oldViewport = $this->camera->getViewport(); - $this->camera->setViewport( - new Rect( - $this->camera->getOffset(), - new Vector2( - $this->settings['screen_width'] ?? $oldViewport->getWidth(), - $this->settings['screen_height'] ?? $oldViewport->getHeight() - ) - ) - ); - } - - return $this; - } - - /** - * Called when the scene is awake. - */ - public abstract function awake(): void; - - /** - * Starts the scene. - * - * @inheritDoc - */ - public final function start(): void - { - Debug::info("Scene started: " . $this->name); - - $this->createWorldSpace(); - $this->loadStaticEnvironment(); - - foreach ($this->rootGameObjects as $gameObject) { - $gameObject->start(); - } - - foreach ($this->uiElements as $uiElement) { - $uiElement->start(); - } - - $this->started = true; - } - - /** - * Stops the scene. - * - * @inheritDoc - */ - public final function stop(): void - { - Debug::info("Scene stopped: " . $this->name); - - foreach ($this->rootGameObjects as $gameObject) { - $gameObject->stop(); - } - - foreach ($this->uiElements as $uiElement) { - $uiElement->stop(); - } - - $this->getCamera()->clearScreen(); - - $this->started = false; - } - - /** - * @inheritDoc - */ - public function isStarted(): bool - { - return $this->started; - } - - /** - * @inheritDoc - */ - public function isStopped(): bool - { - return ! $this->isStarted(); - } - - /** - * @inheritDoc - */ - public final function update(): void - { - foreach ($this->rootGameObjects as $gameObject) { - if ($gameObject->isActive()) { - $gameObject->update(); - } - } - - foreach ($this->uiElements as $uiElement) { - if ($uiElement->isActive()) { - $uiElement->update(); - } - } - - // Update the camera - $this->camera->update(); - } - - /** - * @inheritDoc - */ - public function updatePhysics(): void - { - foreach ($this->rootGameObjects as $gameObject) { - if ($gameObject->isActive()) { - $gameObject->fixedUpdate(); - } - } - $this->physics->simulate(); - } - - /** - * @inheritDoc - */ - public final function render(): void - { - $this->camera->render(); - } - - /** - * @inheritDoc - */ - public final function erase(): void - { - $this->camera->erase(); - } - - /** - * @inheritDoc - */ - public final function suspend(): void - { - Debug::info('Scene suspended: ' . $this->name); - foreach ($this->rootGameObjects as $gameObject) { - if ($gameObject->isActive()) { - $gameObject->suspend(); - } - } - - foreach ($this->uiElements as $uiElement) { - if ($uiElement->isActive()) { - $uiElement->suspend(); - } - } - } - - /** - * @inheritDoc - */ - public final function resume(): void - { - Debug::info('Scene resumed: ' . $this->name); - $this->camera->renderWorldSpace(); - - foreach ($this->rootGameObjects as $gameObject) { - if ($gameObject->isActive()) { - $gameObject->resume(); - } - } - - foreach ($this->uiElements as $uiElement) { - if ($uiElement->isActive()) { - $uiElement->resume(); - } - } - } - - /** - * @inheritDoc - */ - public final function getRootGameObjects(): array - { - return $this->rootGameObjects; - } - - /** - * @inheritDoc - */ - public final function getUIElements(): array - { - return $this->uiElements; - } - - /** - * @inheritDoc - */ - public function serialize(): string - { - return json_encode([ - 'name' => $this->name, - 'settings' => $this->settings, - 'root_game_objects' => $this->rootGameObjects, - 'ui_elements' => $this->uiElements, - ]); - } - - /** - * @inheritDoc - */ - public function unserialize(string $data): void - { - $data = json_decode($data, true); - - $this->name = $data['name']; - $this->settings = $data['settings']; - $this->rootGameObjects = $data['root_game_objects']; - $this->uiElements = $data['ui_elements']; - } - - /** - * Serializes the scene. - * - * @return array The serialized scene. - */ - public function __serialize(): array - { - return [ - 'name' => $this->name, - 'settings' => $this->settings, - 'root_game_objects' => $this->rootGameObjects, - 'ui_elements' => $this->uiElements, - ]; - } - - /** - * Deserializes the scene. - * - * @param array $data The data to unserialize. - * @return void - */ - public function __unserialize(array $data): void - { - $this->name = $data['name']; - $this->settings = $data['settings']; - $this->rootGameObjects = $data['root_game_objects']; - $this->uiElements = $data['ui_elements']; - } - - /** - * Creates the world space. - */ - private function createWorldSpace(): void - { - Debug::info('Creating world space for ' . $this->name); - $width = $this->settings['screen_width']; - $height = $this->settings['screen_height']; - - $this->worldsSpace = new Grid($width, $height, ' '); - $this->collisionWorldSpace = new Grid($width, $height, 0); - } - - /** - * Sets the world space. - * - * @param Grid $worldSpace The new world space. - * @return void - */ - private function setWorldSpace(Grid $worldSpace): void - { - $this->worldsSpace = $worldSpace; - } - - /** - * Sets the collision world space. - * - * @param Grid $worldSpace The new collision world space. - * @return void - */ - private function setCollisionWorldSpace(Grid $worldSpace): void - { - $this->collisionWorldSpace = $worldSpace; - } - - /** - * @inheritDoc - */ - public function getWorldSpace(): Grid - { - return $this->worldsSpace; - } - - /** - * Returns the collision world space. - * - * @return Grid The collision world space. - */ - public function getCollisionWorldSpace(): Grid - { - return $this->collisionWorldSpace; - } - - /** - * @inheritDoc - */ - public function add(GameObjectInterface|UIElementInterface $object): void - { - Debug::info('Adding game object ' . $object->getName()); - if ($object instanceof GameObjectInterface) { - $this->rootGameObjects[] = $object; - if ($collider = $object->getComponent(ColliderInterface::class)) { - $this->physics->addCollider($collider); - } - } else { - $this->uiElements[] = $object; - } - - if ($this->isStarted()) { - $object->start(); - } - } - - /** - * @inheritDoc - */ - public function remove(UIElementInterface|GameObjectInterface $object): void - { - Debug::info('Removing game object ' . $object->getName()); - if ($object instanceof GameObjectInterface) { - $this->rootGameObjects = array_filter($this->rootGameObjects, fn($item) => $item !== $object, $this->rootGameObjects); - if ($collider = $object->getComponent('Collider')) { - $this->physics->removeCollider($collider); - } - } else { - $this->uiElements = array_filter($this->uiElements, fn($item) => $item !== $object, $this->uiElements); - } - - if ($this->isStopped()) { - $object->stop(); - } - } - - /** - * Returns the camera. - * - * @return CameraInterface The camera. - */ - public function getCamera(): CameraInterface - { - return $this->camera; - } - - private function loadStaticEnvironment(): void - { - // Return if no tile map data - if (! $this->environmentTileMapData) { - return; - } - - // Parse the tile map data - $buffer = new Grid(); - $lines = explode("\n", $this->environmentTileMapData); - - foreach ($lines as $y => $line) { - $lineLength = strlen($line); - for ($x = 0; $x < $lineLength; $x++) { - $buffer->set($x, $y, $line[$x]); - } - } - - // Fill the world space with the static tiles - $this->setWorldSpace($buffer); - - $this->camera->renderWorldSpace(); - } - - /** - * Loads the environment tile map data from a file on disk. - * - * @param string|null $path The path to the environment tile map file. - * @return void - * @throws FileNotFoundException If the file does not exist. - */ - private function loadEnvironmentTileMapData(?string $path = null): void - { - Debug::info("Loading environment tile map data: $path"); - // Check if the file exists - if (! file_exists($this->getAbsoluteEnvironmentTileMapPath()) ) { - throw new FileNotFoundException($this->getAbsoluteEnvironmentTileMapPath()); - } - - if (! is_file($this->getAbsoluteEnvironmentTileMapPath()) ) { - throw new FileNotFoundException($this->getAbsoluteEnvironmentTileMapPath()); - } - - // Get the contents of the file - $this->environmentTileMapData = file_get_contents($this->getAbsoluteEnvironmentTileMapPath()); - } - - /** - * Returns the absolute path to the environment tile map file. - * - * @return string The absolute path to the environment tile map file. - */ - private function getAbsoluteEnvironmentTileMapPath(): string - { - return Path::join(Path::getWorkingDirectoryAssetsPath(), $this->environmentTileMapPath) . self::MAP_FILE_EXTENSION; - } - - /** - * @inheritDoc - */ - public function getSettings(?string $key): mixed - { - return $this->settings[$key] ?? $this->settings; - } - - /** - * @inheritDoc - */ - public function getSceneManager(): SceneManager - { - return SceneManager::getInstance(); - } + /** + * @const string MAP_FILE_EXTENSION + */ + const string MAP_FILE_EXTENSION = '.tmap'; + /** + * @var array $rootGameObjects + */ + public array $rootGameObjects = []; + /** + * @var array $uiElements + */ + public array $uiElements = []; + /** + * @var array $settings + */ + protected array $settings = []; + /** + * @var Physics + */ + protected Physics $physics; + + /** + * @var Grid $worldsSpace + */ + protected Grid $worldsSpace; + + /** + * @var Grid $collisionWorldSpace + */ + protected Grid $collisionWorldSpace; + + /** + * @var CameraInterface $camera + */ + protected CameraInterface $camera; + /** + * @var string $environmentTileMapPath + */ + protected string $environmentTileMapPath = ''; + /** + * @var string $environmentTileMapPath + */ + protected string $environmentTileMapData = ''; + + /** + * @var bool $started + */ + protected bool $started = false; + + /** + * Constructs a scene. + * + * @param string $name The name of the scene. + * @throws FileNotFoundException + */ + public final function __construct(protected string $name, protected ?object $sceneMetadata = null) + { + $this->worldsSpace = new Grid(); + $this->collisionWorldSpace = new Grid(); + $this->physics = Physics::getInstance(); + $this->camera = new Camera($this); + + $this->awake(); + + if ($this->environmentTileMapPath) { + $this->loadEnvironmentTileMapData(); + } + } + + /** + * Called when the scene is awake. + */ + public abstract function awake(): void; + + /** + * Loads the environment tile map data from a file on disk. + * + * @param string|null $path The path to the environment tile map file. + * @return void + * @throws FileNotFoundException If the file does not exist. + */ + private function loadEnvironmentTileMapData(?string $path = null): void + { + Debug::info("Loading environment tile map data: $path"); + // Check if the file exists + if (!file_exists($this->getAbsoluteEnvironmentTileMapPath())) { + throw new FileNotFoundException($this->getAbsoluteEnvironmentTileMapPath()); + } + + if (!is_file($this->getAbsoluteEnvironmentTileMapPath())) { + throw new FileNotFoundException($this->getAbsoluteEnvironmentTileMapPath()); + } + + // Get the contents of the file + $this->environmentTileMapData = file_get_contents($this->getAbsoluteEnvironmentTileMapPath()); + } + + /** + * Returns the absolute path to the environment tile map file. + * + * @return string The absolute path to the environment tile map file. + */ + private function getAbsoluteEnvironmentTileMapPath(): string + { + return Path::join(Path::getWorkingDirectoryAssetsPath(), $this->environmentTileMapPath) . self::MAP_FILE_EXTENSION; + } + + /** + * @inheritDoc + */ + public function load(): void + { + // Do nothing. This method is meant to be overridden. + } + + /** + * @inheritDoc + */ + public function unload(): void + { + // Do nothing. This method is meant to be overridden. + } + + /** + * @inheritDoc + */ + public final function renderAt(?int $x = null, ?int $y = null): void + { + $this->camera->renderAt($x, $y); + } + + /** + * @inheritDoc + */ + public final function eraseAt(?int $x = null, ?int $y = null): void + { + // Do nothing. + } + + /** + * @inheritDoc + */ + public final function loadSceneSettings(?array $settings = null): self + { + foreach ($settings as $key => $value) { + $this->settings[$key] = $value; + } + + if (isset($this->settings['screen_width']) && isset($this->settings['screen_height'])) { + $oldViewport = $this->camera->getViewport(); + $this->camera->setViewport( + new Rect( + $this->camera->getOffset(), + new Vector2( + $this->settings['screen_width'] ?? $oldViewport->getWidth(), + $this->settings['screen_height'] ?? $oldViewport->getHeight() + ) + ) + ); + } + + return $this; + } + + /** + * @inheritDoc + */ + public final function update(): void + { + foreach ($this->rootGameObjects as $gameObject) { + if ($gameObject->isActive()) { + $gameObject->update(); + } + } + + foreach ($this->uiElements as $uiElement) { + if ($uiElement->isActive()) { + $uiElement->update(); + } + } + + // Update the camera + $this->camera->update(); + } + + /** + * @inheritDoc + */ + public function updatePhysics(): void + { + foreach ($this->rootGameObjects as $gameObject) { + if ($gameObject->isActive()) { + $gameObject->fixedUpdate(); + } + } + $this->physics->simulate(); + } + + /** + * @inheritDoc + */ + public final function render(): void + { + $this->camera->render(); + } + + /** + * @inheritDoc + */ + public final function erase(): void + { + $this->camera->erase(); + } + + /** + * @inheritDoc + */ + public final function suspend(): void + { + Debug::info('Scene suspended: ' . $this->name); + foreach ($this->rootGameObjects as $gameObject) { + if ($gameObject->isActive()) { + $gameObject->suspend(); + } + } + + foreach ($this->uiElements as $uiElement) { + if ($uiElement->isActive()) { + $uiElement->suspend(); + } + } + } + + /** + * @inheritDoc + */ + public final function resume(): void + { + Debug::info('Scene resumed: ' . $this->name); + $this->camera->renderWorldSpace(); + + foreach ($this->rootGameObjects as $gameObject) { + if ($gameObject->isActive()) { + $gameObject->resume(); + } + } + + foreach ($this->uiElements as $uiElement) { + if ($uiElement->isActive()) { + $uiElement->resume(); + } + } + } + + /** + * @inheritDoc + */ + public final function getRootGameObjects(): array + { + return $this->rootGameObjects; + } + + /** + * @inheritDoc + */ + public final function getUIElements(): array + { + return $this->uiElements; + } + + /** + * @inheritDoc + */ + public function serialize(): string + { + return json_encode([ + 'name' => $this->name, + 'settings' => $this->settings, + 'root_game_objects' => $this->rootGameObjects, + 'ui_elements' => $this->uiElements, + ]); + } + + /** + * @inheritDoc + */ + public function unserialize(string $data): void + { + $data = json_decode($data, true); + + $this->name = $data['name']; + $this->settings = $data['settings']; + $this->rootGameObjects = $data['root_game_objects']; + $this->uiElements = $data['ui_elements']; + } + + /** + * Serializes the scene. + * + * @return array The serialized scene. + */ + public function __serialize(): array + { + return [ + 'name' => $this->name, + 'settings' => $this->settings, + 'root_game_objects' => $this->rootGameObjects, + 'ui_elements' => $this->uiElements, + ]; + } + + /** + * Deserializes the scene. + * + * @param array $data The data to unserialize. + * @return void + */ + public function __unserialize(array $data): void + { + $this->name = $data['name']; + $this->settings = $data['settings']; + $this->rootGameObjects = $data['root_game_objects']; + $this->uiElements = $data['ui_elements']; + } + + /** + * @inheritDoc + */ + public function getWorldSpace(): Grid + { + return $this->worldsSpace; + } + + /** + * Returns the collision world space. + * + * @return Grid The collision world space. + */ + public function getCollisionWorldSpace(): Grid + { + return $this->collisionWorldSpace; + } + + /** + * @inheritDoc + */ + public function add(GameObjectInterface|UIElementInterface $object): void + { + Debug::info('Adding game object ' . $object->getName()); + if ($object instanceof GameObjectInterface) { + $this->rootGameObjects[] = $object; + if ($collider = $object->getComponent(ColliderInterface::class)) { + $this->physics->addCollider($collider); + } + } else { + $this->uiElements[] = $object; + } + + if ($this->isStarted()) { + $object->start(); + } + } + + /** + * @inheritDoc + */ + public function getName(): string + { + return $this->name; + } + + /** + * @inheritDoc + */ + public function isStarted(): bool + { + return $this->started; + } + + /** + * Starts the scene. + * + * @inheritDoc + */ + public final function start(): void + { + Debug::info("Scene started: " . $this->name); + + $this->createWorldSpace(); + $this->loadStaticEnvironment(); + + foreach ($this->rootGameObjects as $gameObject) { + $gameObject->start(); + } + + foreach ($this->uiElements as $uiElement) { + $uiElement->start(); + } + + $this->started = true; + } + + /** + * Creates the world space. + */ + private function createWorldSpace(): void + { + Debug::info('Creating world space for ' . $this->name); + $width = $this->settings['screen_width']; + $height = $this->settings['screen_height']; + + $this->worldsSpace = new Grid($width, $height, ' '); + $this->collisionWorldSpace = new Grid($width, $height, 0); + } + + private function loadStaticEnvironment(): void + { + Debug::info('Loading static environment for ' . $this->name); + + // Return if no tile map data + if (!$this->environmentTileMapData) { + Debug::warn("No environment tile map data found for " . $this->name); + return; + } + + // Parse the tile map data + $buffer = new Grid(); + $lines = explode("\n", $this->environmentTileMapData); + + foreach ($lines as $y => $line) { + $lineLength = strlen($line); + for ($x = 0; $x < $lineLength; $x++) { + $buffer->set($x, $y, $line[$x]); + } + } + + // Fill the world space with the static tiles + $this->setWorldSpace($buffer); + + $this->camera->renderWorldSpace(); + } + + /** + * Sets the world space. + * + * @param Grid $worldSpace The new world space. + * @return void + */ + private function setWorldSpace(Grid $worldSpace): void + { + Debug::info('Setting world space for ' . $this->name); + $this->worldsSpace = $worldSpace; + } + + /** + * @inheritDoc + */ + public function remove(UIElementInterface|GameObjectInterface $object): void + { + Debug::info('Removing game object ' . $object->getName()); + if ($object instanceof GameObjectInterface) { + $this->rootGameObjects = array_filter($this->rootGameObjects, fn($item) => $item !== $object, $this->rootGameObjects); + if ($collider = $object->getComponent('Collider')) { + $this->physics->removeCollider($collider); + } + } else { + $this->uiElements = array_filter($this->uiElements, fn($item) => $item !== $object, $this->uiElements); + } + + if ($this->isStopped()) { + $object->stop(); + } + } + + /** + * @inheritDoc + */ + public function isStopped(): bool + { + return !$this->isStarted(); + } + + /** + * Stops the scene. + * + * @inheritDoc + */ + public final function stop(): void + { + Debug::info("Scene stopped: " . $this->name); + + foreach ($this->rootGameObjects as $gameObject) { + $gameObject->stop(); + } + + foreach ($this->uiElements as $uiElement) { + $uiElement->stop(); + } + + $this->getCamera()->clearScreen(); + + $this->started = false; + } + + /** + * Returns the camera. + * + * @return CameraInterface The camera. + */ + public function getCamera(): CameraInterface + { + return $this->camera; + } + + /** + * @inheritDoc + */ + public function getSettings(?string $key): mixed + { + return $this->settings[$key] ?? $this->settings; + } + + /** + * @inheritDoc + */ + public function getSceneManager(): SceneManager + { + return SceneManager::getInstance(); + } + + /** + * Sets the collision world space. + * + * @param Grid $worldSpace The new collision world space. + * @return void + */ + private function setCollisionWorldSpace(Grid $worldSpace): void + { + $this->collisionWorldSpace = $worldSpace; + } } \ No newline at end of file diff --git a/src/Core/Scenes/SceneManager.php b/src/Core/Scenes/SceneManager.php index bd8c76d..7fcc539 100644 --- a/src/Core/Scenes/SceneManager.php +++ b/src/Core/Scenes/SceneManager.php @@ -3,6 +3,7 @@ namespace Sendama\Engine\Core\Scenes; use Assegai\Collections\ItemList; +use Sendama\Engine\Core\GameObject; use Sendama\Engine\Core\Interfaces\CanLoad; use Sendama\Engine\Core\Interfaces\CanRender; use Sendama\Engine\Core\Interfaces\CanResume; @@ -11,14 +12,19 @@ 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\Vector2; use Sendama\Engine\Debug\Debug; use Sendama\Engine\Events\Enumerations\SceneEventType; use Sendama\Engine\Events\EventManager; use Sendama\Engine\Events\SceneEvent; use Sendama\Engine\Exceptions\IncorrectComponentTypeException; +use Sendama\Engine\Exceptions\Scenes\SceneManagementException; use Sendama\Engine\Exceptions\Scenes\SceneNotFoundException; use Sendama\Engine\Physics\Interfaces\ColliderInterface; use Sendama\Engine\Physics\Physics; +use Sendama\Engine\Util\Path; +use function dispatchEvent; /** * Class SceneManager. Manages the scenes of the game. @@ -27,293 +33,391 @@ */ final class SceneManager implements SingletonInterface, CanStart, CanResume, CanUpdate, CanRender, CanLoad { - /** - * @var SceneManager|null $instance The instance of the SceneManager. - */ - protected static ?SceneManager $instance = null; - /** - * @var ItemList $scenes The list of scenes. - */ - protected ItemList $scenes; - /** - * @var array $settings The settings for the SceneManager. - */ - protected array $settings = []; - /** - * @var SceneNodeInterface|null $activeSceneNode The currently active scene node. - */ - protected ?SceneNodeInterface $activeSceneNode = null; - /** - * @var EventManager $eventManager The event manager. - */ - protected EventManager $eventManager; - protected Physics $physics; - - /** - * Constructs a SceneManager - */ - private final function __construct() - { - $this->eventManager = EventManager::getInstance(); - $this->scenes = new ItemList(SceneInterface::class); - $this->physics = Physics::getInstance(); - } - - /** - * @inheritDoc - * - * @return self - */ - public static function getInstance(): self - { - if (!self::$instance) { - self::$instance = new self(); + public const string SCENE_FILE_EXTENSION = '.scene.php'; + + /** + * @var SceneManager|null $instance The instance of the SceneManager. + */ + protected static ?SceneManager $instance = null; + /** + * @var ItemList $scenes The list of scenes. + */ + protected ItemList $scenes; + /** + * @var array $settings The settings for the SceneManager. + */ + protected array $settings = []; + /** + * @var SceneNodeInterface|null $activeSceneNode The currently active scene node. + */ + protected ?SceneNodeInterface $activeSceneNode = null; + /** + * @var EventManager $eventManager The event manager. + */ + protected EventManager $eventManager; + protected Physics $physics; + + /** + * Constructs a SceneManager + */ + private final function __construct() + { + $this->eventManager = EventManager::getInstance(); + $this->scenes = new ItemList(SceneInterface::class); + $this->physics = Physics::getInstance(); + } + + /** + * @inheritDoc + * + * @return self + */ + public static function getInstance(): self + { + if (!self::$instance) { + self::$instance = new self(); + } + + return self::$instance; + } + + /** + * Returns the currently active scene. + * + * @return SceneInterface|null + */ + public function getActiveScene(): ?SceneInterface + { + return $this->activeSceneNode?->getScene(); + } + + /** + * Adds a scene to the SceneManager. + * + * @param SceneInterface $scene The scene to add. + * @param mixed|null $data The data to associate with the scene. + * @return $this The SceneManager instance. + */ + public function addScene(SceneInterface $scene, mixed $data = null): self + { + $this->scenes->add($scene); + + return $this; + } + + /** + * Removes a scene from the SceneManager. + * + * @param SceneInterface $scene The scene to remove. + * @return $this The SceneManager instance. + */ + public function removeScene(SceneInterface $scene): self + { + $this->scenes->remove($scene); + + return $this; + } + + /** + * Loads the previous scene. + * + * @return $this The SceneManager instance. + * @throws SceneNotFoundException If the previous scene is not found. + */ + public function loadPreviousScene(): self + { + Debug::info("Loading previous scene"); + + if ($this->getPreviousSceneNode()) { + return $this->loadScene($this->getPreviousSceneNode()->getScene()->getName()); + } + + return $this; + } + + /** + * Returns the previous scene. + * + * @return SceneNodeInterface|null The previous scene. + */ + public function getPreviousSceneNode(): ?SceneNodeInterface + { + return $this->activeSceneNode?->getPreviousNode(); + } + + /** + * Loads the scene with the given index. + * + * @param int|string $index The index of the scene to load. If a string is provided, the scene with the name will be + * loaded. If an integer is provided, the scene at the index will be loaded. + * @return $this The SceneManager instance. + * + * @throws SceneNotFoundException + */ + public function loadScene(int|string $index): self + { + Debug::info("Loading scene: $index"); + dispatchEvent(new SceneEvent(SceneEventType::LOAD_START)); + + $sceneToBeLoaded = null; + + $scenes = $this->scenes->toArray(); + /** + * @var SceneInterface $scene + */ + foreach ($scenes as $i => $scene) { + if (is_int($index) && $i === $index) { + $sceneToBeLoaded = $scene; + break; + } + + if (is_string($index) && $scene->getName() === $index) { + $sceneToBeLoaded = $scene; + break; + } + } + + if (!$sceneToBeLoaded) { + throw new SceneNotFoundException($index); + } + + $this->stop(); + $this->unload(); + $this->activeSceneNode = new SceneNode($sceneToBeLoaded->loadSceneSettings($this->settings), $this->activeSceneNode); + $this->load(); + + $this->start(); + return $this; + } + + /** + * @inheritDoc + */ + public function stop(): void + { + $this->activeSceneNode?->getScene()->stop(); + } + + /** + * @inheritDoc + */ + public function start(): void + { + $this->activeSceneNode?->getScene()->start(); + } + + /** + * @inheritDoc + */ + public function render(): void + { + $this->activeSceneNode?->getScene()->render(); + } + + /** + * @inheritDoc + */ + public function renderAt(?int $x = null, ?int $y = null): void + { + $this->activeSceneNode?->getScene()->renderAt($x, $y); + } + + /** + * @inheritDoc + */ + public function erase(): void + { + $this->activeSceneNode?->getScene()->erase(); + } + + /** + * @inheritDoc + */ + public function eraseAt(?int $x = null, ?int $y = null): void + { + $this->activeSceneNode?->getScene()->eraseAt($x, $y); } - return self::$instance; - } - - /** - * Returns the currently active scene. - * - * @return SceneInterface|null - */ - public function getActiveScene(): ?SceneInterface - { - return $this->activeSceneNode?->getScene(); - } - - /** - * Adds a scene to the SceneManager. - * - * @param SceneInterface $scene The scene to add. - * @param mixed|null $data The data to associate with the scene. - * @return $this The SceneManager instance. - */ - public function addScene(SceneInterface $scene, mixed $data = null): self - { - $this->scenes->add($scene); - - return $this; - } - - /** - * Removes a scene from the SceneManager. - * - * @param SceneInterface $scene The scene to remove. - * @return $this The SceneManager instance. - */ - public function removeScene(SceneInterface $scene): self - { - $this->scenes->remove($scene); - - return $this; - } - - /** - * Loads the previous scene. - * - * @return $this The SceneManager instance. - * @throws SceneNotFoundException If the previous scene is not found. - */ - public function loadPreviousScene(): self - { - Debug::info("Loading previous scene"); - - if ($this->getPreviousSceneNode()) { - return $this->loadScene($this->getPreviousSceneNode()->getScene()->getName()); + /** + * @inheritDoc + */ + public function resume(): void + { + $this->activeSceneNode?->getScene()->resume(); } - return $this; - } - - /** - * Returns the previous scene. - * - * @return SceneNodeInterface|null The previous scene. - */ - public function getPreviousSceneNode(): ?SceneNodeInterface - { - return $this->activeSceneNode?->getPreviousNode(); - } - - /** - * Loads the scene with the given index. - * - * @param int|string $index The index of the scene to load. If a string is provided, the scene with the name will be - * loaded. If an integer is provided, the scene at the index will be loaded. - * @return $this The SceneManager instance. - * - * @throws SceneNotFoundException - */ - public function loadScene(int|string $index): self - { - Debug::info("Loading scene: $index"); - $this->eventManager->dispatchEvent(new SceneEvent(SceneEventType::LOAD_START)); - - $sceneToBeLoaded = null; - - $scenes = $this->scenes->toArray(); /** - * @var SceneInterface $scene + * @inheritDoc */ - foreach ($scenes as $i => $scene) { - if (is_int($index) && $i === $index) { - $sceneToBeLoaded = $scene; - break; - } - - if (is_string($index) && $scene->getName() === $index) { - $sceneToBeLoaded = $scene; - break; - } + public function suspend(): void + { + $this->activeSceneNode?->getScene()->suspend(); } - if (!$sceneToBeLoaded) { - throw new SceneNotFoundException($index); + public function updatePhysics(): void + { + if ($this->activeSceneNode) { + $this->activeSceneNode->getScene()->updatePhysics(); + dispatchEvent(new SceneEvent(SceneEventType::UPDATE_PHYSICS, $this->activeSceneNode->getScene())); + } } - $this->stop(); - $this->unload(); - $this->activeSceneNode = new SceneNode($sceneToBeLoaded->loadSceneSettings($this->settings), $this->activeSceneNode); - $this->load(); - - $this->start(); - return $this; - } - - /** - * @inheritDoc - */ - public function stop(): void - { - $this->activeSceneNode?->getScene()->stop(); - } - - /** - * @inheritDoc - */ - public function start(): void - { - $this->activeSceneNode?->getScene()->start(); - } - - /** - * @inheritDoc - */ - public function render(): void - { - $this->activeSceneNode?->getScene()->render(); - } - - /** - * @inheritDoc - */ - public function renderAt(?int $x = null, ?int $y = null): void - { - $this->activeSceneNode?->getScene()->renderAt($x, $y); - } - - /** - * @inheritDoc - */ - public function erase(): void - { - $this->activeSceneNode?->getScene()->erase(); - } - - /** - * @inheritDoc - */ - public function eraseAt(?int $x = null, ?int $y = null): void - { - $this->activeSceneNode?->getScene()->eraseAt($x, $y); - } - - /** - * @inheritDoc - */ - public function resume(): void - { - $this->activeSceneNode?->getScene()->resume(); - } - - /** - * @inheritDoc - */ - public function suspend(): void - { - $this->activeSceneNode?->getScene()->suspend(); - } - - public function updatePhysics(): void - { - if ($this->activeSceneNode) { - $this->activeSceneNode->getScene()->updatePhysics(); - $this->eventManager->dispatchEvent(new SceneEvent(SceneEventType::UPDATE_PHYSICS, $this->activeSceneNode->getScene())); + /** + * @inheritDoc + */ + public function update(): void + { + $this->updatePhysics(); + + if ($this->activeSceneNode) { + $this->activeSceneNode->getScene()->update(); + dispatchEvent(new SceneEvent(SceneEventType::UPDATE, $this->activeSceneNode->getScene())); + } } - } - - /** - * @inheritDoc - */ - public function update(): void - { - $this->updatePhysics(); - - if ($this->activeSceneNode) { - $this->activeSceneNode->getScene()->update(); - $this->eventManager->dispatchEvent(new SceneEvent(SceneEventType::UPDATE, $this->activeSceneNode->getScene())); + + /** + * Loads the settings for the SceneManager. + * + * @param array $settings + */ + public function loadSettings(?array $settings = null): void + { + if ($settings) { + $this->settings = $settings; + } } - } - - /** - * Loads the settings for the SceneManager. - * - * @param array $settings - */ - public function loadSettings(?array $settings = null): void - { - if ($settings) { - $this->settings = $settings; + + /** + * Returns the settings for the SceneManager. + * + * @param string|null $key + * @return mixed + */ + public function getSettings(?string $key = null): mixed + { + return $this->settings[$key] ?? $this->settings; } - } - - /** - * Returns the settings for the SceneManager. - * - * @param string|null $key - * @return mixed - */ - public function getSettings(?string $key = null): mixed - { - return $this->settings[$key] ?? $this->settings; - } - - /** - * @inheritDoc - */ - public function load(): void - { - $this->physics->init(); - foreach ($this->activeSceneNode->getScene()->getRootGameObjects() as $gameObject) { - if ($collider = $gameObject->getComponent(ColliderInterface::class)) { - assert($collider instanceof ColliderInterface, new IncorrectComponentTypeException( - ColliderInterface::class, - get_class($collider) - )); - $this->physics->addCollider($collider);; - } + + /** + * @inheritDoc + */ + public function load(): void + { + $this->physics->init(); + foreach ($this->activeSceneNode->getScene()->getRootGameObjects() as $gameObject) { + if ($collider = $gameObject->getComponent(ColliderInterface::class)) { + assert($collider instanceof ColliderInterface, new IncorrectComponentTypeException( + ColliderInterface::class, + get_class($collider) + )); + $this->physics->addCollider($collider);; + } + } + $this->activeSceneNode->getScene()->load(); + dispatchEvent(new SceneEvent(SceneEventType::LOAD_END)); + } + + /** + * @inheritDoc + */ + public function unload(): void + { + $this->activeSceneNode?->getScene()->unload(); + } + + /** + * Loads a scene from a file. + * + * @param string $path The path to the scene file without the extension. + * @return void + * @throws SceneNotFoundException + */ + public function loadSceneFromFile(string $path): void + { + $filename = $path . self::SCENE_FILE_EXTENSION; + + if (!file_exists($filename)) { + throw new SceneNotFoundException($path); + } + + $sceneMetadata = require($filename); + $sceneMetadata = json_decode(json_encode($sceneMetadata, JSON_UNESCAPED_SLASHES), false); + + $sceneName = $sceneMetadata->name ?? basename($path); + + $scene = new class($sceneName, $sceneMetadata) extends AbstractScene { + 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); + } + + $rotation = new Vector2(); + if (isset($rootGameObject->rotation)) { + $rotation = Vector2::fromArray((array)$rootGameObject->rotation); + } + + $scale = new Vector2(); + if (isset($rootGameObject->scale)) { + $scale = Vector2::fromArray((array)$rootGameObject->scale); + } + + $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); + } + + 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()); + } + + $componentClass = $componentMetadata->class; + $gameObject->addComponent($componentClass); + } + } + + $this->add($gameObject); + } + } + } + }; + + $this->addScene($scene); } - $this->activeSceneNode->getScene()->load(); - $this->eventManager->dispatchEvent(new SceneEvent(SceneEventType::LOAD_END)); - } - - /** - * @inheritDoc - */ - public function unload(): void - { - $this->activeSceneNode?->getScene()->unload(); - } } \ No newline at end of file diff --git a/src/Core/Texture2D.php b/src/Core/Texture2D.php index 6de6d6a..b95334e 100644 --- a/src/Core/Texture2D.php +++ b/src/Core/Texture2D.php @@ -15,170 +15,173 @@ */ class Texture2D implements Stringable { - use DimensionTrait; - - /** - * The texture extension. - */ - const string TEXTURE_EXTENSION = '.texture'; - - /** - * The pixels of the texture. - * - * @var string[][] The pixels of the texture. - */ - protected array $pixels = []; - - /** - * Creates a new instance of the Texture2D class. - * - * @param string $path The path to the image file. - * @param int $width The width of the texture. - * @param int $height The height of the texture. - */ - public function __construct(private readonly string $path, int $width = -1, int $height = -1, private ?Color $color = null, protected array $options = []) - { - if (!str_ends_with($this->getAbsolutePath(), self::TEXTURE_EXTENSION)) { - throw new InvalidArgumentException("The file '" . $this->getAbsolutePath() . "' is not a valid texture file."); + use DimensionTrait; + + /** + * The texture extension. + */ + const string TEXTURE_EXTENSION = '.texture'; + + /** + * The pixels of the texture. + * + * @var string[][] The pixels of the texture. + */ + protected array $pixels = []; + private string $path; + + /** + * Creates a new instance of the Texture2D class. + * + * @param string $path The path to the image file. + * @param int $width The width of the texture. + * @param int $height The height of the texture. + */ + public function __construct(string $path, int $width = -1, int $height = -1, private ?Color $color = null, protected array $options = []) + { + $this->path = $path; + + if (!str_ends_with($this->getAbsolutePath(), self::TEXTURE_EXTENSION)) { + $this->path .= self::TEXTURE_EXTENSION; + } + + if (!file_exists($this->getAbsolutePath())) { + throw new InvalidArgumentException("The file '" . $this->getAbsolutePath() . "' does not exist."); + } + + $this->setWidth($width); + $this->setHeight($height); + + $this->loadImage(); } - if (!file_exists($this->getAbsolutePath())) { - throw new InvalidArgumentException("The file '" . $this->getAbsolutePath() . "' does not exist."); + /** + * Returns the absolute path to the texture. + * + * @return string The absolute path to the texture. + */ + private function getAbsolutePath(): string + { + if (str_starts_with($this->path, '/')) { + return $this->path; + } + + return Path::join(Path::getWorkingDirectoryAssetsPath(), $this->path); } - $this->setWidth($width); - $this->setHeight($height); - - $this->loadImage(); - } - - /** - * Returns the absolute path to the texture. - * - * @return string The absolute path to the texture. - */ - private function getAbsolutePath(): string - { - if (str_starts_with($this->path, '/')) { - return $this->path; + /** + * Loads the image from the specified path. + */ + protected function loadImage(): void + { + // Load the image. + $image = file_get_contents($this->getAbsolutePath()); + + if ($this->color) { + $image = Color::apply($this->color, to: $image); + } + + // Convert the image to an array of pixels. + $imageMatrix = explode("\n", $image); + $height = 0; + $longestRow = 0; + + foreach ($imageMatrix as $row) { + $width = $this->width < 1 ? strlen($row) : $this->width; + $chunks = str_split(substr($row, 0, $width)); + $this->pixels[] = $chunks; + $longestRow = max($longestRow, $width); + $height++; + } + + if ($this->width < 1) { + $this->setWidth($longestRow); + } + + if ($this->height < 1) { + $this->setHeight($height); + } } - return Path::join(Path::getWorkingDirectoryAssetsPath(), $this->path); - } - - /** - * Loads the image from the specified path. - */ - protected function loadImage(): void - { - // Load the image. - $image = file_get_contents($this->getAbsolutePath()); - - if ($this->color) { - $image = Color::apply($this->color, to: $image); + /** + * Sets the color of the texture. + * + * @param Color|null $color The color to set. + * @return void + */ + public function setColor(?Color $color): void + { + $this->color = $color; } - // Convert the image to an array of pixels. - $imageMatrix = explode("\n", $image); - $height = 0; - $longestRow = 0; - - foreach ($imageMatrix as $row) { - $width = $this->width < 1 ? strlen($row) : $this->width; - $chunks = str_split(substr($row, 0, $width)); - $this->pixels[] = $chunks; - $longestRow = max($longestRow, $width); - $height++; + /** + * Returns the color of the texture. + * + * @return Color|null The color of the texture. + */ + public function getColor(): ?Color + { + return $this->color; } - if ($this->width < 1) { - $this->setWidth($longestRow); + /** + * Returns the pixel at the specified coordinates. + * + * @param int $x The x coordinate. + * @param int $y The y coordinate. + * @return string The pixel at the specified coordinates. + */ + public function getPixel(int $x, int $y): string + { + if (!isset($this->pixels[$y][$x])) { + throw new InvalidArgumentException("The pixel at ($x, $y) does not exist."); + } + + return $this->pixels[$y][$x]; } - if ($this->height < 1) { - $this->setHeight($height); - } - } - - /** - * Sets the color of the texture. - * - * @param Color|null $color The color to set. - * @return void - */ - public function setColor(?Color $color): void - { - $this->color = $color; - } - - /** - * Returns the color of the texture. - * - * @return Color|null The color of the texture. - */ - public function getColor(): ?Color - { - return $this->color; - } - - /** - * Returns the pixel at the specified coordinates. - * - * @param int $x The x coordinate. - * @param int $y The y coordinate. - * @return string The pixel at the specified coordinates. - */ - public function getPixel(int $x, int $y): string - { - if (!isset($this->pixels[$y][$x])) { - throw new InvalidArgumentException("The pixel at ($x, $y) does not exist."); + /** + * Sets the pixel at the specified coordinates. + * + * @param int $x The x coordinate. + * @param int $y The y coordinate. + * @param string $pixel The pixel to set. + */ + public function setPixel(int $x, int $y, string $pixel): void + { + if ($x < 0 || $x >= $this->width || $y < 0 || $y >= $this->height) { + throw new InvalidArgumentException("The pixel at ($x, $y) does not fall within range (0, 0) to ($this->width, $this->height)."); + } + + $output = $pixel; + + if ($this->color) { + $output = Color::apply($this->color, to: $output); + } + + $this->pixels[$y][$x] = substr($output, 0, 1); } - return $this->pixels[$y][$x]; - } - - /** - * Sets the pixel at the specified coordinates. - * - * @param int $x The x coordinate. - * @param int $y The y coordinate. - * @param string $pixel The pixel to set. - */ - public function setPixel(int $x, int $y, string $pixel): void - { - if ($x < 0 || $x >= $this->width || $y < 0 || $y >= $this->height) { - throw new InvalidArgumentException("The pixel at ($x, $y) does not fall within range (0, 0) to ($this->width, $this->height)."); - } + public function __toString(): string + { + $output = ''; - $output = $pixel; + foreach ($this->getPixels() as $pixel) { + if (is_array($pixel)) { + $output .= implode($pixel); + } + } - if ($this->color) { - $output = Color::apply($this->color, to: $output); + return $output; } - $this->pixels[$y][$x] = substr($output, 0, 1); - } - - /** - * Returns the pixels of the texture. - * - * @return string[][] The pixels of the texture. - */ - public function getPixels(): array - { - return $this->pixels; - } - - public function __toString(): string - { - $output = ''; - - foreach ($this->getPixels() as $pixel) { - if (is_array($pixel)) { - $output .= implode($pixel); - } + /** + * Returns the pixels of the texture. + * + * @return string[][] The pixels of the texture. + */ + public function getPixels(): array + { + return $this->pixels; } - - return $output; - } } \ No newline at end of file diff --git a/src/Core/Vector2.php b/src/Core/Vector2.php index e684b13..3b40158 100644 --- a/src/Core/Vector2.php +++ b/src/Core/Vector2.php @@ -7,516 +7,521 @@ class Vector2 implements CanEquate, Stringable { - /** - * Vector2 constructor. - * - * @param int $x The x coordinate. - * @param int $y The y coordinate. - */ - public function __construct(protected int $x = 0, protected int $y = 0) - { - } - - /** - * @param array{x: int, y: int} $vector - * @return Vector2 - */ - public static function fromArray(array $vector): Vector2 - { - [$x, $y] = $vector; - - return new Vector2($x, $y); - } - - /** - * Shortcut for Vector2(0, 0). - * - * @return Vector2 Returns a new Vector2(0, 0). - */ - public static function zero(): Vector2 - { - return new Vector2(0, 0); - } - - /** - * Shortcut for Vector2(1, 1). - * - * @return Vector2 Returns a new Vector2(1, 1). - */ - public static function one(): Vector2 - { - return new Vector2(1, 1); - } - - /** - * Shortcut for Vector2(-1, 0). - * - * @return Vector2 Returns a new Vector2(-1, 0). - */ - public static function left(): Vector2 - { - return new Vector2(-1, 0); - } - - /** - * Shortcut for Vector2(1, 0). - * - * @return Vector2 Returns a new Vector2(1, 0). - */ - public static function right(): Vector2 - { - return new Vector2(1, 0); - } - - /** - * Shortcut for Vector2(0, 1). - * - * @return Vector2 Returns a new Vector2(0, 1). - */ - public static function up(): Vector2 - { - return new Vector2(0, 1); - } - - /** - * Shortcut for Vector2(0, -1). - * - * @return Vector2 Returns a new Vector2(0, -1). - */ - public static function down(): Vector2 - { - return new Vector2(0, -1); - } - - /** - * Gets a clone of the given vector. - * - * @param Vector2 $original The original vector. - * @return self The clone. - */ - public static function getClone(Vector2 $original): self - { - return new Vector2($original->getX(), $original->getY()); - } - - /* Getters and Setters */ - - /** - * Calculates the sum of the given vectors. The first vector is the augend, the rest are the addends. - * - * @param Vector2 ...$vectors - * @return self This vector. - */ - public static function sum(Vector2 ...$vectors): self - { - $result = new Vector2(); - - foreach ($vectors as $vector) { - $result->add($vector); - } - - return $result; - } - - /** - * Adds the given vector to this vector. - * - * @param Vector2 $other The vector to add. - * @return void - */ - public function add(Vector2 $other): void - { - $this->setX($this->getX() + $other->getX()); - $this->setY($this->getY() + $other->getY()); - } - - /** - * Calculates the product of the given vectors. The first vector is the multiplicand, the rest are the multipliers. - * - * @param Vector2 ...$vectors The vectors to multiply. - * @return self The product. - */ - public static function product(Vector2 ...$vectors): self - { - $result = new Vector2(); - - foreach ($vectors as $index => $vector) { - if ($index === 0) { - $result = $vector; - continue; - } - $result->setX($result->getX() * $vector->getX()); - $result->setY($result->getY() * $vector->getY()); - } - - return $result; - } - - /** - * Multiplies a vector by a number. Multiplies each component of the vector by the scalar. - * - * @param int|float $scalar The scalar to multiply by. - * @return void - */ - public function multiply(int|float $scalar): void - { - $this->setX($this->getX() * $scalar); - $this->setY($this->getY() * $scalar); - } - - /** - * Calculates the quotient of the given vectors. The first vector is the dividend, the rest are the divisors. - * - * @param Vector2 ...$vectors The vectors to divide. - * @return self The quotient. - */ - public static function quotient(Vector2 ...$vectors): self - { - $result = new Vector2(); - - foreach ($vectors as $vector) { - $result->setX($result->getX() / $vector->getX()); - $result->setY($result->getY() / $vector->getY()); - } - - return $result; - } - - /** - * Divides a vector by a number. Divides each component of the vector by the scalar. - * - * @param int|float $scalar The scalar to divide by. - * @return void - */ - public function divide(int|float $scalar): void - { - $this->setX(intval($this->getX() / $scalar)); - $this->setY(intval($this->getY() / $scalar)); - } - - /** - * Calculates the distance between two vectors. It is the same as ($a - $b).getMagnitude(). - * - * @param Vector2 $a The first vector. - * @param Vector2 $b The second vector. - * @return float The distance between the two vectors. - */ - public static function distance(Vector2 $a, Vector2 $b): float - { - return Vector2::difference($a, $b)->getMagnitude(); - } - - /** - * Calculates the difference of the given vectors. The first vector is the minuend, the rest are the subtrahends. - * - * @param Vector2 ...$vectors The vectors to subtract. - * @return self The difference. - */ - public static function difference(Vector2 ...$vectors): self - { - $result = new Vector2(); - - foreach ($vectors as $index => $vector) { - if ($index === 0) { - $result = $vector; - continue; - } - $result->subtract($vector); - } - - return $result; - } - - /* Static methods */ - - /** - * Subtracts the given vector from this vector. - * - * @param Vector2 $other The vector to subtract. - * @return void - */ - public function subtract(Vector2 $other): void - { - $this->setX($this->getX() - $other->getX()); - $this->setY($this->getY() - $other->getY()); - } - - /** - * Gets the string representation of this vector. - * - * @return string The string representation of this vector. - */ - public function __toString(): string - { - return "($this->x, $this->y)"; - } - - /** - * Returns a new vector that is the normalized version of this vector. - * - * @return Vector2 The normalized vector. - */ - public function getNormalized(): Vector2 - { - $magnitude = $this->getMagnitude(); - - if ($magnitude > PHP_FLOAT_MIN) { - return new Vector2( - (int) round($this->x / $magnitude), - (int) round($this->y / $magnitude) - ); - } - - return new Vector2(0, 0); - } - - /** - * Returns the length of this vector. - * - * @return float - */ - public function getMagnitude(): float - { - return sqrt($this->getSquareMagnitude()); - } - - /** - * Gets the square magnitude of this vector. - * - * @return float The square magnitude of this vector. - */ - public function getSquareMagnitude(): float - { - return $this->x * $this->x + $this->y * $this->y; - } - - public function normalize(): void - { - $length = $this->getMagnitude(); - - if (abs($length) > PHP_FLOAT_MIN) { - $this->setX((int)($this->getX() / $length)); - $this->setY((int)($this->getY() / $length)); - } - } - - /* Operator methods */ - - /** - * Sets the x coordinate. - * - * @param int $x The x coordinate. - * @return Vector2 - */ - public function setX(int $x): self - { - $this->x = $x; - - return $this; - } - - /** - * Gets the x coordinate. - * - * @return int - */ - public function getX(): int - { - return $this->x; - } - - /** - * Sets the y coordinate. - * - * @param int $y The y coordinate. - * @return Vector2 - */ - public function setY(int $y): self - { - $this->y = $y; - - return $this; - } - - /** - * Gets the y coordinate. - * - * @return int - */ - public function getY(): int - { - return $this->y; - } - - /** - * Returns true if the given equatable is not equal to this equatable. - * - * @param CanEquate $equatable The equatable to compare. - * @return bool True if the given equatable is not equal to this equatable. - */ - public function notEquals(CanEquate $equatable): bool - { - return !$this->equals($equatable); - } - - /** - * Returns true if the given equatable is equal to this equatable. - * - * @param CanEquate $equatable The equatable to compare. - * @return bool True if the given equatable is equal to this equatable. - */ - public function equals(CanEquate $equatable): bool - { - return $this->getHash() === $equatable->getHash(); - } - - /** - * Gets the hash of this equatable. - * - * @return string The hash of this equatable. - */ - public function getHash(): string - { - return uniqid(md5(__CLASS__) . '.' . md5($this->x . '.' . $this->y)); - } - - /** - * Linearly interpolates between two vectors. - * - * @param Vector2 $a The first vector. - * @param Vector2 $b The second vector. - * @param float $t The interpolation value. Should be between 0 and 1. - * - * @return Vector2 The interpolated vector. - */ - public static function lerp(Vector2 $a, Vector2 $b, float $t): Vector2 - { - $t = clamp($t, 0, 1); - - $x = lerp($a->getX(), $b->getX(), $t); - $y = lerp($a->getY(), $b->getY(), $t); - - return new Vector2($x, $y); - } - - /** - * Scales this vector by the given scalar. - * - * @param int $scalar The scalar to scale by. - * @return void - */ - public function scale(int $scalar): void - { - $this->setX($this->getX() * $scalar); - $this->setY($this->getY() * $scalar); - } - - /** - * Returns the dot product of two vectors. For normalized vectors dot returns 1 if they point in exactly the same - * direction, -1 if they point in completely opposite directions and 0 if the vectors are perpendicular. - * - * For vectors of arbitrary length the Dot return values are similar: they get larger when the angle between - * vectors decreases. - * - * @param Vector2 $lhs The left-hand side vector. - * @param Vector2 $rhs The right-hand side vector. - * @return float The dot product of the two vectors. - */ - public static function dot(Vector2 $lhs, Vector2 $rhs): float - { - return $lhs->getX() * $rhs->getX() + $lhs->getY() * $rhs->getY(); - } - - /** - * Returns the angle between two vectors. - * - * @param Vector2 $from The first vector. - * @param Vector2 $to The second vector. - * @return float The angle between the two vectors. - */ - public static function angle(Vector2 $from, Vector2 $to): float - { - $dotProduct = self::dot($from, $to); - $magnitudes = $from->getMagnitude() * $to->getMagnitude(); - - // Prevent division by zero - if ($magnitudes == 0) { - return 0.0; - } - - $angleInRadians = acos($dotProduct / $magnitudes); - - // Convert radians to degrees - return rad2deg($angleInRadians); - } - - /** - * Returns a vector that is made up of the largest components of two vectors. - * - * @param Vector2 $lhs The first vector. - * @param Vector2 $rhs The second vector. - * @return Vector2 - */ - public static function max(Vector2 $lhs, Vector2 $rhs): Vector2 - { - return new Vector2(max($lhs->getX(), $rhs->getX()), max($lhs->getY(), $rhs->getY())); - } - - /** - * Returns a vector that is made up of the largest components of two vectors. - * - * @param Vector2 $lhs The first vector. - * @param Vector2 $rhs The second vector. - * @return Vector2 - */ - public static function min(Vector2 $lhs, Vector2 $rhs): Vector2 - { - return new Vector2(min($lhs->getX(), $rhs->getX()), min($lhs->getY(), $rhs->getY())); - } - - /** - * Returns the 2D vector perpendicular to this 2D vector. The result is always rotated 90-degrees - * in a counter-clockwise direction for a 2D coordinate system where the positive Y axis goes up. - * - * @param Vector2 $inDirection The input direction. - * @return Vector2 - */ - public static function perpendicular(Vector2 $inDirection): Vector2 - { - return new Vector2(-$inDirection->getY(), $inDirection->getX()); - } - - /** - * Reflects a vector off the surface defined by a normal. - * - * This method calculates a reflected vector using the following formula: - * `v = inDirection - 2 * inNormal * dot(inDirection inNormal).` - * The inNormal vector defines a surface. A surface's normal is the vector that is perpendicular to its surface. - * The inDirection vector is treated as a directional arrow coming into the surface. The returned value is a - * vector of equal magnitude to inDirection but with its direction reflected. - * - * @param Vector2 $inDirection The direction towards the surface. - * @param Vector2 $inNormal The normal vector that defines the surface. - * @return Vector2 - */ - public static function reflect(Vector2 $inDirection, Vector2 $inNormal): Vector2 - { - // Normalize the normal vector, ensuring integer rounding - $normalizedNormal = $inNormal->getNormalized(); - - // Compute dot product - $dotProduct = self::dot($inDirection, $normalizedNormal); - - // Scale the normal vector by (2 * dot product), ensuring integer rounding - $scaledNormal = new Vector2( - (int) round($normalizedNormal->getX() * (2 * $dotProduct)), - (int) round($normalizedNormal->getY() * (2 * $dotProduct)) - ); - - // Compute the reflected vector, ensuring integer rounding - return new Vector2( - (int) round($inDirection->x - $scaledNormal->x), - (int) round($inDirection->y - $scaledNormal->y) - ); - } + /** + * Vector2 constructor. + * + * @param int $x The x coordinate. + * @param int $y The y coordinate. + */ + public function __construct(protected int $x = 0, protected int $y = 0) + { + } + + /** + * @param array{x: int, y: int} $vector + * @return Vector2 + */ + public static function fromArray(array $vector): Vector2 + { + if (array_is_list($vector)) { + [$x, $y] = $vector; + } else { + $x = $vector['x'] ?? 0; + $y = $vector['y'] ?? 0; + } + + return new Vector2($x, $y); + } + + /** + * Shortcut for Vector2(0, 0). + * + * @return Vector2 Returns a new Vector2(0, 0). + */ + public static function zero(): Vector2 + { + return new Vector2(0, 0); + } + + /** + * Shortcut for Vector2(1, 1). + * + * @return Vector2 Returns a new Vector2(1, 1). + */ + public static function one(): Vector2 + { + return new Vector2(1, 1); + } + + /** + * Shortcut for Vector2(-1, 0). + * + * @return Vector2 Returns a new Vector2(-1, 0). + */ + public static function left(): Vector2 + { + return new Vector2(-1, 0); + } + + /** + * Shortcut for Vector2(1, 0). + * + * @return Vector2 Returns a new Vector2(1, 0). + */ + public static function right(): Vector2 + { + return new Vector2(1, 0); + } + + /** + * Shortcut for Vector2(0, 1). + * + * @return Vector2 Returns a new Vector2(0, 1). + */ + public static function up(): Vector2 + { + return new Vector2(0, 1); + } + + /** + * Shortcut for Vector2(0, -1). + * + * @return Vector2 Returns a new Vector2(0, -1). + */ + public static function down(): Vector2 + { + return new Vector2(0, -1); + } + + /** + * Gets a clone of the given vector. + * + * @param Vector2 $original The original vector. + * @return self The clone. + */ + public static function getClone(Vector2 $original): self + { + return new Vector2($original->getX(), $original->getY()); + } + + /* Getters and Setters */ + + /** + * Gets the x coordinate. + * + * @return int + */ + public function getX(): int + { + return $this->x; + } + + /** + * Gets the y coordinate. + * + * @return int + */ + public function getY(): int + { + return $this->y; + } + + /** + * Calculates the sum of the given vectors. The first vector is the augend, the rest are the addends. + * + * @param Vector2 ...$vectors + * @return self This vector. + */ + public static function sum(Vector2 ...$vectors): self + { + $result = new Vector2(); + + foreach ($vectors as $vector) { + $result->add($vector); + } + + return $result; + } + + /** + * Adds the given vector to this vector. + * + * @param Vector2 $other The vector to add. + * @return void + */ + public function add(Vector2 $other): void + { + $this->setX($this->getX() + $other->getX()); + $this->setY($this->getY() + $other->getY()); + } + + /** + * Sets the x coordinate. + * + * @param int $x The x coordinate. + * @return Vector2 + */ + public function setX(int $x): self + { + $this->x = $x; + + return $this; + } + + /** + * Sets the y coordinate. + * + * @param int $y The y coordinate. + * @return Vector2 + */ + public function setY(int $y): self + { + $this->y = $y; + + return $this; + } + + /** + * Calculates the product of the given vectors. The first vector is the multiplicand, the rest are the multipliers. + * + * @param Vector2 ...$vectors The vectors to multiply. + * @return self The product. + */ + public static function product(Vector2 ...$vectors): self + { + $result = new Vector2(); + + foreach ($vectors as $index => $vector) { + if ($index === 0) { + $result = $vector; + continue; + } + $result->setX($result->getX() * $vector->getX()); + $result->setY($result->getY() * $vector->getY()); + } + + return $result; + } + + /** + * Calculates the quotient of the given vectors. The first vector is the dividend, the rest are the divisors. + * + * @param Vector2 ...$vectors The vectors to divide. + * @return self The quotient. + */ + public static function quotient(Vector2 ...$vectors): self + { + $result = new Vector2(); + + foreach ($vectors as $vector) { + $result->setX($result->getX() / $vector->getX()); + $result->setY($result->getY() / $vector->getY()); + } + + return $result; + } + + /* Static methods */ + + /** + * Calculates the distance between two vectors. It is the same as ($a - $b).getMagnitude(). + * + * @param Vector2 $a The first vector. + * @param Vector2 $b The second vector. + * @return float The distance between the two vectors. + */ + public static function distance(Vector2 $a, Vector2 $b): float + { + return Vector2::difference($a, $b)->getMagnitude(); + } + + /** + * Returns the length of this vector. + * + * @return float + */ + public function getMagnitude(): float + { + return sqrt($this->getSquareMagnitude()); + } + + /** + * Gets the square magnitude of this vector. + * + * @return float The square magnitude of this vector. + */ + public function getSquareMagnitude(): float + { + return $this->x * $this->x + $this->y * $this->y; + } + + /** + * Calculates the difference of the given vectors. The first vector is the minuend, the rest are the subtrahends. + * + * @param Vector2 ...$vectors The vectors to subtract. + * @return self The difference. + */ + public static function difference(Vector2 ...$vectors): self + { + $result = new Vector2(); + + foreach ($vectors as $index => $vector) { + if ($index === 0) { + $result = $vector; + continue; + } + $result->subtract($vector); + } + + return $result; + } + + /** + * Subtracts the given vector from this vector. + * + * @param Vector2 $other The vector to subtract. + * @return void + */ + public function subtract(Vector2 $other): void + { + $this->setX($this->getX() - $other->getX()); + $this->setY($this->getY() - $other->getY()); + } + + /** + * Linearly interpolates between two vectors. + * + * @param Vector2 $a The first vector. + * @param Vector2 $b The second vector. + * @param float $t The interpolation value. Should be between 0 and 1. + * + * @return Vector2 The interpolated vector. + */ + public static function lerp(Vector2 $a, Vector2 $b, float $t): Vector2 + { + $t = clamp($t, 0, 1); + + $x = lerp($a->getX(), $b->getX(), $t); + $y = lerp($a->getY(), $b->getY(), $t); + + return new Vector2($x, $y); + } + + /* Operator methods */ + + /** + * Returns the angle between two vectors. + * + * @param Vector2 $from The first vector. + * @param Vector2 $to The second vector. + * @return float The angle between the two vectors. + */ + public static function angle(Vector2 $from, Vector2 $to): float + { + $dotProduct = self::dot($from, $to); + $magnitudes = $from->getMagnitude() * $to->getMagnitude(); + + // Prevent division by zero + if ($magnitudes == 0) { + return 0.0; + } + + $angleInRadians = acos($dotProduct / $magnitudes); + + // Convert radians to degrees + return rad2deg($angleInRadians); + } + + /** + * Returns the dot product of two vectors. For normalized vectors dot returns 1 if they point in exactly the same + * direction, -1 if they point in completely opposite directions and 0 if the vectors are perpendicular. + * + * For vectors of arbitrary length the Dot return values are similar: they get larger when the angle between + * vectors decreases. + * + * @param Vector2 $lhs The left-hand side vector. + * @param Vector2 $rhs The right-hand side vector. + * @return float The dot product of the two vectors. + */ + public static function dot(Vector2 $lhs, Vector2 $rhs): float + { + return $lhs->getX() * $rhs->getX() + $lhs->getY() * $rhs->getY(); + } + + /** + * Returns a vector that is made up of the largest components of two vectors. + * + * @param Vector2 $lhs The first vector. + * @param Vector2 $rhs The second vector. + * @return Vector2 + */ + public static function max(Vector2 $lhs, Vector2 $rhs): Vector2 + { + return new Vector2(max($lhs->getX(), $rhs->getX()), max($lhs->getY(), $rhs->getY())); + } + + /** + * Returns a vector that is made up of the largest components of two vectors. + * + * @param Vector2 $lhs The first vector. + * @param Vector2 $rhs The second vector. + * @return Vector2 + */ + public static function min(Vector2 $lhs, Vector2 $rhs): Vector2 + { + return new Vector2(min($lhs->getX(), $rhs->getX()), min($lhs->getY(), $rhs->getY())); + } + + /** + * Returns the 2D vector perpendicular to this 2D vector. The result is always rotated 90-degrees + * in a counter-clockwise direction for a 2D coordinate system where the positive Y axis goes up. + * + * @param Vector2 $inDirection The input direction. + * @return Vector2 + */ + public static function perpendicular(Vector2 $inDirection): Vector2 + { + return new Vector2(-$inDirection->getY(), $inDirection->getX()); + } + + /** + * Reflects a vector off the surface defined by a normal. + * + * This method calculates a reflected vector using the following formula: + * `v = inDirection - 2 * inNormal * dot(inDirection inNormal).` + * The inNormal vector defines a surface. A surface's normal is the vector that is perpendicular to its surface. + * The inDirection vector is treated as a directional arrow coming into the surface. The returned value is a + * vector of equal magnitude to inDirection but with its direction reflected. + * + * @param Vector2 $inDirection The direction towards the surface. + * @param Vector2 $inNormal The normal vector that defines the surface. + * @return Vector2 + */ + public static function reflect(Vector2 $inDirection, Vector2 $inNormal): Vector2 + { + // Normalize the normal vector, ensuring integer rounding + $normalizedNormal = $inNormal->getNormalized(); + + // Compute dot product + $dotProduct = self::dot($inDirection, $normalizedNormal); + + // Scale the normal vector by (2 * dot product), ensuring integer rounding + $scaledNormal = new Vector2( + (int)round($normalizedNormal->getX() * (2 * $dotProduct)), + (int)round($normalizedNormal->getY() * (2 * $dotProduct)) + ); + + // Compute the reflected vector, ensuring integer rounding + return new Vector2( + (int)round($inDirection->x - $scaledNormal->x), + (int)round($inDirection->y - $scaledNormal->y) + ); + } + + /** + * Returns a new vector that is the normalized version of this vector. + * + * @return Vector2 The normalized vector. + */ + public function getNormalized(): Vector2 + { + $magnitude = $this->getMagnitude(); + + if ($magnitude > PHP_FLOAT_MIN) { + return new Vector2( + (int)round($this->x / $magnitude), + (int)round($this->y / $magnitude) + ); + } + + return new Vector2(0, 0); + } + + /** + * Multiplies a vector by a number. Multiplies each component of the vector by the scalar. + * + * @param int|float $scalar The scalar to multiply by. + * @return void + */ + public function multiply(int|float $scalar): void + { + $this->setX($this->getX() * $scalar); + $this->setY($this->getY() * $scalar); + } + + /** + * Divides a vector by a number. Divides each component of the vector by the scalar. + * + * @param int|float $scalar The scalar to divide by. + * @return void + */ + public function divide(int|float $scalar): void + { + $this->setX(intval($this->getX() / $scalar)); + $this->setY(intval($this->getY() / $scalar)); + } + + /** + * Gets the string representation of this vector. + * + * @return string The string representation of this vector. + */ + public function __toString(): string + { + return "($this->x, $this->y)"; + } + + public function normalize(): void + { + $length = $this->getMagnitude(); + + if (abs($length) > PHP_FLOAT_MIN) { + $this->setX((int)($this->getX() / $length)); + $this->setY((int)($this->getY() / $length)); + } + } + + /** + * Returns true if the given equatable is not equal to this equatable. + * + * @param CanEquate $equatable The equatable to compare. + * @return bool True if the given equatable is not equal to this equatable. + */ + public function notEquals(CanEquate $equatable): bool + { + return !$this->equals($equatable); + } + + /** + * Returns true if the given equatable is equal to this equatable. + * + * @param CanEquate $equatable The equatable to compare. + * @return bool True if the given equatable is equal to this equatable. + */ + public function equals(CanEquate $equatable): bool + { + return $this->getHash() === $equatable->getHash(); + } + + /** + * Gets the hash of this equatable. + * + * @return string The hash of this equatable. + */ + public function getHash(): string + { + return uniqid(md5(__CLASS__) . '.' . md5($this->x . '.' . $this->y)); + } + + /** + * Scales this vector by the given scalar. + * + * @param int $scalar The scalar to scale by. + * @return void + */ + public function scale(int $scalar): void + { + $this->setX($this->getX() * $scalar); + $this->setY($this->getY() * $scalar); + } } \ No newline at end of file diff --git a/src/Exceptions/Scenes/SceneManagementException.php b/src/Exceptions/Scenes/SceneManagementException.php new file mode 100644 index 0000000..f83c174 --- /dev/null +++ b/src/Exceptions/Scenes/SceneManagementException.php @@ -0,0 +1,12 @@ + - */ - private array $settings = []; - - /* == Managers == */ - /** - * @var ItemList - */ - private ItemList $observers; - /** - * @var ItemList - */ - private ItemList $staticObservers; - /** - * @var int The number of frames that have been rendered. - */ - private int $frameCount = 0; - /** - * @var int The frame rate of the game. - */ - private int $frameRate = 0; - /** - * @var SceneManager $sceneManager - */ - private SceneManager $sceneManager; - /** - * @var EventManager $eventManager - */ - private EventManager $eventManager; - /** - * @var ModalManager $modalManager - */ - private ModalManager $modalManager; - - /* Sentinel properties */ - /** - * @var NotificationsManager $notificationsManager - */ - private NotificationsManager $notificationsManager; - /** - * @var UIManager $uiManager - */ - private UIManager $uiManager; - /** - * @var Cursor $consoleCursor - */ - private Cursor $consoleCursor; - /** - * @var Window $debugWindow - */ - private Window $debugWindow; - /** - * @var bool Determines if the game engine is running. - */ - private bool $isRunning = false; - /** - * @var bool Determines if a modal is showing or not. - */ - private bool $isShowingModal = false; - /** - * @var GameStateInterface $state - */ - private GameStateInterface $state; - - /** - * Game constructor. - * - * @param string $name The name of the game. - * @param int $screenWidth The width of the game screen. - * @param int $screenHeight The height of the game screen. - * @throws Exception - */ - public function __construct(private readonly string $name, private readonly int $screenWidth = DEFAULT_SCREEN_WIDTH, private readonly int $screenHeight = DEFAULT_SCREEN_HEIGHT, private readonly ?string $workingDirectory = null) - { - try { - $this->initializeObservers(); - $this->configureErrorAndExceptionHandlers(); - $this->initializeConsole(); - $this->initializeConfigStore(); - $this->initializeManagers(); - $this->initializeSettings(); - $this->initializeGameStates(); - $this->configureWindowChangeSignalHandler(); - } catch (Error|Exception|Throwable $exception) { - $this->handleException($exception); - } - } - - /** - * Destruct the game engine. - */ - public function __destruct() - { - Console::restoreSettings(); - - if ($lastError = error_get_last()) { - $this->handleError($lastError['type'], $lastError['message'], $lastError['file'], $lastError['line']); - } - } - - /** - * @return void - */ - protected function initializeObservers(): void - { - $this->observers = new ItemList(ObserverInterface::class); - $this->staticObservers = new ItemList(StaticObserverInterface::class); - } - - /** - * @return void - */ - protected function initializeConfigStore(): void - { - ConfigStore::put(AppConfig::class, new AppConfig()); - ConfigStore::put(InputConfig::class, new InputConfig()); - ConfigStore::put(PlayerPreferences::class, new PlayerPreferences()); - } - - /** - * Configure error and exception handlers. - * - * @return void - */ - protected function configureErrorAndExceptionHandlers(): void - { - error_reporting(E_ALL); - - set_exception_handler(function (Throwable|Exception|Error $exception) { - $this->handleException($exception); - }); - - // Handle errors - set_error_handler(function ($errno, $errstr, $errfile, $errline) { - $this->handleError($errno, $errstr, $errfile, $errline); - }); - - $this->debugWindow = new Window(); - } - - /** - * Handle game exceptions. - * - * @param Exception|Throwable|Error $exception The exception to be handled. - * @return never - */ - private function handleException(Exception|Throwable|Error $exception): never - { - Debug::error($exception); - $this->stop(); - - if ($this->getSettings('debug')) { - exit($exception); - } - - exit("$exception\n"); - } - - /** - * Stop the game. - * - * @return void - */ - public function stop(): void - { - Console::reset(); - - Debug::info("Stopping game"); - - // Disable non-blocking input mode - InputManager::disableNonBlockingMode(); - - // Enable echo - InputManager::enableEcho(); - - // Show cursor - $this->consoleCursor->show(); - - // Restore the terminal settings - Console::restoreSettings(); - - // Remove observers - $this->removeObservers(); - - // Stop the game - $this->isRunning = false; - - // Notify listeners that the game has stopped - $this->notify(new GameEvent(GameEventType::STOP)); - - Debug::info("Game stopped"); - } - - /** - * @inheritDoc - */ - public function removeObservers(ObserverInterface|StaticObserverInterface|string|null ...$observers): void - { - if (is_null($observers)) { - $this->observers->clear(); - $this->staticObservers->clear(); - return; - } - - foreach ($observers as $observer) { - if ($observer instanceof ObserverInterface) { - $this->observers->remove($observer); - } else { - $this->staticObservers->remove($observer); - } - } - } - - /** - * @inheritDoc - */ - public function notify(EventInterface $event): void - { - try { - /** @var ObserverInterface $observer */ - foreach ($this->observers as $observer) { - $observer->onNotify($this, $event); - } - - /** @var StaticObserverInterface $observer */ - foreach ($this->staticObservers as $observer) { - $observer::onNotify($this, $event); - } - } catch (Exception $exception) { - $this->handleException($exception); - } - } - - /** - * Retrieve game settings. - * - * @param string|SettingsKey|null $key The key of the setting to retrieve. - * @return mixed The game settings. - */ - public function getSettings(string|SettingsKey|null $key = null): mixed - { - $key = match (true) { - is_null($key) => null, - is_string($key) => $key, - default => $key->value - }; - return $this->settings[$key] ?? $this->settings; - } - - /** - * Handles game errors. - * - * @param int $errno - * @param string $errstr - * @param string $errfile - * @param int $errline - * @return never - */ - private function handleError(int $errno, string $errstr, string $errfile, int $errline): never - { - $errorMessage = "[$errno] $errstr in $errfile on line $errline"; - Debug::error($errorMessage); - $this->stop(); - - if ($this->getSettings('debug')) { - exit($errorMessage); - } - - exit($errno); - } - - /** - * @return void - */ - protected function initializeConsole(): void - { - $this->consoleCursor = Console::cursor(); - Console::init($this, []); - } - - /** - * @return void - */ - protected function initializeManagers(): void - { - $this->sceneManager = SceneManager::getInstance(); - $this->eventManager = EventManager::getInstance(); - $this->modalManager = ModalManager::getInstance(); - $this->notificationsManager = NotificationsManager::getInstance(); - $this->uiManager = UIManager::getInstance(); - } - - /** - * Initialize game settings. - * - * @return void - */ - private function initializeSettings(): void - { - // Load environment variables - if (file_exists($this->workingDirectory ?? getcwd() . '/.env')) { - $dotenv = Dotenv::createImmutable(getcwd()); - $dotenv->load(); - } - - $this->settings[SettingsKey::GAME_NAME->value] = $_ENV['GAME_NAME'] ?? $this->name; - $this->settings[SettingsKey::SCREEN_WIDTH->value] = $this->screenWidth; - $this->settings[SettingsKey::SCREEN_HEIGHT->value] = $this->screenHeight; - $this->settings[SettingsKey::FPS->value] = DEFAULT_FPS; - $this->settings[SettingsKey::ASSETS_DIR->value] = Path::join(getcwd(), DEFAULT_ASSETS_PATH); - - $this->settings[SettingsKey::INITIAL_SCENE->value] = null; - - // Load environment settings - $this->settings[SettingsKey::DEBUG->value] = $_ENV['DEBUG_MODE'] ?? false; - $this->settings[SettingsKey::DEBUG_INFO->value] = $_ENV['SHOW_DEBUG_INFO'] ?? false; - $this->settings[SettingsKey::LOG_LEVEL->value] = $_ENV['LOG_LEVEL'] ?? 'info'; - Debug::setLogLevel(LogLevel::tryFrom($this->getSettings('log_level')) ?? LogLevel::DEBUG); - - $this->settings[SettingsKey::LOG_DIR->value] = Path::join(getcwd(), DEFAULT_LOGS_DIR); - Debug::info("Log directory initialized: {$this->settings[SettingsKey::LOG_DIR->value]}"); - - // Debug settings - Debug::setLogDirectory($this->getSettings(SettingsKey::LOG_DIR->value)); - $this->debugWindow->setPosition([0, $this->settings[SettingsKey::SCREEN_HEIGHT->value] - self::DEBUG_WINDOW_HEIGHT]); - - // Input settings - $this->settings[SettingsKey::BUTTONS->value] = []; - $this->settings[SettingsKey::PAUSE_KEY->value] = $_ENV['PAUSE_KEY'] ?? KeyCode::ESCAPE; - - // Splash screen settings - $this->settings[SettingsKey::SPLASH_TEXTURE->value] = Path::join($this->settings[SettingsKey::ASSETS_DIR->value], basename(DEFAULT_SPLASH_TEXTURE_PATH)); - Debug::info("Splash screen texture init: {$this->settings[SettingsKey::SPLASH_TEXTURE->value]}"); - $this->settings[SettingsKey::SPLASH_DURATION->value] = DEFAULT_SPLASH_SCREEN_DURATION; - - // UI Settings - $this->settings[SettingsKey::BORDER_PACK->value] = null; - - $this->sceneManager->loadSettings($this->settings); - Debug::info("Game settings initialized"); - } - - /** - * Load game settings. - * - * @param array|null $settings The settings to load. If null will load default settings. - * @return $this The current instance of the game engine. - */ - public function loadSettings(?array $settings = null): self - { - try { - Debug::info("Loading environment settings"); - // Environment - $this->settings[SettingsKey::DEBUG->value] = $_ENV['DEBUG_MODE'] ?? false; - $this->settings[SettingsKey::DEBUG_INFO->value] = $_ENV['SHOW_DEBUG_INFO'] ?? false; - $this->settings[SettingsKey::LOG_LEVEL->value] = $_ENV['LOG_LEVEL'] ?? DEFAULT_LOG_LEVEL; - $this->settings[SettingsKey::LOG_DIR->value] = $_ENV['LOG_DIR'] ?? Path::join(getcwd(), DEFAULT_LOGS_DIR); - - Debug::info("Loading game settings"); - // Game - $this->settings[SettingsKey::GAME_NAME->value] = $settings[SettingsKey::GAME_NAME->value] ?? $this->name; - $this->settings[SettingsKey::SCREEN_WIDTH->value] = $settings[SettingsKey::SCREEN_WIDTH->value] ?? $this->screenWidth; - $this->settings[SettingsKey::SCREEN_HEIGHT->value] = $settings[SettingsKey::SCREEN_HEIGHT->value] ?? $this->screenHeight; - $this->settings[SettingsKey::FPS->value] = $settings[SettingsKey::FPS->value] ?? DEFAULT_FPS; - $this->settings[SettingsKey::ASSETS_DIR->value] = $settings[SettingsKey::ASSETS_DIR->value] ?? getcwd() . DEFAULT_ASSETS_PATH; - - Debug::info('Loading scene settings'); - // Scene - $this->settings[SettingsKey::INITIAL_SCENE->value] = 0 ?? throw new InitializationException("Initial scene not found"); - - Debug::info('Loading splash screen settings'); - if (isset($settings[SettingsKey::SPLASH_TEXTURE->value])) { - $this->settings[SettingsKey::SPLASH_TEXTURE->value] = Path::join(getcwd(), $settings[SettingsKey::SPLASH_TEXTURE->value]); - } - - $this->settings[SettingsKey::SPLASH_DURATION->value] = $settings[SettingsKey::SPLASH_DURATION->value] ?? DEFAULT_SPLASH_SCREEN_DURATION; - - // Debug settings - Debug::info('Loading debug settings'); - Debug::setLogDirectory($this->getSettings('log_dir')); - Debug::setLogLevel(LogLevel::tryFrom($this->getSettings('log_level')) ?? LogLevel::DEBUG); - $this->debugWindow->setPosition([0, $this->settings[SettingsKey::SCREEN_HEIGHT->value] - self::DEBUG_WINDOW_HEIGHT]); - - // Input settings - $this->settings[SettingsKey::BUTTONS->value] = $settings[SettingsKey::BUTTONS->value] ?? $this->settings[SettingsKey::BUTTONS->value] ?? []; - $this->settings[SettingsKey::PAUSE_KEY->value] = $settings[SettingsKey::PAUSE_KEY->value] ?? $_ENV['PAUSE_KEY'] ?? KeyCode::ESCAPE; - - $this->sceneManager->loadSettings($this->settings); - Debug::info("Game settings loaded"); - } catch (Exception $exception) { - $this->handleException($exception); - } - - return $this; - } - - /** - * Initialize game states. - * - * @return void - */ - protected function initializeGameStates(): void - { - $this->sceneState = new SceneState($this, $this->sceneManager, $this->eventManager, $this->modalManager, $this->notificationsManager, $this->uiManager); - $this->modalState = new ModalState($this, $this->sceneManager, $this->eventManager, $this->modalManager, $this->notificationsManager, $this->uiManager); - $this->pausedState = new PausedState($this, $this->sceneManager, $this->eventManager, $this->modalManager, $this->notificationsManager, $this->uiManager); - $this->state = $this->sceneState; - } - - /** - * Configure the window change signal handler. - * - * @return void - * @throws Exception - */ - protected function configureWindowChangeSignalHandler(): void - { - pcntl_signal(SIGWINCH, function () { - $terminalSize = Console::getSize(); - $currentScreenWidth = $terminalSize->getWidth(); - $currentScreenHeight = $terminalSize->getHeight(); - - $this->screenWidth = min($currentScreenWidth, $this->screenWidth, DEFAULT_SCREEN_WIDTH); - $this->screenHeight = min($currentScreenHeight, $this->screenHeight, DEFAULT_SCREEN_HEIGHT); - - Debug::info("SIGWINCH received"); - }); - } - - /** - * Quit the game. - * - * @return void - */ - public static function quit(): void - { - if (confirm("Are you sure you want to quit?", "", 40)) { - EventManager::getInstance()->dispatchEvent(new GameEvent(GameEventType::QUIT)); - } - } - - /** - * Run the game. - * - * @return void - */ - public function run(): void - { - try { - $sleepTime = (int)(1000000 / $this->getSettings('fps')); - $this->start(); - $nextFrameTime = microtime(true) + 1; - $lastFrameCountSnapShot = $this->frameCount; - - Debug::info("Running game"); - while ($this->isRunning) { - $this->handleInput(); - $this->update(); - - if (!$this->isRunning) { - break; + const int DEBUG_WINDOW_HEIGHT = 5; + /** + * @var SceneState $sceneState + */ + protected SceneState $sceneState; + /** + * @var ModalState $modalState + */ + protected ModalState $modalState; + /** + * @var PausedState $pausedState + */ + protected PausedState $pausedState; + /** + * @var GameStateInterface|null $previousState The previous state of the game. + */ + protected ?GameStateInterface $previousState = null; + /** + * @var array + */ + private array $settings = []; + + /* == Managers == */ + /** + * @var ItemList + */ + private ItemList $observers; + /** + * @var ItemList + */ + private ItemList $staticObservers; + /** + * @var int The number of frames that have been rendered. + */ + private int $frameCount = 0; + /** + * @var int The frame rate of the game. + */ + private int $frameRate = 0; + /** + * @var SceneManager $sceneManager + */ + private SceneManager $sceneManager; + /** + * @var EventManager $eventManager + */ + private EventManager $eventManager; + /** + * @var ModalManager $modalManager + */ + private ModalManager $modalManager; + + /* Sentinel properties */ + /** + * @var NotificationsManager $notificationsManager + */ + private NotificationsManager $notificationsManager; + /** + * @var UIManager $uiManager + */ + private UIManager $uiManager; + /** + * @var Cursor $consoleCursor + */ + private Cursor $consoleCursor; + /** + * @var Window $debugWindow + */ + private Window $debugWindow; + /** + * @var bool Determines if the game engine is running. + */ + private bool $isRunning = false; + /** + * @var bool Determines if a modal is showing or not. + */ + private bool $isShowingModal = false; + /** + * @var GameStateInterface $state + */ + private GameStateInterface $state; + + private SplashScreen $splashScreen; + + /** + * Game constructor. + * + * @param string $name The name of the game. + * @param int $screenWidth The width of the game screen. + * @param int $screenHeight The height of the game screen. + * @throws Exception + */ + public function __construct(private readonly string $name, private readonly int $screenWidth = DEFAULT_SCREEN_WIDTH, private readonly int $screenHeight = DEFAULT_SCREEN_HEIGHT, private readonly ?string $workingDirectory = null) + { + try { + $this->initializeObservers(); + $this->configureErrorAndExceptionHandlers(); + $this->initializeConsole(); + $this->initializeConfigStore(); + $this->initializeManagers(); + $this->initializeSettings(); + $this->initializeGameStates(); + $this->configureWindowChangeSignalHandler(); + $this->splashScreen = new SplashScreen($this->consoleCursor, $this->settings); + } catch (Error|Throwable $exception) { + $this->handleException($exception); } + } + + /** + * @return void + */ + protected function initializeObservers(): void + { + $this->observers = new ItemList(ObserverInterface::class); + $this->staticObservers = new ItemList(StaticObserverInterface::class); + } + + /** + * Configure error and exception handlers. + * + * @return void + * @throws IOException + */ + protected function configureErrorAndExceptionHandlers(): void + { + error_reporting(E_ALL); + + set_exception_handler(function (Throwable|Exception|Error $exception) { + $this->handleException($exception); + }); + + // Handle errors + set_error_handler(function ($errno, $errstr, $errfile, $errline) { + $this->handleError($errno, $errstr, $errfile, $errline); + }); + + $this->debugWindow = new Window(); + } + + /** + * Handle game exceptions. + * + * @param Throwable|Error $exception The exception to be handled. + * @return never + * @throws IOException + */ + private function handleException(Throwable|Error $exception): never + { + Debug::error($exception); + $this->stop(); + + if ($this->getSettings('debug')) { + exit($exception); + } + + exit("$exception\n"); + } + + /** + * Stop the game. + * + * @return void + * @throws IOException + */ + public function stop(): void + { + Console::reset(); + + Debug::info("Stopping game"); + + // Disable non-blocking input mode + InputManager::disableNonBlockingMode(); + + // Enable echo + InputManager::enableEcho(); + + // Show cursor + $this->consoleCursor->show(); + + // Restore the terminal settings + Console::restoreSettings(); + + // Remove observers + $this->removeObservers(); + + // Stop the game + $this->isRunning = false; + + // Notify listeners that the game has stopped + $this->notify(new GameEvent(GameEventType::STOP)); + + Debug::info("Game stopped"); + } + + /** + * @inheritDoc + */ + public function removeObservers(ObserverInterface|StaticObserverInterface|string|null ...$observers): void + { + if (is_null($observers)) { + $this->observers->clear(); + $this->staticObservers->clear(); + return; + } + + foreach ($observers as $observer) { + if ($observer instanceof ObserverInterface) { + $this->observers->remove($observer); + } else { + $this->staticObservers->remove($observer); + } + } + } + + /** + * @inheritDoc + * + * @throws IOException + */ + public function notify(EventInterface $event): void + { + try { + /** @var ObserverInterface $observer */ + foreach ($this->observers as $observer) { + $observer->onNotify($this, $event); + } + + /** @var StaticObserverInterface $observer */ + foreach ($this->staticObservers as $observer) { + $observer::onNotify($this, $event); + } + } catch (Exception $exception) { + $this->handleException($exception); + } + } - $this->render(); + /** + * Retrieve game settings. + * + * @param string|SettingsKey|null $key The key of the setting to retrieve. + * @return mixed The game settings. + */ + public function getSettings(string|SettingsKey|null $key = null): mixed + { + $key = match (true) { + is_null($key) => null, + is_string($key) => $key, + default => $key->value + }; + return $this->settings[$key] ?? $this->settings; + } + + /** + * Handles game errors. + * + * @param int $errno + * @param string $errstr + * @param string $errfile + * @param int $errline + * @return never + * @throws IOException + */ + private function handleError(int $errno, string $errstr, string $errfile, int $errline): never + { + $errorMessage = "[$errno] $errstr in $errfile on line $errline"; + Debug::error($errorMessage); + $this->stop(); + + if ($this->getSettings('debug')) { + exit($errorMessage); + } + + exit($errno); + } + + /** + * @return void + */ + protected function initializeConsole(): void + { + $this->consoleCursor = Console::cursor(); + Console::init($this, []); + } + + /** + * @return void + */ + protected function initializeConfigStore(): void + { + ConfigStore::put(AppConfig::class, new AppConfig()); + ConfigStore::put(InputConfig::class, new InputConfig()); + ConfigStore::put(PlayerPreferences::class, new PlayerPreferences()); + } + + /** + * @return void + */ + protected function initializeManagers(): void + { + $this->sceneManager = SceneManager::getInstance(); + $this->eventManager = EventManager::getInstance(); + $this->modalManager = ModalManager::getInstance(); + $this->notificationsManager = NotificationsManager::getInstance(); + $this->uiManager = UIManager::getInstance(); + } + + /** + * Initialize game settings. + * + * @return void + */ + private function initializeSettings(): void + { + // Load environment variables + if (file_exists($this->workingDirectory ?? getcwd() . '/.env')) { + $dotenv = Dotenv::createImmutable(getcwd()); + $dotenv->load(); + } + + $this->settings[SettingsKey::GAME_NAME->value] = $_ENV['GAME_NAME'] ?? $this->name; + $this->settings[SettingsKey::SCREEN_WIDTH->value] = $this->screenWidth; + $this->settings[SettingsKey::SCREEN_HEIGHT->value] = $this->screenHeight; + $this->settings[SettingsKey::FPS->value] = DEFAULT_FPS; + $this->settings[SettingsKey::ASSETS_DIR->value] = Path::join(getcwd(), DEFAULT_ASSETS_PATH); - usleep($sleepTime); + $this->settings[SettingsKey::INITIAL_SCENE->value] = null; - if (microtime(true) >= $nextFrameTime) { - $this->frameRate = $this->frameCount - $lastFrameCountSnapShot; - $lastFrameCountSnapShot = $this->frameCount; - $nextFrameTime = microtime(true) + 1; + // Load environment settings + $this->settings[SettingsKey::DEBUG->value] = $_ENV['DEBUG_MODE'] ?? false; + $this->settings[SettingsKey::DEBUG_INFO->value] = $_ENV['SHOW_DEBUG_INFO'] ?? false; + $this->settings[SettingsKey::LOG_LEVEL->value] = $_ENV['LOG_LEVEL'] ?? 'info'; + Debug::setLogLevel(LogLevel::tryFrom($this->getSettings('log_level')) ?? LogLevel::DEBUG); + + $this->settings[SettingsKey::LOG_DIR->value] = Path::join(getcwd(), DEFAULT_LOGS_DIR); + Debug::info("Log directory initialized: {$this->settings[SettingsKey::LOG_DIR->value]}"); + + // Debug settings + Debug::setLogDirectory($this->getSettings(SettingsKey::LOG_DIR->value)); + $this->debugWindow->setPosition([0, $this->settings[SettingsKey::SCREEN_HEIGHT->value] - self::DEBUG_WINDOW_HEIGHT]); + + // Input settings + $this->settings[SettingsKey::BUTTONS->value] = []; + $this->settings[SettingsKey::PAUSE_KEY->value] = $_ENV['PAUSE_KEY'] ?? KeyCode::ESCAPE; + + // Splash screen settings + $this->settings[SettingsKey::SPLASH_TEXTURE->value] = Path::join($this->settings[SettingsKey::ASSETS_DIR->value], basename(DEFAULT_SPLASH_TEXTURE_PATH)); + Debug::info("Splash screen texture init: {$this->settings[SettingsKey::SPLASH_TEXTURE->value]}"); + $this->settings[SettingsKey::SPLASH_DURATION->value] = DEFAULT_SPLASH_SCREEN_DURATION; + + // UI Settings + $this->settings[SettingsKey::BORDER_PACK->value] = null; + + $this->sceneManager->loadSettings($this->settings); + Debug::info("Game settings initialized"); + } + + /** + * Load game settings. + * + * @param array|null $settings The settings to load. If null will load default settings. + * @return $this The current instance of the game engine. + * @throws IOException + */ + public function loadSettings(?array $settings = null): self + { + try { + Debug::info("Loading environment settings"); + // Environment + $this->settings[SettingsKey::DEBUG->value] = $_ENV['DEBUG_MODE'] ?? false; + $this->settings[SettingsKey::DEBUG_INFO->value] = $_ENV['SHOW_DEBUG_INFO'] ?? false; + $this->settings[SettingsKey::LOG_LEVEL->value] = $_ENV['LOG_LEVEL'] ?? DEFAULT_LOG_LEVEL; + $this->settings[SettingsKey::LOG_DIR->value] = $_ENV['LOG_DIR'] ?? Path::join(getcwd(), DEFAULT_LOGS_DIR); + + Debug::info("Loading game settings"); + // Game + $this->settings[SettingsKey::GAME_NAME->value] = $settings[SettingsKey::GAME_NAME->value] ?? $this->name; + $this->settings[SettingsKey::SCREEN_WIDTH->value] = $settings[SettingsKey::SCREEN_WIDTH->value] ?? $this->screenWidth; + $this->settings[SettingsKey::SCREEN_HEIGHT->value] = $settings[SettingsKey::SCREEN_HEIGHT->value] ?? $this->screenHeight; + $this->settings[SettingsKey::FPS->value] = $settings[SettingsKey::FPS->value] ?? DEFAULT_FPS; + $this->settings[SettingsKey::ASSETS_DIR->value] = $settings[SettingsKey::ASSETS_DIR->value] ?? getcwd() . DEFAULT_ASSETS_PATH; + + Debug::info('Loading scene settings'); + // Scene + $this->settings[SettingsKey::INITIAL_SCENE->value] = 0 ?? throw new InitializationException("Initial scene not found"); + + Debug::info('Loading splash screen settings'); + if (isset($settings[SettingsKey::SPLASH_TEXTURE->value])) { + $this->settings[SettingsKey::SPLASH_TEXTURE->value] = Path::join(getcwd(), $settings[SettingsKey::SPLASH_TEXTURE->value]); + } + + $this->settings[SettingsKey::SPLASH_DURATION->value] = $settings[SettingsKey::SPLASH_DURATION->value] ?? DEFAULT_SPLASH_SCREEN_DURATION; + + // Debug settings + Debug::info('Loading debug settings'); + Debug::setLogDirectory($this->getSettings('log_dir')); + Debug::setLogLevel(LogLevel::tryFrom($this->getSettings('log_level')) ?? LogLevel::DEBUG); + $this->debugWindow->setPosition([0, $this->settings[SettingsKey::SCREEN_HEIGHT->value] - self::DEBUG_WINDOW_HEIGHT]); + + // Input settings + $this->settings[SettingsKey::BUTTONS->value] = $settings[SettingsKey::BUTTONS->value] ?? $this->settings[SettingsKey::BUTTONS->value] ?? []; + $this->settings[SettingsKey::PAUSE_KEY->value] = $settings[SettingsKey::PAUSE_KEY->value] ?? $_ENV['PAUSE_KEY'] ?? KeyCode::ESCAPE; + + $this->sceneManager->loadSettings($this->settings); + Debug::info("Game settings loaded"); + } catch (Exception $exception) { + $this->handleException($exception); } - } - } catch (Exception $exception) { - $this->handleException($exception); - } - } - /** - * Start the game. - * - * @return void - */ - private function start(): void - { - Debug::info("Starting game"); - - // Save the terminal settings - Console::saveSettings(); - - // Set the terminal name - Console::setName($this->getSettings('game_name')); - - // Set the terminal size - Console::setSize($this->getSettings('screen_width'), $this->getSettings('screen_height')); - - // Hide the cursor - $this->consoleCursor->hide(); - - // Disable echo - InputManager::disableEcho(); - - // Enable non-blocking input mode - InputManager::enableNonBlockingMode(); - - // Show the splash screen - $this->showSplashScreen(); - - // Handle game events - $this->handleGameEvents(); - - // Load the first scene - try { - $this->sceneManager->loadScene($this->getSettings('initial_scene')); - } catch (SceneNotFoundException $exception) { - $this->handleException($exception); - } - - // Add game observers - $this->addObservers(Time::class); - - // Start the game - $this->isRunning = true; - - // Notify listeners that the game has started - $this->notify(new GameEvent(GameEventType::START)); - - Debug::info("Game started"); - } - - /** - * Display the splash screen. - * - * @return void - */ - private function showSplashScreen(): void - { - try { - Debug::info("Showing splash screen"); - Console::setSize(MAX_SCREEN_WIDTH, MAX_SCREEN_HEIGHT); - - // Check if a splash texture can be loaded - if (!file_exists($this->getSettings('splash_texture'))) { - Debug::warn("Splash screen texture not found: {$this->settings[SettingsKey::SPLASH_TEXTURE->value]}"); - $this->settings[SettingsKey::SPLASH_TEXTURE->value] = Path::join(Path::getVendorAssetsDirectory(), DEFAULT_SPLASH_TEXTURE_PATH); - } - - Debug::info("Loading splash screen texture"); - $splashScreen = file_get_contents($this->getSettings('splash_texture')); - $splashScreenRows = explode("\n", $splashScreen); - $splashScreenWidth = 75; - $splashScreenHeight = 25; - $splashByLine = 'SendamaEngine ™'; - $splashScreenRows[] = sprintf("%s%s", str_repeat(' ', $splashScreenWidth - 12), "powered by"); - $splashScreenRows[] = sprintf("%s%s", str_repeat(' ', $splashScreenWidth - strlen($splashByLine)), $splashByLine); - - $leftMargin = (MAX_SCREEN_WIDTH / 2) - ($splashScreenWidth / 2); - $topMargin = (MAX_SCREEN_HEIGHT / 2) - ($splashScreenHeight / 2); - - Debug::info("Rendering splash screen texture"); - foreach ($splashScreenRows as $rowIndex => $row) { - $this->consoleCursor->moveTo((int)$leftMargin, (int)($topMargin + $rowIndex)); - Console::output()->write($row); - } - - $duration = (int)($this->getSettings('splash_screen_duration') * 1000000); - usleep($duration); - - Console::setSize($this->getSettings('screen_width'), $this->getSettings('screen_height')); - Console::clear(); - - Debug::info("Splash screen hidden"); - } catch (Exception $exception) { - $this->handleException($exception); - } - } - - /** - * @return void - */ - private function handleGameEvents(): void - { - try { - // Handle game events - $this->eventManager->addEventListener(EventType::GAME, function (GameEvent $event) { - Debug::info("Game event received"); - switch ($event->gameEventType) { - case GameEventType::QUIT: - Debug::info("Game quit event received"); - $this->notify(new GameEvent(GameEventType::QUIT)); - $this->stop(); - break; - - default: - break; + return $this; + } + + /** + * Initialize game states. + * + * @return void + */ + protected function initializeGameStates(): void + { + $context = new GameStateContext($this, $this->sceneManager, $this->eventManager, $this->modalManager, $this->notificationsManager, $this->uiManager); + $this->sceneState = new SceneState($context); + $this->modalState = new ModalState($context); + $this->pausedState = new PausedState($context); + $this->state = $this->sceneState; + } + + /** + * Configure the window change signal handler. + * + * @return void + * @throws Exception + */ + protected function configureWindowChangeSignalHandler(): void + { + pcntl_signal(SIGWINCH, function () { + $terminalSize = Console::getSize(); + $currentScreenWidth = $terminalSize->getWidth(); + $currentScreenHeight = $terminalSize->getHeight(); + + $this->screenWidth = min($currentScreenWidth, $this->screenWidth, DEFAULT_SCREEN_WIDTH); + $this->screenHeight = min($currentScreenHeight, $this->screenHeight, DEFAULT_SCREEN_HEIGHT); + + Debug::info("SIGWINCH received"); + }); + } + + /** + * Quit the game. + * + * @return void + */ + public static function quit(): void + { + if (confirm("Are you sure you want to quit?", "", 40)) { + dispatchEvent(new GameEvent(GameEventType::QUIT)); } - }); - - } catch (Exception $exception) { - $this->handleException($exception); - } - } - - /** - * @inheritDoc - */ - public function addObservers(ObserverInterface|StaticObserverInterface|string ...$observers): void - { - foreach ($observers as $observer) { - if ($observer instanceof ObserverInterface) { - $this->observers->add($observer); - } else { - $this->staticObservers->add($observer); - } - } - } - - /** - * Handle game input. - * - * @return void - */ - private function handleInput(): void - { - InputManager::handleInput(); - } - - /** - * Update the game state. - * - * @return void - */ - private function update(): void - { - $this->state->update(); - $this->uiManager->update(); - $this->notify(new GameEvent(GameEventType::UPDATE)); - } - - /** - * Render the game. - * - * @return void - */ - private function render(): void - { - $this->frameCount++; - $this->state->render(); - $this->uiManager->render(); - $this->renderDebugInfo(); - $this->notify(new GameEvent(GameEventType::RENDER)); - } - - /** - * Renders Debug Info. - * - * @return void - */ - private function renderDebugInfo(): void - { - if ($this->isDebug() && $this->showDebugInfo()) { - $content = ["FPS: $this->frameRate", "Delta: " . round(Time::getDeltaTime(), 2), "Time: " . Time::getPrettyTime(ChronoUnit::SECONDS)]; - - $this->debugWindow->setContent($content); - $this->debugWindow->render(); - } - } - - /** - * Get the debug status. - * - * @return bool The debug status. - */ - private function isDebug(): bool - { - return match (gettype($this->getSettings('debug'))) { - 'boolean' => $this->getSettings('debug'), - 'string' => strtolower($this->getSettings('debug')) === 'true', - 'integer' => $this->getSettings('debug') === 1, - default => false - }; - } - - private function showDebugInfo(): bool - { - return match (gettype($this->getSettings('debug_info'))) { - 'boolean' => $this->getSettings('debug_info'), - 'string' => strtolower($this->getSettings('debug_info')) === 'true', - 'integer' => $this->getSettings('debug_info') === 1, - default => false - }; - } - - /** - * Add scenes to the game. - * - * @param SceneInterface ...$scenes The scenes to add. - * @return $this - */ - public function addScenes(SceneInterface ...$scenes): self - { - foreach ($scenes as $scene) { - $this->sceneManager->addScene($scene); - } - - return $this; - } - - /** - * Retrieve a game state. - * - * @param string $stateName The name of the state to retrieve. - * @return GameStateInterface|null The game state or null if not found. - */ - public function getState(string $stateName): ?GameStateInterface - { - return match ($stateName) { - 'scene' => $this->sceneState, - 'modal' => $this->modalState, - 'paused' => $this->pausedState, - default => null - }; - } - - /** - * Set the current game state. - * - * @param GameStateInterface $state The game state to set. - * @return void - */ - public function setState(GameStateInterface $state): void - { - $this->previousState = $this->state; - $this->state = $state; - } - - /** - * Get the previous game state. - * - * @return GameStateInterface|null The previous game state or null if not found. - */ - public function getPreviousState(): GameStateInterface|null - { - return $this->previousState; - } -} \ No newline at end of file + } + + /** + * Destruct the game engine. + * + * @throws IOException + */ + public function __destruct() + { + Console::restoreSettings(); + + if ($lastError = error_get_last()) { + $this->handleError($lastError['type'], $lastError['message'], $lastError['file'], $lastError['line']); + } + } + + /** + * Run the game. + * + * @return void + * @throws IOException + */ + public function run(): void + { + try { + $sleepTime = (int)(1000000 / $this->getSettings('fps')); + $this->start(); + $nextFrameTime = microtime(true) + 1; + $lastFrameCountSnapShot = $this->frameCount; + + Debug::info("Running game"); + while ($this->isRunning) { + $this->handleInput(); + $this->update(); + + if (!$this->isRunning) { + break; + } + + $this->render(); + + usleep($sleepTime); + + if (microtime(true) >= $nextFrameTime) { + $this->frameRate = $this->frameCount - $lastFrameCountSnapShot; + $lastFrameCountSnapShot = $this->frameCount; + $nextFrameTime = microtime(true) + 1; + } + } + } catch (Exception $exception) { + $this->handleException($exception); + } + } + + /** + * Start the game. + * + * @return void + * @throws IOException + */ + private function start(): void + { + Debug::info("Starting game"); + + // Save the terminal settings + Console::saveSettings(); + + // Set the terminal name + Console::setName($this->getSettings('game_name')); + + // Set the terminal size + Console::setSize($this->getSettings('screen_width'), $this->getSettings('screen_height')); + + // Hide the cursor + $this->consoleCursor->hide(); + + // Disable echo + InputManager::disableEcho(); + + // Enable non-blocking input mode + InputManager::enableNonBlockingMode(); + + // Show the splash screen + $this->splashScreen->show(); + + // Handle game events + $this->handleGameEvents(); + + // Load the first scene + try { + $this->sceneManager->loadScene($this->getSettings('initial_scene')); + } catch (SceneNotFoundException $exception) { + $this->handleException($exception); + } + + // Add game observers + $this->addObservers(Time::class); + + // Start the game + $this->isRunning = true; + + // Notify listeners that the game has started + $this->notify(new GameEvent(GameEventType::START)); + + Debug::info("Game started"); + } + + /** + * @return void + * @throws IOException + */ + private function handleGameEvents(): void + { + try { + // Handle game events + $this->eventManager->addEventListener(EventType::GAME, function (GameEvent $event) { + Debug::info("Game event received"); + if ($event->gameEventType === GameEventType::QUIT) { + Debug::info("Game quit event received"); + $this->notify(new GameEvent(GameEventType::QUIT)); + $this->stop(); + } + }); + + } catch (Exception $exception) { + $this->handleException($exception); + } + } + + /** + * @inheritDoc + */ + public function addObservers(ObserverInterface|StaticObserverInterface|string ...$observers): void + { + foreach ($observers as $observer) { + if ($observer instanceof ObserverInterface) { + $this->observers->add($observer); + } else { + $this->staticObservers->add($observer); + } + } + } + + /** + * Handle game input. + * + * @return void + */ + private function handleInput(): void + { + InputManager::handleInput(); + } + + /** + * Update the game state. + * + * @return void + * @throws IOException + */ + private function update(): void + { + $this->state->update(); + $this->uiManager->update(); + $this->notify(new GameEvent(GameEventType::UPDATE)); + } + + /** + * Render the game. + * + * @return void + * @throws IOException + */ + private function render(): void + { + $this->frameCount++; + $this->state->render(); + $this->uiManager->render(); + $this->renderDebugInfo(); + $this->notify(new GameEvent(GameEventType::RENDER)); + } + + /** + * Renders Debug Info. + * + * @return void + */ + private function renderDebugInfo(): void + { + if ($this->isDebug() && $this->showDebugInfo()) { + $content = ["FPS: $this->frameRate", "Delta: " . round(Time::getDeltaTime(), 2), "Time: " . Time::getPrettyTime(ChronoUnit::SECONDS)]; + + $this->debugWindow->setContent($content); + $this->debugWindow->render(); + } + } + + /** + * Get the debug status. + * + * @return bool The debug status. + */ + private function isDebug(): bool + { + return match (gettype($this->getSettings('debug'))) { + 'boolean' => $this->getSettings('debug'), + 'string' => strtolower($this->getSettings('debug')) === 'true', + 'integer' => $this->getSettings('debug') === 1, + default => false + }; + } + + private function showDebugInfo(): bool + { + return match (gettype($this->getSettings('debug_info'))) { + 'boolean' => $this->getSettings('debug_info'), + 'string' => strtolower($this->getSettings('debug_info')) === 'true', + 'integer' => $this->getSettings('debug_info') === 1, + default => false + }; + } + + /** + * Add scenes to the game. + * + * @param SceneInterface ...$scenes The scenes to add. + * @return $this + */ + public function addScenes(SceneInterface ...$scenes): self + { + foreach ($scenes as $scene) { + $this->sceneManager->addScene($scene); + } + + return $this; + } + + /** + * @param string ...$paths + * @return $this + */ + public function loadScenes(string ...$paths): self + { + foreach ($paths as $path) { + $canonicalPath = Path::join(Path::getCurrentWorkingDirectory(), $path); + $this->sceneManager->loadSceneFromFile($canonicalPath); + } + return $this; + } + + /** + * Retrieve a game state. + * + * @param string $stateName The name of the state to retrieve. + * @return GameStateInterface|null The game state or null if not found. + */ + public function getState(string $stateName): ?GameStateInterface + { + return match ($stateName) { + 'scene' => $this->sceneState, + 'modal' => $this->modalState, + 'paused' => $this->pausedState, + default => null + }; + } + + /** + * Set the current game state. + * + * @param GameStateInterface $state The game state to set. + * @return void + */ + public function setState(GameStateInterface $state): void + { + $context = new GameStateContext( + $this, + $this->sceneManager, + $this->eventManager, + $this->modalManager, + $this->notificationsManager, + $this->uiManager + ); + $this->previousState = $this->state; + $this->state->exit($context); + $this->state = $state; + $this->state->enter($context); + } + + /** + * Get the previous game state. + * + * @return GameStateInterface|null The previous game state or null if not found. + */ + public function getPreviousState(): GameStateInterface|null + { + return $this->previousState; + } +} diff --git a/src/Interfaces/GameStateInterface.php b/src/Interfaces/GameStateInterface.php index e976852..f7cc57a 100644 --- a/src/Interfaces/GameStateInterface.php +++ b/src/Interfaces/GameStateInterface.php @@ -2,33 +2,47 @@ namespace Sendama\Engine\Interfaces; +use Sendama\Engine\States\GameStateContext; + interface GameStateInterface { - /** - * Updates the game state. - * - * @return void - */ - public function update(): void; - - /** - * Renders the game state. - * - * @return void - */ - public function render(): void; - - /** - * Suspends the game state. - * - * @return void - */ - public function suspend(): void; - - /** - * Resumes the game state. - * - * @return void - */ - public function resume(): void; + /** + * @param GameStateContext $context + * @return void + */ + public function enter(GameStateContext $context): void; + + /** + * @param GameStateContext $context + * @return void + */ + public function exit(GameStateContext $context): void; + + /** + * Updates the game state. + * + * @return void + */ + public function update(): void; + + /** + * Renders the game state. + * + * @return void + */ + public function render(): void; + + /** + * Suspends the game state. + * + * @return void + */ + public function suspend(): void; + + /** + * Resumes the game state. + * + * @return void + */ + public function resume(): void; } \ No newline at end of file diff --git a/src/States/GameState.php b/src/States/GameState.php index 746eeed..f557a23 100644 --- a/src/States/GameState.php +++ b/src/States/GameState.php @@ -15,14 +15,50 @@ */ abstract class GameState implements GameStateInterface { - public final function __construct( - protected Game $context, - protected SceneManager $sceneManager, - protected EventManager $eventManager, - protected ModalManager $modalManager, - protected NotificationsManager $notificationsManager, - protected UIManager $UIManager, - ) - { - } + protected Game $game; + protected SceneManager $sceneManager; + protected EventManager $eventManager; + protected ModalManager $modalManager; + protected NotificationsManager $notificationsManager; + protected UIManager $UIManager; + + /** + * @param GameStateContext $context + */ + public final function __construct(GameStateContext $context) + { + $this->game = $context->game; + $this->sceneManager = $context->sceneManager; + $this->eventManager = $context->eventManager; + $this->modalManager = $context->modalManager; + $this->notificationsManager = $context->notificationsManager; + } + + /** + * @inheritDoc + */ + public function enter(GameStateContext $context): void + { + $this->game = $context->game; + $this->sceneManager = $context->sceneManager; + $this->eventManager = $context->eventManager; + $this->modalManager = $context->modalManager; + $this->notificationsManager = $context->notificationsManager; + + // Do nothing. + } + + /** + * @inheritDoc + */ + public function exit(GameStateContext $context): void + { + $this->game = $context->game; + $this->sceneManager = $context->sceneManager; + $this->eventManager = $context->eventManager; + $this->modalManager = $context->modalManager; + $this->notificationsManager = $context->notificationsManager; + + // Do nothing. + } } \ No newline at end of file diff --git a/src/States/GameStateContext.php b/src/States/GameStateContext.php new file mode 100644 index 0000000..55cd58b --- /dev/null +++ b/src/States/GameStateContext.php @@ -0,0 +1,35 @@ +context->getSettings('pause_key'))) { + if (Input::isKeyDown($this->game->getSettings('pause_key'))) { $this->resume(); } @@ -50,8 +50,8 @@ public function resume(): void { Console::clear(); $this->sceneManager->getActiveScene()?->resume(); - if ($sceneState = $this->context->getState('scene')) { - $this->context->setState($sceneState); + if ($sceneState = $this->game->getState('scene')) { + $this->game->setState($sceneState); } } @@ -71,8 +71,8 @@ public function suspend(): void private function renderDefaultPauseText(): void { $promptText = 'PAUSED'; - $leftMargin = intval(($this->context->getSettings('screen_width') / 2) - (strlen($promptText) / 2)); - $topMargin = intval(($this->context->getSettings('screen_height') / 2) - 1); + $leftMargin = intval(($this->game->getSettings('screen_width') / 2) - (strlen($promptText) / 2)); + $topMargin = intval(($this->game->getSettings('screen_height') / 2) - 1); Console::cursor()->moveTo($leftMargin, $topMargin); echo $promptText; } diff --git a/src/States/SceneState.php b/src/States/SceneState.php index 7430e57..5743344 100644 --- a/src/States/SceneState.php +++ b/src/States/SceneState.php @@ -25,7 +25,7 @@ public function render(): void */ public function update(): void { - if (Input::isKeyDown($this->context->getSettings(SettingsKey::PAUSE_KEY->value))) { + if (Input::isKeyDown($this->game->getSettings(SettingsKey::PAUSE_KEY->value))) { $this->suspend(); } @@ -38,8 +38,8 @@ public function update(): void */ public function suspend(): void { - if ($pauseState = $this->context->getState('paused')) { - $this->context->setState($pauseState); + if ($pauseState = $this->game->getState('paused')) { + $this->game->setState($pauseState); } } diff --git a/src/Util/SchemaValidator.php b/src/Util/SchemaValidator.php new file mode 100644 index 0000000..415a1f8 --- /dev/null +++ b/src/Util/SchemaValidator.php @@ -0,0 +1,54 @@ +schemaFileContents = file_get_contents($path); + + if ($this->schemaFileContents === false) { + throw new IOException("Could not read schema file at path: $path"); + } + + $this->schema = Schema::import($this->schemaFileContents); + } + + /** + * Validates the given data against the schema. + * + * @param mixed $data + * @param Context|null $options + * @return array|object|null + * @throws InvalidValue + */ + public function validate(mixed $data, ?Context $options = null): array|null|object + { + return $this->schema->in($data, $options); + } +} \ No newline at end of file