tarinai/js/ui_log.js
2026-07-20 14:36:59 +09:00

238 lines
9.2 KiB
JavaScript

"use strict";
const LOG_EVENT_CATEGORIES = Object.freeze(["life", "danger", "social", "living", "environment", "observe"]);
const LOG_EVENT_CATEGORY_LABELS = Object.freeze({
life: "\u751f\u547d",
danger: "\u5371\u967a",
social: "\u4ea4\u6d41",
living: "\u751f\u6d3b",
environment: "\u74b0\u5883",
observe: "\u89b3\u5bdf",
});
const LOG_KIND_TO_EVENT_CATEGORY = Object.freeze({
birth: "life",
death: "life",
accident: "danger",
danger: "danger",
fight: "danger",
relation: "social",
food: "living",
event: "living",
weather: "environment",
grass: "environment",
observe: "observe",
note: "observe",
});
function logEventCategoryForKind(kind = "note") {
return LOG_KIND_TO_EVENT_CATEGORY[String(kind || "note")] || "observe";
}
function logEventCategoryLabel(category = "observe") {
return LOG_EVENT_CATEGORY_LABELS[String(category || "observe")] || LOG_EVENT_CATEGORY_LABELS.observe;
}
function allLogEventCategories() {
return new Set(LOG_EVENT_CATEGORIES);
}
function loadLogPushSettings() {
try {
const raw = localStorage.getItem("tarinai_event_category_settings_v1");
if (raw) {
const parsed = JSON.parse(raw);
uiCache.logPushEnabled = Boolean(parsed.pushEnabled ?? parsed.enabled);
if (Array.isArray(parsed.categories)) {
const categories = parsed.categories.filter(category => LOG_EVENT_CATEGORIES.includes(category));
uiCache.logEventCategories = new Set(categories);
}
return;
}
} catch (_) {}
}
function saveLogPushSettings() {
try {
localStorage.setItem("tarinai_event_category_settings_v1", JSON.stringify({
pushEnabled: Boolean(uiCache.logPushEnabled),
categories: Array.from(uiCache.logEventCategories || []),
}));
} catch (_) {}
}
function syncLogPushControls() {
if (ui.logPushEnabled) ui.logPushEnabled.checked = Boolean(uiCache.logPushEnabled);
for (const input of ui.logPushControls?.querySelectorAll("[data-event-category]") || []) {
input.checked = uiCache.logEventCategories?.has(input.dataset.eventCategory || "") ?? false;
}
}
function syncAudioControls() {
if (!window.TarinaiAudio) return;
for (const input of ui.soundCategoryControls?.querySelectorAll("[data-sound-category]") || []) {
const category = input.dataset.soundCategory || "";
input.checked = audio.categoryOn ? audio.categoryOn(category) : true;
input.disabled = !audio.enabled;
}
if (ui.soundMasterToggle) ui.soundMasterToggle.checked = Boolean(audio.enabled);
if (ui.soundBtn) {
ui.soundBtn.textContent = audio.enabled ? "\u97f3 ON" : "\u97f3 OFF";
ui.soundBtn.classList.toggle("active", Boolean(audio.enabled));
ui.soundBtn.setAttribute("aria-expanded", ui.soundPanel && !ui.soundPanel.classList.contains("hidden") ? "true" : "false");
}
}
function closeSoundPanel() {
ui.soundPanel?.classList.add("hidden");
syncAudioControls();
}
function toggleSoundPanel() {
if (audio.enabled) audio.uiClick?.();
ui.soundPanel?.classList.toggle("hidden");
syncAudioControls();
}
function pushLogNotification(entry = {}) {
if (!uiCache.logPushEnabled || !entry || entry.hiddenFromObservation) return;
const kind = entry.kind || "note";
const category = logEventCategoryForKind(kind);
if (!uiCache.logEventCategories?.has(category)) return;
const overlay = ui.logPushOverlay;
if (!overlay) return;
audio.notify?.();
const card = document.createElement("div");
card.className = `log-push-toast log-push-${category}`;
card.dataset.eventCategory = category;
const tag = document.createElement("span");
tag.className = "log-push-tag";
tag.textContent = logEventCategoryLabel(category);
const time = document.createElement("span");
time.className = "log-push-time";
time.textContent = formatLogTime(entry.time || 0);
const text = document.createElement("div");
text.className = "log-push-text";
text.textContent = formatLogDisplayText(entry.text || "");
card.appendChild(tag);
card.appendChild(time);
card.appendChild(text);
overlay.prepend(card);
while (overlay.children.length > 5) overlay.lastElementChild?.remove();
window.setTimeout(() => {
card.classList.add("leaving");
window.setTimeout(() => card.remove(), 220);
}, 5200);
}
window.pushLogNotification = pushLogNotification;
function shouldShowInEventLog(entry = {}) {
if (!entry || entry.hiddenFromObservation) return false;
const type = String(entry.eventType || "");
const text = String(entry.text || "");
if (type === "player_item_placement" || type.includes("player") || type.includes("tool") || type.includes("robot")) return false;
if (/\u30ed\u30dc\u6383\u9664\u6a5f|\u6383\u9664\u6a5f/.test(text)) return false;
const category = logEventCategoryForKind(entry.kind || "note");
return uiCache.logEventCategories?.has(category) ?? true;
}
function renderLog(logs) {
ui.log.innerHTML = "";
const frag = document.createDocumentFragment();
const touchFirst = Boolean(window.TarinaiInputMode.classification?.touchFirst || document.body.classList.contains("touch-mobile-ui") || window.innerWidth <= 980);
const limit = touchFirst ? 34 : 40;
const visibleLogs = Array.isArray(logs)
? logs.map(raw => typeof raw === "string" ? { time: world.time, text: raw, kind: "note" } : raw).filter(shouldShowInEventLog)
: [];
const rows = visibleLogs.slice(0, limit);
for (const entry of rows) {
const kind = entry.kind || "note";
const category = logEventCategoryForKind(kind);
const div = document.createElement("div");
div.className = `log-entry log-${category}`;
div.dataset.eventCategory = category;
const tag = document.createElement("span");
tag.className = "log-tag";
tag.textContent = logEventCategoryLabel(category);
const time = document.createElement("span");
time.className = "time";
time.textContent = formatLogTime(entry.time || 0);
div.appendChild(tag);
div.appendChild(time);
div.append(document.createTextNode(formatLogDisplayText(entry.text || "")));
frag.appendChild(div);
}
ui.log.appendChild(frag);
}
function formatLogTime(timeValue) {
const t = Math.max(0, Number(timeValue) || 0);
const day = Math.floor(t / CONFIG.dayLength) + 1;
const dayT = ((t % CONFIG.dayLength) + CONFIG.dayLength) % CONFIG.dayLength;
const totalMinutes = Math.floor(dayT / CONFIG.dayLength * 24 * 60);
const hh = String(Math.floor(totalMinutes / 60)).padStart(2, "0");
const mm = String(totalMinutes % 60).padStart(2, "0");
return `${day}\u65e5 ${hh}\u6642${mm}\u5206`;
}
function logKindLabel(kind) {
return logEventCategoryLabel(logEventCategoryForKind(kind));
}
function tarinaiSpritePathForId(id) {
const live = world?.tarinai?.find(t => (t.id === id || t.familyKey === id) && !t.dead);
const sid = live?.spriteId ? live.spriteId() : live?.type;
const fam = world?.family?.[id];
const fallback = fam?.type;
return (SPRITES.find(s => s.id === (sid || fallback)) || SPRITES[0]).path;
}
function relationIconHtml(id) {
if (!id) return "";
return `<img class="relation-face" src="${tarinaiSpritePathForId(id)}" alt="">`;
}
function relationEntries(t, kind = "friend") {
const rows = [];
const liveIds = new Set((world?.tarinai || []).filter(o => o && !o.dead && o !== t).map(o => o.id));
const ids = new Set(Object.keys(t?.relationships || {}));
for (const other of world?.tarinai || []) {
if (other && other !== t && other.id && other.relationships?.[t?.id]) ids.add(other.id);
}
for (const id of ids) {
if (!liveIds.has(id)) continue;
const rel = t?.relationships?.[id] || relationDefaults();
const other = world?.tarinai?.find(o => o && o.id === id) || null;
const reverse = other?.relationships?.[t?.id] || null;
const score = kind === "enemy" ? Math.max(rel.fear || 0, reverse?.fear || 0) : Math.max(rel.affinity || 0, reverse?.affinity || 0);
const threshold = kind === "enemy" ? 0.5 : FRIEND_AFFINITY_THRESHOLD;
if (score <= threshold) continue;
rows.push({ id, score, name: relationDisplayName(world, id) });
}
rows.sort((a, b) => b.score - a.score || a.name.localeCompare(b.name, "ja"));
return rows;
}
function relationListHtml(t, kind = "friend") {
const rows = relationEntries(t, kind);
if (!rows.length) return "\u306a\u3057";
const sign = kind === "friend" ? "+" : "";
return `<div class="relation-chip-list">${rows.map(r => `
<button type="button" class="relation-chip ${kind === "enemy" ? "enemy" : "friend"}" data-focus-tarinai-id="${window.TarinaiUIHelpers.htmlEscape(r.id)}" title="${window.TarinaiUIHelpers.htmlEscape(r.name)}\u3078\u79fb\u52d5">
${relationIconHtml(r.id)}
<span class="relation-chip-name">${window.TarinaiUIHelpers.htmlEscape(r.name)}</span>
<em>${sign}${fmt(r.score)}</em>
</button>`).join("")}</div>`;
}
function focusTarinaiById(id) {
const t = world?.tarinai?.find(o => o && !o.dead && (o.id === id || o.familyKey === id));
if (!t) { showToast("\u305d\u306e\u500b\u4f53\u306f\u3082\u3046\u9078\u629e\u3067\u304d\u307e\u305b\u3093\u3002"); return false; }
window.TarinaiCommands.dispatch(world, { type: "selection.focus", target: t, options: { pulse: 1.25 } });
uiCache.selectedSnapshot = "";
renderSelected();
renderStats();
render?.();
showToast(`\u300c${t.name}\u300d\u3078\u79fb\u52d5\u3057\u307e\u3057\u305f\u3002`);
return true;
}