refactor(ui): new_character_screen.dart 분할 (1016→544 LOC)
- NameInputSection: 이름 입력 섹션 - StatsSection: 능력치 섹션 (스탯 타일, 롤/언두 버튼) - RaceSelectionSection: 종족 선택 섹션 - ClassSelectionSection: 직업 선택 섹션
This commit is contained in:
@@ -2,7 +2,6 @@ import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart' show FilteringTextInputFormatter;
|
||||
|
||||
import 'package:asciineverdie/data/class_data.dart';
|
||||
import 'package:asciineverdie/data/game_text_l10n.dart' as game_l10n;
|
||||
@@ -14,9 +13,12 @@ import 'package:asciineverdie/src/core/model/class_traits.dart';
|
||||
import 'package:asciineverdie/src/core/model/game_state.dart';
|
||||
import 'package:asciineverdie/src/core/model/race_traits.dart';
|
||||
import 'package:asciineverdie/src/core/util/deterministic_random.dart';
|
||||
import 'package:asciineverdie/src/core/l10n/game_data_l10n.dart';
|
||||
import 'package:asciineverdie/src/core/util/pq_logic.dart';
|
||||
import 'package:asciineverdie/src/features/new_character/widgets/class_selection_section.dart';
|
||||
import 'package:asciineverdie/src/features/new_character/widgets/name_input_section.dart';
|
||||
import 'package:asciineverdie/src/features/new_character/widgets/race_preview.dart';
|
||||
import 'package:asciineverdie/src/features/new_character/widgets/race_selection_section.dart';
|
||||
import 'package:asciineverdie/src/features/new_character/widgets/stats_section.dart';
|
||||
import 'package:asciineverdie/src/shared/retro_colors.dart';
|
||||
import 'package:asciineverdie/src/shared/widgets/retro_widgets.dart';
|
||||
|
||||
@@ -69,9 +71,6 @@ class _NewCharacterScreenState extends State<NewCharacterScreen> {
|
||||
// 굴리기/되돌리기 서비스
|
||||
final CharacterRollService _rollService = CharacterRollService.instance;
|
||||
|
||||
// 서비스 초기화 완료 여부
|
||||
bool _isServiceInitialized = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@@ -99,11 +98,6 @@ class _NewCharacterScreenState extends State<NewCharacterScreen> {
|
||||
/// 서비스 초기화
|
||||
Future<void> _initializeService() async {
|
||||
await _rollService.initialize();
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isServiceInitialized = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -227,9 +221,9 @@ class _NewCharacterScreenState extends State<NewCharacterScreen> {
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
backgroundColor: RetroColors.panelBg,
|
||||
title: const Text(
|
||||
'RECHARGE ROLLS',
|
||||
style: TextStyle(
|
||||
title: Text(
|
||||
L10n.of(context).rechargeRollsTitle,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'PressStart2P',
|
||||
fontSize: 14,
|
||||
color: RetroColors.gold,
|
||||
@@ -237,8 +231,8 @@ class _NewCharacterScreenState extends State<NewCharacterScreen> {
|
||||
),
|
||||
content: Text(
|
||||
isPaidUser
|
||||
? 'Recharge 5 rolls for free?'
|
||||
: 'Watch an ad to recharge 5 rolls?',
|
||||
? L10n.of(context).rechargeRollsFree
|
||||
: L10n.of(context).rechargeRollsAd,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'PressStart2P',
|
||||
fontSize: 12,
|
||||
@@ -248,9 +242,9 @@ class _NewCharacterScreenState extends State<NewCharacterScreen> {
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, false),
|
||||
child: const Text(
|
||||
'CANCEL',
|
||||
style: TextStyle(
|
||||
child: Text(
|
||||
L10n.of(context).cancel.toUpperCase(),
|
||||
style: const TextStyle(
|
||||
fontFamily: 'PressStart2P',
|
||||
fontSize: 11,
|
||||
color: RetroColors.textDisabled,
|
||||
@@ -266,9 +260,9 @@ class _NewCharacterScreenState extends State<NewCharacterScreen> {
|
||||
const Icon(Icons.play_circle, size: 14, color: RetroColors.gold),
|
||||
const SizedBox(width: 4),
|
||||
],
|
||||
const Text(
|
||||
'RECHARGE',
|
||||
style: TextStyle(
|
||||
Text(
|
||||
L10n.of(context).rechargeButton,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'PressStart2P',
|
||||
fontSize: 11,
|
||||
color: RetroColors.gold,
|
||||
@@ -331,22 +325,6 @@ class _NewCharacterScreenState extends State<NewCharacterScreen> {
|
||||
});
|
||||
}
|
||||
|
||||
/// Total 값 계산
|
||||
int get _total => _str + _con + _dex + _int + _wis + _cha;
|
||||
|
||||
/// Total 색상 결정 (원본 규칙)
|
||||
/// 63+18(81) 이상 = 빨강, 4*18(72) 초과 = 노랑
|
||||
/// 63-18(45) 이하 = 회색, 3*18(54) 미만 = 은색
|
||||
/// 그 외 = 흰색
|
||||
Color _getTotalColor() {
|
||||
final total = _total;
|
||||
if (total >= 81) return Colors.red;
|
||||
if (total > 72) return Colors.yellow;
|
||||
if (total <= 45) return Colors.grey;
|
||||
if (total < 54) return Colors.grey.shade400;
|
||||
return Colors.white;
|
||||
}
|
||||
|
||||
/// Sold! 버튼 클릭 - 캐릭터 생성 완료
|
||||
/// 원본 Main.pas:1371-1388 RollCharacter 로직
|
||||
void _onSold() {
|
||||
@@ -438,16 +416,32 @@ class _NewCharacterScreenState extends State<NewCharacterScreen> {
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
// 이름 입력 섹션
|
||||
_buildNameSection(),
|
||||
NameInputSection(
|
||||
controller: _nameController,
|
||||
onGenerateName: _onGenerateName,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 능력치 섹션
|
||||
_buildStatsSection(),
|
||||
StatsSection(
|
||||
str: _str,
|
||||
con: _con,
|
||||
dex: _dex,
|
||||
intelligence: _int,
|
||||
wis: _wis,
|
||||
cha: _cha,
|
||||
canRoll: _rollService.canRoll,
|
||||
canUndo: _rollService.canUndo,
|
||||
rollsRemaining: _rollService.rollsRemaining,
|
||||
availableUndos: _rollService.availableUndos,
|
||||
onRoll: _onReroll,
|
||||
onUndo: _onUnroll,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 종족 미리보기 (Phase 5: 종족별 캐릭터 애니메이션)
|
||||
RetroPanel(
|
||||
title: 'PREVIEW',
|
||||
title: L10n.of(context).previewTitle,
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Center(
|
||||
child: RacePreview(raceId: _races[_selectedRaceIndex].raceId),
|
||||
@@ -459,9 +453,25 @@ class _NewCharacterScreenState extends State<NewCharacterScreen> {
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(child: _buildRaceSection()),
|
||||
Expanded(
|
||||
child: RaceSelectionSection(
|
||||
races: _races,
|
||||
selectedIndex: _selectedRaceIndex,
|
||||
scrollController: _raceScrollController,
|
||||
onSelected: (index) =>
|
||||
setState(() => _selectedRaceIndex = index),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(child: _buildKlassSection()),
|
||||
Expanded(
|
||||
child: ClassSelectionSection(
|
||||
klasses: _klasses,
|
||||
selectedIndex: _selectedKlassIndex,
|
||||
scrollController: _klassScrollController,
|
||||
onSelected: (index) =>
|
||||
setState(() => _selectedKlassIndex = index),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
@@ -476,13 +486,21 @@ class _NewCharacterScreenState extends State<NewCharacterScreen> {
|
||||
// 디버그 전용: 치트 모드 토글 (100x 터보 배속)
|
||||
if (kDebugMode) ...[
|
||||
const SizedBox(height: 16),
|
||||
GestureDetector(
|
||||
_buildDebugCheatToggle(context),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 디버그 치트 토글 위젯
|
||||
Widget _buildDebugCheatToggle(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: () => setState(() => _cheatsEnabled = !_cheatsEnabled),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 8,
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: _cheatsEnabled
|
||||
? RetroColors.hpRed.withValues(alpha: 0.3)
|
||||
@@ -498,9 +516,7 @@ class _NewCharacterScreenState extends State<NewCharacterScreen> {
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
_cheatsEnabled
|
||||
? Icons.bug_report
|
||||
: Icons.bug_report_outlined,
|
||||
_cheatsEnabled ? Icons.bug_report : Icons.bug_report_outlined,
|
||||
size: 16,
|
||||
color: _cheatsEnabled
|
||||
? RetroColors.hpRed
|
||||
@@ -509,7 +525,7 @@ class _NewCharacterScreenState extends State<NewCharacterScreen> {
|
||||
const SizedBox(width: 8),
|
||||
Flexible(
|
||||
child: Text(
|
||||
'DEBUG: TURBO (20x)',
|
||||
L10n.of(context).debugTurbo,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontFamily: 'PressStart2P',
|
||||
@@ -523,502 +539,6 @@ class _NewCharacterScreenState extends State<NewCharacterScreen> {
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildNameSection() {
|
||||
final l10n = L10n.of(context);
|
||||
return RetroPanel(
|
||||
title: 'NAME',
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: _nameController,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'PressStart2P',
|
||||
fontSize: 14,
|
||||
color: RetroColors.textLight,
|
||||
),
|
||||
// 영문 알파벳만 허용 (공백 불가)
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.allow(RegExp(r'[a-zA-Z]')),
|
||||
],
|
||||
decoration: InputDecoration(
|
||||
labelText: l10n.name,
|
||||
labelStyle: const TextStyle(
|
||||
fontFamily: 'PressStart2P',
|
||||
fontSize: 13,
|
||||
color: RetroColors.gold,
|
||||
),
|
||||
border: const OutlineInputBorder(
|
||||
borderSide: BorderSide(color: RetroColors.panelBorderInner),
|
||||
),
|
||||
enabledBorder: const OutlineInputBorder(
|
||||
borderSide: BorderSide(color: RetroColors.panelBorderInner),
|
||||
),
|
||||
focusedBorder: const OutlineInputBorder(
|
||||
borderSide: BorderSide(color: RetroColors.gold, width: 2),
|
||||
),
|
||||
counterStyle: const TextStyle(color: RetroColors.textDisabled),
|
||||
),
|
||||
maxLength: 30,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
RetroIconButton(icon: Icons.casino, onPressed: _onGenerateName),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStatsSection() {
|
||||
final l10n = L10n.of(context);
|
||||
return RetroPanel(
|
||||
title: 'STATS',
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 스탯 그리드
|
||||
Row(
|
||||
children: [
|
||||
Expanded(child: _buildStatTile(l10n.statStr, _str)),
|
||||
Expanded(child: _buildStatTile(l10n.statCon, _con)),
|
||||
Expanded(child: _buildStatTile(l10n.statDex, _dex)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(child: _buildStatTile(l10n.statInt, _int)),
|
||||
Expanded(child: _buildStatTile(l10n.statWis, _wis)),
|
||||
Expanded(child: _buildStatTile(l10n.statCha, _cha)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Total
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: _getTotalColor().withValues(alpha: 0.2),
|
||||
border: Border.all(color: _getTotalColor(), width: 2),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
l10n.total.toUpperCase(),
|
||||
style: const TextStyle(
|
||||
fontFamily: 'PressStart2P',
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: RetroColors.textLight,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'$_total',
|
||||
style: TextStyle(
|
||||
fontFamily: 'PressStart2P',
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: _getTotalColor(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Roll 버튼들
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
_buildUndoButton(l10n),
|
||||
const SizedBox(width: 16),
|
||||
_buildRollButton(l10n),
|
||||
],
|
||||
),
|
||||
// 남은 횟수 표시
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
child: Center(
|
||||
child: Text(
|
||||
_rollService.canUndo
|
||||
? 'Undo: ${_rollService.availableUndos} | Rolls: ${_rollService.rollsRemaining}/5'
|
||||
: 'Rolls: ${_rollService.rollsRemaining}/5',
|
||||
style: const TextStyle(
|
||||
fontFamily: 'PressStart2P',
|
||||
fontSize: 11,
|
||||
color: RetroColors.textDisabled,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStatTile(String label, int value) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.all(4),
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: RetroColors.panelBgLight,
|
||||
border: Border.all(color: RetroColors.panelBorderInner),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
label.toUpperCase(),
|
||||
style: const TextStyle(
|
||||
fontFamily: 'PressStart2P',
|
||||
fontSize: 13,
|
||||
color: RetroColors.gold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'$value',
|
||||
style: const TextStyle(
|
||||
fontFamily: 'PressStart2P',
|
||||
fontSize: 17,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: RetroColors.textLight,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRaceSection() {
|
||||
return RetroPanel(
|
||||
title: 'RACE',
|
||||
child: SizedBox(
|
||||
height: 300,
|
||||
child: ListView.builder(
|
||||
controller: _raceScrollController,
|
||||
itemCount: _races.length,
|
||||
itemBuilder: (context, index) {
|
||||
final isSelected = index == _selectedRaceIndex;
|
||||
final race = _races[index];
|
||||
return GestureDetector(
|
||||
onTap: () => setState(() => _selectedRaceIndex = index),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected ? RetroColors.panelBgLight : null,
|
||||
border: isSelected
|
||||
? Border.all(color: RetroColors.gold, width: 1)
|
||||
: null,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
isSelected ? Icons.arrow_right : Icons.remove,
|
||||
size: 12,
|
||||
color: isSelected
|
||||
? RetroColors.gold
|
||||
: RetroColors.textDisabled,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Expanded(
|
||||
child: Text(
|
||||
GameDataL10n.getRaceName(context, race.name),
|
||||
style: TextStyle(
|
||||
fontFamily: 'PressStart2P',
|
||||
fontSize: 14,
|
||||
color: isSelected
|
||||
? RetroColors.gold
|
||||
: RetroColors.textLight,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (isSelected) _buildRaceInfo(race),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 종족 정보 표시 (Phase 5)
|
||||
Widget _buildRaceInfo(RaceTraits race) {
|
||||
final statMods = <String>[];
|
||||
for (final entry in race.statModifiers.entries) {
|
||||
final sign = entry.value > 0 ? '+' : '';
|
||||
statMods.add('${_statName(entry.key)} $sign${entry.value}');
|
||||
}
|
||||
|
||||
final passiveDesc = race.passives.isNotEmpty
|
||||
? race.passives.map((p) => _translateRacePassive(p)).join(', ')
|
||||
: '';
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: 16, top: 4),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (statMods.isNotEmpty)
|
||||
Text(
|
||||
statMods.join(', '),
|
||||
style: const TextStyle(
|
||||
fontSize: 15,
|
||||
color: RetroColors.textLight,
|
||||
),
|
||||
),
|
||||
if (passiveDesc.isNotEmpty)
|
||||
Text(
|
||||
passiveDesc,
|
||||
style: const TextStyle(fontSize: 15, color: RetroColors.expGreen),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 종족 패시브 설명 번역
|
||||
String _translateRacePassive(PassiveAbility passive) {
|
||||
final percent = (passive.value * 100).round();
|
||||
return switch (passive.type) {
|
||||
PassiveType.hpBonus => game_l10n.passiveHpBonus(percent),
|
||||
PassiveType.mpBonus => game_l10n.passiveMpBonus(percent),
|
||||
PassiveType.defenseBonus => game_l10n.passiveDefenseBonus(percent),
|
||||
PassiveType.magicDamageBonus => game_l10n.passiveMagicBonus(percent),
|
||||
PassiveType.criticalBonus => game_l10n.passiveCritBonus(percent),
|
||||
PassiveType.expBonus => passive.description,
|
||||
};
|
||||
}
|
||||
|
||||
String _statName(StatType type) {
|
||||
return switch (type) {
|
||||
StatType.str => game_l10n.statStr,
|
||||
StatType.con => game_l10n.statCon,
|
||||
StatType.dex => game_l10n.statDex,
|
||||
StatType.intelligence => game_l10n.statInt,
|
||||
StatType.wis => game_l10n.statWis,
|
||||
StatType.cha => game_l10n.statCha,
|
||||
};
|
||||
}
|
||||
|
||||
Widget _buildKlassSection() {
|
||||
return RetroPanel(
|
||||
title: 'CLASS',
|
||||
child: SizedBox(
|
||||
height: 300,
|
||||
child: ListView.builder(
|
||||
controller: _klassScrollController,
|
||||
itemCount: _klasses.length,
|
||||
itemBuilder: (context, index) {
|
||||
final isSelected = index == _selectedKlassIndex;
|
||||
final klass = _klasses[index];
|
||||
return GestureDetector(
|
||||
onTap: () => setState(() => _selectedKlassIndex = index),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected ? RetroColors.panelBgLight : null,
|
||||
border: isSelected
|
||||
? Border.all(color: RetroColors.gold, width: 1)
|
||||
: null,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
isSelected ? Icons.arrow_right : Icons.remove,
|
||||
size: 12,
|
||||
color: isSelected
|
||||
? RetroColors.gold
|
||||
: RetroColors.textDisabled,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Expanded(
|
||||
child: Text(
|
||||
GameDataL10n.getKlassName(context, klass.name),
|
||||
style: TextStyle(
|
||||
fontFamily: 'PressStart2P',
|
||||
fontSize: 14,
|
||||
color: isSelected
|
||||
? RetroColors.gold
|
||||
: RetroColors.textLight,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (isSelected) _buildClassInfo(klass),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 클래스 정보 표시 (Phase 5)
|
||||
Widget _buildClassInfo(ClassTraits klass) {
|
||||
final statMods = <String>[];
|
||||
for (final entry in klass.statModifiers.entries) {
|
||||
final sign = entry.value > 0 ? '+' : '';
|
||||
statMods.add('${_statName(entry.key)} $sign${entry.value}');
|
||||
}
|
||||
|
||||
final passiveDesc = klass.passives.isNotEmpty
|
||||
? klass.passives.map((p) => _translateClassPassive(p)).join(', ')
|
||||
: '';
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: 16, top: 4),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (statMods.isNotEmpty)
|
||||
Text(
|
||||
statMods.join(', '),
|
||||
style: const TextStyle(
|
||||
fontSize: 15,
|
||||
color: RetroColors.textLight,
|
||||
),
|
||||
),
|
||||
if (passiveDesc.isNotEmpty)
|
||||
Text(
|
||||
passiveDesc,
|
||||
style: const TextStyle(fontSize: 15, color: RetroColors.expGreen),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 클래스 패시브 설명 번역
|
||||
String _translateClassPassive(ClassPassive passive) {
|
||||
final percent = (passive.value * 100).round();
|
||||
return switch (passive.type) {
|
||||
ClassPassiveType.hpBonus => game_l10n.passiveHpBonus(percent),
|
||||
ClassPassiveType.physicalDamageBonus => game_l10n.passivePhysicalBonus(
|
||||
percent,
|
||||
),
|
||||
ClassPassiveType.defenseBonus => game_l10n.passiveDefenseBonus(percent),
|
||||
ClassPassiveType.magicDamageBonus => game_l10n.passiveMagicBonus(percent),
|
||||
ClassPassiveType.evasionBonus => game_l10n.passiveEvasionBonus(percent),
|
||||
ClassPassiveType.criticalBonus => game_l10n.passiveCritBonus(percent),
|
||||
ClassPassiveType.postCombatHeal => game_l10n.passiveHpRegen(percent),
|
||||
ClassPassiveType.healingBonus => passive.description,
|
||||
ClassPassiveType.multiAttack => passive.description,
|
||||
ClassPassiveType.firstStrikeBonus => passive.description,
|
||||
};
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// 굴리기/되돌리기 버튼 위젯
|
||||
// ===========================================================================
|
||||
|
||||
/// 되돌리기 버튼
|
||||
Widget _buildUndoButton(L10n l10n) {
|
||||
final canUndo = _rollService.canUndo;
|
||||
final isPaidUser = IAPService.instance.isAdRemovalPurchased;
|
||||
|
||||
return GestureDetector(
|
||||
onTap: canUndo ? _onUnroll : null,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: canUndo
|
||||
? RetroColors.panelBgLight
|
||||
: RetroColors.panelBg.withValues(alpha: 0.5),
|
||||
border: Border.all(
|
||||
color: canUndo ? RetroColors.panelBorderInner : RetroColors.panelBg,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// 무료 유저는 광고 아이콘 표시
|
||||
if (!isPaidUser && canUndo) ...[
|
||||
const Icon(
|
||||
Icons.play_circle,
|
||||
size: 14,
|
||||
color: RetroColors.gold,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
],
|
||||
Icon(
|
||||
Icons.undo,
|
||||
size: 14,
|
||||
color: canUndo ? RetroColors.textLight : RetroColors.textDisabled,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
l10n.unroll.toUpperCase(),
|
||||
style: TextStyle(
|
||||
fontFamily: 'PressStart2P',
|
||||
fontSize: 11,
|
||||
color: canUndo ? RetroColors.textLight : RetroColors.textDisabled,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 굴리기 버튼
|
||||
Widget _buildRollButton(L10n l10n) {
|
||||
final canRoll = _rollService.canRoll;
|
||||
|
||||
return GestureDetector(
|
||||
onTap: _onReroll,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: RetroColors.panelBgLight,
|
||||
border: Border.all(color: RetroColors.gold, width: 2),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// 0회일 때 광고 아이콘 표시
|
||||
if (!canRoll) ...[
|
||||
const Icon(Icons.play_circle, size: 14, color: RetroColors.gold),
|
||||
const SizedBox(width: 4),
|
||||
],
|
||||
const Icon(Icons.casino, size: 14, color: RetroColors.gold),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
canRoll
|
||||
? '${l10n.roll.toUpperCase()} (${_rollService.rollsRemaining})'
|
||||
: l10n.roll.toUpperCase(),
|
||||
style: const TextStyle(
|
||||
fontFamily: 'PressStart2P',
|
||||
fontSize: 11,
|
||||
color: RetroColors.gold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:asciineverdie/data/game_text_l10n.dart' as game_l10n;
|
||||
import 'package:asciineverdie/l10n/app_localizations.dart';
|
||||
import 'package:asciineverdie/src/core/l10n/game_data_l10n.dart';
|
||||
import 'package:asciineverdie/src/core/model/class_traits.dart';
|
||||
import 'package:asciineverdie/src/core/model/race_traits.dart' show StatType;
|
||||
import 'package:asciineverdie/src/shared/retro_colors.dart';
|
||||
import 'package:asciineverdie/src/shared/widgets/retro_widgets.dart';
|
||||
|
||||
/// 직업 선택 섹션
|
||||
class ClassSelectionSection extends StatelessWidget {
|
||||
const ClassSelectionSection({
|
||||
super.key,
|
||||
required this.klasses,
|
||||
required this.selectedIndex,
|
||||
required this.scrollController,
|
||||
required this.onSelected,
|
||||
});
|
||||
|
||||
final List<ClassTraits> klasses;
|
||||
final int selectedIndex;
|
||||
final ScrollController scrollController;
|
||||
final ValueChanged<int> onSelected;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return RetroPanel(
|
||||
title: L10n.of(context).classSection,
|
||||
child: SizedBox(
|
||||
height: 300,
|
||||
child: ListView.builder(
|
||||
controller: scrollController,
|
||||
itemCount: klasses.length,
|
||||
itemBuilder: (context, index) {
|
||||
final isSelected = index == selectedIndex;
|
||||
final klass = klasses[index];
|
||||
return GestureDetector(
|
||||
onTap: () => onSelected(index),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected ? RetroColors.panelBgLight : null,
|
||||
border: isSelected
|
||||
? Border.all(color: RetroColors.gold, width: 1)
|
||||
: null,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
isSelected ? Icons.arrow_right : Icons.remove,
|
||||
size: 12,
|
||||
color: isSelected
|
||||
? RetroColors.gold
|
||||
: RetroColors.textDisabled,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Expanded(
|
||||
child: Text(
|
||||
GameDataL10n.getKlassName(context, klass.name),
|
||||
style: TextStyle(
|
||||
fontFamily: 'PressStart2P',
|
||||
fontSize: 14,
|
||||
color: isSelected
|
||||
? RetroColors.gold
|
||||
: RetroColors.textLight,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (isSelected) _ClassInfo(klass: klass),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 클래스 정보 표시 위젯
|
||||
class _ClassInfo extends StatelessWidget {
|
||||
const _ClassInfo({required this.klass});
|
||||
|
||||
final ClassTraits klass;
|
||||
|
||||
String _statName(StatType type) {
|
||||
return switch (type) {
|
||||
StatType.str => game_l10n.statStr,
|
||||
StatType.con => game_l10n.statCon,
|
||||
StatType.dex => game_l10n.statDex,
|
||||
StatType.intelligence => game_l10n.statInt,
|
||||
StatType.wis => game_l10n.statWis,
|
||||
StatType.cha => game_l10n.statCha,
|
||||
};
|
||||
}
|
||||
|
||||
String _translatePassive(ClassPassive passive) {
|
||||
final percent = (passive.value * 100).round();
|
||||
return switch (passive.type) {
|
||||
ClassPassiveType.hpBonus => game_l10n.passiveHpBonus(percent),
|
||||
ClassPassiveType.physicalDamageBonus =>
|
||||
game_l10n.passivePhysicalBonus(percent),
|
||||
ClassPassiveType.defenseBonus => game_l10n.passiveDefenseBonus(percent),
|
||||
ClassPassiveType.magicDamageBonus => game_l10n.passiveMagicBonus(percent),
|
||||
ClassPassiveType.evasionBonus => game_l10n.passiveEvasionBonus(percent),
|
||||
ClassPassiveType.criticalBonus => game_l10n.passiveCritBonus(percent),
|
||||
ClassPassiveType.postCombatHeal => game_l10n.passiveHpRegen(percent),
|
||||
ClassPassiveType.healingBonus => passive.description,
|
||||
ClassPassiveType.multiAttack => passive.description,
|
||||
ClassPassiveType.firstStrikeBonus => passive.description,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final statMods = <String>[];
|
||||
for (final entry in klass.statModifiers.entries) {
|
||||
final sign = entry.value > 0 ? '+' : '';
|
||||
statMods.add('${_statName(entry.key)} $sign${entry.value}');
|
||||
}
|
||||
|
||||
final passiveDesc = klass.passives.isNotEmpty
|
||||
? klass.passives.map((p) => _translatePassive(p)).join(', ')
|
||||
: '';
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: 16, top: 4),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (statMods.isNotEmpty)
|
||||
Text(
|
||||
statMods.join(', '),
|
||||
style: const TextStyle(
|
||||
fontSize: 15,
|
||||
color: RetroColors.textLight,
|
||||
),
|
||||
),
|
||||
if (passiveDesc.isNotEmpty)
|
||||
Text(
|
||||
passiveDesc,
|
||||
style: const TextStyle(fontSize: 15, color: RetroColors.expGreen),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart' show FilteringTextInputFormatter;
|
||||
|
||||
import 'package:asciineverdie/l10n/app_localizations.dart';
|
||||
import 'package:asciineverdie/src/shared/retro_colors.dart';
|
||||
import 'package:asciineverdie/src/shared/widgets/retro_widgets.dart';
|
||||
|
||||
/// 캐릭터 이름 입력 섹션
|
||||
class NameInputSection extends StatelessWidget {
|
||||
const NameInputSection({
|
||||
super.key,
|
||||
required this.controller,
|
||||
required this.onGenerateName,
|
||||
});
|
||||
|
||||
final TextEditingController controller;
|
||||
final VoidCallback onGenerateName;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = L10n.of(context);
|
||||
return RetroPanel(
|
||||
title: l10n.nameTitle,
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: controller,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'PressStart2P',
|
||||
fontSize: 14,
|
||||
color: RetroColors.textLight,
|
||||
),
|
||||
// 영문 알파벳만 허용 (공백 불가)
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.allow(RegExp(r'[a-zA-Z]')),
|
||||
],
|
||||
decoration: InputDecoration(
|
||||
labelText: l10n.name,
|
||||
labelStyle: const TextStyle(
|
||||
fontFamily: 'PressStart2P',
|
||||
fontSize: 13,
|
||||
color: RetroColors.gold,
|
||||
),
|
||||
border: const OutlineInputBorder(
|
||||
borderSide: BorderSide(color: RetroColors.panelBorderInner),
|
||||
),
|
||||
enabledBorder: const OutlineInputBorder(
|
||||
borderSide: BorderSide(color: RetroColors.panelBorderInner),
|
||||
),
|
||||
focusedBorder: const OutlineInputBorder(
|
||||
borderSide: BorderSide(color: RetroColors.gold, width: 2),
|
||||
),
|
||||
counterStyle: const TextStyle(color: RetroColors.textDisabled),
|
||||
),
|
||||
maxLength: 30,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
RetroIconButton(icon: Icons.casino, onPressed: onGenerateName),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:asciineverdie/data/game_text_l10n.dart' as game_l10n;
|
||||
import 'package:asciineverdie/l10n/app_localizations.dart';
|
||||
import 'package:asciineverdie/src/core/l10n/game_data_l10n.dart';
|
||||
import 'package:asciineverdie/src/core/model/race_traits.dart';
|
||||
import 'package:asciineverdie/src/shared/retro_colors.dart';
|
||||
import 'package:asciineverdie/src/shared/widgets/retro_widgets.dart';
|
||||
|
||||
/// 종족 선택 섹션
|
||||
class RaceSelectionSection extends StatelessWidget {
|
||||
const RaceSelectionSection({
|
||||
super.key,
|
||||
required this.races,
|
||||
required this.selectedIndex,
|
||||
required this.scrollController,
|
||||
required this.onSelected,
|
||||
});
|
||||
|
||||
final List<RaceTraits> races;
|
||||
final int selectedIndex;
|
||||
final ScrollController scrollController;
|
||||
final ValueChanged<int> onSelected;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return RetroPanel(
|
||||
title: L10n.of(context).raceTitle,
|
||||
child: SizedBox(
|
||||
height: 300,
|
||||
child: ListView.builder(
|
||||
controller: scrollController,
|
||||
itemCount: races.length,
|
||||
itemBuilder: (context, index) {
|
||||
final isSelected = index == selectedIndex;
|
||||
final race = races[index];
|
||||
return GestureDetector(
|
||||
onTap: () => onSelected(index),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected ? RetroColors.panelBgLight : null,
|
||||
border: isSelected
|
||||
? Border.all(color: RetroColors.gold, width: 1)
|
||||
: null,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
isSelected ? Icons.arrow_right : Icons.remove,
|
||||
size: 12,
|
||||
color: isSelected
|
||||
? RetroColors.gold
|
||||
: RetroColors.textDisabled,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Expanded(
|
||||
child: Text(
|
||||
GameDataL10n.getRaceName(context, race.name),
|
||||
style: TextStyle(
|
||||
fontFamily: 'PressStart2P',
|
||||
fontSize: 14,
|
||||
color: isSelected
|
||||
? RetroColors.gold
|
||||
: RetroColors.textLight,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (isSelected) _RaceInfo(race: race),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 종족 정보 표시 위젯
|
||||
class _RaceInfo extends StatelessWidget {
|
||||
const _RaceInfo({required this.race});
|
||||
|
||||
final RaceTraits race;
|
||||
|
||||
String _statName(StatType type) {
|
||||
return switch (type) {
|
||||
StatType.str => game_l10n.statStr,
|
||||
StatType.con => game_l10n.statCon,
|
||||
StatType.dex => game_l10n.statDex,
|
||||
StatType.intelligence => game_l10n.statInt,
|
||||
StatType.wis => game_l10n.statWis,
|
||||
StatType.cha => game_l10n.statCha,
|
||||
};
|
||||
}
|
||||
|
||||
String _translatePassive(PassiveAbility passive) {
|
||||
final percent = (passive.value * 100).round();
|
||||
return switch (passive.type) {
|
||||
PassiveType.hpBonus => game_l10n.passiveHpBonus(percent),
|
||||
PassiveType.mpBonus => game_l10n.passiveMpBonus(percent),
|
||||
PassiveType.defenseBonus => game_l10n.passiveDefenseBonus(percent),
|
||||
PassiveType.magicDamageBonus => game_l10n.passiveMagicBonus(percent),
|
||||
PassiveType.criticalBonus => game_l10n.passiveCritBonus(percent),
|
||||
PassiveType.expBonus => passive.description,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final statMods = <String>[];
|
||||
for (final entry in race.statModifiers.entries) {
|
||||
final sign = entry.value > 0 ? '+' : '';
|
||||
statMods.add('${_statName(entry.key)} $sign${entry.value}');
|
||||
}
|
||||
|
||||
final passiveDesc = race.passives.isNotEmpty
|
||||
? race.passives.map((p) => _translatePassive(p)).join(', ')
|
||||
: '';
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: 16, top: 4),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (statMods.isNotEmpty)
|
||||
Text(
|
||||
statMods.join(', '),
|
||||
style: const TextStyle(
|
||||
fontSize: 15,
|
||||
color: RetroColors.textLight,
|
||||
),
|
||||
),
|
||||
if (passiveDesc.isNotEmpty)
|
||||
Text(
|
||||
passiveDesc,
|
||||
style: const TextStyle(fontSize: 15, color: RetroColors.expGreen),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
302
lib/src/features/new_character/widgets/stats_section.dart
Normal file
302
lib/src/features/new_character/widgets/stats_section.dart
Normal file
@@ -0,0 +1,302 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:asciineverdie/l10n/app_localizations.dart';
|
||||
import 'package:asciineverdie/src/core/engine/iap_service.dart';
|
||||
import 'package:asciineverdie/src/shared/retro_colors.dart';
|
||||
import 'package:asciineverdie/src/shared/widgets/retro_widgets.dart';
|
||||
|
||||
/// 능력치 표시 섹션
|
||||
class StatsSection extends StatelessWidget {
|
||||
const StatsSection({
|
||||
super.key,
|
||||
required this.str,
|
||||
required this.con,
|
||||
required this.dex,
|
||||
required this.intelligence,
|
||||
required this.wis,
|
||||
required this.cha,
|
||||
required this.canRoll,
|
||||
required this.canUndo,
|
||||
required this.rollsRemaining,
|
||||
required this.availableUndos,
|
||||
required this.onRoll,
|
||||
required this.onUndo,
|
||||
});
|
||||
|
||||
final int str;
|
||||
final int con;
|
||||
final int dex;
|
||||
final int intelligence;
|
||||
final int wis;
|
||||
final int cha;
|
||||
final bool canRoll;
|
||||
final bool canUndo;
|
||||
final int rollsRemaining;
|
||||
final int availableUndos;
|
||||
final VoidCallback onRoll;
|
||||
final VoidCallback onUndo;
|
||||
|
||||
int get _total => str + con + dex + intelligence + wis + cha;
|
||||
|
||||
/// Total 색상 결정 (원본 규칙)
|
||||
/// 63+18(81) 이상 = 빨강, 4*18(72) 초과 = 노랑
|
||||
/// 63-18(45) 이하 = 회색, 3*18(54) 미만 = 은색
|
||||
/// 그 외 = 흰색
|
||||
Color _getTotalColor() {
|
||||
final total = _total;
|
||||
if (total >= 81) return Colors.red;
|
||||
if (total > 72) return Colors.yellow;
|
||||
if (total <= 45) return Colors.grey;
|
||||
if (total < 54) return Colors.grey.shade400;
|
||||
return Colors.white;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = L10n.of(context);
|
||||
return RetroPanel(
|
||||
title: l10n.statsTitle,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 스탯 그리드
|
||||
Row(
|
||||
children: [
|
||||
Expanded(child: _StatTile(label: l10n.statStr, value: str)),
|
||||
Expanded(child: _StatTile(label: l10n.statCon, value: con)),
|
||||
Expanded(child: _StatTile(label: l10n.statDex, value: dex)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _StatTile(label: l10n.statInt, value: intelligence),
|
||||
),
|
||||
Expanded(child: _StatTile(label: l10n.statWis, value: wis)),
|
||||
Expanded(child: _StatTile(label: l10n.statCha, value: cha)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Total
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: _getTotalColor().withValues(alpha: 0.2),
|
||||
border: Border.all(color: _getTotalColor(), width: 2),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
l10n.total.toUpperCase(),
|
||||
style: const TextStyle(
|
||||
fontFamily: 'PressStart2P',
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: RetroColors.textLight,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'$_total',
|
||||
style: TextStyle(
|
||||
fontFamily: 'PressStart2P',
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: _getTotalColor(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Roll 버튼들
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
_UndoButton(
|
||||
canUndo: canUndo,
|
||||
onPressed: onUndo,
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
_RollButton(
|
||||
canRoll: canRoll,
|
||||
rollsRemaining: rollsRemaining,
|
||||
onPressed: onRoll,
|
||||
),
|
||||
],
|
||||
),
|
||||
// 남은 횟수 표시
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
child: Center(
|
||||
child: Text(
|
||||
canUndo
|
||||
? 'Undo: $availableUndos | Rolls: $rollsRemaining/5'
|
||||
: 'Rolls: $rollsRemaining/5',
|
||||
style: const TextStyle(
|
||||
fontFamily: 'PressStart2P',
|
||||
fontSize: 11,
|
||||
color: RetroColors.textDisabled,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 스탯 타일 위젯
|
||||
class _StatTile extends StatelessWidget {
|
||||
const _StatTile({required this.label, required this.value});
|
||||
|
||||
final String label;
|
||||
final int value;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.all(4),
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: RetroColors.panelBgLight,
|
||||
border: Border.all(color: RetroColors.panelBorderInner),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
label.toUpperCase(),
|
||||
style: const TextStyle(
|
||||
fontFamily: 'PressStart2P',
|
||||
fontSize: 13,
|
||||
color: RetroColors.gold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'$value',
|
||||
style: const TextStyle(
|
||||
fontFamily: 'PressStart2P',
|
||||
fontSize: 17,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: RetroColors.textLight,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 되돌리기 버튼
|
||||
class _UndoButton extends StatelessWidget {
|
||||
const _UndoButton({required this.canUndo, required this.onPressed});
|
||||
|
||||
final bool canUndo;
|
||||
final VoidCallback onPressed;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = L10n.of(context);
|
||||
final isPaidUser = IAPService.instance.isAdRemovalPurchased;
|
||||
|
||||
return GestureDetector(
|
||||
onTap: canUndo ? onPressed : null,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: canUndo
|
||||
? RetroColors.panelBgLight
|
||||
: RetroColors.panelBg.withValues(alpha: 0.5),
|
||||
border: Border.all(
|
||||
color: canUndo ? RetroColors.panelBorderInner : RetroColors.panelBg,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// 무료 유저는 광고 아이콘 표시
|
||||
if (!isPaidUser && canUndo) ...[
|
||||
const Icon(
|
||||
Icons.play_circle,
|
||||
size: 14,
|
||||
color: RetroColors.gold,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
],
|
||||
Icon(
|
||||
Icons.undo,
|
||||
size: 14,
|
||||
color: canUndo ? RetroColors.textLight : RetroColors.textDisabled,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
l10n.unroll.toUpperCase(),
|
||||
style: TextStyle(
|
||||
fontFamily: 'PressStart2P',
|
||||
fontSize: 11,
|
||||
color: canUndo ? RetroColors.textLight : RetroColors.textDisabled,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 굴리기 버튼
|
||||
class _RollButton extends StatelessWidget {
|
||||
const _RollButton({
|
||||
required this.canRoll,
|
||||
required this.rollsRemaining,
|
||||
required this.onPressed,
|
||||
});
|
||||
|
||||
final bool canRoll;
|
||||
final int rollsRemaining;
|
||||
final VoidCallback onPressed;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = L10n.of(context);
|
||||
|
||||
return GestureDetector(
|
||||
onTap: onPressed,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: RetroColors.panelBgLight,
|
||||
border: Border.all(color: RetroColors.gold, width: 2),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// 0회일 때 광고 아이콘 표시
|
||||
if (!canRoll) ...[
|
||||
const Icon(Icons.play_circle, size: 14, color: RetroColors.gold),
|
||||
const SizedBox(width: 4),
|
||||
],
|
||||
const Icon(Icons.casino, size: 14, color: RetroColors.gold),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
canRoll
|
||||
? '${l10n.roll.toUpperCase()} ($rollsRemaining)'
|
||||
: l10n.roll.toUpperCase(),
|
||||
style: const TextStyle(
|
||||
fontFamily: 'PressStart2P',
|
||||
fontSize: 11,
|
||||
color: RetroColors.gold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user