- 21개 종족별 고유 ASCII 캐릭터 프레임 데이터 추가 - 각 종족당 5가지 상태 애니메이션: idle, prepare, attack, hit, recover - 종족 특성에 맞는 시각적 차별화 (마법사 ~, 기사 ♦, 언데드 ☠ 등) - 캐릭터 생성 화면 종족 미리보기 위젯 추가 - 프론트 화면 Hero vs Boss 애니메이션 개선 - 게임 플레이 화면 애니메이션 패널 연동 강화
671 lines
21 KiB
Dart
671 lines
21 KiB
Dart
import 'dart:math' as math;
|
||
|
||
import 'package:flutter/material.dart';
|
||
|
||
import 'package:askiineverdie/data/class_data.dart';
|
||
import 'package:askiineverdie/data/game_text_l10n.dart' as game_l10n;
|
||
import 'package:askiineverdie/data/race_data.dart';
|
||
import 'package:askiineverdie/l10n/app_localizations.dart';
|
||
import 'package:askiineverdie/src/core/model/class_traits.dart';
|
||
import 'package:askiineverdie/src/core/model/game_state.dart';
|
||
import 'package:askiineverdie/src/core/model/race_traits.dart';
|
||
import 'package:askiineverdie/src/core/util/deterministic_random.dart';
|
||
import 'package:askiineverdie/src/core/l10n/game_data_l10n.dart';
|
||
import 'package:askiineverdie/src/core/util/pq_logic.dart';
|
||
import 'package:askiineverdie/src/features/new_character/widgets/race_preview.dart';
|
||
|
||
/// 캐릭터 생성 화면 (NewGuy.pas 포팅)
|
||
class NewCharacterScreen extends StatefulWidget {
|
||
const NewCharacterScreen({super.key, this.onCharacterCreated});
|
||
|
||
/// 캐릭터 생성 완료 시 호출되는 콜백
|
||
/// testMode: 웹에서도 모바일 캐로셀 레이아웃 사용
|
||
final void Function(GameState initialState, {bool testMode})?
|
||
onCharacterCreated;
|
||
|
||
@override
|
||
State<NewCharacterScreen> createState() => _NewCharacterScreenState();
|
||
}
|
||
|
||
class _NewCharacterScreenState extends State<NewCharacterScreen> {
|
||
final TextEditingController _nameController = TextEditingController();
|
||
final ScrollController _raceScrollController = ScrollController();
|
||
final ScrollController _klassScrollController = ScrollController();
|
||
|
||
// 종족(races)과 직업(klasses) 목록 (Phase 5)
|
||
final List<RaceTraits> _races = RaceData.all;
|
||
final List<ClassTraits> _klasses = ClassData.all;
|
||
|
||
// 선택된 종족/직업 인덱스
|
||
int _selectedRaceIndex = 0;
|
||
int _selectedKlassIndex = 0;
|
||
|
||
// 능력치(stats)
|
||
int _str = 0;
|
||
int _con = 0;
|
||
int _dex = 0;
|
||
int _int = 0;
|
||
int _wis = 0;
|
||
int _cha = 0;
|
||
|
||
// 롤 이력 (Unroll 기능용) - 원본 OldRolls TListBox
|
||
static const int _maxRollHistory = 20; // 최대 저장 개수
|
||
final List<int> _rollHistory = [];
|
||
|
||
// 현재 RNG 시드 (Re-Roll 전 저장)
|
||
int _currentSeed = 0;
|
||
|
||
// 이름 생성용 RNG
|
||
late DeterministicRandom _nameRng;
|
||
|
||
// 테스트 모드 (웹에서 모바일 캐로셀 레이아웃 활성화)
|
||
bool _testModeEnabled = false;
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
|
||
// 초기 랜덤화
|
||
final random = math.Random();
|
||
_selectedRaceIndex = random.nextInt(_races.length);
|
||
_selectedKlassIndex = random.nextInt(_klasses.length);
|
||
|
||
// 초기 스탯 굴림
|
||
_currentSeed = random.nextInt(0x7FFFFFFF);
|
||
_nameRng = DeterministicRandom(random.nextInt(0x7FFFFFFF));
|
||
_rollStats();
|
||
|
||
// 초기 이름 생성
|
||
_nameController.text = generateName(_nameRng);
|
||
|
||
// 선택된 종족/직업으로 스크롤
|
||
_scrollToSelectedItems();
|
||
}
|
||
|
||
@override
|
||
void dispose() {
|
||
_nameController.dispose();
|
||
_raceScrollController.dispose();
|
||
_klassScrollController.dispose();
|
||
super.dispose();
|
||
}
|
||
|
||
/// 선택된 종족/직업 위치로 스크롤
|
||
void _scrollToSelectedItems() {
|
||
// ListTile 높이 약 48px (dense 모드)
|
||
const itemHeight = 48.0;
|
||
|
||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||
if (_raceScrollController.hasClients) {
|
||
final raceOffset = _selectedRaceIndex * itemHeight;
|
||
_raceScrollController.animateTo(
|
||
raceOffset.clamp(0.0, _raceScrollController.position.maxScrollExtent),
|
||
duration: const Duration(milliseconds: 300),
|
||
curve: Curves.easeOut,
|
||
);
|
||
}
|
||
if (_klassScrollController.hasClients) {
|
||
final klassOffset = _selectedKlassIndex * itemHeight;
|
||
_klassScrollController.animateTo(
|
||
klassOffset.clamp(
|
||
0.0,
|
||
_klassScrollController.position.maxScrollExtent,
|
||
),
|
||
duration: const Duration(milliseconds: 300),
|
||
curve: Curves.easeOut,
|
||
);
|
||
}
|
||
});
|
||
}
|
||
|
||
/// 스탯 굴림 (3d6 × 6)
|
||
void _rollStats() {
|
||
final rng = DeterministicRandom(_currentSeed);
|
||
setState(() {
|
||
_str = rollStat(rng);
|
||
_con = rollStat(rng);
|
||
_dex = rollStat(rng);
|
||
_int = rollStat(rng);
|
||
_wis = rollStat(rng);
|
||
_cha = rollStat(rng);
|
||
});
|
||
}
|
||
|
||
/// Re-Roll 버튼 클릭
|
||
void _onReroll() {
|
||
// 현재 시드를 이력에 저장
|
||
_rollHistory.insert(0, _currentSeed);
|
||
|
||
// 최대 개수 초과 시 가장 오래된 항목 제거
|
||
if (_rollHistory.length > _maxRollHistory) {
|
||
_rollHistory.removeLast();
|
||
}
|
||
|
||
// 새 시드로 굴림
|
||
_currentSeed = math.Random().nextInt(0x7FFFFFFF);
|
||
_rollStats();
|
||
|
||
// 선택된 종족/직업으로 스크롤
|
||
_scrollToSelectedItems();
|
||
}
|
||
|
||
/// Unroll 버튼 클릭 (이전 롤로 복원)
|
||
void _onUnroll() {
|
||
if (_rollHistory.isEmpty) return;
|
||
|
||
setState(() {
|
||
_currentSeed = _rollHistory.removeAt(0);
|
||
});
|
||
_rollStats();
|
||
}
|
||
|
||
/// 이름 생성 버튼 클릭
|
||
void _onGenerateName() {
|
||
setState(() {
|
||
_nameController.text = generateName(_nameRng);
|
||
});
|
||
}
|
||
|
||
/// 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() {
|
||
final name = _nameController.text.trim();
|
||
if (name.isEmpty) {
|
||
ScaffoldMessenger.of(
|
||
context,
|
||
).showSnackBar(SnackBar(content: Text(game_l10n.uiEnterName)));
|
||
return;
|
||
}
|
||
|
||
// 선택된 종족/클래스 (Phase 5)
|
||
final selectedRace = _races[_selectedRaceIndex];
|
||
final selectedClass = _klasses[_selectedKlassIndex];
|
||
|
||
// 게임에 사용할 새 RNG 생성
|
||
final gameSeed = math.Random().nextInt(0x7FFFFFFF);
|
||
|
||
// HP/MP 초기값 계산
|
||
// 원본 공식: Random(8) + CON/6 → 약 1~10 HP (너무 낮음)
|
||
// 수정 공식: 50 + Random(8) + CON → 약 60~76 HP (전투 생존 가능)
|
||
// 이유: 원본 PQ는 "항상 승리"하지만 이 게임은 실제 전투로 사망 가능
|
||
final hpMax = 50 + math.Random().nextInt(8) + _con;
|
||
final mpMax = 30 + math.Random().nextInt(8) + _int;
|
||
|
||
// 원본 Main.pas:1375-1379 - 기본 롤 값 그대로 저장 (보너스 없음)
|
||
final finalStats = Stats(
|
||
str: _str,
|
||
con: _con,
|
||
dex: _dex,
|
||
intelligence: _int,
|
||
wis: _wis,
|
||
cha: _cha,
|
||
hpMax: hpMax,
|
||
mpMax: mpMax,
|
||
);
|
||
|
||
final traits = Traits(
|
||
name: name,
|
||
race: selectedRace.name,
|
||
klass: selectedClass.name,
|
||
level: 1,
|
||
motto: '',
|
||
guild: '',
|
||
raceId: selectedRace.raceId,
|
||
classId: selectedClass.classId,
|
||
);
|
||
|
||
// 초기 게임 상태 생성
|
||
final initialState = GameState.withSeed(
|
||
seed: gameSeed,
|
||
traits: traits,
|
||
stats: finalStats,
|
||
inventory: const Inventory(gold: 0, items: []),
|
||
equipment: Equipment.empty(),
|
||
spellBook: SpellBook.empty(),
|
||
progress: ProgressState.empty(),
|
||
queue: QueueState.empty(),
|
||
);
|
||
|
||
widget.onCharacterCreated?.call(initialState, testMode: _testModeEnabled);
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Scaffold(
|
||
appBar: AppBar(
|
||
title: Text(L10n.of(context).newCharacterTitle),
|
||
centerTitle: true,
|
||
),
|
||
body: SingleChildScrollView(
|
||
padding: const EdgeInsets.all(16),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||
children: [
|
||
// 이름 입력 섹션
|
||
_buildNameSection(),
|
||
const SizedBox(height: 16),
|
||
|
||
// 능력치 섹션
|
||
_buildStatsSection(),
|
||
const SizedBox(height: 16),
|
||
|
||
// 종족 미리보기 (Phase 5: 종족별 캐릭터 애니메이션)
|
||
Center(
|
||
child: RacePreview(
|
||
raceId: _races[_selectedRaceIndex].raceId,
|
||
),
|
||
),
|
||
const SizedBox(height: 16),
|
||
|
||
// 종족/직업 선택 섹션
|
||
Row(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Expanded(child: _buildRaceSection()),
|
||
const SizedBox(width: 16),
|
||
Expanded(child: _buildKlassSection()),
|
||
],
|
||
),
|
||
const SizedBox(height: 16),
|
||
|
||
// 테스트 모드 토글 (웹에서 모바일 레이아웃 테스트)
|
||
_buildTestModeToggle(),
|
||
const SizedBox(height: 24),
|
||
|
||
// Sold! 버튼
|
||
FilledButton.icon(
|
||
onPressed: _onSold,
|
||
icon: const Icon(Icons.check),
|
||
label: Text(L10n.of(context).soldButton),
|
||
style: FilledButton.styleFrom(
|
||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildNameSection() {
|
||
final l10n = L10n.of(context);
|
||
return Card(
|
||
child: Padding(
|
||
padding: const EdgeInsets.all(16),
|
||
child: Row(
|
||
children: [
|
||
Expanded(
|
||
child: TextField(
|
||
controller: _nameController,
|
||
decoration: InputDecoration(
|
||
labelText: l10n.name,
|
||
border: const OutlineInputBorder(),
|
||
),
|
||
maxLength: 30,
|
||
),
|
||
),
|
||
const SizedBox(width: 8),
|
||
IconButton.filled(
|
||
onPressed: _onGenerateName,
|
||
icon: const Icon(Icons.casino),
|
||
tooltip: l10n.generateName,
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildStatsSection() {
|
||
final l10n = L10n.of(context);
|
||
return Card(
|
||
child: Padding(
|
||
padding: const EdgeInsets.all(16),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(l10n.stats, style: Theme.of(context).textTheme.titleMedium),
|
||
const SizedBox(height: 12),
|
||
|
||
// 스탯 그리드
|
||
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.3),
|
||
borderRadius: BorderRadius.circular(8),
|
||
border: Border.all(color: _getTotalColor()),
|
||
),
|
||
child: Row(
|
||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||
children: [
|
||
Text(
|
||
l10n.total,
|
||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||
),
|
||
Text(
|
||
'$_total',
|
||
style: TextStyle(
|
||
fontSize: 20,
|
||
fontWeight: FontWeight.bold,
|
||
color: _getTotalColor() == Colors.white
|
||
? Colors.black
|
||
: _getTotalColor(),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
const SizedBox(height: 12),
|
||
|
||
// Roll 버튼들
|
||
Row(
|
||
mainAxisAlignment: MainAxisAlignment.center,
|
||
children: [
|
||
OutlinedButton.icon(
|
||
onPressed: _onUnroll,
|
||
icon: const Icon(Icons.undo),
|
||
label: Text(l10n.unroll),
|
||
style: OutlinedButton.styleFrom(
|
||
foregroundColor: _rollHistory.isEmpty ? Colors.grey : null,
|
||
),
|
||
),
|
||
const SizedBox(width: 16),
|
||
FilledButton.icon(
|
||
onPressed: _onReroll,
|
||
icon: const Icon(Icons.casino),
|
||
label: Text(l10n.roll),
|
||
),
|
||
],
|
||
),
|
||
if (_rollHistory.isNotEmpty)
|
||
Padding(
|
||
padding: const EdgeInsets.only(top: 8),
|
||
child: Text(
|
||
game_l10n.uiRollHistory(_rollHistory.length),
|
||
style: Theme.of(context).textTheme.bodySmall,
|
||
textAlign: TextAlign.center,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildStatTile(String label, int value) {
|
||
return Container(
|
||
margin: const EdgeInsets.all(4),
|
||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||
decoration: BoxDecoration(
|
||
color: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||
borderRadius: BorderRadius.circular(8),
|
||
),
|
||
child: Column(
|
||
children: [
|
||
Text(label, style: Theme.of(context).textTheme.labelSmall),
|
||
const SizedBox(height: 4),
|
||
Text(
|
||
'$value',
|
||
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildRaceSection() {
|
||
return Card(
|
||
child: Padding(
|
||
padding: const EdgeInsets.all(16),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(
|
||
L10n.of(context).race,
|
||
style: Theme.of(context).textTheme.titleMedium,
|
||
),
|
||
const SizedBox(height: 8),
|
||
SizedBox(
|
||
height: 300,
|
||
child: ListView.builder(
|
||
controller: _raceScrollController,
|
||
itemCount: _races.length,
|
||
itemBuilder: (context, index) {
|
||
final isSelected = index == _selectedRaceIndex;
|
||
final race = _races[index];
|
||
return ListTile(
|
||
leading: Icon(
|
||
isSelected
|
||
? Icons.radio_button_checked
|
||
: Icons.radio_button_unchecked,
|
||
color: isSelected
|
||
? Theme.of(context).colorScheme.primary
|
||
: null,
|
||
),
|
||
title: Text(
|
||
GameDataL10n.getRaceName(context, race.name),
|
||
style: TextStyle(
|
||
fontWeight: isSelected
|
||
? FontWeight.bold
|
||
: FontWeight.normal,
|
||
),
|
||
),
|
||
subtitle: isSelected ? _buildRaceInfo(race) : null,
|
||
dense: !isSelected,
|
||
visualDensity: VisualDensity.compact,
|
||
onTap: () => setState(() => _selectedRaceIndex = index),
|
||
);
|
||
},
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
/// 종족 정보 표시 (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 Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
if (statMods.isNotEmpty)
|
||
Text(
|
||
statMods.join(', '),
|
||
style: Theme.of(context).textTheme.bodySmall,
|
||
),
|
||
if (passiveDesc.isNotEmpty)
|
||
Text(
|
||
passiveDesc,
|
||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||
color: Theme.of(context).colorScheme.primary,
|
||
),
|
||
),
|
||
],
|
||
);
|
||
}
|
||
|
||
/// 종족 패시브 설명 번역
|
||
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,
|
||
PassiveType.deathEquipmentPreserve => 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 Card(
|
||
child: Padding(
|
||
padding: const EdgeInsets.all(16),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(
|
||
L10n.of(context).classTitle,
|
||
style: Theme.of(context).textTheme.titleMedium,
|
||
),
|
||
const SizedBox(height: 8),
|
||
SizedBox(
|
||
height: 300,
|
||
child: ListView.builder(
|
||
controller: _klassScrollController,
|
||
itemCount: _klasses.length,
|
||
itemBuilder: (context, index) {
|
||
final isSelected = index == _selectedKlassIndex;
|
||
final klass = _klasses[index];
|
||
return ListTile(
|
||
leading: Icon(
|
||
isSelected
|
||
? Icons.radio_button_checked
|
||
: Icons.radio_button_unchecked,
|
||
color: isSelected
|
||
? Theme.of(context).colorScheme.primary
|
||
: null,
|
||
),
|
||
title: Text(
|
||
GameDataL10n.getKlassName(context, klass.name),
|
||
style: TextStyle(
|
||
fontWeight: isSelected
|
||
? FontWeight.bold
|
||
: FontWeight.normal,
|
||
),
|
||
),
|
||
subtitle: isSelected ? _buildClassInfo(klass) : null,
|
||
dense: !isSelected,
|
||
visualDensity: VisualDensity.compact,
|
||
onTap: () => setState(() => _selectedKlassIndex = index),
|
||
);
|
||
},
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
/// 클래스 정보 표시 (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 Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
if (statMods.isNotEmpty)
|
||
Text(
|
||
statMods.join(', '),
|
||
style: Theme.of(context).textTheme.bodySmall,
|
||
),
|
||
if (passiveDesc.isNotEmpty)
|
||
Text(
|
||
passiveDesc,
|
||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||
color: Theme.of(context).colorScheme.secondary,
|
||
),
|
||
),
|
||
],
|
||
);
|
||
}
|
||
|
||
/// 클래스 패시브 설명 번역
|
||
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 _buildTestModeToggle() {
|
||
return Card(
|
||
child: SwitchListTile(
|
||
title: Text(game_l10n.uiTestMode),
|
||
subtitle: Text(game_l10n.uiTestModeDesc),
|
||
value: _testModeEnabled,
|
||
onChanged: (value) => setState(() => _testModeEnabled = value),
|
||
secondary: const Icon(Icons.phone_android),
|
||
),
|
||
);
|
||
}
|
||
}
|