Files
HolodoriCalc/scripts/collect-game-events.mjs
dal4segno 37102923ac
Publish GitHub Pages output / publish (push) Successful in 37s
feat: apply captured API configuration for Blue Splash event
2026-09-29 23:43:01 +09:00

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)}`);