Compare commits

..

6 Commits

Author SHA1 Message Date
JiWoong Sul
249394f548 test: GCD 시뮬레이션 테스트 추가
- 다양한 GCD 값(0~3000ms)에 대한 전투 효율성 비교
- 레벨별 GCD 영향 분석 테스트
- DPS, 스킬 사용 빈도, 전투 시간 측정
- 권장 GCD 분석 결과: 1500~2000ms
2026-01-14 23:04:52 +09:00
JiWoong Sul
85413362a2 fix(ui): 몬스터 사망 애니메이션 상태 리셋 버그 수정
- 새 몬스터 등장 시 이전 사망 애니메이션 상태 초기화
- 전투 시작 시 _showDeathAnimation, _deathAnimationMonsterLines 리셋
- 이전 몬스터 사망 애니메이션 도중 새 전투 시작 시 몬스터 숨김 방지
2026-01-14 23:04:45 +09:00
JiWoong Sul
02d4d1d397 feat(engine): GCD 체크 및 스킬 자동 장착 로직 구현
SkillService:
- canUseSkill()에 GCD 체크 추가
- selectAutoSkill() 확률 조정 (70% 일반공격, 30% 스킬)
- 버프/디버프 조건 강화 (HP>80%, 활성 효과 체크)

ProgressService:
- 스킬 사용 후 GCD 시작 로직 추가
- 장착된 스킬 슬롯에서 사용 가능 스킬 조회
- 비전투 태스크 시 currentCombat 초기화

GameMutations:
- winSpell()에서 스펠 획득 시 전투 스킬 자동 장착
2026-01-14 23:04:38 +09:00
JiWoong Sul
c0d32b1c87 feat(core): SkillSystemState에 GCD 및 스킬 슬롯 추가
- globalCooldownEndMs: GCD 종료 시점 추적
- globalCooldownDuration: 1500ms 상수 정의
- equippedSkills: 장착된 스킬 슬롯 (SkillSlots)
- isGlobalCooldownActive, remainingGlobalCooldown getter
- startGlobalCooldown() 메서드 추가
2026-01-14 23:04:13 +09:00
JiWoong Sul
8112173541 feat(data): 모든 스킬에 티어 정보 추가
- 공격/회복/버프/디버프 전 스킬에 tier(1~5) 설정
- 티어 기준: 스킬 강도, MP 소모, 쿨타임 등 종합 고려
- 티어 5: Kernel Panic, Chaos Monkey, Snapshot Restore 등
- 티어 1: Stack Trace, Hot Reload, Step Into 등
2026-01-14 23:04:07 +09:00
JiWoong Sul
2621942ced feat(model): 스킬 티어 및 파워 스코어 시스템 추가
- Skill 클래스에 tier 필드 추가 (1~5, 높을수록 강함)
- 타입별 powerScore 동적 계산 로직 구현
- isStrongerThan() 메서드로 스킬 강도 비교 지원
- SkillFailReason에 onGlobalCooldown 추가
- SkillSlots 클래스 신규 추가 (타입별 슬롯 제한)
2026-01-14 23:03:51 +09:00
9 changed files with 820 additions and 33 deletions

View File

