Issues fixed: - Remove redundant parameter in handleGameInput (line 195) - Add getStateInfo() method to GameSession to return current game state - Return state metadata in both handleGameRender() and handleGameInput() - Add playerInfo (hp, mana, exp) to responses for UI sync - Add state name mapping for debugging Changes: 1. GameSession.php: - Rename getPlayerInfo() to include full stats (mana, exp) - Add getStateInfo() returning state, stateName, playerInfo - Add getStateName() helper to convert state constant to string 2. server.php: - Fix handleGameInput() parameter error - handleGameRender() now returns state metadata - handleGameInput() now returns state metadata 3. index.html (web): - Add console.log for debugging state sync - Add stateName logging to track state transitions - Prepare for renderGame() refresh (commented) These changes allow: - Frontend to verify correct game state after each action - Debugging state sync issues via browser console - Foundation for state validation in UI 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
604 lines
18 KiB
HTML
604 lines
18 KiB
HTML
<!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;
|
||
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 .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;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
<div class="header">
|
||
<h1>凡人修仙传 - 文字版</h1>
|
||
<p>Web Terminal Edition</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>
|
||
<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 isLoggedIn = false;
|
||
|
||
// 初始化终端
|
||
function initTerminal() {
|
||
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();
|
||
});
|
||
}
|
||
|
||
// API 请求
|
||
async function api(endpoint, data = {}) {
|
||
try {
|
||
const response = await fetch('/api/' + endpoint, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify(data),
|
||
credentials: 'include'
|
||
});
|
||
return await response.json();
|
||
} catch (error) {
|
||
console.error('API Error:', error);
|
||
return { success: false, message: '网络错误' };
|
||
}
|
||
}
|
||
|
||
// 显示消息
|
||
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;
|
||
}
|
||
|
||
const result = await api('login', { username, password });
|
||
|
||
if (result.success) {
|
||
showMessage('登录成功!', 'success');
|
||
setTimeout(() => {
|
||
enterGame(username);
|
||
}, 500);
|
||
} else {
|
||
showMessage(result.message || '登录失败');
|
||
}
|
||
}
|
||
|
||
// 注册
|
||
async function register() {
|
||
const username = document.getElementById('username').value.trim();
|
||
const password = document.getElementById('password').value;
|
||
|
||
if (!username || !password) {
|
||
showMessage('请输入用户名和密码');
|
||
return;
|
||
}
|
||
|
||
const result = await api('register', { username, password });
|
||
|
||
if (result.success) {
|
||
showMessage('注册成功!自动登录中...', 'success');
|
||
setTimeout(() => {
|
||
enterGame(username);
|
||
}, 500);
|
||
} else {
|
||
showMessage(result.message || '注册失败');
|
||
}
|
||
}
|
||
|
||
// 退出登录
|
||
async function logout() {
|
||
await api('logout');
|
||
isLoggedIn = false;
|
||
document.getElementById('auth-panel').style.display = 'block';
|
||
document.getElementById('game-panel').style.display = 'none';
|
||
document.getElementById('password').value = '';
|
||
showMessage('', '');
|
||
}
|
||
|
||
// 进入游戏
|
||
function enterGame(username) {
|
||
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');
|
||
|
||
// 渲染游戏界面
|
||
renderGame();
|
||
|
||
// 聚焦输入框
|
||
document.getElementById('game-input').focus();
|
||
}
|
||
|
||
// 渲染游戏
|
||
async function renderGame() {
|
||
const result = await api('game/render');
|
||
|
||
if (result.success) {
|
||
terminal.clear();
|
||
const lines = result.output.split('\n');
|
||
lines.forEach(line => {
|
||
terminal.writeln(line);
|
||
});
|
||
} else {
|
||
terminal.writeln('\x1b[31m' + (result.message || '加载失败') + '\x1b[0m');
|
||
if (result.message === '请先登录') {
|
||
logout();
|
||
}
|
||
}
|
||
}
|
||
|
||
// 发送输入
|
||
async 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('');
|
||
|
||
// 检查是否进入战斗(输入为 "1")
|
||
if (input === '1') {
|
||
// 使用 SSE 流式战斗
|
||
streamBattle(input);
|
||
return;
|
||
}
|
||
|
||
// 普通请求处理
|
||
const result = await api('game/input', { input });
|
||
|
||
if (result.success) {
|
||
console.log('API响应:', result);
|
||
|
||
// 显示调试信息(可选)
|
||
if (result.stateName) {
|
||
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 {
|
||
terminal.writeln('\x1b[31m' + (result.message || '操作失败') + '\x1b[0m');
|
||
if (result.message === '请先登录') {
|
||
logout();
|
||
}
|
||
}
|
||
|
||
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) {
|
||
document.getElementById('game-input').value = value;
|
||
sendInput();
|
||
}
|
||
|
||
// 按键处理
|
||
function handleKeyPress(event) {
|
||
if (event.key === 'Enter') {
|
||
sendInput();
|
||
}
|
||
}
|
||
|
||
// 页面加载时检查登录状态
|
||
window.onload = async function() {
|
||
const result = await api('status');
|
||
if (result.success && result.loggedIn) {
|
||
enterGame(result.username);
|
||
}
|
||
};
|
||
</script>
|
||
</body>
|
||
</html>
|