data: configure Ocean Chase live and score bonuses
Publish GitHub Pages output / publish (push) Successful in 36s
Publish GitHub Pages output / publish (push) Successful in 36s
This commit is contained in:
+7
-1
@@ -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 채보를 추가했습니다. 각 난이도의 점수 계산과 채보 표시를 지원합니다.
|
||||
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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))
|
||||
];
|
||||
|
||||
@@ -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 조건별 합계와 일치한다', () => {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
Reference in New Issue
Block a user