312 lines
9.5 KiB
JavaScript
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();
|
|
}
|
|
})();
|