feat(ui): Phase 8 실시간 피드백 시스템 구현
- StatsPanel: 스탯 변화 애니메이션 (증감 표시) - CombatLog: 전투 이벤트 로그 위젯 - NotificationService: 큐 기반 알림 관리 - NotificationOverlay: 레벨업/퀘스트 완료 팝업 알림 - GamePlayScreen: 새 위젯 통합
This commit is contained in:
150
lib/src/features/game/widgets/combat_log.dart
Normal file
150
lib/src/features/game/widgets/combat_log.dart
Normal file
@@ -0,0 +1,150 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// 전투 로그 엔트리 (Combat Log Entry)
|
||||
class CombatLogEntry {
|
||||
const CombatLogEntry({
|
||||
required this.message,
|
||||
required this.timestamp,
|
||||
this.type = CombatLogType.normal,
|
||||
});
|
||||
|
||||
final String message;
|
||||
final DateTime timestamp;
|
||||
final CombatLogType type;
|
||||
}
|
||||
|
||||
/// 로그 타입에 따른 스타일 구분
|
||||
enum CombatLogType {
|
||||
normal, // 일반 메시지
|
||||
damage, // 피해 입힘
|
||||
heal, // 회복
|
||||
levelUp, // 레벨업
|
||||
questComplete, // 퀘스트 완료
|
||||
loot, // 전리품 획득
|
||||
spell, // 주문 습득
|
||||
}
|
||||
|
||||
/// 전투 로그 위젯 (Phase 8: 실시간 전투 이벤트 표시)
|
||||
///
|
||||
/// 최근 전투 이벤트를 스크롤 가능한 리스트로 표시
|
||||
class CombatLog extends StatefulWidget {
|
||||
const CombatLog({super.key, required this.entries, this.maxEntries = 50});
|
||||
|
||||
final List<CombatLogEntry> entries;
|
||||
final int maxEntries;
|
||||
|
||||
@override
|
||||
State<CombatLog> createState() => _CombatLogState();
|
||||
}
|
||||
|
||||
class _CombatLogState extends State<CombatLog> {
|
||||
final ScrollController _scrollController = ScrollController();
|
||||
int _previousLength = 0;
|
||||
|
||||
@override
|
||||
void didUpdateWidget(CombatLog oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
|
||||
// 새 로그 추가 시 자동 스크롤
|
||||
if (widget.entries.length > _previousLength) {
|
||||
_scrollToBottom();
|
||||
}
|
||||
_previousLength = widget.entries.length;
|
||||
}
|
||||
|
||||
void _scrollToBottom() {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (_scrollController.hasClients) {
|
||||
_scrollController.animateTo(
|
||||
_scrollController.position.maxScrollExtent,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
curve: Curves.easeOut,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_scrollController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListView.builder(
|
||||
controller: _scrollController,
|
||||
itemCount: widget.entries.length,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
itemBuilder: (context, index) {
|
||||
final entry = widget.entries[index];
|
||||
return _LogEntryTile(entry: entry);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 개별 로그 엔트리 타일
|
||||
class _LogEntryTile extends StatelessWidget {
|
||||
const _LogEntryTile({required this.entry});
|
||||
|
||||
final CombatLogEntry entry;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final (color, icon) = _getStyleForType(entry.type);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 1),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 타임스탬프(timestamp)
|
||||
Text(
|
||||
_formatTime(entry.timestamp),
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
fontFamily: 'monospace',
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
// 아이콘
|
||||
if (icon != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 4),
|
||||
child: Icon(icon, size: 12, color: color),
|
||||
),
|
||||
// 메시지
|
||||
Expanded(
|
||||
child: Text(
|
||||
entry.message,
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: color ?? Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _formatTime(DateTime time) {
|
||||
return '${time.hour.toString().padLeft(2, '0')}:'
|
||||
'${time.minute.toString().padLeft(2, '0')}:'
|
||||
'${time.second.toString().padLeft(2, '0')}';
|
||||
}
|
||||
|
||||
(Color?, IconData?) _getStyleForType(CombatLogType type) {
|
||||
return switch (type) {
|
||||
CombatLogType.normal => (null, null),
|
||||
CombatLogType.damage => (Colors.red.shade300, Icons.local_fire_department),
|
||||
CombatLogType.heal => (Colors.green.shade300, Icons.healing),
|
||||
CombatLogType.levelUp => (Colors.amber, Icons.arrow_upward),
|
||||
CombatLogType.questComplete => (Colors.blue.shade300, Icons.check_circle),
|
||||
CombatLogType.loot => (Colors.orange.shade300, Icons.inventory_2),
|
||||
CombatLogType.spell => (Colors.purple.shade300, Icons.auto_fix_high),
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user