218 lines
5.6 KiB
Dart
218 lines
5.6 KiB
Dart
import 'package:flutter/material.dart';
|
|
|
|
import 'package:asciineverdie/l10n/app_localizations.dart';
|
|
import 'package:asciineverdie/src/core/model/game_state.dart';
|
|
|
|
/// 스탯 표시 패널 (Phase 8: 실시간 변화 표시)
|
|
///
|
|
/// 장비 변경이나 버프 시 스탯 변화량을 애니메이션으로 표시
|
|
class StatsPanel extends StatefulWidget {
|
|
const StatsPanel({super.key, required this.stats});
|
|
|
|
final Stats stats;
|
|
|
|
@override
|
|
State<StatsPanel> createState() => _StatsPanelState();
|
|
}
|
|
|
|
class _StatsPanelState extends State<StatsPanel>
|
|
with SingleTickerProviderStateMixin {
|
|
// 변화량 맵 (스탯 이름 -> 변화량)
|
|
final Map<String, int> _statChanges = {};
|
|
|
|
// 변화 애니메이션 컨트롤러
|
|
late AnimationController _animationController;
|
|
late Animation<double> _fadeAnimation;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
|
|
_animationController = AnimationController(
|
|
duration: const Duration(milliseconds: 2000),
|
|
vsync: this,
|
|
);
|
|
|
|
_fadeAnimation = Tween<double>(begin: 1.0, end: 0.0).animate(
|
|
CurvedAnimation(
|
|
parent: _animationController,
|
|
curve: const Interval(0.5, 1.0, curve: Curves.easeOut),
|
|
),
|
|
);
|
|
}
|
|
|
|
@override
|
|
void didUpdateWidget(StatsPanel oldWidget) {
|
|
super.didUpdateWidget(oldWidget);
|
|
|
|
// 스탯 변화 감지
|
|
_detectChanges(oldWidget.stats, widget.stats);
|
|
}
|
|
|
|
void _detectChanges(Stats oldStats, Stats newStats) {
|
|
final changes = <String, int>{};
|
|
|
|
if (newStats.str != oldStats.str) {
|
|
changes['str'] = newStats.str - oldStats.str;
|
|
}
|
|
if (newStats.con != oldStats.con) {
|
|
changes['con'] = newStats.con - oldStats.con;
|
|
}
|
|
if (newStats.dex != oldStats.dex) {
|
|
changes['dex'] = newStats.dex - oldStats.dex;
|
|
}
|
|
if (newStats.intelligence != oldStats.intelligence) {
|
|
changes['int'] = newStats.intelligence - oldStats.intelligence;
|
|
}
|
|
if (newStats.wis != oldStats.wis) {
|
|
changes['wis'] = newStats.wis - oldStats.wis;
|
|
}
|
|
if (newStats.cha != oldStats.cha) {
|
|
changes['cha'] = newStats.cha - oldStats.cha;
|
|
}
|
|
if (newStats.hpMax != oldStats.hpMax) {
|
|
changes['hpMax'] = newStats.hpMax - oldStats.hpMax;
|
|
}
|
|
if (newStats.mpMax != oldStats.mpMax) {
|
|
changes['mpMax'] = newStats.mpMax - oldStats.mpMax;
|
|
}
|
|
|
|
if (changes.isNotEmpty) {
|
|
setState(() {
|
|
_statChanges
|
|
..clear()
|
|
..addAll(changes);
|
|
});
|
|
|
|
// 애니메이션 재시작
|
|
_animationController
|
|
..reset()
|
|
..forward().then((_) {
|
|
if (mounted) {
|
|
setState(() {
|
|
_statChanges.clear();
|
|
});
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_animationController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final l10n = L10n.of(context);
|
|
final stats = [
|
|
('str', l10n.statStr, widget.stats.str),
|
|
('con', l10n.statCon, widget.stats.con),
|
|
('dex', l10n.statDex, widget.stats.dex),
|
|
('int', l10n.statInt, widget.stats.intelligence),
|
|
('wis', l10n.statWis, widget.stats.wis),
|
|
('cha', l10n.statCha, widget.stats.cha),
|
|
('hpMax', l10n.statHpMax, widget.stats.hpMax),
|
|
('mpMax', l10n.statMpMax, widget.stats.mpMax),
|
|
];
|
|
|
|
return ListView.builder(
|
|
itemCount: stats.length,
|
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
|
shrinkWrap: true,
|
|
physics: const NeverScrollableScrollPhysics(),
|
|
itemBuilder: (context, index) {
|
|
final stat = stats[index];
|
|
final change = _statChanges[stat.$1];
|
|
|
|
return _StatRow(
|
|
label: stat.$2,
|
|
value: stat.$3,
|
|
change: change,
|
|
fadeAnimation: _fadeAnimation,
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
/// 개별 스탯 행 위젯
|
|
class _StatRow extends StatelessWidget {
|
|
const _StatRow({
|
|
required this.label,
|
|
required this.value,
|
|
this.change,
|
|
required this.fadeAnimation,
|
|
});
|
|
|
|
final String label;
|
|
final int value;
|
|
final int? change;
|
|
final Animation<double> fadeAnimation;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Row(
|
|
children: [
|
|
// 라벨 (Flexible로 오버플로우 방지)
|
|
Expanded(
|
|
child: Text(
|
|
label,
|
|
style: const TextStyle(fontSize: 11),
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
),
|
|
// 값
|
|
Text(
|
|
'$value',
|
|
style: const TextStyle(fontSize: 11, fontWeight: FontWeight.bold),
|
|
),
|
|
// 변화량 표시
|
|
if (change != null) ...[
|
|
const SizedBox(width: 2),
|
|
AnimatedBuilder(
|
|
animation: fadeAnimation,
|
|
builder: (context, child) {
|
|
return Opacity(
|
|
opacity: fadeAnimation.value,
|
|
child: _ChangeIndicator(change: change!),
|
|
);
|
|
},
|
|
),
|
|
],
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
/// 변화량 표시 위젯
|
|
class _ChangeIndicator extends StatelessWidget {
|
|
const _ChangeIndicator({required this.change});
|
|
|
|
final int change;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final isPositive = change > 0;
|
|
final color = isPositive ? Colors.green : Colors.red;
|
|
final icon = isPositive ? Icons.arrow_upward : Icons.arrow_downward;
|
|
final text = isPositive ? '+$change' : '$change';
|
|
|
|
return Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Icon(icon, size: 10, color: color),
|
|
Text(
|
|
text,
|
|
style: TextStyle(
|
|
fontSize: 10,
|
|
color: color,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|