feat: add collapsible member board effect summaries
Publish GitHub Pages output / publish (push) Successful in 46s

This commit is contained in:
2026-09-02 10:30:51 +09:00
parent a920d65a21
commit 7308428945
6 changed files with 326 additions and 3 deletions
+74 -1
View File
@@ -2,7 +2,7 @@ import { skillTargetBelongsTo } from './skill-condition.ts';
import type { CooldownReduction } from './types.ts';
import type { UnitStat } from './unit-power.ts';
type BoardEffect = { id?: string; type: string; value: number | null; targetId?: string | null; trigger?: string | null };
type BoardEffect = { id?: string; description?: string; type: string; value: number | null; targetId?: string | null; trigger?: string | null };
type BoardNode = { characterIds: string[]; effect: BoardEffect | null };
type BoardPosition = { groupId: string; x: number; y: number };
type BoardMember = { id: string; positionGroupId: string };
@@ -125,6 +125,79 @@ function connectionMultipliers(
return result;
}
export type MemberBoardEffectSummary = {
received: {
flat: Record<UnitStat, number>;
flatByArea: Record<'B' | 'G', Record<UnitStat, number>>;
rate: Record<UnitStat, number>;
activationRateUp: number;
} | null;
groups: {
area: string;
rows: { description: string; nodeCount: number; boosted: boolean }[];
}[];
};
/** Saved-board overview, without assuming a leader or a song. Conditional effects stay in the detail rows. */
export function summarizeMemberBoardEffects(
summary: BoardSummarySource,
memberBoards: Readonly<Record<string, readonly string[]>>,
member: { id: string; name: string },
card: BoardEffectCard | null,
connections?: BoardConnections | null
): MemberBoardEffectSummary {
const uniqueBoards = Object.fromEntries(Object.entries(memberBoards).map(([id, nodes]) => [id, [...new Set(nodes)]]));
const effects = card ? calculateBoardEffects(summary, uniqueBoards, [card], '', null, connections) : null;
const flatForArea = (area: 'B' | 'G'): Record<UnitStat, number> => {
if (!card) return emptyStats();
// Filter effects, not active nodes: connection sockets and duplicate-card handling must stay unchanged.
const areaSummary = { ...summary, nodeGroups: Object.fromEntries(
Object.entries(summary.nodeGroups).filter(([groupId]) => groupId.startsWith(area))
) };
return calculateBoardEffects(areaSummary, uniqueBoards, [card], '', null, connections).flatByCardId[card.id];
};
const multipliers = connectionMultipliers(summary, uniqueBoards, connections)[member.id] ?? {};
const active = new Set(memberBoards[member.id] ?? []);
const format = (value: number) => value.toLocaleString('ko-KR', { maximumFractionDigits: 3 });
const groups = ['B', 'G', 'R', 'Y'].map((area) => {
const rows = new Map<string, { template: string; value: number; nodeCount: number; boosted: boolean }>();
for (const groupId of Object.keys(summary.nodeGroups)) {
if (groupId[0] !== area || !active.has(groupId)) continue;
const effect = nodeForMember(summary, groupId, member.id)?.effect;
if (!effect?.description) continue;
const template = effect.description.replaceAll('[character]', member.name).replace(/\s+/g, ' ').trim();
const numeric = effect.value !== null && /\[value(?:\/10)?\]/.test(template);
// Keep targets, conditions and non-additive skill unlocks separate.
const key = JSON.stringify([effect.type, effect.targetId, effect.trigger, template, numeric ? null : effect.id ?? groupId]);
const multiplier = numeric ? multipliers[groupId] ?? 1 : 1;
const rawValue = (effect.value ?? 0) * multiplier;
const value = affectedStats(effect.type).length > 0 ? Math.ceil(rawValue) : rawValue;
const row = rows.get(key) ?? { template, value: 0, nodeCount: 0, boosted: false };
row.value += value;
row.nodeCount += 1;
row.boosted ||= multiplier > 1;
rows.set(key, row);
}
return {
area,
rows: [...rows.values()].map((row) => ({
description: row.template.replaceAll('[value/10]', format(row.value / 10)).replaceAll('[value]', format(row.value)),
nodeCount: row.nodeCount,
boosted: row.boosted
}))
};
});
return {
received: effects && card ? {
flat: effects.flatByCardId[card.id],
flatByArea: { B: flatForArea('B'), G: flatForArea('G') },
rate: effects.rateByCardId[card.id],
activationRateUp: effects.activationRateUpByCardId[card.id]
} : null,
groups
};
}
export function calculateBoardEffects(
summary: BoardSummarySource,
memberBoards: Readonly<Record<string, readonly string[]>>,