UX·FX v1.5 — 햅틱 단계 · 라이브 띠 · HERO 전광판 · Push 우선순위 · Shield · 이클립스 성공·실패 연출
This commit is contained in:
@@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
|
||||
|
||||
import '../theme/tokens.dart';
|
||||
import 'planet_evolution.dart';
|
||||
import 'planet_fx_scope.dart';
|
||||
import 'adaptive_planet_effects.dart';
|
||||
import '../i18n/app_locale.dart';
|
||||
|
||||
@@ -134,6 +135,8 @@ class Planet25D extends StatefulWidget {
|
||||
this.level,
|
||||
this.evolutionImminent = false,
|
||||
this.isEclipsed = false,
|
||||
this.eclipseFailed = false,
|
||||
this.eclipseSuccessId,
|
||||
this.isHibernating = false,
|
||||
this.isHeroTime = false,
|
||||
this.heroEventIds,
|
||||
@@ -161,6 +164,12 @@ class Planet25D extends StatefulWidget {
|
||||
final int? stage;
|
||||
final int? level;
|
||||
final bool isEclipsed;
|
||||
|
||||
/// 이클립스 실패 확정 — 파괴 표현 없이 밝기 · Glow · Particle 만 줄인다(v1.5 §5).
|
||||
final bool eclipseFailed;
|
||||
|
||||
/// 이클립스 성공 회차. 바뀌면 광량 복원 뒤 짧은 Orbit Burst(v1.5 §5 「그림자 이탈 → 복원 → Orbit Burst」).
|
||||
final String? eclipseSuccessId;
|
||||
final bool isHibernating;
|
||||
final bool isHeroTime;
|
||||
final Set<String>? heroEventIds;
|
||||
@@ -180,7 +189,7 @@ class Planet25D extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _Planet25DState extends State<Planet25D>
|
||||
with SingleTickerProviderStateMixin {
|
||||
with TickerProviderStateMixin {
|
||||
int get _quality => widget.lowEffects ? 2 : PlanetEffectsQuality.of(context);
|
||||
bool get _lowEffects => _quality >= 2;
|
||||
bool get _celebrating =>
|
||||
@@ -198,6 +207,9 @@ class _Planet25DState extends State<Planet25D>
|
||||
? 0.5
|
||||
: _celebrating
|
||||
? 1.3
|
||||
: widget.eclipseFailed
|
||||
// 실패의 감소 폭은 원문에 수치가 없어 이클립스 진행 중과 같은 값을 쓴다(해석 확인 항목).
|
||||
? 0.5
|
||||
: 1;
|
||||
late final AnimationController _c = AnimationController(
|
||||
vsync: this,
|
||||
@@ -217,6 +229,7 @@ class _Planet25DState extends State<Planet25D>
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
_fx = PlanetFxScope.of(context);
|
||||
_syncMotion();
|
||||
}
|
||||
|
||||
@@ -230,10 +243,32 @@ class _Planet25DState extends State<Planet25D>
|
||||
if (_motionEnabled) _c.repeat();
|
||||
}
|
||||
_syncMotion();
|
||||
_maybeBurst(old);
|
||||
}
|
||||
|
||||
/// 이클립스 성공 연출 — 광량 복원([_restore]) 뒤 Orbit Burst([orbitBurstDuration]).
|
||||
late final AnimationController _burst = AnimationController(
|
||||
vsync: this,
|
||||
duration: _restore + orbitBurstDuration,
|
||||
)..addStatusListener((status) {
|
||||
if (status == AnimationStatus.completed) _fx?.setActive(this, false);
|
||||
});
|
||||
static const _restore = Duration(seconds: 2);
|
||||
PlanetFxActivity? _fx;
|
||||
|
||||
void _maybeBurst(Planet25D old) {
|
||||
final id = widget.eclipseSuccessId;
|
||||
if (id == null || id == old.eclipseSuccessId || !_motionEnabled) return;
|
||||
// 한 화면에 강한 연출은 하나 — 진화 · HERO 가 재생 중이면 겹치지 않는다(v1.5 §1.1).
|
||||
if (_fx?.busy ?? false) return;
|
||||
_fx?.setActive(this, true);
|
||||
_burst.forward(from: 0);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_fx?.setActive(this, false);
|
||||
_burst.dispose();
|
||||
_c.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
@@ -252,10 +287,39 @@ class _Planet25DState extends State<Planet25D>
|
||||
heroEnabled: !widget.isEclipsed && !widget.isHibernating,
|
||||
animate: widget.animate,
|
||||
lowEffects: _lowEffects,
|
||||
builder: (stage) => Opacity(
|
||||
key: const ValueKey('planet-surface-light'),
|
||||
opacity: light.clamp(0.0, 1.0),
|
||||
child: _planet(context, stage, light),
|
||||
builder: (stage) => Stack(
|
||||
clipBehavior: Clip.none,
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
Opacity(
|
||||
key: const ValueKey('planet-surface-light'),
|
||||
opacity: light.clamp(0.0, 1.0),
|
||||
child: _planet(context, stage, light),
|
||||
),
|
||||
if (_burst.isAnimating)
|
||||
Positioned.fill(
|
||||
child: IgnorePointer(
|
||||
child: AnimatedBuilder(
|
||||
animation: _burst,
|
||||
builder: (context, _) {
|
||||
final total = _burst.duration!.inMilliseconds;
|
||||
final p =
|
||||
((_burst.value * total - _restore.inMilliseconds) /
|
||||
orbitBurstDuration.inMilliseconds)
|
||||
.clamp(0.0, 1.0);
|
||||
return CustomPaint(
|
||||
key: const ValueKey('eclipse-orbit-burst'),
|
||||
painter: _OrbitBurstPainter(
|
||||
p,
|
||||
widget.glow,
|
||||
_lowEffects,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -818,3 +882,25 @@ class _SustainedAura extends CustomPainter {
|
||||
bool shouldRepaint(_SustainedAura old) =>
|
||||
old.color != color || old.lowEffects != lowEffects;
|
||||
}
|
||||
|
||||
/// 이클립스 성공 Orbit Burst 길이. 원문은 「짧은」만 적어 Level Up 경량 연출(850ms)과 같게 둔다(해석 확인 항목).
|
||||
const orbitBurstDuration = Duration(milliseconds: 850);
|
||||
|
||||
class _OrbitBurstPainter extends CustomPainter {
|
||||
const _OrbitBurstPainter(this.progress, this.color, this.lowEffects);
|
||||
final double progress;
|
||||
final Color color;
|
||||
final bool lowEffects;
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
if (progress <= 0 || progress >= 1) return;
|
||||
paintOrbitBurst(canvas, size, progress, color, lowEffects);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(_OrbitBurstPainter old) =>
|
||||
old.progress != progress ||
|
||||
old.color != color ||
|
||||
old.lowEffects != lowEffects;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user