이클립스 사전 경고 연출 · 브리핑 출처·게시시각 · PASSPORT 역사적 사건 Stamp

This commit is contained in:
Sn-Kinos
2026-10-08 15:43:12 +09:00
parent 62b9637db9
commit 586039da0a
26 changed files with 1223 additions and 1013 deletions
+60 -4
View File
@@ -137,6 +137,8 @@ class Planet25D extends StatefulWidget {
this.isEclipsed = false,
this.eclipseFailed = false,
this.eclipseSuccessId,
this.eclipseWarnAt,
this.eclipseStartAt,
this.isHibernating = false,
this.isHeroTime = false,
this.heroEventIds,
@@ -170,6 +172,11 @@ class Planet25D extends StatefulWidget {
/// 이클립스 성공 회차. 바뀌면 광량 복원 뒤 짧은 Orbit Burst(v1.5 §5 「그림자 이탈 → 복원 → Orbit Burst」).
final String? eclipseSuccessId;
/// 사전 경고 창(서버 회차 `warnAt` → `startAt`, 원장 C-22 발생 3분 전). 이 사이에만
/// v1.5 §5 「행성 뒤 어두운 그림자 접근, 별빛·Glow 점진 감소」를 그린다([eclipseApproach]).
final DateTime? eclipseWarnAt;
final DateTime? eclipseStartAt;
final bool isHibernating;
final bool isHeroTime;
final Set<String>? heroEventIds;
@@ -204,12 +211,12 @@ class _Planet25DState extends State<Planet25D>
double get _light => widget.isHibernating
? 0.35
: widget.isEclipsed
? 0.5
? eclipseLight
: _celebrating
? 1.3
: widget.eclipseFailed
// 실패의 감소 폭은 원문에 수치가 없어 이클립스 진행 중과 같은 값을 쓴다(해석 확인 항목).
? 0.5
? eclipseLight
: 1;
late final AnimationController _c = AnimationController(
vsync: this,
@@ -376,6 +383,13 @@ class _Planet25DState extends State<Planet25D>
builder: (context, _) {
final t = _motionEnabled ? _c.value : 0.0; // 0~1
final wave = math.sin(t * 2 * math.pi); // -1~1
// 사전 경고 — 매 프레임 서버 시각 기준으로 다시 잰다. 동작 줄이기면 프레임이 돌지 않아
// 그린 시점의 위치에 멈춰 있다(움직임 없이 상태만 전달).
final warn = widget.isEclipsed
? 0.0
: eclipseApproach(widget.eclipseWarnAt, widget.eclipseStartAt, DateTime.now()) ?? 0.0;
// 별빛(입자) · Glow 를 이클립스 진행 중 광량(0.5)까지 점진적으로 낮춘다.
final dim = 1 - (1 - eclipseLight) * warn;
return Stack(
clipBehavior: Clip.none,
alignment: Alignment.center,
@@ -400,9 +414,10 @@ class _Planet25DState extends State<Planet25D>
? 0.55
: 0.42) +
wave * (stage == 6 ? 0.02 : 0.05)) *
glowStrength,
glowStrength *
dim,
),
glow.withValues(alpha: 0.10 * glowStrength),
glow.withValues(alpha: 0.10 * glowStrength * dim),
glow.withValues(alpha: 0),
],
stops: const [0.45, 0.72, 1],
@@ -423,6 +438,31 @@ class _Planet25DState extends State<Planet25D>
),
),
),
// 이클립스 사전 경고 — 행성 뒤로 어두운 그림자가 우상단에서 다가와 Glow 를 가린다.
if (warn > 0)
Positioned.fill(
child: IgnorePointer(
child: Transform.translate(
key: const ValueKey('eclipse-shadow'),
offset: Offset(s, -s * 0.5) * eclipseShadowTravel * (1 - warn),
child: Transform.scale(
scale: 1.15,
child: Opacity(
opacity: 0.85 * warn,
child: const DecoratedBox(
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: RadialGradient(
colors: [Color(0xFF05040D), Color(0xCC05040D), Color(0x0005040D)],
stops: [0, 0.6, 1],
),
),
),
),
),
),
),
),
// 6. 링 — 뒤쪽 절반. 본체 뒤에 깔린다.
if (ring)
Positioned.fill(
@@ -522,6 +562,7 @@ class _Planet25DState extends State<Planet25D>
count:
(particleCount *
light *
dim *
(_quality == 2
? 0.3
: _quality == 1
@@ -883,6 +924,21 @@ class _SustainedAura extends CustomPainter {
old.color != color || old.lowEffects != lowEffects;
}
/// 이클립스 진행 중 광량. 사전 경고의 Glow 감소 · 실패 감소가 같은 값을 쓴다(해석 확인 Q-A28).
const eclipseLight = 0.5;
/// 사전 경고 그림자의 출발 거리(행성 한 변 대비). 원문에 수치가 없는 임시값(해석 확인 항목).
const eclipseShadowTravel = 0.9;
/// 사전 경고 진행 0~1 — [warnAt] 에 0, 발생 시각 [startAt] 에 1. 창이 없거나 경고 전이면 null.
/// 발생 시각이 지나도 서버가 회차를 열기 전(스케줄러 지연 수 초)에는 1 로 둔다 — 그림자가 빠졌다 다시 어두워지지 않게.
double? eclipseApproach(DateTime? warnAt, DateTime? startAt, DateTime now) {
if (warnAt == null || startAt == null || !startAt.isAfter(warnAt)) return null;
if (now.isBefore(warnAt)) return null;
return (now.difference(warnAt).inMilliseconds / startAt.difference(warnAt).inMilliseconds)
.clamp(0.0, 1.0);
}
/// 이클립스 성공 Orbit Burst 길이. 원문은 「짧은」만 적어 Level Up 경량 연출(850ms)과 같게 둔다(해석 확인 항목).
const orbitBurstDuration = Duration(milliseconds: 850);