FPI 사용자·국가 랭킹 서버 집계 · YouTube 스밍 · Music Shake 참여
This commit is contained in:
@@ -20,7 +20,9 @@ class PlanetNode {
|
||||
this.stage,
|
||||
this.colorKey,
|
||||
bool? isCenter,
|
||||
}) : isCenter = isCenter ?? isMine;
|
||||
bool? ranked,
|
||||
}) : isCenter = isCenter ?? isMine,
|
||||
ranked = ranked ?? stage != 0;
|
||||
|
||||
/// Firestore `fandoms/{id}` 문서 ID. HUD 에서 행성 상세로 넘어갈 때
|
||||
/// **이 값으로 저장소를 만든다** — 없으면 어느 행성을 눌러도 같은 상세가 뜬다.
|
||||
@@ -38,8 +40,16 @@ class PlanetNode {
|
||||
///
|
||||
/// Stage0 성운은 랭킹 모집단이 아니다(원장 B-08 · B-17). 성운에는 순위를 매기지 않고
|
||||
/// 정식 행성 수 + 1 을 넣어 **가장 바깥 궤도 자리로만** 쓴다 — HUD 는 「-」로 그린다.
|
||||
/// 최근 7 일 유효 활동이 없는 정식 행성도 같다([ranked]).
|
||||
final int rank;
|
||||
|
||||
/// FPI 랭킹 모집단인가(B-17 — 정식 행성 중 최근 7 일 유효 활동 존재). false 면 [rank] 는
|
||||
/// 궤도 자리일 뿐이라 화면은 순위 대신 「-」를 쓴다.
|
||||
final bool ranked;
|
||||
|
||||
/// 화면에 쓰는 순위 문구 — 모집단 밖이면 「-」.
|
||||
String get rankLabel => ranked ? '$rank' : '-';
|
||||
|
||||
/// FPI 총점. 산출식은 정책 설계서 §P-03 — 초기 MVP 는 단순 합산이다.
|
||||
final int fpi;
|
||||
|
||||
@@ -104,7 +114,7 @@ class GalaxySnapshot {
|
||||
GalaxySnapshot get rankingView {
|
||||
PlanetNode? top;
|
||||
for (final p in planets) {
|
||||
if (p.stage != 0 && (top == null || p.rank < top.rank)) top = p;
|
||||
if (p.ranked && (top == null || p.rank < top.rank)) top = p;
|
||||
}
|
||||
return GalaxySnapshot(
|
||||
planets: [
|
||||
@@ -118,6 +128,8 @@ class GalaxySnapshot {
|
||||
online: p.online,
|
||||
isMine: p.isMine,
|
||||
stage: p.stage,
|
||||
colorKey: p.colorKey,
|
||||
ranked: p.ranked,
|
||||
isCenter: identical(p, top),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -57,11 +57,13 @@ class FirestoreGalaxyRepository implements GalaxyRepository {
|
||||
|
||||
// 순위는 1·1·3 공동 순위(C-19). 실값 `fpiScore` 동률만 같은 순위이고 동점 내부는
|
||||
// fandomId 오름차순으로 고정한다 — 쿼리 순서(동점은 ID 내림차순)를 그대로 쓰지 않는다.
|
||||
// Stage0 성운은 순위를 매기기 **전에** 뺀다(B-08 · B-17) — 성운이 끼면 정식 행성의
|
||||
// 순위 번호가 밀린다. 성운은 맨 바깥 궤도 자리(정식 수 + 1)에 둔다.
|
||||
bool isNebula(QueryDocumentSnapshot<Map<String, dynamic>> d) =>
|
||||
d.data()['stage'] == 0;
|
||||
final formal = fandoms.where((d) => !isNebula(d)).toList();
|
||||
// 랭킹 모집단 밖(B-08 · B-17)은 순위를 매기기 **전에** 뺀다 — 끼면 정식 행성의 순위
|
||||
// 번호가 밀린다. Stage0 성운과 서버가 `rankingEligible: false`(최근 7 일 유효 활동 없음)로
|
||||
// 내린 행성이다. 이들은 맨 바깥 궤도 자리(모집단 수 + 1)에 두고 순위는 「-」로 그린다.
|
||||
// 필드가 아직 없는 문서(집계 배포 전)는 Stage 만으로 판정한다.
|
||||
bool unranked(QueryDocumentSnapshot<Map<String, dynamic>> d) =>
|
||||
d.data()['stage'] == 0 || d.data()['rankingEligible'] == false;
|
||||
final formal = fandoms.where((d) => !unranked(d)).toList();
|
||||
final planets = <PlanetNode>[
|
||||
for (final (doc, rank) in rankWithTies(
|
||||
formal,
|
||||
@@ -69,8 +71,8 @@ class FirestoreGalaxyRepository implements GalaxyRepository {
|
||||
(d) => d.id,
|
||||
))
|
||||
_node(doc.id, doc.data(), rank, myFandomId),
|
||||
for (final doc in fandoms.where(isNebula))
|
||||
_node(doc.id, doc.data(), formal.length + 1, myFandomId),
|
||||
for (final doc in fandoms.where(unranked))
|
||||
_node(doc.id, doc.data(), formal.length + 1, myFandomId, ranked: false),
|
||||
];
|
||||
final byId = {for (final p in planets) p.id: p};
|
||||
final membershipIds = [for (final d in (await membershipsF).docs) d.id];
|
||||
@@ -81,8 +83,15 @@ class FirestoreGalaxyRepository implements GalaxyRepository {
|
||||
]);
|
||||
for (final doc in missing) {
|
||||
final data = doc.data();
|
||||
if (data != null)
|
||||
byId[doc.id] = _node(doc.id, data, formal.length + 1, myFandomId);
|
||||
if (data != null) {
|
||||
byId[doc.id] = _node(
|
||||
doc.id,
|
||||
data,
|
||||
formal.length + 1,
|
||||
myFandomId,
|
||||
ranked: false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return GalaxySnapshot(
|
||||
@@ -100,8 +109,10 @@ class FirestoreGalaxyRepository implements GalaxyRepository {
|
||||
String id,
|
||||
Map<String, dynamic> data,
|
||||
int rank,
|
||||
String myFandomId,
|
||||
) => PlanetNode(
|
||||
String myFandomId, {
|
||||
bool ranked = true,
|
||||
}) => PlanetNode(
|
||||
ranked: ranked,
|
||||
id: id,
|
||||
stage: switch (data['stage']) {
|
||||
int n when n >= 0 && n <= 6 => n,
|
||||
|
||||
@@ -909,11 +909,8 @@ class _HudCard extends StatelessWidget {
|
||||
style: t.titleSmall,
|
||||
),
|
||||
),
|
||||
// Stage0 성운은 랭킹 모집단이 아니다(B-17).
|
||||
_Metric(
|
||||
label: 'RANK',
|
||||
value: planet.stage == 0 ? '-' : '${planet.rank}',
|
||||
),
|
||||
// Stage0 성운 · 최근 7 일 활동 없는 행성은 랭킹 모집단이 아니다(B-17).
|
||||
_Metric(label: 'RANK', value: planet.rankLabel),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: AppSpace.s1),
|
||||
|
||||
@@ -286,7 +286,7 @@ class _Hit extends StatelessWidget {
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
'${planet.rank}',
|
||||
planet.rankLabel,
|
||||
style: t.titleMedium?.copyWith(color: AppColors.cosmicAccent),
|
||||
),
|
||||
const SizedBox(width: AppSpace.s4),
|
||||
|
||||
@@ -234,7 +234,7 @@ class _PlanetHit extends StatelessWidget {
|
||||
height: side,
|
||||
child: Semantics(
|
||||
button: true,
|
||||
label: context.l10n.galaxyPlanetRank(planet.name, '${planet.rank}'),
|
||||
label: context.l10n.galaxyPlanetRank(planet.name, planet.rankLabel),
|
||||
selected: selected,
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
|
||||
@@ -446,7 +446,7 @@ class GalaxyMiniMapPainter extends CustomPainter {
|
||||
/// 행성 위 HUD 네 줄 — 프로토타입 텍스처와 같은 문구.
|
||||
static List<String> hudLines(PlanetNode p) => [
|
||||
p.name.toUpperCase(),
|
||||
'RANK #${p.rank}',
|
||||
'RANK #${p.rankLabel}',
|
||||
'FPI: ${p.fpi}',
|
||||
'LIVE: ${formatPop(p.online)}',
|
||||
];
|
||||
@@ -515,7 +515,7 @@ class GalaxyMiniMapPainter extends CustomPainter {
|
||||
|
||||
/// 간략 카드 숫자 줄 — `#순위 · FPI n` / `LIVE n`.
|
||||
static List<String> compactLines(PlanetNode p) => [
|
||||
'#${p.rank} · FPI ${formatPop(p.fpi)}',
|
||||
'#${p.rankLabel} · FPI ${formatPop(p.fpi)}',
|
||||
'LIVE ${formatPop(p.online)}',
|
||||
];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user