let currentModule = "发现";
let currentVideoUrl = "";
let currentVideoTitle = "";
let currentDiscipline = "all";
let currentSubTab = "recommend";
let currentLifeTab = "story";
let allVideos = [];
let allKnowledgeCards = [];
let storyData = null;
let simulatorData = null;
let currentContext = { type: "", title: "" };
let userId = localStorage.getItem("mathAppUserId") || generateUserId();
let userFavorites = { video: {}, knowledge: {} };
let userAvatar = localStorage.getItem("mathAppAvatar") || "葫";
let userAvatarColor = localStorage.getItem("mathAppAvatarColor") || "#A0B9D8";
let vnState = {
currentNode: null,
stats: { shuli: 10, xiayi: 10, xinzhi: 10 },
favorability: { "欧阳侠": 0, "Eddie侠": 0, "厚朴侠": 0, "三力思侠": 0, "溜溜侠": 0, "孔锝侠": 0, "星侠": 0, "甲亢侠": 0 }
};
let simState = {
currentNode: null,
stats: { gpa: 10, interest: 10, skill: 10 },
history: []
};
const SIM_STAT_LABELS = { gpa: "绩点", interest: "兴趣", skill: "技能" };
const SIM_STAT_COLORS = { gpa: "#A0B9D8", interest: "#DAB8B0", skill: "#B8D0C9" };
const SIM_STAT_EMOJIS = { gpa: "📊", interest: "❤️", skill: "🔧" };
function generateUserId() {
var id = "user_" + Date.now().toString(36) + "_" + Math.random().toString(36).substr(2, 6);
localStorage.setItem("mathAppUserId", id);
return id;
}
const STAT_LABELS = { shuli: "数理根基", xiayi: "侠义声望", xinzhi: "心志韧性" };
const STAT_COLORS = { shuli: "#A0B9D8", xiayi: "#DAB8B0", xinzhi: "#B8D0C9" };
const STAT_EMOJIS = { shuli: "📐", xiayi: "⚔️", xinzhi: "🧘" };
function detectChapter(nodeId) {
if (!nodeId) return 1;
var m = nodeId.match(/^ch(\d+)/);
return m ? parseInt(m[1]) : 1;
}
const MODULE_COLORS = {
"发现": { color: "#A0B9D8", gradient: "linear-gradient(135deg, #A0B9D8, #8AA3C2)", lightBg: "#DCE6F0", name: "发现 · 值得一看的数学内容" },
"知识": { color: "#B8D0C9", gradient: "linear-gradient(135deg, #B8D0C9, #A0BBAF)", lightBg: "#E4EFEB", name: "知识 · 数学发现" },
"数学人生": { color: "#DAB8B0", gradient: "linear-gradient(135deg, #DAB8B0, #C9A39A)", lightBg: "#F2E4DF", name: "数学人生 · 大学选择模拟" },
"我的": { color: "#C5CAD0", gradient: "linear-gradient(135deg, #C5CAD0, #AEB3B9)", lightBg: "#EDEEF0", name: "我的 · 个人中心" }
};
const MODULE_BANNERS = {
"发现": { icon: "◎", title: "发现值得一看的数学内容", sub: "从 12 个专业分区进入,直接查看该方向的视频内容" },
"知识": { icon: "≡", title: "用数学视角理解世界", sub: "把复杂概念变成轻量、好懂、适合反复刷的知识卡片" },
"数学人生": { icon: "◇", title: "葫芦侠行 · 互动剧情", sub: "八章完整剧情 | 四属性系统 | 多结局 | 北宋数学武侠世界" },
"我的": { icon: "✦", title: "你的学习空间", sub: "查看资料、收藏、记录和剧情存档" }
};
const MODULE_CSS_CLASS = {
"发现": "shuren", "知识": "shushuo", "数学人生": "shusi", "我的": "shuwu"
};
const DISCIPLINE_ICONS = {
"计算机": { icon: "💻", symbol: "⌨", color: "#fff", bg: "#8BA4C0" },
"机械工程": { icon: "⚙️", symbol: "⚙", color: "#fff", bg: "#B8A99A" },
"电子信息": { icon: "📡", symbol: "⚡", color: "#fff", bg: "#9BB8AE" },
"经济管理": { icon: "📊", symbol: "📈", color: "#fff", bg: "#C5A8A0" },
"生物医学": { icon: "🧬", symbol: "❤", color: "#fff", bg: "#C4B0B8" },
"化工材料": { icon: "🧪", symbol: "⚗", color: "#fff", bg: "#ACAEC5" },
"人工智能": { icon: "🤖", symbol: "AI", color: "#fff", bg: "#95ABB8" },
"微电子": { icon: "🔌", symbol: "μ", color: "#fff", bg: "#8BA4C0" },
"自动化": { icon: "🦾", symbol: "⚙", color: "#fff", bg: "#BCA898" },
"能源动力": { icon: "🔥", symbol: "⚡", color: "#fff", bg: "#A3B4A5" },
"环境工程": { icon: "🌱", symbol: "✿", color: "#fff", bg: "#A8B8A8" },
"数学建模": { icon: "📐", symbol: "Σ", color: "#fff", bg: "#9CAEC4" },
"通用": { icon: "📚", symbol: "◎", color: "#fff", bg: "#ADB5BD" }
};
const AVATAR_COLORS = ["#A0B9D8","#B8D0C9","#DAB8B0","#C5CAD0","#B8A99A","#9BB8AE","#C4B0B8","#ACAEC5"];
const SCRIPTS = [
{
id: "huluxiaxing",
name: "葫芦侠行",
icon: "📖",
desc: "北宋年间,紫金山下秉公习武堂。你扮演葫芦侠,用数学武学闯荡江湖。",
meta: "93节点 · 8章 · 8+结局",
status: "ready",
cssClass: "ss-card-wuxia",
engine: "story"
},
{
id: "simulator",
name: "数学专业模拟器",
icon: "🎓",
desc: "跑完四年大学数学专业的全流程。选课、考试、分流、毕业——你的每个选择决定结局。",
meta: "8学期 · 大三专业分流 · 多结局",
status: "beta",
cssClass: "ss-card-simulator",
engine: "simulator"
},
{
id: "mathbti",
name: "数学人格测试",
icon: "🧮",
desc: "12道场景题,发现你的数学人格类型——你是「梦境探索者」还是「博弈游侠」?",
meta: "12题 · 4轴测评 · 12人格",
status: "ready",
cssClass: "ss-card-mathbti",
engine: "mathbti"
}
];
const COMING_SOON = [
{ name: "数学侦探录", icon: "🔍", desc: "民国上海,用数学破案", color: "#B8A99A" },
{ name: "星际数探", icon: "🔭", desc: "22世纪太空舰队首席数理官", color: "#9BB8AE" },
{ name: "算学千年", icon: "📜", desc: "穿越古代,亲历数学史", color: "#ACAEC5" },
{ name: "数学创业记", icon: "🚀", desc: "硅谷数学PhD辍学创业", color: "#C4B0B8" }
];
const SPRITE_MAP = {
"葫芦侠": "https://coresg-normal.trae.ai/api/ide/v1/text_to_image?prompt=young%20Chinese%20Song%20dynasty%20scholar%20with%20a%20gourd%20hanging%20at%20his%20waist%2C%20simple%20blue-gray%20robes%2C%20gentle%20expression%2C%20clean%20ink%20wash%20painting%20style%20on%20soft%20background%2C%20morandi%20muted%20colors&image_size=portrait_4_3",
"欧阳侠": "https://coresg-normal.trae.ai/api/ide/v1/text_to_image?prompt=diligent%20young%20Chinese%20scholar%20reading%20a%20book%20at%20dawn%2C%20warm%20candle%20light%2C%20ink%20wash%20style%2C%20wearing%20simple%20robes%2C%20soft%20morandi%20beige%20and%20blue%20tones&image_size=portrait_4_3",
"溜溜侠": "https://coresg-normal.trae.ai/api/ide/v1/text_to_image?prompt=mysterious%20young%20Chinese%20female%20warrior%20with%20a%20large%20black%20dog%2C%20veiled%20face%2C%20dark%20grey%20robes%2C%20ink%20wash%20painting%20style%2C%20morandi%20muted%20dark%20tones%2C%20cool%20atmosphere&image_size=portrait_4_3",
"孔锝侠": "https://coresg-normal.trae.ai/api/ide/v1/text_to_image?prompt=strict%20Chinese%20martial%20arts%20instructor%2C%20tall%20and%20upright%2C%20stern%20but%20kind%20eyes%2C%20ink%20wash%20style%2C%20wearing%20neat%20grey-blue%20robes%2C%20morandi%20muted%20tones&image_size=portrait_4_3",
"Eddie侠": "https://coresg-normal.trae.ai/api/ide/v1/text_to_image?prompt=young%20Chinese%20scholar%20with%20copper%20coin%20spinning%20on%20finger%2C%20playful%20expression%2C%20ink%20wash%20style%2C%20soft%20morandi%20tones&image_size=portrait_4_3",
"厚朴侠": "https://coresg-normal.trae.ai/api/ide/v1/text_to_image?prompt=elegant%20Chinese%20theater%20actor%2C%20dramatic%20pose%2C%20traditional%20costume%2C%20ink%20wash%20style%2C%20morandi%20muted%20warm%20tones&image_size=portrait_4_3",
"星侠": "https://coresg-normal.trae.ai/api/ide/v1/text_to_image?prompt=gentle%20Chinese%20female%20scholar%20with%20warm%20smile%2C%20ink%20wash%20style%2C%20flowing%20robes%2C%20morandi%20soft%20pink%20and%20grey%20tones&image_size=portrait_4_3",
"甲亢侠": "https://coresg-normal.trae.ai/api/ide/v1/text_to_image?prompt=passionate%20young%20Chinese%20debater%2C%20animated%20gestures%2C%20ink%20wash%20style%2C%20morandi%20muted%20tones%2C%20intense%20eyes&image_size=portrait_4_3"
};
const BG_MAP = {
"紫金山": "https://coresg-normal.trae.ai/api/ide/v1/text_to_image?prompt=ancient%20Chinese%20academy%20on%20misty%20Purple%20Mountain%2C%20traditional%20architecture%20with%20courtyards%2C%20morning%20fog%2C%20ink%20wash%20painting%20style%2C%20morandi%20muted%20greens%20and%20greys&image_size=landscape_4_3",
"石室": "https://coresg-normal.trae.ai/api/ide/v1/text_to_image?prompt=underground%20stone%20chamber%20turned%20study%20room%2C%20candle%20lit%2C%20wooden%20shelves%20with%20scrolls%2C%20murals%20on%20rock%20walls%2C%20warm%20atmosphere%2C%20ink%20wash%20style%2C%20morandi%20warm%20greys&image_size=landscape_4_3",
"姑苏": "https://coresg-normal.trae.ai/api/ide/v1/text_to_image?prompt=Suzhou%20ancient%20canal%20town%20in%20spring%2C%20stone%20bridges%2C%20willow%20trees%2C%20traditional%20white%20wall%20buildings%2C%20gentle%20rain%2C%20ink%20wash%20painting%20style%2C%20morandi%20muted%20blue-grey%20tones&image_size=landscape_4_3",
"京城": "https://coresg-normal.trae.ai/api/ide/v1/text_to_image?prompt=ancient%20Chinese%20capital%20city%2C%20grand%20government%20hall%2C%20solemn%20atmosphere%2C%20ink%20wash%20style%2C%20morandi%20muted%20grey%20tones&image_size=landscape_4_3"
};
const CHAPTER_BG = {
1: "紫金山",
2: "紫金山",
3: "紫金山",
4: "石室",
5: "紫金山",
6: "石室",
7: "石室",
8: "京城"
};
document.addEventListener("DOMContentLoaded", async function () {
await loadStoryData();
await loadUserFavorites();
loadVNSave();
switchModule("发现");
});
function switchModule(module) {
currentModule = module;
currentDiscipline = "all";
currentSubTab = "recommend";
var colors = MODULE_COLORS[module] || MODULE_COLORS["发现"];
document.documentElement.style.setProperty("--accent", colors.color);
document.documentElement.style.setProperty("--accent-light", colors.lightBg);
document.documentElement.style.setProperty("--accent-gradient", colors.gradient);
var brandIcon = document.querySelector(".brand-icon-sm");
if (brandIcon) { brandIcon.textContent = "葫"; brandIcon.style.background = userAvatarColor; }
document.querySelectorAll(".tab-btn").forEach(function (btn) {
btn.classList.toggle("active", btn.dataset.module === module);
});
hideAllPanels();
if (module === "发现") {
show("searchHeader");
show("subNav");
show("videoList");
document.querySelectorAll(".sub-nav-btn").forEach(function (btn) {
btn.classList.toggle("active", btn.dataset.tab === "recommend");
});
currentSubTab = "recommend";
loadVideos("数人");
return;
} else if (module === "知识") {
show("sectionTitleBar");
show("shushuoHero");
show("shushuoTopics");
show("shushuoFeed");
document.getElementById("contentTitle").textContent = colors.name;
loadKnowledgeCards();
return;
} else if (module === "数学人生") {
show("scriptSquare");
renderScriptSquare();
return;
} else if (module === "我的") {
show("sectionTitleBar");
show("profilePage");
document.getElementById("contentTitle").textContent = colors.name;
loadProfileData();
return;
}
}
function hideAllPanels() {
hide("searchHeader"); hide("subNav"); hide("sectionTitleBar"); hide("lifeSubNav");
hide("moduleBanner"); hide("shushuoHero"); hide("shushuoTopics");
hide("shushuoFeed"); hide("storyContainer"); hide("simulatorContainer");
hide("disciplineGridContainer"); hide("disciplineActiveHeader");
hide("disciplineGrid"); hide("categoryFilter");
hide("profilePage"); hide("scriptSquare");
hide("loadingArea"); hide("videoList");
}
function show(id) { var el = document.getElementById(id); if (el) el.style.display = (id === "videoList" ? "grid" : id === "subNav" ? "flex" : "block"); }
function hide(id) { var el = document.getElementById(id); if (el) el.style.display = "none"; }
function updateBanner(module) {
var info = MODULE_BANNERS[module] || MODULE_BANNERS["发现"];
var el = document.getElementById("moduleBanner");
if (!el) return;
el.style.display = "flex";
el.className = "module-banner " + (MODULE_CSS_CLASS[module] || "shuren");
el.innerHTML = '' + info.icon + '
' + info.title + '
' + info.sub + '
';
}
function handleSearch() {
var q = document.getElementById("videoSearch").value.toLowerCase().trim();
if (!q) { renderVideoList(allVideos); return; }
renderVideoList(allVideos.filter(function (v) {
return (v.title && v.title.toLowerCase().indexOf(q) >= 0) ||
(v.description && v.description.toLowerCase().indexOf(q) >= 0) ||
(v.discipline && v.discipline.toLowerCase().indexOf(q) >= 0) ||
(v.author && v.author.toLowerCase().indexOf(q) >= 0);
}));
}
function switchSubTab(tab) {
currentSubTab = tab;
document.querySelectorAll(".sub-nav-btn").forEach(function (btn) { btn.classList.toggle("active", btn.dataset.tab === tab); });
var gridCtn = document.getElementById("disciplineGridContainer");
var videoList = document.getElementById("videoList");
var activeHdr = document.getElementById("disciplineActiveHeader");
var catFilter = document.getElementById("categoryFilter");
if (gridCtn) gridCtn.style.display = "none";
if (activeHdr) activeHdr.style.display = "none";
if (catFilter) catFilter.style.display = "none";
if (tab === "discipline") {
if (videoList) videoList.style.display = "none";
renderDisciplineGrid();
} else if (tab === "hot") {
if (videoList) videoList.style.display = "grid";
var sorted = [].concat(allVideos).sort(function (a, b) { return (hashStr(b.title) % 1000) - (hashStr(a.title) % 1000); });
renderVideoList(sorted);
} else {
if (videoList) videoList.style.display = "grid";
renderVideoList(allVideos);
}
}
function renderDisciplineGrid() {
var gridCtn = document.getElementById("disciplineGridContainer");
var grid = document.getElementById("disciplineGrid");
var videoList = document.getElementById("videoList");
if (videoList) videoList.style.display = "none";
var keys = Object.keys(DISCIPLINE_ICONS).filter(function (d) { return d !== "通用"; });
grid.innerHTML = keys.map(function (d) {
var info = DISCIPLINE_ICONS[d];
return ''
+ '
' + (info.symbol || info.icon) + '
'
+ '
' + esc(d) + '
';
}).join("");
gridCtn.style.display = "block";
grid.style.display = "grid";
}
function selectDiscipline(d) {
document.getElementById("disciplineGridContainer").style.display = "none";
document.getElementById("categoryFilter").style.display = "none";
document.getElementById("videoList").style.display = "grid";
document.getElementById("disciplineActiveHeader").style.display = "flex";
document.getElementById("activeDisciplineName").textContent = d;
renderVideoList(allVideos.filter(function (v) { return v.discipline === d; }));
}
function backToDisciplineGrid() {
document.getElementById("disciplineActiveHeader").style.display = "none";
document.getElementById("categoryFilter").style.display = "none";
renderDisciplineGrid();
}
async function loadKnowledgeCards(topic) {
show("loadingArea");
hide("shushuoFeed");
var url = "/api/knowledge";
if (topic) url += "?topic=" + encodeURIComponent(topic);
try {
var resp = await fetch(url);
var result = await resp.json();
hide("loadingArea");
if (result.code === 0) {
allKnowledgeCards = result.data || [];
var topics = result.topics || [];
renderShushuoTopics(topics);
renderShushuoFeed(allKnowledgeCards);
}
} catch (e) {
hide("loadingArea");
}
}
function renderShushuoTopics(topics) {
var scrollEl = document.getElementById("topicsScroll");
scrollEl.innerHTML = ('全部 ') +
topics.map(function (t) {
return '' + esc(t) + ' ';
}).join("");
}
function renderShushuoFeed(cards) {
show("shushuoFeed");
updateShushuoHero();
var feed = document.getElementById("shushuoFeed");
feed.innerHTML = cards.map(function (c) {
var isFav = userFavorites.knowledge[c.id] || false;
return ''
+ '
'
+ '
' + (c.icon || "📖") + '
'
+ '
' + esc(c.question || "") + '
'
+ '
' + (isFav ? '★' : '☆') + ' '
+ '
'
+ '
' + esc(c.title) + '
'
+ '
' + esc(c.description) + '
'
+ '
';
}).join("");
}
function filterShushuoByTopic(topic) {
document.querySelectorAll(".topic-pill").forEach(function (p) {
var is = p.textContent.trim() === (topic || "全部");
p.classList.toggle("active", is);
});
if (topic) {
loadKnowledgeCards(topic);
} else {
loadKnowledgeCards();
}
document.querySelector(".content-area").scrollTo({ top: 0, behavior: "smooth" });
}
function openKnowledgeDetail(cardId) {
var card = allKnowledgeCards.find(function (c) { return c.id === cardId; });
if (!card) return;
setAIContext("knowledge", card.title);
recordHistory("knowledge", card.id, card.title, card.icon || "📖");
openChat();
quickAsk(card.title + "是什么?能给我详细讲讲吗?");
}
async function toggleFavorite(type, targetId, title, cover) {
var isFav = (userFavorites[type] || {})[targetId];
if (isFav) {
try {
await fetch("/api/favorites", {
method: "DELETE",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ user_id: userId, target_type: type, target_id: targetId })
});
if (userFavorites[type]) delete userFavorites[type][targetId];
showToast("已取消收藏");
} catch (e) { /* ignore */ }
} else {
try {
await fetch("/api/favorites", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ user_id: userId, target_type: type, target_id: targetId, title: title, cover: cover })
});
if (!userFavorites[type]) userFavorites[type] = {};
userFavorites[type][targetId] = true;
showToast("已加入收藏");
} catch (e) { /* ignore */ }
}
if (currentModule === "知识" && type === "knowledge") {
renderShushuoFeed(allKnowledgeCards);
} else if (currentModule === "发现" && type === "video") {
renderVideoList(allVideos);
}
}
async function loadUserFavorites() {
try {
var resp = await fetch("/api/favorites?user_id=" + userId);
var result = await resp.json();
if (result.code === 0) {
userFavorites = { video: {}, knowledge: {} };
(result.data || []).forEach(function (f) {
if (!userFavorites[f.target_type]) userFavorites[f.target_type] = {};
userFavorites[f.target_type][f.target_id] = true;
});
}
} catch (e) { userFavorites = { video: {}, knowledge: {} }; }
}
async function recordHistory(type, targetId, title, cover) {
try {
await fetch("/api/history", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ user_id: userId, target_type: type, target_id: targetId, title: title, cover: cover })
});
} catch (e) { /* ignore */ }
}
function showToast(message) {
var toast = document.getElementById("globalToast");
if (!toast) {
toast = document.createElement("div");
toast.id = "globalToast";
toast.style.cssText = "position:fixed;bottom:100px;left:50%;transform:translateX(-50%);background:rgba(0,0,0,0.78);color:#fff;padding:8px 20px;border-radius:20px;font-size:13px;z-index:9999;pointer-events:none;transition:opacity 0.3s;opacity:0;";
document.body.appendChild(toast);
}
toast.textContent = message;
toast.style.opacity = "1";
clearTimeout(toast._timer);
toast._timer = setTimeout(function () { toast.style.opacity = "0"; }, 1500);
}
async function loadProfileData() {
try {
var [statsResp, profileResp] = await Promise.all([
fetch("/api/user/stats?user_id=" + userId),
fetch("/api/user/profile?user_id=" + userId)
]);
var stats = (await statsResp.json()).data || { watch_count: 0, favorite_count: 0 };
var profile = (await profileResp.json()).data || {};
var statNums = document.querySelectorAll(".profile-stat-num");
if (statNums.length >= 3) {
statNums[0].textContent = stats.watch_count || 0;
statNums[1].textContent = stats.favorite_count || 0;
statNums[2].textContent = "2"; // 剧情存档数保持固定
}
var avatar = document.getElementById("profileAvatar");
if (avatar) {
avatar.textContent = (profile.avatar_emoji || userAvatar);
avatar.style.background = (profile.avatar_color || userAvatarColor);
userAvatar = profile.avatar_emoji || userAvatar;
userAvatarColor = profile.avatar_color || userAvatarColor;
}
var nameEl = document.getElementById("profileName");
if (nameEl) nameEl.textContent = (profile.nickname || "游客用户");
var idEl = document.getElementById("profileId");
if (idEl) idEl.textContent = "ID: " + userId.substring(0, 14);
} catch (e) { /* ignore */ }
}
function showFavorites() {
loadFavoritesList();
}
async function loadFavoritesList() {
showProfileListPanel("我的收藏");
var panel = document.getElementById("profileListContent");
panel.innerHTML = '';
try {
var resp = await fetch("/api/favorites?user_id=" + userId);
var result = await resp.json();
var items = result.data || [];
if (items.length === 0) {
panel.innerHTML = '';
} else {
panel.innerHTML = items.map(function (f) {
return ''
+ '
' + (f.cover || "◎") + '
'
+ '
' + esc(f.title) + '
' + f.target_type + ' · ' + (f.created_at || "").substring(0, 10) + '
'
+ '
✕ '
+ '
';
}).join("");
}
} catch (e) {
panel.innerHTML = '';
}
}
function showHistory() {
loadHistoryList();
}
async function loadHistoryList() {
showProfileListPanel("观看记录");
var panel = document.getElementById("profileListContent");
panel.innerHTML = '';
try {
var resp = await fetch("/api/history?user_id=" + userId + "&limit=30");
var result = await resp.json();
var items = result.data || [];
if (items.length === 0) {
panel.innerHTML = '';
} else {
panel.innerHTML = items.map(function (h) {
return ''
+ '
' + (h.cover || "◎") + '
'
+ '
' + esc(h.title) + '
' + h.target_type + ' · ' + (h.watched_at || "").substring(0, 16) + '
'
+ '
';
}).join("");
}
} catch (e) {
panel.innerHTML = '';
}
}
function showProfileListPanel(title) {
var panel = document.getElementById("profileListPanel");
var titleEl = document.getElementById("profileListTitle");
if (titleEl) titleEl.textContent = title;
if (panel) panel.style.display = "block";
var profilePage = document.getElementById("profilePage");
if (profilePage) profilePage.style.display = "none";
}
function hideProfileListPanel() {
var panel = document.getElementById("profileListPanel");
if (panel) panel.innerHTML = "";
if (panel) panel.style.display = "none";
var profilePage = document.getElementById("profilePage");
if (profilePage) profilePage.style.display = "block";
loadProfileData();
}
async function removeFavorite(type, targetId) {
await toggleFavorite(type, targetId, "", "");
loadFavoritesList();
loadProfileData();
}
function editNickname() {
var name = prompt("输入新昵称:", document.querySelector(".profile-name").textContent);
if (name && name.trim()) {
var trimmed = name.trim().substring(0, 12);
fetch("/api/user/profile", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ user_id: userId, nickname: trimmed })
}).then(function () {
var el = document.querySelector(".profile-name");
if (el) el.textContent = trimmed;
showToast("昵称已更新");
});
}
}
function updateShushuoHero() {
document.getElementById("heroTitle").textContent = "数学不是公式,是你理解世界的方式";
document.getElementById("heroDesc").textContent = "每天一个数学概念,让思维变得更清晰";
var el = document.querySelector(".hero-stats");
if (el) el.innerHTML = "📖 " + allKnowledgeCards.length + "个概念 ⏱ 阅读约" + (allKnowledgeCards.length * 5) + "分钟 ";
}
function adjustColor(hex, amount) {
var num = parseInt(hex.replace("#",""), 16);
var r = Math.min(255, (num >> 16) + amount);
var g = Math.min(255, ((num >> 8) & 0x00FF) + amount);
var b = Math.min(255, (num & 0x0000FF) + amount);
return "#" + ((r << 16) | (g << 8) | b).toString(16).padStart(6, "0");
}
async function loadVideos(module, discipline) {
show("loadingArea");
document.getElementById("videoList").innerHTML = "";
var url = "/api/videos?module=" + encodeURIComponent(module);
if (discipline && discipline !== "all") url += "&discipline=" + encodeURIComponent(discipline);
try {
var resp = await fetch(url);
var result = await resp.json();
hide("loadingArea");
allVideos = result.data || [];
if (allVideos.length > 0) {
renderVideoList(allVideos);
} else {
document.getElementById("videoList").innerHTML = '';
}
} catch (e) {
hide("loadingArea");
document.getElementById("videoList").innerHTML = '';
}
}
function renderVideoList(videos) {
var list = document.getElementById("videoList");
if (!videos || videos.length === 0) { list.innerHTML = ''; return; }
list.style.display = "grid";
list.innerHTML = videos.map(function (v) {
var avatarColor = AVATAR_COLORS[hashStr(v.author || "") % AVATAR_COLORS.length];
var initial = (v.author || "?").replace(/学院|·|同学|系/g, "").slice(0, 1);
var dur = v.duration_min || 5;
var icon = v.discipline_icon && v.discipline_icon !== "·" ? v.discipline_icon : "◎";
var coverBg = "hsl(" + (hashStr(v.discipline || "") % 360) + ", 40%, 90%)";
var isFav = userFavorites.video[v.id] || false;
return ''
+ '
' + icon + '
' + dur + ':00
'
+ '
' + (isFav ? '★' : '☆') + ' '
+ '
'
+ '
' + esc(v.title) + '
'
+ '
' + initial + ' '
+ '
' + esc(v.author) + ' '
+ '
👁️ ' + ((hashStr(v.title) % 1000) + 100) + '
';
}).join("");
}
function openVideo(id, title, desc, url, author, cat) {
currentVideoUrl = url;
currentVideoTitle = title;
document.getElementById("videoModalTitle").textContent = title;
document.getElementById("videoModalDesc").textContent = desc;
document.getElementById("videoModalAuthor").innerHTML = author;
document.getElementById("videoModalCat").innerHTML = cat || "通用";
document.getElementById("videoLinkBtn").style.display = url ? "block" : "none";
document.getElementById("videoPlaceholderText").textContent = url ? "点击下方按钮查看完整课程视频" : "视频链接待补充";
var favBtn = document.getElementById("videoModalFav");
var vid = parseInt(id);
favBtn.textContent = (userFavorites.video[vid]) ? "★ 已收藏" : "☆ 收藏";
favBtn.dataset.vid = vid;
document.getElementById("videoModal").classList.add("show");
setAIContext("video", title);
recordHistory("video", vid, title, cat || "◎");
}
function closeVideo() { document.getElementById("videoModal").classList.remove("show"); }
function openVideoLink() { if (currentVideoUrl) window.open(currentVideoUrl, "_blank"); }
function toggleVideoFav() {
var btn = document.getElementById("videoModalFav");
var vid = parseInt(btn.dataset.vid);
var title = currentVideoTitle;
toggleFavorite("video", vid, title, "◎");
btn.textContent = (userFavorites.video[vid]) ? "★ 已收藏" : "☆ 收藏";
}
async function loadStoryData() {
try {
var resp = await fetch("/api/story");
var result = await resp.json();
if (result.code === 0) storyData = result.data;
} catch (e) { console.error("故事数据加载失败:", e); }
}
function loadVNSave() {
try { var s = localStorage.getItem("mathAppVNSave"); if (s) vnState = JSON.parse(s); } catch (e) {}
}
function saveVNState() {
localStorage.setItem("mathAppVNSave", JSON.stringify(vnState));
alert("进度已存档!");
}
function resetVNSave() {
var startNode = (storyData && storyData.start_node) || "ch1_start";
vnState = { currentNode: startNode, stats: { shuli: 10, xiayi: 10, xinzhi: 10 }, favorability: { "欧阳侠": 0, "Eddie侠": 0, "厚朴侠": 0, "三力思侠": 0, "溜溜侠": 0, "孔锝侠": 0, "星侠": 0, "甲亢侠": 0 } };
localStorage.removeItem("mathAppVNSave");
navigateVN(startNode);
}
function applyVNStats(effects) {
if (!effects) return;
if (effects.shuli) vnState.stats.shuli = (vnState.stats.shuli || 10) + effects.shuli;
if (effects.xiayi) vnState.stats.xiayi = (vnState.stats.xiayi || 10) + effects.xiayi;
if (effects.xinzhi) vnState.stats.xinzhi = (vnState.stats.xinzhi || 10) + effects.xinzhi;
if (effects.intelligence !== undefined) vnState.stats.shuli = (vnState.stats.shuli || 10) + effects.intelligence;
if (effects.charm !== undefined) vnState.stats.xiayi = (vnState.stats.xiayi || 10) + effects.charm;
if (effects.stress !== undefined) vnState.stats.xinzhi = (vnState.stats.xinzhi || 10) - effects.stress;
if (effects.favorability) {
if (!vnState.favorability) vnState.favorability = {};
for (var char in effects.favorability) {
vnState.favorability[char] = (vnState.favorability[char] || 0) + effects.favorability[char];
}
}
}
function startStoryEngine() {
setAIContext("story", "葫芦侠行");
var startNode = (storyData && storyData.start_node) || "ch1_start";
var savedNode = vnState.currentNode;
if (savedNode && storyData && storyData.nodes && storyData.nodes[savedNode]) {
navigateVN(savedNode);
} else {
vnState.currentNode = startNode;
navigateVN(startNode);
}
}
function navigateVN(nodeId) {
if (!storyData || !storyData.nodes || !storyData.nodes[nodeId]) {
document.getElementById("storyContainer").innerHTML = '';
return;
}
var node = storyData.nodes[nodeId];
vnState.currentNode = nodeId;
var container = document.getElementById("storyContainer");
if (!container) return;
container.style.display = "block";
container.scrollIntoView({ behavior: "smooth" });
var ch = detectChapter(nodeId);
var chNames = ["","初入江湖","西部教习","数理论道","脑学初创","报国之路","暗流涌动","陋室修炼","真相之门"];
var chName = chNames[ch] || "";
var html = '';
html += '';
var bgKey = CHAPTER_BG[ch] || "紫金山";
var bgUrl = BG_MAP[bgKey] || "";
if (node.background) bgUrl = BG_MAP[node.background] || node.background || bgUrl;
var spriteUrl = "";
var characterName = node.character || "";
if (node.sprite) {
spriteUrl = node.sprite;
} else if (characterName) {
var names = characterName.split("&");
for (var ni = 0; ni < names.length; ni++) {
var n = names[ni].trim();
if (SPRITE_MAP[n]) { spriteUrl = SPRITE_MAP[n]; break; }
}
}
html += '';
if (bgUrl) html += '
';
if (spriteUrl) html += '
';
html += '
';
if (characterName) html += '
' + esc(characterName) + '
';
html += '
';
html += '
';
html += '
';
container.innerHTML = html;
var sceneText = document.getElementById("sceneText");
var text = node.scene || "";
var i = 0;
function typeNext() {
if (i < text.length) {
sceneText.textContent += text.charAt(i);
i++;
setTimeout(typeNext, 25);
} else {
buildChoiceButtons(node.choices || []);
}
}
sceneText.textContent = "";
typeNext();
}
function buildChoiceButtons(choices) {
var area = document.getElementById("choicesArea");
if (!area) return;
area.innerHTML = "";
choices.forEach(function (c, idx) {
var btn = document.createElement("button");
btn.className = "choice-btn";
btn.style.animationDelay = (idx * 0.1) + "s";
var effectsHTML = "";
if (c.effects) {
var parts = [];
if (c.effects.shuli) parts.push("数理根基" + (c.effects.shuli > 0 ? "+" : "") + c.effects.shuli);
if (c.effects.xiayi) parts.push("侠义声望" + (c.effects.xiayi > 0 ? "+" : "") + c.effects.xiayi);
if (c.effects.xinzhi) parts.push("心志韧性" + (c.effects.xinzhi > 0 ? "+" : "") + c.effects.xinzhi);
if (c.effects.intelligence) parts.push("数理根基" + (c.effects.intelligence > 0 ? "+" : "") + c.effects.intelligence);
if (c.effects.charm) parts.push("侠义声望" + (c.effects.charm > 0 ? "+" : "") + c.effects.charm);
if (c.effects.stress) parts.push("心志韧性" + (c.effects.stress > 0 ? "+" : "") + c.effects.stress);
if (c.effects.favorability) {
for (var char in c.effects.favorability) {
var val = c.effects.favorability[char];
parts.push(char + "好感" + (val > 0 ? "+" : "") + val);
}
}
if (parts.length > 0) effectsHTML = '[' + parts.join(" | ") + '] ';
}
btn.innerHTML = '' + esc(c.text) + ' ' + effectsHTML;
btn.onclick = (function (nextNode, effects) {
return function () {
applyVNStats(effects);
navigateVN(nextNode);
};
})(c.next, c.effects);
area.appendChild(btn);
});
}
function openChat() { document.getElementById("chatOverlay").classList.add("show"); document.getElementById("chatMessages").scrollTop = document.getElementById("chatMessages").scrollHeight; }
function closeChat() { document.getElementById("chatOverlay").classList.remove("show"); }
function quickAsk(q) { document.getElementById("chatInput").value = q; sendMessage(); }
async function sendMessage() {
var input = document.getElementById("chatInput");
var message = input.value.trim();
if (!message) return;
appendChat("user", message);
input.value = "";
var typingId = appendTyping();
try {
var resp = await fetch("/api/chat", {
method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify({ message: message, context: currentContext })
});
var result = await resp.json();
removeTyping(typingId);
appendChat("assistant", result.code === 0 ? result.reply : "抱歉,AI 助教暂时无法回复。");
} catch (e) {
removeTyping(typingId);
appendChat("assistant", "网络异常,请稍后重试。");
}
}
function setAIContext(type, title) {
currentContext = { type: type, title: title || "" };
var hint = document.getElementById("chatContextHint");
if (!hint) {
hint = document.createElement("div");
hint.id = "chatContextHint";
hint.style.cssText = "padding:6px 12px;font-size:11px;color:var(--accent);background:var(--accent-light);text-align:center;";
var chatPanel = document.querySelector(".chat-panel");
var header = document.querySelector(".chat-header");
if (header && chatPanel) chatPanel.insertBefore(hint, header.nextSibling);
}
hint.textContent = currentContext.title ? "📍 当前:" + currentContext.title : "";
hint.style.display = currentContext.title ? "block" : "none";
}
function appendChat(role, text) {
var container = document.getElementById("chatMessages");
var div = document.createElement("div");
div.className = "chat-msg " + role;
div.innerHTML = '' + esc(text) + "
";
container.appendChild(div);
container.scrollTop = container.scrollHeight;
}
function appendTyping() {
var container = document.getElementById("chatMessages");
var div = document.createElement("div");
div.className = "chat-msg assistant typing-msg";
div.innerHTML = '';
container.appendChild(div);
container.scrollTop = container.scrollHeight;
return div;
}
function removeTyping(el) { if (el && el.parentNode) el.remove(); }
function switchLifeTab(tab) {
currentLifeTab = tab;
document.querySelectorAll(".life-sub-btn").forEach(function (b) { b.classList.toggle("active", b.dataset.life === tab); });
if (tab === "simulator") {
hide("storyContainer");
show("simulatorContainer");
setAIContext("simulator", "数学专业模拟器");
startSimulator();
} else {
hide("simulatorContainer");
show("storyContainer");
setAIContext("story", "葫芦侠行");
startStoryEngine();
}
}
async function loadSimulatorData() {
try {
var resp = await fetch("/api/simulator");
var result = await resp.json();
if (result.code === 0) simulatorData = result.data;
} catch (e) { console.error("模拟器数据加载失败:", e); }
}
async function loadSimSave() {
try {
var resp = await fetch("/api/simulator/save?user_id=" + userId);
var result = await resp.json();
if (result.code === 0 && result.data) {
simState = result.data;
}
} catch (e) {}
}
function saveSimState() {
simState.history.push(simState.currentNode);
fetch("/api/simulator/save", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ user_id: userId, state: simState })
}).catch(function () {});
}
function resetSimulator() {
simState = { currentNode: (simulatorData && simulatorData.start_node) || "y1_welcome", stats: { gpa: 10, interest: 10, skill: 10 }, history: [] };
saveSimState();
navigateSimulator(simState.currentNode);
}
async function startSimulator() {
if (!simulatorData) await loadSimulatorData();
await loadSimSave();
if (!simState.currentNode || !simulatorData || !simulatorData.nodes || !simulatorData.nodes[simState.currentNode]) {
resetSimulator();
} else {
navigateSimulator(simState.currentNode);
}
}
function navigateSimulator(nodeId) {
if (!simulatorData || !simulatorData.nodes || !simulatorData.nodes[nodeId]) {
document.getElementById("simulatorContainer").innerHTML = '
剧情加载失败 ';
return;
}
var node = simulatorData.nodes[nodeId];
simState.currentNode = nodeId;
saveSimState();
var container = document.getElementById("simulatorContainer");
if (!container) return;
container.style.display = "block";
if (node.end) {
showEnding(node);
return;
}
container.scrollIntoView({ behavior: "smooth" });
var ch = (node.year || 1);
var stage = node.stage || "";
var html = '';
html += '';
html += '';
if (node.character) html += '
' + esc(node.character) + '
';
html += '
';
html += '
';
html += '
';
container.innerHTML = html;
var textEl = document.getElementById("simText");
var text = node.scene || "";
var i = 0;
function typeNext() {
if (i < text.length) {
textEl.textContent += text.charAt(i);
i++;
setTimeout(typeNext, 20);
} else {
buildSimChoices(node.choices || []);
}
}
textEl.textContent = "";
typeNext();
}
function buildSimChoices(choices) {
var area = document.getElementById("simChoicesArea");
if (!area) return;
area.innerHTML = "";
choices.forEach(function (c, idx) {
var btn = document.createElement("button");
btn.className = "sim-choice-btn";
btn.style.animationDelay = (idx * 0.1) + "s";
var effectsHTML = "";
if (c.effects) {
var parts = [];
if (c.effects.gpa) parts.push((c.effects.gpa > 0 ? "+" : "") + c.effects.gpa + " 绩点");
if (c.effects.interest) parts.push((c.effects.interest > 0 ? "+" : "") + c.effects.interest + " 兴趣");
if (c.effects.skill) parts.push((c.effects.skill > 0 ? "+" : "") + c.effects.skill + " 技能");
if (parts.length > 0) effectsHTML = '[' + parts.join(" | ") + '] ';
}
if (c.hint) effectsHTML += '💡 ' + esc(c.hint) + ' ';
btn.innerHTML = '' + esc(c.text) + ' ' + effectsHTML;
btn.onclick = (function (nextNode, effects) {
return function () {
if (effects) applySimStats(effects);
navigateSimulator(nextNode);
};
})(c.next, c.effects);
area.appendChild(btn);
});
}
function applySimStats(effects) {
if (!effects) return;
if (effects.gpa) simState.stats.gpa = Math.max(0, Math.min(30, (simState.stats.gpa || 10) + effects.gpa));
if (effects.interest) simState.stats.interest = Math.max(0, Math.min(30, (simState.stats.interest || 10) + effects.interest));
if (effects.skill) simState.stats.skill = Math.max(0, Math.min(30, (simState.stats.skill || 10) + effects.skill));
}
function showEnding(node) {
document.getElementById("endingOverlay").classList.add("show");
var badges = { pure_math: "🔮", ai: "🤖", finance: "💰", research: "🔬", startup: "🚀", teacher: "📚", engineer: "⚙️", security: "🔒" };
document.getElementById("endingBadge").textContent = badges[node.ending_type] || "🎓";
document.getElementById("endingType").textContent = node.character || "结局";
document.getElementById("endingScene").textContent = node.scene || "";
document.getElementById("endingTitle").textContent = node.stage || "";
if (node.ending_type) unlockEnding(node.ending_type);
var statsHtml = '';
Object.keys(simState.stats).forEach(function (k) {
statsHtml += '
' + simState.stats[k] + '
' + SIM_STAT_LABELS[k] + '
';
});
statsHtml += '
';
document.getElementById("endingStats").innerHTML = statsHtml;
}
function closeEnding() { document.getElementById("endingOverlay").classList.remove("show"); }
var PICKER_EMOJIS = ["π","∑","∫","∞","Δ","∂","α","β","γ","λ","μ","σ","Ω","∇","⊥","⊕","⊗","≈","±","÷","√","∏","∃","∀","∈","⊂","∪","∩","ℵ","∮"];
function openAvatarPicker() {
document.getElementById("avatarPickerOverlay").classList.add("show");
var grid = document.getElementById("pickerEmojiGrid");
grid.innerHTML = PICKER_EMOJIS.map(function (e) {
var selected = e === userAvatar ? ' style="border-color:var(--accent);background:var(--accent-light)"' : "";
return '' + e + ' ';
}).join("");
var colors = ["#A0B9D8","#B8D0C9","#DAB8B0","#C5CAD0","#B8A99A","#9BB8AE","#ACAEC5","#C4B0B8"];
var colorRow = document.getElementById("pickerColorRow");
colorRow.innerHTML = colors.map(function (c) {
var selected = c === userAvatarColor ? ' style="box-shadow:0 0 0 4px ' + c + '44"' : "";
return ' ';
}).join("");
}
function selectAvatar(emoji) {
userAvatar = emoji;
localStorage.setItem("mathAppAvatar", emoji);
openAvatarPicker(); // refresh grid
updateAvatarUI();
fetch("/api/user/profile", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ user_id: userId, avatar_emoji: emoji })
});
}
function selectAvatarColor(color) {
userAvatarColor = color;
localStorage.setItem("mathAppAvatarColor", color);
openAvatarPicker();
updateAvatarUI();
fetch("/api/user/profile", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ user_id: userId, avatar_color: color })
});
}
function updateAvatarUI() {
var av = document.getElementById("profileAvatar");
if (av) { av.textContent = userAvatar; av.style.background = userAvatarColor; }
var brandIcon = document.querySelector(".brand-icon-sm");
if (brandIcon) { brandIcon.textContent = "葫"; brandIcon.style.background = userAvatarColor; }
}
function closeAvatarPicker(e) {
if (e && e.target !== document.getElementById("avatarPickerOverlay")) return;
document.getElementById("avatarPickerOverlay").classList.remove("show");
}
function esc(text) {
if (!text) return "";
var div = document.createElement("div");
div.textContent = text;
return div.innerHTML;
}
function hashStr(str) {
var hash = 0;
for (var i = 0; i < str.length; i++) { hash = ((hash << 5) - hash) + str.charCodeAt(i); hash |= 0; }
return Math.abs(hash);
}
function renderScriptSquare() {
try {
var heroCards = document.getElementById("ssHeroCards");
if (!heroCards) return;
heroCards.innerHTML = SCRIPTS.map(function (s) {
var statusLabel = s.status === "beta" ? "开发中 · 抢先体验" : "已上线";
return ''
+ '
'
+ '
'
+ '
' + s.icon + '
'
+ '
' + esc(s.name) + '
'
+ '
' + esc(s.desc) + '
'
+ '
' + s.meta + ' ' + statusLabel + '
'
+ '
';
}).join("");
var comingCards = document.getElementById("ssComingCards");
if (comingCards) {
comingCards.innerHTML = COMING_SOON.map(function (c) {
return ''
+ '
' + c.icon + '
'
+ '
'
+ '
' + esc(c.name) + '
'
+ '
' + esc(c.desc) + '
'
+ '
敬请期待 '
+ '
';
}).join("");
}
updateContinueCard();
} catch (e) {
console.error("剧本广场渲染失败:", e);
}
}
function updateContinueCard() {
try {
var continueEl = document.getElementById("ssContinue");
if (!continueEl) return;
var vnSavedNode = vnState.currentNode;
var simSavedNode = simState.currentNode;
if (vnSavedNode && storyData && storyData.nodes && storyData.nodes[vnSavedNode]) {
var ch = detectChapter(vnSavedNode);
var chNames = ["","初入江湖","西部教习","数理论道","脑学初创","报国之路","暗流涌动","陋室修炼","真相之门"];
document.getElementById("ssContinueTitle").textContent = "葫芦侠行 · 第" + ch + "章";
document.getElementById("ssContinueSub").textContent = chNames[ch] || "";
document.getElementById("ssContinueCard").onclick = function () { enterScript("huluxiaxing"); };
continueEl.style.display = "block";
} else if (simSavedNode && simulatorData && simulatorData.nodes && simulatorData.nodes[simSavedNode]) {
var node = simulatorData.nodes[simSavedNode];
document.getElementById("ssContinueTitle").textContent = "数学专业模拟器";
document.getElementById("ssContinueSub").textContent = (node.stage || "") + (node.year ? " · 大" + ["","一","二","三","四"][node.year] : "");
document.getElementById("ssContinueCard").onclick = function () { enterScript("simulator"); };
continueEl.style.display = "block";
} else {
continueEl.style.display = "none";
}
} catch (e) {
console.error("继续卡片更新失败:", e);
}
}
function enterScript(scriptId) {
hide("scriptSquare");
if (scriptId === "huluxiaxing") {
setAIContext("story", "葫芦侠行");
hide("simulatorContainer");
show("lifeSubNav");
show("storyContainer");
var btns = document.querySelectorAll(".life-sub-btn");
btns.forEach(function (b) {
b.classList.toggle("active", b.dataset.life === "story");
});
startStoryEngine();
} else if (scriptId === "simulator") {
setAIContext("simulator", "数学专业模拟器");
hide("storyContainer");
show("lifeSubNav");
show("simulatorContainer");
var btns = document.querySelectorAll(".life-sub-btn");
btns.forEach(function (b) {
b.classList.toggle("active", b.dataset.life === "simulator");
});
startSimulator();
} else if (scriptId === "mathbti") {
window.location.href = "/mathbti";
}
}
function backToSquare() {
hide("lifeSubNav");
hide("storyContainer");
hide("simulatorContainer");
show("scriptSquare");
renderScriptSquare();
document.querySelector(".content-area").scrollTo({ top: 0, behavior: "smooth" });
}
function getSaveKey(scriptId) { return "mathAppSave_" + scriptId; }
function loadAllSaves(scriptId) {
try {
var raw = localStorage.getItem(getSaveKey(scriptId));
return raw ? JSON.parse(raw) : [];
} catch (e) { return []; }
}
function saveAllSaves(scriptId, saves) {
localStorage.setItem(getSaveKey(scriptId), JSON.stringify(saves));
}
function openSavePanel() {
var scriptId = (currentLifeTab === "simulator") ? "simulator" : "huluxiaxing";
document.getElementById("savePanelOverlay").classList.add("show");
renderSaveSlots(scriptId);
}
function closeSavePanel() {
document.getElementById("savePanelOverlay").classList.remove("show");
}
function renderSaveSlots(scriptId) {
var slots = document.getElementById("saveSlots");
var saves = loadAllSaves(scriptId);
var MAX_SLOTS = 5;
var html = "";
for (var i = 0; i < MAX_SLOTS; i++) {
var s = saves[i];
if (s) {
var icon = scriptId === "simulator" ? "🎓" : "📖";
var name = (scriptId === "simulator") ? "数学专业模拟器" : "葫芦侠行";
var meta = s.chapter || "";
var date = s.savedAt ? s.savedAt.substring(0, 16).replace("T", " ") : "";
html += '';
html += '
' + icon + '
';
html += '
';
html += '
' + esc(name) + '
';
html += '
' + esc(meta) + ' ' + date + '
';
html += '
';
html += '
';
html += '💾 覆盖 ';
html += '✕ ';
html += '
';
} else {
html += '';
html += '+ 空槽位 ' + (i + 1) + ' — 点击保存 ';
html += '
';
}
}
slots.innerHTML = html;
}
function saveToSlot(scriptId, slotIndex) {
var saveData = {
savedAt: new Date().toISOString(),
scriptId: scriptId
};
if (scriptId === "huluxiaxing") {
saveData.chapter = detectChapter(vnState.currentNode) ? "第" + detectChapter(vnState.currentNode) + "章" : "";
saveData.nodeId = vnState.currentNode;
saveData.stats = JSON.parse(JSON.stringify(vnState.stats));
saveData.favorability = JSON.parse(JSON.stringify(vnState.favorability || {}));
saveData.vnState = JSON.parse(JSON.stringify(vnState));
} else {
var node = simulatorData && simulatorData.nodes && simulatorData.nodes[simState.currentNode];
saveData.chapter = (node ? (node.stage || "大" + node.year) : "");
saveData.nodeId = simState.currentNode;
saveData.stats = JSON.parse(JSON.stringify(simState.stats));
saveData.simState = JSON.parse(JSON.stringify(simState));
}
var saves = loadAllSaves(scriptId);
saves[slotIndex] = saveData;
saveAllSaves(scriptId, saves);
renderSaveSlots(scriptId);
showToast("存档成功 ✓");
}
function overwriteSlot(scriptId, slotIndex) {
saveToSlot(scriptId, slotIndex);
}
function loadFromSlot(scriptId, slotIndex) {
var saves = loadAllSaves(scriptId);
var saveData = saves[slotIndex];
if (!saveData) return;
if (scriptId === "huluxiaxing") {
if (saveData.vnState) {
vnState = JSON.parse(JSON.stringify(saveData.vnState));
localStorage.setItem("mathAppVNSave", JSON.stringify(vnState));
}
hide("scriptSquare"); hide("simulatorContainer");
show("lifeSubNav"); show("storyContainer");
document.querySelectorAll(".life-sub-btn").forEach(function (b) { b.classList.toggle("active", b.dataset.life === "story"); });
currentLifeTab = "story";
startStoryEngine();
} else {
if (saveData.simState) {
simState = JSON.parse(JSON.stringify(saveData.simState));
saveSimState();
}
hide("scriptSquare"); hide("storyContainer");
show("lifeSubNav"); show("simulatorContainer");
document.querySelectorAll(".life-sub-btn").forEach(function (b) { b.classList.toggle("active", b.dataset.life === "simulator"); });
currentLifeTab = "simulator";
startSimulator();
}
closeSavePanel();
showToast("读档成功 ✓");
}
function deleteSlot(scriptId, slotIndex) {
var saves = loadAllSaves(scriptId);
saves[slotIndex] = null; // 保留空位
saveAllSaves(scriptId, saves);
renderSaveSlots(scriptId);
showToast("已删除存档");
}
function saveVNState() {
var scriptId = "huluxiaxing";
saveToSlot(scriptId, 0);
}
const ALL_ENDINGS = [
{ id: "truth", name: "真相守望", icon: "🔮", desc: "成为葫芦数学世界首位居民与共创者" },
{ id: "master", name: "数理宗师", icon: "📚", desc: "葫芦侠成为一代宗师,数堂发扬光大" },
{ id: "wander", name: "逍遥江湖", icon: "🍃", desc: "远离纷争,在江湖中自由游历" },
{ id: "sail", name: "出海远航", icon: "⛵", desc: "随欧阳侠远赴新大陆,开创东西方交流" },
{ id: "hero", name: "悲情英雄", icon: "⚔️", desc: "坚守信念,孤身面对审查与命运" },
{ id: "fall", name: "堕落之路", icon: "🌑", desc: "在权力与诱惑中迷失自我" },
{ id: "jail", name: "冤狱", icon: "⛓", desc: "被冤入狱,含恨而终" },
{ id: "void", name: "虚无", icon: "🌫", desc: "发现真相后精神崩溃,消失于江湖" },
{ id: "math_phd", name: "数学博士", icon: "🔬", desc: "坚持学术之路,走向研究巅峰" },
{ id: "quant", name: "量化交易员", icon: "💰", desc: "用数学模型在金融市场叱咤风云" },
{ id: "teacher", name: "高中教师", icon: "📝", desc: "回到家乡,培养下一代数学人才" },
{ id: "data_sci", name: "数据科学家", icon: "📊", desc: "在互联网公司用数据改变世界" }
];
function getUnlockedEndings() {
try {
var raw = localStorage.getItem("mathAppUnlockedEndings");
return raw ? JSON.parse(raw) : [];
} catch (e) { return []; }
}
function unlockEnding(endingId) {
var endings = getUnlockedEndings();
if (endings.indexOf(endingId) < 0) {
endings.push(endingId);
localStorage.setItem("mathAppUnlockedEndings", JSON.stringify(endings));
}
}
function openGallery() {
document.getElementById("galleryOverlay").classList.add("show");
renderGallery();
}
function closeGallery() {
document.getElementById("galleryOverlay").classList.remove("show");
}
function renderGallery() {
var unlocked = getUnlockedEndings();
var progressEl = document.getElementById("galleryProgress");
progressEl.textContent = unlocked.length + "/" + ALL_ENDINGS.length;
var grid = document.getElementById("galleryGrid");
grid.innerHTML = ALL_ENDINGS.map(function (e) {
var isUnlocked = unlocked.indexOf(e.id) >= 0;
if (isUnlocked) {
return ''
+ '
' + e.icon + '
'
+ '
' + esc(e.name) + '
'
+ '
' + esc(e.desc) + '
'
+ '
';
} else {
return ''
+ '
🔒
'
+ '
???
'
+ '
未解锁
'
+ '
';
}
}).join("");
}