feat(combat): Phase 1 핵심 전투 시스템 구현

신규 파일:
- combat_stats.dart: 플레이어 전투 파생 스탯 (ATK, DEF, CRI 등)
- monster_combat_stats.dart: 몬스터 전투 스탯 (레벨 기반 스케일링)
- combat_result.dart: 전투 결과 타입 (AttackResult, CombatTurnResult)
- combat_state.dart: 전투 상태 관리 (HP, 누적 시간, 턴 수)
- combat_calculator.dart: 전투 계산 서비스 (데미지, 명중, 크리티컬)

수정 파일:
- game_state.dart: ProgressState에 currentCombat 필드 추가
- progress_service.dart: 킬 태스크 시 전투 로직 통합
  - CombatStats/MonsterCombatStats 기반 전투 시간 계산
  - 틱마다 전투 턴 처리 (_processCombatTick)
  - 전투 완료 시 플레이어 HP 반영
This commit is contained in:
JiWoong Sul
2025-12-17 16:31:52 +09:00
parent 9ad0cf4b74
commit c62687f7bd
7 changed files with 1148 additions and 7 deletions

View File

@@ -1,9 +1,13 @@
import 'dart:math' as math;
import 'package:askiineverdie/data/game_text_l10n.dart' as l10n;
import 'package:askiineverdie/src/core/engine/combat_calculator.dart';
import 'package:askiineverdie/src/core/engine/game_mutations.dart';
import 'package:askiineverdie/src/core/engine/reward_service.dart';
import 'package:askiineverdie/src/core/model/combat_state.dart';
import 'package:askiineverdie/src/core/model/combat_stats.dart';
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/pq_config.dart';
import 'package:askiineverdie/src/core/util/pq_logic.dart' as pq_logic;
@@ -143,8 +147,16 @@ class ProgressService {
final int newTaskPos = uncapped > progress.task.max
? progress.task.max
: uncapped;
// 킬 태스크 중 전투 진행
var updatedCombat = progress.currentCombat;
if (progress.currentTask.type == TaskType.kill && updatedCombat != null && updatedCombat.isActive) {
updatedCombat = _processCombatTick(nextState, updatedCombat, clamped);
}
progress = progress.copyWith(
task: progress.task.copyWith(position: newTaskPos),
currentCombat: updatedCombat,
);
nextState = _recalculateEncumbrance(
nextState.copyWith(progress: progress),
@@ -155,10 +167,24 @@ class ProgressService {
final gain = progress.currentTask.type == TaskType.kill;
final incrementSeconds = progress.task.max ~/ 1000;
// 킬 태스크 완료 시 전리품 획득 (원본 Main.pas:625-630)
// 킬 태스크 완료 시 전투 결과 반영 및 전리품 획득
if (gain) {
// 전투 결과에 따라 플레이어 HP 업데이트
final combat = progress.currentCombat;
if (combat != null && combat.isActive) {
// 전투 중 받은 데미지를 실제 Stats에 반영
final newHp = combat.playerStats.hpCurrent;
nextState = nextState.copyWith(
stats: nextState.stats.copyWith(hpCurrent: newHp),
);
}
// 전리품 획득 (원본 Main.pas:625-630)
nextState = _winLoot(nextState);
progress = nextState.progress;
// 전투 상태 초기화
progress = nextState.progress.copyWith(currentCombat: null);
nextState = nextState.copyWith(progress: progress);
}
// 시장/판매/구매 태스크 완료 시 처리 (원본 Main.pas:631-649)
@@ -367,11 +393,31 @@ class ProgressService {
questLevel,
);
// 태스크 지속시간 계산 (원본 682줄)
// n := (2 * InventoryLabelAlsoGameStyle.Tag * n * 1000) div l;
// InventoryLabelAlsoGameStyle.Tag는 게임 스타일을 나타내는 값 (1이 기본)
const gameStyleTag = 1;
final durationMillis = (2 * gameStyleTag * level * 1000) ~/ level;
// 전투 스탯 생성
final playerCombatStats = CombatStats.fromStats(
stats: state.stats,
equipment: state.equipment,
level: level,
);
final monsterCombatStats = MonsterCombatStats.fromLevel(
name: monsterResult.displayName,
level: monsterResult.level,
speedType: MonsterCombatStats.inferSpeedType(monsterResult.baseName),
);
// 전투 상태 초기화
final combatState = CombatState.start(
playerStats: playerCombatStats,
monsterStats: monsterCombatStats,
);
// 태스크 지속시간 계산 (CombatCalculator 기반)
final combatCalculator = CombatCalculator(rng: state.rng);
final durationMillis = combatCalculator.estimateCombatDurationMs(
player: playerCombatStats,
monster: monsterCombatStats,
);
final taskResult = pq_logic.startTask(
progress,
@@ -387,6 +433,7 @@ class ProgressService {
monsterPart: monsterResult.part,
monsterLevel: monsterResult.level,
),
currentCombat: combatState,
);
return (progress: progress, queue: queue);
@@ -743,4 +790,66 @@ class ProgressService {
continuesSelling: false,
);
}
/// 전투 틱 처리
///
/// [state] 현재 게임 상태
/// [combat] 현재 전투 상태
/// [elapsedMs] 경과 시간 (밀리초)
/// Returns: 업데이트된 전투 상태
CombatState _processCombatTick(
GameState state,
CombatState combat,
int elapsedMs,
) {
if (!combat.isActive || combat.isCombatOver) {
return combat;
}
final calculator = CombatCalculator(rng: state.rng);
var playerStats = combat.playerStats;
var monsterStats = combat.monsterStats;
var playerAccumulator = combat.playerAttackAccumulatorMs + elapsedMs;
var monsterAccumulator = combat.monsterAttackAccumulatorMs + elapsedMs;
var totalDamageDealt = combat.totalDamageDealt;
var totalDamageTaken = combat.totalDamageTaken;
var turnsElapsed = combat.turnsElapsed;
// 플레이어 공격 체크
if (playerAccumulator >= playerStats.attackDelayMs) {
final attackResult = calculator.playerAttackMonster(
attacker: playerStats,
defender: monsterStats,
);
monsterStats = attackResult.updatedDefender;
totalDamageDealt += attackResult.result.damage;
playerAccumulator -= playerStats.attackDelayMs;
turnsElapsed++;
}
// 몬스터가 살아있으면 반격
if (monsterStats.isAlive && monsterAccumulator >= monsterStats.attackDelayMs) {
final attackResult = calculator.monsterAttackPlayer(
attacker: monsterStats,
defender: playerStats,
);
playerStats = attackResult.updatedDefender;
totalDamageTaken += attackResult.result.damage;
monsterAccumulator -= monsterStats.attackDelayMs;
}
// 전투 종료 체크
final isActive = playerStats.isAlive && monsterStats.isAlive;
return combat.copyWith(
playerStats: playerStats,
monsterStats: monsterStats,
playerAttackAccumulatorMs: playerAccumulator,
monsterAttackAccumulatorMs: monsterAccumulator,
totalDamageDealt: totalDamageDealt,
totalDamageTaken: totalDamageTaken,
turnsElapsed: turnsElapsed,
isActive: isActive,
);
}
}