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

@@ -112,7 +112,9 @@ class ProgressService {
),
plotStageCount: 1, // Prologue
questCount: 0,
plotHistory: [HistoryEntry(caption: l10n.taskPrologue, isComplete: false)],
plotHistory: [
HistoryEntry(caption: l10n.taskPrologue, isComplete: false),
],
questHistory: const [],
);
@@ -156,13 +158,17 @@ class ProgressService {
// 스킬 시스템 시간 업데이트 (Phase 3)
final skillService = SkillService(rng: state.rng);
var skillSystem = skillService.updateElapsedTime(state.skillSystem, clamped);
var skillSystem = skillService.updateElapsedTime(
state.skillSystem,
clamped,
);
// 만료된 버프 정리
skillSystem = skillService.cleanupExpiredBuffs(skillSystem);
// 비전투 시 MP 회복
final isInCombat = progress.currentTask.type == TaskType.kill &&
final isInCombat =
progress.currentTask.type == TaskType.kill &&
progress.currentCombat != null &&
progress.currentCombat!.isActive;
@@ -173,7 +179,10 @@ class ProgressService {
wis: nextState.stats.wis,
);
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(
stats: nextState.stats.copyWith(mpCurrent: newMp),
);
@@ -193,7 +202,9 @@ class ProgressService {
var updatedCombat = progress.currentCombat;
var updatedSkillSystem = nextState.skillSystem;
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(
nextState,
updatedCombat,
@@ -480,7 +491,8 @@ class ProgressService {
final questMonster = state.progress.currentQuestMonster;
final questMonsterData = questMonster?.monsterData;
final questLevel = questMonsterData != null
? int.tryParse(questMonsterData.split('|').elementAtOrNull(1) ?? '') ?? 0
? int.tryParse(questMonsterData.split('|').elementAtOrNull(1) ?? '') ??
0
: null;
final monsterResult = pq_logic.monsterTask(
@@ -501,10 +513,9 @@ class ProgressService {
// 전투용 몬스터 레벨 조정 (밸런스)
// config의 raw 레벨이 플레이어보다 너무 높으면 전투가 불가능
// 플레이어 레벨 ±3 범위로 제한 (최소 1)
final effectiveMonsterLevel = monsterResult.level.clamp(
math.max(1, level - 3),
level + 3,
).toInt();
final effectiveMonsterLevel = monsterResult.level
.clamp(math.max(1, level - 3), level + 3)
.toInt();
final monsterCombatStats = MonsterCombatStats.fromLevel(
name: monsterResult.displayName,
@@ -907,7 +918,8 @@ class ProgressService {
if (hasItemsToSell) {
// 다음 아이템 판매 태스크 시작
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(
state.progress,
l10n.taskSelling(itemDesc),
@@ -945,7 +957,8 @@ class ProgressService {
CombatState combat,
SkillSystemState skillSystem,
PotionInventory? potionInventory,
}) _processCombatTickWithSkills(
})
_processCombatTickWithSkills(
GameState state,
CombatState combat,
SkillSystemState skillSystem,
@@ -988,12 +1001,14 @@ class ProgressService {
dotDamageThisTick += damage;
// DOT 데미지 이벤트 생성
newEvents.add(CombatEvent.dotTick(
timestamp: timestamp,
skillName: dot.skillId,
damage: damage,
targetName: monsterStats.name,
));
newEvents.add(
CombatEvent.dotTick(
timestamp: timestamp,
skillName: dot.skillId,
damage: damage,
targetName: monsterStats.name,
),
);
}
// 만료되지 않은 DOT만 유지
@@ -1004,8 +1019,10 @@ class ProgressService {
// DOT 데미지 적용
if (dotDamageThisTick > 0 && monsterStats.isAlive) {
final newMonsterHp = (monsterStats.hpCurrent - dotDamageThisTick)
.clamp(0, monsterStats.hpMax);
final newMonsterHp = (monsterStats.hpCurrent - dotDamageThisTick).clamp(
0,
monsterStats.hpMax,
);
monsterStats = monsterStats.copyWith(hpCurrent: newMonsterHp);
totalDamageDealt += dotDamageThisTick;
}
@@ -1024,8 +1041,7 @@ class ProgressService {
playerLevel: state.traits.level,
);
if (emergencyPotion != null &&
!usedPotionTypes.contains(PotionType.hp)) {
if (emergencyPotion != null && !usedPotionTypes.contains(PotionType.hp)) {
final result = potionService.usePotion(
potionId: emergencyPotion.id,
inventory: state.potionInventory,
@@ -1040,25 +1056,27 @@ class ProgressService {
usedPotionTypes = {...usedPotionTypes, PotionType.hp};
updatedPotionInventory = result.newInventory;
newEvents.add(CombatEvent.playerPotion(
timestamp: timestamp,
potionName: emergencyPotion.name,
healAmount: result.healedAmount,
isHp: true,
));
newEvents.add(
CombatEvent.playerPotion(
timestamp: timestamp,
potionName: emergencyPotion.name,
healAmount: result.healedAmount,
isHp: true,
),
);
}
}
}
// 플레이어 공격 체크
if (playerAccumulator >= playerStats.attackDelayMs) {
// 스킬 자동 선택
final availableSkillIds = updatedSkillSystem.skillStates
.map((s) => s.skillId)
.toList();
// 기본 스킬이 없으면 기본 스킬 추가
// SpellBook에서 사용 가능한 스킬 ID 목록 조회
var availableSkillIds = skillService.getAvailableSkillIdsFromSpellBook(
state.spellBook,
);
// SpellBook에 스킬이 없으면 기본 스킬 사용
if (availableSkillIds.isEmpty) {
availableSkillIds.addAll(SkillData.defaultSkillIds);
availableSkillIds = SkillData.defaultSkillIds;
}
final selectedSkill = skillService.selectAutoSkill(
@@ -1070,12 +1088,18 @@ class ProgressService {
);
if (selectedSkill != null && selectedSkill.isAttack) {
// 공격 스킬 사용
final skillResult = skillService.useAttackSkill(
// 스펠 랭크 조회 (SpellBook 기반)
final spellRank = skillService.getSkillRankFromSpellBook(
state.spellBook,
selectedSkill.id,
);
// 랭크 스케일링 적용된 공격 스킬 사용
final skillResult = skillService.useAttackSkillWithRank(
skill: selectedSkill,
player: playerStats,
monster: monsterStats,
skillSystem: updatedSkillSystem,
rank: spellRank,
);
playerStats = skillResult.updatedPlayer;
monsterStats = skillResult.updatedMonster;
@@ -1083,12 +1107,14 @@ class ProgressService {
updatedSkillSystem = skillResult.updatedSkillSystem;
// 스킬 공격 이벤트 생성
newEvents.add(CombatEvent.playerSkill(
timestamp: timestamp,
skillName: selectedSkill.name,
damage: skillResult.result.damage,
targetName: monsterStats.name,
));
newEvents.add(
CombatEvent.playerSkill(
timestamp: timestamp,
skillName: selectedSkill.name,
damage: skillResult.result.damage,
targetName: monsterStats.name,
),
);
} else if (selectedSkill != null && selectedSkill.isDot) {
// DOT 스킬 사용
final skillResult = skillService.useDotSkill(
@@ -1107,12 +1133,14 @@ class ProgressService {
}
// DOT 스킬 사용 이벤트 생성
newEvents.add(CombatEvent.playerSkill(
timestamp: timestamp,
skillName: selectedSkill.name,
damage: skillResult.result.damage,
targetName: monsterStats.name,
));
newEvents.add(
CombatEvent.playerSkill(
timestamp: timestamp,
skillName: selectedSkill.name,
damage: skillResult.result.damage,
targetName: monsterStats.name,
),
);
} else if (selectedSkill != null && selectedSkill.isHeal) {
// 회복 스킬 사용
final skillResult = skillService.useHealSkill(
@@ -1124,11 +1152,13 @@ class ProgressService {
updatedSkillSystem = skillResult.updatedSkillSystem;
// 회복 이벤트 생성
newEvents.add(CombatEvent.playerHeal(
timestamp: timestamp,
healAmount: skillResult.result.healedAmount,
skillName: selectedSkill.name,
));
newEvents.add(
CombatEvent.playerHeal(
timestamp: timestamp,
healAmount: skillResult.result.healedAmount,
skillName: selectedSkill.name,
),
);
} else if (selectedSkill != null && selectedSkill.isBuff) {
// 버프 스킬 사용
final skillResult = skillService.useBuffSkill(
@@ -1140,10 +1170,12 @@ class ProgressService {
updatedSkillSystem = skillResult.updatedSkillSystem;
// 버프 이벤트 생성
newEvents.add(CombatEvent.playerBuff(
timestamp: timestamp,
skillName: selectedSkill.name,
));
newEvents.add(
CombatEvent.playerBuff(
timestamp: timestamp,
skillName: selectedSkill.name,
),
);
} else {
// 일반 공격
final attackResult = calculator.playerAttackMonster(
@@ -1156,17 +1188,21 @@ class ProgressService {
// 일반 공격 이벤트 생성
final result = attackResult.result;
if (result.isEvaded) {
newEvents.add(CombatEvent.monsterEvade(
timestamp: timestamp,
targetName: monsterStats.name,
));
newEvents.add(
CombatEvent.monsterEvade(
timestamp: timestamp,
targetName: monsterStats.name,
),
);
} else {
newEvents.add(CombatEvent.playerAttack(
timestamp: timestamp,
damage: result.damage,
targetName: monsterStats.name,
isCritical: result.isCritical,
));
newEvents.add(
CombatEvent.playerAttack(
timestamp: timestamp,
damage: result.damage,
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(
attacker: monsterStats,
defender: playerStats,
@@ -1187,28 +1224,36 @@ class ProgressService {
// 몬스터 공격 이벤트 생성
final result = attackResult.result;
if (result.isEvaded) {
newEvents.add(CombatEvent.playerEvade(
timestamp: timestamp,
attackerName: monsterStats.name,
));
newEvents.add(
CombatEvent.playerEvade(
timestamp: timestamp,
attackerName: monsterStats.name,
),
);
} else if (result.isBlocked) {
newEvents.add(CombatEvent.playerBlock(
timestamp: timestamp,
reducedDamage: result.damage,
attackerName: monsterStats.name,
));
newEvents.add(
CombatEvent.playerBlock(
timestamp: timestamp,
reducedDamage: result.damage,
attackerName: monsterStats.name,
),
);
} else if (result.isParried) {
newEvents.add(CombatEvent.playerParry(
timestamp: timestamp,
reducedDamage: result.damage,
attackerName: monsterStats.name,
));
newEvents.add(
CombatEvent.playerParry(
timestamp: timestamp,
reducedDamage: result.damage,
attackerName: monsterStats.name,
),
);
} else {
newEvents.add(CombatEvent.monsterAttack(
timestamp: timestamp,
damage: result.damage,
attackerName: monsterStats.name,
));
newEvents.add(
CombatEvent.monsterAttack(
timestamp: timestamp,
damage: result.damage,
attackerName: monsterStats.name,
),
);
}
}
@@ -1285,9 +1330,7 @@ class ProgressService {
);
// 전투 상태 초기화
final progress = state.progress.copyWith(
currentCombat: null,
);
final progress = state.progress.copyWith(currentCombat: null);
return state.copyWith(
equipment: emptyEquipment,