Compare commits
No commits in common. "d63e7562655dffcd8ee5155ae0207bccd3fc0232" and "0658960b70aeeff62143e58115dc07248e7cddaa" have entirely different histories.
d63e756265
...
0658960b70
|
|
@ -12,9 +12,6 @@
|
||||||
<Configuration>
|
<Configuration>
|
||||||
<option name="path" value="$PROJECT_DIR$/tests" />
|
<option name="path" value="$PROJECT_DIR$/tests" />
|
||||||
</Configuration>
|
</Configuration>
|
||||||
<Configuration>
|
|
||||||
<option name="path" value="$PROJECT_DIR$/tests" />
|
|
||||||
</Configuration>
|
|
||||||
</list>
|
</list>
|
||||||
</option>
|
</option>
|
||||||
</component>
|
</component>
|
||||||
|
|
|
||||||
|
|
@ -11,9 +11,6 @@
|
||||||
<PhpSpecSuiteConfiguration>
|
<PhpSpecSuiteConfiguration>
|
||||||
<option name="myPath" value="$PROJECT_DIR$" />
|
<option name="myPath" value="$PROJECT_DIR$" />
|
||||||
</PhpSpecSuiteConfiguration>
|
</PhpSpecSuiteConfiguration>
|
||||||
<PhpSpecSuiteConfiguration>
|
|
||||||
<option name="myPath" value="$PROJECT_DIR$" />
|
|
||||||
</PhpSpecSuiteConfiguration>
|
|
||||||
</suites>
|
</suites>
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
|
|
@ -5,8 +5,7 @@
|
||||||
"require": {
|
"require": {
|
||||||
"php": ">=8.0",
|
"php": ">=8.0",
|
||||||
"symfony/console": "^6.4",
|
"symfony/console": "^6.4",
|
||||||
"symfony/var-dumper": "^6.4",
|
"symfony/var-dumper": "^6.4"
|
||||||
"cboden/ratchet": "^0.4"
|
|
||||||
},
|
},
|
||||||
"autoload": {
|
"autoload": {
|
||||||
"psr-4": {
|
"psr-4": {
|
||||||
|
|
|
||||||
1375
composer.lock
generated
1375
composer.lock
generated
File diff suppressed because it is too large
Load Diff
|
|
@ -70,7 +70,6 @@ class Game
|
||||||
$p = $data['player'] ?? [];
|
$p = $data['player'] ?? [];
|
||||||
$this->player->hp = $p['hp'] ?? $this->player->hp;
|
$this->player->hp = $p['hp'] ?? $this->player->hp;
|
||||||
$this->player->maxHp = $p['maxHp'] ?? $this->player->maxHp;
|
$this->player->maxHp = $p['maxHp'] ?? $this->player->maxHp;
|
||||||
$this->player->potionPool = $p['potionPool'] ?? $this->player->potionPool;
|
|
||||||
// 新属性系统,向后兼容旧存档
|
// 新属性系统,向后兼容旧存档
|
||||||
$this->player->patk = $p['patk'] ?? $p['atk'] ?? $this->player->patk;
|
$this->player->patk = $p['patk'] ?? $p['atk'] ?? $this->player->patk;
|
||||||
$this->player->matk = $p['matk'] ?? $this->player->matk;
|
$this->player->matk = $p['matk'] ?? $this->player->matk;
|
||||||
|
|
@ -151,7 +150,6 @@ class Game
|
||||||
'critdmg' => $this->player->critdmg,
|
'critdmg' => $this->player->critdmg,
|
||||||
'level' => $this->player->level,
|
'level' => $this->player->level,
|
||||||
'exp' => $this->player->exp,
|
'exp' => $this->player->exp,
|
||||||
'potionPool' => $this->player->potionPool,
|
|
||||||
'maxExp' => $this->player->maxExp,
|
'maxExp' => $this->player->maxExp,
|
||||||
'inventory' => $this->player->inventory,
|
'inventory' => $this->player->inventory,
|
||||||
'equip' => $this->player->equip,
|
'equip' => $this->player->equip,
|
||||||
|
|
|
||||||
|
|
@ -1,345 +0,0 @@
|
||||||
<?php
|
|
||||||
namespace Game\Core;
|
|
||||||
|
|
||||||
use Ratchet\MessageComponentInterface;
|
|
||||||
use Ratchet\ConnectionInterface;
|
|
||||||
use React\EventLoop\LoopInterface;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 进程转发 WebSocket 服务器
|
|
||||||
* 为每个连接启动一个 bin/game 进程
|
|
||||||
* 将进程的STDOUT/STDERR转发给客户端
|
|
||||||
* 将客户端输入写入进程的STDIN
|
|
||||||
*/
|
|
||||||
class GameProcessServer implements MessageComponentInterface
|
|
||||||
{
|
|
||||||
protected array $clients = [];
|
|
||||||
protected array $processes = [];
|
|
||||||
protected ?LoopInterface $loop = null;
|
|
||||||
protected array $timers = [];
|
|
||||||
|
|
||||||
public function __construct(?LoopInterface $loop = null)
|
|
||||||
{
|
|
||||||
$this->loop = $loop;
|
|
||||||
echo "[进程服务器] 初始化\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置事件循环(用于延迟初始化)
|
|
||||||
*/
|
|
||||||
public function setLoop(LoopInterface $loop): void
|
|
||||||
{
|
|
||||||
$this->loop = $loop;
|
|
||||||
echo "[进程服务器] 事件循环已设置\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 客户端连接时
|
|
||||||
*/
|
|
||||||
public function onOpen(ConnectionInterface $conn)
|
|
||||||
{
|
|
||||||
echo "[连接] 新连接: {$conn->resourceId}\n";
|
|
||||||
$this->clients[$conn->resourceId] = $conn;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 为该连接启动一个游戏进程
|
|
||||||
$process = $this->startGameProcess($conn->resourceId);
|
|
||||||
$this->processes[$conn->resourceId] = $process;
|
|
||||||
|
|
||||||
$this->sendMessage($conn, [
|
|
||||||
'type' => 'system',
|
|
||||||
'message' => '游戏进程已启动,正在加载...'
|
|
||||||
]);
|
|
||||||
|
|
||||||
// 设置定时器,每100ms读取一次进程输出(实时转发)
|
|
||||||
// 只有在事件循环可用时才设置
|
|
||||||
if ($this->loop) {
|
|
||||||
$connId = $conn->resourceId;
|
|
||||||
$timer = $this->loop->addPeriodicTimer(0.1, function() use ($conn, $connId) {
|
|
||||||
// 检查连接是否仍然有效
|
|
||||||
if (!isset($this->clients[$connId]) || !isset($this->processes[$connId])) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 读取进程输出并发送给客户端
|
|
||||||
$this->readProcessOutput($conn);
|
|
||||||
});
|
|
||||||
|
|
||||||
$this->timers[$connId] = $timer;
|
|
||||||
echo "[定时器] 为连接 {$connId} 启动输出轮询\n";
|
|
||||||
} else {
|
|
||||||
echo "[警告] 事件循环未设置,无法启动输出轮询\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
$this->sendError($conn, '启动游戏失败: ' . $e->getMessage());
|
|
||||||
$conn->close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 启动游戏进程
|
|
||||||
*/
|
|
||||||
private function startGameProcess(string $connId): array
|
|
||||||
{
|
|
||||||
$gameDir = __DIR__ . '/../../';
|
|
||||||
$gameScript = $gameDir . 'bin/game';
|
|
||||||
|
|
||||||
// 检查脚本是否存在
|
|
||||||
if (!file_exists($gameScript)) {
|
|
||||||
throw new \Exception("游戏脚本不存在: $gameScript");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设置管道描述符
|
|
||||||
$descriptorspec = [
|
|
||||||
0 => ['pipe', 'r'], // stdin
|
|
||||||
1 => ['pipe', 'w'], // stdout
|
|
||||||
2 => ['pipe', 'w'], // stderr
|
|
||||||
];
|
|
||||||
|
|
||||||
// 启动进程
|
|
||||||
$process = proc_open(
|
|
||||||
'php ' . escapeshellarg($gameScript),
|
|
||||||
$descriptorspec,
|
|
||||||
$pipes,
|
|
||||||
$gameDir,
|
|
||||||
[
|
|
||||||
'TERM' => 'xterm-256color',
|
|
||||||
'LANG' => 'en_US.UTF-8',
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!is_resource($process)) {
|
|
||||||
throw new \Exception('无法启动游戏进程');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设置非阻塞模式
|
|
||||||
stream_set_blocking($pipes[0], false);
|
|
||||||
stream_set_blocking($pipes[1], false);
|
|
||||||
stream_set_blocking($pipes[2], false);
|
|
||||||
|
|
||||||
echo "[进程] 已启动进程 {$connId}: " . getmypid() . "\n";
|
|
||||||
|
|
||||||
// 启动输出读取线程(使用select轮询)
|
|
||||||
$this->startOutputReader($connId, $pipes[1], $pipes[2]);
|
|
||||||
|
|
||||||
return [
|
|
||||||
'process' => $process,
|
|
||||||
'stdin' => $pipes[0],
|
|
||||||
'stdout' => $pipes[1],
|
|
||||||
'stderr' => $pipes[2],
|
|
||||||
'connId' => $connId,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 启动异步输出读取
|
|
||||||
*/
|
|
||||||
private function startOutputReader(string $connId, $stdout, $stderr): void
|
|
||||||
{
|
|
||||||
// 已由 onOpen() 中的事件循环定时器处理
|
|
||||||
// 不再需要额外的初始化
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 接收消息(用户输入)
|
|
||||||
*/
|
|
||||||
public function onMessage(ConnectionInterface $from, $msg)
|
|
||||||
{
|
|
||||||
$data = json_decode($msg, true);
|
|
||||||
if (!$data) {
|
|
||||||
return; // 忽略无效消息
|
|
||||||
}
|
|
||||||
|
|
||||||
$type = $data['type'] ?? null;
|
|
||||||
$input = $data['input'] ?? '';
|
|
||||||
|
|
||||||
if ($type === 'input') {
|
|
||||||
// 将输入写入进程的STDIN
|
|
||||||
$process = $this->processes[$from->resourceId] ?? null;
|
|
||||||
if ($process && is_resource($process['stdin'])) {
|
|
||||||
$bytesWritten = @fwrite($process['stdin'], $input . "\n");
|
|
||||||
if ($bytesWritten === false || $bytesWritten === 0) {
|
|
||||||
// 写入失败,可能进程已关闭
|
|
||||||
echo "[警告] 无法写入进程 {$from->resourceId},进程可能已关闭\n";
|
|
||||||
// 不中断连接,等待下一次尝试或超时
|
|
||||||
} else {
|
|
||||||
fflush($process['stdin']);
|
|
||||||
echo "[输入] {$from->resourceId}: {$input}\n";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} elseif ($type === 'ping') {
|
|
||||||
$this->sendMessage($from, ['type' => 'pong']);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 尝试读取进程输出
|
|
||||||
$this->readProcessOutput($from);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 读取进程输出
|
|
||||||
*/
|
|
||||||
private function readProcessOutput(ConnectionInterface $conn): void
|
|
||||||
{
|
|
||||||
$connId = $conn->resourceId;
|
|
||||||
$process = $this->processes[$connId] ?? null;
|
|
||||||
if (!$process) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查进程是否还在运行
|
|
||||||
$processStatus = proc_get_status($process['process']);
|
|
||||||
if ($processStatus && !$processStatus['running']) {
|
|
||||||
// 进程已经退出,读取最后的输出并关闭连接
|
|
||||||
echo "[进程] 进程 {$connId} 已退出,状态码: " . $processStatus['exitcode'] . "\n";
|
|
||||||
|
|
||||||
// 尝试读取任何剩余的输出
|
|
||||||
$finalOutput = '';
|
|
||||||
$bufferSize = 8192;
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
$chunk = @fread($process['stdout'], $bufferSize);
|
|
||||||
if ($chunk === '' || $chunk === false) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
$finalOutput .= $chunk;
|
|
||||||
}
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
$chunk = @fread($process['stderr'], $bufferSize);
|
|
||||||
if ($chunk === '' || $chunk === false) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
$finalOutput .= $chunk;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($finalOutput) {
|
|
||||||
$this->sendMessage($conn, [
|
|
||||||
'type' => 'output',
|
|
||||||
'text' => $finalOutput
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 发送进程退出消息
|
|
||||||
$this->sendMessage($conn, [
|
|
||||||
'type' => 'system',
|
|
||||||
'message' => '游戏进程已退出'
|
|
||||||
]);
|
|
||||||
|
|
||||||
// 关闭连接
|
|
||||||
$conn->close();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$output = '';
|
|
||||||
|
|
||||||
// 在非阻塞模式下,使用fread读取可用数据
|
|
||||||
// 这比fgets更适合处理交互式程序的输出
|
|
||||||
$bufferSize = 8192;
|
|
||||||
|
|
||||||
// 读取stdout
|
|
||||||
while (true) {
|
|
||||||
$chunk = @fread($process['stdout'], $bufferSize);
|
|
||||||
if ($chunk === '' || $chunk === false) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
$output .= $chunk;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 读取stderr
|
|
||||||
while (true) {
|
|
||||||
$chunk = @fread($process['stderr'], $bufferSize);
|
|
||||||
if ($chunk === '' || $chunk === false) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
$output .= $chunk;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果有输出,发送给客户端
|
|
||||||
if ($output) {
|
|
||||||
$this->sendMessage($conn, [
|
|
||||||
'type' => 'output',
|
|
||||||
'text' => $output
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 客户端关闭连接
|
|
||||||
*/
|
|
||||||
public function onClose(ConnectionInterface $conn)
|
|
||||||
{
|
|
||||||
$connId = $conn->resourceId;
|
|
||||||
|
|
||||||
// 取消定时器
|
|
||||||
if (isset($this->timers[$connId]) && $this->loop) {
|
|
||||||
$this->loop->cancelTimer($this->timers[$connId]);
|
|
||||||
unset($this->timers[$connId]);
|
|
||||||
echo "[定时器] 取消连接 {$connId} 的输出轮询\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
// 关闭进程
|
|
||||||
$process = $this->processes[$connId] ?? null;
|
|
||||||
if ($process) {
|
|
||||||
if (is_resource($process['stdin'])) {
|
|
||||||
fclose($process['stdin']);
|
|
||||||
}
|
|
||||||
if (is_resource($process['stdout'])) {
|
|
||||||
fclose($process['stdout']);
|
|
||||||
}
|
|
||||||
if (is_resource($process['stderr'])) {
|
|
||||||
fclose($process['stderr']);
|
|
||||||
}
|
|
||||||
if (is_resource($process['process'])) {
|
|
||||||
proc_terminate($process['process']);
|
|
||||||
proc_close($process['process']);
|
|
||||||
}
|
|
||||||
unset($this->processes[$connId]);
|
|
||||||
echo "[进程] 已终止进程: {$connId}\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
unset($this->clients[$connId]);
|
|
||||||
echo "[断开] 连接已关闭: {$connId}\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 连接错误
|
|
||||||
*/
|
|
||||||
public function onError(ConnectionInterface $conn, \Exception $e)
|
|
||||||
{
|
|
||||||
echo "[错误] {$conn->resourceId}: {$e->getMessage()}\n";
|
|
||||||
|
|
||||||
// 取消定时器
|
|
||||||
$connId = $conn->resourceId;
|
|
||||||
if (isset($this->timers[$connId]) && $this->loop) {
|
|
||||||
$this->loop->cancelTimer($this->timers[$connId]);
|
|
||||||
unset($this->timers[$connId]);
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->onClose($conn);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 发送消息给客户端
|
|
||||||
*/
|
|
||||||
protected function sendMessage(ConnectionInterface $conn, array $data): void
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
$msg = json_encode($data, JSON_UNESCAPED_UNICODE);
|
|
||||||
$conn->send($msg);
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
echo "[发送错误] {$e->getMessage()}\n";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 发送错误消息
|
|
||||||
*/
|
|
||||||
protected function sendError(ConnectionInterface $conn, string $message): void
|
|
||||||
{
|
|
||||||
$this->sendMessage($conn, [
|
|
||||||
'type' => 'error',
|
|
||||||
'message' => $message
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -40,9 +40,9 @@ class GameSession
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 处理用户输入并返回新界面(支持时间戳数据)
|
* 处理用户输入并返回新界面
|
||||||
*/
|
*/
|
||||||
public function handleInput(string $input): string|array
|
public function handleInput(string $input): string
|
||||||
{
|
{
|
||||||
$this->output->clear();
|
$this->output->clear();
|
||||||
|
|
||||||
|
|
@ -60,47 +60,7 @@ class GameSession
|
||||||
// 保存状态
|
// 保存状态
|
||||||
$this->game->saveState();
|
$this->game->saveState();
|
||||||
|
|
||||||
// 获取当前状态信息
|
return $this->output->getOutput();
|
||||||
$stateInfo = $this->getStateInfo();
|
|
||||||
$output = $this->output->getOutput();
|
|
||||||
|
|
||||||
// 返回包含状态信息的结构化数据
|
|
||||||
return [
|
|
||||||
'output' => $output,
|
|
||||||
'state' => $stateInfo['state'],
|
|
||||||
'stateName' => $stateInfo['stateName'],
|
|
||||||
'playerInfo' => $stateInfo['playerInfo'],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* SSE 流式战斗模式
|
|
||||||
* 当用户进入战斗时,使用SSE直接推送日志
|
|
||||||
*/
|
|
||||||
public function streamBattle(string $input): void
|
|
||||||
{
|
|
||||||
// 使用SSEOutput替换普通输出
|
|
||||||
$sseOutput = new \Game\Core\SSEOutput();
|
|
||||||
|
|
||||||
// 替换game的output对象
|
|
||||||
$this->game->output = $sseOutput;
|
|
||||||
|
|
||||||
// 将输入推入队列
|
|
||||||
$webInput = WebInput::getInstance();
|
|
||||||
$webInput->clear();
|
|
||||||
$webInput->push($input);
|
|
||||||
|
|
||||||
try {
|
|
||||||
$this->runCurrentState();
|
|
||||||
} catch (NeedInputException $e) {
|
|
||||||
// 战斗中可能需要更多输入
|
|
||||||
}
|
|
||||||
|
|
||||||
// 保存状态
|
|
||||||
$this->game->saveState();
|
|
||||||
|
|
||||||
// 发送完成信号
|
|
||||||
$sseOutput->complete();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -108,9 +68,6 @@ class GameSession
|
||||||
*/
|
*/
|
||||||
private function runCurrentState(): void
|
private function runCurrentState(): void
|
||||||
{
|
{
|
||||||
if ($this->game->state == 0){
|
|
||||||
$this->game->state = 1;
|
|
||||||
}
|
|
||||||
switch ($this->game->state) {
|
switch ($this->game->state) {
|
||||||
case Game::MENU:
|
case Game::MENU:
|
||||||
(new \Game\Modules\Menu($this->game))->show();
|
(new \Game\Modules\Menu($this->game))->show();
|
||||||
|
|
@ -136,6 +93,11 @@ class GameSession
|
||||||
case Game::TALENT:
|
case Game::TALENT:
|
||||||
(new \Game\Modules\TalentPanel($this->game))->show();
|
(new \Game\Modules\TalentPanel($this->game))->show();
|
||||||
break;
|
break;
|
||||||
|
case Game::EXIT:
|
||||||
|
exit;
|
||||||
|
$this->output->writeln("再见!");
|
||||||
|
$this->game->saveState();
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -157,42 +119,7 @@ class GameSession
|
||||||
'level' => $this->game->player->level,
|
'level' => $this->game->player->level,
|
||||||
'hp' => $this->game->player->hp,
|
'hp' => $this->game->player->hp,
|
||||||
'maxHp' => $stats['maxHp'],
|
'maxHp' => $stats['maxHp'],
|
||||||
'mana' => $this->game->player->mana,
|
|
||||||
'maxMana' => $stats['maxMana'],
|
|
||||||
'spiritStones' => $this->game->player->spiritStones,
|
'spiritStones' => $this->game->player->spiritStones,
|
||||||
'exp' => $this->game->player->exp,
|
|
||||||
'maxExp' => $this->game->player->maxExp,
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取当前游戏状态信息
|
|
||||||
*/
|
|
||||||
public function getStateInfo(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'state' => $this->game->state,
|
|
||||||
'stateName' => $this->getStateName(),
|
|
||||||
'playerInfo' => $this->getPlayerInfo(),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 将状态常量转换为名称
|
|
||||||
*/
|
|
||||||
private function getStateName(): string
|
|
||||||
{
|
|
||||||
return match($this->game->state) {
|
|
||||||
Game::MENU => 'MENU',
|
|
||||||
Game::DUNGEON_SELECT => 'DUNGEON_SELECT',
|
|
||||||
Game::BATTLE => 'BATTLE',
|
|
||||||
Game::STATS => 'STATS',
|
|
||||||
Game::INVENTORY => 'INVENTORY',
|
|
||||||
Game::EQUIPMENT_ENHANCE => 'EQUIPMENT_ENHANCE',
|
|
||||||
Game::NPC => 'NPC',
|
|
||||||
Game::TALENT => 'TALENT',
|
|
||||||
Game::EXIT => 'EXIT',
|
|
||||||
default => 'UNKNOWN'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,248 +0,0 @@
|
||||||
<?php
|
|
||||||
namespace Game\Core;
|
|
||||||
|
|
||||||
use Ratchet\MessageComponentInterface;
|
|
||||||
use Ratchet\ConnectionInterface;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* WebSocket 游戏服务器
|
|
||||||
* 处理前端的实时通信
|
|
||||||
*/
|
|
||||||
class GameWebSocketServer implements MessageComponentInterface
|
|
||||||
{
|
|
||||||
// 存储所有连接和对应的用户会话
|
|
||||||
protected array $clients = [];
|
|
||||||
protected array $sessions = [];
|
|
||||||
|
|
||||||
public function __construct()
|
|
||||||
{
|
|
||||||
echo "[WebSocket] 游戏服务器初始化\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 客户端连接时
|
|
||||||
*/
|
|
||||||
public function onOpen(ConnectionInterface $conn)
|
|
||||||
{
|
|
||||||
echo "[连接] 新连接: {$conn->resourceId}\n";
|
|
||||||
$this->clients[$conn->resourceId] = $conn;
|
|
||||||
|
|
||||||
// 发送欢迎消息
|
|
||||||
$this->sendMessage($conn, [
|
|
||||||
'type' => 'welcome',
|
|
||||||
'message' => '连接成功,请登录'
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 接收消息
|
|
||||||
*/
|
|
||||||
public function onMessage(ConnectionInterface $from, $msg)
|
|
||||||
{
|
|
||||||
$data = json_decode($msg, true);
|
|
||||||
if (!$data) {
|
|
||||||
$this->sendError($from, '无效的JSON格式');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$type = $data['type'] ?? null;
|
|
||||||
|
|
||||||
switch ($type) {
|
|
||||||
case 'login':
|
|
||||||
$this->handleLogin($from, $data);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'game-input':
|
|
||||||
$this->handleGameInput($from, $data);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'sync-state':
|
|
||||||
$this->handleSyncState($from, $data);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'ping':
|
|
||||||
$this->sendMessage($from, ['type' => 'pong']);
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
$this->sendError($from, "未知消息类型: $type");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 客户端关闭连接
|
|
||||||
*/
|
|
||||||
public function onClose(ConnectionInterface $conn)
|
|
||||||
{
|
|
||||||
unset($this->clients[$conn->resourceId]);
|
|
||||||
if (isset($this->sessions[$conn->resourceId])) {
|
|
||||||
unset($this->sessions[$conn->resourceId]);
|
|
||||||
}
|
|
||||||
echo "[断开] 连接已关闭: {$conn->resourceId}\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 连接错误
|
|
||||||
*/
|
|
||||||
public function onError(ConnectionInterface $conn, \Exception $e)
|
|
||||||
{
|
|
||||||
echo "[错误] {$conn->resourceId}: {$e->getMessage()}\n";
|
|
||||||
$conn->close();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理登录
|
|
||||||
*/
|
|
||||||
private function handleLogin(ConnectionInterface $conn, array $data): void
|
|
||||||
{
|
|
||||||
$userId = $data['userId'] ?? null;
|
|
||||||
$username = $data['username'] ?? null;
|
|
||||||
|
|
||||||
if (!$userId || !$username) {
|
|
||||||
$this->sendError($conn, '缺少userId或username');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 创建游戏会话
|
|
||||||
$session = new GameSession($userId);
|
|
||||||
$this->sessions[$conn->resourceId] = [
|
|
||||||
'userId' => $userId,
|
|
||||||
'username' => $username,
|
|
||||||
'session' => $session,
|
|
||||||
];
|
|
||||||
|
|
||||||
// 发送登录成功和初始界面
|
|
||||||
$output = $session->render();
|
|
||||||
$stateInfo = $session->getStateInfo();
|
|
||||||
|
|
||||||
$this->sendMessage($conn, [
|
|
||||||
'type' => 'login-success',
|
|
||||||
'userId' => $userId,
|
|
||||||
'username' => $username,
|
|
||||||
'output' => $output,
|
|
||||||
'state' => $stateInfo['state'],
|
|
||||||
'stateName' => $stateInfo['stateName'],
|
|
||||||
'playerInfo' => $stateInfo['playerInfo'],
|
|
||||||
]);
|
|
||||||
|
|
||||||
echo "[登录] 用户 {$username} (ID: {$userId}) 已连接\n";
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
$this->sendError($conn, '登录失败: ' . $e->getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理游戏输入
|
|
||||||
*/
|
|
||||||
private function handleGameInput(ConnectionInterface $conn, array $data): void
|
|
||||||
{
|
|
||||||
$input = $data['input'] ?? '';
|
|
||||||
|
|
||||||
if (!isset($this->sessions[$conn->resourceId])) {
|
|
||||||
$this->sendError($conn, '未登录');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$sessionData = $this->sessions[$conn->resourceId];
|
|
||||||
/** @var GameSession $session */
|
|
||||||
$session = $sessionData['session'];
|
|
||||||
|
|
||||||
// 处理输入 - 现在返回结构化数据
|
|
||||||
$result = $session->handleInput($input);
|
|
||||||
|
|
||||||
// result是数组,包含:output, state, stateName, playerInfo
|
|
||||||
$this->sendMessage($conn, array_merge([
|
|
||||||
'type' => 'game-output',
|
|
||||||
], $result));
|
|
||||||
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
$this->sendError($conn, '输入处理失败: ' . $e->getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理战斗流(SSE转WebSocket)
|
|
||||||
*/
|
|
||||||
private function handleBattleStream(ConnectionInterface $conn, GameSession $session): void
|
|
||||||
{
|
|
||||||
// 发送战斗开始信号
|
|
||||||
$this->sendMessage($conn, [
|
|
||||||
'type' => 'battle-start',
|
|
||||||
'message' => '战斗开始'
|
|
||||||
]);
|
|
||||||
|
|
||||||
// 创建SSEOutput替代品 - 收集输出然后发送
|
|
||||||
// 为了简化,我们直接用WebSocket消息逐行发送
|
|
||||||
echo "[战斗] 用户 {$this->sessions[$conn->resourceId]['username']} 进入战斗\n";
|
|
||||||
// 发送战斗结束信号(实际战斗已经完成)
|
|
||||||
$stateInfo = $session->getStateInfo();
|
|
||||||
$this->sendMessage($conn, [
|
|
||||||
'type' => 'battle-end',
|
|
||||||
'state' => $stateInfo['state'],
|
|
||||||
'stateName' => $stateInfo['stateName'],
|
|
||||||
'playerInfo' => $stateInfo['playerInfo'],
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理状态同步
|
|
||||||
*/
|
|
||||||
private function handleSyncState(ConnectionInterface $conn, array $data): void
|
|
||||||
{
|
|
||||||
if (!isset($this->sessions[$conn->resourceId])) {
|
|
||||||
$this->sendError($conn, '未登录');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$sessionData = $this->sessions[$conn->resourceId];
|
|
||||||
$session = $sessionData['session'];
|
|
||||||
|
|
||||||
$stateInfo = $session->getStateInfo();
|
|
||||||
$output = $session->render();
|
|
||||||
|
|
||||||
$this->sendMessage($conn, [
|
|
||||||
'type' => 'state-sync',
|
|
||||||
'output' => $output,
|
|
||||||
'state' => $stateInfo['state'],
|
|
||||||
'stateName' => $stateInfo['stateName'],
|
|
||||||
'playerInfo' => $stateInfo['playerInfo'],
|
|
||||||
]);
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
$this->sendError($conn, '状态同步失败: ' . $e->getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 发送消息给客户端
|
|
||||||
*/
|
|
||||||
protected function sendMessage(ConnectionInterface $conn, array $data): void
|
|
||||||
{
|
|
||||||
$msg = json_encode($data, JSON_UNESCAPED_UNICODE);
|
|
||||||
$conn->send($msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 发送错误消息
|
|
||||||
*/
|
|
||||||
protected function sendError(ConnectionInterface $conn, string $message): void
|
|
||||||
{
|
|
||||||
$this->sendMessage($conn, [
|
|
||||||
'type' => 'error',
|
|
||||||
'message' => $message
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 广播消息给所有连接(管理员通知等)
|
|
||||||
*/
|
|
||||||
protected function broadcast(array $data): void
|
|
||||||
{
|
|
||||||
$msg = json_encode($data, JSON_UNESCAPED_UNICODE);
|
|
||||||
foreach ($this->clients as $conn) {
|
|
||||||
$conn->send($msg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -373,10 +373,7 @@ class ItemDisplay
|
||||||
{
|
{
|
||||||
$quality = $item['quality'] ?? $item['rarity'] ?? 'common';
|
$quality = $item['quality'] ?? $item['rarity'] ?? 'common';
|
||||||
$color = self::getQualityColor($quality);
|
$color = self::getQualityColor($quality);
|
||||||
if ($item['type'] == 'potion_pool'){
|
$name = ($item['name'] ?? '未知物品') . 'lv.' . $item['level'];
|
||||||
return self::$green .$item['name'] . '+' .$item['heal'];
|
|
||||||
}
|
|
||||||
$name = ($item['name'] ?? '未知物品') . 'lv.' . ($item['level'] ?? '');
|
|
||||||
|
|
||||||
$enhanceLevel = $item['enhanceLevel'] ?? 0;
|
$enhanceLevel = $item['enhanceLevel'] ?? 0;
|
||||||
$enhanceStr = $enhanceLevel > 0 ? self::$yellow . "+{$enhanceLevel}" . self::$reset : "";
|
$enhanceStr = $enhanceLevel > 0 ? self::$yellow . "+{$enhanceLevel}" . self::$reset : "";
|
||||||
|
|
|
||||||
|
|
@ -1,137 +0,0 @@
|
||||||
<?php
|
|
||||||
namespace Game\Core;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Server-Sent Events 输出类
|
|
||||||
* 直接推送数据到客户端,用于实时战斗流
|
|
||||||
*/
|
|
||||||
class SSEOutput
|
|
||||||
{
|
|
||||||
private int $eventId = 0;
|
|
||||||
private float $startTime = 0;
|
|
||||||
|
|
||||||
public function __construct()
|
|
||||||
{
|
|
||||||
$this->startTime = microtime(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 写入消息(不换行)
|
|
||||||
*/
|
|
||||||
public function write(string $message): void
|
|
||||||
{
|
|
||||||
// 对于ANSI清屏等控制字符,直接发送不需要特殊处理
|
|
||||||
$this->sendEvent([
|
|
||||||
'type' => 'write',
|
|
||||||
'text' => $message,
|
|
||||||
'time' => round((microtime(true) - $this->startTime) * 1000)
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 写入消息(换行)
|
|
||||||
*/
|
|
||||||
public function writeln(string $message): void
|
|
||||||
{
|
|
||||||
$this->sendEvent([
|
|
||||||
'type' => 'writeln',
|
|
||||||
'text' => $message,
|
|
||||||
'time' => round((microtime(true) - $this->startTime) * 1000)
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 推送事件到客户端
|
|
||||||
*/
|
|
||||||
private function sendEvent(array $data): void
|
|
||||||
{
|
|
||||||
// SSE格式:
|
|
||||||
// event: message\n
|
|
||||||
// id: {eventId}\n
|
|
||||||
// data: {JSON}\n\n
|
|
||||||
|
|
||||||
echo "event: message\n";
|
|
||||||
echo "id: " . ($this->eventId++) . "\n";
|
|
||||||
echo "data: " . json_encode($data, JSON_UNESCAPED_UNICODE) . "\n\n";
|
|
||||||
|
|
||||||
// 立即刷新缓冲区发送给客户端
|
|
||||||
ob_flush();
|
|
||||||
flush();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 发送消息(用于通知而非日志)
|
|
||||||
*/
|
|
||||||
public function sendMessage(string $type, $data): void
|
|
||||||
{
|
|
||||||
echo "event: " . $type . "\n";
|
|
||||||
echo "id: " . ($this->eventId++) . "\n";
|
|
||||||
echo "data: " . json_encode($data, JSON_UNESCAPED_UNICODE) . "\n\n";
|
|
||||||
|
|
||||||
ob_flush();
|
|
||||||
flush();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 添加延迟(在SSE模式下实际延迟)
|
|
||||||
* 这样可以控制日志的显示速度
|
|
||||||
*/
|
|
||||||
public function delay(int $milliseconds): void
|
|
||||||
{
|
|
||||||
if ($milliseconds > 0) {
|
|
||||||
usleep($milliseconds * 1000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 发送完成信号
|
|
||||||
*/
|
|
||||||
public function complete(): void
|
|
||||||
{
|
|
||||||
$this->sendMessage('complete', [
|
|
||||||
'message' => '战斗结束',
|
|
||||||
'totalTime' => round((microtime(true) - $this->startTime) * 1000)
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 发送错误
|
|
||||||
*/
|
|
||||||
public function error(string $message): void
|
|
||||||
{
|
|
||||||
$this->sendMessage('error', ['message' => $message]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 兼容WebOutput的enableTiming方法
|
|
||||||
* SSE模式不需要计时,直接返回
|
|
||||||
*/
|
|
||||||
public function enableTiming(): void
|
|
||||||
{
|
|
||||||
// SSE模式下不需要计时,直接在运行时实时推送
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 兼容WebOutput的getLineCount方法
|
|
||||||
*/
|
|
||||||
public function getLineCount(): int
|
|
||||||
{
|
|
||||||
return 0; // SSE不缓冲
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取缓冲的输出(SSE不使用)
|
|
||||||
*/
|
|
||||||
public function getOutput(): string
|
|
||||||
{
|
|
||||||
return ''; // SSE模式不缓冲
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 清空缓冲区(SSE不使用)
|
|
||||||
*/
|
|
||||||
public function clear(): void
|
|
||||||
{
|
|
||||||
// SSE不缓冲,无需清空
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -34,32 +34,13 @@ class Screen
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Web 兼容的延迟
|
* Web 兼容的延迟
|
||||||
* - 终端模式:使用 usleep
|
* 在 Web 模式下跳过延迟
|
||||||
* - SSE 模式:实际延迟(usleep)
|
|
||||||
* - WebOutput 计时模式:记录虚拟时间戳
|
|
||||||
*
|
|
||||||
* @param int $microseconds 延迟微秒数
|
|
||||||
* @param object|null $out 可选的输出对象(WebOutput 或 SSEOutput)
|
|
||||||
*/
|
*/
|
||||||
public static function delay(int $microseconds, ?object $out = null): void
|
public static function delay(int $microseconds): void
|
||||||
{
|
{
|
||||||
$webInput = WebInput::getInstance();
|
$webInput = WebInput::getInstance();
|
||||||
if (!$webInput->isWebMode()) {
|
if (!$webInput->isWebMode()) {
|
||||||
// 终端模式:直接延迟
|
|
||||||
usleep($microseconds);
|
usleep($microseconds);
|
||||||
} elseif ($out) {
|
|
||||||
// Web模式
|
|
||||||
// 检查是否是SSEOutput
|
|
||||||
$className = substr(strrchr(get_class($out), '\\'), 1);
|
|
||||||
if ($className === 'SSEOutput') {
|
|
||||||
// SSE 模式:实际延迟以控制流式输出的速度
|
|
||||||
$milliseconds = max(1, (int)($microseconds / 1000));
|
|
||||||
$out->delay($milliseconds);
|
|
||||||
} elseif (method_exists($out, 'isTimingEnabled') && $out->isTimingEnabled()) {
|
|
||||||
// WebOutput 计时模式:记录虚拟时间戳
|
|
||||||
$milliseconds = max(1, (int)($microseconds / 1000));
|
|
||||||
$out->delay($milliseconds);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -121,7 +121,7 @@ class SpellCalculator
|
||||||
// 计算方式
|
// 计算方式
|
||||||
$calcType = $spellInfo['calc_type'] ?? 'matk';
|
$calcType = $spellInfo['calc_type'] ?? 'matk';
|
||||||
$healRatio = $spellInfo['heal'] ?? 0.5;
|
$healRatio = $spellInfo['heal'] ?? 0.5;
|
||||||
$healBase = $spellInfo['base'] ?? 0;
|
$healBase = $spellInfo['heal_base'] ?? 20;
|
||||||
|
|
||||||
// 基础治疗量 (根据 calc_type)
|
// 基础治疗量 (根据 calc_type)
|
||||||
switch ($calcType) {
|
switch ($calcType) {
|
||||||
|
|
|
||||||
|
|
@ -4,59 +4,19 @@ namespace Game\Core;
|
||||||
/**
|
/**
|
||||||
* Web 输出缓冲类
|
* Web 输出缓冲类
|
||||||
* 模拟 Symfony Console Output 接口,将输出缓存到内存
|
* 模拟 Symfony Console Output 接口,将输出缓存到内存
|
||||||
* 支持时间戳功能用于战斗日志的流式播放
|
|
||||||
*/
|
*/
|
||||||
class WebOutput
|
class WebOutput
|
||||||
{
|
{
|
||||||
private array $buffer = [];
|
private array $buffer = [];
|
||||||
private array $timedBuffer = [];
|
|
||||||
private bool $enableTiming = false;
|
|
||||||
private float $startTime = 0;
|
|
||||||
private float $currentTime = 0;
|
|
||||||
|
|
||||||
public function write(string $message): void
|
public function write(string $message): void
|
||||||
{
|
{
|
||||||
$this->buffer[] = $message;
|
$this->buffer[] = $message;
|
||||||
if ($this->enableTiming) {
|
|
||||||
$this->timedBuffer[] = [
|
|
||||||
'text' => $message,
|
|
||||||
'timestamp' => round(($this->currentTime - $this->startTime) * 1000),
|
|
||||||
'type' => 'write'
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function writeln(string $message): void
|
public function writeln(string $message): void
|
||||||
{
|
{
|
||||||
$this->buffer[] = $message . "\n";
|
$this->buffer[] = $message . "\n";
|
||||||
if ($this->enableTiming) {
|
|
||||||
$this->timedBuffer[] = [
|
|
||||||
'text' => $message,
|
|
||||||
'timestamp' => round(($this->currentTime - $this->startTime) * 1000),
|
|
||||||
'type' => 'writeln'
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 添加延迟(用于模拟战斗动画速度)
|
|
||||||
* $ms: 延迟毫秒数
|
|
||||||
*/
|
|
||||||
public function delay(int $ms): void
|
|
||||||
{
|
|
||||||
if ($this->enableTiming) {
|
|
||||||
$this->currentTime += $ms / 1000.0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 启用时间戳模式(战斗开始时调用)
|
|
||||||
*/
|
|
||||||
public function enableTiming(): void
|
|
||||||
{
|
|
||||||
$this->enableTiming = true;
|
|
||||||
$this->startTime = microtime(true);
|
|
||||||
$this->currentTime = $this->startTime;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -67,29 +27,12 @@ class WebOutput
|
||||||
return implode('', $this->buffer);
|
return implode('', $this->buffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取带时间戳的输出(JSON格式)
|
|
||||||
* 用于web端流式播放战斗日志
|
|
||||||
*/
|
|
||||||
public function getTimedOutput(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'type' => 'battle_log',
|
|
||||||
'logs' => $this->timedBuffer,
|
|
||||||
'totalDuration' => round(($this->currentTime - $this->startTime) * 1000)
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 清空缓冲区
|
* 清空缓冲区
|
||||||
*/
|
*/
|
||||||
public function clear(): void
|
public function clear(): void
|
||||||
{
|
{
|
||||||
$this->buffer = [];
|
$this->buffer = [];
|
||||||
$this->timedBuffer = [];
|
|
||||||
$this->enableTiming = false;
|
|
||||||
$this->startTime = 0;
|
|
||||||
$this->currentTime = 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -99,12 +42,4 @@ class WebOutput
|
||||||
{
|
{
|
||||||
return count($this->buffer);
|
return count($this->buffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 检查是否启用了时间戳模式
|
|
||||||
*/
|
|
||||||
public function isTimingEnabled(): bool
|
|
||||||
{
|
|
||||||
return $this->enableTiming;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ namespace Game\Entities;
|
||||||
|
|
||||||
class Partner extends Actor
|
class Partner extends Actor
|
||||||
{
|
{
|
||||||
public $id;
|
|
||||||
// Partner特有的天赋加成(与 Actor 不同)
|
// Partner特有的天赋加成(与 Actor 不同)
|
||||||
public static array $talentBonus = [
|
public static array $talentBonus = [
|
||||||
'hp' => 20,
|
'hp' => 20,
|
||||||
|
|
|
||||||
|
|
@ -24,9 +24,6 @@ class Player extends Actor
|
||||||
public int $maxPartners = 2; // 最多可携带同伴数
|
public int $maxPartners = 2; // 最多可携带同伴数
|
||||||
public array $partners = []; // 已招募的同伴
|
public array $partners = []; // 已招募的同伴
|
||||||
|
|
||||||
// Player特有的小绿瓶回复池
|
|
||||||
public int $potionPool = 1000; // 小绿瓶初始回复量
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 增加灵石
|
* 增加灵石
|
||||||
*/
|
*/
|
||||||
|
|
@ -47,32 +44,6 @@ class Player extends Actor
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 添加小绿瓶回复量
|
|
||||||
*/
|
|
||||||
public function addPotionPool(int $amount): void
|
|
||||||
{
|
|
||||||
$this->potionPool += $amount;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 从小绿瓶扣除回复量(返回实际扣除的量)
|
|
||||||
*/
|
|
||||||
public function consumePotionPool(int $amount): int
|
|
||||||
{
|
|
||||||
$consumed = min($amount, $this->potionPool);
|
|
||||||
$this->potionPool = max(0,$this->potionPool - $amount);
|
|
||||||
return $consumed;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 检查小绿瓶是否有足够的回复量
|
|
||||||
*/
|
|
||||||
public function hasPotionPool(int $amount): bool
|
|
||||||
{
|
|
||||||
return $this->potionPool >= $amount;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Player特有的经验获取,升级时会恢复生命值
|
* Player特有的经验获取,升级时会恢复生命值
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ use Game\Core\ItemDisplay;
|
||||||
use Game\Core\SpellDisplay;
|
use Game\Core\SpellDisplay;
|
||||||
use Game\Core\SpellCalculator;
|
use Game\Core\SpellCalculator;
|
||||||
use Game\Core\Colors;
|
use Game\Core\Colors;
|
||||||
use Game\Core\WebInput;
|
|
||||||
use Game\Entities\Player;
|
use Game\Entities\Player;
|
||||||
use Game\Entities\Actor;
|
use Game\Entities\Actor;
|
||||||
use Game\Entities\Monster;
|
use Game\Entities\Monster;
|
||||||
|
|
@ -121,11 +120,6 @@ class Battle
|
||||||
{
|
{
|
||||||
$out = $this->game->output;
|
$out = $this->game->output;
|
||||||
|
|
||||||
// Web端启用计时模式,用于战斗日志流式播放
|
|
||||||
if (WebInput::getInstance()->isWebMode()) {
|
|
||||||
$out->enableTiming();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 初始化同伴HP
|
// 初始化同伴HP
|
||||||
$this->initPartnerHp();
|
$this->initPartnerHp();
|
||||||
|
|
||||||
|
|
@ -137,7 +131,7 @@ class Battle
|
||||||
}
|
}
|
||||||
|
|
||||||
while ($this->player->hp > 0) {
|
while ($this->player->hp > 0) {
|
||||||
Screen::delay(500000, $out);
|
Screen::delay(500000);
|
||||||
|
|
||||||
// 创建敌人群组
|
// 创建敌人群组
|
||||||
$this->enemies = Monster::createGroup($this->game->dungeonId);
|
$this->enemies = Monster::createGroup($this->game->dungeonId);
|
||||||
|
|
@ -163,17 +157,17 @@ class Battle
|
||||||
|
|
||||||
$this->round++;
|
$this->round++;
|
||||||
$this->renderBattleScreen($out, $playerFirst);
|
$this->renderBattleScreen($out, $playerFirst);
|
||||||
Screen::delay(800000, $out); // 回合开始停顿
|
Screen::delay(800000); // 回合开始停顿
|
||||||
|
|
||||||
if ($playerFirst) {
|
if ($playerFirst) {
|
||||||
// 玩家攻击
|
// 玩家攻击
|
||||||
$result = $this->playerAttack($out);
|
$result = $this->playerAttack($out);
|
||||||
Screen::delay(800000, $out);
|
Screen::delay(800000);
|
||||||
if ($result) break;
|
if ($result) break;
|
||||||
|
|
||||||
// 同伴攻击
|
// 同伴攻击
|
||||||
$result = $this->partnersAttack($out);
|
$result = $this->partnersAttack($out);
|
||||||
Screen::delay(800000, $out);
|
Screen::delay(800000);
|
||||||
if ($result) break;
|
if ($result) break;
|
||||||
|
|
||||||
if ($this->checkExit($out)) {
|
if ($this->checkExit($out)) {
|
||||||
|
|
@ -183,19 +177,19 @@ class Battle
|
||||||
|
|
||||||
// 怪物攻击
|
// 怪物攻击
|
||||||
if ($this->enemiesAttack($out)) {
|
if ($this->enemiesAttack($out)) {
|
||||||
Screen::delay(1000000, $out);
|
Screen::delay(1000000);
|
||||||
$this->syncPartnerHp();
|
$this->syncPartnerHp();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Screen::delay(800000, $out);
|
Screen::delay(800000);
|
||||||
} else {
|
} else {
|
||||||
// 怪物先攻
|
// 怪物先攻
|
||||||
if ($this->enemiesAttack($out)) {
|
if ($this->enemiesAttack($out)) {
|
||||||
Screen::delay(1000000, $out);
|
Screen::delay(1000000);
|
||||||
$this->syncPartnerHp();
|
$this->syncPartnerHp();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Screen::delay(800000, $out);
|
Screen::delay(800000);
|
||||||
|
|
||||||
if ($this->checkExit($out)) {
|
if ($this->checkExit($out)) {
|
||||||
$this->syncPartnerHp();
|
$this->syncPartnerHp();
|
||||||
|
|
@ -204,16 +198,16 @@ class Battle
|
||||||
|
|
||||||
// 玩家攻击
|
// 玩家攻击
|
||||||
$result = $this->playerAttack($out);
|
$result = $this->playerAttack($out);
|
||||||
Screen::delay(800000, $out);
|
Screen::delay(800000);
|
||||||
if ($result) break;
|
if ($result) break;
|
||||||
|
|
||||||
// 同伴攻击
|
// 同伴攻击
|
||||||
$result = $this->partnersAttack($out);
|
$result = $this->partnersAttack($out);
|
||||||
Screen::delay(800000, $out);
|
Screen::delay(800000);
|
||||||
if ($result) break;
|
if ($result) break;
|
||||||
}
|
}
|
||||||
|
|
||||||
Screen::delay(500000, $out);
|
Screen::delay(500000);
|
||||||
// 同步队友HP到Partner对象,然后保存状态
|
// 同步队友HP到Partner对象,然后保存状态
|
||||||
$this->syncPartnerHp();
|
$this->syncPartnerHp();
|
||||||
$this->game->saveState();
|
$this->game->saveState();
|
||||||
|
|
@ -236,7 +230,7 @@ class Battle
|
||||||
|
|
||||||
$out->writeln("");
|
$out->writeln("");
|
||||||
$out->writeln("{$this->yellow}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━{$this->reset}");
|
$out->writeln("{$this->yellow}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━{$this->reset}");
|
||||||
Screen::delay(1000000, $out); // 1秒
|
Screen::delay(1000000); // 1秒
|
||||||
}
|
}
|
||||||
|
|
||||||
private function renderBattleScreen($out, bool $playerFirst)
|
private function renderBattleScreen($out, bool $playerFirst)
|
||||||
|
|
@ -302,8 +296,8 @@ class Battle
|
||||||
|
|
||||||
private function renderHpBar(float $percent, int $width): string
|
private function renderHpBar(float $percent, int $width): string
|
||||||
{
|
{
|
||||||
$filled = max((int)($percent * $width),0);
|
$filled = (int)($percent * $width);
|
||||||
$empty = max($width - $filled,0);
|
$empty = max($width - $filled,1);
|
||||||
|
|
||||||
// 根据血量百分比选择颜色
|
// 根据血量百分比选择颜色
|
||||||
if ($percent > 0.6) {
|
if ($percent > 0.6) {
|
||||||
|
|
@ -314,7 +308,7 @@ class Battle
|
||||||
$color = $this->red;
|
$color = $this->red;
|
||||||
}
|
}
|
||||||
|
|
||||||
$bar = $color . str_repeat("█", $filled) . $this->white . str_repeat("░", $empty) . $this->reset;
|
$bar = $color . str_repeat("█", $filled) . $this->white . str_repeat("░", $empty?:1) . $this->reset;
|
||||||
return "[" . $bar . "]";
|
return "[" . $bar . "]";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -581,15 +575,15 @@ class Battle
|
||||||
}
|
}
|
||||||
|
|
||||||
// 应用防护机制:防护角色承受更多伤害
|
// 应用防护机制:防护角色承受更多伤害
|
||||||
// $actualDamage = (int)($damage * (1 + $target->getProtectDamageTakenBonus()));
|
$actualDamage = (int)($damage * (1 + $target->getProtectDamageTakenBonus()));
|
||||||
|
|
||||||
$target->hp -= $damage;
|
$target->hp -= $actualDamage;
|
||||||
|
|
||||||
// // 如果防护角色正在保护队友,需要显示保护效果
|
// 如果防护角色正在保护队友,需要显示保护效果
|
||||||
// if ($target->isProtecting && $actualDamage > $damage) {
|
if ($target->isProtecting && $actualDamage > $damage) {
|
||||||
// $extraDamage = $actualDamage - $damage;
|
$extraDamage = $actualDamage - $damage;
|
||||||
// $out->writeln("{$this->cyan}║{$this->reset} {$this->yellow}🛡️ 防护状态:额外承受 {$extraDamage} 伤害!{$this->reset}");
|
$out->writeln("{$this->cyan}║{$this->reset} {$this->yellow}🛡️ 防护状态:额外承受 {$extraDamage} 伤害!{$this->reset}");
|
||||||
// }
|
}
|
||||||
|
|
||||||
if ($target->hp <= 0) {
|
if ($target->hp <= 0) {
|
||||||
$target->hp = 0;
|
$target->hp = 0;
|
||||||
|
|
@ -597,14 +591,14 @@ class Battle
|
||||||
|
|
||||||
// 如果是玩家击败了所有敌人
|
// 如果是玩家击败了所有敌人
|
||||||
if (($caster instanceof Player || $caster instanceof Partner) && empty($this->getAliveEnemies())) {
|
if (($caster instanceof Player || $caster instanceof Partner) && empty($this->getAliveEnemies())) {
|
||||||
Screen::delay(500000, $out);
|
Screen::delay(500000);
|
||||||
$this->showVictory($out, $stats);
|
$this->showVictory($out, $stats);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果是敌人击败了玩家
|
// 如果是敌人击败了玩家
|
||||||
if ($target instanceof Player) {
|
if ($target instanceof Player) {
|
||||||
Screen::delay(500000, $out);
|
Screen::delay(500000);
|
||||||
$this->showDefeat($out, $caster);
|
$this->showDefeat($out, $caster);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -646,7 +640,7 @@ class Battle
|
||||||
$out->writeln("{$this->cyan}║{$this->reset} {$this->red}💀 {$enemy->name} 被击败了!{$this->reset}");
|
$out->writeln("{$this->cyan}║{$this->reset} {$this->red}💀 {$enemy->name} 被击败了!{$this->reset}");
|
||||||
|
|
||||||
if ($enemy instanceof Player) {
|
if ($enemy instanceof Player) {
|
||||||
Screen::delay(500000, $out);
|
Screen::delay(500000);
|
||||||
$this->showDefeat($out, $caster);
|
$this->showDefeat($out, $caster);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -665,7 +659,7 @@ class Battle
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($allOpponentsDefeated && ($caster instanceof Player || $caster instanceof Partner) && empty($this->getAliveEnemies())) {
|
if ($allOpponentsDefeated && ($caster instanceof Player || $caster instanceof Partner) && empty($this->getAliveEnemies())) {
|
||||||
Screen::delay(500000, $out);
|
Screen::delay(500000);
|
||||||
$this->showVictory($out, $stats);
|
$this->showVictory($out, $stats);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -683,8 +677,7 @@ class Battle
|
||||||
$allies = $this->getAllies($caster);
|
$allies = $this->getAllies($caster);
|
||||||
$lowestHpRatio = 1.0;
|
$lowestHpRatio = 1.0;
|
||||||
foreach ($allies as $ally) {
|
foreach ($allies as $ally) {
|
||||||
$status = $ally->getStats();
|
$ratio = $ally->hp / $ally->maxHp;
|
||||||
$ratio = $status['hp'] / $status['maxHp'];
|
|
||||||
if ($ratio < $lowestHpRatio) {
|
if ($ratio < $lowestHpRatio) {
|
||||||
$lowestHpRatio = $ratio;
|
$lowestHpRatio = $ratio;
|
||||||
$target = $ally;
|
$target = $ally;
|
||||||
|
|
@ -742,7 +735,7 @@ class Battle
|
||||||
if ($ally->hp <= 0) continue;
|
if ($ally->hp <= 0) continue;
|
||||||
|
|
||||||
// 如果是施法者本人,全额治疗;如果是队友,80%效果
|
// 如果是施法者本人,全额治疗;如果是队友,80%效果
|
||||||
$finalHeal = ($ally === $caster) ? $healAmount : (int)($healAmount * 0.9);
|
$finalHeal = ($ally === $caster) ? $healAmount : (int)($healAmount * 0.8);
|
||||||
|
|
||||||
$actualHeal = $ally->heal($finalHeal);
|
$actualHeal = $ally->heal($finalHeal);
|
||||||
|
|
||||||
|
|
@ -837,37 +830,8 @@ class Battle
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果需要进入防护状态,检查是否有人已经在防护
|
// 自动进入防护状态
|
||||||
if ($hasLowHpAlly) {
|
if ($hasLowHpAlly) {
|
||||||
$protectingAlly = null;
|
|
||||||
foreach ($allies as $ally) {
|
|
||||||
if ($ally !== $actor && $ally->isProtecting) {
|
|
||||||
$protectingAlly = $ally;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果有人在防护,比较血量比例,让血量更健康的人进入防护状态
|
|
||||||
if ($protectingAlly !== null) {
|
|
||||||
$protectingStats = $protectingAlly->getStats();
|
|
||||||
$protectingHealthRatio = $protectingStats['hp'] / $protectingStats['maxHp'];
|
|
||||||
|
|
||||||
// 当前角色血量更健康,则替换防护者
|
|
||||||
if ($healthRatio > $protectingHealthRatio) {
|
|
||||||
$protectingAlly->exitProtectMode();
|
|
||||||
|
|
||||||
$protectingName = ($protectingAlly instanceof Player) ? "你" : $protectingAlly->name;
|
|
||||||
$out->writeln("{$this->cyan}║{$this->reset} {$this->yellow}🛡️{$this->reset} {$protectingName} 退出防护状态。{$this->reset}");
|
|
||||||
|
|
||||||
$actor->enterProtectMode();
|
|
||||||
|
|
||||||
$actorName = ($actor instanceof Player) ? "你" : $actor->name;
|
|
||||||
$out->writeln("{$this->cyan}║{$this->reset} {$this->yellow}🛡️{$this->reset} {$actorName} 主动进入防护状态,为队友抗伤!{$this->reset}");
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 没有人在防护,直接进入
|
|
||||||
$actor->enterProtectMode();
|
$actor->enterProtectMode();
|
||||||
|
|
||||||
$actorName = ($actor instanceof Player) ? "你" : $actor->name;
|
$actorName = ($actor instanceof Player) ? "你" : $actor->name;
|
||||||
|
|
@ -960,13 +924,13 @@ class Battle
|
||||||
// 应用防护机制:防护角色承受更多伤害
|
// 应用防护机制:防护角色承受更多伤害
|
||||||
$actualDamage = (int)($damage * (1 + $target->getProtectDamageTakenBonus()));
|
$actualDamage = (int)($damage * (1 + $target->getProtectDamageTakenBonus()));
|
||||||
|
|
||||||
$target->hp -= $damage;
|
$target->hp -= $actualDamage;
|
||||||
|
|
||||||
// 如果防护角色正在保护队友,需要显示保护效果
|
// 如果防护角色正在保护队友,需要显示保护效果
|
||||||
// if ($target->isProtecting && $actualDamage > $damage) {
|
if ($target->isProtecting && $actualDamage > $damage) {
|
||||||
// $extraDamage = $actualDamage - $damage;
|
$extraDamage = $actualDamage - $damage;
|
||||||
// $out->writeln("{$this->cyan}║{$this->reset} {$this->yellow}🛡️ 防护状态:额外承受 {$extraDamage} 伤害!{$this->reset}");
|
$out->writeln("{$this->cyan}║{$this->reset} {$this->yellow}🛡️ 防护状态:额外承受 {$extraDamage} 伤害!{$this->reset}");
|
||||||
// }
|
}
|
||||||
|
|
||||||
// 蓝量恢复机制
|
// 蓝量恢复机制
|
||||||
// 攻击者恢复 15 点
|
// 攻击者恢复 15 点
|
||||||
|
|
@ -984,13 +948,13 @@ class Battle
|
||||||
$out->writeln("{$this->cyan}║{$this->reset} {$this->red}💀 {$targetName} 倒下了!{$this->reset}");
|
$out->writeln("{$this->cyan}║{$this->reset} {$this->red}💀 {$targetName} 倒下了!{$this->reset}");
|
||||||
|
|
||||||
if (($actor instanceof Player || $actor instanceof Partner) && empty($this->getAliveEnemies())) {
|
if (($actor instanceof Player || $actor instanceof Partner) && empty($this->getAliveEnemies())) {
|
||||||
Screen::delay(500000, $out);
|
Screen::delay(500000);
|
||||||
$this->showVictory($out, $stats);
|
$this->showVictory($out, $stats);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($target instanceof Player) {
|
if ($target instanceof Player) {
|
||||||
Screen::delay(500000, $out);
|
Screen::delay(500000);
|
||||||
$this->showDefeat($out, $actor);
|
$this->showDefeat($out, $actor);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -1005,7 +969,7 @@ class Battle
|
||||||
if ($this->executeActorTurn($this->player, $out)) {
|
if ($this->executeActorTurn($this->player, $out)) {
|
||||||
return true; // 战斗结束
|
return true; // 战斗结束
|
||||||
}
|
}
|
||||||
Screen::delay(300000, $out);
|
Screen::delay(300000);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1020,7 +984,7 @@ class Battle
|
||||||
if ($this->executeActorTurn($partner, $out)) {
|
if ($this->executeActorTurn($partner, $out)) {
|
||||||
return true; // 战斗结束
|
return true; // 战斗结束
|
||||||
}
|
}
|
||||||
Screen::delay(300000, $out);
|
Screen::delay(300000);
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -1037,7 +1001,7 @@ class Battle
|
||||||
if ($this->executeActorTurn($enemy, $out)) {
|
if ($this->executeActorTurn($enemy, $out)) {
|
||||||
return true; // 战斗结束
|
return true; // 战斗结束
|
||||||
}
|
}
|
||||||
Screen::delay(300000, $out);
|
Screen::delay(300000);
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -1082,22 +1046,8 @@ class Battle
|
||||||
// 掉落 - 从掉落表中随机掉落物品
|
// 掉落 - 从掉落表中随机掉落物品
|
||||||
foreach ($enemy->dropTable as $drop) {
|
foreach ($enemy->dropTable as $drop) {
|
||||||
if (rand(1, 100) <= $drop['rate']) {
|
if (rand(1, 100) <= $drop['rate']) {
|
||||||
$item = $drop['item'];
|
$this->player->addItem($drop['item']);
|
||||||
// 如果是回复品(消耗品且有heal属性),直接添加到小绿瓶池
|
$allDrops[] = $drop['item'];
|
||||||
if ($item['type'] === 'consume' && isset($item['heal']) && $item['heal'] > 0) {
|
|
||||||
$this->player->addPotionPool($item['heal']);
|
|
||||||
// 显示为小绿瓶增加
|
|
||||||
$allDrops[] = [
|
|
||||||
'name' => '小绿瓶',
|
|
||||||
'type' => 'potion_pool',
|
|
||||||
'heal' => $item['heal'],
|
|
||||||
'quantity' => 1
|
|
||||||
];
|
|
||||||
} else {
|
|
||||||
// 其他物品正常添加到背包
|
|
||||||
$this->player->addItem($item);
|
|
||||||
$allDrops[] = $item;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1152,7 +1102,7 @@ class Battle
|
||||||
$out->writeln("");
|
$out->writeln("");
|
||||||
|
|
||||||
$this->game->saveState();
|
$this->game->saveState();
|
||||||
Screen::delay(1500000, $out);
|
Screen::delay(1500000);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function showDefeat($out, ?Actor $killer = null)
|
private function showDefeat($out, ?Actor $killer = null)
|
||||||
|
|
@ -1197,7 +1147,7 @@ class Battle
|
||||||
$out->writeln("");
|
$out->writeln("");
|
||||||
$out->writeln("{$this->yellow}🏃 你逃离了战斗...{$this->reset}");
|
$out->writeln("{$this->yellow}🏃 你逃离了战斗...{$this->reset}");
|
||||||
$out->writeln("");
|
$out->writeln("");
|
||||||
Screen::delay(800000, $out);
|
Screen::delay(800000);
|
||||||
$this->game->state = Game::MENU;
|
$this->game->state = Game::MENU;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -69,21 +69,14 @@ class InventoryPanel
|
||||||
$out->writeln("╔════════════════════════════╗");
|
$out->writeln("╔════════════════════════════╗");
|
||||||
$out->writeln("║ 背 包 [{$categoryName}] ║");
|
$out->writeln("║ 背 包 [{$categoryName}] ║");
|
||||||
$out->writeln("║ 第 {$page} / {$totalPages} 页 ║");
|
$out->writeln("║ 第 {$page} / {$totalPages} 页 ║");
|
||||||
$out->writeln("╠════════════════════════════╣");
|
|
||||||
$out->writeln("║ 🟢 小绿瓶: {$player->potionPool} ║");
|
|
||||||
$out->writeln("╚════════════════════════════╝");
|
$out->writeln("╚════════════════════════════╝");
|
||||||
|
|
||||||
if (empty($items)) {
|
if (empty($items)) {
|
||||||
$out->writeln("该分类下没有物品...");
|
$out->writeln("该分类下没有物品...");
|
||||||
$out->writeln("");
|
$out->writeln("");
|
||||||
$out->writeln("[h] 使用小绿瓶回血");
|
|
||||||
$out->writeln("[c] 切换分类 | [0] 返回");
|
$out->writeln("[c] 切换分类 | [0] 返回");
|
||||||
|
|
||||||
$choice = Screen::input($out, "选择操作:");
|
$choice = Screen::input($out, "选择操作:");
|
||||||
if ($choice === 'h'){
|
|
||||||
$this->autoHeal();
|
|
||||||
return $this->show($page, $category);
|
|
||||||
}
|
|
||||||
if ($choice === "c" || $choice === "C") {
|
if ($choice === "c" || $choice === "C") {
|
||||||
return $this->showCategoryMenu($page, $category);
|
return $this->showCategoryMenu($page, $category);
|
||||||
}
|
}
|
||||||
|
|
@ -245,32 +238,14 @@ class InventoryPanel
|
||||||
|
|
||||||
// 使用消耗品进行恢复
|
// 使用消耗品进行恢复
|
||||||
if ($target === 'player') {
|
if ($target === 'player') {
|
||||||
// 从小绿瓶池获取实际恢复量
|
$actualHeal = $player->heal($item['heal']);
|
||||||
$healAmount = $item['heal'];
|
$out->writeln("你使用了 {$item['name']},恢复了 {$actualHeal} HP!(当前: {$player->hp}/{$maxHp})");
|
||||||
$actualPotionHeal = $player->consumePotionPool($healAmount);
|
|
||||||
if ($actualPotionHeal == 0){
|
|
||||||
$out->writeln("小绿瓶已用尽");
|
|
||||||
Screen::sleep(1);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
$actualHeal = $player->heal($actualPotionHeal);
|
|
||||||
$out->writeln("你使用了 {$item['name']},从小绿瓶中恢复了 {$actualHeal} HP!(当前: {$player->hp}/{$maxHp})");
|
|
||||||
$out->writeln("小绿瓶剩余: {$player->potionPool}");
|
|
||||||
} else {
|
} else {
|
||||||
// 恢复队友
|
// 恢复队友
|
||||||
$partnerStats = $target->getStats();
|
$partnerStats = $target->getStats();
|
||||||
$partnerMaxHp = $partnerStats['maxHp'];
|
$partnerMaxHp = $partnerStats['maxHp'];
|
||||||
// 从小绿瓶池获取实际恢复量
|
$actualHeal = $target->heal($item['heal']);
|
||||||
$healAmount = $item['heal'];
|
$out->writeln("你使用了 {$item['name']} 来恢复 {$target->name},恢复了 {$actualHeal} HP!(当前: {$target->hp}/{$partnerMaxHp})");
|
||||||
$actualPotionHeal = $player->consumePotionPool($healAmount);
|
|
||||||
if ($actualPotionHeal == 0){
|
|
||||||
$out->writeln("小绿瓶已用尽");
|
|
||||||
Screen::sleep(1);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
$actualHeal = $target->heal($actualPotionHeal);
|
|
||||||
$out->writeln("你使用了 {$item['name']} 来恢复 {$target->name},从小绿瓶中恢复了 {$actualHeal} HP!(当前: {$target->hp}/{$partnerMaxHp})");
|
|
||||||
$out->writeln("小绿瓶剩余: {$player->potionPool}");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Decrease quantity or remove
|
// Decrease quantity or remove
|
||||||
|
|
@ -402,29 +377,86 @@ class InventoryPanel
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取所有消耗品并按治疗量排序(优先使用小瓶)
|
||||||
|
$consumables = [];
|
||||||
|
foreach ($player->inventory as $index => $item) {
|
||||||
|
if (($item['type'] ?? '') === 'consume' && ($item['heal'] ?? 0) > 0) {
|
||||||
|
$consumables[] = ['index' => $index, 'item' => $item];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($consumables)) {
|
||||||
|
$out->writeln("没有可用的回复药品!");
|
||||||
|
Screen::sleep(1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按治疗量从小到大排序(优先使用小瓶,避免浪费)
|
||||||
|
usort($consumables, fn($a, $b) => ($a['item']['heal'] ?? 0) <=> ($b['item']['heal'] ?? 0));
|
||||||
|
|
||||||
$totalHealed = 0;
|
$totalHealed = 0;
|
||||||
|
$itemsUsed = 0;
|
||||||
$healLog = [];
|
$healLog = [];
|
||||||
|
|
||||||
// 回复玩家
|
// 回复玩家
|
||||||
while ($playerNeedsHeal && $player->hp < $playerMaxHp) {
|
while ($playerNeedsHeal && $player->hp < $playerMaxHp && !empty($consumables)) {
|
||||||
// 计算需要恢复的量
|
// 计算需要恢复的量
|
||||||
$needHeal = $playerMaxHp - $player->hp;
|
$needHeal = $playerMaxHp - $player->hp;
|
||||||
|
|
||||||
// 使用药品,从小绿瓶池里扣除
|
// 找到最合适的药品(不浪费或浪费最少的)
|
||||||
$actualPotionHeal = $player->consumePotionPool($needHeal);
|
$bestIndex = null;
|
||||||
if ($actualPotionHeal == 0){
|
$bestWaste = PHP_INT_MAX;
|
||||||
$out->writeln("小绿瓶已用尽");
|
|
||||||
Screen::sleep(1);
|
foreach ($consumables as $i => $c) {
|
||||||
return;
|
$healAmount = $c['item']['heal'] ?? 0;
|
||||||
|
$waste = max(0, $healAmount - $needHeal);
|
||||||
|
|
||||||
|
if ($waste < $bestWaste) {
|
||||||
|
$bestWaste = $waste;
|
||||||
|
$bestIndex = $i;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果找到完美匹配或不浪费的,直接使用
|
||||||
|
if ($waste === 0) break;
|
||||||
}
|
}
|
||||||
$actualHeal = $player->heal($actualPotionHeal);
|
|
||||||
|
if ($bestIndex === null) break;
|
||||||
|
|
||||||
|
$selected = $consumables[$bestIndex];
|
||||||
|
$inventoryIndex = $selected['index'];
|
||||||
|
$item = $selected['item'];
|
||||||
|
|
||||||
|
// 使用药品
|
||||||
|
$actualHeal = $player->heal($item['heal']);
|
||||||
$totalHealed += $actualHeal;
|
$totalHealed += $actualHeal;
|
||||||
|
$itemsUsed++;
|
||||||
$healLog[] = "玩家: +{$actualHeal} HP";
|
$healLog[] = "玩家: +{$actualHeal} HP";
|
||||||
|
|
||||||
|
// 减少数量或移除物品
|
||||||
|
if (($player->inventory[$inventoryIndex]['quantity'] ?? 1) > 1) {
|
||||||
|
$player->inventory[$inventoryIndex]['quantity']--;
|
||||||
|
$consumables[$bestIndex]['item']['quantity']--;
|
||||||
|
} else {
|
||||||
|
unset($player->inventory[$inventoryIndex]);
|
||||||
|
unset($consumables[$bestIndex]);
|
||||||
|
$consumables = array_values($consumables);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重新整理背包索引
|
||||||
|
$player->inventory = array_values($player->inventory);
|
||||||
|
|
||||||
|
// 更新 consumables 的索引引用
|
||||||
|
$consumables = [];
|
||||||
|
foreach ($player->inventory as $index => $invItem) {
|
||||||
|
if (($invItem['type'] ?? '') === 'consume' && ($invItem['heal'] ?? 0) > 0) {
|
||||||
|
$consumables[] = ['index' => $index, 'item' => $invItem];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
usort($consumables, fn($a, $b) => ($a['item']['heal'] ?? 0) <=> ($b['item']['heal'] ?? 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 回复队友(按需要程度优先恢复)
|
// 回复队友(按需要程度优先恢复)
|
||||||
if (!empty($partnersNeedHeal)) {
|
if (!empty($partnersNeedHeal) && !empty($consumables)) {
|
||||||
// 按需要治疗量从大到小排序(优先治疗伤势最重的)
|
// 按需要治疗量从大到小排序(优先治疗伤势最重的)
|
||||||
usort($partnersNeedHeal, fn($a, $b) => $b['needHeal'] <=> $a['needHeal']);
|
usort($partnersNeedHeal, fn($a, $b) => $b['needHeal'] <=> $a['needHeal']);
|
||||||
|
|
||||||
|
|
@ -432,20 +464,59 @@ class InventoryPanel
|
||||||
$partner = $healData['partner'];
|
$partner = $healData['partner'];
|
||||||
$partnerMaxHp = $healData['maxHp'];
|
$partnerMaxHp = $healData['maxHp'];
|
||||||
|
|
||||||
while ($partner->hp < $partnerMaxHp) {
|
while ($partner->hp < $partnerMaxHp && !empty($consumables)) {
|
||||||
// 计算需要恢复的量
|
// 计算需要恢复的量
|
||||||
$needHeal = $partnerMaxHp - $partner->hp;
|
$needHeal = $partnerMaxHp - $partner->hp;
|
||||||
|
|
||||||
// 使用药品回复队友,从小绿瓶池里扣除
|
// 找到最合适的药品
|
||||||
$actualPotionHeal = $player->consumePotionPool($needHeal);
|
$bestIndex = null;
|
||||||
if ($actualPotionHeal == 0){
|
$bestWaste = PHP_INT_MAX;
|
||||||
$out->writeln("小绿瓶已用尽");
|
|
||||||
Screen::sleep(1);
|
foreach ($consumables as $i => $c) {
|
||||||
return;
|
$healAmount = $c['item']['heal'] ?? 0;
|
||||||
|
$waste = max(0, $healAmount - $needHeal);
|
||||||
|
|
||||||
|
if ($waste < $bestWaste) {
|
||||||
|
$bestWaste = $waste;
|
||||||
|
$bestIndex = $i;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($waste === 0) break;
|
||||||
}
|
}
|
||||||
$actualHeal = $partner->heal($actualPotionHeal);
|
|
||||||
|
if ($bestIndex === null) break;
|
||||||
|
|
||||||
|
$selected = $consumables[$bestIndex];
|
||||||
|
$inventoryIndex = $selected['index'];
|
||||||
|
$item = $selected['item'];
|
||||||
|
|
||||||
|
// 使用药品回复队友
|
||||||
|
$actualHeal = $partner->heal($item['heal']);
|
||||||
$totalHealed += $actualHeal;
|
$totalHealed += $actualHeal;
|
||||||
|
$itemsUsed++;
|
||||||
$healLog[] = "{$partner->name}: +{$actualHeal} HP";
|
$healLog[] = "{$partner->name}: +{$actualHeal} HP";
|
||||||
|
|
||||||
|
// 减少数量或移除物品
|
||||||
|
if (($player->inventory[$inventoryIndex]['quantity'] ?? 1) > 1) {
|
||||||
|
$player->inventory[$inventoryIndex]['quantity']--;
|
||||||
|
$consumables[$bestIndex]['item']['quantity']--;
|
||||||
|
} else {
|
||||||
|
unset($player->inventory[$inventoryIndex]);
|
||||||
|
unset($consumables[$bestIndex]);
|
||||||
|
$consumables = array_values($consumables);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重新整理背包索引
|
||||||
|
$player->inventory = array_values($player->inventory);
|
||||||
|
|
||||||
|
// 更新 consumables 的索引引用
|
||||||
|
$consumables = [];
|
||||||
|
foreach ($player->inventory as $index => $invItem) {
|
||||||
|
if (($invItem['type'] ?? '') === 'consume' && ($invItem['heal'] ?? 0) > 0) {
|
||||||
|
$consumables[] = ['index' => $index, 'item' => $invItem];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
usort($consumables, fn($a, $b) => ($a['item']['heal'] ?? 0) <=> ($b['item']['heal'] ?? 0));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -457,7 +528,7 @@ class InventoryPanel
|
||||||
$out->writeln("╔════════════════════════════════════╗");
|
$out->writeln("╔════════════════════════════════════╗");
|
||||||
$out->writeln("║ 一键回血结果 ║");
|
$out->writeln("║ 一键回血结果 ║");
|
||||||
$out->writeln("╚════════════════════════════════════╝");
|
$out->writeln("╚════════════════════════════════════╝");
|
||||||
$out->writeln("共恢复 {$totalHealed} HP!");
|
$out->writeln("使用了 {$itemsUsed} 个药品,共恢复 {$totalHealed} HP!");
|
||||||
$out->writeln("");
|
$out->writeln("");
|
||||||
$out->writeln("治疗详情:");
|
$out->writeln("治疗详情:");
|
||||||
foreach ($healLog as $log) {
|
foreach ($healLog as $log) {
|
||||||
|
|
@ -668,7 +739,7 @@ class InventoryPanel
|
||||||
$reset = Colors::RESET;
|
$reset = Colors::RESET;
|
||||||
|
|
||||||
$out->writeln("╔════════════════════════════════════════════════════════╗");
|
$out->writeln("╔════════════════════════════════════════════════════════╗");
|
||||||
$out->writeln("║ {$cyan}角色属性{$reset} Lv.{$player->level} | {$green}💰 {$player->spiritStones}{$reset} 灵石 | 🟢 {$player->potionPool} ║");
|
$out->writeln("║ {$cyan}角色属性{$reset} Lv.{$player->level} | {$green}💰 {$player->spiritStones}{$reset} 灵石 ║");
|
||||||
$out->writeln("╠════════════════════════════════════════════════════════╣");
|
$out->writeln("╠════════════════════════════════════════════════════════╣");
|
||||||
|
|
||||||
// First row: HP (current/max), patk, matk
|
// First row: HP (current/max), patk, matk
|
||||||
|
|
|
||||||
|
|
@ -1,39 +0,0 @@
|
||||||
<?php
|
|
||||||
/**
|
|
||||||
* WebSocket 服务器测试脚本
|
|
||||||
*/
|
|
||||||
|
|
||||||
require_once __DIR__ . '/vendor/autoload.php';
|
|
||||||
|
|
||||||
use WebSocket\Client;
|
|
||||||
|
|
||||||
echo "连接到 WebSocket 服务器: ws://localhost:9002\n";
|
|
||||||
|
|
||||||
try {
|
|
||||||
$client = new Client("ws://localhost:9002");
|
|
||||||
echo "[成功] 连接到服务器\n";
|
|
||||||
|
|
||||||
// 接收欢迎消息
|
|
||||||
$message = $client->receive();
|
|
||||||
echo "[收到] " . $message . "\n";
|
|
||||||
|
|
||||||
// 发送测试输入
|
|
||||||
echo "[发送] 测试命令\n";
|
|
||||||
$client->send(json_encode([
|
|
||||||
'type' => 'input',
|
|
||||||
'input' => '1'
|
|
||||||
]));
|
|
||||||
|
|
||||||
// 接收响应
|
|
||||||
for ($i = 0; $i < 5; $i++) {
|
|
||||||
$message = $client->receive();
|
|
||||||
echo "[收到] " . substr($message, 0, 100) . "...\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
$client->close();
|
|
||||||
echo "[完成] 测试成功\n";
|
|
||||||
|
|
||||||
} catch (Exception $e) {
|
|
||||||
echo "[错误] " . $e->getMessage() . "\n";
|
|
||||||
exit(1);
|
|
||||||
}
|
|
||||||
|
|
@ -86,4 +86,4 @@ $bootsTemplate = [
|
||||||
|
|
||||||
$res = Item::createSpell(1, 'common', 10);
|
$res = Item::createSpell(1, 'common', 10);
|
||||||
// dd($monster->getRandomEquipmentDrops(100));
|
// dd($monster->getRandomEquipmentDrops(100));
|
||||||
\Game\Core\SpellCalculator::calculateHeal();
|
dd(Item::randomItem('armor', 30));
|
||||||
677
web/game-ws.html
677
web/game-ws.html
|
|
@ -1,677 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="zh-CN">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>凡人修仙传 - WebSocket版</title>
|
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm@5.3.0/css/xterm.css">
|
|
||||||
<style>
|
|
||||||
* {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
background: #1a1a2e;
|
|
||||||
min-height: 100vh;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-family: 'Consolas', 'Monaco', monospace;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
width: 100%;
|
|
||||||
max-width: 900px;
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header {
|
|
||||||
text-align: center;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header h1 {
|
|
||||||
color: #eee;
|
|
||||||
font-size: 24px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header p {
|
|
||||||
color: #888;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 登录界面 */
|
|
||||||
#auth-panel {
|
|
||||||
background: #16213e;
|
|
||||||
border-radius: 10px;
|
|
||||||
padding: 30px;
|
|
||||||
max-width: 400px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
#auth-panel h2 {
|
|
||||||
color: #eee;
|
|
||||||
text-align: center;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group {
|
|
||||||
margin-bottom: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group label {
|
|
||||||
display: block;
|
|
||||||
color: #aaa;
|
|
||||||
margin-bottom: 5px;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group input {
|
|
||||||
width: 100%;
|
|
||||||
padding: 10px 15px;
|
|
||||||
border: 1px solid #0f3460;
|
|
||||||
border-radius: 5px;
|
|
||||||
background: #1a1a2e;
|
|
||||||
color: #eee;
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group input:focus {
|
|
||||||
outline: none;
|
|
||||||
border-color: #e94560;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-group {
|
|
||||||
display: flex;
|
|
||||||
gap: 10px;
|
|
||||||
margin-top: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn {
|
|
||||||
flex: 1;
|
|
||||||
padding: 12px;
|
|
||||||
border: none;
|
|
||||||
border-radius: 5px;
|
|
||||||
font-size: 16px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary {
|
|
||||||
background: #e94560;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary:hover {
|
|
||||||
background: #ff6b6b;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-secondary {
|
|
||||||
background: #0f3460;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-secondary:hover {
|
|
||||||
background: #16213e;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message {
|
|
||||||
padding: 10px;
|
|
||||||
border-radius: 5px;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
text-align: center;
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message.error {
|
|
||||||
background: rgba(233, 69, 96, 0.2);
|
|
||||||
color: #e94560;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message.success {
|
|
||||||
background: rgba(46, 213, 115, 0.2);
|
|
||||||
color: #2ed573;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 游戏终端 */
|
|
||||||
#game-panel {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.terminal-header {
|
|
||||||
background: #16213e;
|
|
||||||
padding: 10px 15px;
|
|
||||||
border-radius: 10px 10px 0 0;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.terminal-header .user-info {
|
|
||||||
color: #2ed573;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.terminal-header .status {
|
|
||||||
font-size: 12px;
|
|
||||||
color: #888;
|
|
||||||
}
|
|
||||||
|
|
||||||
.terminal-header .logout-btn {
|
|
||||||
background: #e94560;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
padding: 5px 15px;
|
|
||||||
border-radius: 5px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.terminal-header .logout-btn:hover {
|
|
||||||
background: #ff6b6b;
|
|
||||||
}
|
|
||||||
|
|
||||||
#terminal {
|
|
||||||
background: #0d0d0d;
|
|
||||||
border-radius: 0 0 10px 10px;
|
|
||||||
padding: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.input-area {
|
|
||||||
background: #16213e;
|
|
||||||
padding: 10px;
|
|
||||||
border-radius: 0 0 10px 10px;
|
|
||||||
display: flex;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.input-area input {
|
|
||||||
flex: 1;
|
|
||||||
padding: 10px;
|
|
||||||
border: 1px solid #0f3460;
|
|
||||||
border-radius: 5px;
|
|
||||||
background: #1a1a2e;
|
|
||||||
color: #eee;
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.input-area input:focus {
|
|
||||||
outline: none;
|
|
||||||
border-color: #e94560;
|
|
||||||
}
|
|
||||||
|
|
||||||
.input-area button {
|
|
||||||
padding: 10px 20px;
|
|
||||||
background: #e94560;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-radius: 5px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.quick-buttons {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 5px;
|
|
||||||
margin-top: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.quick-btn {
|
|
||||||
padding: 8px 15px;
|
|
||||||
background: #0f3460;
|
|
||||||
color: #eee;
|
|
||||||
border: none;
|
|
||||||
border-radius: 5px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.quick-btn:hover {
|
|
||||||
background: #16213e;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ws-status {
|
|
||||||
display: inline-block;
|
|
||||||
width: 12px;
|
|
||||||
height: 12px;
|
|
||||||
border-radius: 50%;
|
|
||||||
margin-left: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ws-connected {
|
|
||||||
background: #2ed573;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ws-disconnected {
|
|
||||||
background: #e94560;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="container">
|
|
||||||
<div class="header">
|
|
||||||
<h1>凡人修仙传 - WebSocket版</h1>
|
|
||||||
<p>Web Terminal Edition (Real-time)</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 登录/注册面板 -->
|
|
||||||
<div id="auth-panel">
|
|
||||||
<h2>欢迎</h2>
|
|
||||||
<div id="auth-message" class="message"></div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>用户名</label>
|
|
||||||
<input type="text" id="username" placeholder="请输入用户名">
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>密码</label>
|
|
||||||
<input type="password" id="password" placeholder="请输入密码">
|
|
||||||
</div>
|
|
||||||
<div class="btn-group">
|
|
||||||
<button class="btn btn-primary" onclick="login()">登录</button>
|
|
||||||
<button class="btn btn-secondary" onclick="register()">注册</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 游戏面板 -->
|
|
||||||
<div id="game-panel">
|
|
||||||
<div class="terminal-header">
|
|
||||||
<span class="user-info">玩家: <span id="player-name">-</span></span>
|
|
||||||
<span class="status">
|
|
||||||
状态: <span id="game-status">连接中</span>
|
|
||||||
<span class="ws-status ws-disconnected" id="ws-indicator"></span>
|
|
||||||
</span>
|
|
||||||
<button class="logout-btn" onclick="logout()">退出登录</button>
|
|
||||||
</div>
|
|
||||||
<div id="terminal"></div>
|
|
||||||
<div class="input-area">
|
|
||||||
<input type="text" id="game-input" placeholder="输入命令..." onkeypress="handleKeyPress(event)">
|
|
||||||
<button onclick="sendInput()">发送</button>
|
|
||||||
</div>
|
|
||||||
<!-- <div class="quick-buttons">-->
|
|
||||||
<!-- <button class="quick-btn" onclick="quickSend('1')">1.战斗</button>-->
|
|
||||||
<!-- <button class="quick-btn" onclick="quickSend('2')">2.属性</button>-->
|
|
||||||
<!-- <button class="quick-btn" onclick="quickSend('3')">3.背包</button>-->
|
|
||||||
<!-- <button class="quick-btn" onclick="quickSend('4')">4.故人</button>-->
|
|
||||||
<!-- <button class="quick-btn" onclick="quickSend('5')">5.同伴</button>-->
|
|
||||||
<!-- <button class="quick-btn" onclick="quickSend('6')">6.天赋</button>-->
|
|
||||||
<!-- <button class="quick-btn" onclick="quickSend('7')">7.地图</button>-->
|
|
||||||
<!-- <button class="quick-btn" onclick="quickSend('8')">8.休息</button>-->
|
|
||||||
<!-- <button class="quick-btn" onclick="quickSend('0')">0.返回</button>-->
|
|
||||||
<!-- </div>-->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/xterm@5.3.0/lib/xterm.min.js"></script>
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.8.0/lib/xterm-addon-fit.min.js"></script>
|
|
||||||
<script>
|
|
||||||
let terminal = null;
|
|
||||||
let fitAddon = null;
|
|
||||||
let ws = null;
|
|
||||||
let isLoggedIn = false;
|
|
||||||
let currentUserId = null;
|
|
||||||
let currentUsername = null;
|
|
||||||
|
|
||||||
// 初始化终端
|
|
||||||
function initTerminal() {
|
|
||||||
if (terminal) return; // 已初始化
|
|
||||||
|
|
||||||
terminal = new Terminal({
|
|
||||||
cursorBlink: true,
|
|
||||||
fontSize: 16,
|
|
||||||
fontFamily: 'Consolas, Monaco, monospace',
|
|
||||||
theme: {
|
|
||||||
background: '#0d0d0d',
|
|
||||||
foreground: '#eee',
|
|
||||||
cursor: '#e94560',
|
|
||||||
cursorAccent: '#0d0d0d',
|
|
||||||
selection: 'rgba(233, 69, 96, 0.3)',
|
|
||||||
black: '#1a1a2e',
|
|
||||||
red: '#e94560',
|
|
||||||
green: '#2ed573',
|
|
||||||
yellow: '#ffa502',
|
|
||||||
blue: '#70a1ff',
|
|
||||||
magenta: '#ff6b81',
|
|
||||||
cyan: '#1e90ff',
|
|
||||||
white: '#eee',
|
|
||||||
brightBlack: '#666',
|
|
||||||
brightRed: '#ff6b6b',
|
|
||||||
brightGreen: '#7bed9f',
|
|
||||||
brightYellow: '#ffda79',
|
|
||||||
brightBlue: '#a4b0be',
|
|
||||||
brightMagenta: '#ff7f9f',
|
|
||||||
brightCyan: '#34ace0',
|
|
||||||
brightWhite: '#fff'
|
|
||||||
},
|
|
||||||
rows: 24,
|
|
||||||
cols: 80
|
|
||||||
});
|
|
||||||
|
|
||||||
fitAddon = new FitAddon.FitAddon();
|
|
||||||
terminal.loadAddon(fitAddon);
|
|
||||||
terminal.open(document.getElementById('terminal'));
|
|
||||||
fitAddon.fit();
|
|
||||||
|
|
||||||
window.addEventListener('resize', () => {
|
|
||||||
fitAddon.fit();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// WebSocket 连接
|
|
||||||
function connectWebSocket() {
|
|
||||||
const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
||||||
const wsUrl = protocol + '//' + location.hostname + ':9001';
|
|
||||||
|
|
||||||
ws = new WebSocket(wsUrl);
|
|
||||||
|
|
||||||
ws.onopen = () => {
|
|
||||||
console.log('[WebSocket] 已连接');
|
|
||||||
updateWSStatus(true);
|
|
||||||
terminal.writeln('\x1b[32m[✓] WebSocket 连接成功\x1b[0m');
|
|
||||||
};
|
|
||||||
|
|
||||||
ws.onmessage = (event) => {
|
|
||||||
try {
|
|
||||||
const data = JSON.parse(event.data);
|
|
||||||
handleMessage(data);
|
|
||||||
} catch (e) {
|
|
||||||
console.error('消息解析错误:', e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
ws.onerror = (error) => {
|
|
||||||
console.error('[WebSocket] 错误:', error);
|
|
||||||
updateWSStatus(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
ws.onclose = () => {
|
|
||||||
console.log('[WebSocket] 已断开');
|
|
||||||
updateWSStatus(false);
|
|
||||||
if (isLoggedIn) {
|
|
||||||
terminal.writeln('\x1b[31m[✗] WebSocket 连接已断开,尝试重新连接...\x1b[0m');
|
|
||||||
setTimeout(connectWebSocket, 3000);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理服务器消息
|
|
||||||
function handleMessage(data) {
|
|
||||||
console.log('[收到] ', data.type, data);
|
|
||||||
|
|
||||||
switch (data.type) {
|
|
||||||
case 'welcome':
|
|
||||||
terminal.writeln('\x1b[33m' + data.message + '\x1b[0m');
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'login-success':
|
|
||||||
isLoggedIn = true;
|
|
||||||
currentUserId = data.userId;
|
|
||||||
currentUsername = data.username;
|
|
||||||
document.getElementById('player-name').textContent = data.username;
|
|
||||||
document.getElementById('game-status').textContent = 'MENU';
|
|
||||||
terminal.clear();
|
|
||||||
displayOutput(data.output);
|
|
||||||
document.getElementById('game-input').focus();
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'game-output':
|
|
||||||
console.log(data)
|
|
||||||
terminal.clear();
|
|
||||||
displayOutput(data.output);
|
|
||||||
updateGameStatus(data.stateName);
|
|
||||||
document.getElementById('game-input').focus();
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'battle-start':
|
|
||||||
terminal.clear();
|
|
||||||
terminal.writeln('\x1b[33m' + data.message + '\x1b[0m');
|
|
||||||
document.getElementById('game-status').textContent = 'BATTLE';
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'battle-end':
|
|
||||||
terminal.writeln('\x1b[32m[战斗结束]\x1b[0m');
|
|
||||||
updateGameStatus(data.stateName);
|
|
||||||
document.getElementById('game-input').focus();
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'state-sync':
|
|
||||||
displayOutput(data.output);
|
|
||||||
updateGameStatus(data.stateName);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'error':
|
|
||||||
terminal.writeln('\x1b[31m[错误] ' + data.message + '\x1b[0m');
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'pong':
|
|
||||||
// 心跳响应
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
console.warn('未知消息类型:', data.type);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 发送消息给服务器
|
|
||||||
function sendMessage(data) {
|
|
||||||
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
|
||||||
terminal.writeln('\x1b[31m[错误] WebSocket 未连接\x1b[0m');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
ws.send(JSON.stringify(data));
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 显示消息
|
|
||||||
function showMessage(message, type = 'error') {
|
|
||||||
const el = document.getElementById('auth-message');
|
|
||||||
el.textContent = message;
|
|
||||||
el.className = 'message ' + type;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 登录
|
|
||||||
async function login() {
|
|
||||||
const username = document.getElementById('username').value.trim();
|
|
||||||
const password = document.getElementById('password').value;
|
|
||||||
|
|
||||||
if (!username || !password) {
|
|
||||||
showMessage('请输入用户名和密码');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 这里仍然使用HTTP登录进行认证
|
|
||||||
try {
|
|
||||||
const response = await fetch('/api/login', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ username, password }),
|
|
||||||
credentials: 'include'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await response.json();
|
|
||||||
|
|
||||||
if (result.success) {
|
|
||||||
showMessage('登录成功!连接游戏服务器...', 'success');
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
// 获取userId用于WebSocket登录
|
|
||||||
fetch('/api/status', { credentials: 'include' })
|
|
||||||
.then(r => r.json())
|
|
||||||
.then(data => {
|
|
||||||
if (data.success && data.loggedIn) {
|
|
||||||
enterGame(username, data.userId);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, 500);
|
|
||||||
} else {
|
|
||||||
showMessage(result.message || '登录失败');
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
showMessage('网络错误: ' + e.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 注册
|
|
||||||
async function register() {
|
|
||||||
const username = document.getElementById('username').value.trim();
|
|
||||||
const password = document.getElementById('password').value;
|
|
||||||
|
|
||||||
if (!username || !password) {
|
|
||||||
showMessage('请输入用户名和密码');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch('/api/register', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ username, password }),
|
|
||||||
credentials: 'include'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await response.json();
|
|
||||||
|
|
||||||
if (result.success) {
|
|
||||||
showMessage('注册成功!自动登录中...', 'success');
|
|
||||||
setTimeout(() => {
|
|
||||||
fetch('/api/status', { credentials: 'include' })
|
|
||||||
.then(r => r.json())
|
|
||||||
.then(data => {
|
|
||||||
if (data.success && data.loggedIn) {
|
|
||||||
enterGame(username, data.userId);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, 500);
|
|
||||||
} else {
|
|
||||||
showMessage(result.message || '注册失败');
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
showMessage('网络错误: ' + e.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 进入游戏
|
|
||||||
function enterGame(username, userId) {
|
|
||||||
isLoggedIn = true;
|
|
||||||
document.getElementById('auth-panel').style.display = 'none';
|
|
||||||
document.getElementById('game-panel').style.display = 'block';
|
|
||||||
document.getElementById('player-name').textContent = username;
|
|
||||||
|
|
||||||
if (!terminal) {
|
|
||||||
initTerminal();
|
|
||||||
}
|
|
||||||
|
|
||||||
terminal.clear();
|
|
||||||
terminal.writeln('\x1b[33m正在连接游戏服务器...\x1b[0m');
|
|
||||||
|
|
||||||
// 连接WebSocket
|
|
||||||
connectWebSocket();
|
|
||||||
|
|
||||||
// WebSocket连接后登录
|
|
||||||
setTimeout(() => {
|
|
||||||
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
||||||
sendMessage({
|
|
||||||
type: 'login',
|
|
||||||
userId: userId,
|
|
||||||
username: username
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, 500);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 退出登录
|
|
||||||
async function logout() {
|
|
||||||
isLoggedIn = false;
|
|
||||||
if (ws) {
|
|
||||||
ws.close();
|
|
||||||
}
|
|
||||||
await fetch('/api/logout', { credentials: 'include' });
|
|
||||||
document.getElementById('auth-panel').style.display = 'block';
|
|
||||||
document.getElementById('game-panel').style.display = 'none';
|
|
||||||
document.getElementById('password').value = '';
|
|
||||||
showMessage('', '');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 发送输入
|
|
||||||
function sendInput() {
|
|
||||||
const inputEl = document.getElementById('game-input');
|
|
||||||
const input = inputEl.value.trim();
|
|
||||||
inputEl.value = '';
|
|
||||||
|
|
||||||
if (!input) return;
|
|
||||||
|
|
||||||
terminal.writeln('\x1b[36m> ' + input + '\x1b[0m');
|
|
||||||
terminal.writeln('');
|
|
||||||
|
|
||||||
if (sendMessage({
|
|
||||||
type: 'game-input',
|
|
||||||
input: input
|
|
||||||
})) {
|
|
||||||
console.log('[发送] 游戏输入:', input);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 快捷按钮发送
|
|
||||||
function quickSend(value) {
|
|
||||||
document.getElementById('game-input').value = value;
|
|
||||||
sendInput();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 按键处理
|
|
||||||
function handleKeyPress(event) {
|
|
||||||
if (event.key === 'Enter') {
|
|
||||||
sendInput();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 显示输出
|
|
||||||
function displayOutput(output) {
|
|
||||||
if (!output) return;
|
|
||||||
const lines = output.split('\n');
|
|
||||||
lines.forEach(line => {
|
|
||||||
if (line.trim()) {
|
|
||||||
terminal.writeln(line);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新游戏状态显示
|
|
||||||
function updateGameStatus(stateName) {
|
|
||||||
document.getElementById('game-status').textContent = stateName || '未知';
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新WebSocket状态指示器
|
|
||||||
function updateWSStatus(connected) {
|
|
||||||
const indicator = document.getElementById('ws-indicator');
|
|
||||||
if (connected) {
|
|
||||||
indicator.className = 'ws-status ws-connected';
|
|
||||||
} else {
|
|
||||||
indicator.className = 'ws-status ws-disconnected';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 心跳保活
|
|
||||||
setInterval(() => {
|
|
||||||
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
||||||
sendMessage({ type: 'ping' });
|
|
||||||
}
|
|
||||||
}, 30000);
|
|
||||||
|
|
||||||
// 页面加载完成
|
|
||||||
window.onload = async () => {
|
|
||||||
const result = await fetch('/api/status', { credentials: 'include' })
|
|
||||||
.then(r => r.json())
|
|
||||||
.catch(() => ({ success: false }));
|
|
||||||
|
|
||||||
if (result.success && result.loggedIn) {
|
|
||||||
enterGame(result.username, result.userId);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
109
web/index.html
109
web/index.html
|
|
@ -461,41 +461,14 @@
|
||||||
terminal.writeln('\x1b[36m> ' + input + '\x1b[0m');
|
terminal.writeln('\x1b[36m> ' + input + '\x1b[0m');
|
||||||
terminal.writeln('');
|
terminal.writeln('');
|
||||||
|
|
||||||
// 检查是否进入战斗(输入为 "1")
|
|
||||||
if (input === '1') {
|
|
||||||
// 使用 SSE 流式战斗
|
|
||||||
streamBattle(input);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 普通请求处理
|
|
||||||
const result = await api('game/input', { input });
|
const result = await api('game/input', { input });
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
console.log('API响应:', result);
|
terminal.clear();
|
||||||
|
const lines = result.output.split('\n');
|
||||||
// 显示调试信息(可选)
|
lines.forEach(line => {
|
||||||
if (result.stateName) {
|
terminal.writeln(line);
|
||||||
console.log('当前状态:', result.stateName);
|
});
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否是时间戳战斗日志
|
|
||||||
if (result.type === 'battle_log' && result.logs) {
|
|
||||||
terminal.clear();
|
|
||||||
playBattleLog(result.logs);
|
|
||||||
} else if (result.output) {
|
|
||||||
// 普通文本输出
|
|
||||||
terminal.clear();
|
|
||||||
const lines = result.output.split('\n');
|
|
||||||
lines.forEach(line => {
|
|
||||||
terminal.writeln(line);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 输入后立即刷新当前界面(确保状态同步)
|
|
||||||
// setTimeout(() => {
|
|
||||||
// renderGame();
|
|
||||||
// }, 100);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
terminal.writeln('\x1b[31m' + (result.message || '操作失败') + '\x1b[0m');
|
terminal.writeln('\x1b[31m' + (result.message || '操作失败') + '\x1b[0m');
|
||||||
if (result.message === '请先登录') {
|
if (result.message === '请先登录') {
|
||||||
|
|
@ -506,78 +479,6 @@
|
||||||
inputEl.focus();
|
inputEl.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
// SSE 流式战斗
|
|
||||||
async function streamBattle(input) {
|
|
||||||
const eventSource = new EventSource('/api/game/battle-stream?input=' + encodeURIComponent(input), {
|
|
||||||
withCredentials: true
|
|
||||||
});
|
|
||||||
|
|
||||||
const inputEl = document.getElementById('game-input');
|
|
||||||
let battleEnded = false;
|
|
||||||
|
|
||||||
eventSource.addEventListener('start', (event) => {
|
|
||||||
// 清屏,准备显示战斗内容
|
|
||||||
terminal.clear();
|
|
||||||
});
|
|
||||||
|
|
||||||
eventSource.addEventListener('message', (event) => {
|
|
||||||
const log = JSON.parse(event.data);
|
|
||||||
|
|
||||||
// 根据类型显示日志
|
|
||||||
if (log.type === 'writeln') {
|
|
||||||
terminal.writeln(log.text);
|
|
||||||
} else if (log.type === 'write') {
|
|
||||||
terminal.write(log.text);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
eventSource.addEventListener('complete', (event) => {
|
|
||||||
const data = JSON.parse(event.data);
|
|
||||||
console.log('战斗完成:', data);
|
|
||||||
battleEnded = true;
|
|
||||||
eventSource.close();
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
inputEl.focus();
|
|
||||||
}, 500);
|
|
||||||
});
|
|
||||||
|
|
||||||
eventSource.addEventListener('error', (event) => {
|
|
||||||
console.error('SSE 错误:', event);
|
|
||||||
battleEnded = true;
|
|
||||||
eventSource.close();
|
|
||||||
|
|
||||||
if (eventSource.readyState === EventSource.CLOSED) {
|
|
||||||
terminal.writeln('\x1b[31m连接已关闭\x1b[0m');
|
|
||||||
inputEl.focus();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 流式播放战斗日志
|
|
||||||
async function playBattleLog(logs) {
|
|
||||||
let lastTimestamp = 0;
|
|
||||||
|
|
||||||
for (let i = 0; i < logs.length; i++) {
|
|
||||||
const log = logs[i];
|
|
||||||
const delay = log.timestamp - lastTimestamp;
|
|
||||||
|
|
||||||
// 等待适当的延迟
|
|
||||||
if (delay > 0) {
|
|
||||||
await new Promise(resolve => setTimeout(resolve, delay));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 写入日志行
|
|
||||||
if (log.type === 'writeln') {
|
|
||||||
terminal.writeln(log.text);
|
|
||||||
} else if (log.type === 'write') {
|
|
||||||
terminal.write(log.text);
|
|
||||||
}
|
|
||||||
|
|
||||||
lastTimestamp = log.timestamp;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 快捷按钮发送
|
// 快捷按钮发送
|
||||||
function quickSend(value) {
|
function quickSend(value) {
|
||||||
document.getElementById('game-input').value = value;
|
document.getElementById('game-input').value = value;
|
||||||
|
|
|
||||||
362
web/process.html
362
web/process.html
|
|
@ -1,362 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="zh-CN">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>凡人修仙传 - 进程转发版</title>
|
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm@5.3.0/css/xterm.css">
|
|
||||||
<style>
|
|
||||||
* {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
background: #1a1a2e;
|
|
||||||
min-height: 100vh;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
font-family: 'Consolas', 'Monaco', monospace;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header {
|
|
||||||
background: #16213e;
|
|
||||||
padding: 15px 20px;
|
|
||||||
border-bottom: 1px solid #0f3460;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header h1 {
|
|
||||||
color: #eee;
|
|
||||||
font-size: 18px;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-right {
|
|
||||||
display: flex;
|
|
||||||
gap: 15px;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status {
|
|
||||||
font-size: 12px;
|
|
||||||
color: #888;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-indicator {
|
|
||||||
display: inline-block;
|
|
||||||
width: 10px;
|
|
||||||
height: 10px;
|
|
||||||
border-radius: 50%;
|
|
||||||
margin-left: 5px;
|
|
||||||
background: #e94560;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-indicator.connected {
|
|
||||||
background: #2ed573;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-disconnect {
|
|
||||||
padding: 6px 12px;
|
|
||||||
background: #e94560;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-disconnect:hover {
|
|
||||||
background: #ff6b6b;
|
|
||||||
}
|
|
||||||
|
|
||||||
#terminal {
|
|
||||||
flex: 1;
|
|
||||||
overflow: hidden;
|
|
||||||
padding: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.input-area {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading {
|
|
||||||
text-align: center;
|
|
||||||
padding: 20px;
|
|
||||||
color: #888;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error {
|
|
||||||
color: #e94560;
|
|
||||||
padding: 20px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="header">
|
|
||||||
<h1>🎮 凡人修仙传 - 进程转发版</h1>
|
|
||||||
<div class="header-right">
|
|
||||||
<span class="status">
|
|
||||||
连接状态: <span id="conn-status">连接中</span>
|
|
||||||
<span class="status-indicator" id="conn-indicator"></span>
|
|
||||||
</span>
|
|
||||||
<button class="btn-disconnect" onclick="disconnect()">断开连接</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="container">
|
|
||||||
<div id="terminal">
|
|
||||||
</div>
|
|
||||||
<div class="input-area">
|
|
||||||
<input type="text" id="game-input" placeholder="输入命令..." disabled>
|
|
||||||
<button id="send-btn" onclick="sendInput()" disabled>发送</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/xterm@5.3.0/lib/xterm.min.js"></script>
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.8.0/lib/xterm-addon-fit.min.js"></script>
|
|
||||||
<script>
|
|
||||||
let terminal = null;
|
|
||||||
let fitAddon = null;
|
|
||||||
let ws = null;
|
|
||||||
let connected = false;
|
|
||||||
let inputBuffer = ''; // 缓存用户输入
|
|
||||||
|
|
||||||
// 初始化终端
|
|
||||||
function initTerminal() {
|
|
||||||
if (terminal) return;
|
|
||||||
|
|
||||||
terminal = new Terminal({
|
|
||||||
cursorBlink: true,
|
|
||||||
fontSize: 14,
|
|
||||||
fontFamily: '"Source Code Pro", "Courier New", monospace',
|
|
||||||
letterSpacing: 0,
|
|
||||||
lineHeight: 1.3,
|
|
||||||
theme: {
|
|
||||||
background: '#1a1a2e',
|
|
||||||
foreground: '#eee',
|
|
||||||
cursor: '#e94560',
|
|
||||||
cursorAccent: '#1a1a2e',
|
|
||||||
selection: 'rgba(233, 69, 96, 0.3)',
|
|
||||||
black: '#1a1a2e',
|
|
||||||
red: '#e94560',
|
|
||||||
green: '#2ed573',
|
|
||||||
yellow: '#ffa502',
|
|
||||||
blue: '#70a1ff',
|
|
||||||
magenta: '#ff6b81',
|
|
||||||
cyan: '#1e90ff',
|
|
||||||
white: '#eee',
|
|
||||||
brightBlack: '#666',
|
|
||||||
brightRed: '#ff6b6b',
|
|
||||||
brightGreen: '#7bed9f',
|
|
||||||
brightYellow: '#ffda79',
|
|
||||||
brightBlue: '#a4b0be',
|
|
||||||
brightMagenta: '#ff7f9f',
|
|
||||||
brightCyan: '#34ace0',
|
|
||||||
brightWhite: '#fff'
|
|
||||||
},
|
|
||||||
rows: 30,
|
|
||||||
cols: 100,
|
|
||||||
allowProposedApi: true
|
|
||||||
});
|
|
||||||
|
|
||||||
fitAddon = new FitAddon.FitAddon();
|
|
||||||
terminal.loadAddon(fitAddon);
|
|
||||||
terminal.open(document.getElementById('terminal'));
|
|
||||||
fitAddon.fit();
|
|
||||||
|
|
||||||
// 监听终端输入事件
|
|
||||||
terminal.onData((data) => {
|
|
||||||
// 处理输入
|
|
||||||
if (data === '\r' || data === '\n') {
|
|
||||||
// 用户按了回车
|
|
||||||
if (inputBuffer) {
|
|
||||||
sendInput();
|
|
||||||
}
|
|
||||||
} else if (data === '\u007F') {
|
|
||||||
// 处理退格键
|
|
||||||
if (inputBuffer.length > 0) {
|
|
||||||
inputBuffer = inputBuffer.slice(0, -1);
|
|
||||||
terminal.write('\b \b');
|
|
||||||
}
|
|
||||||
} else if (data === '\u0003') {
|
|
||||||
// Ctrl+C - 中断
|
|
||||||
inputBuffer = '';
|
|
||||||
terminal.write('^C\r\n');
|
|
||||||
} else {
|
|
||||||
// 普通字符
|
|
||||||
inputBuffer += data;
|
|
||||||
terminal.write(data);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
window.addEventListener('resize', () => {
|
|
||||||
fitAddon.fit();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 发送输入到服务器
|
|
||||||
function sendInput() {
|
|
||||||
if (!inputBuffer) return;
|
|
||||||
|
|
||||||
const input = inputBuffer;
|
|
||||||
inputBuffer = '';
|
|
||||||
|
|
||||||
sendMessage({
|
|
||||||
type: 'input',
|
|
||||||
input: input
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 连接WebSocket
|
|
||||||
function connectWebSocket() {
|
|
||||||
const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
||||||
const wsUrl = protocol + '//' + location.hostname + ':9002';
|
|
||||||
|
|
||||||
ws = new WebSocket(wsUrl);
|
|
||||||
|
|
||||||
ws.onopen = () => {
|
|
||||||
console.log('[WebSocket] 已连接');
|
|
||||||
connected = true;
|
|
||||||
updateStatus(true);
|
|
||||||
initTerminal();
|
|
||||||
|
|
||||||
if (terminal) {
|
|
||||||
terminal.clear();
|
|
||||||
terminal.writeln('\x1b[32m[✓] WebSocket 连接成功,游戏进程已启动\x1b[0m');
|
|
||||||
terminal.writeln('');
|
|
||||||
terminal.focus(); // 设置终端焦点以接收输入
|
|
||||||
}
|
|
||||||
|
|
||||||
// 启动心跳
|
|
||||||
startHeartbeat();
|
|
||||||
};
|
|
||||||
|
|
||||||
ws.onmessage = (event) => {
|
|
||||||
try {
|
|
||||||
const data = JSON.parse(event.data);
|
|
||||||
handleMessage(data);
|
|
||||||
} catch (e) {
|
|
||||||
console.error('消息解析错误:', e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
ws.onerror = (error) => {
|
|
||||||
console.error('[WebSocket] 错误:', error);
|
|
||||||
if (terminal) {
|
|
||||||
terminal.writeln('\x1b[31m[✗] 连接错误\x1b[0m');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
ws.onclose = () => {
|
|
||||||
console.log('[WebSocket] 已断开');
|
|
||||||
connected = false;
|
|
||||||
updateStatus(false);
|
|
||||||
if (terminal) {
|
|
||||||
terminal.writeln('\x1b[31m[✗] 连接已断开\x1b[0m');
|
|
||||||
}
|
|
||||||
document.getElementById('game-input').disabled = true;
|
|
||||||
document.getElementById('send-btn').disabled = true;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理服务器消息
|
|
||||||
function handleMessage(data) {
|
|
||||||
if (!terminal) {
|
|
||||||
initTerminal();
|
|
||||||
}
|
|
||||||
switch (data.type) {
|
|
||||||
case 'output':
|
|
||||||
// terminal.clear();
|
|
||||||
const lines = data.text.split('\n');
|
|
||||||
lines.forEach(line => {
|
|
||||||
terminal.writeln(line);
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'system':
|
|
||||||
terminal.writeln('\x1b[33m' + data.message + '\x1b[0m');
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'error':
|
|
||||||
terminal.writeln('\x1b[31m[错误] ' + data.message + '\x1b[0m');
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'pong':
|
|
||||||
// 心跳响应
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
console.warn('未知消息类型:', data.type);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 发送消息给服务器
|
|
||||||
function sendMessage(data) {
|
|
||||||
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
|
||||||
if (terminal) {
|
|
||||||
terminal.writeln('\x1b[31m[错误] 未连接到服务器\x1b[0m');
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
ws.send(JSON.stringify(data));
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 断开连接
|
|
||||||
function disconnect() {
|
|
||||||
if (ws) {
|
|
||||||
ws.close();
|
|
||||||
}
|
|
||||||
location.reload();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新连接状态显示
|
|
||||||
function updateStatus(isConnected) {
|
|
||||||
const statusEl = document.getElementById('conn-status');
|
|
||||||
const indicatorEl = document.getElementById('conn-indicator');
|
|
||||||
|
|
||||||
if (isConnected) {
|
|
||||||
statusEl.textContent = '已连接';
|
|
||||||
indicatorEl.className = 'status-indicator connected';
|
|
||||||
} else {
|
|
||||||
statusEl.textContent = '已断开';
|
|
||||||
indicatorEl.className = 'status-indicator';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 心跳保活
|
|
||||||
function startHeartbeat() {
|
|
||||||
setInterval(() => {
|
|
||||||
if (connected && ws && ws.readyState === WebSocket.OPEN) {
|
|
||||||
sendMessage({ type: 'ping' });
|
|
||||||
}
|
|
||||||
}, 30000);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 页面加载完成
|
|
||||||
window.onload = () => {
|
|
||||||
connectWebSocket();
|
|
||||||
};
|
|
||||||
|
|
||||||
// 页面关闭时断开连接
|
|
||||||
window.onbeforeunload = () => {
|
|
||||||
if (ws) {
|
|
||||||
ws.close();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
@ -38,9 +38,9 @@ $requestUri = $_SERVER['REQUEST_URI'];
|
||||||
$path = parse_url($requestUri, PHP_URL_PATH);
|
$path = parse_url($requestUri, PHP_URL_PATH);
|
||||||
|
|
||||||
// 静态文件处理
|
// 静态文件处理
|
||||||
if ($path === '/' || $path === '/process.html') {
|
if ($path === '/' || $path === '/index.html') {
|
||||||
header('Content-Type: text/html; charset=utf-8');
|
header('Content-Type: text/html; charset=utf-8');
|
||||||
readfile(__DIR__ . '/process.html');
|
readfile(__DIR__ . '/index.html');
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -73,12 +73,6 @@ try {
|
||||||
$response = handleGameInput();
|
$response = handleGameInput();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case '/api/game/battle-stream':
|
|
||||||
// SSE 实时战斗流
|
|
||||||
handleBattleStream();
|
|
||||||
exit;
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
// 检查是否是静态文件
|
// 检查是否是静态文件
|
||||||
$filePath = __DIR__ . $path;
|
$filePath = __DIR__ . $path;
|
||||||
|
|
@ -176,14 +170,10 @@ function handleGameRender(): array
|
||||||
|
|
||||||
$session = new GameSession($_SESSION['user_id']);
|
$session = new GameSession($_SESSION['user_id']);
|
||||||
$output = $session->render();
|
$output = $session->render();
|
||||||
$stateInfo = $session->getStateInfo();
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'success' => true,
|
'success' => true,
|
||||||
'output' => $output,
|
'output' => $output,
|
||||||
'state' => $stateInfo['state'],
|
|
||||||
'stateName' => $stateInfo['stateName'],
|
|
||||||
'playerInfo' => $stateInfo['playerInfo'],
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -197,46 +187,10 @@ function handleGameInput(): array
|
||||||
$input = $data['input'] ?? '';
|
$input = $data['input'] ?? '';
|
||||||
|
|
||||||
$session = new GameSession($_SESSION['user_id']);
|
$session = new GameSession($_SESSION['user_id']);
|
||||||
$result = $session->handleInput($input);
|
$output = $session->handleInput($input);
|
||||||
|
|
||||||
// 现在handleInput返回的是数组:output, state, stateName, playerInfo
|
return [
|
||||||
return array_merge(['success' => true], $result);
|
'success' => true,
|
||||||
}
|
'output' => $output,
|
||||||
|
];
|
||||||
/**
|
|
||||||
* 处理 SSE 实时战斗流
|
|
||||||
*/
|
|
||||||
function handleBattleStream(): void
|
|
||||||
{
|
|
||||||
if (empty($_SESSION['user_id'])) {
|
|
||||||
http_response_code(401);
|
|
||||||
echo "data: " . json_encode(['error' => '请先登录']) . "\n\n";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 从URL参数或POST数据获取输入
|
|
||||||
$input = $_GET['input'] ?? $_POST['input'] ?? '';
|
|
||||||
|
|
||||||
// 设置 SSE 响应头
|
|
||||||
header('Content-Type: text/event-stream');
|
|
||||||
header('Cache-Control: no-cache');
|
|
||||||
header('Connection: keep-alive');
|
|
||||||
header('X-Accel-Buffering: no'); // 禁用代理缓冲
|
|
||||||
|
|
||||||
// 发送初始化消息
|
|
||||||
echo "event: start\n";
|
|
||||||
echo "data: " . json_encode(['message' => '战斗开始']) . "\n\n";
|
|
||||||
ob_flush();
|
|
||||||
flush();
|
|
||||||
|
|
||||||
// 创建游戏会话并流式处理战斗
|
|
||||||
try {
|
|
||||||
$session = new GameSession($_SESSION['user_id']);
|
|
||||||
$session->streamBattle($input);
|
|
||||||
} catch (Exception $e) {
|
|
||||||
echo "event: error\n";
|
|
||||||
echo "data: " . json_encode(['message' => $e->getMessage()]) . "\n\n";
|
|
||||||
ob_flush();
|
|
||||||
flush();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,84 +0,0 @@
|
||||||
#!/usr/bin/env php
|
|
||||||
<?php
|
|
||||||
/**
|
|
||||||
* 进程转发 WebSocket 服务器启动脚本
|
|
||||||
* 使用方法: php websocket-process-server.php
|
|
||||||
*
|
|
||||||
* 这个服务器为每个连接启动一个独立的 bin/game 进程
|
|
||||||
* 将进程的输出实时转发给WebSocket客户端
|
|
||||||
* 将客户端输入直接写入进程的STDIN
|
|
||||||
*/
|
|
||||||
|
|
||||||
// 自动加载
|
|
||||||
require_once __DIR__ . '/vendor/autoload.php';
|
|
||||||
|
|
||||||
use Ratchet\Server\IoServer;
|
|
||||||
use Ratchet\Http\HttpServer;
|
|
||||||
use Ratchet\WebSocket\WsServer;
|
|
||||||
use Game\Core\GameProcessServer;
|
|
||||||
|
|
||||||
// 分步骤创建应用栈,以便能访问 GameProcessServer 实例
|
|
||||||
echo "[初始化] 创建 GameProcessServer 实例...\n";
|
|
||||||
$gameServer = new GameProcessServer(null);
|
|
||||||
|
|
||||||
echo "[初始化] 创建 WsServer...\n";
|
|
||||||
$wsServer = new WsServer($gameServer);
|
|
||||||
|
|
||||||
echo "[初始化] 创建 HttpServer...\n";
|
|
||||||
$httpServer = new HttpServer($wsServer);
|
|
||||||
|
|
||||||
// 创建IO服务器(内部创建事件循环)
|
|
||||||
echo "[初始化] 创建 IO 服务器...\n";
|
|
||||||
$server = IoServer::factory($httpServer, 9002, '0.0.0.0');
|
|
||||||
echo "[初始化] IO 服务器创建成功\n";
|
|
||||||
|
|
||||||
// 现在获取事件循环并设置给 GameProcessServer 实例
|
|
||||||
$loop = $server->loop;
|
|
||||||
echo "[初始化] 获取事件循环成功\n";
|
|
||||||
|
|
||||||
$gameServer->setLoop($loop);
|
|
||||||
echo "[初始化] 为 GameProcessServer 设置了事件循环\n";
|
|
||||||
|
|
||||||
echo <<<'ASCII'
|
|
||||||
╔══════════════════════════════════════════╗
|
|
||||||
║ 凡人修仙传 - 进程转发 WebSocket 服务器 ║
|
|
||||||
╚══════════════════════════════════════════╝
|
|
||||||
|
|
||||||
⚡ WebSocket 服务器启动
|
|
||||||
📍 地址: 0.0.0.0:9002
|
|
||||||
🔗 客户端连接: ws://localhost:9002
|
|
||||||
|
|
||||||
📋 运作原理:
|
|
||||||
1. 为每个WebSocket连接启动一个 php bin/game 进程
|
|
||||||
2. 实时读取进程的STDOUT/STDERR
|
|
||||||
3. 通过WebSocket发送给客户端
|
|
||||||
4. 客户端输入直接写入进程的STDIN
|
|
||||||
|
|
||||||
💡 特点:
|
|
||||||
✓ 无需修改游戏代码
|
|
||||||
✓ 每个用户独立的游戏进程
|
|
||||||
✓ 实时输出(100ms轮询)
|
|
||||||
✓ 完整的ANSI颜色支持
|
|
||||||
✓ 实时交互
|
|
||||||
|
|
||||||
按 Ctrl+C 停止服务器...
|
|
||||||
|
|
||||||
ASCII;
|
|
||||||
|
|
||||||
ob_implicit_flush();
|
|
||||||
fflush(STDOUT);
|
|
||||||
|
|
||||||
echo "\n[启动] 准备启动事件循环...\n";
|
|
||||||
fflush(STDOUT);
|
|
||||||
|
|
||||||
try {
|
|
||||||
echo "[启动] 调用 \$loop->run()...\n";
|
|
||||||
fflush(STDOUT);
|
|
||||||
$loop->run();
|
|
||||||
echo "[启动] \$loop->run() 已返回\n";
|
|
||||||
} catch (Exception $e) {
|
|
||||||
echo "\n[错误] 事件循环异常: " . $e->getMessage() . "\n";
|
|
||||||
echo $e->getTraceAsString() . "\n";
|
|
||||||
exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
@ -1,42 +0,0 @@
|
||||||
#!/usr/bin/env php
|
|
||||||
<?php
|
|
||||||
/**
|
|
||||||
* WebSocket 游戏服务器启动脚本
|
|
||||||
* 使用方法: php websocket-server.php
|
|
||||||
*/
|
|
||||||
|
|
||||||
// 自动加载
|
|
||||||
require_once __DIR__ . '/vendor/autoload.php';
|
|
||||||
|
|
||||||
use Ratchet\Server\IoServer;
|
|
||||||
use Ratchet\Http\HttpServer;
|
|
||||||
use Ratchet\WebSocket\WsServer;
|
|
||||||
use Game\Core\GameWebSocketServer;
|
|
||||||
|
|
||||||
// 创建WebSocket服务器
|
|
||||||
$ws = new WsServer(new GameWebSocketServer());
|
|
||||||
|
|
||||||
// 用HTTP服务器包装
|
|
||||||
$http = new HttpServer($ws);
|
|
||||||
|
|
||||||
// 创建IO服务器,监听9001端口
|
|
||||||
$server = IoServer::factory(
|
|
||||||
$http,
|
|
||||||
9001,
|
|
||||||
'0.0.0.0'
|
|
||||||
);
|
|
||||||
|
|
||||||
echo <<<'ASCII'
|
|
||||||
╔══════════════════════════════════════╗
|
|
||||||
║ 凡人修仙传 - WebSocket 游戏服务器 ║
|
|
||||||
╚══════════════════════════════════════╝
|
|
||||||
|
|
||||||
⚡ WebSocket 服务器启动
|
|
||||||
📍 地址: 0.0.0.0:9001
|
|
||||||
🔗 客户端连接: ws://localhost:9001
|
|
||||||
|
|
||||||
按 Ctrl+C 停止服务器...
|
|
||||||
|
|
||||||
ASCII;
|
|
||||||
|
|
||||||
$server->run();
|
|
||||||
Loading…
Reference in New Issue
Block a user