- flutter_localizations 및 intl 패키지 추가 - l10n.yaml 설정 파일 및 app_ko.arb 메시지 파일 생성 - 모든 화면(app, front, game_play, new_character, save_picker)의 하드코딩 텍스트를 L10n 키로 변환 - 테스트 파일에 localizationsDelegates 추가하여 L10n 지원
706 lines
21 KiB
Dart
706 lines
21 KiB
Dart
import 'package:flutter/material.dart';
|
|
|
|
import 'package:askiineverdie/l10n/app_localizations.dart';
|
|
import 'package:askiineverdie/src/core/animation/ascii_animation_data.dart';
|
|
import 'package:askiineverdie/src/core/animation/ascii_animation_type.dart';
|
|
import 'package:askiineverdie/src/core/model/game_state.dart';
|
|
import 'package:askiineverdie/src/core/storage/theme_preferences.dart';
|
|
import 'package:askiineverdie/src/core/util/pq_logic.dart' as pq_logic;
|
|
import 'package:askiineverdie/src/features/game/game_session_controller.dart';
|
|
import 'package:askiineverdie/src/features/game/widgets/task_progress_panel.dart';
|
|
|
|
/// 게임 진행 화면 (Main.dfm 기반 3패널 레이아웃)
|
|
class GamePlayScreen extends StatefulWidget {
|
|
const GamePlayScreen({super.key, required this.controller});
|
|
|
|
final GameSessionController controller;
|
|
|
|
@override
|
|
State<GamePlayScreen> createState() => _GamePlayScreenState();
|
|
}
|
|
|
|
class _GamePlayScreenState extends State<GamePlayScreen>
|
|
with WidgetsBindingObserver {
|
|
AsciiColorTheme _colorTheme = AsciiColorTheme.green;
|
|
AsciiAnimationType? _specialAnimation;
|
|
|
|
// 이전 상태 추적 (레벨업/퀘스트/Act 완료 감지용)
|
|
int _lastLevel = 0;
|
|
int _lastQuestCount = 0;
|
|
int _lastPlotStageCount = 0;
|
|
|
|
void _cycleColorTheme() {
|
|
setState(() {
|
|
_colorTheme = switch (_colorTheme) {
|
|
AsciiColorTheme.green => AsciiColorTheme.amber,
|
|
AsciiColorTheme.amber => AsciiColorTheme.white,
|
|
AsciiColorTheme.white => AsciiColorTheme.system,
|
|
AsciiColorTheme.system => AsciiColorTheme.green,
|
|
};
|
|
});
|
|
// 테마 변경 시 저장
|
|
ThemePreferences.saveColorTheme(_colorTheme);
|
|
}
|
|
|
|
Future<void> _loadColorTheme() async {
|
|
final theme = await ThemePreferences.loadColorTheme();
|
|
if (mounted) {
|
|
setState(() {
|
|
_colorTheme = theme;
|
|
});
|
|
}
|
|
}
|
|
|
|
void _checkSpecialEvents(GameState state) {
|
|
// 레벨업 감지
|
|
if (state.traits.level > _lastLevel && _lastLevel > 0) {
|
|
_specialAnimation = AsciiAnimationType.levelUp;
|
|
_resetSpecialAnimationAfterFrame();
|
|
}
|
|
_lastLevel = state.traits.level;
|
|
|
|
// 퀘스트 완료 감지
|
|
if (state.progress.questCount > _lastQuestCount && _lastQuestCount > 0) {
|
|
_specialAnimation = AsciiAnimationType.questComplete;
|
|
_resetSpecialAnimationAfterFrame();
|
|
}
|
|
_lastQuestCount = state.progress.questCount;
|
|
|
|
// Act 완료 감지 (plotStageCount 증가)
|
|
if (state.progress.plotStageCount > _lastPlotStageCount &&
|
|
_lastPlotStageCount > 0) {
|
|
_specialAnimation = AsciiAnimationType.actComplete;
|
|
_resetSpecialAnimationAfterFrame();
|
|
}
|
|
_lastPlotStageCount = state.progress.plotStageCount;
|
|
}
|
|
|
|
void _resetSpecialAnimationAfterFrame() {
|
|
// 다음 프레임에서 리셋 (AsciiAnimationCard가 값을 받은 후)
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
if (mounted) {
|
|
setState(() {
|
|
_specialAnimation = null;
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
widget.controller.addListener(_onControllerChanged);
|
|
WidgetsBinding.instance.addObserver(this);
|
|
_loadColorTheme();
|
|
|
|
// 초기 상태 설정
|
|
final state = widget.controller.state;
|
|
if (state != null) {
|
|
_lastLevel = state.traits.level;
|
|
_lastQuestCount = state.progress.questCount;
|
|
_lastPlotStageCount = state.progress.plotStageCount;
|
|
}
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
WidgetsBinding.instance.removeObserver(this);
|
|
widget.controller.removeListener(_onControllerChanged);
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
void didChangeAppLifecycleState(AppLifecycleState state) {
|
|
super.didChangeAppLifecycleState(state);
|
|
|
|
// 앱이 백그라운드로 가거나 비활성화될 때 자동 저장
|
|
if (state == AppLifecycleState.paused ||
|
|
state == AppLifecycleState.inactive ||
|
|
state == AppLifecycleState.detached) {
|
|
_saveGameState();
|
|
}
|
|
}
|
|
|
|
Future<void> _saveGameState() async {
|
|
final currentState = widget.controller.state;
|
|
if (currentState == null || !widget.controller.isRunning) return;
|
|
|
|
await widget.controller.saveManager.saveState(currentState);
|
|
}
|
|
|
|
/// 뒤로가기 시 저장 확인 다이얼로그
|
|
Future<bool> _onPopInvoked() async {
|
|
final l10n = L10n.of(context);
|
|
final shouldPop = await showDialog<bool>(
|
|
context: context,
|
|
builder: (context) => AlertDialog(
|
|
title: Text(l10n.exitGame),
|
|
content: Text(l10n.saveProgressQuestion),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.of(context).pop(false),
|
|
child: Text(l10n.cancel),
|
|
),
|
|
TextButton(
|
|
onPressed: () {
|
|
Navigator.of(context).pop(true);
|
|
},
|
|
child: Text(l10n.exitWithoutSaving),
|
|
),
|
|
FilledButton(
|
|
onPressed: () async {
|
|
await _saveGameState();
|
|
if (context.mounted) {
|
|
Navigator.of(context).pop(true);
|
|
}
|
|
},
|
|
child: Text(l10n.saveAndExit),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
return shouldPop ?? false;
|
|
}
|
|
|
|
void _onControllerChanged() {
|
|
final state = widget.controller.state;
|
|
if (state != null) {
|
|
_checkSpecialEvents(state);
|
|
}
|
|
setState(() {});
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final state = widget.controller.state;
|
|
if (state == null) {
|
|
return const Scaffold(body: Center(child: CircularProgressIndicator()));
|
|
}
|
|
|
|
return PopScope(
|
|
canPop: false,
|
|
onPopInvokedWithResult: (didPop, result) async {
|
|
if (didPop) return;
|
|
final shouldPop = await _onPopInvoked();
|
|
if (shouldPop && context.mounted) {
|
|
await widget.controller.pause(saveOnStop: false);
|
|
if (context.mounted) {
|
|
Navigator.of(context).pop();
|
|
}
|
|
}
|
|
},
|
|
child: Scaffold(
|
|
appBar: AppBar(
|
|
title: Text(L10n.of(context).progressQuestTitle(state.traits.name)),
|
|
actions: [
|
|
// 치트 버튼 (디버그용)
|
|
if (widget.controller.cheatsEnabled) ...[
|
|
IconButton(
|
|
icon: const Text('L+1'),
|
|
tooltip: L10n.of(context).levelUp,
|
|
onPressed: () => widget.controller.loop?.cheatCompleteTask(),
|
|
),
|
|
IconButton(
|
|
icon: const Text('Q!'),
|
|
tooltip: L10n.of(context).completeQuest,
|
|
onPressed: () => widget.controller.loop?.cheatCompleteQuest(),
|
|
),
|
|
IconButton(
|
|
icon: const Text('P!'),
|
|
tooltip: L10n.of(context).completePlot,
|
|
onPressed: () => widget.controller.loop?.cheatCompletePlot(),
|
|
),
|
|
],
|
|
],
|
|
),
|
|
body: Column(
|
|
children: [
|
|
// 상단: ASCII 애니메이션 + Task Progress
|
|
TaskProgressPanel(
|
|
progress: state.progress,
|
|
speedMultiplier: widget.controller.loop?.speedMultiplier ?? 1,
|
|
onSpeedCycle: () {
|
|
widget.controller.loop?.cycleSpeed();
|
|
setState(() {});
|
|
},
|
|
colorTheme: _colorTheme,
|
|
onThemeCycle: _cycleColorTheme,
|
|
specialAnimation: _specialAnimation,
|
|
),
|
|
|
|
// 메인 3패널 영역
|
|
Expanded(
|
|
child: Row(
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
// 좌측 패널: Character Sheet
|
|
Expanded(flex: 2, child: _buildCharacterPanel(state)),
|
|
|
|
// 중앙 패널: Equipment/Inventory
|
|
Expanded(flex: 3, child: _buildEquipmentPanel(state)),
|
|
|
|
// 우측 패널: Plot/Quest
|
|
Expanded(flex: 2, child: _buildQuestPanel(state)),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
/// 좌측 패널: Character Sheet (Traits, Stats, Experience, Spells)
|
|
Widget _buildCharacterPanel(GameState state) {
|
|
final l10n = L10n.of(context);
|
|
return Card(
|
|
margin: const EdgeInsets.all(4),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
_buildPanelHeader(l10n.characterSheet),
|
|
|
|
// Traits 목록
|
|
_buildSectionHeader(l10n.traits),
|
|
_buildTraitsList(state),
|
|
|
|
// Stats 목록
|
|
_buildSectionHeader(l10n.stats),
|
|
Expanded(flex: 2, child: _buildStatsList(state)),
|
|
|
|
// Experience 바
|
|
_buildSectionHeader(l10n.experience),
|
|
_buildProgressBar(
|
|
state.progress.exp.position,
|
|
state.progress.exp.max,
|
|
Colors.blue,
|
|
tooltip:
|
|
'${state.progress.exp.max - state.progress.exp.position} '
|
|
'${l10n.xpNeededForNextLevel}',
|
|
),
|
|
|
|
// Spell Book
|
|
_buildSectionHeader(l10n.spellBook),
|
|
Expanded(flex: 2, child: _buildSpellsList(state)),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
/// 중앙 패널: Equipment/Inventory
|
|
Widget _buildEquipmentPanel(GameState state) {
|
|
final l10n = L10n.of(context);
|
|
return Card(
|
|
margin: const EdgeInsets.all(4),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
_buildPanelHeader(l10n.equipment),
|
|
|
|
// Equipment 목록
|
|
Expanded(flex: 2, child: _buildEquipmentList(state)),
|
|
|
|
// Inventory
|
|
_buildPanelHeader(l10n.inventory),
|
|
Expanded(flex: 3, child: _buildInventoryList(state)),
|
|
|
|
// Encumbrance 바
|
|
_buildSectionHeader(l10n.encumbrance),
|
|
_buildProgressBar(
|
|
state.progress.encumbrance.position,
|
|
state.progress.encumbrance.max,
|
|
Colors.orange,
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
/// 우측 패널: Plot/Quest
|
|
Widget _buildQuestPanel(GameState state) {
|
|
final l10n = L10n.of(context);
|
|
return Card(
|
|
margin: const EdgeInsets.all(4),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
_buildPanelHeader(l10n.plotDevelopment),
|
|
|
|
// Plot 목록
|
|
Expanded(child: _buildPlotList(state)),
|
|
|
|
// Plot 바
|
|
_buildProgressBar(
|
|
state.progress.plot.position,
|
|
state.progress.plot.max,
|
|
Colors.purple,
|
|
tooltip: state.progress.plot.max > 0
|
|
? '${pq_logic.roughTime(state.progress.plot.max - state.progress.plot.position)} remaining'
|
|
: null,
|
|
),
|
|
|
|
_buildPanelHeader(l10n.quests),
|
|
|
|
// Quest 목록
|
|
Expanded(child: _buildQuestList(state)),
|
|
|
|
// Quest 바
|
|
_buildProgressBar(
|
|
state.progress.quest.position,
|
|
state.progress.quest.max,
|
|
Colors.green,
|
|
tooltip: state.progress.quest.max > 0
|
|
? l10n.percentComplete(
|
|
100 * state.progress.quest.position ~/
|
|
state.progress.quest.max,
|
|
)
|
|
: null,
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildPanelHeader(String title) {
|
|
return Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
|
color: Theme.of(context).colorScheme.primaryContainer,
|
|
child: Text(
|
|
title,
|
|
style: TextStyle(
|
|
fontWeight: FontWeight.bold,
|
|
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildSectionHeader(String title) {
|
|
return Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
|
child: Text(title, style: Theme.of(context).textTheme.labelSmall),
|
|
);
|
|
}
|
|
|
|
Widget _buildProgressBar(
|
|
int position,
|
|
int max,
|
|
Color color, {
|
|
String? tooltip,
|
|
}) {
|
|
final progress = max > 0 ? (position / max).clamp(0.0, 1.0) : 0.0;
|
|
final bar = Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
|
child: LinearProgressIndicator(
|
|
value: progress,
|
|
backgroundColor: color.withValues(alpha: 0.2),
|
|
valueColor: AlwaysStoppedAnimation<Color>(color),
|
|
minHeight: 12,
|
|
),
|
|
);
|
|
|
|
if (tooltip != null && tooltip.isNotEmpty) {
|
|
return Tooltip(message: tooltip, child: bar);
|
|
}
|
|
return bar;
|
|
}
|
|
|
|
Widget _buildTraitsList(GameState state) {
|
|
final l10n = L10n.of(context);
|
|
final traits = [
|
|
(l10n.traitName, state.traits.name),
|
|
(l10n.traitRace, state.traits.race),
|
|
(l10n.traitClass, state.traits.klass),
|
|
(l10n.traitLevel, '${state.traits.level}'),
|
|
];
|
|
|
|
return Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
|
child: Column(
|
|
children: traits.map((t) {
|
|
return Row(
|
|
children: [
|
|
SizedBox(
|
|
width: 50,
|
|
child: Text(t.$1, style: const TextStyle(fontSize: 11)),
|
|
),
|
|
Expanded(
|
|
child: Text(
|
|
t.$2,
|
|
style: const TextStyle(
|
|
fontSize: 11,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}).toList(),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildStatsList(GameState state) {
|
|
final l10n = L10n.of(context);
|
|
final stats = [
|
|
(l10n.statStr, state.stats.str),
|
|
(l10n.statCon, state.stats.con),
|
|
(l10n.statDex, state.stats.dex),
|
|
(l10n.statInt, state.stats.intelligence),
|
|
(l10n.statWis, state.stats.wis),
|
|
(l10n.statCha, state.stats.cha),
|
|
(l10n.statHpMax, state.stats.hpMax),
|
|
(l10n.statMpMax, state.stats.mpMax),
|
|
];
|
|
|
|
return ListView.builder(
|
|
itemCount: stats.length,
|
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
|
itemBuilder: (context, index) {
|
|
final stat = stats[index];
|
|
return Row(
|
|
children: [
|
|
SizedBox(
|
|
width: 50,
|
|
child: Text(stat.$1, style: const TextStyle(fontSize: 11)),
|
|
),
|
|
Text(
|
|
'${stat.$2}',
|
|
style: const TextStyle(fontSize: 11, fontWeight: FontWeight.bold),
|
|
),
|
|
],
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
Widget _buildSpellsList(GameState state) {
|
|
if (state.spellBook.spells.isEmpty) {
|
|
return Center(
|
|
child: Text(L10n.of(context).noSpellsYet, style: const TextStyle(fontSize: 11)),
|
|
);
|
|
}
|
|
|
|
return ListView.builder(
|
|
itemCount: state.spellBook.spells.length,
|
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
|
itemBuilder: (context, index) {
|
|
final spell = state.spellBook.spells[index];
|
|
return Row(
|
|
children: [
|
|
Expanded(
|
|
child: Text(
|
|
spell.name,
|
|
style: const TextStyle(fontSize: 11),
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
),
|
|
Text(
|
|
spell.rank,
|
|
style: const TextStyle(fontSize: 11, fontWeight: FontWeight.bold),
|
|
),
|
|
],
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
Widget _buildEquipmentList(GameState state) {
|
|
// 원본 Main.dfm Equips ListView - 11개 슬롯
|
|
final l10n = L10n.of(context);
|
|
final equipment = [
|
|
(l10n.equipWeapon, state.equipment.weapon),
|
|
(l10n.equipShield, state.equipment.shield),
|
|
(l10n.equipHelm, state.equipment.helm),
|
|
(l10n.equipHauberk, state.equipment.hauberk),
|
|
(l10n.equipBrassairts, state.equipment.brassairts),
|
|
(l10n.equipVambraces, state.equipment.vambraces),
|
|
(l10n.equipGauntlets, state.equipment.gauntlets),
|
|
(l10n.equipGambeson, state.equipment.gambeson),
|
|
(l10n.equipCuisses, state.equipment.cuisses),
|
|
(l10n.equipGreaves, state.equipment.greaves),
|
|
(l10n.equipSollerets, state.equipment.sollerets),
|
|
];
|
|
|
|
return ListView.builder(
|
|
itemCount: equipment.length,
|
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
|
itemBuilder: (context, index) {
|
|
final equip = equipment[index];
|
|
return Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 2),
|
|
child: Row(
|
|
children: [
|
|
SizedBox(
|
|
width: 60,
|
|
child: Text(equip.$1, style: const TextStyle(fontSize: 11)),
|
|
),
|
|
Expanded(
|
|
child: Text(
|
|
equip.$2.isNotEmpty ? equip.$2 : '-',
|
|
style: const TextStyle(
|
|
fontSize: 11,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
Widget _buildInventoryList(GameState state) {
|
|
final l10n = L10n.of(context);
|
|
if (state.inventory.items.isEmpty) {
|
|
return Center(
|
|
child: Text(
|
|
l10n.goldAmount(state.inventory.gold),
|
|
style: const TextStyle(fontSize: 11),
|
|
),
|
|
);
|
|
}
|
|
|
|
return ListView.builder(
|
|
itemCount: state.inventory.items.length + 1, // +1 for gold
|
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
|
itemBuilder: (context, index) {
|
|
if (index == 0) {
|
|
return Row(
|
|
children: [
|
|
Expanded(
|
|
child: Text(l10n.gold, style: const TextStyle(fontSize: 11)),
|
|
),
|
|
Text(
|
|
'${state.inventory.gold}',
|
|
style: const TextStyle(
|
|
fontSize: 11,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
final item = state.inventory.items[index - 1];
|
|
return Row(
|
|
children: [
|
|
Expanded(
|
|
child: Text(
|
|
item.name,
|
|
style: const TextStyle(fontSize: 11),
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
),
|
|
Text(
|
|
'${item.count}',
|
|
style: const TextStyle(fontSize: 11, fontWeight: FontWeight.bold),
|
|
),
|
|
],
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
Widget _buildPlotList(GameState state) {
|
|
// 플롯 단계를 표시 (Act I, Act II, ...)
|
|
final l10n = L10n.of(context);
|
|
final plotCount = state.progress.plotStageCount;
|
|
if (plotCount == 0) {
|
|
return Center(
|
|
child: Text(l10n.prologue, style: const TextStyle(fontSize: 11)),
|
|
);
|
|
}
|
|
|
|
return ListView.builder(
|
|
itemCount: plotCount,
|
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
|
itemBuilder: (context, index) {
|
|
final isCompleted = index < plotCount - 1;
|
|
return Row(
|
|
children: [
|
|
Icon(
|
|
isCompleted ? Icons.check_box : Icons.check_box_outline_blank,
|
|
size: 14,
|
|
color: isCompleted ? Colors.green : Colors.grey,
|
|
),
|
|
const SizedBox(width: 4),
|
|
Text(
|
|
index == 0 ? l10n.prologue : l10n.actNumber(_toRoman(index)),
|
|
style: TextStyle(
|
|
fontSize: 11,
|
|
decoration: isCompleted
|
|
? TextDecoration.lineThrough
|
|
: TextDecoration.none,
|
|
),
|
|
),
|
|
],
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
Widget _buildQuestList(GameState state) {
|
|
final l10n = L10n.of(context);
|
|
final questCount = state.progress.questCount;
|
|
if (questCount == 0) {
|
|
return Center(
|
|
child: Text(l10n.noActiveQuests, style: const TextStyle(fontSize: 11)),
|
|
);
|
|
}
|
|
|
|
// 현재 퀘스트 캡션이 있으면 표시
|
|
final currentTask = state.progress.currentTask;
|
|
return ListView(
|
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
|
children: [
|
|
Row(
|
|
children: [
|
|
const Icon(Icons.arrow_right, size: 14),
|
|
Expanded(
|
|
child: Text(
|
|
currentTask.caption.isNotEmpty
|
|
? currentTask.caption
|
|
: l10n.questNumber(questCount),
|
|
style: const TextStyle(fontSize: 11),
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
/// 로마 숫자 변환 (간단 버전)
|
|
String _toRoman(int number) {
|
|
const romanNumerals = [
|
|
(1000, 'M'),
|
|
(900, 'CM'),
|
|
(500, 'D'),
|
|
(400, 'CD'),
|
|
(100, 'C'),
|
|
(90, 'XC'),
|
|
(50, 'L'),
|
|
(40, 'XL'),
|
|
(10, 'X'),
|
|
(9, 'IX'),
|
|
(5, 'V'),
|
|
(4, 'IV'),
|
|
(1, 'I'),
|
|
];
|
|
|
|
var result = '';
|
|
var remaining = number;
|
|
for (final (value, numeral) in romanNumerals) {
|
|
while (remaining >= value) {
|
|
result += numeral;
|
|
remaining -= value;
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
}
|