From ec27389e9b8fd5873c62aea2683afcf78619aa0e Mon Sep 17 00:00:00 2001 From: JiWoong Sul Date: Wed, 17 Dec 2025 17:42:27 +0900 Subject: [PATCH] =?UTF-8?q?feat(phase5):=20=EC=A2=85=EC=A1=B1/=ED=81=B4?= =?UTF-8?q?=EB=9E=98=EC=8A=A4=20=EC=8B=9C=EC=8A=A4=ED=85=9C=20=EA=B7=A0?= =?UTF-8?q?=ED=98=95=20=EB=B0=8F=20UI=20=ED=86=B5=ED=95=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 21개 종족 균형 재설계 (스탯 합계 = 0) - 18개 클래스 균형 재설계 (스탯 합계 = +3) - Traits에 raceId, classId 필드 추가 - 저장/불러오기에 종족/클래스 ID 추가 - 캐릭터 생성 UI에서 RaceData/ClassData 사용 - 선택 시 스탯 보정 및 패시브 정보 표시 --- lib/data/class_data.dart | 43 ++- lib/data/race_data.dart | 337 ++++++++++++------ lib/src/core/model/game_state.dart | 19 + lib/src/core/model/save_data.dart | 4 + .../new_character/new_character_screen.dart | 120 +++++-- 5 files changed, 364 insertions(+), 159 deletions(-) diff --git a/lib/data/class_data.dart b/lib/data/class_data.dart index 3a722b7..862a52d 100644 --- a/lib/data/class_data.dart +++ b/lib/data/class_data.dart @@ -12,7 +12,7 @@ class ClassData { // 전사 계열 (STR 기반) // ========================================================================== - /// Bug Hunter: STR + INT + /// Bug Hunter: STR + INT (스탯 합계: +3) static const bugHunter = ClassTraits( classId: 'bug_hunter', name: 'Bug Hunter', @@ -32,12 +32,13 @@ class ClassData { restriction: EquipmentRestriction.none, ); - /// Overflow Warrior: STR + /// Overflow Warrior: STR + CON (스탯 합계: +3) static const overflowWarrior = ClassTraits( classId: 'overflow_warrior', name: 'Overflow Warrior', statModifiers: { StatType.str: 2, + StatType.con: 1, }, startingSkills: ['power_strike'], classSkills: ['power_strike', 'overflow_slash', 'buffer_break'], @@ -51,7 +52,7 @@ class ClassData { restriction: EquipmentRestriction.none, ); - /// Stack Crusher: STR + CON + /// Stack Crusher: STR + CON (스탯 합계: +3) static const stackCrusher = ClassTraits( classId: 'stack_crusher', name: 'Stack Crusher', @@ -76,7 +77,7 @@ class ClassData { restriction: EquipmentRestriction.none, ); - /// Assertion Knight: STR + WIS + /// Assertion Knight: STR + WIS (스탯 합계: +3) static const assertionKnight = ClassTraits( classId: 'assertion_knight', name: 'Assertion Knight', @@ -102,7 +103,7 @@ class ClassData { // 탱커 계열 (CON 기반) // ========================================================================== - /// Debugger Paladin: WIS + CON + /// Debugger Paladin: WIS + CON (스탯 합계: +3) static const debuggerPaladin = ClassTraits( classId: 'debugger_paladin', name: 'Debugger Paladin', @@ -129,12 +130,13 @@ class ClassData { ), ); - /// Loop Breaker: CON + /// Loop Breaker: CON + STR (스탯 합계: +3) static const loopBreaker = ClassTraits( classId: 'loop_breaker', name: 'Loop Breaker', statModifiers: { StatType.con: 2, + StatType.str: 1, }, startingSkills: ['shield_bash'], classSkills: ['shield_bash', 'infinite_guard', 'break_stance'], @@ -150,7 +152,7 @@ class ClassData { ), ); - /// Garbage Collector: CON + STR + /// Garbage Collector: CON + STR (스탯 합계: +3) static const garbageCollector = ClassTraits( classId: 'garbage_collector', name: 'Garbage Collector', @@ -181,12 +183,13 @@ class ClassData { // 마법사 계열 (INT 기반) // ========================================================================== - /// Compiler Mage: INT + MP Max + /// Compiler Mage: INT + WIS (스탯 합계: +3) static const compilerMage = ClassTraits( classId: 'compiler_mage', name: 'Compiler Mage', statModifiers: { StatType.intelligence: 2, + StatType.wis: 1, }, startingSkills: ['fireball'], classSkills: ['fireball', 'compile_blast', 'syntax_storm'], @@ -202,12 +205,13 @@ class ClassData { ), ); - /// Recursion Master: INT + /// Recursion Master: INT + DEX (스탯 합계: +3) static const recursionMaster = ClassTraits( classId: 'recursion_master', name: 'Recursion Master', statModifiers: { StatType.intelligence: 2, + StatType.dex: 1, }, startingSkills: ['fireball'], classSkills: ['fireball', 'recursive_bolt', 'stack_overflow'], @@ -223,7 +227,7 @@ class ClassData { ), ); - /// Memory Leaker: INT + WIS + /// Memory Leaker: INT + WIS (스탯 합계: +3) static const memoryLeaker = ClassTraits( classId: 'memory_leaker', name: 'Memory Leaker', @@ -245,7 +249,7 @@ class ClassData { ), ); - /// Type Caster: INT + CHA + /// Type Caster: INT + CHA (스탯 합계: +3) static const typeCaster = ClassTraits( classId: 'type_caster', name: 'Type Caster', @@ -267,7 +271,7 @@ class ClassData { ), ); - /// DevOps Shaman: CON + INT + /// DevOps Shaman: CON + INT (스탯 합계: +3) static const devOpsShaman = ClassTraits( classId: 'devops_shaman', name: 'DevOps Shaman', @@ -298,12 +302,13 @@ class ClassData { // 민첩 계열 (DEX 기반) // ========================================================================== - /// Refactor Monk: DEX + /// Refactor Monk: DEX + CON (스탯 합계: +3) static const refactorMonk = ClassTraits( classId: 'refactor_monk', name: 'Refactor Monk', statModifiers: { StatType.dex: 2, + StatType.con: 1, }, startingSkills: ['flurry'], classSkills: ['flurry', 'clean_code_strike', 'refactor_combo'], @@ -324,12 +329,13 @@ class ClassData { ), ); - /// Pointer Assassin: DEX + /// Pointer Assassin: DEX + STR (스탯 합계: +3) static const pointerAssassin = ClassTraits( classId: 'pointer_assassin', name: 'Pointer Assassin', statModifiers: { StatType.dex: 2, + StatType.str: 1, }, startingSkills: ['backstab'], classSkills: ['backstab', 'null_strike', 'dereference_kill'], @@ -350,7 +356,7 @@ class ClassData { ), ); - /// Callback Samurai: DEX + STR + /// Callback Samurai: DEX + STR (스탯 합계: +3) static const callbackSamurai = ClassTraits( classId: 'callback_samurai', name: 'Callback Samurai', @@ -375,7 +381,7 @@ class ClassData { restriction: EquipmentRestriction.none, ); - /// Tester Jester: DEX + CHA + /// Tester Jester: DEX + CHA (스탯 합계: +3) static const testerJester = ClassTraits( classId: 'tester_jester', name: 'Tester Jester', @@ -406,12 +412,13 @@ class ClassData { // 지혜 계열 (WIS 기반) // ========================================================================== - /// Exception Handler: WIS + /// Exception Handler: WIS + INT (스탯 합계: +3) static const exceptionHandler = ClassTraits( classId: 'exception_handler', name: 'Exception Handler', statModifiers: { StatType.wis: 2, + StatType.intelligence: 1, }, startingSkills: ['heal'], classSkills: ['heal', 'try_catch', 'finally_heal'], @@ -427,7 +434,7 @@ class ClassData { ), ); - /// Null Checker: WIS + INT + /// Null Checker: WIS + INT (스탯 합계: +3) static const nullChecker = ClassTraits( classId: 'null_checker', name: 'Null Checker', diff --git a/lib/data/race_data.dart b/lib/data/race_data.dart index bfbbe30..4664cd4 100644 --- a/lib/data/race_data.dart +++ b/lib/data/race_data.dart @@ -2,20 +2,42 @@ import 'package:askiineverdie/src/core/model/race_traits.dart'; /// 종족 데이터 정의 (race data) /// -/// 프로그래밍 테마의 21가지 종족 정의 -/// pq_config_data.dart의 Races 데이터 기반 +/// 21가지 종족 - 모든 종족의 스탯 합계 = 0 (균형) +/// 패시브는 각 종족의 고유한 플레이스타일을 정의 class RaceData { RaceData._(); // ========================================================================== - // 기본 종족 (HP/밸런스형) + // HP/균형형 종족 // ========================================================================== - /// Byte Human: HP 보너스 + /// Byte Human: 균형형 (스탯 합계: 0) + /// 특화 없이 경험치로 보상 static const byteHuman = RaceTraits( raceId: 'byte_human', name: 'Byte Human', statModifiers: {}, + passives: [ + PassiveAbility( + type: PassiveType.expBonus, + value: 0.05, + description: '경험치 +5%', + ), + ], + expMultiplier: 1.05, + ); + + /// Kernel Giant: 탱커형 (스탯 합계: 0) + /// STR/CON +2, DEX/INT -2 + static const kernelGiant = RaceTraits( + raceId: 'kernel_giant', + name: 'Kernel Giant', + statModifiers: { + StatType.str: 2, + StatType.con: 2, + StatType.dex: -2, + StatType.intelligence: -2, + }, passives: [ PassiveAbility( type: PassiveType.hpBonus, @@ -25,45 +47,32 @@ class RaceData { ], ); - /// Kernel Giant: STR + HP 보너스 - static const kernelGiant = RaceTraits( - raceId: 'kernel_giant', - name: 'Kernel Giant', - statModifiers: { - StatType.str: 3, - StatType.dex: -2, - }, - passives: [ - PassiveAbility( - type: PassiveType.hpBonus, - value: 0.15, - description: 'HP +15%', - ), - ], - ); - // ========================================================================== - // 지혜 종족 (WIS 기반) + // 지혜/마법 종족 (WIS 기반) // ========================================================================== - /// Null Elf: WIS 보너스 + /// Null Elf: 마법형 (스탯 합계: 0) + /// WIS/INT +2, STR/CON -2 static const nullElf = RaceTraits( raceId: 'null_elf', name: 'Null Elf', statModifiers: { StatType.wis: 2, + StatType.intelligence: 1, + StatType.str: -2, StatType.con: -1, }, passives: [ PassiveAbility( type: PassiveType.magicDamageBonus, - value: 0.10, - description: '마법 데미지 +10%', + value: 0.08, + description: '마법 데미지 +8%', ), ], ); - /// Recursive Sage: WIS + INT 보너스 + /// Recursive Sage: 순수 마법형 (스탯 합계: 0) + /// WIS/INT +2, STR -2, DEX -1, CHA +1 static const recursiveSage = RaceTraits( raceId: 'recursive_sage', name: 'Recursive Sage', @@ -71,6 +80,8 @@ class RaceData { StatType.wis: 2, StatType.intelligence: 2, StatType.str: -2, + StatType.dex: -1, + StatType.con: -1, }, passives: [ PassiveAbility( @@ -81,47 +92,62 @@ class RaceData { ], ); - /// Callback Priest: WIS + CHA 보너스 + /// Callback Priest: 지원형 (스탯 합계: 0) + /// WIS +2, CHA +1, STR -1, DEX -1, CON -1 static const callbackPriest = RaceTraits( raceId: 'callback_priest', name: 'Callback Priest', statModifiers: { StatType.wis: 2, - StatType.cha: 2, + StatType.cha: 1, StatType.str: -1, StatType.dex: -1, + StatType.con: -1, }, - passives: [], + passives: [ + PassiveAbility( + type: PassiveType.expBonus, + value: 0.03, + description: '경험치 +3%', + ), + ], + expMultiplier: 1.03, ); // ========================================================================== - // 체력 종족 (CON 기반) + // 체력/방어 종족 (CON 기반) // ========================================================================== - /// Buffer Dwarf: CON 보너스 + /// Buffer Dwarf: 방어형 (스탯 합계: 0) + /// CON +2, STR +1, DEX -2, CHA -1 static const bufferDwarf = RaceTraits( raceId: 'buffer_dwarf', name: 'Buffer Dwarf', statModifiers: { StatType.con: 2, - StatType.dex: -1, + StatType.str: 1, + StatType.dex: -2, + StatType.cha: -1, }, passives: [ PassiveAbility( type: PassiveType.defenseBonus, - value: 0.05, - description: '방어력 +5%', + value: 0.08, + description: '방어력 +8%', ), ], ); - /// Coredump Undead: CON 보너스 + /// Coredump Undead: 생존형 (스탯 합계: 0) + /// CON +2, STR +1, CHA -2, DEX -1 static const coredumpUndead = RaceTraits( raceId: 'coredump_undead', name: 'Coredump Undead', statModifiers: { StatType.con: 2, + StatType.str: 1, StatType.cha: -2, + StatType.dex: -1, }, passives: [ PassiveAbility( @@ -133,16 +159,19 @@ class RaceData { ); // ========================================================================== - // 민첩 종족 (DEX 기반) + // 민첩/크리티컬 종족 (DEX 기반) // ========================================================================== - /// Bit Halfling: DEX 보너스 + /// Bit Halfling: 민첩형 (스탯 합계: 0) + /// DEX +2, CHA +1, STR -2, CON -1 static const bitHalfling = RaceTraits( raceId: 'bit_halfling', name: 'Bit Halfling', statModifiers: { StatType.dex: 2, - StatType.str: -1, + StatType.cha: 1, + StatType.str: -2, + StatType.con: -1, }, passives: [ PassiveAbility( @@ -153,69 +182,81 @@ class RaceData { ], ); - /// Cache Imp: DEX 보너스 + /// Cache Imp: 속도형 (스탯 합계: 0) + /// DEX +2, INT +1, CON -2, WIS -1 static const cacheImp = RaceTraits( raceId: 'cache_imp', name: 'Cache Imp', statModifiers: { StatType.dex: 2, - StatType.con: -1, - }, - passives: [], - ); - - /// Iterator Rogue: DEX 보너스 - static const iteratorRogue = RaceTraits( - raceId: 'iterator_rogue', - name: 'Iterator Rogue', - statModifiers: { - StatType.dex: 2, + StatType.intelligence: 1, + StatType.con: -2, StatType.wis: -1, }, passives: [ PassiveAbility( type: PassiveType.criticalBonus, - value: 0.05, - description: '크리티컬 +5%', + value: 0.02, + description: '크리티컬 +2%', + ), + ], + ); + + /// Iterator Rogue: 암살형 (스탯 합계: 0) + /// DEX +3, STR +1, CON -2, WIS -1, CHA -1 + static const iteratorRogue = RaceTraits( + raceId: 'iterator_rogue', + name: 'Iterator Rogue', + statModifiers: { + StatType.dex: 3, + StatType.str: 1, + StatType.con: -2, + StatType.wis: -1, + StatType.cha: -1, + }, + passives: [ + PassiveAbility( + type: PassiveType.criticalBonus, + value: 0.04, + description: '크리티컬 +4%', ), ], ); // ========================================================================== - // 힘 종족 (STR 기반) + // 힘/물리 종족 (STR 기반) // ========================================================================== - /// Array Orc: STR 보너스 + /// Array Orc: 공격형 (스탯 합계: 0) + /// STR +2, CON +1, INT -2, WIS -1 static const arrayOrc = RaceTraits( raceId: 'array_orc', name: 'Array Orc', statModifiers: { StatType.str: 2, - StatType.intelligence: -1, + StatType.con: 1, + StatType.intelligence: -2, + StatType.wis: -1, }, - passives: [], + passives: [ + PassiveAbility( + type: PassiveType.hpBonus, + value: 0.05, + description: 'HP +5%', + ), + ], ); - /// Flag Knight: CHA + STR 보너스 + /// Flag Knight: 전사형 (스탯 합계: 0) + /// STR +2, CHA +1, INT -2, WIS -1 static const flagKnight = RaceTraits( raceId: 'flag_knight', name: 'Flag Knight', statModifiers: { - StatType.cha: 2, StatType.str: 2, + StatType.cha: 1, StatType.intelligence: -2, - }, - passives: [], - ); - - /// Protocol Paladin: STR + CHA 보너스 - static const protocolPaladin = RaceTraits( - raceId: 'protocol_paladin', - name: 'Protocol Paladin', - statModifiers: { - StatType.str: 2, - StatType.cha: 2, - StatType.dex: -2, + StatType.wis: -1, }, passives: [ PassiveAbility( @@ -226,23 +267,53 @@ class RaceData { ], ); + /// Protocol Paladin: 수호자형 (스탯 합계: 0) + /// STR +2, CON +1, CHA +1, DEX -2, INT -2 + static const protocolPaladin = RaceTraits( + raceId: 'protocol_paladin', + name: 'Protocol Paladin', + statModifiers: { + StatType.str: 2, + StatType.con: 1, + StatType.cha: 1, + StatType.dex: -2, + StatType.intelligence: -2, + }, + passives: [ + PassiveAbility( + type: PassiveType.defenseBonus, + value: 0.06, + description: '방어력 +6%', + ), + ], + ); + // ========================================================================== // 복합 스탯 종족 // ========================================================================== - /// Stack Goblin: DEX + CON 보너스 + /// Stack Goblin: 기동형 (스탯 합계: 0) + /// DEX +2, CON +1, STR -1, CHA -2 static const stackGoblin = RaceTraits( raceId: 'stack_goblin', name: 'Stack Goblin', statModifiers: { StatType.dex: 2, StatType.con: 1, + StatType.str: -1, StatType.cha: -2, }, - passives: [], + passives: [ + PassiveAbility( + type: PassiveType.criticalBonus, + value: 0.02, + description: '크리티컬 +2%', + ), + ], ); - /// Heap Troll: CON + STR 보너스 + /// Heap Troll: 중장갑형 (스탯 합계: 0) + /// CON +2, STR +2, INT -2, DEX -2 static const heapTroll = RaceTraits( raceId: 'heap_troll', name: 'Heap Troll', @@ -250,18 +321,19 @@ class RaceData { StatType.con: 2, StatType.str: 2, StatType.intelligence: -2, - StatType.dex: -1, + StatType.dex: -2, }, passives: [ PassiveAbility( type: PassiveType.hpBonus, - value: 0.10, - description: 'HP +10%', + value: 0.12, + description: 'HP +12%', ), ], ); - /// Index Ranger: DEX + CON 보너스 + /// Index Ranger: 정찰형 (스탯 합계: 0) + /// DEX +2, CON +1, INT -1, CHA -2 static const indexRanger = RaceTraits( raceId: 'index_ranger', name: 'Index Ranger', @@ -269,21 +341,70 @@ class RaceData { StatType.dex: 2, StatType.con: 1, StatType.intelligence: -1, + StatType.cha: -2, }, - passives: [], + passives: [ + PassiveAbility( + type: PassiveType.criticalBonus, + value: 0.03, + description: '크리티컬 +3%', + ), + ], ); // ========================================================================== - // 마법 종족 (MP/INT 기반) + // MP/주문 종족 (INT/MP 기반) // ========================================================================== - /// Pointer Fairy: MP Max + WIS 보너스 + /// Pointer Fairy: MP 특화형 (스탯 합계: 0) + /// WIS +2, INT +1, STR -2, CON -1 static const pointerFairy = RaceTraits( raceId: 'pointer_fairy', name: 'Pointer Fairy', statModifiers: { StatType.wis: 2, + StatType.intelligence: 1, StatType.str: -2, + StatType.con: -1, + }, + passives: [ + PassiveAbility( + type: PassiveType.mpBonus, + value: 0.12, + description: 'MP +12%', + ), + ], + ); + + /// Register Gnome: 지능형 (스탯 합계: 0) + /// INT +2, DEX +1, STR -2, CON -1 + static const registerGnome = RaceTraits( + raceId: 'register_gnome', + name: 'Register Gnome', + statModifiers: { + StatType.intelligence: 2, + StatType.dex: 1, + StatType.str: -2, + StatType.con: -1, + }, + passives: [ + PassiveAbility( + type: PassiveType.magicDamageBonus, + value: 0.05, + description: '마법 데미지 +5%', + ), + ], + ); + + /// Thread Spirit: 영체형 (스탯 합계: 0) + /// INT +1, WIS +1, CON -2 + static const threadSpirit = RaceTraits( + raceId: 'thread_spirit', + name: 'Thread Spirit', + statModifiers: { + StatType.intelligence: 1, + StatType.wis: 1, + StatType.con: -2, }, passives: [ PassiveAbility( @@ -294,57 +415,34 @@ class RaceData { ], ); - /// Register Gnome: INT 보너스 - static const registerGnome = RaceTraits( - raceId: 'register_gnome', - name: 'Register Gnome', - statModifiers: { - StatType.intelligence: 2, - StatType.str: -1, - }, - passives: [], - ); - - /// Thread Spirit: MP Max 보너스 - static const threadSpirit = RaceTraits( - raceId: 'thread_spirit', - name: 'Thread Spirit', - statModifiers: { - StatType.con: -1, - }, - passives: [ - PassiveAbility( - type: PassiveType.mpBonus, - value: 0.20, - description: 'MP +20%', - ), - ], - ); - - /// Loop Wizard: INT + MP Max 보너스 + /// Loop Wizard: 순환 마법형 (스탯 합계: 0) + /// INT +2, WIS +1, STR -1, CON -1, DEX -1 static const loopWizard = RaceTraits( raceId: 'loop_wizard', name: 'Loop Wizard', statModifiers: { StatType.intelligence: 2, + StatType.wis: 1, StatType.str: -1, StatType.con: -1, + StatType.dex: -1, }, passives: [ PassiveAbility( type: PassiveType.mpBonus, - value: 0.10, - description: 'MP +10%', + value: 0.08, + description: 'MP +8%', ), PassiveAbility( type: PassiveType.magicDamageBonus, - value: 0.05, - description: '마법 데미지 +5%', + value: 0.04, + description: '마법 데미지 +4%', ), ], ); - /// Lambda Druid: INT + WIS 보너스 + /// Lambda Druid: 자연 마법형 (스탯 합계: 0) + /// INT +2, WIS +2, STR -2, CON -2 static const lambdaDruid = RaceTraits( raceId: 'lambda_druid', name: 'Lambda Druid', @@ -352,20 +450,25 @@ class RaceData { StatType.intelligence: 2, StatType.wis: 2, StatType.str: -2, - StatType.con: -1, + StatType.con: -2, }, passives: [ PassiveAbility( type: PassiveType.magicDamageBonus, - value: 0.10, - description: '마법 데미지 +10%', + value: 0.06, + description: '마법 데미지 +6%', + ), + PassiveAbility( + type: PassiveType.mpBonus, + value: 0.06, + description: 'MP +6%', ), ], ); /// 모든 종족 목록 (21개) static const List all = [ - // 기본/HP형 + // 균형형 byteHuman, kernelGiant, // 지혜형 diff --git a/lib/src/core/model/game_state.dart b/lib/src/core/model/game_state.dart index e164faa..6636520 100644 --- a/lib/src/core/model/game_state.dart +++ b/lib/src/core/model/game_state.dart @@ -296,15 +296,28 @@ class Traits { required this.level, required this.motto, required this.guild, + this.raceId = '', + this.classId = '', }); final String name; + + /// 종족 표시 이름 (예: "Kernel Giant") final String race; + + /// 클래스 표시 이름 (예: "Bug Hunter") final String klass; + final int level; final String motto; final String guild; + /// 종족 ID (Phase 5, 예: "kernel_giant") + final String raceId; + + /// 클래스 ID (Phase 5, 예: "bug_hunter") + final String classId; + factory Traits.empty() => const Traits( name: '', race: '', @@ -312,6 +325,8 @@ class Traits { level: 1, motto: '', guild: '', + raceId: '', + classId: '', ); Traits copyWith({ @@ -321,6 +336,8 @@ class Traits { int? level, String? motto, String? guild, + String? raceId, + String? classId, }) { return Traits( name: name ?? this.name, @@ -329,6 +346,8 @@ class Traits { level: level ?? this.level, motto: motto ?? this.motto, guild: guild ?? this.guild, + raceId: raceId ?? this.raceId, + classId: classId ?? this.classId, ); } } diff --git a/lib/src/core/model/save_data.dart b/lib/src/core/model/save_data.dart index 0ba1081..a8aa1b6 100644 --- a/lib/src/core/model/save_data.dart +++ b/lib/src/core/model/save_data.dart @@ -53,6 +53,8 @@ class GameSave { 'level': traits.level, 'motto': traits.motto, 'guild': traits.guild, + 'raceId': traits.raceId, + 'classId': traits.classId, }, 'stats': { 'str': stats.str, @@ -148,6 +150,8 @@ class GameSave { level: traitsJson['level'] as int? ?? 1, motto: traitsJson['motto'] as String? ?? '', guild: traitsJson['guild'] as String? ?? '', + raceId: traitsJson['raceId'] as String? ?? '', + classId: traitsJson['classId'] as String? ?? '', ), stats: Stats( str: statsJson['str'] as int? ?? 0, diff --git a/lib/src/features/new_character/new_character_screen.dart b/lib/src/features/new_character/new_character_screen.dart index 4c2853c..eca411c 100644 --- a/lib/src/features/new_character/new_character_screen.dart +++ b/lib/src/features/new_character/new_character_screen.dart @@ -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 { - final PqConfig _config = const PqConfig(); final TextEditingController _nameController = TextEditingController(); - // 종족(races)과 직업(klasses) 목록 - late final List _races; - late final List _klasses; + // 종족(races)과 직업(klasses) 목록 (Phase 5) + final List _races = RaceData.all; + final List _klasses = ClassData.all; // 선택된 종족/직업 인덱스 int _selectedRaceIndex = 0; @@ -54,10 +55,6 @@ class _NewCharacterScreenState extends State { 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 { 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 { 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 { 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 { : 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 { ); } + /// 종족 정보 표시 (Phase 5) + Widget _buildRaceInfo(RaceTraits race) { + final statMods = []; + 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 { 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 { : 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 { ), ); } + + /// 클래스 정보 표시 (Phase 5) + Widget _buildClassInfo(ClassTraits klass) { + final statMods = []; + 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, + ), + ), + ], + ); + } }