Music Shake 첫 박 시각 · 카운트다운 · 옛 일시 정지 판 정리
This commit is contained in:
@@ -178,6 +178,8 @@ export const fnQuestProgress = onCall(async (req) => {
|
||||
total: count(data, 'total', 1_000_000),
|
||||
positionMs: count(data, 'positionMs', 86_400_000),
|
||||
...(hits !== undefined ? { hits } : {}),
|
||||
// 판을 시작할 때의 첫 박 — 운영값이 바뀌면 앱이 옛 일시 정지를 버리고 새 판으로 시작한다.
|
||||
...(typeof data.firstBeatMs === 'number' ? { firstBeatMs: count(data, 'firstBeatMs', 86_400_000) } : {}),
|
||||
...(tally ? { judgement: tally } : {}),
|
||||
updatedAt: FieldValue.serverTimestamp(),
|
||||
},
|
||||
|
||||
@@ -119,6 +119,13 @@ export const fnQuestVerifyMusicShake = onCall(async (req) => {
|
||||
if (typeof questSegments === 'number' && questSegments !== totalSegments) {
|
||||
fail(ERR.questAbnormal, '곡의 구간 수가 퀘스트 정의와 다릅니다.');
|
||||
}
|
||||
// 첫 박 시각(영상 시각 기준 박 위치 `firstBeatMs + i·60/bpm`)도 같은 기준이어야 한다.
|
||||
const questFirstBeat = quest.get('firstBeatMs');
|
||||
const firstBeatMs = typeof questFirstBeat === 'number' ? questFirstBeat : 0;
|
||||
const sentFirstBeat = req.data?.firstBeatMs;
|
||||
if (sentFirstBeat !== undefined && sentFirstBeat !== firstBeatMs) {
|
||||
fail(ERR.questAbnormal, '첫 박 시각이 퀘스트 정의와 다릅니다.');
|
||||
}
|
||||
const limit = await dailyLimitOf(questId);
|
||||
const state = await progress(uid, questId, key);
|
||||
if (state.count >= limit) {
|
||||
@@ -134,7 +141,7 @@ export const fnQuestVerifyMusicShake = onCall(async (req) => {
|
||||
type: 'musicShake',
|
||||
fandomId: optionalStr(req.data, 'fandomId') ?? '',
|
||||
value: judgement.perfect,
|
||||
evidence: { musicShake: { trackId, totalSegments, judgements: judgement } },
|
||||
evidence: { musicShake: { trackId, totalSegments, firstBeatMs, judgements: judgement } },
|
||||
questId,
|
||||
verified: grade !== 'F',
|
||||
rejectReason: grade === 'F' ? ERR.questIncomplete : null,
|
||||
|
||||
@@ -321,10 +321,10 @@ describe('클라이언트가 고른 ID 로 한도를 다시 열 수 없다 (리
|
||||
|
||||
it('Music Shake: 곡은 퀘스트 카탈로그와 대조하고 하루 한도는 곡이 아니라 퀘스트 단위다', async () => {
|
||||
await db().doc('quests/music-e2e').set({
|
||||
type: 'musicShake', trackId: 't1', totalSegments: 10, dailyLimit: 1,
|
||||
type: 'musicShake', trackId: 't1', totalSegments: 10, firstBeatMs: 17_000, dailyLimit: 1,
|
||||
});
|
||||
const shake = (trackId: string, extra: Record<string, unknown> = {}) => call(fnQuestVerifyMusicShake, {
|
||||
questId: 'music-e2e', trackId, totalSegments: 10,
|
||||
questId: 'music-e2e', trackId, totalSegments: 10, firstBeatMs: 17_000,
|
||||
judgement: { perfect: 10, good: 0, miss: 0, maxCombo: 10 }, ...extra,
|
||||
});
|
||||
expect(await codeOf(() => shake('t1', { questId: 'no-such-music' }))).toBe('E-QST-01');
|
||||
@@ -332,11 +332,15 @@ describe('클라이언트가 고른 ID 로 한도를 다시 열 수 없다 (리
|
||||
expect(await codeOf(() => shake('t1', {
|
||||
totalSegments: 1, judgement: { perfect: 1, good: 0, miss: 0, maxCombo: 1 },
|
||||
}))).toBe('E-QST-02');
|
||||
// 첫 박 시각이 퀘스트 정의와 다르면 판정표를 받지 않는다.
|
||||
expect(await codeOf(() => shake('t1', { firstBeatMs: 0 }))).toBe('E-QST-02');
|
||||
// 동시 요청도 하루 1회. 곡 ID 를 바꿔도 다시 열리지 않는다.
|
||||
await Promise.allSettled([shake('t1'), shake('t1')]);
|
||||
expect(await codeOf(() => shake('t1'))).toBe('E-POP-03');
|
||||
const ledger = await db().collection(`users/${UID}/popTransactions`).get();
|
||||
expect(ledger.size).toBe(1);
|
||||
const logs = await db().collection(`users/${UID}/activityLogs`).where('questId', '==', 'music-e2e').get();
|
||||
expect(logs.docs[0]!.get('evidence.musicShake.firstBeatMs')).toBe(17_000);
|
||||
});
|
||||
|
||||
/** 오늘의 출석으로 기본 보상 원장 행을 만든다 — 광고 증분의 기준 행이다. */
|
||||
|
||||
Reference in New Issue
Block a user