feat: streamline search results and open captured analyses
Publish GitHub Pages output / publish (push) Successful in 54s

This commit is contained in:
2026-09-09 19:15:25 +09:00
parent 28035110f9
commit d26b9ca884
8 changed files with 365 additions and 53 deletions
+63
View File
@@ -0,0 +1,63 @@
import type { AnalysisCapture, AnalysisSettings, GrowthSnapshot } from './analysis-history.ts';
import type { ExactOptimizationResult, ExactSongChart, OptimizerCard, ScoreAchievementProbabilities } from '../engine/types.ts';
import { resolveCandidateAwakening } from '../engine/combination-search.ts';
export const ANALYSIS_TRANSFER_KEY = 'holodori.analysis-transfer.v1';
export type AnalysisTransfer = {
schemaVersion: 1;
source?: 'combination' | 'rating';
capture: AnalysisCapture;
result: ExactOptimizationResult;
cards: OptimizerCard[];
chart: ExactSongChart;
directSongScoreBonusPercent: number;
achievementProbabilities?: ScoreAchievementProbabilities;
};
export type CombinationConditions = Pick<AnalysisSettings, 'songId' | 'difficulty' | 'leaderCharacterId' | 'leaderOutfitCardId' | 'songScoreBonusOverride'> & {
fixedCardIds: string[]; fixedAwakenings?: number[];
fixedCooldownReductions?: AnalysisSettings['fixedCooldownReductions'];
fixedAllBlueNodes?: boolean[]; fixedGenerationBonusOverrides?: (number | null)[];
awakening: number; candidateGenerationBonus?: number | null;
};
export function combinationAnalysisSettings(state: CombinationConditions, cardIds: readonly string[], growth: GrowthSnapshot): AnalysisSettings {
const slots = cardIds.map(id => state.fixedCardIds.indexOf(id));
return {
songId: state.songId, difficulty: state.difficulty, cardIds: [...cardIds],
awakenings: cardIds.map((id, i) => slots[i] >= 0 ? state.fixedAwakenings?.[slots[i]] ?? 0
: resolveCandidateAwakening(state.awakening, Boolean(growth.collection[id]?.owned), growth.collection[id]?.awakening ?? 5)),
fixedCooldownReductions: slots.map(slot => slot < 0 ? null : state.fixedCooldownReductions?.[slot] ?? null),
allBlueNodes: slots.map(slot => slot < 0 || state.fixedAllBlueNodes?.[slot] === true),
generationBonusOverrides: slots.map(slot => slot < 0 ? state.candidateGenerationBonus ?? null : state.fixedGenerationBonusOverrides?.[slot] ?? null),
songScoreBonusOverride: state.songScoreBonusOverride ?? null,
leaderCharacterId: state.leaderCharacterId, leaderOutfitCardId: state.leaderOutfitCardId
};
}
// Match the analysis page's key so a transferred result can flag newer growth data.
export function transferredAnalysisKey({ settings: s, growth: g }: AnalysisCapture): string {
return JSON.stringify({ songId: s.songId, difficulty: s.difficulty, input: JSON.stringify({
selectedCardIds: s.cardIds, selectedAwakenings: s.awakenings,
fixedCooldownReductions: s.fixedCooldownReductions, allBlueNodes: s.allBlueNodes,
generationBonusOverrides: s.generationBonusOverrides, songScoreBonusOverride: s.songScoreBonusOverride,
leaderCharacterId: s.leaderCharacterId, leaderOutfitCardId: s.leaderOutfitCardId,
collection: g.collection, memberBoards: g.memberBoards, boardCards: g.boardCards,
memoryCount: g.memoryCount, memberPowerUpPercent: g.memberPowerUpPercent
}) });
}
export function parseAnalysisTransfer(raw: string): AnalysisTransfer {
const value = JSON.parse(raw) as AnalysisTransfer;
const ids = value?.capture?.settings?.cardIds;
if (value?.schemaVersion !== 1 || !Array.isArray(ids) || ids.length !== 5 || new Set(ids).size !== 5 ||
!value.capture.growth || !Array.isArray(value.cards) || value.cards.length !== 5 ||
!ids.every(id => value.cards.some(card => card.id === id)) ||
!Array.isArray(value.result?.order) || value.result.order.length !== 5 || new Set(value.result.order).size !== 5 ||
!value.result.order.every(id => ids.includes(id)) || !Array.isArray(value.result.notes) ||
!Array.isArray(value.chart?.notes) || value.chart.songId !== value.capture.settings.songId ||
value.chart.difficulty !== value.capture.settings.difficulty || !Number.isFinite(value.directSongScoreBonusPercent)) {
throw new Error('전달된 분석 결과를 읽지 못했습니다. 원래 결과 목록에서 다시 열어 주세요.');
}
return value;
}