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:
JiWoong Sul
2025-12-22 19:00:58 +09:00
parent f606fca063
commit 99f5b74802
63 changed files with 3403 additions and 2740 deletions

View File

@@ -233,7 +233,8 @@ class CombatStats {
final effectiveStr = stats.str + equipStats.strBonus + raceStr + classStr;
final effectiveCon = stats.con + equipStats.conBonus + raceCon + classCon;
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 effectiveCha = stats.cha + equipStats.chaBonus + raceCha + classCha;
@@ -276,7 +277,10 @@ class CombatStats {
final weaponSpeed = weaponItem.stats.attackSpeed;
final baseAttackSpeed = weaponSpeed > 0 ? weaponSpeed : 1000;
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: 기본 + 장비 보너스
var totalHpMax = stats.hpMax + equipStats.hpBonus;
@@ -299,7 +303,8 @@ class CombatStats {
}
// 마법 데미지 보너스 (Null Elf: +15%)
final raceMagicBonus = race?.getPassiveValue(PassiveType.magicDamageBonus) ?? 0.0;
final raceMagicBonus =
race?.getPassiveValue(PassiveType.magicDamageBonus) ?? 0.0;
if (raceMagicBonus > 0) {
baseMagAtk = (baseMagAtk * (1 + raceMagicBonus)).round();
}
@@ -311,7 +316,8 @@ class CombatStats {
}
// 크리티컬 보너스 (Stack Goblin: +5%)
final raceCritBonus = race?.getPassiveValue(PassiveType.criticalBonus) ?? 0.0;
final raceCritBonus =
race?.getPassiveValue(PassiveType.criticalBonus) ?? 0.0;
criRate += raceCritBonus;
// ========================================================================
@@ -319,35 +325,41 @@ class CombatStats {
// ========================================================================
// HP 보너스 (Garbage Collector: +30%)
final classHpBonus = klass?.getPassiveValue(ClassPassiveType.hpBonus) ?? 0.0;
final classHpBonus =
klass?.getPassiveValue(ClassPassiveType.hpBonus) ?? 0.0;
if (classHpBonus > 0) {
totalHpMax = (totalHpMax * (1 + classHpBonus)).round();
}
// 물리 공격력 보너스 (Bug Hunter: +20%)
final classPhysBonus = klass?.getPassiveValue(ClassPassiveType.physicalDamageBonus) ?? 0.0;
final classPhysBonus =
klass?.getPassiveValue(ClassPassiveType.physicalDamageBonus) ?? 0.0;
if (classPhysBonus > 0) {
baseAtk = (baseAtk * (1 + classPhysBonus)).round();
}
// 방어력 보너스 (Debugger Paladin: +15%)
final classDefBonus = klass?.getPassiveValue(ClassPassiveType.defenseBonus) ?? 0.0;
final classDefBonus =
klass?.getPassiveValue(ClassPassiveType.defenseBonus) ?? 0.0;
if (classDefBonus > 0) {
baseDef = (baseDef * (1 + classDefBonus)).round();
}
// 마법 데미지 보너스 (Compiler Mage: +25%)
final classMagBonus = klass?.getPassiveValue(ClassPassiveType.magicDamageBonus) ?? 0.0;
final classMagBonus =
klass?.getPassiveValue(ClassPassiveType.magicDamageBonus) ?? 0.0;
if (classMagBonus > 0) {
baseMagAtk = (baseMagAtk * (1 + classMagBonus)).round();
}
// 회피율 보너스 (Refactor Monk: +15%)
final classEvasionBonus = klass?.getPassiveValue(ClassPassiveType.evasionBonus) ?? 0.0;
final classEvasionBonus =
klass?.getPassiveValue(ClassPassiveType.evasionBonus) ?? 0.0;
evasion += classEvasionBonus;
// 크리티컬 보너스 (Pointer Assassin: +20%)
final classCritBonus = klass?.getPassiveValue(ClassPassiveType.criticalBonus) ?? 0.0;
final classCritBonus =
klass?.getPassiveValue(ClassPassiveType.criticalBonus) ?? 0.0;
criRate += classCritBonus;
// 최종 클램핑

View File

@@ -209,11 +209,8 @@ class SkillSystemState {
/// 게임 진행 경과 시간 (밀리초, 스킬 쿨타임 계산용)
final int elapsedMs;
factory SkillSystemState.empty() => const SkillSystemState(
skillStates: [],
activeBuffs: [],
elapsedMs: 0,
);
factory SkillSystemState.empty() =>
const SkillSystemState(skillStates: [], activeBuffs: [], elapsedMs: 0);
/// 특정 스킬 상태 가져오기
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 defMod = 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({
@@ -477,10 +480,8 @@ class Inventory {
/// Phase 2에서 EquipmentItem 기반으로 확장됨.
/// 기존 문자열 API(weapon, shield 등)는 호환성을 위해 유지.
class Equipment {
Equipment({
required this.items,
required this.bestIndex,
}) : assert(items.length == slotCount, 'Equipment must have $slotCount items');
Equipment({required this.items, required this.bestIndex})
: assert(items.length == slotCount, 'Equipment must have $slotCount items');
/// 장비 아이템 목록 (11개 슬롯)
final List<EquipmentItem> items;
@@ -525,10 +526,7 @@ class Equipment {
/// 모든 장비 스탯 합산
ItemStats get totalStats {
return items.fold(
ItemStats.empty,
(sum, item) => sum + item.stats,
);
return items.fold(ItemStats.empty, (sum, item) => sum + item.stats);
}
/// 모든 장비 무게 합산
@@ -647,10 +645,7 @@ class Equipment {
return Equipment(items: newItems, bestIndex: bestIndex);
}
Equipment copyWith({
List<EquipmentItem>? items,
int? bestIndex,
}) {
Equipment copyWith({List<EquipmentItem>? items, int? bestIndex}) {
return Equipment(
items: items ?? List<EquipmentItem>.from(this.items),
bestIndex: bestIndex ?? this.bestIndex,

View File

@@ -175,9 +175,7 @@ class HallOfFame {
/// JSON으로 직렬화
Map<String, dynamic> toJson() {
return {
'entries': entries.map((e) => e.toJson()).toList(),
};
return {'entries': entries.map((e) => e.toJson()).toList()};
}
/// JSON에서 역직렬화

View File

@@ -88,10 +88,7 @@ class PotionInventory {
PotionInventory addPotion(String potionId, [int count = 1]) {
final newPotions = Map<String, int>.from(potions);
newPotions[potionId] = (newPotions[potionId] ?? 0) + count;
return PotionInventory(
potions: newPotions,
usedInBattle: usedInBattle,
);
return PotionInventory(potions: newPotions, usedInBattle: usedInBattle);
}
/// 물약 사용 (수량 감소)
@@ -107,18 +104,12 @@ class PotionInventory {
final newUsed = Set<PotionType>.from(usedInBattle)..add(type);
return PotionInventory(
potions: newPotions,
usedInBattle: newUsed,
);
return PotionInventory(potions: newPotions, usedInBattle: newUsed);
}
/// 전투 종료 시 사용 기록 초기화
PotionInventory resetBattleUsage() {
return PotionInventory(
potions: potions,
usedInBattle: const {},
);
return PotionInventory(potions: potions, usedInBattle: const {});
}
/// 빈 인벤토리

View File

@@ -1,12 +1,5 @@
/// 스탯 타입 열거형 (stat type)
enum StatType {
str,
con,
dex,
intelligence,
wis,
cha,
}
enum StatType { str, con, dex, intelligence, wis, cha }
/// 패시브 능력 타입 (passive ability type)
enum PassiveType {

View File

@@ -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 {
/// 공격 스킬
@@ -103,6 +128,9 @@ class Skill {
this.baseDotDamage,
this.baseDotDurationMs,
this.baseDotTickMs,
this.hitCount = 1,
this.lifestealPercent = 0.0,
this.mpHealAmount = 0,
});
/// 스킬 ID
@@ -156,6 +184,15 @@ class Skill {
/// DOT 기본 틱 간격 (밀리초, 스킬 레벨로 결정)
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;
@@ -207,11 +244,7 @@ class SkillState {
return cooldownMs - elapsed;
}
SkillState copyWith({
String? skillId,
int? lastUsedMs,
int? rank,
}) {
SkillState copyWith({String? skillId, int? lastUsedMs, int? rank}) {
return SkillState(
skillId: skillId ?? this.skillId,
lastUsedMs: lastUsedMs ?? this.lastUsedMs,
@@ -302,11 +335,7 @@ class SkillUseResult {
/// 실패 결과 생성
factory SkillUseResult.failed(Skill skill, SkillFailReason reason) {
return SkillUseResult(
skill: skill,
success: false,
failReason: reason,
);
return SkillUseResult(skill: skill, success: false, failReason: reason);
}
}
@@ -384,7 +413,11 @@ class DotEffect {
/// [skill] DOT 스킬
/// [playerInt] 플레이어 INT (틱당 데미지 보정)
/// [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.baseDotDamage != null, 'baseDotDamage 필수');
assert(skill.baseDotDurationMs != null, 'baseDotDurationMs 필수');
@@ -396,7 +429,9 @@ class DotEffect {
// WIS → 틱 간격 보정 (WIS 10 기준, ±2%/포인트, 빨라짐)
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(
skillId: skill.id,