world: WZ.world && WZ.world.id, tianzi: WZ.tianzi && WZ.tianzi.id, origin: WZ.origin && WZ.origin.id,
linggen: WZ.linggen && WZ.linggen.id, talents: WZ.talents.map(function (t) { return t.id; }),
six: WZ.six, gender: WZ.gender, age: WZ.age, goal: WZ.goal.id
}));
toast('预设已存储到当前浏览器', 'ok');
} catch (e) { toast('预设存储失败:' + e.message, 'err'); }
});
$('#wzLoadPreset').addEventListener('click', function () {
var raw = localStorage.getItem('ddwj21_preset');
if (!raw) { toast('没有已存储的预设', 'err'); return; }
try {
var p = JSON.parse(raw);
WZ.world = D.WORLDS.filter(function (x) { return x.id === p.world; })[0] || null;
WZ.tianzi = D.TIANZI.filter(function (x) { return x.id === p.tianzi; })[0] || null;
WZ.origin = D.ORIGINS.filter(function (x) { return x.id === p.origin; })[0] || null;
WZ.linggen = D.LINGGEN.filter(function (x) { return x.id === p.linggen; })[0] || null;
WZ.talents = (p.talents || []).map(function (id) { return D.TALENTS.filter(function (t) { return t.id === id; })[0]; }).filter(Boolean);
if (p.six) for (var k in p.six) if (k in WZ.six) WZ.six[k] = p.six[k];
if (p.gender) WZ.gender = p.gender;
if (p.age) WZ.age = p.age;
if (p.goal) WZ.goal = D.GOALS.filter(function (g) { return g.id === p.goal; })[0] || WZ.goal;
renderStep(); toast('预设已加载', 'ok');
} catch (e) { toast('预设读取失败', 'err'); }
});
/* ============================================================
SECTION: start 开局
============================================================ */
function startGame() {
/* 自定义角色四维校验 */
if (WZ.mode === 'custom' && WZ.customText) {
var chk = validateCustom(WZ.customText);
if (!chk.ok) { showCustomAdvice(chk); return; }
}
if (wzRemain() < 0) { toast('天道点已超支,无法开档', 'err'); WZ.step = Math.max(0, WZ.step - 1); renderStep(); return; }
var seed = (Date.now() ^ Math.floor(Math.random() * 0x7fffffff)) & 0x7fffffff;
G = new E.Game(seed);
G.poolSeed = WZ.poolSeed;
var kw = WZ.keywords ? WZ.keywords.split(/[\s,,、]+/).filter(Boolean).slice(0, 3) : null;
var custom = null;
if (WZ.mode === 'custom' && WZ.customText) custom = parseCustom(WZ.customText);
/* 半随机候选写入的身份维度强制覆盖 */
if (WZ.mode === 'semi' && WZ.semiPick != null) {
var c = WZ.semiCands[WZ.semiPick];
if (c) {
custom = { birth: c.birth || '人族', status: c.origin.name, tag: c.tag || '无名', plight: '身陷绝境' };
}
}
var engineMode = WZ.mode === 'fullrandom' ? 'random' : (WZ.mode === 'semi' ? 'custom' : WZ.mode);
var carry = Math.max(0, wzRemain());
G.openFile({
mode: engineMode, world: WZ.world, tianzi: WZ.tianzi, origin: WZ.origin, linggen: WZ.linggen,
talents: WZ.talents, flaws: WZ.flaws, six: WZ.six, daohao: WZ.daohao, gender: WZ.gender, age: WZ.age, systemId: WZ.systemId,
goal: WZ.goal, keywords: kw, custom: custom
});
/* 创角期剩余天道点带入游戏 */
G.pc.tianPoints = carry;
/* 半随机候选的因果线注入 */
if (WZ.mode === 'semi' && WZ.semiPick != null && WZ.semiCands[WZ.semiPick]) {
var ck = WZ.semiCands[WZ.semiPick].karmas || [];
ck.forEach(function (k, i) {
if (G.karmas.some(function (x) { return x.name === k.name; })) return;
G.karmas.push({
id: 'semi' + i, name: k.name, level: k.level, power: k.power, strength: k.power,
trigger: k.trigger, endings: k.endings, desc: k.desc,
surface: k.hidden ? G._surfaceMask(k) : null, realRevealed: !k.hidden,
status: 'active', createdTurn: 0, relatedNpc: null
});
});
}
$('#wizard').classList.remove('on');
$('#startScreen').style.display = 'none';
$('#game').classList.add('on');
curRound = 0;
renderOpening();
autoSave();
}
function validateCustom(txt) {
var advice = [];
var ok = true;
/* 1 力量来源 */
if (!/血脉|传承|奇遇|赐|交换|代价|系统|重生|穿越|觉醒|封印|捡|得到|获得/.test(txt)) { advice.push('【力量来源校验】未说明初始力量从何而来(血脉 / 传承 / 奇遇 / 天道赐予 / 代价交换)。'); ok = false; }
/* 2 代价限制 */
if (!/代价|但是|然而|失去|无法|不能|每|折损|诅咒|伤|寿元|残缺|限制|副作用/.test(txt)) { advice.push('【代价限制校验】无代价的力量不被天道允许。请补充道伤、因果、寿元、道心或血脉诅咒等限制。'); ok = false; }
/* 3 软肋 */
if (!/软肋|弱点|怕|惧|执念|在乎|唯一|不能失去|牵挂|亲|师|友|爱/.test(txt)) { advice.push('【软肋校验】无敌的角色不修仙。请补充一个致命弱点:情感、执念、道基、因果或环境依赖。'); ok = false; }
/* 4 成长路径 */
if (!/想|要|目标|为了|追求|最终|希望|证道|复仇|找回|建立|了结|长生/.test(txt)) { advice.push('【成长路径校验】没有方向的修仙者只是流浪的蝼蚁。请补充长期发展方向。'); ok = false; }
/* 过强检测 */
if (/仙帝|无敌|最强|全能|不死不灭|秒杀一切/.test(txt) && !/代价|寿元|记忆全失|追杀|三天|十年/.test(txt)) {
advice.push('【平衡性校验】设定过强且未配套等量代价。若坚持,需附加:记忆全失 / 寿元仅剩三天 / 被天道追杀 之一。');
ok = false;
}
return { ok: ok, advice: advice };
}
function showCustomAdvice(chk) {
modal('四维校验未通过', '
天道驳回了这份设定,但不是拒绝——它给了修改建议:
' +
chk.advice.map(function (a) { return '· ' + esc(a) + '
'; }).join('') +
'修仙的本质就是在脆弱中寻找大道。补上缺口,或者直接改用完全随机开档。
',
[{ label: '返回修改', cls: 'btn-gold' }]);
}
function parseCustom(txt) {
var birth = /妖|兽/.test(txt) ? '妖族'
: /器|剑灵|物灵/.test(txt) ? '器物'
: /诡|鬼|邪/.test(txt) ? '诡异'
: /穿越|现代|都市/.test(txt) ? '跨界者' : '人族';
var status = /宗门|弟子/.test(txt) ? '宗门弟子'
: /世家|族/.test(txt) ? '落魄世家子'
: /商|贾|账/.test(txt) ? '账房'
: /医|丹|药/.test(txt) ? '药师'
: /剑/.test(txt) ? '剑客' : '散修';
var tag = /重生|重来/.test(txt) ? '重生者'
: /穿越/.test(txt) ? '穿越者'
: /系统|面板/.test(txt) ? '系统宿主'
: /诅咒|血/.test(txt) ? '被诅咒之人'
: /封印/.test(txt) ? '被封印之人'
: /失忆|忘/.test(txt) ? '被遗忘之人' : '被遗忘之人';
var plight = /追杀/.test(txt) ? '被追杀'
: /囚|牢/.test(txt) ? '被囚禁'
: /背叛|叛/.test(txt) ? '被背叛'
: /误解|冤/.test(txt) ? '被误解' : '身陷绝境';
return { birth: birth, status: status, tag: tag, plight: plight, text: txt };
}
/* ============================================================
SECTION: opening 开档呈现
============================================================ */
function foldBlock(label, html) {
var d = el('div', 'fold');
var b = el('button', 'fold-head', '' + esc(label) + ' 展开 ');
var body = el('div', 'fold-body local-hidden');
body.innerHTML = html;
b.addEventListener('click', function () {
var opened = !body.classList.toggle('local-hidden');
b.querySelector('.fold-arrow').textContent = opened ? '收起' : '展开';
});
d.appendChild(b); d.appendChild(body);
return d;
}
function renderOpening() {
var o = G.opening, pc = G.pc;
var story = $('#storyWrap'); story.innerHTML = '';
var rnd = el('div', 'round');
rnd.appendChild(headRow('第一章', '混沌种子 #' + G.seed));
var opts = G.makeOptions('opening');
var ch = N.openingChapter(G, opts);
/* 章节头 */
var head = el('div', 'chap-head');
head.innerHTML = '序章 · 开篇
' +
'' + esc(ch.title) + ' ' +
'' + esc(pc.world.name) + ' · ' + esc(o.era.name) + ' · ' + esc(D.REALMS[pc.realmIdx].name) + '
';
rnd.appendChild(head);
/* 三个问题:我是谁 / 我在哪 / 我要做什么 */
var goal = pc.goal ? (pc.goal.name + ' —— ' + pc.goal.desc) : '活下去';
var guide = el('div', 'guide');
guide.innerHTML =
'你是 ' + esc(o.identity.status + ' · ' + o.identity.birth + '之身') + '?
' +
'你在 ' + esc(ch.place) + '
' +
'你要 ' + esc(goal) + '
' +
'你修 ' + esc(D.systemOf(pc).name + ' · ' + D.realmNameOf(pc, pc.realmIdx)) + '
';
rnd.appendChild(guide);
/* 正文 */
var st = el('div', 'story chapter');
st.innerHTML = ch.paras.map(function (p) { return '' + esc(p) + '
'; }).join('') +
'「' + esc(ch.golden) + '」
';
rnd.appendChild(st);
/* 完整属性与时代:默认折叠,先读故事 */
var kv = function (k, v) { return '' + esc(k) + ' ' + v + '
'; };
var flaws = G.activeFlaws ? G.activeFlaws() : [];
var cardHtml = '【人物道碑】
' +
kv('身份', esc(o.identity.status + ' · ' + o.identity.birth + '之身')) +
kv('标签', esc(o.identity.tag + ' | 处境:' + o.identity.plight)) +
kv('境界', esc(D.REALMS[pc.realmIdx].name + (pc.realmSub ? ' ' + (pc.realmSub + 1) + '层' : ''))) +
kv('时代', esc(o.era.name)) +
kv('世界状态', o.states.map(function (s) { return '' + esc(s.name) + ' '; }).join('')) +
kv('反套路', '' + esc(o.identity.antiText || (o.anti && o.anti[0]) || '') + ' ') +
kv('先天代价', o.costs.map(function (c) { return '' + esc(c.txt) + ' '; }).join('')) +
(flaws.length ? kv('命途缺陷', flaws.map(function (f) { return '' + esc(f.name) + ' '; }).join('') +
'这些缺陷会周期性真实发作:血咒逢月圆索取、债务每二十回合催收、宿敌会上门、被夺舍的残识会接管身体。承受它们换来的天道点,可在「天道点」面板中兑换。
') : '') +
kv('开档方式', esc({ random: '完全随机', fullrandom: '全随机', semi: '半随机', keyword: '关键词', custom: '自定义角色' }[WZ.mode] || WZ.mode || '完全随机') +
(WZ.keywords ? ' · 关键词:' + WZ.keywords : '') + ' | 本局种子 #' + WZ.poolSeed) +
kv('带入天道点', '' + (pc.tianPoints || 0) + ' 点 ') +
kv('初始因果', o.karmas.map(function (k) { return '' + esc(k.name) + '·' + esc(k.level) + ' '; }).join('')) +
kv('眼前劫数', '' + esc(o.tribulation.txt) + ' ') +
kv('终极目的', esc(pc.goal.name + ' ' + pc.goal.desc));
var eraHtml = '【世界基底 · ' + esc(o.era.name) + '】
' +
o.era.desc.filter(Boolean).map(function (d) { return '· ' + esc(d) + '
'; }).join('') +
(o.era.conflict ? '· 逻辑自洽校验:' + esc(o.era.conflict) + '
' : '') +
'· ' + esc(pc.world.desc) + '
';
rnd.appendChild(foldBlock('人物道碑 · 完整属性', cardHtml));
rnd.appendChild(foldBlock('世界基底 · ' + o.era.name, eraHtml));
story.appendChild(rnd);
renderChoices(opts);
renderAll();
scrollStory(true);
}
function headRow(num, act) {
var h = el('div', 'round-head');
h.innerHTML = '' + esc(num) + ' ' +
(act ? '' + esc(act) + ' ' : '') + ' ';
return h;
}
/* ============================================================
SECTION: choices 抉择区渲染
============================================================ */
var curOptions = [];
var pickedCombo = {};
/* 此刻栏:每回合都回答"我在哪、在追什么、最要紧的是什么" */
function urgentLine() {
var f = (G.foreshadow || []).filter(function (x) { return !x.resolved && x.dueTurn; })
.sort(function (a, b) { return a.dueTurn - b.dueTurn; })[0];
if (f) {
var label = String(f.text || '');
if (label.length > 26) label = label.slice(0, 26) + '…';
return '【' + label + '】还有 ' + Math.max(0, f.dueTurn - G.turn) + ' 回合到期';
}
var k = (G.karmas || []).filter(function (x) { return x.status === 'active'; })
.sort(function (a, b) { return (b.strength || 0) - (a.strength || 0); })[0];
if (k) return '【' + k.name + '】(' + k.level + ')还没了结';
if (G.opening && G.opening.tribulation && !G.opening.tribulation.resolved) {
var t = String(G.opening.tribulation.txt || '');
return t.length > 52 ? t.slice(0, 52) + '…' : t;
}
return '暂无近忧——这在修仙界本身就是一种奢侈';
}
function sitStrip() {
var pc = G.pc;
var d = el('div', 'sit');
d.innerHTML = '此刻 ' +
'在 ' + esc(pc.lastPlace || '不明之地') + ' ' +
'· ' + esc(pc.goal.name) + ' ' +
'· 灵石 ' + n2s(pc.money) + '
' +
'最要紧 ' + esc(urgentLine()) + '
';
return d;
}
function renderChoices(opts) {
curOptions = opts || [];
pickedCombo = {};
var w = $('#choiceWrap'); w.innerHTML = '';
if (!curOptions.length) {
w.appendChild(el('div', 'empty', '你已经死了。请在右侧选择死亡转化形态。'));
return;
}
w.appendChild(sitStrip());
var head = el('div', 'ch-head');
head.innerHTML = '局势推演与抉择 ' +
'每个选项标注风险 / 收益 / 代价;可输入组合(如 13)同回合并行执行 ' +
'并行执行 ';
w.appendChild(head);
var list = el('div', 'ch-list');
curOptions.forEach(function (o, i) {
var b = el('button', 'choice' + (o.main ? ' main' : ''));
b.setAttribute('data-idx', String(i));
var gains = (o.gain || []).map(function (g) { return '收益·' + esc(g) + ' '; }).join('');
var costs = (o.cost || []).map(function (c) { return '代价·' + esc(c) + ' '; }).join('');
b.innerHTML = '' + (i + 1) + ' ' +
'' + esc(o.text) + ' ' +
'' +
'' + esc(o.tag || (o.main ? '主线抉择' : '支线行动')) + ' ' +
'风险·' + esc(o.risk || '中') + ' ' +
gains + costs +
' ';
if (o.free) {
b.addEventListener('click', function () { toggleFreeInput(); });
} else {
b.addEventListener('click', function () { doChoice(i, null); });
}
list.appendChild(b);
});
w.appendChild(list);
/* 自由输入区(默认隐藏,点击第5项展开) */
var fi = el('div', 'free-input');
fi.id = 'freeBox'; fi.style.display = 'none';
fi.innerHTML = '' +
'天道推演 ';
w.appendChild(fi);
$('#comboGo').addEventListener('click', function () {
var v = $('#comboIn').value.trim();
if (!/^\d{2,4}$/.test(v)) { toast('组合选择请输入 2~4 位数字,如 13 或 134', 'err'); return; }
var combo = G.parseCombo(v, curOptions);
if (!combo) { toast('无法解析该组合', 'err'); return; }
if (!combo.ok) { toast(combo.msg, 'err'); return; }
doCombo(combo.chosen);
});
$('#freeGo').addEventListener('click', function () {
var t = $('#freeTxt').value.trim();
if (!t) { toast('请先写下你要做的事', 'err'); return; }
var freeOpt = curOptions.filter(function (o) { return o.free; })[0];
doChoice(curOptions.indexOf(freeOpt), t);
});
$('#freeTxt').addEventListener('keydown', function (e) {
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) { $('#freeGo').click(); }
});
}
function toggleFreeInput() {
var b = $('#freeBox');
if (!b) return;
b.style.display = b.style.display === 'none' ? 'flex' : 'none';
if (b.style.display === 'flex') $('#freeTxt').focus();
}
/* 组合并行执行:按时序编排,资源不足则提示 */
function doCombo(chosen) {
if (G.over) { toast('你已经死了', 'err'); return; }
var pc = G.pc;
/* 时序编排:先低风险后高风险 */
var order = { cultivate: 0, mind: 1, art: 2, trade: 3, meet: 4, investigate: 5, explore: 6, karma: 7, war: 8, fight: 9, taboo: 10 };
chosen.sort(function (a, b) { return (order[a.act] || 5) - (order[b.act] || 5); });
var res = null;
var comboTxt = '【并行选择编排】本回合执行 ' + chosen.length + ' 项行动,按时序排列:' +
chosen.map(function (c, i) { return (i + 1) + '.' + esc(c.text.slice(0, 18)) + '…'; }).join(' → ') + '
';
/* 逐个执行,但只渲染最后一个回合的完整叙事 */
var merged = null;
chosen.forEach(function (c, i) {
if (pc.energy < 20) { toast('体力不支,第 ' + (i + 1) + ' 项行动无法执行', 'err'); return; }
var r = G.doAction(c, null);
if (!merged) merged = r;
else {
/* 合并结算 */
r.parts.forEach(function (p) { if (p.cls !== 'chronicle') merged.parts.push(p); });
var ms = merged.settlement, rs = r.settlement;
['karma', 'npc', 'world', 'injury', 'bp', 'arts'].forEach(function (k) { ms[k] = ms[k].concat(rs[k]); });
ms.daoyun += rs.daoyun;
merged.ticks = merged.ticks.concat(r.ticks);
merged.chronicle = r.chronicle;
merged.options = r.options;
merged.over = r.over; merged.deathForms = r.deathForms;
}
});
if (!merged) return;
appendRound(merged, comboTxt, '并行·' + chosen.length + '项');
renderAll();
}
function doChoice(idx, freeText) {
if (G.over) { toast('你已经死了,先选择死亡转化形态', 'err'); return; }
var o = curOptions[idx];
if (!o) return;
if (G.pc.energy < 15 && o.act !== 'mind') {
toast('精力耗尽(' + Math.round(G.pc.energy) + '/' + G.pc.energyMax + ')。可选择静坐修心恢复,或用「岁月如梭」跳过一段时间。', 'err');
return;
}
var res = G.doAction(o, freeText);
appendRound(res, null, freeText ? '自由行动:' + freeText.slice(0, 24) : (o.text.slice(0, 26) + (o.text.length > 26 ? '…' : '')));
maybeNarrate(o, res);
renderAll();
autoSave();
}
/* ============================================================
SECTION: ai 叙事(LLM 只重写事实,不参与判定)
============================================================ */
function aiSettleText(s) {
if (!s) return '无';
var bits = [];
if (s.karma && s.karma.length) bits.push('因果:' + s.karma.join(';'));
if (s.npc && s.npc.length) bits.push('人物:' + s.npc.join(';'));
if (s.world && s.world.length) bits.push('世界:' + s.world.join(';'));
if (s.injury && s.injury.length) bits.push('道伤:' + s.injury.join(';'));
if (s.daoyun) bits.push('道蕴 ' + (s.daoyun > 0 ? '+' : '') + s.daoyun);
if (s.bp && s.bp.length) bits.push('塑道:' + s.bp.join(';'));
return bits.join('|') || '无';
}
function aiKarmaText() {
var act = (G.karmas || []).filter(function (k) { return k.status === 'active'; }).slice(0, 4);
if (!act.length) return '无';
return act.map(function (k) { return '[' + k.level + ']' + k.name; }).join('、');
}
function aiNpcText(opt, res) {
var names = [];
if (opt && opt.npc) {
var n = G.npcs.filter(function (x) { return x.id === opt.npc; })[0];
if (n) names.push(n.fullName + '(' + n.race + ',' + D.REALMS[Math.min(D.REALMS.length - 1, n.realmIdx || 0)].name + ',' + (n.person || '') + ')');
}
return names.join('、') || '无';
}
function buildAIContext(opt, res, storyEl) {
var pc = G.pc;
var facts = storyEl ? storyEl.innerText.replace(/\s+/g, ' ').trim() : '';
return {
world: pc.world.name, worldTag: pc.world.tag, era: (G.opening && G.opening.era && G.opening.era.name) || '',
realm: D.REALMS[pc.realmIdx].name, age: Math.round(pc.age), life: Math.round(pc.lifeMax),
place: pc.lastPlace || '不知何处',
action: (opt && opt.text) || '静观其变',
facts: facts.slice(0, 900),
settle: aiSettleText(res && res.settlement),
karma: aiKarmaText(),
npc: aiNpcText(opt, res),
stage: D.TRIBULATION[G.stage].name,
state: '气血 ' + Math.round(pc.hp) + '/' + pc.hpMax + ',道心污染 ' + (D.POLLUTION[pc.pollution] || D.POLLUTION[0]).name +
',天道监察 ' + (D.MONITOR[pc.monitor] || D.MONITOR[0]).lv + ',灵石 ' + Math.round(pc.money)
};
}
function maybeNarrate(opt, res) {
if (!window.LLM || !LLM.available()) return;
var c = LLM.cfg();
var major = !!(opt && opt.main) || !!(res && res.over) ||
((res && res.parts) || []).some(function (p) {
return p.cls === 'break' || p.cls === 'fail' || p.cls === 'battle' || p.cls === 'stage' || p.cls === 'monitor';
}) || !!(res && res.stageUp && res.stageUp.length) || !!(res && res.bigFired && res.bigFired.length);
if (c.freq === 'major' && !major) return;
var rounds = document.querySelectorAll('#storyWrap .round');
var last = rounds[rounds.length - 1];
if (!last) return;
var story = last.querySelector('.story');
if (!story) return;
var wrap = el('div', 'ai-wrap');
wrap.innerHTML = 'AI 叙事 推演中…
';
last.insertBefore(wrap, story.nextSibling);
story.classList.add('local-hidden');
var body = wrap.querySelector('.ai-body');
var status = wrap.querySelector('.ai-status');
var t0 = Date.now();
var ctx = buildAIContext(opt, res, story);
LLM.narrate(ctx).then(function (r) {
if (!r.ok) {
wrap.remove();
story.classList.remove('local-hidden');
toast('AI 叙事失败:' + (r.error || '未知错误') + '(已回退本地推演)', 'err');
return;
}
body.innerHTML = r.text.split(/\n+/).filter(function (p) { return p.trim(); })
.map(function (p) { return '' + esc(p.trim()) + '
'; }).join('');
var tok = r.usage ? ' ' + ((r.usage.total_tokens || r.usage.completion_tokens || 0)) + ' tokens' : '';
status.textContent = '已生成 · ' + (r.model || '') + ' · ' + ((Date.now() - t0) / 1000).toFixed(1) + 's' + tok;
var tg = el('button', 'ai-toggle', '查看本地推演');
tg.addEventListener('click', function () {
story.classList.toggle('local-hidden');
tg.textContent = story.classList.contains('local-hidden') ? '查看本地推演' : '收起本地推演';
});
wrap.querySelector('.ai-head').appendChild(tg);
G.aiLog = G.aiLog || [];
G.aiLog.push({ turn: G.turn, text: r.text, model: r.model });
if (G.aiLog.length > 200) G.aiLog.shift();
});
}
function appendRound(res, extraHtml, actLabel) {
curRound++;
var story = $('#storyWrap');
var rnd = el('div', 'round');
rnd.appendChild(headRow('第 ' + G.turn + ' 回合', actLabel));
if (extraHtml) rnd.appendChild(el('div', '', extraHtml));
res.parts.forEach(function (p) {
rnd.appendChild(el('div', p.cls === 'chronicle' ? 'story' : 'sysbox box-' + p.cls, p.html));
});
/* 因果结算表 */
if (res.settlement) rnd.appendChild(settleTable(res.settlement));
story.appendChild(rnd);
/* 长局性能:手机只保留最近 26 回合的 DOM,更早的折叠(数据仍完整保存在 G 中) */
var rounds = story.querySelectorAll('.round');
if (rounds.length > 26) {
var drop = rounds.length - 26;
for (var i = 0; i < drop; i++) { rounds[i].remove(); }
foldedRounds += drop;
}
renderFoldNote();
renderChoices(res.options);
if (res.over) showDeath(res.deathForms);
scrollStory();
}
function renderFoldNote() {
var w = $('#storyWrap');
var n = w.querySelector('.fold-note');
if (!foldedRounds) { if (n) n.remove(); return; }
if (!n) { n = el('div', 'fold-note'); w.insertBefore(n, w.firstChild); }
n.innerHTML = '更早的 ' + foldedRounds + ' 回合已折叠,以保持长局流畅。完整记录在「结算」与「存档」里。';
}
function settleTable(s) {
var rows = [];
if (s.karma.length) rows.push(['因果网变化', s.karma.map(esc).join(' ')]);
if (s.npc.length) rows.push(['NPC关系变化', s.npc.map(esc).join(' ')]);
if (s.world.length) rows.push(['世界状态变化', s.world.map(esc).join(' ')]);
if (s.daoyun) rows.push(['道蕴消耗', (s.daoyun > 0 ? '+' : '') + s.daoyun]);
if (s.injury.length) rows.push(['道伤变化', s.injury.map(esc).join(' ')]);
if (s.bp.length) rows.push(['行为塑道', s.bp.map(esc).join(' ')]);
if (s.arts.length) rows.push(['百艺进度', s.arts.map(esc).join(' ')]);
if (!rows.length) return el('div');
var d = el('div', 'sysbox');
d.innerHTML = '【因果结算表】
' +
'' + rows.map(function (r) { return '' + r[0] + ' ' + r[1] + ' '; }).join('') + '
';
return d;
}
function scrollStory(top) {
var w = $('#storyWrap');
if (window.innerWidth <= 980) {
var last = w.querySelector('.round:last-child');
var hdr = document.querySelector('.topbar');
var hh = hdr ? hdr.getBoundingClientRect().height : 80;
var y = (top || !last) ? 0 : Math.max(0, last.getBoundingClientRect().top + window.scrollY - hh - 26);
window.scrollTo({ top: y, behavior: top ? 'auto' : 'smooth' });
} else {
if (top) { w.scrollTop = 0; }
else {
var last2 = w.querySelector('.round:last-child');
w.scrollTop = last2 ? last2.offsetTop - 8 : w.scrollHeight;
}
}
}
/* ============================================================
SECTION: left 人物道碑
============================================================ */
function renderLeft() {
var pc = G.pc, p = $('#leftPanel');
var realm = D.REALMS[pc.realmIdx];
var next = D.REALMS[Math.min(pc.realmIdx + 1, D.REALMS.length - 1)];
var ls = G.lifespanStage(pc);
var pol = D.POLLUTION[pc.pollution] || D.POLLUTION[0];
var mon = D.MONITOR[pc.monitor] || D.MONITOR[0];
var injHtml = D.INJURY_TYPES.map(function (t) {
var lv = pc.injury[t] || 0;
return '' + esc(t) + '·' + esc(D.INJURY_LEVELS[lv]) + ' ';
}).join('');
/* 主道途 / 副道途 / 歧路 */
var paths = Object.keys(pc.bp).map(function (k) { return { k: k, v: pc.bp[k], st: pc.bpStage[k] || 0 }; }).sort(function (a, b) { return b.v - a.v; });
var mainP = paths[0], subP = paths[1], qiP = paths.filter(function (x) { return x.st === 0 && x.v > 0; }).slice(0, 2);
var kv = function (k, v) { return '' + esc(k) + ' ' + v + '
'; };
var pct = function (a, b) { return b > 0 && isFinite(b) ? Math.min(100, a / b * 100) : 0; };
var bioHtml = '';
if (pc.bio) {
bioHtml = '' +
'
我是谁
' +
'
' + esc(pc.status) + ' · ' + esc(pc.race) + '之身 ? ' +
(pc.bio.originName ? '
来路:' + esc(pc.bio.originName) + '。' + esc(pc.bio.originDesc || '') + '
' : '') +
'
' + esc(pc.bio.past || '') + '
' +
'
为何在此:' + esc(pc.bio.why || '') + '
' +
'
执念:' + esc(pc.goal.name) + '——' + esc(pc.goal.desc) + '
' +
(pc.bio.secret ? '
未说出口:' + esc(pc.bio.secret) + '
' : '') +
'
';
}
p.innerHTML =
'' +
'
' + esc(pc.name) + (pc.daoTitle ? ' 「' + esc(pc.daoTitle) + '」 ' : '') + ' ' +
'
' + esc(pc.status + '(' + pc.race + ') ' + pc.gender + ' ' + pc.age + ' 岁') + ' ?
' + bioHtml +
'
' + esc(realm.name) + ' ' +
(realm.sub ? '' + (pc.realmSub + 1) + '/' + realm.sub + ' 层 ' : '') +
'寿元 ' + pc.age + '/' + (isFinite(pc.lifeMax) ? pc.lifeMax : '∞') + ' ' + esc(ls.name) + '
' +
'
道蕴积累 ' + n2s(pc.qi) + ' / ' + n2s(pc.qiNeed) + '
' +
'
' +
'
气血 ' + Math.round(pc.hp) + ' / ' + Math.round(pc.hpMax) + '
' +
'
' +
'
精力 ' + Math.round(pc.energy) + ' / ' + pc.energyMax + '
' +
'
' +
'
道蕴储备 ' + n2s(pc.daoyun) + '
' +
'
' +
'
核心状态 ' +
kv('战力', '' + n2s(pc.combat) + ' (同境界上限 ' + n2s(realm.base * 1.15) + ') ') +
kv('灵石', n2s(pc.money)) +
kv('本体状态', esc(pc.hp / pc.hpMax > 0.7 ? '健康' : pc.hp / pc.hpMax > 0.35 ? '受伤' : pc.hp > 0 ? '重伤' : '濒死')) +
kv('寿元阶段', esc(ls.name + ' ' + ls.fx.desc)) +
'' +
'
六司(先天资质 · 后天可增减) ' +
D.SIX.map(function (s) {
var v = pc.six[s.key] || 0;
var base = (pc.sixBase && pc.sixBase[s.key]) != null ? pc.sixBase[s.key] : v;
var per = (D.SIX_PER_POINT && D.SIX_PER_POINT[s.key]) || 0.02;
var pct = Math.round(v * per * 100);
var mark = v > base ? '
↑ ' : (v < base ? '
↓ ' : '');
return '
' +
'' + esc(s.key) + ' ' +
'' + v + mark + ' +' + pct + '%
';
}).join('') +
'
' +
'
道途(主 / 副 / 歧路) ' +
kv('主道途', mainP ? esc(mainP.k) + ' ' + esc(D.BP_STAGE_NAMES[Math.max(0, mainP.st - 1)] || '道蕴微光') + ' ' + mainP.v.toFixed(1) + ' BP' : '尚未成形 ') +
kv('副道途', subP ? esc(subP.k) + ' ' + subP.v.toFixed(1) + ' BP' : '无 ') +
kv('歧路', qiP.length ? qiP.map(function (x) { return '' + esc(x.k) + ' '; }).join('') : '无 ') +
(pc.fusionDao ? kv('融合道途', '' + esc(pc.fusionDao) + ' ') : '') +
(pc.conflictPct ? kv('道心冲突', '' + pc.conflictPct + '% ') : '') +
'' +
'
道伤分级 ' + kv('', injHtml) + '' +
'
道心污染 ' +
kv('阶段', '
' + esc(pol.name) + ' ') +
'
' + esc(pol.desc) + '
' +
(pc.pollution >= 4 ? '
即将转化,形态候选:' + D.POLLUTION_FORMS.join(' / ') + '
' : '') +
'
' +
'
天道监察 / 代价 ' +
kv('监察等级', '' + esc(mon.lv + ' ' + mon.name) + ' ') +
kv('成功率惩罚', '-' + Math.round(mon.pen * 100) + '% ') +
kv('代价状态', '' + esc(D.COST_LEVELS[pc.costLevel].name) + ' ') +
kv('天道点', '' + (pc.tianPoints || 0) + ' 点 ') +
'' +
(function () {
var flaws = G.activeFlaws ? G.activeFlaws() : [];
if (!flaws.length) return '';
return '
命途缺陷(' + flaws.length + ') ' +
'
' + flaws.map(function (f) { return '' + esc(f.name) + ' '; }).join('') + '
' +
'
这些缺陷会周期性真实发作。详情见「天道点」面板。
';
})() +
(G.myForce ? '
自建势力 ' +
kv('名号', '' + esc(G.myForce.name) + ' ') +
kv('驻地', esc(G.myForce.base)) +
kv('成员', G.myForce.members.length + ' 人') +
kv('规矩', G.myForce.rules.map(esc).join('、') || '未立') +
'' : '') +
'
因果羁绊 ' +
(G.karmas.filter(function (k) { return k.status === 'active'; }).slice(0, 5).map(function (k) {
return '
' +
'' + esc(k.level) + ' ' +
'' + esc(k.name) + ' 强度' + k.strength + '
';
}).join('') || '
暂无活跃因果
') +
'
' +
'
眼前劫数 ' +
'
' + esc(G.opening.tribulation.txt) + '
' +
(function () {
var due = G.foreshadow.filter(function (f) { return !f.resolved && f.dueTurn; }).sort(function (a, b) { return a.dueTurn - b.dueTurn; })[0];
return due ? '
最近期限:' + Math.max(0, due.dueTurn - G.turn) + ' 回合后 · ' + esc(due.text.slice(0, 22)) + '
' : '';
})() +
'
' +
'
设定 ' +
kv('世界', esc(pc.world.name)) +
kv('时代', esc(G.opening.era.name)) +
kv('天资 / 灵根', esc(pc.tianzi.name + ' / ' + pc.linggen.name)) +
kv('终极目的', esc(pc.goal.name)) +
kv('声誉', esc(G.reputationWord())) +
(pc.gongfa ? kv('功法', esc(pc.gongfa)) : '') +
(G.daoLineage ? kv('道统', esc(G.daoLineage.name)) : '') +
'' +
'
随身之物(' + pc.items.length + ') ' +
(pc.items.length ? pc.items.slice(0, 12).map(function (it) {
return '
' + esc(it.n) + (it.qty ? ' ×' + it.qty : '') + ' Lv' + (it.lv || 1) + '
';
}).join('') : '
身无长物
') +
'
' +
'
';
}
/* ============================================================
SECTION: topbar
============================================================ */
function renderTop() {
var pc = G.pc, trib = D.TRIBULATION[G.stage];
var nextStage = D.TRIBULATION[Math.min(D.TRIBULATION.length - 1, G.stage + 1)];
var chips = [];
chips.push('回合 ' + G.turn + ' ');
chips.push('所在 ' + esc(D.planeOf(pc).name) + ' ');
chips.push('大劫 ' + esc(trib.name) + ' (' + Math.max(0, nextStage.turn - G.turn) + ' 回合后推进) ');
chips.push('气血 ' + Math.round(pc.hp) + ' ');
chips.push('灵石 ' + n2s(pc.money) + ' ');
chips.push('道蕴 ' + n2s(pc.daoyun) + ' ');
chips.push('道心 ' + esc((D.POLLUTION[pc.pollution] || D.POLLUTION[0]).name) + ' ');
chips.push('监察 ' + esc((D.MONITOR[pc.monitor] || D.MONITOR[0]).lv) + ' ');
chips.push('精力 ' + Math.round(pc.energy) + ' ');
chips.push('天道点 ' + (pc.tianPoints || 0) + ' ');
var flawN = G.activeFlaws ? G.activeFlaws().length : 0;
if (flawN) chips.push('命途缺陷 ' + flawN + ' ');
if (window.LLM && LLM.available()) chips.push('AI 叙事 ' + esc(LLM.cfg().model) + ' ');
var unres = G.foreshadow.filter(function (f) { return !f.resolved; }).length;
chips.push('伏笔 ' + unres + ' ');
chips.push('存活NPC ' + G.npcs.filter(function (n) { return n.alive; }).length + ' ');
$('#tbChips').innerHTML = chips.join('');
}
/* ============================================================
SECTION: tabs 右侧标签页
============================================================ */
var TABS = [
{ id: 'rumor', name: '风闻' },
{ id: 'karma', name: '因果' },
{ id: 'tian', name: '天道点' },
{ id: 'settle', name: '结算' },
{ id: 'npc', name: '人物' },
{ id: 'realm', name: '境界' },
{ id: 'world', name: '世界' },
{ id: 'lore', name: '天地志' },
{ id: 'art', name: '百艺' },
{ id: 'market', name: '坊市' },
{ id: 'stele', name: '道碑' },
{ id: 'save', name: '存档' }
];
function renderTabs() {
var t = $('#tabs'); t.innerHTML = '';
var unread = G.rumors.filter(function (r) { return !r.read; }).length;
TABS.forEach(function (tb) {
var b = el('button', 'tab' + (curTab === tb.id ? ' on' : ''), esc(tb.name) + (tb.id === 'rumor' && unread ? ' ' : ''));
b.addEventListener('click', function () { curTab = tb.id; renderTabs(); renderTabBody(); });
t.appendChild(b);
});
}
function renderTabBody() {
var b = $('#tabBody'); b.innerHTML = '';
if (curTab === 'rumor') tabRumor(b);
else if (curTab === 'karma') tabKarma(b);
else if (curTab === 'tian') tabTian(b);
else if (curTab === 'settle') tabSettle(b);
else if (curTab === 'npc') tabNpc(b);
else if (curTab === 'realm') tabRealm(b);
else if (curTab === 'world') tabWorld(b);
else if (curTab === 'lore') tabLore(b);
else if (curTab === 'art') tabArt(b);
else if (curTab === 'market') tabMarket(b);
else if (curTab === 'stele') tabStele(b);
else if (curTab === 'save') tabSave(b);
}
/* --- 修仙界风闻 --- */
function tabRumor(b) {
b.appendChild(secHead('修仙界风闻(可信度 + 来源标注)'));
if (!G.rumors.length) { b.appendChild(el('div', 'empty', '暂无风闻。每十回合坊市与密探会带来新的消息。')); return; }
G.rumors.slice(0, 24).forEach(function (r) {
var d = el('div', 'rumor' + (r.big ? ' big' : '') + (r.read ? '' : ' unread'));
d.innerHTML = '' + esc(r.kind) + ' ' +
'' + esc(r.trust) + ' ' +
'第 ' + r.turn + ' 回合
' +
'' + esc(r.text) + '
' +
'来源:' + esc(r.source) + ' · 可信度 ' + r.pct + '% · ' + esc(r.hint) + '
';
var acts = el('div', 'ru-acts');
if (!r.revealed) {
[['调查真相', '消耗时间与资源核实(可信度越高越准)'], ['提前布局', '前往涉及地点做准备,可能得先机也可能落入陷阱'],
['主动介入', '直接前往介入,高风险高回报,可能改变事件走向'], ['无视', '继续自己的计划,可能错过机缘也可能避开灾祸']].forEach(function (a) {
var btn = el('button', 'btn btn-sm btn-ghost', a[0]);
btn.title = a[1];
btn.addEventListener('click', function () { respondRumor(r, a[0]); });
acts.appendChild(btn);
});
d.appendChild(acts);
} else {
d.appendChild(el('div', 'ru-truth', '你的应对:' + esc(r.response || '—') + (r.truthKnown != null ? ' | 核实结果:' + (r.real ? '属实' : '不实') : '')));
}
r.read = true;
b.appendChild(d);
});
}
function respondRumor(r, kind) {
if (r.revealed) return;
r.revealed = true; r.response = kind;
var pc = G.pc, out = [];
if (kind === '调查真相') {
pc.energy = Math.max(0, pc.energy - 20);
pc.money = Math.max(0, pc.money - 50);
r.truthKnown = true;
out.push('你花了时间和五十灵石去核实。' + (r.real ? '这条消息属实,细节大体对得上。' : '这条消息不实——有人故意放出来的,或者传话的人自己也没搞清楚。'));
if (!r.real && r.big) out.push('更重要的是:放出这条消息的人,希望你去做某件事。');
G.addBp('谋', 0.8, '调查风闻');
} else if (kind === '提前布局') {
pc.energy = Math.max(0, pc.energy - 25);
if (r.real) { out.push('你提前去了。因为早到,你占了一个别人抢不走的位置。'); pc.daoyun += 25; pc.money += Math.round(100 + Math.random() * 300); out.push('道蕴 +25,另有资源进账。'); }
else { out.push('你提前去了,但那里什么都没有。你为一个假消息花了三天,这三天里别的事在发生。'); pc.age += 0; }
} else if (kind === '主动介入') {
pc.energy = Math.max(0, pc.energy - 40);
var d = G.dice(0.45, { attr: '气运' });
if (d.result === 'great' || d.result === 'ok') {
out.push('你介入了,而且改变了走向。' + (r.real ? '这件事因为你的介入,结局和所有人预料的都不一样。' : '这件事本来就不该发生,你的介入让它彻底散了。'));
pc.daoyun += 60; pc.reputation = E.clamp(pc.reputation + 10, -100, 100);
G.addBp('谋', 3.0, '介入大事件');
out.push('道蕴 +60,声誉 +10,谋道 BP +3.0(重大抉择级)。');
} else {
out.push('你介入了,但被卷进去了。' + (d.result === 'greatfail' ? '事情比你以为的大得多,而你站在最中间。' : '你没能改变什么,只是让自己成了这件事的一部分。'));
pc.hp = Math.max(1, pc.hp - Math.round(pc.hpMax * (d.result === 'greatfail' ? 0.4 : 0.18)));
G.addInjury('肉身', Math.min(5, pc.injury['肉身'] + 1), '介入风波');
G.addCost(2, '介入风波');
}
} else {
out.push('你选择不理会。' + (r.real ? '这件事后来真的发生了,你不在场。' : '这件事后来被证明是假的,你的判断对了——虽然你当时并不确定。'));
pc.energy = Math.min(pc.energyMax, pc.energy + 5);
}
modal('风闻应对·' + kind, out.map(function (t) { return '' + esc(t) + '
'; }).join('') +
(r.big ? '注:这是一条大事件预告。无论你是否介入,它都会在到期时触发,并插入世界级叙事,所有相关人物线同步推进。
' : ''),
[{ label: '知道了', cls: 'btn-gold' }]);
renderAll(); autoSave();
}
/* --- 因果网 --- */
function tabKarma(b) {
var act = G.karmas.filter(function (k) { return k.status === 'active'; });
var res = G.karmas.filter(function (k) { return k.status !== 'active'; });
b.appendChild(secHead('活跃因果线(' + act.length + ')'));
if (!act.length) b.appendChild(el('div', 'empty', '身上没有活跃的因果线。这很少见,也不一定是好事。'));
act.forEach(function (k) {
var npc = k.relatedNpc ? G.npc(k.relatedNpc) : null;
var d = el('div', 'karma');
d.innerHTML = '' + esc(k.name) + ' ' +
'' + esc(k.level) + ' ' +
(k.unbreakable ? '不可转化 ' : '') +
(k.transformed ? '已' + esc(k.transformed) + ' ' : '') +
'强度 ' + k.strength + '
' +
'' + esc(k.realRevealed ? (k.desc || '') : ('【表象】' + (k.surface || '你只看得见这一头。'))) + '
' +
'触发:' + esc(k.trigger || '—') + ' | 可能结局:' + esc((k.endings || []).join(' / ') || '未知') +
(npc ? ' | 关联:' + esc(npc.fullName) + (npc.alive ? '' : '(已故)') : '') +
(k.dueTurn ? ' | 期限:第 ' + k.dueTurn + ' 回合' : '') + (k.latent ? ' | 业障潜伏中 ' : '') + '
';
var acts = el('div', 'ka-acts');
if (k.level !== '天道级' && !k.unbreakable) {
['赎罪', '补偿', '传承', '牺牲', '斩断'].forEach(function (w) {
var btn = el('button', 'btn btn-sm btn-ghost', w);
btn.addEventListener('click', function () {
var r = G.transformKarma(k.id, w);
if (!r) return;
modal('因果转化·' + w, '' + esc(r.msg) + '
铁律:已发生之因果不可撤销,只能被转化、被承受,或被更高级别的因果覆盖。时间之河不可逆流。
', [{ label: '承受', cls: 'btn-gold' }]);
renderAll(); autoSave();
});
acts.appendChild(btn);
});
} else {
acts.appendChild(el('span', 'ka-meta', k.level === '天道级' ? '天道级因果不可转化,只能承受。' : '这条线是你在绝境里自己绑上的,它不给解。'));
}
d.appendChild(acts);
b.appendChild(d);
});
if (res.length) {
b.appendChild(secHead('已了结 / 归档(' + res.length + ')'));
res.slice(0, 12).forEach(function (k) {
b.appendChild(el('div', 'karma', '' + esc(k.name) + ' ' + esc(k.status === 'resolved' ? '已了结' : '已归档') + '
'));
});
}
/* 未解伏笔 */
var fs = G.foreshadow.filter(function (f) { return !f.resolved; });
b.appendChild(secHead('未解伏笔(' + fs.length + ')· 按优先级排序'));
if (!fs.length) b.appendChild(el('div', 'empty', '没有未解伏笔。'));
var order = { '天道级': 0, '因果级': 1, '个人级': 2, '日常级': 3, '背景设定': 4 };
fs.sort(function (a, c) { return (order[a.level] == null ? 9 : order[a.level]) - (order[c.level] == null ? 9 : order[c.level]); })
.slice(0, 20).forEach(function (f) {
b.appendChild(el('div', 'karma', '' + esc(f.text) + ' ' +
'' + esc(f.level) + ' ' +
'' + (f.dueTurn ? '剩 ' + Math.max(0, f.dueTurn - G.turn) + ' 回合' : '无期限') + '
' +
'触发条件:' + esc(f.trigger || '未知') + ' | ' + (f.level === '天道级' || f.level === '因果级' ? '永不压缩,仅摘要保留' : f.level === '个人级' ? '超 50 回合未触发将强制触发或废弃' : '超 30 回合未触发降级为背景设定') + '
'));
});
}
/* --- 天道点:残酷经济(越惨越有筹码) --- */
function tabTian(b) {
var pc = G.pc;
var banner = el('div', 'tp-banner');
banner.innerHTML = '' + (pc.tianPoints || 0) + ' ' +
'当前持有天道点天道不劝你,也不拦你——它只是记账。 ';
b.appendChild(banner);
/* 承受:获取途径 */
b.appendChild(secHead('承受(以自己的一部分换取天道点)'));
b.appendChild(el('div', 'ka-meta', '这里没有免费的东西。你换到的每一点,都是从自己身上割下来的,且不会自动长回去。'));
D.TIAN_EARN.forEach(function (e) {
var d = el('div', 'tian-item');
d.innerHTML = '' + esc(e.name) + '
' +
'' + esc(e.desc) + '
';
var acts = el('div', 'ti-act ka-acts');
/* 需要参数的途径提供选项 */
if (e.act === 'injury') {
D.INJURY_TYPES.forEach(function (t) {
var lv = pc.injury[t] || 0;
var btn = el('button', 'btn btn-sm btn-ghost', t + '(' + D.INJURY_LEVELS[lv] + ')');
btn.disabled = lv >= 5;
btn.addEventListener('click', function () { doTianEarn(e.act, t); });
acts.appendChild(btn);
});
} else if (e.act === 'life') {
[10, 30, 50].forEach(function (y) {
var btn = el('button', 'btn btn-sm btn-ghost', y + ' 年');
btn.disabled = pc.age + y >= pc.lifeMax;
btn.addEventListener('click', function () { doTianEarn(e.act, y); });
acts.appendChild(btn);
});
} else if (e.act === 'art') {
var aids = Object.keys(pc.arts || {}).filter(function (a) { return pc.arts[a].bp > 0; });
if (!aids.length) acts.appendChild(el('span', 'ka-meta', '你没有可自废的百艺。'));
aids.forEach(function (a) {
var btn = el('button', 'btn btn-sm btn-ghost', a + '(' + Math.round(pc.arts[a].bp) + ' BP)');
btn.addEventListener('click', function () { doTianEarn(e.act, a); });
acts.appendChild(btn);
});
} else {
var btn2 = el('button', 'btn btn-sm', '承受');
btn2.addEventListener('click', function () { doTianEarn(e.act); });
acts.appendChild(btn2);
}
d.appendChild(acts);
b.appendChild(d);
});
/* 兑换 */
b.appendChild(secHead('兑换(天道点可以买到的东西)'));
b.appendChild(el('div', 'ka-meta', '每一项都附带后果。抵消的天劫会连本带利回来,洗白的因果只是暂时不疼,净化的污染记着你。'));
D.TIAN_SHOP.forEach(function (it) {
var d = el('div', 'tian-item');
var can = (pc.tianPoints || 0) >= it.cost;
d.innerHTML = '' + esc(it.name) + ' ' + it.cost + ' 点
' +
'' + esc(it.desc) + '
';
var acts = el('div', 'ti-act ka-acts');
if (it.id === 'six') {
/* 淬炼六司:需选择具体属性 */
var PER = D.SIX_PER_POINT || {};
D.SIX.forEach(function (s) {
var v = pc.six[s.key] || 0;
var hardMax = D.SIX_HARD_MAX || 15;
var sb = el('button', 'btn btn-sm' + (can && v < hardMax ? ' btn-ghost' : ' btn-ghost'), s.key + ' ' + v + (v > D.SIX_MAX ? '↑' : '') + '(+' + Math.round(v * (PER[s.key] || 0.02) * 100) + '%)');
sb.disabled = !can || v >= hardMax;
sb.title = v >= hardMax ? '已达硬上限 ' + hardMax : '淬炼 ' + s.key + ' +1(突破初始上限 10 后天道会记账,可能被改回)';
sb.addEventListener('click', function () {
modal('淬炼六司·' + s.key, '' + esc(s.key) + ' ' + v + ' → ' + (v + 1) + ' -' + it.cost + ' 天道点
' +
'' + esc(D.SIX_MAIN_EFFECT ? D.SIX_MAIN_EFFECT[s.key] : s.affects) + '
' +
(v >= D.SIX_MAX ? '此项已超初始上限 10。天道记了一笔——改上去的东西,也可能在岁月里被改回来。
' : '这不是恩赐,是延期付款。
'),
[{ label: '再想想', cls: 'btn-ghost' }, {
label: '淬炼', cls: 'btn-gold', act: function () {
var r = G.tpBuy('six', s.key);
if (!r.ok) { toast(r.msg, 'err'); return; }
modal('淬炼完成', '' + esc(r.txt) + '
', [{ label: '好', cls: 'btn-gold' }]);
renderAll(); autoSave();
}
}]);
});
acts.appendChild(sb);
});
} else {
var btn = el('button', 'btn btn-sm' + (can ? ' btn-gold' : ' btn-ghost'), can ? '兑换' : '点数不足');
btn.disabled = !can;
btn.addEventListener('click', function () {
modal('确认兑换', '' + esc(it.name) + ' -' + it.cost + ' 天道点
' + esc(it.desc) + '
这不是恩赐,是延期付款。
',
[{ label: '再想想', cls: 'btn-ghost' }, {
label: '我认这个代价', cls: 'btn-gold', act: function () {
var r = G.tpBuy(it.id);
if (!r.ok) { toast(r.msg, 'err'); return; }
modal('兑换完成', '' + esc(r.txt) + '
', [{ label: '承受', cls: 'btn-gold' }]);
renderAll(); autoSave();
}
}]);
});
acts.appendChild(btn);
}
d.appendChild(acts);
b.appendChild(d);
});
/* 当前身上的负担总览 */
b.appendChild(secHead('你身上现在的负担'));
var flaws = G.activeFlaws();
var burden = [];
burden.push('道伤:' + D.INJURY_TYPES.map(function (t) { return t + '·' + D.INJURY_LEVELS[pc.injury[t] || 0]; }).join(' '));
burden.push('道心污染:' + (D.POLLUTION[pc.pollution] || D.POLLUTION[0]).name);
burden.push('天道监察:' + (D.MONITOR[pc.monitor] || D.MONITOR[0]).lv + ' ' + (D.MONITOR[pc.monitor] || D.MONITOR[0]).name);
burden.push('代价状态:' + D.COST_LEVELS[pc.costLevel || 0].name);
burden.push('寿元:' + pc.age + ' / ' + (isFinite(pc.lifeMax) ? pc.lifeMax : '∞') + '(' + G.lifespanStage(pc).name + ')');
burden.push('声誉:' + pc.reputation + '(' + G.reputationWord() + ')');
burden.push('活跃因果:' + G.karmas.filter(function (k) { return k.status === 'active'; }).length + ' 条');
b.appendChild(el('div', 'sysbox', burden.map(function (x) { return '· ' + esc(x) + '
'; }).join('')));
if (flaws.length) {
b.appendChild(secHead('命途缺陷(' + flaws.length + ' 项,会周期性发作)'));
flaws.forEach(function (f) {
var cur = (f.fx || {}).curse;
var evs = cur && D.CURSE_EVENTS[cur] ? D.CURSE_EVENTS[cur] : null;
var d = el('div', 'tian-item');
d.innerHTML = '' + esc(f.name) + '
' +
'' + esc(f.desc) + '
' +
(evs ? '已记录的发作形式(共 ' + evs.length + ' 种): ' + evs.map(function (x) { return '· ' + esc(x); }).join(' ') + '
' : '');
b.appendChild(d);
});
}
}
function doTianEarn(act, arg) {
var r = G.tpEarn(act, arg);
if (!r.ok) { toast(r.msg, 'err'); return; }
modal('承受完成', '' + esc(r.txt) + '
天道点 +' + r.gained + ' 当前持有 ' + r.total + '
' +
'这笔账记下了。它不会自己消失。
', [{ label: '我认', cls: 'btn-gold' }]);
if (G.pc.hp <= 0 && !G.over) { G.die('死于自我承受'); showDeath(D.DEATH_FORMS); }
renderAll(); autoSave();
}
/* --- 因果结算表历史 --- */
function tabSettle(b) {
b.appendChild(secHead('因果结算表·历史'));
if (!G.settlements.length) { b.appendChild(el('div', 'empty', '尚无结算记录。')); return; }
G.settlements.slice(0, 14).forEach(function (rec) {
var s = rec.s, rows = [];
if (s.karma.length) rows.push(['因果网', s.karma.join(';')]);
if (s.npc.length) rows.push(['NPC关系', s.npc.join(';')]);
if (s.world.length) rows.push(['世界状态', s.world.join(';')]);
if (s.daoyun) rows.push(['道蕴', (s.daoyun > 0 ? '+' : '') + s.daoyun]);
if (s.injury.length) rows.push(['道伤', s.injury.join(';')]);
if (s.bp.length) rows.push(['行为塑道', s.bp.join(';')]);
if (s.arts.length) rows.push(['百艺', s.arts.join(';')]);
var d = el('div', 'karma');
d.innerHTML = '第 ' + rec.turn + ' 回合
' +
(rows.length ? '' + rows.map(function (r) { return '' + esc(r[0]) + ' ' + esc(r[1]) + ' '; }).join('') + '
' : '本回合无显著变化。
');
b.appendChild(d);
});
/* 久远记录:压缩为摘要但永不丢弃(铁律·绝对不遗忘) */
if (G.settleArchive && G.settleArchive.length) {
b.appendChild(secHead('久远记录·摘要保留(' + G.settleArchive.length + ' 回合)'));
b.appendChild(el('div', 'ka-meta', '每十回合触发一次记忆压缩。日常级信息降级为背景设定,但因果本身永久保留、永不丢弃。'));
G.settleArchive.slice(0, 40).forEach(function (a) {
b.appendChild(el('div', 'tick hidden-ev', '第' + a.turn + '回合 ' + esc(a.brief)));
});
}
}
/* --- NPC --- */
function tabNpc(b) {
var alive = G.npcs.filter(function (n) { return n.alive; });
var dead = G.npcs.filter(function (n) { return !n.alive; });
b.appendChild(secHead('存活人物(' + alive.length + ')· 六要素齐备'));
if (!alive.length) b.appendChild(el('div', 'empty', '没有认识的人了。'));
alive.sort(function (x, y) { return (y.met ? 1 : 0) - (x.met ? 1 : 0) || y.power - x.power; }).forEach(function (n) {
var atts = D.ATTITUDES.filter(function (a) { return (n.att[a] || 0) >= 15; });
var d = el('div', 'npc');
d.addEventListener('click', function () { openNpc(n.id); });
d.innerHTML = '' + esc(n.fullName) + ' ' +
'' + esc(D.REALMS[n.realmIdx].name) + ' ' +
(n.isQiyun ? '气运之子 ' : '') +
(n.met ? '' : '未谋面 ') +
'' + esc(n.factionName) + '
' +
'战力 ' + n.power + ' | ' + n.age + ' 岁 | 性格' + esc(n.personality) + ' | 目标:' + esc(n.goal) +
(n.met ? ' 羁绊:' + esc(D.BONDS[n.bond || 0]) + ' | 背叛概率:' + Math.round(n.betrayalBase * 100) + '%' : '') +
(n.motiveRevealed ? '隐藏动机:' + esc(n.hiddenMotive) + ' ' : '') + '
' +
'' + (atts.length ? atts.map(function (a) { return '' + a + ' ' + n.att[a] + ' '; }).join('') : '漠视 ' + (n.att['漠视'] || 0) + ' ') + '
';
if (n.memory.length) {
d.innerHTML += '' + n.memory.slice(0, 4).map(function (m) {
return '
· 第' + m.turn + '回合【' + esc(m.level) + '】' + esc(m.summary) + (m.fuzzy >= 0.5 ? '(记忆已模糊)' : '') + '
';
}).join('') + '
';
}
if (n.actionLog && n.actionLog.length) {
d.innerHTML += '' + n.actionLog.slice(0, 2).map(function (a) { return '
· 第' + a.turn + '回合:' + esc(a.act) + '
'; }).join('') + '
';
}
if (n.met && n.alive) {
var acts = el('div', 'ka-acts');
[['交谈', 'meet'], ['赠礼', 'gift'], ['切磋', 'spar']].forEach(function (a) {
var btn = el('button', 'btn btn-sm btn-ghost', a[0]);
btn.addEventListener('click', function () { npcAct(n, a[1]); });
acts.appendChild(btn);
});
d.appendChild(acts);
}
b.appendChild(d);
});
if (dead.length) {
b.appendChild(secHead('已故(' + dead.length + ')· 死亡不可跳过'));
dead.forEach(function (n) {
b.appendChild(el('div', 'npc dead', '' + esc(n.fullName) + ' 第 ' + n.deathTurn + ' 回合
' +
'' + esc(n.deathWay) + ' | 享年 ' + n.age + '
'));
});
}
}
function npcAct(n, kind) {
var pc = G.pc, out = [];
if (kind === 'meet') {
pc.energy = Math.max(0, pc.energy - 15);
var d = G.dice(0.45 + (pc.six['魅力'] - 3) * 0.06 + (n.att['感恩'] || 0) / 400 - (n.att['仇恨'] || 0) / 300, { attr: '魅力' });
if (d.result === 'great' || d.result === 'ok') {
G.npcAttitude(n, '感恩', 10, '交谈');
n.bond = Math.min(pc.bondCap, (n.bond || 0) + 1); pc.bonds[n.id] = n.bond;
out.push('谈得还行。' + (d.result === 'great' ? '他多说了一些本来不打算说的话。' : '他把该说的说了,不该说的没说。'));
if (d.result === 'great' && !n.motiveRevealed && Math.random() < 0.4) { n.motiveRevealed = true; out.push('他透露了:' + n.hiddenMotive); }
G.addBp('情', 1.0, '交谈');
} else {
G.npcAttitude(n, '漠视', 8, '交谈无果');
out.push('话没谈拢。他找了个理由先走了,理由很体面。');
}
} else if (kind === 'gift') {
var cost = Math.min(pc.money, 200);
if (cost < 50) { toast('灵石不足(至少需要 50)', 'err'); return; }
pc.money -= cost; pc.energy = Math.max(0, pc.energy - 8);
G.npcAttitude(n, '感恩', Math.round(cost / 12), '收到 ' + cost + ' 灵石的赠礼');
n.bond = Math.min(pc.bondCap, (n.bond || 0) + 1); pc.bonds[n.id] = n.bond;
out.push('你送了他 ' + cost + ' 灵石。他收了,收得' + (cost > 150 ? '有点犹豫,犹豫不是因为嫌少' : '很自然') + '。');
out.push('感恩 +' + Math.round(cost / 12) + ',羁绊 → ' + D.BONDS[n.bond]);
if (n.bond >= 4) out.push('羁绊已达「生死」。可触发羁绊技:替死、共鸣、传承。这不是白来的——它意味着他出事的时候,你也会痛。');
} else {
pc.energy = Math.max(0, pc.energy - 25);
var d2 = G.dice(0.5 * (pc.combat / Math.max(1, n.power)), { attr: '根骨' });
out.push(d2.result === 'great' || d2.result === 'ok' ? '你赢了这一场。他抱拳,笑得不太自然。' : '你输了。他没有追击,这算是给面子。');
G.npcAttitude(n, d2.result === 'great' ? '敬畏' : '忌惮', 12, '切磋');
pc.hp = Math.max(1, pc.hp - Math.round(pc.hpMax * 0.12));
G.addBp('剑', 1.5, '切磋');
}
modal(n.fullName, out.map(function (t) { return '' + esc(t) + '
'; }).join(''), [{ label: '好', cls: 'btn-gold' }]);
renderAll(); autoSave();
}
/* --- 世界动态 --- */
function tabWorld(b) {
var trib = D.TRIBULATION[G.stage];
b.appendChild(secHead('大劫九阶段'));
var tw = el('div', 'karma');
tw.innerHTML = '' + esc(trib.name) + ' 第 ' + G.turn + ' 回合
' +
'' + esc(trib.desc) + '
' +
'' + D.TRIBULATION.map(function (s, i) {
return '' + esc(s.name) + ' ';
}).join('') + '
' +
'每 50 回合推进一次;下一阶段:' + esc(D.TRIBULATION[Math.min(D.TRIBULATION.length - 1, G.stage + 1)].name) + '(第 ' + D.TRIBULATION[Math.min(D.TRIBULATION.length - 1, G.stage + 1)].turn + ' 回合)
';
b.appendChild(tw);
b.appendChild(secHead('世界大事件日历(未来一百回合)'));
var upcoming = G.bigEvents.filter(function (e) { return !e.done; }).slice(0, 8);
if (!upcoming.length) b.appendChild(el('div', 'empty', '日历上没有已确定的节点。这不代表不会出事。'));
upcoming.forEach(function (e) {
b.appendChild(el('div', 'karma', '' + esc(e.kind) + ' ' +
'' + Math.max(0, e.turn - G.turn) + ' 回合后
' +
'' + esc(e.desc) + '
' +
'权重 ' + e.weight + ' | 触发时插入世界级叙事,所有相关人物线同步推进
'));
});
b.appendChild(secHead('世界 TICK·后台事件(每回合 ≥3 件)'));
b.appendChild(el('div', 'ka-meta', '这些是你在场之外发生的事。世界不因你的不在场而暂停。'));
G.ticks.slice(0, 26).forEach(function (t) {
b.appendChild(el('div', 'tick' + (t.hidden ? ' hidden-ev' : ''), '' + esc(t.type) + ' ' + esc(t.text) + (t.hidden ? '(你当时并不知情)' : '')));
});
b.appendChild(secHead('势力格局'));
G.factions.forEach(function (f) {
var war = f.warWith ? G.factions.filter(function (x) { return x.id === f.warWith; })[0] : null;
var d = el('div', 'karma');
d.innerHTML = '' + esc(f.name) + ' ' +
'' + esc(f.kind) + ' ' +
'实力 ' + f.power + '
' +
'' + esc(f.desc) + '
' +
'地盘:' + esc(f.territory) + (war ? ' | 交战中:' + esc(war.name) + ' ' : '') + '
';
b.appendChild(d);
});
if (G.brands && G.brands.length) {
b.appendChild(secHead('世界刻印(永久改变世界规则)'));
G.brands.forEach(function (br) {
b.appendChild(el('div', 'karma', '' + esc(br.name) + ' ' + esc(br.place) + '
' +
'' + esc(br.desc) + '
刻于第 ' + br.turn + ' 回合,以「' + esc(br.medium) + '」为媒,刻印者:' + esc(br.owner) + '
'));
});
}
if ((G.daoTraces || []).length) {
b.appendChild(secHead('道痕(行为模因,可被感知与传播)'));
G.daoTraces.slice(0, 14).forEach(function (t) {
b.appendChild(el('div', 'tick', '' + esc(t.path) + ' ' + esc(t.place) + '留下' + esc(t.color) + ',强度 ' + t.strength.toFixed(1) +
(t.mutant ? ' 已变异为「' + esc(t.mutant) + '」 ' : '') + '(第 ' + t.turn + ' 回合)'));
});
}
}
/* --- 天地志:这个世界本身的样子 --- */
function tabLore(b) {
var pc = G.pc, w = pc.world;
b.appendChild(secHead('脚下这块地方'));
var wd = el('div', 'karma');
wd.innerHTML = '' + esc(w.name) + ' ' +
'' + esc(w.tag) + '
' +
'' + esc(w.desc) + '
' +
'灵气 ×' + w.qi + ' 法则 ×' + w.law + ' 凶险 ×' + w.danger +
' | ' + (w.states || []).map(function (s) { return esc(s); }).join('、') + '
';
b.appendChild(wd);
b.appendChild(secHead('界域·可远行之处'));
(D.WORLDS || []).slice(0, 10).forEach(function (x) {
var here = x.id === w.id;
b.appendChild(el('div', 'tick' + (here ? '' : '') ,
'' + (here ? '此地' : esc(x.tag)) + ' ' +
'' + esc(x.name) + ' ' +
' 灵气×' + x.qi + ' 凶险×' + x.danger + '' + esc(x.desc) + ' '));
});
var groups = {};
(D.LORE || []).forEach(function (e) { (groups[e.k] = groups[e.k] || []).push(e); });
Object.keys(groups).forEach(function (k) {
b.appendChild(secHead(k));
groups[k].forEach(function (e) {
b.appendChild(el('div', 'karma', '' + esc(e.n) + '
' +
'' + esc(e.d) + '
'));
});
});
b.appendChild(el('div', 'ka-meta', '天地志只记已确知的部分。界域之外的空白,要自己走过去才知道。'));
}
/* --- 境界体系 --- */
function tabRealm(b) {
var pc = G.pc, sys = D.systemOf(pc), plane = D.planeOf(pc);
b.appendChild(secHead('我走的道 · ' + sys.name));
var head = el('div', 'karma');
head.innerHTML = '' + esc(sys.name) + ' ' +
'' + esc(plane.name) + '
' +
'' + esc(sys.desc) + '
' +
'当前境界:' + esc(D.realmNameOf(pc, pc.realmIdx)) +
' | 下一境:' + esc(pc.realmIdx >= sys.ladder.length - 1 ? '已至尽头' : D.realmNameOf(pc, pc.realmIdx + 1)) +
' | 道蕴 ' + n2s(pc.qi) + ' / ' + n2s(pc.qiNeed) + '
';
b.appendChild(head);
b.appendChild(secHead('境界阶梯(共 ' + sys.ladder.length + ' 阶,当前已标记)'));
sys.ladder.forEach(function (r, i) {
var cls = i === pc.realmIdx ? 'cur' : (i < pc.realmIdx ? 'past' : '');
var d = el('div', 'ladder-row ' + cls);
d.innerHTML = '' + (i + 1) + ' ' +
'' + esc(r[0]) + (i === pc.realmIdx ? '当前 ' : '') + ' ' +
'' + esc(r[1]) + ' ';
b.appendChild(d);
});
if (sys.custom) {
b.appendChild(secHead('与原有境界的对应(战力同档)'));
var baseR = D.systemById('qi');
sys.ladder.forEach(function (r, i) {
b.appendChild(el('div', 'ladder-row', '' + (i + 1) + ' ' + esc(r[0]) + ' = 气道 · ' + esc(baseR.ladder[i][0]) + '(战力系数 1.00) '));
});
}
b.appendChild(secHead('上界 · 位面'));
b.appendChild(el('div', 'ka-meta', '每个初始世界按四条道各有一座上界;四座之上还有一处位面,所有飞升者最后都到那里。'));
var wid = (pc.world && (pc.world.upperOf || pc.world.id)) || null;
(wid ? D.upperRealmsFor(wid) : []).forEach(function (u) {
var mine = u.systemId === sys.id;
b.appendChild(el('div', 'karma', '' + esc(u.name) + ' ' +
'' + esc(D.systemById(u.systemId).name) + (mine ? '·本道' : '') + '
' +
'' + esc(u.desc) + '
' +
'灵气 ×' + u.qi + ' 危险 ×' + u.danger + '
'));
});
b.appendChild(el('div', 'karma', '' + esc(D.TOP_PLANE.name) + ' ' +
'位面
' + esc(D.TOP_PLANE.desc) + '
'));
}
/* --- 百艺道途 --- */
function tabArt(b) {
var pc = G.pc;
b.appendChild(secHead('行为塑道·BP 阶跃(10/25/50/100/200)'));
var paths = Object.keys(pc.bp).map(function (k) {
var p = D.DAO_PATHS.filter(function (x) { return x.key === k; })[0] || { id: k, color: '#888', desc: '' };
return { k: k, v: pc.bp[k], st: pc.bpStage[k] || 0, p: p };
}).sort(function (a, c) { return c.v - a.v; });
if (!paths.length) b.appendChild(el('div', 'empty', '还没有形成任何道途。行为塑道不靠选择,靠累积——你做过什么,你就是什么。'));
paths.forEach(function (x) {
var nextT = D.BP_STAGES[x.st] || null;
var prevT = x.st > 0 ? D.BP_STAGES[x.st - 1] : 0;
var pctv = nextT ? Math.min(100, (x.v - prevT) / (nextT - prevT) * 100) : 100;
var br = D.BEHAVIOR_REALMS[Math.min(D.BEHAVIOR_REALMS.length - 1, x.st)];
var d = el('div', 'art');
d.innerHTML = '' + esc(x.p.id) + ' ' +
' ' +
'' + esc(x.st ? D.BP_STAGE_NAMES[x.st - 1] : '未觉醒') + '
' +
'' + x.v.toFixed(1) + ' BP' + (nextT ? ' | 下一阶跃 ' + nextT + ' BP(' + esc(D.BP_STAGE_NAMES[x.st]) + ')' : ' | 已达圆满') +
' | 行为境界:' + esc(br.name) + '(效率 +' + Math.round(br.eff * 100) + '%)' + (pc.silent && pc.silent[x.k] ? ' 已寂灭 ' : '') + '
' +
'' + esc(x.p.desc) + '
' +
'' + esc(br.desc) + '
';
b.appendChild(d);
});
if (pc.conflictPct) {
b.appendChild(el('div', 'sysbox box-threat', '【道心冲突 ' + pc.conflictPct + '%】' +
(pc.conflictPct >= 90 ? '道心崩裂:心魔劫已至,必须立即解决,否则道途废弛。' : pc.conflictPct >= 60 ? '明显裂痕:效率下降 30%,有失败风险。' : '轻微不适:效率下降 10%。') +
'
解决路径:① 选择一方(强化一道,另一道寂灭)② 寻求统一(需特殊契机)③ 承受分裂(每回合道心伤害)
'));
}
b.appendChild(secHead('修仙百艺(' + Object.keys(pc.arts).length + ' 门已入门)'));
if (!Object.keys(pc.arts).length) b.appendChild(el('div', 'empty', '尚未修习任何百艺。选择「打磨一门百艺」可以开始。'));
Object.keys(pc.arts).forEach(function (aid) {
var a = pc.arts[aid];
var def = D.ARTS.filter(function (x) { return x.id === aid; })[0] || { levels: ['习艺', '悟艺', '化艺', '创艺', '道艺'], desc: '', stages: [] };
var nextT = D.ART_BP_STAGES[a.level] || null;
var prevT = a.level > 0 ? D.ART_BP_STAGES[a.level - 1] : 0;
var pctv = nextT ? Math.min(100, (a.bp - prevT) / (nextT - prevT) * 100) : 100;
var d = el('div', 'art');
d.innerHTML = '' + esc(aid) + ' ' +
'' + esc(a.level ? def.levels[a.level - 1] : '未入门') + ' ' +
'' + Math.round(a.bp) + ' BP
' +
'
' +
'' + (nextT ? '下一阶跃:' + nextT + ' BP(' + esc(def.levels[a.level] || '—') + ')' : '已达道艺,举手投足皆含法则') +
' | 闲置 ' + (G.turn - (a.last || G.turn)) + ' 回合' + ((G.turn - (a.last || G.turn)) > 30 ? '(每日衰减 5%) ' : '') + '
' +
(def.desc ? '' + esc(def.desc) + '
' : '') +
(def.taboo ? '禁忌:' + esc(def.taboo) + '(触发天道监察)
' : '');
b.appendChild(d);
});
if (pc.fusionDao) {
b.appendChild(el('div', 'sysbox box-break', '【融合道途】' + esc(pc.fusionDao) + '
两种百艺在你手里互相说话。融合奖励:独特能力、特殊道途、世界刻印、道统建立。融合风险:道心冲突加剧、反噬叠加、道基不稳、被天道监察标记。
'));
}
b.appendChild(secHead('禁忌技艺(威力强大,必触发监察)'));
D.TABOOS.forEach(function (t) {
var used = (pc.tabooUsed || []).some(function (u) { return u.id === t.id; });
var d = el('div', 'karma');
d.innerHTML = '' + esc(t.name) + ' ' +
'监察 L' + t.monitor + ' ' +
'威力 ×' + t.power + '
' +
'' + esc(t.desc) + '
' +
'' + (t.art ? '所属百艺:' + esc(t.art) + ' | ' : '') + '堕落道途:' + esc(t.dao) + (used ? ' 你已用过 ' : '') + '
';
if (!used) {
var acts = el('div', 'ka-acts');
var btn = el('button', 'btn btn-sm', '修炼此禁忌');
btn.addEventListener('click', function () {
modal('确认修炼禁忌', '' + esc(t.name) + '
' + esc(t.desc) + '
' +
'后果:天道监察直接升至 L' + t.monitor + '(成功率 -' + Math.round(D.MONITOR[t.monitor].pen * 100) + '%),道心污染 +2 阶,并绑定一条不可转化的天道级因果「禁忌之债」。豁免不是免单,是记账以后一起算。
',
[{ label: '再想想', cls: 'btn-ghost' }, {
label: '我认这个代价', cls: 'btn-gold', act: function () {
var ur = G.useTaboo(t.id);
pc.daoyun += Math.round(80 * t.power);
pc.pollution = E.clamp(pc.pollution + 2, 0, 4);
G.addCost(3, t.name);
toast('禁忌已成:道蕴 +' + Math.round(80 * t.power) + ',监察 L' + (ur && ur.monitor ? ur.monitor.to : t.monitor), 'err');
renderAll(); autoSave();
}
}]);
});
acts.appendChild(btn);
d.appendChild(acts);
}
b.appendChild(d);
});
b.appendChild(secHead('证道 / 道统 / 刻印'));
var asc = G.canAscend();
var ad = el('div', 'karma');
ad.innerHTML = '证道条件 ' + (asc.ok ? '已满足 ' : '未满足') + '
' +
asc.conds.map(function (c) { return '' + (c.ok ? '✓ ' : '✗ ') + ' ' + esc(c.name) + '
'; }).join('') +
'证道不是终点,而是新形态的开始。成功后可选择:飞升上界 / 隐世 / 化道 / 继续游戏。
';
var aa = el('div', 'ka-acts');
if (asc.ok) {
['飞升上界', '隐世', '化道', '继续游戏'].forEach(function (c) {
var btn = el('button', 'btn btn-sm btn-gold', c);
btn.addEventListener('click', function () {
modal('证道·' + c, '' + esc(G.ascend(c)) + '
', [{ label: '铭记', cls: 'btn-gold' }]);
if (c !== '继续游戏') { G.over = true; G.overReason = '证道·' + c; }
renderAll();
});
aa.appendChild(btn);
});
}
ad.appendChild(aa);
b.appendChild(ad);
var lb = el('button', 'btn btn-sm', '尝试建立道统');
lb.addEventListener('click', function () {
var r = G.foundLineage();
modal('道统建立', '' + esc(r.msg) + '
', [{ label: '知道了', cls: 'btn-gold' }]);
renderAll(); autoSave();
});
b.appendChild(el('div', 'ka-acts')).appendChild(lb);
b.appendChild(el('div', 'ka-acts')).appendChild((function () {
var btn = el('button', 'btn btn-sm', '尝试创造世界刻印');
btn.addEventListener('click', function () {
var list = D.WORLD_BRANDS.map(function (w, i) { return '' + (i + 1) + '. ' + esc(w.name) + '(' + esc(w.art) + ') '; }).join('');
modal('选择刻印类型', '' + list + '
条件:道途第三境以上 + 道蕴 ≥300 + 一件材料 + 特定时机(大劫推进前后或地脉异动时)。刻印永久改变该地规则,并绑定一条天道级因果。
',
D.WORLD_BRANDS.map(function (w) {
return {
label: w.name, cls: 'btn-sm', close: true, act: function () {
var r = G.createBrand(w.name);
modal('世界刻印', '' + esc(r ? r.msg : '无法创造。') + '
', [{ label: '知道了', cls: 'btn-gold' }]);
renderAll(); autoSave();
}
};
}));
});
return btn;
})());
/* 斩道 */
var zd = el('button', 'btn btn-sm btn-ghost', '斩道(放弃当前道途)');
zd.addEventListener('click', function () {
modal('斩道', '斩道需付出:道基重铸、寿元献祭、因果断裂。斩道后道途积分清零,保留部分能力碎片,并触发「大道反噬」——随机失去一项感官、记忆或羁绊。
道途不可逆:一旦成型(第三境以上),逆转需要极端代价。禁止轻松转职。
',
[{ label: '取消', cls: 'btn-ghost' }, {
label: '斩', cls: 'btn-gold', act: function () {
var pc2 = G.pc;
var top = Object.keys(pc2.bp).sort(function (a, c) { return pc2.bp[c] - pc2.bp[a]; })[0];
if (top) { pc2.bp[top] = 0; pc2.bpStage[top] = 0; }
pc2.lifeMax = Math.round(pc2.lifeMax * 0.85);
G.addInjury('道基', Math.min(5, pc2.injury['道基'] + 2), '斩道');
var losses = ['一项感官', '一段记忆', '一段羁绊'];
var lost = losses[Math.floor(Math.random() * losses.length)];
if (lost === '一段羁绊') { var bs = Object.keys(pc2.bonds); if (bs.length) { var bn = G.npc(bs[0]); if (bn) { bn.bond = 0; pc2.bonds[bs[0]] = 0; } } }
pc2.combat = G.calcPower(pc2);
toast('斩道完成。大道反噬:你失去了' + lost + '。', 'err');
renderAll(); autoSave();
}
}]);
});
b.appendChild(el('div', 'ka-acts')).appendChild(zd);
}
/* --- 坊市经济 --- */
function tabMarket(b) {
var pc = G.pc;
var pm = G.priceModifier();
var last = G.priceHist[0];
b.appendChild(secHead('修仙界经济(每十回合更新价格表)'));
var mktReasonHtml = (last && last.reason && last.reason.length)
? '本次变动的因果解释: · ' + last.reason.map(esc).join(' · ') + '
'
: '价格变动必须有明确因果:供需变化 / 事件驱动 / 势力行为 / 天象影响。禁止无理由暴涨暴跌。
';
b.appendChild(el('div', 'mkt-note',
'你的持有:' + n2s(pc.money) + ' 灵石 | 声誉系数 ×' + pm.toFixed(2) + ' (' + esc(G.reputationWord()) + ')
' +
mktReasonHtml));
b.appendChild(el('div', 'ka-meta', '经商四重判定:运输风险(劫修/妖兽/天灾)· 市场影响(大额买卖改变当地价格)· 引来觊觎(商会征税/黑市设局/正道通缉)· 口碑影响(声誉决定进价与商机)'));
var prevPrices = G.priceHist[1] ? G.priceHist[1].prices : G.basePrices;
D.MARKET.forEach(function (cat) {
b.appendChild(el('div', 'mkt-cat', esc(cat.cat)));
cat.items.forEach(function (it) {
var cur = G.prices[it.n] || it.p;
var before = prevPrices[it.n] || it.p;
var shown = Math.max(1, Math.round(cur * pm));
var delta = before ? Math.round((cur - before) / before * 100) : 0;
var own = pc.items.filter(function (x) { return x.n === it.n && x.qty; }).reduce(function (a, c) { return a + (c.qty || 0); }, 0);
var row = el('div', 'mkt-row');
row.innerHTML = '' + esc(it.n) + ' ' +
'' + (own ? '持' + own : '') + ' ' +
'' + (delta > 0 ? '▲' : delta < 0 ? '▼' : '—') + Math.abs(delta) + '% ' +
'' + n2s(shown) + ' ';
var buy = el('button', 'btn btn-sm btn-ghost', '买');
buy.addEventListener('click', function () { tradeDlg(it.n, true, shown); });
var sell = el('button', 'btn btn-sm btn-ghost', '卖');
sell.disabled = !own;
sell.addEventListener('click', function () { tradeDlg(it.n, false, shown); });
row.appendChild(buy); row.appendChild(sell);
b.appendChild(row);
});
});
}
function tradeDlg(name, buy, price) {
var qty = 1;
var html = '' + (buy ? '购入' : '售出') + ' ' + esc(name) + ' 单价 ' + n2s(price) + ' 灵石
' +
'数量 ' +
'' + n2s(price) + ' 灵石
' +
'' + (buy ? '大量买入会推高当地价格。' : '大量卖出会压低当地价格;运输途中可能遭遇劫修、妖兽或天灾;大额交易会引来商会、黑市或正道宗门的注意。') + '
';
modal((buy ? '购入·' : '售出·') + name, html, [
{ label: '取消', cls: 'btn-ghost' }, {
label: buy ? '确认购入' : '确认售出', cls: 'btn-gold', act: function () {
var q = Math.max(1, parseInt($('#tq') ? $('#tq').value : '1', 10) || 1);
var r = G.trade(name, q, buy);
modal('交易结果', '' + esc(r.msg) + '
' + (r.ok ? '' : '交易未达成。
'), [{ label: '好', cls: 'btn-gold' }]);
renderAll(); autoSave();
}
}]);
var inp = $('#tq');
if (inp) inp.addEventListener('input', function () {
var q = Math.max(1, parseInt(this.value, 10) || 1);
var s = $('#tsum'); if (s) s.textContent = n2s(price * q) + ' 灵石';
});
}
/* --- 万古道碑 --- */
function tabStele(b) {
b.appendChild(secHead('万古道碑(道号一旦获得,永久记录)'));
if (!G.daotitles.length) b.appendChild(el('div', 'empty', '尚无道碑。道号由自身行为自然凝结——需要某条道途达到第三境「道途初凝」以上。'));
G.daotitles.forEach(function (s) {
b.appendChild(el('div', 'karma', '「' + esc(s.title) + '」 ' +
'' + esc(s.path) + ' ' + esc(s.realm) + '
' +
'' + esc(s.deed) + '
' +
'' + esc(s.name) + ' | ' + esc(s.world + '·' + s.era) + ' | 第 ' + s.turn + ' 回合 | 种子 ' + s.seed + '
'));
});
if (G.prevLife && G.prevLife.length) {
b.appendChild(secHead('前世(跨档因果)'));
G.prevLife.forEach(function (p) {
b.appendChild(el('div', 'karma', '' + esc(p.name) + ' ' + esc(p.realm) + '
' +
'享年 ' + p.age + ' | 第 ' + p.turn + ' 回合 | 死因:' + esc(p.cause) + '
'));
});
}
b.appendChild(secHead('种子链(确保跨档继承时世界状态可追溯)'));
b.appendChild(el('div', 'ka-meta', G.seedChain.map(function (s, i) { return (i ? ' → ' : '') + s; }).join('')));
}
/* --- 存档 --- */
function tabSave(b) {
b.appendChild(secHead('天道存档码(含扩展字段)'));
b.appendChild(el('div', 'ka-meta', '每次存档必含:因果网快照 · NPC关系网 · 世界大事件日历(未来一百回合)· 当前大劫阶段与下一阶段触发条件 · 随机种子链。'));
var gen = el('button', 'btn btn-sm btn-blue', '生成存档码');
var ta = el('textarea', 'code');
ta.id = 'saveCode'; ta.placeholder = '点击「生成存档码」导出,或粘贴一段存档码后点击「读取」。';
var acts = el('div', 'ka-acts');
gen.addEventListener('click', function () {
var r = G.makeSaveCode();
if (r.code) { ta.value = r.code; toast('存档码已生成(' + Math.round(r.size / 1024) + ' KB 数据)', 'ok'); }
else toast('生成失败:' + r.error, 'err');
});
var load = el('button', 'btn btn-sm btn-gold', '从存档码读取');
load.addEventListener('click', function () {
if (!ta.value.trim()) { toast('请先粘贴存档码', 'err'); return; }
var g2 = E.Game.loadFromCode(ta.value.trim());
if (g2 && g2.error) { toast('读取失败:' + g2.error, 'err'); return; }
if (!g2 || !g2.pc) { toast('读取失败:存档码无效', 'err'); return; }
G = g2; curRound = 0;
$('#storyWrap').innerHTML = '';
var rnd = el('div', 'round');
rnd.appendChild(headRow('读档', '第 ' + G.turn + ' 回合'));
rnd.appendChild(el('div', 'sysbox box-break', '【续前世因缘】存档码已解析,世界状态已恢复。
' + esc(G.worldBrief()) + '
'));
$('#storyWrap').appendChild(rnd);
renderAll(); renderChoices(G.makeOptions());
toast('读档成功', 'ok');
});
acts.appendChild(gen); acts.appendChild(load);
b.appendChild(acts); b.appendChild(ta);
b.appendChild(secHead('本地存档槽'));
var saves = getSaves();
if (!saves.length) b.appendChild(el('div', 'empty', '当前浏览器尚无存档。'));
saves.forEach(function (s, i) {
var d = el('div', 'save-slot');
d.innerHTML = '' + esc(s.name) + ' ' + esc(s.realm) + ' ' + esc(s.time) + '
' +
'第 ' + s.turn + ' 回合 | 大劫 ' + esc(s.stage) + ' | ' + esc(s.world) + ' | ' + esc(s.brief) + '
';
var a = el('div', 'ss-acts');
var ld = el('button', 'btn btn-sm btn-blue', '读取');
ld.addEventListener('click', function () {
var g2 = E.Game.loadFromCode(s.code);
if (g2 && g2.error) { toast('读取失败:' + g2.error, 'err'); return; }
if (!g2 || !g2.pc) { toast('存档损坏', 'err'); return; }
G = g2; curRound = 0;
$('#storyWrap').innerHTML = '';
var rnd = el('div', 'round');
rnd.appendChild(headRow('读档', '第 ' + G.turn + ' 回合'));
rnd.appendChild(el('div', 'sysbox box-break', '【读档成功】' + esc(s.name) + ',' + esc(s.realm) + '。
' + esc(G.worldBrief()) + '
' +
'铁律·绝对不撤销:读档不能撤销已发生的因果。你回到的是这个时间点,不是那个时间点之前。
'));
$('#storyWrap').appendChild(rnd);
renderAll(); renderChoices(G.makeOptions());
toast('读档成功', 'ok');
});
var del = el('button', 'btn btn-sm btn-ghost', '删除');
del.addEventListener('click', function () {
modal('删除存档', '确定删除「' + esc(s.name) + '」的存档吗?此操作不可撤销。
', [
{ label: '取消', cls: 'btn-ghost' }, {
label: '删除', cls: 'btn-gold', act: function () {
var arr = getSaves(); arr.splice(i, 1); setSaves(arr); renderAll(); toast('存档已删除');
}
}]);
});
a.appendChild(ld); a.appendChild(del);
d.appendChild(a); b.appendChild(d);
});
var ns = el('button', 'btn btn-sm btn-gold', '存入新槽位');
ns.addEventListener('click', function () { saveToSlot(); });
b.appendChild(el('div', 'ka-acts')).appendChild(ns);
b.appendChild(secHead('自动存档'));
b.appendChild(el('div', 'ka-meta', '每回合自动保存到当前浏览器的「自动存档」槽位。关闭页面不会丢失进度;清理浏览器数据会丢失。建议每十回合手动导出一份存档码另存。'));
}
function getSaves() { try { return JSON.parse(localStorage.getItem(LS_KEY) || '[]'); } catch (e) { return []; } }
function setSaves(a) { try { localStorage.setItem(LS_KEY, JSON.stringify(a.slice(0, 12))); } catch (e) { toast('存档写入失败:浏览器存储空间可能已满', 'err'); } }
function saveToSlot() {
var r = G.makeSaveCode();
if (!r.code) { toast('存档码生成失败', 'err'); return; }
var arr = getSaves();
arr.unshift({
name: G.pc.name + (G.pc.daoTitle ? '「' + G.pc.daoTitle + '」' : ''), realm: D.REALMS[G.pc.realmIdx].name,
turn: G.turn, stage: D.TRIBULATION[G.stage].name, world: G.pc.world.name,
brief: '因果' + G.karmas.filter(function (k) { return k.status === 'active'; }).length + '条 · 伏笔' + G.foreshadow.filter(function (f) { return !f.resolved; }).length + '条',
time: new Date().toLocaleString('zh-CN'), code: r.code, auto: false
});
setSaves(arr); renderAll(); toast('已存入新槽位', 'ok');
}
function autoSave() {
try {
var r = G.makeSaveCode();
if (!r.code) return;
var arr = getSaves().filter(function (s) { return !s.auto; });
arr.unshift({
name: G.pc.name + (G.pc.daoTitle ? '「' + G.pc.daoTitle + '」' : ''), realm: D.REALMS[G.pc.realmIdx].name,
turn: G.turn, stage: D.TRIBULATION[G.stage].name, world: G.pc.world.name,
brief: '自动存档', time: new Date().toLocaleString('zh-CN'), code: r.code, auto: true
});
setSaves(arr);
} catch (e) { /* 静默 */ }
}
function secHead(t) { return el('div', 'sec').appendChild(el('h5', null, esc(t))).parentNode; }
/* ============================================================
SECTION: death 死亡转化
============================================================ */
function showDeath(forms) {
var info = G.deathInfo || {};
var html = '' + esc(N.deathTxt('zhuanshi', G.pc.age)) + '
' +
'死因:' + esc(info.cause || '未知') + ' | 享年 ' + (info.age || G.pc.age) + ' | 境界 ' + esc(info.realm || '') + ' | 第 ' + (info.turn || G.turn) + ' 回合
' +
'死亡不是 Game Over,是形态转变。请选择:
';
var sel = null;
var modeSel = 'suhui';
html += '' + forms.map(function (f, i) {
return '
';
}).join('') + '
' +
'转世方式 ' +
'宿慧(保留前世记忆与部分道蕴,寿元折损) ' +
'诅咒(继承前世道伤与因果,污染至不可名状) ' +
'因果继承(继承前世因果线,道蕴大损) ' +
'随机新生(完全新生,无前世痕迹) ' +
'
' +
'跨档继承:前世未解伏笔按 30% 权重继承,因果网自动继承关联节点,且继承的因果在新一档中自动提升一个优先级。继承必须付出对应代价。
';
modal('死亡·形态转变', html, [{ label: '取消', cls: 'btn-ghost' }, {
label: '确认转化', cls: 'btn-gold', act: function () {
if (sel == null) { toast('请选择一种转化形态', 'err'); return; }
var mode = $('#selReMode') ? $('#sel