사용자 화면 문구·표본 데이터 · 퀘스트 참여 흐름 · HOME 진입 구획 · 미니맵 · 공통 확인 창 배치 수정

This commit is contained in:
Sn-Kinos
2026-10-05 00:44:39 +09:00
parent c8d29d032b
commit 8a0297eb92
66 changed files with 1812 additions and 959 deletions
@@ -19,6 +19,8 @@ import '../../../core/i18n/app_locale.dart';
/// - **게이지 조절 8개는 넣지 않는다(C-51).**
/// - 색은 테마 결정 전까지 프로토타입 값을 쓴다(C-49).
/// - 데이터는 받은 [GalaxySnapshot] 만 쓴다. 프로토타입의 표본 5행성은 쓰지 않는다.
/// - 궤도는 FPI 순위, 첫 화면 시점은 내 행성 중앙(C-103). 140pt 에서는 내 행성 간략 카드
/// 한 장만, 다른 행성 HUD 없음(C-105). 드래그 회전은 관성 없음(C-105).
///
/// 반응: 스냅샷이 바뀌면(순위·FPI) 행성이 새 궤도로 미끄러지고 HUD 숫자가 바뀐다(프로토타입
/// `updateFPI`). 가로로 끌면 장면이 돈다(±180°, 프로토타입 View Rotation 범위). 누르면 [onOpen].
@@ -33,7 +35,7 @@ class GalaxyMiniMap extends StatefulWidget {
final GalaxySnapshot data;
final VoidCallback onOpen;
/// 정사각 한 변. HOME 행성 영역 우측 상단(Q-DS-06 권고안 A · 회신 전 임시).
/// 정사각 한 변. HOME 은 약 140pt(C-105 · 자리는 C-113 → `HomeHero.miniMapAlignment`).
final double size;
@override
@@ -172,6 +174,10 @@ final _tanHalfFov = math.tan(35 * math.pi / 180);
const _hudW = 512.0;
const _hudH = 256.0;
/// 별·위성 점 크기(논리 px). 프로토타입 점은 원근으로 1px 아래로 줄어 GL 최소 크기(버퍼
/// 1px, 픽셀비 2 → 0.5pt)로 찍힌다. 0 이면 1 물리 px 이라 3x 기기에서 1/3 로 옅어진다.
const _dot = 0.4;
/// 1 논리 픽셀보다 가는 선은 머리카락 선(1 물리 픽셀)으로.
double _px(double w) => w < 1 ? 0 : w;
@@ -293,18 +299,22 @@ ui.Picture _hudPicture(PlanetNode p, Color color, double share) {
return rec.endRecording();
}
/// 반지름 1 단위의 광륜(6R, 가산 합성) + 발광 구체.
/// 프로토타입 광륜 반지름(행성 반지름 배수). `aura.scale = R × curScale × 6` 이 다시
/// curScale(Mass x1.5)로 커지는 group 안에 있어 지름은 `6 × 1.5 R` 이다.
const _auraR = 3 * _massVolume;
/// 반지름 1 단위의 광륜(가산 합성) + 발광 구체.
ui.Picture _spherePicture(Color c) {
final rec = ui.PictureRecorder();
final canvas = Canvas(rec);
canvas.drawCircle(
Offset.zero,
3,
_auraR,
Paint()
..blendMode = BlendMode.plus
..shader = ui.Gradient.radial(
Offset.zero,
3,
_auraR,
[
c.withValues(alpha: 0.4),
c.withValues(alpha: 0.15),
@@ -360,19 +370,22 @@ class GalaxyMiniMapPainter extends CustomPainter {
/// 그리는 행성 수(내 행성은 순위 밖이어도 더한다).
static const maxPlanets = 10;
/// 간략 카드(내 행성)의 최대 폭(미니맵 폭 대비).
static const hudFloor = 0.7;
/// 간략 카드(내 행성)의 최대 폭(미니맵 폭 대비). 넘치면 카드를 통째로 줄인다(숫자는 자르지 않는다).
static const hudFloor = 0.55;
/// 이 폭 이상이면 프로토타입 HUD 카드를 모든 행성에 그대로 그린다.
static const fullHudMin = 280.0;
/// 간략 카드 글자 크기(pt) — 이름 · 둘째 줄.
static const compactNamePt = 11.0;
static const compactLinePt = 9.0;
/// 간략 카드 글자 크기(pt) — 이름 · 숫자 줄. 프로토타입 텍스처의 줄 구성(이름 / RANK · FPI /
/// LIVE)을 세 줄로 줄인 것이다. 140pt 에서 카드가 장면을 덮지 않게 한 줄을 짧게 둔다.
static const compactNamePt = 9.0;
static const compactLinePt = 8.0;
/// 간략 카드 둘째 줄 — `#순위 · FPI n · LIVE n`.
static String compactLine(PlanetNode p) =>
'#${p.rank} · FPI ${formatPop(p.fpi)} · LIVE ${formatPop(p.online)}';
/// 간략 카드 숫자 줄 — `#순위 · FPI n` / `LIVE n`.
static List<String> compactLines(PlanetNode p) => [
'#${p.rank} · FPI ${formatPop(p.fpi)}',
'LIVE ${formatPop(p.online)}',
];
ui.Picture? _compact;
Object? _compactKey;
@@ -388,42 +401,49 @@ class GalaxyMiniMapPainter extends CustomPainter {
if (key == _compactKey && _compact != null) return _compact!;
_compact?.dispose();
_compactKey = key;
const pad = 6.0, barW = 3.0, gap = 4.0;
final textMax = maxW - pad * 2 - barW - gap;
TextPainter line(String text, double pt, FontWeight weight, Color c) =>
TextPainter(
text: TextSpan(
text: text,
style: TextStyle(
fontFamily: AppFonts.body,
fontSize: pt,
fontWeight: weight,
height: 1.2,
color: c,
),
),
textDirection: TextDirection.ltr,
maxLines: 2,
ellipsis: '…',
)..layout(maxWidth: textMax);
final name = line(
b.node.name.toUpperCase(),
compactNamePt,
FontWeight.w800,
b.color,
);
// 좁으면 LIVE 앞에서 둘째 줄이 한 번 꺾인다(글자 크기는 줄이지 않는다).
final stats = line(
compactLine(b.node),
compactLinePt,
FontWeight.w700,
const Color(0xE6FFFFFF),
);
final textW = math.max(name.width, stats.width);
const pad = 4.0, barW = 2.0, gap = 3.0;
TextPainter line(
String text,
double pt,
FontWeight weight,
Color c, [
double maxWidth = double.infinity,
]) => TextPainter(
text: TextSpan(
text: text,
style: TextStyle(
fontFamily: AppFonts.body,
fontSize: pt,
fontWeight: weight,
height: 1.15,
color: c,
),
),
textDirection: TextDirection.ltr,
maxLines: 1,
ellipsis: '…',
)..layout(maxWidth: maxWidth);
final texts = [
(b.node.name.toUpperCase(), compactNamePt, FontWeight.w800, b.color),
for (final t in compactLines(b.node))
(t, compactLinePt, FontWeight.w700, const Color(0xE6FFFFFF)),
];
final stats = [
for (final (t, pt, w, c) in texts.skip(1)) line(t, pt, w, c),
];
final statsW = stats.fold<double>(0, (m, l) => math.max(m, l.width));
// 긴 이름은 말줄임 — 카드 폭은 숫자 줄(또는 최대 폭)이 정한다.
final (nt, npt, nw, nc) = texts.first;
final nameMax = math.max(statsW, maxW - pad * 2 - gap - barW);
final lines = [line(nt, npt, nw, nc, nameMax), ...stats];
final textW = lines.fold<double>(0, (m, l) => math.max(m, l.width));
final textH = lines.fold<double>(0, (m, l) => m + l.height);
final w = pad * 2 + textW + gap + barW;
final h = pad * 2 + name.height + 2 + stats.height;
final h = pad * 2 + textH;
// 숫자 줄이 최대 폭을 넘을 때만 카드를 줄인다 — 줄을 자르면 순위·FPI 숫자가 사라진다.
final k = math.min(1.0, maxW / w);
final rec = ui.PictureRecorder();
final canvas = Canvas(rec);
final canvas = Canvas(rec)..scale(k);
final box = Rect.fromLTWH(0, 0, w, h);
canvas.drawRect(box, Paint()..color = const Color(0xE6000514));
canvas.drawRect(
@@ -433,8 +453,13 @@ class GalaxyMiniMapPainter extends CustomPainter {
..strokeWidth = 1
..color = b.color.withValues(alpha: 0.8),
);
name.paint(canvas, const Offset(pad, pad));
stats.paint(canvas, Offset(pad, pad + name.height + 2));
var y = pad;
for (final l in lines) {
l.paint(canvas, Offset(pad, y));
y += l.height;
l.dispose();
}
// 프로토타입 막대 `160 * fpi / 5000` — 1위 대비 비율.
final barH = h - pad * 2;
canvas.drawRect(
Rect.fromLTWH(
@@ -445,13 +470,8 @@ class GalaxyMiniMapPainter extends CustomPainter {
),
Paint()..color = _emerald,
);
compactText = [
(b.node.name.toUpperCase(), compactNamePt),
(compactLine(b.node), compactLinePt),
];
name.dispose();
stats.dispose();
_compactSize = Size(w, h);
compactText = [for (final (t, pt, _, _) in texts) (t, pt * k)];
_compactSize = Size(w * k, h * k);
return _compact = rec.endRecording();
}
@@ -466,6 +486,15 @@ class GalaxyMiniMapPainter extends CustomPainter {
@visibleForTesting
Color? colorOf(String id) => _bodies[id]?.color;
/// 행성의 현재 월드 좌표(x, z). 장면 원점 = 화면 중앙. 없으면 null.
@visibleForTesting
Offset? worldOf(String id) =>
_bodies[id] == null ? null : Offset(_bodies[id]!.wx, _bodies[id]!.wz);
/// 마지막으로 그린 간략 카드 크기(논리 px).
@visibleForTesting
Size get compactSize => _compactSize;
/// 행성의 현재 궤도 반지름(월드 단위). 없으면 null.
@visibleForTesting
double? orbitOf(String id) => _bodies[id]?.rad;
@@ -607,13 +636,14 @@ class GalaxyMiniMapPainter extends CustomPainter {
..shader = ui.Gradient.radial(
center,
r,
const [Color(0x0A10B981), Color(0x033B82F6), Color(0x00000000)],
const [Color(0x0410B981), Color(0x013B82F6), Color(0x00000000)],
const [0, 0.4, 0.8],
),
);
canvas.restore();
}
// 프로토타입 별 8000개. 화면 넓이당 밀도는 390×844 화면 기준으로 맞춘다.
// 프로토타입 별 8000개. 미니맵은 휴대폰 전체 화면 장면의 축소판으로 보이게, 화면 넓이당
// 밀도를 390×844 기준으로 맞춘다(덜어내지 않으면 140pt 에 별이 5배 빽빽하다).
final keep = (size.width * size.height / (390 * 844)).clamp(0.0, 1.0);
final pts = <double>[];
for (var i = 0; i < 8000; i++) {
@@ -632,7 +662,7 @@ class GalaxyMiniMapPainter extends CustomPainter {
ui.PointMode.points,
Float32List.fromList(pts),
Paint()
..strokeWidth = 0
..strokeWidth = _dot
..color = const Color(0xCCBFBFFF),
);
_bg = rec.endRecording();
@@ -708,11 +738,9 @@ class GalaxyMiniMapPainter extends CustomPainter {
}
if (out.isNotEmpty) {
_fill
..strokeWidth = 0
..strokeWidth = _dot
..blendMode = BlendMode.plus
..color = b.color.withValues(
alpha: 0.9 * (2 * _scale * focal / d).clamp(0.2, 1.0),
);
..color = b.color.withValues(alpha: 0.9);
canvas.drawRawPoints(ui.PointMode.points, out, _fill);
_fill.blendMode = BlendMode.srcOver;
}