feat(game): 포션 시스템 및 UI 패널 추가
- 포션 시스템 구현 (PotionService, Potion 모델) - 포션 인벤토리 패널 위젯 - 활성 버프 패널 위젯 - 장비 스탯 패널 위젯 - 스킬 시스템 확장 - 일본어 번역 추가 - 전투 이벤트/상태 모델 개선
This commit is contained in:
@@ -4,6 +4,7 @@ import 'package:askiineverdie/data/game_text_l10n.dart' as l10n;
|
||||
import 'package:askiineverdie/data/skill_data.dart';
|
||||
import 'package:askiineverdie/src/core/engine/combat_calculator.dart';
|
||||
import 'package:askiineverdie/src/core/engine/game_mutations.dart';
|
||||
import 'package:askiineverdie/src/core/engine/potion_service.dart';
|
||||
import 'package:askiineverdie/src/core/engine/reward_service.dart';
|
||||
import 'package:askiineverdie/src/core/engine/skill_service.dart';
|
||||
import 'package:askiineverdie/src/core/model/combat_event.dart';
|
||||
@@ -13,7 +14,9 @@ import 'package:askiineverdie/src/core/model/equipment_item.dart';
|
||||
import 'package:askiineverdie/src/core/model/equipment_slot.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/potion.dart';
|
||||
import 'package:askiineverdie/src/core/model/pq_config.dart';
|
||||
import 'package:askiineverdie/src/core/model/skill.dart';
|
||||
import 'package:askiineverdie/src/core/util/pq_logic.dart' as pq_logic;
|
||||
|
||||
class ProgressTickResult {
|
||||
@@ -186,9 +189,10 @@ class ProgressService {
|
||||
? progress.task.max
|
||||
: uncapped;
|
||||
|
||||
// 킬 태스크 중 전투 진행 (스킬 자동 사용 포함)
|
||||
// 킬 태스크 중 전투 진행 (스킬 자동 사용, DOT, 물약 포함)
|
||||
var updatedCombat = progress.currentCombat;
|
||||
var updatedSkillSystem = nextState.skillSystem;
|
||||
var updatedPotionInventory = nextState.potionInventory;
|
||||
if (progress.currentTask.type == TaskType.kill && updatedCombat != null && updatedCombat.isActive) {
|
||||
final combatResult = _processCombatTickWithSkills(
|
||||
nextState,
|
||||
@@ -198,6 +202,9 @@ class ProgressService {
|
||||
);
|
||||
updatedCombat = combatResult.combat;
|
||||
updatedSkillSystem = combatResult.skillSystem;
|
||||
if (combatResult.potionInventory != null) {
|
||||
updatedPotionInventory = combatResult.potionInventory!;
|
||||
}
|
||||
|
||||
// Phase 4: 플레이어 사망 체크
|
||||
if (!updatedCombat.playerStats.isAlive) {
|
||||
@@ -216,7 +223,11 @@ class ProgressService {
|
||||
currentCombat: updatedCombat,
|
||||
);
|
||||
nextState = _recalculateEncumbrance(
|
||||
nextState.copyWith(progress: progress, skillSystem: updatedSkillSystem),
|
||||
nextState.copyWith(
|
||||
progress: progress,
|
||||
skillSystem: updatedSkillSystem,
|
||||
potionInventory: updatedPotionInventory,
|
||||
),
|
||||
);
|
||||
return ProgressTickResult(state: nextState);
|
||||
}
|
||||
@@ -245,11 +256,33 @@ class ProgressService {
|
||||
}
|
||||
|
||||
// 전리품 획득 (원본 Main.pas:625-630)
|
||||
nextState = _winLoot(nextState);
|
||||
final lootResult = _winLoot(nextState);
|
||||
nextState = lootResult.state;
|
||||
|
||||
// 전투 상태 초기화
|
||||
progress = nextState.progress.copyWith(currentCombat: null);
|
||||
nextState = nextState.copyWith(progress: progress);
|
||||
// 물약 드랍 시 전투 로그에 이벤트 추가
|
||||
var combatForReset = progress.currentCombat;
|
||||
if (lootResult.droppedPotion != null && combatForReset != null) {
|
||||
final potionDropEvent = CombatEvent.potionDrop(
|
||||
timestamp: nextState.skillSystem.elapsedMs,
|
||||
potionName: lootResult.droppedPotion!.name,
|
||||
isHp: lootResult.droppedPotion!.isHpPotion,
|
||||
);
|
||||
final updatedEvents = [...combatForReset.recentEvents, potionDropEvent];
|
||||
combatForReset = combatForReset.copyWith(
|
||||
recentEvents: updatedEvents.length > 10
|
||||
? updatedEvents.sublist(updatedEvents.length - 10)
|
||||
: updatedEvents,
|
||||
);
|
||||
progress = progress.copyWith(currentCombat: combatForReset);
|
||||
}
|
||||
|
||||
// 전투 상태 초기화 및 물약 사용 기록 초기화
|
||||
progress = progress.copyWith(currentCombat: null);
|
||||
final resetPotionInventory = nextState.potionInventory.resetBattleUsage();
|
||||
nextState = nextState.copyWith(
|
||||
progress: progress,
|
||||
potionInventory: resetPotionInventory,
|
||||
);
|
||||
}
|
||||
|
||||
// 시장/판매/구매 태스크 완료 시 처리 (원본 Main.pas:631-649)
|
||||
@@ -728,39 +761,60 @@ class ProgressService {
|
||||
}
|
||||
|
||||
/// 킬 태스크 완료 시 전리품 획득 (원본 Main.pas:625-630)
|
||||
GameState _winLoot(GameState state) {
|
||||
/// 전리품 획득 결과
|
||||
///
|
||||
/// [state] 업데이트된 게임 상태
|
||||
/// [droppedPotion] 드랍된 물약 (없으면 null)
|
||||
({GameState state, Potion? droppedPotion}) _winLoot(GameState state) {
|
||||
final taskInfo = state.progress.currentTask;
|
||||
final monsterPart = taskInfo.monsterPart ?? '';
|
||||
final monsterBaseName = taskInfo.monsterBaseName ?? '';
|
||||
|
||||
var resultState = state;
|
||||
|
||||
// 부위가 '*'이면 WinItem 호출 (특수 아이템)
|
||||
if (monsterPart == '*') {
|
||||
return mutations.winItem(state);
|
||||
}
|
||||
resultState = mutations.winItem(resultState);
|
||||
} else if (monsterPart.isNotEmpty && monsterBaseName.isNotEmpty) {
|
||||
// 원본: Add(Inventory, LowerCase(Split(fTask.Caption,1) + ' ' +
|
||||
// ProperCase(Split(fTask.Caption,3))), 1);
|
||||
// 예: "goblin Claw" 형태로 인벤토리 추가
|
||||
final itemName =
|
||||
'${monsterBaseName.toLowerCase()} ${_properCase(monsterPart)}';
|
||||
|
||||
// 부위가 비어있으면 전리품 없음
|
||||
if (monsterPart.isEmpty || monsterBaseName.isEmpty) {
|
||||
return state;
|
||||
}
|
||||
// 인벤토리에 추가
|
||||
final items = [...resultState.inventory.items];
|
||||
final existing = items.indexWhere((e) => e.name == itemName);
|
||||
if (existing >= 0) {
|
||||
items[existing] = items[existing].copyWith(
|
||||
count: items[existing].count + 1,
|
||||
);
|
||||
} else {
|
||||
items.add(InventoryEntry(name: itemName, count: 1));
|
||||
}
|
||||
|
||||
// 원본: Add(Inventory, LowerCase(Split(fTask.Caption,1) + ' ' +
|
||||
// ProperCase(Split(fTask.Caption,3))), 1);
|
||||
// 예: "goblin Claw" 형태로 인벤토리 추가
|
||||
final itemName =
|
||||
'${monsterBaseName.toLowerCase()} ${_properCase(monsterPart)}';
|
||||
|
||||
// 인벤토리에 추가
|
||||
final items = [...state.inventory.items];
|
||||
final existing = items.indexWhere((e) => e.name == itemName);
|
||||
if (existing >= 0) {
|
||||
items[existing] = items[existing].copyWith(
|
||||
count: items[existing].count + 1,
|
||||
resultState = resultState.copyWith(
|
||||
inventory: resultState.inventory.copyWith(items: items),
|
||||
);
|
||||
} else {
|
||||
items.add(InventoryEntry(name: itemName, count: 1));
|
||||
}
|
||||
|
||||
return state.copyWith(inventory: state.inventory.copyWith(items: items));
|
||||
// 물약 드랍 시도
|
||||
final potionService = const PotionService();
|
||||
final rng = resultState.rng;
|
||||
final (updatedPotionInventory, droppedPotion) = potionService.tryPotionDrop(
|
||||
playerLevel: resultState.traits.level,
|
||||
inventory: resultState.potionInventory,
|
||||
roll: rng.nextInt(100),
|
||||
typeRoll: rng.nextInt(100),
|
||||
);
|
||||
|
||||
return (
|
||||
state: resultState.copyWith(
|
||||
rng: rng,
|
||||
potionInventory: updatedPotionInventory,
|
||||
),
|
||||
droppedPotion: droppedPotion,
|
||||
);
|
||||
}
|
||||
|
||||
/// 첫 글자만 대문자로 변환 (원본 ProperCase)
|
||||
@@ -796,6 +850,22 @@ class ProgressService {
|
||||
final slotIndex = nextState.rng.nextInt(Equipment.slotCount);
|
||||
nextState = mutations.winEquipByIndex(nextState, level, slotIndex);
|
||||
|
||||
// 물약 자동 구매 (남은 골드의 20% 사용)
|
||||
final potionService = const PotionService();
|
||||
final purchaseResult = potionService.autoPurchasePotions(
|
||||
playerLevel: level,
|
||||
inventory: nextState.potionInventory,
|
||||
gold: nextState.inventory.gold,
|
||||
spendRatio: 0.20,
|
||||
);
|
||||
|
||||
if (purchaseResult.success && purchaseResult.newInventory != null) {
|
||||
nextState = nextState.copyWith(
|
||||
inventory: nextState.inventory.copyWith(gold: purchaseResult.newGold),
|
||||
potionInventory: purchaseResult.newInventory,
|
||||
);
|
||||
}
|
||||
|
||||
return nextState;
|
||||
}
|
||||
|
||||
@@ -864,25 +934,30 @@ class ProgressService {
|
||||
);
|
||||
}
|
||||
|
||||
/// 전투 틱 처리 (스킬 자동 사용 포함)
|
||||
/// 전투 틱 처리 (스킬 자동 사용, DOT, 물약 포함)
|
||||
///
|
||||
/// [state] 현재 게임 상태
|
||||
/// [combat] 현재 전투 상태
|
||||
/// [skillSystem] 스킬 시스템 상태
|
||||
/// [elapsedMs] 경과 시간 (밀리초)
|
||||
/// Returns: 업데이트된 전투 상태 및 스킬 시스템 상태
|
||||
({CombatState combat, SkillSystemState skillSystem}) _processCombatTickWithSkills(
|
||||
/// Returns: 업데이트된 전투 상태, 스킬 시스템 상태, 물약 인벤토리
|
||||
({
|
||||
CombatState combat,
|
||||
SkillSystemState skillSystem,
|
||||
PotionInventory? potionInventory,
|
||||
}) _processCombatTickWithSkills(
|
||||
GameState state,
|
||||
CombatState combat,
|
||||
SkillSystemState skillSystem,
|
||||
int elapsedMs,
|
||||
) {
|
||||
if (!combat.isActive || combat.isCombatOver) {
|
||||
return (combat: combat, skillSystem: skillSystem);
|
||||
return (combat: combat, skillSystem: skillSystem, potionInventory: null);
|
||||
}
|
||||
|
||||
final calculator = CombatCalculator(rng: state.rng);
|
||||
final skillService = SkillService(rng: state.rng);
|
||||
final potionService = const PotionService();
|
||||
var playerStats = combat.playerStats;
|
||||
var monsterStats = combat.monsterStats;
|
||||
var playerAccumulator = combat.playerAttackAccumulatorMs + elapsedMs;
|
||||
@@ -891,11 +966,90 @@ class ProgressService {
|
||||
var totalDamageTaken = combat.totalDamageTaken;
|
||||
var turnsElapsed = combat.turnsElapsed;
|
||||
var updatedSkillSystem = skillSystem;
|
||||
var activeDoTs = [...combat.activeDoTs];
|
||||
var usedPotionTypes = {...combat.usedPotionTypes};
|
||||
PotionInventory? updatedPotionInventory;
|
||||
|
||||
// 새 전투 이벤트 수집
|
||||
final newEvents = <CombatEvent>[];
|
||||
final timestamp = updatedSkillSystem.elapsedMs;
|
||||
|
||||
// =========================================================================
|
||||
// DOT 틱 처리
|
||||
// =========================================================================
|
||||
var dotDamageThisTick = 0;
|
||||
final updatedDoTs = <DotEffect>[];
|
||||
|
||||
for (final dot in activeDoTs) {
|
||||
final (updatedDot, ticksTriggered) = dot.tick(elapsedMs);
|
||||
|
||||
if (ticksTriggered > 0) {
|
||||
final damage = dot.damagePerTick * ticksTriggered;
|
||||
dotDamageThisTick += damage;
|
||||
|
||||
// DOT 데미지 이벤트 생성
|
||||
newEvents.add(CombatEvent.dotTick(
|
||||
timestamp: timestamp,
|
||||
skillName: dot.skillId,
|
||||
damage: damage,
|
||||
targetName: monsterStats.name,
|
||||
));
|
||||
}
|
||||
|
||||
// 만료되지 않은 DOT만 유지
|
||||
if (updatedDot.isActive) {
|
||||
updatedDoTs.add(updatedDot);
|
||||
}
|
||||
}
|
||||
|
||||
// DOT 데미지 적용
|
||||
if (dotDamageThisTick > 0 && monsterStats.isAlive) {
|
||||
final newMonsterHp = (monsterStats.hpCurrent - dotDamageThisTick)
|
||||
.clamp(0, monsterStats.hpMax);
|
||||
monsterStats = monsterStats.copyWith(hpCurrent: newMonsterHp);
|
||||
totalDamageDealt += dotDamageThisTick;
|
||||
}
|
||||
|
||||
activeDoTs = updatedDoTs;
|
||||
|
||||
// =========================================================================
|
||||
// 긴급 물약 자동 사용 (HP < 30%)
|
||||
// =========================================================================
|
||||
final hpRatio = playerStats.hpCurrent / playerStats.hpMax;
|
||||
if (hpRatio <= PotionService.emergencyHpThreshold) {
|
||||
final emergencyPotion = potionService.selectEmergencyHpPotion(
|
||||
currentHp: playerStats.hpCurrent,
|
||||
maxHp: playerStats.hpMax,
|
||||
inventory: state.potionInventory,
|
||||
playerLevel: state.traits.level,
|
||||
);
|
||||
|
||||
if (emergencyPotion != null &&
|
||||
!usedPotionTypes.contains(PotionType.hp)) {
|
||||
final result = potionService.usePotion(
|
||||
potionId: emergencyPotion.id,
|
||||
inventory: state.potionInventory,
|
||||
currentHp: playerStats.hpCurrent,
|
||||
maxHp: playerStats.hpMax,
|
||||
currentMp: playerStats.mpCurrent,
|
||||
maxMp: playerStats.mpMax,
|
||||
);
|
||||
|
||||
if (result.success) {
|
||||
playerStats = playerStats.copyWith(hpCurrent: result.newHp);
|
||||
usedPotionTypes = {...usedPotionTypes, PotionType.hp};
|
||||
updatedPotionInventory = result.newInventory;
|
||||
|
||||
newEvents.add(CombatEvent.playerPotion(
|
||||
timestamp: timestamp,
|
||||
potionName: emergencyPotion.name,
|
||||
healAmount: result.healedAmount,
|
||||
isHp: true,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 플레이어 공격 체크
|
||||
if (playerAccumulator >= playerStats.attackDelayMs) {
|
||||
// 스킬 자동 선택
|
||||
@@ -912,6 +1066,7 @@ class ProgressService {
|
||||
monster: monsterStats,
|
||||
skillSystem: updatedSkillSystem,
|
||||
availableSkillIds: availableSkillIds,
|
||||
activeDoTs: activeDoTs,
|
||||
);
|
||||
|
||||
if (selectedSkill != null && selectedSkill.isAttack) {
|
||||
@@ -934,6 +1089,30 @@ class ProgressService {
|
||||
damage: skillResult.result.damage,
|
||||
targetName: monsterStats.name,
|
||||
));
|
||||
} else if (selectedSkill != null && selectedSkill.isDot) {
|
||||
// DOT 스킬 사용
|
||||
final skillResult = skillService.useDotSkill(
|
||||
skill: selectedSkill,
|
||||
player: playerStats,
|
||||
skillSystem: updatedSkillSystem,
|
||||
playerInt: state.stats.intelligence,
|
||||
playerWis: state.stats.wis,
|
||||
);
|
||||
playerStats = skillResult.updatedPlayer;
|
||||
updatedSkillSystem = skillResult.updatedSkillSystem;
|
||||
|
||||
// DOT 효과 추가
|
||||
if (skillResult.dotEffect != null) {
|
||||
activeDoTs.add(skillResult.dotEffect!);
|
||||
}
|
||||
|
||||
// DOT 스킬 사용 이벤트 생성
|
||||
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(
|
||||
@@ -1053,8 +1232,11 @@ class ProgressService {
|
||||
turnsElapsed: turnsElapsed,
|
||||
isActive: isActive,
|
||||
recentEvents: recentEvents,
|
||||
activeDoTs: activeDoTs,
|
||||
usedPotionTypes: usedPotionTypes,
|
||||
),
|
||||
skillSystem: updatedSkillSystem,
|
||||
potionInventory: updatedPotionInventory,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user