FPI 사용자·국가 랭킹 서버 집계 · YouTube 스밍 · Music Shake 참여

This commit is contained in:
Sn-Kinos
2026-10-05 17:49:47 +09:00
parent 5238fc4ef3
commit 96a95a12d1
31 changed files with 738 additions and 116 deletions
@@ -8,8 +8,11 @@
/// 생성자로 다른 구현을 넣는다 — 센서 없는 환경에서도 옮기는 규칙을 검증할 수 있어야 한다.
library;
import 'dart:math' as math;
import 'package:geolocator/geolocator.dart';
import 'package:pedometer/pedometer.dart';
import 'package:sensors_plus/sensors_plus.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:shared_preferences/shared_preferences.dart';
@@ -42,6 +45,72 @@ typedef StepCountProbe = Stream<int> Function();
/// 현재 좌표 한 점을 내는 함수.
typedef PlaceProbe = Future<PlaceFix> Function();
/// 흔들기 한 번마다 그 시각을 내는 스트림.
typedef ShakeProbe = Stream<DateTime> Function();
/// 흔들기 감지 — 가속도 크기가 중력(9.8)을 크게 넘는 순간. 이클립스 방어 화면과 같은 문턱 ·
/// 간격이다(같은 동작을 같은 기준으로 센다).
Stream<DateTime> deviceShakes() async* {
var last = DateTime.fromMillisecondsSinceEpoch(0);
await for (final e in accelerometerEventStream()) {
final magnitude = math.sqrt(e.x * e.x + e.y * e.y + e.z * e.z);
final now = DateTime.now();
if (!magnitude.isFinite || magnitude < 18 ||
now.difference(last) < const Duration(milliseconds: 220)) {
continue;
}
last = now;
yield now;
}
}
/// Music Shake 구간 판정표 — `fn-quest-verify-music-shake` 가 받는 **증거**(원장 B-24).
///
/// 곡의 박(`quests.bpm`)마다 한 구간이다. 구간 박 시각 ±반 박 안에 흔들기가 있으면 맞춘 것,
/// 박에서 [perfectRatio] 박 이내면 perfect · 아니면 good, 없으면 miss. **등급은 서버가 다시
/// 매긴다** — 이 표가 스스로 모순되면(합 ≠ 구간 수) 서버가 F 로 끊는다.
class ShakeJudge {
ShakeJudge({required this.bpm, required this.segments, required this.start})
: assert(bpm > 0 && segments > 0);
final double bpm;
final int segments;
final DateTime start;
/// perfect 로 보는 박과의 거리(박 단위). 🟡 표시 판정용 — 등급 문턱은 서버가 가진다.
static const perfectRatio = 0.2;
final _shakes = <DateTime>[];
Duration get beat => Duration(microseconds: (60e6 / bpm).round());
/// 마지막 구간의 판정 창(박 + 반 박)까지 끝나는 시각.
DateTime get end => start.add(beat * segments - beat ~/ 2);
void add(DateTime at) => _shakes.add(at);
/// 구간별 판정 합계 — 서버 `ShakeJudgement` 와 같은 키.
Map<String, int> judgement() {
var perfect = 0, good = 0, miss = 0, combo = 0, maxCombo = 0;
final beatUs = beat.inMicroseconds;
for (var i = 0; i < segments; i++) {
final at = start.microsecondsSinceEpoch + beatUs * i;
final best = _shakes
.map((s) => (s.microsecondsSinceEpoch - at).abs())
.where((d) => d < beatUs / 2)
.fold<int?>(null, (m, d) => m == null || d < m ? d : m);
if (best == null) {
miss++;
combo = 0;
continue;
}
best <= beatUs * perfectRatio ? perfect++ : good++;
maxCombo = math.max(maxCombo, ++combo);
}
return {'perfect': perfect, 'good': good, 'miss': miss, 'maxCombo': maxCombo};
}
}
/// 걸음 센서 — `pedometer` 의 `stepCountStream`.
///
/// 값은 **부팅 이후 누적**이다. 오늘치 합계가 아니므로 저장소가 시작 시점을 기준으로