data: configure Ocean Chase live and score bonuses
Publish GitHub Pages output / publish (push) Successful in 36s

This commit is contained in:
2026-10-07 12:30:17 +09:00
parent 814148e25d
commit 1b94c781be
5 changed files with 121 additions and 3 deletions
+7 -1
View File
@@ -1,9 +1,15 @@
---
updatedAt: 2026-10-07T03:00:00.000Z
updatedAt: 2026-10-07T03:27:00.000Z
---
## 2026년 10월 7일
### 오션 체이스 이벤트 계산
`NEW` 이벤트 기간 동안 HOLOGRAM CIRCUS는 폴카, Lamy's Baribari Workout은 라미, Tokyo Shandy Rendez-vous는 리리카의 신규 수영복 ★5 카드를 편성하면 최종 스코어에 10% 보너스를 적용합니다.
`NEW` 이벤트 PT 비교에 라미·폴카·리리카 멤버 보너스 +30%, 신규 수영복 카드 보너스 +30%, 등급별 개화 보너스를 반영합니다. 신규 수영복 카드는 미개화 시 +60%, 5회 개화 시 +90%를 적용합니다.
### 신규 악곡과 메모리 추가
`NEW` HOLOGRAM CIRCUS, Lamy's Baribari Workout, Tokyo Shandy Rendez-vous의 Easy·Normal·Hard·Expert 채보를 추가했습니다. 각 난이도의 점수 계산과 채보 표시를 지원합니다.
+37
View File
@@ -0,0 +1,37 @@
{
"source": {
"kind": "in-game-notice-screenshots-and-user-confirmation",
"notice": "https://x.com/hololive_dreams/status/2107078362217029679",
"noticePublishedAt": "2026-10-06T19:00:00+09:00",
"configuredAt": "2026-10-07T12:27:00+09:00",
"scope": "Event period confirmed by the in-game notice and live portal response. Live acquisition member, featured-card and awakening bonuses confirmed by in-game screenshots. Challenge-song score bonuses and new swimsuit card targets explicitly confirmed by the user. This is manual configuration, not a captured full Event/ListEventInfo response; minigame rules are not included."
},
"events": [
{
"id": "marathon_event-normal-006",
"name": "양보할 수 없는 오션 체이스",
"startsAt": "2026-10-07T03:00:00.000Z",
"endsAt": "2026-10-13T10:59:59.000Z",
"exchangeEndsAt": "2026-10-19T10:59:59.000Z",
"bonusCharacterIds": ["chr-00030", "chr-00034", "chr-06003"],
"featuredCardIds": [
"card-00030-5-uniq-0089-00",
"card-00034-5-uniq-0090-00",
"card-06003-5-uniq-0091-00"
],
"formationBonusPercent": 30,
"featuredCardBonusPercent": 30,
"awakeningBonusPercentByRarity": {
"3": [0, 1, 1, 2, 2, 3],
"4": [0, 6, 7, 8, 9, 10],
"5": [0, 15, 18, 22, 26, 30]
},
"challengeSongBonusCardIds": {
"m0360": ["card-00034-5-uniq-0090-00"],
"m0361": ["card-00030-5-uniq-0089-00"],
"m0362": ["card-06003-5-uniq-0091-00"]
},
"challengeSongScoreBonusPercent": 10
}
]
}
+5 -1
View File
@@ -1,5 +1,6 @@
import type { EventDefinition } from '$lib/engine/event-optimization.ts';
import apiEvents from './events-api.json' with { type: 'json' };
import noticeEvents from './events-notice.json' with { type: 'json' };
const gamersFeaturedCards = [
'card-00006-5-uniq-0074-00',
@@ -70,5 +71,8 @@ export const events: readonly EventDefinition[] = [
challengeSongBonusCardIds: { [songId]: [summerFeaturedCards[index]] }
}))
},
...apiEvents.events
...apiEvents.events,
// Live acquisition and score settings confirmed by in-game screenshots and the
// user. A subsequently captured full API configuration takes precedence.
...noticeEvents.events.filter(event => !apiEvents.events.some(apiEvent => apiEvent.id === event.id))
];
+1 -1
View File
@@ -26,7 +26,7 @@ test('이벤트 원본 해시와 생성된 일정이 확보한 API 설정과 일
assert.equal(activeEventAt(events, raw.startTime)?.id, raw.eventId);
assert.equal(activeEventAt(events, raw.endTime)?.id, raw.eventId);
assert.equal(activeEventAt(events, raw.endTime + 1), null);
assert.equal(activeEventAt(events, raw.exchangeEndTime), null);
assert.notEqual(activeEventAt(events, raw.exchangeEndTime)?.id, raw.eventId);
});
test('전체 카드와 개화 0~5의 라이브 보너스가 API 조건별 합계와 일치한다', () => {
+71
View File
@@ -0,0 +1,71 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { events } from '../data/events.ts';
import { activeEventAt, eventBonusBreakdown, eventChallengeSongScoreBonusPercent } from './event-optimization.ts';
const event = events.find(event => event.id === 'marathon_event-normal-006')!;
const cards = JSON.parse(readFileSync(new URL('../data/cards-summary.json', import.meta.url), 'utf8')).cards as {
id: string; characterId: string; versionName: string; rarity: number
}[];
const targets = [
['m0360', 'card-00034-5-uniq-0090-00', 'chr-00034'],
['m0361', 'card-00030-5-uniq-0089-00', 'chr-00030'],
['m0362', 'card-06003-5-uniq-0091-00', 'chr-06003']
] as const;
test('오션 체이스는 확인된 기간에만 활성화하고 다른 이벤트를 중복 추가하지 않는다', () => {
const start = Date.parse('2026-10-07T03:00:00.000Z');
const end = Date.parse('2026-10-13T10:59:59.000Z');
assert.equal(event.startsAt, new Date(start).toISOString());
assert.equal(event.endsAt, new Date(end).toISOString());
assert.equal(events.filter(entry => entry.id === event.id).length, 1);
assert.equal(activeEventAt(events, start - 1), null);
assert.equal(activeEventAt(events, start)?.id, event.id);
assert.equal(activeEventAt(events, end)?.id, event.id);
assert.equal(activeEventAt(events, end + 1), null);
});
test('각 신규 악곡은 같은 가창자의 신규 수영복 카드 한 장에만 스코어 10%를 적용한다', () => {
assert.deepEqual(Object.keys(event.challengeSongBonusCardIds).sort(), targets.map(([song]) => song).sort());
for (const [song, target, characterId] of targets) {
const targetCard = cards.find(card => card.id === target)!;
assert.equal(targetCard.characterId, characterId);
assert.equal(targetCard.versionName, '수영복');
for (const card of cards) {
assert.equal(eventChallengeSongScoreBonusPercent(event, song, [card.id]), card.id === target ? 10 : 0, `${song}, ${card.id}`);
}
assert.equal(eventChallengeSongScoreBonusPercent(event, song, targets.map(([, card]) => card)), 10);
assert.equal(eventChallengeSongScoreBonusPercent(event, song, []), 0);
}
assert.equal(eventChallengeSongScoreBonusPercent(event, 'm0358', targets.map(([, card]) => card)), 0);
});
test('전체 카드와 개화 단계의 획득 보너스는 인게임에서 확인한 비누적 합계와 일치한다', () => {
const awakeningBonuses: Record<number, number[]> = {
3: [0, 1, 1, 2, 2, 3],
4: [0, 6, 7, 8, 9, 10],
5: [0, 15, 18, 22, 26, 30]
};
const bonusCharacters = new Set(targets.map(([, , characterId]) => characterId) as readonly string[]);
const featuredIds = new Set(targets.map(([, cardId]) => cardId) as readonly string[]);
assert.deepEqual([...event.bonusCharacterIds].sort(), [...bonusCharacters].sort());
assert.deepEqual([...event.featuredCardIds].sort(), [...featuredIds].sort());
for (const card of cards) {
for (let awakening = 0; awakening <= 5; awakening++) {
const formation = bonusCharacters.has(card.characterId) ? 30 : 0;
const featured = featuredIds.has(card.id) ? 30 : 0;
const upgrade = awakeningBonuses[card.rarity]?.[awakening] ?? 0;
assert.deepEqual(eventBonusBreakdown(event, [card], () => awakening), {
formationPercent: formation,
featuredCardPercent: featured,
awakeningPercent: upgrade,
totalPercent: formation + featured + upgrade
}, `${card.id}, awakening ${awakening}`);
}
}
const featured = cards.filter(card => featuredIds.has(card.id));
assert.equal(featured.length, 3);
assert.equal(eventBonusBreakdown(event, featured, () => 0).totalPercent, 180);
assert.equal(eventBonusBreakdown(event, featured, () => 5).totalPercent, 270);
});