fix(death): 사망 시 희생 아이템 선택 디버그 로그 추가

- 장비 슬롯 상태 콘솔 로그 추가
- resurrection_service에 lostItemSlot 설정 누락 수정
- resetBattleUsage 존재하지 않는 메서드 호출 제거
This commit is contained in:
JiWoong Sul
2026-01-15 23:33:31 +09:00
parent 7e1936b34f
commit b8a4d73461
6 changed files with 117 additions and 101 deletions

View File

@@ -7,7 +7,6 @@ import 'package:asciineverdie/src/core/model/combat_state.dart';
import 'package:asciineverdie/src/core/model/combat_stats.dart'; import 'package:asciineverdie/src/core/model/combat_stats.dart';
import 'package:asciineverdie/src/core/model/game_state.dart'; import 'package:asciineverdie/src/core/model/game_state.dart';
import 'package:asciineverdie/src/core/model/monster_combat_stats.dart'; import 'package:asciineverdie/src/core/model/monster_combat_stats.dart';
import 'package:asciineverdie/src/core/model/potion.dart';
import 'package:asciineverdie/src/core/model/skill.dart'; import 'package:asciineverdie/src/core/model/skill.dart';
import 'package:asciineverdie/src/core/util/deterministic_random.dart'; import 'package:asciineverdie/src/core/util/deterministic_random.dart';
@@ -68,7 +67,7 @@ class CombatTickService {
var turnsElapsed = combat.turnsElapsed; var turnsElapsed = combat.turnsElapsed;
var updatedSkillSystem = skillSystem; var updatedSkillSystem = skillSystem;
var activeDoTs = [...combat.activeDoTs]; var activeDoTs = [...combat.activeDoTs];
var usedPotionTypes = {...combat.usedPotionTypes}; var lastPotionUsedMs = combat.lastPotionUsedMs;
var activeDebuffs = [...combat.activeDebuffs]; var activeDebuffs = [...combat.activeDebuffs];
PotionInventory? updatedPotionInventory; PotionInventory? updatedPotionInventory;
@@ -94,18 +93,18 @@ class CombatTickService {
totalDamageDealt = dotResult.totalDamageDealt; totalDamageDealt = dotResult.totalDamageDealt;
newEvents.addAll(dotResult.events); newEvents.addAll(dotResult.events);
// 긴급 물약 자동 사용 (HP < 30%) // 긴급 물약 자동 사용 (HP < 30% 또는 MP < 50%)
final potionResult = _tryEmergencyPotion( final potionResult = _tryEmergencyPotion(
playerStats: playerStats, playerStats: playerStats,
potionInventory: state.potionInventory, potionInventory: state.potionInventory,
usedPotionTypes: usedPotionTypes, lastPotionUsedMs: lastPotionUsedMs,
playerLevel: state.traits.level, playerLevel: state.traits.level,
timestamp: timestamp, timestamp: timestamp,
potionService: potionService, potionService: potionService,
); );
if (potionResult != null) { if (potionResult != null) {
playerStats = potionResult.playerStats; playerStats = potionResult.playerStats;
usedPotionTypes = potionResult.usedPotionTypes; lastPotionUsedMs = potionResult.lastPotionUsedMs;
updatedPotionInventory = potionResult.potionInventory; updatedPotionInventory = potionResult.potionInventory;
newEvents.addAll(potionResult.events); newEvents.addAll(potionResult.events);
} }
@@ -176,7 +175,7 @@ class CombatTickService {
isActive: isActive, isActive: isActive,
recentEvents: recentEvents, recentEvents: recentEvents,
activeDoTs: activeDoTs, activeDoTs: activeDoTs,
usedPotionTypes: usedPotionTypes, lastPotionUsedMs: lastPotionUsedMs,
activeDebuffs: activeDebuffs, activeDebuffs: activeDebuffs,
), ),
skillSystem: updatedSkillSystem, skillSystem: updatedSkillSystem,
@@ -246,38 +245,38 @@ class CombatTickService {
); );
} }
/// 긴급 물약 자동 사용 /// 긴급 물약 자동 사용 (HP/MP 통합 글로벌 쿨타임)
({ ({
CombatStats playerStats, CombatStats playerStats,
Set<PotionType> usedPotionTypes, int lastPotionUsedMs,
PotionInventory potionInventory, PotionInventory potionInventory,
List<CombatEvent> events, List<CombatEvent> events,
})? _tryEmergencyPotion({ })? _tryEmergencyPotion({
required CombatStats playerStats, required CombatStats playerStats,
required PotionInventory potionInventory, required PotionInventory potionInventory,
required Set<PotionType> usedPotionTypes, required int lastPotionUsedMs,
required int playerLevel, required int playerLevel,
required int timestamp, required int timestamp,
required PotionService potionService, required PotionService potionService,
}) { }) {
final hpRatio = playerStats.hpCurrent / playerStats.hpMax; // 글로벌 쿨타임 체크
if (hpRatio > PotionService.emergencyHpThreshold) { if (timestamp - lastPotionUsedMs < PotionService.globalPotionCooldownMs) {
return null; return null;
} }
final emergencyPotion = potionService.selectEmergencyHpPotion( // 우선순위 1: HP 물약 (HP <= 30%)
final hpRatio = playerStats.hpCurrent / playerStats.hpMax;
if (hpRatio <= PotionService.emergencyHpThreshold) {
final hpPotion = potionService.selectEmergencyHpPotion(
currentHp: playerStats.hpCurrent, currentHp: playerStats.hpCurrent,
maxHp: playerStats.hpMax, maxHp: playerStats.hpMax,
inventory: potionInventory, inventory: potionInventory,
playerLevel: playerLevel, playerLevel: playerLevel,
); );
if (emergencyPotion == null || usedPotionTypes.contains(PotionType.hp)) { if (hpPotion != null) {
return null;
}
final result = potionService.usePotion( final result = potionService.usePotion(
potionId: emergencyPotion.id, potionId: hpPotion.id,
inventory: potionInventory, inventory: potionInventory,
currentHp: playerStats.hpCurrent, currentHp: playerStats.hpCurrent,
maxHp: playerStats.hpMax, maxHp: playerStats.hpMax,
@@ -285,24 +284,64 @@ class CombatTickService {
maxMp: playerStats.mpMax, maxMp: playerStats.mpMax,
); );
if (!result.success) { if (result.success) {
return null;
}
return ( return (
playerStats: playerStats.copyWith(hpCurrent: result.newHp), playerStats: playerStats.copyWith(hpCurrent: result.newHp),
usedPotionTypes: {...usedPotionTypes, PotionType.hp}, lastPotionUsedMs: timestamp,
potionInventory: result.newInventory!, potionInventory: result.newInventory!,
events: [ events: [
CombatEvent.playerPotion( CombatEvent.playerPotion(
timestamp: timestamp, timestamp: timestamp,
potionName: emergencyPotion.name, potionName: hpPotion.name,
healAmount: result.healedAmount, healAmount: result.healedAmount,
isHp: true, isHp: true,
), ),
], ],
); );
} }
}
}
// 우선순위 2: MP 물약 (MP <= 50%)
final mpRatio = playerStats.mpCurrent / playerStats.mpMax;
if (mpRatio <= PotionService.emergencyMpThreshold) {
final mpPotion = potionService.selectEmergencyMpPotion(
currentMp: playerStats.mpCurrent,
maxMp: playerStats.mpMax,
inventory: potionInventory,
playerLevel: playerLevel,
);
if (mpPotion != null) {
final result = potionService.usePotion(
potionId: mpPotion.id,
inventory: potionInventory,
currentHp: playerStats.hpCurrent,
maxHp: playerStats.hpMax,
currentMp: playerStats.mpCurrent,
maxMp: playerStats.mpMax,
);
if (result.success) {
return (
playerStats: playerStats.copyWith(mpCurrent: result.newMp),
lastPotionUsedMs: timestamp,
potionInventory: result.newInventory!,
events: [
CombatEvent.playerPotion(
timestamp: timestamp,
potionName: mpPotion.name,
healAmount: result.healedAmount,
isHp: false,
),
],
);
}
}
}
return null;
}
/// 플레이어 공격 처리 /// 플레이어 공격 처리
({ ({

View File

@@ -10,11 +10,14 @@ import 'package:asciineverdie/src/core/model/potion.dart';
class PotionService { class PotionService {
const PotionService(); const PotionService();
/// 글로벌 물약 쿨타임 (1배속 기준 3초)
static const int globalPotionCooldownMs = 3000;
/// 긴급 물약 사용 HP 임계치 (30%) /// 긴급 물약 사용 HP 임계치 (30%)
static const double emergencyHpThreshold = 0.30; static const double emergencyHpThreshold = 0.30;
/// 긴급 물약 사용 MP 임계치 (20%) /// 긴급 물약 사용 MP 임계치 (50%)
static const double emergencyMpThreshold = 0.20; static const double emergencyMpThreshold = 0.50;
// ============================================================================ // ============================================================================
// 물약 사용 가능 여부 // 물약 사용 가능 여부
@@ -40,11 +43,6 @@ class PotionService {
return (false, PotionUseFailReason.outOfStock); return (false, PotionUseFailReason.outOfStock);
} }
// 전투당 종류별 1회 제한 체크
if (!inventory.canUseType(potion.type)) {
return (false, PotionUseFailReason.alreadyUsedThisBattle);
}
return (true, null); return (true, null);
} }
@@ -88,7 +86,7 @@ class PotionService {
healedAmount = newMp - currentMp; // 실제 회복량 healedAmount = newMp - currentMp; // 실제 회복량
} }
final newInventory = inventory.usePotion(potionId, potion.type); final newInventory = inventory.usePotion(potionId);
return PotionUseResult( return PotionUseResult(
success: true, success: true,
@@ -121,9 +119,6 @@ class PotionService {
final hpRatio = currentHp / maxHp; final hpRatio = currentHp / maxHp;
if (hpRatio > emergencyHpThreshold) return null; if (hpRatio > emergencyHpThreshold) return null;
// 전투 중 이미 HP 물약 사용했으면 불가
if (!inventory.canUseType(PotionType.hp)) return null;
// 적정 티어 계산 // 적정 티어 계산
final targetTier = PotionData.tierForLevel(playerLevel); final targetTier = PotionData.tierForLevel(playerLevel);
@@ -159,9 +154,6 @@ class PotionService {
final mpRatio = currentMp / maxMp; final mpRatio = currentMp / maxMp;
if (mpRatio > emergencyMpThreshold) return null; if (mpRatio > emergencyMpThreshold) return null;
// 전투 중 이미 MP 물약 사용했으면 불가
if (!inventory.canUseType(PotionType.mp)) return null;
// 적정 티어 계산 // 적정 티어 계산
final targetTier = PotionData.tierForLevel(playerLevel); final targetTier = PotionData.tierForLevel(playerLevel);
@@ -188,11 +180,6 @@ class PotionService {
// 인벤토리 관리 // 인벤토리 관리
// ============================================================================ // ============================================================================
/// 전투 종료 시 사용 기록 초기화
PotionInventory resetBattleUsage(PotionInventory inventory) {
return inventory.resetBattleUsage();
}
/// 물약 드랍 추가 /// 물약 드랍 추가
PotionInventory addPotionDrop( PotionInventory addPotionDrop(
PotionInventory inventory, PotionInventory inventory,
@@ -470,8 +457,8 @@ enum PotionUseFailReason {
/// 보유 물약 없음 (재고 부족) /// 보유 물약 없음 (재고 부족)
outOfStock, outOfStock,
/// 이번 전투에서 이미 해당 종류 물약 사용 /// 글로벌 쿨타임 중
alreadyUsedThisBattle, onCooldown,
} }
/// 물약 구매 결과 /// 물약 구매 결과

View File

@@ -327,11 +327,9 @@ class ProgressService {
); );
} }
final resetPotionInventory = nextState.potionInventory.resetBattleUsage();
nextState = nextState.copyWith( nextState = nextState.copyWith(
progress: progress, progress: progress,
queue: queue, queue: queue,
potionInventory: resetPotionInventory,
); );
// 최종 보스 처치 체크 // 최종 보스 처치 체크
@@ -977,10 +975,17 @@ class ProgressService {
// 무기(슬롯 0)를 제외한 장착된 장비 중 1개를 제물로 삭제 // 무기(슬롯 0)를 제외한 장착된 장비 중 1개를 제물로 삭제
final equippedNonWeaponSlots = <int>[]; final equippedNonWeaponSlots = <int>[];
for (var i = 1; i < Equipment.slotCount; i++) { for (var i = 1; i < Equipment.slotCount; i++) {
if (state.equipment.getItemByIndex(i).isNotEmpty) { final item = state.equipment.getItemByIndex(i);
// 디버그: 장비 슬롯 상태 확인
// ignore: avoid_print
print('[Death] Slot $i: "${item.name}" isEmpty=${item.isEmpty}');
if (item.isNotEmpty) {
equippedNonWeaponSlots.add(i); equippedNonWeaponSlots.add(i);
} }
} }
// 디버그: 장착된 슬롯 목록
// ignore: avoid_print
print('[Death] equippedNonWeaponSlots: $equippedNonWeaponSlots');
if (equippedNonWeaponSlots.isNotEmpty) { if (equippedNonWeaponSlots.isNotEmpty) {
lostCount = 1; lostCount = 1;

View File

@@ -47,6 +47,7 @@ class ResurrectionService {
} }
String? lostItemName; String? lostItemName;
EquipmentSlot? lostItemSlot;
var newEquipment = state.equipment; var newEquipment = state.equipment;
if (equippedItems.isNotEmpty) { if (equippedItems.isNotEmpty) {
@@ -55,12 +56,12 @@ class ResurrectionService {
final slotIndex = equippedItems[random.nextInt(equippedItems.length)]; final slotIndex = equippedItems[random.nextInt(equippedItems.length)];
final lostItem = state.equipment.getItemByIndex(slotIndex); final lostItem = state.equipment.getItemByIndex(slotIndex);
lostItemName = lostItem.name; lostItemName = lostItem.name;
lostItemSlot = EquipmentSlot.values[slotIndex];
// 해당 슬롯만 빈 아이템으로 교체 // 해당 슬롯만 빈 아이템으로 교체
final slot = EquipmentSlot.values[slotIndex];
newEquipment = state.equipment.setItemByIndex( newEquipment = state.equipment.setItemByIndex(
slotIndex, slotIndex,
EquipmentItem.empty(slot), EquipmentItem.empty(lostItemSlot),
); );
} }
@@ -70,6 +71,7 @@ class ResurrectionService {
killerName: killerName, killerName: killerName,
lostEquipmentCount: lostItemName != null ? 1 : 0, lostEquipmentCount: lostItemName != null ? 1 : 0,
lostItemName: lostItemName, lostItemName: lostItemName,
lostItemSlot: lostItemSlot,
goldAtDeath: state.inventory.gold, goldAtDeath: state.inventory.gold,
levelAtDeath: state.traits.level, levelAtDeath: state.traits.level,
timestamp: state.skillSystem.elapsedMs, timestamp: state.skillSystem.elapsedMs,

View File

@@ -1,7 +1,6 @@
import 'package:asciineverdie/src/core/model/combat_event.dart'; import 'package:asciineverdie/src/core/model/combat_event.dart';
import 'package:asciineverdie/src/core/model/combat_stats.dart'; import 'package:asciineverdie/src/core/model/combat_stats.dart';
import 'package:asciineverdie/src/core/model/monster_combat_stats.dart'; import 'package:asciineverdie/src/core/model/monster_combat_stats.dart';
import 'package:asciineverdie/src/core/model/potion.dart';
import 'package:asciineverdie/src/core/model/skill.dart'; import 'package:asciineverdie/src/core/model/skill.dart';
/// 현재 전투 상태 /// 현재 전투 상태
@@ -20,7 +19,7 @@ class CombatState {
required this.isActive, required this.isActive,
this.recentEvents = const [], this.recentEvents = const [],
this.activeDoTs = const [], this.activeDoTs = const [],
this.usedPotionTypes = const {}, this.lastPotionUsedMs = 0,
this.activeDebuffs = const [], this.activeDebuffs = const [],
}); });
@@ -54,8 +53,8 @@ class CombatState {
/// 활성 DOT 효과 목록 /// 활성 DOT 효과 목록
final List<DotEffect> activeDoTs; final List<DotEffect> activeDoTs;
/// 이번 전투에서 사용한 물약 종류 (종류별 1회 제한) /// 마지막 물약 사용 시간 (글로벌 쿨타임용)
final Set<PotionType> usedPotionTypes; final int lastPotionUsedMs;
/// 몬스터에 적용된 활성 디버프 목록 /// 몬스터에 적용된 활성 디버프 목록
final List<ActiveBuff> activeDebuffs; final List<ActiveBuff> activeDebuffs;
@@ -79,8 +78,10 @@ class CombatState {
/// 몬스터 HP 비율 /// 몬스터 HP 비율
double get monsterHpRatio => monsterStats.hpRatio; double get monsterHpRatio => monsterStats.hpRatio;
/// 특정 종류 물약 사용 가능 여부 /// 물약 사용 가능 여부 (글로벌 쿨타임 체크)
bool canUsePotionType(PotionType type) => !usedPotionTypes.contains(type); bool canUsePotion(int currentMs, int cooldownMs) {
return currentMs - lastPotionUsedMs >= cooldownMs;
}
/// 활성 DOT 존재 여부 /// 활성 DOT 존재 여부
bool get hasActiveDoTs => activeDoTs.isNotEmpty; bool get hasActiveDoTs => activeDoTs.isNotEmpty;
@@ -121,7 +122,7 @@ class CombatState {
bool? isActive, bool? isActive,
List<CombatEvent>? recentEvents, List<CombatEvent>? recentEvents,
List<DotEffect>? activeDoTs, List<DotEffect>? activeDoTs,
Set<PotionType>? usedPotionTypes, int? lastPotionUsedMs,
List<ActiveBuff>? activeDebuffs, List<ActiveBuff>? activeDebuffs,
}) { }) {
return CombatState( return CombatState(
@@ -137,7 +138,7 @@ class CombatState {
isActive: isActive ?? this.isActive, isActive: isActive ?? this.isActive,
recentEvents: recentEvents ?? this.recentEvents, recentEvents: recentEvents ?? this.recentEvents,
activeDoTs: activeDoTs ?? this.activeDoTs, activeDoTs: activeDoTs ?? this.activeDoTs,
usedPotionTypes: usedPotionTypes ?? this.usedPotionTypes, lastPotionUsedMs: lastPotionUsedMs ?? this.lastPotionUsedMs,
activeDebuffs: activeDebuffs ?? this.activeDebuffs, activeDebuffs: activeDebuffs ?? this.activeDebuffs,
); );
} }

View File

@@ -60,39 +60,30 @@ class Potion {
/// 물약 인벤토리 상태 /// 물약 인벤토리 상태
/// ///
/// 보유 물약 수량 및 전투 중 사용 기록 관리 /// 보유 물약 수량 관리 (쿨타임은 CombatState에서 관리)
class PotionInventory { class PotionInventory {
const PotionInventory({ const PotionInventory({
this.potions = const {}, this.potions = const {},
this.usedInBattle = const {},
}); });
/// 보유 물약 (물약 ID → 수량) /// 보유 물약 (물약 ID → 수량)
final Map<String, int> potions; final Map<String, int> potions;
/// 현재 전투에서 사용한 물약 종류
final Set<PotionType> usedInBattle;
/// 물약 보유 여부 /// 물약 보유 여부
bool hasPotion(String potionId) => (potions[potionId] ?? 0) > 0; bool hasPotion(String potionId) => (potions[potionId] ?? 0) > 0;
/// 물약 수량 조회 /// 물약 수량 조회
int getQuantity(String potionId) => potions[potionId] ?? 0; int getQuantity(String potionId) => potions[potionId] ?? 0;
/// 특정 종류 물약 사용 가능 여부
///
/// 전투당 종류별 1회 제한 체크
bool canUseType(PotionType type) => !usedInBattle.contains(type);
/// 물약 추가 /// 물약 추가
PotionInventory addPotion(String potionId, [int count = 1]) { PotionInventory addPotion(String potionId, [int count = 1]) {
final newPotions = Map<String, int>.from(potions); final newPotions = Map<String, int>.from(potions);
newPotions[potionId] = (newPotions[potionId] ?? 0) + count; newPotions[potionId] = (newPotions[potionId] ?? 0) + count;
return PotionInventory(potions: newPotions, usedInBattle: usedInBattle); return PotionInventory(potions: newPotions);
} }
/// 물약 사용 (수량 감소) /// 물약 사용 (수량 감소)
PotionInventory usePotion(String potionId, PotionType type) { PotionInventory usePotion(String potionId) {
final currentQty = potions[potionId] ?? 0; final currentQty = potions[potionId] ?? 0;
if (currentQty <= 0) return this; if (currentQty <= 0) return this;
@@ -102,14 +93,7 @@ class PotionInventory {
newPotions.remove(potionId); newPotions.remove(potionId);
} }
final newUsed = Set<PotionType>.from(usedInBattle)..add(type); return PotionInventory(potions: newPotions);
return PotionInventory(potions: newPotions, usedInBattle: newUsed);
}
/// 전투 종료 시 사용 기록 초기화
PotionInventory resetBattleUsage() {
return PotionInventory(potions: potions, usedInBattle: const {});
} }
/// 빈 인벤토리 /// 빈 인벤토리
@@ -117,11 +101,9 @@ class PotionInventory {
PotionInventory copyWith({ PotionInventory copyWith({
Map<String, int>? potions, Map<String, int>? potions,
Set<PotionType>? usedInBattle,
}) { }) {
return PotionInventory( return PotionInventory(
potions: potions ?? this.potions, potions: potions ?? this.potions,
usedInBattle: usedInBattle ?? this.usedInBattle,
); );
} }
} }