Guanghu Domestic Migration d1e47f4565
Some checks are pending
自动更新代码和重启 / update-and-restart (push) Waiting to run
CI检查 + 自动部署 / check (push) Waiting to run
CI检查 + 自动部署 / deploy (push) Blocked by required conditions
重启聊天服务 / restart (push) Waiting to run
chore: import sanitized domestic snapshot for REPO-002
Source snapshot: ca48d3ddf926d79aa138306164169baf764bb829
2026-07-17 15:54:41 +08:00

312 lines
9.5 KiB
JavaScript

/**
* M-PALACE · 游戏交互逻辑
* 管理叙事渲染、选项交互、状态栏更新、存档操作
*/
(function () {
'use strict';
var API_BASE = window.location.origin;
var saveId = sessionStorage.getItem('palace_save_id') || '';
var choicesMeta = null;
var isTyping = false;
// ---------- Initialization ----------
function init() {
var startData = sessionStorage.getItem('palace_state');
var loadedData = sessionStorage.getItem('palace_loaded');
if (startData) {
var data = JSON.parse(startData);
sessionStorage.removeItem('palace_state');
renderGameData(data);
} else if (loadedData) {
var loaded = JSON.parse(loadedData);
sessionStorage.removeItem('palace_loaded');
renderLoadedData(loaded);
} else if (saveId) {
loadSave(saveId);
} else {
window.location.href = 'index.html';
}
bindEvents();
}
// ---------- Rendering ----------
function renderGameData(data) {
saveId = data.save_id || saveId;
document.getElementById('save-id-display').textContent = '存档号: ' + saveId;
if (data.state && data.state.player) {
var p = data.state.player;
document.getElementById('top-title').textContent =
'🏯 宫廷纪 · ' + (p.worldview || '') + ' · ' + (p.role || '');
}
if (data.narrative) {
renderNarrative(data.narrative);
}
if (data.four_dimensions) {
updateStatusBar(data.four_dimensions);
}
updateProgressInfo(data.chapter || 1, data.paragraph || 1);
}
function renderLoadedData(data) {
document.getElementById('save-id-display').textContent = '存档号: ' + saveId;
if (data.state) {
var s = data.state;
if (s.player) {
document.getElementById('top-title').textContent =
'🏯 宫廷纪 · ' + (s.player.worldview || '') + ' · ' + (s.player.role || '');
}
if (s.four_dimensions) {
updateStatusBar(s.four_dimensions);
}
updateProgressInfo(s.chapter || 1, s.paragraph || 1);
}
// Render last narrative from history or show resume message
var narrativeEl = document.getElementById('narrative-text');
narrativeEl.textContent = '存档已恢复。继续你的宫廷故事……';
renderChoices(['继续前行', '回顾往事', '观察四周']);
}
function renderNarrative(narrative) {
if (narrative.chapter_title) {
document.getElementById('chapter-title').textContent = narrative.chapter_title;
}
var textEl = document.getElementById('narrative-text');
if (narrative.narrative) {
typewriter(textEl, narrative.narrative);
}
if (narrative.choices && narrative.choices.length > 0) {
// Delay choices until typewriter finishes
var delay = narrative.narrative ? narrative.narrative.length * 50 + 500 : 200;
setTimeout(function () {
renderChoices(narrative.choices);
}, delay);
}
choicesMeta = narrative.choices_meta || null;
}
function renderChoices(choices) {
var container = document.getElementById('choices-container');
container.innerHTML = '';
var nums = ['①', '②', '③'];
choices.forEach(function (text, i) {
var btn = document.createElement('button');
btn.className = 'choice-btn fade-in';
btn.innerHTML = '<span class="choice-num">' + (nums[i] || '·') + '</span>' + escapeHtml(text);
btn.addEventListener('click', function () {
submitChoice(i, text);
});
container.appendChild(btn);
});
}
// ---------- Typewriter Effect ----------
function typewriter(el, text) {
isTyping = true;
el.textContent = '';
var cursor = document.createElement('span');
cursor.className = 'typewriter-cursor';
el.appendChild(cursor);
var i = 0;
var interval = setInterval(function () {
if (i < text.length) {
el.insertBefore(document.createTextNode(text[i]), cursor);
i++;
} else {
clearInterval(interval);
if (cursor.parentNode) cursor.remove();
isTyping = false;
}
}, 45);
}
// ---------- Status Bar ----------
function updateStatusBar(dims) {
var keys = ['power', 'status', 'emotion', 'conflict'];
keys.forEach(function (k) {
var val = dims[k] != null ? dims[k] : 50;
var bar = document.getElementById('bar-' + k);
var valEl = document.getElementById('val-' + k);
if (bar) bar.style.width = val + '%';
if (valEl) valEl.textContent = val;
});
}
function updateProgressInfo(chapter, paragraph) {
var el = document.getElementById('progress-info');
if (el) el.textContent = '📖 第' + chapter + '章 · 第' + paragraph + '段';
}
// ---------- Interactions ----------
function submitChoice(index, text) {
if (isTyping) return;
disableChoices();
fetch(API_BASE + '/api/palace/interact', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
save_id: saveId,
input: text,
choice_index: index,
option_meta: choicesMeta
})
})
.then(function (r) { return r.json(); })
.then(function (data) {
if (data.error) {
showToast(data.message || '互动失败');
enableChoices();
return;
}
choicesMeta = data.choices_meta || null;
if (data.narrative) renderNarrative(data.narrative);
if (data.four_dimensions) updateStatusBar(data.four_dimensions);
updateProgressInfo(data.chapter || 1, data.paragraph || 1);
})
.catch(function (err) {
showToast('网络错误:' + err.message);
enableChoices();
});
}
function submitFreeInput() {
var input = document.getElementById('free-input');
var text = input.value.trim();
if (!text || isTyping) return;
input.value = '';
disableChoices();
fetch(API_BASE + '/api/palace/interact', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
save_id: saveId,
input: text,
choice_index: null,
option_meta: choicesMeta
})
})
.then(function (r) { return r.json(); })
.then(function (data) {
if (data.error) {
showToast(data.message || '互动失败');
enableChoices();
return;
}
choicesMeta = data.choices_meta || null;
if (data.narrative) renderNarrative(data.narrative);
if (data.four_dimensions) updateStatusBar(data.four_dimensions);
updateProgressInfo(data.chapter || 1, data.paragraph || 1);
})
.catch(function (err) {
showToast('网络错误:' + err.message);
enableChoices();
});
}
function disableChoices() {
var btns = document.querySelectorAll('.choice-btn');
btns.forEach(function (b) { b.disabled = true; b.style.opacity = '0.5'; });
}
function enableChoices() {
var btns = document.querySelectorAll('.choice-btn');
btns.forEach(function (b) { b.disabled = false; b.style.opacity = '1'; });
}
// ---------- Save / Load ----------
function doSave() {
fetch(API_BASE + '/api/palace/save', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ save_id: saveId, state: {}, persona: {}, history: {} })
})
.then(function (r) { return r.json(); })
.then(function (data) {
if (data.error) { showToast(data.message); return; }
showToast('存档成功:' + data.save_id);
})
.catch(function (err) {
showToast('存档失败:' + err.message);
});
}
function loadSave(id) {
fetch(API_BASE + '/api/palace/save/load', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ save_id: id })
})
.then(function (r) { return r.json(); })
.then(function (data) {
if (data.error) {
showToast(data.message || '读档失败');
window.location.href = 'index.html';
return;
}
renderLoadedData(data);
})
.catch(function (err) {
showToast('网络错误:' + err.message);
window.location.href = 'index.html';
});
}
// ---------- Events ----------
function bindEvents() {
document.getElementById('btn-save').addEventListener('click', doSave);
document.getElementById('btn-free-submit').addEventListener('click', submitFreeInput);
document.getElementById('free-input').addEventListener('keydown', function (e) {
if (e.key === 'Enter') submitFreeInput();
});
var toggle = document.getElementById('status-toggle');
var bar = document.getElementById('status-bar');
toggle.addEventListener('click', function () {
bar.classList.toggle('collapsed');
toggle.textContent = bar.classList.contains('collapsed') ? '▶ 状态栏' : '▼ 状态栏';
});
}
// ---------- Utilities ----------
function escapeHtml(str) {
var div = document.createElement('div');
div.appendChild(document.createTextNode(str));
return div.innerHTML;
}
function showToast(msg) {
var old = document.querySelector('.toast');
if (old) old.remove();
var el = document.createElement('div');
el.className = 'toast';
el.textContent = msg;
document.body.appendChild(el);
setTimeout(function () { if (el.parentNode) el.remove(); }, 3000);
}
// ---------- Boot ----------
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();