- 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 허브로 유지 (역호환성)
328 lines
9.2 KiB
Dart
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);
|
|
}
|