이클립스 흐름 — 배너 진입 · 이벤트 버튼 · 오버레이 · 방어 진행 표시 · 흔들기 감지 통일

This commit is contained in:
Sn-Kinos
2026-10-05 19:05:21 +09:00
parent be623d2e46
commit 576dcfbdfd
27 changed files with 1369 additions and 369 deletions
+5
View File
@@ -112,8 +112,13 @@ enum QuestType {
int? rewardFrom(Map<String, dynamic> quest, Map<String, dynamic> policy) {
// 시청 · 스트리밍은 무보상 참여다(C-04) — 문서 값과 무관하게 0.
if (this == QuestType.streaming) return 0;
// Music Shake 는 BPM 판정 성공 시 일반 POP(원장 B-24). 서버(`fn-quest-verify-music-shake`)가
// 퀘스트의 `rewardPop` 을, 없으면 `config/policy.stepsPop` 을 기본액으로 쓰고 판정 등급
// 배수를 곱한다. 화면은 기본액(C 등급)을 적는다 — 운영값이 둘 다 없을 때만 중립 문구.
final raw = this == QuestType.attend
? policy['attendPop']
: this == QuestType.musicShake
? quest['rewardPop'] ?? policy['stepsPop']
: quest['rewardPop'];
return raw is num ? raw.round() : null;
}
+52 -12
View File
@@ -48,22 +48,62 @@ 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;
/// 흔들기 한 번을 세는 규칙 — Music Shake · 이클립스 방어 · HERO TIME 이 함께 쓴다.
///
/// **단위는 두 플랫폼 모두 m/s²(중력 포함)다.** sensors_plus 가 iOS 의 g 값에 9.81 을
/// 곱해 넘긴다. 플랫폼 차이는 **표본 주기**였다(2026-10-05 실기기 — 같은 시간 흔들어
/// iPhone 16 · Galaxy 63). 기본 주기(200ms)를 iOS 는 그대로 지켜 초당 5 표본만 오고,
/// Android 는 요청을 힌트로만 받아 훨씬 촘촘히 보낸다. 흔들기의 가속도 정점은 수십 ms
/// 라 5Hz 표본은 대부분을 놓친다. 그래서 [sampling] 을 명시하고(약 50Hz),
/// 정점 하나를 한 번만 세도록 [rearm] 아래로 내려와야 다시 센다(히스테리시스).
class ShakeDetector {
ShakeDetector({
this.threshold = 18,
this.rearm = 13,
this.gap = const Duration(milliseconds: 220),
});
/// 흔들기로 보는 가속도 크기(m/s²). 정지 상태는 중력 9.8 이다.
final double threshold;
/// 이 아래로 내려와야 다음 흔들기를 센다. 한 정점이 표본 여럿에 걸쳐도 한 번이다.
final double rearm;
/// 두 흔들기 사이 최소 간격. 이보다 촘촘한 값은 같은 동작의 진동이다.
final Duration gap;
/// 표본 주기. 두 플랫폼이 같은 주기로 보도록 기본값(200ms)을 쓰지 않는다.
static const sampling = SensorInterval.gameInterval;
bool _armed = true;
DateTime _last = DateTime.fromMillisecondsSinceEpoch(0);
/// 표본 하나를 넣는다. 흔들기 한 번으로 셀 표본이면 true.
bool add(double x, double y, double z, DateTime at) {
final magnitude = math.sqrt(x * x + y * y + z * z);
if (!magnitude.isFinite) return false;
if (magnitude < rearm) {
_armed = true;
return false;
}
last = now;
yield now;
if (!_armed || magnitude < threshold || at.difference(_last) < gap) {
return false;
}
_armed = false;
_last = at;
return true;
}
}
/// 단말 가속도계에서 흔들기 한 번마다 그 시각을 낸다.
Stream<DateTime> deviceShakes() {
final detector = ShakeDetector();
return accelerometerEventStream(samplingPeriod: ShakeDetector.sampling)
.map((e) => (e, DateTime.now()))
.where((s) => detector.add(s.$1.x, s.$1.y, s.$1.z, s.$2))
.map((s) => s.$2);
}
/// Music Shake 구간 판정표 — `fn-quest-verify-music-shake` 가 받는 **증거**(원장 B-24).
///
/// 곡의 박(`quests.bpm`)마다 한 구간이다. 구간 박 시각 ±반 박 안에 흔들기가 있으면 맞춘 것,