Compare commits
6 Commits
f9a4ae105a
...
249394f548
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
249394f548 | ||
|
|
85413362a2 | ||
|
|
02d4d1d397 | ||
|
|
c0d32b1c87 | ||
|
|
8112173541 | ||
|
|
2621942ced |
@@ -16,6 +16,7 @@ class SkillData {
|
|||||||
id: 'stack_trace',
|
id: 'stack_trace',
|
||||||
name: 'Stack Trace',
|
name: 'Stack Trace',
|
||||||
type: SkillType.attack,
|
type: SkillType.attack,
|
||||||
|
tier: 1,
|
||||||
mpCost: 10,
|
mpCost: 10,
|
||||||
cooldownMs: 3000,
|
cooldownMs: 3000,
|
||||||
power: 15,
|
power: 15,
|
||||||
@@ -27,6 +28,7 @@ class SkillData {
|
|||||||
id: 'core_dump',
|
id: 'core_dump',
|
||||||
name: 'Core Dump',
|
name: 'Core Dump',
|
||||||
type: SkillType.attack,
|
type: SkillType.attack,
|
||||||
|
tier: 3,
|
||||||
mpCost: 35,
|
mpCost: 35,
|
||||||
cooldownMs: 12000,
|
cooldownMs: 12000,
|
||||||
power: 30,
|
power: 30,
|
||||||
@@ -38,6 +40,7 @@ class SkillData {
|
|||||||
id: 'memory_dump',
|
id: 'memory_dump',
|
||||||
name: 'Memory Dump',
|
name: 'Memory Dump',
|
||||||
type: SkillType.attack,
|
type: SkillType.attack,
|
||||||
|
tier: 3,
|
||||||
mpCost: 25,
|
mpCost: 25,
|
||||||
cooldownMs: 15000,
|
cooldownMs: 15000,
|
||||||
power: 0,
|
power: 0,
|
||||||
@@ -53,6 +56,7 @@ class SkillData {
|
|||||||
id: 'kernel_panic',
|
id: 'kernel_panic',
|
||||||
name: 'Kernel Panic',
|
name: 'Kernel Panic',
|
||||||
type: SkillType.attack,
|
type: SkillType.attack,
|
||||||
|
tier: 5,
|
||||||
mpCost: 80,
|
mpCost: 80,
|
||||||
cooldownMs: 45000,
|
cooldownMs: 45000,
|
||||||
power: 60,
|
power: 60,
|
||||||
@@ -65,6 +69,7 @@ class SkillData {
|
|||||||
id: 'blue_screen',
|
id: 'blue_screen',
|
||||||
name: 'Blue Screen',
|
name: 'Blue Screen',
|
||||||
type: SkillType.attack,
|
type: SkillType.attack,
|
||||||
|
tier: 4,
|
||||||
mpCost: 60,
|
mpCost: 60,
|
||||||
cooldownMs: 30000,
|
cooldownMs: 30000,
|
||||||
power: 50,
|
power: 50,
|
||||||
@@ -76,6 +81,7 @@ class SkillData {
|
|||||||
id: 'inject_code',
|
id: 'inject_code',
|
||||||
name: 'Inject Code',
|
name: 'Inject Code',
|
||||||
type: SkillType.attack,
|
type: SkillType.attack,
|
||||||
|
tier: 4,
|
||||||
mpCost: 40,
|
mpCost: 40,
|
||||||
cooldownMs: 18000,
|
cooldownMs: 18000,
|
||||||
power: 35,
|
power: 35,
|
||||||
@@ -88,6 +94,7 @@ class SkillData {
|
|||||||
id: 'spawn_shell',
|
id: 'spawn_shell',
|
||||||
name: 'Spawn Shell',
|
name: 'Spawn Shell',
|
||||||
type: SkillType.attack,
|
type: SkillType.attack,
|
||||||
|
tier: 3,
|
||||||
mpCost: 30,
|
mpCost: 30,
|
||||||
cooldownMs: 10000,
|
cooldownMs: 10000,
|
||||||
power: 12,
|
power: 12,
|
||||||
@@ -100,6 +107,7 @@ class SkillData {
|
|||||||
id: 'thread_pool',
|
id: 'thread_pool',
|
||||||
name: 'Thread Pool',
|
name: 'Thread Pool',
|
||||||
type: SkillType.attack,
|
type: SkillType.attack,
|
||||||
|
tier: 4,
|
||||||
mpCost: 45,
|
mpCost: 45,
|
||||||
cooldownMs: 15000,
|
cooldownMs: 15000,
|
||||||
power: 10,
|
power: 10,
|
||||||
@@ -112,6 +120,7 @@ class SkillData {
|
|||||||
id: 'exfiltrate_data',
|
id: 'exfiltrate_data',
|
||||||
name: 'Exfiltrate Data',
|
name: 'Exfiltrate Data',
|
||||||
type: SkillType.attack,
|
type: SkillType.attack,
|
||||||
|
tier: 4,
|
||||||
mpCost: 35,
|
mpCost: 35,
|
||||||
cooldownMs: 12000,
|
cooldownMs: 12000,
|
||||||
power: 25,
|
power: 25,
|
||||||
@@ -124,6 +133,7 @@ class SkillData {
|
|||||||
id: 'fuzzing',
|
id: 'fuzzing',
|
||||||
name: 'Fuzzing',
|
name: 'Fuzzing',
|
||||||
type: SkillType.attack,
|
type: SkillType.attack,
|
||||||
|
tier: 2,
|
||||||
mpCost: 20,
|
mpCost: 20,
|
||||||
cooldownMs: 8000,
|
cooldownMs: 8000,
|
||||||
power: 20,
|
power: 20,
|
||||||
@@ -136,6 +146,7 @@ class SkillData {
|
|||||||
id: 'chaos_monkey',
|
id: 'chaos_monkey',
|
||||||
name: 'Chaos Monkey',
|
name: 'Chaos Monkey',
|
||||||
type: SkillType.attack,
|
type: SkillType.attack,
|
||||||
|
tier: 5,
|
||||||
mpCost: 50,
|
mpCost: 50,
|
||||||
cooldownMs: 25000,
|
cooldownMs: 25000,
|
||||||
power: 40,
|
power: 40,
|
||||||
@@ -148,6 +159,7 @@ class SkillData {
|
|||||||
id: 'saga_pattern',
|
id: 'saga_pattern',
|
||||||
name: 'Saga Pattern',
|
name: 'Saga Pattern',
|
||||||
type: SkillType.attack,
|
type: SkillType.attack,
|
||||||
|
tier: 4,
|
||||||
mpCost: 55,
|
mpCost: 55,
|
||||||
cooldownMs: 20000,
|
cooldownMs: 20000,
|
||||||
power: 18,
|
power: 18,
|
||||||
@@ -160,6 +172,7 @@ class SkillData {
|
|||||||
id: 'event_store',
|
id: 'event_store',
|
||||||
name: 'Event Store',
|
name: 'Event Store',
|
||||||
type: SkillType.attack,
|
type: SkillType.attack,
|
||||||
|
tier: 4,
|
||||||
mpCost: 40,
|
mpCost: 40,
|
||||||
cooldownMs: 18000,
|
cooldownMs: 18000,
|
||||||
power: 0,
|
power: 0,
|
||||||
@@ -175,6 +188,7 @@ class SkillData {
|
|||||||
id: 'auto_scale',
|
id: 'auto_scale',
|
||||||
name: 'Auto Scale',
|
name: 'Auto Scale',
|
||||||
type: SkillType.attack,
|
type: SkillType.attack,
|
||||||
|
tier: 4,
|
||||||
mpCost: 45,
|
mpCost: 45,
|
||||||
cooldownMs: 20000,
|
cooldownMs: 20000,
|
||||||
power: 30,
|
power: 30,
|
||||||
@@ -186,6 +200,7 @@ class SkillData {
|
|||||||
id: 'disassemble',
|
id: 'disassemble',
|
||||||
name: 'Disassemble',
|
name: 'Disassemble',
|
||||||
type: SkillType.attack,
|
type: SkillType.attack,
|
||||||
|
tier: 3,
|
||||||
mpCost: 30,
|
mpCost: 30,
|
||||||
cooldownMs: 12000,
|
cooldownMs: 12000,
|
||||||
power: 22,
|
power: 22,
|
||||||
@@ -198,6 +213,7 @@ class SkillData {
|
|||||||
id: 'decompile',
|
id: 'decompile',
|
||||||
name: 'Decompile',
|
name: 'Decompile',
|
||||||
type: SkillType.attack,
|
type: SkillType.attack,
|
||||||
|
tier: 2,
|
||||||
mpCost: 25,
|
mpCost: 25,
|
||||||
cooldownMs: 10000,
|
cooldownMs: 10000,
|
||||||
power: 20,
|
power: 20,
|
||||||
@@ -209,6 +225,7 @@ class SkillData {
|
|||||||
id: 'canary_release',
|
id: 'canary_release',
|
||||||
name: 'Canary Release',
|
name: 'Canary Release',
|
||||||
type: SkillType.attack,
|
type: SkillType.attack,
|
||||||
|
tier: 1,
|
||||||
mpCost: 15,
|
mpCost: 15,
|
||||||
cooldownMs: 6000,
|
cooldownMs: 6000,
|
||||||
power: 12,
|
power: 12,
|
||||||
@@ -220,6 +237,7 @@ class SkillData {
|
|||||||
id: 'ab_test',
|
id: 'ab_test',
|
||||||
name: 'A/B Test',
|
name: 'A/B Test',
|
||||||
type: SkillType.attack,
|
type: SkillType.attack,
|
||||||
|
tier: 2,
|
||||||
mpCost: 35,
|
mpCost: 35,
|
||||||
cooldownMs: 12000,
|
cooldownMs: 12000,
|
||||||
power: 15,
|
power: 15,
|
||||||
@@ -232,6 +250,7 @@ class SkillData {
|
|||||||
id: 'pivot_network',
|
id: 'pivot_network',
|
||||||
name: 'Pivot Network',
|
name: 'Pivot Network',
|
||||||
type: SkillType.attack,
|
type: SkillType.attack,
|
||||||
|
tier: 3,
|
||||||
mpCost: 30,
|
mpCost: 30,
|
||||||
cooldownMs: 10000,
|
cooldownMs: 10000,
|
||||||
power: 25,
|
power: 25,
|
||||||
@@ -244,6 +263,7 @@ class SkillData {
|
|||||||
id: 'async_await',
|
id: 'async_await',
|
||||||
name: 'Async Await',
|
name: 'Async Await',
|
||||||
type: SkillType.attack,
|
type: SkillType.attack,
|
||||||
|
tier: 4,
|
||||||
mpCost: 35,
|
mpCost: 35,
|
||||||
cooldownMs: 14000,
|
cooldownMs: 14000,
|
||||||
power: 35,
|
power: 35,
|
||||||
@@ -255,6 +275,7 @@ class SkillData {
|
|||||||
id: 'event_source',
|
id: 'event_source',
|
||||||
name: 'Event Source',
|
name: 'Event Source',
|
||||||
type: SkillType.attack,
|
type: SkillType.attack,
|
||||||
|
tier: 3,
|
||||||
mpCost: 30,
|
mpCost: 30,
|
||||||
cooldownMs: 12000,
|
cooldownMs: 12000,
|
||||||
power: 0,
|
power: 0,
|
||||||
@@ -270,6 +291,7 @@ class SkillData {
|
|||||||
id: 'cqrs_split',
|
id: 'cqrs_split',
|
||||||
name: 'CQRS Split',
|
name: 'CQRS Split',
|
||||||
type: SkillType.attack,
|
type: SkillType.attack,
|
||||||
|
tier: 4,
|
||||||
mpCost: 40,
|
mpCost: 40,
|
||||||
cooldownMs: 15000,
|
cooldownMs: 15000,
|
||||||
power: 20,
|
power: 20,
|
||||||
@@ -286,6 +308,7 @@ class SkillData {
|
|||||||
id: 'garbage_collection',
|
id: 'garbage_collection',
|
||||||
name: 'Garbage Collection',
|
name: 'Garbage Collection',
|
||||||
type: SkillType.heal,
|
type: SkillType.heal,
|
||||||
|
tier: 3,
|
||||||
mpCost: 25,
|
mpCost: 25,
|
||||||
cooldownMs: 15000,
|
cooldownMs: 15000,
|
||||||
power: 0,
|
power: 0,
|
||||||
@@ -297,6 +320,7 @@ class SkillData {
|
|||||||
id: 'hot_reload',
|
id: 'hot_reload',
|
||||||
name: 'Hot Reload',
|
name: 'Hot Reload',
|
||||||
type: SkillType.heal,
|
type: SkillType.heal,
|
||||||
|
tier: 1,
|
||||||
mpCost: 15,
|
mpCost: 15,
|
||||||
cooldownMs: 8000,
|
cooldownMs: 8000,
|
||||||
power: 0,
|
power: 0,
|
||||||
@@ -308,6 +332,7 @@ class SkillData {
|
|||||||
id: 'rollback',
|
id: 'rollback',
|
||||||
name: 'Rollback',
|
name: 'Rollback',
|
||||||
type: SkillType.heal,
|
type: SkillType.heal,
|
||||||
|
tier: 4,
|
||||||
mpCost: 30,
|
mpCost: 30,
|
||||||
cooldownMs: 18000,
|
cooldownMs: 18000,
|
||||||
power: 0,
|
power: 0,
|
||||||
@@ -319,6 +344,7 @@ class SkillData {
|
|||||||
id: 'hotfix',
|
id: 'hotfix',
|
||||||
name: 'Hotfix',
|
name: 'Hotfix',
|
||||||
type: SkillType.heal,
|
type: SkillType.heal,
|
||||||
|
tier: 1,
|
||||||
mpCost: 10,
|
mpCost: 10,
|
||||||
cooldownMs: 6000,
|
cooldownMs: 6000,
|
||||||
power: 0,
|
power: 0,
|
||||||
@@ -330,6 +356,7 @@ class SkillData {
|
|||||||
id: 'snapshot_restore',
|
id: 'snapshot_restore',
|
||||||
name: 'Snapshot Restore',
|
name: 'Snapshot Restore',
|
||||||
type: SkillType.heal,
|
type: SkillType.heal,
|
||||||
|
tier: 5,
|
||||||
mpCost: 50,
|
mpCost: 50,
|
||||||
cooldownMs: 30000,
|
cooldownMs: 30000,
|
||||||
power: 0,
|
power: 0,
|
||||||
@@ -341,6 +368,7 @@ class SkillData {
|
|||||||
id: 'patch_binary',
|
id: 'patch_binary',
|
||||||
name: 'Patch Binary',
|
name: 'Patch Binary',
|
||||||
type: SkillType.heal,
|
type: SkillType.heal,
|
||||||
|
tier: 4,
|
||||||
mpCost: 35,
|
mpCost: 35,
|
||||||
cooldownMs: 20000,
|
cooldownMs: 20000,
|
||||||
power: 0,
|
power: 0,
|
||||||
@@ -358,6 +386,7 @@ class SkillData {
|
|||||||
id: 'git_commit',
|
id: 'git_commit',
|
||||||
name: 'Git Commit',
|
name: 'Git Commit',
|
||||||
type: SkillType.heal,
|
type: SkillType.heal,
|
||||||
|
tier: 2,
|
||||||
mpCost: 20,
|
mpCost: 20,
|
||||||
cooldownMs: 12000,
|
cooldownMs: 12000,
|
||||||
power: 0,
|
power: 0,
|
||||||
@@ -369,6 +398,7 @@ class SkillData {
|
|||||||
id: 'git_push',
|
id: 'git_push',
|
||||||
name: 'Git Push',
|
name: 'Git Push',
|
||||||
type: SkillType.heal,
|
type: SkillType.heal,
|
||||||
|
tier: 3,
|
||||||
mpCost: 25,
|
mpCost: 25,
|
||||||
cooldownMs: 15000,
|
cooldownMs: 15000,
|
||||||
power: 0,
|
power: 0,
|
||||||
@@ -380,6 +410,7 @@ class SkillData {
|
|||||||
id: 'connection_pool',
|
id: 'connection_pool',
|
||||||
name: 'Connection Pool',
|
name: 'Connection Pool',
|
||||||
type: SkillType.heal,
|
type: SkillType.heal,
|
||||||
|
tier: 3,
|
||||||
mpCost: 0,
|
mpCost: 0,
|
||||||
cooldownMs: 20000,
|
cooldownMs: 20000,
|
||||||
power: 0,
|
power: 0,
|
||||||
@@ -391,6 +422,7 @@ class SkillData {
|
|||||||
id: 'load_balance',
|
id: 'load_balance',
|
||||||
name: 'Load Balance',
|
name: 'Load Balance',
|
||||||
type: SkillType.heal,
|
type: SkillType.heal,
|
||||||
|
tier: 4,
|
||||||
mpCost: 20,
|
mpCost: 20,
|
||||||
cooldownMs: 15000,
|
cooldownMs: 15000,
|
||||||
power: 0,
|
power: 0,
|
||||||
@@ -403,6 +435,7 @@ class SkillData {
|
|||||||
id: 'blue_green_deploy',
|
id: 'blue_green_deploy',
|
||||||
name: 'Blue Green Deploy',
|
name: 'Blue Green Deploy',
|
||||||
type: SkillType.heal,
|
type: SkillType.heal,
|
||||||
|
tier: 4,
|
||||||
mpCost: 30,
|
mpCost: 30,
|
||||||
cooldownMs: 25000,
|
cooldownMs: 25000,
|
||||||
power: 0,
|
power: 0,
|
||||||
@@ -414,6 +447,7 @@ class SkillData {
|
|||||||
id: 'cache_invalidate',
|
id: 'cache_invalidate',
|
||||||
name: 'Cache Invalidate',
|
name: 'Cache Invalidate',
|
||||||
type: SkillType.heal,
|
type: SkillType.heal,
|
||||||
|
tier: 2,
|
||||||
mpCost: 25,
|
mpCost: 25,
|
||||||
cooldownMs: 18000,
|
cooldownMs: 18000,
|
||||||
power: 0,
|
power: 0,
|
||||||
@@ -429,6 +463,7 @@ class SkillData {
|
|||||||
id: 'debug_mode',
|
id: 'debug_mode',
|
||||||
name: 'Debug Mode',
|
name: 'Debug Mode',
|
||||||
type: SkillType.buff,
|
type: SkillType.buff,
|
||||||
|
tier: 3,
|
||||||
mpCost: 20,
|
mpCost: 20,
|
||||||
cooldownMs: 20000,
|
cooldownMs: 20000,
|
||||||
power: 0,
|
power: 0,
|
||||||
@@ -445,6 +480,7 @@ class SkillData {
|
|||||||
id: 'safe_mode',
|
id: 'safe_mode',
|
||||||
name: 'Safe Mode',
|
name: 'Safe Mode',
|
||||||
type: SkillType.buff,
|
type: SkillType.buff,
|
||||||
|
tier: 3,
|
||||||
mpCost: 25,
|
mpCost: 25,
|
||||||
cooldownMs: 25000,
|
cooldownMs: 25000,
|
||||||
power: 0,
|
power: 0,
|
||||||
@@ -461,6 +497,7 @@ class SkillData {
|
|||||||
id: 'memory_optimization',
|
id: 'memory_optimization',
|
||||||
name: 'Memory Optimization',
|
name: 'Memory Optimization',
|
||||||
type: SkillType.buff,
|
type: SkillType.buff,
|
||||||
|
tier: 3,
|
||||||
mpCost: 30,
|
mpCost: 30,
|
||||||
cooldownMs: 30000,
|
cooldownMs: 30000,
|
||||||
power: 0,
|
power: 0,
|
||||||
@@ -480,6 +517,7 @@ class SkillData {
|
|||||||
id: 'breakpoint',
|
id: 'breakpoint',
|
||||||
name: 'Breakpoint',
|
name: 'Breakpoint',
|
||||||
type: SkillType.buff,
|
type: SkillType.buff,
|
||||||
|
tier: 2,
|
||||||
mpCost: 15,
|
mpCost: 15,
|
||||||
cooldownMs: 12000,
|
cooldownMs: 12000,
|
||||||
power: 0,
|
power: 0,
|
||||||
@@ -496,6 +534,7 @@ class SkillData {
|
|||||||
id: 'watch_variable',
|
id: 'watch_variable',
|
||||||
name: 'Watch Variable',
|
name: 'Watch Variable',
|
||||||
type: SkillType.buff,
|
type: SkillType.buff,
|
||||||
|
tier: 2,
|
||||||
mpCost: 18,
|
mpCost: 18,
|
||||||
cooldownMs: 15000,
|
cooldownMs: 15000,
|
||||||
power: 0,
|
power: 0,
|
||||||
@@ -512,6 +551,7 @@ class SkillData {
|
|||||||
id: 'step_into',
|
id: 'step_into',
|
||||||
name: 'Step Into',
|
name: 'Step Into',
|
||||||
type: SkillType.buff,
|
type: SkillType.buff,
|
||||||
|
tier: 1,
|
||||||
mpCost: 15,
|
mpCost: 15,
|
||||||
cooldownMs: 12000,
|
cooldownMs: 12000,
|
||||||
power: 0,
|
power: 0,
|
||||||
@@ -528,6 +568,7 @@ class SkillData {
|
|||||||
id: 'profile_run',
|
id: 'profile_run',
|
||||||
name: 'Profile Run',
|
name: 'Profile Run',
|
||||||
type: SkillType.buff,
|
type: SkillType.buff,
|
||||||
|
tier: 3,
|
||||||
mpCost: 20,
|
mpCost: 20,
|
||||||
cooldownMs: 18000,
|
cooldownMs: 18000,
|
||||||
power: 0,
|
power: 0,
|
||||||
@@ -544,6 +585,7 @@ class SkillData {
|
|||||||
id: 'benchmark',
|
id: 'benchmark',
|
||||||
name: 'Benchmark',
|
name: 'Benchmark',
|
||||||
type: SkillType.buff,
|
type: SkillType.buff,
|
||||||
|
tier: 4,
|
||||||
mpCost: 25,
|
mpCost: 25,
|
||||||
cooldownMs: 20000,
|
cooldownMs: 20000,
|
||||||
power: 0,
|
power: 0,
|
||||||
@@ -560,6 +602,7 @@ class SkillData {
|
|||||||
id: 'elevate_privilege',
|
id: 'elevate_privilege',
|
||||||
name: 'Elevate Privilege',
|
name: 'Elevate Privilege',
|
||||||
type: SkillType.buff,
|
type: SkillType.buff,
|
||||||
|
tier: 5,
|
||||||
mpCost: 40,
|
mpCost: 40,
|
||||||
cooldownMs: 35000,
|
cooldownMs: 35000,
|
||||||
power: 0,
|
power: 0,
|
||||||
@@ -579,6 +622,7 @@ class SkillData {
|
|||||||
id: 'scale_up',
|
id: 'scale_up',
|
||||||
name: 'Scale Up',
|
name: 'Scale Up',
|
||||||
type: SkillType.buff,
|
type: SkillType.buff,
|
||||||
|
tier: 5,
|
||||||
mpCost: 35,
|
mpCost: 35,
|
||||||
cooldownMs: 30000,
|
cooldownMs: 30000,
|
||||||
power: 0,
|
power: 0,
|
||||||
@@ -595,6 +639,7 @@ class SkillData {
|
|||||||
id: 'failover',
|
id: 'failover',
|
||||||
name: 'Failover',
|
name: 'Failover',
|
||||||
type: SkillType.buff,
|
type: SkillType.buff,
|
||||||
|
tier: 5,
|
||||||
mpCost: 30,
|
mpCost: 30,
|
||||||
cooldownMs: 45000,
|
cooldownMs: 45000,
|
||||||
power: 0,
|
power: 0,
|
||||||
@@ -611,6 +656,7 @@ class SkillData {
|
|||||||
id: 'containerize',
|
id: 'containerize',
|
||||||
name: 'Containerize',
|
name: 'Containerize',
|
||||||
type: SkillType.buff,
|
type: SkillType.buff,
|
||||||
|
tier: 3,
|
||||||
mpCost: 25,
|
mpCost: 25,
|
||||||
cooldownMs: 20000,
|
cooldownMs: 20000,
|
||||||
power: 0,
|
power: 0,
|
||||||
@@ -627,6 +673,7 @@ class SkillData {
|
|||||||
id: 'orchestrate',
|
id: 'orchestrate',
|
||||||
name: 'Orchestrate',
|
name: 'Orchestrate',
|
||||||
type: SkillType.buff,
|
type: SkillType.buff,
|
||||||
|
tier: 4,
|
||||||
mpCost: 45,
|
mpCost: 45,
|
||||||
cooldownMs: 40000,
|
cooldownMs: 40000,
|
||||||
power: 0,
|
power: 0,
|
||||||
@@ -645,6 +692,7 @@ class SkillData {
|
|||||||
id: 'promise_resolve',
|
id: 'promise_resolve',
|
||||||
name: 'Promise Resolve',
|
name: 'Promise Resolve',
|
||||||
type: SkillType.buff,
|
type: SkillType.buff,
|
||||||
|
tier: 2,
|
||||||
mpCost: 20,
|
mpCost: 20,
|
||||||
cooldownMs: 15000,
|
cooldownMs: 15000,
|
||||||
power: 0,
|
power: 0,
|
||||||
@@ -661,6 +709,7 @@ class SkillData {
|
|||||||
id: 'feature_toggle',
|
id: 'feature_toggle',
|
||||||
name: 'Feature Toggle',
|
name: 'Feature Toggle',
|
||||||
type: SkillType.buff,
|
type: SkillType.buff,
|
||||||
|
tier: 1,
|
||||||
mpCost: 15,
|
mpCost: 15,
|
||||||
cooldownMs: 10000,
|
cooldownMs: 10000,
|
||||||
power: 0,
|
power: 0,
|
||||||
@@ -678,6 +727,7 @@ class SkillData {
|
|||||||
id: 'dark_launch',
|
id: 'dark_launch',
|
||||||
name: 'Dark Launch',
|
name: 'Dark Launch',
|
||||||
type: SkillType.buff,
|
type: SkillType.buff,
|
||||||
|
tier: 4,
|
||||||
mpCost: 35,
|
mpCost: 35,
|
||||||
cooldownMs: 30000,
|
cooldownMs: 30000,
|
||||||
power: 0,
|
power: 0,
|
||||||
@@ -694,6 +744,7 @@ class SkillData {
|
|||||||
id: 'static_analysis',
|
id: 'static_analysis',
|
||||||
name: 'Static Analysis',
|
name: 'Static Analysis',
|
||||||
type: SkillType.buff,
|
type: SkillType.buff,
|
||||||
|
tier: 1,
|
||||||
mpCost: 15,
|
mpCost: 15,
|
||||||
cooldownMs: 12000,
|
cooldownMs: 12000,
|
||||||
power: 0,
|
power: 0,
|
||||||
@@ -710,6 +761,7 @@ class SkillData {
|
|||||||
id: 'dynamic_analysis',
|
id: 'dynamic_analysis',
|
||||||
name: 'Dynamic Analysis',
|
name: 'Dynamic Analysis',
|
||||||
type: SkillType.buff,
|
type: SkillType.buff,
|
||||||
|
tier: 4,
|
||||||
mpCost: 25,
|
mpCost: 25,
|
||||||
cooldownMs: 18000,
|
cooldownMs: 18000,
|
||||||
power: 0,
|
power: 0,
|
||||||
@@ -727,6 +779,7 @@ class SkillData {
|
|||||||
id: 'reverse_engineer',
|
id: 'reverse_engineer',
|
||||||
name: 'Reverse Engineer',
|
name: 'Reverse Engineer',
|
||||||
type: SkillType.buff,
|
type: SkillType.buff,
|
||||||
|
tier: 4,
|
||||||
mpCost: 30,
|
mpCost: 30,
|
||||||
cooldownMs: 25000,
|
cooldownMs: 25000,
|
||||||
power: 0,
|
power: 0,
|
||||||
@@ -743,6 +796,7 @@ class SkillData {
|
|||||||
id: 'cover_tracks',
|
id: 'cover_tracks',
|
||||||
name: 'Cover Tracks',
|
name: 'Cover Tracks',
|
||||||
type: SkillType.buff,
|
type: SkillType.buff,
|
||||||
|
tier: 2,
|
||||||
mpCost: 20,
|
mpCost: 20,
|
||||||
cooldownMs: 15000,
|
cooldownMs: 15000,
|
||||||
power: 0,
|
power: 0,
|
||||||
@@ -759,6 +813,7 @@ class SkillData {
|
|||||||
id: 'deploy',
|
id: 'deploy',
|
||||||
name: 'Deploy',
|
name: 'Deploy',
|
||||||
type: SkillType.buff,
|
type: SkillType.buff,
|
||||||
|
tier: 4,
|
||||||
mpCost: 35,
|
mpCost: 35,
|
||||||
cooldownMs: 30000,
|
cooldownMs: 30000,
|
||||||
power: 0,
|
power: 0,
|
||||||
@@ -775,6 +830,7 @@ class SkillData {
|
|||||||
id: 'retry_logic',
|
id: 'retry_logic',
|
||||||
name: 'Retry Logic',
|
name: 'Retry Logic',
|
||||||
type: SkillType.buff,
|
type: SkillType.buff,
|
||||||
|
tier: 2,
|
||||||
mpCost: 20,
|
mpCost: 20,
|
||||||
cooldownMs: 15000,
|
cooldownMs: 15000,
|
||||||
power: 0,
|
power: 0,
|
||||||
@@ -791,6 +847,7 @@ class SkillData {
|
|||||||
id: 'state_machine',
|
id: 'state_machine',
|
||||||
name: 'State Machine',
|
name: 'State Machine',
|
||||||
type: SkillType.buff,
|
type: SkillType.buff,
|
||||||
|
tier: 2,
|
||||||
mpCost: 30,
|
mpCost: 30,
|
||||||
cooldownMs: 25000,
|
cooldownMs: 25000,
|
||||||
power: 0,
|
power: 0,
|
||||||
@@ -812,6 +869,7 @@ class SkillData {
|
|||||||
id: 'step_over',
|
id: 'step_over',
|
||||||
name: 'Step Over',
|
name: 'Step Over',
|
||||||
type: SkillType.debuff,
|
type: SkillType.debuff,
|
||||||
|
tier: 1,
|
||||||
mpCost: 20,
|
mpCost: 20,
|
||||||
cooldownMs: 15000,
|
cooldownMs: 15000,
|
||||||
power: 0,
|
power: 0,
|
||||||
@@ -828,6 +886,7 @@ class SkillData {
|
|||||||
id: 'cold_boot',
|
id: 'cold_boot',
|
||||||
name: 'Cold Boot',
|
name: 'Cold Boot',
|
||||||
type: SkillType.debuff,
|
type: SkillType.debuff,
|
||||||
|
tier: 4,
|
||||||
mpCost: 30,
|
mpCost: 30,
|
||||||
cooldownMs: 25000,
|
cooldownMs: 25000,
|
||||||
power: 0,
|
power: 0,
|
||||||
@@ -845,6 +904,7 @@ class SkillData {
|
|||||||
id: 'heap_analysis',
|
id: 'heap_analysis',
|
||||||
name: 'Heap Analysis',
|
name: 'Heap Analysis',
|
||||||
type: SkillType.debuff,
|
type: SkillType.debuff,
|
||||||
|
tier: 2,
|
||||||
mpCost: 25,
|
mpCost: 25,
|
||||||
cooldownMs: 18000,
|
cooldownMs: 18000,
|
||||||
power: 0,
|
power: 0,
|
||||||
@@ -861,6 +921,7 @@ class SkillData {
|
|||||||
id: 'unit_test',
|
id: 'unit_test',
|
||||||
name: 'Unit Test',
|
name: 'Unit Test',
|
||||||
type: SkillType.debuff,
|
type: SkillType.debuff,
|
||||||
|
tier: 1,
|
||||||
mpCost: 20,
|
mpCost: 20,
|
||||||
cooldownMs: 15000,
|
cooldownMs: 15000,
|
||||||
power: 0,
|
power: 0,
|
||||||
@@ -877,6 +938,7 @@ class SkillData {
|
|||||||
id: 'integration_test',
|
id: 'integration_test',
|
||||||
name: 'Integration Test',
|
name: 'Integration Test',
|
||||||
type: SkillType.debuff,
|
type: SkillType.debuff,
|
||||||
|
tier: 2,
|
||||||
mpCost: 25,
|
mpCost: 25,
|
||||||
cooldownMs: 18000,
|
cooldownMs: 18000,
|
||||||
power: 0,
|
power: 0,
|
||||||
@@ -894,6 +956,7 @@ class SkillData {
|
|||||||
id: 'sanitizer',
|
id: 'sanitizer',
|
||||||
name: 'Sanitizer',
|
name: 'Sanitizer',
|
||||||
type: SkillType.debuff,
|
type: SkillType.debuff,
|
||||||
|
tier: 3,
|
||||||
mpCost: 30,
|
mpCost: 30,
|
||||||
cooldownMs: 20000,
|
cooldownMs: 20000,
|
||||||
power: 0,
|
power: 0,
|
||||||
@@ -911,6 +974,7 @@ class SkillData {
|
|||||||
id: 'hook_function',
|
id: 'hook_function',
|
||||||
name: 'Hook Function',
|
name: 'Hook Function',
|
||||||
type: SkillType.debuff,
|
type: SkillType.debuff,
|
||||||
|
tier: 2,
|
||||||
mpCost: 25,
|
mpCost: 25,
|
||||||
cooldownMs: 22000,
|
cooldownMs: 22000,
|
||||||
power: 0,
|
power: 0,
|
||||||
@@ -927,6 +991,7 @@ class SkillData {
|
|||||||
id: 'rate_limit',
|
id: 'rate_limit',
|
||||||
name: 'Rate Limit',
|
name: 'Rate Limit',
|
||||||
type: SkillType.debuff,
|
type: SkillType.debuff,
|
||||||
|
tier: 3,
|
||||||
mpCost: 30,
|
mpCost: 30,
|
||||||
cooldownMs: 25000,
|
cooldownMs: 25000,
|
||||||
power: 0,
|
power: 0,
|
||||||
@@ -943,6 +1008,7 @@ class SkillData {
|
|||||||
id: 'circuit_break',
|
id: 'circuit_break',
|
||||||
name: 'Circuit Break',
|
name: 'Circuit Break',
|
||||||
type: SkillType.debuff,
|
type: SkillType.debuff,
|
||||||
|
tier: 4,
|
||||||
mpCost: 35,
|
mpCost: 35,
|
||||||
cooldownMs: 30000,
|
cooldownMs: 30000,
|
||||||
power: 0,
|
power: 0,
|
||||||
@@ -959,6 +1025,7 @@ class SkillData {
|
|||||||
id: 'backpressure',
|
id: 'backpressure',
|
||||||
name: 'Backpressure',
|
name: 'Backpressure',
|
||||||
type: SkillType.debuff,
|
type: SkillType.debuff,
|
||||||
|
tier: 2,
|
||||||
mpCost: 25,
|
mpCost: 25,
|
||||||
cooldownMs: 20000,
|
cooldownMs: 20000,
|
||||||
power: 0,
|
power: 0,
|
||||||
@@ -975,6 +1042,7 @@ class SkillData {
|
|||||||
id: 'git_merge',
|
id: 'git_merge',
|
||||||
name: 'Git Merge',
|
name: 'Git Merge',
|
||||||
type: SkillType.debuff,
|
type: SkillType.debuff,
|
||||||
|
tier: 2,
|
||||||
mpCost: 20,
|
mpCost: 20,
|
||||||
cooldownMs: 15000,
|
cooldownMs: 15000,
|
||||||
power: 0,
|
power: 0,
|
||||||
|
|||||||
@@ -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/engine/item_service.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';
|
||||||
@@ -43,12 +44,17 @@ class GameMutations {
|
|||||||
return state.copyWith(rng: state.rng, stats: updatedStats);
|
return state.copyWith(rng: state.rng, stats: updatedStats);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 스펠 획득 (원본 WinSpell)
|
||||||
|
///
|
||||||
|
/// 스펠북에 추가하고, 전투용 스킬 슬롯에도 자동으로 장착 시도.
|
||||||
|
/// 슬롯이 가득 찬 경우 기존 스킬보다 강할 때만 교체됨.
|
||||||
GameState winSpell(GameState state, int wisdom, int level) {
|
GameState winSpell(GameState state, int wisdom, int level) {
|
||||||
final result = pq_logic.winSpell(config, state.rng, wisdom, level);
|
final result = pq_logic.winSpell(config, state.rng, wisdom, level);
|
||||||
final parts = result.split('|');
|
final parts = result.split('|');
|
||||||
final name = parts[0];
|
final name = parts[0];
|
||||||
final rank = parts.length > 1 ? parts[1] : 'I';
|
final rank = parts.length > 1 ? parts[1] : 'I';
|
||||||
|
|
||||||
|
// 스펠북 업데이트
|
||||||
final skills = [...state.skillBook.skills];
|
final skills = [...state.skillBook.skills];
|
||||||
final index = skills.indexWhere((s) => s.name == name);
|
final index = skills.indexWhere((s) => s.name == name);
|
||||||
if (index >= 0) {
|
if (index >= 0) {
|
||||||
@@ -57,9 +63,20 @@ class GameMutations {
|
|||||||
skills.add(SkillEntry(name: name, rank: rank));
|
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(
|
return state.copyWith(
|
||||||
rng: state.rng,
|
rng: state.rng,
|
||||||
skillBook: state.skillBook.copyWith(skills: skills),
|
skillBook: state.skillBook.copyWith(skills: skills),
|
||||||
|
skillSystem: skillSystem,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -512,6 +512,7 @@ class ProgressService {
|
|||||||
caption: taskResult.caption,
|
caption: taskResult.caption,
|
||||||
type: TaskType.market,
|
type: TaskType.market,
|
||||||
),
|
),
|
||||||
|
currentCombat: null, // 비전투 태스크이므로 전투 상태 초기화
|
||||||
);
|
);
|
||||||
return (progress: progress, queue: queue);
|
return (progress: progress, queue: queue);
|
||||||
}
|
}
|
||||||
@@ -536,6 +537,7 @@ class ProgressService {
|
|||||||
caption: taskResult.caption,
|
caption: taskResult.caption,
|
||||||
type: TaskType.buying,
|
type: TaskType.buying,
|
||||||
),
|
),
|
||||||
|
currentCombat: null, // 비전투 태스크이므로 전투 상태 초기화
|
||||||
);
|
);
|
||||||
return (progress: progress, queue: queue);
|
return (progress: progress, queue: queue);
|
||||||
}
|
}
|
||||||
@@ -551,6 +553,7 @@ class ProgressService {
|
|||||||
caption: taskResult.caption,
|
caption: taskResult.caption,
|
||||||
type: TaskType.neutral,
|
type: TaskType.neutral,
|
||||||
),
|
),
|
||||||
|
currentCombat: null, // 비전투 태스크이므로 전투 상태 초기화
|
||||||
);
|
);
|
||||||
return (progress: progress, queue: queue);
|
return (progress: progress, queue: queue);
|
||||||
}
|
}
|
||||||
@@ -672,7 +675,7 @@ class ProgressService {
|
|||||||
type: TaskType.kill,
|
type: TaskType.kill,
|
||||||
monsterBaseName: monsterResult.baseName,
|
monsterBaseName: monsterResult.baseName,
|
||||||
monsterPart: monsterResult.part,
|
monsterPart: monsterResult.part,
|
||||||
monsterLevel: monsterResult.level,
|
monsterLevel: effectiveMonsterLevel,
|
||||||
monsterGrade: monsterResult.grade,
|
monsterGrade: monsterResult.grade,
|
||||||
),
|
),
|
||||||
currentCombat: combatState,
|
currentCombat: combatState,
|
||||||
@@ -1205,6 +1208,7 @@ class ProgressService {
|
|||||||
);
|
);
|
||||||
final progress = taskResult.progress.copyWith(
|
final progress = taskResult.progress.copyWith(
|
||||||
currentTask: TaskInfo(caption: taskResult.caption, type: TaskType.sell),
|
currentTask: TaskInfo(caption: taskResult.caption, type: TaskType.sell),
|
||||||
|
currentCombat: null, // 비전투 태스크이므로 전투 상태 초기화
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
state: state.copyWith(
|
state: state.copyWith(
|
||||||
@@ -1358,11 +1362,11 @@ class ProgressService {
|
|||||||
|
|
||||||
// 플레이어 공격 체크
|
// 플레이어 공격 체크
|
||||||
if (playerAccumulator >= playerStats.attackDelayMs) {
|
if (playerAccumulator >= playerStats.attackDelayMs) {
|
||||||
// SkillBook에서 사용 가능한 스킬 ID 목록 조회
|
// 장착된 스킬 슬롯에서 사용 가능한 스킬 ID 목록 조회
|
||||||
var availableSkillIds = skillService.getAvailableSkillIdsFromSkillBook(
|
var availableSkillIds = state.skillSystem.equippedSkills.allSkills
|
||||||
state.skillBook,
|
.map((s) => s.id)
|
||||||
);
|
.toList();
|
||||||
// SkillBook에 스킬이 없으면 기본 스킬 사용
|
// 장착된 스킬이 없으면 기본 스킬 사용
|
||||||
if (availableSkillIds.isEmpty) {
|
if (availableSkillIds.isEmpty) {
|
||||||
availableSkillIds = SkillData.defaultSkillIds;
|
availableSkillIds = SkillData.defaultSkillIds;
|
||||||
}
|
}
|
||||||
@@ -1395,6 +1399,9 @@ class ProgressService {
|
|||||||
totalDamageDealt += skillResult.result.damage;
|
totalDamageDealt += skillResult.result.damage;
|
||||||
updatedSkillSystem = skillResult.updatedSkillSystem;
|
updatedSkillSystem = skillResult.updatedSkillSystem;
|
||||||
|
|
||||||
|
// GCD 시작 (스킬 사용 후)
|
||||||
|
updatedSkillSystem = updatedSkillSystem.startGlobalCooldown();
|
||||||
|
|
||||||
// 스킬 공격 이벤트 생성
|
// 스킬 공격 이벤트 생성
|
||||||
newEvents.add(
|
newEvents.add(
|
||||||
CombatEvent.playerSkill(
|
CombatEvent.playerSkill(
|
||||||
@@ -1417,6 +1424,9 @@ class ProgressService {
|
|||||||
playerStats = skillResult.updatedPlayer;
|
playerStats = skillResult.updatedPlayer;
|
||||||
updatedSkillSystem = skillResult.updatedSkillSystem;
|
updatedSkillSystem = skillResult.updatedSkillSystem;
|
||||||
|
|
||||||
|
// GCD 시작 (스킬 사용 후)
|
||||||
|
updatedSkillSystem = updatedSkillSystem.startGlobalCooldown();
|
||||||
|
|
||||||
// DOT 효과 추가
|
// DOT 효과 추가
|
||||||
if (skillResult.dotEffect != null) {
|
if (skillResult.dotEffect != null) {
|
||||||
activeDoTs.add(skillResult.dotEffect!);
|
activeDoTs.add(skillResult.dotEffect!);
|
||||||
@@ -1442,6 +1452,9 @@ class ProgressService {
|
|||||||
playerStats = skillResult.updatedPlayer;
|
playerStats = skillResult.updatedPlayer;
|
||||||
updatedSkillSystem = skillResult.updatedSkillSystem;
|
updatedSkillSystem = skillResult.updatedSkillSystem;
|
||||||
|
|
||||||
|
// GCD 시작 (스킬 사용 후)
|
||||||
|
updatedSkillSystem = updatedSkillSystem.startGlobalCooldown();
|
||||||
|
|
||||||
// 회복 이벤트 생성
|
// 회복 이벤트 생성
|
||||||
newEvents.add(
|
newEvents.add(
|
||||||
CombatEvent.playerHeal(
|
CombatEvent.playerHeal(
|
||||||
@@ -1460,6 +1473,9 @@ class ProgressService {
|
|||||||
playerStats = skillResult.updatedPlayer;
|
playerStats = skillResult.updatedPlayer;
|
||||||
updatedSkillSystem = skillResult.updatedSkillSystem;
|
updatedSkillSystem = skillResult.updatedSkillSystem;
|
||||||
|
|
||||||
|
// GCD 시작 (스킬 사용 후)
|
||||||
|
updatedSkillSystem = updatedSkillSystem.startGlobalCooldown();
|
||||||
|
|
||||||
// 버프 이벤트 생성
|
// 버프 이벤트 생성
|
||||||
newEvents.add(
|
newEvents.add(
|
||||||
CombatEvent.playerBuff(
|
CombatEvent.playerBuff(
|
||||||
@@ -1478,6 +1494,9 @@ class ProgressService {
|
|||||||
playerStats = skillResult.updatedPlayer;
|
playerStats = skillResult.updatedPlayer;
|
||||||
updatedSkillSystem = skillResult.updatedSkillSystem;
|
updatedSkillSystem = skillResult.updatedSkillSystem;
|
||||||
|
|
||||||
|
// GCD 시작 (스킬 사용 후)
|
||||||
|
updatedSkillSystem = updatedSkillSystem.startGlobalCooldown();
|
||||||
|
|
||||||
// 디버프 효과 추가 (기존 같은 디버프 제거 후)
|
// 디버프 효과 추가 (기존 같은 디버프 제거 후)
|
||||||
if (skillResult.debuffEffect != null) {
|
if (skillResult.debuffEffect != null) {
|
||||||
activeDebuffs =
|
activeDebuffs =
|
||||||
@@ -1708,8 +1727,10 @@ class ProgressService {
|
|||||||
if (equippedNonWeaponSlots.isNotEmpty) {
|
if (equippedNonWeaponSlots.isNotEmpty) {
|
||||||
lostCount = 1;
|
lostCount = 1;
|
||||||
// 랜덤하게 1개 슬롯 선택
|
// 랜덤하게 1개 슬롯 선택
|
||||||
final sacrificeIndex = equippedNonWeaponSlots[
|
final sacrificeIndex =
|
||||||
state.rng.nextInt(equippedNonWeaponSlots.length)];
|
equippedNonWeaponSlots[state.rng.nextInt(
|
||||||
|
equippedNonWeaponSlots.length,
|
||||||
|
)];
|
||||||
final slot = EquipmentSlot.values[sacrificeIndex];
|
final slot = EquipmentSlot.values[sacrificeIndex];
|
||||||
|
|
||||||
// 해당 슬롯을 빈 장비로 교체
|
// 해당 슬롯을 빈 장비로 교체
|
||||||
@@ -1733,7 +1754,8 @@ class ProgressService {
|
|||||||
|
|
||||||
// 보스전 사망 시 5분 레벨링 모드 진입
|
// 보스전 사망 시 5분 레벨링 모드 진입
|
||||||
final bossLevelingEndTime = isBossDeath
|
final bossLevelingEndTime = isBossDeath
|
||||||
? DateTime.now().millisecondsSinceEpoch + (5 * 60 * 1000) // 5분
|
? DateTime.now().millisecondsSinceEpoch +
|
||||||
|
(5 * 60 * 1000) // 5분
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
// 전투 상태 초기화 및 사망 횟수 증가
|
// 전투 상태 초기화 및 사망 횟수 증가
|
||||||
|
|||||||
@@ -24,6 +24,11 @@ class SkillService {
|
|||||||
required int currentMp,
|
required int currentMp,
|
||||||
required SkillSystemState skillSystem,
|
required SkillSystemState skillSystem,
|
||||||
}) {
|
}) {
|
||||||
|
// GCD 체크 (글로벌 쿨타임 1500ms)
|
||||||
|
if (skillSystem.isGlobalCooldownActive) {
|
||||||
|
return SkillFailReason.onGlobalCooldown;
|
||||||
|
}
|
||||||
|
|
||||||
// MP 체크
|
// MP 체크
|
||||||
if (currentMp < skill.mpCost) {
|
if (currentMp < skill.mpCost) {
|
||||||
return SkillFailReason.notEnoughMp;
|
return SkillFailReason.notEnoughMp;
|
||||||
@@ -297,13 +302,14 @@ class SkillService {
|
|||||||
/// 전투 중 자동 스킬 선택
|
/// 전투 중 자동 스킬 선택
|
||||||
///
|
///
|
||||||
/// 우선순위:
|
/// 우선순위:
|
||||||
/// 1. HP < 30% → 회복 스킬
|
/// 1. HP < 30% → 회복 스킬 (최우선)
|
||||||
/// 2. HP > 70% & MP > 50% → 버프 스킬 (안전할 때)
|
/// 2. 70% 확률로 일반 공격 (MP 절약, 기본 전투)
|
||||||
/// 3. 몬스터 HP > 70% & 활성 디버프 없음 → 디버프 스킬
|
/// 3. 30% 확률로 스킬 사용:
|
||||||
/// 4. 몬스터 HP > 50% & DOT 없음 → DOT 스킬 (장기전 유리)
|
/// - 버프: HP > 80% & MP > 60% & 활성 버프 없음
|
||||||
/// 5. 보스전 (레벨 차이 10 이상) → 가장 강력한 공격 스킬
|
/// - 디버프: 몬스터 HP > 80% & 활성 디버프 없음
|
||||||
/// 6. 일반 전투 → MP 효율이 좋은 스킬
|
/// - DOT: 몬스터 HP > 60% & 활성 DOT 없음
|
||||||
/// 7. MP < 20% → null (일반 공격)
|
/// - 공격: 보스전이면 강력한 스킬, 일반전이면 효율적 스킬
|
||||||
|
/// 4. MP < 20% → 일반 공격
|
||||||
Skill? selectAutoSkill({
|
Skill? selectAutoSkill({
|
||||||
required CombatStats player,
|
required CombatStats player,
|
||||||
required MonsterCombatStats monster,
|
required MonsterCombatStats monster,
|
||||||
@@ -336,39 +342,49 @@ class SkillService {
|
|||||||
|
|
||||||
if (availableSkills.isEmpty) return null;
|
if (availableSkills.isEmpty) return null;
|
||||||
|
|
||||||
// HP < 30% → 회복 스킬 우선
|
// HP < 30% → 회복 스킬 최우선 (생존)
|
||||||
if (hpRatio < 0.3) {
|
if (hpRatio < 0.3) {
|
||||||
final healSkill = _findBestHealSkill(availableSkills, currentMp);
|
final healSkill = _findBestHealSkill(availableSkills, currentMp);
|
||||||
if (healSkill != null) return healSkill;
|
if (healSkill != null) return healSkill;
|
||||||
}
|
}
|
||||||
|
|
||||||
// HP > 70% & MP > 50% → 버프 스킬 (안전할 때)
|
// 70% 확률로 일반 공격 (스킬은 특별한 상황에서만)
|
||||||
if (hpRatio > 0.7 && mpRatio > 0.5) {
|
final useNormalAttack = rng.nextInt(100) < 70;
|
||||||
final buffSkill = _findBestBuffSkill(availableSkills, currentMp);
|
if (useNormalAttack) return null;
|
||||||
if (buffSkill != null) return buffSkill;
|
|
||||||
|
// === 아래부터 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% & 활성 디버프 없음 → 디버프 스킬
|
// 디버프: 몬스터 HP > 80% & 활성 디버프 없음 (전투 초반)
|
||||||
if (monster.hpRatio > 0.7 && activeDebuffs.isEmpty) {
|
if (monster.hpRatio > 0.8 && activeDebuffs.isEmpty) {
|
||||||
final debuffSkill = _findBestDebuffSkill(availableSkills, currentMp);
|
final debuffSkill = _findBestDebuffSkill(availableSkills, currentMp);
|
||||||
if (debuffSkill != null) return debuffSkill;
|
if (debuffSkill != null) return debuffSkill;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 몬스터 HP > 50% & 활성 DOT 없음 → DOT 스킬 사용
|
// DOT: 몬스터 HP > 60% & 활성 DOT 없음 (장기전 유리)
|
||||||
if (monster.hpRatio > 0.5 && activeDoTs.isEmpty) {
|
if (monster.hpRatio > 0.6 && activeDoTs.isEmpty) {
|
||||||
final dotSkill = _findBestDotSkill(availableSkills, currentMp);
|
final dotSkill = _findBestDotSkill(availableSkills, currentMp);
|
||||||
if (dotSkill != null) return dotSkill;
|
if (dotSkill != null) return dotSkill;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 보스전 판단 (몬스터 레벨이 높음)
|
// 보스전 판단 (몬스터 레벨 20 이상 & HP 50% 이상)
|
||||||
final isBossFight = monster.level >= 10 && monster.hpRatio > 0.5;
|
final isBossFight = monster.level >= 20 && monster.hpRatio > 0.5;
|
||||||
|
|
||||||
if (isBossFight) {
|
if (isBossFight) {
|
||||||
// 가장 강력한 공격 스킬
|
// 가장 강력한 공격 스킬
|
||||||
return _findStrongestAttackSkill(availableSkills);
|
return _findStrongestAttackSkill(availableSkills);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 일반 전투 → MP 효율 좋은 스킬
|
// 일반 전투 → MP 효율 좋은 공격 스킬
|
||||||
return _findEfficientAttackSkill(availableSkills);
|
return _findEfficientAttackSkill(availableSkills);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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/monster_grade.dart';
|
||||||
import 'package:asciineverdie/src/core/model/potion.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/model/skill_slots.dart';
|
||||||
import 'package:asciineverdie/src/core/util/deterministic_random.dart';
|
import 'package:asciineverdie/src/core/util/deterministic_random.dart';
|
||||||
|
|
||||||
/// Minimal skeletal state to mirror Progress Quest structures.
|
/// Minimal skeletal state to mirror Progress Quest structures.
|
||||||
@@ -193,14 +194,19 @@ enum DeathCause {
|
|||||||
|
|
||||||
/// 스킬 시스템 상태 (Phase 3)
|
/// 스킬 시스템 상태 (Phase 3)
|
||||||
///
|
///
|
||||||
/// 스킬 쿨타임, 활성 버프, 게임 경과 시간 등을 관리
|
/// 스킬 쿨타임, 활성 버프, 게임 경과 시간, 장착 스킬 등을 관리
|
||||||
class SkillSystemState {
|
class SkillSystemState {
|
||||||
const SkillSystemState({
|
const SkillSystemState({
|
||||||
required this.skillStates,
|
required this.skillStates,
|
||||||
required this.activeBuffs,
|
required this.activeBuffs,
|
||||||
required this.elapsedMs,
|
required this.elapsedMs,
|
||||||
|
this.equippedSkills = const SkillSlots(),
|
||||||
|
this.globalCooldownEndMs = 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/// 글로벌 쿨타임 (GCD) 상수: 1500ms
|
||||||
|
static const int globalCooldownDuration = 1500;
|
||||||
|
|
||||||
/// 스킬별 쿨타임 상태
|
/// 스킬별 쿨타임 상태
|
||||||
final List<SkillState> skillStates;
|
final List<SkillState> skillStates;
|
||||||
|
|
||||||
@@ -210,8 +216,26 @@ class SkillSystemState {
|
|||||||
/// 게임 진행 경과 시간 (밀리초, 스킬 쿨타임 계산용)
|
/// 게임 진행 경과 시간 (밀리초, 스킬 쿨타임 계산용)
|
||||||
final int elapsedMs;
|
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) {
|
SkillState? getSkillState(String skillId) {
|
||||||
@@ -254,13 +278,22 @@ class SkillSystemState {
|
|||||||
List<SkillState>? skillStates,
|
List<SkillState>? skillStates,
|
||||||
List<ActiveBuff>? activeBuffs,
|
List<ActiveBuff>? activeBuffs,
|
||||||
int? elapsedMs,
|
int? elapsedMs,
|
||||||
|
SkillSlots? equippedSkills,
|
||||||
|
int? globalCooldownEndMs,
|
||||||
}) {
|
}) {
|
||||||
return SkillSystemState(
|
return SkillSystemState(
|
||||||
skillStates: skillStates ?? this.skillStates,
|
skillStates: skillStates ?? this.skillStates,
|
||||||
activeBuffs: activeBuffs ?? this.activeBuffs,
|
activeBuffs: activeBuffs ?? this.activeBuffs,
|
||||||
elapsedMs: elapsedMs ?? this.elapsedMs,
|
elapsedMs: elapsedMs ?? this.elapsedMs,
|
||||||
|
equippedSkills: equippedSkills ?? this.equippedSkills,
|
||||||
|
globalCooldownEndMs: globalCooldownEndMs ?? this.globalCooldownEndMs,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// GCD 시작 (스킬 사용 후 호출)
|
||||||
|
SkillSystemState startGlobalCooldown() {
|
||||||
|
return copyWith(globalCooldownEndMs: elapsedMs + globalCooldownDuration);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 태스크 타입 (원본 fTask.Caption 값들에 대응)
|
/// 태스크 타입 (원본 fTask.Caption 값들에 대응)
|
||||||
@@ -594,8 +627,11 @@ class Equipment {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 초보자 방어구 생성 헬퍼
|
/// 초보자 방어구 생성 헬퍼
|
||||||
static EquipmentItem _starterArmor(String name, EquipmentSlot slot,
|
static EquipmentItem _starterArmor(
|
||||||
{required int def}) {
|
String name,
|
||||||
|
EquipmentSlot slot, {
|
||||||
|
required int def,
|
||||||
|
}) {
|
||||||
return EquipmentItem(
|
return EquipmentItem(
|
||||||
name: name,
|
name: name,
|
||||||
slot: slot,
|
slot: slot,
|
||||||
|
|||||||
@@ -117,6 +117,7 @@ class Skill {
|
|||||||
required this.mpCost,
|
required this.mpCost,
|
||||||
required this.cooldownMs,
|
required this.cooldownMs,
|
||||||
required this.power,
|
required this.power,
|
||||||
|
this.tier = 1,
|
||||||
this.damageMultiplier = 1.0,
|
this.damageMultiplier = 1.0,
|
||||||
this.healAmount = 0,
|
this.healAmount = 0,
|
||||||
this.healPercent = 0.0,
|
this.healPercent = 0.0,
|
||||||
@@ -133,6 +134,9 @@ class Skill {
|
|||||||
this.mpHealAmount = 0,
|
this.mpHealAmount = 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/// 스킬 티어 (1~5, 높을수록 강함)
|
||||||
|
final int tier;
|
||||||
|
|
||||||
/// 스킬 ID
|
/// 스킬 ID
|
||||||
final String id;
|
final String id;
|
||||||
|
|
||||||
@@ -213,6 +217,79 @@ class Skill {
|
|||||||
if (type != SkillType.attack || damageMultiplier <= 0) return 0;
|
if (type != SkillType.attack || damageMultiplier <= 0) return 0;
|
||||||
return damageMultiplier / mpCost;
|
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,
|
onCooldown,
|
||||||
|
|
||||||
|
/// 글로벌 쿨타임(GCD) 중
|
||||||
|
onGlobalCooldown,
|
||||||
|
|
||||||
/// 스킬 없음
|
/// 스킬 없음
|
||||||
skillNotFound,
|
skillNotFound,
|
||||||
|
|
||||||
|
|||||||
277
lib/src/core/model/skill_slots.dart
Normal file
277
lib/src/core/model/skill_slots.dart
Normal 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,
|
||||||
|
}
|
||||||
@@ -242,6 +242,14 @@ class _AsciiAnimationCardState extends State<AsciiAnimationCard> {
|
|||||||
_lastMonsterBaseName = widget.monsterBaseName;
|
_lastMonsterBaseName = widget.monsterBaseName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 새 몬스터 등장 시 사망 애니메이션 상태 리셋
|
||||||
|
// (이전 몬스터 사망 애니메이션이 끝나기 전에 새 전투 시작 시 대응)
|
||||||
|
if (oldWidget.monsterBaseName != widget.monsterBaseName &&
|
||||||
|
widget.monsterBaseName != null) {
|
||||||
|
_showDeathAnimation = false;
|
||||||
|
_deathAnimationMonsterLines = null;
|
||||||
|
}
|
||||||
|
|
||||||
if (oldWidget.taskType != widget.taskType ||
|
if (oldWidget.taskType != widget.taskType ||
|
||||||
oldWidget.monsterBaseName != widget.monsterBaseName ||
|
oldWidget.monsterBaseName != widget.monsterBaseName ||
|
||||||
oldWidget.weaponName != widget.weaponName ||
|
oldWidget.weaponName != widget.weaponName ||
|
||||||
@@ -550,6 +558,9 @@ class _AsciiAnimationCardState extends State<AsciiAnimationCard> {
|
|||||||
_battleSubFrame = 0;
|
_battleSubFrame = 0;
|
||||||
_phaseIndex = 0;
|
_phaseIndex = 0;
|
||||||
_phaseFrameCount = 0;
|
_phaseFrameCount = 0;
|
||||||
|
// 새 전투 시작 시 사망 애니메이션 상태 리셋 (몬스터 숨김 방지)
|
||||||
|
_showDeathAnimation = false;
|
||||||
|
_deathAnimationMonsterLines = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
case AsciiAnimationType.town:
|
case AsciiAnimationType.town:
|
||||||
|
|||||||
260
test/core/engine/gcd_simulation_test.dart
Normal file
260
test/core/engine/gcd_simulation_test.dart
Normal 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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user