feat(phase5): 종족/클래스 시스템 균형 및 UI 통합
- 21개 종족 균형 재설계 (스탯 합계 = 0) - 18개 클래스 균형 재설계 (스탯 합계 = +3) - Traits에 raceId, classId 필드 추가 - 저장/불러오기에 종족/클래스 ID 추가 - 캐릭터 생성 UI에서 RaceData/ClassData 사용 - 선택 시 스탯 보정 및 패시브 정보 표시
This commit is contained in:
@@ -2,10 +2,12 @@ import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:askiineverdie/data/class_data.dart';
|
||||
import 'package:askiineverdie/data/race_data.dart';
|
||||
import 'package:askiineverdie/l10n/app_localizations.dart';
|
||||
import 'package:askiineverdie/src/core/l10n/game_data_l10n.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/pq_config.dart';
|
||||
import 'package:askiineverdie/src/core/model/race_traits.dart';
|
||||
import 'package:askiineverdie/src/core/util/deterministic_random.dart';
|
||||
import 'package:askiineverdie/src/core/util/pq_logic.dart';
|
||||
|
||||
@@ -21,12 +23,11 @@ class NewCharacterScreen extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _NewCharacterScreenState extends State<NewCharacterScreen> {
|
||||
final PqConfig _config = const PqConfig();
|
||||
final TextEditingController _nameController = TextEditingController();
|
||||
|
||||
// 종족(races)과 직업(klasses) 목록
|
||||
late final List<String> _races;
|
||||
late final List<String> _klasses;
|
||||
// 종족(races)과 직업(klasses) 목록 (Phase 5)
|
||||
final List<RaceTraits> _races = RaceData.all;
|
||||
final List<ClassTraits> _klasses = ClassData.all;
|
||||
|
||||
// 선택된 종족/직업 인덱스
|
||||
int _selectedRaceIndex = 0;
|
||||
@@ -54,10 +55,6 @@ class _NewCharacterScreenState extends State<NewCharacterScreen> {
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
// 종족/직업 목록 로드 (name|attribute 형식에서 name만 추출)
|
||||
_races = _config.races.map((e) => e.split('|').first).toList();
|
||||
_klasses = _config.klasses.map((e) => e.split('|').first).toList();
|
||||
|
||||
// 초기 랜덤화
|
||||
final random = math.Random();
|
||||
_selectedRaceIndex = random.nextInt(_races.length);
|
||||
@@ -150,6 +147,10 @@ class _NewCharacterScreenState extends State<NewCharacterScreen> {
|
||||
return;
|
||||
}
|
||||
|
||||
// 선택된 종족/클래스 (Phase 5)
|
||||
final selectedRace = _races[_selectedRaceIndex];
|
||||
final selectedClass = _klasses[_selectedKlassIndex];
|
||||
|
||||
// 게임에 사용할 새 RNG 생성
|
||||
final gameSeed = math.Random().nextInt(0x7FFFFFFF);
|
||||
|
||||
@@ -174,11 +175,13 @@ class _NewCharacterScreenState extends State<NewCharacterScreen> {
|
||||
|
||||
final traits = Traits(
|
||||
name: name,
|
||||
race: _races[_selectedRaceIndex],
|
||||
klass: _klasses[_selectedKlassIndex],
|
||||
race: selectedRace.name,
|
||||
klass: selectedClass.name,
|
||||
level: 1,
|
||||
motto: '',
|
||||
guild: '',
|
||||
raceId: selectedRace.raceId,
|
||||
classId: selectedClass.classId,
|
||||
);
|
||||
|
||||
// 초기 게임 상태 생성
|
||||
@@ -401,10 +404,7 @@ class _NewCharacterScreenState extends State<NewCharacterScreen> {
|
||||
itemCount: _races.length,
|
||||
itemBuilder: (context, index) {
|
||||
final isSelected = index == _selectedRaceIndex;
|
||||
final raceName = GameDataL10n.getRaceName(
|
||||
context,
|
||||
_races[index],
|
||||
);
|
||||
final race = _races[index];
|
||||
return ListTile(
|
||||
leading: Icon(
|
||||
isSelected
|
||||
@@ -415,14 +415,15 @@ class _NewCharacterScreenState extends State<NewCharacterScreen> {
|
||||
: null,
|
||||
),
|
||||
title: Text(
|
||||
raceName,
|
||||
race.name,
|
||||
style: TextStyle(
|
||||
fontWeight: isSelected
|
||||
? FontWeight.bold
|
||||
: FontWeight.normal,
|
||||
),
|
||||
),
|
||||
dense: true,
|
||||
subtitle: isSelected ? _buildRaceInfo(race) : null,
|
||||
dense: !isSelected,
|
||||
visualDensity: VisualDensity.compact,
|
||||
onTap: () => setState(() => _selectedRaceIndex = index),
|
||||
);
|
||||
@@ -435,6 +436,48 @@ class _NewCharacterScreenState extends State<NewCharacterScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
/// 종족 정보 표시 (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) => p.description).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 _statName(StatType type) {
|
||||
return switch (type) {
|
||||
StatType.str => 'STR',
|
||||
StatType.con => 'CON',
|
||||
StatType.dex => 'DEX',
|
||||
StatType.intelligence => 'INT',
|
||||
StatType.wis => 'WIS',
|
||||
StatType.cha => 'CHA',
|
||||
};
|
||||
}
|
||||
|
||||
Widget _buildKlassSection() {
|
||||
return Card(
|
||||
child: Padding(
|
||||
@@ -450,10 +493,7 @@ class _NewCharacterScreenState extends State<NewCharacterScreen> {
|
||||
itemCount: _klasses.length,
|
||||
itemBuilder: (context, index) {
|
||||
final isSelected = index == _selectedKlassIndex;
|
||||
final klassName = GameDataL10n.getKlassName(
|
||||
context,
|
||||
_klasses[index],
|
||||
);
|
||||
final klass = _klasses[index];
|
||||
return ListTile(
|
||||
leading: Icon(
|
||||
isSelected
|
||||
@@ -464,14 +504,15 @@ class _NewCharacterScreenState extends State<NewCharacterScreen> {
|
||||
: null,
|
||||
),
|
||||
title: Text(
|
||||
klassName,
|
||||
klass.name,
|
||||
style: TextStyle(
|
||||
fontWeight: isSelected
|
||||
? FontWeight.bold
|
||||
: FontWeight.normal,
|
||||
),
|
||||
),
|
||||
dense: true,
|
||||
subtitle: isSelected ? _buildClassInfo(klass) : null,
|
||||
dense: !isSelected,
|
||||
visualDensity: VisualDensity.compact,
|
||||
onTap: () => setState(() => _selectedKlassIndex = index),
|
||||
);
|
||||
@@ -483,4 +524,35 @@ class _NewCharacterScreenState extends State<NewCharacterScreen> {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 클래스 정보 표시 (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) => p.description).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,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user