feat(engine): 엔진 서비스 개선 및 테스트 캐릭터 서비스 추가
- ProgressService 로직 개선 - RewardService 확장 - CombatCalculator, ItemService 정리 - TestCharacterService 추가
This commit is contained in:
@@ -98,9 +98,9 @@ class CombatCalculator {
|
||||
final isParried = parryRoll < defenderParryRate;
|
||||
|
||||
// 3. 기본 데미지 계산 (0.8 ~ 1.2 변동)
|
||||
// DEF 감산 비율: 0.3 (기존 0.5에서 축소하여 의미있는 피해 보장)
|
||||
// DEF 감산 비율: 0.5 (방어력 효과 상향, 몬스터 ATK 하향과 연동)
|
||||
final damageVariation = 0.8 + rng.nextDouble() * 0.4;
|
||||
var baseDamage = (attackerAtk * damageVariation - defenderDef * 0.3);
|
||||
var baseDamage = (attackerAtk * damageVariation - defenderDef * 0.5);
|
||||
|
||||
// 4. 크리티컬 판정
|
||||
final criRoll = rng.nextDouble();
|
||||
|
||||
@@ -135,30 +135,36 @@ class ItemService {
|
||||
}
|
||||
|
||||
/// 방패 스탯 생성
|
||||
///
|
||||
/// DEF 배율 조정 (v2): 방패 DEF를 0.15배로 축소
|
||||
ItemStats _generateShieldStats(int baseValue, ItemRarity rarity) {
|
||||
final blockBonus = 0.05 + rarity.index * 0.02;
|
||||
final def = (baseValue * 0.15).round();
|
||||
|
||||
return ItemStats(def: baseValue ~/ 2, blockRate: blockBonus);
|
||||
return ItemStats(def: def, blockRate: blockBonus);
|
||||
}
|
||||
|
||||
/// 방어구 스탯 생성
|
||||
///
|
||||
/// DEF 배율 조정 (v2): 9개 방어구 합산 DEF ≈ 무기 ATK × 1.6
|
||||
/// 기존 배율(합계 8.0)에서 대폭 축소하여 일반 공격 데미지 정상화
|
||||
ItemStats _generateArmorStats(
|
||||
int baseValue,
|
||||
ItemRarity rarity,
|
||||
EquipmentSlot slot,
|
||||
) {
|
||||
// 슬롯별 방어력 가중치
|
||||
// 슬롯별 방어력 가중치 (총합 ~1.6으로 축소)
|
||||
final defMultiplier = switch (slot) {
|
||||
EquipmentSlot.hauberk => 1.5, // 갑옷류 최고
|
||||
EquipmentSlot.helm => 1.2,
|
||||
EquipmentSlot.gambeson => 1.0,
|
||||
EquipmentSlot.cuisses => 0.9,
|
||||
EquipmentSlot.greaves => 0.8,
|
||||
EquipmentSlot.brassairts => 0.7,
|
||||
EquipmentSlot.vambraces => 0.7,
|
||||
EquipmentSlot.gauntlets => 0.6,
|
||||
EquipmentSlot.sollerets => 0.6,
|
||||
_ => 0.5,
|
||||
EquipmentSlot.hauberk => 0.30, // 갑옷류 최고
|
||||
EquipmentSlot.helm => 0.25,
|
||||
EquipmentSlot.gambeson => 0.20,
|
||||
EquipmentSlot.cuisses => 0.18,
|
||||
EquipmentSlot.greaves => 0.16,
|
||||
EquipmentSlot.brassairts => 0.14,
|
||||
EquipmentSlot.vambraces => 0.14,
|
||||
EquipmentSlot.gauntlets => 0.12,
|
||||
EquipmentSlot.sollerets => 0.12,
|
||||
_ => 0.10,
|
||||
};
|
||||
|
||||
final def = (baseValue * defMultiplier).round();
|
||||
|
||||
@@ -6,6 +6,7 @@ import 'package:asciineverdie/src/core/engine/combat_calculator.dart';
|
||||
import 'package:asciineverdie/src/core/engine/game_mutations.dart';
|
||||
import 'package:asciineverdie/src/core/engine/potion_service.dart';
|
||||
import 'package:asciineverdie/src/core/engine/reward_service.dart';
|
||||
import 'package:asciineverdie/src/core/engine/shop_service.dart';
|
||||
import 'package:asciineverdie/src/core/engine/skill_service.dart';
|
||||
import 'package:asciineverdie/src/core/model/combat_event.dart';
|
||||
import 'package:asciineverdie/src/core/model/combat_state.dart';
|
||||
@@ -13,6 +14,7 @@ import 'package:asciineverdie/src/core/model/combat_stats.dart';
|
||||
import 'package:asciineverdie/src/core/model/equipment_item.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/item_stats.dart';
|
||||
import 'package:asciineverdie/src/core/model/monster_combat_stats.dart';
|
||||
import 'package:asciineverdie/src/core/model/monster_grade.dart';
|
||||
import 'package:asciineverdie/src/core/model/potion.dart';
|
||||
@@ -514,12 +516,15 @@ class ProgressService {
|
||||
return (progress: progress, queue: queue);
|
||||
}
|
||||
|
||||
// 2. kill 태스크가 아니었고 heading도 아니면 heading 또는 buying 태스크 실행
|
||||
// (원본 670-677줄)
|
||||
if (oldTaskType != TaskType.kill && oldTaskType != TaskType.neutral) {
|
||||
// Gold가 충분하면 장비 구매 (원본 671-673줄)
|
||||
// 2. kill/heading/buying 태스크가 아니었으면 heading 또는 buying 태스크 실행
|
||||
// (원본 670-677줄) - buying 완료 후 무한 루프 방지
|
||||
if (oldTaskType != TaskType.kill &&
|
||||
oldTaskType != TaskType.neutral &&
|
||||
oldTaskType != TaskType.buying) {
|
||||
// Gold가 충분하면 장비 구매 (Common 장비 가격 기준)
|
||||
// 실제 구매 가격과 동일한 공식 사용: level * 50
|
||||
final gold = _getGold(state);
|
||||
final equipPrice = _equipPrice(state.traits.level);
|
||||
final equipPrice = state.traits.level * 50; // Common 장비 1개 가격
|
||||
if (gold > equipPrice) {
|
||||
final taskResult = pq_logic.startTask(
|
||||
progress,
|
||||
@@ -966,8 +971,12 @@ class ProgressService {
|
||||
|
||||
final nextLevel = state.traits.level + 1;
|
||||
final rng = state.rng;
|
||||
final hpGain = state.stats.con ~/ 3 + 1 + rng.nextInt(4);
|
||||
final mpGain = state.stats.intelligence ~/ 3 + 1 + rng.nextInt(4);
|
||||
|
||||
// HP/MP 증가량 (PlayerScaling 기반 + 랜덤 변동)
|
||||
// 기존: CON/3 + 1 + random(0-3) → ~6-9 HP/레벨 (너무 낮음)
|
||||
// 신규: 18 + CON/5 + random(0-4) → ~20-25 HP/레벨 (생존율 개선)
|
||||
final hpGain = 18 + state.stats.con ~/ 5 + rng.nextInt(5);
|
||||
final mpGain = 6 + state.stats.intelligence ~/ 5 + rng.nextInt(3);
|
||||
|
||||
var nextState = state.copyWith(
|
||||
traits: state.traits.copyWith(level: nextLevel),
|
||||
@@ -1078,29 +1087,48 @@ class ProgressService {
|
||||
return state.inventory.gold;
|
||||
}
|
||||
|
||||
/// 장비 가격 계산 (원본 Main.pas:612-616)
|
||||
/// Result := 5 * Level^2 + 10 * Level + 20
|
||||
int _equipPrice(int level) {
|
||||
return 5 * level * level + 10 * level + 20;
|
||||
/// 장비 구매 완료 처리 (개선된 로직)
|
||||
///
|
||||
/// 1순위: 빈 슬롯에 Common 장비 최대한 채우기
|
||||
/// 2순위: 골드 남으면 물약 구매
|
||||
GameState _completeBuying(GameState state) {
|
||||
var nextState = state;
|
||||
final level = state.traits.level;
|
||||
final shopService = ShopService(rng: nextState.rng);
|
||||
|
||||
// 1. 빈 슬롯 목록 수집
|
||||
final emptySlots = <int>[];
|
||||
for (var i = 0; i < Equipment.slotCount; i++) {
|
||||
if (nextState.equipment.getItemByIndex(i).isEmpty) {
|
||||
emptySlots.add(i);
|
||||
}
|
||||
}
|
||||
|
||||
/// 장비 구매 완료 처리 (원본 Main.pas:631-634)
|
||||
GameState _completeBuying(GameState state) {
|
||||
final level = state.traits.level;
|
||||
final price = _equipPrice(level);
|
||||
|
||||
// Gold 차감 (inventory.gold 필드 사용)
|
||||
final newGold = math.max(0, state.inventory.gold - price);
|
||||
var nextState = state.copyWith(
|
||||
inventory: state.inventory.copyWith(gold: newGold),
|
||||
// 2. 골드가 허용하는 한 빈 슬롯에 Common 장비 구매
|
||||
for (final slotIndex in emptySlots) {
|
||||
final slot = EquipmentSlot.values[slotIndex];
|
||||
final item = shopService.generateShopItem(
|
||||
playerLevel: level,
|
||||
slot: slot,
|
||||
targetRarity: ItemRarity.common,
|
||||
);
|
||||
final price = shopService.calculateBuyPrice(item);
|
||||
|
||||
// 장비 획득 (WinEquip)
|
||||
// 원본 Main.pas:797 - posn := Random(Equips.Items.Count); (11개 슬롯)
|
||||
final slotIndex = nextState.rng.nextInt(Equipment.slotCount);
|
||||
nextState = mutations.winEquipByIndex(nextState, level, slotIndex);
|
||||
if (nextState.inventory.gold >= price) {
|
||||
nextState = nextState.copyWith(
|
||||
inventory: nextState.inventory.copyWith(
|
||||
gold: nextState.inventory.gold - price,
|
||||
),
|
||||
equipment: nextState.equipment
|
||||
.setItemByIndex(slotIndex, item)
|
||||
.copyWith(bestIndex: slotIndex),
|
||||
);
|
||||
} else {
|
||||
break; // 골드 부족 시 중단
|
||||
}
|
||||
}
|
||||
|
||||
// 물약 자동 구매 (남은 골드의 20% 사용)
|
||||
// 3. 물약 자동 구매 (남은 골드의 20% 사용)
|
||||
final potionService = const PotionService();
|
||||
final purchaseResult = potionService.autoPurchasePotions(
|
||||
playerLevel: level,
|
||||
@@ -1644,30 +1672,35 @@ class ProgressService {
|
||||
required String killerName,
|
||||
required DeathCause cause,
|
||||
}) {
|
||||
// 상실할 장비 개수 계산
|
||||
final lostCount = state.equipment.equippedItems.length;
|
||||
|
||||
// 사망 직전 전투 이벤트 저장 (최대 10개)
|
||||
final lastCombatEvents =
|
||||
state.progress.currentCombat?.recentEvents ?? const [];
|
||||
|
||||
// 빈 장비 생성 (기본 무기만 유지)
|
||||
final emptyEquipment = Equipment(
|
||||
items: [
|
||||
EquipmentItem.defaultWeapon(),
|
||||
EquipmentItem.empty(EquipmentSlot.shield),
|
||||
EquipmentItem.empty(EquipmentSlot.helm),
|
||||
EquipmentItem.empty(EquipmentSlot.hauberk),
|
||||
EquipmentItem.empty(EquipmentSlot.brassairts),
|
||||
EquipmentItem.empty(EquipmentSlot.vambraces),
|
||||
EquipmentItem.empty(EquipmentSlot.gauntlets),
|
||||
EquipmentItem.empty(EquipmentSlot.gambeson),
|
||||
EquipmentItem.empty(EquipmentSlot.cuisses),
|
||||
EquipmentItem.empty(EquipmentSlot.greaves),
|
||||
EquipmentItem.empty(EquipmentSlot.sollerets),
|
||||
],
|
||||
bestIndex: 0,
|
||||
// 무기(슬롯 0)를 제외한 장착된 장비 중 1개를 제물로 삭제
|
||||
// 장착된 비무기 슬롯 인덱스 수집 (슬롯 1~10 중 장비가 있는 것)
|
||||
final equippedNonWeaponSlots = <int>[];
|
||||
for (var i = 1; i < Equipment.slotCount; i++) {
|
||||
if (state.equipment.getItemByIndex(i).isNotEmpty) {
|
||||
equippedNonWeaponSlots.add(i);
|
||||
}
|
||||
}
|
||||
|
||||
// 제물로 바칠 장비 선택 및 삭제
|
||||
var newEquipment = state.equipment;
|
||||
final lostCount = equippedNonWeaponSlots.isNotEmpty ? 1 : 0;
|
||||
|
||||
if (equippedNonWeaponSlots.isNotEmpty) {
|
||||
// 랜덤하게 1개 슬롯 선택
|
||||
final sacrificeIndex =
|
||||
equippedNonWeaponSlots[state.rng.nextInt(equippedNonWeaponSlots.length)];
|
||||
final slot = EquipmentSlot.values[sacrificeIndex];
|
||||
|
||||
// 해당 슬롯을 빈 장비로 교체
|
||||
newEquipment = newEquipment.setItemByIndex(
|
||||
sacrificeIndex,
|
||||
EquipmentItem.empty(slot),
|
||||
);
|
||||
}
|
||||
|
||||
// 사망 정보 생성 (전투 로그 포함)
|
||||
final deathInfo = DeathInfo(
|
||||
@@ -1689,7 +1722,7 @@ class ProgressService {
|
||||
);
|
||||
|
||||
return state.copyWith(
|
||||
equipment: emptyEquipment,
|
||||
equipment: newEquipment,
|
||||
progress: progress,
|
||||
deathInfo: deathInfo,
|
||||
);
|
||||
|
||||
@@ -1,25 +1,82 @@
|
||||
import 'package:asciineverdie/src/core/engine/game_mutations.dart';
|
||||
import 'package:asciineverdie/src/core/engine/item_service.dart';
|
||||
import 'package:asciineverdie/src/core/engine/shop_service.dart';
|
||||
import 'package:asciineverdie/src/core/model/equipment_slot.dart';
|
||||
import 'package:asciineverdie/src/core/model/game_state.dart';
|
||||
import 'package:asciineverdie/src/core/util/pq_logic.dart';
|
||||
import 'package:asciineverdie/src/core/model/pq_config.dart';
|
||||
import 'package:asciineverdie/src/core/util/pq_logic.dart' as pq_logic;
|
||||
|
||||
/// Applies quest/act rewards to the GameState using shared RNG.
|
||||
class RewardService {
|
||||
RewardService(this.mutations);
|
||||
RewardService(this.mutations, this.config);
|
||||
|
||||
final GameMutations mutations;
|
||||
final PqConfig config;
|
||||
|
||||
GameState applyReward(GameState state, RewardKind reward) {
|
||||
GameState applyReward(GameState state, pq_logic.RewardKind reward) {
|
||||
switch (reward) {
|
||||
case RewardKind.spell:
|
||||
case pq_logic.RewardKind.spell:
|
||||
return mutations.winSpell(state, state.stats.wis, state.traits.level);
|
||||
case RewardKind.equip:
|
||||
// 원본 Main.pas:797 - Random(Equips.Items.Count) (11개 슬롯)
|
||||
final slotIndex = state.rng.nextInt(Equipment.slotCount);
|
||||
return mutations.winEquipByIndex(state, state.traits.level, slotIndex);
|
||||
case RewardKind.stat:
|
||||
case pq_logic.RewardKind.equip:
|
||||
return _applyEquipReward(state);
|
||||
case pq_logic.RewardKind.stat:
|
||||
return mutations.winStat(state);
|
||||
case RewardKind.item:
|
||||
case pq_logic.RewardKind.item:
|
||||
return mutations.winItem(state);
|
||||
}
|
||||
}
|
||||
|
||||
/// 장비 보상 처리 (개선된 로직)
|
||||
///
|
||||
/// - 빈 슬롯: 무조건 장착
|
||||
/// - 기존 장비 있음: 점수 비교 → 업그레이드만 / 다운그레이드 시 판매
|
||||
GameState _applyEquipReward(GameState state) {
|
||||
final slotIndex = state.rng.nextInt(Equipment.slotCount);
|
||||
final slot = EquipmentSlot.values[slotIndex];
|
||||
final currentItem = state.equipment.getItemByIndex(slotIndex);
|
||||
final level = state.traits.level;
|
||||
|
||||
// 새 장비 생성
|
||||
final itemService = ItemService(rng: state.rng);
|
||||
final name = pq_logic.winEquip(config, state.rng, level, slotIndex);
|
||||
final newItem = itemService.generateEquipment(
|
||||
name: name,
|
||||
slot: slot,
|
||||
level: level,
|
||||
);
|
||||
|
||||
// 빈 슬롯이면 무조건 장착
|
||||
if (currentItem.isEmpty) {
|
||||
return state.copyWith(
|
||||
rng: state.rng,
|
||||
equipment: state.equipment
|
||||
.setItemByIndex(slotIndex, newItem)
|
||||
.copyWith(bestIndex: slotIndex),
|
||||
);
|
||||
}
|
||||
|
||||
// 점수 비교
|
||||
final currentScore = ItemService.calculateEquipmentScore(currentItem);
|
||||
final newScore = ItemService.calculateEquipmentScore(newItem);
|
||||
|
||||
if (newScore > currentScore) {
|
||||
// 업그레이드: 새 장비 장착
|
||||
return state.copyWith(
|
||||
rng: state.rng,
|
||||
equipment: state.equipment
|
||||
.setItemByIndex(slotIndex, newItem)
|
||||
.copyWith(bestIndex: slotIndex),
|
||||
);
|
||||
} else {
|
||||
// 다운그레이드: 새 장비 판매 (골드로 변환)
|
||||
final shopService = ShopService(rng: state.rng);
|
||||
final sellPrice = shopService.calculateSellPrice(newItem);
|
||||
return state.copyWith(
|
||||
rng: state.rng,
|
||||
inventory: state.inventory.copyWith(
|
||||
gold: state.inventory.gold + sellPrice,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
268
lib/src/core/engine/test_character_service.dart
Normal file
268
lib/src/core/engine/test_character_service.dart
Normal file
@@ -0,0 +1,268 @@
|
||||
import 'package:asciineverdie/src/core/engine/item_service.dart';
|
||||
import 'package:asciineverdie/src/core/model/combat_stats.dart';
|
||||
import 'package:asciineverdie/src/core/model/equipment_item.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/hall_of_fame.dart';
|
||||
import 'package:asciineverdie/src/core/model/item_stats.dart';
|
||||
import 'package:asciineverdie/src/core/model/pq_config.dart';
|
||||
import 'package:asciineverdie/src/core/util/deterministic_random.dart';
|
||||
import 'package:asciineverdie/src/core/util/roman.dart';
|
||||
|
||||
/// 테스트 캐릭터 생성 서비스 (디버그 모드 전용)
|
||||
///
|
||||
/// 현재 캐릭터를 레벨 100, 고급 장비, 다수의 스킬을 가진
|
||||
/// 캐릭터로 변환하여 명예의 전당에 등록할 수 있게 함.
|
||||
class TestCharacterService {
|
||||
TestCharacterService({
|
||||
required this.config,
|
||||
required this.rng,
|
||||
});
|
||||
|
||||
final PqConfig config;
|
||||
final DeterministicRandom rng;
|
||||
|
||||
/// 테스트 캐릭터 데이터 생성
|
||||
///
|
||||
/// 현재 GameState를 기반으로:
|
||||
/// - 레벨 100
|
||||
/// - 장비: Rare+, 최소 Epic 2개, Legendary 1개
|
||||
/// - 스킬: 20~30개 랜덤
|
||||
HallOfFameEntry createTestCharacter(GameState state) {
|
||||
// 1. 장비 생성 (Rare+, Epic 2+, Legendary 1+)
|
||||
final equipment = _generateTestEquipment();
|
||||
|
||||
// 2. 스킬 생성 (20~30개)
|
||||
final skills = _generateTestSkills();
|
||||
|
||||
// 3. 레벨 100 기준 스탯 계산
|
||||
final testStats = _generateTestStats();
|
||||
|
||||
// 4. CombatStats 계산
|
||||
final combatStats = CombatStats.fromStats(
|
||||
stats: testStats,
|
||||
equipment: equipment,
|
||||
level: 100,
|
||||
);
|
||||
|
||||
// 5. HallOfFameEntry 생성
|
||||
return HallOfFameEntry(
|
||||
id: DateTime.now().millisecondsSinceEpoch.toString(),
|
||||
characterName: state.traits.name,
|
||||
race: state.traits.race,
|
||||
raceId: state.traits.raceId,
|
||||
klass: state.traits.klass,
|
||||
level: 100,
|
||||
totalPlayTimeMs: 36000000, // 10시간 (테스트용 기본값)
|
||||
totalDeaths: rng.nextInt(50), // 0~49 랜덤 사망 횟수
|
||||
monstersKilled: 5000 + rng.nextInt(5000), // 5000~10000 처치
|
||||
questsCompleted: 100 + rng.nextInt(50), // 100~150 퀘스트
|
||||
clearedAt: DateTime.now(),
|
||||
finalStats: combatStats,
|
||||
finalEquipment: equipment.items,
|
||||
finalSkills: skills,
|
||||
);
|
||||
}
|
||||
|
||||
/// 테스트용 장비 생성
|
||||
///
|
||||
/// 조건:
|
||||
/// - 모든 장비 Rare 이상
|
||||
/// - 최소 Epic 2개
|
||||
/// - 최소 Legendary 1개
|
||||
Equipment _generateTestEquipment() {
|
||||
final itemService = ItemService(rng: rng);
|
||||
final items = <EquipmentItem>[];
|
||||
|
||||
// 희귀도 분배 결정
|
||||
// 11개 슬롯: 1 Legendary, 2 Epic, 나머지 Rare
|
||||
final rarities = <ItemRarity>[
|
||||
ItemRarity.legendary, // 1개
|
||||
ItemRarity.epic, ItemRarity.epic, // 2개
|
||||
ItemRarity.rare, ItemRarity.rare, ItemRarity.rare, // 3개
|
||||
ItemRarity.rare, ItemRarity.rare, ItemRarity.rare, // 3개
|
||||
ItemRarity.rare, ItemRarity.rare, // 2개
|
||||
];
|
||||
|
||||
// 희귀도 셔플
|
||||
_shuffleList(rarities);
|
||||
|
||||
// 각 슬롯별 장비 생성
|
||||
for (var i = 0; i < Equipment.slotCount; i++) {
|
||||
final slot = EquipmentSlot.values[i];
|
||||
final rarity = rarities[i];
|
||||
final name = _generateEquipmentName(slot, rarity);
|
||||
|
||||
final item = itemService.generateEquipment(
|
||||
name: name,
|
||||
slot: slot,
|
||||
level: 100,
|
||||
rarity: rarity,
|
||||
);
|
||||
items.add(item);
|
||||
}
|
||||
|
||||
// Legendary 슬롯을 bestIndex로 설정
|
||||
final legendaryIndex = rarities.indexOf(ItemRarity.legendary);
|
||||
|
||||
return Equipment(items: items, bestIndex: legendaryIndex);
|
||||
}
|
||||
|
||||
/// 희귀도에 맞는 장비 이름 생성
|
||||
String _generateEquipmentName(EquipmentSlot slot, ItemRarity rarity) {
|
||||
final prefixes = switch (rarity) {
|
||||
ItemRarity.legendary => [
|
||||
'Divine',
|
||||
'Mythical',
|
||||
'Godlike',
|
||||
'Eternal',
|
||||
'Transcendent',
|
||||
],
|
||||
ItemRarity.epic => [
|
||||
'Ancient',
|
||||
'Infernal',
|
||||
'Celestial',
|
||||
'Void',
|
||||
'Corrupted',
|
||||
],
|
||||
ItemRarity.rare => [
|
||||
'Superior',
|
||||
'Enchanted',
|
||||
'Reinforced',
|
||||
'Blessed',
|
||||
'Refined',
|
||||
],
|
||||
_ => ['Standard', 'Basic', 'Common'],
|
||||
};
|
||||
|
||||
final slotNames = switch (slot) {
|
||||
EquipmentSlot.weapon => [
|
||||
'Keyboard of Power',
|
||||
'Binary Blade',
|
||||
'Compiler Crusher',
|
||||
],
|
||||
EquipmentSlot.shield => [
|
||||
'Firewall Shield',
|
||||
'Exception Handler',
|
||||
'Null Guard',
|
||||
],
|
||||
EquipmentSlot.helm => [
|
||||
'Neural Helm',
|
||||
'Thought Processor',
|
||||
'Mind Buffer',
|
||||
],
|
||||
EquipmentSlot.hauberk => [
|
||||
'Matrix Armor',
|
||||
'Byte Mail',
|
||||
'Kernel Plate',
|
||||
],
|
||||
EquipmentSlot.brassairts => [
|
||||
'Bit Guards',
|
||||
'Stream Bracers',
|
||||
'Thread Wraps',
|
||||
],
|
||||
EquipmentSlot.vambraces => [
|
||||
'Code Vambraces',
|
||||
'Debug Cuffs',
|
||||
'Loop Guards',
|
||||
],
|
||||
EquipmentSlot.gauntlets => [
|
||||
'Input Gloves',
|
||||
'Handler Mitts',
|
||||
'Pointer Grips',
|
||||
],
|
||||
EquipmentSlot.gambeson => [
|
||||
'Layer Vest',
|
||||
'Cache Coat',
|
||||
'Buffer Jacket',
|
||||
],
|
||||
EquipmentSlot.cuisses => [
|
||||
'Register Guards',
|
||||
'Stack Protectors',
|
||||
'Heap Leggings',
|
||||
],
|
||||
EquipmentSlot.greaves => [
|
||||
'Runtime Greaves',
|
||||
'Compile Shins',
|
||||
'Execute Boots',
|
||||
],
|
||||
EquipmentSlot.sollerets => [
|
||||
'Boot Loader',
|
||||
'System Treads',
|
||||
'Process Soles',
|
||||
],
|
||||
};
|
||||
|
||||
final prefix = prefixes[rng.nextInt(prefixes.length)];
|
||||
final slotName = slotNames[rng.nextInt(slotNames.length)];
|
||||
|
||||
return '$prefix $slotName';
|
||||
}
|
||||
|
||||
/// 테스트용 스킬 생성 (20~30개)
|
||||
List<Map<String, String>> _generateTestSkills() {
|
||||
final spells = config.spells;
|
||||
final skillCount = 20 + rng.nextInt(11); // 20~30개
|
||||
final selectedSpells = <String>{};
|
||||
final skills = <Map<String, String>>[];
|
||||
|
||||
// 중복 없이 스킬 선택
|
||||
while (selectedSpells.length < skillCount && selectedSpells.length < spells.length) {
|
||||
final index = rng.nextInt(spells.length);
|
||||
final spell = spells[index].split('|')[0];
|
||||
if (selectedSpells.add(spell)) {
|
||||
// 랭크: I~X 랜덤 (레벨 100 기준 높은 랭크 선호)
|
||||
final rank = _randomHighRank();
|
||||
skills.add({'name': spell, 'rank': rank});
|
||||
}
|
||||
}
|
||||
|
||||
return skills;
|
||||
}
|
||||
|
||||
/// 높은 랭크 생성 (V~X 선호)
|
||||
String _randomHighRank() {
|
||||
// 5~10 범위 (V~X)
|
||||
final rankValue = 5 + rng.nextInt(6);
|
||||
return intToRoman(rankValue);
|
||||
}
|
||||
|
||||
/// 테스트용 스탯 생성 (레벨 100 기준)
|
||||
Stats _generateTestStats() {
|
||||
// 레벨 100 기준 높은 스탯
|
||||
// 기본 스탯: 50~80 범위
|
||||
final str = 50 + rng.nextInt(31);
|
||||
final con = 50 + rng.nextInt(31);
|
||||
final dex = 50 + rng.nextInt(31);
|
||||
final int_ = 50 + rng.nextInt(31);
|
||||
final wis = 50 + rng.nextInt(31);
|
||||
final cha = 50 + rng.nextInt(31);
|
||||
|
||||
// HP/MP 계산 (레벨 100 기준)
|
||||
final hpMax = 500 + con * 10;
|
||||
final mpMax = 200 + int_ * 5 + wis * 3;
|
||||
|
||||
return Stats(
|
||||
str: str,
|
||||
con: con,
|
||||
dex: dex,
|
||||
intelligence: int_,
|
||||
wis: wis,
|
||||
cha: cha,
|
||||
hpMax: hpMax,
|
||||
mpMax: mpMax,
|
||||
hpCurrent: hpMax,
|
||||
mpCurrent: mpMax,
|
||||
);
|
||||
}
|
||||
|
||||
/// 리스트 셔플 (Fisher-Yates)
|
||||
void _shuffleList<T>(List<T> list) {
|
||||
for (var i = list.length - 1; i > 0; i--) {
|
||||
final j = rng.nextInt(i + 1);
|
||||
final temp = list[i];
|
||||
list[i] = list[j];
|
||||
list[j] = temp;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user