Compare commits
2 Commits
f18f3ceaee
...
c3a8bc305a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c3a8bc305a | ||
|
|
a2d62f1f4f |
@@ -2,7 +2,6 @@ import 'package:asciineverdie/data/skill_data.dart';
|
|||||||
import 'package:asciineverdie/src/core/engine/combat_calculator.dart';
|
import 'package:asciineverdie/src/core/engine/combat_calculator.dart';
|
||||||
import 'package:asciineverdie/src/core/engine/skill_service.dart';
|
import 'package:asciineverdie/src/core/engine/skill_service.dart';
|
||||||
import 'package:asciineverdie/src/core/model/arena_match.dart';
|
import 'package:asciineverdie/src/core/model/arena_match.dart';
|
||||||
import 'package:asciineverdie/src/core/model/combat_stats.dart';
|
|
||||||
import 'package:asciineverdie/src/core/model/equipment_item.dart';
|
import 'package:asciineverdie/src/core/model/equipment_item.dart';
|
||||||
import 'package:asciineverdie/src/core/model/equipment_slot.dart';
|
import 'package:asciineverdie/src/core/model/equipment_slot.dart';
|
||||||
import 'package:asciineverdie/src/core/model/game_state.dart';
|
import 'package:asciineverdie/src/core/model/game_state.dart';
|
||||||
@@ -47,64 +46,38 @@ class ArenaService {
|
|||||||
return skills;
|
return skills;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// AI 스킬 선택 (우선순위: 회복 > 버프 > 공격)
|
/// 스킬 ID 목록 추출 (HallOfFameEntry에서)
|
||||||
Skill? _selectBestSkill({
|
List<String> _getSkillIdsFromEntry(HallOfFameEntry entry) {
|
||||||
required List<Skill> skills,
|
return _getSkillsFromEntry(entry).map((s) => s.id).toList();
|
||||||
required CombatStats stats,
|
}
|
||||||
required SkillSystemState skillSystem,
|
|
||||||
required MonsterCombatStats? target,
|
|
||||||
}) {
|
|
||||||
if (skills.isEmpty) return null;
|
|
||||||
|
|
||||||
final currentMp = stats.mpCurrent;
|
/// 스킬 랭크 조회 (HallOfFameEntry의 finalSkills에서)
|
||||||
final hpRatio = stats.hpCurrent / stats.hpMax;
|
int _getSkillRankFromEntry(HallOfFameEntry entry, String skillId) {
|
||||||
|
final skill = SkillData.getSkillById(skillId);
|
||||||
|
if (skill == null) return 1;
|
||||||
|
|
||||||
// HP가 낮으면 회복 스킬 우선
|
final skillData = entry.finalSkills;
|
||||||
if (hpRatio < 0.4) {
|
if (skillData == null || skillData.isEmpty) return 1;
|
||||||
for (final skill in skills) {
|
|
||||||
if (skill.type == SkillType.heal &&
|
for (final data in skillData) {
|
||||||
_skillService.canUseSkill(
|
if (data['name'] == skill.name) {
|
||||||
skill: skill,
|
final rankStr = data['rank'] ?? 'I';
|
||||||
currentMp: currentMp,
|
return _romanToInt(rankStr);
|
||||||
skillSystem: skillSystem,
|
|
||||||
) ==
|
|
||||||
null) {
|
|
||||||
return skill;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
// 버프가 없으면 버프 스킬
|
/// 로마 숫자 → 정수 변환
|
||||||
if (skillSystem.activeBuffs.isEmpty) {
|
int _romanToInt(String roman) {
|
||||||
for (final skill in skills) {
|
return switch (roman) {
|
||||||
if (skill.type == SkillType.buff &&
|
'I' => 1,
|
||||||
_skillService.canUseSkill(
|
'II' => 2,
|
||||||
skill: skill,
|
'III' => 3,
|
||||||
currentMp: currentMp,
|
'IV' => 4,
|
||||||
skillSystem: skillSystem,
|
'V' => 5,
|
||||||
) ==
|
_ => 1,
|
||||||
null) {
|
};
|
||||||
return skill;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MP가 충분하면 공격 스킬
|
|
||||||
if (currentMp >= 30) {
|
|
||||||
for (final skill in skills) {
|
|
||||||
if (skill.type == SkillType.attack &&
|
|
||||||
_skillService.canUseSkill(
|
|
||||||
skill: skill,
|
|
||||||
currentMp: currentMp,
|
|
||||||
skillSystem: skillSystem,
|
|
||||||
) ==
|
|
||||||
null) {
|
|
||||||
return skill;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null; // 스킬 사용 안 함 (기본 공격)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -223,8 +196,40 @@ class ArenaService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 시뮬레이션 결과를 기반으로 전투 결과 생성
|
||||||
|
///
|
||||||
|
/// [match] 대전 정보
|
||||||
|
/// [challengerHp] 도전자 최종 HP
|
||||||
|
/// [opponentHp] 상대 최종 HP
|
||||||
|
/// [turns] 총 턴 수
|
||||||
|
/// Returns: 대전 결과 (승패, 장비 교환 후 캐릭터)
|
||||||
|
ArenaMatchResult createResultFromSimulation({
|
||||||
|
required ArenaMatch match,
|
||||||
|
required int challengerHp,
|
||||||
|
required int opponentHp,
|
||||||
|
required int turns,
|
||||||
|
}) {
|
||||||
|
// 도전자 HP가 0보다 크면 승리
|
||||||
|
final isVictory = challengerHp > 0 && opponentHp <= 0;
|
||||||
|
|
||||||
|
// 장비 교환
|
||||||
|
final (updatedChallenger, updatedOpponent) = _exchangeEquipment(
|
||||||
|
match: match,
|
||||||
|
isVictory: isVictory,
|
||||||
|
);
|
||||||
|
|
||||||
|
return ArenaMatchResult(
|
||||||
|
match: match,
|
||||||
|
isVictory: isVictory,
|
||||||
|
turns: turns,
|
||||||
|
updatedChallenger: updatedChallenger,
|
||||||
|
updatedOpponent: updatedOpponent,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// 전투 시뮬레이션 (애니메이션용 스트림)
|
/// 전투 시뮬레이션 (애니메이션용 스트림)
|
||||||
///
|
///
|
||||||
|
/// progress_service._processCombatTickWithSkills()와 동일한 로직 사용
|
||||||
/// [match] 대전 정보
|
/// [match] 대전 정보
|
||||||
/// Returns: 턴별 전투 상황 스트림
|
/// Returns: 턴별 전투 상황 스트림
|
||||||
Stream<ArenaCombatTurn> simulateCombat(ArenaMatch match) async* {
|
Stream<ArenaCombatTurn> simulateCombat(ArenaMatch match) async* {
|
||||||
@@ -237,30 +242,48 @@ class ArenaService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 스킬 목록 로드
|
// 스킬 ID 목록 로드 (SkillBook과 동일한 방식)
|
||||||
final challengerSkills = _getSkillsFromEntry(match.challenger);
|
var challengerSkillIds = _getSkillIdsFromEntry(match.challenger);
|
||||||
final opponentSkills = _getSkillsFromEntry(match.opponent);
|
var opponentSkillIds = _getSkillIdsFromEntry(match.opponent);
|
||||||
|
|
||||||
|
// 스킬이 없으면 기본 스킬 사용
|
||||||
|
if (challengerSkillIds.isEmpty) {
|
||||||
|
challengerSkillIds = SkillData.defaultSkillIds;
|
||||||
|
}
|
||||||
|
if (opponentSkillIds.isEmpty) {
|
||||||
|
opponentSkillIds = SkillData.defaultSkillIds;
|
||||||
|
}
|
||||||
|
|
||||||
// 스킬 시스템 상태 초기화
|
// 스킬 시스템 상태 초기화
|
||||||
var challengerSkillSystem = SkillSystemState.empty();
|
var challengerSkillSystem = SkillSystemState.empty();
|
||||||
var opponentSkillSystem = SkillSystemState.empty();
|
var opponentSkillSystem = SkillSystemState.empty();
|
||||||
|
|
||||||
|
// DOT 및 디버프 추적 (일반 전투와 동일)
|
||||||
|
var challengerDoTs = <DotEffect>[];
|
||||||
|
var opponentDoTs = <DotEffect>[];
|
||||||
|
var challengerDebuffs = <ActiveBuff>[];
|
||||||
|
var opponentDebuffs = <ActiveBuff>[];
|
||||||
|
|
||||||
var playerCombatStats = challengerStats.copyWith(
|
var playerCombatStats = challengerStats.copyWith(
|
||||||
hpCurrent: challengerStats.hpMax,
|
hpCurrent: challengerStats.hpMax,
|
||||||
mpCurrent: challengerStats.mpMax,
|
mpCurrent: challengerStats.mpMax,
|
||||||
);
|
);
|
||||||
|
|
||||||
// 상대도 CombatStats로 관리 (스킬 사용 위해)
|
|
||||||
var opponentCombatStats = opponentStats.copyWith(
|
var opponentCombatStats = opponentStats.copyWith(
|
||||||
hpCurrent: opponentStats.hpMax,
|
hpCurrent: opponentStats.hpMax,
|
||||||
mpCurrent: opponentStats.mpMax,
|
mpCurrent: opponentStats.mpMax,
|
||||||
);
|
);
|
||||||
|
|
||||||
var opponentMonsterStats = MonsterCombatStats.fromCombatStats(
|
var opponentMonsterStats = MonsterCombatStats.fromCombatStats(
|
||||||
opponentStats,
|
opponentCombatStats,
|
||||||
match.opponent.characterName,
|
match.opponent.characterName,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
var challengerMonsterStats = MonsterCombatStats.fromCombatStats(
|
||||||
|
playerCombatStats,
|
||||||
|
match.challenger.characterName,
|
||||||
|
);
|
||||||
|
|
||||||
int playerAccum = 0;
|
int playerAccum = 0;
|
||||||
int opponentAccum = 0;
|
int opponentAccum = 0;
|
||||||
int elapsedMs = 0;
|
int elapsedMs = 0;
|
||||||
@@ -301,47 +324,164 @@ class ArenaService {
|
|||||||
int? challengerHealAmount;
|
int? challengerHealAmount;
|
||||||
int? opponentHealAmount;
|
int? opponentHealAmount;
|
||||||
|
|
||||||
// 도전자 턴
|
// =========================================================================
|
||||||
|
// DOT 틱 처리 (도전자 → 상대에게 적용된 DOT)
|
||||||
|
// =========================================================================
|
||||||
|
var dotDamageToOpponent = 0;
|
||||||
|
final updatedChallengerDoTs = <DotEffect>[];
|
||||||
|
for (final dot in challengerDoTs) {
|
||||||
|
final (updatedDot, ticksTriggered) = dot.tick(tickMs);
|
||||||
|
if (ticksTriggered > 0) {
|
||||||
|
dotDamageToOpponent += dot.damagePerTick * ticksTriggered;
|
||||||
|
}
|
||||||
|
if (updatedDot.isActive) {
|
||||||
|
updatedChallengerDoTs.add(updatedDot);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
challengerDoTs = updatedChallengerDoTs;
|
||||||
|
|
||||||
|
if (dotDamageToOpponent > 0 && opponentCombatStats.hpCurrent > 0) {
|
||||||
|
opponentCombatStats = opponentCombatStats.copyWith(
|
||||||
|
hpCurrent: (opponentCombatStats.hpCurrent - dotDamageToOpponent)
|
||||||
|
.clamp(0, opponentCombatStats.hpMax),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// DOT 틱 처리 (상대 → 도전자에게 적용된 DOT)
|
||||||
|
var dotDamageToChallenger = 0;
|
||||||
|
final updatedOpponentDoTs = <DotEffect>[];
|
||||||
|
for (final dot in opponentDoTs) {
|
||||||
|
final (updatedDot, ticksTriggered) = dot.tick(tickMs);
|
||||||
|
if (ticksTriggered > 0) {
|
||||||
|
dotDamageToChallenger += dot.damagePerTick * ticksTriggered;
|
||||||
|
}
|
||||||
|
if (updatedDot.isActive) {
|
||||||
|
updatedOpponentDoTs.add(updatedDot);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
opponentDoTs = updatedOpponentDoTs;
|
||||||
|
|
||||||
|
if (dotDamageToChallenger > 0 && playerCombatStats.isAlive) {
|
||||||
|
playerCombatStats = playerCombatStats.copyWith(
|
||||||
|
hpCurrent: (playerCombatStats.hpCurrent - dotDamageToChallenger)
|
||||||
|
.clamp(0, playerCombatStats.hpMax),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// 만료된 디버프 정리
|
||||||
|
// =========================================================================
|
||||||
|
challengerDebuffs = challengerDebuffs
|
||||||
|
.where((ActiveBuff d) => !d.isExpired(elapsedMs))
|
||||||
|
.toList();
|
||||||
|
opponentDebuffs = opponentDebuffs
|
||||||
|
.where((ActiveBuff d) => !d.isExpired(elapsedMs))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// 도전자 턴 (selectAutoSkill 사용 - 일반 전투와 동일)
|
||||||
|
// =========================================================================
|
||||||
if (playerAccum >= playerCombatStats.attackDelayMs) {
|
if (playerAccum >= playerCombatStats.attackDelayMs) {
|
||||||
playerAccum = 0;
|
playerAccum = 0;
|
||||||
|
|
||||||
// 스킬 선택
|
// 상대 몬스터 스탯 동기화
|
||||||
final skill = _selectBestSkill(
|
opponentMonsterStats = MonsterCombatStats.fromCombatStats(
|
||||||
skills: challengerSkills,
|
opponentCombatStats,
|
||||||
stats: playerCombatStats,
|
match.opponent.characterName,
|
||||||
skillSystem: challengerSkillSystem,
|
|
||||||
target: opponentMonsterStats,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (skill != null) {
|
// 스킬 자동 선택 (progress_service와 동일한 로직)
|
||||||
// 스킬 사용
|
final selectedSkill = _skillService.selectAutoSkill(
|
||||||
final skillResult = _useSkill(
|
player: playerCombatStats,
|
||||||
skill: skill,
|
monster: opponentMonsterStats,
|
||||||
attacker: playerCombatStats,
|
skillSystem: challengerSkillSystem,
|
||||||
defender: opponentCombatStats,
|
availableSkillIds: challengerSkillIds,
|
||||||
|
activeDoTs: challengerDoTs,
|
||||||
|
activeDebuffs: opponentDebuffs,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (selectedSkill != null && selectedSkill.isAttack) {
|
||||||
|
// 스킬 랭크 조회 및 적용
|
||||||
|
final skillRank = _getSkillRankFromEntry(
|
||||||
|
match.challenger,
|
||||||
|
selectedSkill.id,
|
||||||
|
);
|
||||||
|
final skillResult = _skillService.useAttackSkillWithRank(
|
||||||
|
skill: selectedSkill,
|
||||||
|
player: playerCombatStats,
|
||||||
|
monster: opponentMonsterStats,
|
||||||
|
skillSystem: challengerSkillSystem,
|
||||||
|
rank: skillRank,
|
||||||
|
);
|
||||||
|
playerCombatStats = skillResult.updatedPlayer;
|
||||||
|
opponentCombatStats = opponentCombatStats.copyWith(
|
||||||
|
hpCurrent: skillResult.updatedMonster.hpCurrent,
|
||||||
|
);
|
||||||
|
challengerSkillSystem = skillResult.updatedSkillSystem;
|
||||||
|
challengerSkillUsed = selectedSkill.name;
|
||||||
|
challengerDamage = skillResult.result.damage;
|
||||||
|
} else if (selectedSkill != null && selectedSkill.isDot) {
|
||||||
|
// DOT 스킬 사용
|
||||||
|
final skillResult = _skillService.useDotSkill(
|
||||||
|
skill: selectedSkill,
|
||||||
|
player: playerCombatStats,
|
||||||
|
skillSystem: challengerSkillSystem,
|
||||||
|
playerInt: playerCombatStats.atk ~/ 10,
|
||||||
|
playerWis: playerCombatStats.def ~/ 10,
|
||||||
|
);
|
||||||
|
playerCombatStats = skillResult.updatedPlayer;
|
||||||
|
challengerSkillSystem = skillResult.updatedSkillSystem;
|
||||||
|
if (skillResult.dotEffect != null) {
|
||||||
|
challengerDoTs.add(skillResult.dotEffect!);
|
||||||
|
}
|
||||||
|
challengerSkillUsed = selectedSkill.name;
|
||||||
|
} else if (selectedSkill != null && selectedSkill.isHeal) {
|
||||||
|
// 회복 스킬 사용
|
||||||
|
final skillResult = _skillService.useHealSkill(
|
||||||
|
skill: selectedSkill,
|
||||||
|
player: playerCombatStats,
|
||||||
skillSystem: challengerSkillSystem,
|
skillSystem: challengerSkillSystem,
|
||||||
);
|
);
|
||||||
playerCombatStats = skillResult.updatedAttacker;
|
playerCombatStats = skillResult.updatedPlayer;
|
||||||
opponentCombatStats = skillResult.updatedDefender;
|
|
||||||
challengerSkillSystem = skillResult.updatedSkillSystem;
|
challengerSkillSystem = skillResult.updatedSkillSystem;
|
||||||
challengerSkillUsed = skill.name;
|
challengerSkillUsed = selectedSkill.name;
|
||||||
challengerDamage = skillResult.damage;
|
challengerHealAmount = skillResult.result.healedAmount;
|
||||||
challengerHealAmount = skillResult.healAmount;
|
} else if (selectedSkill != null && selectedSkill.isBuff) {
|
||||||
isChallengerCritical = skillResult.isCritical;
|
// 버프 스킬 사용
|
||||||
} else {
|
final skillResult = _skillService.useBuffSkill(
|
||||||
// 기본 공격
|
skill: selectedSkill,
|
||||||
// 상대 몬스터 스탯 동기화
|
player: playerCombatStats,
|
||||||
opponentMonsterStats = MonsterCombatStats.fromCombatStats(
|
skillSystem: challengerSkillSystem,
|
||||||
opponentCombatStats,
|
|
||||||
match.opponent.characterName,
|
|
||||||
);
|
);
|
||||||
|
playerCombatStats = skillResult.updatedPlayer;
|
||||||
|
challengerSkillSystem = skillResult.updatedSkillSystem;
|
||||||
|
challengerSkillUsed = selectedSkill.name;
|
||||||
|
} else if (selectedSkill != null && selectedSkill.isDebuff) {
|
||||||
|
// 디버프 스킬 사용
|
||||||
|
final skillResult = _skillService.useDebuffSkill(
|
||||||
|
skill: selectedSkill,
|
||||||
|
player: playerCombatStats,
|
||||||
|
skillSystem: challengerSkillSystem,
|
||||||
|
currentDebuffs: opponentDebuffs,
|
||||||
|
);
|
||||||
|
playerCombatStats = skillResult.updatedPlayer;
|
||||||
|
challengerSkillSystem = skillResult.updatedSkillSystem;
|
||||||
|
final debuffEffect = skillResult.debuffEffect;
|
||||||
|
if (debuffEffect != null) {
|
||||||
|
opponentDebuffs = opponentDebuffs
|
||||||
|
.where((ActiveBuff d) => d.effect.id != debuffEffect.effect.id)
|
||||||
|
.toList()
|
||||||
|
..add(debuffEffect);
|
||||||
|
}
|
||||||
|
challengerSkillUsed = selectedSkill.name;
|
||||||
|
} else {
|
||||||
|
// 일반 공격
|
||||||
final result = calculator.playerAttackMonster(
|
final result = calculator.playerAttackMonster(
|
||||||
attacker: playerCombatStats,
|
attacker: playerCombatStats,
|
||||||
defender: opponentMonsterStats,
|
defender: opponentMonsterStats,
|
||||||
);
|
);
|
||||||
opponentMonsterStats = result.updatedDefender;
|
|
||||||
opponentCombatStats = opponentCombatStats.copyWith(
|
opponentCombatStats = opponentCombatStats.copyWith(
|
||||||
hpCurrent: opponentMonsterStats.hpCurrent,
|
hpCurrent: result.updatedDefender.hpCurrent,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (result.result.isHit) {
|
if (result.result.isHit) {
|
||||||
@@ -353,38 +493,121 @@ class ArenaService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 상대 턴
|
// =========================================================================
|
||||||
|
// 상대 턴 (selectAutoSkill 사용 - 일반 전투와 동일)
|
||||||
|
// =========================================================================
|
||||||
if (opponentCombatStats.hpCurrent > 0 &&
|
if (opponentCombatStats.hpCurrent > 0 &&
|
||||||
opponentAccum >= opponentCombatStats.attackDelayMs) {
|
opponentAccum >= opponentCombatStats.attackDelayMs) {
|
||||||
opponentAccum = 0;
|
opponentAccum = 0;
|
||||||
|
|
||||||
// 상대 스킬 선택
|
// 도전자 몬스터 스탯 동기화
|
||||||
final skill = _selectBestSkill(
|
challengerMonsterStats = MonsterCombatStats.fromCombatStats(
|
||||||
skills: opponentSkills,
|
playerCombatStats,
|
||||||
stats: opponentCombatStats,
|
match.challenger.characterName,
|
||||||
skillSystem: opponentSkillSystem,
|
|
||||||
target: null,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (skill != null) {
|
// 스킬 자동 선택 (progress_service와 동일한 로직)
|
||||||
// 스킬 사용
|
final selectedSkill = _skillService.selectAutoSkill(
|
||||||
final skillResult = _useSkill(
|
player: opponentCombatStats,
|
||||||
skill: skill,
|
monster: challengerMonsterStats,
|
||||||
attacker: opponentCombatStats,
|
skillSystem: opponentSkillSystem,
|
||||||
defender: playerCombatStats,
|
availableSkillIds: opponentSkillIds,
|
||||||
|
activeDoTs: opponentDoTs,
|
||||||
|
activeDebuffs: challengerDebuffs,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (selectedSkill != null && selectedSkill.isAttack) {
|
||||||
|
// 스킬 랭크 조회 및 적용
|
||||||
|
final skillRank = _getSkillRankFromEntry(
|
||||||
|
match.opponent,
|
||||||
|
selectedSkill.id,
|
||||||
|
);
|
||||||
|
final skillResult = _skillService.useAttackSkillWithRank(
|
||||||
|
skill: selectedSkill,
|
||||||
|
player: opponentCombatStats,
|
||||||
|
monster: challengerMonsterStats,
|
||||||
|
skillSystem: opponentSkillSystem,
|
||||||
|
rank: skillRank,
|
||||||
|
);
|
||||||
|
opponentCombatStats = skillResult.updatedPlayer;
|
||||||
|
playerCombatStats = playerCombatStats.copyWith(
|
||||||
|
hpCurrent: skillResult.updatedMonster.hpCurrent,
|
||||||
|
);
|
||||||
|
opponentSkillSystem = skillResult.updatedSkillSystem;
|
||||||
|
opponentSkillUsed = selectedSkill.name;
|
||||||
|
opponentDamage = skillResult.result.damage;
|
||||||
|
} else if (selectedSkill != null && selectedSkill.isDot) {
|
||||||
|
// DOT 스킬 사용
|
||||||
|
final skillResult = _skillService.useDotSkill(
|
||||||
|
skill: selectedSkill,
|
||||||
|
player: opponentCombatStats,
|
||||||
|
skillSystem: opponentSkillSystem,
|
||||||
|
playerInt: opponentCombatStats.atk ~/ 10,
|
||||||
|
playerWis: opponentCombatStats.def ~/ 10,
|
||||||
|
);
|
||||||
|
opponentCombatStats = skillResult.updatedPlayer;
|
||||||
|
opponentSkillSystem = skillResult.updatedSkillSystem;
|
||||||
|
if (skillResult.dotEffect != null) {
|
||||||
|
opponentDoTs.add(skillResult.dotEffect!);
|
||||||
|
}
|
||||||
|
opponentSkillUsed = selectedSkill.name;
|
||||||
|
} else if (selectedSkill != null && selectedSkill.isHeal) {
|
||||||
|
// 회복 스킬 사용
|
||||||
|
final skillResult = _skillService.useHealSkill(
|
||||||
|
skill: selectedSkill,
|
||||||
|
player: opponentCombatStats,
|
||||||
skillSystem: opponentSkillSystem,
|
skillSystem: opponentSkillSystem,
|
||||||
);
|
);
|
||||||
opponentCombatStats = skillResult.updatedAttacker;
|
opponentCombatStats = skillResult.updatedPlayer;
|
||||||
playerCombatStats = skillResult.updatedDefender;
|
|
||||||
opponentSkillSystem = skillResult.updatedSkillSystem;
|
opponentSkillSystem = skillResult.updatedSkillSystem;
|
||||||
opponentSkillUsed = skill.name;
|
opponentSkillUsed = selectedSkill.name;
|
||||||
opponentDamage = skillResult.damage;
|
opponentHealAmount = skillResult.result.healedAmount;
|
||||||
opponentHealAmount = skillResult.healAmount;
|
} else if (selectedSkill != null && selectedSkill.isBuff) {
|
||||||
isOpponentCritical = skillResult.isCritical;
|
// 버프 스킬 사용
|
||||||
|
final skillResult = _skillService.useBuffSkill(
|
||||||
|
skill: selectedSkill,
|
||||||
|
player: opponentCombatStats,
|
||||||
|
skillSystem: opponentSkillSystem,
|
||||||
|
);
|
||||||
|
opponentCombatStats = skillResult.updatedPlayer;
|
||||||
|
opponentSkillSystem = skillResult.updatedSkillSystem;
|
||||||
|
opponentSkillUsed = selectedSkill.name;
|
||||||
|
} else if (selectedSkill != null && selectedSkill.isDebuff) {
|
||||||
|
// 디버프 스킬 사용
|
||||||
|
final skillResult = _skillService.useDebuffSkill(
|
||||||
|
skill: selectedSkill,
|
||||||
|
player: opponentCombatStats,
|
||||||
|
skillSystem: opponentSkillSystem,
|
||||||
|
currentDebuffs: challengerDebuffs,
|
||||||
|
);
|
||||||
|
opponentCombatStats = skillResult.updatedPlayer;
|
||||||
|
opponentSkillSystem = skillResult.updatedSkillSystem;
|
||||||
|
final debuffEffect = skillResult.debuffEffect;
|
||||||
|
if (debuffEffect != null) {
|
||||||
|
challengerDebuffs = challengerDebuffs
|
||||||
|
.where((ActiveBuff d) => d.effect.id != debuffEffect.effect.id)
|
||||||
|
.toList()
|
||||||
|
..add(debuffEffect);
|
||||||
|
}
|
||||||
|
opponentSkillUsed = selectedSkill.name;
|
||||||
} else {
|
} else {
|
||||||
// 기본 공격 (몬스터 형태로)
|
// 일반 공격 (디버프 효과 적용)
|
||||||
|
var debuffedOpponent = opponentCombatStats;
|
||||||
|
if (challengerDebuffs.isNotEmpty) {
|
||||||
|
double atkMod = 0;
|
||||||
|
for (final debuff in challengerDebuffs) {
|
||||||
|
if (!debuff.isExpired(elapsedMs)) {
|
||||||
|
atkMod += debuff.effect.atkModifier;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final newAtk = (opponentCombatStats.atk * (1 + atkMod))
|
||||||
|
.round()
|
||||||
|
.clamp(opponentCombatStats.atk ~/ 10, opponentCombatStats.atk);
|
||||||
|
debuffedOpponent = opponentCombatStats.copyWith(atk: newAtk);
|
||||||
|
}
|
||||||
|
|
||||||
opponentMonsterStats = MonsterCombatStats.fromCombatStats(
|
opponentMonsterStats = MonsterCombatStats.fromCombatStats(
|
||||||
opponentCombatStats,
|
debuffedOpponent,
|
||||||
match.opponent.characterName,
|
match.opponent.characterName,
|
||||||
);
|
);
|
||||||
final result = calculator.monsterAttackPlayer(
|
final result = calculator.monsterAttackPlayer(
|
||||||
@@ -444,84 +667,6 @@ class ArenaService {
|
|||||||
if (turns > 1000) break;
|
if (turns > 1000) break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 스킬 사용 (내부 헬퍼)
|
|
||||||
({
|
|
||||||
CombatStats updatedAttacker,
|
|
||||||
CombatStats updatedDefender,
|
|
||||||
SkillSystemState updatedSkillSystem,
|
|
||||||
int? damage,
|
|
||||||
int? healAmount,
|
|
||||||
bool isCritical,
|
|
||||||
}) _useSkill({
|
|
||||||
required Skill skill,
|
|
||||||
required CombatStats attacker,
|
|
||||||
required CombatStats defender,
|
|
||||||
required SkillSystemState skillSystem,
|
|
||||||
}) {
|
|
||||||
int? damage;
|
|
||||||
int? healAmount;
|
|
||||||
var updatedAttacker = attacker;
|
|
||||||
var updatedDefender = defender;
|
|
||||||
var updatedSkillSystem = skillSystem;
|
|
||||||
|
|
||||||
switch (skill.type) {
|
|
||||||
case SkillType.attack:
|
|
||||||
final monsterStats = MonsterCombatStats.fromCombatStats(defender, '');
|
|
||||||
final result = _skillService.useAttackSkill(
|
|
||||||
skill: skill,
|
|
||||||
player: attacker,
|
|
||||||
monster: monsterStats,
|
|
||||||
skillSystem: skillSystem,
|
|
||||||
);
|
|
||||||
updatedAttacker = result.updatedPlayer;
|
|
||||||
updatedDefender = defender.copyWith(
|
|
||||||
hpCurrent: result.updatedMonster.hpCurrent,
|
|
||||||
);
|
|
||||||
updatedSkillSystem = result.updatedSkillSystem;
|
|
||||||
damage = result.result.damage;
|
|
||||||
|
|
||||||
case SkillType.heal:
|
|
||||||
final result = _skillService.useHealSkill(
|
|
||||||
skill: skill,
|
|
||||||
player: attacker,
|
|
||||||
skillSystem: skillSystem,
|
|
||||||
);
|
|
||||||
updatedAttacker = result.updatedPlayer;
|
|
||||||
updatedSkillSystem = result.updatedSkillSystem;
|
|
||||||
healAmount = result.result.healedAmount;
|
|
||||||
|
|
||||||
case SkillType.buff:
|
|
||||||
final result = _skillService.useBuffSkill(
|
|
||||||
skill: skill,
|
|
||||||
player: attacker,
|
|
||||||
skillSystem: skillSystem,
|
|
||||||
);
|
|
||||||
updatedAttacker = result.updatedPlayer;
|
|
||||||
updatedSkillSystem = result.updatedSkillSystem;
|
|
||||||
|
|
||||||
case SkillType.debuff:
|
|
||||||
// 디버프 스킬 사용
|
|
||||||
final debuffResult = _skillService.useDebuffSkill(
|
|
||||||
skill: skill,
|
|
||||||
player: attacker,
|
|
||||||
skillSystem: skillSystem,
|
|
||||||
currentDebuffs: [],
|
|
||||||
);
|
|
||||||
updatedAttacker = debuffResult.updatedPlayer;
|
|
||||||
updatedSkillSystem = debuffResult.updatedSkillSystem;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
updatedAttacker: updatedAttacker,
|
|
||||||
updatedDefender: updatedDefender,
|
|
||||||
updatedSkillSystem: updatedSkillSystem,
|
|
||||||
damage: damage,
|
|
||||||
healAmount: healAmount,
|
|
||||||
isCritical: false, // 스킬 크리티컬은 별도 처리 필요
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// 장비 교환
|
// 장비 교환
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import 'package:asciineverdie/src/core/model/combat_event.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/hall_of_fame.dart';
|
import 'package:asciineverdie/src/core/model/hall_of_fame.dart';
|
||||||
import 'package:asciineverdie/src/core/animation/race_character_frames.dart';
|
import 'package:asciineverdie/src/core/animation/race_character_frames.dart';
|
||||||
|
import 'package:asciineverdie/src/features/arena/widgets/arena_combat_log.dart';
|
||||||
import 'package:asciineverdie/src/features/arena/widgets/arena_result_panel.dart';
|
import 'package:asciineverdie/src/features/arena/widgets/arena_result_panel.dart';
|
||||||
import 'package:asciineverdie/src/features/arena/widgets/ascii_disintegrate_widget.dart';
|
import 'package:asciineverdie/src/features/arena/widgets/ascii_disintegrate_widget.dart';
|
||||||
import 'package:asciineverdie/src/features/game/widgets/ascii_animation_card.dart';
|
import 'package:asciineverdie/src/features/game/widgets/ascii_animation_card.dart';
|
||||||
@@ -376,8 +377,13 @@ class _ArenaBattleScreenState extends State<ArenaBattleScreen>
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _endBattle() {
|
void _endBattle() {
|
||||||
// 최종 결과 계산
|
// 시뮬레이션 HP 결과를 기반으로 최종 결과 계산
|
||||||
_result = _arenaService.executeCombat(widget.match);
|
_result = _arenaService.createResultFromSimulation(
|
||||||
|
match: widget.match,
|
||||||
|
challengerHp: _challengerHp,
|
||||||
|
opponentHp: _opponentHp,
|
||||||
|
turns: _currentTurn,
|
||||||
|
);
|
||||||
|
|
||||||
// 전투 종료 상태로 전환 (인라인 결과 패널 표시)
|
// 전투 종료 상태로 전환 (인라인 결과 패널 표시)
|
||||||
setState(() {
|
setState(() {
|
||||||
@@ -836,7 +842,7 @@ class _ArenaBattleScreenState extends State<ArenaBattleScreen>
|
|||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
border: Border.all(color: RetroColors.borderOf(context)),
|
border: Border.all(color: RetroColors.borderOf(context)),
|
||||||
),
|
),
|
||||||
child: CombatLog(entries: _battleLog),
|
child: ArenaCombatLog(entries: _battleLog),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
199
lib/src/features/arena/widgets/arena_combat_log.dart
Normal file
199
lib/src/features/arena/widgets/arena_combat_log.dart
Normal file
@@ -0,0 +1,199 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:asciineverdie/src/features/game/widgets/combat_log.dart';
|
||||||
|
|
||||||
|
/// 아레나 전용 전투 로그 위젯
|
||||||
|
///
|
||||||
|
/// 일반 CombatLog와 다른 점:
|
||||||
|
/// - 최신 메시지가 상단에 표시 (reverse order)
|
||||||
|
/// - 사용자 조작 시 자동 스크롤 중지
|
||||||
|
/// - 5초 미조작 시 자동 스크롤 재개
|
||||||
|
class ArenaCombatLog extends StatefulWidget {
|
||||||
|
const ArenaCombatLog({
|
||||||
|
super.key,
|
||||||
|
required this.entries,
|
||||||
|
this.maxEntries = 50,
|
||||||
|
});
|
||||||
|
|
||||||
|
final List<CombatLogEntry> entries;
|
||||||
|
final int maxEntries;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ArenaCombatLog> createState() => _ArenaCombatLogState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ArenaCombatLogState extends State<ArenaCombatLog> {
|
||||||
|
final ScrollController _scrollController = ScrollController();
|
||||||
|
|
||||||
|
/// 자동 스크롤 활성화 여부
|
||||||
|
bool _autoScrollEnabled = true;
|
||||||
|
|
||||||
|
/// 사용자 조작 후 자동 스크롤 재개 타이머
|
||||||
|
Timer? _resumeAutoScrollTimer;
|
||||||
|
|
||||||
|
/// 자동 스크롤 재개까지 대기 시간 (5초)
|
||||||
|
static const _autoScrollResumeDelay = Duration(seconds: 5);
|
||||||
|
|
||||||
|
int _previousLength = 0;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(ArenaCombatLog oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
|
||||||
|
// 새 로그 추가 시 자동 스크롤 (활성화된 경우에만)
|
||||||
|
if (widget.entries.length > _previousLength && _autoScrollEnabled) {
|
||||||
|
_scrollToTop();
|
||||||
|
}
|
||||||
|
_previousLength = widget.entries.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 최상단으로 스크롤 (최신 메시지가 상단이므로 position 0)
|
||||||
|
void _scrollToTop() {
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
if (_scrollController.hasClients) {
|
||||||
|
_scrollController.animateTo(
|
||||||
|
0,
|
||||||
|
duration: const Duration(milliseconds: 200),
|
||||||
|
curve: Curves.easeOut,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 사용자 스크롤 감지 시 호출
|
||||||
|
void _onUserScroll() {
|
||||||
|
// 자동 스크롤 비활성화
|
||||||
|
setState(() {
|
||||||
|
_autoScrollEnabled = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 기존 타이머 취소
|
||||||
|
_resumeAutoScrollTimer?.cancel();
|
||||||
|
|
||||||
|
// 5초 후 자동 스크롤 재활성화
|
||||||
|
_resumeAutoScrollTimer = Timer(_autoScrollResumeDelay, () {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_autoScrollEnabled = true;
|
||||||
|
});
|
||||||
|
// 재활성화 시 최상단으로 스크롤
|
||||||
|
_scrollToTop();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_resumeAutoScrollTimer?.cancel();
|
||||||
|
_scrollController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
// 최신 메시지가 상단에 오도록 역순 리스트 생성
|
||||||
|
final reversedEntries = widget.entries.reversed.toList();
|
||||||
|
|
||||||
|
return NotificationListener<ScrollNotification>(
|
||||||
|
onNotification: (notification) {
|
||||||
|
// 사용자가 직접 스크롤할 때만 감지 (UserScrollNotification)
|
||||||
|
if (notification is UserScrollNotification) {
|
||||||
|
_onUserScroll();
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
child: ListView.builder(
|
||||||
|
controller: _scrollController,
|
||||||
|
itemCount: reversedEntries.length,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final entry = reversedEntries[index];
|
||||||
|
return _ArenaLogEntryTile(entry: entry);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 개별 로그 엔트리 타일 (아레나용)
|
||||||
|
class _ArenaLogEntryTile extends StatelessWidget {
|
||||||
|
const _ArenaLogEntryTile({required this.entry});
|
||||||
|
|
||||||
|
final CombatLogEntry entry;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final (color, icon) = _getStyleForType(entry.type);
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 1),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// 타임스탬프(timestamp)
|
||||||
|
Text(
|
||||||
|
_formatTime(entry.timestamp),
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 10,
|
||||||
|
color: Theme.of(context).colorScheme.outline,
|
||||||
|
fontFamily: 'JetBrainsMono',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
// 아이콘
|
||||||
|
if (icon != null)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(right: 4),
|
||||||
|
child: Icon(icon, size: 12, color: color),
|
||||||
|
),
|
||||||
|
// 메시지
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
entry.message,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 11,
|
||||||
|
color: color ?? Theme.of(context).colorScheme.onSurface,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _formatTime(DateTime time) {
|
||||||
|
return '${time.hour.toString().padLeft(2, '0')}:'
|
||||||
|
'${time.minute.toString().padLeft(2, '0')}:'
|
||||||
|
'${time.second.toString().padLeft(2, '0')}';
|
||||||
|
}
|
||||||
|
|
||||||
|
(Color?, IconData?) _getStyleForType(CombatLogType type) {
|
||||||
|
return switch (type) {
|
||||||
|
CombatLogType.normal => (null, null),
|
||||||
|
CombatLogType.damage => (
|
||||||
|
Colors.red.shade300,
|
||||||
|
Icons.local_fire_department,
|
||||||
|
),
|
||||||
|
CombatLogType.heal => (Colors.green.shade300, Icons.healing),
|
||||||
|
CombatLogType.levelUp => (Colors.amber, Icons.arrow_upward),
|
||||||
|
CombatLogType.questComplete => (Colors.blue.shade300, Icons.check_circle),
|
||||||
|
CombatLogType.loot => (Colors.orange.shade300, Icons.inventory_2),
|
||||||
|
CombatLogType.skill => (Colors.purple.shade300, Icons.auto_fix_high),
|
||||||
|
CombatLogType.critical => (Colors.yellow.shade300, Icons.flash_on),
|
||||||
|
CombatLogType.evade => (Colors.cyan.shade300, Icons.directions_run),
|
||||||
|
CombatLogType.block => (Colors.blueGrey.shade300, Icons.shield),
|
||||||
|
CombatLogType.parry => (Colors.teal.shade300, Icons.sports_kabaddi),
|
||||||
|
CombatLogType.monsterAttack => (
|
||||||
|
Colors.deepOrange.shade300,
|
||||||
|
Icons.dangerous,
|
||||||
|
),
|
||||||
|
CombatLogType.buff => (Colors.lightBlue.shade300, Icons.trending_up),
|
||||||
|
CombatLogType.debuff => (Colors.deepOrange.shade300, Icons.trending_down),
|
||||||
|
CombatLogType.dotTick => (Colors.deepPurple.shade300, Icons.whatshot),
|
||||||
|
CombatLogType.potion => (Colors.pink.shade300, Icons.local_drink),
|
||||||
|
CombatLogType.potionDrop => (Colors.lime.shade300, Icons.card_giftcard),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user