212 lines
6.5 KiB
Dart
212 lines
6.5 KiB
Dart
import 'dart:async';
|
|
|
|
import 'package:fancake/features/eclipse/eclipse_data.dart';
|
|
import 'package:fancake/features/galaxy/galaxy_data.dart';
|
|
|
|
/// 활성 이클립스가 없는 상태.
|
|
///
|
|
/// **이클립스와 무관한 화면 검증의 기본값이다.** [SampleEclipseRepository] 는 목
|
|
/// 모드 시연을 위해 늘 활성 이클립스를 내므로, 그것을 쓰면 배너와 라이브 띠가
|
|
/// 화면 상단을 차지해 F1·F2·F3 의 레이아웃 검증이 이클립스 유무에 딸려 흔들린다.
|
|
class NoEclipseRepository implements EclipseRepository {
|
|
const NoEclipseRepository();
|
|
|
|
@override
|
|
Stream<EclipseState?> watch({String? fandomId, bool includeResults = false}) => Stream.value(null);
|
|
|
|
@override
|
|
Stream<List<LiveEvent>> liveEvents({String? fandomId, int limit = 20}) =>
|
|
Stream.value(const []);
|
|
|
|
@override
|
|
Future<DefendResult> defend({
|
|
required String fandomId,
|
|
required String eclipseId,
|
|
required String clientSeq,
|
|
required int shakes,
|
|
}) async =>
|
|
const DefendResult(accepted: 0, clamped: 0, myTotal: 0, duplicate: false);
|
|
}
|
|
|
|
/// 값을 밖에서 밀어 넣는 저장소. 스트림이 바뀔 때 화면이 따라오는지 본다.
|
|
///
|
|
/// 방어 호출은 실제로 나가지 않고 [calls] 에 기록만 한다 — 연타가 **몇 번의
|
|
/// 호출로 묶였는지**와 `count` 합이 맞는지가 검증 대상이다.
|
|
class FakeEclipseRepository implements EclipseRepository {
|
|
FakeEclipseRepository({EclipseState? state}) {
|
|
if (state != null) push(state);
|
|
}
|
|
|
|
final _eclipse = StreamController<EclipseState?>.broadcast();
|
|
final _events = StreamController<List<LiveEvent>>.broadcast();
|
|
|
|
/// 마지막으로 밀어 넣은 값. 새 구독자가 곧바로 받는다.
|
|
EclipseState? _last;
|
|
List<LiveEvent> _lastEvents = const [];
|
|
|
|
/// (eclipseId, clientSeq, shakes) 기록.
|
|
final List<(String, String, int)> calls = [];
|
|
|
|
/// 다음 방어 호출이 던질 실패. null 이면 성공한다.
|
|
Object? failWith;
|
|
|
|
/// 서버가 받아들였다고 답할 양. null 이면 보낸 값을 그대로 받아들인다.
|
|
int? acceptedOverride;
|
|
|
|
void push(EclipseState? state) {
|
|
_last = state;
|
|
if (_eclipse.hasListener) _eclipse.add(state);
|
|
}
|
|
|
|
void pushEvents(List<LiveEvent> events) {
|
|
_lastEvents = events;
|
|
if (_events.hasListener) _events.add(events);
|
|
}
|
|
|
|
Future<void> dispose() async {
|
|
await _eclipse.close();
|
|
await _events.close();
|
|
}
|
|
|
|
@override
|
|
Stream<EclipseState?> watch({String? fandomId, bool includeResults = false}) async* {
|
|
yield _last;
|
|
yield* _eclipse.stream;
|
|
}
|
|
|
|
@override
|
|
Stream<List<LiveEvent>> liveEvents({String? fandomId, int limit = 20}) async* {
|
|
yield _lastEvents;
|
|
yield* _events.stream;
|
|
}
|
|
|
|
@override
|
|
Future<DefendResult> defend({
|
|
required String fandomId,
|
|
required String eclipseId,
|
|
required String clientSeq,
|
|
required int shakes,
|
|
}) async {
|
|
calls.add((eclipseId, clientSeq, shakes));
|
|
final failure = failWith;
|
|
if (failure != null) throw failure;
|
|
final accepted = acceptedOverride ?? shakes;
|
|
return DefendResult(
|
|
accepted: accepted,
|
|
clamped: shakes - accepted,
|
|
myTotal: calls.fold(0, (a, c) => a + c.$3),
|
|
duplicate: false,
|
|
);
|
|
}
|
|
}
|
|
|
|
/// 검증에 쓰는 이클립스 한 건. 값은 인자로 덮는다.
|
|
EclipseState eclipseFixture({
|
|
String fandomId = 'bts',
|
|
String eclipseId = 'ecl-1',
|
|
int erosionRate = 40,
|
|
int defense = 60_000,
|
|
int targetDefense = 100_000,
|
|
int defenseCount = 12,
|
|
bool defended = false,
|
|
EclipseResult? result,
|
|
DateTime? endsAt,
|
|
}) => EclipseState(
|
|
fandomId: fandomId,
|
|
eclipseId: eclipseId,
|
|
fandomName: 'BTS',
|
|
erosionRate: erosionRate,
|
|
defense: defense,
|
|
targetDefense: targetDefense,
|
|
defenseCount: defenseCount,
|
|
defended: defended,
|
|
result: result,
|
|
// 마감이 없으면 화면이 「지났는가」를 물을 수 없다. 기본을 넉넉히 둔다.
|
|
endsAt: endsAt ?? DateTime.now().add(const Duration(minutes: 20)),
|
|
);
|
|
|
|
/// 구독을 몇 번 열었고 몇 개가 살아 있는지 센다.
|
|
///
|
|
/// 실시간 동기화의 두 고장은 **끊지 않는 것**(백그라운드에서 읽기 비용이 계속
|
|
/// 든다)과 **다시 열지 않는 것**(복귀한 화면이 잠들기 전 값을 그린다)이다. 둘 다
|
|
/// 화면만 보고는 구분되지 않으므로 구독 수를 직접 센다.
|
|
class CountingEclipseRepository implements EclipseRepository {
|
|
CountingEclipseRepository(this.state);
|
|
|
|
final EclipseState? state;
|
|
|
|
/// 지금까지 열린 구독 수(누적).
|
|
int subscriptions = 0;
|
|
|
|
/// 아직 살아 있는 구독 수.
|
|
int active = 0;
|
|
|
|
@override
|
|
Stream<EclipseState?> watch({String? fandomId, bool includeResults = false}) {
|
|
late StreamController<EclipseState?> controller;
|
|
controller = StreamController<EclipseState?>(
|
|
onListen: () {
|
|
subscriptions += 1;
|
|
active += 1;
|
|
controller.add(state);
|
|
},
|
|
onCancel: () {
|
|
active -= 1;
|
|
},
|
|
);
|
|
return controller.stream;
|
|
}
|
|
|
|
@override
|
|
Stream<List<LiveEvent>> liveEvents({String? fandomId, int limit = 20}) =>
|
|
Stream.value(const []);
|
|
|
|
@override
|
|
Future<DefendResult> defend({
|
|
required String fandomId,
|
|
required String eclipseId,
|
|
required String clientSeq,
|
|
required int shakes,
|
|
}) async =>
|
|
const DefendResult(accepted: 0, clamped: 0, myTotal: 0, duplicate: false);
|
|
}
|
|
|
|
/// ZAP(행성 성장 기여) 호출을 기록하는 갤럭시 저장소.
|
|
///
|
|
/// 별첨 B **B-23** — ZAP 은 이클립스 방어가 아니라 행성 성장 기여다. 이 스텁으로
|
|
/// 「ZAP 이 방어 함수가 아니라 POP 소비 경로로 나가는가」를 본다.
|
|
class RecordingGalaxyRepository implements GalaxyRepository {
|
|
RecordingGalaxyRepository({this.snapshot});
|
|
|
|
/// 돌려줄 스냅샷. 없으면 표본을 쓴다.
|
|
final GalaxySnapshot? snapshot;
|
|
|
|
/// (fandomId, clientSeq, count) 기록.
|
|
final List<(String, String, int)> zaps = [];
|
|
|
|
/// 서버가 확정했다고 답할 잔액. -1 이면 「모름」이다.
|
|
int balanceAfter = -1;
|
|
|
|
/// 다음 호출이 던질 실패. null 이면 성공한다.
|
|
Object? failWith;
|
|
|
|
@override
|
|
Future<void> setPrimary(String fandomId) async {}
|
|
|
|
@override
|
|
Future<GalaxySnapshot> load() async =>
|
|
snapshot ?? await const SampleGalaxyRepository().load();
|
|
|
|
@override
|
|
Future<ZapResult> zap({
|
|
required String fandomId,
|
|
required int count,
|
|
required String clientSeq,
|
|
}) async {
|
|
zaps.add((fandomId, clientSeq, count));
|
|
final failure = failWith;
|
|
if (failure != null) throw failure;
|
|
return ZapResult(txId: 'tx-${zaps.length}', balanceAfter: balanceAfter);
|
|
}
|
|
}
|