feat: complete placement analysis workflow
This commit is contained in:
@@ -10,6 +10,7 @@ const rawRoot = path.join(projectRoot, 'data', 'raw', 'holodoridb');
|
||||
const outputRoot = path.join(projectRoot, 'data', 'holodoridb');
|
||||
const appDataRoot = path.join(projectRoot, 'src', 'lib', 'data');
|
||||
const cardOverridesPath = path.join(projectRoot, 'data', 'card-overrides.csv');
|
||||
const offline = process.argv.includes('--offline');
|
||||
|
||||
function parseCsvLine(line) {
|
||||
const values = [];
|
||||
@@ -97,12 +98,18 @@ const masterTableNames = [
|
||||
'LiveSkillTrigger',
|
||||
'LiveSkillEffectTarget',
|
||||
'LiveSkillEffectFilterCategory',
|
||||
'Poster',
|
||||
'PosterCollectEffect',
|
||||
'SkillTreeConnectEffect',
|
||||
'SkillTreeConnectEffectExtent',
|
||||
'SkillTreeEffect',
|
||||
'SkillTreeEffectPassiveTrigger',
|
||||
'SkillTreeEffectTarget',
|
||||
'SkillTreeEffectValueLimit'
|
||||
'SkillTreeEffectValueLimit',
|
||||
'SkillTreeNode',
|
||||
'SkillTreeNodeAutoSelection',
|
||||
'SkillTreeNodePosition',
|
||||
'SkillTreePoint'
|
||||
];
|
||||
|
||||
const localizationTableNames = [
|
||||
@@ -148,8 +155,12 @@ async function prepareSource(locale, source) {
|
||||
|
||||
try {
|
||||
gitAt(repositoryPath, ['rev-parse', '--git-dir']);
|
||||
process.stdout.write(`Updating ${locale} source...\n`);
|
||||
gitAt(repositoryPath, ['fetch', '--depth', '1', 'origin', 'main']);
|
||||
if (offline) {
|
||||
process.stdout.write(`Using cached ${locale} source...\n`);
|
||||
} else {
|
||||
process.stdout.write(`Updating ${locale} source...\n`);
|
||||
gitAt(repositoryPath, ['fetch', '--depth', '1', 'origin', 'main']);
|
||||
}
|
||||
} catch {
|
||||
process.stdout.write(`Cloning ${locale} source...\n`);
|
||||
await mkdir(path.dirname(repositoryPath), { recursive: true });
|
||||
@@ -331,6 +342,9 @@ const triggersByGroup = groupBy(tables.LiveSkillTrigger, 'groupId');
|
||||
const effectTargetsById = indexBy(tables.LiveSkillEffectTarget);
|
||||
const connectEffectsById = groupBy(tables.SkillTreeConnectEffect, 'id');
|
||||
const connectExtentsByGroup = groupBy(tables.SkillTreeConnectEffectExtent, 'groupId');
|
||||
const skillTreeEffectsById = indexBy(tables.SkillTreeEffect);
|
||||
const skillTreeNodesByGroup = groupBy(tables.SkillTreeNode, 'groupId');
|
||||
const skillTreePositionsByGroup = groupBy(tables.SkillTreeNodePosition, 'groupId');
|
||||
const costumesById = indexBy(tables.Costume);
|
||||
const sdCostumesById = indexBy(tables.SdCostume);
|
||||
const leaderSkillsById = indexBy(tables.LiveLeaderSkill);
|
||||
@@ -588,9 +602,17 @@ const cardSummary = {
|
||||
const cardOverride = cardOverrides.get(card.id);
|
||||
const highestStat = Object.entries(statLabels)
|
||||
.sort(([left], [right]) => Number(card[right]) - Number(card[left]))[0][1];
|
||||
const maxLevel = cardRecord.progression.levels.at(-1);
|
||||
const parameterBaseValue = Number(maxLevel?.parameterBaseValue ?? 0);
|
||||
const statsAtMaxLevel = {
|
||||
performance: Math.round(parameterBaseValue * Number(card.performancePermilMultiply ?? 0) / 1000),
|
||||
technique: Math.round(parameterBaseValue * Number(card.techniquePermilMultiply ?? 0) / 1000),
|
||||
sense: Math.round(parameterBaseValue * Number(card.sensePermilMultiply ?? 0) / 1000)
|
||||
};
|
||||
|
||||
return {
|
||||
id: card.id,
|
||||
characterId: card.characterId,
|
||||
memberOrder: character.order,
|
||||
rarity: Number(compactEnum(card.rarity)),
|
||||
type: typeLabels[compactEnum(card.attributeType)],
|
||||
@@ -598,11 +620,24 @@ const cardSummary = {
|
||||
versionName: cardOverride?.versionName || '',
|
||||
generation: textLookups.kor.get(character.regularCharacterGroupingNameLangId) || '',
|
||||
highestStat,
|
||||
power: {
|
||||
maxLevel: Number(maxLevel?.level ?? 0),
|
||||
statsAtMaxLevel
|
||||
},
|
||||
details: {
|
||||
title: textLookups.kor.get(card.nameLangId),
|
||||
passive: summarizeSkillLevels(cardRecord.skills.passive.levels),
|
||||
active: summarizeConditionalSkillLevels(cardRecord.skills.active.levels),
|
||||
special: summarizeSpecialSkillLevels(cardRecord.skills.special.levels),
|
||||
connect: cardRecord.skillTreeConnectEffects.map((effect) => ({
|
||||
level: Number(effect.level ?? 1),
|
||||
description: effect.translations?.description?.kor ?? '',
|
||||
effectPermilUp: Number(effect.effectPermilUp ?? 0),
|
||||
extent: effect.extent.map((position) => ({
|
||||
x: Number(position.positionX ?? 0),
|
||||
y: Number(position.positionY ?? 0)
|
||||
}))
|
||||
})),
|
||||
costume: cardRecord.rewards.costume
|
||||
? {
|
||||
name: cardRecord.rewards.costume.translations?.name?.kor ?? '',
|
||||
@@ -622,16 +657,103 @@ const cardSummary = {
|
||||
.map(({ memberOrder: _memberOrder, ...card }) => card)
|
||||
};
|
||||
|
||||
function skillTreeNodeForCharacter(characterId, groupId) {
|
||||
const candidates = skillTreeNodesByGroup.get(groupId) ?? [];
|
||||
return candidates.find((node) => node.characterIds?.includes(characterId))
|
||||
?? candidates.find((node) => !node.characterIds?.length)
|
||||
?? null;
|
||||
}
|
||||
|
||||
function stripEnumPrefix(value, prefix) {
|
||||
return typeof value === 'string' ? value.replace(prefix, '').toLowerCase() : value;
|
||||
}
|
||||
|
||||
function summarizeBoardNode(node) {
|
||||
const effect = skillTreeEffectsById.get(node.skillTreeEffectId);
|
||||
return {
|
||||
number: node.number,
|
||||
characterIds: node.characterIds ?? [],
|
||||
grade: node.grade,
|
||||
type: stripEnumPrefix(node.type, 'SkillTreeNodeType_SKILL_TREE_NODE_TYPE_'),
|
||||
skillTreePointCost: node.consumptionSkillTreePointQuantity ?? 0,
|
||||
consumptions: node.consumptions ?? [],
|
||||
viewConditionGroupId: node.viewConditionGroupId ?? null,
|
||||
effect: effect ? {
|
||||
id: effect.id,
|
||||
type: stripEnumPrefix(effect.effectType, 'SkillTreeEffectType_SKILL_TREE_EFFECT_TYPE_'),
|
||||
value: effect.value == null ? null : Number(effect.value),
|
||||
targetId: effect.skillTreeEffectTargetId ?? null,
|
||||
trigger: stripEnumPrefix(effect.characterTriggerType, 'SkillTreeEffectCharacterTriggerType_SKILL_TREE_EFFECT_CHARACTER_TRIGGER_TYPE_'),
|
||||
description: textLookups.kor.get(effect.descriptionLangId) ?? ''
|
||||
} : null
|
||||
};
|
||||
}
|
||||
|
||||
const boardSummary = {
|
||||
schemaVersion: 1,
|
||||
masterDataVersion: [...masterVersions][0],
|
||||
members: tables.Character
|
||||
.map(unwrap)
|
||||
.filter((character) => character.isPlayable && character.skillTreeNodePositionGroupId)
|
||||
.map((character) => ({
|
||||
id: character.id,
|
||||
name: textLookups.kor.get(character.nameLangId) ?? character.nameEng,
|
||||
order: character.order,
|
||||
positionGroupId: character.skillTreeNodePositionGroupId
|
||||
}))
|
||||
.sort((left, right) => left.order - right.order),
|
||||
models: Object.fromEntries(
|
||||
[...new Set(tables.Character.map(unwrap).map((character) => character.skillTreeNodePositionGroupId).filter(Boolean))]
|
||||
.sort()
|
||||
.map((groupId) => [groupId, (skillTreePositionsByGroup.get(groupId) ?? [])
|
||||
.map((position) => ({
|
||||
groupId: position.skillTreeNodeGroupId,
|
||||
x: position.positionX ?? 0,
|
||||
y: position.positionY ?? 0
|
||||
}))
|
||||
.sort((left, right) => left.y - right.y || left.x - right.x || left.groupId.localeCompare(right.groupId))])
|
||||
),
|
||||
nodeGroups: Object.fromEntries(
|
||||
[...skillTreeNodesByGroup.entries()]
|
||||
.sort(([left], [right]) => left.localeCompare(right))
|
||||
.map(([groupId, nodes]) => [groupId, sortByNumber(nodes, 'number').map(summarizeBoardNode)])
|
||||
)
|
||||
};
|
||||
|
||||
const memorySummary = {
|
||||
schemaVersion: 1,
|
||||
masterDataVersion: [...masterVersions][0],
|
||||
maxCount: tables.Poster.length,
|
||||
effects: tables.PosterCollectEffect
|
||||
.map(unwrap)
|
||||
.map((effect) => ({
|
||||
count: Number(effect.threshold),
|
||||
permil: Number(effect.liveDeckAllParameterUpPermilUp)
|
||||
}))
|
||||
.sort((left, right) => left.count - right.count)
|
||||
};
|
||||
|
||||
assert(boardSummary.members.length > 0, 'no member boards were collected');
|
||||
assert(Object.keys(boardSummary.models).length > 0, 'no board position models were collected');
|
||||
assert(boardSummary.members.every((member) => boardSummary.models[member.positionGroupId]?.length === 152), 'one or more members have incomplete board positions');
|
||||
assert(boardSummary.members.every((member) => boardSummary.models[member.positionGroupId].every((position) => skillTreeNodeForCharacter(member.id, position.groupId))), 'one or more members have unresolved board nodes');
|
||||
assert(memorySummary.maxCount > 0, 'no memory posters were collected');
|
||||
assert(memorySummary.effects.length > 0, 'no memory collection effects were collected');
|
||||
|
||||
await mkdir(outputRoot, { recursive: true });
|
||||
await mkdir(appDataRoot, { recursive: true });
|
||||
await Promise.all([
|
||||
writeFile(path.join(outputRoot, 'manifest.json'), `${JSON.stringify(manifest, null, 2)}\n`),
|
||||
writeFile(path.join(outputRoot, 'card-master.json'), `${JSON.stringify(masterBundle, null, 2)}\n`),
|
||||
writeFile(path.join(outputRoot, 'cards.json'), `${JSON.stringify(cardBundle, null, 2)}\n`),
|
||||
writeFile(path.join(appDataRoot, 'cards-summary.json'), `${JSON.stringify(cardSummary, null, 2)}\n`)
|
||||
writeFile(path.join(appDataRoot, 'cards-summary.json'), `${JSON.stringify(cardSummary, null, 2)}\n`),
|
||||
writeFile(path.join(appDataRoot, 'boards-summary.json'), `${JSON.stringify(boardSummary, null, 2)}\n`),
|
||||
writeFile(path.join(appDataRoot, 'memory-summary.json'), `${JSON.stringify(memorySummary, null, 2)}\n`)
|
||||
]);
|
||||
|
||||
process.stdout.write(`Collected ${cards.length} cards.\n`);
|
||||
process.stdout.write(`Rarities: ${JSON.stringify(rarityCounts)}\n`);
|
||||
process.stdout.write(`Localized names: ${JSON.stringify(cardNameCoverage)}\n`);
|
||||
process.stdout.write(`Member boards: ${boardSummary.members.length}\n`);
|
||||
process.stdout.write(`Memory posters: ${memorySummary.maxCount}\n`);
|
||||
process.stdout.write(`Output: ${outputRoot}\n`);
|
||||
|
||||
Reference in New Issue
Block a user