@@ -16,6 +16,7 @@ class SkillData {
id: 'stack_trace',
name: 'Stack Trace',
type: SkillType.attack,
tier: 1,
mpCost: 10,
cooldownMs: 3000,
power: 15,
@@ -27,6 +28,7 @@ class SkillData {
id: 'core_dump',
name: 'Core Dump',
type: SkillType.attack,
tier: 3,
mpCost: 35,
cooldownMs: 12000,
power: 30,
@@ -38,6 +40,7 @@ class SkillData {
id: 'memory_dump',
name: 'Memory Dump',
type: SkillType.attack,
tier: 3,
mpCost: 25,
cooldownMs: 15000,
power: 0,
@@ -53,6 +56,7 @@ class SkillData {
id: 'kernel_panic',
name: 'Kernel Panic',
type: SkillType.attack,
tier: 5,
mpCost: 80,
cooldownMs: 45000,
power: 60,
@@ -65,6 +69,7 @@ class SkillData {
id: 'blue_screen',
name: 'Blue Screen',
type: SkillType.attack,
tier: 4,
mpCost: 60,
cooldownMs: 30000,
power: 50,
@@ -76,6 +81,7 @@ class SkillData {
id: 'inject_code',
name: 'Inject Code',
type: SkillType.attack,
tier: 4,
mpCost: 40,
cooldownMs: 18000,
power: 35,
@@ -88,6 +94,7 @@ class SkillData {
id: 'spawn_shell',
name: 'Spawn Shell',
type: SkillType.attack,
tier: 3,
mpCost: 30,
cooldownMs: 10000,
power: 12,
@@ -100,6 +107,7 @@ class SkillData {
id: 'thread_pool',
name: 'Thread Pool',
type: SkillType.attack,
tier: 4,
mpCost: 45,
cooldownMs: 15000,
power: 10,
@@ -112,6 +120,7 @@ class SkillData {
id: 'exfiltrate_data',
name: 'Exfiltrate Data',
type: SkillType.attack,
tier: 4,
mpCost: 35,
cooldownMs: 12000,
power: 25,
@@ -124,6 +133,7 @@ class SkillData {
id: 'fuzzing',
name: 'Fuzzing',
type: SkillType.attack,
tier: 2,
mpCost: 20,
cooldownMs: 8000,
power: 20,
@@ -136,6 +146,7 @@ class SkillData {
id: 'chaos_monkey',
name: 'Chaos Monkey',
type: SkillType.attack,
tier: 5,
mpCost: 50,
cooldownMs: 25000,
power: 40,
@@ -148,6 +159,7 @@ class SkillData {
id: 'saga_pattern',
name: 'Saga Pattern',
type: SkillType.attack,
tier: 4,
mpCost: 55,
cooldownMs: 20000,
power: 18,
@@ -160,6 +172,7 @@ class SkillData {
id: 'event_store',
name: 'Event Store',
type: SkillType.attack,
tier: 4,
mpCost: 40,
cooldownMs: 18000,
power: 0,
@@ -175,6 +188,7 @@ class SkillData {
id: 'auto_scale',
name: 'Auto Scale',
type: SkillType.attack,
tier: 4,
mpCost: 45,
cooldownMs: 20000,
power: 30,
@@ -186,6 +200,7 @@ class SkillData {
id: 'disassemble',
name: 'Disassemble',
type: SkillType.attack,
tier: 3,
mpCost: 30,
cooldownMs: 12000,
power: 22,
@@ -198,6 +213,7 @@ class SkillData {
id: 'decompile',
name: 'Decompile',
type: SkillType.attack,
tier: 2,
mpCost: 25,
cooldownMs: 10000,
power: 20,
@@ -209,6 +225,7 @@ class SkillData {
id: 'canary_release',
name: 'Canary Release',
type: SkillType.attack,
tier: 1,
mpCost: 15,
cooldownMs: 6000,
power: 12,
@@ -220,6 +237,7 @@ class SkillData {
id: 'ab_test',
name: 'A/B Test',
type: SkillType.attack,
tier: 2,
mpCost: 35,
cooldownMs: 12000,
power: 15,
@@ -232,6 +250,7 @@ class SkillData {
id: 'pivot_network',
name: 'Pivot Network',
type: SkillType.attack,
tier: 3,
mpCost: 30,
cooldownMs: 10000,
power: 25,
@@ -244,6 +263,7 @@ class SkillData {
id: 'async_await',
name: 'Async Await',
type: SkillType.attack,
tier: 4,
mpCost: 35,
cooldownMs: 14000,
power: 35,
@@ -255,6 +275,7 @@ class SkillData {
id: 'event_source',
name: 'Event Source',
type: SkillType.attack,
tier: 3,
mpCost: 30,
cooldownMs: 12000,
power: 0,
@@ -270,6 +291,7 @@ class SkillData {
id: 'cqrs_split',
name: 'CQRS Split',
type: SkillType.attack,
tier: 4,
mpCost: 40,
cooldownMs: 15000,
power: 20,
@@ -286,6 +308,7 @@ class SkillData {
id: 'garbage_collection',
name: 'Garbage Collection',
type: SkillType.heal,
tier: 3,
mpCost: 25,
cooldownMs: 15000,
power: 0,
@@ -297,6 +320,7 @@ class SkillData {
id: 'hot_reload',
name: 'Hot Reload',
type: SkillType.heal,
tier: 1,
mpCost: 15,
cooldownMs: 8000,
power: 0,
@@ -308,6 +332,7 @@ class SkillData {
id: 'rollback',
name: 'Rollback',
type: SkillType.heal,
tier: 4,
mpCost: 30,
cooldownMs: 18000,
power: 0,
@@ -319,6 +344,7 @@ class SkillData {
id: 'hotfix',
name: 'Hotfix',
type: SkillType.heal,
tier: 1,
mpCost: 10,
cooldownMs: 6000,
power: 0,
@@ -330,6 +356,7 @@ class SkillData {
id: 'snapshot_restore',
name: 'Snapshot Restore',
type: SkillType.heal,
tier: 5,
mpCost: 50,
cooldownMs: 30000,
power: 0,
@@ -341,6 +368,7 @@ class SkillData {
id: 'patch_binary',
name: 'Patch Binary',
type: SkillType.heal,
tier: 4,
mpCost: 35,
cooldownMs: 20000,
power: 0,
@@ -358,6 +386,7 @@ class SkillData {
id: 'git_commit',
name: 'Git Commit',
type: SkillType.heal,
tier: 2,
mpCost: 20,
cooldownMs: 12000,
power: 0,
@@ -369,6 +398,7 @@ class SkillData {
id: 'git_push',
name: 'Git Push',
type: SkillType.heal,
tier: 3,
mpCost: 25,
cooldownMs: 15000,
power: 0,
@@ -380,6 +410,7 @@ class SkillData {
id: 'connection_pool',
name: 'Connection Pool',
type: SkillType.heal,
tier: 3,
mpCost: 0,
cooldownMs: 20000,
power: 0,
@@ -391,6 +422,7 @@ class SkillData {
id: 'load_balance',
name: 'Load Balance',
type: SkillType.heal,
tier: 4,
mpCost: 20,
cooldownMs: 15000,
power: 0,
@@ -403,6 +435,7 @@ class SkillData {
id: 'blue_green_deploy',
name: 'Blue Green Deploy',
type: SkillType.heal,
tier: 4,
mpCost: 30,
cooldownMs: 25000,
power: 0,
@@ -414,6 +447,7 @@ class SkillData {
id: 'cache_invalidate',
name: 'Cache Invalidate',
type: SkillType.heal,
tier: 2,
mpCost: 25,
cooldownMs: 18000,
power: 0,
@@ -429,6 +463,7 @@ class SkillData {
id: 'debug_mode',
name: 'Debug Mode',
type: SkillType.buff,
tier: 3,
mpCost: 20,
cooldownMs: 20000,
power: 0,
@@ -445,6 +480,7 @@ class SkillData {
id: 'safe_mode',
name: 'Safe Mode',
type: SkillType.buff,
tier: 3,
mpCost: 25,
cooldownMs: 25000,
power: 0,
@@ -461,6 +497,7 @@ class SkillData {
id: 'memory_optimization',
name: 'Memory Optimization',
type: SkillType.buff,
tier: 3,
mpCost: 30,
cooldownMs: 30000,
power: 0,
@@ -480,6 +517,7 @@ class SkillData {
id: 'breakpoint',
name: 'Breakpoint',
type: SkillType.buff,
tier: 2,
mpCost: 15,
cooldownMs: 12000,
power: 0,
@@ -496,6 +534,7 @@ class SkillData {
id: 'watch_variable',
name: 'Watch Variable',
type: SkillType.buff,
tier: 2,
mpCost: 18,
cooldownMs: 15000,
power: 0,
@@ -512,6 +551,7 @@ class SkillData {
id: 'step_into',
name: 'Step Into',
type: SkillType.buff,
tier: 1,
mpCost: 15,
cooldownMs: 12000,
power: 0,
@@ -528,6 +568,7 @@ class SkillData {
id: 'profile_run',
name: 'Profile Run',
type: SkillType.buff,
tier: 3,
mpCost: 20,
cooldownMs: 18000,
power: 0,
@@ -544,6 +585,7 @@ class SkillData {
id: 'benchmark',
name: 'Benchmark',
type: SkillType.buff,
tier: 4,
mpCost: 25,
cooldownMs: 20000,
power: 0,
@@ -560,6 +602,7 @@ class SkillData {
id: 'elevate_privilege',
name: 'Elevate Privilege',
type: SkillType.buff,
tier: 5,
mpCost: 40,
cooldownMs: 35000,
power: 0,
@@ -579,6 +622,7 @@ class SkillData {
id: 'scale_up',
name: 'Scale Up',
type: SkillType.buff,
tier: 5,
mpCost: 35,
cooldownMs: 30000,
power: 0,
@@ -595,6 +639,7 @@ class SkillData {
id: 'failover',
name: 'Failover',
type: SkillType.buff,
tier: 5,
mpCost: 30,
cooldownMs: 45000,
power: 0,
@@ -611,6 +656,7 @@ class SkillData {
id: 'containerize',
name: 'Containerize',
type: SkillType.buff,
tier: 3,
mpCost: 25,
cooldownMs: 20000,
power: 0,
@@ -627,6 +673,7 @@ class SkillData {
id: 'orchestrate',
name: 'Orchestrate',
type: SkillType.buff,
tier: 4,
mpCost: 45,
cooldownMs: 40000,
power: 0,
@@ -645,6 +692,7 @@ class SkillData {
id: 'promise_resolve',
name: 'Promise Resolve',
type: SkillType.buff,
tier: 2,
mpCost: 20,
cooldownMs: 15000,
power: 0,
@@ -661,6 +709,7 @@ class SkillData {
id: 'feature_toggle',
name: 'Feature Toggle',
type: SkillType.buff,
tier: 1,
mpCost: 15,
cooldownMs: 10000,
power: 0,
@@ -678,6 +727,7 @@ class SkillData {
id: 'dark_launch',
name: 'Dark Launch',
type: SkillType.buff,
tier: 4,
mpCost: 35,
cooldownMs: 30000,
power: 0,
@@ -694,6 +744,7 @@ class SkillData {
id: 'static_analysis',
name: 'Static Analysis',
type: SkillType.buff,
tier: 1,
mpCost: 15,
cooldownMs: 12000,
power: 0,
@@ -710,6 +761,7 @@ class SkillData {
id: 'dynamic_analysis',
name: 'Dynamic Analysis',
type: SkillType.buff,
tier: 4,
mpCost: 25,
cooldownMs: 18000,
power: 0,
@@ -727,6 +779,7 @@ class SkillData {
id: 'reverse_engineer',
name: 'Reverse Engineer',
type: SkillType.buff,
tier: 4,
mpCost: 30,
cooldownMs: 25000,
power: 0,
@@ -743,6 +796,7 @@ class SkillData {
id: 'cover_tracks',
name: 'Cover Tracks',
type: SkillType.buff,
tier: 2,
mpCost: 20,
cooldownMs: 15000,
power: 0,
@@ -759,6 +813,7 @@ class SkillData {
id: 'deploy',
name: 'Deploy',
type: SkillType.buff,
tier: 4,
mpCost: 35,
cooldownMs: 30000,
power: 0,
@@ -775,6 +830,7 @@ class SkillData {
id: 'retry_logic',
name: 'Retry Logic',
type: SkillType.buff,
tier: 2,
mpCost: 20,
cooldownMs: 15000,
power: 0,
@@ -791,6 +847,7 @@ class SkillData {
id: 'state_machine',
name: 'State Machine',
type: SkillType.buff,
tier: 2,
mpCost: 30,
cooldownMs: 25000,
power: 0,
@@ -812,6 +869,7 @@ class SkillData {
id: 'step_over',
name: 'Step Over',
type: SkillType.debuff,
tier: 1,
mpCost: 20,
cooldownMs: 15000,
power: 0,
@@ -828,6 +886,7 @@ class SkillData {
id: 'cold_boot',
name: 'Cold Boot',
type: SkillType.debuff,
tier: 4,
mpCost: 30,
cooldownMs: 25000,
power: 0,
@@ -845,6 +904,7 @@ class SkillData {
id: 'heap_analysis',
name: 'Heap Analysis',
type: SkillType.debuff,
tier: 2,
mpCost: 25,
cooldownMs: 18000,
power: 0,
@@ -861,6 +921,7 @@ class SkillData {
id: 'unit_test',
name: 'Unit Test',
type: SkillType.debuff,
tier: 1,
mpCost: 20,
cooldownMs: 15000,
power: 0,
@@ -877,6 +938,7 @@ class SkillData {
id: 'integration_test',
name: 'Integration Test',
type: SkillType.debuff,
tier: 2,
mpCost: 25,
cooldownMs: 18000,
power: 0,
@@ -894,6 +956,7 @@ class SkillData {
id: 'sanitizer',
name: 'Sanitizer',
type: SkillType.debuff,
tier: 3,
mpCost: 30,
cooldownMs: 20000,
power: 0,
@@ -911,6 +974,7 @@ class SkillData {
id: 'hook_function',
name: 'Hook Function',
type: SkillType.debuff,
tier: 2,
mpCost: 25,
cooldownMs: 22000,
power: 0,
@@ -927,6 +991,7 @@ class SkillData {
id: 'rate_limit',
name: 'Rate Limit',
type: SkillType.debuff,
tier: 3,
mpCost: 30,
cooldownMs: 25000,
power: 0,
@@ -943,6 +1008,7 @@ class SkillData {
id: 'circuit_break',
name: 'Circuit Break',
type: SkillType.debuff,
tier: 4,
mpCost: 35,
cooldownMs: 30000,
power: 0,
@@ -959,6 +1025,7 @@ class SkillData {
id: 'backpressure',
name: 'Backpressure',
type: SkillType.debuff,
tier: 2,
mpCost: 25,
cooldownMs: 20000,
power: 0,
@@ -975,6 +1042,7 @@ class SkillData {
id: 'git_merge',
name: 'Git Merge',
type: SkillType.debuff,
tier: 2,
mpCost: 20,
cooldownMs: 15000,
power: 0,

View File

@@ -1,3 +1,4 @@
import 'package:asciineverdie/data/skill_data.dart';
import 'package:asciineverdie/src/core/engine/item_service.dart';
import 'package:asciineverdie/src/core/model/equipment_slot.dart';
import 'package:asciineverdie/src/core/model/game_state.dart';
@@ -43,12 +44,17 @@ class GameMutations {
return state.copyWith(rng: state.rng, stats: updatedStats);
}
/// 스펠 획득 (원본 WinSpell)
///
/// 스펠북에 추가하고, 전투용 스킬 슬롯에도 자동으로 장착 시도.
/// 슬롯이 가득 찬 경우 기존 스킬보다 강할 때만 교체됨.
GameState winSpell(GameState state, int wisdom, int level) {
final result = pq_logic.winSpell(config, state.rng, wisdom, level);
final parts = result.split('|');
final name = parts[0];
final rank = parts.length > 1 ? parts[1] : 'I';
// 스펠북 업데이트
final skills = [...state.skillBook.skills];
final index = skills.indexWhere((s) => s.name == name);
if (index >= 0) {
@@ -57,9 +63,20 @@ class GameMutations {
skills.add(SkillEntry(name: name, rank: rank));
}
// 전투 스킬 슬롯에 추가 시도
var skillSystem = state.skillSystem;
final combatSkill = SkillData.getSkillBySpellName(name);
if (combatSkill != null) {
final addResult = skillSystem.equippedSkills.tryAddSkill(combatSkill);
if (addResult.success) {
skillSystem = skillSystem.copyWith(equippedSkills: addResult.slots);
}
}
return state.copyWith(
rng: state.rng,
skillBook: state.skillBook.copyWith(skills: skills),
skillSystem: skillSystem,
);
}

View File

@@ -512,6 +512,7 @@ class ProgressService {
caption: taskResult.caption,
type: TaskType.market,
),
currentCombat: null, // 비전투 태스크이므로 전투 상태 초기화
);
return (progress: progress, queue: queue);
}
@@ -536,6 +537,7 @@ class ProgressService {
caption: taskResult.caption,
type: TaskType.buying,
),
currentCombat: null, // 비전투 태스크이므로 전투 상태 초기화
);
return (progress: progress, queue: queue);
}
@@ -551,6 +553,7 @@ class ProgressService {
caption: taskResult.caption,
type: TaskType.neutral,
),
currentCombat: null, // 비전투 태스크이므로 전투 상태 초기화
);
return (progress: progress, queue: queue);
}
@@ -672,7 +675,7 @@ class ProgressService {
type: TaskType.kill,
monsterBaseName: monsterResult.baseName,
monsterPart: monsterResult.part,
monsterLevel: monsterResult.level,
monsterLevel: effectiveMonsterLevel,
monsterGrade: monsterResult.grade,
),
currentCombat: combatState,
@@ -1205,6 +1208,7 @@ class ProgressService {
);
final progress = taskResult.progress.copyWith(
currentTask: TaskInfo(caption: taskResult.caption, type: TaskType.sell),
currentCombat: null, // 비전투 태스크이므로 전투 상태 초기화
);
return (
state: state.copyWith(
@@ -1358,11 +1362,11 @@ class ProgressService {
// 플레이어 공격 체크
if (playerAccumulator >= playerStats.attackDelayMs) {
// SkillBook에서 사용 가능한 스킬 ID 목록 조회
var availableSkillIds = skillService.getAvailableSkillIdsFromSkillBook(
state.skillBook,
);
// SkillBook에 스킬이 없으면 기본 스킬 사용
// 장착된 스킬 슬롯에서 사용 가능한 스킬 ID 목록 조회
var availableSkillIds = state.skillSystem.equippedSkills.allSkills
.map((s) => s.id)
.toList();
// 장착된 스킬이 없으면 기본 스킬 사용
if (availableSkillIds.isEmpty) {
availableSkillIds = SkillData.defaultSkillIds;
}
@@ -1395,6 +1399,9 @@ class ProgressService {
totalDamageDealt += skillResult.result.damage;
updatedSkillSystem = skillResult.updatedSkillSystem;
// GCD 시작 (스킬 사용 후)
updatedSkillSystem = updatedSkillSystem.startGlobalCooldown();
// 스킬 공격 이벤트 생성
newEvents.add(
CombatEvent.playerSkill(
@@ -1417,6 +1424,9 @@ class ProgressService {
playerStats = skillResult.updatedPlayer;
updatedSkillSystem = skillResult.updatedSkillSystem;
// GCD 시작 (스킬 사용 후)
updatedSkillSystem = updatedSkillSystem.startGlobalCooldown();
// DOT 효과 추가
if (skillResult.dotEffect != null) {
activeDoTs.add(skillResult.dotEffect!);
@@ -1442,6 +1452,9 @@ class ProgressService {
playerStats = skillResult.updatedPlayer;
updatedSkillSystem = skillResult.updatedSkillSystem;
// GCD 시작 (스킬 사용 후)
updatedSkillSystem = updatedSkillSystem.startGlobalCooldown();
// 회복 이벤트 생성
newEvents.add(
CombatEvent.playerHeal(
@@ -1460,6 +1473,9 @@ class ProgressService {
playerStats = skillResult.updatedPlayer;
updatedSkillSystem = skillResult.updatedSkillSystem;
// GCD 시작 (스킬 사용 후)
updatedSkillSystem = updatedSkillSystem.startGlobalCooldown();
// 버프 이벤트 생성
newEvents.add(
CombatEvent.playerBuff(
@@ -1478,6 +1494,9 @@ class ProgressService {
playerStats = skillResult.updatedPlayer;
updatedSkillSystem = skillResult.updatedSkillSystem;
// GCD 시작 (스킬 사용 후)
updatedSkillSystem = updatedSkillSystem.startGlobalCooldown();
// 디버프 효과 추가 (기존 같은 디버프 제거 후)
if (skillResult.debuffEffect != null) {
activeDebuffs =
@@ -1708,8 +1727,10 @@ class ProgressService {
if (equippedNonWeaponSlots.isNotEmpty) {
lostCount = 1;
// 랜덤하게 1개 슬롯 선택
final sacrificeIndex = equippedNonWeaponSlots[
state.rng.nextInt(equippedNonWeaponSlots.length)];
final sacrificeIndex =
equippedNonWeaponSlots[state.rng.nextInt(
equippedNonWeaponSlots.length,
)];
final slot = EquipmentSlot.values[sacrificeIndex];
// 해당 슬롯을 빈 장비로 교체
@@ -1733,7 +1754,8 @@ class ProgressService {
// 보스전 사망 시 5분 레벨링 모드 진입
final bossLevelingEndTime = isBossDeath
? DateTime.now().millisecondsSinceEpoch + (5 * 60 * 1000) // 5분
? DateTime.now().millisecondsSinceEpoch +
(5 * 60 * 1000) // 5분
: null;
// 전투 상태 초기화 및 사망 횟수 증가

View File

@@ -24,6 +24,11 @@ class SkillService {
required int currentMp,
required SkillSystemState skillSystem,
}) {
// GCD 체크 (글로벌 쿨타임 1500ms)
if (skillSystem.isGlobalCooldownActive) {
return SkillFailReason.onGlobalCooldown;
}
// MP 체크
if (currentMp < skill.mpCost) {
return SkillFailReason.notEnoughMp;
@@ -297,13 +302,14 @@ class SkillService {
/// 전투 중 자동 스킬 선택
///
/// 우선순위:
/// 1. HP < 30% → 회복 스킬
/// 2. HP > 70% & MP > 50% → 버프 스킬 (안전할 때)
/// 3. 몬스터 HP > 70% & 활성 디버프 없음 → 디버프 스킬
/// 4. 몬스터 HP > 50% & DOT 없음 → DOT 스킬 (장기전 유리)
/// 5. 보스전 (레벨 차이 10 이상) → 가장 강력한 공격 스킬
/// 6. 일반 전투 → MP 효율이 좋은 스킬
/// 7. MP < 20% → null (일반 공격)
/// 1. HP < 30% → 회복 스킬 (최우선)
/// 2. 70% 확률로 일반 공격 (MP 절약, 기본 전투)
/// 3. 30% 확률로 스킬 사용:
/// - 버프: HP > 80% & MP > 60% & 활성 버프 없음
/// - 디버프: 몬스터 HP > 80% & 활성 디버프 없음
/// - DOT: 몬스터 HP > 60% & 활성 DOT 없음
/// - 공격: 보스전이면 강력한 스킬, 일반전이면 효율적 스킬
/// 4. MP < 20% → 일반 공격
Skill? selectAutoSkill({
required CombatStats player,
required MonsterCombatStats monster,
@@ -336,39 +342,49 @@ class SkillService {
if (availableSkills.isEmpty) return null;
// HP < 30% → 회복 스킬 우선
// HP < 30% → 회복 스킬 우선 (생존)
if (hpRatio < 0.3) {
final healSkill = _findBestHealSkill(availableSkills, currentMp);
if (healSkill != null) return healSkill;
}
// HP > 70% & MP > 50% → 버프 스킬 (안전할 때)
if (hpRatio > 0.7 && mpRatio > 0.5) {
// 70% 확률로 일반 공격 (스킬은 특별한 상황에서만)
final useNormalAttack = rng.nextInt(100) < 70;
if (useNormalAttack) return null;
// === 아래부터 30% 확률로 스킬 사용 ===
// 버프: HP > 80% & MP > 60% (매우 안전할 때만)
// 활성 버프가 있으면 건너뜀 (중복 방지)
if (hpRatio > 0.8 && mpRatio > 0.6) {
final hasActiveBuff = skillSystem.activeBuffs.isNotEmpty;
if (!hasActiveBuff) {
final buffSkill = _findBestBuffSkill(availableSkills, currentMp);
if (buffSkill != null) return buffSkill;
}
}
// 몬스터 HP > 70% & 활성 디버프 없음 → 디버프 스킬
if (monster.hpRatio > 0.7 && activeDebuffs.isEmpty) {
// 디버프: 몬스터 HP > 80% & 활성 디버프 없음 (전투 초반)
if (monster.hpRatio > 0.8 && activeDebuffs.isEmpty) {
final debuffSkill = _findBestDebuffSkill(availableSkills, currentMp);
if (debuffSkill != null) return debuffSkill;
}
// 몬스터 HP > 50% & 활성 DOT 없음 → DOT 스킬 사용
if (monster.hpRatio > 0.5 && activeDoTs.isEmpty) {
// DOT: 몬스터 HP > 60% & 활성 DOT 없음 (장기전 유리)
if (monster.hpRatio > 0.6 && activeDoTs.isEmpty) {
final dotSkill = _findBestDotSkill(availableSkills, currentMp);
if (dotSkill != null) return dotSkill;
}
// 보스전 판단 (몬스터 레벨이 높음)
final isBossFight = monster.level >= 10 && monster.hpRatio > 0.5;
// 보스전 판단 (몬스터 레벨 20 이상 & HP 50% 이상)
final isBossFight = monster.level >= 20 && monster.hpRatio > 0.5;
if (isBossFight) {
// 가장 강력한 공격 스킬
return _findStrongestAttackSkill(availableSkills);
}
// 일반 전투 → MP 효율 좋은 스킬
// 일반 전투 → MP 효율 좋은 공격 스킬
return _findEfficientAttackSkill(availableSkills);
}

View File

@@ -8,6 +8,7 @@ import 'package:asciineverdie/src/core/model/item_stats.dart';
import 'package:asciineverdie/src/core/model/monster_grade.dart';
import 'package:asciineverdie/src/core/model/potion.dart';
import 'package:asciineverdie/src/core/model/skill.dart';
import 'package:asciineverdie/src/core/model/skill_slots.dart';
import 'package:asciineverdie/src/core/util/deterministic_random.dart';
/// Minimal skeletal state to mirror Progress Quest structures.
@@ -193,14 +194,19 @@ enum DeathCause {
/// 스킬 시스템 상태 (Phase 3)
///
/// 스킬 쿨타임, 활성 버프, 게임 경과 시간 등을 관리
/// 스킬 쿨타임, 활성 버프, 게임 경과 시간, 장착 스킬 등을 관리
class SkillSystemState {
const SkillSystemState({
required this.skillStates,
required this.activeBuffs,
required this.elapsedMs,
this.equippedSkills = const SkillSlots(),
this.globalCooldownEndMs = 0,
});
/// 글로벌 쿨타임 (GCD) 상수: 1500ms
static const int globalCooldownDuration = 1500;
/// 스킬별 쿨타임 상태
final List<SkillState> skillStates;
@@ -210,8 +216,26 @@ class SkillSystemState {
/// 게임 진행 경과 시간 (밀리초, 스킬 쿨타임 계산용)
final int elapsedMs;
factory SkillSystemState.empty() =>
const SkillSystemState(skillStates: [], activeBuffs: [], elapsedMs: 0);
/// 장착된 스킬 슬롯 (타입별 제한 있음)
final SkillSlots equippedSkills;
/// 글로벌 쿨타임 종료 시점 (elapsedMs 기준)
final int globalCooldownEndMs;
/// GCD가 활성화 중인지 확인
bool get isGlobalCooldownActive => elapsedMs < globalCooldownEndMs;
/// 남은 GCD 시간 (ms)
int get remainingGlobalCooldown =>
isGlobalCooldownActive ? globalCooldownEndMs - elapsedMs : 0;
factory SkillSystemState.empty() => const SkillSystemState(
skillStates: [],
activeBuffs: [],
elapsedMs: 0,
equippedSkills: SkillSlots(),
globalCooldownEndMs: 0,
);
/// 특정 스킬 상태 가져오기
SkillState? getSkillState(String skillId) {
@@ -254,13 +278,22 @@ class SkillSystemState {
List<SkillState>? skillStates,
List<ActiveBuff>? activeBuffs,
int? elapsedMs,
SkillSlots? equippedSkills,
int? globalCooldownEndMs,
}) {
return SkillSystemState(
skillStates: skillStates ?? this.skillStates,
activeBuffs: activeBuffs ?? this.activeBuffs,
elapsedMs: elapsedMs ?? this.elapsedMs,
equippedSkills: equippedSkills ?? this.equippedSkills,
globalCooldownEndMs: globalCooldownEndMs ?? this.globalCooldownEndMs,
);
}
/// GCD 시작 (스킬 사용 후 호출)
SkillSystemState startGlobalCooldown() {
return copyWith(globalCooldownEndMs: elapsedMs + globalCooldownDuration);
}
}
/// 태스크 타입 (원본 fTask.Caption 값들에 대응)
@@ -594,8 +627,11 @@ class Equipment {
}
/// 초보자 방어구 생성 헬퍼
static EquipmentItem _starterArmor(String name, EquipmentSlot slot,
{required int def}) {
static EquipmentItem _starterArmor(
String name,
EquipmentSlot slot, {
required int def,
}) {
return EquipmentItem(
name: name,
slot: slot,

View File

@@ -117,6 +117,7 @@ class Skill {
required this.mpCost,
required this.cooldownMs,
required this.power,
this.tier = 1,
this.damageMultiplier = 1.0,
this.healAmount = 0,
this.healPercent = 0.0,
@@ -133,6 +134,9 @@ class Skill {
this.mpHealAmount = 0,
});
/// 스킬 티어 (1~5, 높을수록 강함)
final int tier;
/// 스킬 ID
final String id;
@@ -213,6 +217,79 @@ class Skill {
if (type != SkillType.attack || damageMultiplier <= 0) return 0;
return damageMultiplier / mpCost;
}
/// 스킬 파워 점수 (동적 계산, 같은 티어 내 비교용)
///
/// 타입별 다른 공식:
/// - 공격: DPS 기반
/// - 회복: HPS 기반
/// - 버프/디버프: 효과 강도 × 지속시간 / 쿨타임
double get powerScore {
final cdSec = cooldownMs / 1000;
if (cdSec <= 0) return 0;
return switch (type) {
SkillType.attack => _attackPowerScore(cdSec),
SkillType.heal => _healPowerScore(cdSec),
SkillType.buff => _buffPowerScore(cdSec),
SkillType.debuff => _debuffPowerScore(cdSec),
};
}
double _attackPowerScore(double cdSec) {
// DOT 스킬
if (isDot &&
baseDotDamage != null &&
baseDotDurationMs != null &&
baseDotTickMs != null &&
baseDotTickMs! > 0) {
final ticks = baseDotDurationMs! / baseDotTickMs!;
return (baseDotDamage! * ticks) / cdSec;
}
// 즉발 공격: power × 배율 × 타수 / 쿨타임
var score = power * damageMultiplier * hitCount / cdSec;
score *= (1 + lifestealPercent); // 흡혈 보너스
score *= (1 + targetDefReduction); // 방감 보너스
return score;
}
double _healPowerScore(double cdSec) {
// 고정 회복 + %회복(1000HP 기준) + MP회복
final totalHeal = healAmount + (healPercent * 1000) + mpHealAmount;
var score = totalHeal / cdSec;
if (buff != null) score *= 1.2; // 추가 버프 보너스
return score;
}
double _buffPowerScore(double cdSec) {
if (buff == null) return 0;
final b = buff!;
final strength =
b.atkModifier.abs() +
b.defModifier.abs() +
b.criRateModifier.abs() +
b.evasionModifier.abs();
return strength * (b.durationMs / 1000) / cdSec * 100;
}
double _debuffPowerScore(double cdSec) {
if (buff == null) return 0;
final b = buff!;
final strength = b.atkModifier.abs() + b.defModifier.abs();
return strength * (b.durationMs / 1000) / cdSec * 100;
}
/// 다른 스킬과 비교하여 이 스킬이 더 강한지 판단
///
/// 1. 티어가 다르면 티어로 판단
/// 2. 티어가 같으면 파워 점수로 판단
bool isStrongerThan(Skill other) {
if (tier != other.tier) {
return tier > other.tier;
}
return powerScore > other.powerScore;
}
}
/// 스킬 사용 상태 (쿨타임 추적)
@@ -347,6 +424,9 @@ enum SkillFailReason {
/// 쿨타임 중
onCooldown,
/// 글로벌 쿨타임(GCD) 중
onGlobalCooldown,
/// 스킬 없음
skillNotFound,

View File

@@ -0,0 +1,277 @@
import 'package:asciineverdie/data/skill_data.dart';
import 'package:asciineverdie/src/core/model/skill.dart';
/// 스킬 슬롯 제한 상수
///
/// 타입별로 장착 가능한 최대 스킬 수
class SkillSlotLimits {
SkillSlotLimits._();
static const int attack = 3;
static const int heal = 2;
static const int buff = 2;
static const int debuff = 1;
/// 타입별 슬롯 제한 반환
static int getLimit(SkillType type) {
return switch (type) {
SkillType.attack => attack,
SkillType.heal => heal,
SkillType.buff => buff,
SkillType.debuff => debuff,
};
}
}
/// 캐릭터가 장착한 스킬 슬롯
///
/// 타입별로 슬롯 제한이 있으며, 새 스킬이 기존 스킬보다
/// 강할 경우 자동으로 교체됨
class SkillSlots {
const SkillSlots({
this.attackSkills = const [],
this.healSkills = const [],
this.buffSkills = const [],
this.debuffSkills = const [],
});
/// 공격 스킬 (최대 3개)
final List<Skill> attackSkills;
/// 회복 스킬 (최대 2개)
final List<Skill> healSkills;
/// 버프 스킬 (최대 2개)
final List<Skill> buffSkills;
/// 디버프 스킬 (최대 1개)
final List<Skill> debuffSkills;
/// 장착된 모든 스킬 목록
List<Skill> get allSkills => [
...attackSkills,
...healSkills,
...buffSkills,
...debuffSkills,
];
/// 타입별 스킬 목록 반환
List<Skill> getSkillsByType(SkillType type) {
return switch (type) {
SkillType.attack => attackSkills,
SkillType.heal => healSkills,
SkillType.buff => buffSkills,
SkillType.debuff => debuffSkills,
};
}
/// 해당 타입 슬롯이 가득 찼는지 확인
bool isSlotFull(SkillType type) {
final current = getSkillsByType(type).length;
final limit = SkillSlotLimits.getLimit(type);
return current >= limit;
}
/// 스킬이 이미 장착되어 있는지 확인
bool hasSkill(String skillId) {
return allSkills.any((s) => s.id == skillId);
}
/// 새 스킬 추가 시도
///
/// 반환값: (성공 여부, 새 SkillSlots, 교체된 스킬)
/// - 슬롯에 여유가 있으면 추가
/// - 슬롯이 가득 차면 가장 약한 스킬과 비교 후 교체
/// - 새 스킬이 더 약하면 추가 실패
SkillAddResult tryAddSkill(Skill newSkill) {
// 이미 장착된 스킬인지 확인
if (hasSkill(newSkill.id)) {
return SkillAddResult(
success: false,
slots: this,
replacedSkill: null,
reason: SkillAddFailReason.alreadyEquipped,
);
}
final type = newSkill.type;
final currentSkills = List<Skill>.from(getSkillsByType(type));
final limit = SkillSlotLimits.getLimit(type);
// 슬롯에 여유가 있으면 그냥 추가
if (currentSkills.length < limit) {
currentSkills.add(newSkill);
return SkillAddResult(
success: true,
slots: _copyWith(type, currentSkills),
replacedSkill: null,
reason: null,
);
}
// 슬롯이 가득 찼으면 가장 약한 스킬 찾기
final weakest = _findWeakestSkill(currentSkills);
// 새 스킬이 가장 약한 스킬보다 강한지 확인
if (newSkill.isStrongerThan(weakest)) {
currentSkills.remove(weakest);
currentSkills.add(newSkill);
return SkillAddResult(
success: true,
slots: _copyWith(type, currentSkills),
replacedSkill: weakest,
reason: null,
);
}
// 새 스킬이 더 약함
return SkillAddResult(
success: false,
slots: this,
replacedSkill: null,
reason: SkillAddFailReason.weakerThanExisting,
);
}
/// 특정 스킬 제거
SkillSlots removeSkill(String skillId) {
for (final type in SkillType.values) {
final skills = getSkillsByType(type);
final index = skills.indexWhere((s) => s.id == skillId);
if (index != -1) {
final newSkills = List<Skill>.from(skills)..removeAt(index);
return _copyWith(type, newSkills);
}
}
return this;
}
/// 가장 약한 스킬 찾기 (powerScore 기준)
Skill _findWeakestSkill(List<Skill> skills) {
if (skills.isEmpty) {
throw ArgumentError('스킬 목록이 비어있습니다');
}
var weakest = skills.first;
for (final skill in skills.skip(1)) {
// tier가 낮거나, tier가 같으면 powerScore가 낮은 것이 약함
if (weakest.isStrongerThan(skill)) {
weakest = skill;
}
}
return weakest;
}
/// 타입별 스킬 목록을 교체한 새 SkillSlots 반환
SkillSlots _copyWith(SkillType type, List<Skill> skills) {
return switch (type) {
SkillType.attack => SkillSlots(
attackSkills: skills,
healSkills: healSkills,
buffSkills: buffSkills,
debuffSkills: debuffSkills,
),
SkillType.heal => SkillSlots(
attackSkills: attackSkills,
healSkills: skills,
buffSkills: buffSkills,
debuffSkills: debuffSkills,
),
SkillType.buff => SkillSlots(
attackSkills: attackSkills,
healSkills: healSkills,
buffSkills: skills,
debuffSkills: debuffSkills,
),
SkillType.debuff => SkillSlots(
attackSkills: attackSkills,
healSkills: healSkills,
buffSkills: buffSkills,
debuffSkills: skills,
),
};
}
/// copyWith 메서드
SkillSlots copyWith({
List<Skill>? attackSkills,
List<Skill>? healSkills,
List<Skill>? buffSkills,
List<Skill>? debuffSkills,
}) {
return SkillSlots(
attackSkills: attackSkills ?? this.attackSkills,
healSkills: healSkills ?? this.healSkills,
buffSkills: buffSkills ?? this.buffSkills,
debuffSkills: debuffSkills ?? this.debuffSkills,
);
}
/// JSON 직렬화 (저장용)
Map<String, dynamic> toJson() {
return {
'attackSkills': attackSkills.map((s) => s.id).toList(),
'healSkills': healSkills.map((s) => s.id).toList(),
'buffSkills': buffSkills.map((s) => s.id).toList(),
'debuffSkills': debuffSkills.map((s) => s.id).toList(),
};
}
/// JSON 역직렬화
factory SkillSlots.fromJson(Map<String, dynamic> json) {
List<Skill> parseSkillIds(List<dynamic>? ids) {
if (ids == null) return [];
return ids
.map((id) => SkillData.getSkillById(id as String))
.whereType<Skill>()
.toList();
}
return SkillSlots(
attackSkills: parseSkillIds(json['attackSkills'] as List<dynamic>?),
healSkills: parseSkillIds(json['healSkills'] as List<dynamic>?),
buffSkills: parseSkillIds(json['buffSkills'] as List<dynamic>?),
debuffSkills: parseSkillIds(json['debuffSkills'] as List<dynamic>?),
);
}
@override
String toString() {
return 'SkillSlots('
'attack: ${attackSkills.length}/${SkillSlotLimits.attack}, '
'heal: ${healSkills.length}/${SkillSlotLimits.heal}, '
'buff: ${buffSkills.length}/${SkillSlotLimits.buff}, '
'debuff: ${debuffSkills.length}/${SkillSlotLimits.debuff})';
}
}
/// 스킬 추가 결과
class SkillAddResult {
const SkillAddResult({
required this.success,
required this.slots,
required this.replacedSkill,
required this.reason,
});
/// 추가 성공 여부
final bool success;
/// 결과 SkillSlots (성공 시 새 슬롯, 실패 시 기존 슬롯)
final SkillSlots slots;
/// 교체된 스킬 (슬롯이 가득 차서 교체된 경우)
final Skill? replacedSkill;
/// 실패 사유 (실패 시에만 설정)
final SkillAddFailReason? reason;
}
/// 스킬 추가 실패 사유
enum SkillAddFailReason {
/// 이미 장착된 스킬
alreadyEquipped,
/// 기존 스킬보다 약함
weakerThanExisting,
}

View File

@@ -242,6 +242,14 @@ class _AsciiAnimationCardState extends State<AsciiAnimationCard> {
_lastMonsterBaseName = widget.monsterBaseName;
}
// 새 몬스터 등장 시 사망 애니메이션 상태 리셋
// (이전 몬스터 사망 애니메이션이 끝나기 전에 새 전투 시작 시 대응)
if (oldWidget.monsterBaseName != widget.monsterBaseName &&
widget.monsterBaseName != null) {
_showDeathAnimation = false;
_deathAnimationMonsterLines = null;
}
if (oldWidget.taskType != widget.taskType ||
oldWidget.monsterBaseName != widget.monsterBaseName ||
oldWidget.weaponName != widget.weaponName ||
@@ -550,6 +558,9 @@ class _AsciiAnimationCardState extends State<AsciiAnimationCard> {
_battleSubFrame = 0;
_phaseIndex = 0;
_phaseFrameCount = 0;
// 새 전투 시작 시 사망 애니메이션 상태 리셋 (몬스터 숨김 방지)
_showDeathAnimation = false;
_deathAnimationMonsterLines = null;
}
case AsciiAnimationType.town:

View File

@@ -0,0 +1,260 @@
import 'dart:math';
import 'package:flutter_test/flutter_test.dart';
/// 글로벌 쿨타임(GCD) 시뮬레이션 테스트
///
/// 다양한 GCD 값에 대해 전투 효율성을 측정
void main() {
test('GCD 시뮬레이션 - 다양한 값 비교', () {
print('\n' + '=' * 80);
print('글로벌 쿨타임(GCD) 시뮬레이션 결과');
print('=' * 80);
// 테스트할 GCD 값들 (ms)
final gcdValues = [0, 500, 1000, 1500, 2000, 2500, 3000];
// 시뮬레이션 파라미터
const playerAttackDelay = 1000; // 플레이어 기본 공격 딜레이
const monsterAttackDelay = 1200; // 몬스터 공격 딜레이
const skillUseProbability = 0.30; // 스킬 사용 확률 (30%)
const normalAttackDamage = 50; // 일반 공격 데미지
const skillDamage = 120; // 스킬 데미지 (평균)
const monsterHp = 1000; // 몬스터 HP
const simulationRuns = 1000; // 시뮬레이션 반복 횟수
final results = <int, SimulationResult>{};
for (final gcd in gcdValues) {
final result = _runSimulation(
gcd: gcd,
playerAttackDelay: playerAttackDelay,
monsterAttackDelay: monsterAttackDelay,
skillUseProbability: skillUseProbability,
normalAttackDamage: normalAttackDamage,
skillDamage: skillDamage,
monsterHp: monsterHp,
runs: simulationRuns,
);
results[gcd] = result;
}
// 결과 출력
print('\n### 시뮬레이션 조건');
print('- 플레이어 공격 딜레이: ${playerAttackDelay}ms');
print('- 몬스터 HP: $monsterHp');
print('- 일반 공격 데미지: $normalAttackDamage');
print('- 스킬 데미지 (평균): $skillDamage');
print('- 스킬 사용 확률: ${(skillUseProbability * 100).toInt()}%');
print('- 시뮬레이션 횟수: $simulationRuns회');
print('\n### 결과 비교');
print('| GCD (ms) | 전투시간 (ms) | 총 공격 | 스킬 사용 | 스킬비율 | DPS | 효율 |');
print(
'|----------|---------------|---------|-----------|----------|-----|------|',
);
final baselineDps = results[0]!.avgDps;
for (final gcd in gcdValues) {
final r = results[gcd]!;
final efficiency = (r.avgDps / baselineDps * 100).toStringAsFixed(0);
print(
'| ${gcd.toString().padLeft(8)} '
'| ${r.avgCombatTime.toStringAsFixed(0).padLeft(13)} '
'| ${r.avgTotalAttacks.toStringAsFixed(1).padLeft(7)} '
'| ${r.avgSkillUses.toStringAsFixed(1).padLeft(9)} '
'| ${(r.skillRatio * 100).toStringAsFixed(1).padLeft(7)}% '
'| ${r.avgDps.toStringAsFixed(1).padLeft(3)} '
'| ${efficiency.padLeft(4)}% |',
);
}
// 권장 GCD 분석
print('\n### 분석 및 권장');
// GCD 1500ms 기준 분석
final gcd1500 = results[1500]!;
final gcd2000 = results[2000]!;
// gcd1000은 비교용으로 추후 활용 가능
print('\n#### GCD별 특징');
print('- **0ms (없음)**: 기준선, 스킬 남용 가능');
print('- **500ms**: 거의 제한 없음, 빠른 연속 스킬 가능');
print('- **1000ms**: 공격 딜레이와 동일, 매 공격마다 스킬 선택 가능');
print('- **1500ms**: 스킬 후 1회 일반공격 강제, 적절한 제한');
print('- **2000ms**: 스킬 후 2회 일반공격 강제, 스킬이 특별해짐');
print('- **2500ms+**: 스킬 사용이 매우 제한적, 전략적 선택 필요');
print('\n#### 권장 GCD');
print('**1500ms ~ 2000ms 권장**');
print('- 스킬 사용 빈도가 자연스럽게 제한됨');
print('- 일반 공격의 중요성이 유지됨');
print(
'- DPS 손실이 크지 않음 (${((1 - gcd1500.avgDps / baselineDps) * 100).toStringAsFixed(0)}% ~ ${((1 - gcd2000.avgDps / baselineDps) * 100).toStringAsFixed(0)}%)',
);
print('\n' + '=' * 80);
// 테스트는 항상 통과 (정보 출력용)
expect(true, isTrue);
});
test('GCD 상세 시뮬레이션 - 레벨별 영향', () {
print('\n' + '=' * 80);
print('레벨별 GCD 영향 분석');
print('=' * 80);
// 레벨별 파라미터
final levelConfigs = [
{'level': 1, 'playerAtk': 30, 'skillDmg': 80, 'monsterHp': 200},
{'level': 10, 'playerAtk': 50, 'skillDmg': 130, 'monsterHp': 500},
{'level': 30, 'playerAtk': 90, 'skillDmg': 220, 'monsterHp': 1500},
{'level': 50, 'playerAtk': 130, 'skillDmg': 320, 'monsterHp': 3000},
];
const gcdToTest = [0, 1500, 2000];
print('\n| 레벨 | GCD | 전투시간 | 스킬횟수 | DPS | 효율 |');
print('|------|-----|----------|----------|-----|------|');
for (final config in levelConfigs) {
final level = config['level'] as int;
final playerAtk = config['playerAtk'] as int;
final skillDmg = config['skillDmg'] as int;
final monsterHp = config['monsterHp'] as int;
double? baselineDps;
for (final gcd in gcdToTest) {
final result = _runSimulation(
gcd: gcd,
playerAttackDelay: 1000,
monsterAttackDelay: 1200,
skillUseProbability: 0.30,
normalAttackDamage: playerAtk,
skillDamage: skillDmg,
monsterHp: monsterHp,
runs: 500,
);
baselineDps ??= result.avgDps;
final efficiency = (result.avgDps / baselineDps * 100).toStringAsFixed(
0,
);
print(
'| ${level.toString().padLeft(4)} '
'| ${gcd.toString().padLeft(3)} '
'| ${result.avgCombatTime.toStringAsFixed(0).padLeft(8)} '
'| ${result.avgSkillUses.toStringAsFixed(1).padLeft(8)} '
'| ${result.avgDps.toStringAsFixed(1).padLeft(3)} '
'| ${efficiency.padLeft(4)}% |',
);
}
}
print('\n' + '=' * 80);
expect(true, isTrue);
});
}
/// 시뮬레이션 실행
SimulationResult _runSimulation({
required int gcd,
required int playerAttackDelay,
required int monsterAttackDelay,
required double skillUseProbability,
required int normalAttackDamage,
required int skillDamage,
required int monsterHp,
required int runs,
}) {
final random = Random(42); // 재현 가능한 시드
var totalCombatTime = 0;
var totalAttacks = 0;
var totalSkillUses = 0;
var totalDamage = 0;
for (var i = 0; i < runs; i++) {
var currentHp = monsterHp;
var elapsedTime = 0;
var attackCount = 0;
var skillCount = 0;
var gcdRemaining = 0; // 남은 GCD 시간
var playerAccum = 0;
while (currentHp > 0) {
// 시간 진행 (100ms 단위)
const tickMs = 100;
elapsedTime += tickMs;
playerAccum += tickMs;
// GCD 감소
if (gcdRemaining > 0) {
gcdRemaining -= tickMs;
if (gcdRemaining < 0) gcdRemaining = 0;
}
// 플레이어 공격 체크
if (playerAccum >= playerAttackDelay) {
playerAccum -= playerAttackDelay;
attackCount++;
// 스킬 사용 여부 결정
final canUseSkill = gcdRemaining <= 0;
final wantsToUseSkill = random.nextDouble() < skillUseProbability;
if (canUseSkill && wantsToUseSkill) {
// 스킬 사용
currentHp -= skillDamage;
totalDamage += skillDamage;
skillCount++;
gcdRemaining = gcd; // GCD 시작
} else {
// 일반 공격
currentHp -= normalAttackDamage;
totalDamage += normalAttackDamage;
}
}
// 무한 루프 방지
if (elapsedTime > 60000) break;
}
totalCombatTime += elapsedTime;
totalAttacks += attackCount;
totalSkillUses += skillCount;
}
final avgCombatTime = totalCombatTime / runs;
final avgTotalAttacks = totalAttacks / runs;
final avgSkillUses = totalSkillUses / runs;
final skillRatio = avgSkillUses / avgTotalAttacks;
final avgDps = totalDamage / runs / (avgCombatTime / 1000);
return SimulationResult(
avgCombatTime: avgCombatTime,
avgTotalAttacks: avgTotalAttacks,
avgSkillUses: avgSkillUses,
skillRatio: skillRatio,
avgDps: avgDps,
);
}
class SimulationResult {
const SimulationResult({
required this.avgCombatTime,
required this.avgTotalAttacks,
required this.avgSkillUses,
required this.skillRatio,
required this.avgDps,
});
final double avgCombatTime;
final double avgTotalAttacks;
final double avgSkillUses;
final double skillRatio;
final double avgDps;
}