- game_play_screen에서 desktop 패널 위젯 분리 - death_overlay에서 death_buttons, death_combat_log 분리 - mobile_carousel_layout에서 mobile_options_menu 분리 - 아레나 위젯 개선 (arena_hp_bar, result_panel 등) - settings_screen에서 retro_settings_widgets 분리 - 기타 위젯 리팩토링 및 import 경로 업데이트
255 lines
8.1 KiB
Dart
255 lines
8.1 KiB
Dart
import 'package:flutter/material.dart';
|
|
|
|
import 'package:asciineverdie/l10n/app_localizations.dart';
|
|
import 'package:asciineverdie/src/shared/retro_colors.dart';
|
|
|
|
/// 아레나 전투 HP 바 (좌우 대칭 레이아웃)
|
|
class ArenaHpBars extends StatelessWidget {
|
|
const ArenaHpBars({
|
|
super.key,
|
|
required this.challengerName,
|
|
required this.challengerHp,
|
|
required this.challengerHpMax,
|
|
required this.challengerFlashAnimation,
|
|
required this.challengerHpChange,
|
|
required this.opponentName,
|
|
required this.opponentHp,
|
|
required this.opponentHpMax,
|
|
required this.opponentFlashAnimation,
|
|
required this.opponentHpChange,
|
|
});
|
|
|
|
final String challengerName;
|
|
final int challengerHp;
|
|
final int challengerHpMax;
|
|
final Animation<double> challengerFlashAnimation;
|
|
final int challengerHpChange;
|
|
|
|
final String opponentName;
|
|
final int opponentHp;
|
|
final int opponentHpMax;
|
|
final Animation<double> opponentFlashAnimation;
|
|
final int opponentHpChange;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
|
|
decoration: BoxDecoration(
|
|
color: RetroColors.panelBgOf(context),
|
|
border: Border(
|
|
bottom: BorderSide(color: RetroColors.borderOf(context), width: 2),
|
|
),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Expanded(
|
|
child: _ArenaHpBar(
|
|
name: challengerName,
|
|
hp: challengerHp,
|
|
hpMax: challengerHpMax,
|
|
fillColor: RetroColors.mpBlue,
|
|
accentColor: Colors.blue,
|
|
flashAnimation: challengerFlashAnimation,
|
|
hpChange: challengerHpChange,
|
|
isReversed: false,
|
|
),
|
|
),
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
|
child: Text(
|
|
'VS',
|
|
style: TextStyle(
|
|
fontFamily: 'PressStart2P',
|
|
fontSize: 13,
|
|
color: RetroColors.goldOf(context),
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
),
|
|
Expanded(
|
|
child: _ArenaHpBar(
|
|
name: opponentName,
|
|
hp: opponentHp,
|
|
hpMax: opponentHpMax,
|
|
fillColor: RetroColors.hpRed,
|
|
accentColor: Colors.red,
|
|
flashAnimation: opponentFlashAnimation,
|
|
hpChange: opponentHpChange,
|
|
isReversed: true,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// 레트로 세그먼트 HP 바 (개별)
|
|
class _ArenaHpBar extends StatelessWidget {
|
|
const _ArenaHpBar({
|
|
required this.name,
|
|
required this.hp,
|
|
required this.hpMax,
|
|
required this.fillColor,
|
|
required this.accentColor,
|
|
required this.flashAnimation,
|
|
required this.hpChange,
|
|
required this.isReversed,
|
|
});
|
|
|
|
final String name;
|
|
final int hp;
|
|
final int hpMax;
|
|
final Color fillColor;
|
|
final Color accentColor;
|
|
final Animation<double> flashAnimation;
|
|
final int hpChange;
|
|
final bool isReversed;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final hpRatio = hpMax > 0 ? hp / hpMax : 0.0;
|
|
final isLow = hpRatio < 0.2 && hpRatio > 0;
|
|
|
|
return AnimatedBuilder(
|
|
animation: flashAnimation,
|
|
builder: (context, child) {
|
|
final isDamage = hpChange < 0;
|
|
final flashColor = isDamage
|
|
? RetroColors.hpRed.withValues(alpha: flashAnimation.value * 0.4)
|
|
: RetroColors.expGreen.withValues(
|
|
alpha: flashAnimation.value * 0.4,
|
|
);
|
|
|
|
return Container(
|
|
padding: const EdgeInsets.all(6),
|
|
decoration: BoxDecoration(
|
|
color: flashAnimation.value > 0.1
|
|
? flashColor
|
|
: accentColor.withValues(alpha: 0.1),
|
|
borderRadius: BorderRadius.circular(4),
|
|
border: Border.all(color: accentColor, width: 2),
|
|
),
|
|
child: Stack(
|
|
clipBehavior: Clip.none,
|
|
children: [
|
|
Column(
|
|
crossAxisAlignment: isReversed
|
|
? CrossAxisAlignment.end
|
|
: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
name,
|
|
style: TextStyle(
|
|
fontFamily: 'PressStart2P',
|
|
fontSize: 11,
|
|
color: RetroColors.textPrimaryOf(context),
|
|
),
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
const SizedBox(height: 4),
|
|
_buildSegmentBar(context, hpRatio, isLow),
|
|
const SizedBox(height: 2),
|
|
Row(
|
|
mainAxisAlignment: isReversed
|
|
? MainAxisAlignment.end
|
|
: MainAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
L10n.of(context).hpLabel,
|
|
style: TextStyle(
|
|
fontFamily: 'PressStart2P',
|
|
fontSize: 12,
|
|
color: accentColor.withValues(alpha: 0.8),
|
|
),
|
|
),
|
|
const SizedBox(width: 4),
|
|
Text(
|
|
'$hp/$hpMax',
|
|
style: TextStyle(
|
|
fontFamily: 'PressStart2P',
|
|
fontSize: 12,
|
|
color: isLow ? RetroColors.hpRed : fillColor,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
if (hpChange != 0 && flashAnimation.value > 0.05)
|
|
Positioned(
|
|
left: isReversed ? null : 0,
|
|
right: isReversed ? 0 : null,
|
|
top: -12,
|
|
child: Transform.translate(
|
|
offset: Offset(0, -12 * (1 - flashAnimation.value)),
|
|
child: Opacity(
|
|
opacity: flashAnimation.value,
|
|
child: Text(
|
|
hpChange > 0 ? '+$hpChange' : '$hpChange',
|
|
style: TextStyle(
|
|
fontFamily: 'PressStart2P',
|
|
fontSize: 13,
|
|
fontWeight: FontWeight.bold,
|
|
color: isDamage
|
|
? RetroColors.hpRed
|
|
: RetroColors.expGreen,
|
|
shadows: const [
|
|
Shadow(color: Colors.black, blurRadius: 3),
|
|
Shadow(color: Colors.black, blurRadius: 6),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
/// 세그먼트 바 (8-bit 스타일)
|
|
Widget _buildSegmentBar(BuildContext context, double ratio, bool isLow) {
|
|
const segmentCount = 10;
|
|
final filledSegments = (ratio.clamp(0.0, 1.0) * segmentCount).round();
|
|
|
|
final segments = List.generate(segmentCount, (index) {
|
|
final isFilled = isReversed
|
|
? index >= segmentCount - filledSegments
|
|
: index < filledSegments;
|
|
|
|
return Expanded(
|
|
child: Container(
|
|
height: 8,
|
|
decoration: BoxDecoration(
|
|
color: isFilled
|
|
? (isLow ? RetroColors.hpRed : fillColor)
|
|
: fillColor.withValues(alpha: 0.2),
|
|
border: Border(
|
|
right: index < segmentCount - 1
|
|
? BorderSide(
|
|
color: RetroColors.borderOf(
|
|
context,
|
|
).withValues(alpha: 0.3),
|
|
width: 1,
|
|
)
|
|
: BorderSide.none,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
});
|
|
|
|
return Container(
|
|
decoration: BoxDecoration(
|
|
border: Border.all(color: RetroColors.borderOf(context), width: 1),
|
|
),
|
|
child: Row(children: isReversed ? segments.reversed.toList() : segments),
|
|
);
|
|
}
|
|
}
|