이클립스 방어 횟수 즉시 표시·진동 · Music Shake 영상 재생 중 흔들기 판정 · 결과 공유
This commit is contained in:
@@ -44,9 +44,6 @@ class FirestoreQuestRepository implements QuestRepository {
|
||||
/// 마지막 Music Shake 한 판의 판정표. 다 끝난 판만 서버에 보낸다.
|
||||
ShakeJudge? _shakeRun;
|
||||
|
||||
/// 곡 BPM 이 문서에 없을 때 — 🟡 운영 Quest 는 `quests.bpm` 을 채운다.
|
||||
static const defaultBpm = 100.0;
|
||||
|
||||
/// 오늘 걸은 수를 내는 기준선. 하루 단위라 앱을 껐다 켜도 이어진다.
|
||||
final WalkBaseline _baseline;
|
||||
|
||||
@@ -166,9 +163,10 @@ class FirestoreQuestRepository implements QuestRepository {
|
||||
final steps = type == QuestType.steps;
|
||||
final goal = steps
|
||||
? tiers[count.clamp(0, tiers.length - 1)]
|
||||
// Music Shake 의 목표는 곡의 구간 수다 — 진행도가 「n / 구간 수」로 찬다.
|
||||
// Music Shake 의 목표 줄은 Figma 「목표BPM 350」(`480:14466` · `480:14506`) — 곡 BPM 이다.
|
||||
// 없으면 0(화면은 「-」). 🟡 「목표BPM」의 의미는 회신 대기(Q-MS-01).
|
||||
: type == QuestType.musicShake
|
||||
? quest.intOr('totalSegments', dailyLimit)
|
||||
? quest.intOr('bpm')
|
||||
: dailyLimit;
|
||||
|
||||
return QuestCard(
|
||||
@@ -447,7 +445,6 @@ class FirestoreQuestRepository implements QuestRepository {
|
||||
|
||||
@override
|
||||
Stream<int> progressStream(QuestCard card) {
|
||||
if (card.type == QuestType.musicShake) return _shakeProgress(card);
|
||||
if (card.type != QuestType.steps) return const Stream<int>.empty();
|
||||
// 센서는 부팅 이후 누적을 준다. 하루 단위 기준선을 빼야 정책 원장 **B-26** 의
|
||||
// 구간(1 천·5 천·1 만·1.5 만보)과 같은 것을 세게 된다.
|
||||
@@ -519,49 +516,53 @@ class FirestoreQuestRepository implements QuestRepository {
|
||||
});
|
||||
}
|
||||
|
||||
/// 곡 박마다 한 구간 — 시작부터 박 수를 세어 내고, 마지막 구간 판정 창이 닫히면 구간 수를 낸다.
|
||||
/// 흔들기는 그동안만 모은다.
|
||||
Stream<int> _shakeProgress(QuestCard card) async* {
|
||||
_shakeRun = null;
|
||||
final quest = (await _backend.db.doc('quests/${card.id}').get()).data() ??
|
||||
@override
|
||||
Future<MusicShakeSetup> musicShakeSetup(String questId) async {
|
||||
final quest = (await _backend.db.doc('quests/$questId').get()).data() ??
|
||||
const <String, dynamic>{};
|
||||
final link = quest['link'] is Map
|
||||
? Map<String, dynamic>.from(quest['link'] as Map)
|
||||
: const <String, dynamic>{};
|
||||
final videoId = youtubeVideoId(link.strOr('url'));
|
||||
final bpm = quest['bpm'];
|
||||
final segments = quest.intOr('totalSegments');
|
||||
final bpm = quest['bpm'] is num && (quest['bpm'] as num) > 0
|
||||
? (quest['bpm'] as num).toDouble()
|
||||
: defaultBpm;
|
||||
if (segments <= 0) {
|
||||
// 영상 · BPM · 구간 수 중 하나라도 없으면 판을 열지 않는다 — 기본값을 지어내지 않는다.
|
||||
if (videoId == null || bpm is! num || bpm <= 0 || segments <= 0) {
|
||||
throw BackendException('E-QST-01', appL10n.questTypeUnverifiable);
|
||||
}
|
||||
final run = ShakeJudge(bpm: bpm, segments: segments, start: DateTime.now());
|
||||
final sub = _shakes().listen(run.add, onError: (_) {});
|
||||
try {
|
||||
for (var i = 1; i < segments; i++) {
|
||||
await Future<void>.delayed(run.beat);
|
||||
yield i;
|
||||
}
|
||||
await Future<void>.delayed(run.end.difference(DateTime.now()));
|
||||
_shakeRun = run;
|
||||
yield segments;
|
||||
} finally {
|
||||
await sub.cancel();
|
||||
}
|
||||
return MusicShakeSetup(
|
||||
videoId: videoId,
|
||||
title: link.strOr('title', _title(quest)),
|
||||
bpm: bpm.toDouble(),
|
||||
segments: segments,
|
||||
windows: ShakeWindows.from(await _policy()),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<DateTime> shakeEvents() => _shakes();
|
||||
|
||||
@override
|
||||
void recordShake(ShakeJudge run) => _shakeRun = run;
|
||||
|
||||
Future<Map<String, dynamic>> _verifyShake(QuestCard card) async {
|
||||
final run = _shakeRun;
|
||||
if (run == null) throw BackendException('E-QST-01', appL10n.questNoShake);
|
||||
final quest = (await _backend.db.doc('quests/${card.id}').get()).data() ??
|
||||
const <String, dynamic>{};
|
||||
final ids = quest['trackIds'];
|
||||
// 곡 ID 는 서버 대조용이다 — 화면에 띄우지 않는다.
|
||||
final trackId = ids is List && ids.isNotEmpty ? '${ids.first}' : quest.strOr('trackId');
|
||||
final link = quest['link'] is Map ? Map<String, dynamic>.from(quest['link'] as Map) : null;
|
||||
// 곡 ID 는 서버 대조용이다 — 화면에 띄우지 않는다. 카탈로그가 없으면 영상 ID 다.
|
||||
final trackId = ids is List && ids.isNotEmpty
|
||||
? '${ids.first}'
|
||||
: quest.strOr('trackId', youtubeVideoId(link?.strOr('url') ?? '') ?? '');
|
||||
_shakeRun = null;
|
||||
return _backend.call('fnQuestVerifyMusicShake', {
|
||||
'questId': card.id,
|
||||
'trackId': trackId,
|
||||
'totalSegments': run.segments,
|
||||
'judgement': run.judgement(),
|
||||
'occurredAt': run.end.toUtc().toIso8601String(),
|
||||
'occurredAt': DateTime.now().toUtc().toIso8601String(),
|
||||
...await _fandomField(),
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user