행성 Level 1~100 수치표 적용 · 행성 컬러 마스터

This commit is contained in:
Sn-Kinos
2026-10-05 22:20:51 +09:00
parent cc0f2fb292
commit 000c058097
12 changed files with 424 additions and 64 deletions
+33
View File
@@ -22,8 +22,41 @@ const planetColors = <(String, Color)>[
('purple', Color(0xFFC300C8)),
];
/// 컬러 마스터 `config/planetColors` 에서 읽은 **전체** 목록(숨긴 색 포함 — 이미 그 색인 행성을 그려야 한다).
/// 10/3 통합회신 #6 「발주사가 컬러 항목을 추가할 수 있는 구조 · 8색 고정 적용 안 함」. 읽기 전에는 [planetColors].
List<(String, Color)> _planetColorMaster = planetColors;
/// `config/planetColors` 문서 → (그리기용 전체, 생성 화면에 내놓을 노출 목록). 형식이 틀린 항목은 건너뛴다.
/// 읽을 수 있는 항목이 없으면 기본 목록([planetColors]).
({List<(String, Color)> all, List<(String, Color)> visible}) planetColorsFrom(
Map<String, dynamic>? doc,
) {
final all = <(String, Color)>[];
final visible = <(String, Color)>[];
final raw = doc?['colors'];
if (raw is List) {
for (final c in raw) {
if (c is! Map) continue;
final key = c['key'];
final hex = c['hex'];
final m = hex is String ? RegExp(r'^#?([0-9a-fA-F]{6})$').firstMatch(hex) : null;
if (key is! String || key.isEmpty || m == null) continue;
final entry = (key, Color(0xFF000000 | int.parse(m.group(1)!, radix: 16)));
all.add(entry);
if (c['visible'] != false) visible.add(entry);
}
}
return visible.isEmpty
? (all: [...all, ...planetColors], visible: planetColors)
: (all: all, visible: visible);
}
/// 마스터를 읽은 뒤 그리기 목록을 바꾼다.
void setPlanetColorMaster(List<(String, Color)> all) => _planetColorMaster = all;
/// 서버 `colorKey` → 칩 색. 모르는 키(서버 기본값 `violet` 포함)는 null — 원래 일러스트 그대로.
Color? planetColorOf(String? key) =>
_planetColorMaster.where((c) => c.$1 == key).firstOrNull?.$2 ??
planetColors.where((c) => c.$1 == key).firstOrNull?.$2;
/// Figma 본체에 UX/FX v1.5 §2의 Stage별 레이어를 조합한다.
+8
View File
@@ -8,7 +8,9 @@
/// 정책값이 아니다 — 정책은 `정책 설계서 최종본 v1.0` 이 정한다.
library;
import 'package:flutter/painting.dart' show Color;
import '../../core/i18n/app_locale.dart';
import '../../core/ui/planet_25d.dart' show planetColors;
import '../quest/quest_data.dart' show QuestType;
import '../inside/inside_data.dart' show InsideQuestion, SampleInsideRepository;
@@ -414,6 +416,9 @@ abstract interface class HomeRepository {
/// 성운 전환 목표 — 서버(`fn-nebula-create` · `fn-planet-create`)와 같은 `config/policy`
/// 키(`nebulaMinPop` · `nebulaMinMembers` · `nebulaDays`). 읽지 못하면 [defaultNebulaTargets].
Future<NebulaTargets> nebulaTargets();
/// 행성 생성 화면이 내놓을 컬러 — 컬러 마스터 `config/planetColors`(10/3 #6 · C-115). 읽지 못하면 Figma 기본 8색.
Future<List<(String, Color)>> planetColorOptions();
}
/// 성운 전환 목표 — POP · 창립 인원 · 목표 기간(일).
@@ -451,6 +456,9 @@ class SampleHomeRepository implements HomeRepository {
@override
Future<NebulaTargets> nebulaTargets() async => defaultNebulaTargets;
@override
Future<List<(String, Color)>> planetColorOptions() async => planetColors;
@override
Future<({String fandomId, bool primaryChanged})> createNebula(
String name,
@@ -3,6 +3,8 @@ import 'package:cloud_firestore/cloud_firestore.dart' show Timestamp;
import '../../core/data/backend.dart';
import '../../core/data/invite_code.dart';
import '../../core/data/server_clock.dart';
import '../../core/ui/planet_25d.dart' show planetColors, planetColorsFrom, setPlanetColorMaster;
import 'package:flutter/painting.dart' show Color;
import 'home_data.dart';
import '../inside/inside_repository_firestore.dart' show insideQuestionFrom;
import '../quest/quest_data.dart' show QuestType;
@@ -66,6 +68,19 @@ class FirestoreHomeRepository implements HomeRepository {
'colorKey': colorKey,
});
@override
Future<List<(String, Color)>> planetColorOptions() async {
try {
final master = planetColorsFrom(
(await _backend.db.doc('config/planetColors').get()).data(),
);
setPlanetColorMaster(master.all);
return master.visible;
} catch (_) {
return planetColors;
}
}
@override
Future<NebulaTargets> nebulaTargets() async {
try {
@@ -82,6 +97,8 @@ class FirestoreHomeRepository implements HomeRepository {
final uid = _backend.requireUid;
// 공유 링크 `?ref=` 용 — 기다리지 않는다.
InviteCode.ensure(_backend);
// 컬러 마스터 — HOME · GALAXY 행성이 발주사가 더한 색도 그리도록 먼저 읽어 둔다. 기다리지 않는다.
planetColorOptions();
final db = _backend.db;
final userSnap = await db.doc('users/$uid').get();
@@ -291,8 +308,15 @@ class FirestoreHomeRepository implements HomeRepository {
int? validInt(dynamic value) => value is int && value >= 0 ? value : null;
num? validNumber(dynamic value) =>
value is num && value.isFinite && value >= 0 ? value : null;
final current = validNumber(fandom['growthPop']);
final required = validInt(next?['requiredPop']);
// 정식 행성은 서버가 수치표 v1.4 대로 적은 레벨업 POP 진행도(생성 기준점 이후 · ZAP 가중치 반영)와
// 다음 Stage 진입 누계를 쓴다. 아직 판정 전인 행성만 구 마스터(`planetStages`)로 그린다.
final leveled = stage != null && stage > 0 && fandom['levelPop'] is num;
final current = leveled
? validNumber(fandom['levelPop'])
: validNumber(fandom['growthPop']);
final required = leveled
? validInt(fandom['nextStagePop'])
: validInt(next?['requiredPop']);
// 성운(Stage0)의 전환 목표는 서버와 같은 `config/policy` 키로 읽는다.
final policy = stage == 0
? (await _backend.db.doc('config/policy').get()).data()
@@ -325,7 +349,9 @@ class FirestoreHomeRepository implements HomeRepository {
// Q-PN-01 A안 — 탄생은 창립자만.
isFounder:
fandom['founderUid'] != null && fandom['founderUid'] == _backend.uid,
requiredSfpi: validNumber(next?['requiredSfpi']),
requiredSfpi: leveled
? validNumber(fandom['nextStageSfpi'])
: validNumber(next?['requiredSfpi']),
nebulaGoalPassed: goalPassed,
);
}
@@ -42,7 +42,8 @@ class PlanetCreateScreen extends StatefulWidget {
'shape6',
];
/// 색 키와 칩 색 — 렌더링과 같은 표([planetColors]).
/// 기본 색 키와 칩 색(Figma `2066:17910`). 화면은 컬러 마스터(`config/planetColors`, 10/3 #6)를
/// [HomeRepository.planetColorOptions] 로 읽어 덮는다 — 발주사가 더한 색도 고를 수 있다.
static const colors = planetColors;
@override
@@ -53,6 +54,19 @@ class _PlanetCreateScreenState extends State<PlanetCreateScreen> {
int _shape = 0;
int _color = 0;
bool _colorStep = false;
List<(String, Color)> _colors = PlanetCreateScreen.colors;
@override
void initState() {
super.initState();
widget.repository.planetColorOptions().then((options) {
if (!mounted || options.isEmpty) return;
setState(() {
_colors = options;
_color = _color.clamp(0, options.length - 1);
});
});
}
static const _placeholder = AssetImage(
'assets/figma/planet-shape-placeholder.png',
@@ -72,7 +86,7 @@ class _PlanetCreateScreenState extends State<PlanetCreateScreen> {
child: _colorStep
? ColorFiltered(
colorFilter: ColorFilter.mode(
PlanetCreateScreen.colors[_color].$2,
_colors[_color].$2,
BlendMode.modulate,
),
child: const Image(image: _placeholder),
@@ -141,7 +155,7 @@ class _PlanetCreateScreenState extends State<PlanetCreateScreen> {
);
/// 사본 `Frame 1984081340` — 28 원 · 간격 8 · 고른 칩 분홍 4 테두리, 나머지 흰 2.
Widget _colors() => Column(
Widget _colorPicker() => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(context.l10n.planetBgColor, style: _heading),
@@ -150,19 +164,19 @@ class _PlanetCreateScreenState extends State<PlanetCreateScreen> {
spacing: 8,
runSpacing: 8,
children: [
for (var i = 0; i < PlanetCreateScreen.colors.length; i++)
for (var i = 0; i < _colors.length; i++)
GestureDetector(
onTap: () => setState(() => _color = i),
child: Semantics(
button: true,
selected: _color == i,
label: PlanetCreateScreen.colors[i].$1,
label: _colors[i].$1,
child: Container(
width: 28,
height: 28,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: PlanetCreateScreen.colors[i].$2,
color: _colors[i].$2,
border: Border.all(
color: _color == i
? AppColors.cosmicAccent
@@ -200,7 +214,7 @@ class _PlanetCreateScreenState extends State<PlanetCreateScreen> {
children: [
_preview(),
const SizedBox(height: AppSpace.s5),
if (_colorStep) _colors() else _shapes(),
if (_colorStep) _colorPicker() else _shapes(),
],
),
),
@@ -257,7 +271,7 @@ class _PlanetCreateScreenState extends State<PlanetCreateScreen> {
await widget.repository.createPlanet(
fandomId: widget.fandomId,
planetType: PlanetCreateScreen.shapes[_shape],
colorKey: PlanetCreateScreen.colors[_color].$1,
colorKey: _colors[_color].$1,
);
if (mounted) Navigator.of(context).pop(widget.fandomId);
} on BackendException catch (e) {