commit 8e3c3a52de110ec17c3524d443ce4ed626297dc6 Author: hant Date: Mon Dec 15 23:02:21 2025 +0800 first diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ac3bc73 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +/vendor/ +/tmp/ +/saves/ +/data/ +/build/ +/.claude/ +/.idea diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..6df13a9 --- /dev/null +++ b/composer.json @@ -0,0 +1,18 @@ +{ + "name": "hant/fanren", + "description": "A PHP terminal game using Symfony src/Console", + "type": "project", + "require": { + "php": ">=8.1", + "symfony/var-dumper": "^6.4", + "symfony/console": "^6.4", + "symfony/yaml": "^6.4", + "doctrine/dbal": "^3.5", + "ext-readline": "*" + }, + "autoload": { + "psr-4": { + "Game\\": "src/" + } + } +} diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..a8c58ea --- /dev/null +++ b/composer.lock @@ -0,0 +1,1438 @@ +{ + "_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": "895bf2bfd94cd6e8d77b39de29436071", + "packages": [ + { + "name": "doctrine/cache", + "version": "2.2.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/cache.git", + "reference": "1ca8f21980e770095a31456042471a57bc4c68fb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/cache/zipball/1ca8f21980e770095a31456042471a57bc4c68fb", + "reference": "1ca8f21980e770095a31456042471a57bc4c68fb", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": "~7.1 || ^8.0" + }, + "conflict": { + "doctrine/common": ">2.2,<2.4" + }, + "require-dev": { + "cache/integration-tests": "dev-master", + "doctrine/coding-standard": "^9", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", + "psr/cache": "^1.0 || ^2.0 || ^3.0", + "symfony/cache": "^4.4 || ^5.4 || ^6", + "symfony/var-exporter": "^4.4 || ^5.4 || ^6" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\Cache\\": "lib/Doctrine/Common/Cache" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Cache library is a popular cache implementation that supports many different drivers such as redis, memcache, apc, mongodb and others.", + "homepage": "https://www.doctrine-project.org/projects/cache.html", + "keywords": [ + "abstraction", + "apcu", + "cache", + "caching", + "couchdb", + "memcached", + "php", + "redis", + "xcache" + ], + "support": { + "issues": "https://github.com/doctrine/cache/issues", + "source": "https://github.com/doctrine/cache/tree/2.2.0" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fcache", + "type": "tidelift" + } + ], + "abandoned": true, + "time": "2022-05-20T20:07:39+00:00" + }, + { + "name": "doctrine/dbal", + "version": "3.9.5", + "source": { + "type": "git", + "url": "https://github.com/doctrine/dbal.git", + "reference": "4a4e2eed3134036ee36a147ee0dac037dfa17868" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/4a4e2eed3134036ee36a147ee0dac037dfa17868", + "reference": "4a4e2eed3134036ee36a147ee0dac037dfa17868", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "composer-runtime-api": "^2", + "doctrine/cache": "^1.11|^2.0", + "doctrine/deprecations": "^0.5.3|^1", + "doctrine/event-manager": "^1|^2", + "php": "^7.4 || ^8.0", + "psr/cache": "^1|^2|^3", + "psr/log": "^1|^2|^3" + }, + "require-dev": { + "doctrine/coding-standard": "13.0.0", + "fig/log-test": "^1", + "jetbrains/phpstorm-stubs": "2023.1", + "phpstan/phpstan": "2.1.17", + "phpstan/phpstan-strict-rules": "^2", + "phpunit/phpunit": "9.6.23", + "slevomat/coding-standard": "8.16.2", + "squizlabs/php_codesniffer": "3.13.1", + "symfony/cache": "^5.4|^6.0|^7.0", + "symfony/console": "^4.4|^5.4|^6.0|^7.0" + }, + "suggest": { + "symfony/console": "For helpful console commands such as SQL execution and import of files." + }, + "bin": [ + "bin/doctrine-dbal" + ], + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\DBAL\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + } + ], + "description": "Powerful PHP database abstraction layer (DBAL) with many features for database schema introspection and management.", + "homepage": "https://www.doctrine-project.org/projects/dbal.html", + "keywords": [ + "abstraction", + "database", + "db2", + "dbal", + "mariadb", + "mssql", + "mysql", + "oci8", + "oracle", + "pdo", + "pgsql", + "postgresql", + "queryobject", + "sasql", + "sql", + "sqlite", + "sqlserver", + "sqlsrv" + ], + "support": { + "issues": "https://github.com/doctrine/dbal/issues", + "source": "https://github.com/doctrine/dbal/tree/3.9.5" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fdbal", + "type": "tidelift" + } + ], + "time": "2025-06-15T22:40:05+00:00" + }, + { + "name": "doctrine/deprecations", + "version": "1.1.4", + "source": { + "type": "git", + "url": "https://github.com/doctrine/deprecations.git", + "reference": "31610dbb31faa98e6b5447b62340826f54fbc4e9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/deprecations/zipball/31610dbb31faa98e6b5447b62340826f54fbc4e9", + "reference": "31610dbb31faa98e6b5447b62340826f54fbc4e9", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^9 || ^12", + "phpstan/phpstan": "1.4.10 || 2.0.3", + "phpstan/phpstan-phpunit": "^1.0 || ^2", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", + "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.4" + }, + "time": "2024-12-07T21:18:45+00:00" + }, + { + "name": "doctrine/event-manager", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/doctrine/event-manager.git", + "reference": "b680156fa328f1dfd874fd48c7026c41570b9c6e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/event-manager/zipball/b680156fa328f1dfd874fd48c7026c41570b9c6e", + "reference": "b680156fa328f1dfd874fd48c7026c41570b9c6e", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": "^8.1" + }, + "conflict": { + "doctrine/common": "<2.9" + }, + "require-dev": { + "doctrine/coding-standard": "^12", + "phpstan/phpstan": "^1.8.8", + "phpunit/phpunit": "^10.5", + "vimeo/psalm": "^5.24" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + }, + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com" + } + ], + "description": "The Doctrine Event Manager is a simple PHP event system that was built to be used with the various Doctrine projects.", + "homepage": "https://www.doctrine-project.org/projects/event-manager.html", + "keywords": [ + "event", + "event dispatcher", + "event manager", + "event system", + "events" + ], + "support": { + "issues": "https://github.com/doctrine/event-manager/issues", + "source": "https://github.com/doctrine/event-manager/tree/2.0.1" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fevent-manager", + "type": "tidelift" + } + ], + "time": "2024-05-22T20:47:39+00:00" + }, + { + "name": "psr/cache", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/cache.git", + "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/cache/zipball/aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", + "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Cache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for caching libraries", + "keywords": [ + "cache", + "psr", + "psr-6" + ], + "support": { + "source": "https://github.com/php-fig/cache/tree/3.0.0" + }, + "time": "2021-02-03T23:26:27+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": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "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.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "79dff0b268932c640297f5208d6298f71855c03e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/79dff0b268932c640297f5208d6298f71855c03e", + "reference": "79dff0b268932c640297f5208d6298f71855c03e", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "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.1" + }, + "time": "2024-08-21T13:31:24+00:00" + }, + { + "name": "symfony/console", + "version": "v6.4.27", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "13d3176cf8ad8ced24202844e9f95af11e2959fc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/13d3176cf8ad8ced24202844e9f95af11e2959fc", + "reference": "13d3176cf8ad8ced24202844e9f95af11e2959fc", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/string": "^5.4|^6.0|^7.0" + }, + "conflict": { + "symfony/dependency-injection": "<5.4", + "symfony/dotenv": "<5.4", + "symfony/event-dispatcher": "<5.4", + "symfony/lock": "<5.4", + "symfony/process": "<5.4" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^5.4|^6.0|^7.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/event-dispatcher": "^5.4|^6.0|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/lock": "^5.4|^6.0|^7.0", + "symfony/messenger": "^5.4|^6.0|^7.0", + "symfony/process": "^5.4|^6.0|^7.0", + "symfony/stopwatch": "^5.4|^6.0|^7.0", + "symfony/var-dumper": "^5.4|^6.0|^7.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/v6.4.27" + }, + "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-10-06T10:25:16+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.5.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6", + "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.5-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.5.1" + }, + "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:20:29+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.32.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": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "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.32.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-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.32.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", + "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "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.32.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-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.32.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": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "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.32.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-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.32.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": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "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.32.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-12-23T08:48:59+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": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "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": "v6.4.26", + "source": { + "type": "git", + "url": "https://github.com/symfony/string.git", + "reference": "5621f039a71a11c87c106c1c598bdcd04a19aeea" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/string/zipball/5621f039a71a11c87c106c1c598bdcd04a19aeea", + "reference": "5621f039a71a11c87c106c1c598bdcd04a19aeea", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": ">=8.1", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-intl-grapheme": "~1.0", + "symfony/polyfill-intl-normalizer": "~1.0", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/translation-contracts": "<2.5" + }, + "require-dev": { + "symfony/http-client": "^5.4|^6.0|^7.0", + "symfony/intl": "^6.2|^7.0", + "symfony/translation-contracts": "^2.5|^3.0", + "symfony/var-exporter": "^5.4|^6.0|^7.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/v6.4.26" + }, + "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-09-11T14:32:46+00:00" + }, + { + "name": "symfony/var-dumper", + "version": "v6.4.21", + "source": { + "type": "git", + "url": "https://github.com/symfony/var-dumper.git", + "reference": "22560f80c0c5cd58cc0bcaf73455ffd81eb380d5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/22560f80c0c5cd58cc0bcaf73455ffd81eb380d5", + "reference": "22560f80c0c5cd58cc0bcaf73455ffd81eb380d5", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/console": "<5.4" + }, + "require-dev": { + "ext-iconv": "*", + "symfony/console": "^5.4|^6.0|^7.0", + "symfony/error-handler": "^6.3|^7.0", + "symfony/http-kernel": "^5.4|^6.0|^7.0", + "symfony/process": "^5.4|^6.0|^7.0", + "symfony/uid": "^5.4|^6.0|^7.0", + "twig/twig": "^2.13|^3.0.4" + }, + "bin": [ + "Resources/bin/var-dump-server" + ], + "type": "library", + "autoload": { + "files": [ + "Resources/functions/dump.php" + ], + "psr-4": { + "Symfony\\Component\\VarDumper\\": "" + }, + "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 mechanisms for walking through any arbitrary PHP variable", + "homepage": "https://symfony.com", + "keywords": [ + "debug", + "dump" + ], + "support": { + "source": "https://github.com/symfony/var-dumper/tree/v6.4.21" + }, + "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": "2025-04-09T07:34:50+00:00" + }, + { + "name": "symfony/yaml", + "version": "v6.4.30", + "source": { + "type": "git", + "url": "https://github.com/symfony/yaml.git", + "reference": "8207ae83da19ee3748d6d4f567b4d9a7c656e331" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/yaml/zipball/8207ae83da19ee3748d6d4f567b4d9a7c656e331", + "reference": "8207ae83da19ee3748d6d4f567b4d9a7c656e331", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "symfony/console": "<5.4" + }, + "require-dev": { + "symfony/console": "^5.4|^6.0|^7.0" + }, + "bin": [ + "Resources/bin/yaml-lint" + ], + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Yaml\\": "" + }, + "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": "Loads and dumps YAML files", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/yaml/tree/v6.4.30" + }, + "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-12-02T11:50:18+00:00" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": {}, + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": ">=8.1", + "ext-readline": "*" + }, + "platform-dev": {}, + "plugin-api-version": "2.6.0" +} diff --git a/config/enemy_data.json b/config/enemy_data.json new file mode 100644 index 0000000..eac38e0 --- /dev/null +++ b/config/enemy_data.json @@ -0,0 +1,18 @@ +[ + { + "name": "森林哥布林", + "health": 30, + "attack": 10, + "defense": 5, + "xp": 25, + "dropId": 1 + }, + { + "name": "愤怒的野猪", + "health": 45, + "attack": 15, + "defense": 5, + "xp": 40, + "dropId": 2 + } +] \ No newline at end of file diff --git a/config/map_data.json b/config/map_data.json new file mode 100644 index 0000000..f010f34 --- /dev/null +++ b/config/map_data.json @@ -0,0 +1,20 @@ +{ + "TOWN_01": { + "name": "新手村", + "description": "安全的小镇,可以休息。", + "connections": {"N": "FIELD_01"}, + "eventPoolId": 0 + }, + "FIELD_01": { + "name": "新手田野", + "description": "微风习习,有低级怪物出没。", + "connections": {"S": "TOWN_01", "E": "FOREST_01"}, + "eventPoolId": 1 + }, + "FOREST_01": { + "name": "幽暗密林", + "description": "树木茂密,光线昏暗,怪物更强。", + "connections": {"W": "FIELD_01"}, + "eventPoolId": 2 + } +} \ No newline at end of file diff --git a/index.php b/index.php new file mode 100644 index 0000000..af198a5 --- /dev/null +++ b/index.php @@ -0,0 +1,23 @@ +add(new GameCommand()); + +// 3. 设置默认命令,让用户直接运行 `php index.php` 即可进入游戏 +$application->setDefaultCommand('game:start', true); + +// 4. 运行 Application +$application->run(); \ No newline at end of file diff --git a/src/Core/GameCommand.php b/src/Core/GameCommand.php new file mode 100644 index 0000000..91c5156 --- /dev/null +++ b/src/Core/GameCommand.php @@ -0,0 +1,123 @@ +setDescription('Starts the main PHP CLI RPG game loop.') + ->setHelp('Runs the main interactive game loop in the console.'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + // 1. 初始化数据库管理器 + $this->dbManager = new DatabaseManager(); + $this->dbManager->loadInitialData(); + + // 2. 初始化 Event Dispatcher 和所有服务 + $this->initializeServices($input, $output); + + // 3. 角色创建/加载存档 + $helper = $this->getHelper('question'); + $question = new Question("请输入你的角色名称:", "旅行者"); + $playerName = $helper->ask($input, $output, $question); + + // 创建玩家实例 + $player = new Player($playerName, 100, 15, 5); + + // **将玩家实例交给 StateManager 管理** + $this->stateManager->setPlayer($player); + + // 通知 UI 服务 + $this->eventDispatcher->dispatch(new Event('SystemMessage', ['message' => "角色 {$player->getName()} 创建成功!"])); + + return $this->mainLoop($input, $output); + } + + private function initializeServices(InputInterface $input, OutputInterface $output): void { + + // 实例化 QuestionHelper + $questionHelper = $this->getHelper('question'); + // 实例化 Event Dispatcher + $this->eventDispatcher = new EventDispatcher(); + $this->stateManager = new StateManager($this->dbManager->getConnection()); + + + + // ⭐ 实例化 CharacterService + $characterService = new CharacterService($this->eventDispatcher, $this->stateManager); + $this->eventDispatcher->registerListener($characterService); + + // ⭐ 实例化 LootService + $lootService = new LootService($this->eventDispatcher, $this->stateManager); + $this->eventDispatcher->registerListener($lootService); + + // ⭐ 实例化 InteractionSystem + $interactionSystem = new InteractionSystem($this->eventDispatcher, $this->stateManager, $input, $output, $questionHelper); + $this->eventDispatcher->registerListener($interactionSystem); + + // ⭐ 实例化 QuestService + $questService = new QuestService($this->eventDispatcher, $this->stateManager); + $this->eventDispatcher->registerListener($questService); + + // ⭐ 实例化 InputHandler 并注入依赖 + $this->inputHandler = new InputHandler($this->eventDispatcher, $input, $output, $questionHelper); + + // 实例化和注册 UIService (监听器) + $this->uiService = new UIService($output, $this->stateManager); + $this->eventDispatcher->registerListener($this->uiService); + + // MapSystem 注册 (需要 EventDispatcher 和 DB 连接) + $this->mapSystem = new MapSystem($this->eventDispatcher, $this->stateManager); + $this->eventDispatcher->registerListener($this->mapSystem); + + $questionHelper = $this->getHelper('question'); + $battleService = new BattleService($this->eventDispatcher, $this->stateManager, $input,$output, $questionHelper); + $this->eventDispatcher->registerListener($battleService); + + // 触发一个初始事件,让 UI 服务打印欢迎信息 + $welcomeEvent = new Event('GameStartEvent', ['message' => '核心系统已就绪,请输入指令开始游戏。']); + $this->eventDispatcher->dispatch($welcomeEvent); + } + // src/Core/GameCommand.php (mainLoop 方法片段) + + private function mainLoop(InputInterface $input, OutputInterface $output): int { + $running = true; + while ($running) { + $running = $this->inputHandler->handleMainCommand(); + } + $output->writeln("游戏结束。感谢游玩!"); + return Command::SUCCESS; + } +} \ No newline at end of file diff --git a/src/Database/DatabaseManager.php b/src/Database/DatabaseManager.php new file mode 100644 index 0000000..e4147ac --- /dev/null +++ b/src/Database/DatabaseManager.php @@ -0,0 +1,104 @@ + "sqlite:///{$dbPath}", + ]; + + // 2. 建立连接 + $this->connection = DriverManager::getConnection($connectionParams); + + // 3. 检查并创建数据库结构 + $this->initializeSchema(); + } + + /** + * 初始化/更新数据库结构 (Schema) + */ + private function initializeSchema(): void { + $schemaManager = $this->connection->getSchemaManager(); + + // 只有在数据库为空时才尝试创建 Schema + if (!$schemaManager->getDatabasePlatform()->supportsSchemas() && empty($schemaManager->listTables())) { + $schema = new Schema(); + + // --- 游戏配置表:敌人数据 --- + $enemiesTable = $schema->createTable('enemies'); + $enemiesTable->addColumn('id', 'integer', ['autoincrement' => true]); + $enemiesTable->addColumn('name', 'string', ['length' => 50]); + $enemiesTable->addColumn('health', 'integer'); + $enemiesTable->addColumn('attack', 'integer'); + $enemiesTable->addColumn('defense', 'integer'); // 新增 defense 字段 + $enemiesTable->addColumn('xp_value', 'integer'); + $enemiesTable->addColumn('drop_table_id', 'integer'); + $enemiesTable->setPrimaryKey(['id']); + + // --- 玩家存档表:存档数据 (后续使用) --- + $saveTable = $schema->createTable('player_save'); + $saveTable->addColumn('id', 'integer', ['autoincrement' => true]); + $saveTable->addColumn('player_name', 'string'); + $saveTable->addColumn('data_json', 'text'); // 存储玩家对象序列化数据 + $saveTable->setPrimaryKey(['id']); + + // 应用 Schema 变更 + $queries = $schema->toSql($this->connection->getDatabasePlatform()); + foreach ($queries as $query) { + // 安全地执行 SQL 语句 + $this->connection->executeStatement($query); + } + } + } + + public function getConnection(): Connection { + return $this->connection; + } + + /** + * 初始配置加载:将 JSON 数据填充到 SQLite + */ + public function loadInitialData(): void { + $jsonPath = __DIR__ . '/../../config/enemy_data.json'; + if (!file_exists($jsonPath)) { + echo "❌ 警告: 找不到初始配置数据文件 enemy_data.json\n"; + return; + } + + // 仅在敌人表为空时填充数据 + if ($this->connection->fetchOne('SELECT COUNT(*) FROM enemies') == 0) { + $data = json_decode(file_get_contents($jsonPath), true); + $this->connection->beginTransaction(); + try { + foreach ($data as $enemy) { + $this->connection->insert('enemies', [ + 'name' => $enemy['name'], + 'health' => $enemy['health'], + 'attack' => $enemy['attack'], + 'defense' => $enemy['defense'], + 'xp_value' => $enemy['xp'], + 'drop_table_id' => $enemy['dropId'] + ]); + } + $this->connection->commit(); + echo "✅ 初始敌人数据已载入 SQLite 数据库。\n"; + } catch (\Exception $e) { + $this->connection->rollBack(); + echo "❌ 数据填充失败: " . $e->getMessage() . "\n"; + } + } + } +} \ No newline at end of file diff --git a/src/Event/BattleEndEvent.php b/src/Event/BattleEndEvent.php new file mode 100644 index 0000000..1573616 --- /dev/null +++ b/src/Event/BattleEndEvent.php @@ -0,0 +1,17 @@ + $isWin, + 'enemy' => $enemy + ]); + } +} \ No newline at end of file diff --git a/src/Event/Event.php b/src/Event/Event.php new file mode 100644 index 0000000..e7cfbe5 --- /dev/null +++ b/src/Event/Event.php @@ -0,0 +1,32 @@ +type = $type; + $this->payload = $payload; + } + + /** + * 获取事件的类型字符串。 + */ + public final function getType(): string { + return $this->type; + } + + /** + * 获取事件携带的数据。 + */ + public final function getPayload(): array { + return $this->payload; + } + + // final 关键字防止子类修改核心 getter 方法,保证事件数据的一致性。 +} \ No newline at end of file diff --git a/src/Event/EventDispatcher.php b/src/Event/EventDispatcher.php new file mode 100644 index 0000000..99ae1a1 --- /dev/null +++ b/src/Event/EventDispatcher.php @@ -0,0 +1,43 @@ +> + */ + private array $listeners = []; + + /** + * 注册一个监听器到总线。 + * 监听器需要自己判断对哪些事件感兴趣。 + */ + public function registerListener(EventListenerInterface $listener): void { + // 我们可以简化处理,直接将监听器按类名存储,因为我们只有一个 handleEvent 方法 + $listenerClass = get_class($listener); + if (!in_array($listener, $this->listeners, true)) { + $this->listeners[] = $listener; + } + + // 更好做法是按事件类型注册,但这里为了简化,我们让所有监听器接收所有事件 + } + + /** + * 分发一个事件给所有注册的监听器。 + */ + public function dispatch(Event $event): void { + // + + $eventType = $event->getType(); + + // 打印调试信息(可选,但推荐) + // echo ">> DEBUG: 触发事件: {$eventType}\n"; + + foreach ($this->listeners as $listener) { + // 将事件转发给每个监听器处理 + $listener->handleEvent($event); + } + } +} \ No newline at end of file diff --git a/src/Event/EventListenerInterface.php b/src/Event/EventListenerInterface.php new file mode 100644 index 0000000..2121133 --- /dev/null +++ b/src/Event/EventListenerInterface.php @@ -0,0 +1,6 @@ + $tileId]); + } +} \ No newline at end of file diff --git a/src/Event/MapMoveEvent.php b/src/Event/MapMoveEvent.php new file mode 100644 index 0000000..a24380e --- /dev/null +++ b/src/Event/MapMoveEvent.php @@ -0,0 +1,12 @@ + $newTileId]); + } +} \ No newline at end of file diff --git a/src/Event/ShowStatsEvent.php b/src/Event/ShowStatsEvent.php new file mode 100644 index 0000000..b76b0a3 --- /dev/null +++ b/src/Event/ShowStatsEvent.php @@ -0,0 +1,14 @@ + $player]); + } +} \ No newline at end of file diff --git a/src/Event/StartBattleEvent.php b/src/Event/StartBattleEvent.php new file mode 100644 index 0000000..d146167 --- /dev/null +++ b/src/Event/StartBattleEvent.php @@ -0,0 +1,12 @@ + $enemyId]); + } +} \ No newline at end of file diff --git a/src/Model/Character.php b/src/Model/Character.php new file mode 100644 index 0000000..c02c4ef --- /dev/null +++ b/src/Model/Character.php @@ -0,0 +1,105 @@ + ['currentCount' => 0, 'isCompleted' => false]] + protected array $completedQuests = []; // 存储已完成的任务 ID +// ⭐ 新增方法:添加/接受任务 + public function addActiveQuest(string $questId, int $targetCount): void { + $this->activeQuests[$questId] = ['currentCount' => 0, 'isCompleted' => false, 'targetCount' => $targetCount]; + } + + // ⭐ 新增方法:获取进行中的任务 + public function getActiveQuests(): array { + return $this->activeQuests; + } + + // ⭐ 新增方法:更新任务进度 + public function updateQuestProgress(string $questId, int $count = 1): void { + if (isset($this->activeQuests[$questId])) { + $progress = &$this->activeQuests[$questId]; // 使用引用 + if (!$progress['isCompleted']) { + $progress['currentCount'] += $count; + if ($progress['currentCount'] >= $progress['targetCount']) { + $progress['currentCount'] = $progress['targetCount']; + $progress['isCompleted'] = true; + // 触发 QuestCompletedEventRequest + // 注意:实际的奖励和标记完成应在 QuestService 确认后进行 + } + } + } + } + + // ⭐ 新增方法:标记任务完成 + public function markQuestCompleted(string $questId): void { + unset($this->activeQuests[$questId]); + $this->completedQuests[] = $questId; + } + + // ⭐ 新增方法:检查任务是否已完成 + public function isQuestCompleted(string $questId): bool { + return in_array($questId, $this->completedQuests); + } + + // ⭐ 新增:玩家背包 (存储 Item 实例) + protected array $inventory = []; + + // ... 现有构造函数和 Getter ... + + // ⭐ 新增方法:添加物品到背包 + public function addItem(Item $item): void { + $this->inventory[] = $item; + } + + // ⭐ 新增方法:获取背包 + public function getInventory(): array { + return $this->inventory; + } + + public function __construct(string $name, int $maxHealth, int $attack, int $defense) { + $this->name = $name; + $this->maxHealth = $maxHealth; + $this->health = $maxHealth; + $this->attack = $attack; + $this->defense = $defense; + } + + public function getName(): string { return $this->name; } + public function getHealth(): int { return $this->health; } + public function getMaxHealth(): int { return $this->maxHealth; } + public function getAttack(): int { return $this->attack; } + public function getDefense(): int { return $this->defense; } + + public function isAlive(): bool { return $this->health > 0; } + + /** + * 接收伤害,返回实际受到的伤害量 + */ + public function takeDamage(int $damage): int { + $actualDamage = max(0, $damage - $this->defense); + $this->health -= $actualDamage; + if ($this->health < 0) { + $this->health = 0; + } + return $actualDamage; + } + + /** + * 治疗角色 + */ + public function heal(int $amount): void { + $this->health += $amount; + if ($this->health > $this->maxHealth) { + $this->health = $this->maxHealth; + } + } +} \ No newline at end of file diff --git a/src/Model/Enemy.php b/src/Model/Enemy.php new file mode 100644 index 0000000..d398600 --- /dev/null +++ b/src/Model/Enemy.php @@ -0,0 +1,22 @@ +id = $id; + $this->xpValue = $xpValue; + } + + // Enemy 特有的 Getter 方法 + public function getId(): string { return $this->id; } + public function getXpValue(): int { return $this->xpValue; } + + // 注意:takeDamage() 和 isAlive() 等方法都直接继承自 Character,无需重复实现! +} \ No newline at end of file diff --git a/src/Model/Item.php b/src/Model/Item.php new file mode 100644 index 0000000..4299e3b --- /dev/null +++ b/src/Model/Item.php @@ -0,0 +1,21 @@ +id = $id; + $this->name = $name; + $this->type = $type; + $this->description = $description; + $this->value = $value; + } +} \ No newline at end of file diff --git a/src/Model/MapTile.php b/src/Model/MapTile.php new file mode 100644 index 0000000..03ee8a0 --- /dev/null +++ b/src/Model/MapTile.php @@ -0,0 +1,21 @@ + 'FOREST_02', 'S' => 'TOWN_01'] + public int $eventPoolId; // 区域对应的事件池ID + + public function __construct(string $id, string $name, string $description, array $connections, int $eventPoolId) { + $this->id = $id; + $this->name = $name; + $this->description = $description; + $this->connections = $connections; + $this->eventPoolId = $eventPoolId; + } +} \ No newline at end of file diff --git a/src/Model/NPC.php b/src/Model/NPC.php new file mode 100644 index 0000000..5354da7 --- /dev/null +++ b/src/Model/NPC.php @@ -0,0 +1,28 @@ +id = $id; + $this->name = $name; + $this->dialogue = $dialogue; + } + + public function getName(): string { + return $this->name; + } + + /** + * 获取指定对话ID的文本 + */ + public function getDialogueText(string $key): string { + return $this->dialogue[$key] ?? "NPC:对不起,我不知道你在说什么。"; + } +} \ No newline at end of file diff --git a/src/Model/Player.php b/src/Model/Player.php new file mode 100644 index 0000000..8b1979d --- /dev/null +++ b/src/Model/Player.php @@ -0,0 +1,25 @@ +level; } + public function getCurrentXp(): int { return $this->currentXp; } + public function getXpToNextLevel(): int { return $this->xpToNextLevel; } + + // Player 特有的 Setter/Modifier 方法 + public function gainXp(int $amount): void { + $this->currentXp += $amount; + // TODO: 未来在这里实现升级逻辑 (LevelUpEvent) + } +} \ No newline at end of file diff --git a/src/Model/Quest.php b/src/Model/Quest.php new file mode 100644 index 0000000..18810f7 --- /dev/null +++ b/src/Model/Quest.php @@ -0,0 +1,24 @@ + 'GOBLIN_1', 'count' => 5] + public array $rewards; // 奖励,e.g., ['xp' => 100, 'itemId' => 1] + public bool $isRepeatable = false; + + public function __construct(string $id, string $name, string $description, string $type, array $target, array $rewards) { + $this->id = $id; + $this->name = $name; + $this->description = $description; + $this->type = $type; + $this->target = $target; + $this->rewards = $rewards; + } +} \ No newline at end of file diff --git a/src/System/BattleService.php b/src/System/BattleService.php new file mode 100644 index 0000000..b95bd6f --- /dev/null +++ b/src/System/BattleService.php @@ -0,0 +1,232 @@ +dispatcher = $dispatcher; + $this->stateManager = $stateManager; + $this->input = $input; + $this->output = $output; + $this->helper = $helper; + } + + public function handleEvent(Event $event): void { + if ($this->inBattle) { + // 如果在战斗中,可以监听 'BattleCommandEvent' 等事件来处理输入 + // 当前版本,我们通过 battleLoop() 内部阻塞输入 + return; + } + + switch ($event->getType()) { + case 'StartBattleEvent': + // 收到 MapSystem 触发的战斗开始事件 + $enemyId = $event->getPayload()['enemyId']; + $this->startBattle($enemyId); + break; + } + } + + /** + * 1. 初始化战斗状态 + */ + private function startBattle(int $enemyId): void { + // ... 初始化敌人逻辑 (继承自 Character) ... + $enemyData = $this->loadEnemyData($enemyId); + $this->currentEnemy = new Enemy( + (string)$enemyId, + $enemyData['name'], + $enemyData['health'], + $enemyData['attack'], + $enemyData['defense'], + $enemyData['xp'] + ); + $this->inBattle = true; + + $this->dispatcher->dispatch(new Event('SystemMessage', [ + 'message' => "⚔️ 你遭遇了 {$this->currentEnemy->getName()}!战斗开始!" + ])); + + // 立即进入战斗循环 + $this->battleLoop(); + } + + /** + * 2. 核心战斗循环 + */ + private function battleLoop(): void { + while ($this->inBattle) { + + // 确保 UI 服务打印了战斗状态 + $this->dispatcher->dispatch(new Event('ShowBattleStatsEvent', [ + 'enemy' => $this->currentEnemy, + 'player' => $this->stateManager->getPlayer(), + ])); + + // 玩家回合 (阻塞输入) + $playerAction = $this->promptPlayerAction(); + + if ($playerAction === 'A') { + $this->playerAttack(); + } elseif ($playerAction === 'R') { + if ($this->tryRunAway()) { + $this->endBattle(false); + return; + } + } + + if (!$this->inBattle) break; + + // 敌人回合 + $this->enemyAttack(); + + if (!$this->inBattle) break; + } + } + + /** + * 获取玩家战斗指令 (直接使用注入的 I/O 接口) + */ + private function promptPlayerAction(): string { + $this->dispatcher->dispatch(new Event('SystemMessage', [ + 'message' => "\n--- 你的回合 --- [A] 攻击 | [R] 逃跑" + ])); + + $question = new Question("> 请选择指令 (A/R):"); + + // 关键:使用注入的 I/O 接口 + $choice = strtoupper($this->helper->ask($this->input, $this->output, $question) ?? ''); + + // 简单的输入验证 + if (in_array($choice, ['A', 'R'])) { + return $choice; + } else { + $this->dispatcher->dispatch(new Event('SystemMessage', ['message' => '无效的战斗指令。'])); + return $this->promptPlayerAction(); + } + } + + /** + * 玩家攻击逻辑 + */ + private function playerAttack(): void { + $player = $this->stateManager->getPlayer(); + $rawDamage = $player->getAttack(); + + $actualDamage = $this->currentEnemy->takeDamage($rawDamage); + + $this->dispatcher->dispatch(new Event('SystemMessage', [ + 'message' => "⚡ 你对 {$this->currentEnemy->getName()} 造成了 {$actualDamage} 点伤害!" + ])); + + if (!$this->currentEnemy->isAlive()) { + $this->handleWin(); + } + } + + /** + * 敌人攻击逻辑 + */ + private function enemyAttack(): void { + $player = $this->stateManager->getPlayer(); + $rawDamage = $this->currentEnemy->getAttack(); + + // Player 的 takeDamage 会更新 StateManager 中的 Player 实例 + $actualDamage = $player->takeDamage($rawDamage); + + $this->dispatcher->dispatch(new Event('SystemMessage', [ + 'message' => "💥 {$this->currentEnemy->getName()} 对你造成了 {$actualDamage} 点伤害!" + ])); + + if (!$player->isAlive()) { + $this->handleLoss(); + } + } + + /** + * 尝试逃跑 + */ + private function tryRunAway(): bool { + if (rand(1, 100) > 50) { + $this->dispatcher->dispatch(new Event('SystemMessage', ['message' => "💨 你成功逃离了战斗!"])); + return true; + } else { + $this->dispatcher->dispatch(new Event('SystemMessage', ['message' => "被 {$this->currentEnemy->getName()} 拦住了!逃跑失败。"])); + return false; + } + } + + /** + * 3. 战斗胜利处理 + */ + private function handleWin(): void { + $xpGained = $this->currentEnemy->getXpValue(); + $this->dispatcher->dispatch(new Event('SystemMessage', [ + 'message' => "🏆 恭喜你击败了 {$this->currentEnemy->getName()}!" + ])); + + // 触发 XP 和 Loot 事件,交由其他系统处理 + $this->dispatcher->dispatch(new Event('XpGainedEvent', ['xp' => $xpGained])); + $this->dispatcher->dispatch(new Event('LootDropEvent', ['enemyId' => $this->currentEnemy->getId()])); + + $this->endBattle(true); + } + + /** + * 4. 战斗失败处理 + */ + private function handleLoss(): void { + $this->dispatcher->dispatch(new Event('SystemMessage', ['message' => "💀 你被击败了... 游戏结束。"])); + // TODO: 触发 GameEndEvent 或 RespawnEvent + $this->endBattle(false); + } + + /** + * 结束战斗状态 + */ + private function endBattle(bool $isWin): void { + $this->inBattle = false; + $this->currentEnemy = null; + + // 战斗结束后,重新打印主菜单请求,以继续主循环 + $this->dispatcher->dispatch(new Event('ShowMenuEvent')); + } + + /** + * 模拟从配置中加载敌人数据 + */ + private function loadEnemyData(int $id): array { + // 在实际项目中,这应该从数据库或 JSON 文件加载 + return match ($id) { + 1 => ['name' => '弱小的哥布林', 'health' => 20, 'attack' => 5, 'defense' => 1, 'xp' => 10], + 2 => ['name' => '愤怒的野猪', 'health' => 35, 'attack' => 8, 'defense' => 3, 'xp' => 25], + 3 => ['name' => '森林狼', 'health' => 40, 'attack' => 10, 'defense' => 5, 'xp' => 40], + default => ['name' => '未知生物', 'health' => 1, 'attack' => 1, 'defense' => 0, 'xp' => 1], + }; + } +} \ No newline at end of file diff --git a/src/System/CharacterService.php b/src/System/CharacterService.php new file mode 100644 index 0000000..dd4d5f3 --- /dev/null +++ b/src/System/CharacterService.php @@ -0,0 +1,76 @@ +dispatcher = $dispatcher; + $this->stateManager = $stateManager; + } + + public function handleEvent(Event $event): void { + switch ($event->getType()) { + case 'XpGainedEvent': + $this->handleXpGain($event->getPayload()['xp']); + break; + // TODO: 未来添加 'HealRequest' 或 'UseItemEvent' 等事件处理 + } + } + + /** + * 处理经验值获取和潜在升级逻辑 + */ + private function handleXpGain(int $xpAmount): void { + $player = $this->stateManager->getPlayer(); + $initialLevel = $player->getLevel(); + + // 尝试添加经验值 (这个方法现在在 Player 模型中) + $player->gainXp($xpAmount); + + $this->dispatcher->dispatch(new Event('SystemMessage', [ + 'message' => "🌟 获得了 {$xpAmount} 点经验值!" + ])); + + // 检查是否升级 + while ($player->getCurrentXp() >= $player->getXpToNextLevel()) { + $this->levelUp($player); + } + + if ($player->getLevel() > $initialLevel) { + $this->dispatcher->dispatch(new Event('SystemMessage', [ + 'message' => "🎉 恭喜你,升到了等级 {$player->getLevel()}!" + ])); + } + } + + /** + * 执行升级操作 + */ + private function levelUp(Player $player): void { + // 1. 扣除经验,提升等级 + $player->subtractXpToNextLevel(); // 假设我们在 Player 模型中添加此方法 + $player->incrementLevel(); // 假设我们在 Player 模型中添加此方法 + + // 2. 提升属性 (简化版:每次升级增加固定属性) + $player->increaseMaxHealth(10); + $player->increaseAttack(2); + $player->increaseDefense(1); + + // 3. 升级后回满血 + $player->heal($player->getMaxHealth()); // 使用 Character 基类中的 heal() + + // 4. 触发升级事件 (如果需要其他系统知道) + $this->dispatcher->dispatch(new Event('LevelUpEvent', ['newLevel' => $player->getLevel()])); + } +} \ No newline at end of file diff --git a/src/System/InputHandler.php b/src/System/InputHandler.php new file mode 100644 index 0000000..bedbbd1 --- /dev/null +++ b/src/System/InputHandler.php @@ -0,0 +1,79 @@ +dispatcher = $dispatcher; + $this->input = $input; + $this->output = $output; + $this->helper = $helper; + } + + /** + * 获取主循环操作指令,并分派事件 + */ + public function handleMainCommand(): bool { + // 1. 请求 UI 服务打印主菜单 (确保 UI 已输出提示) + $this->dispatcher->dispatch(new Event('ShowMenuEvent')); + + $question = new Question("> 请选择操作 (M/E/S/I/Q):"); + $choice = strtoupper($this->helper->ask($this->input, $this->output, $question) ?? ''); + + // 2. 解析并分派事件 + switch ($choice) { + case 'M': + $this->handleMoveInput(); + break; + case 'E': + $this->dispatcher->dispatch(new Event('MapExploreRequest')); + break; + case 'S': + // 状态显示请求,由于 StateManager 是状态持有者,EventDispatcher 会分发给 UIService + $this->dispatcher->dispatch(new Event('ShowStatsRequest')); + break; + case 'I': // ⭐ 新增交互指令 + // 模拟 MapSystem 发现了一个 NPC + $this->dispatcher->dispatch(new Event('AttemptInteractEvent', ['npcId' => 'VILLAGER_1'])); + break; + case 'Q': + return false; // 返回 false 通知 GameCommand 退出主循环 + default: + $this->dispatcher->dispatch(new Event('SystemMessage', ['message' => '无效的指令。请使用 M, E, S, I, Q。'])); + } + return true; // 继续主循环 + } + + /** + * 处理移动输入和事件分派 + */ + private function handleMoveInput(): void { + $directionQuestion = new Question("请输入移动方向 (N/S/E/W):"); + $direction = strtoupper($this->helper->ask($this->input, $this->output, $directionQuestion) ?? ''); + + $validDirections = ['N', 'S', 'E', 'W']; + if (in_array($direction, $validDirections)) { + $this->dispatcher->dispatch(new Event('AttemptMoveEvent', ['direction' => $direction])); + } else { + $this->dispatcher->dispatch(new Event('SystemMessage', ['message' => '无效的移动方向。'])); + } + } + + // TODO: 可以在这里添加 handleBattleInput() 等,进一步解耦 BattleService +} \ No newline at end of file diff --git a/src/System/InteractionSystem.php b/src/System/InteractionSystem.php new file mode 100644 index 0000000..28fc6ff --- /dev/null +++ b/src/System/InteractionSystem.php @@ -0,0 +1,137 @@ +dispatcher = $dispatcher; + $this->stateManager = $stateManager; + $this->input = $input; + $this->output = $output; + $this->helper = $helper; + } + + public function handleEvent(Event $event): void { + switch ($event->getType()) { + case 'AttemptInteractEvent': + // 假设 MapSystem 会提供当前 Tile 上的 NPC ID + $npcId = $event->getPayload()['npcId']; + $this->startInteraction($npcId); + break; + } + } + + /** + * 1. 初始化 NPC 交互 + */ + private function startInteraction(string $npcId): void { + $npc = $this->loadNPC($npcId); + if (!$npc) { + $this->dispatcher->dispatch(new Event('SystemMessage', ['message' => "这里没有人可以交谈。"])); + return; + } + + $this->dispatcher->dispatch(new Event('SystemMessage', [ + 'message' => "👤 你走近了 {$npc->getName()}。" + ])); + + // 启动对话流程 + $this->dialogueLoop($npc); + + // 交互结束后,重新打印主菜单请求 + $this->dispatcher->dispatch(new Event('ShowMenuEvent')); + } + + /** + * 2. 核心对话循环 + */ + private function dialogueLoop(NPC $npc): void { + $currentDialogueKey = 'greeting'; + $running = true; + + while ($running) { + + // 打印 NPC 对话 + $text = $npc->getDialogueText($currentDialogueKey); + $this->dispatcher->dispatch(new Event('SystemMessage', [ + 'message' => "{$npc->getName()}:{$text}" + ])); + + // 检查交互类型并获取玩家选择 + $choice = $this->promptPlayerChoice(); + + switch ($choice) { + case 'T': // 触发任务/特殊事件 + $this->dispatcher->dispatch(new Event('QuestCheckEvent', ['npcId' => $npc->id])); + $currentDialogueKey = 'quest_response'; + break; + case 'S': // 触发商店 + $this->dispatcher->dispatch(new Event('OpenShopEvent', ['npcId' => $npc->id])); + $currentDialogueKey = 'shop_response'; + break; + case 'E': // 结束对话 + $running = false; + $this->dispatcher->dispatch(new Event('SystemMessage', ['message' => "🤝 你结束了与 {$npc->getName()} 的对话。"])); + break; + default: + $this->dispatcher->dispatch(new Event('SystemMessage', ['message' => '无效的交互指令。'])); + } + } + } + + /** + * 获取玩家交互指令 + */ + private function promptPlayerChoice(): string { + $this->dispatcher->dispatch(new Event('SystemMessage', [ + 'message' => "\n--- 交互菜单 --- [T] 任务 | [S] 商店 | [E] 结束" + ])); + + $question = new Question("> 请选择指令 (T/S/E):"); + $choice = strtoupper($this->helper->ask($this->input, $this->output, $question) ?? ''); + + return $choice; + } + + /** + * 模拟从配置中加载 NPC 数据 + */ + private function loadNPC(string $id): ?NPC { + $data = match ($id) { + 'VILLAGER_1' => [ + 'name' => '老村长', + 'dialogue' => [ + 'greeting' => '你好,旅行者。你看起来很强大。', + 'quest_response' => '你想要帮忙吗?我们的地窖里有老鼠。', + 'shop_response' => '我现在没有东西卖给你。', + ] + ], + default => null, + }; + + if ($data) { + return new NPC($id, $data['name'], $data['dialogue']); + } + return null; + } +} \ No newline at end of file diff --git a/src/System/LootService.php b/src/System/LootService.php new file mode 100644 index 0000000..8922517 --- /dev/null +++ b/src/System/LootService.php @@ -0,0 +1,99 @@ +dispatcher = $dispatcher; + $this->stateManager = $stateManager; + } + + public function handleEvent(Event $event): void { + switch ($event->getType()) { + case 'LootDropEvent': + $enemyId = $event->getPayload()['enemyId']; + $this->handleLootDrop($enemyId); + break; + case 'LootFoundEvent': // 响应 MapSystem 探索时发现的宝箱 + $lootId = $event->getPayload()['lootId']; + $this->handleLootFound($lootId); + break; + } + } + + /** + * 处理敌人死亡时的掉落逻辑 + */ + private function handleLootDrop(string $enemyId): void { + // 简化:总是掉落物品 ID 1 (小药水) + $roll = rand(1, 100); + if ($roll <= 70) { // 70% 掉落几率 + $this->giveItemToPlayer(1); + } else { + $this->dispatcher->dispatch(new Event('SystemMessage', [ + 'message' => "战利品很少,只找到了一些零钱。" + ])); + } + } + + /** + * 处理探索时发现的宝箱/固定物品 + */ + private function handleLootFound(int $lootId): void { + // 假设 lootId 5 是一个宝箱,里面是物品 ID 2 + if ($lootId === 5) { + $this->dispatcher->dispatch(new Event('SystemMessage', [ + 'message' => "🗝️ 你打开了宝箱!" + ])); + $this->giveItemToPlayer(2); + } + } + + /** + * 核心逻辑:创建物品实例并添加到玩家背包 + */ + private function giveItemToPlayer(int $itemId): void { + $itemData = $this->loadItemData($itemId); + + $item = new Item( + $itemId, + $itemData['name'], + $itemData['type'], + $itemData['description'], + $itemData['value'] + ); + + $player = $this->stateManager->getPlayer(); + $player->addItem($item); + + $this->dispatcher->dispatch(new Event('SystemMessage', [ + 'message' => "➕ 获得了物品:{$item->name}!" + ])); + + // 触发 InventoryUpdateEvent (未来用于 UI 实时更新) + $this->dispatcher->dispatch(new Event('InventoryUpdateEvent', ['playerInventory' => $player->getInventory()])); + } + + /** + * 模拟从配置中加载物品数据 + */ + private function loadItemData(int $id): array { + // 实际项目中应从数据库或 JSON 加载 + return match ($id) { + 1 => ['name' => '小型治疗药水', 'type' => 'potion', 'description' => '恢复少量生命。', 'value' => 10], + 2 => ['name' => '破旧的短剑', 'type' => 'weapon', 'description' => '攻击力微弱。', 'value' => 50], + default => ['name' => '垃圾', 'type' => 'misc', 'description' => '毫无价值的杂物。', 'value' => 1], + }; + } +} \ No newline at end of file diff --git a/src/System/MapSystem.php b/src/System/MapSystem.php new file mode 100644 index 0000000..744e6d1 --- /dev/null +++ b/src/System/MapSystem.php @@ -0,0 +1,115 @@ +dispatcher = $dispatcher; + $this->stateManager = $stateManager; + $this->loadMapData(); + + // 游戏启动时,设置初始区域状态到 StateManager + $this->stateManager->setCurrentTile($this->getTile('TOWN_01')); + } + + private function loadMapData(): void { + $jsonPath = __DIR__ . '/../../config/map_data.json'; + if (!file_exists($jsonPath)) { + throw new \Exception("Map configuration file not found."); + } + $this->mapData = json_decode(file_get_contents($jsonPath), true); + } + + public function getTile(string $tileId): MapTile { + if (!isset($this->mapData[$tileId])) { + throw new \Exception("MapTile ID '{$tileId}' not found in configuration."); + } + $data = $this->mapData[$tileId]; + return new MapTile($tileId, $data['name'], $data['description'], $data['connections'], $data['eventPoolId']); + } + + public function handleEvent(Event $event): void { + switch ($event->getType()) { + case 'AttemptMoveEvent': + $this->handleMoveAttempt($event->getPayload()['direction']); + break; + case 'MapExploreRequest': // 修正:监听 MapExploreRequest 事件 (来自 GameCommand) + $this->handleExplore(); + break; + case 'GameStartEvent': + // 游戏开始,通知 UI 打印初始位置 + $initialTile = $this->stateManager->getCurrentTile(); + $this->dispatcher->dispatch(new Event('MapMoveEvent', ['newTileId' => $initialTile->id])); + break; + } + } + + /** + * 处理玩家移动逻辑 + */ + private function handleMoveAttempt(string $direction): void { + $direction = strtoupper($direction); + + // 从 StateManager 获取当前 Tile 的连接信息 + $currentTile = $this->stateManager->getCurrentTile(); + $connections = $currentTile->connections; + + if (isset($connections[$direction])) { + $newTileId = $connections[$direction]; + $newTile = $this->getTile($newTileId); + + // 1. 成功移动:更新 StateManager 中的状态 + $this->stateManager->setCurrentTile($newTile); + + // 2. 触发 MapMoveEvent,通知 UI 和其他系统 + $this->dispatcher->dispatch(new Event('MapMoveEvent', ['newTileId' => $newTileId])); + } else { + // 移动失败:通知 UI + $this->dispatcher->dispatch(new Event('SystemMessage', ['message' => "❌ 该方向 ({$direction}) 没有道路或无法通行。"])); + } + } + + /** + * 处理玩家探索逻辑:根据 eventPoolId 决定遭遇什么 + */ + private function handleExplore(): void { + // 从 StateManager 获取当前 Tile + $currentTile = $this->stateManager->getCurrentTile(); + + $roll = rand(1, 100); + + if ($currentTile->eventPoolId === 0) { // 城镇 + $this->dispatcher->dispatch(new Event('SystemMessage', ['message' => "你在镇上休息了一会儿,没有发现任何危险。"])); + } elseif ($roll <= 60) { + // 60% 几率遭遇战斗 + $enemyId = $this->getEnemyIdForArea($currentTile->eventPoolId); // 使用当前 Tile 的 eventPoolId + $this->dispatcher->dispatch(new StartBattleEvent($enemyId)); + } elseif ($roll <= 80) { + // 20% 几率发现宝箱/物品 + $this->dispatcher->dispatch(new Event('LootFoundEvent', ['lootId' => 5])); + $this->dispatcher->dispatch(new Event('SystemMessage', ['message' => "🎁 你发现了一个宝箱!"])); + } else { + // 20% 几率没有遭遇 + $this->dispatcher->dispatch(new Event('SystemMessage', ['message' => "你在周围探索了一番,但什么也没发现。"])); + } + } + + // 简化的敌人 ID 获取 (应该从配置中获取) + private function getEnemyIdForArea(int $poolId): int { + return match ($poolId) { + 1 => rand(1, 2), // 新手区敌人 ID 1 或 2 + 2 => 3, // 森林敌人 ID 3 + default => 1, + }; + } +} \ No newline at end of file diff --git a/src/System/QuestService.php b/src/System/QuestService.php new file mode 100644 index 0000000..b037623 --- /dev/null +++ b/src/System/QuestService.php @@ -0,0 +1,154 @@ +dispatcher = $dispatcher; + $this->stateManager = $stateManager; + $this->loadQuestData(); + } + + private function loadQuestData(): void { + // 模拟从配置加载任务数据 + $this->questData = [ + 'RATS_1' => [ + 'name' => '地窖里的老鼠', + 'description' => '为老村长清除地窖里 5 只弱小的哥布林。', + 'type' => 'kill', + 'target' => ['targetId' => 1, 'count' => 5], // 目标敌人 ID 1 + 'rewards' => ['xp' => 100, 'itemId' => 1] + ], + // ... 其他任务 ... + ]; + } + + public function handleEvent(Event $event): void { + switch ($event->getType()) { + case 'QuestCheckEvent': // 响应 InteractionSystem 的任务请求 + $this->handleQuestCheck($event->getPayload()['npcId']); + break; + case 'BattleEndEvent': // 响应战斗结束,检查击杀目标 + $this->checkKillQuests($event->getPayload()['enemyId']); + break; + } + } + + /** + * 1. 处理 NPC 交互时的任务检查/接受 + */ + private function handleQuestCheck(string $npcId): void { + $player = $this->stateManager->getPlayer(); + $questId = $this->getQuestIdForNpc($npcId); + + if (!$questId) return; + + if ($player->isQuestCompleted($questId)) { + $this->dispatcher->dispatch(new Event('SystemMessage', ['message' => "(村长)我已经没什么可以教你的了,旅行者。"])); + } elseif (isset($player->getActiveQuests()[$questId])) { + $this->checkQuestCompletion($questId); // 检查是否可以提交 + } else { + // 接受任务 + $this->acceptQuest($questId); + } + } + + /** + * 2. 接受任务逻辑 + */ + private function acceptQuest(string $questId): void { + $player = $this->stateManager->getPlayer(); + $questConfig = $this->questData[$questId]; + + $player->addActiveQuest($questId, $questConfig['target']['count']); + + $this->dispatcher->dispatch(new Event('SystemMessage', [ + 'message' => "📜 接受任务:{$questConfig['name']} - 目标:{$questConfig['description']}" + ])); + } + + /** + * 3. 检查击杀类任务进度 + */ + private function checkKillQuests(string $killedEnemyId): void { + $player = $this->stateManager->getPlayer(); + $activeQuests = $player->getActiveQuests(); + + foreach ($activeQuests as $questId => $progress) { + $questConfig = $this->questData[$questId]; + + if ($questConfig['type'] === 'kill' && $questConfig['target']['targetId'] == $killedEnemyId) { + // 更新玩家任务进度 + $player->updateQuestProgress($questId, 1); + $this->dispatcher->dispatch(new Event('SystemMessage', [ + 'message' => "任务进度更新:[{$questConfig['name']}] {$player->getActiveQuests()[$questId]['currentCount']}/{$questConfig['target']['count']}" + ])); + + // 如果任务完成,触发 QuestCompletedEventRequest + if ($player->getActiveQuests()[$questId]['isCompleted']) { + $this->dispatcher->dispatch(new Event('SystemMessage', [ + 'message' => "任务 [{$questConfig['name']}] 已完成!请回去找NPC提交。" + ])); + } + } + } + } + + /** + * 4. 检查任务是否可以提交并给予奖励 + */ + private function checkQuestCompletion(string $questId): void { + $player = $this->stateManager->getPlayer(); + $progress = $player->getActiveQuests()[$questId]; + $questConfig = $this->questData[$questId]; + + if ($progress['isCompleted']) { + // 发放奖励 + $rewards = $questConfig['rewards']; + + // 经验值奖励 (交给 CharacterService) + if (isset($rewards['xp'])) { + $this->dispatcher->dispatch(new Event('XpGainedEvent', ['xp' => $rewards['xp']])); + } + + // 物品奖励 (交给 LootService) + if (isset($rewards['itemId'])) { + $this->dispatcher->dispatch(new Event('LootFoundEvent', ['lootId' => $rewards['itemId']])); + } + + // 标记玩家任务完成 + $player->markQuestCompleted($questId); + + $this->dispatcher->dispatch(new Event('SystemMessage', [ + 'message' => "🎉 任务提交成功! 获得了奖励。" + ])); + } else { + $this->dispatcher->dispatch(new Event('SystemMessage', [ + 'message' => "(村长)任务 [{$questConfig['name']}] 还没有完成。目标:{$progress['currentCount']}/{$progress['targetCount']}" + ])); + } + } + + /** + * 模拟:根据 NPC ID 获取对应的任务 ID + */ + private function getQuestIdForNpc(string $npcId): ?string { + return match ($npcId) { + 'VILLAGER_1' => 'RATS_1', + default => null, + }; + } +} \ No newline at end of file diff --git a/src/System/StateManager.php b/src/System/StateManager.php new file mode 100644 index 0000000..a5263fe --- /dev/null +++ b/src/System/StateManager.php @@ -0,0 +1,67 @@ +db = $db; + } + + /** + * 初始化或加载玩家状态 + * @param Player|null $player 如果为空,则尝试从数据库加载 + */ + public function setPlayer(Player $player): void { + $this->player = $player; + } + + public function getPlayer(): Player { + // 确保在访问前玩家实例已设置 + if (!isset($this->player)) { + throw new \RuntimeException("Player instance not initialized in StateManager."); + } + return $this->player; + } + + // --- 地图状态管理 --- + + public function setCurrentTile(MapTile $tile): void { + $this->currentTile = $tile; + } + + public function getCurrentTile(): MapTile { + if (!isset($this->currentTile)) { + // 假设 MapSystem 会在初始化时设置初始 Tile + throw new \RuntimeException("Current MapTile not set in StateManager."); + } + return $this->currentTile; + } + + /** + * TODO: 实现存档逻辑 (使用 Doctrine DBAL) + */ + public function saveGame(): void { + // 示例:将玩家对象序列化并存入数据库 + /* + $data = serialize($this->player); + $this->db->update('player_save', + ['data_json' => $data], + ['player_name' => $this->player->getName()] + ); + */ + // $this->eventDispatcher->dispatch(new Event('SystemMessage', ['message' => '游戏已保存。'])); + } + + // TODO: loadGame() 方法 +} \ No newline at end of file diff --git a/src/System/UIService.php b/src/System/UIService.php new file mode 100644 index 0000000..500e9cb --- /dev/null +++ b/src/System/UIService.php @@ -0,0 +1,102 @@ +output = $output; + $this->stateManager = $stateManager; + } + + public function handleEvent(Event $event): void { + switch ($event->getType()) { + case 'GameStartEvent': + $this->output->writeln("🔔 {$event->getPayload()['message']}"); + break; + + case 'ShowStatsEvent': + // 确保 Payload 中包含 Player 实例 + if (isset($event->getPayload()['player']) && $event->getPayload()['player'] instanceof Player) { + $this->displayPlayerStats($event->getPayload()['player']); + } + break; + + case 'SystemMessage': + $this->output->writeln("📣 {$event->getPayload()['message']}"); + break; + case 'ShowStatsRequest': + // UIService 现在从 StateManager 获取 Player 实例 + $player = $this->stateManager->getPlayer(); + $this->displayPlayerStats($player); + break; + + case 'MapMoveEvent': + // 收到 MapSystem 触发的移动成功事件 + $tileId = $event->getPayload()['newTileId']; + try { + // 通过 MapSystem 获取最新的 MapTile 数据 + $tile = $this->stateManager->getCurrentTile(); +// dd($tile); + $this->displayLocation($tile); + } catch (\Exception $e) { + $this->output->writeln("UI 错误:无法加载地图区域 {$tileId} {$e->getMessage()}。"); + } + break; + + case 'StartBattleEvent': + $this->output->writeln("\n\n⚔️ 遭遇战触发!请选择战斗指令..."); + break; + + // TODO: 在后续步骤中添加 BattleEndEvent, DamageDealtEvent 等处理 + } + } + + /** + * 打印玩家状态信息 + */ + private function displayPlayerStats(Player $player): void { + $this->output->writeln("\n--- 角色状态:{$player->getName()} ---"); + $this->output->writeln("等级: {$player->getLevel()}"); + $this->output->writeln("HP: {$player->getHealth()}/{$player->getMaxHealth()}"); + $this->output->writeln("攻击力: {$player->getAttack()}"); + $this->output->writeln("防御力: {$player->getDefense()}"); + $this->output->writeln("经验值: {$player->getCurrentXp()}/{$player->getXpToNextLevel()}"); + $this->output->writeln("--------------------------\n"); + } + + private function displayMainMenu(): void { + $this->output->writeln("\n--- 主菜单 ---"); + $this->output->writeln(" [M] 移动 | [E] 探索 | [S] 状态 | [I] 交互 | [Q] 退出"); // ⭐ 增加 I 选项 + } + /** + * 打印当前地图区域信息 + */ + private function displayLocation(MapTile $tile): void { + // 清屏(可选,增强沉浸感) + // $this->output->write("\033[2J\033[H"); + + $this->output->writeln("\n======== [ {$tile->name} ] ========"); + $this->output->writeln(" {$tile->description}"); + + // 格式化连接信息 + $connections = array_map(fn($dir, $id) => "{$dir}({$id})", array_keys($tile->connections), $tile->connections); + $this->output->writeln(" -> 可移动方向: " . implode(' | ', $connections)); + $this->output->writeln("===================================\n"); + } +} \ No newline at end of file diff --git a/storage/game.sqlite b/storage/game.sqlite new file mode 100644 index 0000000..071e274 Binary files /dev/null and b/storage/game.sqlite differ