생성 때 고른 행성 형태·색 표시 · HOME 주간 정보

This commit is contained in:
Sn-Kinos
2026-10-05 23:21:44 +09:00
parent 55424bd4a2
commit c8c1a1ae67
40 changed files with 688 additions and 40 deletions
+85
View File
@@ -333,8 +333,12 @@ class HomeSnapshot {
required this.insides,
this.items = const [],
this.nebulas = const [],
this.weekly,
});
/// HOME 주간 정보(A-37 · HOME 정보위계 v1.0 §2 · §3). 소속 행성이 없으면 null.
final HomeWeekly? weekly;
/// 활동 POP 잔액. 정책 설계서 §P-02 — 활동/지원 원장은 분리되어 있고
/// 홈 상단에는 활동 POP 을 띄운다.
final int popBalance;
@@ -364,6 +368,87 @@ class HomeSnapshot {
final List<ItemProduct> items;
}
/// HOME 주간 정보 — 발주사 9/9 검토회신 2항 「행성의 주간 순위 · 이번 주 성장 정도 · 이번 주
/// 기여량 및 기여 비율 · 보유 POP」(HOME 정보위계 v1.0 §2 의 2 · 3 · 6 · 7번). 값의 뜻은 §3(별첨 B):
///
/// - 순위는 **주간 공식 Snapshot**(`rankingSnapshots`, B-20)의 순위다 — 실시간 순위가 아니다.
/// 변동은 직전 Snapshot 대비(C-43 「순위 동향 · 변동값」). 양수 = 올라감.
/// - 이번 주 성장은 **두 축(Growth POP · sFPI)** 각각의 증분 — 단일 백분율로 합치지 않는다.
/// - 기여 비율은 **행성 주간 성장 총량 대비 본인 비중**.
/// - 보유 POP 은 **일반 POP / 기여 POP** 분리 표기.
///
/// 「이번 주」의 시작은 마지막 주간 Snapshot 시각(KST 월요일 00:10, Q-A11 임시값)이다. 그 시각의
/// 행성 누계가 기준선이라 Snapshot 이 없거나 기준선 필드가 없는 옛 Snapshot 이면 그 값은 null.
class HomeWeekly {
const HomeWeekly({
this.rank,
this.rankChange,
this.weekGrowthPop,
this.weekSfpi,
this.myWeekPop,
this.myShare,
required this.generalPop,
required this.supportPop,
});
final int? rank;
final int? rankChange;
final num? weekGrowthPop;
final num? weekSfpi;
/// 이번 주 내가 이 행성 성장에 보탠 Growth POP(`users/{uid}/growthEvents` 합). 못 읽으면 null.
final num? myWeekPop;
/// 0~1. 행성 주간 성장이 0 이거나 모르면 null.
final double? myShare;
final int generalPop;
final int supportPop;
/// Snapshot 두 개(최신 · 직전)의 `planets` 와 현재 행성 누계 · 내 이번 주 성장 기여로 만든다.
factory HomeWeekly.from({
required String fandomId,
required List<Map<String, dynamic>> snapshots,
required Map<String, dynamic> fandom,
required num? myWeekPop,
required Map<String, dynamic> wallet,
}) {
Map<String, dynamic>? entryOf(Map<String, dynamic>? snap) {
final planets = snap?['planets'];
if (planets is! List) return null;
for (final p in planets) {
if (p is Map && p['fandomId'] == fandomId) return p.cast<String, dynamic>();
}
return null;
}
final latest = entryOf(snapshots.firstOrNull);
final previous = entryOf(snapshots.length > 1 ? snapshots[1] : null);
int? rankOf(Map<String, dynamic>? e) =>
e?['rank'] is int ? e!['rank'] as int : null;
num? delta(String key) {
final now = fandom[key];
final base = latest?[key];
return now is num && base is num ? now - base : null;
}
final rank = rankOf(latest);
final prev = rankOf(previous);
final weekGrowth = delta('growthPop');
int intOf(dynamic v) => v is num ? v.toInt() : 0;
return HomeWeekly(
rank: rank,
rankChange: rank != null && prev != null ? prev - rank : null,
weekGrowthPop: weekGrowth,
weekSfpi: delta('sfpi'),
myWeekPop: myWeekPop,
myShare: weekGrowth != null && weekGrowth > 0 && myWeekPop != null
? (myWeekPop / weekGrowth).clamp(0.0, 1.0).toDouble()
: null,
generalPop: intOf(wallet['activityBalance']),
supportPop: intOf(wallet['supportBalance']),
);
}
}
/// 아이템(Hero Gift) 상품 한 줄 — 사본 `아이템 구입`(`252:160233`) 카드.
///
/// 구매하면 구매자에게 기여 POP, 팬덤 Drop Pool 에 같은 수량의 일반 POP 이 생긴다