Compare commits
6 Commits
f9a4ae105a
...
249394f548
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
249394f548 | ||
|
|
85413362a2 | ||
|
|
02d4d1d397 | ||
|
|
c0d32b1c87 | ||
|
|
8112173541 | ||
|
|
2621942ced |
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
// 전투 상태 초기화 및 사망 횟수 증가
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// 새 몬스터 등장 시 사망 애니메이션 상태 리셋
|
||||
// (이전 몬스터 사망 애니메이션이 끝나기 전에 새 전투 시작 시 대응)
|
||||
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:
|
||||
|
||||
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