이클립스 사전 경고 연출 · 브리핑 출처·게시시각 · PASSPORT 역사적 사건 Stamp
This commit is contained in:
@@ -6,6 +6,8 @@ class PlanetVisualState {
|
||||
this.isEclipsed = false,
|
||||
this.eclipseFailed = false,
|
||||
this.eclipseSuccessId,
|
||||
this.eclipseWarnAt,
|
||||
this.eclipseStartAt,
|
||||
this.isHibernating = false,
|
||||
this.isHeroTime = false,
|
||||
this.heroEventIds,
|
||||
@@ -51,6 +53,10 @@ class PlanetVisualState {
|
||||
|
||||
/// 서버 판정 성공 회차 ID — 바뀌는 순간 Orbit Burst 한 번(v1.5 §5 성공).
|
||||
final String? eclipseSuccessId;
|
||||
|
||||
/// 경보 단계(`scheduled`) 회차의 사전 경고 창 — v1.5 §5 「그림자 접근」. 그 밖에는 null.
|
||||
final DateTime? eclipseWarnAt;
|
||||
final DateTime? eclipseStartAt;
|
||||
final bool isHibernating;
|
||||
final bool isHeroTime;
|
||||
|
||||
|
||||
@@ -158,6 +158,7 @@ class Repositories {
|
||||
var isEclipsed = false;
|
||||
var eclipseFailed = false;
|
||||
String? eclipseSuccessId;
|
||||
(DateTime, DateTime)? eclipseWarning;
|
||||
var isHeroTime = false;
|
||||
Set<String>? heroEventIds;
|
||||
Timer? heroExpiry;
|
||||
@@ -178,6 +179,8 @@ class Repositories {
|
||||
isEclipsed: isEclipsed,
|
||||
eclipseFailed: eclipseFailed,
|
||||
eclipseSuccessId: eclipseSuccessId,
|
||||
eclipseWarnAt: eclipseWarning?.$1,
|
||||
eclipseStartAt: eclipseWarning?.$2,
|
||||
isHeroTime: isHeroTime,
|
||||
heroEventIds: heroEventIds,
|
||||
heroEndsAt: heroEndsAt,
|
||||
@@ -196,8 +199,12 @@ class Repositories {
|
||||
emit();
|
||||
}, onError: out.addError);
|
||||
// 판정 결과까지 받는다 — 활성 회차만 보면 마감 순간 null 이 되어 성공 · 실패를 가를 수 없다.
|
||||
final danger = eclipse.watch(fandomId: fandomId, includeResults: true).listen((state) {
|
||||
// 경보 단계 회차도 받는다 — 사전 경고 「그림자 접근」(v1.5 §5).
|
||||
final danger = eclipse
|
||||
.watch(fandomId: fandomId, includeResults: true, includeScheduled: true)
|
||||
.listen((state) {
|
||||
(isEclipsed, eclipseFailed, eclipseSuccessId) = eclipseLook(state, DateTime.now());
|
||||
eclipseWarning = eclipseWarnWindow(state);
|
||||
emit();
|
||||
}, onError: out.addError);
|
||||
// 기존 선물 이벤트만 읽는다. 구매 시도/로컬 캐시는 축제 확정이 아니다.
|
||||
@@ -272,7 +279,9 @@ class Repositories {
|
||||
/// 최근 이클립스 회차 → 행성 표현 (진행 중 감소 · 실패 감소 · 성공 회차 ID).
|
||||
/// 판정 대기(마감 후 결과 전)는 셋 다 아니다 — 오류를 실패처럼 보이지 않는다(v1.5 §5 집계 오류).
|
||||
(bool, bool, String?) eclipseLook(EclipseState? state, DateTime now) {
|
||||
if (state == null) return (false, false, null);
|
||||
// 경보 단계는 아직 이클립스가 아니다(그림자 접근은 [eclipseWarnWindow]). 다음 회차가 열렸으니
|
||||
// 이전 회차의 실패 감소도 여기서 풀린다.
|
||||
if (state == null || state.scheduled) return (false, false, null);
|
||||
final won = state.defended || state.result == EclipseResult.success;
|
||||
return (
|
||||
state.result == null && !won && !state.isExpired(now),
|
||||
@@ -280,3 +289,9 @@ class Repositories {
|
||||
won ? state.eclipseId : null,
|
||||
);
|
||||
}
|
||||
|
||||
/// 경보 단계 회차의 (warnAt, startAt). 경보가 아니거나 시각이 없으면 null.
|
||||
(DateTime, DateTime)? eclipseWarnWindow(EclipseState? state) => switch (state) {
|
||||
EclipseState(scheduled: true, warnAt: final warn?, startAt: final start?) => (warn, start),
|
||||
_ => null,
|
||||
};
|
||||
|
||||
@@ -47,6 +47,8 @@ class LivePlanet25D extends StatelessWidget {
|
||||
isEclipsed: state?.isEclipsed ?? false,
|
||||
eclipseFailed: state?.eclipseFailed ?? false,
|
||||
eclipseSuccessId: state?.eclipseSuccessId,
|
||||
eclipseWarnAt: state?.eclipseWarnAt,
|
||||
eclipseStartAt: state?.eclipseStartAt,
|
||||
isHibernating: state?.isHibernating ?? false,
|
||||
isHeroTime: state?.isHeroTime ?? false,
|
||||
heroEventIds: state?.heroEventIds,
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user