feat: add superlatives computation with tests
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,308 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { World } from '../../ecs/World.js';
|
||||
import { computeSuperlatives } from '../superlativesComputer.js';
|
||||
import { createBondRegistry, addBond, dissolveBond } from '../bondRegistry.js';
|
||||
import type { BondRegistry } from '../bondRegistry.js';
|
||||
import type { Relationships } from '@dflike/shared';
|
||||
|
||||
function setupNPC(
|
||||
world: World,
|
||||
name: string,
|
||||
relationships: [number, number, 'active' | 'memory'][],
|
||||
): number {
|
||||
const entity = world.createEntity();
|
||||
world.addComponent(entity, 'name', name);
|
||||
world.addComponent(entity, 'npcBrain', {});
|
||||
const rels: Relationships = new Map();
|
||||
for (const [targetId, value, status] of relationships) {
|
||||
rels.set(targetId, { value, interactions: 5, lastInteractionTick: 0, status });
|
||||
}
|
||||
world.addComponent(entity, 'relationships', rels);
|
||||
return entity;
|
||||
}
|
||||
|
||||
describe('computeSuperlatives', () => {
|
||||
it('returns all nulls when no NPCs exist', () => {
|
||||
const world = new World();
|
||||
const registry = createBondRegistry();
|
||||
const result = computeSuperlatives(world, registry);
|
||||
expect(result.mostLoved).toBeNull();
|
||||
expect(result.mostPopular).toBeNull();
|
||||
expect(result.mostReviled).toBeNull();
|
||||
expect(result.mostAnnoying).toBeNull();
|
||||
expect(result.shyest).toBeNull();
|
||||
expect(result.mostOutgoing).toBeNull();
|
||||
expect(result.biggestHeartbreaker).toBeNull();
|
||||
expect(result.mostDevoted).toBeNull();
|
||||
expect(result.loneliest).toBeNull();
|
||||
expect(result.mostPolarizing).toBeNull();
|
||||
expect(result.socialButterfly).toBeNull();
|
||||
});
|
||||
|
||||
it('computes mostLoved (highest single incoming value)', () => {
|
||||
const world = new World();
|
||||
const registry = createBondRegistry();
|
||||
// A likes B at 70, A likes C at 90
|
||||
const a = setupNPC(world, 'Alice', []);
|
||||
const b = setupNPC(world, 'Bob', []);
|
||||
const c = setupNPC(world, 'Carol', []);
|
||||
// Set Alice's relationships
|
||||
world.getComponent<Relationships>(a, 'relationships')!.set(b, { value: 70, interactions: 5, lastInteractionTick: 0, status: 'active' });
|
||||
world.getComponent<Relationships>(a, 'relationships')!.set(c, { value: 90, interactions: 5, lastInteractionTick: 0, status: 'active' });
|
||||
|
||||
const result = computeSuperlatives(world, registry);
|
||||
expect(result.mostLoved).not.toBeNull();
|
||||
expect(result.mostLoved!.entityId).toBe(c);
|
||||
expect(result.mostLoved!.name).toBe('Carol');
|
||||
expect(result.mostLoved!.value).toBe(90);
|
||||
});
|
||||
|
||||
it('computes mostReviled (lowest single incoming value)', () => {
|
||||
const world = new World();
|
||||
const registry = createBondRegistry();
|
||||
const a = setupNPC(world, 'Alice', []);
|
||||
const b = setupNPC(world, 'Bob', []);
|
||||
const c = setupNPC(world, 'Carol', []);
|
||||
world.getComponent<Relationships>(a, 'relationships')!.set(b, { value: -30, interactions: 5, lastInteractionTick: 0, status: 'active' });
|
||||
world.getComponent<Relationships>(a, 'relationships')!.set(c, { value: -80, interactions: 5, lastInteractionTick: 0, status: 'active' });
|
||||
|
||||
const result = computeSuperlatives(world, registry);
|
||||
expect(result.mostReviled).not.toBeNull();
|
||||
expect(result.mostReviled!.entityId).toBe(c);
|
||||
expect(result.mostReviled!.value).toBe(-80);
|
||||
});
|
||||
|
||||
it('computes mostPopular (highest avg incoming, min 2 required)', () => {
|
||||
const world = new World();
|
||||
const registry = createBondRegistry();
|
||||
const a = setupNPC(world, 'Alice', []);
|
||||
const b = setupNPC(world, 'Bob', []);
|
||||
const c = setupNPC(world, 'Carol', []);
|
||||
// Both A and B like C
|
||||
world.getComponent<Relationships>(a, 'relationships')!.set(c, { value: 60, interactions: 5, lastInteractionTick: 0, status: 'active' });
|
||||
world.getComponent<Relationships>(b, 'relationships')!.set(c, { value: 80, interactions: 5, lastInteractionTick: 0, status: 'active' });
|
||||
|
||||
const result = computeSuperlatives(world, registry);
|
||||
expect(result.mostPopular).not.toBeNull();
|
||||
expect(result.mostPopular!.entityId).toBe(c);
|
||||
expect(result.mostPopular!.value).toBe(70); // (60+80)/2
|
||||
});
|
||||
|
||||
it('returns mostPopular null with only 1 incoming', () => {
|
||||
const world = new World();
|
||||
const registry = createBondRegistry();
|
||||
const a = setupNPC(world, 'Alice', []);
|
||||
const b = setupNPC(world, 'Bob', []);
|
||||
world.getComponent<Relationships>(a, 'relationships')!.set(b, { value: 90, interactions: 5, lastInteractionTick: 0, status: 'active' });
|
||||
|
||||
const result = computeSuperlatives(world, registry);
|
||||
expect(result.mostPopular).toBeNull();
|
||||
});
|
||||
|
||||
it('computes mostAnnoying (lowest avg incoming, min 2)', () => {
|
||||
const world = new World();
|
||||
const registry = createBondRegistry();
|
||||
const a = setupNPC(world, 'Alice', []);
|
||||
const b = setupNPC(world, 'Bob', []);
|
||||
const c = setupNPC(world, 'Carol', []);
|
||||
world.getComponent<Relationships>(a, 'relationships')!.set(c, { value: -40, interactions: 5, lastInteractionTick: 0, status: 'active' });
|
||||
world.getComponent<Relationships>(b, 'relationships')!.set(c, { value: -60, interactions: 5, lastInteractionTick: 0, status: 'active' });
|
||||
|
||||
const result = computeSuperlatives(world, registry);
|
||||
expect(result.mostAnnoying).not.toBeNull();
|
||||
expect(result.mostAnnoying!.entityId).toBe(c);
|
||||
expect(result.mostAnnoying!.value).toBe(-50); // (-40+-60)/2
|
||||
});
|
||||
|
||||
it('computes shyest (fewest active outgoing)', () => {
|
||||
const world = new World();
|
||||
const registry = createBondRegistry();
|
||||
const a = setupNPC(world, 'Alice', []);
|
||||
const b = setupNPC(world, 'Bob', []);
|
||||
const c = setupNPC(world, 'Carol', []);
|
||||
// Alice: 1 active outgoing, Bob: 2, Carol: 0
|
||||
world.getComponent<Relationships>(a, 'relationships')!.set(b, { value: 10, interactions: 5, lastInteractionTick: 0, status: 'active' });
|
||||
world.getComponent<Relationships>(b, 'relationships')!.set(a, { value: 10, interactions: 5, lastInteractionTick: 0, status: 'active' });
|
||||
world.getComponent<Relationships>(b, 'relationships')!.set(c, { value: 10, interactions: 5, lastInteractionTick: 0, status: 'active' });
|
||||
|
||||
const result = computeSuperlatives(world, registry);
|
||||
expect(result.shyest).not.toBeNull();
|
||||
expect(result.shyest!.entityId).toBe(c);
|
||||
expect(result.shyest!.value).toBe(0);
|
||||
});
|
||||
|
||||
it('computes mostOutgoing (most active outgoing)', () => {
|
||||
const world = new World();
|
||||
const registry = createBondRegistry();
|
||||
const a = setupNPC(world, 'Alice', []);
|
||||
const b = setupNPC(world, 'Bob', []);
|
||||
const c = setupNPC(world, 'Carol', []);
|
||||
world.getComponent<Relationships>(b, 'relationships')!.set(a, { value: 10, interactions: 5, lastInteractionTick: 0, status: 'active' });
|
||||
world.getComponent<Relationships>(b, 'relationships')!.set(c, { value: 20, interactions: 5, lastInteractionTick: 0, status: 'active' });
|
||||
world.getComponent<Relationships>(a, 'relationships')!.set(c, { value: 10, interactions: 5, lastInteractionTick: 0, status: 'active' });
|
||||
|
||||
const result = computeSuperlatives(world, registry);
|
||||
expect(result.mostOutgoing).not.toBeNull();
|
||||
expect(result.mostOutgoing!.entityId).toBe(b);
|
||||
expect(result.mostOutgoing!.name).toBe('Bob');
|
||||
expect(result.mostOutgoing!.value).toBe(2);
|
||||
});
|
||||
|
||||
it('computes biggestHeartbreaker (most former partner bonds)', () => {
|
||||
const world = new World();
|
||||
const registry = createBondRegistry();
|
||||
const a = setupNPC(world, 'Alice', []);
|
||||
const b = setupNPC(world, 'Bob', []);
|
||||
const c = setupNPC(world, 'Carol', []);
|
||||
const d = setupNPC(world, 'Dave', []);
|
||||
// Alice had former partners with B, C, and D
|
||||
addBond(registry, a, b, 'partner', 10);
|
||||
dissolveBond(registry, a, b, 'partner');
|
||||
addBond(registry, a, c, 'partner', 20);
|
||||
dissolveBond(registry, a, c, 'partner');
|
||||
addBond(registry, a, d, 'partner', 30);
|
||||
dissolveBond(registry, a, d, 'partner');
|
||||
// Bob had former partner with C
|
||||
addBond(registry, b, c, 'partner', 40);
|
||||
dissolveBond(registry, b, c, 'partner');
|
||||
|
||||
const result = computeSuperlatives(world, registry);
|
||||
expect(result.biggestHeartbreaker).not.toBeNull();
|
||||
expect(result.biggestHeartbreaker!.entityId).toBe(a);
|
||||
expect(result.biggestHeartbreaker!.value).toBe(3);
|
||||
});
|
||||
|
||||
it('returns biggestHeartbreaker null when no former partners', () => {
|
||||
const world = new World();
|
||||
const registry = createBondRegistry();
|
||||
setupNPC(world, 'Alice', []);
|
||||
setupNPC(world, 'Bob', []);
|
||||
|
||||
const result = computeSuperlatives(world, registry);
|
||||
expect(result.biggestHeartbreaker).toBeNull();
|
||||
});
|
||||
|
||||
it('computes loneliest (most memory-status relationships)', () => {
|
||||
const world = new World();
|
||||
const registry = createBondRegistry();
|
||||
const a = setupNPC(world, 'Alice', []);
|
||||
const b = setupNPC(world, 'Bob', []);
|
||||
const c = setupNPC(world, 'Carol', []);
|
||||
// Alice has 2 memory rels, Bob has 1
|
||||
world.getComponent<Relationships>(a, 'relationships')!.set(b, { value: 30, interactions: 5, lastInteractionTick: 0, status: 'memory' });
|
||||
world.getComponent<Relationships>(a, 'relationships')!.set(c, { value: 40, interactions: 5, lastInteractionTick: 0, status: 'memory' });
|
||||
world.getComponent<Relationships>(b, 'relationships')!.set(c, { value: 20, interactions: 5, lastInteractionTick: 0, status: 'memory' });
|
||||
|
||||
const result = computeSuperlatives(world, registry);
|
||||
expect(result.loneliest).not.toBeNull();
|
||||
expect(result.loneliest!.entityId).toBe(a);
|
||||
expect(result.loneliest!.value).toBe(2);
|
||||
});
|
||||
|
||||
it('returns loneliest null when no memory relationships', () => {
|
||||
const world = new World();
|
||||
const registry = createBondRegistry();
|
||||
const a = setupNPC(world, 'Alice', []);
|
||||
const b = setupNPC(world, 'Bob', []);
|
||||
world.getComponent<Relationships>(a, 'relationships')!.set(b, { value: 30, interactions: 5, lastInteractionTick: 0, status: 'active' });
|
||||
|
||||
const result = computeSuperlatives(world, registry);
|
||||
expect(result.loneliest).toBeNull();
|
||||
});
|
||||
|
||||
it('computes mostDevoted (highest mutual average)', () => {
|
||||
const world = new World();
|
||||
const registry = createBondRegistry();
|
||||
const a = setupNPC(world, 'Alice', []);
|
||||
const b = setupNPC(world, 'Bob', []);
|
||||
const c = setupNPC(world, 'Carol', []);
|
||||
// A<->B mutual: 80 and 60, avg 70
|
||||
world.getComponent<Relationships>(a, 'relationships')!.set(b, { value: 80, interactions: 5, lastInteractionTick: 0, status: 'active' });
|
||||
world.getComponent<Relationships>(b, 'relationships')!.set(a, { value: 60, interactions: 5, lastInteractionTick: 0, status: 'active' });
|
||||
// A<->C mutual: 90 and 95, avg 92.5
|
||||
world.getComponent<Relationships>(a, 'relationships')!.set(c, { value: 90, interactions: 5, lastInteractionTick: 0, status: 'active' });
|
||||
world.getComponent<Relationships>(c, 'relationships')!.set(a, { value: 95, interactions: 5, lastInteractionTick: 0, status: 'active' });
|
||||
|
||||
const result = computeSuperlatives(world, registry);
|
||||
expect(result.mostDevoted).not.toBeNull();
|
||||
// The winner should be the NPC with highest mutual avg - that's either A or C from the A<->C pair
|
||||
// A has mutual with C at avg 92.5, C has mutual with A at avg 92.5
|
||||
// Both A and C qualify; implementation picks whichever comes first
|
||||
expect(result.mostDevoted!.value).toBe(92.5);
|
||||
});
|
||||
|
||||
it('computes mostPolarizing (highest std dev, min 3 incoming)', () => {
|
||||
const world = new World();
|
||||
const registry = createBondRegistry();
|
||||
const a = setupNPC(world, 'Alice', []);
|
||||
const b = setupNPC(world, 'Bob', []);
|
||||
const c = setupNPC(world, 'Carol', []);
|
||||
const d = setupNPC(world, 'Dave', []);
|
||||
// 3 NPCs have opinions on Alice: 80, -80, 0 -> std dev = sqrt((80^2+80^2+0^2)/3) = sqrt(4266.67) ≈ 65.3
|
||||
// Wait, std dev: mean = 0, variance = (80^2 + 80^2 + 0^2)/3 = 12800/3, sd = sqrt(4266.67) ≈ 65.3
|
||||
world.getComponent<Relationships>(b, 'relationships')!.set(a, { value: 80, interactions: 5, lastInteractionTick: 0, status: 'active' });
|
||||
world.getComponent<Relationships>(c, 'relationships')!.set(a, { value: -80, interactions: 5, lastInteractionTick: 0, status: 'active' });
|
||||
world.getComponent<Relationships>(d, 'relationships')!.set(a, { value: 0, interactions: 5, lastInteractionTick: 0, status: 'active' });
|
||||
|
||||
const result = computeSuperlatives(world, registry);
|
||||
expect(result.mostPolarizing).not.toBeNull();
|
||||
expect(result.mostPolarizing!.entityId).toBe(a);
|
||||
expect(result.mostPolarizing!.value).toBeCloseTo(65.3, 0);
|
||||
});
|
||||
|
||||
it('returns mostPolarizing null with fewer than 3 incoming', () => {
|
||||
const world = new World();
|
||||
const registry = createBondRegistry();
|
||||
const a = setupNPC(world, 'Alice', []);
|
||||
const b = setupNPC(world, 'Bob', []);
|
||||
const c = setupNPC(world, 'Carol', []);
|
||||
world.getComponent<Relationships>(b, 'relationships')!.set(a, { value: 80, interactions: 5, lastInteractionTick: 0, status: 'active' });
|
||||
world.getComponent<Relationships>(c, 'relationships')!.set(a, { value: -80, interactions: 5, lastInteractionTick: 0, status: 'active' });
|
||||
|
||||
const result = computeSuperlatives(world, registry);
|
||||
expect(result.mostPolarizing).toBeNull();
|
||||
});
|
||||
|
||||
it('computes socialButterfly (most partners + close friends, value >= 50)', () => {
|
||||
const world = new World();
|
||||
const registry = createBondRegistry();
|
||||
const a = setupNPC(world, 'Alice', []);
|
||||
const b = setupNPC(world, 'Bob', []);
|
||||
const c = setupNPC(world, 'Carol', []);
|
||||
const d = setupNPC(world, 'Dave', []);
|
||||
// Alice: 3 relationships >= 50 (Devoted or Close Friend)
|
||||
world.getComponent<Relationships>(a, 'relationships')!.set(b, { value: 85, interactions: 5, lastInteractionTick: 0, status: 'active' });
|
||||
world.getComponent<Relationships>(a, 'relationships')!.set(c, { value: 55, interactions: 5, lastInteractionTick: 0, status: 'active' });
|
||||
world.getComponent<Relationships>(a, 'relationships')!.set(d, { value: 60, interactions: 5, lastInteractionTick: 0, status: 'active' });
|
||||
// Bob: 1 relationship >= 50
|
||||
world.getComponent<Relationships>(b, 'relationships')!.set(a, { value: 50, interactions: 5, lastInteractionTick: 0, status: 'active' });
|
||||
|
||||
const result = computeSuperlatives(world, registry);
|
||||
expect(result.socialButterfly).not.toBeNull();
|
||||
expect(result.socialButterfly!.entityId).toBe(a);
|
||||
expect(result.socialButterfly!.value).toBe(3);
|
||||
});
|
||||
|
||||
it('returns socialButterfly null when nobody has value >= 50', () => {
|
||||
const world = new World();
|
||||
const registry = createBondRegistry();
|
||||
const a = setupNPC(world, 'Alice', []);
|
||||
const b = setupNPC(world, 'Bob', []);
|
||||
world.getComponent<Relationships>(a, 'relationships')!.set(b, { value: 40, interactions: 5, lastInteractionTick: 0, status: 'active' });
|
||||
|
||||
const result = computeSuperlatives(world, registry);
|
||||
expect(result.socialButterfly).toBeNull();
|
||||
});
|
||||
|
||||
it('only counts active relationships for incoming values', () => {
|
||||
const world = new World();
|
||||
const registry = createBondRegistry();
|
||||
const a = setupNPC(world, 'Alice', []);
|
||||
const b = setupNPC(world, 'Bob', []);
|
||||
// Memory relationships should not count as incoming for mostLoved
|
||||
world.getComponent<Relationships>(a, 'relationships')!.set(b, { value: 90, interactions: 5, lastInteractionTick: 0, status: 'memory' });
|
||||
|
||||
const result = computeSuperlatives(world, registry);
|
||||
expect(result.mostLoved).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,238 @@
|
||||
import type { EntityId, Relationships, SuperlativesData, SuperlativeEntry } from '@dflike/shared';
|
||||
import { World } from '../ecs/World.js';
|
||||
import type { BondRegistry } from './bondRegistry.js';
|
||||
import { getBonds } from './bondRegistry.js';
|
||||
import { classify } from './relationshipHelpers.js';
|
||||
|
||||
export function computeSuperlatives(world: World, registry: BondRegistry): SuperlativesData {
|
||||
const npcs = world.query('npcBrain');
|
||||
|
||||
// Per-NPC aggregated data
|
||||
const incomingValues = new Map<EntityId, number[]>(); // incoming active relationship values
|
||||
const activeOutgoingCount = new Map<EntityId, number>(); // count of active outgoing
|
||||
const memoryCount = new Map<EntityId, number>(); // count of memory-status outgoing
|
||||
const socialButterflyCount = new Map<EntityId, number>(); // count of outgoing active with value >= 50
|
||||
|
||||
// Initialize all NPCs
|
||||
for (const id of npcs) {
|
||||
incomingValues.set(id, []);
|
||||
activeOutgoingCount.set(id, 0);
|
||||
memoryCount.set(id, 0);
|
||||
socialButterflyCount.set(id, 0);
|
||||
}
|
||||
|
||||
const npcSet = new Set(npcs);
|
||||
|
||||
// Single pass: gather data
|
||||
for (const id of npcs) {
|
||||
const rels = world.getComponent<Relationships>(id, 'relationships');
|
||||
if (!rels) continue;
|
||||
|
||||
let activeOut = 0;
|
||||
let memOut = 0;
|
||||
let butterflyCount = 0;
|
||||
|
||||
for (const [targetId, rel] of rels) {
|
||||
if (!npcSet.has(targetId)) continue;
|
||||
|
||||
if (rel.status === 'active') {
|
||||
activeOut++;
|
||||
// This is an incoming value for the target
|
||||
incomingValues.get(targetId)?.push(rel.value);
|
||||
// Social butterfly: active outgoing with value >= 50
|
||||
if (rel.value >= 50) {
|
||||
butterflyCount++;
|
||||
}
|
||||
} else if (rel.status === 'memory') {
|
||||
memOut++;
|
||||
}
|
||||
}
|
||||
|
||||
activeOutgoingCount.set(id, activeOut);
|
||||
memoryCount.set(id, memOut);
|
||||
socialButterflyCount.set(id, butterflyCount);
|
||||
}
|
||||
|
||||
// Helper to get NPC name
|
||||
const getName = (id: EntityId): string => world.getComponent<string>(id, 'name') ?? `NPC ${id}`;
|
||||
|
||||
// Helper to make an entry
|
||||
const makeEntry = (id: EntityId, value: number): SuperlativeEntry => ({
|
||||
entityId: id,
|
||||
name: getName(id),
|
||||
value: Math.round(value * 10) / 10,
|
||||
});
|
||||
|
||||
// --- Reduce per category ---
|
||||
|
||||
// Most Loved: highest single incoming value
|
||||
let mostLoved: SuperlativeEntry | null = null;
|
||||
let mostLovedVal = -Infinity;
|
||||
|
||||
// Most Reviled: lowest single incoming value
|
||||
let mostReviled: SuperlativeEntry | null = null;
|
||||
let mostReviledVal = Infinity;
|
||||
|
||||
// Most Popular: highest avg incoming (min 2)
|
||||
let mostPopular: SuperlativeEntry | null = null;
|
||||
let mostPopularVal = -Infinity;
|
||||
|
||||
// Most Annoying: lowest avg incoming (min 2)
|
||||
let mostAnnoying: SuperlativeEntry | null = null;
|
||||
let mostAnnoyingVal = Infinity;
|
||||
|
||||
// Most Polarizing: highest std dev incoming (min 3)
|
||||
let mostPolarizing: SuperlativeEntry | null = null;
|
||||
let mostPolarizingVal = -Infinity;
|
||||
|
||||
for (const id of npcs) {
|
||||
const incoming = incomingValues.get(id)!;
|
||||
|
||||
// Most Loved / Most Reviled
|
||||
for (const v of incoming) {
|
||||
if (v > mostLovedVal) {
|
||||
mostLovedVal = v;
|
||||
mostLoved = makeEntry(id, v);
|
||||
}
|
||||
if (v < mostReviledVal) {
|
||||
mostReviledVal = v;
|
||||
mostReviled = makeEntry(id, v);
|
||||
}
|
||||
}
|
||||
|
||||
// Most Popular / Most Annoying (min 2 incoming)
|
||||
if (incoming.length >= 2) {
|
||||
const avg = incoming.reduce((a, b) => a + b, 0) / incoming.length;
|
||||
if (avg > mostPopularVal) {
|
||||
mostPopularVal = avg;
|
||||
mostPopular = makeEntry(id, avg);
|
||||
}
|
||||
if (avg < mostAnnoyingVal) {
|
||||
mostAnnoyingVal = avg;
|
||||
mostAnnoying = makeEntry(id, avg);
|
||||
}
|
||||
}
|
||||
|
||||
// Most Polarizing (min 3 incoming)
|
||||
if (incoming.length >= 3) {
|
||||
const mean = incoming.reduce((a, b) => a + b, 0) / incoming.length;
|
||||
const variance = incoming.reduce((sum, v) => sum + (v - mean) ** 2, 0) / incoming.length;
|
||||
const stdDev = Math.sqrt(variance);
|
||||
if (stdDev > mostPolarizingVal) {
|
||||
mostPolarizingVal = stdDev;
|
||||
mostPolarizing = makeEntry(id, stdDev);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Shyest: fewest active outgoing
|
||||
let shyest: SuperlativeEntry | null = null;
|
||||
let shyestVal = Infinity;
|
||||
|
||||
// Most Outgoing: most active outgoing
|
||||
let mostOutgoing: SuperlativeEntry | null = null;
|
||||
let mostOutgoingVal = -Infinity;
|
||||
|
||||
for (const id of npcs) {
|
||||
const count = activeOutgoingCount.get(id)!;
|
||||
if (count < shyestVal) {
|
||||
shyestVal = count;
|
||||
shyest = makeEntry(id, count);
|
||||
}
|
||||
if (count > mostOutgoingVal) {
|
||||
mostOutgoingVal = count;
|
||||
mostOutgoing = makeEntry(id, count);
|
||||
}
|
||||
}
|
||||
|
||||
// Biggest Heartbreaker: most former partner bonds (must be > 0)
|
||||
let biggestHeartbreaker: SuperlativeEntry | null = null;
|
||||
let heartbreakerVal = 0;
|
||||
|
||||
for (const id of npcs) {
|
||||
let formerCount = 0;
|
||||
for (const [key, bonds] of registry) {
|
||||
const [a, b] = key.split(':').map(Number);
|
||||
if (a !== id && b !== id) continue;
|
||||
formerCount += bonds.filter(bond => bond.type === 'partner' && bond.status === 'former').length;
|
||||
}
|
||||
if (formerCount > heartbreakerVal) {
|
||||
heartbreakerVal = formerCount;
|
||||
biggestHeartbreaker = makeEntry(id, formerCount);
|
||||
}
|
||||
}
|
||||
if (heartbreakerVal === 0) biggestHeartbreaker = null;
|
||||
|
||||
// Most Devoted: highest mutual relationship average (both directions active)
|
||||
let mostDevoted: SuperlativeEntry | null = null;
|
||||
let mostDevotedVal = -Infinity;
|
||||
const processedPairs = new Set<string>();
|
||||
|
||||
for (const id of npcs) {
|
||||
const rels = world.getComponent<Relationships>(id, 'relationships');
|
||||
if (!rels) continue;
|
||||
for (const [targetId, rel] of rels) {
|
||||
if (!npcSet.has(targetId)) continue;
|
||||
if (rel.status !== 'active') continue;
|
||||
|
||||
const pairKey = id < targetId ? `${id}:${targetId}` : `${targetId}:${id}`;
|
||||
if (processedPairs.has(pairKey)) continue;
|
||||
|
||||
const reverseRels = world.getComponent<Relationships>(targetId, 'relationships');
|
||||
const reverseRel = reverseRels?.get(id);
|
||||
if (!reverseRel || reverseRel.status !== 'active') continue;
|
||||
|
||||
processedPairs.add(pairKey);
|
||||
const avg = (rel.value + reverseRel.value) / 2;
|
||||
|
||||
// Award to both NPCs - pick the one with higher individual contribution,
|
||||
// or just pick the first one in the pair
|
||||
if (avg > mostDevotedVal) {
|
||||
mostDevotedVal = avg;
|
||||
// Pick the entity that appears first (the one iterating)
|
||||
mostDevoted = makeEntry(id, avg);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (mostDevotedVal === -Infinity) mostDevoted = null;
|
||||
|
||||
// Loneliest: most memory-status relationships (must be > 0)
|
||||
let loneliest: SuperlativeEntry | null = null;
|
||||
let loneliestVal = 0;
|
||||
|
||||
for (const id of npcs) {
|
||||
const count = memoryCount.get(id)!;
|
||||
if (count > loneliestVal) {
|
||||
loneliestVal = count;
|
||||
loneliest = makeEntry(id, count);
|
||||
}
|
||||
}
|
||||
if (loneliestVal === 0) loneliest = null;
|
||||
|
||||
// Social Butterfly: most partners + close friends (value >= 50, must be > 0)
|
||||
let socialButterfly: SuperlativeEntry | null = null;
|
||||
let socialButterflyVal = 0;
|
||||
|
||||
for (const id of npcs) {
|
||||
const count = socialButterflyCount.get(id)!;
|
||||
if (count > socialButterflyVal) {
|
||||
socialButterflyVal = count;
|
||||
socialButterfly = makeEntry(id, count);
|
||||
}
|
||||
}
|
||||
if (socialButterflyVal === 0) socialButterfly = null;
|
||||
|
||||
return {
|
||||
mostLoved,
|
||||
mostPopular,
|
||||
mostReviled,
|
||||
mostAnnoying,
|
||||
shyest,
|
||||
mostOutgoing,
|
||||
biggestHeartbreaker,
|
||||
mostDevoted,
|
||||
loneliest,
|
||||
mostPolarizing,
|
||||
socialButterfly,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user