hanli/web/index.html
hant 10e64d4766 Fix web state management: add state metadata to API responses
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>
2025-12-07 11:19:28 +08:00

604 lines
18 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!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>