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:
@@ -24,7 +24,8 @@ class CombatCalculator {
|
||||
/// [attacker] 공격자 (플레이어) 스탯
|
||||
/// [defender] 방어자 (몬스터) 스탯
|
||||
/// Returns: 공격 결과 및 업데이트된 몬스터 스탯
|
||||
({AttackResult result, MonsterCombatStats updatedDefender}) playerAttackMonster({
|
||||
({AttackResult result, MonsterCombatStats updatedDefender})
|
||||
playerAttackMonster({
|
||||
required CombatStats attacker,
|
||||
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;
|
||||
|
||||
return CombatTurnResult(
|
||||
@@ -206,7 +208,8 @@ class CombatCalculator {
|
||||
// 플레이어 DPS (초당 데미지)
|
||||
final playerDamagePerHit = math.max(1, player.atk - monster.def * 0.5);
|
||||
final playerHitsPerSecond = 1000 / player.attackDelayMs;
|
||||
final playerDps = playerDamagePerHit * playerHitsPerSecond * player.accuracy;
|
||||
final playerDps =
|
||||
playerDamagePerHit * playerHitsPerSecond * player.accuracy;
|
||||
|
||||
// 몬스터를 처치하는 데 필요한 시간 (밀리초)
|
||||
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 monsterHitsPerSecond = 1000 / monster.attackDelayMs;
|
||||
final monsterDps = monsterDamagePerHit * monsterHitsPerSecond * monster.accuracy;
|
||||
final monsterDps =
|
||||
monsterDamagePerHit * monsterHitsPerSecond * monster.accuracy;
|
||||
final playerSurvivalTime = player.hpCurrent / monsterDps;
|
||||
|
||||
// 몬스터 예상 생존 시간
|
||||
final playerDamagePerHit = math.max(1, player.atk - monster.def * 0.5);
|
||||
final playerHitsPerSecond = 1000 / player.attackDelayMs;
|
||||
final playerDps = playerDamagePerHit * playerHitsPerSecond * player.accuracy;
|
||||
final playerDps =
|
||||
playerDamagePerHit * playerHitsPerSecond * player.accuracy;
|
||||
final monsterSurvivalTime = monster.hpCurrent / playerDps;
|
||||
|
||||
// 난이도 = 몬스터 생존시간 / (플레이어 생존시간 + 몬스터 생존시간)
|
||||
final difficulty = monsterSurvivalTime / (playerSurvivalTime + monsterSurvivalTime);
|
||||
final difficulty =
|
||||
monsterSurvivalTime / (playerSurvivalTime + monsterSurvivalTime);
|
||||
|
||||
return difficulty.clamp(0.0, 1.0);
|
||||
}
|
||||
|
||||
@@ -45,7 +45,9 @@ class ItemService {
|
||||
|
||||
// 교체할 슬롯이 있으면 해당 아이템 무게 제외
|
||||
if (replacingSlot != null) {
|
||||
final existingItem = currentItems.where((i) => i.slot == replacingSlot).firstOrNull;
|
||||
final existingItem = currentItems
|
||||
.where((i) => i.slot == replacingSlot)
|
||||
.firstOrNull;
|
||||
if (existingItem != null) {
|
||||
currentWeight -= existingItem.weight;
|
||||
}
|
||||
@@ -70,7 +72,8 @@ class ItemService {
|
||||
|
||||
if (roll < legendaryChance) return ItemRarity.legendary;
|
||||
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) {
|
||||
return ItemRarity.uncommon;
|
||||
}
|
||||
@@ -112,7 +115,8 @@ class ItemService {
|
||||
|
||||
// 공속 결정 (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 attackSpeed = (1000 + speedOffset).clamp(600, 1500);
|
||||
|
||||
@@ -133,14 +137,15 @@ class ItemService {
|
||||
ItemStats _generateShieldStats(int baseValue, ItemRarity rarity) {
|
||||
final blockBonus = 0.05 + rarity.index * 0.02;
|
||||
|
||||
return ItemStats(
|
||||
def: baseValue ~/ 2,
|
||||
blockRate: blockBonus,
|
||||
);
|
||||
return ItemStats(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) {
|
||||
EquipmentSlot.hauberk => 1.5, // 갑옷류 최고
|
||||
@@ -161,11 +166,7 @@ class ItemService {
|
||||
final hpBonus = rarity.index >= ItemRarity.rare.index ? baseValue ~/ 2 : 0;
|
||||
final evasionBonus = rarity.index >= ItemRarity.epic.index ? 0.01 : 0.0;
|
||||
|
||||
return ItemStats(
|
||||
def: def,
|
||||
hpBonus: hpBonus,
|
||||
evasion: evasionBonus,
|
||||
);
|
||||
return ItemStats(def: def, hpBonus: hpBonus, evasion: evasionBonus);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -173,10 +174,7 @@ class ItemService {
|
||||
// ============================================================================
|
||||
|
||||
/// 무게 계산 (레벨/슬롯 기반)
|
||||
int calculateWeight({
|
||||
required int level,
|
||||
required EquipmentSlot slot,
|
||||
}) {
|
||||
int calculateWeight({required int level, required EquipmentSlot slot}) {
|
||||
// 슬롯별 기본 무게
|
||||
final baseWeight = switch (slot) {
|
||||
EquipmentSlot.weapon => 10,
|
||||
@@ -209,7 +207,11 @@ class ItemService {
|
||||
ItemRarity? rarity,
|
||||
}) {
|
||||
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);
|
||||
|
||||
return EquipmentItem(
|
||||
@@ -253,7 +255,8 @@ class ItemService {
|
||||
score += stats.mpBonus;
|
||||
|
||||
// 능력치 보너스 (가중치 5배)
|
||||
score += (stats.strBonus +
|
||||
score +=
|
||||
(stats.strBonus +
|
||||
stats.conBonus +
|
||||
stats.dexBonus +
|
||||
stats.intBonus +
|
||||
|
||||
@@ -238,12 +238,16 @@ class PotionService {
|
||||
}) {
|
||||
final potion = PotionData.getById(potionId);
|
||||
if (potion == null) {
|
||||
return PotionPurchaseResult.failed(PotionPurchaseFailReason.potionNotFound);
|
||||
return PotionPurchaseResult.failed(
|
||||
PotionPurchaseFailReason.potionNotFound,
|
||||
);
|
||||
}
|
||||
|
||||
final totalCost = potion.price * count;
|
||||
if (gold < totalCost) {
|
||||
return PotionPurchaseResult.failed(PotionPurchaseFailReason.insufficientGold);
|
||||
return PotionPurchaseResult.failed(
|
||||
PotionPurchaseFailReason.insufficientGold,
|
||||
);
|
||||
}
|
||||
|
||||
final newInventory = inventory.addPotion(potionId, count);
|
||||
@@ -277,13 +281,17 @@ class PotionService {
|
||||
final mpPotion = PotionData.getMpPotionByTier(tier);
|
||||
|
||||
if (hpPotion == null && mpPotion == null) {
|
||||
return PotionPurchaseResult.failed(PotionPurchaseFailReason.potionNotFound);
|
||||
return PotionPurchaseResult.failed(
|
||||
PotionPurchaseFailReason.potionNotFound,
|
||||
);
|
||||
}
|
||||
|
||||
// 사용 가능 골드
|
||||
final spendableGold = (gold * spendRatio).floor();
|
||||
if (spendableGold <= 0) {
|
||||
return PotionPurchaseResult.failed(PotionPurchaseFailReason.insufficientGold);
|
||||
return PotionPurchaseResult.failed(
|
||||
PotionPurchaseFailReason.insufficientGold,
|
||||
);
|
||||
}
|
||||
|
||||
var currentInventory = inventory;
|
||||
@@ -317,7 +325,9 @@ class PotionService {
|
||||
}
|
||||
|
||||
if (totalSpent == 0) {
|
||||
return PotionPurchaseResult.failed(PotionPurchaseFailReason.insufficientGold);
|
||||
return PotionPurchaseResult.failed(
|
||||
PotionPurchaseFailReason.insufficientGold,
|
||||
);
|
||||
}
|
||||
|
||||
return PotionPurchaseResult(
|
||||
@@ -426,10 +436,7 @@ class PotionUseResult {
|
||||
|
||||
/// 실패 결과 생성
|
||||
factory PotionUseResult.failed(PotionUseFailReason reason) {
|
||||
return PotionUseResult(
|
||||
success: false,
|
||||
failReason: reason,
|
||||
);
|
||||
return PotionUseResult(success: false, failReason: reason);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -480,10 +487,7 @@ class PotionPurchaseResult {
|
||||
|
||||
/// 실패 결과 생성
|
||||
factory PotionPurchaseResult.failed(PotionPurchaseFailReason reason) {
|
||||
return PotionPurchaseResult(
|
||||
success: false,
|
||||
failReason: reason,
|
||||
);
|
||||
return PotionPurchaseResult(success: false, failReason: reason);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'dart:math';
|
||||
|
||||
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/src/core/engine/shop_service.dart';
|
||||
import 'package:askiineverdie/src/core/model/class_traits.dart';
|
||||
@@ -75,9 +76,7 @@ class ResurrectionService {
|
||||
);
|
||||
|
||||
// 전투 상태 초기화
|
||||
final progress = state.progress.copyWith(
|
||||
currentCombat: null,
|
||||
);
|
||||
final progress = state.progress.copyWith(currentCombat: null);
|
||||
|
||||
return state.copyWith(
|
||||
equipment: newEquipment,
|
||||
@@ -109,9 +108,7 @@ class ResurrectionService {
|
||||
// 장비 적용
|
||||
var nextState = state.copyWith(
|
||||
equipment: autoBuyResult.updatedEquipment,
|
||||
inventory: state.inventory.copyWith(
|
||||
gold: autoBuyResult.remainingGold,
|
||||
),
|
||||
inventory: state.inventory.copyWith(gold: autoBuyResult.remainingGold),
|
||||
);
|
||||
|
||||
// 2. 전체 HP/MP 계산 (장비 + 종족 + 클래스 보너스 포함)
|
||||
@@ -137,22 +134,22 @@ class ResurrectionService {
|
||||
// 4. 부활 후 태스크 시퀀스 설정 (큐에 추가)
|
||||
// 순서: 마을 귀환 → 샵 정비 → 사냥터 이동 → 전투
|
||||
final resurrectionQueue = <QueueEntry>[
|
||||
const QueueEntry(
|
||||
QueueEntry(
|
||||
kind: QueueKind.task,
|
||||
durationMillis: 3000, // 3초
|
||||
caption: 'Returning to town...',
|
||||
caption: l10n.taskReturningToTown,
|
||||
taskType: TaskType.neutral, // 걷기 애니메이션
|
||||
),
|
||||
const QueueEntry(
|
||||
QueueEntry(
|
||||
kind: QueueKind.task,
|
||||
durationMillis: 3000, // 3초
|
||||
caption: 'Restocking at shop...',
|
||||
caption: l10n.taskRestockingAtShop,
|
||||
taskType: TaskType.market, // town 애니메이션
|
||||
),
|
||||
const QueueEntry(
|
||||
QueueEntry(
|
||||
kind: QueueKind.task,
|
||||
durationMillis: 2000, // 2초
|
||||
caption: 'Heading to hunting grounds...',
|
||||
caption: l10n.taskHeadingToHuntingGrounds,
|
||||
taskType: TaskType.neutral, // 걷기 애니메이션
|
||||
),
|
||||
];
|
||||
@@ -164,10 +161,7 @@ class ResurrectionService {
|
||||
),
|
||||
// 현재 태스크를 빈 상태로 설정하여 큐에서 다음 태스크를 가져오도록 함
|
||||
progress: nextState.progress.copyWith(
|
||||
currentTask: const TaskInfo(
|
||||
caption: '',
|
||||
type: TaskType.neutral,
|
||||
),
|
||||
currentTask: const TaskInfo(caption: '', type: TaskType.neutral),
|
||||
task: const ProgressBarState(
|
||||
position: 0,
|
||||
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 baseValue = (level * multiplier).round();
|
||||
|
||||
@@ -145,10 +149,7 @@ class ShopService {
|
||||
magDef: baseValue ~/ 2,
|
||||
intBonus: level ~/ 10,
|
||||
),
|
||||
EquipmentSlot.hauberk => ItemStats(
|
||||
def: baseValue,
|
||||
hpBonus: level * 2,
|
||||
),
|
||||
EquipmentSlot.hauberk => ItemStats(def: baseValue, hpBonus: level * 2),
|
||||
EquipmentSlot.brassairts => ItemStats(
|
||||
def: baseValue ~/ 2,
|
||||
strBonus: level ~/ 15,
|
||||
@@ -273,11 +274,7 @@ class ShopService {
|
||||
/// 장비 판매
|
||||
SellResult sellItem(EquipmentItem item, int currentGold) {
|
||||
final price = calculateSellPrice(item);
|
||||
return SellResult(
|
||||
item: item,
|
||||
price: price,
|
||||
newGold: currentGold + price,
|
||||
);
|
||||
return SellResult(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/skill.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);
|
||||
if (skillState != null && !skillState.isReady(skillSystem.elapsedMs, skill.cooldownMs)) {
|
||||
if (skillState != null &&
|
||||
!skillState.isReady(skillSystem.elapsedMs, skill.cooldownMs)) {
|
||||
return SkillFailReason.onCooldown;
|
||||
}
|
||||
|
||||
@@ -49,7 +51,8 @@ class SkillService {
|
||||
CombatStats updatedPlayer,
|
||||
MonsterCombatStats updatedMonster,
|
||||
SkillSystemState updatedSkillSystem,
|
||||
}) useAttackSkill({
|
||||
})
|
||||
useAttackSkill({
|
||||
required Skill skill,
|
||||
required CombatStats player,
|
||||
required MonsterCombatStats monster,
|
||||
@@ -66,7 +69,9 @@ class SkillService {
|
||||
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);
|
||||
@@ -79,17 +84,15 @@ class SkillService {
|
||||
}
|
||||
|
||||
// MP 소모
|
||||
updatedPlayer = updatedPlayer.withMp(updatedPlayer.mpCurrent - skill.mpCost);
|
||||
updatedPlayer = updatedPlayer.withMp(
|
||||
updatedPlayer.mpCurrent - skill.mpCost,
|
||||
);
|
||||
|
||||
// 스킬 상태 업데이트 (쿨타임 시작)
|
||||
final updatedSkillSystem = _updateSkillCooldown(skillSystem, skill.id);
|
||||
|
||||
return (
|
||||
result: SkillUseResult(
|
||||
skill: skill,
|
||||
success: true,
|
||||
damage: finalDamage,
|
||||
),
|
||||
result: SkillUseResult(skill: skill, success: true, damage: finalDamage),
|
||||
updatedPlayer: updatedPlayer,
|
||||
updatedMonster: updatedMonster,
|
||||
updatedSkillSystem: updatedSkillSystem,
|
||||
@@ -101,7 +104,8 @@ class SkillService {
|
||||
SkillUseResult result,
|
||||
CombatStats updatedPlayer,
|
||||
SkillSystemState updatedSkillSystem,
|
||||
}) useHealSkill({
|
||||
})
|
||||
useHealSkill({
|
||||
required Skill skill,
|
||||
required CombatStats player,
|
||||
required SkillSystemState skillSystem,
|
||||
@@ -116,7 +120,9 @@ class SkillService {
|
||||
var updatedPlayer = player.applyHeal(healAmount);
|
||||
|
||||
// MP 소모
|
||||
updatedPlayer = updatedPlayer.withMp(updatedPlayer.mpCurrent - skill.mpCost);
|
||||
updatedPlayer = updatedPlayer.withMp(
|
||||
updatedPlayer.mpCurrent - skill.mpCost,
|
||||
);
|
||||
|
||||
// 스킬 상태 업데이트
|
||||
final updatedSkillSystem = _updateSkillCooldown(skillSystem, skill.id);
|
||||
@@ -137,7 +143,8 @@ class SkillService {
|
||||
SkillUseResult result,
|
||||
CombatStats updatedPlayer,
|
||||
SkillSystemState updatedSkillSystem,
|
||||
}) useBuffSkill({
|
||||
})
|
||||
useBuffSkill({
|
||||
required Skill skill,
|
||||
required CombatStats player,
|
||||
required SkillSystemState skillSystem,
|
||||
@@ -158,10 +165,11 @@ class SkillService {
|
||||
);
|
||||
|
||||
// 기존 같은 버프 제거 후 새 버프 추가
|
||||
final updatedBuffs = skillSystem.activeBuffs
|
||||
.where((b) => b.effect.id != skill.buff!.id)
|
||||
.toList()
|
||||
..add(newBuff);
|
||||
final updatedBuffs =
|
||||
skillSystem.activeBuffs
|
||||
.where((b) => b.effect.id != skill.buff!.id)
|
||||
.toList()
|
||||
..add(newBuff);
|
||||
|
||||
// MP 소모
|
||||
var updatedPlayer = player.withMp(player.mpCurrent - skill.mpCost);
|
||||
@@ -171,11 +179,7 @@ class SkillService {
|
||||
updatedSkillSystem = updatedSkillSystem.copyWith(activeBuffs: updatedBuffs);
|
||||
|
||||
return (
|
||||
result: SkillUseResult(
|
||||
skill: skill,
|
||||
success: true,
|
||||
appliedBuff: newBuff,
|
||||
),
|
||||
result: SkillUseResult(skill: skill, success: true, appliedBuff: newBuff),
|
||||
updatedPlayer: updatedPlayer,
|
||||
updatedSkillSystem: updatedSkillSystem,
|
||||
);
|
||||
@@ -190,7 +194,8 @@ class SkillService {
|
||||
CombatStats updatedPlayer,
|
||||
SkillSystemState updatedSkillSystem,
|
||||
DotEffect? dotEffect,
|
||||
}) useDotSkill({
|
||||
})
|
||||
useDotSkill({
|
||||
required Skill skill,
|
||||
required CombatStats player,
|
||||
required SkillSystemState skillSystem,
|
||||
@@ -265,12 +270,15 @@ class SkillService {
|
||||
final availableSkills = availableSkillIds
|
||||
.map((id) => SkillData.getSkillById(id))
|
||||
.whereType<Skill>()
|
||||
.where((skill) => canUseSkill(
|
||||
skill: skill,
|
||||
currentMp: currentMp,
|
||||
skillSystem: skillSystem,
|
||||
) ==
|
||||
null)
|
||||
.where(
|
||||
(skill) =>
|
||||
canUseSkill(
|
||||
skill: skill,
|
||||
currentMp: currentMp,
|
||||
skillSystem: skillSystem,
|
||||
) ==
|
||||
null,
|
||||
)
|
||||
.toList();
|
||||
|
||||
if (availableSkills.isEmpty) return null;
|
||||
@@ -311,9 +319,11 @@ class SkillService {
|
||||
|
||||
// 예상 총 데미지 기준 정렬
|
||||
dotSkills.sort((a, b) {
|
||||
final aTotal = (a.baseDotDamage ?? 0) *
|
||||
final aTotal =
|
||||
(a.baseDotDamage ?? 0) *
|
||||
((a.baseDotDurationMs ?? 0) ~/ (a.baseDotTickMs ?? 1000));
|
||||
final bTotal = (b.baseDotDamage ?? 0) *
|
||||
final bTotal =
|
||||
(b.baseDotDamage ?? 0) *
|
||||
((b.baseDotDurationMs ?? 0) ~/ (b.baseDotTickMs ?? 1000));
|
||||
return bTotal.compareTo(aTotal);
|
||||
});
|
||||
@@ -344,7 +354,9 @@ class SkillService {
|
||||
final attackSkills = skills.where((s) => s.isAttack).toList();
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
// 기존 상태 찾기
|
||||
@@ -412,11 +427,9 @@ class SkillService {
|
||||
);
|
||||
} else {
|
||||
// 새 상태 추가
|
||||
skillStates.add(SkillState(
|
||||
skillId: skillId,
|
||||
lastUsedMs: state.elapsedMs,
|
||||
rank: 1,
|
||||
));
|
||||
skillStates.add(
|
||||
SkillState(skillId: skillId, lastUsedMs: state.elapsedMs, rank: 1),
|
||||
);
|
||||
}
|
||||
|
||||
return state.copyWith(skillStates: skillStates);
|
||||
@@ -426,4 +439,142 @@ class SkillService {
|
||||
SkillSystemState updateElapsedTime(SkillSystemState state, int 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 con = baseStats.con + race.getModifier(StatType.con);
|
||||
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 cha = baseStats.cha + race.getModifier(StatType.cha);
|
||||
|
||||
@@ -108,31 +109,41 @@ class StatCalculator {
|
||||
// 클래스 패시브 적용
|
||||
|
||||
// 물리 공격력 보너스 (Bug Hunter: +20%)
|
||||
final classPhysicalBonus = klass.getPassiveValue(ClassPassiveType.physicalDamageBonus);
|
||||
final classPhysicalBonus = klass.getPassiveValue(
|
||||
ClassPassiveType.physicalDamageBonus,
|
||||
);
|
||||
if (classPhysicalBonus > 0) {
|
||||
atk = (atk * (1 + classPhysicalBonus)).round();
|
||||
}
|
||||
|
||||
// 방어력 보너스 (Debugger Paladin: +15%)
|
||||
final classDefenseBonus = klass.getPassiveValue(ClassPassiveType.defenseBonus);
|
||||
final classDefenseBonus = klass.getPassiveValue(
|
||||
ClassPassiveType.defenseBonus,
|
||||
);
|
||||
if (classDefenseBonus > 0) {
|
||||
def = (def * (1 + classDefenseBonus)).round();
|
||||
}
|
||||
|
||||
// 마법 데미지 보너스 (Compiler Mage: +25%)
|
||||
final classMagicBonus = klass.getPassiveValue(ClassPassiveType.magicDamageBonus);
|
||||
final classMagicBonus = klass.getPassiveValue(
|
||||
ClassPassiveType.magicDamageBonus,
|
||||
);
|
||||
if (classMagicBonus > 0) {
|
||||
magAtk = (magAtk * (1 + classMagicBonus)).round();
|
||||
}
|
||||
|
||||
// 회피율 보너스 (Refactor Monk: +15%)
|
||||
final classEvasionBonus = klass.getPassiveValue(ClassPassiveType.evasionBonus);
|
||||
final classEvasionBonus = klass.getPassiveValue(
|
||||
ClassPassiveType.evasionBonus,
|
||||
);
|
||||
if (classEvasionBonus > 0) {
|
||||
evasion = (evasion + classEvasionBonus).clamp(0.0, 0.6);
|
||||
}
|
||||
|
||||
// 크리티컬 보너스 (Pointer Assassin: +20%)
|
||||
final classCritBonus = klass.getPassiveValue(ClassPassiveType.criticalBonus);
|
||||
final classCritBonus = klass.getPassiveValue(
|
||||
ClassPassiveType.criticalBonus,
|
||||
);
|
||||
if (classCritBonus > 0) {
|
||||
criRate = (criRate + classCritBonus).clamp(0.0, 0.8);
|
||||
}
|
||||
|
||||
@@ -13,11 +13,7 @@ enum StoryEventType {
|
||||
|
||||
/// 스토리 이벤트 (Story Event)
|
||||
class StoryEvent {
|
||||
const StoryEvent({
|
||||
required this.type,
|
||||
required this.act,
|
||||
this.data,
|
||||
});
|
||||
const StoryEvent({required this.type, required this.act, this.data});
|
||||
|
||||
final StoryEventType type;
|
||||
final StoryAct act;
|
||||
@@ -73,18 +69,14 @@ class StoryService {
|
||||
// 이전 Act 완료 처리
|
||||
if (_currentAct != StoryAct.prologue) {
|
||||
_completedActs.add(_currentAct);
|
||||
_eventController.add(StoryEvent(
|
||||
type: StoryEventType.actComplete,
|
||||
act: _currentAct,
|
||||
));
|
||||
_eventController.add(
|
||||
StoryEvent(type: StoryEventType.actComplete, act: _currentAct),
|
||||
);
|
||||
}
|
||||
|
||||
// 새 Act 시작
|
||||
_currentAct = newAct;
|
||||
final event = StoryEvent(
|
||||
type: StoryEventType.actStart,
|
||||
act: newAct,
|
||||
);
|
||||
final event = StoryEvent(type: StoryEventType.actStart, act: newAct);
|
||||
_eventController.add(event);
|
||||
return event;
|
||||
}
|
||||
@@ -126,10 +118,9 @@ class StoryService {
|
||||
void _triggerEnding() {
|
||||
_completedActs.add(StoryAct.act5);
|
||||
_currentAct = StoryAct.ending;
|
||||
_eventController.add(StoryEvent(
|
||||
type: StoryEventType.ending,
|
||||
act: StoryAct.ending,
|
||||
));
|
||||
_eventController.add(
|
||||
StoryEvent(type: StoryEventType.ending, act: StoryAct.ending),
|
||||
);
|
||||
}
|
||||
|
||||
/// 시네마틱 데이터 가져오기 (Get Cinematic Data)
|
||||
|
||||
Reference in New Issue
Block a user