이클립스 방어 횟수 즉시 표시·진동 · Music Shake 영상 재생 중 흔들기 판정 · 결과 공유

This commit is contained in:
Sn-Kinos
2026-10-05 21:39:22 +09:00
parent 65669b969e
commit 17c658b272
30 changed files with 1337 additions and 144 deletions
+27 -27
View File
@@ -1,4 +1,5 @@
import 'package:fake_cloud_firestore/fake_cloud_firestore.dart';
import 'package:cloud_firestore/cloud_firestore.dart' show SetOptions;
import 'package:fancake/core/data/backend.dart';
import 'package:fancake/features/quest/quest_data.dart';
import 'package:fancake/features/quest/quest_evidence.dart';
@@ -335,52 +336,51 @@ service cloud.firestore {
expect(reward.rewardedPop, 0);
});
test('Music Shake — 박마다 구간을 세고 판정표 · 곡 ID 를 서버에 보낸다', () async {
test('Music Shake — 영상 · BPM · 구간 수를 퀘스트에서 읽고 판정표 · 곡 ID 를 서버에 보낸다', () async {
final db = await _seed();
// 600 BPM = 박 100ms · 3 구간. 박 0 과 박 1 근처에서만 흔든다.
await db.doc('config/policy').set({
'musicShake': {'perfectWindow': 0.1, 'goodWindow': 0.4, 'pop': 3},
}, SetOptions(merge: true));
await db.doc('quests/shake').set({
'type': 'musicshake', 'title': '응원 흔들기', 'dailyLimit': 1,
'trackIds': ['track-a'], 'totalSegments': 3, 'bpm': 600,
'totalSegments': 3, 'bpm': 120,
'link': {'url': 'https://youtu.be/WMweEpGlu_U', 'title': 'BTS - Butter'},
});
final backend = _RecordingBackend(db);
final repo = FirestoreQuestRepository(
backend,
shakes: () async* {
final start = DateTime.now();
yield start;
await Future<void>.delayed(const Duration(milliseconds: 100));
yield start.add(const Duration(milliseconds: 140));
},
);
final repo = FirestoreQuestRepository(backend);
final card = _card(await repo.load(), 'shake');
expect(card.type.verifiable, isTrue);
expect(card.goal, 3);
expect(card.progress, 0);
// Figma 「목표BPM」 — 목표 줄은 곡 BPM 이다. 보상은 운영값 musicShake.pop.
expect(card.goal, 120);
expect(card.rewardPop, 3);
final setup = await repo.musicShakeSetup('shake');
expect(setup.videoId, 'WMweEpGlu_U');
expect(setup.title, 'BTS - Butter');
expect((setup.bpm, setup.segments), (120.0, 3));
expect((setup.windows.perfect, setup.windows.good), (0.1, 0.4));
// 판을 끝내기 전에는 서버를 부르지 않는다.
await expectLater(repo.verify(card), throwsA(isA<BackendException>()));
expect(backend.calls, isEmpty);
expect(await repo.progressStream(card).toList(), [1, 2, 3]);
repo.recordShake(ShakeJudge(bpm: 120, segments: 3, windows: setup.windows)
..add(Duration.zero));
await repo.verify(card);
final call = backend.calls.single;
expect(call.name, 'fnQuestVerifyMusicShake');
expect(call.payload['questId'], 'shake');
expect(call.payload['trackId'], 'track-a');
expect(call.payload['trackId'], 'WMweEpGlu_U');
expect(call.payload['totalSegments'], 3);
final j = call.payload['judgement'] as Map;
expect(j['perfect']! + j['good']! + j['miss']!, 3);
expect(j['miss'], 1);
expect(call.payload['judgement'], {'perfect': 1, 'good': 0, 'miss': 2, 'maxCombo': 1});
});
test('ShakeJudge — 박과의 거리로 perfect · good · miss 를 가르고 최장 연속을 센다', () {
final start = DateTime(2026, 10, 5);
final judge = ShakeJudge(bpm: 60, segments: 4, start: start)
..add(start.add(const Duration(milliseconds: 100))) // 박 0 · perfect
..add(start.add(const Duration(milliseconds: 1400))) // 박 1 · good
..add(start.add(const Duration(milliseconds: 3000))); // 박 3 · perfect, 박 2 miss
expect(judge.judgement(), {'perfect': 2, 'good': 1, 'miss': 1, 'maxCombo': 2});
expect(judge.end, start.add(const Duration(milliseconds: 3500)));
test('Music Shake — 영상 · BPM · 구간 수가 없으면 판을 열지 않는다(값을 지어내지 않는다)', () async {
final db = await _seed();
await db.doc('quests/shake').set({'type': 'musicshake', 'title': '흔들기', 'totalSegments': 3});
final repo = FirestoreQuestRepository(_RecordingBackend(db));
await expectLater(repo.musicShakeSetup('shake'), throwsA(isA<BackendException>()));
expect(_card(await repo.load(), 'shake').goal, 0);
});
test('생성은 fn-quest-create 로 — 종료일은 날짜 + 그 시각의 UTC 오프셋(C-06)', () async {