feat(game): 게임 시스템 전면 개편 및 다국어 지원 확장
## 스킬 시스템 개선 - skill_data.dart: 스킬 데이터 구조 전면 개편 (+1176 라인) - skill_service.dart: 스킬 발동 로직 확장 및 버프 시스템 연동 - skill.dart: 스킬 모델 개선, 쿨다운/효과 타입 추가 ## Canvas 애니메이션 리팩토링 - battle_composer.dart 삭제 (레거시 위젯 기반 렌더러) - monster_colors.dart 삭제 (AsciiCell 색상 시스템으로 통합) - canvas_battle_composer.dart: z-index 정렬 (몬스터 z=1, 캐릭터 z=2, 이펙트 z=3) - ascii_cell.dart, ascii_layer.dart: 코드 정리 ## UI/UX 개선 - hp_mp_bar.dart: l10n 적용, 몬스터 HP 바 컴팩트화 - death_overlay.dart: 사망 화면 개선 - equipment_stats_panel.dart: 장비 스탯 표시 확장 - active_buff_panel.dart: 버프 패널 개선 - notification_overlay.dart: 알림 시스템 개선 ## 다국어 지원 확장 - game_text_l10n.dart: 게임 텍스트 통합 (+758 라인) - 한국어/일본어/영어/중국어 번역 업데이트 - ARB 파일 동기화 ## 게임 로직 개선 - progress_service.dart: 진행 로직 리팩토링 - combat_calculator.dart: 전투 계산 로직 개선 - stat_calculator.dart: 스탯 계산 시스템 개선 - story_service.dart: 스토리 진행 로직 개선 ## 기타 - theme_preferences.dart 삭제 (미사용) - 테스트 파일 업데이트 - class_data.dart: 클래스 데이터 정리
This commit is contained in:
@@ -16,10 +16,7 @@ class ClassData {
|
|||||||
static const bugHunter = ClassTraits(
|
static const bugHunter = ClassTraits(
|
||||||
classId: 'bug_hunter',
|
classId: 'bug_hunter',
|
||||||
name: 'Bug Hunter',
|
name: 'Bug Hunter',
|
||||||
statModifiers: {
|
statModifiers: {StatType.str: 2, StatType.intelligence: 1},
|
||||||
StatType.str: 2,
|
|
||||||
StatType.intelligence: 1,
|
|
||||||
},
|
|
||||||
startingSkills: ['power_strike'],
|
startingSkills: ['power_strike'],
|
||||||
classSkills: ['power_strike', 'execute', 'bug_smash'],
|
classSkills: ['power_strike', 'execute', 'bug_smash'],
|
||||||
passives: [
|
passives: [
|
||||||
@@ -36,10 +33,7 @@ class ClassData {
|
|||||||
static const overflowWarrior = ClassTraits(
|
static const overflowWarrior = ClassTraits(
|
||||||
classId: 'overflow_warrior',
|
classId: 'overflow_warrior',
|
||||||
name: 'Overflow Warrior',
|
name: 'Overflow Warrior',
|
||||||
statModifiers: {
|
statModifiers: {StatType.str: 2, StatType.con: 1},
|
||||||
StatType.str: 2,
|
|
||||||
StatType.con: 1,
|
|
||||||
},
|
|
||||||
startingSkills: ['power_strike'],
|
startingSkills: ['power_strike'],
|
||||||
classSkills: ['power_strike', 'overflow_slash', 'buffer_break'],
|
classSkills: ['power_strike', 'overflow_slash', 'buffer_break'],
|
||||||
passives: [
|
passives: [
|
||||||
@@ -56,10 +50,7 @@ class ClassData {
|
|||||||
static const stackCrusher = ClassTraits(
|
static const stackCrusher = ClassTraits(
|
||||||
classId: 'stack_crusher',
|
classId: 'stack_crusher',
|
||||||
name: 'Stack Crusher',
|
name: 'Stack Crusher',
|
||||||
statModifiers: {
|
statModifiers: {StatType.str: 2, StatType.con: 1},
|
||||||
StatType.str: 2,
|
|
||||||
StatType.con: 1,
|
|
||||||
},
|
|
||||||
startingSkills: ['power_strike'],
|
startingSkills: ['power_strike'],
|
||||||
classSkills: ['power_strike', 'stack_smash', 'heap_slam'],
|
classSkills: ['power_strike', 'stack_smash', 'heap_slam'],
|
||||||
passives: [
|
passives: [
|
||||||
@@ -81,10 +72,7 @@ class ClassData {
|
|||||||
static const assertionKnight = ClassTraits(
|
static const assertionKnight = ClassTraits(
|
||||||
classId: 'assertion_knight',
|
classId: 'assertion_knight',
|
||||||
name: 'Assertion Knight',
|
name: 'Assertion Knight',
|
||||||
statModifiers: {
|
statModifiers: {StatType.str: 2, StatType.wis: 1},
|
||||||
StatType.str: 2,
|
|
||||||
StatType.wis: 1,
|
|
||||||
},
|
|
||||||
startingSkills: ['shield_bash'],
|
startingSkills: ['shield_bash'],
|
||||||
classSkills: ['shield_bash', 'assert_strike', 'validation_guard'],
|
classSkills: ['shield_bash', 'assert_strike', 'validation_guard'],
|
||||||
passives: [
|
passives: [
|
||||||
@@ -94,9 +82,7 @@ class ClassData {
|
|||||||
description: '방어력 +10%',
|
description: '방어력 +10%',
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
restriction: EquipmentRestriction(
|
restriction: EquipmentRestriction(armorWeight: ArmorWeight.heavy),
|
||||||
armorWeight: ArmorWeight.heavy,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// ==========================================================================
|
// ==========================================================================
|
||||||
@@ -107,10 +93,7 @@ class ClassData {
|
|||||||
static const debuggerPaladin = ClassTraits(
|
static const debuggerPaladin = ClassTraits(
|
||||||
classId: 'debugger_paladin',
|
classId: 'debugger_paladin',
|
||||||
name: 'Debugger Paladin',
|
name: 'Debugger Paladin',
|
||||||
statModifiers: {
|
statModifiers: {StatType.wis: 2, StatType.con: 1},
|
||||||
StatType.wis: 2,
|
|
||||||
StatType.con: 1,
|
|
||||||
},
|
|
||||||
startingSkills: ['shield_bash'],
|
startingSkills: ['shield_bash'],
|
||||||
classSkills: ['shield_bash', 'debug_heal', 'breakpoint_guard'],
|
classSkills: ['shield_bash', 'debug_heal', 'breakpoint_guard'],
|
||||||
passives: [
|
passives: [
|
||||||
@@ -125,19 +108,14 @@ class ClassData {
|
|||||||
description: '회복력 +10%',
|
description: '회복력 +10%',
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
restriction: EquipmentRestriction(
|
restriction: EquipmentRestriction(armorWeight: ArmorWeight.heavy),
|
||||||
armorWeight: ArmorWeight.heavy,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
/// Loop Breaker: CON + STR (스탯 합계: +3)
|
/// Loop Breaker: CON + STR (스탯 합계: +3)
|
||||||
static const loopBreaker = ClassTraits(
|
static const loopBreaker = ClassTraits(
|
||||||
classId: 'loop_breaker',
|
classId: 'loop_breaker',
|
||||||
name: 'Loop Breaker',
|
name: 'Loop Breaker',
|
||||||
statModifiers: {
|
statModifiers: {StatType.con: 2, StatType.str: 1},
|
||||||
StatType.con: 2,
|
|
||||||
StatType.str: 1,
|
|
||||||
},
|
|
||||||
startingSkills: ['shield_bash'],
|
startingSkills: ['shield_bash'],
|
||||||
classSkills: ['shield_bash', 'infinite_guard', 'break_stance'],
|
classSkills: ['shield_bash', 'infinite_guard', 'break_stance'],
|
||||||
passives: [
|
passives: [
|
||||||
@@ -147,19 +125,14 @@ class ClassData {
|
|||||||
description: 'HP +15%',
|
description: 'HP +15%',
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
restriction: EquipmentRestriction(
|
restriction: EquipmentRestriction(armorWeight: ArmorWeight.heavy),
|
||||||
armorWeight: ArmorWeight.heavy,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
/// Garbage Collector: CON + STR (스탯 합계: +3)
|
/// Garbage Collector: CON + STR (스탯 합계: +3)
|
||||||
static const garbageCollector = ClassTraits(
|
static const garbageCollector = ClassTraits(
|
||||||
classId: 'garbage_collector',
|
classId: 'garbage_collector',
|
||||||
name: 'Garbage Collector',
|
name: 'Garbage Collector',
|
||||||
statModifiers: {
|
statModifiers: {StatType.con: 2, StatType.str: 1},
|
||||||
StatType.con: 2,
|
|
||||||
StatType.str: 1,
|
|
||||||
},
|
|
||||||
startingSkills: ['absorb'],
|
startingSkills: ['absorb'],
|
||||||
classSkills: ['absorb', 'recycle', 'memory_sweep'],
|
classSkills: ['absorb', 'recycle', 'memory_sweep'],
|
||||||
passives: [
|
passives: [
|
||||||
@@ -174,9 +147,7 @@ class ClassData {
|
|||||||
description: '전투 후 HP 5% 회복',
|
description: '전투 후 HP 5% 회복',
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
restriction: EquipmentRestriction(
|
restriction: EquipmentRestriction(armorWeight: ArmorWeight.heavy),
|
||||||
armorWeight: ArmorWeight.heavy,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// ==========================================================================
|
// ==========================================================================
|
||||||
@@ -187,10 +158,7 @@ class ClassData {
|
|||||||
static const compilerMage = ClassTraits(
|
static const compilerMage = ClassTraits(
|
||||||
classId: 'compiler_mage',
|
classId: 'compiler_mage',
|
||||||
name: 'Compiler Mage',
|
name: 'Compiler Mage',
|
||||||
statModifiers: {
|
statModifiers: {StatType.intelligence: 2, StatType.wis: 1},
|
||||||
StatType.intelligence: 2,
|
|
||||||
StatType.wis: 1,
|
|
||||||
},
|
|
||||||
startingSkills: ['fireball'],
|
startingSkills: ['fireball'],
|
||||||
classSkills: ['fireball', 'compile_blast', 'syntax_storm'],
|
classSkills: ['fireball', 'compile_blast', 'syntax_storm'],
|
||||||
passives: [
|
passives: [
|
||||||
@@ -200,19 +168,14 @@ class ClassData {
|
|||||||
description: '마법 데미지 +15%',
|
description: '마법 데미지 +15%',
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
restriction: EquipmentRestriction(
|
restriction: EquipmentRestriction(armorWeight: ArmorWeight.light),
|
||||||
armorWeight: ArmorWeight.light,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
/// Recursion Master: INT + DEX (스탯 합계: +3)
|
/// Recursion Master: INT + DEX (스탯 합계: +3)
|
||||||
static const recursionMaster = ClassTraits(
|
static const recursionMaster = ClassTraits(
|
||||||
classId: 'recursion_master',
|
classId: 'recursion_master',
|
||||||
name: 'Recursion Master',
|
name: 'Recursion Master',
|
||||||
statModifiers: {
|
statModifiers: {StatType.intelligence: 2, StatType.dex: 1},
|
||||||
StatType.intelligence: 2,
|
|
||||||
StatType.dex: 1,
|
|
||||||
},
|
|
||||||
startingSkills: ['fireball'],
|
startingSkills: ['fireball'],
|
||||||
classSkills: ['fireball', 'recursive_bolt', 'stack_overflow'],
|
classSkills: ['fireball', 'recursive_bolt', 'stack_overflow'],
|
||||||
passives: [
|
passives: [
|
||||||
@@ -222,19 +185,14 @@ class ClassData {
|
|||||||
description: '마법 데미지 +20%',
|
description: '마법 데미지 +20%',
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
restriction: EquipmentRestriction(
|
restriction: EquipmentRestriction(armorWeight: ArmorWeight.light),
|
||||||
armorWeight: ArmorWeight.light,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
/// Memory Leaker: INT + WIS (스탯 합계: +3)
|
/// Memory Leaker: INT + WIS (스탯 합계: +3)
|
||||||
static const memoryLeaker = ClassTraits(
|
static const memoryLeaker = ClassTraits(
|
||||||
classId: 'memory_leaker',
|
classId: 'memory_leaker',
|
||||||
name: 'Memory Leaker',
|
name: 'Memory Leaker',
|
||||||
statModifiers: {
|
statModifiers: {StatType.intelligence: 2, StatType.wis: 1},
|
||||||
StatType.intelligence: 2,
|
|
||||||
StatType.wis: 1,
|
|
||||||
},
|
|
||||||
startingSkills: ['fireball'],
|
startingSkills: ['fireball'],
|
||||||
classSkills: ['fireball', 'leak_drain', 'memory_corrupt'],
|
classSkills: ['fireball', 'leak_drain', 'memory_corrupt'],
|
||||||
passives: [
|
passives: [
|
||||||
@@ -244,19 +202,14 @@ class ClassData {
|
|||||||
description: '마법 데미지 +10%',
|
description: '마법 데미지 +10%',
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
restriction: EquipmentRestriction(
|
restriction: EquipmentRestriction(armorWeight: ArmorWeight.light),
|
||||||
armorWeight: ArmorWeight.light,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
/// Type Caster: INT + CHA (스탯 합계: +3)
|
/// Type Caster: INT + CHA (스탯 합계: +3)
|
||||||
static const typeCaster = ClassTraits(
|
static const typeCaster = ClassTraits(
|
||||||
classId: 'type_caster',
|
classId: 'type_caster',
|
||||||
name: 'Type Caster',
|
name: 'Type Caster',
|
||||||
statModifiers: {
|
statModifiers: {StatType.intelligence: 2, StatType.cha: 1},
|
||||||
StatType.intelligence: 2,
|
|
||||||
StatType.cha: 1,
|
|
||||||
},
|
|
||||||
startingSkills: ['fireball'],
|
startingSkills: ['fireball'],
|
||||||
classSkills: ['fireball', 'type_coercion', 'cast_spell'],
|
classSkills: ['fireball', 'type_coercion', 'cast_spell'],
|
||||||
passives: [
|
passives: [
|
||||||
@@ -266,19 +219,14 @@ class ClassData {
|
|||||||
description: '마법 데미지 +10%',
|
description: '마법 데미지 +10%',
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
restriction: EquipmentRestriction(
|
restriction: EquipmentRestriction(armorWeight: ArmorWeight.light),
|
||||||
armorWeight: ArmorWeight.light,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
/// DevOps Shaman: CON + INT (스탯 합계: +3)
|
/// DevOps Shaman: CON + INT (스탯 합계: +3)
|
||||||
static const devOpsShaman = ClassTraits(
|
static const devOpsShaman = ClassTraits(
|
||||||
classId: 'devops_shaman',
|
classId: 'devops_shaman',
|
||||||
name: 'DevOps Shaman',
|
name: 'DevOps Shaman',
|
||||||
statModifiers: {
|
statModifiers: {StatType.con: 1, StatType.intelligence: 2},
|
||||||
StatType.con: 1,
|
|
||||||
StatType.intelligence: 2,
|
|
||||||
},
|
|
||||||
startingSkills: ['fireball'],
|
startingSkills: ['fireball'],
|
||||||
classSkills: ['fireball', 'deploy_strike', 'ci_cd_flow'],
|
classSkills: ['fireball', 'deploy_strike', 'ci_cd_flow'],
|
||||||
passives: [
|
passives: [
|
||||||
@@ -293,9 +241,7 @@ class ClassData {
|
|||||||
description: 'HP +10%',
|
description: 'HP +10%',
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
restriction: EquipmentRestriction(
|
restriction: EquipmentRestriction(armorWeight: ArmorWeight.light),
|
||||||
armorWeight: ArmorWeight.light,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// ==========================================================================
|
// ==========================================================================
|
||||||
@@ -306,10 +252,7 @@ class ClassData {
|
|||||||
static const refactorMonk = ClassTraits(
|
static const refactorMonk = ClassTraits(
|
||||||
classId: 'refactor_monk',
|
classId: 'refactor_monk',
|
||||||
name: 'Refactor Monk',
|
name: 'Refactor Monk',
|
||||||
statModifiers: {
|
statModifiers: {StatType.dex: 2, StatType.con: 1},
|
||||||
StatType.dex: 2,
|
|
||||||
StatType.con: 1,
|
|
||||||
},
|
|
||||||
startingSkills: ['flurry'],
|
startingSkills: ['flurry'],
|
||||||
classSkills: ['flurry', 'clean_code_strike', 'refactor_combo'],
|
classSkills: ['flurry', 'clean_code_strike', 'refactor_combo'],
|
||||||
passives: [
|
passives: [
|
||||||
@@ -324,19 +267,14 @@ class ClassData {
|
|||||||
description: '연속 공격',
|
description: '연속 공격',
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
restriction: EquipmentRestriction(
|
restriction: EquipmentRestriction(armorWeight: ArmorWeight.light),
|
||||||
armorWeight: ArmorWeight.light,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
/// Pointer Assassin: DEX + STR (스탯 합계: +3)
|
/// Pointer Assassin: DEX + STR (스탯 합계: +3)
|
||||||
static const pointerAssassin = ClassTraits(
|
static const pointerAssassin = ClassTraits(
|
||||||
classId: 'pointer_assassin',
|
classId: 'pointer_assassin',
|
||||||
name: 'Pointer Assassin',
|
name: 'Pointer Assassin',
|
||||||
statModifiers: {
|
statModifiers: {StatType.dex: 2, StatType.str: 1},
|
||||||
StatType.dex: 2,
|
|
||||||
StatType.str: 1,
|
|
||||||
},
|
|
||||||
startingSkills: ['backstab'],
|
startingSkills: ['backstab'],
|
||||||
classSkills: ['backstab', 'null_strike', 'dereference_kill'],
|
classSkills: ['backstab', 'null_strike', 'dereference_kill'],
|
||||||
passives: [
|
passives: [
|
||||||
@@ -351,19 +289,14 @@ class ClassData {
|
|||||||
description: '첫 공격 1.5배',
|
description: '첫 공격 1.5배',
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
restriction: EquipmentRestriction(
|
restriction: EquipmentRestriction(armorWeight: ArmorWeight.light),
|
||||||
armorWeight: ArmorWeight.light,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
/// Callback Samurai: DEX + STR (스탯 합계: +3)
|
/// Callback Samurai: DEX + STR (스탯 합계: +3)
|
||||||
static const callbackSamurai = ClassTraits(
|
static const callbackSamurai = ClassTraits(
|
||||||
classId: 'callback_samurai',
|
classId: 'callback_samurai',
|
||||||
name: 'Callback Samurai',
|
name: 'Callback Samurai',
|
||||||
statModifiers: {
|
statModifiers: {StatType.dex: 2, StatType.str: 1},
|
||||||
StatType.dex: 2,
|
|
||||||
StatType.str: 1,
|
|
||||||
},
|
|
||||||
startingSkills: ['power_strike'],
|
startingSkills: ['power_strike'],
|
||||||
classSkills: ['power_strike', 'async_slash', 'promise_blade'],
|
classSkills: ['power_strike', 'async_slash', 'promise_blade'],
|
||||||
passives: [
|
passives: [
|
||||||
@@ -385,10 +318,7 @@ class ClassData {
|
|||||||
static const testerJester = ClassTraits(
|
static const testerJester = ClassTraits(
|
||||||
classId: 'tester_jester',
|
classId: 'tester_jester',
|
||||||
name: 'Tester Jester',
|
name: 'Tester Jester',
|
||||||
statModifiers: {
|
statModifiers: {StatType.dex: 2, StatType.cha: 1},
|
||||||
StatType.dex: 2,
|
|
||||||
StatType.cha: 1,
|
|
||||||
},
|
|
||||||
startingSkills: ['flurry'],
|
startingSkills: ['flurry'],
|
||||||
classSkills: ['flurry', 'mock_strike', 'assert_fail'],
|
classSkills: ['flurry', 'mock_strike', 'assert_fail'],
|
||||||
passives: [
|
passives: [
|
||||||
@@ -403,9 +333,7 @@ class ClassData {
|
|||||||
description: '크리티컬 +5%',
|
description: '크리티컬 +5%',
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
restriction: EquipmentRestriction(
|
restriction: EquipmentRestriction(armorWeight: ArmorWeight.light),
|
||||||
armorWeight: ArmorWeight.light,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// ==========================================================================
|
// ==========================================================================
|
||||||
@@ -416,10 +344,7 @@ class ClassData {
|
|||||||
static const exceptionHandler = ClassTraits(
|
static const exceptionHandler = ClassTraits(
|
||||||
classId: 'exception_handler',
|
classId: 'exception_handler',
|
||||||
name: 'Exception Handler',
|
name: 'Exception Handler',
|
||||||
statModifiers: {
|
statModifiers: {StatType.wis: 2, StatType.intelligence: 1},
|
||||||
StatType.wis: 2,
|
|
||||||
StatType.intelligence: 1,
|
|
||||||
},
|
|
||||||
startingSkills: ['heal'],
|
startingSkills: ['heal'],
|
||||||
classSkills: ['heal', 'try_catch', 'finally_heal'],
|
classSkills: ['heal', 'try_catch', 'finally_heal'],
|
||||||
passives: [
|
passives: [
|
||||||
@@ -429,19 +354,14 @@ class ClassData {
|
|||||||
description: '회복력 +15%',
|
description: '회복력 +15%',
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
restriction: EquipmentRestriction(
|
restriction: EquipmentRestriction(armorWeight: ArmorWeight.light),
|
||||||
armorWeight: ArmorWeight.light,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
/// Null Checker: WIS + INT (스탯 합계: +3)
|
/// Null Checker: WIS + INT (스탯 합계: +3)
|
||||||
static const nullChecker = ClassTraits(
|
static const nullChecker = ClassTraits(
|
||||||
classId: 'null_checker',
|
classId: 'null_checker',
|
||||||
name: 'Null Checker',
|
name: 'Null Checker',
|
||||||
statModifiers: {
|
statModifiers: {StatType.wis: 2, StatType.intelligence: 1},
|
||||||
StatType.wis: 2,
|
|
||||||
StatType.intelligence: 1,
|
|
||||||
},
|
|
||||||
startingSkills: ['heal'],
|
startingSkills: ['heal'],
|
||||||
classSkills: ['heal', 'null_guard', 'safe_call'],
|
classSkills: ['heal', 'null_guard', 'safe_call'],
|
||||||
passives: [
|
passives: [
|
||||||
@@ -456,9 +376,7 @@ class ClassData {
|
|||||||
description: '방어력 +5%',
|
description: '방어력 +5%',
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
restriction: EquipmentRestriction(
|
restriction: EquipmentRestriction(armorWeight: ArmorWeight.light),
|
||||||
armorWeight: ArmorWeight.light,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
/// 모든 클래스 목록 (18개)
|
/// 모든 클래스 목록 (18개)
|
||||||
|
|||||||
@@ -99,6 +99,362 @@ String taskSelling(String itemDescription) {
|
|||||||
return 'Selling $itemDescription';
|
return 'Selling $itemDescription';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// 부활 시퀀스 메시지
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
String get taskReturningToTown {
|
||||||
|
if (isKoreanLocale) return '마을로 귀환 중...';
|
||||||
|
if (isJapaneseLocale) return '町に戻っている...';
|
||||||
|
return 'Returning to town...';
|
||||||
|
}
|
||||||
|
|
||||||
|
String get taskRestockingAtShop {
|
||||||
|
if (isKoreanLocale) return '상점에서 장비 정비 중...';
|
||||||
|
if (isJapaneseLocale) return 'ショップで装備を整備中...';
|
||||||
|
return 'Restocking at shop...';
|
||||||
|
}
|
||||||
|
|
||||||
|
String get taskHeadingToHuntingGrounds {
|
||||||
|
if (isKoreanLocale) return '사냥터로 이동 중...';
|
||||||
|
if (isJapaneseLocale) return '狩り場へ向かっている...';
|
||||||
|
return 'Heading to hunting grounds...';
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// 사망 화면 메시지
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
String get deathYouDied {
|
||||||
|
if (isKoreanLocale) return '사망';
|
||||||
|
if (isJapaneseLocale) return '死亡';
|
||||||
|
return 'YOU DIED';
|
||||||
|
}
|
||||||
|
|
||||||
|
String get deathSacrificedToResurrect {
|
||||||
|
if (isKoreanLocale) return '부활 대가로 희생됨';
|
||||||
|
if (isJapaneseLocale) return '復活のために犠牲';
|
||||||
|
return 'Sacrificed to Resurrect';
|
||||||
|
}
|
||||||
|
|
||||||
|
String get deathEquipment {
|
||||||
|
if (isKoreanLocale) return '장비';
|
||||||
|
if (isJapaneseLocale) return '装備';
|
||||||
|
return 'Equipment';
|
||||||
|
}
|
||||||
|
|
||||||
|
String get deathNoSacrificeNeeded {
|
||||||
|
if (isKoreanLocale) return '희생 없이 부활';
|
||||||
|
if (isJapaneseLocale) return '犠牲なしで復活';
|
||||||
|
return 'No sacrifice needed';
|
||||||
|
}
|
||||||
|
|
||||||
|
String get deathGoldRemaining {
|
||||||
|
if (isKoreanLocale) return '남은 골드';
|
||||||
|
if (isJapaneseLocale) return '残りゴールド';
|
||||||
|
return 'Gold Remaining';
|
||||||
|
}
|
||||||
|
|
||||||
|
String get deathResurrect {
|
||||||
|
if (isKoreanLocale) return '부활';
|
||||||
|
if (isJapaneseLocale) return '復活';
|
||||||
|
return 'Resurrect';
|
||||||
|
}
|
||||||
|
|
||||||
|
String get deathCombatLog {
|
||||||
|
if (isKoreanLocale) return '전투 기록';
|
||||||
|
if (isJapaneseLocale) return '戦闘ログ';
|
||||||
|
return 'Combat Log';
|
||||||
|
}
|
||||||
|
|
||||||
|
String deathKilledBy(String killerName) {
|
||||||
|
if (isKoreanLocale) return '$killerName에게 사망';
|
||||||
|
if (isJapaneseLocale) return '$killerNameに倒された';
|
||||||
|
return 'Killed by $killerName';
|
||||||
|
}
|
||||||
|
|
||||||
|
String get deathEnvironmentalHazard {
|
||||||
|
if (isKoreanLocale) return '환경 피해로 사망';
|
||||||
|
if (isJapaneseLocale) return '環境ダメージで死亡';
|
||||||
|
return 'Environmental hazard';
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// UI 일반 메시지
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
String get uiNoPotions {
|
||||||
|
if (isKoreanLocale) return '포션 없음';
|
||||||
|
if (isJapaneseLocale) return 'ポーションなし';
|
||||||
|
return 'No potions';
|
||||||
|
}
|
||||||
|
|
||||||
|
String get uiTapToContinue {
|
||||||
|
if (isKoreanLocale) return '탭하여 계속';
|
||||||
|
if (isJapaneseLocale) return 'タップして続行';
|
||||||
|
return 'Tap to continue';
|
||||||
|
}
|
||||||
|
|
||||||
|
String get uiNoSkills {
|
||||||
|
if (isKoreanLocale) return '습득한 스킬이 없습니다';
|
||||||
|
if (isJapaneseLocale) return 'スキルなし';
|
||||||
|
return 'No skills';
|
||||||
|
}
|
||||||
|
|
||||||
|
String get uiNoBonusStats {
|
||||||
|
if (isKoreanLocale) return '추가 스탯 없음';
|
||||||
|
if (isJapaneseLocale) return 'ボーナスステータスなし';
|
||||||
|
return 'No bonus stats';
|
||||||
|
}
|
||||||
|
|
||||||
|
String get uiNoActiveBuffs {
|
||||||
|
if (isKoreanLocale) return '활성 버프 없음';
|
||||||
|
if (isJapaneseLocale) return 'アクティブバフなし';
|
||||||
|
return 'No active buffs';
|
||||||
|
}
|
||||||
|
|
||||||
|
String get uiReady {
|
||||||
|
if (isKoreanLocale) return '준비';
|
||||||
|
if (isJapaneseLocale) return '準備完了';
|
||||||
|
return 'Ready';
|
||||||
|
}
|
||||||
|
|
||||||
|
String get uiPotions {
|
||||||
|
if (isKoreanLocale) return '포션';
|
||||||
|
if (isJapaneseLocale) return 'ポーション';
|
||||||
|
return 'Potions';
|
||||||
|
}
|
||||||
|
|
||||||
|
String get uiBuffs {
|
||||||
|
if (isKoreanLocale) return '버프';
|
||||||
|
if (isJapaneseLocale) return 'バフ';
|
||||||
|
return 'Buffs';
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// 스탯 약어
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
String get statStr {
|
||||||
|
if (isKoreanLocale) return '힘';
|
||||||
|
if (isJapaneseLocale) return '筋力';
|
||||||
|
return 'STR';
|
||||||
|
}
|
||||||
|
|
||||||
|
String get statCon {
|
||||||
|
if (isKoreanLocale) return '체력';
|
||||||
|
if (isJapaneseLocale) return '耐久';
|
||||||
|
return 'CON';
|
||||||
|
}
|
||||||
|
|
||||||
|
String get statDex {
|
||||||
|
if (isKoreanLocale) return '민첩';
|
||||||
|
if (isJapaneseLocale) return '敏捷';
|
||||||
|
return 'DEX';
|
||||||
|
}
|
||||||
|
|
||||||
|
String get statInt {
|
||||||
|
if (isKoreanLocale) return '지능';
|
||||||
|
if (isJapaneseLocale) return '知力';
|
||||||
|
return 'INT';
|
||||||
|
}
|
||||||
|
|
||||||
|
String get statWis {
|
||||||
|
if (isKoreanLocale) return '지혜';
|
||||||
|
if (isJapaneseLocale) return '精神';
|
||||||
|
return 'WIS';
|
||||||
|
}
|
||||||
|
|
||||||
|
String get statCha {
|
||||||
|
if (isKoreanLocale) return '매력';
|
||||||
|
if (isJapaneseLocale) return '魅力';
|
||||||
|
return 'CHA';
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// 패시브 능력 설명
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
String passiveHpBonus(int percent) {
|
||||||
|
if (isKoreanLocale) return 'HP +$percent%';
|
||||||
|
if (isJapaneseLocale) return 'HP +$percent%';
|
||||||
|
return 'HP +$percent%';
|
||||||
|
}
|
||||||
|
|
||||||
|
String passiveMpBonus(int percent) {
|
||||||
|
if (isKoreanLocale) return 'MP +$percent%';
|
||||||
|
if (isJapaneseLocale) return 'MP +$percent%';
|
||||||
|
return 'MP +$percent%';
|
||||||
|
}
|
||||||
|
|
||||||
|
String passivePhysicalBonus(int percent) {
|
||||||
|
if (isKoreanLocale) return '물리 공격 +$percent%';
|
||||||
|
if (isJapaneseLocale) return '物理攻撃 +$percent%';
|
||||||
|
return 'Physical +$percent%';
|
||||||
|
}
|
||||||
|
|
||||||
|
String passiveDefenseBonus(int percent) {
|
||||||
|
if (isKoreanLocale) return '방어력 +$percent%';
|
||||||
|
if (isJapaneseLocale) return '防御力 +$percent%';
|
||||||
|
return 'Defense +$percent%';
|
||||||
|
}
|
||||||
|
|
||||||
|
String passiveMagicBonus(int percent) {
|
||||||
|
if (isKoreanLocale) return '마법 공격 +$percent%';
|
||||||
|
if (isJapaneseLocale) return '魔法攻撃 +$percent%';
|
||||||
|
return 'Magic +$percent%';
|
||||||
|
}
|
||||||
|
|
||||||
|
String passiveEvasionBonus(int percent) {
|
||||||
|
if (isKoreanLocale) return '회피율 +$percent%';
|
||||||
|
if (isJapaneseLocale) return '回避率 +$percent%';
|
||||||
|
return 'Evasion +$percent%';
|
||||||
|
}
|
||||||
|
|
||||||
|
String passiveCritBonus(int percent) {
|
||||||
|
if (isKoreanLocale) return '크리티컬 +$percent%';
|
||||||
|
if (isJapaneseLocale) return 'クリティカル +$percent%';
|
||||||
|
return 'Critical +$percent%';
|
||||||
|
}
|
||||||
|
|
||||||
|
String passiveHpRegen(int percent) {
|
||||||
|
if (isKoreanLocale) return '전투 후 HP $percent% 회복';
|
||||||
|
if (isJapaneseLocale) return '戦闘後HP $percent%回復';
|
||||||
|
return 'Recover $percent% HP after combat';
|
||||||
|
}
|
||||||
|
|
||||||
|
String passiveMpRegen(int percent) {
|
||||||
|
if (isKoreanLocale) return '전투 후 MP $percent% 회복';
|
||||||
|
if (isJapaneseLocale) return '戦闘後MP $percent%回復';
|
||||||
|
return 'Recover $percent% MP after combat';
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// 전투 로그 메시지
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
String combatYouHit(String targetName, int damage) {
|
||||||
|
if (isKoreanLocale) return '$targetName에게 $damage 데미지';
|
||||||
|
if (isJapaneseLocale) return '$targetNameに$damageダメージ';
|
||||||
|
return 'You hit $targetName for $damage damage';
|
||||||
|
}
|
||||||
|
|
||||||
|
String combatYouEvaded(String targetName) {
|
||||||
|
if (isKoreanLocale) return '$targetName의 공격 회피!';
|
||||||
|
if (isJapaneseLocale) return '$targetNameの攻撃を回避!';
|
||||||
|
return 'You evaded $targetName\'s attack!';
|
||||||
|
}
|
||||||
|
|
||||||
|
String combatEvadedAttackFrom(String targetName) {
|
||||||
|
if (isKoreanLocale) return '$targetName의 공격 회피';
|
||||||
|
if (isJapaneseLocale) return '$targetNameの攻撃を回避';
|
||||||
|
return 'Evaded attack from $targetName';
|
||||||
|
}
|
||||||
|
|
||||||
|
String combatHealedFor(int amount) {
|
||||||
|
if (isKoreanLocale) return 'HP $amount 회복';
|
||||||
|
if (isJapaneseLocale) return 'HP $amount回復';
|
||||||
|
return 'Healed for $amount HP';
|
||||||
|
}
|
||||||
|
|
||||||
|
String combatCritical(int damage, String targetName) {
|
||||||
|
if (isKoreanLocale) return '크리티컬! $targetName에게 $damage 데미지!';
|
||||||
|
if (isJapaneseLocale) return 'クリティカル! $targetNameに$damageダメージ!';
|
||||||
|
return 'CRITICAL! $damage damage to $targetName!';
|
||||||
|
}
|
||||||
|
|
||||||
|
String combatMonsterHitsYou(String monsterName, int damage) {
|
||||||
|
if (isKoreanLocale) return '$monsterName이(가) $damage 데미지';
|
||||||
|
if (isJapaneseLocale) return '$monsterNameが$damageダメージを与えた';
|
||||||
|
return '$monsterName hits you for $damage damage';
|
||||||
|
}
|
||||||
|
|
||||||
|
String combatMonsterEvaded(String monsterName) {
|
||||||
|
if (isKoreanLocale) return '$monsterName이(가) 공격 회피!';
|
||||||
|
if (isJapaneseLocale) return '$monsterNameが攻撃を回避!';
|
||||||
|
return '$monsterName evaded your attack!';
|
||||||
|
}
|
||||||
|
|
||||||
|
String combatBlocked(int damage) {
|
||||||
|
if (isKoreanLocale) return '방어! $damage 데미지로 감소';
|
||||||
|
if (isJapaneseLocale) return 'ブロック! $damageダメージに軽減';
|
||||||
|
return 'Blocked! Reduced to $damage damage';
|
||||||
|
}
|
||||||
|
|
||||||
|
String combatParried(int damage) {
|
||||||
|
if (isKoreanLocale) return '패리! $damage 데미지로 감소';
|
||||||
|
if (isJapaneseLocale) return 'パリィ! $damageダメージに軽減';
|
||||||
|
return 'Parried! Reduced to $damage damage';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 스킬 관련 전투 메시지
|
||||||
|
String combatSkillCritical(String skillName, int damage) {
|
||||||
|
if (isKoreanLocale) return '크리티컬 $skillName! $damage 데미지!';
|
||||||
|
if (isJapaneseLocale) return 'クリティカル$skillName! $damageダメージ!';
|
||||||
|
return 'CRITICAL $skillName! $damage damage!';
|
||||||
|
}
|
||||||
|
|
||||||
|
String combatSkillDamage(String skillName, int damage) {
|
||||||
|
if (isKoreanLocale) return '$skillName: $damage 데미지';
|
||||||
|
if (isJapaneseLocale) return '$skillName: $damageダメージ';
|
||||||
|
return '$skillName: $damage damage';
|
||||||
|
}
|
||||||
|
|
||||||
|
String combatSkillHeal(String skillName, int amount) {
|
||||||
|
if (isKoreanLocale) return '$skillName: +$amount HP';
|
||||||
|
if (isJapaneseLocale) return '$skillName: +$amount HP';
|
||||||
|
return '$skillName: +$amount HP';
|
||||||
|
}
|
||||||
|
|
||||||
|
String get uiHeal {
|
||||||
|
if (isKoreanLocale) return '힐';
|
||||||
|
if (isJapaneseLocale) return 'ヒール';
|
||||||
|
return 'Heal';
|
||||||
|
}
|
||||||
|
|
||||||
|
String combatBuffActivated(String skillName) {
|
||||||
|
if (isKoreanLocale) return '$skillName 발동!';
|
||||||
|
if (isJapaneseLocale) return '$skillName 発動!';
|
||||||
|
return '$skillName activated!';
|
||||||
|
}
|
||||||
|
|
||||||
|
String combatDotTick(String skillName, int damage) {
|
||||||
|
if (isKoreanLocale) return '$skillName: $damage 지속 데미지';
|
||||||
|
if (isJapaneseLocale) return '$skillName: $damage 継続ダメージ';
|
||||||
|
return '$skillName ticks for $damage damage';
|
||||||
|
}
|
||||||
|
|
||||||
|
String combatPotionUsed(String potionName, int amount, String statName) {
|
||||||
|
if (isKoreanLocale) return '$potionName: +$amount $statName';
|
||||||
|
if (isJapaneseLocale) return '$potionName: +$amount $statName';
|
||||||
|
return '$potionName: +$amount $statName';
|
||||||
|
}
|
||||||
|
|
||||||
|
String combatPotionDrop(String potionName) {
|
||||||
|
if (isKoreanLocale) return '획득: $potionName';
|
||||||
|
if (isJapaneseLocale) return '獲得: $potionName';
|
||||||
|
return 'Dropped: $potionName';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 사망 화면 전투 로그 (death overlay)
|
||||||
|
String combatBlockedAttack(String monsterName, int reducedDamage) {
|
||||||
|
if (isKoreanLocale) return '$monsterName의 공격 방어 ($reducedDamage 감소)';
|
||||||
|
if (isJapaneseLocale) return '$monsterNameの攻撃を防御 ($reducedDamage軽減)';
|
||||||
|
return 'Blocked $monsterName\'s attack ($reducedDamage reduced)';
|
||||||
|
}
|
||||||
|
|
||||||
|
String combatParriedAttack(String monsterName, int reducedDamage) {
|
||||||
|
if (isKoreanLocale) return '$monsterName의 공격 패리 ($reducedDamage 감소)';
|
||||||
|
if (isJapaneseLocale) return '$monsterNameの攻撃をパリィ ($reducedDamage軽減)';
|
||||||
|
return 'Parried $monsterName\'s attack ($reducedDamage reduced)';
|
||||||
|
}
|
||||||
|
|
||||||
|
String get deathSelfInflicted {
|
||||||
|
if (isKoreanLocale) return '자해 데미지로 사망';
|
||||||
|
if (isJapaneseLocale) return '自傷ダメージで死亡';
|
||||||
|
return 'Self-inflicted damage';
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// 퀘스트 캡션
|
// 퀘스트 캡션
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -572,7 +928,8 @@ String translateImpressiveTitle(String englishName) {
|
|||||||
/// 특수 아이템 이름 번역
|
/// 특수 아이템 이름 번역
|
||||||
String translateSpecial(String englishName) {
|
String translateSpecial(String englishName) {
|
||||||
if (isKoreanLocale) return specialTranslationsKo[englishName] ?? englishName;
|
if (isKoreanLocale) return specialTranslationsKo[englishName] ?? englishName;
|
||||||
if (isJapaneseLocale) return specialTranslationsJa[englishName] ?? englishName;
|
if (isJapaneseLocale)
|
||||||
|
return specialTranslationsJa[englishName] ?? englishName;
|
||||||
return englishName;
|
return englishName;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -627,14 +984,16 @@ String translateBoringItem(String englishName) {
|
|||||||
/// 예: "Golden Iterator" → "황금 이터레이터" / "黄金のイテレーター"
|
/// 예: "Golden Iterator" → "황금 이터레이터" / "黄金のイテレーター"
|
||||||
String translateInterestingItem(String attrib, String special) {
|
String translateInterestingItem(String attrib, String special) {
|
||||||
if (isKoreanLocale) {
|
if (isKoreanLocale) {
|
||||||
final translatedAttrib = itemAttribTranslationsKo[attrib] ??
|
final translatedAttrib =
|
||||||
|
itemAttribTranslationsKo[attrib] ??
|
||||||
additionalItemAttribTranslationsKo[attrib] ??
|
additionalItemAttribTranslationsKo[attrib] ??
|
||||||
attrib;
|
attrib;
|
||||||
final translatedSpecial = specialTranslationsKo[special] ?? special;
|
final translatedSpecial = specialTranslationsKo[special] ?? special;
|
||||||
return '$translatedAttrib $translatedSpecial';
|
return '$translatedAttrib $translatedSpecial';
|
||||||
}
|
}
|
||||||
if (isJapaneseLocale) {
|
if (isJapaneseLocale) {
|
||||||
final translatedAttrib = itemAttribTranslationsJa[attrib] ??
|
final translatedAttrib =
|
||||||
|
itemAttribTranslationsJa[attrib] ??
|
||||||
additionalItemAttribTranslationsJa[attrib] ??
|
additionalItemAttribTranslationsJa[attrib] ??
|
||||||
attrib;
|
attrib;
|
||||||
final translatedSpecial = specialTranslationsJa[special] ?? special;
|
final translatedSpecial = specialTranslationsJa[special] ?? special;
|
||||||
@@ -643,48 +1002,419 @@ String translateInterestingItem(String attrib, String special) {
|
|||||||
return '$attrib $special';
|
return '$attrib $special';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 아이템 이름 문자열 전체 번역 (판매, 로그 등에 사용)
|
||||||
|
/// 예: "Golden Iterator of Compilation" → "컴파일의 황금 이터레이터"
|
||||||
|
/// 예: "Syntax Error fragment" → "구문 오류 조각"
|
||||||
|
String translateItemNameL10n(String itemString) {
|
||||||
|
if (!isKoreanLocale && !isJapaneseLocale) return itemString;
|
||||||
|
if (itemString.isEmpty) return itemString;
|
||||||
|
|
||||||
|
// 1. specialItem 형식: "Attrib Special of ItemOf"
|
||||||
|
final ofMatch = RegExp(r'^(.+)\s+of\s+(.+)$').firstMatch(itemString);
|
||||||
|
if (ofMatch != null) {
|
||||||
|
final beforeOf = ofMatch.group(1)!;
|
||||||
|
final afterOf = ofMatch.group(2)!;
|
||||||
|
|
||||||
|
// afterOf가 itemOfs 목록에 있는지 확인
|
||||||
|
final itemOfKo =
|
||||||
|
itemOfsTranslationsKo[afterOf] ??
|
||||||
|
additionalItemOfsTranslationsKo[afterOf];
|
||||||
|
final itemOfJa =
|
||||||
|
itemOfsTranslationsJa[afterOf] ??
|
||||||
|
additionalItemOfsTranslationsJa[afterOf];
|
||||||
|
|
||||||
|
if (itemOfKo != null || itemOfJa != null) {
|
||||||
|
// beforeOf를 interestingItem으로 분리 (attrib + special)
|
||||||
|
final beforeWords = beforeOf.split(' ');
|
||||||
|
if (beforeWords.length >= 2) {
|
||||||
|
final attrib = beforeWords[0];
|
||||||
|
final special = beforeWords.sublist(1).join(' ');
|
||||||
|
final translatedBefore = translateInterestingItem(attrib, special);
|
||||||
|
if (isKoreanLocale) {
|
||||||
|
return '$itemOfKo의 $translatedBefore';
|
||||||
|
} else if (isJapaneseLocale) {
|
||||||
|
return '$itemOfJaの$translatedBefore';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 몬스터 드롭 형식: "{monster} {drop}" (예: "syntax error fragment")
|
||||||
|
final words = itemString.split(' ');
|
||||||
|
if (words.length >= 2) {
|
||||||
|
// 마지막 단어가 드롭 아이템인지 확인
|
||||||
|
final lastWord = words.last.toLowerCase();
|
||||||
|
final dropKo =
|
||||||
|
dropItemTranslationsKo[lastWord] ??
|
||||||
|
additionalDropTranslationsKo[lastWord];
|
||||||
|
final dropJa =
|
||||||
|
dropItemTranslationsJa[lastWord] ??
|
||||||
|
additionalDropTranslationsJa[lastWord];
|
||||||
|
|
||||||
|
if (dropKo != null || dropJa != null) {
|
||||||
|
// 앞 부분은 몬스터 이름
|
||||||
|
final monsterPart = words.sublist(0, words.length - 1).join(' ');
|
||||||
|
final translatedMonster = translateMonster(monsterPart);
|
||||||
|
if (isKoreanLocale && dropKo != null) {
|
||||||
|
return '$translatedMonster $dropKo';
|
||||||
|
} else if (isJapaneseLocale && dropJa != null) {
|
||||||
|
return '$translatedMonsterの$dropJa';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. interestingItem 형식: "Attrib Special" (2단어)
|
||||||
|
if (words.length == 2) {
|
||||||
|
return translateInterestingItem(words[0], words[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 단일 단어 - boringItem 번역 시도
|
||||||
|
return translateBoringItem(itemString);
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// 스토리/시네마틱 번역 함수 (Story/Cinematic Translation Functions)
|
// 스토리/시네마틱 번역 함수 (Story/Cinematic Translation Functions)
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
/// Act 제목 번역
|
/// Act 제목 번역
|
||||||
String translateActTitle(String englishTitle) {
|
String translateActTitle(String englishTitle) {
|
||||||
if (isKoreanLocale) return actTitleTranslationsKo[englishTitle] ?? englishTitle;
|
if (isKoreanLocale)
|
||||||
if (isJapaneseLocale) return actTitleTranslationsJa[englishTitle] ?? englishTitle;
|
return actTitleTranslationsKo[englishTitle] ?? englishTitle;
|
||||||
|
if (isJapaneseLocale)
|
||||||
|
return actTitleTranslationsJa[englishTitle] ?? englishTitle;
|
||||||
return englishTitle;
|
return englishTitle;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Act 보스 이름 번역
|
/// Act 보스 이름 번역
|
||||||
String translateActBoss(String englishBoss) {
|
String translateActBoss(String englishBoss) {
|
||||||
if (isKoreanLocale) return actBossTranslationsKo[englishBoss] ?? englishBoss;
|
if (isKoreanLocale) return actBossTranslationsKo[englishBoss] ?? englishBoss;
|
||||||
if (isJapaneseLocale) return actBossTranslationsJa[englishBoss] ?? englishBoss;
|
if (isJapaneseLocale)
|
||||||
|
return actBossTranslationsJa[englishBoss] ?? englishBoss;
|
||||||
return englishBoss;
|
return englishBoss;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Act 퀘스트 번역
|
/// Act 퀘스트 번역
|
||||||
String translateActQuest(String englishQuest) {
|
String translateActQuest(String englishQuest) {
|
||||||
if (isKoreanLocale) return actQuestTranslationsKo[englishQuest] ?? englishQuest;
|
if (isKoreanLocale)
|
||||||
if (isJapaneseLocale) return actQuestTranslationsJa[englishQuest] ?? englishQuest;
|
return actQuestTranslationsKo[englishQuest] ?? englishQuest;
|
||||||
|
if (isJapaneseLocale)
|
||||||
|
return actQuestTranslationsJa[englishQuest] ?? englishQuest;
|
||||||
return englishQuest;
|
return englishQuest;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 시네마틱 텍스트 번역
|
/// 시네마틱 텍스트 번역
|
||||||
String translateCinematic(String englishText) {
|
String translateCinematic(String englishText) {
|
||||||
if (isKoreanLocale) return cinematicTranslationsKo[englishText] ?? englishText;
|
if (isKoreanLocale)
|
||||||
if (isJapaneseLocale) return cinematicTranslationsJa[englishText] ?? englishText;
|
return cinematicTranslationsKo[englishText] ?? englishText;
|
||||||
|
if (isJapaneseLocale)
|
||||||
|
return cinematicTranslationsJa[englishText] ?? englishText;
|
||||||
return englishText;
|
return englishText;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 지역 이름 번역
|
/// 지역 이름 번역
|
||||||
String translateLocation(String englishLocation) {
|
String translateLocation(String englishLocation) {
|
||||||
if (isKoreanLocale) return locationTranslationsKo[englishLocation] ?? englishLocation;
|
if (isKoreanLocale)
|
||||||
if (isJapaneseLocale) return locationTranslationsJa[englishLocation] ?? englishLocation;
|
return locationTranslationsKo[englishLocation] ?? englishLocation;
|
||||||
|
if (isJapaneseLocale)
|
||||||
|
return locationTranslationsJa[englishLocation] ?? englishLocation;
|
||||||
return englishLocation;
|
return englishLocation;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 세력/조직 이름 번역
|
/// 세력/조직 이름 번역
|
||||||
String translateFaction(String englishFaction) {
|
String translateFaction(String englishFaction) {
|
||||||
if (isKoreanLocale) return factionTranslationsKo[englishFaction] ?? englishFaction;
|
if (isKoreanLocale)
|
||||||
if (isJapaneseLocale) return factionTranslationsJa[englishFaction] ?? englishFaction;
|
return factionTranslationsKo[englishFaction] ?? englishFaction;
|
||||||
|
if (isJapaneseLocale)
|
||||||
|
return factionTranslationsJa[englishFaction] ?? englishFaction;
|
||||||
return englishFaction;
|
return englishFaction;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// 프론트 화면 텍스트
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
String get uiHallOfFame {
|
||||||
|
if (isKoreanLocale) return '명예의 전당';
|
||||||
|
if (isJapaneseLocale) return '栄誉の殿堂';
|
||||||
|
return 'Hall of Fame';
|
||||||
|
}
|
||||||
|
|
||||||
|
String get frontDescription {
|
||||||
|
if (isKoreanLocale) return 'Flutter로 재구축된 오프라인 Progress Quest (PQ 6.4)';
|
||||||
|
if (isJapaneseLocale) return 'Flutterで再構築されたオフラインProgress Quest (PQ 6.4)';
|
||||||
|
return 'Offline Progress Quest (PQ 6.4) rebuilt with Flutter.';
|
||||||
|
}
|
||||||
|
|
||||||
|
String get frontTodayFocus {
|
||||||
|
if (isKoreanLocale) return '오늘의 중점';
|
||||||
|
if (isJapaneseLocale) return '今日のフォーカス';
|
||||||
|
return "Today's focus";
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// 명예의 전당 화면 텍스트
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
String get hofNoHeroes {
|
||||||
|
if (isKoreanLocale) return '영웅이 아직 없습니다';
|
||||||
|
if (isJapaneseLocale) return 'まだ英雄がいません';
|
||||||
|
return 'No heroes yet';
|
||||||
|
}
|
||||||
|
|
||||||
|
String get hofDefeatGlitchGod {
|
||||||
|
if (isKoreanLocale) return '글리치 신을 처치하여 전설을 남기세요!';
|
||||||
|
if (isJapaneseLocale) return 'グリッチゴッドを倒して伝説を刻もう!';
|
||||||
|
return 'Defeat the Glitch God to enshrine your legend!';
|
||||||
|
}
|
||||||
|
|
||||||
|
String get hofVictory {
|
||||||
|
if (isKoreanLocale) return '승리!';
|
||||||
|
if (isJapaneseLocale) return '勝利!';
|
||||||
|
return 'VICTORY!';
|
||||||
|
}
|
||||||
|
|
||||||
|
String get hofDefeatedGlitchGod {
|
||||||
|
if (isKoreanLocale) return '글리치 신을 처치했습니다!';
|
||||||
|
if (isJapaneseLocale) return 'グリッチゴッドを倒しました!';
|
||||||
|
return 'You have defeated the Glitch God!';
|
||||||
|
}
|
||||||
|
|
||||||
|
String get hofLegendEnshrined {
|
||||||
|
if (isKoreanLocale) return '당신의 전설이 명예의 전당에 기록되었습니다!';
|
||||||
|
if (isJapaneseLocale) return 'あなたの伝説が栄誉の殿堂に刻まれました!';
|
||||||
|
return 'Your legend has been enshrined in the Hall of Fame!';
|
||||||
|
}
|
||||||
|
|
||||||
|
String get hofViewHallOfFame {
|
||||||
|
if (isKoreanLocale) return '명예의 전당 보기';
|
||||||
|
if (isJapaneseLocale) return '栄誉の殿堂を見る';
|
||||||
|
return 'View Hall of Fame';
|
||||||
|
}
|
||||||
|
|
||||||
|
String get hofNewGame {
|
||||||
|
if (isKoreanLocale) return '새 게임';
|
||||||
|
if (isJapaneseLocale) return '新しいゲーム';
|
||||||
|
return 'New Game';
|
||||||
|
}
|
||||||
|
|
||||||
|
String get hofLevel {
|
||||||
|
if (isKoreanLocale) return '레벨';
|
||||||
|
if (isJapaneseLocale) return 'レベル';
|
||||||
|
return 'Level';
|
||||||
|
}
|
||||||
|
|
||||||
|
String get hofTime {
|
||||||
|
if (isKoreanLocale) return '시간';
|
||||||
|
if (isJapaneseLocale) return '時間';
|
||||||
|
return 'Time';
|
||||||
|
}
|
||||||
|
|
||||||
|
String get hofDeaths {
|
||||||
|
if (isKoreanLocale) return '사망';
|
||||||
|
if (isJapaneseLocale) return '死亡';
|
||||||
|
return 'Deaths';
|
||||||
|
}
|
||||||
|
|
||||||
|
String get hofQuests {
|
||||||
|
if (isKoreanLocale) return '퀘스트';
|
||||||
|
if (isJapaneseLocale) return 'クエスト';
|
||||||
|
return 'Quests';
|
||||||
|
}
|
||||||
|
|
||||||
|
String uiLevel(int level) {
|
||||||
|
if (isKoreanLocale) return 'Lv.$level';
|
||||||
|
if (isJapaneseLocale) return 'Lv.$level';
|
||||||
|
return 'Lv.$level';
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// 시네마틱 뷰 텍스트
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
String get uiSkip {
|
||||||
|
if (isKoreanLocale) return '건너뛰기';
|
||||||
|
if (isJapaneseLocale) return 'スキップ';
|
||||||
|
return 'SKIP';
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// 게임 플레이 화면 텍스트
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
String get uiLevelUp {
|
||||||
|
if (isKoreanLocale) return '레벨 업!';
|
||||||
|
if (isJapaneseLocale) return 'レベルアップ!';
|
||||||
|
return 'Level Up!';
|
||||||
|
}
|
||||||
|
|
||||||
|
String uiQuestComplete(String questName) {
|
||||||
|
if (isKoreanLocale) return '퀘스트 완료: $questName';
|
||||||
|
if (isJapaneseLocale) return 'クエスト完了: $questName';
|
||||||
|
return 'Quest Complete: $questName';
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// 장비 패널 텍스트
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
String get uiEquipmentScore {
|
||||||
|
if (isKoreanLocale) return '장비 점수';
|
||||||
|
if (isJapaneseLocale) return '装備スコア';
|
||||||
|
return 'Equipment Score';
|
||||||
|
}
|
||||||
|
|
||||||
|
String get uiEmpty {
|
||||||
|
if (isKoreanLocale) return '(비어있음)';
|
||||||
|
if (isJapaneseLocale) return '(空)';
|
||||||
|
return '(empty)';
|
||||||
|
}
|
||||||
|
|
||||||
|
String uiWeight(int weight) {
|
||||||
|
if (isKoreanLocale) return '무게 $weight';
|
||||||
|
if (isJapaneseLocale) return '重量 $weight';
|
||||||
|
return 'Wt.$weight';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 장비 슬롯 이름
|
||||||
|
String get slotWeapon {
|
||||||
|
if (isKoreanLocale) return '무기';
|
||||||
|
if (isJapaneseLocale) return '武器';
|
||||||
|
return 'Weapon';
|
||||||
|
}
|
||||||
|
|
||||||
|
String get slotShield {
|
||||||
|
if (isKoreanLocale) return '방패';
|
||||||
|
if (isJapaneseLocale) return '盾';
|
||||||
|
return 'Shield';
|
||||||
|
}
|
||||||
|
|
||||||
|
String get slotHelm {
|
||||||
|
if (isKoreanLocale) return '투구';
|
||||||
|
if (isJapaneseLocale) return '兜';
|
||||||
|
return 'Helm';
|
||||||
|
}
|
||||||
|
|
||||||
|
String get slotHauberk {
|
||||||
|
if (isKoreanLocale) return '갑옷';
|
||||||
|
if (isJapaneseLocale) return '鎧';
|
||||||
|
return 'Hauberk';
|
||||||
|
}
|
||||||
|
|
||||||
|
String get slotBrassairts {
|
||||||
|
if (isKoreanLocale) return '상완갑';
|
||||||
|
if (isJapaneseLocale) return '上腕甲';
|
||||||
|
return 'Brassairts';
|
||||||
|
}
|
||||||
|
|
||||||
|
String get slotVambraces {
|
||||||
|
if (isKoreanLocale) return '전완갑';
|
||||||
|
if (isJapaneseLocale) return '前腕甲';
|
||||||
|
return 'Vambraces';
|
||||||
|
}
|
||||||
|
|
||||||
|
String get slotGauntlets {
|
||||||
|
if (isKoreanLocale) return '건틀릿';
|
||||||
|
if (isJapaneseLocale) return 'ガントレット';
|
||||||
|
return 'Gauntlets';
|
||||||
|
}
|
||||||
|
|
||||||
|
String get slotGambeson {
|
||||||
|
if (isKoreanLocale) return '패딩';
|
||||||
|
if (isJapaneseLocale) return 'パッデッドアーマー';
|
||||||
|
return 'Gambeson';
|
||||||
|
}
|
||||||
|
|
||||||
|
String get slotCuisses {
|
||||||
|
if (isKoreanLocale) return '허벅지갑';
|
||||||
|
if (isJapaneseLocale) return '腿当て';
|
||||||
|
return 'Cuisses';
|
||||||
|
}
|
||||||
|
|
||||||
|
String get slotGreaves {
|
||||||
|
if (isKoreanLocale) return '정강이갑';
|
||||||
|
if (isJapaneseLocale) return '脛当て';
|
||||||
|
return 'Greaves';
|
||||||
|
}
|
||||||
|
|
||||||
|
String get slotSollerets {
|
||||||
|
if (isKoreanLocale) return '철제부츠';
|
||||||
|
if (isJapaneseLocale) return '鉄靴';
|
||||||
|
return 'Sollerets';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 스탯 약어 (장비용)
|
||||||
|
String get statAtk {
|
||||||
|
if (isKoreanLocale) return '공격';
|
||||||
|
if (isJapaneseLocale) return '攻撃';
|
||||||
|
return 'ATK';
|
||||||
|
}
|
||||||
|
|
||||||
|
String get statMAtk {
|
||||||
|
if (isKoreanLocale) return '마공';
|
||||||
|
if (isJapaneseLocale) return '魔攻';
|
||||||
|
return 'MATK';
|
||||||
|
}
|
||||||
|
|
||||||
|
String get statCri {
|
||||||
|
if (isKoreanLocale) return '치명';
|
||||||
|
if (isJapaneseLocale) return 'クリ';
|
||||||
|
return 'CRI';
|
||||||
|
}
|
||||||
|
|
||||||
|
String get statParry {
|
||||||
|
if (isKoreanLocale) return '패리';
|
||||||
|
if (isJapaneseLocale) return 'パリィ';
|
||||||
|
return 'PARRY';
|
||||||
|
}
|
||||||
|
|
||||||
|
String get statDef {
|
||||||
|
if (isKoreanLocale) return '방어';
|
||||||
|
if (isJapaneseLocale) return '防御';
|
||||||
|
return 'DEF';
|
||||||
|
}
|
||||||
|
|
||||||
|
String get statMDef {
|
||||||
|
if (isKoreanLocale) return '마방';
|
||||||
|
if (isJapaneseLocale) return '魔防';
|
||||||
|
return 'MDEF';
|
||||||
|
}
|
||||||
|
|
||||||
|
String get statBlock {
|
||||||
|
if (isKoreanLocale) return '블록';
|
||||||
|
if (isJapaneseLocale) return 'ブロック';
|
||||||
|
return 'BLOCK';
|
||||||
|
}
|
||||||
|
|
||||||
|
String get statEva {
|
||||||
|
if (isKoreanLocale) return '회피';
|
||||||
|
if (isJapaneseLocale) return '回避';
|
||||||
|
return 'EVA';
|
||||||
|
}
|
||||||
|
|
||||||
|
String get statHp {
|
||||||
|
if (isKoreanLocale) return 'HP';
|
||||||
|
if (isJapaneseLocale) return 'HP';
|
||||||
|
return 'HP';
|
||||||
|
}
|
||||||
|
|
||||||
|
String get statMp {
|
||||||
|
if (isKoreanLocale) return 'MP';
|
||||||
|
if (isJapaneseLocale) return 'MP';
|
||||||
|
return 'MP';
|
||||||
|
}
|
||||||
|
|
||||||
|
String get statSpeed {
|
||||||
|
if (isKoreanLocale) return '속도';
|
||||||
|
if (isJapaneseLocale) return '速度';
|
||||||
|
return 'SPEED';
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// 스킬 패널 텍스트
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
String get uiDot {
|
||||||
|
if (isKoreanLocale) return '지속';
|
||||||
|
if (isJapaneseLocale) return 'DOT';
|
||||||
|
return 'DOT';
|
||||||
|
}
|
||||||
|
|||||||
@@ -1175,7 +1175,8 @@ const Map<String, String> cinematicTranslationsJa = {
|
|||||||
// Act I: 覚醒
|
// Act I: 覚醒
|
||||||
'=== ACT I: AWAKENING ===': '=== 第1幕: 覚醒 ===',
|
'=== ACT I: AWAKENING ===': '=== 第1幕: 覚醒 ===',
|
||||||
'You have proven yourself against the lesser bugs.': '下級バグとの戦いで実力を証明した。',
|
'You have proven yourself against the lesser bugs.': '下級バグとの戦いで実力を証明した。',
|
||||||
'The Debugger Knights take notice of your potential.': 'デバッガー騎士団があなたの可能性に注目する。',
|
'The Debugger Knights take notice of your potential.':
|
||||||
|
'デバッガー騎士団があなたの可能性に注目する。',
|
||||||
'But a greater threat lurks in the Bug Nest...': 'しかしより大きな脅威がバグの巣に潜んでいる…',
|
'But a greater threat lurks in the Bug Nest...': 'しかしより大きな脅威がバグの巣に潜んでいる…',
|
||||||
'The Syntax Error Dragon awaits.': '構文エラードラゴンが待ち構えている。',
|
'The Syntax Error Dragon awaits.': '構文エラードラゴンが待ち構えている。',
|
||||||
|
|
||||||
@@ -1217,7 +1218,8 @@ const Map<String, String> cinematicTranslationsJa = {
|
|||||||
'The Glitch God falls. The corruption fades.': 'グリッチゴッドが倒れた。破損が消えていく。',
|
'The Glitch God falls. The corruption fades.': 'グリッチゴッドが倒れた。破損が消えていく。',
|
||||||
'System Reboot initiated...': 'システム再起動開始…',
|
'System Reboot initiated...': 'システム再起動開始…',
|
||||||
'Peace returns to the Digital Realm.': 'デジタル世界に平和が戻った。',
|
'Peace returns to the Digital Realm.': 'デジタル世界に平和が戻った。',
|
||||||
'Your legend will be compiled into the eternal logs.': 'あなたの伝説は永遠のログにコンパイルされるだろう。',
|
'Your legend will be compiled into the eternal logs.':
|
||||||
|
'あなたの伝説は永遠のログにコンパイルされるだろう。',
|
||||||
'THE END': '完',
|
'THE END': '完',
|
||||||
'...or is it?': '…本当に?',
|
'...or is it?': '…本当に?',
|
||||||
};
|
};
|
||||||
@@ -1485,37 +1487,37 @@ const Map<String, String> additionalItemOfsTranslationsJa = {
|
|||||||
|
|
||||||
/// すべてのモンスター翻訳を統合して返す
|
/// すべてのモンスター翻訳を統合して返す
|
||||||
Map<String, String> get allMonsterTranslationsJa => {
|
Map<String, String> get allMonsterTranslationsJa => {
|
||||||
...monsterTranslationsJa,
|
...monsterTranslationsJa,
|
||||||
...advancedMonsterTranslationsJa,
|
...advancedMonsterTranslationsJa,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// すべてのアイテム属性翻訳を統合して返す
|
/// すべてのアイテム属性翻訳を統合して返す
|
||||||
Map<String, String> get allItemAttribTranslationsJa => {
|
Map<String, String> get allItemAttribTranslationsJa => {
|
||||||
...itemAttribTranslationsJa,
|
...itemAttribTranslationsJa,
|
||||||
...additionalItemAttribTranslationsJa,
|
...additionalItemAttribTranslationsJa,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// すべてのアイテム接尾辞(~の)翻訳を統合して返す
|
/// すべてのアイテム接尾辞(~の)翻訳を統合して返す
|
||||||
Map<String, String> get allItemOfsTranslationsJa => {
|
Map<String, String> get allItemOfsTranslationsJa => {
|
||||||
...itemOfsTranslationsJa,
|
...itemOfsTranslationsJa,
|
||||||
...additionalItemOfsTranslationsJa,
|
...additionalItemOfsTranslationsJa,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// すべてのドロップアイテム翻訳を統合して返す
|
/// すべてのドロップアイテム翻訳を統合して返す
|
||||||
Map<String, String> get allDropTranslationsJa => {
|
Map<String, String> get allDropTranslationsJa => {
|
||||||
...boringItemTranslationsJa,
|
...boringItemTranslationsJa,
|
||||||
...dropItemTranslationsJa,
|
...dropItemTranslationsJa,
|
||||||
...additionalDropTranslationsJa,
|
...additionalDropTranslationsJa,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// すべての鎧翻訳を統合して返す
|
/// すべての鎧翻訳を統合して返す
|
||||||
Map<String, String> get allArmorTranslationsJa => {
|
Map<String, String> get allArmorTranslationsJa => {
|
||||||
...armorTranslationsJa,
|
...armorTranslationsJa,
|
||||||
...additionalArmorTranslationsJa,
|
...additionalArmorTranslationsJa,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// すべての盾翻訳を統合して返す
|
/// すべての盾翻訳を統合して返す
|
||||||
Map<String, String> get allShieldTranslationsJa => {
|
Map<String, String> get allShieldTranslationsJa => {
|
||||||
...shieldTranslationsJa,
|
...shieldTranslationsJa,
|
||||||
...additionalShieldTranslationsJa,
|
...additionalShieldTranslationsJa,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1165,16 +1165,13 @@ const Map<String, String> actQuestTranslationsKo = {
|
|||||||
/// 시네마틱 텍스트 한국어 번역
|
/// 시네마틱 텍스트 한국어 번역
|
||||||
const Map<String, String> cinematicTranslationsKo = {
|
const Map<String, String> cinematicTranslationsKo = {
|
||||||
// 프롤로그
|
// 프롤로그
|
||||||
'In the beginning, there was only the Void...':
|
'In the beginning, there was only the Void...': '태초에, 오직 공허(Void)만이 존재했다...',
|
||||||
'태초에, 오직 공허(Void)만이 존재했다...',
|
|
||||||
'Then came the First Commit, and Light filled the Codebase.':
|
'Then came the First Commit, and Light filled the Codebase.':
|
||||||
'그리고 첫 번째 커밋이 도래하여, 빛이 코드베이스를 가득 채웠다.',
|
'그리고 첫 번째 커밋이 도래하여, 빛이 코드베이스를 가득 채웠다.',
|
||||||
'The Code God spoke: "Let there be Functions."':
|
'The Code God spoke: "Let there be Functions."': '코드의 신이 말씀하셨다: "함수가 있으라."',
|
||||||
'코드의 신이 말씀하셨다: "함수가 있으라."',
|
|
||||||
'And so the Digital Realm was born...': '그리하여 디지털 세계가 탄생하였다...',
|
'And so the Digital Realm was born...': '그리하여 디지털 세계가 탄생하였다...',
|
||||||
'But from the shadows emerged the Glitch.': '그러나 어둠 속에서 글리치가 출현했다.',
|
'But from the shadows emerged the Glitch.': '그러나 어둠 속에서 글리치가 출현했다.',
|
||||||
'Now, a new hero awakens to defend the Code.':
|
'Now, a new hero awakens to defend the Code.': '이제, 코드를 수호할 새로운 영웅이 깨어난다.',
|
||||||
'이제, 코드를 수호할 새로운 영웅이 깨어난다.',
|
|
||||||
'Your journey begins...': '당신의 여정이 시작된다...',
|
'Your journey begins...': '당신의 여정이 시작된다...',
|
||||||
|
|
||||||
// Act I: 각성
|
// Act I: 각성
|
||||||
@@ -1191,11 +1188,9 @@ const Map<String, String> cinematicTranslationsKo = {
|
|||||||
'=== ACT II: GROWTH ===': '=== 제2막: 성장 ===',
|
'=== ACT II: GROWTH ===': '=== 제2막: 성장 ===',
|
||||||
'With the Dragon slain, you join the Debugger Knights.':
|
'With the Dragon slain, you join the Debugger Knights.':
|
||||||
'드래곤을 처치하고, 당신은 디버거 기사단에 입단한다.',
|
'드래곤을 처치하고, 당신은 디버거 기사단에 입단한다.',
|
||||||
'The Corrupted Network spreads its infection...':
|
'The Corrupted Network spreads its infection...': '손상된 네트워크가 감염을 퍼뜨리고 있다...',
|
||||||
'손상된 네트워크가 감염을 퍼뜨리고 있다...',
|
|
||||||
'A traitor among the Knights is revealed!': '기사단 내 배신자가 드러났다!',
|
'A traitor among the Knights is revealed!': '기사단 내 배신자가 드러났다!',
|
||||||
'The Memory Leak Hydra threatens all data.':
|
'The Memory Leak Hydra threatens all data.': '메모리 누수 히드라가 모든 데이터를 위협한다.',
|
||||||
'메모리 누수 히드라가 모든 데이터를 위협한다.',
|
|
||||||
'You must stop the corruption before it consumes everything.':
|
'You must stop the corruption before it consumes everything.':
|
||||||
'모든 것을 삼키기 전에 손상을 멈춰야 한다.',
|
'모든 것을 삼키기 전에 손상을 멈춰야 한다.',
|
||||||
|
|
||||||
@@ -1206,20 +1201,16 @@ const Map<String, String> cinematicTranslationsKo = {
|
|||||||
'고대 컴파일러가 당신에게 시련을 건넨다.',
|
'고대 컴파일러가 당신에게 시련을 건넨다.',
|
||||||
'A companion falls... their sacrifice not in vain.':
|
'A companion falls... their sacrifice not in vain.':
|
||||||
'동료가 쓰러진다... 그들의 희생은 헛되지 않으리.',
|
'동료가 쓰러진다... 그들의 희생은 헛되지 않으리.',
|
||||||
'The Buffer Overflow Titan guards the gate.':
|
'The Buffer Overflow Titan guards the gate.': '버퍼 오버플로우 타이탄이 문을 지키고 있다.',
|
||||||
'버퍼 오버플로우 타이탄이 문을 지키고 있다.',
|
|
||||||
'Only through great sacrifice can you proceed.':
|
'Only through great sacrifice can you proceed.':
|
||||||
'오직 큰 희생을 통해서만 앞으로 나아갈 수 있다.',
|
'오직 큰 희생을 통해서만 앞으로 나아갈 수 있다.',
|
||||||
|
|
||||||
// Act IV: 결전
|
// Act IV: 결전
|
||||||
'=== ACT IV: CONFRONTATION ===': '=== 제4막: 결전 ===',
|
'=== ACT IV: CONFRONTATION ===': '=== 제4막: 결전 ===',
|
||||||
"The Glitch God's Citadel looms before you.":
|
"The Glitch God's Citadel looms before you.": '글리치 신의 성채가 눈앞에 어렴풋이 보인다.',
|
||||||
'글리치 신의 성채가 눈앞에 어렴풋이 보인다.',
|
'Former enemies unite against the common threat.': '이전의 적들이 공동의 위협에 맞서 연합한다.',
|
||||||
'Former enemies unite against the common threat.':
|
|
||||||
'이전의 적들이 공동의 위협에 맞서 연합한다.',
|
|
||||||
'The Final Alliance is forged.': '최후의 동맹이 결성되었다.',
|
'The Final Alliance is forged.': '최후의 동맹이 결성되었다.',
|
||||||
'The Kernel Panic Archon blocks your path.':
|
'The Kernel Panic Archon blocks your path.': '커널 패닉 아르콘이 당신의 길을 막는다.',
|
||||||
'커널 패닉 아르콘이 당신의 길을 막는다.',
|
|
||||||
'One final battle before the end...': '종말 전의 마지막 전투...',
|
'One final battle before the end...': '종말 전의 마지막 전투...',
|
||||||
|
|
||||||
// Act V: 종말
|
// Act V: 종말
|
||||||
@@ -1227,13 +1218,11 @@ const Map<String, String> cinematicTranslationsKo = {
|
|||||||
'The Glitch God reveals its true form.': '글리치 신이 진정한 모습을 드러낸다.',
|
'The Glitch God reveals its true form.': '글리치 신이 진정한 모습을 드러낸다.',
|
||||||
'Reality itself begins to corrupt.': '현실 그 자체가 손상되기 시작한다.',
|
'Reality itself begins to corrupt.': '현실 그 자체가 손상되기 시작한다.',
|
||||||
'All hope rests upon your shoulders.': '모든 희망이 당신의 어깨에 달려 있다.',
|
'All hope rests upon your shoulders.': '모든 희망이 당신의 어깨에 달려 있다.',
|
||||||
'The final battle for the Codebase begins!':
|
'The final battle for the Codebase begins!': '코드베이스를 위한 최후의 전투가 시작된다!',
|
||||||
'코드베이스를 위한 최후의 전투가 시작된다!',
|
|
||||||
|
|
||||||
// 엔딩
|
// 엔딩
|
||||||
'=== THE END ===': '=== 완결 ===',
|
'=== THE END ===': '=== 완결 ===',
|
||||||
'The Glitch God falls. The corruption fades.':
|
'The Glitch God falls. The corruption fades.': '글리치 신이 쓰러진다. 손상이 사라진다.',
|
||||||
'글리치 신이 쓰러진다. 손상이 사라진다.',
|
|
||||||
'System Reboot initiated...': '시스템 재부팅 시작...',
|
'System Reboot initiated...': '시스템 재부팅 시작...',
|
||||||
'Peace returns to the Digital Realm.': '디지털 세계에 평화가 돌아온다.',
|
'Peace returns to the Digital Realm.': '디지털 세계에 평화가 돌아온다.',
|
||||||
'Your legend will be compiled into the eternal logs.':
|
'Your legend will be compiled into the eternal logs.':
|
||||||
@@ -1505,37 +1494,37 @@ const Map<String, String> additionalItemOfsTranslationsKo = {
|
|||||||
|
|
||||||
/// 모든 몬스터 번역을 통합하여 반환
|
/// 모든 몬스터 번역을 통합하여 반환
|
||||||
Map<String, String> get allMonsterTranslationsKo => {
|
Map<String, String> get allMonsterTranslationsKo => {
|
||||||
...monsterTranslationsKo,
|
...monsterTranslationsKo,
|
||||||
...advancedMonsterTranslationsKo,
|
...advancedMonsterTranslationsKo,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// 모든 아이템 속성 번역을 통합하여 반환
|
/// 모든 아이템 속성 번역을 통합하여 반환
|
||||||
Map<String, String> get allItemAttribTranslationsKo => {
|
Map<String, String> get allItemAttribTranslationsKo => {
|
||||||
...itemAttribTranslationsKo,
|
...itemAttribTranslationsKo,
|
||||||
...additionalItemAttribTranslationsKo,
|
...additionalItemAttribTranslationsKo,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// 모든 아이템 접미사("~의") 번역을 통합하여 반환
|
/// 모든 아이템 접미사("~의") 번역을 통합하여 반환
|
||||||
Map<String, String> get allItemOfsTranslationsKo => {
|
Map<String, String> get allItemOfsTranslationsKo => {
|
||||||
...itemOfsTranslationsKo,
|
...itemOfsTranslationsKo,
|
||||||
...additionalItemOfsTranslationsKo,
|
...additionalItemOfsTranslationsKo,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// 모든 드롭 아이템 번역을 통합하여 반환
|
/// 모든 드롭 아이템 번역을 통합하여 반환
|
||||||
Map<String, String> get allDropTranslationsKo => {
|
Map<String, String> get allDropTranslationsKo => {
|
||||||
...boringItemTranslationsKo,
|
...boringItemTranslationsKo,
|
||||||
...dropItemTranslationsKo,
|
...dropItemTranslationsKo,
|
||||||
...additionalDropTranslationsKo,
|
...additionalDropTranslationsKo,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// 모든 갑옷 번역을 통합하여 반환
|
/// 모든 갑옷 번역을 통합하여 반환
|
||||||
Map<String, String> get allArmorTranslationsKo => {
|
Map<String, String> get allArmorTranslationsKo => {
|
||||||
...armorTranslationsKo,
|
...armorTranslationsKo,
|
||||||
...additionalArmorTranslationsKo,
|
...additionalArmorTranslationsKo,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// 모든 방패 번역을 통합하여 반환
|
/// 모든 방패 번역을 통합하여 반환
|
||||||
Map<String, String> get allShieldTranslationsKo => {
|
Map<String, String> get allShieldTranslationsKo => {
|
||||||
...shieldTranslationsKo,
|
...shieldTranslationsKo,
|
||||||
...additionalShieldTranslationsKo,
|
...additionalShieldTranslationsKo,
|
||||||
};
|
};
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -80,18 +80,12 @@ const Map<StoryAct, List<CinematicStep>> cinematicData = {
|
|||||||
text: 'Now, a new hero awakens to defend the Code.',
|
text: 'Now, a new hero awakens to defend the Code.',
|
||||||
durationMs: 3500,
|
durationMs: 3500,
|
||||||
),
|
),
|
||||||
CinematicStep(
|
CinematicStep(text: 'Your journey begins...', durationMs: 2500),
|
||||||
text: 'Your journey begins...',
|
|
||||||
durationMs: 2500,
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
|
|
||||||
// Act I: 각성 (레벨 1-20)
|
// Act I: 각성 (레벨 1-20)
|
||||||
StoryAct.act1: [
|
StoryAct.act1: [
|
||||||
CinematicStep(
|
CinematicStep(text: '=== ACT I: AWAKENING ===', durationMs: 3000),
|
||||||
text: '=== ACT I: AWAKENING ===',
|
|
||||||
durationMs: 3000,
|
|
||||||
),
|
|
||||||
CinematicStep(
|
CinematicStep(
|
||||||
text: 'You have proven yourself against the lesser bugs.',
|
text: 'You have proven yourself against the lesser bugs.',
|
||||||
durationMs: 3000,
|
durationMs: 3000,
|
||||||
@@ -114,10 +108,7 @@ const Map<StoryAct, List<CinematicStep>> cinematicData = {
|
|||||||
|
|
||||||
// Act II: 성장 (레벨 21-40)
|
// Act II: 성장 (레벨 21-40)
|
||||||
StoryAct.act2: [
|
StoryAct.act2: [
|
||||||
CinematicStep(
|
CinematicStep(text: '=== ACT II: GROWTH ===', durationMs: 3000),
|
||||||
text: '=== ACT II: GROWTH ===',
|
|
||||||
durationMs: 3000,
|
|
||||||
),
|
|
||||||
CinematicStep(
|
CinematicStep(
|
||||||
text: 'With the Dragon slain, you join the Debugger Knights.',
|
text: 'With the Dragon slain, you join the Debugger Knights.',
|
||||||
durationMs: 3500,
|
durationMs: 3500,
|
||||||
@@ -144,10 +135,7 @@ const Map<StoryAct, List<CinematicStep>> cinematicData = {
|
|||||||
|
|
||||||
// Act III: 시련 (레벨 41-60)
|
// Act III: 시련 (레벨 41-60)
|
||||||
StoryAct.act3: [
|
StoryAct.act3: [
|
||||||
CinematicStep(
|
CinematicStep(text: '=== ACT III: TRIALS ===', durationMs: 3000),
|
||||||
text: '=== ACT III: TRIALS ===',
|
|
||||||
durationMs: 3000,
|
|
||||||
),
|
|
||||||
CinematicStep(
|
CinematicStep(
|
||||||
text: 'The path leads to the Null Kingdom...',
|
text: 'The path leads to the Null Kingdom...',
|
||||||
asciiArt: _asciiNullKingdom,
|
asciiArt: _asciiNullKingdom,
|
||||||
@@ -174,10 +162,7 @@ const Map<StoryAct, List<CinematicStep>> cinematicData = {
|
|||||||
|
|
||||||
// Act IV: 결전 (레벨 61-80)
|
// Act IV: 결전 (레벨 61-80)
|
||||||
StoryAct.act4: [
|
StoryAct.act4: [
|
||||||
CinematicStep(
|
CinematicStep(text: '=== ACT IV: CONFRONTATION ===', durationMs: 3000),
|
||||||
text: '=== ACT IV: CONFRONTATION ===',
|
|
||||||
durationMs: 3000,
|
|
||||||
),
|
|
||||||
CinematicStep(
|
CinematicStep(
|
||||||
text: "The Glitch God's Citadel looms before you.",
|
text: "The Glitch God's Citadel looms before you.",
|
||||||
asciiArt: _asciiCitadel,
|
asciiArt: _asciiCitadel,
|
||||||
@@ -187,36 +172,24 @@ const Map<StoryAct, List<CinematicStep>> cinematicData = {
|
|||||||
text: 'Former enemies unite against the common threat.',
|
text: 'Former enemies unite against the common threat.',
|
||||||
durationMs: 3500,
|
durationMs: 3500,
|
||||||
),
|
),
|
||||||
CinematicStep(
|
CinematicStep(text: 'The Final Alliance is forged.', durationMs: 3000),
|
||||||
text: 'The Final Alliance is forged.',
|
|
||||||
durationMs: 3000,
|
|
||||||
),
|
|
||||||
CinematicStep(
|
CinematicStep(
|
||||||
text: 'The Kernel Panic Archon blocks your path.',
|
text: 'The Kernel Panic Archon blocks your path.',
|
||||||
asciiArt: _asciiArchon,
|
asciiArt: _asciiArchon,
|
||||||
durationMs: 4000,
|
durationMs: 4000,
|
||||||
),
|
),
|
||||||
CinematicStep(
|
CinematicStep(text: 'One final battle before the end...', durationMs: 3500),
|
||||||
text: 'One final battle before the end...',
|
|
||||||
durationMs: 3500,
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
|
|
||||||
// Act V: 종말 (레벨 81-100)
|
// Act V: 종말 (레벨 81-100)
|
||||||
StoryAct.act5: [
|
StoryAct.act5: [
|
||||||
CinematicStep(
|
CinematicStep(text: '=== ACT V: ENDGAME ===', durationMs: 3000),
|
||||||
text: '=== ACT V: ENDGAME ===',
|
|
||||||
durationMs: 3000,
|
|
||||||
),
|
|
||||||
CinematicStep(
|
CinematicStep(
|
||||||
text: 'The Glitch God reveals its true form.',
|
text: 'The Glitch God reveals its true form.',
|
||||||
asciiArt: _asciiGlitchGod,
|
asciiArt: _asciiGlitchGod,
|
||||||
durationMs: 4000,
|
durationMs: 4000,
|
||||||
),
|
),
|
||||||
CinematicStep(
|
CinematicStep(text: 'Reality itself begins to corrupt.', durationMs: 3500),
|
||||||
text: 'Reality itself begins to corrupt.',
|
|
||||||
durationMs: 3500,
|
|
||||||
),
|
|
||||||
CinematicStep(
|
CinematicStep(
|
||||||
text: 'All hope rests upon your shoulders.',
|
text: 'All hope rests upon your shoulders.',
|
||||||
durationMs: 3000,
|
durationMs: 3000,
|
||||||
@@ -229,19 +202,13 @@ const Map<StoryAct, List<CinematicStep>> cinematicData = {
|
|||||||
|
|
||||||
// 엔딩: 시스템 재부팅, 평화 회복
|
// 엔딩: 시스템 재부팅, 평화 회복
|
||||||
StoryAct.ending: [
|
StoryAct.ending: [
|
||||||
CinematicStep(
|
CinematicStep(text: '=== THE END ===', durationMs: 3000),
|
||||||
text: '=== THE END ===',
|
|
||||||
durationMs: 3000,
|
|
||||||
),
|
|
||||||
CinematicStep(
|
CinematicStep(
|
||||||
text: 'The Glitch God falls. The corruption fades.',
|
text: 'The Glitch God falls. The corruption fades.',
|
||||||
asciiArt: _asciiVictory,
|
asciiArt: _asciiVictory,
|
||||||
durationMs: 4000,
|
durationMs: 4000,
|
||||||
),
|
),
|
||||||
CinematicStep(
|
CinematicStep(text: 'System Reboot initiated...', durationMs: 3000),
|
||||||
text: 'System Reboot initiated...',
|
|
||||||
durationMs: 3000,
|
|
||||||
),
|
|
||||||
CinematicStep(
|
CinematicStep(
|
||||||
text: 'Peace returns to the Digital Realm.',
|
text: 'Peace returns to the Digital Realm.',
|
||||||
durationMs: 3500,
|
durationMs: 3500,
|
||||||
@@ -250,15 +217,8 @@ const Map<StoryAct, List<CinematicStep>> cinematicData = {
|
|||||||
text: 'Your legend will be compiled into the eternal logs.',
|
text: 'Your legend will be compiled into the eternal logs.',
|
||||||
durationMs: 4000,
|
durationMs: 4000,
|
||||||
),
|
),
|
||||||
CinematicStep(
|
CinematicStep(text: 'THE END', asciiArt: _asciiTheEnd, durationMs: 5000),
|
||||||
text: 'THE END',
|
CinematicStep(text: '...or is it?', durationMs: 3000),
|
||||||
asciiArt: _asciiTheEnd,
|
|
||||||
durationMs: 5000,
|
|
||||||
),
|
|
||||||
CinematicStep(
|
|
||||||
text: '...or is it?',
|
|
||||||
durationMs: 3000,
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -78,11 +78,11 @@
|
|||||||
"xpNeededForNextLevel": "XP needed for next level",
|
"xpNeededForNextLevel": "XP needed for next level",
|
||||||
"@xpNeededForNextLevel": { "description": "XP needed tooltip" },
|
"@xpNeededForNextLevel": { "description": "XP needed tooltip" },
|
||||||
|
|
||||||
"spellBook": "Spell Book",
|
"spellBook": "Skills",
|
||||||
"@spellBook": { "description": "Spell book section title" },
|
"@spellBook": { "description": "Skills section title (unified spellbook + skills)" },
|
||||||
|
|
||||||
"noSpellsYet": "No spells yet",
|
"noSpellsYet": "No skills yet",
|
||||||
"@noSpellsYet": { "description": "Empty spell book message" },
|
"@noSpellsYet": { "description": "Empty skills message" },
|
||||||
|
|
||||||
"equipment": "Equipment",
|
"equipment": "Equipment",
|
||||||
"@equipment": { "description": "Equipment panel title" },
|
"@equipment": { "description": "Equipment panel title" },
|
||||||
|
|||||||
@@ -25,8 +25,8 @@
|
|||||||
"stats": "Stats",
|
"stats": "Stats",
|
||||||
"experience": "Experience",
|
"experience": "Experience",
|
||||||
"xpNeededForNextLevel": "XP needed for next level",
|
"xpNeededForNextLevel": "XP needed for next level",
|
||||||
"spellBook": "Spell Book",
|
"spellBook": "スキル",
|
||||||
"noSpellsYet": "No spells yet",
|
"noSpellsYet": "習得したスキルがありません",
|
||||||
"equipment": "Equipment",
|
"equipment": "Equipment",
|
||||||
"inventory": "Inventory",
|
"inventory": "Inventory",
|
||||||
"encumbrance": "Encumbrance",
|
"encumbrance": "Encumbrance",
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
"stats": "능력치",
|
"stats": "능력치",
|
||||||
"experience": "경험치",
|
"experience": "경험치",
|
||||||
"xpNeededForNextLevel": "다음 레벨까지 필요한 XP",
|
"xpNeededForNextLevel": "다음 레벨까지 필요한 XP",
|
||||||
"spellBook": "스킬북",
|
"spellBook": "스킬",
|
||||||
"noSpellsYet": "습득한 스킬이 없습니다",
|
"noSpellsYet": "습득한 스킬이 없습니다",
|
||||||
"equipment": "장비",
|
"equipment": "장비",
|
||||||
"inventory": "인벤토리",
|
"inventory": "인벤토리",
|
||||||
|
|||||||
@@ -245,16 +245,16 @@ abstract class L10n {
|
|||||||
/// **'XP needed for next level'**
|
/// **'XP needed for next level'**
|
||||||
String get xpNeededForNextLevel;
|
String get xpNeededForNextLevel;
|
||||||
|
|
||||||
/// Spell book section title
|
/// Skills section title (unified spellbook + skills)
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'Spell Book'**
|
/// **'Skills'**
|
||||||
String get spellBook;
|
String get spellBook;
|
||||||
|
|
||||||
/// Empty spell book message
|
/// Empty skills message
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'No spells yet'**
|
/// **'No skills yet'**
|
||||||
String get noSpellsYet;
|
String get noSpellsYet;
|
||||||
|
|
||||||
/// Equipment panel title
|
/// Equipment panel title
|
||||||
|
|||||||
@@ -83,10 +83,10 @@ class L10nEn extends L10n {
|
|||||||
String get xpNeededForNextLevel => 'XP needed for next level';
|
String get xpNeededForNextLevel => 'XP needed for next level';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get spellBook => 'Spell Book';
|
String get spellBook => 'Skills';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get noSpellsYet => 'No spells yet';
|
String get noSpellsYet => 'No skills yet';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get equipment => 'Equipment';
|
String get equipment => 'Equipment';
|
||||||
|
|||||||
@@ -83,10 +83,10 @@ class L10nJa extends L10n {
|
|||||||
String get xpNeededForNextLevel => 'XP needed for next level';
|
String get xpNeededForNextLevel => 'XP needed for next level';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get spellBook => 'Spell Book';
|
String get spellBook => 'スキル';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get noSpellsYet => 'No spells yet';
|
String get noSpellsYet => '習得したスキルがありません';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get equipment => 'Equipment';
|
String get equipment => 'Equipment';
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ class L10nKo extends L10n {
|
|||||||
String get xpNeededForNextLevel => '다음 레벨까지 필요한 XP';
|
String get xpNeededForNextLevel => '다음 레벨까지 필요한 XP';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get spellBook => '스킬북';
|
String get spellBook => '스킬';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get noSpellsYet => '습득한 스킬이 없습니다';
|
String get noSpellsYet => '습득한 스킬이 없습니다';
|
||||||
|
|||||||
@@ -83,10 +83,10 @@ class L10nZh extends L10n {
|
|||||||
String get xpNeededForNextLevel => 'XP needed for next level';
|
String get xpNeededForNextLevel => 'XP needed for next level';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get spellBook => 'Spell Book';
|
String get spellBook => '技能';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get noSpellsYet => 'No spells yet';
|
String get noSpellsYet => '暂无技能';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get equipment => 'Equipment';
|
String get equipment => 'Equipment';
|
||||||
|
|||||||
@@ -25,8 +25,8 @@
|
|||||||
"stats": "Stats",
|
"stats": "Stats",
|
||||||
"experience": "Experience",
|
"experience": "Experience",
|
||||||
"xpNeededForNextLevel": "XP needed for next level",
|
"xpNeededForNextLevel": "XP needed for next level",
|
||||||
"spellBook": "Spell Book",
|
"spellBook": "技能",
|
||||||
"noSpellsYet": "No spells yet",
|
"noSpellsYet": "暂无技能",
|
||||||
"equipment": "Equipment",
|
"equipment": "Equipment",
|
||||||
"inventory": "Inventory",
|
"inventory": "Inventory",
|
||||||
"encumbrance": "Encumbrance",
|
"encumbrance": "Encumbrance",
|
||||||
|
|||||||
@@ -97,9 +97,9 @@ class _AskiiNeverDieAppState extends State<AskiiNeverDieApp> {
|
|||||||
|
|
||||||
if (saves.isEmpty) {
|
if (saves.isEmpty) {
|
||||||
// 저장 파일이 없으면 안내 메시지
|
// 저장 파일이 없으면 안내 메시지
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(
|
||||||
SnackBar(content: Text(L10n.of(context).noSavedGames)),
|
context,
|
||||||
);
|
).showSnackBar(SnackBar(content: Text(L10n.of(context).noSavedGames)));
|
||||||
return;
|
return;
|
||||||
} else if (saves.length == 1) {
|
} else if (saves.length == 1) {
|
||||||
// 파일이 하나면 바로 선택
|
// 파일이 하나면 바로 선택
|
||||||
@@ -158,9 +158,7 @@ class _AskiiNeverDieAppState extends State<AskiiNeverDieApp> {
|
|||||||
/// Phase 10: 명예의 전당 화면으로 이동
|
/// Phase 10: 명예의 전당 화면으로 이동
|
||||||
void _navigateToHallOfFame(BuildContext context) {
|
void _navigateToHallOfFame(BuildContext context) {
|
||||||
Navigator.of(context).push(
|
Navigator.of(context).push(
|
||||||
MaterialPageRoute<void>(
|
MaterialPageRoute<void>(builder: (context) => const HallOfFameScreen()),
|
||||||
builder: (context) => const HallOfFameScreen(),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,815 +0,0 @@
|
|||||||
// BattleComposer - 전투 프레임 실시간 합성
|
|
||||||
// Stone Story RPG 스타일 참고 - 8줄 캐릭터/몬스터, 60자 폭
|
|
||||||
// ASCII Patrol 스타일 패럴렉스 배경
|
|
||||||
|
|
||||||
import 'package:askiineverdie/src/core/animation/ascii_animation_data.dart';
|
|
||||||
import 'package:askiineverdie/src/core/animation/background_data.dart';
|
|
||||||
import 'package:askiineverdie/src/core/animation/background_layer.dart';
|
|
||||||
import 'package:askiineverdie/src/core/animation/character_frames.dart';
|
|
||||||
import 'package:askiineverdie/src/core/animation/monster_size.dart';
|
|
||||||
import 'package:askiineverdie/src/core/animation/weapon_category.dart';
|
|
||||||
import 'package:askiineverdie/src/core/animation/weapon_effects.dart';
|
|
||||||
|
|
||||||
/// 전투 프레임 합성기
|
|
||||||
class BattleComposer {
|
|
||||||
const BattleComposer({
|
|
||||||
required this.weaponCategory,
|
|
||||||
required this.hasShield,
|
|
||||||
required this.monsterCategory,
|
|
||||||
required this.monsterSize,
|
|
||||||
});
|
|
||||||
|
|
||||||
final WeaponCategory weaponCategory;
|
|
||||||
final bool hasShield;
|
|
||||||
final MonsterCategory monsterCategory;
|
|
||||||
final MonsterSize monsterSize;
|
|
||||||
|
|
||||||
/// 전체 프레임 폭 (문자 수)
|
|
||||||
static const int frameWidth = 60;
|
|
||||||
|
|
||||||
/// 프레임 높이 (줄 수)
|
|
||||||
static const int frameHeight = 8;
|
|
||||||
|
|
||||||
/// 영역 분할
|
|
||||||
static const int characterWidth = 18;
|
|
||||||
static const int effectWidth = 24;
|
|
||||||
static const int monsterWidth = 18;
|
|
||||||
|
|
||||||
/// 전투 프레임 생성 (배경 없음)
|
|
||||||
String composeFrame(BattlePhase phase, int subFrame, String? monsterBaseName) {
|
|
||||||
// 캐릭터 프레임
|
|
||||||
var charFrame = getCharacterFrame(phase, subFrame);
|
|
||||||
if (hasShield) {
|
|
||||||
charFrame = charFrame.withShield();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 몬스터 프레임 (애니메이션 포함)
|
|
||||||
final monsterFrames =
|
|
||||||
_getAnimatedMonsterFrames(monsterCategory, monsterSize, phase);
|
|
||||||
final monsterFrame = monsterFrames[subFrame % monsterFrames.length];
|
|
||||||
|
|
||||||
// 무기 이펙트 (단일 라인)
|
|
||||||
final effect = getWeaponEffect(weaponCategory);
|
|
||||||
final effectLine = _getEffectLine(effect, phase, subFrame);
|
|
||||||
|
|
||||||
// 프레임 합성
|
|
||||||
return _compose(charFrame.lines, monsterFrame, effectLine, phase);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 전투 프레임 생성 (배경 포함, ASCII Patrol 스타일)
|
|
||||||
String composeFrameWithBackground(
|
|
||||||
BattlePhase phase,
|
|
||||||
int subFrame,
|
|
||||||
String? monsterBaseName,
|
|
||||||
EnvironmentType environment,
|
|
||||||
int globalTick,
|
|
||||||
) {
|
|
||||||
// 1. 8x60 캔버스 생성 (공백으로 초기화)
|
|
||||||
final canvas =
|
|
||||||
List.generate(frameHeight, (_) => List.filled(frameWidth, ' '));
|
|
||||||
|
|
||||||
// 2. 배경 레이어 그리기 (뒤에서 앞으로)
|
|
||||||
final layers = getBackgroundLayers(environment);
|
|
||||||
for (final layer in layers) {
|
|
||||||
_drawBackgroundLayer(canvas, layer, globalTick);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. 캐릭터 프레임 (페이즈에 따라 X 위치 변경 - 근접 전투)
|
|
||||||
var charFrame = getCharacterFrame(phase, subFrame);
|
|
||||||
if (hasShield) {
|
|
||||||
charFrame = charFrame.withShield();
|
|
||||||
}
|
|
||||||
final normalizedChar = _normalizeSprite(charFrame.lines, characterWidth);
|
|
||||||
// 바닥 레이어(Y=7) 위에 서있도록 -1
|
|
||||||
final charY = frameHeight - normalizedChar.length - 1;
|
|
||||||
// 페이즈별 캐릭터 X 위치 (몬스터에게 접근)
|
|
||||||
final charX = switch (phase) {
|
|
||||||
BattlePhase.idle => 0,
|
|
||||||
BattlePhase.prepare => 12,
|
|
||||||
BattlePhase.attack => 24,
|
|
||||||
BattlePhase.hit => 28,
|
|
||||||
BattlePhase.recover => 8,
|
|
||||||
};
|
|
||||||
_overlaySpriteWithSpaces(canvas, normalizedChar, charX, charY);
|
|
||||||
|
|
||||||
// 4. 몬스터 프레임 (정규화하여 오른쪽 정렬)
|
|
||||||
// idle 프레임 기준 너비로 정렬하여 hit/alert 시 위치 이동 방지
|
|
||||||
final monsterRefWidth = _getMonsterReferenceWidth(monsterCategory, monsterSize);
|
|
||||||
final monsterFrames =
|
|
||||||
_getAnimatedMonsterFrames(monsterCategory, monsterSize, phase);
|
|
||||||
final monsterFrame = monsterFrames[subFrame % monsterFrames.length];
|
|
||||||
final normalizedMonster = _normalizeSpriteRight(
|
|
||||||
monsterFrame,
|
|
||||||
monsterWidth,
|
|
||||||
referenceWidth: monsterRefWidth,
|
|
||||||
);
|
|
||||||
final monsterX = frameWidth - monsterWidth;
|
|
||||||
// 바닥 레이어(Y=7) 위에 서있도록 -1
|
|
||||||
final monsterY = frameHeight - normalizedMonster.length - 1;
|
|
||||||
// 몬스터는 경계 내 완전 렌더링 (내부 공백에 배경이 비치지 않도록)
|
|
||||||
_overlaySpriteWithBounds(canvas, normalizedMonster, monsterX, monsterY);
|
|
||||||
|
|
||||||
// 5. 멀티라인 이펙트 오버레이 (공격/히트 페이즈)
|
|
||||||
if (phase == BattlePhase.attack || phase == BattlePhase.hit) {
|
|
||||||
final effect = getWeaponEffect(weaponCategory);
|
|
||||||
final effectLines = _getEffectLines(effect, phase, subFrame);
|
|
||||||
if (effectLines.isNotEmpty) {
|
|
||||||
// 이펙트 Y 위치: 캐릭터 머리 높이 (1번째 줄) 기준 - 수정됨
|
|
||||||
final effectY = charY;
|
|
||||||
// 이펙트 X 위치: 캐릭터 오른쪽에 붙여서 표시
|
|
||||||
final effectX = charX + 6;
|
|
||||||
for (var i = 0; i < effectLines.length; i++) {
|
|
||||||
final y = effectY + i;
|
|
||||||
if (y >= 0 && y < frameHeight && effectLines[i].isNotEmpty) {
|
|
||||||
_overlayText(canvas, effectLines[i], effectX, y);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 6. 문자열로 변환
|
|
||||||
return canvas.map((row) => row.join()).join('\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 스프라이트를 지정 폭으로 정규화 (왼쪽 정렬)
|
|
||||||
List<String> _normalizeSprite(List<String> sprite, int width) {
|
|
||||||
return sprite.map((line) => line.padRight(width).substring(0, width)).toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 스프라이트를 지정 폭으로 정규화 (오른쪽 정렬, 전체 스프라이트 기준)
|
|
||||||
///
|
|
||||||
/// 모든 줄을 동일한 기준점에서 오른쪽 정렬하여
|
|
||||||
/// 머리와 몸통이 분리되지 않도록 함
|
|
||||||
///
|
|
||||||
/// [referenceWidth] 지정 시 해당 너비를 기준으로 정렬 (idle/hit 프레임 일관성용)
|
|
||||||
List<String> _normalizeSpriteRight(
|
|
||||||
List<String> sprite,
|
|
||||||
int width, {
|
|
||||||
int? referenceWidth,
|
|
||||||
}) {
|
|
||||||
// 1. 각 줄의 실제 너비(오른쪽 공백 제외) 계산
|
|
||||||
final trimmedLines = sprite.map((line) => line.trimRight()).toList();
|
|
||||||
|
|
||||||
// 2. 기준 너비 결정 (referenceWidth 있으면 사용, 없으면 현재 스프라이트 기준)
|
|
||||||
int maxLineWidth;
|
|
||||||
if (referenceWidth != null) {
|
|
||||||
maxLineWidth = referenceWidth;
|
|
||||||
} else {
|
|
||||||
maxLineWidth = 0;
|
|
||||||
for (final line in trimmedLines) {
|
|
||||||
if (line.length > maxLineWidth) {
|
|
||||||
maxLineWidth = line.length;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. 전체 스프라이트를 오른쪽 정렬 (width 기준)
|
|
||||||
// 모든 줄에 동일한 왼쪽 패딩 적용
|
|
||||||
final leftPadding = width - maxLineWidth;
|
|
||||||
final paddingStr = leftPadding > 0 ? ' ' * leftPadding : '';
|
|
||||||
|
|
||||||
return trimmedLines.map((line) {
|
|
||||||
// 각 줄을 왼쪽에 공통 패딩 추가 후 width로 자르기
|
|
||||||
final paddedLine = paddingStr + line;
|
|
||||||
if (paddedLine.length > width) {
|
|
||||||
return paddedLine.substring(paddedLine.length - width);
|
|
||||||
}
|
|
||||||
return paddedLine.padRight(width);
|
|
||||||
}).toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 몬스터 스프라이트의 기준 너비 계산 (idle 프레임 기준)
|
|
||||||
int _getMonsterReferenceWidth(MonsterCategory category, MonsterSize size) {
|
|
||||||
final idleFrames = _getMonsterIdleFrames(category, size);
|
|
||||||
int maxWidth = 0;
|
|
||||||
for (final frame in idleFrames) {
|
|
||||||
for (final line in frame) {
|
|
||||||
final trimmedLength = line.trimRight().length;
|
|
||||||
if (trimmedLength > maxWidth) {
|
|
||||||
maxWidth = trimmedLength;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return maxWidth;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 스프라이트를 캔버스에 오버레이 (공백은 투명 처리)
|
|
||||||
void _overlaySpriteWithSpaces(
|
|
||||||
List<List<String>> canvas,
|
|
||||||
List<String> sprite,
|
|
||||||
int startX,
|
|
||||||
int startY,
|
|
||||||
) {
|
|
||||||
for (var i = 0; i < sprite.length; i++) {
|
|
||||||
final y = startY + i;
|
|
||||||
if (y < 0 || y >= frameHeight) continue;
|
|
||||||
|
|
||||||
final line = sprite[i];
|
|
||||||
for (var j = 0; j < line.length; j++) {
|
|
||||||
final x = startX + j;
|
|
||||||
if (x < 0 || x >= frameWidth) continue;
|
|
||||||
|
|
||||||
final char = line[j];
|
|
||||||
// 공백이 아닌 문자만 덮어쓰기 (투명 배경 효과)
|
|
||||||
if (char != ' ') {
|
|
||||||
canvas[y][x] = char;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 스프라이트를 캔버스에 오버레이 (라인별 경계 내 완전 렌더링)
|
|
||||||
///
|
|
||||||
/// 각 라인에서 첫 번째와 마지막 비공백 문자 사이의 모든 문자를 그림.
|
|
||||||
/// 내부 공백도 그려져서 스크롤링 배경이 비치지 않음.
|
|
||||||
void _overlaySpriteWithBounds(
|
|
||||||
List<List<String>> canvas,
|
|
||||||
List<String> sprite,
|
|
||||||
int startX,
|
|
||||||
int startY,
|
|
||||||
) {
|
|
||||||
for (var i = 0; i < sprite.length; i++) {
|
|
||||||
final y = startY + i;
|
|
||||||
if (y < 0 || y >= frameHeight) continue;
|
|
||||||
|
|
||||||
final line = sprite[i];
|
|
||||||
|
|
||||||
// 각 라인에서 첫/마지막 비공백 문자 위치 찾기
|
|
||||||
int firstNonSpace = -1;
|
|
||||||
int lastNonSpace = -1;
|
|
||||||
for (var j = 0; j < line.length; j++) {
|
|
||||||
if (line[j] != ' ') {
|
|
||||||
if (firstNonSpace == -1) firstNonSpace = j;
|
|
||||||
lastNonSpace = j;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (firstNonSpace == -1) continue; // 빈 라인
|
|
||||||
|
|
||||||
// 경계 내 모든 문자 그리기 (공백 포함)
|
|
||||||
for (var j = firstNonSpace; j <= lastNonSpace; j++) {
|
|
||||||
final x = startX + j;
|
|
||||||
if (x < 0 || x >= frameWidth) continue;
|
|
||||||
canvas[y][x] = line[j];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 배경 레이어를 캔버스에 그리기
|
|
||||||
void _drawBackgroundLayer(
|
|
||||||
List<List<String>> canvas,
|
|
||||||
BackgroundLayer layer,
|
|
||||||
int globalTick,
|
|
||||||
) {
|
|
||||||
for (var i = 0; i < layer.lines.length; i++) {
|
|
||||||
final y = layer.yStart + i;
|
|
||||||
if (y >= frameHeight) break;
|
|
||||||
|
|
||||||
final pattern = layer.lines[i];
|
|
||||||
if (pattern.isEmpty) continue;
|
|
||||||
|
|
||||||
// 스크롤 오프셋 계산
|
|
||||||
final offset = (globalTick * layer.scrollSpeed).toInt() % pattern.length;
|
|
||||||
|
|
||||||
// 패턴을 스크롤하며 그리기
|
|
||||||
for (var x = 0; x < frameWidth; x++) {
|
|
||||||
final patternIdx = (x + offset) % pattern.length;
|
|
||||||
final char = pattern[patternIdx];
|
|
||||||
if (char != ' ') {
|
|
||||||
canvas[y][x] = char;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 텍스트를 캔버스에 오버레이
|
|
||||||
void _overlayText(
|
|
||||||
List<List<String>> canvas,
|
|
||||||
String text,
|
|
||||||
int startX,
|
|
||||||
int y,
|
|
||||||
) {
|
|
||||||
if (y < 0 || y >= frameHeight) return;
|
|
||||||
|
|
||||||
for (var i = 0; i < text.length; i++) {
|
|
||||||
final x = startX + i;
|
|
||||||
if (x < 0 || x >= frameWidth) continue;
|
|
||||||
|
|
||||||
final char = text[i];
|
|
||||||
if (char != ' ') {
|
|
||||||
canvas[y][x] = char;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 멀티라인 이펙트 프레임 반환
|
|
||||||
List<String> _getEffectLines(
|
|
||||||
WeaponEffect effect, BattlePhase phase, int subFrame) {
|
|
||||||
final frames = switch (phase) {
|
|
||||||
BattlePhase.idle => <List<String>>[],
|
|
||||||
BattlePhase.prepare => effect.prepareFrames,
|
|
||||||
BattlePhase.attack => effect.attackFrames,
|
|
||||||
BattlePhase.hit => effect.hitFrames,
|
|
||||||
BattlePhase.recover => <List<String>>[],
|
|
||||||
};
|
|
||||||
if (frames.isEmpty) return [];
|
|
||||||
return frames[subFrame % frames.length];
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 단일 라인 이펙트 (하위 호환용)
|
|
||||||
String _getEffectLine(WeaponEffect effect, BattlePhase phase, int subFrame) {
|
|
||||||
final lines = _getEffectLines(effect, phase, subFrame);
|
|
||||||
if (lines.isEmpty) return '';
|
|
||||||
// 멀티라인 중 중간 라인 반환 (메인 이펙트)
|
|
||||||
final midIndex = lines.length ~/ 2;
|
|
||||||
return lines.length > midIndex ? lines[midIndex] : lines.first;
|
|
||||||
}
|
|
||||||
|
|
||||||
String _compose(
|
|
||||||
List<String> charLines,
|
|
||||||
List<String> monsterLines,
|
|
||||||
String effectLine,
|
|
||||||
BattlePhase phase,
|
|
||||||
) {
|
|
||||||
final result = <String>[];
|
|
||||||
|
|
||||||
// 캐릭터와 몬스터를 하단 정렬 (8줄 기준)
|
|
||||||
final charOffset = frameHeight - charLines.length;
|
|
||||||
final monsterOffset = frameHeight - monsterLines.length;
|
|
||||||
|
|
||||||
// 이펙트 Y 위치: 캐릭터 body/arm 줄 (charOffset + 1)
|
|
||||||
final effectRow = charOffset + 1;
|
|
||||||
|
|
||||||
for (var i = 0; i < frameHeight; i++) {
|
|
||||||
// 캐릭터 파트 (왼쪽 18자)
|
|
||||||
final charIdx = i - charOffset;
|
|
||||||
final charPart =
|
|
||||||
(charIdx >= 0 && charIdx < charLines.length ? charLines[charIdx] : '')
|
|
||||||
.padRight(characterWidth);
|
|
||||||
|
|
||||||
// 이펙트 파트 (중앙 24자) - 캐릭터 팔 높이에 표시
|
|
||||||
String effectPart = '';
|
|
||||||
if (i == effectRow &&
|
|
||||||
(phase == BattlePhase.attack || phase == BattlePhase.hit)) {
|
|
||||||
effectPart = effectLine;
|
|
||||||
}
|
|
||||||
effectPart = effectPart.padRight(effectWidth);
|
|
||||||
|
|
||||||
// 몬스터 파트 (오른쪽 18자)
|
|
||||||
final monsterIdx = i - monsterOffset;
|
|
||||||
final monsterPart = (monsterIdx >= 0 && monsterIdx < monsterLines.length
|
|
||||||
? monsterLines[monsterIdx]
|
|
||||||
: '')
|
|
||||||
.padLeft(monsterWidth);
|
|
||||||
|
|
||||||
result.add('$charPart$effectPart$monsterPart');
|
|
||||||
}
|
|
||||||
|
|
||||||
return result.join('\n');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// 몬스터 애니메이션 프레임
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/// 몬스터 애니메이션 프레임 반환 (페이즈별 다른 동작)
|
|
||||||
List<List<String>> _getAnimatedMonsterFrames(
|
|
||||||
MonsterCategory category,
|
|
||||||
MonsterSize size,
|
|
||||||
BattlePhase phase,
|
|
||||||
) {
|
|
||||||
// 피격 상태
|
|
||||||
if (phase == BattlePhase.hit) {
|
|
||||||
return _getMonsterHitFrames(category, size);
|
|
||||||
}
|
|
||||||
// 경계 상태 (prepare, attack)
|
|
||||||
if (phase == BattlePhase.prepare || phase == BattlePhase.attack) {
|
|
||||||
return _getMonsterAlertFrames(category, size);
|
|
||||||
}
|
|
||||||
// 일반 상태 (idle, recover)
|
|
||||||
return _getMonsterIdleFrames(category, size);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 일반 상태 몬스터 프레임
|
|
||||||
List<List<String>> _getMonsterIdleFrames(MonsterCategory category, MonsterSize size) {
|
|
||||||
return switch (size) {
|
|
||||||
MonsterSize.tiny => _tinyIdleFrames(category),
|
|
||||||
MonsterSize.small => _smallIdleFrames(category),
|
|
||||||
MonsterSize.medium => _mediumIdleFrames(category),
|
|
||||||
MonsterSize.large => _largeIdleFrames(category),
|
|
||||||
_ => _hugeIdleFrames(category), // huge 이상은 같은 프레임 사용
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 피격 상태 몬스터 프레임
|
|
||||||
List<List<String>> _getMonsterHitFrames(MonsterCategory category, MonsterSize size) {
|
|
||||||
return switch (size) {
|
|
||||||
MonsterSize.tiny => _tinyHitFrames(category),
|
|
||||||
MonsterSize.small => _smallHitFrames(category),
|
|
||||||
MonsterSize.medium => _mediumHitFrames(category),
|
|
||||||
MonsterSize.large => _largeHitFrames(category),
|
|
||||||
_ => _hugeHitFrames(category),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 경계 상태 몬스터 프레임 (prepare/attack 시)
|
|
||||||
List<List<String>> _getMonsterAlertFrames(MonsterCategory category, MonsterSize size) {
|
|
||||||
return switch (size) {
|
|
||||||
MonsterSize.tiny => _tinyAlertFrames(category),
|
|
||||||
MonsterSize.small => _smallAlertFrames(category),
|
|
||||||
MonsterSize.medium => _mediumAlertFrames(category),
|
|
||||||
MonsterSize.large => _largeAlertFrames(category),
|
|
||||||
_ => _hugeAlertFrames(category),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Tiny 몬스터 (2줄, 8줄 캔버스 하단 정렬)
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
List<List<String>> _tinyIdleFrames(MonsterCategory category) {
|
|
||||||
return switch (category) {
|
|
||||||
MonsterCategory.bug => [
|
|
||||||
[r'*', r'/\'],
|
|
||||||
[r'o', r'\/'],
|
|
||||||
],
|
|
||||||
MonsterCategory.malware => [
|
|
||||||
[r'><', r'\/'],
|
|
||||||
[r'<>', r'/\'],
|
|
||||||
],
|
|
||||||
MonsterCategory.network => [
|
|
||||||
[r'o', r'|'],
|
|
||||||
[r'O', r'|'],
|
|
||||||
],
|
|
||||||
MonsterCategory.system => [
|
|
||||||
[r'+', r'|'],
|
|
||||||
[r'x', r'|'],
|
|
||||||
],
|
|
||||||
MonsterCategory.crypto => [
|
|
||||||
[r'~<', r'>>'],
|
|
||||||
[r'<~', r'<<'],
|
|
||||||
],
|
|
||||||
MonsterCategory.ai => [
|
|
||||||
[r'()', r''],
|
|
||||||
[r'{}', r''],
|
|
||||||
],
|
|
||||||
MonsterCategory.boss => [
|
|
||||||
[r'^v', r'\/'],
|
|
||||||
[r'v^', r'/\'],
|
|
||||||
],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
List<List<String>> _tinyHitFrames(MonsterCategory category) {
|
|
||||||
return [
|
|
||||||
[r'*!', r'><'],
|
|
||||||
[r'!*', r'<>'],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
List<List<String>> _tinyAlertFrames(MonsterCategory category) {
|
|
||||||
return switch (category) {
|
|
||||||
MonsterCategory.bug => [
|
|
||||||
[r'!!', r'/\'],
|
|
||||||
[r'OO', r'><'],
|
|
||||||
],
|
|
||||||
MonsterCategory.malware => [
|
|
||||||
[r'!!', r'\/'],
|
|
||||||
[r'@@', r'/\'],
|
|
||||||
],
|
|
||||||
MonsterCategory.network => [
|
|
||||||
[r'O!', r'|'],
|
|
||||||
[r'!O', r'X'],
|
|
||||||
],
|
|
||||||
MonsterCategory.system => [
|
|
||||||
[r'!!', r'X'],
|
|
||||||
[r'@@', r'|'],
|
|
||||||
],
|
|
||||||
MonsterCategory.crypto => [
|
|
||||||
[r'!<', r'>>'],
|
|
||||||
[r'>!', r'<<'],
|
|
||||||
],
|
|
||||||
MonsterCategory.ai => [
|
|
||||||
[r'(!)', r''],
|
|
||||||
[r'{!}', r''],
|
|
||||||
],
|
|
||||||
MonsterCategory.boss => [
|
|
||||||
[r'^!', r'><'],
|
|
||||||
[r'!^', r'<>'],
|
|
||||||
],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Small 몬스터 (4줄)
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
List<List<String>> _smallIdleFrames(MonsterCategory category) {
|
|
||||||
return switch (category) {
|
|
||||||
MonsterCategory.bug => [
|
|
||||||
[r' /\_/\', r'( o.o )', r' > ^ <', r' /| |\'],
|
|
||||||
[r' /\_/\', r'( o o )', r' > v <', r' \| |/'],
|
|
||||||
],
|
|
||||||
MonsterCategory.malware => [
|
|
||||||
[r' /\/\', r' (O O)', r' / \', r' \/ \/'],
|
|
||||||
[r' \/\/\', r' (O O)', r' \ /', r' /\ /\'],
|
|
||||||
],
|
|
||||||
MonsterCategory.network => [
|
|
||||||
[r' O', r' /|\', r' / \', r' _| |_'],
|
|
||||||
[r' O', r' \|/', r' | |', r' _/ \_'],
|
|
||||||
],
|
|
||||||
MonsterCategory.system => [
|
|
||||||
[r' _+_', r' (x_x)', r' /|\', r' _/ \_'],
|
|
||||||
[r' _+_', r' (X_X)', r' \|/', r' _| |_'],
|
|
||||||
],
|
|
||||||
MonsterCategory.crypto => [
|
|
||||||
[r' __', r' <(oo)~', r' / \', r' <_ _>'],
|
|
||||||
[r' __', r' (oo)>', r' \ /', r' <_ _>'],
|
|
||||||
],
|
|
||||||
MonsterCategory.ai => [
|
|
||||||
[r' ___', r' ( )', r' ( )', r' \_/'],
|
|
||||||
[r' _', r' / \', r' { }', r' \_/'],
|
|
||||||
],
|
|
||||||
MonsterCategory.boss => [
|
|
||||||
[r' ^w^', r' (|o|)', r' /|\', r' V V'],
|
|
||||||
[r' ^W^', r' (|O|)', r' \|/', r' v v'],
|
|
||||||
],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
List<List<String>> _smallHitFrames(MonsterCategory category) {
|
|
||||||
return [
|
|
||||||
[r' *!*', r' (>_<)', r' \X/', r' _/_\_'],
|
|
||||||
[r' !*!', r' (@_@)', r' /X\', r' _\_/_'],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
List<List<String>> _smallAlertFrames(MonsterCategory category) {
|
|
||||||
return switch (category) {
|
|
||||||
MonsterCategory.bug => [
|
|
||||||
[r' /\_/\', r'( O!O )', r' > ! <', r' /| |\'],
|
|
||||||
[r' /\_/\', r'( !O! )', r' > ! <', r' \| |/'],
|
|
||||||
],
|
|
||||||
MonsterCategory.malware => [
|
|
||||||
[r' /\/\', r' (! !)', r' / \', r' \/ \/'],
|
|
||||||
[r' \/\/\', r' (! !)', r' \ /', r' /\ /\'],
|
|
||||||
],
|
|
||||||
MonsterCategory.network => [
|
|
||||||
[r' O!', r' /|\', r' / \', r' _| |_'],
|
|
||||||
[r' !O', r' \|/', r' | |', r' _/ \_'],
|
|
||||||
],
|
|
||||||
MonsterCategory.system => [
|
|
||||||
[r' _!_', r' (!_!)', r' /|\', r' _/ \_'],
|
|
||||||
[r' _!_', r' (!_!)', r' \|/', r' _| |_'],
|
|
||||||
],
|
|
||||||
MonsterCategory.crypto => [
|
|
||||||
[r' __', r' <(!!)~', r' / \', r' <_ _>'],
|
|
||||||
[r' __', r' (!!)>', r' \ /', r' <_ _>'],
|
|
||||||
],
|
|
||||||
MonsterCategory.ai => [
|
|
||||||
[r' ___', r' ( ! )', r' ( ! )', r' \_/'],
|
|
||||||
[r' _', r' /!\', r' { ! }', r' \_/'],
|
|
||||||
],
|
|
||||||
MonsterCategory.boss => [
|
|
||||||
[r' ^!^', r' (|!|)', r' /|\', r' V V'],
|
|
||||||
[r' ^!^', r' (|!|)', r' \|/', r' v v'],
|
|
||||||
],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Medium 몬스터 (6줄)
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
List<List<String>> _mediumIdleFrames(MonsterCategory category) {
|
|
||||||
return switch (category) {
|
|
||||||
MonsterCategory.bug => [
|
|
||||||
[r' /\_/\', r' ( O.O )', r' > ^ <', r' /| |\', r' | | | |', r'_|_| |_|_'],
|
|
||||||
[r' /\_/\', r' ( O O )', r' > v <', r' \| |/', r' | | | |', r'_|_| |_|_'],
|
|
||||||
],
|
|
||||||
MonsterCategory.malware => [
|
|
||||||
[r' /\/\', r' /O O\', r' \ /', r' / \', r' \/ \/', r' _/ \_'],
|
|
||||||
[r' \/\/\', r' \O O/', r' / \', r' \ /', r' /\ /\', r' _\ /_'],
|
|
||||||
],
|
|
||||||
MonsterCategory.network => [
|
|
||||||
[r' O', r' /|\', r' / \', r' | |', r' | |', r' _| |_'],
|
|
||||||
[r' O', r' \|/', r' | |', r' | |', r' | |', r' _/ \_'],
|
|
||||||
],
|
|
||||||
MonsterCategory.system => [
|
|
||||||
[r' _+_', r' (X_X)', r' /|\', r' / | \', r' | | |', r'_/ | \_'],
|
|
||||||
[r' _x_', r' (x_x)', r' \|/', r' \ | /', r' | | |', r'_\ | /_'],
|
|
||||||
],
|
|
||||||
MonsterCategory.crypto => [
|
|
||||||
[r' __', r' <(OO)~', r' / \', r' / \', r' | |', r'<__ __>'],
|
|
||||||
[r' __', r' (OO)>', r' \ /', r' \ /', r' | |', r'<__ __>'],
|
|
||||||
],
|
|
||||||
MonsterCategory.ai => [
|
|
||||||
[r' ____', r' / \', r' ( )', r' ( )', r' \ /', r' \__/'],
|
|
||||||
[r' __', r' / \', r' / \', r' { }', r' \ /', r' \__/'],
|
|
||||||
],
|
|
||||||
MonsterCategory.boss => [
|
|
||||||
[r' ^W^', r' (|O|)', r' /|\', r' / | \', r' V V', r' _/ \_'],
|
|
||||||
[r' ^w^', r' (|o|)', r' \|/', r' \ | /', r' v v', r' _\ /_'],
|
|
||||||
],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
List<List<String>> _mediumHitFrames(MonsterCategory category) {
|
|
||||||
return [
|
|
||||||
[r' *!*', r' (>.<)', r' \X/', r' / \', r' | |', r'_/_ \_\'],
|
|
||||||
[r' !*!', r' (@_@)', r' /X\', r' \ /', r' | |', r'_\_ /_/'],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
List<List<String>> _mediumAlertFrames(MonsterCategory category) {
|
|
||||||
return switch (category) {
|
|
||||||
MonsterCategory.bug => [
|
|
||||||
[r' /\_/\', r' ( O!O )', r' > ! <', r' /| |\', r' | | | |', r'_|_| |_|_'],
|
|
||||||
[r' /\_/\', r' ( !O! )', r' > ! <', r' \| |/', r' | | | |', r'_|_| |_|_'],
|
|
||||||
],
|
|
||||||
MonsterCategory.malware => [
|
|
||||||
[r' /\/\', r' /! !\', r' \ /', r' / \', r' \/ \/', r' _/ \_'],
|
|
||||||
[r' \/\/\', r' \! !/', r' / \', r' \ /', r' /\ /\', r' _\ /_'],
|
|
||||||
],
|
|
||||||
MonsterCategory.network => [
|
|
||||||
[r' O!', r' /|\', r' / \', r' | |', r' | |', r' _| |_'],
|
|
||||||
[r' !O', r' \|/', r' | |', r' | |', r' | |', r' _/ \_'],
|
|
||||||
],
|
|
||||||
MonsterCategory.system => [
|
|
||||||
[r' _!_', r' (!_!)', r' /|\', r' / | \', r' | | |', r'_/ | \_'],
|
|
||||||
[r' _!_', r' (!_!)', r' \|/', r' \ | /', r' | | |', r'_\ | /_'],
|
|
||||||
],
|
|
||||||
MonsterCategory.crypto => [
|
|
||||||
[r' __', r' <(!!)~', r' / \', r' / \', r' | |', r'<__ __>'],
|
|
||||||
[r' __', r' (!!)>', r' \ /', r' \ /', r' | |', r'<__ __>'],
|
|
||||||
],
|
|
||||||
MonsterCategory.ai => [
|
|
||||||
[r' ____', r' / ! \', r' ( ! )', r' ( ! )', r' \ /', r' \__/'],
|
|
||||||
[r' __', r' / !\', r' / ! \', r' { ! }', r' \ /', r' \__/'],
|
|
||||||
],
|
|
||||||
MonsterCategory.boss => [
|
|
||||||
[r' ^!^', r' (|!|)', r' /|\', r' / | \', r' V V', r' _/ \_'],
|
|
||||||
[r' ^!^', r' (|!|)', r' \|/', r' \ | /', r' v v', r' _\ /_'],
|
|
||||||
],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Large 몬스터 (8줄)
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
List<List<String>> _largeIdleFrames(MonsterCategory category) {
|
|
||||||
return switch (category) {
|
|
||||||
MonsterCategory.bug => [
|
|
||||||
[r' /\__/\', r' ( O O )', r' > ^^ <', r' /| |\', r' | | | |', r' | | | |', r'_| | | |_', r'|__|____|__|'],
|
|
||||||
[r' /\__/\', r' ( O O )', r' > vv <', r' \| |/', r' | | | |', r' | | | |', r'_| | | |_', r'|__|____|__|'],
|
|
||||||
],
|
|
||||||
MonsterCategory.malware => [
|
|
||||||
[r' /\/\/\', r' /O O\', r' \ /', r' / \', r' \/ \/', r' | \ / |', r' _| \/ |_', r'|___________|'],
|
|
||||||
[r' \/\/\/\', r' \O O/', r' / \', r' \ /', r' /\ /\', r' | / \ |', r' _| /\ |_', r'|___________|'],
|
|
||||||
],
|
|
||||||
MonsterCategory.network => [
|
|
||||||
[r' O', r' /|\', r' / \', r' | |', r' | |', r' | |', r' _| |_', r'|_________|'],
|
|
||||||
[r' O', r' \|/', r' | |', r' | |', r' | |', r' | |', r' _/ \_', r'|_________|'],
|
|
||||||
],
|
|
||||||
MonsterCategory.system => [
|
|
||||||
[r' _/+\_', r' (X___X)', r' /|||\', r' / ||| \', r' | ||| |', r' | / \ |', r' _|/ \|_', r'|_/ \_|'],
|
|
||||||
[r' _\+/_', r' (x___x)', r' \|||/', r' \ ||| /', r' | ||| |', r' | \ / |', r' _|\ /|_', r'|_\ /_|'],
|
|
||||||
],
|
|
||||||
MonsterCategory.crypto => [
|
|
||||||
[r' ___', r' <<(O O)~~', r' / || \', r' / || \', r' | || |', r' | || |', r' _| || |_', r'<___________|>'],
|
|
||||||
[r' ___', r' (O O)>>', r' \ || /', r' \ || /', r' | || |', r' | || |', r' _| || |_', r'<___________|>'],
|
|
||||||
],
|
|
||||||
MonsterCategory.ai => [
|
|
||||||
[r' _____', r' / \', r' / \', r' ( )', r' ( )', r' \ /', r' \_____/', r' \___/'],
|
|
||||||
[r' ___', r' / \', r' / \', r' { }', r' { }', r' \ /', r' \___/', r' \_/'],
|
|
||||||
],
|
|
||||||
MonsterCategory.boss => [
|
|
||||||
[r' ^W^', r' /|O|\', r' /|\', r' / | \', r' | | |', r' V | V', r' _/ | \_', r'|____|____|'],
|
|
||||||
[r' ^w^', r' \|o|/', r' \|/', r' \ | /', r' | | |', r' v | v', r' _\ | /_', r'|____|____|'],
|
|
||||||
],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
List<List<String>> _largeHitFrames(MonsterCategory category) {
|
|
||||||
return [
|
|
||||||
[r' *!*!*', r' (>___<)', r' \\X//', r' / \\// \', r' | \\/ |', r' | / \ |', r' _|/ \|_', r'|___/\\___|'],
|
|
||||||
[r' !*!*!', r' (@___@)', r' //X\\', r' \ /\\/ /', r' | //\\ |', r' | \ / |', r' _|\ /|_', r'|___\\/__|'],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
List<List<String>> _largeAlertFrames(MonsterCategory category) {
|
|
||||||
return switch (category) {
|
|
||||||
MonsterCategory.bug => [
|
|
||||||
[r' /\__/\', r' ( O!!O )', r' > !! <', r' /| |\', r' | | | |', r' | | | |', r'_| | | |_', r'|__|____|__|'],
|
|
||||||
[r' /\__/\', r' ( !!O! )', r' > !! <', r' \| |/', r' | | | |', r' | | | |', r'_| | | |_', r'|__|____|__|'],
|
|
||||||
],
|
|
||||||
MonsterCategory.malware => [
|
|
||||||
[r' /\/\/\', r' /! !\', r' \ /', r' / \', r' \/ \/', r' | \ / |', r' _| \/ |_', r'|___________|'],
|
|
||||||
[r' \/\/\/\', r' \! !/', r' / \', r' \ /', r' /\ /\', r' | / \ |', r' _| /\ |_', r'|___________|'],
|
|
||||||
],
|
|
||||||
MonsterCategory.network => [
|
|
||||||
[r' O!', r' /|\', r' / \', r' | |', r' | |', r' | |', r' _| |_', r'|_________|'],
|
|
||||||
[r' !O', r' \|/', r' | |', r' | |', r' | |', r' | |', r' _/ \_', r'|_________|'],
|
|
||||||
],
|
|
||||||
MonsterCategory.system => [
|
|
||||||
[r' _/!\_', r' (!___!)', r' /|||\', r' / ||| \', r' | ||| |', r' | / \ |', r' _|/ \|_', r'|_/ \_|'],
|
|
||||||
[r' _\!/_', r' (!___!)', r' \|||/', r' \ ||| /', r' | ||| |', r' | \ / |', r' _|\ /|_', r'|_\ /_|'],
|
|
||||||
],
|
|
||||||
MonsterCategory.crypto => [
|
|
||||||
[r' ___', r' <<(! !)~~', r' / || \', r' / || \', r' | || |', r' | || |', r' _| || |_', r'<___________|>'],
|
|
||||||
[r' ___', r' (! !)>>', r' \ || /', r' \ || /', r' | || |', r' | || |', r' _| || |_', r'<___________|>'],
|
|
||||||
],
|
|
||||||
MonsterCategory.ai => [
|
|
||||||
[r' _____', r' / ! \', r' / ! \', r' ( ! )', r' ( ! )', r' \ /', r' \_____/', r' \___/'],
|
|
||||||
[r' ___', r' / ! \', r' / ! \', r' { ! }', r' { ! }', r' \ /', r' \___/', r' \_/'],
|
|
||||||
],
|
|
||||||
MonsterCategory.boss => [
|
|
||||||
[r' ^!^', r' /|!|\', r' /|\', r' / | \', r' | | |', r' V | V', r' _/ | \_', r'|____|____|'],
|
|
||||||
[r' ^!^', r' \|!|/', r' \|/', r' \ | /', r' | | |', r' v | v', r' _\ | /_', r'|____|____|'],
|
|
||||||
],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Huge+ 몬스터 (8줄, 더 넓게)
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
List<List<String>> _hugeIdleFrames(MonsterCategory category) {
|
|
||||||
return switch (category) {
|
|
||||||
MonsterCategory.bug => [
|
|
||||||
[r' /\____/\', r' ( O O )', r' > ^^^^ <', r' /| |\', r' | | | |', r' | | | |', r' _| | | |_', r'|___|______|___|'],
|
|
||||||
[r' /\____/\', r' ( O O )', r' > vvvv <', r' \| |/', r' | | | |', r' | | | |', r' _| | | |_', r'|___|______|___|'],
|
|
||||||
],
|
|
||||||
MonsterCategory.malware => [
|
|
||||||
[r' /\/\/\/\', r' /O O\', r' \ /', r' / \', r' \/ \/', r' | \ / |', r' _| \ / |_', r'|_______________|'],
|
|
||||||
[r' \/\/\/\/\', r' \O O/', r' / \', r' \ /', r' /\ /\', r' | / \ |', r' _| / \ |_', r'|_______________|'],
|
|
||||||
],
|
|
||||||
MonsterCategory.network => [
|
|
||||||
[r' O', r' _/|\\_', r' / | \\', r' | |', r' | |', r' | |', r' _| |_', r'|___________|'],
|
|
||||||
[r' O', r' \\_|_/', r' \\|/', r' | |', r' | |', r' | |', r' _/ \\_', r'|___________|'],
|
|
||||||
],
|
|
||||||
MonsterCategory.system => [
|
|
||||||
[r' _/+\\_', r' (X_____X)', r' /|||||\', r' / ||||| \\', r' | ||||| |', r' | / \\ |', r' _|/ \\|_', r'|_/ \\_|'],
|
|
||||||
[r' _\\+/_', r' (x_____x)', r' \\|||||/', r' \\ ||||| /', r' | ||||| |', r' | \\ / |', r' _|\\ /|_', r'|_\\ /_|'],
|
|
||||||
],
|
|
||||||
MonsterCategory.crypto => [
|
|
||||||
[r' ____', r' <<<(O O)~~~', r' / |||| \\', r' / |||| \\', r' | |||| |', r' | |||| |', r' _| |||| |_', r'<_______________|>'],
|
|
||||||
[r' ____', r' (O O)>>>', r' \\ |||| /', r' \\ |||| /', r' | |||| |', r' | |||| |', r' _| |||| |_', r'<_______________|>'],
|
|
||||||
],
|
|
||||||
MonsterCategory.ai => [
|
|
||||||
[r' ______', r' / \\', r' / \\', r' ( )', r' ( )', r' \\ /', r' \\______/', r' \\____/'],
|
|
||||||
[r' ____', r' / \\', r' / \\', r' { }', r' { }', r' \\ /', r' \\____/', r' \\__/'],
|
|
||||||
],
|
|
||||||
MonsterCategory.boss => [
|
|
||||||
[r' ^W^', r' /|O|\\ ', r' /|\\', r' / | \\', r' | | |', r' V | V', r' _/ | \\_', r'|_____|_____|'],
|
|
||||||
[r' ^w^', r' \\|o|/', r' \\|/', r' \\ | /', r' | | |', r' v | v', r' _\\ | /_', r'|_____|_____|'],
|
|
||||||
],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
List<List<String>> _hugeHitFrames(MonsterCategory category) {
|
|
||||||
return [
|
|
||||||
[r' *!*!*!*', r' (>_____<)', r' \\\\X////', r' / \\\\// \\', r' | \\\\/ |', r' | / \\ |', r' _|/ \\|_', r'|____/\\\\___|'],
|
|
||||||
[r' !*!*!*!', r' (@_____@)', r' ////X\\\\', r' \\ /\\\\/ /', r' | ////\\\\ |', r' | \\ / |', r' _|\\ /|_', r'|____\\\\/___|'],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
List<List<String>> _hugeAlertFrames(MonsterCategory category) {
|
|
||||||
return switch (category) {
|
|
||||||
MonsterCategory.bug => [
|
|
||||||
[r' /\____/\', r' ( ! ! )', r' > !!!! <', r' /| |\', r' | | | |', r' | | | |', r' _| | | |_', r'|___|______|___|'],
|
|
||||||
[r' /\____/\', r' ( ! ! )', r' > !!!! <', r' \| |/', r' | | | |', r' | | | |', r' _| | | |_', r'|___|______|___|'],
|
|
||||||
],
|
|
||||||
MonsterCategory.malware => [
|
|
||||||
[r' /\/\/\/\', r' /! !\', r' \ /', r' / \', r' \/ \/', r' | \ / |', r' _| \ / |_', r'|_______________|'],
|
|
||||||
[r' \/\/\/\/\', r' \! !/', r' / \', r' \ /', r' /\ /\', r' | / \ |', r' _| / \ |_', r'|_______________|'],
|
|
||||||
],
|
|
||||||
MonsterCategory.network => [
|
|
||||||
[r' O!', r' _/|\\__', r' / | \\', r' | |', r' | |', r' | |', r' _| |_', r'|___________|'],
|
|
||||||
[r' !O', r' \\_|_/', r' \\|/', r' | |', r' | |', r' | |', r' _/ \\_', r'|___________|'],
|
|
||||||
],
|
|
||||||
MonsterCategory.system => [
|
|
||||||
[r' _/!\\__', r' (!_____!)', r' /|||||\', r' / ||||| \\', r' | ||||| |', r' | / \\ |', r' _|/ \\|_', r'|_/ \\_|'],
|
|
||||||
[r' _\\!/_', r' (!_____!)', r' \\|||||/', r' \\ ||||| /', r' | ||||| |', r' | \\ / |', r' _|\\ /|_', r'|_\\ /_|'],
|
|
||||||
],
|
|
||||||
MonsterCategory.crypto => [
|
|
||||||
[r' ____', r' <<<(! !)~~~', r' / |||| \\', r' / |||| \\', r' | |||| |', r' | |||| |', r' _| |||| |_', r'<_______________|>'],
|
|
||||||
[r' ____', r' (! !)>>>', r' \\ |||| /', r' \\ |||| /', r' | |||| |', r' | |||| |', r' _| |||| |_', r'<_______________|>'],
|
|
||||||
],
|
|
||||||
MonsterCategory.ai => [
|
|
||||||
[r' ______', r' / ! \\', r' / ! \\', r' ( ! )', r' ( ! )', r' \\ /', r' \\______/', r' \\____/'],
|
|
||||||
[r' ____', r' / ! \\', r' / ! \\', r' { ! }', r' { ! }', r' \\ /', r' \\____/', r' \\__/'],
|
|
||||||
],
|
|
||||||
MonsterCategory.boss => [
|
|
||||||
[r' ^!^', r' /|!|\\ ', r' /|\\', r' / | \\', r' | | |', r' V | V', r' _/ | \\_', r'|_____|_____|'],
|
|
||||||
[r' ^!^', r' \\|!|/', r' \\|/', r' \\ | /', r' | | |', r' v | v', r' _\\ | /_', r'|_____|_____|'],
|
|
||||||
],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// 레거시 호환용 함수
|
|
||||||
List<List<String>> getMonsterFrames(MonsterCategory category, MonsterSize size) {
|
|
||||||
return _getMonsterIdleFrames(category, size);
|
|
||||||
}
|
|
||||||
@@ -15,10 +15,7 @@ enum AsciiCellColor {
|
|||||||
|
|
||||||
/// 단일 ASCII 셀 데이터
|
/// 단일 ASCII 셀 데이터
|
||||||
class AsciiCell {
|
class AsciiCell {
|
||||||
const AsciiCell({
|
const AsciiCell({required this.char, this.color = AsciiCellColor.object});
|
||||||
required this.char,
|
|
||||||
this.color = AsciiCellColor.object,
|
|
||||||
});
|
|
||||||
|
|
||||||
/// 표시할 문자 (단일 문자)
|
/// 표시할 문자 (단일 문자)
|
||||||
final String char;
|
final String char;
|
||||||
@@ -45,10 +42,7 @@ class AsciiCell {
|
|||||||
/// 문자열에서 AsciiCell 생성 (자동 색상)
|
/// 문자열에서 AsciiCell 생성 (자동 색상)
|
||||||
factory AsciiCell.fromChar(String char) {
|
factory AsciiCell.fromChar(String char) {
|
||||||
if (char.isEmpty || char == ' ') return empty;
|
if (char.isEmpty || char == ' ') return empty;
|
||||||
return AsciiCell(
|
return AsciiCell(char: char, color: colorFromChar(char));
|
||||||
char: char,
|
|
||||||
color: colorFromChar(char),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -37,11 +37,7 @@ class AsciiLayer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 빈 레이어 생성
|
/// 빈 레이어 생성
|
||||||
factory AsciiLayer.empty({
|
factory AsciiLayer.empty({int width = 60, int height = 8, int zIndex = 0}) {
|
||||||
int width = 60,
|
|
||||||
int height = 8,
|
|
||||||
int zIndex = 0,
|
|
||||||
}) {
|
|
||||||
final cells = List.generate(
|
final cells = List.generate(
|
||||||
height,
|
height,
|
||||||
(_) => List.filled(width, AsciiCell.empty),
|
(_) => List.filled(width, AsciiCell.empty),
|
||||||
|
|||||||
@@ -279,8 +279,7 @@ List<List<String>> _getMonsterIdleFrames(
|
|||||||
MonsterSize.large ||
|
MonsterSize.large ||
|
||||||
MonsterSize.huge ||
|
MonsterSize.huge ||
|
||||||
MonsterSize.giant ||
|
MonsterSize.giant ||
|
||||||
MonsterSize.titanic =>
|
MonsterSize.titanic => _largeIdleFrames(category),
|
||||||
_largeIdleFrames(category),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -295,8 +294,7 @@ List<List<String>> _getMonsterHitFrames(
|
|||||||
MonsterSize.large ||
|
MonsterSize.large ||
|
||||||
MonsterSize.huge ||
|
MonsterSize.huge ||
|
||||||
MonsterSize.giant ||
|
MonsterSize.giant ||
|
||||||
MonsterSize.titanic =>
|
MonsterSize.titanic => _largeHitFrames(category),
|
||||||
_largeHitFrames(category),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -311,8 +309,7 @@ List<List<String>> _getMonsterAlertFrames(
|
|||||||
MonsterSize.large ||
|
MonsterSize.large ||
|
||||||
MonsterSize.huge ||
|
MonsterSize.huge ||
|
||||||
MonsterSize.giant ||
|
MonsterSize.giant ||
|
||||||
MonsterSize.titanic =>
|
MonsterSize.titanic => _largeAlertFrames(category),
|
||||||
_largeAlertFrames(category),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -483,7 +480,7 @@ List<List<String>> _mediumIdleFrames(MonsterCategory category) {
|
|||||||
r' > ^ <',
|
r' > ^ <',
|
||||||
r' /| |\',
|
r' /| |\',
|
||||||
r' | | | |',
|
r' | | | |',
|
||||||
r'_|_| |_|_'
|
r'_|_| |_|_',
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
r' /\_/\',
|
r' /\_/\',
|
||||||
@@ -491,7 +488,7 @@ List<List<String>> _mediumIdleFrames(MonsterCategory category) {
|
|||||||
r' > v <',
|
r' > v <',
|
||||||
r' \| |/',
|
r' \| |/',
|
||||||
r' | | | |',
|
r' | | | |',
|
||||||
r'_|_| |_|_'
|
r'_|_| |_|_',
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
MonsterCategory.malware => [
|
MonsterCategory.malware => [
|
||||||
@@ -501,7 +498,7 @@ List<List<String>> _mediumIdleFrames(MonsterCategory category) {
|
|||||||
r' \ /',
|
r' \ /',
|
||||||
r' / \',
|
r' / \',
|
||||||
r' \/ \/',
|
r' \/ \/',
|
||||||
r' _/ \_'
|
r' _/ \_',
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
r' \/\/\',
|
r' \/\/\',
|
||||||
@@ -509,7 +506,7 @@ List<List<String>> _mediumIdleFrames(MonsterCategory category) {
|
|||||||
r' / \',
|
r' / \',
|
||||||
r' \ /',
|
r' \ /',
|
||||||
r' /\ /\',
|
r' /\ /\',
|
||||||
r' _\ /_'
|
r' _\ /_',
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
MonsterCategory.network => [
|
MonsterCategory.network => [
|
||||||
@@ -517,22 +514,8 @@ List<List<String>> _mediumIdleFrames(MonsterCategory category) {
|
|||||||
[r' O', r' \|/', r' | |', r' | |', r' | |', r' _/ \_'],
|
[r' O', r' \|/', r' | |', r' | |', r' | |', r' _/ \_'],
|
||||||
],
|
],
|
||||||
MonsterCategory.system => [
|
MonsterCategory.system => [
|
||||||
[
|
[r' _+_', r' (X_X)', r' /|\', r' / | \', r' | | |', r'_/ | \_'],
|
||||||
r' _+_',
|
[r' _x_', r' (x_x)', r' \|/', r' \ | /', r' | | |', r'_\ | /_'],
|
||||||
r' (X_X)',
|
|
||||||
r' /|\',
|
|
||||||
r' / | \',
|
|
||||||
r' | | |',
|
|
||||||
r'_/ | \_'
|
|
||||||
],
|
|
||||||
[
|
|
||||||
r' _x_',
|
|
||||||
r' (x_x)',
|
|
||||||
r' \|/',
|
|
||||||
r' \ | /',
|
|
||||||
r' | | |',
|
|
||||||
r'_\ | /_'
|
|
||||||
],
|
|
||||||
],
|
],
|
||||||
MonsterCategory.crypto => [
|
MonsterCategory.crypto => [
|
||||||
[
|
[
|
||||||
@@ -541,7 +524,7 @@ List<List<String>> _mediumIdleFrames(MonsterCategory category) {
|
|||||||
r' / \',
|
r' / \',
|
||||||
r' / \',
|
r' / \',
|
||||||
r' | |',
|
r' | |',
|
||||||
r'<__ __>'
|
r'<__ __>',
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
r' __',
|
r' __',
|
||||||
@@ -549,12 +532,26 @@ List<List<String>> _mediumIdleFrames(MonsterCategory category) {
|
|||||||
r' \ /',
|
r' \ /',
|
||||||
r' \ /',
|
r' \ /',
|
||||||
r' | |',
|
r' | |',
|
||||||
r'<__ __>'
|
r'<__ __>',
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
MonsterCategory.ai => [
|
MonsterCategory.ai => [
|
||||||
[r' ____', r' / \', r' ( )', r' ( )', r' \ /', r' \__/'],
|
[
|
||||||
[r' __', r' / \', r' / \', r' { }', r' \ /', r' \__/'],
|
r' ____',
|
||||||
|
r' / \',
|
||||||
|
r' ( )',
|
||||||
|
r' ( )',
|
||||||
|
r' \ /',
|
||||||
|
r' \__/',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
r' __',
|
||||||
|
r' / \',
|
||||||
|
r' / \',
|
||||||
|
r' { }',
|
||||||
|
r' \ /',
|
||||||
|
r' \__/',
|
||||||
|
],
|
||||||
],
|
],
|
||||||
MonsterCategory.boss => [
|
MonsterCategory.boss => [
|
||||||
[r' ^W^', r' (|O|)', r' /|\', r' / | \', r' V V', r' _/ \_'],
|
[r' ^W^', r' (|O|)', r' /|\', r' / | \', r' V V', r' _/ \_'],
|
||||||
@@ -579,7 +576,7 @@ List<List<String>> _mediumAlertFrames(MonsterCategory category) {
|
|||||||
r' > ! <',
|
r' > ! <',
|
||||||
r' /| |\',
|
r' /| |\',
|
||||||
r' | | | |',
|
r' | | | |',
|
||||||
r'_|_| |_|_'
|
r'_|_| |_|_',
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
r' /\_/\',
|
r' /\_/\',
|
||||||
@@ -587,7 +584,7 @@ List<List<String>> _mediumAlertFrames(MonsterCategory category) {
|
|||||||
r' > ! <',
|
r' > ! <',
|
||||||
r' \| |/',
|
r' \| |/',
|
||||||
r' | | | |',
|
r' | | | |',
|
||||||
r'_|_| |_|_'
|
r'_|_| |_|_',
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
MonsterCategory.malware => [
|
MonsterCategory.malware => [
|
||||||
@@ -597,7 +594,7 @@ List<List<String>> _mediumAlertFrames(MonsterCategory category) {
|
|||||||
r' \ /',
|
r' \ /',
|
||||||
r' / \',
|
r' / \',
|
||||||
r' \/ \/',
|
r' \/ \/',
|
||||||
r' _/ \_'
|
r' _/ \_',
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
r' \/\/\',
|
r' \/\/\',
|
||||||
@@ -605,7 +602,7 @@ List<List<String>> _mediumAlertFrames(MonsterCategory category) {
|
|||||||
r' / \',
|
r' / \',
|
||||||
r' \ /',
|
r' \ /',
|
||||||
r' /\ /\',
|
r' /\ /\',
|
||||||
r' _\ /_'
|
r' _\ /_',
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
MonsterCategory.network => [
|
MonsterCategory.network => [
|
||||||
@@ -613,22 +610,8 @@ List<List<String>> _mediumAlertFrames(MonsterCategory category) {
|
|||||||
[r' !O', r' \|/', r' | |', r' | |', r' | |', r' _/ \_'],
|
[r' !O', r' \|/', r' | |', r' | |', r' | |', r' _/ \_'],
|
||||||
],
|
],
|
||||||
MonsterCategory.system => [
|
MonsterCategory.system => [
|
||||||
[
|
[r' _!_', r' (!_!)', r' /|\', r' / | \', r' | | |', r'_/ | \_'],
|
||||||
r' _!_',
|
[r' _!_', r' (!_!)', r' \|/', r' \ | /', r' | | |', r'_\ | /_'],
|
||||||
r' (!_!)',
|
|
||||||
r' /|\',
|
|
||||||
r' / | \',
|
|
||||||
r' | | |',
|
|
||||||
r'_/ | \_'
|
|
||||||
],
|
|
||||||
[
|
|
||||||
r' _!_',
|
|
||||||
r' (!_!)',
|
|
||||||
r' \|/',
|
|
||||||
r' \ | /',
|
|
||||||
r' | | |',
|
|
||||||
r'_\ | /_'
|
|
||||||
],
|
|
||||||
],
|
],
|
||||||
MonsterCategory.crypto => [
|
MonsterCategory.crypto => [
|
||||||
[
|
[
|
||||||
@@ -637,7 +620,7 @@ List<List<String>> _mediumAlertFrames(MonsterCategory category) {
|
|||||||
r' / \',
|
r' / \',
|
||||||
r' / \',
|
r' / \',
|
||||||
r' | |',
|
r' | |',
|
||||||
r'<__ __>'
|
r'<__ __>',
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
r' __',
|
r' __',
|
||||||
@@ -645,7 +628,7 @@ List<List<String>> _mediumAlertFrames(MonsterCategory category) {
|
|||||||
r' \ /',
|
r' \ /',
|
||||||
r' \ /',
|
r' \ /',
|
||||||
r' | |',
|
r' | |',
|
||||||
r'<__ __>'
|
r'<__ __>',
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
MonsterCategory.ai => [
|
MonsterCategory.ai => [
|
||||||
@@ -655,9 +638,16 @@ List<List<String>> _mediumAlertFrames(MonsterCategory category) {
|
|||||||
r' ( ! )',
|
r' ( ! )',
|
||||||
r' ( ! )',
|
r' ( ! )',
|
||||||
r' \ /',
|
r' \ /',
|
||||||
r' \__/'
|
r' \__/',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
r' __',
|
||||||
|
r' / !\',
|
||||||
|
r' / ! \',
|
||||||
|
r' { ! }',
|
||||||
|
r' \ /',
|
||||||
|
r' \__/',
|
||||||
],
|
],
|
||||||
[r' __', r' / !\', r' / ! \', r' { ! }', r' \ /', r' \__/'],
|
|
||||||
],
|
],
|
||||||
MonsterCategory.boss => [
|
MonsterCategory.boss => [
|
||||||
[r' ^!^', r' (|!|)', r' /|\', r' / | \', r' V V', r' _/ \_'],
|
[r' ^!^', r' (|!|)', r' /|\', r' / | \', r' V V', r' _/ \_'],
|
||||||
@@ -681,7 +671,7 @@ List<List<String>> _largeIdleFrames(MonsterCategory category) {
|
|||||||
r' | | | |',
|
r' | | | |',
|
||||||
r' | | | |',
|
r' | | | |',
|
||||||
r'_| | | |_',
|
r'_| | | |_',
|
||||||
r'|__|____|__|'
|
r'|__|____|__|',
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
r' /\__/\',
|
r' /\__/\',
|
||||||
@@ -691,7 +681,7 @@ List<List<String>> _largeIdleFrames(MonsterCategory category) {
|
|||||||
r' | | | |',
|
r' | | | |',
|
||||||
r' | | | |',
|
r' | | | |',
|
||||||
r'_| | | |_',
|
r'_| | | |_',
|
||||||
r'|__|____|__|'
|
r'|__|____|__|',
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
MonsterCategory.malware => [
|
MonsterCategory.malware => [
|
||||||
@@ -703,7 +693,7 @@ List<List<String>> _largeIdleFrames(MonsterCategory category) {
|
|||||||
r' / \',
|
r' / \',
|
||||||
r' \/ \/',
|
r' \/ \/',
|
||||||
r' _/ \_',
|
r' _/ \_',
|
||||||
r'/__ __\\'
|
r'/__ __\\',
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
r' \/\/\',
|
r' \/\/\',
|
||||||
@@ -713,7 +703,7 @@ List<List<String>> _largeIdleFrames(MonsterCategory category) {
|
|||||||
r' \ /',
|
r' \ /',
|
||||||
r' /\ /\',
|
r' /\ /\',
|
||||||
r' _\ /_',
|
r' _\ /_',
|
||||||
r'\__ __/'
|
r'\__ __/',
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
MonsterCategory.network => [
|
MonsterCategory.network => [
|
||||||
@@ -725,7 +715,7 @@ List<List<String>> _largeIdleFrames(MonsterCategory category) {
|
|||||||
r' | |',
|
r' | |',
|
||||||
r' | |',
|
r' | |',
|
||||||
r' _| |_',
|
r' _| |_',
|
||||||
r'|__ __|'
|
r'|__ __|',
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
r' O',
|
r' O',
|
||||||
@@ -735,7 +725,7 @@ List<List<String>> _largeIdleFrames(MonsterCategory category) {
|
|||||||
r' | |',
|
r' | |',
|
||||||
r' | |',
|
r' | |',
|
||||||
r' _/ \_',
|
r' _/ \_',
|
||||||
r'/__ __\\'
|
r'/__ __\\',
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
MonsterCategory.system => [
|
MonsterCategory.system => [
|
||||||
@@ -747,7 +737,7 @@ List<List<String>> _largeIdleFrames(MonsterCategory category) {
|
|||||||
r' | | |',
|
r' | | |',
|
||||||
r' | | |',
|
r' | | |',
|
||||||
r' _/ | \_',
|
r' _/ | \_',
|
||||||
r'|____|____|'
|
r'|____|____|',
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
r' _x_',
|
r' _x_',
|
||||||
@@ -757,7 +747,7 @@ List<List<String>> _largeIdleFrames(MonsterCategory category) {
|
|||||||
r' | | |',
|
r' | | |',
|
||||||
r' | | |',
|
r' | | |',
|
||||||
r' _\ | /_',
|
r' _\ | /_',
|
||||||
r'|____|____|'
|
r'|____|____|',
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
MonsterCategory.crypto => [
|
MonsterCategory.crypto => [
|
||||||
@@ -769,7 +759,7 @@ List<List<String>> _largeIdleFrames(MonsterCategory category) {
|
|||||||
r' | |',
|
r' | |',
|
||||||
r' | |',
|
r' | |',
|
||||||
r' <__ __>',
|
r' <__ __>',
|
||||||
r'|___ ___|'
|
r'|___ ___|',
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
r' ___',
|
r' ___',
|
||||||
@@ -779,7 +769,7 @@ List<List<String>> _largeIdleFrames(MonsterCategory category) {
|
|||||||
r' | |',
|
r' | |',
|
||||||
r' | |',
|
r' | |',
|
||||||
r' <__ __>',
|
r' <__ __>',
|
||||||
r'|___ ___|'
|
r'|___ ___|',
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
MonsterCategory.ai => [
|
MonsterCategory.ai => [
|
||||||
@@ -791,7 +781,7 @@ List<List<String>> _largeIdleFrames(MonsterCategory category) {
|
|||||||
r' ( )',
|
r' ( )',
|
||||||
r' \ /',
|
r' \ /',
|
||||||
r' \ /',
|
r' \ /',
|
||||||
r' \_/'
|
r' \_/',
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
r' ___',
|
r' ___',
|
||||||
@@ -801,7 +791,7 @@ List<List<String>> _largeIdleFrames(MonsterCategory category) {
|
|||||||
r' { }',
|
r' { }',
|
||||||
r' \ /',
|
r' \ /',
|
||||||
r' \ /',
|
r' \ /',
|
||||||
r' \_/'
|
r' \_/',
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
MonsterCategory.boss => [
|
MonsterCategory.boss => [
|
||||||
@@ -813,7 +803,7 @@ List<List<String>> _largeIdleFrames(MonsterCategory category) {
|
|||||||
r' | | |',
|
r' | | |',
|
||||||
r' V | V',
|
r' V | V',
|
||||||
r' _/ | \_',
|
r' _/ | \_',
|
||||||
r'|_____|_____|'
|
r'|_____|_____|',
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
r' ^w^',
|
r' ^w^',
|
||||||
@@ -823,7 +813,7 @@ List<List<String>> _largeIdleFrames(MonsterCategory category) {
|
|||||||
r' | | |',
|
r' | | |',
|
||||||
r' v | v',
|
r' v | v',
|
||||||
r' _\ | /_',
|
r' _\ | /_',
|
||||||
r'|_____|_____|'
|
r'|_____|_____|',
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
@@ -839,7 +829,7 @@ List<List<String>> _largeHitFrames(MonsterCategory category) {
|
|||||||
r' | | |',
|
r' | | |',
|
||||||
r' X | X',
|
r' X | X',
|
||||||
r' _/ | \_',
|
r' _/ | \_',
|
||||||
r'|_____|_____|'
|
r'|_____|_____|',
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
r' !*!',
|
r' !*!',
|
||||||
@@ -849,7 +839,7 @@ List<List<String>> _largeHitFrames(MonsterCategory category) {
|
|||||||
r' | | |',
|
r' | | |',
|
||||||
r' x | x',
|
r' x | x',
|
||||||
r' _\ | /_',
|
r' _\ | /_',
|
||||||
r'|_____|_____|'
|
r'|_____|_____|',
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -865,7 +855,7 @@ List<List<String>> _largeAlertFrames(MonsterCategory category) {
|
|||||||
r' | | | |',
|
r' | | | |',
|
||||||
r' | | | |',
|
r' | | | |',
|
||||||
r'_| | | |_',
|
r'_| | | |_',
|
||||||
r'|__|____|__|'
|
r'|__|____|__|',
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
r' /\__/\',
|
r' /\__/\',
|
||||||
@@ -875,7 +865,7 @@ List<List<String>> _largeAlertFrames(MonsterCategory category) {
|
|||||||
r' | | | |',
|
r' | | | |',
|
||||||
r' | | | |',
|
r' | | | |',
|
||||||
r'_| | | |_',
|
r'_| | | |_',
|
||||||
r'|__|____|__|'
|
r'|__|____|__|',
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
MonsterCategory.malware => [
|
MonsterCategory.malware => [
|
||||||
@@ -887,7 +877,7 @@ List<List<String>> _largeAlertFrames(MonsterCategory category) {
|
|||||||
r' / \',
|
r' / \',
|
||||||
r' \/ \/',
|
r' \/ \/',
|
||||||
r' _/ \_',
|
r' _/ \_',
|
||||||
r'/__ __\\'
|
r'/__ __\\',
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
r' \/\/\',
|
r' \/\/\',
|
||||||
@@ -897,7 +887,7 @@ List<List<String>> _largeAlertFrames(MonsterCategory category) {
|
|||||||
r' \ /',
|
r' \ /',
|
||||||
r' /\ /\',
|
r' /\ /\',
|
||||||
r' _\ /_',
|
r' _\ /_',
|
||||||
r'\__ __/'
|
r'\__ __/',
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
MonsterCategory.network => [
|
MonsterCategory.network => [
|
||||||
@@ -909,7 +899,7 @@ List<List<String>> _largeAlertFrames(MonsterCategory category) {
|
|||||||
r' | |',
|
r' | |',
|
||||||
r' | |',
|
r' | |',
|
||||||
r' _| |_',
|
r' _| |_',
|
||||||
r'|__ __|'
|
r'|__ __|',
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
r' !O',
|
r' !O',
|
||||||
@@ -919,7 +909,7 @@ List<List<String>> _largeAlertFrames(MonsterCategory category) {
|
|||||||
r' | |',
|
r' | |',
|
||||||
r' | |',
|
r' | |',
|
||||||
r' _/ \_',
|
r' _/ \_',
|
||||||
r'/__ __\\'
|
r'/__ __\\',
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
MonsterCategory.system => [
|
MonsterCategory.system => [
|
||||||
@@ -931,7 +921,7 @@ List<List<String>> _largeAlertFrames(MonsterCategory category) {
|
|||||||
r' | | |',
|
r' | | |',
|
||||||
r' | | |',
|
r' | | |',
|
||||||
r' _/ | \_',
|
r' _/ | \_',
|
||||||
r'|____|____|'
|
r'|____|____|',
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
r' _!_',
|
r' _!_',
|
||||||
@@ -941,7 +931,7 @@ List<List<String>> _largeAlertFrames(MonsterCategory category) {
|
|||||||
r' | | |',
|
r' | | |',
|
||||||
r' | | |',
|
r' | | |',
|
||||||
r' _\ | /_',
|
r' _\ | /_',
|
||||||
r'|____|____|'
|
r'|____|____|',
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
MonsterCategory.crypto => [
|
MonsterCategory.crypto => [
|
||||||
@@ -953,7 +943,7 @@ List<List<String>> _largeAlertFrames(MonsterCategory category) {
|
|||||||
r' | |',
|
r' | |',
|
||||||
r' | |',
|
r' | |',
|
||||||
r' <__ __>',
|
r' <__ __>',
|
||||||
r'|___ ___|'
|
r'|___ ___|',
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
r' ___',
|
r' ___',
|
||||||
@@ -963,7 +953,7 @@ List<List<String>> _largeAlertFrames(MonsterCategory category) {
|
|||||||
r' | |',
|
r' | |',
|
||||||
r' | |',
|
r' | |',
|
||||||
r' <__ __>',
|
r' <__ __>',
|
||||||
r'|___ ___|'
|
r'|___ ___|',
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
MonsterCategory.ai => [
|
MonsterCategory.ai => [
|
||||||
@@ -975,7 +965,7 @@ List<List<String>> _largeAlertFrames(MonsterCategory category) {
|
|||||||
r' ( ! )',
|
r' ( ! )',
|
||||||
r' \ /',
|
r' \ /',
|
||||||
r' \ /',
|
r' \ /',
|
||||||
r' \_/'
|
r' \_/',
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
r' ___',
|
r' ___',
|
||||||
@@ -985,7 +975,7 @@ List<List<String>> _largeAlertFrames(MonsterCategory category) {
|
|||||||
r' { ! }',
|
r' { ! }',
|
||||||
r' \ /',
|
r' \ /',
|
||||||
r' \ /',
|
r' \ /',
|
||||||
r' \_/'
|
r' \_/',
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
MonsterCategory.boss => [
|
MonsterCategory.boss => [
|
||||||
@@ -997,7 +987,7 @@ List<List<String>> _largeAlertFrames(MonsterCategory category) {
|
|||||||
r' | | |',
|
r' | | |',
|
||||||
r' V | V',
|
r' V | V',
|
||||||
r' _/ | \_',
|
r' _/ | \_',
|
||||||
r'|_____|_____|'
|
r'|_____|_____|',
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
r' ^!^',
|
r' ^!^',
|
||||||
@@ -1007,7 +997,7 @@ List<List<String>> _largeAlertFrames(MonsterCategory category) {
|
|||||||
r' | | |',
|
r' | | |',
|
||||||
r' v | v',
|
r' v | v',
|
||||||
r' _\ | /_',
|
r' _\ | /_',
|
||||||
r'|_____|_____|'
|
r'|_____|_____|',
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -20,12 +20,18 @@ class CanvasSpecialComposer {
|
|||||||
) {
|
) {
|
||||||
return switch (type) {
|
return switch (type) {
|
||||||
AsciiAnimationType.levelUp => _composeLevelUp(frameIndex, globalTick),
|
AsciiAnimationType.levelUp => _composeLevelUp(frameIndex, globalTick),
|
||||||
AsciiAnimationType.questComplete =>
|
AsciiAnimationType.questComplete => _composeQuestComplete(
|
||||||
_composeQuestComplete(frameIndex, globalTick),
|
frameIndex,
|
||||||
AsciiAnimationType.actComplete =>
|
globalTick,
|
||||||
_composeActComplete(frameIndex, globalTick),
|
),
|
||||||
AsciiAnimationType.resurrection =>
|
AsciiAnimationType.actComplete => _composeActComplete(
|
||||||
_composeResurrection(frameIndex, globalTick),
|
frameIndex,
|
||||||
|
globalTick,
|
||||||
|
),
|
||||||
|
AsciiAnimationType.resurrection => _composeResurrection(
|
||||||
|
frameIndex,
|
||||||
|
globalTick,
|
||||||
|
),
|
||||||
_ => [AsciiLayer.empty()],
|
_ => [AsciiLayer.empty()],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -44,7 +50,8 @@ class CanvasSpecialComposer {
|
|||||||
final layers = <AsciiLayer>[
|
final layers = <AsciiLayer>[
|
||||||
_createEffectBackground(globalTick, '+'),
|
_createEffectBackground(globalTick, '+'),
|
||||||
_createCenteredSprite(
|
_createCenteredSprite(
|
||||||
_questCompleteFrames[frameIndex % _questCompleteFrames.length]),
|
_questCompleteFrames[frameIndex % _questCompleteFrames.length],
|
||||||
|
),
|
||||||
];
|
];
|
||||||
return layers;
|
return layers;
|
||||||
}
|
}
|
||||||
@@ -54,7 +61,8 @@ class CanvasSpecialComposer {
|
|||||||
final layers = <AsciiLayer>[
|
final layers = <AsciiLayer>[
|
||||||
_createEffectBackground(globalTick, '~'),
|
_createEffectBackground(globalTick, '~'),
|
||||||
_createCenteredSprite(
|
_createCenteredSprite(
|
||||||
_actCompleteFrames[frameIndex % _actCompleteFrames.length]),
|
_actCompleteFrames[frameIndex % _actCompleteFrames.length],
|
||||||
|
),
|
||||||
];
|
];
|
||||||
return layers;
|
return layers;
|
||||||
}
|
}
|
||||||
@@ -64,7 +72,8 @@ class CanvasSpecialComposer {
|
|||||||
final layers = <AsciiLayer>[
|
final layers = <AsciiLayer>[
|
||||||
_createEffectBackground(globalTick, '.'),
|
_createEffectBackground(globalTick, '.'),
|
||||||
_createCenteredSprite(
|
_createCenteredSprite(
|
||||||
_resurrectionFrames[frameIndex % _resurrectionFrames.length]),
|
_resurrectionFrames[frameIndex % _resurrectionFrames.length],
|
||||||
|
),
|
||||||
];
|
];
|
||||||
return layers;
|
return layers;
|
||||||
}
|
}
|
||||||
@@ -119,41 +128,11 @@ class CanvasSpecialComposer {
|
|||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
const _levelUpFrames = [
|
const _levelUpFrames = [
|
||||||
[
|
[r' * ', r' \|/ ', r' o ', r' /|\ ', r' / \ '],
|
||||||
r' * ',
|
[r' * * ', r' \|/ ', r' O ', r' </|\> ', r' / \ '],
|
||||||
r' \|/ ',
|
[r' * * * ', r' \|/ ', r' O ', r' <\|/> ', r' / \ '],
|
||||||
r' o ',
|
[r' * * * * ', r' LEVEL ', r' UP! ', r' \O/ ', r' / \ '],
|
||||||
r' /|\ ',
|
[r'* * * * *', r' LEVEL ', r' UP! ', r' \O/ ', r' | | '],
|
||||||
r' / \ ',
|
|
||||||
],
|
|
||||||
[
|
|
||||||
r' * * ',
|
|
||||||
r' \|/ ',
|
|
||||||
r' O ',
|
|
||||||
r' </|\> ',
|
|
||||||
r' / \ ',
|
|
||||||
],
|
|
||||||
[
|
|
||||||
r' * * * ',
|
|
||||||
r' \|/ ',
|
|
||||||
r' O ',
|
|
||||||
r' <\|/> ',
|
|
||||||
r' / \ ',
|
|
||||||
],
|
|
||||||
[
|
|
||||||
r' * * * * ',
|
|
||||||
r' LEVEL ',
|
|
||||||
r' UP! ',
|
|
||||||
r' \O/ ',
|
|
||||||
r' / \ ',
|
|
||||||
],
|
|
||||||
[
|
|
||||||
r'* * * * *',
|
|
||||||
r' LEVEL ',
|
|
||||||
r' UP! ',
|
|
||||||
r' \O/ ',
|
|
||||||
r' | | ',
|
|
||||||
],
|
|
||||||
];
|
];
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -161,34 +140,10 @@ const _levelUpFrames = [
|
|||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
const _questCompleteFrames = [
|
const _questCompleteFrames = [
|
||||||
[
|
[r' [?] ', r' | ', r' o ', r' /|\ ', r' / \ '],
|
||||||
r' [?] ',
|
[r' [???] ', r' | ', r' o! ', r' /|\ ', r' / \ '],
|
||||||
r' | ',
|
[r' [DONE] ', r' ! ', r' \o/ ', r' | ', r' / \ '],
|
||||||
r' o ',
|
[r' +[DONE]+', r' \!/ ', r' \o/ ', r' | ', r' / \ '],
|
||||||
r' /|\ ',
|
|
||||||
r' / \ ',
|
|
||||||
],
|
|
||||||
[
|
|
||||||
r' [???] ',
|
|
||||||
r' | ',
|
|
||||||
r' o! ',
|
|
||||||
r' /|\ ',
|
|
||||||
r' / \ ',
|
|
||||||
],
|
|
||||||
[
|
|
||||||
r' [DONE] ',
|
|
||||||
r' ! ',
|
|
||||||
r' \o/ ',
|
|
||||||
r' | ',
|
|
||||||
r' / \ ',
|
|
||||||
],
|
|
||||||
[
|
|
||||||
r' +[DONE]+',
|
|
||||||
r' \!/ ',
|
|
||||||
r' \o/ ',
|
|
||||||
r' | ',
|
|
||||||
r' / \ ',
|
|
||||||
],
|
|
||||||
];
|
];
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -196,34 +151,10 @@ const _questCompleteFrames = [
|
|||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
const _actCompleteFrames = [
|
const _actCompleteFrames = [
|
||||||
[
|
[r'=========', r' ACT ', r' CLEAR ', r' o ', r' /|\ '],
|
||||||
r'=========',
|
[r'~~~~~~~~~', r' ACT ', r' CLEAR! ', r' \o/ ', r' | '],
|
||||||
r' ACT ',
|
[r'*~*~*~*~*', r' ACT ', r' CLEAR!! ', r' \O/ ', r' / \ '],
|
||||||
r' CLEAR ',
|
[r'*********', r' ACT ', r' CLEAR!! ', r' \O/ ', r' | | '],
|
||||||
r' o ',
|
|
||||||
r' /|\ ',
|
|
||||||
],
|
|
||||||
[
|
|
||||||
r'~~~~~~~~~',
|
|
||||||
r' ACT ',
|
|
||||||
r' CLEAR! ',
|
|
||||||
r' \o/ ',
|
|
||||||
r' | ',
|
|
||||||
],
|
|
||||||
[
|
|
||||||
r'*~*~*~*~*',
|
|
||||||
r' ACT ',
|
|
||||||
r' CLEAR!! ',
|
|
||||||
r' \O/ ',
|
|
||||||
r' / \ ',
|
|
||||||
],
|
|
||||||
[
|
|
||||||
r'*********',
|
|
||||||
r' ACT ',
|
|
||||||
r' CLEAR!! ',
|
|
||||||
r' \O/ ',
|
|
||||||
r' | | ',
|
|
||||||
],
|
|
||||||
];
|
];
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -232,38 +163,13 @@ const _actCompleteFrames = [
|
|||||||
|
|
||||||
const _resurrectionFrames = [
|
const _resurrectionFrames = [
|
||||||
// 프레임 1: R.I.P 묘비
|
// 프레임 1: R.I.P 묘비
|
||||||
[
|
[r' ___ ', r' |RIP| ', r' | | ', r'__|___|__'],
|
||||||
r' ___ ',
|
|
||||||
r' |RIP| ',
|
|
||||||
r' | | ',
|
|
||||||
r'__|___|__',
|
|
||||||
],
|
|
||||||
// 프레임 2: 빛 내림
|
// 프레임 2: 빛 내림
|
||||||
[
|
[r' \|/ ', r' -|R|- ', r' | | ', r'__|___|__'],
|
||||||
r' \|/ ',
|
|
||||||
r' -|R|- ',
|
|
||||||
r' | | ',
|
|
||||||
r'__|___|__',
|
|
||||||
],
|
|
||||||
// 프레임 3: 일어남
|
// 프레임 3: 일어남
|
||||||
[
|
[r' \o/ ', r' --|-- ', r' | | ', r'__|___|__'],
|
||||||
r' \o/ ',
|
|
||||||
r' --|-- ',
|
|
||||||
r' | | ',
|
|
||||||
r'__|___|__',
|
|
||||||
],
|
|
||||||
// 프레임 4: 서있음
|
// 프레임 4: 서있음
|
||||||
[
|
[r' o ', r' /|\ ', r' / \ ', r'_________'],
|
||||||
r' o ',
|
|
||||||
r' /|\ ',
|
|
||||||
r' / \ ',
|
|
||||||
r'_________',
|
|
||||||
],
|
|
||||||
// 프레임 5: 부활 완료
|
// 프레임 5: 부활 완료
|
||||||
[
|
[r' REVIVED ', r' \o/ ', r' | ', r'___/ \___'],
|
||||||
r' REVIVED ',
|
|
||||||
r' \o/ ',
|
|
||||||
r' | ',
|
|
||||||
r'___/ \___',
|
|
||||||
],
|
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -63,12 +63,7 @@ class CanvasTownComposer {
|
|||||||
const shopX = 32;
|
const shopX = 32;
|
||||||
final shopY = frameHeight - cells.length - 1;
|
final shopY = frameHeight - cells.length - 1;
|
||||||
|
|
||||||
return AsciiLayer(
|
return AsciiLayer(cells: cells, zIndex: 1, offsetX: shopX, offsetY: shopY);
|
||||||
cells: cells,
|
|
||||||
zIndex: 1,
|
|
||||||
offsetX: shopX,
|
|
||||||
offsetY: shopY,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 캐릭터 레이어 생성 (z=2)
|
/// 캐릭터 레이어 생성 (z=2)
|
||||||
@@ -82,12 +77,7 @@ class CanvasTownComposer {
|
|||||||
const charX = 25;
|
const charX = 25;
|
||||||
final charY = frameHeight - cells.length - 1;
|
final charY = frameHeight - cells.length - 1;
|
||||||
|
|
||||||
return AsciiLayer(
|
return AsciiLayer(cells: cells, zIndex: 2, offsetX: charX, offsetY: charY);
|
||||||
cells: cells,
|
|
||||||
zIndex: 2,
|
|
||||||
offsetX: charX,
|
|
||||||
offsetY: charY,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 문자열 스프라이트를 AsciiCell 2D 배열로 변환
|
/// 문자열 스프라이트를 AsciiCell 2D 배열로 변환
|
||||||
@@ -104,27 +94,11 @@ class CanvasTownComposer {
|
|||||||
|
|
||||||
const _shopIdleFrames = [
|
const _shopIdleFrames = [
|
||||||
// 프레임 1: 기본
|
// 프레임 1: 기본
|
||||||
[
|
[r' o ', r' /|\ ', r' / \ '],
|
||||||
r' o ',
|
|
||||||
r' /|\ ',
|
|
||||||
r' / \ ',
|
|
||||||
],
|
|
||||||
// 프레임 2: 머리 숙임
|
// 프레임 2: 머리 숙임
|
||||||
[
|
[r' o ', r' /|~ ', r' / \ '],
|
||||||
r' o ',
|
|
||||||
r' /|~ ',
|
|
||||||
r' / \ ',
|
|
||||||
],
|
|
||||||
// 프레임 3: 물건 보기
|
// 프레임 3: 물건 보기
|
||||||
[
|
[r' o? ', r' /| ', r' / \ '],
|
||||||
r' o? ',
|
|
||||||
r' /| ',
|
|
||||||
r' / \ ',
|
|
||||||
],
|
|
||||||
// 프레임 4: 고개 끄덕
|
// 프레임 4: 고개 끄덕
|
||||||
[
|
[r' o! ', r' /|\ ', r' / \ '],
|
||||||
r' o! ',
|
|
||||||
r' /|\ ',
|
|
||||||
r' / \ ',
|
|
||||||
],
|
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -62,26 +62,10 @@ CharacterFrame getCharacterFrame(BattlePhase phase, int subFrame) {
|
|||||||
// 구조: [머리, 몸통+팔, 다리]
|
// 구조: [머리, 몸통+팔, 다리]
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
const _idleFrames = [
|
const _idleFrames = [
|
||||||
CharacterFrame([
|
CharacterFrame([r' o ', r' /|\ ', r' / \ ']),
|
||||||
r' o ',
|
CharacterFrame([r' o ', r' /|\ ', r' | | ']),
|
||||||
r' /|\ ',
|
CharacterFrame([r' o ', r' /|\ ', r' / \ ']),
|
||||||
r' / \ ',
|
CharacterFrame([r' O ', r' /|\ ', r' / \ ']),
|
||||||
]),
|
|
||||||
CharacterFrame([
|
|
||||||
r' o ',
|
|
||||||
r' /|\ ',
|
|
||||||
r' | | ',
|
|
||||||
]),
|
|
||||||
CharacterFrame([
|
|
||||||
r' o ',
|
|
||||||
r' /|\ ',
|
|
||||||
r' / \ ',
|
|
||||||
]),
|
|
||||||
CharacterFrame([
|
|
||||||
r' O ',
|
|
||||||
r' /|\ ',
|
|
||||||
r' / \ ',
|
|
||||||
]),
|
|
||||||
];
|
];
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -89,21 +73,9 @@ const _idleFrames = [
|
|||||||
// 구조: [머리, 몸통+팔, 다리]
|
// 구조: [머리, 몸통+팔, 다리]
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
const _prepareFrames = [
|
const _prepareFrames = [
|
||||||
CharacterFrame([
|
CharacterFrame([r' o ', r' \|\ ', r' / \ ']),
|
||||||
r' o ',
|
CharacterFrame([r' o_ ', r' \| ', r' / \ ']),
|
||||||
r' \|\ ',
|
CharacterFrame([r' o/ ', r' \| ', r' / \ ']),
|
||||||
r' / \ ',
|
|
||||||
]),
|
|
||||||
CharacterFrame([
|
|
||||||
r' o_ ',
|
|
||||||
r' \| ',
|
|
||||||
r' / \ ',
|
|
||||||
]),
|
|
||||||
CharacterFrame([
|
|
||||||
r' o/ ',
|
|
||||||
r' \| ',
|
|
||||||
r' / \ ',
|
|
||||||
]),
|
|
||||||
];
|
];
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -112,31 +84,11 @@ const _prepareFrames = [
|
|||||||
// 수정: 공격 이펙트를 머리 줄로 통일 (1칸 위로)
|
// 수정: 공격 이펙트를 머리 줄로 통일 (1칸 위로)
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
const _attackFrames = [
|
const _attackFrames = [
|
||||||
CharacterFrame([
|
CharacterFrame([r' o\ ', r' /| ', r' / \ ']),
|
||||||
r' o\ ',
|
CharacterFrame([r' o- ', r' /| ', r' / \ ']),
|
||||||
r' /| ',
|
CharacterFrame([r' o-- ', r' /| ', r' / \ ']),
|
||||||
r' / \ ',
|
CharacterFrame([r' o-=>', r' /| ', r' / \ ']),
|
||||||
]),
|
CharacterFrame([r' o ', r' /|\ ', r' / \ ']),
|
||||||
CharacterFrame([
|
|
||||||
r' o- ',
|
|
||||||
r' /| ',
|
|
||||||
r' / \ ',
|
|
||||||
]),
|
|
||||||
CharacterFrame([
|
|
||||||
r' o-- ',
|
|
||||||
r' /| ',
|
|
||||||
r' / \ ',
|
|
||||||
]),
|
|
||||||
CharacterFrame([
|
|
||||||
r' o-=>',
|
|
||||||
r' /| ',
|
|
||||||
r' / \ ',
|
|
||||||
]),
|
|
||||||
CharacterFrame([
|
|
||||||
r' o ',
|
|
||||||
r' /|\ ',
|
|
||||||
r' / \ ',
|
|
||||||
]),
|
|
||||||
];
|
];
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -145,21 +97,9 @@ const _attackFrames = [
|
|||||||
// 수정: 히트 이펙트를 머리 줄로 통일 (1칸 위로)
|
// 수정: 히트 이펙트를 머리 줄로 통일 (1칸 위로)
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
const _hitFrames = [
|
const _hitFrames = [
|
||||||
CharacterFrame([
|
CharacterFrame([r' o-* ', r' /| ', r' / \ ']),
|
||||||
r' o-* ',
|
CharacterFrame([r' o=* ', r' /| ', r' / \ ']),
|
||||||
r' /| ',
|
CharacterFrame([r' o~* ', r' /| ', r' / \ ']),
|
||||||
r' / \ ',
|
|
||||||
]),
|
|
||||||
CharacterFrame([
|
|
||||||
r' o=* ',
|
|
||||||
r' /| ',
|
|
||||||
r' / \ ',
|
|
||||||
]),
|
|
||||||
CharacterFrame([
|
|
||||||
r' o~* ',
|
|
||||||
r' /| ',
|
|
||||||
r' / \ ',
|
|
||||||
]),
|
|
||||||
];
|
];
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -167,19 +107,7 @@ const _hitFrames = [
|
|||||||
// 구조: [머리, 몸통+팔, 다리]
|
// 구조: [머리, 몸통+팔, 다리]
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
const _recoverFrames = [
|
const _recoverFrames = [
|
||||||
CharacterFrame([
|
CharacterFrame([r' o ', r' /|\ ', r' | ']),
|
||||||
r' o ',
|
CharacterFrame([r' o ', r' /|\ ', r' / \ ']),
|
||||||
r' /|\ ',
|
CharacterFrame([r' o ', r' /|\ ', r' / \ ']),
|
||||||
r' | ',
|
|
||||||
]),
|
|
||||||
CharacterFrame([
|
|
||||||
r' o ',
|
|
||||||
r' /|\ ',
|
|
||||||
r' / \ ',
|
|
||||||
]),
|
|
||||||
CharacterFrame([
|
|
||||||
r' o ',
|
|
||||||
r' /|\ ',
|
|
||||||
r' / \ ',
|
|
||||||
]),
|
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,192 +0,0 @@
|
|||||||
// 몬스터 카테고리별 색상 시스템
|
|
||||||
// 각 몬스터 카테고리에 따라 다른 색상 적용
|
|
||||||
|
|
||||||
import 'dart:ui';
|
|
||||||
|
|
||||||
/// 몬스터 카테고리 (ascii_animation_data.dart의 MonsterCategory와 매칭)
|
|
||||||
enum MonsterColorCategory {
|
|
||||||
beast,
|
|
||||||
insect,
|
|
||||||
humanoid,
|
|
||||||
undead,
|
|
||||||
dragon,
|
|
||||||
slime,
|
|
||||||
demon,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 몬스터 색상 정보
|
|
||||||
class MonsterColors {
|
|
||||||
const MonsterColors({
|
|
||||||
required this.normal,
|
|
||||||
required this.hit,
|
|
||||||
});
|
|
||||||
|
|
||||||
/// 일반 상태 색상
|
|
||||||
final Color normal;
|
|
||||||
|
|
||||||
/// 피격 상태 색상
|
|
||||||
final Color hit;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 카테고리별 몬스터 색상 반환
|
|
||||||
MonsterColors getMonsterColors(MonsterColorCategory category) {
|
|
||||||
return switch (category) {
|
|
||||||
MonsterColorCategory.beast => const MonsterColors(
|
|
||||||
normal: Color(0xFF00FF00), // 녹색
|
|
||||||
hit: Color(0xFFFF0000), // 빨강
|
|
||||||
),
|
|
||||||
MonsterColorCategory.insect => const MonsterColors(
|
|
||||||
normal: Color(0xFFFFFF00), // 노랑
|
|
||||||
hit: Color(0xFFFF6600), // 주황
|
|
||||||
),
|
|
||||||
MonsterColorCategory.humanoid => const MonsterColors(
|
|
||||||
normal: Color(0xFF00FFFF), // 시안
|
|
||||||
hit: Color(0xFFFF00FF), // 마젠타
|
|
||||||
),
|
|
||||||
MonsterColorCategory.undead => const MonsterColors(
|
|
||||||
normal: Color(0xFF9966FF), // 보라
|
|
||||||
hit: Color(0xFFCCCCCC), // 회색
|
|
||||||
),
|
|
||||||
MonsterColorCategory.dragon => const MonsterColors(
|
|
||||||
normal: Color(0xFFFF6600), // 주황
|
|
||||||
hit: Color(0xFFFFFF00), // 노랑
|
|
||||||
),
|
|
||||||
MonsterColorCategory.slime => const MonsterColors(
|
|
||||||
normal: Color(0xFF66FF66), // 연녹색
|
|
||||||
hit: Color(0xFF00CC00), // 진녹색
|
|
||||||
),
|
|
||||||
MonsterColorCategory.demon => const MonsterColors(
|
|
||||||
normal: Color(0xFFFF0066), // 핑크
|
|
||||||
hit: Color(0xFFFFFFFF), // 흰색
|
|
||||||
),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 몬스터 기본 이름에서 색상 카테고리 추론
|
|
||||||
///
|
|
||||||
/// ascii_animation_data.dart의 getMonsterCategory 결과를 변환
|
|
||||||
MonsterColorCategory getMonsterColorCategory(String? baseName) {
|
|
||||||
if (baseName == null || baseName.isEmpty) {
|
|
||||||
return MonsterColorCategory.beast;
|
|
||||||
}
|
|
||||||
|
|
||||||
final lower = baseName.toLowerCase();
|
|
||||||
|
|
||||||
// insect (곤충류)
|
|
||||||
if (_matchesAny(lower, _insectKeywords)) {
|
|
||||||
return MonsterColorCategory.insect;
|
|
||||||
}
|
|
||||||
|
|
||||||
// undead (언데드)
|
|
||||||
if (_matchesAny(lower, _undeadKeywords)) {
|
|
||||||
return MonsterColorCategory.undead;
|
|
||||||
}
|
|
||||||
|
|
||||||
// dragon (드래곤류)
|
|
||||||
if (_matchesAny(lower, _dragonKeywords)) {
|
|
||||||
return MonsterColorCategory.dragon;
|
|
||||||
}
|
|
||||||
|
|
||||||
// slime (슬라임류)
|
|
||||||
if (_matchesAny(lower, _slimeKeywords)) {
|
|
||||||
return MonsterColorCategory.slime;
|
|
||||||
}
|
|
||||||
|
|
||||||
// demon (악마류)
|
|
||||||
if (_matchesAny(lower, _demonKeywords)) {
|
|
||||||
return MonsterColorCategory.demon;
|
|
||||||
}
|
|
||||||
|
|
||||||
// humanoid (인간형)
|
|
||||||
if (_matchesAny(lower, _humanoidKeywords)) {
|
|
||||||
return MonsterColorCategory.humanoid;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 기본은 beast
|
|
||||||
return MonsterColorCategory.beast;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool _matchesAny(String text, List<String> keywords) {
|
|
||||||
return keywords.any((kw) => text.contains(kw));
|
|
||||||
}
|
|
||||||
|
|
||||||
const _insectKeywords = [
|
|
||||||
'bug',
|
|
||||||
'beetle',
|
|
||||||
'spider',
|
|
||||||
'ant',
|
|
||||||
'bee',
|
|
||||||
'wasp',
|
|
||||||
'moth',
|
|
||||||
'worm',
|
|
||||||
'larva',
|
|
||||||
'crawler',
|
|
||||||
'centipede',
|
|
||||||
'scorpion',
|
|
||||||
];
|
|
||||||
|
|
||||||
const _undeadKeywords = [
|
|
||||||
'zombie',
|
|
||||||
'skeleton',
|
|
||||||
'ghost',
|
|
||||||
'wraith',
|
|
||||||
'vampire',
|
|
||||||
'lich',
|
|
||||||
'specter',
|
|
||||||
'phantom',
|
|
||||||
'revenant',
|
|
||||||
'undead',
|
|
||||||
'corpse',
|
|
||||||
'bone',
|
|
||||||
];
|
|
||||||
|
|
||||||
const _dragonKeywords = [
|
|
||||||
'dragon',
|
|
||||||
'drake',
|
|
||||||
'wyrm',
|
|
||||||
'wyvern',
|
|
||||||
'serpent',
|
|
||||||
'hydra',
|
|
||||||
'basilisk',
|
|
||||||
];
|
|
||||||
|
|
||||||
const _slimeKeywords = [
|
|
||||||
'slime',
|
|
||||||
'ooze',
|
|
||||||
'blob',
|
|
||||||
'jelly',
|
|
||||||
'pudding',
|
|
||||||
'gel',
|
|
||||||
'goo',
|
|
||||||
];
|
|
||||||
|
|
||||||
const _demonKeywords = [
|
|
||||||
'demon',
|
|
||||||
'devil',
|
|
||||||
'imp',
|
|
||||||
'fiend',
|
|
||||||
'daemon',
|
|
||||||
'succubus',
|
|
||||||
'incubus',
|
|
||||||
'hell',
|
|
||||||
'infernal',
|
|
||||||
];
|
|
||||||
|
|
||||||
const _humanoidKeywords = [
|
|
||||||
'goblin',
|
|
||||||
'orc',
|
|
||||||
'troll',
|
|
||||||
'ogre',
|
|
||||||
'giant',
|
|
||||||
'bandit',
|
|
||||||
'knight',
|
|
||||||
'mage',
|
|
||||||
'wizard',
|
|
||||||
'warrior',
|
|
||||||
'guard',
|
|
||||||
'soldier',
|
|
||||||
'cultist',
|
|
||||||
'hacker',
|
|
||||||
'admin',
|
|
||||||
'user',
|
|
||||||
];
|
|
||||||
@@ -65,12 +65,7 @@ bool _matchesAny(String text, List<String> keywords) {
|
|||||||
|
|
||||||
// 카테고리별 키워드 목록
|
// 카테고리별 키워드 목록
|
||||||
|
|
||||||
const _cosmicKeywords = [
|
const _cosmicKeywords = ['dyson', 'black hole', 'universe', 'singularity'];
|
||||||
'dyson',
|
|
||||||
'black hole',
|
|
||||||
'universe',
|
|
||||||
'singularity',
|
|
||||||
];
|
|
||||||
|
|
||||||
const _cableKeywords = [
|
const _cableKeywords = [
|
||||||
'cable',
|
'cable',
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ const _bluntEffect = WeaponEffect(
|
|||||||
[r' _/ ', r' / ', r'/ '],
|
[r' _/ ', r' / ', r'/ '],
|
||||||
[r' /__ ', r'/ ', r' '],
|
[r' /__ ', r'/ ', r' '],
|
||||||
[r'/__ ', r' ', r' '],
|
[r'/__ ', r' ', r' '],
|
||||||
[r'/__=>', r' ', r' '],
|
[r'/__=>', r' ', r' '],
|
||||||
],
|
],
|
||||||
hitFrames: [
|
hitFrames: [
|
||||||
[r' *BASH* ', r'/__=> ', r' '],
|
[r' *BASH* ', r'/__=> ', r' '],
|
||||||
|
|||||||
@@ -24,7 +24,8 @@ class CombatCalculator {
|
|||||||
/// [attacker] 공격자 (플레이어) 스탯
|
/// [attacker] 공격자 (플레이어) 스탯
|
||||||
/// [defender] 방어자 (몬스터) 스탯
|
/// [defender] 방어자 (몬스터) 스탯
|
||||||
/// Returns: 공격 결과 및 업데이트된 몬스터 스탯
|
/// Returns: 공격 결과 및 업데이트된 몬스터 스탯
|
||||||
({AttackResult result, MonsterCombatStats updatedDefender}) playerAttackMonster({
|
({AttackResult result, MonsterCombatStats updatedDefender})
|
||||||
|
playerAttackMonster({
|
||||||
required CombatStats attacker,
|
required CombatStats attacker,
|
||||||
required MonsterCombatStats defender,
|
required MonsterCombatStats defender,
|
||||||
}) {
|
}) {
|
||||||
@@ -178,7 +179,8 @@ class CombatCalculator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 전투 종료 체크
|
// 전투 종료 체크
|
||||||
final isCombatOver = currentPlayerStats.isDead || currentMonsterStats.isDead;
|
final isCombatOver =
|
||||||
|
currentPlayerStats.isDead || currentMonsterStats.isDead;
|
||||||
final isPlayerVictory = isCombatOver && currentMonsterStats.isDead;
|
final isPlayerVictory = isCombatOver && currentMonsterStats.isDead;
|
||||||
|
|
||||||
return CombatTurnResult(
|
return CombatTurnResult(
|
||||||
@@ -206,7 +208,8 @@ class CombatCalculator {
|
|||||||
// 플레이어 DPS (초당 데미지)
|
// 플레이어 DPS (초당 데미지)
|
||||||
final playerDamagePerHit = math.max(1, player.atk - monster.def * 0.5);
|
final playerDamagePerHit = math.max(1, player.atk - monster.def * 0.5);
|
||||||
final playerHitsPerSecond = 1000 / player.attackDelayMs;
|
final playerHitsPerSecond = 1000 / player.attackDelayMs;
|
||||||
final playerDps = playerDamagePerHit * playerHitsPerSecond * player.accuracy;
|
final playerDps =
|
||||||
|
playerDamagePerHit * playerHitsPerSecond * player.accuracy;
|
||||||
|
|
||||||
// 몬스터를 처치하는 데 필요한 시간 (밀리초)
|
// 몬스터를 처치하는 데 필요한 시간 (밀리초)
|
||||||
final timeToKillMonster = (monster.hpMax / playerDps * 1000).round();
|
final timeToKillMonster = (monster.hpMax / playerDps * 1000).round();
|
||||||
@@ -225,17 +228,20 @@ class CombatCalculator {
|
|||||||
// 플레이어 예상 생존 시간
|
// 플레이어 예상 생존 시간
|
||||||
final monsterDamagePerHit = math.max(1, monster.atk - player.def * 0.5);
|
final monsterDamagePerHit = math.max(1, monster.atk - player.def * 0.5);
|
||||||
final monsterHitsPerSecond = 1000 / monster.attackDelayMs;
|
final monsterHitsPerSecond = 1000 / monster.attackDelayMs;
|
||||||
final monsterDps = monsterDamagePerHit * monsterHitsPerSecond * monster.accuracy;
|
final monsterDps =
|
||||||
|
monsterDamagePerHit * monsterHitsPerSecond * monster.accuracy;
|
||||||
final playerSurvivalTime = player.hpCurrent / monsterDps;
|
final playerSurvivalTime = player.hpCurrent / monsterDps;
|
||||||
|
|
||||||
// 몬스터 예상 생존 시간
|
// 몬스터 예상 생존 시간
|
||||||
final playerDamagePerHit = math.max(1, player.atk - monster.def * 0.5);
|
final playerDamagePerHit = math.max(1, player.atk - monster.def * 0.5);
|
||||||
final playerHitsPerSecond = 1000 / player.attackDelayMs;
|
final playerHitsPerSecond = 1000 / player.attackDelayMs;
|
||||||
final playerDps = playerDamagePerHit * playerHitsPerSecond * player.accuracy;
|
final playerDps =
|
||||||
|
playerDamagePerHit * playerHitsPerSecond * player.accuracy;
|
||||||
final monsterSurvivalTime = monster.hpCurrent / playerDps;
|
final monsterSurvivalTime = monster.hpCurrent / playerDps;
|
||||||
|
|
||||||
// 난이도 = 몬스터 생존시간 / (플레이어 생존시간 + 몬스터 생존시간)
|
// 난이도 = 몬스터 생존시간 / (플레이어 생존시간 + 몬스터 생존시간)
|
||||||
final difficulty = monsterSurvivalTime / (playerSurvivalTime + monsterSurvivalTime);
|
final difficulty =
|
||||||
|
monsterSurvivalTime / (playerSurvivalTime + monsterSurvivalTime);
|
||||||
|
|
||||||
return difficulty.clamp(0.0, 1.0);
|
return difficulty.clamp(0.0, 1.0);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,7 +45,9 @@ class ItemService {
|
|||||||
|
|
||||||
// 교체할 슬롯이 있으면 해당 아이템 무게 제외
|
// 교체할 슬롯이 있으면 해당 아이템 무게 제외
|
||||||
if (replacingSlot != null) {
|
if (replacingSlot != null) {
|
||||||
final existingItem = currentItems.where((i) => i.slot == replacingSlot).firstOrNull;
|
final existingItem = currentItems
|
||||||
|
.where((i) => i.slot == replacingSlot)
|
||||||
|
.firstOrNull;
|
||||||
if (existingItem != null) {
|
if (existingItem != null) {
|
||||||
currentWeight -= existingItem.weight;
|
currentWeight -= existingItem.weight;
|
||||||
}
|
}
|
||||||
@@ -70,7 +72,8 @@ class ItemService {
|
|||||||
|
|
||||||
if (roll < legendaryChance) return ItemRarity.legendary;
|
if (roll < legendaryChance) return ItemRarity.legendary;
|
||||||
if (roll < legendaryChance + epicChance) return ItemRarity.epic;
|
if (roll < legendaryChance + epicChance) return ItemRarity.epic;
|
||||||
if (roll < legendaryChance + epicChance + rareChance) return ItemRarity.rare;
|
if (roll < legendaryChance + epicChance + rareChance)
|
||||||
|
return ItemRarity.rare;
|
||||||
if (roll < legendaryChance + epicChance + rareChance + uncommonChance) {
|
if (roll < legendaryChance + epicChance + rareChance + uncommonChance) {
|
||||||
return ItemRarity.uncommon;
|
return ItemRarity.uncommon;
|
||||||
}
|
}
|
||||||
@@ -112,7 +115,8 @@ class ItemService {
|
|||||||
|
|
||||||
// 공속 결정 (600ms ~ 1500ms 범위)
|
// 공속 결정 (600ms ~ 1500ms 범위)
|
||||||
// 희귀도가 높을수록 공속 변동 폭 증가
|
// 희귀도가 높을수록 공속 변동 폭 증가
|
||||||
final speedVariance = 300 + rarity.index * 100; // Common: 300, Legendary: 700
|
final speedVariance =
|
||||||
|
300 + rarity.index * 100; // Common: 300, Legendary: 700
|
||||||
final speedOffset = rng.nextInt(speedVariance * 2) - speedVariance;
|
final speedOffset = rng.nextInt(speedVariance * 2) - speedVariance;
|
||||||
final attackSpeed = (1000 + speedOffset).clamp(600, 1500);
|
final attackSpeed = (1000 + speedOffset).clamp(600, 1500);
|
||||||
|
|
||||||
@@ -133,14 +137,15 @@ class ItemService {
|
|||||||
ItemStats _generateShieldStats(int baseValue, ItemRarity rarity) {
|
ItemStats _generateShieldStats(int baseValue, ItemRarity rarity) {
|
||||||
final blockBonus = 0.05 + rarity.index * 0.02;
|
final blockBonus = 0.05 + rarity.index * 0.02;
|
||||||
|
|
||||||
return ItemStats(
|
return ItemStats(def: baseValue ~/ 2, blockRate: blockBonus);
|
||||||
def: baseValue ~/ 2,
|
|
||||||
blockRate: blockBonus,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 방어구 스탯 생성
|
/// 방어구 스탯 생성
|
||||||
ItemStats _generateArmorStats(int baseValue, ItemRarity rarity, EquipmentSlot slot) {
|
ItemStats _generateArmorStats(
|
||||||
|
int baseValue,
|
||||||
|
ItemRarity rarity,
|
||||||
|
EquipmentSlot slot,
|
||||||
|
) {
|
||||||
// 슬롯별 방어력 가중치
|
// 슬롯별 방어력 가중치
|
||||||
final defMultiplier = switch (slot) {
|
final defMultiplier = switch (slot) {
|
||||||
EquipmentSlot.hauberk => 1.5, // 갑옷류 최고
|
EquipmentSlot.hauberk => 1.5, // 갑옷류 최고
|
||||||
@@ -161,11 +166,7 @@ class ItemService {
|
|||||||
final hpBonus = rarity.index >= ItemRarity.rare.index ? baseValue ~/ 2 : 0;
|
final hpBonus = rarity.index >= ItemRarity.rare.index ? baseValue ~/ 2 : 0;
|
||||||
final evasionBonus = rarity.index >= ItemRarity.epic.index ? 0.01 : 0.0;
|
final evasionBonus = rarity.index >= ItemRarity.epic.index ? 0.01 : 0.0;
|
||||||
|
|
||||||
return ItemStats(
|
return ItemStats(def: def, hpBonus: hpBonus, evasion: evasionBonus);
|
||||||
def: def,
|
|
||||||
hpBonus: hpBonus,
|
|
||||||
evasion: evasionBonus,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -173,10 +174,7 @@ class ItemService {
|
|||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
/// 무게 계산 (레벨/슬롯 기반)
|
/// 무게 계산 (레벨/슬롯 기반)
|
||||||
int calculateWeight({
|
int calculateWeight({required int level, required EquipmentSlot slot}) {
|
||||||
required int level,
|
|
||||||
required EquipmentSlot slot,
|
|
||||||
}) {
|
|
||||||
// 슬롯별 기본 무게
|
// 슬롯별 기본 무게
|
||||||
final baseWeight = switch (slot) {
|
final baseWeight = switch (slot) {
|
||||||
EquipmentSlot.weapon => 10,
|
EquipmentSlot.weapon => 10,
|
||||||
@@ -209,7 +207,11 @@ class ItemService {
|
|||||||
ItemRarity? rarity,
|
ItemRarity? rarity,
|
||||||
}) {
|
}) {
|
||||||
final itemRarity = rarity ?? determineRarity(level);
|
final itemRarity = rarity ?? determineRarity(level);
|
||||||
final stats = generateItemStats(level: level, rarity: itemRarity, slot: slot);
|
final stats = generateItemStats(
|
||||||
|
level: level,
|
||||||
|
rarity: itemRarity,
|
||||||
|
slot: slot,
|
||||||
|
);
|
||||||
final weight = calculateWeight(level: level, slot: slot);
|
final weight = calculateWeight(level: level, slot: slot);
|
||||||
|
|
||||||
return EquipmentItem(
|
return EquipmentItem(
|
||||||
@@ -253,7 +255,8 @@ class ItemService {
|
|||||||
score += stats.mpBonus;
|
score += stats.mpBonus;
|
||||||
|
|
||||||
// 능력치 보너스 (가중치 5배)
|
// 능력치 보너스 (가중치 5배)
|
||||||
score += (stats.strBonus +
|
score +=
|
||||||
|
(stats.strBonus +
|
||||||
stats.conBonus +
|
stats.conBonus +
|
||||||
stats.dexBonus +
|
stats.dexBonus +
|
||||||
stats.intBonus +
|
stats.intBonus +
|
||||||
|
|||||||
@@ -238,12 +238,16 @@ class PotionService {
|
|||||||
}) {
|
}) {
|
||||||
final potion = PotionData.getById(potionId);
|
final potion = PotionData.getById(potionId);
|
||||||
if (potion == null) {
|
if (potion == null) {
|
||||||
return PotionPurchaseResult.failed(PotionPurchaseFailReason.potionNotFound);
|
return PotionPurchaseResult.failed(
|
||||||
|
PotionPurchaseFailReason.potionNotFound,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final totalCost = potion.price * count;
|
final totalCost = potion.price * count;
|
||||||
if (gold < totalCost) {
|
if (gold < totalCost) {
|
||||||
return PotionPurchaseResult.failed(PotionPurchaseFailReason.insufficientGold);
|
return PotionPurchaseResult.failed(
|
||||||
|
PotionPurchaseFailReason.insufficientGold,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final newInventory = inventory.addPotion(potionId, count);
|
final newInventory = inventory.addPotion(potionId, count);
|
||||||
@@ -277,13 +281,17 @@ class PotionService {
|
|||||||
final mpPotion = PotionData.getMpPotionByTier(tier);
|
final mpPotion = PotionData.getMpPotionByTier(tier);
|
||||||
|
|
||||||
if (hpPotion == null && mpPotion == null) {
|
if (hpPotion == null && mpPotion == null) {
|
||||||
return PotionPurchaseResult.failed(PotionPurchaseFailReason.potionNotFound);
|
return PotionPurchaseResult.failed(
|
||||||
|
PotionPurchaseFailReason.potionNotFound,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 사용 가능 골드
|
// 사용 가능 골드
|
||||||
final spendableGold = (gold * spendRatio).floor();
|
final spendableGold = (gold * spendRatio).floor();
|
||||||
if (spendableGold <= 0) {
|
if (spendableGold <= 0) {
|
||||||
return PotionPurchaseResult.failed(PotionPurchaseFailReason.insufficientGold);
|
return PotionPurchaseResult.failed(
|
||||||
|
PotionPurchaseFailReason.insufficientGold,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
var currentInventory = inventory;
|
var currentInventory = inventory;
|
||||||
@@ -317,7 +325,9 @@ class PotionService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (totalSpent == 0) {
|
if (totalSpent == 0) {
|
||||||
return PotionPurchaseResult.failed(PotionPurchaseFailReason.insufficientGold);
|
return PotionPurchaseResult.failed(
|
||||||
|
PotionPurchaseFailReason.insufficientGold,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return PotionPurchaseResult(
|
return PotionPurchaseResult(
|
||||||
@@ -426,10 +436,7 @@ class PotionUseResult {
|
|||||||
|
|
||||||
/// 실패 결과 생성
|
/// 실패 결과 생성
|
||||||
factory PotionUseResult.failed(PotionUseFailReason reason) {
|
factory PotionUseResult.failed(PotionUseFailReason reason) {
|
||||||
return PotionUseResult(
|
return PotionUseResult(success: false, failReason: reason);
|
||||||
success: false,
|
|
||||||
failReason: reason,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -480,10 +487,7 @@ class PotionPurchaseResult {
|
|||||||
|
|
||||||
/// 실패 결과 생성
|
/// 실패 결과 생성
|
||||||
factory PotionPurchaseResult.failed(PotionPurchaseFailReason reason) {
|
factory PotionPurchaseResult.failed(PotionPurchaseFailReason reason) {
|
||||||
return PotionPurchaseResult(
|
return PotionPurchaseResult(success: false, failReason: reason);
|
||||||
success: false,
|
|
||||||
failReason: reason,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -112,7 +112,9 @@ class ProgressService {
|
|||||||
),
|
),
|
||||||
plotStageCount: 1, // Prologue
|
plotStageCount: 1, // Prologue
|
||||||
questCount: 0,
|
questCount: 0,
|
||||||
plotHistory: [HistoryEntry(caption: l10n.taskPrologue, isComplete: false)],
|
plotHistory: [
|
||||||
|
HistoryEntry(caption: l10n.taskPrologue, isComplete: false),
|
||||||
|
],
|
||||||
questHistory: const [],
|
questHistory: const [],
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -156,13 +158,17 @@ class ProgressService {
|
|||||||
|
|
||||||
// 스킬 시스템 시간 업데이트 (Phase 3)
|
// 스킬 시스템 시간 업데이트 (Phase 3)
|
||||||
final skillService = SkillService(rng: state.rng);
|
final skillService = SkillService(rng: state.rng);
|
||||||
var skillSystem = skillService.updateElapsedTime(state.skillSystem, clamped);
|
var skillSystem = skillService.updateElapsedTime(
|
||||||
|
state.skillSystem,
|
||||||
|
clamped,
|
||||||
|
);
|
||||||
|
|
||||||
// 만료된 버프 정리
|
// 만료된 버프 정리
|
||||||
skillSystem = skillService.cleanupExpiredBuffs(skillSystem);
|
skillSystem = skillService.cleanupExpiredBuffs(skillSystem);
|
||||||
|
|
||||||
// 비전투 시 MP 회복
|
// 비전투 시 MP 회복
|
||||||
final isInCombat = progress.currentTask.type == TaskType.kill &&
|
final isInCombat =
|
||||||
|
progress.currentTask.type == TaskType.kill &&
|
||||||
progress.currentCombat != null &&
|
progress.currentCombat != null &&
|
||||||
progress.currentCombat!.isActive;
|
progress.currentCombat!.isActive;
|
||||||
|
|
||||||
@@ -173,7 +179,10 @@ class ProgressService {
|
|||||||
wis: nextState.stats.wis,
|
wis: nextState.stats.wis,
|
||||||
);
|
);
|
||||||
if (mpRegen > 0) {
|
if (mpRegen > 0) {
|
||||||
final newMp = (nextState.stats.mp + mpRegen).clamp(0, nextState.stats.mpMax);
|
final newMp = (nextState.stats.mp + mpRegen).clamp(
|
||||||
|
0,
|
||||||
|
nextState.stats.mpMax,
|
||||||
|
);
|
||||||
nextState = nextState.copyWith(
|
nextState = nextState.copyWith(
|
||||||
stats: nextState.stats.copyWith(mpCurrent: newMp),
|
stats: nextState.stats.copyWith(mpCurrent: newMp),
|
||||||
);
|
);
|
||||||
@@ -193,7 +202,9 @@ class ProgressService {
|
|||||||
var updatedCombat = progress.currentCombat;
|
var updatedCombat = progress.currentCombat;
|
||||||
var updatedSkillSystem = nextState.skillSystem;
|
var updatedSkillSystem = nextState.skillSystem;
|
||||||
var updatedPotionInventory = nextState.potionInventory;
|
var updatedPotionInventory = nextState.potionInventory;
|
||||||
if (progress.currentTask.type == TaskType.kill && updatedCombat != null && updatedCombat.isActive) {
|
if (progress.currentTask.type == TaskType.kill &&
|
||||||
|
updatedCombat != null &&
|
||||||
|
updatedCombat.isActive) {
|
||||||
final combatResult = _processCombatTickWithSkills(
|
final combatResult = _processCombatTickWithSkills(
|
||||||
nextState,
|
nextState,
|
||||||
updatedCombat,
|
updatedCombat,
|
||||||
@@ -480,7 +491,8 @@ class ProgressService {
|
|||||||
final questMonster = state.progress.currentQuestMonster;
|
final questMonster = state.progress.currentQuestMonster;
|
||||||
final questMonsterData = questMonster?.monsterData;
|
final questMonsterData = questMonster?.monsterData;
|
||||||
final questLevel = questMonsterData != null
|
final questLevel = questMonsterData != null
|
||||||
? int.tryParse(questMonsterData.split('|').elementAtOrNull(1) ?? '') ?? 0
|
? int.tryParse(questMonsterData.split('|').elementAtOrNull(1) ?? '') ??
|
||||||
|
0
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
final monsterResult = pq_logic.monsterTask(
|
final monsterResult = pq_logic.monsterTask(
|
||||||
@@ -501,10 +513,9 @@ class ProgressService {
|
|||||||
// 전투용 몬스터 레벨 조정 (밸런스)
|
// 전투용 몬스터 레벨 조정 (밸런스)
|
||||||
// config의 raw 레벨이 플레이어보다 너무 높으면 전투가 불가능
|
// config의 raw 레벨이 플레이어보다 너무 높으면 전투가 불가능
|
||||||
// 플레이어 레벨 ±3 범위로 제한 (최소 1)
|
// 플레이어 레벨 ±3 범위로 제한 (최소 1)
|
||||||
final effectiveMonsterLevel = monsterResult.level.clamp(
|
final effectiveMonsterLevel = monsterResult.level
|
||||||
math.max(1, level - 3),
|
.clamp(math.max(1, level - 3), level + 3)
|
||||||
level + 3,
|
.toInt();
|
||||||
).toInt();
|
|
||||||
|
|
||||||
final monsterCombatStats = MonsterCombatStats.fromLevel(
|
final monsterCombatStats = MonsterCombatStats.fromLevel(
|
||||||
name: monsterResult.displayName,
|
name: monsterResult.displayName,
|
||||||
@@ -907,7 +918,8 @@ class ProgressService {
|
|||||||
if (hasItemsToSell) {
|
if (hasItemsToSell) {
|
||||||
// 다음 아이템 판매 태스크 시작
|
// 다음 아이템 판매 태스크 시작
|
||||||
final nextItem = items.first;
|
final nextItem = items.first;
|
||||||
final itemDesc = l10n.indefiniteL10n(nextItem.name, nextItem.count);
|
final translatedName = l10n.translateItemNameL10n(nextItem.name);
|
||||||
|
final itemDesc = l10n.indefiniteL10n(translatedName, nextItem.count);
|
||||||
final taskResult = pq_logic.startTask(
|
final taskResult = pq_logic.startTask(
|
||||||
state.progress,
|
state.progress,
|
||||||
l10n.taskSelling(itemDesc),
|
l10n.taskSelling(itemDesc),
|
||||||
@@ -945,7 +957,8 @@ class ProgressService {
|
|||||||
CombatState combat,
|
CombatState combat,
|
||||||
SkillSystemState skillSystem,
|
SkillSystemState skillSystem,
|
||||||
PotionInventory? potionInventory,
|
PotionInventory? potionInventory,
|
||||||
}) _processCombatTickWithSkills(
|
})
|
||||||
|
_processCombatTickWithSkills(
|
||||||
GameState state,
|
GameState state,
|
||||||
CombatState combat,
|
CombatState combat,
|
||||||
SkillSystemState skillSystem,
|
SkillSystemState skillSystem,
|
||||||
@@ -988,12 +1001,14 @@ class ProgressService {
|
|||||||
dotDamageThisTick += damage;
|
dotDamageThisTick += damage;
|
||||||
|
|
||||||
// DOT 데미지 이벤트 생성
|
// DOT 데미지 이벤트 생성
|
||||||
newEvents.add(CombatEvent.dotTick(
|
newEvents.add(
|
||||||
timestamp: timestamp,
|
CombatEvent.dotTick(
|
||||||
skillName: dot.skillId,
|
timestamp: timestamp,
|
||||||
damage: damage,
|
skillName: dot.skillId,
|
||||||
targetName: monsterStats.name,
|
damage: damage,
|
||||||
));
|
targetName: monsterStats.name,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 만료되지 않은 DOT만 유지
|
// 만료되지 않은 DOT만 유지
|
||||||
@@ -1004,8 +1019,10 @@ class ProgressService {
|
|||||||
|
|
||||||
// DOT 데미지 적용
|
// DOT 데미지 적용
|
||||||
if (dotDamageThisTick > 0 && monsterStats.isAlive) {
|
if (dotDamageThisTick > 0 && monsterStats.isAlive) {
|
||||||
final newMonsterHp = (monsterStats.hpCurrent - dotDamageThisTick)
|
final newMonsterHp = (monsterStats.hpCurrent - dotDamageThisTick).clamp(
|
||||||
.clamp(0, monsterStats.hpMax);
|
0,
|
||||||
|
monsterStats.hpMax,
|
||||||
|
);
|
||||||
monsterStats = monsterStats.copyWith(hpCurrent: newMonsterHp);
|
monsterStats = monsterStats.copyWith(hpCurrent: newMonsterHp);
|
||||||
totalDamageDealt += dotDamageThisTick;
|
totalDamageDealt += dotDamageThisTick;
|
||||||
}
|
}
|
||||||
@@ -1024,8 +1041,7 @@ class ProgressService {
|
|||||||
playerLevel: state.traits.level,
|
playerLevel: state.traits.level,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (emergencyPotion != null &&
|
if (emergencyPotion != null && !usedPotionTypes.contains(PotionType.hp)) {
|
||||||
!usedPotionTypes.contains(PotionType.hp)) {
|
|
||||||
final result = potionService.usePotion(
|
final result = potionService.usePotion(
|
||||||
potionId: emergencyPotion.id,
|
potionId: emergencyPotion.id,
|
||||||
inventory: state.potionInventory,
|
inventory: state.potionInventory,
|
||||||
@@ -1040,25 +1056,27 @@ class ProgressService {
|
|||||||
usedPotionTypes = {...usedPotionTypes, PotionType.hp};
|
usedPotionTypes = {...usedPotionTypes, PotionType.hp};
|
||||||
updatedPotionInventory = result.newInventory;
|
updatedPotionInventory = result.newInventory;
|
||||||
|
|
||||||
newEvents.add(CombatEvent.playerPotion(
|
newEvents.add(
|
||||||
timestamp: timestamp,
|
CombatEvent.playerPotion(
|
||||||
potionName: emergencyPotion.name,
|
timestamp: timestamp,
|
||||||
healAmount: result.healedAmount,
|
potionName: emergencyPotion.name,
|
||||||
isHp: true,
|
healAmount: result.healedAmount,
|
||||||
));
|
isHp: true,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 플레이어 공격 체크
|
// 플레이어 공격 체크
|
||||||
if (playerAccumulator >= playerStats.attackDelayMs) {
|
if (playerAccumulator >= playerStats.attackDelayMs) {
|
||||||
// 스킬 자동 선택
|
// SpellBook에서 사용 가능한 스킬 ID 목록 조회
|
||||||
final availableSkillIds = updatedSkillSystem.skillStates
|
var availableSkillIds = skillService.getAvailableSkillIdsFromSpellBook(
|
||||||
.map((s) => s.skillId)
|
state.spellBook,
|
||||||
.toList();
|
);
|
||||||
// 기본 스킬이 없으면 기본 스킬 추가
|
// SpellBook에 스킬이 없으면 기본 스킬 사용
|
||||||
if (availableSkillIds.isEmpty) {
|
if (availableSkillIds.isEmpty) {
|
||||||
availableSkillIds.addAll(SkillData.defaultSkillIds);
|
availableSkillIds = SkillData.defaultSkillIds;
|
||||||
}
|
}
|
||||||
|
|
||||||
final selectedSkill = skillService.selectAutoSkill(
|
final selectedSkill = skillService.selectAutoSkill(
|
||||||
@@ -1070,12 +1088,18 @@ class ProgressService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (selectedSkill != null && selectedSkill.isAttack) {
|
if (selectedSkill != null && selectedSkill.isAttack) {
|
||||||
// 공격 스킬 사용
|
// 스펠 랭크 조회 (SpellBook 기반)
|
||||||
final skillResult = skillService.useAttackSkill(
|
final spellRank = skillService.getSkillRankFromSpellBook(
|
||||||
|
state.spellBook,
|
||||||
|
selectedSkill.id,
|
||||||
|
);
|
||||||
|
// 랭크 스케일링 적용된 공격 스킬 사용
|
||||||
|
final skillResult = skillService.useAttackSkillWithRank(
|
||||||
skill: selectedSkill,
|
skill: selectedSkill,
|
||||||
player: playerStats,
|
player: playerStats,
|
||||||
monster: monsterStats,
|
monster: monsterStats,
|
||||||
skillSystem: updatedSkillSystem,
|
skillSystem: updatedSkillSystem,
|
||||||
|
rank: spellRank,
|
||||||
);
|
);
|
||||||
playerStats = skillResult.updatedPlayer;
|
playerStats = skillResult.updatedPlayer;
|
||||||
monsterStats = skillResult.updatedMonster;
|
monsterStats = skillResult.updatedMonster;
|
||||||
@@ -1083,12 +1107,14 @@ class ProgressService {
|
|||||||
updatedSkillSystem = skillResult.updatedSkillSystem;
|
updatedSkillSystem = skillResult.updatedSkillSystem;
|
||||||
|
|
||||||
// 스킬 공격 이벤트 생성
|
// 스킬 공격 이벤트 생성
|
||||||
newEvents.add(CombatEvent.playerSkill(
|
newEvents.add(
|
||||||
timestamp: timestamp,
|
CombatEvent.playerSkill(
|
||||||
skillName: selectedSkill.name,
|
timestamp: timestamp,
|
||||||
damage: skillResult.result.damage,
|
skillName: selectedSkill.name,
|
||||||
targetName: monsterStats.name,
|
damage: skillResult.result.damage,
|
||||||
));
|
targetName: monsterStats.name,
|
||||||
|
),
|
||||||
|
);
|
||||||
} else if (selectedSkill != null && selectedSkill.isDot) {
|
} else if (selectedSkill != null && selectedSkill.isDot) {
|
||||||
// DOT 스킬 사용
|
// DOT 스킬 사용
|
||||||
final skillResult = skillService.useDotSkill(
|
final skillResult = skillService.useDotSkill(
|
||||||
@@ -1107,12 +1133,14 @@ class ProgressService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DOT 스킬 사용 이벤트 생성
|
// DOT 스킬 사용 이벤트 생성
|
||||||
newEvents.add(CombatEvent.playerSkill(
|
newEvents.add(
|
||||||
timestamp: timestamp,
|
CombatEvent.playerSkill(
|
||||||
skillName: selectedSkill.name,
|
timestamp: timestamp,
|
||||||
damage: skillResult.result.damage,
|
skillName: selectedSkill.name,
|
||||||
targetName: monsterStats.name,
|
damage: skillResult.result.damage,
|
||||||
));
|
targetName: monsterStats.name,
|
||||||
|
),
|
||||||
|
);
|
||||||
} else if (selectedSkill != null && selectedSkill.isHeal) {
|
} else if (selectedSkill != null && selectedSkill.isHeal) {
|
||||||
// 회복 스킬 사용
|
// 회복 스킬 사용
|
||||||
final skillResult = skillService.useHealSkill(
|
final skillResult = skillService.useHealSkill(
|
||||||
@@ -1124,11 +1152,13 @@ class ProgressService {
|
|||||||
updatedSkillSystem = skillResult.updatedSkillSystem;
|
updatedSkillSystem = skillResult.updatedSkillSystem;
|
||||||
|
|
||||||
// 회복 이벤트 생성
|
// 회복 이벤트 생성
|
||||||
newEvents.add(CombatEvent.playerHeal(
|
newEvents.add(
|
||||||
timestamp: timestamp,
|
CombatEvent.playerHeal(
|
||||||
healAmount: skillResult.result.healedAmount,
|
timestamp: timestamp,
|
||||||
skillName: selectedSkill.name,
|
healAmount: skillResult.result.healedAmount,
|
||||||
));
|
skillName: selectedSkill.name,
|
||||||
|
),
|
||||||
|
);
|
||||||
} else if (selectedSkill != null && selectedSkill.isBuff) {
|
} else if (selectedSkill != null && selectedSkill.isBuff) {
|
||||||
// 버프 스킬 사용
|
// 버프 스킬 사용
|
||||||
final skillResult = skillService.useBuffSkill(
|
final skillResult = skillService.useBuffSkill(
|
||||||
@@ -1140,10 +1170,12 @@ class ProgressService {
|
|||||||
updatedSkillSystem = skillResult.updatedSkillSystem;
|
updatedSkillSystem = skillResult.updatedSkillSystem;
|
||||||
|
|
||||||
// 버프 이벤트 생성
|
// 버프 이벤트 생성
|
||||||
newEvents.add(CombatEvent.playerBuff(
|
newEvents.add(
|
||||||
timestamp: timestamp,
|
CombatEvent.playerBuff(
|
||||||
skillName: selectedSkill.name,
|
timestamp: timestamp,
|
||||||
));
|
skillName: selectedSkill.name,
|
||||||
|
),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
// 일반 공격
|
// 일반 공격
|
||||||
final attackResult = calculator.playerAttackMonster(
|
final attackResult = calculator.playerAttackMonster(
|
||||||
@@ -1156,17 +1188,21 @@ class ProgressService {
|
|||||||
// 일반 공격 이벤트 생성
|
// 일반 공격 이벤트 생성
|
||||||
final result = attackResult.result;
|
final result = attackResult.result;
|
||||||
if (result.isEvaded) {
|
if (result.isEvaded) {
|
||||||
newEvents.add(CombatEvent.monsterEvade(
|
newEvents.add(
|
||||||
timestamp: timestamp,
|
CombatEvent.monsterEvade(
|
||||||
targetName: monsterStats.name,
|
timestamp: timestamp,
|
||||||
));
|
targetName: monsterStats.name,
|
||||||
|
),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
newEvents.add(CombatEvent.playerAttack(
|
newEvents.add(
|
||||||
timestamp: timestamp,
|
CombatEvent.playerAttack(
|
||||||
damage: result.damage,
|
timestamp: timestamp,
|
||||||
targetName: monsterStats.name,
|
damage: result.damage,
|
||||||
isCritical: result.isCritical,
|
targetName: monsterStats.name,
|
||||||
));
|
isCritical: result.isCritical,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1175,7 +1211,8 @@ class ProgressService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 몬스터가 살아있으면 반격
|
// 몬스터가 살아있으면 반격
|
||||||
if (monsterStats.isAlive && monsterAccumulator >= monsterStats.attackDelayMs) {
|
if (monsterStats.isAlive &&
|
||||||
|
monsterAccumulator >= monsterStats.attackDelayMs) {
|
||||||
final attackResult = calculator.monsterAttackPlayer(
|
final attackResult = calculator.monsterAttackPlayer(
|
||||||
attacker: monsterStats,
|
attacker: monsterStats,
|
||||||
defender: playerStats,
|
defender: playerStats,
|
||||||
@@ -1187,28 +1224,36 @@ class ProgressService {
|
|||||||
// 몬스터 공격 이벤트 생성
|
// 몬스터 공격 이벤트 생성
|
||||||
final result = attackResult.result;
|
final result = attackResult.result;
|
||||||
if (result.isEvaded) {
|
if (result.isEvaded) {
|
||||||
newEvents.add(CombatEvent.playerEvade(
|
newEvents.add(
|
||||||
timestamp: timestamp,
|
CombatEvent.playerEvade(
|
||||||
attackerName: monsterStats.name,
|
timestamp: timestamp,
|
||||||
));
|
attackerName: monsterStats.name,
|
||||||
|
),
|
||||||
|
);
|
||||||
} else if (result.isBlocked) {
|
} else if (result.isBlocked) {
|
||||||
newEvents.add(CombatEvent.playerBlock(
|
newEvents.add(
|
||||||
timestamp: timestamp,
|
CombatEvent.playerBlock(
|
||||||
reducedDamage: result.damage,
|
timestamp: timestamp,
|
||||||
attackerName: monsterStats.name,
|
reducedDamage: result.damage,
|
||||||
));
|
attackerName: monsterStats.name,
|
||||||
|
),
|
||||||
|
);
|
||||||
} else if (result.isParried) {
|
} else if (result.isParried) {
|
||||||
newEvents.add(CombatEvent.playerParry(
|
newEvents.add(
|
||||||
timestamp: timestamp,
|
CombatEvent.playerParry(
|
||||||
reducedDamage: result.damage,
|
timestamp: timestamp,
|
||||||
attackerName: monsterStats.name,
|
reducedDamage: result.damage,
|
||||||
));
|
attackerName: monsterStats.name,
|
||||||
|
),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
newEvents.add(CombatEvent.monsterAttack(
|
newEvents.add(
|
||||||
timestamp: timestamp,
|
CombatEvent.monsterAttack(
|
||||||
damage: result.damage,
|
timestamp: timestamp,
|
||||||
attackerName: monsterStats.name,
|
damage: result.damage,
|
||||||
));
|
attackerName: monsterStats.name,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1285,9 +1330,7 @@ class ProgressService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// 전투 상태 초기화
|
// 전투 상태 초기화
|
||||||
final progress = state.progress.copyWith(
|
final progress = state.progress.copyWith(currentCombat: null);
|
||||||
currentCombat: null,
|
|
||||||
);
|
|
||||||
|
|
||||||
return state.copyWith(
|
return state.copyWith(
|
||||||
equipment: emptyEquipment,
|
equipment: emptyEquipment,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:askiineverdie/data/class_data.dart';
|
import 'package:askiineverdie/data/class_data.dart';
|
||||||
|
import 'package:askiineverdie/data/game_text_l10n.dart' as l10n;
|
||||||
import 'package:askiineverdie/data/race_data.dart';
|
import 'package:askiineverdie/data/race_data.dart';
|
||||||
import 'package:askiineverdie/src/core/engine/shop_service.dart';
|
import 'package:askiineverdie/src/core/engine/shop_service.dart';
|
||||||
import 'package:askiineverdie/src/core/model/class_traits.dart';
|
import 'package:askiineverdie/src/core/model/class_traits.dart';
|
||||||
@@ -75,9 +76,7 @@ class ResurrectionService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// 전투 상태 초기화
|
// 전투 상태 초기화
|
||||||
final progress = state.progress.copyWith(
|
final progress = state.progress.copyWith(currentCombat: null);
|
||||||
currentCombat: null,
|
|
||||||
);
|
|
||||||
|
|
||||||
return state.copyWith(
|
return state.copyWith(
|
||||||
equipment: newEquipment,
|
equipment: newEquipment,
|
||||||
@@ -109,9 +108,7 @@ class ResurrectionService {
|
|||||||
// 장비 적용
|
// 장비 적용
|
||||||
var nextState = state.copyWith(
|
var nextState = state.copyWith(
|
||||||
equipment: autoBuyResult.updatedEquipment,
|
equipment: autoBuyResult.updatedEquipment,
|
||||||
inventory: state.inventory.copyWith(
|
inventory: state.inventory.copyWith(gold: autoBuyResult.remainingGold),
|
||||||
gold: autoBuyResult.remainingGold,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// 2. 전체 HP/MP 계산 (장비 + 종족 + 클래스 보너스 포함)
|
// 2. 전체 HP/MP 계산 (장비 + 종족 + 클래스 보너스 포함)
|
||||||
@@ -137,22 +134,22 @@ class ResurrectionService {
|
|||||||
// 4. 부활 후 태스크 시퀀스 설정 (큐에 추가)
|
// 4. 부활 후 태스크 시퀀스 설정 (큐에 추가)
|
||||||
// 순서: 마을 귀환 → 샵 정비 → 사냥터 이동 → 전투
|
// 순서: 마을 귀환 → 샵 정비 → 사냥터 이동 → 전투
|
||||||
final resurrectionQueue = <QueueEntry>[
|
final resurrectionQueue = <QueueEntry>[
|
||||||
const QueueEntry(
|
QueueEntry(
|
||||||
kind: QueueKind.task,
|
kind: QueueKind.task,
|
||||||
durationMillis: 3000, // 3초
|
durationMillis: 3000, // 3초
|
||||||
caption: 'Returning to town...',
|
caption: l10n.taskReturningToTown,
|
||||||
taskType: TaskType.neutral, // 걷기 애니메이션
|
taskType: TaskType.neutral, // 걷기 애니메이션
|
||||||
),
|
),
|
||||||
const QueueEntry(
|
QueueEntry(
|
||||||
kind: QueueKind.task,
|
kind: QueueKind.task,
|
||||||
durationMillis: 3000, // 3초
|
durationMillis: 3000, // 3초
|
||||||
caption: 'Restocking at shop...',
|
caption: l10n.taskRestockingAtShop,
|
||||||
taskType: TaskType.market, // town 애니메이션
|
taskType: TaskType.market, // town 애니메이션
|
||||||
),
|
),
|
||||||
const QueueEntry(
|
QueueEntry(
|
||||||
kind: QueueKind.task,
|
kind: QueueKind.task,
|
||||||
durationMillis: 2000, // 2초
|
durationMillis: 2000, // 2초
|
||||||
caption: 'Heading to hunting grounds...',
|
caption: l10n.taskHeadingToHuntingGrounds,
|
||||||
taskType: TaskType.neutral, // 걷기 애니메이션
|
taskType: TaskType.neutral, // 걷기 애니메이션
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
@@ -164,10 +161,7 @@ class ResurrectionService {
|
|||||||
),
|
),
|
||||||
// 현재 태스크를 빈 상태로 설정하여 큐에서 다음 태스크를 가져오도록 함
|
// 현재 태스크를 빈 상태로 설정하여 큐에서 다음 태스크를 가져오도록 함
|
||||||
progress: nextState.progress.copyWith(
|
progress: nextState.progress.copyWith(
|
||||||
currentTask: const TaskInfo(
|
currentTask: const TaskInfo(caption: '', type: TaskType.neutral),
|
||||||
caption: '',
|
|
||||||
type: TaskType.neutral,
|
|
||||||
),
|
|
||||||
task: const ProgressBarState(
|
task: const ProgressBarState(
|
||||||
position: 0,
|
position: 0,
|
||||||
max: 1, // 즉시 완료되어 큐에서 다음 태스크 가져옴
|
max: 1, // 즉시 완료되어 큐에서 다음 태스크 가져옴
|
||||||
|
|||||||
@@ -126,7 +126,11 @@ class ShopService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 슬롯과 레벨에 따른 스탯 생성
|
/// 슬롯과 레벨에 따른 스탯 생성
|
||||||
ItemStats _generateItemStats(EquipmentSlot slot, int level, ItemRarity rarity) {
|
ItemStats _generateItemStats(
|
||||||
|
EquipmentSlot slot,
|
||||||
|
int level,
|
||||||
|
ItemRarity rarity,
|
||||||
|
) {
|
||||||
final multiplier = rarity.multiplier;
|
final multiplier = rarity.multiplier;
|
||||||
final baseValue = (level * multiplier).round();
|
final baseValue = (level * multiplier).round();
|
||||||
|
|
||||||
@@ -145,10 +149,7 @@ class ShopService {
|
|||||||
magDef: baseValue ~/ 2,
|
magDef: baseValue ~/ 2,
|
||||||
intBonus: level ~/ 10,
|
intBonus: level ~/ 10,
|
||||||
),
|
),
|
||||||
EquipmentSlot.hauberk => ItemStats(
|
EquipmentSlot.hauberk => ItemStats(def: baseValue, hpBonus: level * 2),
|
||||||
def: baseValue,
|
|
||||||
hpBonus: level * 2,
|
|
||||||
),
|
|
||||||
EquipmentSlot.brassairts => ItemStats(
|
EquipmentSlot.brassairts => ItemStats(
|
||||||
def: baseValue ~/ 2,
|
def: baseValue ~/ 2,
|
||||||
strBonus: level ~/ 15,
|
strBonus: level ~/ 15,
|
||||||
@@ -273,11 +274,7 @@ class ShopService {
|
|||||||
/// 장비 판매
|
/// 장비 판매
|
||||||
SellResult sellItem(EquipmentItem item, int currentGold) {
|
SellResult sellItem(EquipmentItem item, int currentGold) {
|
||||||
final price = calculateSellPrice(item);
|
final price = calculateSellPrice(item);
|
||||||
return SellResult(
|
return SellResult(item: item, price: price, newGold: currentGold + price);
|
||||||
item: item,
|
|
||||||
price: price,
|
|
||||||
newGold: currentGold + price,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import 'package:askiineverdie/src/core/model/game_state.dart';
|
|||||||
import 'package:askiineverdie/src/core/model/monster_combat_stats.dart';
|
import 'package:askiineverdie/src/core/model/monster_combat_stats.dart';
|
||||||
import 'package:askiineverdie/src/core/model/skill.dart';
|
import 'package:askiineverdie/src/core/model/skill.dart';
|
||||||
import 'package:askiineverdie/src/core/util/deterministic_random.dart';
|
import 'package:askiineverdie/src/core/util/deterministic_random.dart';
|
||||||
|
import 'package:askiineverdie/src/core/util/roman.dart';
|
||||||
|
|
||||||
/// 스킬 시스템 서비스
|
/// 스킬 시스템 서비스
|
||||||
///
|
///
|
||||||
@@ -30,7 +31,8 @@ class SkillService {
|
|||||||
|
|
||||||
// 쿨타임 체크
|
// 쿨타임 체크
|
||||||
final skillState = skillSystem.getSkillState(skill.id);
|
final skillState = skillSystem.getSkillState(skill.id);
|
||||||
if (skillState != null && !skillState.isReady(skillSystem.elapsedMs, skill.cooldownMs)) {
|
if (skillState != null &&
|
||||||
|
!skillState.isReady(skillSystem.elapsedMs, skill.cooldownMs)) {
|
||||||
return SkillFailReason.onCooldown;
|
return SkillFailReason.onCooldown;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,7 +51,8 @@ class SkillService {
|
|||||||
CombatStats updatedPlayer,
|
CombatStats updatedPlayer,
|
||||||
MonsterCombatStats updatedMonster,
|
MonsterCombatStats updatedMonster,
|
||||||
SkillSystemState updatedSkillSystem,
|
SkillSystemState updatedSkillSystem,
|
||||||
}) useAttackSkill({
|
})
|
||||||
|
useAttackSkill({
|
||||||
required Skill skill,
|
required Skill skill,
|
||||||
required CombatStats player,
|
required CombatStats player,
|
||||||
required MonsterCombatStats monster,
|
required MonsterCombatStats monster,
|
||||||
@@ -66,7 +69,9 @@ class SkillService {
|
|||||||
final effectiveMonsterDef = monster.def * (1 - skill.targetDefReduction);
|
final effectiveMonsterDef = monster.def * (1 - skill.targetDefReduction);
|
||||||
|
|
||||||
// 최종 데미지 계산 (방어력 감산)
|
// 최종 데미지 계산 (방어력 감산)
|
||||||
final finalDamage = (buffedDamage - effectiveMonsterDef * 0.5).round().clamp(1, 9999);
|
final finalDamage = (buffedDamage - effectiveMonsterDef * 0.5)
|
||||||
|
.round()
|
||||||
|
.clamp(1, 9999);
|
||||||
|
|
||||||
// 몬스터에 데미지 적용
|
// 몬스터에 데미지 적용
|
||||||
var updatedMonster = monster.applyDamage(finalDamage);
|
var updatedMonster = monster.applyDamage(finalDamage);
|
||||||
@@ -79,17 +84,15 @@ class SkillService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// MP 소모
|
// MP 소모
|
||||||
updatedPlayer = updatedPlayer.withMp(updatedPlayer.mpCurrent - skill.mpCost);
|
updatedPlayer = updatedPlayer.withMp(
|
||||||
|
updatedPlayer.mpCurrent - skill.mpCost,
|
||||||
|
);
|
||||||
|
|
||||||
// 스킬 상태 업데이트 (쿨타임 시작)
|
// 스킬 상태 업데이트 (쿨타임 시작)
|
||||||
final updatedSkillSystem = _updateSkillCooldown(skillSystem, skill.id);
|
final updatedSkillSystem = _updateSkillCooldown(skillSystem, skill.id);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
result: SkillUseResult(
|
result: SkillUseResult(skill: skill, success: true, damage: finalDamage),
|
||||||
skill: skill,
|
|
||||||
success: true,
|
|
||||||
damage: finalDamage,
|
|
||||||
),
|
|
||||||
updatedPlayer: updatedPlayer,
|
updatedPlayer: updatedPlayer,
|
||||||
updatedMonster: updatedMonster,
|
updatedMonster: updatedMonster,
|
||||||
updatedSkillSystem: updatedSkillSystem,
|
updatedSkillSystem: updatedSkillSystem,
|
||||||
@@ -101,7 +104,8 @@ class SkillService {
|
|||||||
SkillUseResult result,
|
SkillUseResult result,
|
||||||
CombatStats updatedPlayer,
|
CombatStats updatedPlayer,
|
||||||
SkillSystemState updatedSkillSystem,
|
SkillSystemState updatedSkillSystem,
|
||||||
}) useHealSkill({
|
})
|
||||||
|
useHealSkill({
|
||||||
required Skill skill,
|
required Skill skill,
|
||||||
required CombatStats player,
|
required CombatStats player,
|
||||||
required SkillSystemState skillSystem,
|
required SkillSystemState skillSystem,
|
||||||
@@ -116,7 +120,9 @@ class SkillService {
|
|||||||
var updatedPlayer = player.applyHeal(healAmount);
|
var updatedPlayer = player.applyHeal(healAmount);
|
||||||
|
|
||||||
// MP 소모
|
// MP 소모
|
||||||
updatedPlayer = updatedPlayer.withMp(updatedPlayer.mpCurrent - skill.mpCost);
|
updatedPlayer = updatedPlayer.withMp(
|
||||||
|
updatedPlayer.mpCurrent - skill.mpCost,
|
||||||
|
);
|
||||||
|
|
||||||
// 스킬 상태 업데이트
|
// 스킬 상태 업데이트
|
||||||
final updatedSkillSystem = _updateSkillCooldown(skillSystem, skill.id);
|
final updatedSkillSystem = _updateSkillCooldown(skillSystem, skill.id);
|
||||||
@@ -137,7 +143,8 @@ class SkillService {
|
|||||||
SkillUseResult result,
|
SkillUseResult result,
|
||||||
CombatStats updatedPlayer,
|
CombatStats updatedPlayer,
|
||||||
SkillSystemState updatedSkillSystem,
|
SkillSystemState updatedSkillSystem,
|
||||||
}) useBuffSkill({
|
})
|
||||||
|
useBuffSkill({
|
||||||
required Skill skill,
|
required Skill skill,
|
||||||
required CombatStats player,
|
required CombatStats player,
|
||||||
required SkillSystemState skillSystem,
|
required SkillSystemState skillSystem,
|
||||||
@@ -158,10 +165,11 @@ class SkillService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// 기존 같은 버프 제거 후 새 버프 추가
|
// 기존 같은 버프 제거 후 새 버프 추가
|
||||||
final updatedBuffs = skillSystem.activeBuffs
|
final updatedBuffs =
|
||||||
.where((b) => b.effect.id != skill.buff!.id)
|
skillSystem.activeBuffs
|
||||||
.toList()
|
.where((b) => b.effect.id != skill.buff!.id)
|
||||||
..add(newBuff);
|
.toList()
|
||||||
|
..add(newBuff);
|
||||||
|
|
||||||
// MP 소모
|
// MP 소모
|
||||||
var updatedPlayer = player.withMp(player.mpCurrent - skill.mpCost);
|
var updatedPlayer = player.withMp(player.mpCurrent - skill.mpCost);
|
||||||
@@ -171,11 +179,7 @@ class SkillService {
|
|||||||
updatedSkillSystem = updatedSkillSystem.copyWith(activeBuffs: updatedBuffs);
|
updatedSkillSystem = updatedSkillSystem.copyWith(activeBuffs: updatedBuffs);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
result: SkillUseResult(
|
result: SkillUseResult(skill: skill, success: true, appliedBuff: newBuff),
|
||||||
skill: skill,
|
|
||||||
success: true,
|
|
||||||
appliedBuff: newBuff,
|
|
||||||
),
|
|
||||||
updatedPlayer: updatedPlayer,
|
updatedPlayer: updatedPlayer,
|
||||||
updatedSkillSystem: updatedSkillSystem,
|
updatedSkillSystem: updatedSkillSystem,
|
||||||
);
|
);
|
||||||
@@ -190,7 +194,8 @@ class SkillService {
|
|||||||
CombatStats updatedPlayer,
|
CombatStats updatedPlayer,
|
||||||
SkillSystemState updatedSkillSystem,
|
SkillSystemState updatedSkillSystem,
|
||||||
DotEffect? dotEffect,
|
DotEffect? dotEffect,
|
||||||
}) useDotSkill({
|
})
|
||||||
|
useDotSkill({
|
||||||
required Skill skill,
|
required Skill skill,
|
||||||
required CombatStats player,
|
required CombatStats player,
|
||||||
required SkillSystemState skillSystem,
|
required SkillSystemState skillSystem,
|
||||||
@@ -265,12 +270,15 @@ class SkillService {
|
|||||||
final availableSkills = availableSkillIds
|
final availableSkills = availableSkillIds
|
||||||
.map((id) => SkillData.getSkillById(id))
|
.map((id) => SkillData.getSkillById(id))
|
||||||
.whereType<Skill>()
|
.whereType<Skill>()
|
||||||
.where((skill) => canUseSkill(
|
.where(
|
||||||
skill: skill,
|
(skill) =>
|
||||||
currentMp: currentMp,
|
canUseSkill(
|
||||||
skillSystem: skillSystem,
|
skill: skill,
|
||||||
) ==
|
currentMp: currentMp,
|
||||||
null)
|
skillSystem: skillSystem,
|
||||||
|
) ==
|
||||||
|
null,
|
||||||
|
)
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
if (availableSkills.isEmpty) return null;
|
if (availableSkills.isEmpty) return null;
|
||||||
@@ -311,9 +319,11 @@ class SkillService {
|
|||||||
|
|
||||||
// 예상 총 데미지 기준 정렬
|
// 예상 총 데미지 기준 정렬
|
||||||
dotSkills.sort((a, b) {
|
dotSkills.sort((a, b) {
|
||||||
final aTotal = (a.baseDotDamage ?? 0) *
|
final aTotal =
|
||||||
|
(a.baseDotDamage ?? 0) *
|
||||||
((a.baseDotDurationMs ?? 0) ~/ (a.baseDotTickMs ?? 1000));
|
((a.baseDotDurationMs ?? 0) ~/ (a.baseDotTickMs ?? 1000));
|
||||||
final bTotal = (b.baseDotDamage ?? 0) *
|
final bTotal =
|
||||||
|
(b.baseDotDamage ?? 0) *
|
||||||
((b.baseDotDurationMs ?? 0) ~/ (b.baseDotTickMs ?? 1000));
|
((b.baseDotDurationMs ?? 0) ~/ (b.baseDotTickMs ?? 1000));
|
||||||
return bTotal.compareTo(aTotal);
|
return bTotal.compareTo(aTotal);
|
||||||
});
|
});
|
||||||
@@ -344,7 +354,9 @@ class SkillService {
|
|||||||
final attackSkills = skills.where((s) => s.isAttack).toList();
|
final attackSkills = skills.where((s) => s.isAttack).toList();
|
||||||
if (attackSkills.isEmpty) return null;
|
if (attackSkills.isEmpty) return null;
|
||||||
|
|
||||||
attackSkills.sort((a, b) => b.damageMultiplier.compareTo(a.damageMultiplier));
|
attackSkills.sort(
|
||||||
|
(a, b) => b.damageMultiplier.compareTo(a.damageMultiplier),
|
||||||
|
);
|
||||||
return attackSkills.first;
|
return attackSkills.first;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -399,7 +411,10 @@ class SkillService {
|
|||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
/// 스킬 쿨타임 업데이트
|
/// 스킬 쿨타임 업데이트
|
||||||
SkillSystemState _updateSkillCooldown(SkillSystemState state, String skillId) {
|
SkillSystemState _updateSkillCooldown(
|
||||||
|
SkillSystemState state,
|
||||||
|
String skillId,
|
||||||
|
) {
|
||||||
final skillStates = List<SkillState>.from(state.skillStates);
|
final skillStates = List<SkillState>.from(state.skillStates);
|
||||||
|
|
||||||
// 기존 상태 찾기
|
// 기존 상태 찾기
|
||||||
@@ -412,11 +427,9 @@ class SkillService {
|
|||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// 새 상태 추가
|
// 새 상태 추가
|
||||||
skillStates.add(SkillState(
|
skillStates.add(
|
||||||
skillId: skillId,
|
SkillState(skillId: skillId, lastUsedMs: state.elapsedMs, rank: 1),
|
||||||
lastUsedMs: state.elapsedMs,
|
);
|
||||||
rank: 1,
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return state.copyWith(skillStates: skillStates);
|
return state.copyWith(skillStates: skillStates);
|
||||||
@@ -426,4 +439,142 @@ class SkillService {
|
|||||||
SkillSystemState updateElapsedTime(SkillSystemState state, int deltaMs) {
|
SkillSystemState updateElapsedTime(SkillSystemState state, int deltaMs) {
|
||||||
return state.copyWith(elapsedMs: state.elapsedMs + deltaMs);
|
return state.copyWith(elapsedMs: state.elapsedMs + deltaMs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// SpellBook 연동
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/// SpellBook에서 사용 가능한 스킬 목록 조회
|
||||||
|
///
|
||||||
|
/// SpellEntry 이름을 Skill로 매핑하여 반환
|
||||||
|
List<Skill> getAvailableSkillsFromSpellBook(SpellBook spellBook) {
|
||||||
|
return spellBook.spells
|
||||||
|
.map((spell) => SkillData.getSkillBySpellName(spell.name))
|
||||||
|
.whereType<Skill>()
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// SpellBook에서 스킬의 랭크(레벨) 조회
|
||||||
|
///
|
||||||
|
/// 로마숫자 랭크(I, II, III)를 정수로 변환하여 반환
|
||||||
|
/// 스펠이 없으면 1 반환
|
||||||
|
int getSkillRankFromSpellBook(SpellBook spellBook, String skillId) {
|
||||||
|
// skillId로 스킬 찾기
|
||||||
|
final skill = SkillData.getSkillById(skillId);
|
||||||
|
if (skill == null) return 1;
|
||||||
|
|
||||||
|
// 스킬 이름으로 SpellEntry 찾기
|
||||||
|
for (final spell in spellBook.spells) {
|
||||||
|
if (spell.name == skill.name) {
|
||||||
|
return romanToInt(spell.rank);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 1; // 기본 랭크
|
||||||
|
}
|
||||||
|
|
||||||
|
/// SpellBook에서 스킬 ID 목록 조회
|
||||||
|
///
|
||||||
|
/// 전투 시스템에서 사용 가능한 스킬 ID 목록 반환
|
||||||
|
List<String> getAvailableSkillIdsFromSpellBook(SpellBook spellBook) {
|
||||||
|
return getAvailableSkillsFromSpellBook(
|
||||||
|
spellBook,
|
||||||
|
).map((skill) => skill.id).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 랭크 스케일링이 적용된 공격 스킬 사용
|
||||||
|
///
|
||||||
|
/// [rank] 스펠 랭크 (SpellBook에서 조회)
|
||||||
|
({
|
||||||
|
SkillUseResult result,
|
||||||
|
CombatStats updatedPlayer,
|
||||||
|
MonsterCombatStats updatedMonster,
|
||||||
|
SkillSystemState updatedSkillSystem,
|
||||||
|
})
|
||||||
|
useAttackSkillWithRank({
|
||||||
|
required Skill skill,
|
||||||
|
required CombatStats player,
|
||||||
|
required MonsterCombatStats monster,
|
||||||
|
required SkillSystemState skillSystem,
|
||||||
|
required int rank,
|
||||||
|
}) {
|
||||||
|
// 랭크 스케일링 적용
|
||||||
|
final rankMult = getRankMultiplier(rank);
|
||||||
|
final mpMult = getRankMpMultiplier(rank);
|
||||||
|
|
||||||
|
// 실제 MP 비용 계산
|
||||||
|
final actualMpCost = (skill.mpCost * mpMult).round();
|
||||||
|
|
||||||
|
// 기본 데미지 계산 (랭크 배율 적용)
|
||||||
|
final baseDamage = player.atk * skill.damageMultiplier * rankMult;
|
||||||
|
|
||||||
|
// 버프 효과 적용
|
||||||
|
final buffMods = skillSystem.totalBuffModifiers;
|
||||||
|
final buffedDamage = baseDamage * (1 + buffMods.atkMod);
|
||||||
|
|
||||||
|
// 적 방어력 감소 적용
|
||||||
|
final effectiveMonsterDef = monster.def * (1 - skill.targetDefReduction);
|
||||||
|
|
||||||
|
// 최종 데미지 계산 (방어력 감산)
|
||||||
|
final finalDamage = (buffedDamage - effectiveMonsterDef * 0.5)
|
||||||
|
.round()
|
||||||
|
.clamp(1, 9999);
|
||||||
|
|
||||||
|
// 몬스터에 데미지 적용
|
||||||
|
var updatedMonster = monster.applyDamage(finalDamage);
|
||||||
|
|
||||||
|
// 자해 데미지 적용
|
||||||
|
var updatedPlayer = player;
|
||||||
|
if (skill.selfDamagePercent > 0) {
|
||||||
|
final selfDamage = (player.hpMax * skill.selfDamagePercent).round();
|
||||||
|
updatedPlayer = player.applyDamage(selfDamage);
|
||||||
|
}
|
||||||
|
|
||||||
|
// MP 소모 (랭크 스케일링 적용)
|
||||||
|
updatedPlayer = updatedPlayer.withMp(
|
||||||
|
updatedPlayer.mpCurrent - actualMpCost,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 스킬 상태 업데이트 (쿨타임 시작, 랭크 저장)
|
||||||
|
// 쿨타임 스케일링은 isReady 체크 시 적용됨
|
||||||
|
final updatedSkillSystem = _updateSkillCooldownWithRank(
|
||||||
|
skillSystem,
|
||||||
|
skill.id,
|
||||||
|
rank,
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
result: SkillUseResult(skill: skill, success: true, damage: finalDamage),
|
||||||
|
updatedPlayer: updatedPlayer,
|
||||||
|
updatedMonster: updatedMonster,
|
||||||
|
updatedSkillSystem: updatedSkillSystem,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 랭크 정보를 포함한 스킬 쿨타임 업데이트
|
||||||
|
SkillSystemState _updateSkillCooldownWithRank(
|
||||||
|
SkillSystemState state,
|
||||||
|
String skillId,
|
||||||
|
int rank,
|
||||||
|
) {
|
||||||
|
final skillStates = List<SkillState>.from(state.skillStates);
|
||||||
|
|
||||||
|
// 기존 상태 찾기
|
||||||
|
final existingIndex = skillStates.indexWhere((s) => s.skillId == skillId);
|
||||||
|
|
||||||
|
if (existingIndex >= 0) {
|
||||||
|
// 기존 상태 업데이트
|
||||||
|
skillStates[existingIndex] = skillStates[existingIndex].copyWith(
|
||||||
|
lastUsedMs: state.elapsedMs,
|
||||||
|
rank: rank,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// 새 상태 추가
|
||||||
|
skillStates.add(
|
||||||
|
SkillState(skillId: skillId, lastUsedMs: state.elapsedMs, rank: rank),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return state.copyWith(skillStates: skillStates);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,8 @@ class StatCalculator {
|
|||||||
var str = baseStats.str + race.getModifier(StatType.str);
|
var str = baseStats.str + race.getModifier(StatType.str);
|
||||||
var con = baseStats.con + race.getModifier(StatType.con);
|
var con = baseStats.con + race.getModifier(StatType.con);
|
||||||
var dex = baseStats.dex + race.getModifier(StatType.dex);
|
var dex = baseStats.dex + race.getModifier(StatType.dex);
|
||||||
var intel = baseStats.intelligence + race.getModifier(StatType.intelligence);
|
var intel =
|
||||||
|
baseStats.intelligence + race.getModifier(StatType.intelligence);
|
||||||
var wis = baseStats.wis + race.getModifier(StatType.wis);
|
var wis = baseStats.wis + race.getModifier(StatType.wis);
|
||||||
var cha = baseStats.cha + race.getModifier(StatType.cha);
|
var cha = baseStats.cha + race.getModifier(StatType.cha);
|
||||||
|
|
||||||
@@ -108,31 +109,41 @@ class StatCalculator {
|
|||||||
// 클래스 패시브 적용
|
// 클래스 패시브 적용
|
||||||
|
|
||||||
// 물리 공격력 보너스 (Bug Hunter: +20%)
|
// 물리 공격력 보너스 (Bug Hunter: +20%)
|
||||||
final classPhysicalBonus = klass.getPassiveValue(ClassPassiveType.physicalDamageBonus);
|
final classPhysicalBonus = klass.getPassiveValue(
|
||||||
|
ClassPassiveType.physicalDamageBonus,
|
||||||
|
);
|
||||||
if (classPhysicalBonus > 0) {
|
if (classPhysicalBonus > 0) {
|
||||||
atk = (atk * (1 + classPhysicalBonus)).round();
|
atk = (atk * (1 + classPhysicalBonus)).round();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 방어력 보너스 (Debugger Paladin: +15%)
|
// 방어력 보너스 (Debugger Paladin: +15%)
|
||||||
final classDefenseBonus = klass.getPassiveValue(ClassPassiveType.defenseBonus);
|
final classDefenseBonus = klass.getPassiveValue(
|
||||||
|
ClassPassiveType.defenseBonus,
|
||||||
|
);
|
||||||
if (classDefenseBonus > 0) {
|
if (classDefenseBonus > 0) {
|
||||||
def = (def * (1 + classDefenseBonus)).round();
|
def = (def * (1 + classDefenseBonus)).round();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 마법 데미지 보너스 (Compiler Mage: +25%)
|
// 마법 데미지 보너스 (Compiler Mage: +25%)
|
||||||
final classMagicBonus = klass.getPassiveValue(ClassPassiveType.magicDamageBonus);
|
final classMagicBonus = klass.getPassiveValue(
|
||||||
|
ClassPassiveType.magicDamageBonus,
|
||||||
|
);
|
||||||
if (classMagicBonus > 0) {
|
if (classMagicBonus > 0) {
|
||||||
magAtk = (magAtk * (1 + classMagicBonus)).round();
|
magAtk = (magAtk * (1 + classMagicBonus)).round();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 회피율 보너스 (Refactor Monk: +15%)
|
// 회피율 보너스 (Refactor Monk: +15%)
|
||||||
final classEvasionBonus = klass.getPassiveValue(ClassPassiveType.evasionBonus);
|
final classEvasionBonus = klass.getPassiveValue(
|
||||||
|
ClassPassiveType.evasionBonus,
|
||||||
|
);
|
||||||
if (classEvasionBonus > 0) {
|
if (classEvasionBonus > 0) {
|
||||||
evasion = (evasion + classEvasionBonus).clamp(0.0, 0.6);
|
evasion = (evasion + classEvasionBonus).clamp(0.0, 0.6);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 크리티컬 보너스 (Pointer Assassin: +20%)
|
// 크리티컬 보너스 (Pointer Assassin: +20%)
|
||||||
final classCritBonus = klass.getPassiveValue(ClassPassiveType.criticalBonus);
|
final classCritBonus = klass.getPassiveValue(
|
||||||
|
ClassPassiveType.criticalBonus,
|
||||||
|
);
|
||||||
if (classCritBonus > 0) {
|
if (classCritBonus > 0) {
|
||||||
criRate = (criRate + classCritBonus).clamp(0.0, 0.8);
|
criRate = (criRate + classCritBonus).clamp(0.0, 0.8);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,11 +13,7 @@ enum StoryEventType {
|
|||||||
|
|
||||||
/// 스토리 이벤트 (Story Event)
|
/// 스토리 이벤트 (Story Event)
|
||||||
class StoryEvent {
|
class StoryEvent {
|
||||||
const StoryEvent({
|
const StoryEvent({required this.type, required this.act, this.data});
|
||||||
required this.type,
|
|
||||||
required this.act,
|
|
||||||
this.data,
|
|
||||||
});
|
|
||||||
|
|
||||||
final StoryEventType type;
|
final StoryEventType type;
|
||||||
final StoryAct act;
|
final StoryAct act;
|
||||||
@@ -73,18 +69,14 @@ class StoryService {
|
|||||||
// 이전 Act 완료 처리
|
// 이전 Act 완료 처리
|
||||||
if (_currentAct != StoryAct.prologue) {
|
if (_currentAct != StoryAct.prologue) {
|
||||||
_completedActs.add(_currentAct);
|
_completedActs.add(_currentAct);
|
||||||
_eventController.add(StoryEvent(
|
_eventController.add(
|
||||||
type: StoryEventType.actComplete,
|
StoryEvent(type: StoryEventType.actComplete, act: _currentAct),
|
||||||
act: _currentAct,
|
);
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 새 Act 시작
|
// 새 Act 시작
|
||||||
_currentAct = newAct;
|
_currentAct = newAct;
|
||||||
final event = StoryEvent(
|
final event = StoryEvent(type: StoryEventType.actStart, act: newAct);
|
||||||
type: StoryEventType.actStart,
|
|
||||||
act: newAct,
|
|
||||||
);
|
|
||||||
_eventController.add(event);
|
_eventController.add(event);
|
||||||
return event;
|
return event;
|
||||||
}
|
}
|
||||||
@@ -126,10 +118,9 @@ class StoryService {
|
|||||||
void _triggerEnding() {
|
void _triggerEnding() {
|
||||||
_completedActs.add(StoryAct.act5);
|
_completedActs.add(StoryAct.act5);
|
||||||
_currentAct = StoryAct.ending;
|
_currentAct = StoryAct.ending;
|
||||||
_eventController.add(StoryEvent(
|
_eventController.add(
|
||||||
type: StoryEventType.ending,
|
StoryEvent(type: StoryEventType.ending, act: StoryAct.ending),
|
||||||
act: StoryAct.ending,
|
);
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 시네마틱 데이터 가져오기 (Get Cinematic Data)
|
/// 시네마틱 데이터 가져오기 (Get Cinematic Data)
|
||||||
|
|||||||
@@ -1,14 +1,19 @@
|
|||||||
|
import 'package:askiineverdie/data/game_text_l10n.dart' as l10n;
|
||||||
import 'package:askiineverdie/data/game_translations_ko.dart';
|
import 'package:askiineverdie/data/game_translations_ko.dart';
|
||||||
import 'package:askiineverdie/src/core/util/pq_logic.dart';
|
import 'package:askiineverdie/src/core/util/pq_logic.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
/// 게임 데이터 번역을 위한 헬퍼 클래스
|
/// 게임 데이터 번역을 위한 헬퍼 클래스
|
||||||
/// 현재 로케일에 따라 게임 데이터의 번역을 제공합니다.
|
/// 현재 로케일에 따라 게임 데이터의 번역을 제공합니다.
|
||||||
|
///
|
||||||
|
/// 글로벌 로케일 시스템(game_text_l10n.dart)을 사용하여 일관성 보장
|
||||||
class GameDataL10n {
|
class GameDataL10n {
|
||||||
GameDataL10n._();
|
GameDataL10n._();
|
||||||
|
|
||||||
/// 현재 로케일이 한국어인지 확인
|
/// 현재 로케일이 한국어인지 확인 (글로벌 로케일 사용)
|
||||||
static bool _isKorean(BuildContext context) {
|
static bool _isKorean(BuildContext context) {
|
||||||
|
// 글로벌 로케일 우선, 폴백으로 context 로케일 사용
|
||||||
|
if (l10n.isKoreanLocale) return true;
|
||||||
final locale = Localizations.localeOf(context);
|
final locale = Localizations.localeOf(context);
|
||||||
return locale.languageCode == 'ko';
|
return locale.languageCode == 'ko';
|
||||||
}
|
}
|
||||||
@@ -307,17 +312,19 @@ class GameDataL10n {
|
|||||||
for (final entry in baseMap.entries) {
|
for (final entry in baseMap.entries) {
|
||||||
if (remaining.endsWith(entry.key)) {
|
if (remaining.endsWith(entry.key)) {
|
||||||
baseTranslated = entry.value;
|
baseTranslated = entry.value;
|
||||||
modifierPart = remaining.substring(
|
modifierPart = remaining
|
||||||
0,
|
.substring(0, remaining.length - entry.key.length)
|
||||||
remaining.length - entry.key.length,
|
.trim();
|
||||||
).trim();
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 수식어 번역
|
// 3. 수식어 번역
|
||||||
final isWeapon = slotIndex == 0;
|
final isWeapon = slotIndex == 0;
|
||||||
final modWords = modifierPart.split(' ').where((s) => s.isNotEmpty).toList();
|
final modWords = modifierPart
|
||||||
|
.split(' ')
|
||||||
|
.where((s) => s.isNotEmpty)
|
||||||
|
.toList();
|
||||||
final translatedMods = modWords.map((mod) {
|
final translatedMods = modWords.map((mod) {
|
||||||
if (isWeapon) {
|
if (isWeapon) {
|
||||||
return offenseAttribTranslationsKo[mod] ??
|
return offenseAttribTranslationsKo[mod] ??
|
||||||
@@ -423,19 +430,20 @@ class GameDataL10n {
|
|||||||
// 드롭 아이템 앞 부분이 몬스터 이름
|
// 드롭 아이템 앞 부분이 몬스터 이름
|
||||||
String monsterPart;
|
String monsterPart;
|
||||||
if (itemString.endsWith(dropItemProperCase)) {
|
if (itemString.endsWith(dropItemProperCase)) {
|
||||||
monsterPart =
|
monsterPart = itemString
|
||||||
itemString.substring(0, itemString.length - dropItemProperCase.length).trim();
|
.substring(0, itemString.length - dropItemProperCase.length)
|
||||||
|
.trim();
|
||||||
} else {
|
} else {
|
||||||
monsterPart =
|
monsterPart = itemString
|
||||||
itemString.substring(0, itemString.length - dropItem.length).trim();
|
.substring(0, itemString.length - dropItem.length)
|
||||||
|
.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (monsterPart.isEmpty) continue;
|
if (monsterPart.isEmpty) continue;
|
||||||
|
|
||||||
// 몬스터 이름 번역 (소문자를 원래 형태로 변환하여 찾기)
|
// 몬스터 이름 번역 (소문자를 원래 형태로 변환하여 찾기)
|
||||||
final monsterNameKey = _toTitleCase(monsterPart);
|
final monsterNameKey = _toTitleCase(monsterPart);
|
||||||
final monsterKo =
|
final monsterKo = monsterTranslationsKo[monsterNameKey] ?? monsterPart;
|
||||||
monsterTranslationsKo[monsterNameKey] ?? monsterPart;
|
|
||||||
|
|
||||||
final dropKo = entry.value;
|
final dropKo = entry.value;
|
||||||
return '$monsterKo의 $dropKo';
|
return '$monsterKo의 $dropKo';
|
||||||
@@ -452,9 +460,12 @@ class GameDataL10n {
|
|||||||
|
|
||||||
/// 각 단어의 첫 글자를 대문자로 (Title Case)
|
/// 각 단어의 첫 글자를 대문자로 (Title Case)
|
||||||
static String _toTitleCase(String s) {
|
static String _toTitleCase(String s) {
|
||||||
return s.split(' ').map((word) {
|
return s
|
||||||
if (word.isEmpty) return word;
|
.split(' ')
|
||||||
return word[0].toUpperCase() + word.substring(1);
|
.map((word) {
|
||||||
}).join(' ');
|
if (word.isEmpty) return word;
|
||||||
|
return word[0].toUpperCase() + word.substring(1);
|
||||||
|
})
|
||||||
|
.join(' ');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -233,7 +233,8 @@ class CombatStats {
|
|||||||
final effectiveStr = stats.str + equipStats.strBonus + raceStr + classStr;
|
final effectiveStr = stats.str + equipStats.strBonus + raceStr + classStr;
|
||||||
final effectiveCon = stats.con + equipStats.conBonus + raceCon + classCon;
|
final effectiveCon = stats.con + equipStats.conBonus + raceCon + classCon;
|
||||||
final effectiveDex = stats.dex + equipStats.dexBonus + raceDex + classDex;
|
final effectiveDex = stats.dex + equipStats.dexBonus + raceDex + classDex;
|
||||||
final effectiveInt = stats.intelligence + equipStats.intBonus + raceInt + classInt;
|
final effectiveInt =
|
||||||
|
stats.intelligence + equipStats.intBonus + raceInt + classInt;
|
||||||
final effectiveWis = stats.wis + equipStats.wisBonus + raceWis + classWis;
|
final effectiveWis = stats.wis + equipStats.wisBonus + raceWis + classWis;
|
||||||
final effectiveCha = stats.cha + equipStats.chaBonus + raceCha + classCha;
|
final effectiveCha = stats.cha + equipStats.chaBonus + raceCha + classCha;
|
||||||
|
|
||||||
@@ -276,7 +277,10 @@ class CombatStats {
|
|||||||
final weaponSpeed = weaponItem.stats.attackSpeed;
|
final weaponSpeed = weaponItem.stats.attackSpeed;
|
||||||
final baseAttackSpeed = weaponSpeed > 0 ? weaponSpeed : 1000;
|
final baseAttackSpeed = weaponSpeed > 0 ? weaponSpeed : 1000;
|
||||||
final speedModifier = 1.0 + (effectiveDex - 10) * 0.02;
|
final speedModifier = 1.0 + (effectiveDex - 10) * 0.02;
|
||||||
final attackDelayMs = (baseAttackSpeed / speedModifier).round().clamp(300, 2000);
|
final attackDelayMs = (baseAttackSpeed / speedModifier).round().clamp(
|
||||||
|
300,
|
||||||
|
2000,
|
||||||
|
);
|
||||||
|
|
||||||
// HP/MP: 기본 + 장비 보너스
|
// HP/MP: 기본 + 장비 보너스
|
||||||
var totalHpMax = stats.hpMax + equipStats.hpBonus;
|
var totalHpMax = stats.hpMax + equipStats.hpBonus;
|
||||||
@@ -299,7 +303,8 @@ class CombatStats {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 마법 데미지 보너스 (Null Elf: +15%)
|
// 마법 데미지 보너스 (Null Elf: +15%)
|
||||||
final raceMagicBonus = race?.getPassiveValue(PassiveType.magicDamageBonus) ?? 0.0;
|
final raceMagicBonus =
|
||||||
|
race?.getPassiveValue(PassiveType.magicDamageBonus) ?? 0.0;
|
||||||
if (raceMagicBonus > 0) {
|
if (raceMagicBonus > 0) {
|
||||||
baseMagAtk = (baseMagAtk * (1 + raceMagicBonus)).round();
|
baseMagAtk = (baseMagAtk * (1 + raceMagicBonus)).round();
|
||||||
}
|
}
|
||||||
@@ -311,7 +316,8 @@ class CombatStats {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 크리티컬 보너스 (Stack Goblin: +5%)
|
// 크리티컬 보너스 (Stack Goblin: +5%)
|
||||||
final raceCritBonus = race?.getPassiveValue(PassiveType.criticalBonus) ?? 0.0;
|
final raceCritBonus =
|
||||||
|
race?.getPassiveValue(PassiveType.criticalBonus) ?? 0.0;
|
||||||
criRate += raceCritBonus;
|
criRate += raceCritBonus;
|
||||||
|
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
@@ -319,35 +325,41 @@ class CombatStats {
|
|||||||
// ========================================================================
|
// ========================================================================
|
||||||
|
|
||||||
// HP 보너스 (Garbage Collector: +30%)
|
// HP 보너스 (Garbage Collector: +30%)
|
||||||
final classHpBonus = klass?.getPassiveValue(ClassPassiveType.hpBonus) ?? 0.0;
|
final classHpBonus =
|
||||||
|
klass?.getPassiveValue(ClassPassiveType.hpBonus) ?? 0.0;
|
||||||
if (classHpBonus > 0) {
|
if (classHpBonus > 0) {
|
||||||
totalHpMax = (totalHpMax * (1 + classHpBonus)).round();
|
totalHpMax = (totalHpMax * (1 + classHpBonus)).round();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 물리 공격력 보너스 (Bug Hunter: +20%)
|
// 물리 공격력 보너스 (Bug Hunter: +20%)
|
||||||
final classPhysBonus = klass?.getPassiveValue(ClassPassiveType.physicalDamageBonus) ?? 0.0;
|
final classPhysBonus =
|
||||||
|
klass?.getPassiveValue(ClassPassiveType.physicalDamageBonus) ?? 0.0;
|
||||||
if (classPhysBonus > 0) {
|
if (classPhysBonus > 0) {
|
||||||
baseAtk = (baseAtk * (1 + classPhysBonus)).round();
|
baseAtk = (baseAtk * (1 + classPhysBonus)).round();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 방어력 보너스 (Debugger Paladin: +15%)
|
// 방어력 보너스 (Debugger Paladin: +15%)
|
||||||
final classDefBonus = klass?.getPassiveValue(ClassPassiveType.defenseBonus) ?? 0.0;
|
final classDefBonus =
|
||||||
|
klass?.getPassiveValue(ClassPassiveType.defenseBonus) ?? 0.0;
|
||||||
if (classDefBonus > 0) {
|
if (classDefBonus > 0) {
|
||||||
baseDef = (baseDef * (1 + classDefBonus)).round();
|
baseDef = (baseDef * (1 + classDefBonus)).round();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 마법 데미지 보너스 (Compiler Mage: +25%)
|
// 마법 데미지 보너스 (Compiler Mage: +25%)
|
||||||
final classMagBonus = klass?.getPassiveValue(ClassPassiveType.magicDamageBonus) ?? 0.0;
|
final classMagBonus =
|
||||||
|
klass?.getPassiveValue(ClassPassiveType.magicDamageBonus) ?? 0.0;
|
||||||
if (classMagBonus > 0) {
|
if (classMagBonus > 0) {
|
||||||
baseMagAtk = (baseMagAtk * (1 + classMagBonus)).round();
|
baseMagAtk = (baseMagAtk * (1 + classMagBonus)).round();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 회피율 보너스 (Refactor Monk: +15%)
|
// 회피율 보너스 (Refactor Monk: +15%)
|
||||||
final classEvasionBonus = klass?.getPassiveValue(ClassPassiveType.evasionBonus) ?? 0.0;
|
final classEvasionBonus =
|
||||||
|
klass?.getPassiveValue(ClassPassiveType.evasionBonus) ?? 0.0;
|
||||||
evasion += classEvasionBonus;
|
evasion += classEvasionBonus;
|
||||||
|
|
||||||
// 크리티컬 보너스 (Pointer Assassin: +20%)
|
// 크리티컬 보너스 (Pointer Assassin: +20%)
|
||||||
final classCritBonus = klass?.getPassiveValue(ClassPassiveType.criticalBonus) ?? 0.0;
|
final classCritBonus =
|
||||||
|
klass?.getPassiveValue(ClassPassiveType.criticalBonus) ?? 0.0;
|
||||||
criRate += classCritBonus;
|
criRate += classCritBonus;
|
||||||
|
|
||||||
// 최종 클램핑
|
// 최종 클램핑
|
||||||
|
|||||||
@@ -209,11 +209,8 @@ class SkillSystemState {
|
|||||||
/// 게임 진행 경과 시간 (밀리초, 스킬 쿨타임 계산용)
|
/// 게임 진행 경과 시간 (밀리초, 스킬 쿨타임 계산용)
|
||||||
final int elapsedMs;
|
final int elapsedMs;
|
||||||
|
|
||||||
factory SkillSystemState.empty() => const SkillSystemState(
|
factory SkillSystemState.empty() =>
|
||||||
skillStates: [],
|
const SkillSystemState(skillStates: [], activeBuffs: [], elapsedMs: 0);
|
||||||
activeBuffs: [],
|
|
||||||
elapsedMs: 0,
|
|
||||||
);
|
|
||||||
|
|
||||||
/// 특정 스킬 상태 가져오기
|
/// 특정 스킬 상태 가져오기
|
||||||
SkillState? getSkillState(String skillId) {
|
SkillState? getSkillState(String skillId) {
|
||||||
@@ -224,7 +221,8 @@ class SkillSystemState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 버프 효과 합산 (동일 버프는 중복 적용 안 됨)
|
/// 버프 효과 합산 (동일 버프는 중복 적용 안 됨)
|
||||||
({double atkMod, double defMod, double criMod, double evasionMod}) get totalBuffModifiers {
|
({double atkMod, double defMod, double criMod, double evasionMod})
|
||||||
|
get totalBuffModifiers {
|
||||||
double atkMod = 0;
|
double atkMod = 0;
|
||||||
double defMod = 0;
|
double defMod = 0;
|
||||||
double criMod = 0;
|
double criMod = 0;
|
||||||
@@ -243,7 +241,12 @@ class SkillSystemState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (atkMod: atkMod, defMod: defMod, criMod: criMod, evasionMod: evasionMod);
|
return (
|
||||||
|
atkMod: atkMod,
|
||||||
|
defMod: defMod,
|
||||||
|
criMod: criMod,
|
||||||
|
evasionMod: evasionMod,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
SkillSystemState copyWith({
|
SkillSystemState copyWith({
|
||||||
@@ -477,10 +480,8 @@ class Inventory {
|
|||||||
/// Phase 2에서 EquipmentItem 기반으로 확장됨.
|
/// Phase 2에서 EquipmentItem 기반으로 확장됨.
|
||||||
/// 기존 문자열 API(weapon, shield 등)는 호환성을 위해 유지.
|
/// 기존 문자열 API(weapon, shield 등)는 호환성을 위해 유지.
|
||||||
class Equipment {
|
class Equipment {
|
||||||
Equipment({
|
Equipment({required this.items, required this.bestIndex})
|
||||||
required this.items,
|
: assert(items.length == slotCount, 'Equipment must have $slotCount items');
|
||||||
required this.bestIndex,
|
|
||||||
}) : assert(items.length == slotCount, 'Equipment must have $slotCount items');
|
|
||||||
|
|
||||||
/// 장비 아이템 목록 (11개 슬롯)
|
/// 장비 아이템 목록 (11개 슬롯)
|
||||||
final List<EquipmentItem> items;
|
final List<EquipmentItem> items;
|
||||||
@@ -525,10 +526,7 @@ class Equipment {
|
|||||||
|
|
||||||
/// 모든 장비 스탯 합산
|
/// 모든 장비 스탯 합산
|
||||||
ItemStats get totalStats {
|
ItemStats get totalStats {
|
||||||
return items.fold(
|
return items.fold(ItemStats.empty, (sum, item) => sum + item.stats);
|
||||||
ItemStats.empty,
|
|
||||||
(sum, item) => sum + item.stats,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 모든 장비 무게 합산
|
/// 모든 장비 무게 합산
|
||||||
@@ -647,10 +645,7 @@ class Equipment {
|
|||||||
return Equipment(items: newItems, bestIndex: bestIndex);
|
return Equipment(items: newItems, bestIndex: bestIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
Equipment copyWith({
|
Equipment copyWith({List<EquipmentItem>? items, int? bestIndex}) {
|
||||||
List<EquipmentItem>? items,
|
|
||||||
int? bestIndex,
|
|
||||||
}) {
|
|
||||||
return Equipment(
|
return Equipment(
|
||||||
items: items ?? List<EquipmentItem>.from(this.items),
|
items: items ?? List<EquipmentItem>.from(this.items),
|
||||||
bestIndex: bestIndex ?? this.bestIndex,
|
bestIndex: bestIndex ?? this.bestIndex,
|
||||||
|
|||||||
@@ -175,9 +175,7 @@ class HallOfFame {
|
|||||||
|
|
||||||
/// JSON으로 직렬화
|
/// JSON으로 직렬화
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
return {
|
return {'entries': entries.map((e) => e.toJson()).toList()};
|
||||||
'entries': entries.map((e) => e.toJson()).toList(),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// JSON에서 역직렬화
|
/// JSON에서 역직렬화
|
||||||
|
|||||||
@@ -88,10 +88,7 @@ class PotionInventory {
|
|||||||
PotionInventory addPotion(String potionId, [int count = 1]) {
|
PotionInventory addPotion(String potionId, [int count = 1]) {
|
||||||
final newPotions = Map<String, int>.from(potions);
|
final newPotions = Map<String, int>.from(potions);
|
||||||
newPotions[potionId] = (newPotions[potionId] ?? 0) + count;
|
newPotions[potionId] = (newPotions[potionId] ?? 0) + count;
|
||||||
return PotionInventory(
|
return PotionInventory(potions: newPotions, usedInBattle: usedInBattle);
|
||||||
potions: newPotions,
|
|
||||||
usedInBattle: usedInBattle,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 물약 사용 (수량 감소)
|
/// 물약 사용 (수량 감소)
|
||||||
@@ -107,18 +104,12 @@ class PotionInventory {
|
|||||||
|
|
||||||
final newUsed = Set<PotionType>.from(usedInBattle)..add(type);
|
final newUsed = Set<PotionType>.from(usedInBattle)..add(type);
|
||||||
|
|
||||||
return PotionInventory(
|
return PotionInventory(potions: newPotions, usedInBattle: newUsed);
|
||||||
potions: newPotions,
|
|
||||||
usedInBattle: newUsed,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 전투 종료 시 사용 기록 초기화
|
/// 전투 종료 시 사용 기록 초기화
|
||||||
PotionInventory resetBattleUsage() {
|
PotionInventory resetBattleUsage() {
|
||||||
return PotionInventory(
|
return PotionInventory(potions: potions, usedInBattle: const {});
|
||||||
potions: potions,
|
|
||||||
usedInBattle: const {},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 빈 인벤토리
|
/// 빈 인벤토리
|
||||||
|
|||||||
@@ -1,12 +1,5 @@
|
|||||||
/// 스탯 타입 열거형 (stat type)
|
/// 스탯 타입 열거형 (stat type)
|
||||||
enum StatType {
|
enum StatType { str, con, dex, intelligence, wis, cha }
|
||||||
str,
|
|
||||||
con,
|
|
||||||
dex,
|
|
||||||
intelligence,
|
|
||||||
wis,
|
|
||||||
cha,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 패시브 능력 타입 (passive ability type)
|
/// 패시브 능력 타입 (passive ability type)
|
||||||
enum PassiveType {
|
enum PassiveType {
|
||||||
|
|||||||
@@ -1,3 +1,28 @@
|
|||||||
|
// ============================================================================
|
||||||
|
// 랭크 스케일링 (Rank Scaling)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/// 스펠 랭크에 따른 스킬 배율 계산
|
||||||
|
///
|
||||||
|
/// 랭크 1: 1.0x, 랭크 2: 1.15x, 랭크 3: 1.30x, ...
|
||||||
|
double getRankMultiplier(int rank) => 1.0 + (rank - 1) * 0.15;
|
||||||
|
|
||||||
|
/// 랭크에 따른 쿨타임 감소율 계산
|
||||||
|
///
|
||||||
|
/// 랭크당 5% 감소 (최대 50% 감소)
|
||||||
|
double getRankCooldownMultiplier(int rank) =>
|
||||||
|
(1.0 - (rank - 1) * 0.05).clamp(0.5, 1.0);
|
||||||
|
|
||||||
|
/// 랭크에 따른 MP 비용 감소율 계산
|
||||||
|
///
|
||||||
|
/// 랭크당 3% 감소 (최대 30% 감소)
|
||||||
|
double getRankMpMultiplier(int rank) =>
|
||||||
|
(1.0 - (rank - 1) * 0.03).clamp(0.7, 1.0);
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// 스킬 타입 (Skill Types)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
/// 스킬 타입
|
/// 스킬 타입
|
||||||
enum SkillType {
|
enum SkillType {
|
||||||
/// 공격 스킬
|
/// 공격 스킬
|
||||||
@@ -103,6 +128,9 @@ class Skill {
|
|||||||
this.baseDotDamage,
|
this.baseDotDamage,
|
||||||
this.baseDotDurationMs,
|
this.baseDotDurationMs,
|
||||||
this.baseDotTickMs,
|
this.baseDotTickMs,
|
||||||
|
this.hitCount = 1,
|
||||||
|
this.lifestealPercent = 0.0,
|
||||||
|
this.mpHealAmount = 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// 스킬 ID
|
/// 스킬 ID
|
||||||
@@ -156,6 +184,15 @@ class Skill {
|
|||||||
/// DOT 기본 틱 간격 (밀리초, 스킬 레벨로 결정)
|
/// DOT 기본 틱 간격 (밀리초, 스킬 레벨로 결정)
|
||||||
final int? baseDotTickMs;
|
final int? baseDotTickMs;
|
||||||
|
|
||||||
|
/// 다중 타격 횟수 (기본 1)
|
||||||
|
final int hitCount;
|
||||||
|
|
||||||
|
/// HP 흡수율 (0.0 ~ 1.0, 데미지의 N% 회복)
|
||||||
|
final double lifestealPercent;
|
||||||
|
|
||||||
|
/// MP 회복량 (MP 회복 스킬용)
|
||||||
|
final int mpHealAmount;
|
||||||
|
|
||||||
/// 공격 스킬 여부
|
/// 공격 스킬 여부
|
||||||
bool get isAttack => type == SkillType.attack;
|
bool get isAttack => type == SkillType.attack;
|
||||||
|
|
||||||
@@ -207,11 +244,7 @@ class SkillState {
|
|||||||
return cooldownMs - elapsed;
|
return cooldownMs - elapsed;
|
||||||
}
|
}
|
||||||
|
|
||||||
SkillState copyWith({
|
SkillState copyWith({String? skillId, int? lastUsedMs, int? rank}) {
|
||||||
String? skillId,
|
|
||||||
int? lastUsedMs,
|
|
||||||
int? rank,
|
|
||||||
}) {
|
|
||||||
return SkillState(
|
return SkillState(
|
||||||
skillId: skillId ?? this.skillId,
|
skillId: skillId ?? this.skillId,
|
||||||
lastUsedMs: lastUsedMs ?? this.lastUsedMs,
|
lastUsedMs: lastUsedMs ?? this.lastUsedMs,
|
||||||
@@ -302,11 +335,7 @@ class SkillUseResult {
|
|||||||
|
|
||||||
/// 실패 결과 생성
|
/// 실패 결과 생성
|
||||||
factory SkillUseResult.failed(Skill skill, SkillFailReason reason) {
|
factory SkillUseResult.failed(Skill skill, SkillFailReason reason) {
|
||||||
return SkillUseResult(
|
return SkillUseResult(skill: skill, success: false, failReason: reason);
|
||||||
skill: skill,
|
|
||||||
success: false,
|
|
||||||
failReason: reason,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -384,7 +413,11 @@ class DotEffect {
|
|||||||
/// [skill] DOT 스킬
|
/// [skill] DOT 스킬
|
||||||
/// [playerInt] 플레이어 INT (틱당 데미지 보정)
|
/// [playerInt] 플레이어 INT (틱당 데미지 보정)
|
||||||
/// [playerWis] 플레이어 WIS (틱 간격 보정)
|
/// [playerWis] 플레이어 WIS (틱 간격 보정)
|
||||||
factory DotEffect.fromSkill(Skill skill, {int playerInt = 10, int playerWis = 10}) {
|
factory DotEffect.fromSkill(
|
||||||
|
Skill skill, {
|
||||||
|
int playerInt = 10,
|
||||||
|
int playerWis = 10,
|
||||||
|
}) {
|
||||||
assert(skill.isDot, 'DOT 스킬만 DotEffect 생성 가능');
|
assert(skill.isDot, 'DOT 스킬만 DotEffect 생성 가능');
|
||||||
assert(skill.baseDotDamage != null, 'baseDotDamage 필수');
|
assert(skill.baseDotDamage != null, 'baseDotDamage 필수');
|
||||||
assert(skill.baseDotDurationMs != null, 'baseDotDurationMs 필수');
|
assert(skill.baseDotDurationMs != null, 'baseDotDurationMs 필수');
|
||||||
@@ -396,7 +429,9 @@ class DotEffect {
|
|||||||
|
|
||||||
// WIS → 틱 간격 보정 (WIS 10 기준, ±2%/포인트, 빨라짐)
|
// WIS → 틱 간격 보정 (WIS 10 기준, ±2%/포인트, 빨라짐)
|
||||||
final wisMod = 1.0 + (playerWis - 10) * 0.02;
|
final wisMod = 1.0 + (playerWis - 10) * 0.02;
|
||||||
final actualTickMs = (skill.baseDotTickMs! / wisMod).clamp(200, 2000).round();
|
final actualTickMs = (skill.baseDotTickMs! / wisMod)
|
||||||
|
.clamp(200, 2000)
|
||||||
|
.round();
|
||||||
|
|
||||||
return DotEffect(
|
return DotEffect(
|
||||||
skillId: skill.id,
|
skillId: skill.id,
|
||||||
|
|||||||
@@ -56,24 +56,28 @@ class NotificationService {
|
|||||||
|
|
||||||
/// 레벨업 알림 (Level Up Notification)
|
/// 레벨업 알림 (Level Up Notification)
|
||||||
void showLevelUp(int newLevel) {
|
void showLevelUp(int newLevel) {
|
||||||
show(GameNotification(
|
show(
|
||||||
type: NotificationType.levelUp,
|
GameNotification(
|
||||||
title: 'LEVEL UP!',
|
type: NotificationType.levelUp,
|
||||||
subtitle: 'Level $newLevel',
|
title: 'LEVEL UP!',
|
||||||
data: {'level': newLevel},
|
subtitle: 'Level $newLevel',
|
||||||
duration: const Duration(seconds: 2),
|
data: {'level': newLevel},
|
||||||
));
|
duration: const Duration(seconds: 2),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 퀘스트 완료 알림
|
/// 퀘스트 완료 알림
|
||||||
void showQuestComplete(String questName) {
|
void showQuestComplete(String questName) {
|
||||||
show(GameNotification(
|
show(
|
||||||
type: NotificationType.questComplete,
|
GameNotification(
|
||||||
title: 'QUEST COMPLETE!',
|
type: NotificationType.questComplete,
|
||||||
subtitle: questName,
|
title: 'QUEST COMPLETE!',
|
||||||
data: {'quest': questName},
|
subtitle: questName,
|
||||||
duration: const Duration(seconds: 2),
|
data: {'quest': questName},
|
||||||
));
|
duration: const Duration(seconds: 2),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 막 완료 알림 (Act Complete)
|
/// 막 완료 알림 (Act Complete)
|
||||||
@@ -82,44 +86,52 @@ class NotificationService {
|
|||||||
final title = actNumber == 0
|
final title = actNumber == 0
|
||||||
? 'PROLOGUE COMPLETE!'
|
? 'PROLOGUE COMPLETE!'
|
||||||
: 'ACT $actNumber COMPLETE!';
|
: 'ACT $actNumber COMPLETE!';
|
||||||
show(GameNotification(
|
show(
|
||||||
type: NotificationType.actComplete,
|
GameNotification(
|
||||||
title: title,
|
type: NotificationType.actComplete,
|
||||||
duration: const Duration(seconds: 3),
|
title: title,
|
||||||
));
|
duration: const Duration(seconds: 3),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 새 주문 알림
|
/// 새 주문 알림
|
||||||
void showNewSpell(String spellName) {
|
void showNewSpell(String spellName) {
|
||||||
show(GameNotification(
|
show(
|
||||||
type: NotificationType.newSpell,
|
GameNotification(
|
||||||
title: 'NEW SPELL!',
|
type: NotificationType.newSpell,
|
||||||
subtitle: spellName,
|
title: 'NEW SPELL!',
|
||||||
data: {'spell': spellName},
|
subtitle: spellName,
|
||||||
duration: const Duration(seconds: 2),
|
data: {'spell': spellName},
|
||||||
));
|
duration: const Duration(seconds: 2),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 새 장비 알림
|
/// 새 장비 알림
|
||||||
void showNewEquipment(String equipmentName, String slot) {
|
void showNewEquipment(String equipmentName, String slot) {
|
||||||
show(GameNotification(
|
show(
|
||||||
type: NotificationType.newEquipment,
|
GameNotification(
|
||||||
title: 'NEW EQUIPMENT!',
|
type: NotificationType.newEquipment,
|
||||||
subtitle: equipmentName,
|
title: 'NEW EQUIPMENT!',
|
||||||
data: {'equipment': equipmentName, 'slot': slot},
|
subtitle: equipmentName,
|
||||||
duration: const Duration(seconds: 2),
|
data: {'equipment': equipmentName, 'slot': slot},
|
||||||
));
|
duration: const Duration(seconds: 2),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 보스 처치 알림
|
/// 보스 처치 알림
|
||||||
void showBossDefeat(String bossName) {
|
void showBossDefeat(String bossName) {
|
||||||
show(GameNotification(
|
show(
|
||||||
type: NotificationType.bossDefeat,
|
GameNotification(
|
||||||
title: 'BOSS DEFEATED!',
|
type: NotificationType.bossDefeat,
|
||||||
subtitle: bossName,
|
title: 'BOSS DEFEATED!',
|
||||||
data: {'boss': bossName},
|
subtitle: bossName,
|
||||||
duration: const Duration(seconds: 3),
|
data: {'boss': bossName},
|
||||||
));
|
duration: const Duration(seconds: 3),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 큐 처리 (Process Queue)
|
/// 큐 처리 (Process Queue)
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
import 'package:shared_preferences/shared_preferences.dart';
|
|
||||||
|
|
||||||
import 'package:askiineverdie/src/core/animation/ascii_animation_data.dart';
|
|
||||||
|
|
||||||
/// 테마 설정 저장/로드 서비스
|
|
||||||
class ThemePreferences {
|
|
||||||
static const _keyColorTheme = 'ascii_color_theme';
|
|
||||||
|
|
||||||
/// 테마 설정 저장
|
|
||||||
static Future<void> saveColorTheme(AsciiColorTheme theme) async {
|
|
||||||
final prefs = await SharedPreferences.getInstance();
|
|
||||||
await prefs.setInt(_keyColorTheme, theme.index);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 테마 설정 로드 (기본값: green)
|
|
||||||
static Future<AsciiColorTheme> loadColorTheme() async {
|
|
||||||
final prefs = await SharedPreferences.getInstance();
|
|
||||||
final index = prefs.getInt(_keyColorTheme);
|
|
||||||
if (index == null || index < 0 || index >= AsciiColorTheme.values.length) {
|
|
||||||
return AsciiColorTheme.green;
|
|
||||||
}
|
|
||||||
return AsciiColorTheme.values[index];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -42,12 +42,7 @@ class EquipResult {
|
|||||||
|
|
||||||
/// 아이템 생성 결과 (구조화된 데이터로 l10n 지원)
|
/// 아이템 생성 결과 (구조화된 데이터로 l10n 지원)
|
||||||
class ItemResult {
|
class ItemResult {
|
||||||
const ItemResult({
|
const ItemResult({this.attrib, this.special, this.itemOf, this.boringItem});
|
||||||
this.attrib,
|
|
||||||
this.special,
|
|
||||||
this.itemOf,
|
|
||||||
this.boringItem,
|
|
||||||
});
|
|
||||||
|
|
||||||
/// 아이템 속성 (예: "Golden")
|
/// 아이템 속성 (예: "Golden")
|
||||||
final String? attrib;
|
final String? attrib;
|
||||||
@@ -592,7 +587,8 @@ MonsterTaskResult monsterTask(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!definite) {
|
if (!definite) {
|
||||||
name = indefinite(name, qty);
|
// l10n 지원: 한국어/일본어에서는 관사 불필요
|
||||||
|
name = l10n.indefiniteL10n(name, qty);
|
||||||
}
|
}
|
||||||
|
|
||||||
return MonsterTaskResult(
|
return MonsterTaskResult(
|
||||||
@@ -638,10 +634,13 @@ class QuestResult {
|
|||||||
|
|
||||||
final String caption;
|
final String caption;
|
||||||
final RewardKind reward;
|
final RewardKind reward;
|
||||||
|
|
||||||
/// 몬스터 전체 데이터 (예: "Rat|5|tail") - 원본 fQuest.Caption
|
/// 몬스터 전체 데이터 (예: "Rat|5|tail") - 원본 fQuest.Caption
|
||||||
final String? monsterName;
|
final String? monsterName;
|
||||||
|
|
||||||
/// 몬스터 레벨 (파싱된 값)
|
/// 몬스터 레벨 (파싱된 값)
|
||||||
final int? monsterLevel;
|
final int? monsterLevel;
|
||||||
|
|
||||||
/// 몬스터 인덱스 (config.monsters에서의 위치) - 원본 fQuest.Tag
|
/// 몬스터 인덱스 (config.monsters에서의 위치) - 원본 fQuest.Tag
|
||||||
final int? monsterIndex;
|
final int? monsterIndex;
|
||||||
}
|
}
|
||||||
@@ -694,10 +693,7 @@ QuestResult completeQuest(PqConfig config, DeterministicRandom rng, int level) {
|
|||||||
case 2:
|
case 2:
|
||||||
final itemEn = boringItem(config, rng);
|
final itemEn = boringItem(config, rng);
|
||||||
final item = l10n.translateBoringItem(itemEn);
|
final item = l10n.translateBoringItem(itemEn);
|
||||||
return QuestResult(
|
return QuestResult(caption: l10n.questTransfer(item), reward: reward);
|
||||||
caption: l10n.questTransfer(item),
|
|
||||||
reward: reward,
|
|
||||||
);
|
|
||||||
case 3:
|
case 3:
|
||||||
final itemEn = boringItem(config, rng);
|
final itemEn = boringItem(config, rng);
|
||||||
final item = l10n.translateBoringItem(itemEn);
|
final item = l10n.translateBoringItem(itemEn);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:askiineverdie/data/game_text_l10n.dart' as game_l10n;
|
||||||
import 'package:askiineverdie/l10n/app_localizations.dart';
|
import 'package:askiineverdie/l10n/app_localizations.dart';
|
||||||
|
|
||||||
class FrontScreen extends StatelessWidget {
|
class FrontScreen extends StatelessWidget {
|
||||||
@@ -128,7 +129,7 @@ class _HeroHeader extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 6),
|
const SizedBox(height: 6),
|
||||||
Text(
|
Text(
|
||||||
'Offline Progress Quest (PQ 6.4) rebuilt with Flutter.',
|
game_l10n.frontDescription,
|
||||||
style: theme.textTheme.titleMedium?.copyWith(
|
style: theme.textTheme.titleMedium?.copyWith(
|
||||||
color: colorScheme.onPrimary.withValues(alpha: 0.9),
|
color: colorScheme.onPrimary.withValues(alpha: 0.9),
|
||||||
),
|
),
|
||||||
@@ -146,9 +147,15 @@ class _HeroHeader extends StatelessWidget {
|
|||||||
spacing: 8,
|
spacing: 8,
|
||||||
runSpacing: 8,
|
runSpacing: 8,
|
||||||
children: [
|
children: [
|
||||||
_Tag(icon: Icons.cloud_off_outlined, label: l10n.tagNoNetwork),
|
_Tag(
|
||||||
|
icon: Icons.cloud_off_outlined,
|
||||||
|
label: l10n.tagNoNetwork,
|
||||||
|
),
|
||||||
_Tag(icon: Icons.timer_outlined, label: l10n.tagIdleRpg),
|
_Tag(icon: Icons.timer_outlined, label: l10n.tagIdleRpg),
|
||||||
_Tag(icon: Icons.storage_rounded, label: l10n.tagLocalSaves),
|
_Tag(
|
||||||
|
icon: Icons.storage_rounded,
|
||||||
|
label: l10n.tagLocalSaves,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -208,7 +215,7 @@ class _ActionRow extends StatelessWidget {
|
|||||||
TextButton.icon(
|
TextButton.icon(
|
||||||
onPressed: onHallOfFame,
|
onPressed: onHallOfFame,
|
||||||
icon: const Icon(Icons.emoji_events_outlined),
|
icon: const Icon(Icons.emoji_events_outlined),
|
||||||
label: const Text('Hall of Fame'),
|
label: Text(game_l10n.uiHallOfFame),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@@ -245,7 +252,7 @@ class _StatusCards extends StatelessWidget {
|
|||||||
SizedBox(height: 16),
|
SizedBox(height: 16),
|
||||||
_InfoCard(
|
_InfoCard(
|
||||||
icon: Icons.checklist_rtl,
|
icon: Icons.checklist_rtl,
|
||||||
title: 'Today’s focus',
|
title: game_l10n.frontTodayFocus,
|
||||||
points: [
|
points: [
|
||||||
'Set up scaffold + lints.',
|
'Set up scaffold + lints.',
|
||||||
'Wire seed theme and initial navigation shell.',
|
'Wire seed theme and initial navigation shell.',
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:askiineverdie/data/game_text_l10n.dart' as game_l10n;
|
||||||
|
import 'package:askiineverdie/data/skill_data.dart';
|
||||||
import 'package:askiineverdie/data/story_data.dart';
|
import 'package:askiineverdie/data/story_data.dart';
|
||||||
import 'package:askiineverdie/l10n/app_localizations.dart';
|
import 'package:askiineverdie/l10n/app_localizations.dart';
|
||||||
import 'package:askiineverdie/src/core/animation/ascii_animation_type.dart';
|
import 'package:askiineverdie/src/core/animation/ascii_animation_type.dart';
|
||||||
@@ -8,6 +10,7 @@ import 'package:askiineverdie/src/core/model/combat_event.dart';
|
|||||||
import 'package:askiineverdie/src/core/l10n/game_data_l10n.dart';
|
import 'package:askiineverdie/src/core/l10n/game_data_l10n.dart';
|
||||||
import 'package:askiineverdie/src/core/model/game_state.dart';
|
import 'package:askiineverdie/src/core/model/game_state.dart';
|
||||||
import 'package:askiineverdie/src/core/model/hall_of_fame.dart';
|
import 'package:askiineverdie/src/core/model/hall_of_fame.dart';
|
||||||
|
import 'package:askiineverdie/src/core/model/skill.dart';
|
||||||
import 'package:askiineverdie/src/core/notification/notification_service.dart';
|
import 'package:askiineverdie/src/core/notification/notification_service.dart';
|
||||||
import 'package:askiineverdie/src/core/storage/hall_of_fame_storage.dart';
|
import 'package:askiineverdie/src/core/storage/hall_of_fame_storage.dart';
|
||||||
import 'package:askiineverdie/src/core/util/pq_logic.dart' as pq_logic;
|
import 'package:askiineverdie/src/core/util/pq_logic.dart' as pq_logic;
|
||||||
@@ -18,7 +21,6 @@ import 'package:askiineverdie/src/features/game/widgets/combat_log.dart';
|
|||||||
import 'package:askiineverdie/src/features/game/widgets/death_overlay.dart';
|
import 'package:askiineverdie/src/features/game/widgets/death_overlay.dart';
|
||||||
import 'package:askiineverdie/src/features/game/widgets/hp_mp_bar.dart';
|
import 'package:askiineverdie/src/features/game/widgets/hp_mp_bar.dart';
|
||||||
import 'package:askiineverdie/src/features/game/widgets/notification_overlay.dart';
|
import 'package:askiineverdie/src/features/game/widgets/notification_overlay.dart';
|
||||||
import 'package:askiineverdie/src/features/game/widgets/skill_panel.dart';
|
|
||||||
import 'package:askiineverdie/src/features/game/widgets/stats_panel.dart';
|
import 'package:askiineverdie/src/features/game/widgets/stats_panel.dart';
|
||||||
import 'package:askiineverdie/src/features/game/widgets/equipment_stats_panel.dart';
|
import 'package:askiineverdie/src/features/game/widgets/equipment_stats_panel.dart';
|
||||||
import 'package:askiineverdie/src/features/game/widgets/potion_inventory_panel.dart';
|
import 'package:askiineverdie/src/features/game/widgets/potion_inventory_panel.dart';
|
||||||
@@ -78,7 +80,10 @@ class _GamePlayScreenState extends State<GamePlayScreen>
|
|||||||
if (state.traits.level > _lastLevel && _lastLevel > 0) {
|
if (state.traits.level > _lastLevel && _lastLevel > 0) {
|
||||||
_specialAnimation = AsciiAnimationType.levelUp;
|
_specialAnimation = AsciiAnimationType.levelUp;
|
||||||
_notificationService.showLevelUp(state.traits.level);
|
_notificationService.showLevelUp(state.traits.level);
|
||||||
_addCombatLog('Level Up! Now level ${state.traits.level}', CombatLogType.levelUp);
|
_addCombatLog(
|
||||||
|
'${game_l10n.uiLevelUp} Lv.${state.traits.level}',
|
||||||
|
CombatLogType.levelUp,
|
||||||
|
);
|
||||||
_resetSpecialAnimationAfterFrame();
|
_resetSpecialAnimationAfterFrame();
|
||||||
|
|
||||||
// Phase 9: Act 변경 감지 (레벨 기반)
|
// Phase 9: Act 변경 감지 (레벨 기반)
|
||||||
@@ -111,7 +116,10 @@ class _GamePlayScreenState extends State<GamePlayScreen>
|
|||||||
.lastOrNull;
|
.lastOrNull;
|
||||||
if (completedQuest != null) {
|
if (completedQuest != null) {
|
||||||
_notificationService.showQuestComplete(completedQuest.caption);
|
_notificationService.showQuestComplete(completedQuest.caption);
|
||||||
_addCombatLog('Quest Complete: ${completedQuest.caption}', CombatLogType.questComplete);
|
_addCombatLog(
|
||||||
|
game_l10n.uiQuestComplete(completedQuest.caption),
|
||||||
|
CombatLogType.questComplete,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
_resetSpecialAnimationAfterFrame();
|
_resetSpecialAnimationAfterFrame();
|
||||||
}
|
}
|
||||||
@@ -131,11 +139,9 @@ class _GamePlayScreenState extends State<GamePlayScreen>
|
|||||||
|
|
||||||
/// Phase 8: 전투 로그 추가 (Add Combat Log Entry)
|
/// Phase 8: 전투 로그 추가 (Add Combat Log Entry)
|
||||||
void _addCombatLog(String message, CombatLogType type) {
|
void _addCombatLog(String message, CombatLogType type) {
|
||||||
_combatLogEntries.add(CombatLogEntry(
|
_combatLogEntries.add(
|
||||||
message: message,
|
CombatLogEntry(message: message, timestamp: DateTime.now(), type: type),
|
||||||
timestamp: DateTime.now(),
|
);
|
||||||
type: type,
|
|
||||||
));
|
|
||||||
// 최대 50개 유지
|
// 최대 50개 유지
|
||||||
if (_combatLogEntries.length > 50) {
|
if (_combatLogEntries.length > 50) {
|
||||||
_combatLogEntries.removeAt(0);
|
_combatLogEntries.removeAt(0);
|
||||||
@@ -167,53 +173,78 @@ class _GamePlayScreenState extends State<GamePlayScreen>
|
|||||||
|
|
||||||
/// 전투 이벤트를 메시지와 타입으로 변환
|
/// 전투 이벤트를 메시지와 타입으로 변환
|
||||||
(String, CombatLogType) _formatCombatEvent(CombatEvent event) {
|
(String, CombatLogType) _formatCombatEvent(CombatEvent event) {
|
||||||
|
final target = event.targetName ?? '';
|
||||||
return switch (event.type) {
|
return switch (event.type) {
|
||||||
CombatEventType.playerAttack => event.isCritical
|
CombatEventType.playerAttack =>
|
||||||
? ('CRITICAL! ${event.damage} damage to ${event.targetName}!', CombatLogType.critical)
|
event.isCritical
|
||||||
: ('You hit ${event.targetName} for ${event.damage} damage', CombatLogType.damage),
|
? (
|
||||||
|
game_l10n.combatCritical(event.damage, target),
|
||||||
|
CombatLogType.critical,
|
||||||
|
)
|
||||||
|
: (
|
||||||
|
game_l10n.combatYouHit(target, event.damage),
|
||||||
|
CombatLogType.damage,
|
||||||
|
),
|
||||||
CombatEventType.monsterAttack => (
|
CombatEventType.monsterAttack => (
|
||||||
'${event.targetName} hits you for ${event.damage} damage',
|
game_l10n.combatMonsterHitsYou(target, event.damage),
|
||||||
CombatLogType.monsterAttack,
|
CombatLogType.monsterAttack,
|
||||||
),
|
),
|
||||||
CombatEventType.playerEvade => (
|
CombatEventType.playerEvade => (
|
||||||
'You evaded ${event.targetName}\'s attack!',
|
game_l10n.combatYouEvaded(target),
|
||||||
CombatLogType.evade,
|
CombatLogType.evade,
|
||||||
),
|
),
|
||||||
CombatEventType.monsterEvade => (
|
CombatEventType.monsterEvade => (
|
||||||
'${event.targetName} evaded your attack!',
|
game_l10n.combatMonsterEvaded(target),
|
||||||
CombatLogType.evade,
|
CombatLogType.evade,
|
||||||
),
|
),
|
||||||
CombatEventType.playerBlock => (
|
CombatEventType.playerBlock => (
|
||||||
'Blocked! Reduced to ${event.damage} damage',
|
game_l10n.combatBlocked(event.damage),
|
||||||
CombatLogType.block,
|
CombatLogType.block,
|
||||||
),
|
),
|
||||||
CombatEventType.playerParry => (
|
CombatEventType.playerParry => (
|
||||||
'Parried! Reduced to ${event.damage} damage',
|
game_l10n.combatParried(event.damage),
|
||||||
CombatLogType.parry,
|
CombatLogType.parry,
|
||||||
),
|
),
|
||||||
CombatEventType.playerSkill => event.isCritical
|
CombatEventType.playerSkill =>
|
||||||
? ('CRITICAL ${event.skillName}! ${event.damage} damage!', CombatLogType.critical)
|
event.isCritical
|
||||||
: ('${event.skillName}: ${event.damage} damage', CombatLogType.spell),
|
? (
|
||||||
|
game_l10n.combatSkillCritical(
|
||||||
|
event.skillName ?? '',
|
||||||
|
event.damage,
|
||||||
|
),
|
||||||
|
CombatLogType.critical,
|
||||||
|
)
|
||||||
|
: (
|
||||||
|
game_l10n.combatSkillDamage(event.skillName ?? '', event.damage),
|
||||||
|
CombatLogType.spell,
|
||||||
|
),
|
||||||
CombatEventType.playerHeal => (
|
CombatEventType.playerHeal => (
|
||||||
'${event.skillName ?? "Heal"}: +${event.healAmount} HP',
|
game_l10n.combatSkillHeal(
|
||||||
CombatLogType.heal,
|
event.skillName ?? game_l10n.uiHeal,
|
||||||
|
event.healAmount,
|
||||||
),
|
),
|
||||||
|
CombatLogType.heal,
|
||||||
|
),
|
||||||
CombatEventType.playerBuff => (
|
CombatEventType.playerBuff => (
|
||||||
'${event.skillName} activated!',
|
game_l10n.combatBuffActivated(event.skillName ?? ''),
|
||||||
CombatLogType.buff,
|
CombatLogType.buff,
|
||||||
),
|
),
|
||||||
CombatEventType.dotTick => (
|
CombatEventType.dotTick => (
|
||||||
'${event.skillName} ticks for ${event.damage} damage',
|
game_l10n.combatDotTick(event.skillName ?? '', event.damage),
|
||||||
CombatLogType.dotTick,
|
CombatLogType.dotTick,
|
||||||
),
|
),
|
||||||
CombatEventType.playerPotion => (
|
CombatEventType.playerPotion => (
|
||||||
'${event.skillName}: +${event.healAmount} ${event.targetName}',
|
game_l10n.combatPotionUsed(
|
||||||
CombatLogType.potion,
|
event.skillName ?? '',
|
||||||
|
event.healAmount,
|
||||||
|
target,
|
||||||
),
|
),
|
||||||
|
CombatLogType.potion,
|
||||||
|
),
|
||||||
CombatEventType.potionDrop => (
|
CombatEventType.potionDrop => (
|
||||||
'Dropped: ${event.skillName}',
|
game_l10n.combatPotionDrop(event.skillName ?? ''),
|
||||||
CombatLogType.potionDrop,
|
CombatLogType.potionDrop,
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -394,105 +425,107 @@ class _GamePlayScreenState extends State<GamePlayScreen>
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text(L10n.of(context).progressQuestTitle(state.traits.name)),
|
title: Text(L10n.of(context).progressQuestTitle(state.traits.name)),
|
||||||
actions: [
|
actions: [
|
||||||
// 치트 버튼 (디버그용)
|
// 치트 버튼 (디버그용)
|
||||||
if (widget.controller.cheatsEnabled) ...[
|
if (widget.controller.cheatsEnabled) ...[
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Text('L+1'),
|
icon: const Text('L+1'),
|
||||||
tooltip: L10n.of(context).levelUp,
|
tooltip: L10n.of(context).levelUp,
|
||||||
onPressed: () => widget.controller.loop?.cheatCompleteTask(),
|
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: Stack(
|
|
||||||
children: [
|
|
||||||
// 메인 게임 UI
|
|
||||||
Column(
|
|
||||||
children: [
|
|
||||||
// 상단: ASCII 애니메이션 + Task Progress (Phase 7: 고정 4색 팔레트)
|
|
||||||
TaskProgressPanel(
|
|
||||||
progress: state.progress,
|
|
||||||
speedMultiplier: widget.controller.loop?.speedMultiplier ?? 1,
|
|
||||||
onSpeedCycle: () {
|
|
||||||
widget.controller.loop?.cycleSpeed();
|
|
||||||
setState(() {});
|
|
||||||
},
|
|
||||||
isPaused: !widget.controller.isRunning,
|
|
||||||
onPauseToggle: () async {
|
|
||||||
await widget.controller.togglePause();
|
|
||||||
setState(() {});
|
|
||||||
},
|
|
||||||
specialAnimation: _specialAnimation,
|
|
||||||
weaponName: state.equipment.weapon,
|
|
||||||
shieldName: state.equipment.shield,
|
|
||||||
characterLevel: state.traits.level,
|
|
||||||
monsterLevel: state.progress.currentTask.monsterLevel,
|
|
||||||
latestCombatEvent: state.progress.currentCombat?.recentEvents.lastOrNull,
|
|
||||||
),
|
),
|
||||||
|
IconButton(
|
||||||
// 메인 3패널 영역
|
icon: const Text('Q!'),
|
||||||
Expanded(
|
tooltip: L10n.of(context).completeQuest,
|
||||||
child: Row(
|
onPressed: () => widget.controller.loop?.cheatCompleteQuest(),
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
),
|
||||||
children: [
|
IconButton(
|
||||||
// 좌측 패널: Character Sheet
|
icon: const Text('P!'),
|
||||||
Expanded(flex: 2, child: _buildCharacterPanel(state)),
|
tooltip: L10n.of(context).completePlot,
|
||||||
|
onPressed: () => widget.controller.loop?.cheatCompletePlot(),
|
||||||
// 중앙 패널: Equipment/Inventory
|
|
||||||
Expanded(flex: 3, child: _buildEquipmentPanel(state)),
|
|
||||||
|
|
||||||
// 우측 패널: Plot/Quest
|
|
||||||
Expanded(flex: 2, child: _buildQuestPanel(state)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
],
|
||||||
|
),
|
||||||
|
body: Stack(
|
||||||
|
children: [
|
||||||
|
// 메인 게임 UI
|
||||||
|
Column(
|
||||||
|
children: [
|
||||||
|
// 상단: ASCII 애니메이션 + Task Progress (Phase 7: 고정 4색 팔레트)
|
||||||
|
TaskProgressPanel(
|
||||||
|
progress: state.progress,
|
||||||
|
speedMultiplier:
|
||||||
|
widget.controller.loop?.speedMultiplier ?? 1,
|
||||||
|
onSpeedCycle: () {
|
||||||
|
widget.controller.loop?.cycleSpeed();
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
|
isPaused: !widget.controller.isRunning,
|
||||||
|
onPauseToggle: () async {
|
||||||
|
await widget.controller.togglePause();
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
|
specialAnimation: _specialAnimation,
|
||||||
|
weaponName: state.equipment.weapon,
|
||||||
|
shieldName: state.equipment.shield,
|
||||||
|
characterLevel: state.traits.level,
|
||||||
|
monsterLevel: state.progress.currentTask.monsterLevel,
|
||||||
|
latestCombatEvent:
|
||||||
|
state.progress.currentCombat?.recentEvents.lastOrNull,
|
||||||
|
),
|
||||||
|
|
||||||
// Phase 4: 사망 오버레이 (Death Overlay)
|
// 메인 3패널 영역
|
||||||
if (state.isDead && state.deathInfo != null)
|
Expanded(
|
||||||
DeathOverlay(
|
child: Row(
|
||||||
deathInfo: state.deathInfo!,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
traits: state.traits,
|
children: [
|
||||||
onResurrect: () async {
|
// 좌측 패널: Character Sheet
|
||||||
// 1. 부활 처리 (HP/MP 회복, 장비 구매 - 게임 재개 없음)
|
Expanded(flex: 2, child: _buildCharacterPanel(state)),
|
||||||
await widget.controller.resurrect();
|
|
||||||
|
|
||||||
// 2. 부활 애니메이션 재생
|
// 중앙 패널: Equipment/Inventory
|
||||||
setState(() {
|
Expanded(flex: 3, child: _buildEquipmentPanel(state)),
|
||||||
_specialAnimation = AsciiAnimationType.resurrection;
|
|
||||||
});
|
|
||||||
|
|
||||||
// 3. 애니메이션 종료 후 게임 재개
|
// 우측 패널: Plot/Quest
|
||||||
final duration = getSpecialAnimationDuration(
|
Expanded(flex: 2, child: _buildQuestPanel(state)),
|
||||||
AsciiAnimationType.resurrection,
|
],
|
||||||
);
|
),
|
||||||
Future.delayed(Duration(milliseconds: duration), () async {
|
),
|
||||||
if (mounted) {
|
],
|
||||||
setState(() {
|
|
||||||
_specialAnimation = null;
|
|
||||||
});
|
|
||||||
// 부활 후 게임 재개 (새 루프 시작)
|
|
||||||
await widget.controller.resumeAfterResurrection();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
],
|
|
||||||
),
|
// Phase 4: 사망 오버레이 (Death Overlay)
|
||||||
|
if (state.isDead && state.deathInfo != null)
|
||||||
|
DeathOverlay(
|
||||||
|
deathInfo: state.deathInfo!,
|
||||||
|
traits: state.traits,
|
||||||
|
onResurrect: () async {
|
||||||
|
// 1. 부활 처리 (HP/MP 회복, 장비 구매 - 게임 재개 없음)
|
||||||
|
await widget.controller.resurrect();
|
||||||
|
|
||||||
|
// 2. 부활 애니메이션 재생
|
||||||
|
setState(() {
|
||||||
|
_specialAnimation = AsciiAnimationType.resurrection;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 3. 애니메이션 종료 후 게임 재개
|
||||||
|
final duration = getSpecialAnimationDuration(
|
||||||
|
AsciiAnimationType.resurrection,
|
||||||
|
);
|
||||||
|
Future.delayed(Duration(milliseconds: duration), () async {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_specialAnimation = null;
|
||||||
|
});
|
||||||
|
// 부활 후 게임 재개 (새 루프 시작)
|
||||||
|
await widget.controller.resumeAfterResurrection();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -519,13 +552,17 @@ class _GamePlayScreenState extends State<GamePlayScreen>
|
|||||||
// Phase 8: HP/MP 바 (사망 위험 시 깜빡임, 전투 중 몬스터 HP 표시)
|
// Phase 8: HP/MP 바 (사망 위험 시 깜빡임, 전투 중 몬스터 HP 표시)
|
||||||
// 전투 중에는 전투 스탯의 HP/MP 사용, 비전투 시 기본 스탯 사용
|
// 전투 중에는 전투 스탯의 HP/MP 사용, 비전투 시 기본 스탯 사용
|
||||||
HpMpBar(
|
HpMpBar(
|
||||||
hpCurrent: state.progress.currentCombat?.playerStats.hpCurrent ??
|
hpCurrent:
|
||||||
|
state.progress.currentCombat?.playerStats.hpCurrent ??
|
||||||
state.stats.hp,
|
state.stats.hp,
|
||||||
hpMax: state.progress.currentCombat?.playerStats.hpMax ??
|
hpMax:
|
||||||
|
state.progress.currentCombat?.playerStats.hpMax ??
|
||||||
state.stats.hpMax,
|
state.stats.hpMax,
|
||||||
mpCurrent: state.progress.currentCombat?.playerStats.mpCurrent ??
|
mpCurrent:
|
||||||
|
state.progress.currentCombat?.playerStats.mpCurrent ??
|
||||||
state.stats.mp,
|
state.stats.mp,
|
||||||
mpMax: state.progress.currentCombat?.playerStats.mpMax ??
|
mpMax:
|
||||||
|
state.progress.currentCombat?.playerStats.mpMax ??
|
||||||
state.stats.mpMax,
|
state.stats.mpMax,
|
||||||
// 전투 중일 때 몬스터 HP 정보 전달
|
// 전투 중일 때 몬스터 HP 정보 전달
|
||||||
monsterHpCurrent:
|
monsterHpCurrent:
|
||||||
@@ -545,16 +582,12 @@ class _GamePlayScreenState extends State<GamePlayScreen>
|
|||||||
'${l10n.xpNeededForNextLevel}',
|
'${l10n.xpNeededForNextLevel}',
|
||||||
),
|
),
|
||||||
|
|
||||||
// Spell Book
|
// 스킬 (Skills - SpellBook 기반)
|
||||||
_buildSectionHeader(l10n.spellBook),
|
_buildSectionHeader(l10n.spellBook),
|
||||||
Expanded(flex: 2, child: _buildSpellsList(state)),
|
Expanded(flex: 3, child: _buildSkillsList(state)),
|
||||||
|
|
||||||
// Phase 8: 스킬 (Skills with cooldown glow)
|
|
||||||
_buildSectionHeader('Skills'),
|
|
||||||
Expanded(flex: 2, child: SkillPanel(skillSystem: state.skillSystem)),
|
|
||||||
|
|
||||||
// 활성 버프 (Active Buffs)
|
// 활성 버프 (Active Buffs)
|
||||||
_buildSectionHeader('Buffs'),
|
_buildSectionHeader(game_l10n.uiBuffs),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: ActiveBuffPanel(
|
child: ActiveBuffPanel(
|
||||||
activeBuffs: state.skillSystem.activeBuffs,
|
activeBuffs: state.skillSystem.activeBuffs,
|
||||||
@@ -587,7 +620,7 @@ class _GamePlayScreenState extends State<GamePlayScreen>
|
|||||||
Expanded(child: _buildInventoryList(state)),
|
Expanded(child: _buildInventoryList(state)),
|
||||||
|
|
||||||
// Potions (물약 인벤토리)
|
// Potions (물약 인벤토리)
|
||||||
_buildSectionHeader('Potions'),
|
_buildSectionHeader(game_l10n.uiPotions),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: PotionInventoryPanel(
|
child: PotionInventoryPanel(
|
||||||
inventory: state.potionInventory,
|
inventory: state.potionInventory,
|
||||||
@@ -647,7 +680,8 @@ class _GamePlayScreenState extends State<GamePlayScreen>
|
|||||||
Colors.green,
|
Colors.green,
|
||||||
tooltip: state.progress.quest.max > 0
|
tooltip: state.progress.quest.max > 0
|
||||||
? l10n.percentComplete(
|
? l10n.percentComplete(
|
||||||
100 * state.progress.quest.position ~/
|
100 *
|
||||||
|
state.progress.quest.position ~/
|
||||||
state.progress.quest.max,
|
state.progress.quest.max,
|
||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
@@ -737,10 +771,16 @@ class _GamePlayScreenState extends State<GamePlayScreen>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildSpellsList(GameState state) {
|
/// 통합 스킬 목록 (SpellBook 기반)
|
||||||
|
///
|
||||||
|
/// 스펠 이름, 랭크, 스킬 타입, 쿨타임 표시
|
||||||
|
Widget _buildSkillsList(GameState state) {
|
||||||
if (state.spellBook.spells.isEmpty) {
|
if (state.spellBook.spells.isEmpty) {
|
||||||
return Center(
|
return Center(
|
||||||
child: Text(L10n.of(context).noSpellsYet, style: const TextStyle(fontSize: 11)),
|
child: Text(
|
||||||
|
L10n.of(context).noSpellsYet,
|
||||||
|
style: const TextStyle(fontSize: 11),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -749,21 +789,22 @@ class _GamePlayScreenState extends State<GamePlayScreen>
|
|||||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final spell = state.spellBook.spells[index];
|
final spell = state.spellBook.spells[index];
|
||||||
|
final skill = SkillData.getSkillBySpellName(spell.name);
|
||||||
final spellName = GameDataL10n.getSpellName(context, spell.name);
|
final spellName = GameDataL10n.getSpellName(context, spell.name);
|
||||||
return Row(
|
|
||||||
children: [
|
// 쿨타임 상태 확인
|
||||||
Expanded(
|
final skillState = skill != null
|
||||||
child: Text(
|
? state.skillSystem.getSkillState(skill.id)
|
||||||
spellName,
|
: null;
|
||||||
style: const TextStyle(fontSize: 11),
|
final isOnCooldown =
|
||||||
overflow: TextOverflow.ellipsis,
|
skillState != null &&
|
||||||
),
|
!skillState.isReady(state.skillSystem.elapsedMs, skill!.cooldownMs);
|
||||||
),
|
|
||||||
Text(
|
return _SkillRow(
|
||||||
spell.rank,
|
spellName: spellName,
|
||||||
style: const TextStyle(fontSize: 11, fontWeight: FontWeight.bold),
|
rank: spell.rank,
|
||||||
),
|
skill: skill,
|
||||||
],
|
isOnCooldown: isOnCooldown,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -880,7 +921,8 @@ class _GamePlayScreenState extends State<GamePlayScreen>
|
|||||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final quest = questHistory[index];
|
final quest = questHistory[index];
|
||||||
final isCurrentQuest = index == questHistory.length - 1 && !quest.isComplete;
|
final isCurrentQuest =
|
||||||
|
index == questHistory.length - 1 && !quest.isComplete;
|
||||||
|
|
||||||
return Row(
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
@@ -942,3 +984,70 @@ class _GamePlayScreenState extends State<GamePlayScreen>
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 스킬 행 위젯
|
||||||
|
///
|
||||||
|
/// 스펠 이름, 랭크, 스킬 타입 아이콘, 쿨타임 상태 표시
|
||||||
|
class _SkillRow extends StatelessWidget {
|
||||||
|
const _SkillRow({
|
||||||
|
required this.spellName,
|
||||||
|
required this.rank,
|
||||||
|
required this.skill,
|
||||||
|
required this.isOnCooldown,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String spellName;
|
||||||
|
final String rank;
|
||||||
|
final Skill? skill;
|
||||||
|
final bool isOnCooldown;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 1),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
// 스킬 타입 아이콘
|
||||||
|
_buildTypeIcon(),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
// 스킬 이름
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
spellName,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 11,
|
||||||
|
color: isOnCooldown ? Colors.grey : null,
|
||||||
|
),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// 쿨타임 표시
|
||||||
|
if (isOnCooldown)
|
||||||
|
const Icon(Icons.hourglass_empty, size: 10, color: Colors.orange),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
// 랭크
|
||||||
|
Text(
|
||||||
|
rank,
|
||||||
|
style: const TextStyle(fontSize: 11, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 스킬 타입별 아이콘
|
||||||
|
Widget _buildTypeIcon() {
|
||||||
|
if (skill == null) {
|
||||||
|
return const SizedBox(width: 12);
|
||||||
|
}
|
||||||
|
|
||||||
|
final (IconData icon, Color color) = switch (skill!.type) {
|
||||||
|
SkillType.attack => (Icons.flash_on, Colors.red),
|
||||||
|
SkillType.heal => (Icons.favorite, Colors.green),
|
||||||
|
SkillType.buff => (Icons.arrow_upward, Colors.blue),
|
||||||
|
SkillType.debuff => (Icons.arrow_downward, Colors.purple),
|
||||||
|
};
|
||||||
|
|
||||||
|
return Icon(icon, size: 12, color: color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -186,5 +186,6 @@ class GameSessionController extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 사망 상태 여부
|
/// 사망 상태 여부
|
||||||
bool get isDead => _status == GameSessionStatus.dead || (_state?.isDead ?? false);
|
bool get isDead =>
|
||||||
|
_status == GameSessionStatus.dead || (_state?.isDead ?? false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:askiineverdie/data/game_text_l10n.dart' as l10n;
|
||||||
import 'package:askiineverdie/src/core/model/skill.dart';
|
import 'package:askiineverdie/src/core/model/skill.dart';
|
||||||
|
|
||||||
/// 활성 버프 패널 위젯
|
/// 활성 버프 패널 위젯
|
||||||
@@ -18,10 +19,10 @@ class ActiveBuffPanel extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (activeBuffs.isEmpty) {
|
if (activeBuffs.isEmpty) {
|
||||||
return const Center(
|
return Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
'No active buffs',
|
l10n.uiNoActiveBuffs,
|
||||||
style: TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
color: Colors.grey,
|
color: Colors.grey,
|
||||||
fontStyle: FontStyle.italic,
|
fontStyle: FontStyle.italic,
|
||||||
@@ -43,10 +44,7 @@ class ActiveBuffPanel extends StatelessWidget {
|
|||||||
|
|
||||||
/// 개별 버프 행 위젯
|
/// 개별 버프 행 위젯
|
||||||
class _BuffRow extends StatelessWidget {
|
class _BuffRow extends StatelessWidget {
|
||||||
const _BuffRow({
|
const _BuffRow({required this.buff, required this.currentMs});
|
||||||
required this.buff,
|
|
||||||
required this.currentMs,
|
|
||||||
});
|
|
||||||
|
|
||||||
final ActiveBuff buff;
|
final ActiveBuff buff;
|
||||||
final int currentMs;
|
final int currentMs;
|
||||||
@@ -66,11 +64,7 @@ class _BuffRow extends StatelessWidget {
|
|||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
// 버프 아이콘
|
// 버프 아이콘
|
||||||
const Icon(
|
const Icon(Icons.trending_up, size: 14, color: Colors.lightBlue),
|
||||||
Icons.trending_up,
|
|
||||||
size: 14,
|
|
||||||
color: Colors.lightBlue,
|
|
||||||
),
|
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
|
|
||||||
// 버프 이름
|
// 버프 이름
|
||||||
@@ -92,8 +86,9 @@ class _BuffRow extends StatelessWidget {
|
|||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
color: remainingMs < 3000 ? Colors.orange : Colors.grey,
|
color: remainingMs < 3000 ? Colors.orange : Colors.grey,
|
||||||
fontWeight:
|
fontWeight: remainingMs < 3000
|
||||||
remainingMs < 3000 ? FontWeight.bold : FontWeight.normal,
|
? FontWeight.bold
|
||||||
|
: FontWeight.normal,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -117,11 +112,7 @@ class _BuffRow extends StatelessWidget {
|
|||||||
// 효과 목록
|
// 효과 목록
|
||||||
if (modifiers.isNotEmpty) ...[
|
if (modifiers.isNotEmpty) ...[
|
||||||
const SizedBox(height: 2),
|
const SizedBox(height: 2),
|
||||||
Wrap(
|
Wrap(spacing: 6, runSpacing: 2, children: modifiers),
|
||||||
spacing: 6,
|
|
||||||
runSpacing: 2,
|
|
||||||
children: modifiers,
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -134,35 +125,43 @@ class _BuffRow extends StatelessWidget {
|
|||||||
final effect = buff.effect;
|
final effect = buff.effect;
|
||||||
|
|
||||||
if (effect.atkModifier != 0) {
|
if (effect.atkModifier != 0) {
|
||||||
modifiers.add(_ModifierChip(
|
modifiers.add(
|
||||||
label: 'ATK',
|
_ModifierChip(
|
||||||
value: effect.atkModifier,
|
label: 'ATK',
|
||||||
isPositive: effect.atkModifier > 0,
|
value: effect.atkModifier,
|
||||||
));
|
isPositive: effect.atkModifier > 0,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (effect.defModifier != 0) {
|
if (effect.defModifier != 0) {
|
||||||
modifiers.add(_ModifierChip(
|
modifiers.add(
|
||||||
label: 'DEF',
|
_ModifierChip(
|
||||||
value: effect.defModifier,
|
label: 'DEF',
|
||||||
isPositive: effect.defModifier > 0,
|
value: effect.defModifier,
|
||||||
));
|
isPositive: effect.defModifier > 0,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (effect.criRateModifier != 0) {
|
if (effect.criRateModifier != 0) {
|
||||||
modifiers.add(_ModifierChip(
|
modifiers.add(
|
||||||
label: 'CRI',
|
_ModifierChip(
|
||||||
value: effect.criRateModifier,
|
label: 'CRI',
|
||||||
isPositive: effect.criRateModifier > 0,
|
value: effect.criRateModifier,
|
||||||
));
|
isPositive: effect.criRateModifier > 0,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (effect.evasionModifier != 0) {
|
if (effect.evasionModifier != 0) {
|
||||||
modifiers.add(_ModifierChip(
|
modifiers.add(
|
||||||
label: 'EVA',
|
_ModifierChip(
|
||||||
value: effect.evasionModifier,
|
label: 'EVA',
|
||||||
isPositive: effect.evasionModifier > 0,
|
value: effect.evasionModifier,
|
||||||
));
|
isPositive: effect.evasionModifier > 0,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return modifiers;
|
return modifiers;
|
||||||
|
|||||||
@@ -117,6 +117,9 @@ class _AsciiAnimationCardState extends State<AsciiAnimationCard> {
|
|||||||
// 전투 이벤트 동기화용 (Phase 5)
|
// 전투 이벤트 동기화용 (Phase 5)
|
||||||
int? _lastEventTimestamp;
|
int? _lastEventTimestamp;
|
||||||
bool _showCriticalEffect = false;
|
bool _showCriticalEffect = false;
|
||||||
|
bool _showBlockEffect = false;
|
||||||
|
bool _showParryEffect = false;
|
||||||
|
bool _showSkillEffect = false;
|
||||||
|
|
||||||
// 특수 애니메이션 프레임 수는 ascii_animation_type.dart의
|
// 특수 애니메이션 프레임 수는 ascii_animation_type.dart의
|
||||||
// specialAnimationFrameCounts 상수 사용
|
// specialAnimationFrameCounts 상수 사용
|
||||||
@@ -177,33 +180,114 @@ class _AsciiAnimationCardState extends State<AsciiAnimationCard> {
|
|||||||
// 전투 모드가 아니면 무시
|
// 전투 모드가 아니면 무시
|
||||||
if (_animationMode != AnimationMode.battle) return;
|
if (_animationMode != AnimationMode.battle) return;
|
||||||
|
|
||||||
// 이벤트 타입에 따라 페이즈 강제 전환
|
// 이벤트 타입에 따라 페이즈 및 효과 결정
|
||||||
final (targetPhase, isCritical) = switch (event.type) {
|
final (
|
||||||
|
targetPhase,
|
||||||
|
isCritical,
|
||||||
|
isBlock,
|
||||||
|
isParry,
|
||||||
|
isSkill,
|
||||||
|
) = switch (event.type) {
|
||||||
// 플레이어 공격 → attack 페이즈
|
// 플레이어 공격 → attack 페이즈
|
||||||
CombatEventType.playerAttack => (BattlePhase.attack, event.isCritical),
|
CombatEventType.playerAttack => (
|
||||||
CombatEventType.playerSkill => (BattlePhase.attack, event.isCritical),
|
BattlePhase.attack,
|
||||||
|
event.isCritical,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
// 스킬 사용 → attack 페이즈 + 스킬 이펙트
|
||||||
|
CombatEventType.playerSkill => (
|
||||||
|
BattlePhase.attack,
|
||||||
|
event.isCritical,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
|
||||||
// 몬스터 공격/플레이어 피격 → hit 페이즈
|
// 몬스터 공격 → hit 페이즈
|
||||||
CombatEventType.monsterAttack => (BattlePhase.hit, false),
|
CombatEventType.monsterAttack => (
|
||||||
CombatEventType.playerBlock => (BattlePhase.hit, false),
|
BattlePhase.hit,
|
||||||
CombatEventType.playerParry => (BattlePhase.hit, false),
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
// 블록 → hit 페이즈 + 블록 이펙트
|
||||||
|
CombatEventType.playerBlock => (
|
||||||
|
BattlePhase.hit,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
// 패리 → hit 페이즈 + 패리 이펙트
|
||||||
|
CombatEventType.playerParry => (
|
||||||
|
BattlePhase.hit,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
|
||||||
// 회피 → recover 페이즈 (빠른 회피 동작)
|
// 회피 → recover 페이즈 (빠른 회피 동작)
|
||||||
CombatEventType.playerEvade => (BattlePhase.recover, false),
|
CombatEventType.playerEvade => (
|
||||||
CombatEventType.monsterEvade => (BattlePhase.idle, false),
|
BattlePhase.recover,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
CombatEventType.monsterEvade => (
|
||||||
|
BattlePhase.idle,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
|
||||||
// 회복/버프 → idle 페이즈 유지
|
// 회복/버프 → idle 페이즈 유지
|
||||||
CombatEventType.playerHeal => (BattlePhase.idle, false),
|
CombatEventType.playerHeal => (
|
||||||
CombatEventType.playerBuff => (BattlePhase.idle, false),
|
BattlePhase.idle,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
CombatEventType.playerBuff => (
|
||||||
|
BattlePhase.idle,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
|
||||||
// DOT 틱 → attack 페이즈 (지속 피해)
|
// DOT 틱 → attack 페이즈 (지속 피해)
|
||||||
CombatEventType.dotTick => (BattlePhase.attack, false),
|
CombatEventType.dotTick => (
|
||||||
|
BattlePhase.attack,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
|
||||||
// 물약 사용 → idle 페이즈 유지
|
// 물약 사용 → idle 페이즈 유지
|
||||||
CombatEventType.playerPotion => (BattlePhase.idle, false),
|
CombatEventType.playerPotion => (
|
||||||
|
BattlePhase.idle,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
|
||||||
// 물약 드랍 → idle 페이즈 유지
|
// 물약 드랍 → idle 페이즈 유지
|
||||||
CombatEventType.potionDrop => (BattlePhase.idle, false),
|
CombatEventType.potionDrop => (
|
||||||
|
BattlePhase.idle,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
@@ -211,6 +295,9 @@ class _AsciiAnimationCardState extends State<AsciiAnimationCard> {
|
|||||||
_battleSubFrame = 0;
|
_battleSubFrame = 0;
|
||||||
_phaseFrameCount = 0;
|
_phaseFrameCount = 0;
|
||||||
_showCriticalEffect = isCritical;
|
_showCriticalEffect = isCritical;
|
||||||
|
_showBlockEffect = isBlock;
|
||||||
|
_showParryEffect = isParry;
|
||||||
|
_showSkillEffect = isSkill;
|
||||||
|
|
||||||
// 페이즈 인덱스 동기화
|
// 페이즈 인덱스 동기화
|
||||||
_phaseIndex = _battlePhaseSequence.indexWhere((p) => p.$1 == targetPhase);
|
_phaseIndex = _battlePhaseSequence.indexWhere((p) => p.$1 == targetPhase);
|
||||||
@@ -322,8 +409,11 @@ class _AsciiAnimationCardState extends State<AsciiAnimationCard> {
|
|||||||
_phaseIndex = (_phaseIndex + 1) % _battlePhaseSequence.length;
|
_phaseIndex = (_phaseIndex + 1) % _battlePhaseSequence.length;
|
||||||
_phaseFrameCount = 0;
|
_phaseFrameCount = 0;
|
||||||
_battleSubFrame = 0;
|
_battleSubFrame = 0;
|
||||||
// 크리티컬 이펙트 리셋 (페이즈 전환 시)
|
// 이펙트 리셋 (페이즈 전환 시)
|
||||||
_showCriticalEffect = false;
|
_showCriticalEffect = false;
|
||||||
|
_showBlockEffect = false;
|
||||||
|
_showParryEffect = false;
|
||||||
|
_showSkillEffect = false;
|
||||||
} else {
|
} else {
|
||||||
_battleSubFrame++;
|
_battleSubFrame++;
|
||||||
}
|
}
|
||||||
@@ -340,21 +430,22 @@ class _AsciiAnimationCardState extends State<AsciiAnimationCard> {
|
|||||||
/// 현재 애니메이션 레이어 생성
|
/// 현재 애니메이션 레이어 생성
|
||||||
List<AsciiLayer> _composeLayers() {
|
List<AsciiLayer> _composeLayers() {
|
||||||
return switch (_animationMode) {
|
return switch (_animationMode) {
|
||||||
AnimationMode.battle => _battleComposer?.composeLayers(
|
AnimationMode.battle =>
|
||||||
_battlePhase,
|
_battleComposer?.composeLayers(
|
||||||
_battleSubFrame,
|
_battlePhase,
|
||||||
widget.monsterBaseName,
|
_battleSubFrame,
|
||||||
_environment,
|
widget.monsterBaseName,
|
||||||
_globalTick,
|
_environment,
|
||||||
) ??
|
_globalTick,
|
||||||
[AsciiLayer.empty()],
|
) ??
|
||||||
|
[AsciiLayer.empty()],
|
||||||
AnimationMode.walking => _walkingComposer.composeLayers(_globalTick),
|
AnimationMode.walking => _walkingComposer.composeLayers(_globalTick),
|
||||||
AnimationMode.town => _townComposer.composeLayers(_globalTick),
|
AnimationMode.town => _townComposer.composeLayers(_globalTick),
|
||||||
AnimationMode.special => _specialComposer.composeLayers(
|
AnimationMode.special => _specialComposer.composeLayers(
|
||||||
_currentSpecialAnimation ?? AsciiAnimationType.levelUp,
|
_currentSpecialAnimation ?? AsciiAnimationType.levelUp,
|
||||||
_currentFrame,
|
_currentFrame,
|
||||||
_globalTick,
|
_globalTick,
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -363,17 +454,38 @@ class _AsciiAnimationCardState extends State<AsciiAnimationCard> {
|
|||||||
// Phase 7: 고정 4색 팔레트 사용 (colorTheme 무시)
|
// Phase 7: 고정 4색 팔레트 사용 (colorTheme 무시)
|
||||||
const bgColor = AsciiColors.background;
|
const bgColor = AsciiColors.background;
|
||||||
|
|
||||||
// 테두리 효과 결정 (특수 애니메이션 또는 크리티컬 히트)
|
// 테두리 효과 결정 (전투 이벤트 또는 특수 애니메이션)
|
||||||
final isSpecial = _currentSpecialAnimation != null;
|
final isSpecial = _currentSpecialAnimation != null;
|
||||||
Border? borderEffect;
|
Border? borderEffect;
|
||||||
if (_showCriticalEffect) {
|
if (_showCriticalEffect) {
|
||||||
// 크리티컬 히트: 노란색 테두리 (Phase 5)
|
// 크리티컬 히트: 노란색 테두리
|
||||||
borderEffect =
|
borderEffect = Border.all(
|
||||||
Border.all(color: Colors.yellow.withValues(alpha: 0.8), width: 2);
|
color: Colors.yellow.withValues(alpha: 0.8),
|
||||||
|
width: 2,
|
||||||
|
);
|
||||||
|
} else if (_showBlockEffect) {
|
||||||
|
// 블록 (방패 방어): 파란색 테두리
|
||||||
|
borderEffect = Border.all(
|
||||||
|
color: Colors.blue.withValues(alpha: 0.8),
|
||||||
|
width: 2,
|
||||||
|
);
|
||||||
|
} else if (_showParryEffect) {
|
||||||
|
// 패리 (무기 쳐내기): 주황색 테두리
|
||||||
|
borderEffect = Border.all(
|
||||||
|
color: Colors.orange.withValues(alpha: 0.8),
|
||||||
|
width: 2,
|
||||||
|
);
|
||||||
|
} else if (_showSkillEffect) {
|
||||||
|
// 스킬 사용: 마젠타 테두리
|
||||||
|
borderEffect = Border.all(
|
||||||
|
color: Colors.purple.withValues(alpha: 0.8),
|
||||||
|
width: 2,
|
||||||
|
);
|
||||||
} else if (isSpecial) {
|
} else if (isSpecial) {
|
||||||
// 특수 애니메이션: 시안 테두리
|
// 특수 애니메이션: 시안 테두리
|
||||||
borderEffect =
|
borderEffect = Border.all(
|
||||||
Border.all(color: AsciiColors.positive.withValues(alpha: 0.5));
|
color: AsciiColors.positive.withValues(alpha: 0.5),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'dart:async';
|
|||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:askiineverdie/data/game_text_l10n.dart' as l10n;
|
||||||
import 'package:askiineverdie/data/story_data.dart';
|
import 'package:askiineverdie/data/story_data.dart';
|
||||||
|
|
||||||
/// 시네마틱 뷰 위젯 (Phase 9: Cinematic UI)
|
/// 시네마틱 뷰 위젯 (Phase 9: Cinematic UI)
|
||||||
@@ -162,12 +163,9 @@ class _CinematicViewState extends State<CinematicView>
|
|||||||
right: 16,
|
right: 16,
|
||||||
child: TextButton(
|
child: TextButton(
|
||||||
onPressed: _skip,
|
onPressed: _skip,
|
||||||
child: const Text(
|
child: Text(
|
||||||
'SKIP',
|
l10n.uiSkip,
|
||||||
style: TextStyle(
|
style: const TextStyle(color: Colors.white54, fontSize: 14),
|
||||||
color: Colors.white54,
|
|
||||||
fontSize: 14,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -178,7 +176,7 @@ class _CinematicViewState extends State<CinematicView>
|
|||||||
left: 0,
|
left: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
child: Text(
|
child: Text(
|
||||||
'Tap to continue',
|
l10n.uiTapToContinue,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.white.withValues(alpha: 0.3),
|
color: Colors.white.withValues(alpha: 0.3),
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
@@ -246,8 +244,8 @@ class _ProgressDots extends StatelessWidget {
|
|||||||
color: isActive
|
color: isActive
|
||||||
? Colors.cyan
|
? Colors.cyan
|
||||||
: isPast
|
: isPast
|
||||||
? Colors.cyan.withValues(alpha: 0.5)
|
? Colors.cyan.withValues(alpha: 0.5)
|
||||||
: Colors.white.withValues(alpha: 0.2),
|
: Colors.white.withValues(alpha: 0.2),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -148,7 +148,10 @@ class _LogEntryTile extends StatelessWidget {
|
|||||||
(Color?, IconData?) _getStyleForType(CombatLogType type) {
|
(Color?, IconData?) _getStyleForType(CombatLogType type) {
|
||||||
return switch (type) {
|
return switch (type) {
|
||||||
CombatLogType.normal => (null, null),
|
CombatLogType.normal => (null, null),
|
||||||
CombatLogType.damage => (Colors.red.shade300, Icons.local_fire_department),
|
CombatLogType.damage => (
|
||||||
|
Colors.red.shade300,
|
||||||
|
Icons.local_fire_department,
|
||||||
|
),
|
||||||
CombatLogType.heal => (Colors.green.shade300, Icons.healing),
|
CombatLogType.heal => (Colors.green.shade300, Icons.healing),
|
||||||
CombatLogType.levelUp => (Colors.amber, Icons.arrow_upward),
|
CombatLogType.levelUp => (Colors.amber, Icons.arrow_upward),
|
||||||
CombatLogType.questComplete => (Colors.blue.shade300, Icons.check_circle),
|
CombatLogType.questComplete => (Colors.blue.shade300, Icons.check_circle),
|
||||||
@@ -158,7 +161,10 @@ class _LogEntryTile extends StatelessWidget {
|
|||||||
CombatLogType.evade => (Colors.cyan.shade300, Icons.directions_run),
|
CombatLogType.evade => (Colors.cyan.shade300, Icons.directions_run),
|
||||||
CombatLogType.block => (Colors.blueGrey.shade300, Icons.shield),
|
CombatLogType.block => (Colors.blueGrey.shade300, Icons.shield),
|
||||||
CombatLogType.parry => (Colors.teal.shade300, Icons.sports_kabaddi),
|
CombatLogType.parry => (Colors.teal.shade300, Icons.sports_kabaddi),
|
||||||
CombatLogType.monsterAttack => (Colors.deepOrange.shade300, Icons.dangerous),
|
CombatLogType.monsterAttack => (
|
||||||
|
Colors.deepOrange.shade300,
|
||||||
|
Icons.dangerous,
|
||||||
|
),
|
||||||
CombatLogType.buff => (Colors.lightBlue.shade300, Icons.trending_up),
|
CombatLogType.buff => (Colors.lightBlue.shade300, Icons.trending_up),
|
||||||
CombatLogType.dotTick => (Colors.deepPurple.shade300, Icons.whatshot),
|
CombatLogType.dotTick => (Colors.deepPurple.shade300, Icons.whatshot),
|
||||||
CombatLogType.potion => (Colors.pink.shade300, Icons.local_drink),
|
CombatLogType.potion => (Colors.pink.shade300, Icons.local_drink),
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:askiineverdie/data/game_text_l10n.dart' as l10n;
|
||||||
|
import 'package:askiineverdie/src/core/l10n/game_data_l10n.dart';
|
||||||
import 'package:askiineverdie/src/core/model/combat_event.dart';
|
import 'package:askiineverdie/src/core/model/combat_event.dart';
|
||||||
import 'package:askiineverdie/src/core/model/game_state.dart';
|
import 'package:askiineverdie/src/core/model/game_state.dart';
|
||||||
|
|
||||||
@@ -109,7 +111,7 @@ class DeathOverlay extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Text(
|
Text(
|
||||||
'YOU DIED',
|
l10n.deathYouDied,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 32,
|
fontSize: 32,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
@@ -133,7 +135,7 @@ class DeathOverlay extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
Text(
|
Text(
|
||||||
'Level ${deathInfo.levelAtDeath} ${traits.klass}',
|
'Level ${deathInfo.levelAtDeath} ${GameDataL10n.getKlassName(context, traits.klass)}',
|
||||||
style: theme.textTheme.bodyLarge?.copyWith(
|
style: theme.textTheme.bodyLarge?.copyWith(
|
||||||
color: theme.colorScheme.onSurfaceVariant,
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
@@ -174,9 +176,9 @@ class DeathOverlay extends StatelessWidget {
|
|||||||
|
|
||||||
String _getDeathCauseText() {
|
String _getDeathCauseText() {
|
||||||
return switch (deathInfo.cause) {
|
return switch (deathInfo.cause) {
|
||||||
DeathCause.monster => 'Killed by ${deathInfo.killerName}',
|
DeathCause.monster => l10n.deathKilledBy(deathInfo.killerName),
|
||||||
DeathCause.selfDamage => 'Self-inflicted damage',
|
DeathCause.selfDamage => l10n.deathSelfInflicted,
|
||||||
DeathCause.environment => 'Environmental hazard',
|
DeathCause.environment => l10n.deathEnvironmentalHazard,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -210,7 +212,7 @@ class DeathOverlay extends StatelessWidget {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'Sacrificed to Resurrect',
|
l10n.deathSacrificedToResurrect,
|
||||||
style: theme.textTheme.labelSmall?.copyWith(
|
style: theme.textTheme.labelSmall?.copyWith(
|
||||||
color: theme.colorScheme.onSurfaceVariant,
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
@@ -234,8 +236,8 @@ class DeathOverlay extends StatelessWidget {
|
|||||||
_buildInfoRow(
|
_buildInfoRow(
|
||||||
context,
|
context,
|
||||||
icon: Icons.check_circle_outline,
|
icon: Icons.check_circle_outline,
|
||||||
label: 'Equipment',
|
label: l10n.deathEquipment,
|
||||||
value: 'No sacrifice needed',
|
value: l10n.deathNoSacrificeNeeded,
|
||||||
isNegative: false,
|
isNegative: false,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
@@ -243,7 +245,7 @@ class DeathOverlay extends StatelessWidget {
|
|||||||
_buildInfoRow(
|
_buildInfoRow(
|
||||||
context,
|
context,
|
||||||
icon: Icons.monetization_on_outlined,
|
icon: Icons.monetization_on_outlined,
|
||||||
label: 'Gold Remaining',
|
label: l10n.deathGoldRemaining,
|
||||||
value: _formatGold(deathInfo.goldAtDeath),
|
value: _formatGold(deathInfo.goldAtDeath),
|
||||||
isNegative: false,
|
isNegative: false,
|
||||||
),
|
),
|
||||||
@@ -306,7 +308,7 @@ class DeathOverlay extends StatelessWidget {
|
|||||||
child: FilledButton.icon(
|
child: FilledButton.icon(
|
||||||
onPressed: onResurrect,
|
onPressed: onResurrect,
|
||||||
icon: const Icon(Icons.replay),
|
icon: const Icon(Icons.replay),
|
||||||
label: const Text('Resurrect'),
|
label: Text(l10n.deathResurrect),
|
||||||
style: FilledButton.styleFrom(
|
style: FilledButton.styleFrom(
|
||||||
backgroundColor: theme.colorScheme.primary,
|
backgroundColor: theme.colorScheme.primary,
|
||||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||||
@@ -324,7 +326,7 @@ class DeathOverlay extends StatelessWidget {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'Combat Log',
|
l10n.deathCombatLog,
|
||||||
style: theme.textTheme.labelMedium?.copyWith(
|
style: theme.textTheme.labelMedium?.copyWith(
|
||||||
color: theme.colorScheme.onSurfaceVariant,
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
@@ -378,69 +380,70 @@ class DeathOverlay extends StatelessWidget {
|
|||||||
|
|
||||||
/// 전투 이벤트를 아이콘, 색상, 메시지로 포맷
|
/// 전투 이벤트를 아이콘, 색상, 메시지로 포맷
|
||||||
(IconData, Color, String) _formatCombatEvent(CombatEvent event) {
|
(IconData, Color, String) _formatCombatEvent(CombatEvent event) {
|
||||||
|
final target = event.targetName ?? '';
|
||||||
return switch (event.type) {
|
return switch (event.type) {
|
||||||
CombatEventType.playerAttack => (
|
CombatEventType.playerAttack => (
|
||||||
event.isCritical ? Icons.flash_on : Icons.local_fire_department,
|
event.isCritical ? Icons.flash_on : Icons.local_fire_department,
|
||||||
event.isCritical ? Colors.yellow.shade300 : Colors.green.shade300,
|
event.isCritical ? Colors.yellow.shade300 : Colors.green.shade300,
|
||||||
event.isCritical
|
event.isCritical
|
||||||
? 'CRITICAL! ${event.damage} damage to ${event.targetName}'
|
? l10n.combatCritical(event.damage, target)
|
||||||
: 'Hit ${event.targetName} for ${event.damage} damage',
|
: l10n.combatYouHit(target, event.damage),
|
||||||
),
|
),
|
||||||
CombatEventType.monsterAttack => (
|
CombatEventType.monsterAttack => (
|
||||||
Icons.dangerous,
|
Icons.dangerous,
|
||||||
Colors.red.shade300,
|
Colors.red.shade300,
|
||||||
'${event.targetName} hits you for ${event.damage} damage',
|
l10n.combatMonsterHitsYou(target, event.damage),
|
||||||
),
|
),
|
||||||
CombatEventType.playerEvade => (
|
CombatEventType.playerEvade => (
|
||||||
Icons.directions_run,
|
Icons.directions_run,
|
||||||
Colors.cyan.shade300,
|
Colors.cyan.shade300,
|
||||||
'Evaded attack from ${event.targetName}',
|
l10n.combatEvadedAttackFrom(target),
|
||||||
),
|
),
|
||||||
CombatEventType.monsterEvade => (
|
CombatEventType.monsterEvade => (
|
||||||
Icons.directions_run,
|
Icons.directions_run,
|
||||||
Colors.orange.shade300,
|
Colors.orange.shade300,
|
||||||
'${event.targetName} evaded your attack',
|
l10n.combatMonsterEvaded(target),
|
||||||
),
|
),
|
||||||
CombatEventType.playerBlock => (
|
CombatEventType.playerBlock => (
|
||||||
Icons.shield,
|
Icons.shield,
|
||||||
Colors.blueGrey.shade300,
|
Colors.blueGrey.shade300,
|
||||||
'Blocked ${event.targetName}\'s attack (${event.damage} reduced)',
|
l10n.combatBlockedAttack(target, event.damage),
|
||||||
),
|
),
|
||||||
CombatEventType.playerParry => (
|
CombatEventType.playerParry => (
|
||||||
Icons.sports_kabaddi,
|
Icons.sports_kabaddi,
|
||||||
Colors.teal.shade300,
|
Colors.teal.shade300,
|
||||||
'Parried ${event.targetName}\'s attack (${event.damage} reduced)',
|
l10n.combatParriedAttack(target, event.damage),
|
||||||
),
|
),
|
||||||
CombatEventType.playerSkill => (
|
CombatEventType.playerSkill => (
|
||||||
Icons.auto_fix_high,
|
Icons.auto_fix_high,
|
||||||
Colors.purple.shade300,
|
Colors.purple.shade300,
|
||||||
'${event.skillName} deals ${event.damage} damage',
|
l10n.combatSkillDamage(event.skillName ?? '', event.damage),
|
||||||
),
|
),
|
||||||
CombatEventType.playerHeal => (
|
CombatEventType.playerHeal => (
|
||||||
Icons.healing,
|
Icons.healing,
|
||||||
Colors.green.shade300,
|
Colors.green.shade300,
|
||||||
'Healed for ${event.healAmount} HP',
|
l10n.combatHealedFor(event.healAmount),
|
||||||
),
|
),
|
||||||
CombatEventType.playerBuff => (
|
CombatEventType.playerBuff => (
|
||||||
Icons.trending_up,
|
Icons.trending_up,
|
||||||
Colors.lightBlue.shade300,
|
Colors.lightBlue.shade300,
|
||||||
'${event.skillName} activated',
|
l10n.combatBuffActivated(event.skillName ?? ''),
|
||||||
),
|
),
|
||||||
CombatEventType.dotTick => (
|
CombatEventType.dotTick => (
|
||||||
Icons.whatshot,
|
Icons.whatshot,
|
||||||
Colors.deepOrange.shade300,
|
Colors.deepOrange.shade300,
|
||||||
'${event.skillName} ticks for ${event.damage} damage',
|
l10n.combatDotTick(event.skillName ?? '', event.damage),
|
||||||
),
|
),
|
||||||
CombatEventType.playerPotion => (
|
CombatEventType.playerPotion => (
|
||||||
Icons.local_drink,
|
Icons.local_drink,
|
||||||
Colors.lightGreen.shade300,
|
Colors.lightGreen.shade300,
|
||||||
'${event.skillName}: +${event.healAmount} ${event.targetName}',
|
l10n.combatPotionUsed(event.skillName ?? '', event.healAmount, target),
|
||||||
),
|
),
|
||||||
CombatEventType.potionDrop => (
|
CombatEventType.potionDrop => (
|
||||||
Icons.card_giftcard,
|
Icons.card_giftcard,
|
||||||
Colors.lime.shade300,
|
Colors.lime.shade300,
|
||||||
'Dropped: ${event.skillName}',
|
l10n.combatPotionDrop(event.skillName ?? ''),
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:askiineverdie/data/game_text_l10n.dart' as l10n;
|
||||||
import 'package:askiineverdie/src/core/engine/item_service.dart';
|
import 'package:askiineverdie/src/core/engine/item_service.dart';
|
||||||
import 'package:askiineverdie/src/core/model/equipment_item.dart';
|
import 'package:askiineverdie/src/core/model/equipment_item.dart';
|
||||||
import 'package:askiineverdie/src/core/model/equipment_slot.dart';
|
import 'package:askiineverdie/src/core/model/equipment_slot.dart';
|
||||||
@@ -135,7 +136,7 @@ class _EmptySlotTile extends StatelessWidget {
|
|||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 8),
|
contentPadding: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
leading: _SlotIcon(slot: slot, isEmpty: true),
|
leading: _SlotIcon(slot: slot, isEmpty: true),
|
||||||
title: Text(
|
title: Text(
|
||||||
'[${_getSlotName(slot)}] (empty)',
|
'[${_getSlotName(slot)}] ${l10n.uiEmpty}',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
color: Colors.grey.shade600,
|
color: Colors.grey.shade600,
|
||||||
@@ -222,10 +223,7 @@ class _TotalScoreHeader extends StatelessWidget {
|
|||||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
gradient: LinearGradient(
|
gradient: LinearGradient(
|
||||||
colors: [
|
colors: [Colors.blueGrey.shade700, Colors.blueGrey.shade600],
|
||||||
Colors.blueGrey.shade700,
|
|
||||||
Colors.blueGrey.shade600,
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
@@ -239,11 +237,7 @@ class _TotalScoreHeader extends StatelessWidget {
|
|||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
// 장비 아이콘
|
// 장비 아이콘
|
||||||
const Icon(
|
const Icon(Icons.shield, size: 20, color: Colors.white70),
|
||||||
Icons.shield,
|
|
||||||
size: 20,
|
|
||||||
color: Colors.white70,
|
|
||||||
),
|
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
|
|
||||||
// 총합 점수
|
// 총합 점수
|
||||||
@@ -251,12 +245,9 @@ class _TotalScoreHeader extends StatelessWidget {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
const Text(
|
Text(
|
||||||
'Equipment Score',
|
l10n.uiEquipmentScore,
|
||||||
style: TextStyle(
|
style: const TextStyle(fontSize: 10, color: Colors.white70),
|
||||||
fontSize: 10,
|
|
||||||
color: Colors.white70,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
'$totalScore',
|
'$totalScore',
|
||||||
@@ -304,46 +295,80 @@ class _StatsGrid extends StatelessWidget {
|
|||||||
final entries = <_StatEntry>[];
|
final entries = <_StatEntry>[];
|
||||||
|
|
||||||
// 공격 스탯
|
// 공격 스탯
|
||||||
if (stats.atk > 0) entries.add(_StatEntry('ATK', '+${stats.atk}'));
|
if (stats.atk > 0) entries.add(_StatEntry(l10n.statAtk, '+${stats.atk}'));
|
||||||
if (stats.magAtk > 0) entries.add(_StatEntry('MATK', '+${stats.magAtk}'));
|
if (stats.magAtk > 0) {
|
||||||
|
entries.add(_StatEntry(l10n.statMAtk, '+${stats.magAtk}'));
|
||||||
|
}
|
||||||
if (stats.criRate > 0) {
|
if (stats.criRate > 0) {
|
||||||
entries.add(_StatEntry('CRI', '${(stats.criRate * 100).toStringAsFixed(1)}%'));
|
entries.add(
|
||||||
|
_StatEntry(l10n.statCri, '${(stats.criRate * 100).toStringAsFixed(1)}%'),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (stats.parryRate > 0) {
|
if (stats.parryRate > 0) {
|
||||||
entries.add(_StatEntry('PARRY', '${(stats.parryRate * 100).toStringAsFixed(1)}%'));
|
entries.add(
|
||||||
|
_StatEntry(
|
||||||
|
l10n.statParry,
|
||||||
|
'${(stats.parryRate * 100).toStringAsFixed(1)}%',
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 방어 스탯
|
// 방어 스탯
|
||||||
if (stats.def > 0) entries.add(_StatEntry('DEF', '+${stats.def}'));
|
if (stats.def > 0) entries.add(_StatEntry(l10n.statDef, '+${stats.def}'));
|
||||||
if (stats.magDef > 0) entries.add(_StatEntry('MDEF', '+${stats.magDef}'));
|
if (stats.magDef > 0) {
|
||||||
|
entries.add(_StatEntry(l10n.statMDef, '+${stats.magDef}'));
|
||||||
|
}
|
||||||
if (stats.blockRate > 0) {
|
if (stats.blockRate > 0) {
|
||||||
entries.add(_StatEntry('BLOCK', '${(stats.blockRate * 100).toStringAsFixed(1)}%'));
|
entries.add(
|
||||||
|
_StatEntry(
|
||||||
|
l10n.statBlock,
|
||||||
|
'${(stats.blockRate * 100).toStringAsFixed(1)}%',
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (stats.evasion > 0) {
|
if (stats.evasion > 0) {
|
||||||
entries.add(_StatEntry('EVA', '${(stats.evasion * 100).toStringAsFixed(1)}%'));
|
entries.add(
|
||||||
|
_StatEntry(l10n.statEva, '${(stats.evasion * 100).toStringAsFixed(1)}%'),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 자원 스탯
|
// 자원 스탯
|
||||||
if (stats.hpBonus > 0) entries.add(_StatEntry('HP', '+${stats.hpBonus}'));
|
if (stats.hpBonus > 0) {
|
||||||
if (stats.mpBonus > 0) entries.add(_StatEntry('MP', '+${stats.mpBonus}'));
|
entries.add(_StatEntry(l10n.statHp, '+${stats.hpBonus}'));
|
||||||
|
}
|
||||||
|
if (stats.mpBonus > 0) {
|
||||||
|
entries.add(_StatEntry(l10n.statMp, '+${stats.mpBonus}'));
|
||||||
|
}
|
||||||
|
|
||||||
// 능력치 보너스
|
// 능력치 보너스
|
||||||
if (stats.strBonus > 0) entries.add(_StatEntry('STR', '+${stats.strBonus}'));
|
if (stats.strBonus > 0) {
|
||||||
if (stats.conBonus > 0) entries.add(_StatEntry('CON', '+${stats.conBonus}'));
|
entries.add(_StatEntry(l10n.statStr, '+${stats.strBonus}'));
|
||||||
if (stats.dexBonus > 0) entries.add(_StatEntry('DEX', '+${stats.dexBonus}'));
|
}
|
||||||
if (stats.intBonus > 0) entries.add(_StatEntry('INT', '+${stats.intBonus}'));
|
if (stats.conBonus > 0) {
|
||||||
if (stats.wisBonus > 0) entries.add(_StatEntry('WIS', '+${stats.wisBonus}'));
|
entries.add(_StatEntry(l10n.statCon, '+${stats.conBonus}'));
|
||||||
if (stats.chaBonus > 0) entries.add(_StatEntry('CHA', '+${stats.chaBonus}'));
|
}
|
||||||
|
if (stats.dexBonus > 0) {
|
||||||
|
entries.add(_StatEntry(l10n.statDex, '+${stats.dexBonus}'));
|
||||||
|
}
|
||||||
|
if (stats.intBonus > 0) {
|
||||||
|
entries.add(_StatEntry(l10n.statInt, '+${stats.intBonus}'));
|
||||||
|
}
|
||||||
|
if (stats.wisBonus > 0) {
|
||||||
|
entries.add(_StatEntry(l10n.statWis, '+${stats.wisBonus}'));
|
||||||
|
}
|
||||||
|
if (stats.chaBonus > 0) {
|
||||||
|
entries.add(_StatEntry(l10n.statCha, '+${stats.chaBonus}'));
|
||||||
|
}
|
||||||
|
|
||||||
// 무기 공속
|
// 무기 공속
|
||||||
if (slot == EquipmentSlot.weapon && stats.attackSpeed > 0) {
|
if (slot == EquipmentSlot.weapon && stats.attackSpeed > 0) {
|
||||||
entries.add(_StatEntry('SPEED', '${stats.attackSpeed}ms'));
|
entries.add(_StatEntry(l10n.statSpeed, '${stats.attackSpeed}ms'));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (entries.isEmpty) {
|
if (entries.isEmpty) {
|
||||||
return const Text(
|
return Text(
|
||||||
'No bonus stats',
|
l10n.uiNoBonusStats,
|
||||||
style: TextStyle(fontSize: 10, color: Colors.grey),
|
style: const TextStyle(fontSize: 10, color: Colors.grey),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -406,7 +431,7 @@ class _ItemMetaRow extends StatelessWidget {
|
|||||||
return Row(
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'Lv.${item.level}',
|
l10n.uiLevel(item.level),
|
||||||
style: const TextStyle(fontSize: 9, color: Colors.grey),
|
style: const TextStyle(fontSize: 9, color: Colors.grey),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
@@ -420,7 +445,7 @@ class _ItemMetaRow extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Text(
|
Text(
|
||||||
'Wt.${item.weight}',
|
l10n.uiWeight(item.weight),
|
||||||
style: const TextStyle(fontSize: 9, color: Colors.grey),
|
style: const TextStyle(fontSize: 9, color: Colors.grey),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -441,16 +466,16 @@ class _ItemMetaRow extends StatelessWidget {
|
|||||||
/// 슬롯 이름 반환
|
/// 슬롯 이름 반환
|
||||||
String _getSlotName(EquipmentSlot slot) {
|
String _getSlotName(EquipmentSlot slot) {
|
||||||
return switch (slot) {
|
return switch (slot) {
|
||||||
EquipmentSlot.weapon => 'Weapon',
|
EquipmentSlot.weapon => l10n.slotWeapon,
|
||||||
EquipmentSlot.shield => 'Shield',
|
EquipmentSlot.shield => l10n.slotShield,
|
||||||
EquipmentSlot.helm => 'Helm',
|
EquipmentSlot.helm => l10n.slotHelm,
|
||||||
EquipmentSlot.hauberk => 'Hauberk',
|
EquipmentSlot.hauberk => l10n.slotHauberk,
|
||||||
EquipmentSlot.brassairts => 'Brassairts',
|
EquipmentSlot.brassairts => l10n.slotBrassairts,
|
||||||
EquipmentSlot.vambraces => 'Vambraces',
|
EquipmentSlot.vambraces => l10n.slotVambraces,
|
||||||
EquipmentSlot.gauntlets => 'Gauntlets',
|
EquipmentSlot.gauntlets => l10n.slotGauntlets,
|
||||||
EquipmentSlot.gambeson => 'Gambeson',
|
EquipmentSlot.gambeson => l10n.slotGambeson,
|
||||||
EquipmentSlot.cuisses => 'Cuisses',
|
EquipmentSlot.cuisses => l10n.slotCuisses,
|
||||||
EquipmentSlot.greaves => 'Greaves',
|
EquipmentSlot.greaves => l10n.slotGreaves,
|
||||||
EquipmentSlot.sollerets => 'Sollerets',
|
EquipmentSlot.sollerets => l10n.slotSollerets,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:askiineverdie/data/game_text_l10n.dart' as l10n;
|
||||||
|
|
||||||
/// HP/MP 바 위젯 (Phase 8: 변화 시 시각 효과)
|
/// HP/MP 바 위젯 (Phase 8: 변화 시 시각 효과)
|
||||||
///
|
///
|
||||||
/// - HP가 20% 미만일 때 빨간색 깜빡임
|
/// - HP가 20% 미만일 때 빨간색 깜빡임
|
||||||
@@ -151,7 +153,8 @@ class _HpMpBarState extends State<HpMpBar> with TickerProviderStateMixin {
|
|||||||
final hpRatio = widget.hpMax > 0 ? widget.hpCurrent / widget.hpMax : 0.0;
|
final hpRatio = widget.hpMax > 0 ? widget.hpCurrent / widget.hpMax : 0.0;
|
||||||
final mpRatio = widget.mpMax > 0 ? widget.mpCurrent / widget.mpMax : 0.0;
|
final mpRatio = widget.mpMax > 0 ? widget.mpCurrent / widget.mpMax : 0.0;
|
||||||
|
|
||||||
final hasMonster = widget.monsterHpCurrent != null &&
|
final hasMonster =
|
||||||
|
widget.monsterHpCurrent != null &&
|
||||||
widget.monsterHpMax != null &&
|
widget.monsterHpMax != null &&
|
||||||
widget.monsterHpMax! > 0;
|
widget.monsterHpMax! > 0;
|
||||||
|
|
||||||
@@ -162,7 +165,7 @@ class _HpMpBarState extends State<HpMpBar> with TickerProviderStateMixin {
|
|||||||
children: [
|
children: [
|
||||||
// HP 바 (플래시 효과 포함)
|
// HP 바 (플래시 효과 포함)
|
||||||
_buildAnimatedBar(
|
_buildAnimatedBar(
|
||||||
label: 'HP',
|
label: l10n.statHp,
|
||||||
current: widget.hpCurrent,
|
current: widget.hpCurrent,
|
||||||
max: widget.hpMax,
|
max: widget.hpMax,
|
||||||
ratio: hpRatio,
|
ratio: hpRatio,
|
||||||
@@ -176,7 +179,7 @@ class _HpMpBarState extends State<HpMpBar> with TickerProviderStateMixin {
|
|||||||
|
|
||||||
// MP 바 (플래시 효과 포함)
|
// MP 바 (플래시 효과 포함)
|
||||||
_buildAnimatedBar(
|
_buildAnimatedBar(
|
||||||
label: 'MP',
|
label: l10n.statMp,
|
||||||
current: widget.mpCurrent,
|
current: widget.mpCurrent,
|
||||||
max: widget.mpMax,
|
max: widget.mpMax,
|
||||||
ratio: mpRatio,
|
ratio: mpRatio,
|
||||||
@@ -188,10 +191,7 @@ class _HpMpBarState extends State<HpMpBar> with TickerProviderStateMixin {
|
|||||||
),
|
),
|
||||||
|
|
||||||
// 몬스터 HP 바 (전투 중일 때만)
|
// 몬스터 HP 바 (전투 중일 때만)
|
||||||
if (hasMonster) ...[
|
if (hasMonster) ...[const SizedBox(height: 8), _buildMonsterBar()],
|
||||||
const SizedBox(height: 8),
|
|
||||||
_buildMonsterBar(),
|
|
||||||
],
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -228,7 +228,13 @@ class _HpMpBarState extends State<HpMpBar> with TickerProviderStateMixin {
|
|||||||
),
|
),
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
_buildBar(label: label, current: current, max: max, ratio: ratio, color: color),
|
_buildBar(
|
||||||
|
label: label,
|
||||||
|
current: current,
|
||||||
|
max: max,
|
||||||
|
ratio: ratio,
|
||||||
|
color: color,
|
||||||
|
),
|
||||||
|
|
||||||
// 플로팅 변화량 텍스트 (위로 떠오르며 사라짐)
|
// 플로팅 변화량 텍스트 (위로 떠오르며 사라짐)
|
||||||
if (change != 0 && flashController.value > 0.05)
|
if (change != 0 && flashController.value > 0.05)
|
||||||
@@ -340,8 +346,9 @@ class _HpMpBarState extends State<HpMpBar> with TickerProviderStateMixin {
|
|||||||
child: LinearProgressIndicator(
|
child: LinearProgressIndicator(
|
||||||
value: ratio.clamp(0.0, 1.0),
|
value: ratio.clamp(0.0, 1.0),
|
||||||
backgroundColor: Colors.orange.withValues(alpha: 0.2),
|
backgroundColor: Colors.orange.withValues(alpha: 0.2),
|
||||||
valueColor:
|
valueColor: const AlwaysStoppedAnimation<Color>(
|
||||||
const AlwaysStoppedAnimation<Color>(Colors.orange),
|
Colors.orange,
|
||||||
|
),
|
||||||
minHeight: 8,
|
minHeight: 8,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -40,20 +40,21 @@ class _NotificationOverlayState extends State<NotificationOverlay>
|
|||||||
vsync: this,
|
vsync: this,
|
||||||
);
|
);
|
||||||
|
|
||||||
_slideAnimation = Tween<Offset>(
|
_slideAnimation =
|
||||||
begin: const Offset(0, -1),
|
Tween<Offset>(begin: const Offset(0, -1), end: Offset.zero).animate(
|
||||||
end: Offset.zero,
|
CurvedAnimation(
|
||||||
).animate(CurvedAnimation(
|
parent: _animationController,
|
||||||
parent: _animationController,
|
curve: Curves.easeOutBack,
|
||||||
curve: Curves.easeOutBack,
|
),
|
||||||
));
|
);
|
||||||
|
|
||||||
_fadeAnimation = Tween<double>(begin: 0, end: 1).animate(
|
_fadeAnimation = Tween<double>(begin: 0, end: 1).animate(
|
||||||
CurvedAnimation(parent: _animationController, curve: Curves.easeIn),
|
CurvedAnimation(parent: _animationController, curve: Curves.easeIn),
|
||||||
);
|
);
|
||||||
|
|
||||||
_notificationSub =
|
_notificationSub = widget.notificationService.notifications.listen(
|
||||||
widget.notificationService.notifications.listen(_onNotification);
|
_onNotification,
|
||||||
|
);
|
||||||
_dismissSub = widget.notificationService.dismissals.listen(_onDismiss);
|
_dismissSub = widget.notificationService.dismissals.listen(_onDismiss);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -184,35 +185,35 @@ class _NotificationCard extends StatelessWidget {
|
|||||||
(Color, IconData, Color) _getStyleForType(NotificationType type) {
|
(Color, IconData, Color) _getStyleForType(NotificationType type) {
|
||||||
return switch (type) {
|
return switch (type) {
|
||||||
NotificationType.levelUp => (
|
NotificationType.levelUp => (
|
||||||
const Color(0xFF1565C0),
|
const Color(0xFF1565C0),
|
||||||
Icons.trending_up,
|
Icons.trending_up,
|
||||||
Colors.amber,
|
Colors.amber,
|
||||||
),
|
),
|
||||||
NotificationType.questComplete => (
|
NotificationType.questComplete => (
|
||||||
const Color(0xFF2E7D32),
|
const Color(0xFF2E7D32),
|
||||||
Icons.check_circle,
|
Icons.check_circle,
|
||||||
Colors.lightGreen,
|
Colors.lightGreen,
|
||||||
),
|
),
|
||||||
NotificationType.actComplete => (
|
NotificationType.actComplete => (
|
||||||
const Color(0xFF6A1B9A),
|
const Color(0xFF6A1B9A),
|
||||||
Icons.flag,
|
Icons.flag,
|
||||||
Colors.purpleAccent,
|
Colors.purpleAccent,
|
||||||
),
|
),
|
||||||
NotificationType.newSpell => (
|
NotificationType.newSpell => (
|
||||||
const Color(0xFF4527A0),
|
const Color(0xFF4527A0),
|
||||||
Icons.auto_fix_high,
|
Icons.auto_fix_high,
|
||||||
Colors.deepPurpleAccent,
|
Colors.deepPurpleAccent,
|
||||||
),
|
),
|
||||||
NotificationType.newEquipment => (
|
NotificationType.newEquipment => (
|
||||||
const Color(0xFFE65100),
|
const Color(0xFFE65100),
|
||||||
Icons.shield,
|
Icons.shield,
|
||||||
Colors.orange,
|
Colors.orange,
|
||||||
),
|
),
|
||||||
NotificationType.bossDefeat => (
|
NotificationType.bossDefeat => (
|
||||||
const Color(0xFFC62828),
|
const Color(0xFFC62828),
|
||||||
Icons.whatshot,
|
Icons.whatshot,
|
||||||
Colors.redAccent,
|
Colors.redAccent,
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:askiineverdie/data/game_text_l10n.dart' as l10n;
|
||||||
import 'package:askiineverdie/data/potion_data.dart';
|
import 'package:askiineverdie/data/potion_data.dart';
|
||||||
import 'package:askiineverdie/src/core/model/potion.dart';
|
import 'package:askiineverdie/src/core/model/potion.dart';
|
||||||
|
|
||||||
@@ -22,10 +23,10 @@ class PotionInventoryPanel extends StatelessWidget {
|
|||||||
final potionEntries = _buildPotionEntries();
|
final potionEntries = _buildPotionEntries();
|
||||||
|
|
||||||
if (potionEntries.isEmpty) {
|
if (potionEntries.isEmpty) {
|
||||||
return const Center(
|
return Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
'No potions',
|
l10n.uiNoPotions,
|
||||||
style: TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
color: Colors.grey,
|
color: Colors.grey,
|
||||||
fontStyle: FontStyle.italic,
|
fontStyle: FontStyle.italic,
|
||||||
@@ -146,11 +147,7 @@ class _PotionRow extends StatelessWidget {
|
|||||||
// 전투 중 사용 불가 표시
|
// 전투 중 사용 불가 표시
|
||||||
if (isUsedThisBattle) ...[
|
if (isUsedThisBattle) ...[
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
const Icon(
|
const Icon(Icons.block, size: 12, color: Colors.grey),
|
||||||
Icons.block,
|
|
||||||
size: 12,
|
|
||||||
color: Colors.grey,
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -213,10 +210,7 @@ class _HealBadge extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
healText,
|
healText,
|
||||||
style: TextStyle(
|
style: TextStyle(fontSize: 9, color: Colors.grey.shade700),
|
||||||
fontSize: 9,
|
|
||||||
color: Colors.grey.shade700,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:askiineverdie/data/game_text_l10n.dart' as l10n;
|
||||||
import 'package:askiineverdie/data/skill_data.dart';
|
import 'package:askiineverdie/data/skill_data.dart';
|
||||||
import 'package:askiineverdie/src/core/model/game_state.dart';
|
import 'package:askiineverdie/src/core/model/game_state.dart';
|
||||||
import 'package:askiineverdie/src/core/model/skill.dart';
|
import 'package:askiineverdie/src/core/model/skill.dart';
|
||||||
@@ -90,8 +91,8 @@ class _SkillPanelState extends State<SkillPanel>
|
|||||||
final skillStates = widget.skillSystem.skillStates;
|
final skillStates = widget.skillSystem.skillStates;
|
||||||
|
|
||||||
if (skillStates.isEmpty) {
|
if (skillStates.isEmpty) {
|
||||||
return const Center(
|
return Center(
|
||||||
child: Text('No skills', style: TextStyle(fontSize: 11)),
|
child: Text(l10n.uiNoSkills, style: const TextStyle(fontSize: 11)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,7 +144,7 @@ class _SkillRow extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final cooldownText = isReady
|
final cooldownText = isReady
|
||||||
? 'Ready'
|
? l10n.uiReady
|
||||||
: '${(remainingMs / 1000).toStringAsFixed(1)}s';
|
: '${(remainingMs / 1000).toStringAsFixed(1)}s';
|
||||||
|
|
||||||
final skillIcon = _getSkillIcon(skill.type);
|
final skillIcon = _getSkillIcon(skill.type);
|
||||||
@@ -192,9 +193,9 @@ class _SkillRow extends StatelessWidget {
|
|||||||
color: elementColor.withValues(alpha: 0.3),
|
color: elementColor.withValues(alpha: 0.3),
|
||||||
borderRadius: BorderRadius.circular(3),
|
borderRadius: BorderRadius.circular(3),
|
||||||
),
|
),
|
||||||
child: const Text(
|
child: Text(
|
||||||
'DOT',
|
l10n.uiDot,
|
||||||
style: TextStyle(fontSize: 7, color: Colors.white70),
|
style: const TextStyle(fontSize: 7, color: Colors.white70),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
@@ -202,7 +203,7 @@ class _SkillRow extends StatelessWidget {
|
|||||||
|
|
||||||
// 랭크
|
// 랭크
|
||||||
Text(
|
Text(
|
||||||
'Lv.$rank',
|
l10n.uiLevel(rank),
|
||||||
style: const TextStyle(fontSize: 9, color: Colors.grey),
|
style: const TextStyle(fontSize: 9, color: Colors.grey),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
@@ -233,7 +234,9 @@ class _SkillRow extends StatelessWidget {
|
|||||||
borderRadius: BorderRadius.circular(4),
|
borderRadius: BorderRadius.circular(4),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: skillColor.withValues(alpha: glowAnimation.value * 0.5),
|
color: skillColor.withValues(
|
||||||
|
alpha: glowAnimation.value * 0.5,
|
||||||
|
),
|
||||||
blurRadius: 8 * glowAnimation.value,
|
blurRadius: 8 * glowAnimation.value,
|
||||||
spreadRadius: 2 * glowAnimation.value,
|
spreadRadius: 2 * glowAnimation.value,
|
||||||
),
|
),
|
||||||
@@ -332,11 +335,7 @@ class _ElementBadge extends StatelessWidget {
|
|||||||
? Border.all(color: color.withValues(alpha: 0.7), width: 1)
|
? Border.all(color: color.withValues(alpha: 0.7), width: 1)
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
child: Icon(
|
child: Icon(icon, size: 10, color: color),
|
||||||
icon,
|
|
||||||
size: 10,
|
|
||||||
color: color,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -137,8 +137,9 @@ class TaskProgressPanel extends StatelessWidget {
|
|||||||
child: Text(
|
child: Text(
|
||||||
'${speedMultiplier}x',
|
'${speedMultiplier}x',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontWeight:
|
fontWeight: speedMultiplier > 1
|
||||||
speedMultiplier > 1 ? FontWeight.bold : FontWeight.normal,
|
? FontWeight.bold
|
||||||
|
: FontWeight.normal,
|
||||||
color: speedMultiplier > 1
|
color: speedMultiplier > 1
|
||||||
? Theme.of(context).colorScheme.primary
|
? Theme.of(context).colorScheme.primary
|
||||||
: null,
|
: null,
|
||||||
@@ -157,8 +158,9 @@ class TaskProgressPanel extends StatelessWidget {
|
|||||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||||
child: LinearProgressIndicator(
|
child: LinearProgressIndicator(
|
||||||
value: progressValue,
|
value: progressValue,
|
||||||
backgroundColor:
|
backgroundColor: Theme.of(
|
||||||
Theme.of(context).colorScheme.primary.withValues(alpha: 0.2),
|
context,
|
||||||
|
).colorScheme.primary.withValues(alpha: 0.2),
|
||||||
valueColor: AlwaysStoppedAnimation<Color>(
|
valueColor: AlwaysStoppedAnimation<Color>(
|
||||||
Theme.of(context).colorScheme.primary,
|
Theme.of(context).colorScheme.primary,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:askiineverdie/data/game_text_l10n.dart' as l10n;
|
||||||
import 'package:askiineverdie/src/core/l10n/game_data_l10n.dart';
|
import 'package:askiineverdie/src/core/l10n/game_data_l10n.dart';
|
||||||
import 'package:askiineverdie/src/core/model/hall_of_fame.dart';
|
import 'package:askiineverdie/src/core/model/hall_of_fame.dart';
|
||||||
import 'package:askiineverdie/src/core/storage/hall_of_fame_storage.dart';
|
import 'package:askiineverdie/src/core/storage/hall_of_fame_storage.dart';
|
||||||
@@ -36,10 +37,7 @@ class _HallOfFameScreenState extends State<HallOfFameScreen> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(title: Text(l10n.uiHallOfFame), centerTitle: true),
|
||||||
title: const Text('Hall of Fame'),
|
|
||||||
centerTitle: true,
|
|
||||||
),
|
|
||||||
body: _isLoading
|
body: _isLoading
|
||||||
? const Center(child: CircularProgressIndicator())
|
? const Center(child: CircularProgressIndicator())
|
||||||
: _buildContent(),
|
: _buildContent(),
|
||||||
@@ -67,19 +65,13 @@ class _HallOfFameScreenState extends State<HallOfFameScreen> {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Text(
|
Text(
|
||||||
'No heroes yet',
|
l10n.hofNoHeroes,
|
||||||
style: TextStyle(
|
style: TextStyle(fontSize: 20, color: Colors.grey.shade600),
|
||||||
fontSize: 20,
|
|
||||||
color: Colors.grey.shade600,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text(
|
Text(
|
||||||
'Defeat the Glitch God to enshrine your legend!',
|
l10n.hofDefeatGlitchGod,
|
||||||
style: TextStyle(
|
style: TextStyle(fontSize: 14, color: Colors.grey.shade500),
|
||||||
fontSize: 14,
|
|
||||||
color: Colors.grey.shade500,
|
|
||||||
),
|
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -106,22 +98,22 @@ class _HallOfFameScreenState extends State<HallOfFameScreen> {
|
|||||||
topRight: Radius.circular(6),
|
topRight: Radius.circular(6),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: const Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Icon(Icons.emoji_events, color: Colors.white),
|
const Icon(Icons.emoji_events, color: Colors.white),
|
||||||
SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Text(
|
Text(
|
||||||
'HALL OF FAME',
|
l10n.uiHallOfFame.toUpperCase(),
|
||||||
style: TextStyle(
|
style: const TextStyle(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
letterSpacing: 2,
|
letterSpacing: 2,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Icon(Icons.emoji_events, color: Colors.white),
|
const Icon(Icons.emoji_events, color: Colors.white),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -132,10 +124,7 @@ class _HallOfFameScreenState extends State<HallOfFameScreen> {
|
|||||||
itemCount: hallOfFame.entries.length,
|
itemCount: hallOfFame.entries.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final entry = hallOfFame.entries[index];
|
final entry = hallOfFame.entries[index];
|
||||||
return _HallOfFameEntryCard(
|
return _HallOfFameEntryCard(entry: entry, rank: index + 1);
|
||||||
entry: entry,
|
|
||||||
rank: index + 1,
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -147,10 +136,7 @@ class _HallOfFameScreenState extends State<HallOfFameScreen> {
|
|||||||
|
|
||||||
/// 명예의 전당 엔트리 카드
|
/// 명예의 전당 엔트리 카드
|
||||||
class _HallOfFameEntryCard extends StatelessWidget {
|
class _HallOfFameEntryCard extends StatelessWidget {
|
||||||
const _HallOfFameEntryCard({
|
const _HallOfFameEntryCard({required this.entry, required this.rank});
|
||||||
required this.entry,
|
|
||||||
required this.rank,
|
|
||||||
});
|
|
||||||
|
|
||||||
final HallOfFameEntry entry;
|
final HallOfFameEntry entry;
|
||||||
final int rank;
|
final int rank;
|
||||||
@@ -217,7 +203,7 @@ class _HallOfFameEntryCard extends StatelessWidget {
|
|||||||
borderRadius: BorderRadius.circular(4),
|
borderRadius: BorderRadius.circular(4),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
'Lv.${entry.level}',
|
l10n.uiLevel(entry.level),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.blue.shade800,
|
color: Colors.blue.shade800,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
@@ -232,10 +218,7 @@ class _HallOfFameEntryCard extends StatelessWidget {
|
|||||||
Text(
|
Text(
|
||||||
'${GameDataL10n.getRaceName(context, entry.race)} '
|
'${GameDataL10n.getRaceName(context, entry.race)} '
|
||||||
'${GameDataL10n.getKlassName(context, entry.klass)}',
|
'${GameDataL10n.getKlassName(context, entry.klass)}',
|
||||||
style: TextStyle(
|
style: TextStyle(fontSize: 13, color: Colors.grey.shade700),
|
||||||
fontSize: 13,
|
|
||||||
color: Colors.grey.shade700,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
// 통계
|
// 통계
|
||||||
@@ -267,14 +250,15 @@ class _HallOfFameEntryCard extends StatelessWidget {
|
|||||||
Column(
|
Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
Icon(Icons.calendar_today, size: 14, color: Colors.grey.shade500),
|
Icon(
|
||||||
|
Icons.calendar_today,
|
||||||
|
size: 14,
|
||||||
|
color: Colors.grey.shade500,
|
||||||
|
),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
Text(
|
Text(
|
||||||
entry.formattedClearedDate,
|
entry.formattedClearedDate,
|
||||||
style: TextStyle(
|
style: TextStyle(fontSize: 11, color: Colors.grey.shade600),
|
||||||
fontSize: 11,
|
|
||||||
color: Colors.grey.shade600,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -362,22 +346,22 @@ class _GameClearDialog extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
title: const Row(
|
title: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Icon(Icons.emoji_events, color: Colors.amber, size: 32),
|
const Icon(Icons.emoji_events, color: Colors.amber, size: 32),
|
||||||
SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Text('VICTORY!'),
|
Text(l10n.hofVictory),
|
||||||
SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Icon(Icons.emoji_events, color: Colors.amber, size: 32),
|
const Icon(Icons.emoji_events, color: Colors.amber, size: 32),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
content: Column(
|
content: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
const Text(
|
Text(
|
||||||
'You have defeated the Glitch God!',
|
l10n.hofDefeatedGlitchGod,
|
||||||
style: TextStyle(fontSize: 16),
|
style: const TextStyle(fontSize: 16),
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
@@ -386,14 +370,11 @@ class _GameClearDialog extends StatelessWidget {
|
|||||||
// 캐릭터 정보
|
// 캐릭터 정보
|
||||||
Text(
|
Text(
|
||||||
'"${entry.characterName}"',
|
'"${entry.characterName}"',
|
||||||
style: const TextStyle(
|
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||||
fontSize: 18,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
Text(
|
Text(
|
||||||
'${entry.race} ${entry.klass}',
|
'${GameDataL10n.getRaceName(context, entry.race)} ${GameDataL10n.getKlassName(context, entry.klass)}',
|
||||||
style: TextStyle(color: Colors.grey.shade600),
|
style: TextStyle(color: Colors.grey.shade600),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
@@ -401,16 +382,16 @@ class _GameClearDialog extends StatelessWidget {
|
|||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
children: [
|
children: [
|
||||||
_buildStat('Level', '${entry.level}'),
|
_buildStat(l10n.hofLevel, '${entry.level}'),
|
||||||
_buildStat('Time', entry.formattedPlayTime),
|
_buildStat(l10n.hofTime, entry.formattedPlayTime),
|
||||||
_buildStat('Deaths', '${entry.totalDeaths}'),
|
_buildStat(l10n.hofDeaths, '${entry.totalDeaths}'),
|
||||||
_buildStat('Quests', '${entry.questsCompleted}'),
|
_buildStat(l10n.hofQuests, '${entry.questsCompleted}'),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
const Text(
|
Text(
|
||||||
'Your legend has been enshrined in the Hall of Fame!',
|
l10n.hofLegendEnshrined,
|
||||||
style: TextStyle(
|
style: const TextStyle(
|
||||||
fontStyle: FontStyle.italic,
|
fontStyle: FontStyle.italic,
|
||||||
color: Colors.amber,
|
color: Colors.amber,
|
||||||
),
|
),
|
||||||
@@ -424,14 +405,14 @@ class _GameClearDialog extends StatelessWidget {
|
|||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
onViewHallOfFame();
|
onViewHallOfFame();
|
||||||
},
|
},
|
||||||
child: const Text('View Hall of Fame'),
|
child: Text(l10n.hofViewHallOfFame),
|
||||||
),
|
),
|
||||||
FilledButton(
|
FilledButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
onNewGame();
|
onNewGame();
|
||||||
},
|
},
|
||||||
child: const Text('New Game'),
|
child: Text(l10n.hofNewGame),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@@ -442,17 +423,11 @@ class _GameClearDialog extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
value,
|
value,
|
||||||
style: const TextStyle(
|
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||||
fontSize: 18,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
label,
|
label,
|
||||||
style: TextStyle(
|
style: TextStyle(fontSize: 11, color: Colors.grey.shade600),
|
||||||
fontSize: 11,
|
|
||||||
color: Colors.grey.shade600,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,12 +3,14 @@ import 'dart:math' as math;
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:askiineverdie/data/class_data.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/data/race_data.dart';
|
||||||
import 'package:askiineverdie/l10n/app_localizations.dart';
|
import 'package:askiineverdie/l10n/app_localizations.dart';
|
||||||
import 'package:askiineverdie/src/core/model/class_traits.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/game_state.dart';
|
||||||
import 'package:askiineverdie/src/core/model/race_traits.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/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/core/util/pq_logic.dart';
|
||||||
|
|
||||||
/// 캐릭터 생성 화면 (NewGuy.pas 포팅)
|
/// 캐릭터 생성 화면 (NewGuy.pas 포팅)
|
||||||
@@ -396,7 +398,10 @@ class _NewCharacterScreenState extends State<NewCharacterScreen> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(L10n.of(context).race, style: Theme.of(context).textTheme.titleMedium),
|
Text(
|
||||||
|
L10n.of(context).race,
|
||||||
|
style: Theme.of(context).textTheme.titleMedium,
|
||||||
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: 300,
|
height: 300,
|
||||||
@@ -415,7 +420,7 @@ class _NewCharacterScreenState extends State<NewCharacterScreen> {
|
|||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
title: Text(
|
title: Text(
|
||||||
race.name,
|
GameDataL10n.getRaceName(context, race.name),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontWeight: isSelected
|
fontWeight: isSelected
|
||||||
? FontWeight.bold
|
? FontWeight.bold
|
||||||
@@ -445,7 +450,7 @@ class _NewCharacterScreenState extends State<NewCharacterScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final passiveDesc = race.passives.isNotEmpty
|
final passiveDesc = race.passives.isNotEmpty
|
||||||
? race.passives.map((p) => p.description).join(', ')
|
? race.passives.map((p) => _translateRacePassive(p)).join(', ')
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
@@ -460,21 +465,35 @@ class _NewCharacterScreenState extends State<NewCharacterScreen> {
|
|||||||
Text(
|
Text(
|
||||||
passiveDesc,
|
passiveDesc,
|
||||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||||
color: Theme.of(context).colorScheme.primary,
|
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) {
|
String _statName(StatType type) {
|
||||||
return switch (type) {
|
return switch (type) {
|
||||||
StatType.str => 'STR',
|
StatType.str => game_l10n.statStr,
|
||||||
StatType.con => 'CON',
|
StatType.con => game_l10n.statCon,
|
||||||
StatType.dex => 'DEX',
|
StatType.dex => game_l10n.statDex,
|
||||||
StatType.intelligence => 'INT',
|
StatType.intelligence => game_l10n.statInt,
|
||||||
StatType.wis => 'WIS',
|
StatType.wis => game_l10n.statWis,
|
||||||
StatType.cha => 'CHA',
|
StatType.cha => game_l10n.statCha,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -485,7 +504,10 @@ class _NewCharacterScreenState extends State<NewCharacterScreen> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(L10n.of(context).classTitle, style: Theme.of(context).textTheme.titleMedium),
|
Text(
|
||||||
|
L10n.of(context).classTitle,
|
||||||
|
style: Theme.of(context).textTheme.titleMedium,
|
||||||
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: 300,
|
height: 300,
|
||||||
@@ -504,7 +526,7 @@ class _NewCharacterScreenState extends State<NewCharacterScreen> {
|
|||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
title: Text(
|
title: Text(
|
||||||
klass.name,
|
GameDataL10n.getKlassName(context, klass.name),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontWeight: isSelected
|
fontWeight: isSelected
|
||||||
? FontWeight.bold
|
? FontWeight.bold
|
||||||
@@ -534,7 +556,7 @@ class _NewCharacterScreenState extends State<NewCharacterScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final passiveDesc = klass.passives.isNotEmpty
|
final passiveDesc = klass.passives.isNotEmpty
|
||||||
? klass.passives.map((p) => p.description).join(', ')
|
? klass.passives.map((p) => _translateClassPassive(p)).join(', ')
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
@@ -549,10 +571,28 @@ class _NewCharacterScreenState extends State<NewCharacterScreen> {
|
|||||||
Text(
|
Text(
|
||||||
passiveDesc,
|
passiveDesc,
|
||||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||||
color: Theme.of(context).colorScheme.secondary,
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,7 +48,10 @@ void main() {
|
|||||||
// 아스키나라(ASCII-Nara) 세계관 데이터로 업데이트
|
// 아스키나라(ASCII-Nara) 세계관 데이터로 업데이트
|
||||||
// 결정론적 결과가 일관되게 생성되는지 확인 (비어있지 않음)
|
// 결정론적 결과가 일관되게 생성되는지 확인 (비어있지 않음)
|
||||||
expect(pq_logic.boringItem(config, DeterministicRandom(12)), isNotEmpty);
|
expect(pq_logic.boringItem(config, DeterministicRandom(12)), isNotEmpty);
|
||||||
expect(pq_logic.interestingItem(config, DeterministicRandom(12)), isNotEmpty);
|
expect(
|
||||||
|
pq_logic.interestingItem(config, DeterministicRandom(12)),
|
||||||
|
isNotEmpty,
|
||||||
|
);
|
||||||
expect(pq_logic.specialItem(config, DeterministicRandom(12)), isNotEmpty);
|
expect(pq_logic.specialItem(config, DeterministicRandom(12)), isNotEmpty);
|
||||||
// 원본 Main.pas:770-774 RandomLow 방식으로 수정됨
|
// 원본 Main.pas:770-774 RandomLow 방식으로 수정됨
|
||||||
final spell = pq_logic.winSpell(config, DeterministicRandom(22), 7, 4);
|
final spell = pq_logic.winSpell(config, DeterministicRandom(22), 7, 4);
|
||||||
|
|||||||
@@ -113,11 +113,7 @@ void main() {
|
|||||||
test('winItem produces consistent items', () {
|
test('winItem produces consistent items', () {
|
||||||
// 아스키나라(ASCII-Nara) 세계관 데이터로 업데이트
|
// 아스키나라(ASCII-Nara) 세계관 데이터로 업데이트
|
||||||
// 시드 42에서 아이템 획득
|
// 시드 42에서 아이템 획득
|
||||||
final item1 = pq_logic.winItem(
|
final item1 = pq_logic.winItem(config, DeterministicRandom(testSeed), 5);
|
||||||
config,
|
|
||||||
DeterministicRandom(testSeed),
|
|
||||||
5,
|
|
||||||
);
|
|
||||||
expect(item1, isNotEmpty);
|
expect(item1, isNotEmpty);
|
||||||
expect(item1, contains(' of '));
|
expect(item1, contains(' of '));
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user