97 lines
5.2 KiB
JavaScript
97 lines
5.2 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import { readFileSync, writeFileSync } from 'node:fs';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { prepareGameDbSource, readGameDbText, sha256 } from './game-db-source.mjs';
|
|
|
|
// Explicitly select the event to apply; older manual records retain their provenance.
|
|
const root = new URL('../data/game-events/', import.meta.url);
|
|
const pointer = JSON.parse(readFileSync(new URL('current.json', root), 'utf8'));
|
|
assert.match(pointer.file, /^\d{4}-\d{2}-\d{2}\.json$/);
|
|
const bytes = readFileSync(new URL(pointer.file, root));
|
|
assert.equal(sha256(bytes), pointer.sha256, 'Event API snapshot hash mismatch');
|
|
const snapshot = JSON.parse(bytes);
|
|
assert.equal(snapshot.schemaVersion, 1);
|
|
assert.equal(snapshot.source.kind, 'android-event-api-memory');
|
|
assert.equal(snapshot.source.locale, 'kor');
|
|
assert(pointer.eventIds.length > 0);
|
|
assert.equal(new Set(pointer.eventIds).size, pointer.eventIds.length);
|
|
|
|
const db = prepareGameDbSource('kor', { review: true });
|
|
const table = name => JSON.parse(readGameDbText(db, name)).map(row => row.data);
|
|
const posters = table('Poster.json');
|
|
const posterNames = new Map(table('LangPoster_Kor.json').map(row => [row.id, row.text]));
|
|
const cards = new Set(table('Card.json').map(row => row.id));
|
|
const characters = new Set(table('Character.json').map(row => row.id));
|
|
const songs = new Set(table('Music.json').map(row => row.id));
|
|
const iso = value => {
|
|
assert(Number.isSafeInteger(value) && value > 0, 'Invalid event timestamp');
|
|
return new Date(value).toISOString();
|
|
};
|
|
function uniform(rows, field) {
|
|
const values = new Set(rows.map(row => row[field]));
|
|
assert(values.size <= 1, `Calculator cannot represent mixed ${field}`);
|
|
const value = values.size ? [...values][0] : 0;
|
|
assert(Number.isInteger(value) && value >= 0);
|
|
return value / 10;
|
|
}
|
|
function project(event) {
|
|
assert(event && event.marathonInfo, 'Selected event is missing marathon configuration');
|
|
// Relay chapters remain historical records until their exact transition semantics are applied.
|
|
assert.equal(event.marathonInfo.marathonChapters.length, 1, 'Only single-chapter events are supported by this projection');
|
|
const chapter = event.marathonInfo.marathonChapters[0];
|
|
assert(event.startTime < event.endTime && event.endTime < event.exchangeEndTime);
|
|
assert.equal(chapter.endTime, event.endTime);
|
|
const formation = [], featured = [], awakening = {};
|
|
for (const row of chapter.marathonLiveMarathonScoreBonuses) {
|
|
assert.equal(row.cardAttributeType, 0, 'Attribute bonus needs an explicit calculator implementation');
|
|
const { characterId, cardId, cardRarity: rarity, cardPotentialUpgradeCount: count } = row;
|
|
if (characterId && !cardId && !rarity && !count) {
|
|
assert(characters.has(characterId)); formation.push(row);
|
|
} else if (cardId && !characterId && !rarity && !count) {
|
|
assert(cards.has(cardId)); featured.push(row);
|
|
} else {
|
|
assert(!characterId && !cardId && Number.isInteger(rarity) && rarity > 0);
|
|
assert(Number.isInteger(count) && count > 0 && count <= 5);
|
|
awakening[rarity] ??= [0];
|
|
assert.equal(awakening[rarity][count], undefined, 'Duplicate awakening rule');
|
|
awakening[rarity][count] = row.marathonScoreQuantityUpPermilUp / 10;
|
|
}
|
|
}
|
|
for (const values of Object.values(awakening)) {
|
|
assert.equal(values.length, 6);
|
|
assert(Array.from(values).every(value => Number.isFinite(value) && value >= 0), 'Incomplete awakening rules');
|
|
}
|
|
const musicRules = chapter.marathonMusicScoreBonuses;
|
|
const challengeSongBonusCardIds = {};
|
|
for (const row of musicRules) {
|
|
assert(songs.has(row.musicId) && cards.has(row.cardId));
|
|
assert(!row.characterId && !row.cardAttributeType && !row.cardRarity && !row.cardPotentialUpgradeCount);
|
|
(challengeSongBonusCardIds[row.musicId] ??= []).push(row.cardId);
|
|
}
|
|
assert.equal(new Set(formation.map(row => row.characterId)).size, formation.length);
|
|
assert.equal(new Set(featured.map(row => row.cardId)).size, featured.length);
|
|
const poster = posters.find(row => row.id === `poster-${chapter.id}`);
|
|
return {
|
|
id: event.eventId,
|
|
name: posterNames.get(poster?.nameLangId) ?? event.name,
|
|
startsAt: iso(event.startTime),
|
|
endsAt: iso(event.endTime),
|
|
exchangeEndsAt: iso(event.exchangeEndTime),
|
|
bonusCharacterIds: formation.map(row => row.characterId),
|
|
featuredCardIds: featured.map(row => row.cardId),
|
|
challengeSongBonusCardIds,
|
|
formationBonusPercent: uniform(formation, 'marathonScoreQuantityUpPermilUp'),
|
|
featuredCardBonusPercent: uniform(featured, 'marathonScoreQuantityUpPermilUp'),
|
|
challengeSongScoreBonusPercent: uniform(musicRules, 'scoreUpPermilUp'),
|
|
awakeningBonusPercentByRarity: awakening
|
|
};
|
|
}
|
|
const output = {
|
|
source: { file: `data/game-events/${pointer.file}`, sha256: pointer.sha256,
|
|
capturedAt: snapshot.source.capturedAt, masterDataVersion: db.masterDataVersion },
|
|
events: pointer.eventIds.map(id => project(snapshot.events.find(event => event.eventId === id)))
|
|
};
|
|
const destination = new URL('../src/lib/data/events-api.json', import.meta.url);
|
|
writeFileSync(destination, JSON.stringify(output, null, 2) + '\n');
|
|
console.log(`Generated ${output.events.length} event(s): ${fileURLToPath(destination)}`);
|