This commit is contained in:
33333-33333 2026-06-21 14:09:35 +09:00
commit 37087d08a7
189 changed files with 12864 additions and 9589 deletions

152
js/ui_family_data.js Normal file
View file

@ -0,0 +1,152 @@
"use strict";
function buildArchiveRows(family) {
return liveFamilyComponents(family);
}
function lineageEdgeFamily(family = world.family || {}) {
return family || {};
}
let lineageRelationIndexCache = { family: null, version: null, index: null };
function lineageBuildRelationIndex(family) {
const known = new Set(Object.keys(family || {}));
const parentsByChild = new Map();
const childrenByParent = new Map();
const addParent = (childId, parentId) => {
if (!childId || !parentId || childId === parentId || !known.has(childId) || !known.has(parentId)) return;
if (!parentsByChild.has(childId)) parentsByChild.set(childId, new Set());
if (!childrenByParent.has(parentId)) childrenByParent.set(parentId, new Set());
parentsByChild.get(childId).add(parentId);
childrenByParent.get(parentId).add(childId);
};
for (const n of Object.values(family || {})) {
if (!n?.id) continue;
for (const parentId of n.parents || []) addParent(n.id, parentId);
for (const childId of n.children || []) addParent(childId, n.id);
}
return { parentsByChild, childrenByParent };
}
function lineageRelationIndex(family = world.family || {}) {
family = lineageEdgeFamily(family);
if (family !== (world.family || {})) return lineageBuildRelationIndex(family);
const version = world.familyVersion || 0;
if (lineageRelationIndexCache.family !== family || lineageRelationIndexCache.version !== version) {
lineageRelationIndexCache = { family, version, index: lineageBuildRelationIndex(family) };
}
return lineageRelationIndexCache.index;
}
function lineageMutualParentIds(n, family = world.family || {}, idSet = null) {
const seen = new Set();
const parents = [];
if (!n?.id) return parents;
const relationIndex = lineageRelationIndex(family);
const candidates = new Set((n.parents || []).filter(Boolean));
// Runtime updates can briefly touch one side first; recover the reciprocal
// edge from the relation index so the archive does not flicker.
for (const parentId of relationIndex.parentsByChild.get(n.id) || []) candidates.add(parentId);
for (const id of candidates) {
if (!id || id === n.id || seen.has(id)) continue;
if (idSet && !idSet.has(id)) continue;
const parent = family[id];
if (!parent) continue;
const childListsParent = Array.isArray(n.parents) && n.parents.includes(id);
const parentListsChild = Array.isArray(parent.children) && parent.children.includes(n.id);
if (!childListsParent && !parentListsChild) continue;
seen.add(id);
parents.push(id);
}
parents.sort((a, b) => lineageNodeSort(family[a], family[b]));
return parents;
}
function lineageMutualChildIds(n, family = world.family || {}, idSet = null) {
const seen = new Set();
const children = [];
if (!n?.id) return children;
const relationIndex = lineageRelationIndex(family);
const candidates = new Set((n.children || []).filter(Boolean));
// Same recovery as above, in the opposite direction.
for (const childId of relationIndex.childrenByParent.get(n.id) || []) candidates.add(childId);
for (const id of candidates) {
if (!id || id === n.id || seen.has(id)) continue;
if (idSet && !idSet.has(id)) continue;
const child = family[id];
if (!child) continue;
const parentListsChild = Array.isArray(n.children) && n.children.includes(id);
const childListsParent = Array.isArray(child.parents) && child.parents.includes(n.id);
if (!parentListsChild && !childListsParent) continue;
seen.add(id);
children.push(id);
}
children.sort((a, b) => lineageNodeSort(family[a], family[b]));
return children;
}
function lineageHasActualRelation(n, family = world.family || {}) {
return lineageMutualParentIds(n, family).length > 0 || lineageMutualChildIds(n, family).length > 0;
}
function lineageRelatedIdSet(family) {
family = lineageEdgeFamily(family);
const relationIndex = lineageRelationIndex(family);
const ids = new Set();
for (const n of Object.values(family || {}).filter(Boolean)) {
const parents = relationIndex.parentsByChild.get(n.id) || [];
const children = relationIndex.childrenByParent.get(n.id) || [];
if (parents.size || children.size) ids.add(n.id);
for (const id of parents) ids.add(id);
for (const id of children) ids.add(id);
}
return ids;
}
function liveFamilyComponents(family) {
// Childless pairs, including pairs currently performing the birth ritual, are
// intentionally excluded. A \u5bb6\u7cfb\u56f3 node is created only after an actual parent/child
// relation exists; otherwise ritual participants become isolated pseudo-families.
family = lineageEdgeFamily(family);
const relatedIds = lineageRelatedIdSet(family);
const nodes = Array.from(relatedIds).map(id => family[id]).filter(Boolean);
const ids = new Set(nodes.map(n => n.id));
const graph = new Map(nodes.map(n => [n.id, new Set()]));
const addEdge = (a, b) => {
if (!a || !b || !ids.has(a) || !ids.has(b)) return;
graph.get(a).add(b);
graph.get(b).add(a);
};
for (const n of nodes) {
for (const p of lineageMutualParentIds(n, family, ids)) addEdge(n.id, p);
for (const c of lineageMutualChildIds(n, family, ids)) addEdge(n.id, c);
}
const seen = new Set();
const comps = [];
for (const n of nodes.sort(lineageNodeSort)) {
if (seen.has(n.id)) continue;
const stack = [n.id];
const comp = [];
seen.add(n.id);
while (stack.length) {
const id = stack.pop();
const item = family[id];
if (item) comp.push(item);
for (const next of graph.get(id) || []) {
if (seen.has(next)) continue;
seen.add(next);
stack.push(next);
}
}
if (comp.some(x => x.alive)) comps.push(comp);
}
comps.sort((a, b) => {
const ag = Math.min(...a.map(n => n.generation || 1));
const bg = Math.min(...b.map(n => n.generation || 1));
const at = Math.min(...a.map(n => n.birthTime || 0));
const bt = Math.min(...b.map(n => n.birthTime || 0));
return ag - bg || at - bt || b.length - a.length;
});
return comps;
}