371 lines
19 KiB
JavaScript
371 lines
19 KiB
JavaScript
"use strict";
|
|
|
|
function quoteNameInRecordText(text, name) {
|
|
const raw = String(text || "");
|
|
const n = String(name || "").trim();
|
|
if (!n) return raw;
|
|
const parts = raw.split(n);
|
|
if (parts.length <= 1) return raw;
|
|
let out = "";
|
|
for (let i = 0; i < parts.length - 1; i++) {
|
|
const before = parts[i].slice(-1);
|
|
const after = parts[i + 1].charAt(0);
|
|
out += parts[i] + ((before === "\u300c" && after === "\u300d") ? n : `\u300c${n}\u300d`);
|
|
}
|
|
return out + parts[parts.length - 1];
|
|
}
|
|
|
|
function renameSelectedTarinai(rawName) {
|
|
const t = world.selected;
|
|
if (!t || t.dead || !world.tarinai.includes(t)) return false;
|
|
const next = String(rawName || "").replace(/\s+/g, " ").trim().slice(0, 18);
|
|
if (!next) {
|
|
uiCache.selectedSnapshot = "";
|
|
renderSelected();
|
|
return false;
|
|
}
|
|
if (next === t.name) return false;
|
|
const oldName = t.name;
|
|
t.name = next;
|
|
if (t.isZunchiSlave) t.formerName = next;
|
|
world.recordFamily?.(t);
|
|
world.markFamilyTreeDirty?.("rename");
|
|
uiCache.archiveFamilyVersion = null;
|
|
uiCache.selectedSnapshot = "";
|
|
renderSelected();
|
|
renderArchive?.();
|
|
render?.();
|
|
showToast(`\u300c${oldName}\u300d\u3092\u300c${next}\u300d\u306b\u6539\u540d\u3057\u307e\u3057\u305f\u3002`);
|
|
return true;
|
|
}
|
|
|
|
function personalityValueText(value) {
|
|
const n = clamp(Number(value) || 0, -1, 1);
|
|
return `${n >= 0 ? "+" : ""}${n.toFixed(2)}`;
|
|
}
|
|
|
|
function personalityRowsHtml(personality = {}) {
|
|
const p = normalizePersonality(personality);
|
|
return PERSONALITY_KEYS.map(key => `<span>${escapeHtml(PERSONALITY_LABELS[key]?.label || key)}: ${personalityValueText(p[key])}</span>`).join("");
|
|
}
|
|
|
|
function personalityShortLabel(key) {
|
|
return ({
|
|
aggression: "\u653b\u6483",
|
|
openness: "\u958b\u653e",
|
|
sociability: "\u793e\u4ea4",
|
|
neuroticism: "\u795e\u7d4c",
|
|
})[key] || (PERSONALITY_LABELS[key]?.label || key);
|
|
}
|
|
|
|
|
|
function personalityDisplayCauseText(cause = "") {
|
|
return String(cause || "").replace(/(?:\u306e)?\u5f71\u97ff\u3067$/g, "").trim();
|
|
}
|
|
|
|
function geneticPercentText(t, key = "") {
|
|
const ratio = t?.geneticStatRatio ? t.geneticStatRatio(key) : 1;
|
|
const pct = Math.round((ratio - 1) * 100);
|
|
return `${pct >= 0 ? "+" : ""}${pct}%`;
|
|
}
|
|
|
|
function geneticSizeText(t) {
|
|
const adult = Number.isFinite(t?.adultScale) ? t.adultScale : (t?.scale || 0.28);
|
|
const now = Number.isFinite(t?.scale) ? t.scale : adult;
|
|
const growth = Math.round((t?.growth ?? 1) * 100);
|
|
return `${geneticPercentText(t, "size")} / ${now.toFixed(2)}\u2192${adult.toFixed(2)} (${growth}%)`;
|
|
}
|
|
|
|
function personalityDailyChangeHtml(t) {
|
|
const state = typeof personalityDailyState === "function" ? personalityDailyState(t) : (t?.personalityDaily || { day: world?.day || 1, byKey: {}, byCause: {} });
|
|
const dayLabel = `D${state.day || world?.day || 1}`;
|
|
const causeMap = new Map();
|
|
const addCauseChange = (cause, key, up, down) => {
|
|
const cleanCause = personalityDisplayCauseText(cause || "") || "\u8981\u56e0\u4e0d\u660e";
|
|
if (!causeMap.has(cleanCause)) causeMap.set(cleanCause, new Map());
|
|
const byKey = causeMap.get(cleanCause);
|
|
const current = byKey.get(key) || { up: 0, down: 0 };
|
|
current.up += Math.max(0, Number(up) || 0);
|
|
current.down += Math.max(0, Number(down) || 0);
|
|
byKey.set(key, current);
|
|
};
|
|
for (const [cause, entry] of Object.entries(state.byCause || {})) {
|
|
for (const key of PERSONALITY_KEYS) {
|
|
const v = entry?.byKey?.[key] || {};
|
|
const up = Math.max(0, Number(v.up) || 0);
|
|
const down = Math.max(0, Number(v.down) || 0);
|
|
if (up > 0.0001 || down > 0.0001) addCauseChange(cause, key, up, down);
|
|
}
|
|
}
|
|
if (!causeMap.size) {
|
|
for (const key of PERSONALITY_KEYS) {
|
|
const entry = state.byKey?.[key] || {};
|
|
const up = Math.max(0, Number(entry.up) || 0);
|
|
const down = Math.max(0, Number(entry.down) || 0);
|
|
if (up <= 0.0001 && down <= 0.0001) continue;
|
|
const causes = Array.isArray(entry.causes) ? [...new Set(entry.causes.map(c => personalityDisplayCauseText(c)).filter(Boolean))].slice(0, 4) : [];
|
|
for (const cause of causes.length ? causes : ["\u8981\u56e0\u4e0d\u660e"]) addCauseChange(cause, key, up, down);
|
|
}
|
|
}
|
|
const rows = [];
|
|
for (const [cause, byKey] of causeMap.entries()) {
|
|
const parts = [];
|
|
for (const key of PERSONALITY_KEYS) {
|
|
const entry = byKey.get(key);
|
|
if (!entry) continue;
|
|
const up = Math.max(0, Number(entry.up) || 0);
|
|
const down = Math.max(0, Number(entry.down) || 0);
|
|
if (up > 0.0001) parts.push(`${personalityShortLabel(key)} +${up.toFixed(3)}`);
|
|
if (down > 0.0001) parts.push(`${personalityShortLabel(key)} -${down.toFixed(3)}`);
|
|
}
|
|
if (parts.length) rows.push(`<li><span>${escapeHtml(cause)}</span><strong>${escapeHtml(parts.join(" / "))}</strong><em>${escapeHtml(dayLabel)}</em></li>`);
|
|
}
|
|
return rows.join("") || `<li><span>\u8981\u56e0</span><strong>\u5909\u5316\u306a\u3057</strong><em>${escapeHtml(dayLabel)}</em></li>`;
|
|
}
|
|
|
|
function personalityDailySnapshot(t) {
|
|
const state = typeof personalityDailyState === "function" ? personalityDailyState(t) : (t?.personalityDaily || {});
|
|
const keyPart = PERSONALITY_KEYS.map(key => {
|
|
const entry = state.byKey?.[key] || {};
|
|
const causes = Array.isArray(entry.causes) ? entry.causes.join("/") : "";
|
|
return `${key}:${Number(entry.up || 0).toFixed(4)}:${Number(entry.down || 0).toFixed(4)}:${causes}`;
|
|
}).join(",");
|
|
const causePart = Object.entries(state.byCause || {}).map(([cause, entry]) => {
|
|
const values = PERSONALITY_KEYS.map(key => {
|
|
const v = entry?.byKey?.[key] || {};
|
|
return `${key}:${Number(v.up || 0).toFixed(4)}:${Number(v.down || 0).toFixed(4)}`;
|
|
}).join(";");
|
|
return `${cause}=${values}`;
|
|
}).join("|");
|
|
return `${keyPart}#${causePart}`;
|
|
}
|
|
|
|
function recentChangeCausesHtml(t) {
|
|
const rows = Array.isArray(t?.recentChangeCauses) ? t.recentChangeCauses.slice(0, 8) : [];
|
|
if (!rows.length) return `<li><span>-</span><strong>\u6700\u8fd1\u306e\u5909\u5316\u306a\u3057</strong></li>`;
|
|
return rows.map(entry => {
|
|
const timeText = world.clockStringFromTime ? world.clockStringFromTime(entry.time || 0) : formatRecordTime(entry.time || 0);
|
|
const reason = String(entry.reason || "\u5909\u5316");
|
|
const target = String(entry.target || "\u72b6\u614b");
|
|
const value = entry.value === "" || entry.value == null ? "" : ` ${entry.value}`;
|
|
return `<li><span>${escapeHtml(timeText)}</span><strong>${escapeHtml(reason)} / ${escapeHtml(target)}${escapeHtml(value)}</strong></li>`;
|
|
}).join("");
|
|
}
|
|
|
|
function selectedDataCategoryHtml(id, label, bodyHtml) {
|
|
if (!uiCache.selectedCollapsedCategories) uiCache.selectedCollapsedCategories = new Set();
|
|
const collapsed = uiCache.selectedCollapsedCategories.has(id);
|
|
return `<section class="selected-category${collapsed ? " collapsed" : ""}" data-selected-category="${escapeHtml(id)}">
|
|
<button class="selected-category-toggle" type="button" aria-expanded="${collapsed ? "false" : "true"}">${escapeHtml(label)}</button>
|
|
<div class="selected-category-body">${bodyHtml}</div>
|
|
</section>`;
|
|
}
|
|
|
|
function selectedInfoRowHtml(label, valueHtml, metric = "") {
|
|
const metricAttr = metric ? ` data-metric="${escapeHtml(metric)}"` : "";
|
|
return `<div class="info-row"${metricAttr}><span>${escapeHtml(label)}</span><strong>${valueHtml}</strong></div>`;
|
|
}
|
|
|
|
function selectedNeedRowsHtml(t) {
|
|
const system = window.TarinaiNeedSystem;
|
|
const keys = system?.NEED_KEYS || ["food", "sleep", "health", "safety", "social", "fulfill"];
|
|
const labels = system?.NEED_LABELS || {
|
|
food: "\u6442\u990c",
|
|
sleep: "\u7761\u7720",
|
|
health: "\u5065\u5eb7",
|
|
safety: "\u5b89\u5168",
|
|
social: "\u95a2\u4fc2",
|
|
fulfill: "\u5145\u8db3",
|
|
};
|
|
const active = t?.intent?.need || t?.need || "";
|
|
return keys.map(key => {
|
|
const value = system?.getNeedDisplayValue ? system.getNeedDisplayValue(t?.needs?.[key] || 0) : Math.round((t?.needs?.[key] || 0) / 10);
|
|
const mark = key === active ? ` <span class="selected-need-mark">\u25b2</span>` : "";
|
|
return `<div class="info-row selected-need-row${key === active ? " active" : ""}" data-need="${escapeHtml(key)}"><span>${escapeHtml(labels[key] || key)}</span><strong>${escapeHtml(value)}${mark}</strong></div>`;
|
|
}).join("");
|
|
}
|
|
|
|
function renderSelected() {
|
|
const t = world.selected;
|
|
if (!t || t.dead || !world.tarinai.includes(t)) {
|
|
ui.selectedCard?.classList.add("hidden");
|
|
if (uiCache.selectedEmpty) return;
|
|
uiCache.selectedSnapshot = "";
|
|
uiCache.selectedEmpty = true;
|
|
ui.selectedInfo.className = "selected-info empty";
|
|
ui.selectedInfo.textContent = "\u672a\u9078\u629e";
|
|
return;
|
|
}
|
|
|
|
ui.selectedCard?.classList.remove("hidden");
|
|
uiCache.selectedEmpty = false;
|
|
const activeNameInput = document.activeElement?.matches?.("[data-selected-name]") && document.activeElement?.dataset?.selectedId === t.id;
|
|
if (activeNameInput) return;
|
|
const rel = t.relationSummary ? t.relationSummary() : {};
|
|
const generationDisplay = t.displayGeneration ? t.displayGeneration() : ((t.hasPaired || t.parents?.length || t.children?.length) ? t.generation : "");
|
|
ensurePersonality(t);
|
|
const personalityTags = t.personalityTags ? t.personalityTags() : getPersonalityTraitTags(t);
|
|
const personalitySnapshot = [
|
|
...PERSONALITY_KEYS.map(key => personalityValueText(t.birthPersonality?.[key])),
|
|
...PERSONALITY_KEYS.map(key => personalityValueText(t.currentPersonality?.[key])),
|
|
personalityTags.join(",")
|
|
].join(",");
|
|
const itemEffectSummary = t.activeItemEffectSummary ? t.activeItemEffectSummary() : [];
|
|
const snapshot = [
|
|
t.id, t.type, t.name, generationDisplay, t.state, t.thought, targetLabel(t.target),
|
|
fmt(t.age), fmt(t.lifeSpan), fmt(t.hunger), fmt(t.loneliness),
|
|
fmt(t.energy), fmt(t.stress), fmt(t.mood),
|
|
t.parentNames?.join("+") || "", t.children?.length || 0, fmt(t.lack),
|
|
relationEntries(t, "friend").map(r => `${r.id}:${fmt(r.score)}:${r.name}`).join(","), relationEntries(t, "enemy").map(r => `${r.id}:${fmt(r.score)}:${r.name}`).join(","), fightWinRateLabel(t),
|
|
t.isZunchiSlave ? 1 : 0, fmt(t.totalFightLosses || 0), fmt(t.loveMochiTimer || 0), fmt(t.fightMochiTimer || 0),
|
|
t.zunchiDisease ? 1 : 0, fmt(t.zunchiDiseaseSeverity || 0), fmt(t.zunchiStain || 0),
|
|
t.sleepDisease ? 1 : 0, t.explosionDisease ? 1 : 0, fmt(t.explosionDiseaseTimer || 0), t.fightDisease ? 1 : 0, t.favorite ? 1 : 0,
|
|
JSON.stringify(t.needs || {}), t.intent?.need || "", t.intent?.actionId || "", t.intent?.actionLabel || "", t.intent?.reasonText || "",
|
|
personalitySnapshot, personalityDailySnapshot(t),
|
|
["life", "attack", "speed", "size"].map(key => geneticPercentText(t, key)).join(","), fmt((t.growth || 0) * 100),
|
|
itemEffectSummary.map(e => `${e.id}:${e.detail}`).join(","),
|
|
(t.recentChangeCauses || []).slice(0, 8).map(c => `${Math.round(c.time || 0)}:${c.reason || ""}:${c.target || ""}:${c.value ?? ""}`).join("~"),
|
|
(t.records || []).slice(0, 6).map(r => `${Math.round(r.time || 0)}:${r.kind || ""}:${r.text || ""}`).join("~")
|
|
].join("|");
|
|
if (uiCache.selectedSnapshot === snapshot) return;
|
|
uiCache.selectedSnapshot = snapshot;
|
|
|
|
ui.selectedInfo.className = "selected-info";
|
|
const effectBadges = [];
|
|
if (t.isZunchiSlave) effectBadges.push(`<span class="badge">\u305a\u3093\u3061\u3069\u308c\u3044</span>`);
|
|
if (t.favorite) effectBadges.push(`<span class="badge">\u2605</span>`);
|
|
if (t.zunchiDisease) effectBadges.push(`<span class="badge">\u305a\u3093\u3061\u75c5 ${fmt((t.zunchiDiseaseSeverity || 0) * 100)}</span>`);
|
|
if (t.sleepDisease) effectBadges.push(`<span class="badge">\u306d\u3080\u308a\u75c5</span>`);
|
|
if (t.explosionDisease) effectBadges.push(`<span class="badge">\u7206\u767a\u75c5 ${fmt(t.explosionDiseaseTimer || 0)}</span>`);
|
|
if (t.fightDisease) effectBadges.push(`<span class="badge">\u304d\u305a\u3064\u304d\u75c5</span>`);
|
|
if ((t.loveMochiTimer || 0) > 0.1) effectBadges.push(`<span class="badge">\u3078\u3053\u9905 ${fmt(t.loveMochiTimer)}</span>`);
|
|
if ((t.fightMochiTimer || 0) > 0.1) effectBadges.push(`<span class="badge">\u3051\u3093\u304b\u9905 ${fmt(t.fightMochiTimer)}</span>`);
|
|
const personalityTagHtml = personalityTags.length
|
|
? personalityTags.map(tag => `<span class="personality-tag">${escapeHtml(tag)}</span>`).join("")
|
|
: `<span class="personality-tag neutral">\u4e2d\u7acb</span>`;
|
|
const recentChangeHtml = personalityDailyChangeHtml(t);
|
|
const personalityHtml = `
|
|
<div class="info-row personality-values"><span>\u6027\u683c\u30bf\u30b0</span><strong>${personalityTagHtml}</strong></div>
|
|
<div class="info-row personality-values"><span>\u8a95\u751f\u6642\u6027\u683c</span><strong>${personalityRowsHtml(t.birthPersonality)}</strong></div>
|
|
<div class="info-row personality-values"><span>\u73fe\u5728\u6027\u683c</span><strong>${personalityRowsHtml(t.currentPersonality)}</strong></div>
|
|
<div class="info-row selected-personality-changes"><span>\u4eca\u65e5\u306e\u5909\u5316</span><strong><ol class="selected-change-list">${recentChangeHtml}</ol></strong></div>
|
|
`;
|
|
const recordHtml = (t.records || []).slice(0, 8).map(r => {
|
|
const timeText = world.clockString ? (world.clockStringFromTime ? world.clockStringFromTime(r.time || 0) : formatRecordTime(r.time || 0)) : formatRecordTime(r.time || 0);
|
|
const recordText = quoteNameInRecordText(r.text || "", t.name);
|
|
return `<li><span>${escapeHtml(timeText)}</span><strong>${escapeHtml(recordText)}</strong></li>`;
|
|
}).join("") || `<li><span>-</span><strong>\u8a18\u9332\u306a\u3057</strong></li>`;
|
|
const recentCauseHtml = recentChangeCausesHtml(t);
|
|
const basicHtml = `
|
|
<div class="info-row"><span>\u4e16\u4ee3</span><strong>${escapeHtml(generationDisplay || "\u4e0d\u660e")}</strong></div>
|
|
<div class="info-row"><span>\u72b6\u614b</span><strong>${escapeHtml(stateLabel(t.state))}</strong></div>
|
|
<div class="info-row"><span>\u3044\u307e</span><strong>${escapeHtml(t.intent?.actionLabel || stateLabel(t.state))}</strong></div>
|
|
<div class="info-row"><span>\u7406\u7531</span><strong>${escapeHtml(t.intent?.reasonText || reasonLabel(t))}</strong></div>
|
|
<div class="info-row"><span>\u5bfe\u8c61</span><strong>${escapeHtml(targetLabel(t.target))}</strong></div>
|
|
<div class="info-row"><span>\u5e74\u9f62</span><strong>${fmt(t.age)} / ${fmt(t.lifeSpan)}</strong></div>
|
|
<div class="info-row"><span>\u6700\u5927\u5bff\u547d</span><strong>${geneticPercentText(t, "life")}</strong></div>
|
|
<div class="info-row"><span>\u653b\u6483\u529b</span><strong>${geneticPercentText(t, "attack")}</strong></div>
|
|
<div class="info-row"><span>\u79fb\u52d5\u901f\u5ea6</span><strong>${geneticPercentText(t, "speed")}</strong></div>
|
|
<div class="info-row"><span>\u30b5\u30a4\u30ba</span><strong>${geneticSizeText(t)}</strong></div>
|
|
`;
|
|
const healthHtml = selectedNeedRowsHtml(t) + selectedInfoRowHtml("\u30b9\u30c8\u30ec\u30b9", fmt(t.stress), "stress");
|
|
const itemEffectHtml = itemEffectSummary.length
|
|
? itemEffectSummary.map(e => `<div class="info-row"><span>${escapeHtml(e.label)}</span><strong>${escapeHtml(e.detail)}</strong></div>`).join("")
|
|
: `<div class="info-row"><span>\u30a2\u30a4\u30c6\u30e0\u52b9\u679c</span><strong>\u306a\u3057</strong></div>`;
|
|
const relationHtml = `
|
|
<div class="info-row relation-row relation-list-row"><span>\u53cb\u9054</span><strong>${relationListHtml(t, "friend")}</strong></div>
|
|
<div class="info-row relation-row relation-list-row"><span>\u6575\u5bfe</span><strong>${relationListHtml(t, "enemy")}</strong></div>
|
|
<div class="info-row"><span>\u3051\u3093\u304b\u52dd\u7387</span><strong>${escapeHtml(fightWinRateLabel(t))}</strong></div>
|
|
`;
|
|
const familyHtml = `
|
|
<div class="info-row"><span>\u89aa</span><strong>${escapeHtml(t.parentNames?.length ? t.parentNames.join(" + ") : "\u4e0d\u660e")}</strong></div>
|
|
<div class="info-row"><span>\u5b50</span><strong>${t.children?.length || 0}</strong></div>
|
|
`;
|
|
ui.selectedInfo.innerHTML = `
|
|
<div class="selected-name-editor">
|
|
<input class="selected-name-input" data-selected-name data-selected-id="${escapeHtml(t.id)}" value="${escapeHtml(t.name)}" maxlength="18" aria-label="\u305f\u308a\u306a\u3044\u306e\u540d\u524d" title="\u540d\u524d\u3092\u7de8\u96c6">
|
|
</div>
|
|
${effectBadges.length ? `<div class="selected-effects">${effectBadges.join("")}</div>` : ""}
|
|
<div class="selected-data-groups">
|
|
${selectedDataCategoryHtml("basic", "\u57fa\u672c", basicHtml)}
|
|
${selectedDataCategoryHtml("health", "\u4f53\u8abf", healthHtml)}
|
|
${selectedDataCategoryHtml("effects", "\u30a2\u30a4\u30c6\u30e0\u52b9\u679c", itemEffectHtml)}
|
|
${selectedDataCategoryHtml("personality", "\u6027\u683c", personalityHtml)}
|
|
${selectedDataCategoryHtml("relation", "\u95a2\u4fc2", relationHtml)}
|
|
${selectedDataCategoryHtml("family", "\u5bb6\u65cf", familyHtml)}
|
|
${selectedDataCategoryHtml("records", "\u8a18\u9332", `<div class="selected-records"><h3>\u6700\u8fd1\u306e\u5909\u5316\u539f\u56e0</h3><ol class="selected-change-list">${recentCauseHtml}</ol><h3>\u500b\u4f53\u5225\u306e\u8a18\u9332</h3><ol>${recordHtml}</ol></div>`)}
|
|
</div>
|
|
<div class="selected-actions"><button type="button" class="favorite-toggle ${t.favorite ? "active" : ""}" data-selected-action="favorite">${t.favorite ? "\u2605 \u304a\u6c17\u306b\u5165\u308a\u89e3\u9664" : "\u2606 \u304a\u6c17\u306b\u5165\u308a"}</button></div>
|
|
`;
|
|
}
|
|
|
|
function formatRecordTime(time) {
|
|
const totalMinutes = Math.floor(((time || 0) % (CONFIG.dayLength || 120)) / (CONFIG.dayLength || 120) * 24 * 60);
|
|
const hh = String(Math.floor(totalMinutes / 60)).padStart(2, "0");
|
|
const mm = String(totalMinutes % 60).padStart(2, "0");
|
|
return `${hh}:${mm}`;
|
|
}
|
|
|
|
function stateLabel(s) {
|
|
return window.TEXT_CATALOG?.stateLabel?.(s) || s || "";
|
|
}
|
|
|
|
function reasonLabel(t) {
|
|
return window.TEXT_CATALOG?.reasonLabel?.(t) || "なし";
|
|
}
|
|
|
|
function targetLabel(target) {
|
|
return window.TEXT_CATALOG?.targetLabel?.(target) || "なし";
|
|
}
|
|
|
|
function fightWinRateLabel(t) {
|
|
const won = Number(t.totalFightWins || 0);
|
|
const lost = Number(t.totalFightLosses || 0);
|
|
const total = won + lost;
|
|
if (!total) return "\u306a\u3057";
|
|
return `${Math.round((won / total) * 100)}% (${won}\u52dd${lost}\u6557)`;
|
|
}
|
|
|
|
function setTextIfChanged(el, key, value) {
|
|
if (!el) return;
|
|
const next = String(value);
|
|
if (uiCache.stats[key] === next) return;
|
|
uiCache.stats[key] = next;
|
|
el.textContent = next;
|
|
}
|
|
|
|
function setValueIfChanged(el, key, value) {
|
|
if (!el) return;
|
|
if (uiCache.stats[key] === value) return;
|
|
uiCache.stats[key] = value;
|
|
el.value = value;
|
|
}
|
|
|
|
function escapeHtml(value) {
|
|
const helper = window.TarinaiUIHelpers?.htmlEscape;
|
|
if (typeof helper === "function" && helper !== escapeHtml) return helper(value);
|
|
return String(value ?? "").replace(/[&<>"']/g, c => ({
|
|
"&": "&",
|
|
"<": "<",
|
|
">": ">",
|
|
'"': """,
|
|
"'": "'"
|
|
}[c] || c));
|
|
}
|
|
|
|
function escapeRegExp(value) {
|
|
return String(value ?? "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
}
|
|
|
|
function formatLogDisplayText(text) {
|
|
let out = String(text ?? "");
|
|
const names = new Set();
|
|
for (const t of world?.tarinai || []) if (t?.name) names.add(t.name);
|
|
for (const n of Object.values(world?.family || {})) if (n?.name) names.add(n.name);
|
|
const sorted = [...names].filter(Boolean).sort((a, b) => b.length - a.length);
|
|
for (const name of sorted) {
|
|
const re = new RegExp(`(^|[^\u300c])(${escapeRegExp(name)})(?=[\u3068\u306f\u304c\u3092\u306b\u306e\u3082\u3078\u304b\u3089\u3067\u3001\u3002]|$)`, "g");
|
|
out = out.replace(re, (m, prefix, found) => `${prefix}\u300c${found}\u300d`);
|
|
}
|
|
return out;
|
|
}
|