import 'package:asciineverdie/data/skill_data.dart'; import 'package:asciineverdie/src/core/engine/skill_service.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/monster_combat_stats.dart'; import 'package:asciineverdie/src/core/model/skill.dart'; import 'package:asciineverdie/src/core/util/deterministic_random.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { group('SkillService', () { group('canUseSkill', () { test('GCD 활성화 시 onGlobalCooldown 반환', () { final rng = DeterministicRandom(42); final service = SkillService(rng: rng); const skill = SkillData.stackTrace; final skillSystem = SkillSystemState.empty().copyWith( elapsedMs: 1000, globalCooldownEndMs: 2500, // GCD 활성화 중 ); final result = service.canUseSkill( skill: skill, currentMp: 100, skillSystem: skillSystem, ); expect(result, equals(SkillFailReason.onGlobalCooldown)); }); test('MP 부족 시 notEnoughMp 반환', () { final rng = DeterministicRandom(42); final service = SkillService(rng: rng); const skill = SkillData.stackTrace; // MP 10 필요 final skillSystem = SkillSystemState.empty().copyWith(elapsedMs: 5000); final result = service.canUseSkill( skill: skill, currentMp: 5, // MP 부족 skillSystem: skillSystem, ); expect(result, equals(SkillFailReason.notEnoughMp)); }); test('쿨타임 중 onCooldown 반환', () { final rng = DeterministicRandom(42); final service = SkillService(rng: rng); const skill = SkillData.stackTrace; // 쿨타임 3000ms final skillSystem = SkillSystemState.empty().copyWith( elapsedMs: 2000, skillStates: [ const SkillState( skillId: 'stack_trace', lastUsedMs: 1000, // 1초 전 사용 rank: 1, ), ], ); final result = service.canUseSkill( skill: skill, currentMp: 100, skillSystem: skillSystem, ); expect(result, equals(SkillFailReason.onCooldown)); }); test('모든 조건 충족 시 null 반환', () { final rng = DeterministicRandom(42); final service = SkillService(rng: rng); const skill = SkillData.stackTrace; final skillSystem = SkillSystemState.empty().copyWith(elapsedMs: 5000); final result = service.canUseSkill( skill: skill, currentMp: 100, skillSystem: skillSystem, ); expect(result, isNull); }); }); group('useAttackSkill', () { test('기본 공격 데미지 계산', () { final rng = DeterministicRandom(42); final service = SkillService(rng: rng); const skill = SkillData.stackTrace; // 2.0x 배율 final player = CombatStats.empty().copyWith( atk: 100, mpCurrent: 50, mpMax: 100, ); final monster = MonsterCombatStats( name: 'Test Monster', level: 1, atk: 10, def: 50, // DEF 50 → 50 * 0.3 = 15 감소 magDef: 50, hpMax: 500, hpCurrent: 500, criRate: 0.05, criDamage: 1.5, evasion: 0.0, accuracy: 0.8, attackDelayMs: 1000, expReward: 100, ); final skillSystem = SkillSystemState.empty().copyWith(elapsedMs: 5000); final result = service.useAttackSkill( skill: skill, player: player, monster: monster, skillSystem: skillSystem, ); // ATK 100 * 2.0 - DEF 50 * 0.3 = 200 - 15 = 185 expect(result.result.success, isTrue); expect(result.result.damage, equals(185)); expect(result.updatedPlayer.mpCurrent, equals(20)); // 50 - 30 (mpCost 30) expect(result.updatedMonster.hpCurrent, equals(315)); // 500 - 185 }); test('버프 적용 시 데미지 증가', () { final rng = DeterministicRandom(42); final service = SkillService(rng: rng); const skill = SkillData.stackTrace; final player = CombatStats.empty().copyWith( atk: 100, mpCurrent: 50, mpMax: 100, ); final monster = MonsterCombatStats( name: 'Test Monster', level: 1, atk: 10, def: 0, magDef: 0, hpMax: 500, hpCurrent: 500, criRate: 0.05, criDamage: 1.5, evasion: 0.0, accuracy: 0.8, attackDelayMs: 1000, expReward: 100, ); final skillSystem = SkillSystemState.empty().copyWith( elapsedMs: 5000, activeBuffs: [ const ActiveBuff( effect: BuffEffect( id: 'test_buff', name: 'Test Buff', durationMs: 10000, atkModifier: 0.5, // +50% ATK ), startedMs: 4000, sourceSkillId: 'test', ), ], ); final result = service.useAttackSkill( skill: skill, player: player, monster: monster, skillSystem: skillSystem, ); // ATK 100 * 2.0 * 1.5 = 300 expect(result.result.damage, equals(300)); }); }); group('useAttackSkillWithRank', () { test('랭크 1 데미지 (기본 배율)', () { final rng = DeterministicRandom(42); final service = SkillService(rng: rng); const skill = SkillData.stackTrace; final player = CombatStats.empty().copyWith( atk: 100, mpCurrent: 50, mpMax: 100, ); final monster = MonsterCombatStats( name: 'Test Monster', level: 1, atk: 10, def: 0, magDef: 0, hpMax: 500, hpCurrent: 500, criRate: 0.05, criDamage: 1.5, evasion: 0.0, accuracy: 0.8, attackDelayMs: 1000, expReward: 100, ); final skillSystem = SkillSystemState.empty().copyWith(elapsedMs: 5000); final result = service.useAttackSkillWithRank( skill: skill, player: player, monster: monster, skillSystem: skillSystem, rank: 1, ); // 랭크 1: 1.0x → ATK 100 * 2.0 * 1.0 = 200 expect(result.result.damage, equals(200)); expect(result.updatedPlayer.mpCurrent, equals(20)); // MP 30 소모 }); test('랭크 5 데미지 스케일링', () { final rng = DeterministicRandom(42); final service = SkillService(rng: rng); const skill = SkillData.stackTrace; final player = CombatStats.empty().copyWith( atk: 100, mpCurrent: 50, mpMax: 100, ); final monster = MonsterCombatStats( name: 'Test Monster', level: 1, atk: 10, def: 0, magDef: 0, hpMax: 500, hpCurrent: 500, criRate: 0.05, criDamage: 1.5, evasion: 0.0, accuracy: 0.8, attackDelayMs: 1000, expReward: 100, ); final skillSystem = SkillSystemState.empty().copyWith(elapsedMs: 5000); final result = service.useAttackSkillWithRank( skill: skill, player: player, monster: monster, skillSystem: skillSystem, rank: 5, ); // 랭크 5: 1.32x (1.0 + 4 * 0.08) - 랭크 배율 하향 // ATK 100 * 2.0 * 1.32 = 264 expect(result.result.damage, equals(264)); // MP 비용: 30 * (1.0 - 4 * 0.03) = 30 * 0.88 = 26 (반올림) expect(result.updatedPlayer.mpCurrent, equals(24)); // 50 - 26 }); }); group('useHealSkill', () { test('고정 회복량', () { final rng = DeterministicRandom(42); final service = SkillService(rng: rng); const skill = SkillData.hotReload; // healAmount: 30, mpCost: 80 final player = CombatStats.empty().copyWith( hpMax: 200, hpCurrent: 100, mpMax: 200, mpCurrent: 150, ); final skillSystem = SkillSystemState.empty().copyWith(elapsedMs: 5000); final result = service.useHealSkill( skill: skill, player: player, skillSystem: skillSystem, ); expect(result.result.success, isTrue); expect(result.result.healedAmount, equals(30)); expect(result.updatedPlayer.hpCurrent, equals(130)); // 100 + 30 expect(result.updatedPlayer.mpCurrent, equals(70)); // 150 - 80 }); test('퍼센트 회복량', () { final rng = DeterministicRandom(42); final service = SkillService(rng: rng); const skill = SkillData.garbageCollection; // healPercent: 0.3 final player = CombatStats.empty().copyWith( hpMax: 200, hpCurrent: 100, mpMax: 100, mpCurrent: 50, ); final skillSystem = SkillSystemState.empty().copyWith(elapsedMs: 5000); final result = service.useHealSkill( skill: skill, player: player, skillSystem: skillSystem, ); // healPercent 0.3 * hpMax 200 = 60 expect(result.result.healedAmount, equals(60)); expect(result.updatedPlayer.hpCurrent, equals(160)); // 100 + 60 }); test('HP 캡 적용', () { final rng = DeterministicRandom(42); final service = SkillService(rng: rng); const skill = SkillData.snapshotRestore; // healPercent: 0.5 final player = CombatStats.empty().copyWith( hpMax: 200, hpCurrent: 180, // 거의 만피 mpMax: 100, mpCurrent: 80, ); final skillSystem = SkillSystemState.empty().copyWith(elapsedMs: 5000); final result = service.useHealSkill( skill: skill, player: player, skillSystem: skillSystem, ); // healPercent 0.5 * hpMax 200 = 100 // 하지만 hpMax 캡으로 200까지만 expect(result.updatedPlayer.hpCurrent, equals(200)); }); }); group('useDotSkill', () { test('DOT 효과 생성', () { final rng = DeterministicRandom(42); final service = SkillService(rng: rng); const skill = SkillData.memoryDump; // baseDotDamage: 10, baseDotDurationMs: 6000, baseDotTickMs: 1000 final player = CombatStats.empty().copyWith( mpMax: 100, mpCurrent: 50, ); final skillSystem = SkillSystemState.empty().copyWith(elapsedMs: 5000); final result = service.useDotSkill( skill: skill, player: player, skillSystem: skillSystem, playerInt: 10, // 기본 playerWis: 10, // 기본 ); expect(result.result.success, isTrue); expect(result.dotEffect, isNotNull); expect(result.dotEffect!.baseDamage, equals(10)); expect(result.dotEffect!.damagePerTick, equals(10)); // INT 10 → 1.0x expect(result.dotEffect!.tickIntervalMs, equals(1000)); // WIS 10 → 1.0x expect(result.dotEffect!.totalDurationMs, equals(6000)); // 예상 총 데미지: 6틱 × 10 = 60 expect(result.result.damage, equals(60)); }); test('INT 보정 적용 (데미지 증가)', () { final rng = DeterministicRandom(42); final service = SkillService(rng: rng); const skill = SkillData.memoryDump; final player = CombatStats.empty().copyWith( mpMax: 100, mpCurrent: 50, ); final skillSystem = SkillSystemState.empty().copyWith(elapsedMs: 5000); final result = service.useDotSkill( skill: skill, player: player, skillSystem: skillSystem, playerInt: 20, // INT +10 → +30% 데미지 playerWis: 10, ); // INT 20: 1.0 + (20-10) * 0.03 = 1.3x // baseDotDamage 10 * 1.3 = 13 expect(result.dotEffect!.damagePerTick, equals(13)); }); test('WIS 보정 적용 (틱 간격 감소)', () { final rng = DeterministicRandom(42); final service = SkillService(rng: rng); const skill = SkillData.memoryDump; final player = CombatStats.empty().copyWith( mpMax: 100, mpCurrent: 50, ); final skillSystem = SkillSystemState.empty().copyWith(elapsedMs: 5000); final result = service.useDotSkill( skill: skill, player: player, skillSystem: skillSystem, playerInt: 10, playerWis: 20, // WIS +10 → 틱 간격 감소 ); // WIS 20: 1.0 + (20-10) * 0.02 = 1.2x // baseDotTickMs 1000 / 1.2 = 833ms expect(result.dotEffect!.tickIntervalMs, equals(833)); }); }); group('calculateMpRegen', () { test('비전투 중 MP 회복 (50ms당 1)', () { final rng = DeterministicRandom(42); final service = SkillService(rng: rng); final result = service.calculateMpRegen( elapsedMs: 1000, // 1초 isInCombat: false, wis: 10, ); // 1000ms / 50ms = 20 expect(result, equals(20)); }); test('전투 중 MP 회복 (WIS 기반)', () { final rng = DeterministicRandom(42); final service = SkillService(rng: rng); final result = service.calculateMpRegen( elapsedMs: 1000, // 1초 isInCombat: true, wis: 10, ); // 전투 중: 500ms당 (1 + WIS/20) // WIS 10: 1 + 10/20 = 1 (정수 연산) // 1000ms / 500ms = 2틱 // 2틱 * 1 = 2 expect(result, equals(2)); }); test('높은 WIS 전투 중 MP 회복', () { final rng = DeterministicRandom(42); final service = SkillService(rng: rng); final result = service.calculateMpRegen( elapsedMs: 1000, isInCombat: true, wis: 40, ); // WIS 40: 1 + 40/20 = 3 // 2틱 * 3 = 6 expect(result, equals(6)); }); }); group('selectAutoSkill', () { test('MP 부족 시 null 반환 (일반 공격)', () { final rng = DeterministicRandom(42); final service = SkillService(rng: rng); final player = CombatStats.empty().copyWith( hpMax: 100, hpCurrent: 100, mpMax: 100, mpCurrent: 10, // 10% MP ); final monster = MonsterCombatStats( name: 'Test Monster', level: 1, atk: 10, def: 10, magDef: 10, hpMax: 100, hpCurrent: 100, criRate: 0.05, criDamage: 1.5, evasion: 0.0, accuracy: 0.8, attackDelayMs: 1000, expReward: 100, ); final skillSystem = SkillSystemState.empty().copyWith(elapsedMs: 5000); final result = service.selectAutoSkill( player: player, monster: monster, skillSystem: skillSystem, availableSkillIds: ['stack_trace', 'garbage_collection'], ); expect(result, isNull); }); test('HP 30% 미만 시 회복 스킬 우선', () { // 시드 조정하여 일반 공격 확률을 넘어가도록 final rng = DeterministicRandom(999); final service = SkillService(rng: rng); final player = CombatStats.empty().copyWith( hpMax: 100, hpCurrent: 20, // 20% HP mpMax: 200, mpCurrent: 150, // 힐 스킬 사용 가능한 MP (garbageCollection: 130) ); final monster = MonsterCombatStats( name: 'Test Monster', level: 1, atk: 10, def: 10, magDef: 10, hpMax: 100, hpCurrent: 100, criRate: 0.05, criDamage: 1.5, evasion: 0.0, accuracy: 0.8, attackDelayMs: 1000, expReward: 100, ); final skillSystem = SkillSystemState.empty().copyWith(elapsedMs: 5000); final result = service.selectAutoSkill( player: player, monster: monster, skillSystem: skillSystem, availableSkillIds: ['stack_trace', 'garbage_collection'], ); // HP < 30%이면 회복 스킬 반환 (garbage_collection) expect(result?.type, equals(SkillType.heal)); }); }); group('useBuffSkill', () { test('버프 적용', () { final rng = DeterministicRandom(42); final service = SkillService(rng: rng); const skill = SkillData.debugMode; // ATK +25% 버프, mpCost: 100 final player = CombatStats.empty().copyWith( mpMax: 200, mpCurrent: 150, ); final skillSystem = SkillSystemState.empty().copyWith(elapsedMs: 5000); final result = service.useBuffSkill( skill: skill, player: player, skillSystem: skillSystem, ); expect(result.result.success, isTrue); expect(result.result.appliedBuff, isNotNull); expect( result.result.appliedBuff!.effect.atkModifier, equals(0.25), ); expect(result.updatedSkillSystem.activeBuffs.length, equals(1)); expect(result.updatedPlayer.mpCurrent, equals(50)); // 150 - 100 }); test('중복 버프 제거 후 새 버프 적용', () { final rng = DeterministicRandom(42); final service = SkillService(rng: rng); const skill = SkillData.debugMode; final player = CombatStats.empty().copyWith( mpMax: 100, mpCurrent: 50, ); final existingBuff = const ActiveBuff( effect: BuffEffect( id: 'debug_mode_buff', name: 'Debug Mode', durationMs: 10000, atkModifier: 0.25, ), startedMs: 1000, sourceSkillId: 'debug_mode', ); final skillSystem = SkillSystemState.empty().copyWith( elapsedMs: 5000, activeBuffs: [existingBuff], ); final result = service.useBuffSkill( skill: skill, player: player, skillSystem: skillSystem, ); // 기존 버프 제거 후 새 버프 추가 → 총 1개 expect(result.updatedSkillSystem.activeBuffs.length, equals(1)); expect( result.updatedSkillSystem.activeBuffs.first.startedMs, equals(5000), // 새 버프 ); }); }); group('cleanupExpiredBuffs', () { test('만료된 버프 제거', () { final rng = DeterministicRandom(42); final service = SkillService(rng: rng); final skillSystem = SkillSystemState.empty().copyWith( elapsedMs: 20000, activeBuffs: [ const ActiveBuff( effect: BuffEffect( id: 'buff1', name: 'Active Buff', durationMs: 15000, // 아직 활성 ), startedMs: 10000, sourceSkillId: 'test1', ), const ActiveBuff( effect: BuffEffect( id: 'buff2', name: 'Expired Buff', durationMs: 5000, // 만료됨 (시작 5000 + 지속 5000 = 10000 < 20000) ), startedMs: 5000, sourceSkillId: 'test2', ), ], ); final result = service.cleanupExpiredBuffs(skillSystem); expect(result.activeBuffs.length, equals(1)); expect(result.activeBuffs.first.effect.id, equals('buff1')); }); }); group('getRankMultiplier', () { test('랭크별 배율 계산', () { // 랭크 배율 하향: 0.15 → 0.08 per rank expect(getRankMultiplier(1), equals(1.0)); expect(getRankMultiplier(2), closeTo(1.08, 0.001)); expect(getRankMultiplier(3), closeTo(1.16, 0.001)); expect(getRankMultiplier(5), closeTo(1.32, 0.001)); expect(getRankMultiplier(10), closeTo(1.72, 0.001)); }); }); group('getRankCooldownMultiplier', () { test('랭크별 쿨타임 감소율', () { expect(getRankCooldownMultiplier(1), equals(1.0)); expect(getRankCooldownMultiplier(2), closeTo(0.95, 0.001)); expect(getRankCooldownMultiplier(5), closeTo(0.80, 0.001)); // 최대 50% 감소 expect(getRankCooldownMultiplier(20), closeTo(0.5, 0.001)); }); }); group('getRankMpMultiplier', () { test('랭크별 MP 비용 감소율', () { expect(getRankMpMultiplier(1), equals(1.0)); expect(getRankMpMultiplier(2), closeTo(0.97, 0.001)); expect(getRankMpMultiplier(5), closeTo(0.88, 0.001)); // 최대 30% 감소 expect(getRankMpMultiplier(20), closeTo(0.7, 0.001)); }); }); }); }