Files
asciinevrdie/lib/src/core/util/pq_item.dart
JiWoong Sul 23f15f41d3 refactor(util): pq_logic.dart 모듈 분할
- pq_random.dart: 랜덤/확률 함수 (61줄)
- pq_string.dart: 문자열 유틸리티 (55줄)
- pq_item.dart: 아이템/장비 생성 (327줄)
- pq_monster.dart: 몬스터 생성 (283줄)
- pq_quest.dart: 퀘스트/Act/시네마틱 (283줄)
- pq_task.dart: 태스크/큐 (97줄)
- pq_stat.dart: 스탯 관련 (64줄)
- 원본은 re-export 허브로 유지 (역호환성)
2026-01-15 17:05:46 +09:00

328 lines
9.2 KiB
Dart

import 'dart:math' as math;
import 'package:asciineverdie/src/core/model/equipment_slot.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/pq_random.dart';
/// 아이템/장비 생성 관련 함수들
/// 원본 Main.pas의 아이템 생성 로직을 포팅
/// 장비 생성 결과 (구조화된 데이터로 l10n 지원)
class EquipResult {
const EquipResult({
required this.baseName,
this.modifiers = const [],
this.plusValue = 0,
});
/// 기본 장비 이름 (예: "VPN Cloak")
final String baseName;
/// 수식어 목록 (예: ["Holey", "Deprecated"])
final List<String> modifiers;
/// +/- 수치 (예: -1, +2)
final int plusValue;
/// 영문 전체 이름 생성 (기존 방식)
String get displayName {
var name = baseName;
for (final mod in modifiers) {
name = '$mod $name';
}
if (plusValue != 0) {
name = '${plusValue > 0 ? '+' : ''}$plusValue $name';
}
return name;
}
}
/// 아이템 생성 결과 (구조화된 데이터로 l10n 지원)
class ItemResult {
const ItemResult({this.attrib, this.special, this.itemOf, this.boringItem});
/// 아이템 속성 (예: "Golden")
final String? attrib;
/// 특수 아이템 (예: "Iterator")
final String? special;
/// "~의" 접미사 (예: "Monitoring")
final String? itemOf;
/// 단순 아이템 (보링 아이템용)
final String? boringItem;
/// 영문 전체 이름 생성 (기존 방식)
String get displayName {
if (boringItem != null) return boringItem!;
if (attrib != null && special != null && itemOf != null) {
return '$attrib $special of $itemOf';
}
if (attrib != null && special != null) {
return '$attrib $special';
}
return '';
}
}
// =============================================================================
// 아이템 생성 함수들
// =============================================================================
/// 단순 아이템 (Boring Item)
String boringItem(PqConfig config, DeterministicRandom rng) {
return pick(config.boringItems, rng);
}
/// 흥미로운 아이템 (Interesting Item)
String interestingItem(PqConfig config, DeterministicRandom rng) {
final attr = pick(config.itemAttrib, rng);
final special = pick(config.specials, rng);
return '$attr $special';
}
/// 특수 아이템 (Special Item)
String specialItem(PqConfig config, DeterministicRandom rng) {
return '${interestingItem(config, rng)} of ${pick(config.itemOfs, rng)}';
}
/// 구조화된 단순 아이템 결과 반환 (l10n 지원)
ItemResult boringItemStructured(PqConfig config, DeterministicRandom rng) {
return ItemResult(boringItem: pick(config.boringItems, rng));
}
/// 구조화된 흥미로운 아이템 결과 반환 (l10n 지원)
ItemResult interestingItemStructured(PqConfig config, DeterministicRandom rng) {
return ItemResult(
attrib: pick(config.itemAttrib, rng),
special: pick(config.specials, rng),
);
}
/// 구조화된 특수 아이템 결과 반환 (l10n 지원)
ItemResult specialItemStructured(PqConfig config, DeterministicRandom rng) {
return ItemResult(
attrib: pick(config.itemAttrib, rng),
special: pick(config.specials, rng),
itemOf: pick(config.itemOfs, rng),
);
}
/// 구조화된 승리 아이템 결과 반환 (l10n 지원)
ItemResult winItemStructured(
PqConfig config,
DeterministicRandom rng,
int inventoryCount,
) {
final threshold = math.max(250, rng.nextInt(999));
if (inventoryCount > threshold) {
return const ItemResult(); // 빈 결과
}
return specialItemStructured(config, rng);
}
/// 승리 아이템 (문자열 반환)
String winItem(PqConfig config, DeterministicRandom rng, int inventoryCount) {
final threshold = math.max(250, rng.nextInt(999));
if (inventoryCount > threshold) return '';
return specialItem(config, rng);
}
// =============================================================================
// 장비 선택 함수들
// =============================================================================
/// 무기 선택
String pickWeapon(PqConfig config, DeterministicRandom rng, int level) {
return _lPick(config.weapons, rng, level);
}
/// 방패 선택
String pickShield(PqConfig config, DeterministicRandom rng, int level) {
return _lPick(config.shields, rng, level);
}
/// 방어구 선택
String pickArmor(PqConfig config, DeterministicRandom rng, int level) {
return _lPick(config.armors, rng, level);
}
/// 주문 선택
String pickSpell(PqConfig config, DeterministicRandom rng, int goalLevel) {
return _lPick(config.spells, rng, goalLevel);
}
/// 원본 Main.pas:776-789 LPick: 6회 시도하여 목표 레벨에 가장 가까운 아이템 선택
String _lPick(List<String> items, DeterministicRandom rng, int goal) {
if (items.isEmpty) return '';
var result = pick(items, rng);
var bestLevel = _parseLevel(result);
for (var i = 0; i < 5; i++) {
final candidate = pick(items, rng);
final candLevel = _parseLevel(candidate);
if ((goal - candLevel).abs() < (goal - bestLevel).abs()) {
result = candidate;
bestLevel = candLevel;
}
}
return result;
}
/// 아이템 문자열에서 레벨 파싱
int _parseLevel(String entry) {
final parts = entry.split('|');
if (parts.length < 2) return 0;
return int.tryParse(parts[1].replaceAll('+', '')) ?? 0;
}
// =============================================================================
// 장비 생성 함수들
// =============================================================================
/// 수식어 추가
String addModifier(
DeterministicRandom rng,
String baseName,
List<String> modifiers,
int plus,
) {
var name = baseName;
var remaining = plus;
var count = 0;
while (count < 2 && remaining != 0) {
final modifier = pick(modifiers, rng);
final parts = modifier.split('|');
if (parts.isEmpty) break;
final label = parts[0];
final qual = parts.length > 1 ? int.tryParse(parts[1]) ?? 0 : 0;
if (name.contains(label)) break; // avoid repeats
if (remaining.abs() < qual.abs()) break;
name = '$label $name';
remaining -= qual;
count++;
}
if (remaining != 0) {
name = '${remaining > 0 ? '+' : ''}$remaining $name';
}
return name;
}
/// 장비 생성 (원본 Main.pas:791-830 WinEquip)
/// [slotIndex]: 0=Weapon, 1=Shield, 2-10=Armor 계열
String winEquip(
PqConfig config,
DeterministicRandom rng,
int level,
int slotIndex,
) {
final bool isWeapon = slotIndex == 0;
final List<String> items;
if (slotIndex == 0) {
items = config.weapons;
} else if (slotIndex == 1) {
items = config.shields;
} else {
items = config.armors;
}
final better = isWeapon ? config.offenseAttrib : config.defenseAttrib;
final worse = isWeapon ? config.offenseBad : config.defenseBad;
final base = _lPick(items, rng, level);
final parts = base.split('|');
final baseName = parts[0];
final qual = parts.length > 1
? int.tryParse(parts[1].replaceAll('+', '')) ?? 0
: 0;
final plus = level - qual;
final modifierList = plus >= 0 ? better : worse;
return addModifier(rng, baseName, modifierList, plus);
}
/// EquipmentSlot enum을 사용하는 편의 함수
String winEquipBySlot(
PqConfig config,
DeterministicRandom rng,
int level,
EquipmentSlot slot,
) {
return winEquip(config, rng, level, slot.index);
}
/// 구조화된 장비 생성 결과 반환 (l10n 지원)
EquipResult winEquipStructured(
PqConfig config,
DeterministicRandom rng,
int level,
int slotIndex,
) {
final bool isWeapon = slotIndex == 0;
final List<String> items;
if (slotIndex == 0) {
items = config.weapons;
} else if (slotIndex == 1) {
items = config.shields;
} else {
items = config.armors;
}
final better = isWeapon ? config.offenseAttrib : config.defenseAttrib;
final worse = isWeapon ? config.offenseBad : config.defenseBad;
final base = _lPick(items, rng, level);
final parts = base.split('|');
final baseName = parts[0];
final qual = parts.length > 1
? int.tryParse(parts[1].replaceAll('+', '')) ?? 0
: 0;
final plus = level - qual;
final modifierList = plus >= 0 ? better : worse;
return _addModifierStructured(rng, baseName, modifierList, plus);
}
/// 구조화된 장비 결과 반환 (내부 함수)
EquipResult _addModifierStructured(
DeterministicRandom rng,
String baseName,
List<String> modifiers,
int plus,
) {
final collectedModifiers = <String>[];
var remaining = plus;
var count = 0;
while (count < 2 && remaining != 0) {
final modifier = pick(modifiers, rng);
final parts = modifier.split('|');
if (parts.isEmpty) break;
final label = parts[0];
final qual = parts.length > 1 ? int.tryParse(parts[1]) ?? 0 : 0;
if (collectedModifiers.contains(label)) break; // avoid repeats
if (remaining.abs() < qual.abs()) break;
collectedModifiers.add(label);
remaining -= qual;
count++;
}
return EquipResult(
baseName: baseName,
modifiers: collectedModifiers,
plusValue: remaining,
);
}
/// EquipmentSlot enum을 사용하는 구조화된 버전
EquipResult winEquipBySlotStructured(
PqConfig config,
DeterministicRandom rng,
int level,
EquipmentSlot slot,
) {
return winEquipStructured(config, rng, level, slot.index);
}