- 다양한 GCD 값(0~3000ms)에 대한 전투 효율성 비교 - 레벨별 GCD 영향 분석 테스트 - DPS, 스킬 사용 빈도, 전투 시간 측정 - 권장 GCD 분석 결과: 1500~2000ms
261 lines
8.3 KiB
Dart
261 lines
8.3 KiB
Dart
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;
|
|
}
|