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 허브로 유지 (역호환성)
This commit is contained in:
327
lib/src/core/util/pq_item.dart
Normal file
327
lib/src/core/util/pq_item.dart
Normal file
@@ -0,0 +1,327 @@
|
||||
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);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
283
lib/src/core/util/pq_monster.dart
Normal file
283
lib/src/core/util/pq_monster.dart
Normal file
@@ -0,0 +1,283 @@
|
||||
import 'package:asciineverdie/data/game_text_l10n.dart' as l10n;
|
||||
import 'package:asciineverdie/src/core/model/monster_grade.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의 몬스터 생성 로직을 포팅
|
||||
|
||||
/// monsterTask의 반환 타입 (원본 fTask.Caption 정보 포함)
|
||||
class MonsterTaskResult {
|
||||
const MonsterTaskResult({
|
||||
required this.displayName,
|
||||
required this.baseName,
|
||||
required this.level,
|
||||
required this.part,
|
||||
required this.grade,
|
||||
});
|
||||
|
||||
/// 화면에 표시할 몬스터 이름 (형용사 포함, 예: "a sick Goblin")
|
||||
final String displayName;
|
||||
|
||||
/// 기본 몬스터 이름 (형용사 제외, 예: "Goblin")
|
||||
final String baseName;
|
||||
|
||||
/// 몬스터 레벨
|
||||
final int level;
|
||||
|
||||
/// 전리품 부위 (예: "claw", "tail", "*"는 WinItem 호출)
|
||||
final String part;
|
||||
|
||||
/// 몬스터 등급 (Normal/Elite/Boss)
|
||||
final MonsterGrade grade;
|
||||
}
|
||||
|
||||
/// 몬스터 등급 결정 (Normal 85%, Elite 12%, Boss 3%)
|
||||
/// 몬스터 레벨이 플레이어 레벨보다 높으면 상위 등급 확률 증가
|
||||
MonsterGrade _determineGrade(
|
||||
int monsterLevel,
|
||||
int playerLevel,
|
||||
DeterministicRandom rng,
|
||||
) {
|
||||
final levelDiff = monsterLevel - playerLevel;
|
||||
final eliteBonus = (levelDiff * 2).clamp(0, 10);
|
||||
final bossBonus = (levelDiff * 0.5).clamp(0, 3).toInt();
|
||||
|
||||
final roll = rng.nextInt(100);
|
||||
if (roll < 3 + bossBonus) return MonsterGrade.boss;
|
||||
if (roll < 15 + eliteBonus) return MonsterGrade.elite;
|
||||
return MonsterGrade.normal;
|
||||
}
|
||||
|
||||
/// 몬스터 문자열에서 레벨 파싱
|
||||
int _monsterLevel(String entry) {
|
||||
final parts = entry.split('|');
|
||||
if (parts.length < 2) return 0;
|
||||
return int.tryParse(parts[1]) ?? 0;
|
||||
}
|
||||
|
||||
/// 몬스터 태스크 생성 (원본 Main.pas:523-640)
|
||||
MonsterTaskResult monsterTask(
|
||||
PqConfig config,
|
||||
DeterministicRandom rng,
|
||||
int level,
|
||||
String? questMonster,
|
||||
int? questLevel,
|
||||
) {
|
||||
var targetLevel = level;
|
||||
|
||||
for (var i = level; i > 0; i--) {
|
||||
if (rng.nextInt(5) < 2) {
|
||||
targetLevel += rng.nextInt(2) * 2 - 1; // RandSign
|
||||
}
|
||||
}
|
||||
if (targetLevel < 1) targetLevel = 1;
|
||||
|
||||
String monster;
|
||||
int monsterLevel;
|
||||
String part;
|
||||
bool definite = false;
|
||||
|
||||
// 원본 Main.pas:537-547: 가끔 NPC를 몬스터로 사용
|
||||
if (rng.nextInt(25) == 0) {
|
||||
final raceEn = pick(config.races, rng).split('|').first;
|
||||
final race = l10n.translateRace(raceEn);
|
||||
if (rng.nextInt(2) == 0) {
|
||||
final klassEn = pick(config.klasses, rng).split('|').first;
|
||||
final klass = l10n.translateKlass(klassEn);
|
||||
monster = l10n.modifierPassing('$race $klass');
|
||||
} else {
|
||||
final titleEn = pickLow(config.titles, rng);
|
||||
final title = l10n.translateTitle(titleEn);
|
||||
monster = l10n.namedMonsterFormat(generateName(rng), '$title $race');
|
||||
definite = true;
|
||||
}
|
||||
monsterLevel = targetLevel;
|
||||
part = '*';
|
||||
} else if (questMonster != null && rng.nextInt(4) == 0) {
|
||||
monster = questMonster;
|
||||
final parts = questMonster.split('|');
|
||||
monsterLevel = questLevel ?? targetLevel;
|
||||
part = parts.length > 2 ? parts[2] : '';
|
||||
} else {
|
||||
monster = pick(config.monsters, rng);
|
||||
monsterLevel = _monsterLevel(monster);
|
||||
for (var i = 0; i < 5; i++) {
|
||||
final candidate = pick(config.monsters, rng);
|
||||
final candLevel = _monsterLevel(candidate);
|
||||
if ((targetLevel - candLevel).abs() <
|
||||
(targetLevel - monsterLevel).abs()) {
|
||||
monster = candidate;
|
||||
monsterLevel = candLevel;
|
||||
}
|
||||
}
|
||||
final monsterParts = monster.split('|');
|
||||
part = monsterParts.length > 2 ? monsterParts[2] : '';
|
||||
}
|
||||
|
||||
final baseName = monster.split('|').first;
|
||||
|
||||
var qty = 1;
|
||||
final levelDiff = targetLevel - monsterLevel;
|
||||
var name = l10n.translateMonster(baseName);
|
||||
|
||||
if (levelDiff > 10) {
|
||||
qty =
|
||||
(targetLevel + rng.nextInt(monsterLevel == 0 ? 1 : monsterLevel)) ~/
|
||||
(monsterLevel == 0 ? 1 : monsterLevel);
|
||||
if (qty < 1) qty = 1;
|
||||
targetLevel ~/= qty;
|
||||
}
|
||||
|
||||
if (levelDiff <= -10) {
|
||||
name = l10n.modifierImaginary(name);
|
||||
} else if (levelDiff < -5) {
|
||||
final i = 5 - rng.nextInt(10 + levelDiff + 1);
|
||||
name = _sick(i, _young((monsterLevel - targetLevel) - i, name));
|
||||
} else if (levelDiff < 0) {
|
||||
if (rng.nextInt(2) == 1) {
|
||||
name = _sick(levelDiff, name);
|
||||
} else {
|
||||
name = _young(levelDiff, name);
|
||||
}
|
||||
} else if (levelDiff >= 10) {
|
||||
name = l10n.modifierMessianic(name);
|
||||
} else if (levelDiff > 5) {
|
||||
final i = 5 - rng.nextInt(10 - levelDiff + 1);
|
||||
name = _big(i, _special((levelDiff) - i, name));
|
||||
} else if (levelDiff > 0) {
|
||||
if (rng.nextInt(2) == 1) {
|
||||
name = _big(levelDiff, name);
|
||||
} else {
|
||||
name = _special(levelDiff, name);
|
||||
}
|
||||
}
|
||||
|
||||
if (!definite) {
|
||||
name = l10n.indefiniteL10n(name, qty);
|
||||
}
|
||||
|
||||
final grade = _determineGrade(monsterLevel, level, rng);
|
||||
|
||||
return MonsterTaskResult(
|
||||
displayName: name,
|
||||
baseName: baseName,
|
||||
level: monsterLevel * qty,
|
||||
part: part,
|
||||
grade: grade,
|
||||
);
|
||||
}
|
||||
|
||||
/// 이름 있는 몬스터 생성 (NamedMonster, Main.pas:498-512)
|
||||
String namedMonster(PqConfig config, DeterministicRandom rng, int level) {
|
||||
String best = '';
|
||||
int bestLevel = 0;
|
||||
|
||||
for (var i = 0; i < 5; i++) {
|
||||
final m = pick(config.monsters, rng);
|
||||
final parts = m.split('|');
|
||||
final name = parts.first;
|
||||
final lev = parts.length > 1 ? (int.tryParse(parts[1]) ?? 0) : 0;
|
||||
|
||||
if (best.isEmpty || (level - lev).abs() < (level - bestLevel).abs()) {
|
||||
best = name;
|
||||
bestLevel = lev;
|
||||
}
|
||||
}
|
||||
|
||||
final translatedMonster = l10n.translateMonster(best);
|
||||
return l10n.namedMonsterFormat(generateName(rng), translatedMonster);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 몬스터 수식어 함수들
|
||||
// =============================================================================
|
||||
|
||||
String _sick(int m, String s) {
|
||||
switch (m) {
|
||||
case -5:
|
||||
case 5:
|
||||
return l10n.modifierDead(s);
|
||||
case -4:
|
||||
case 4:
|
||||
return l10n.modifierComatose(s);
|
||||
case -3:
|
||||
case 3:
|
||||
return l10n.modifierCrippled(s);
|
||||
case -2:
|
||||
case 2:
|
||||
return l10n.modifierSick(s);
|
||||
case -1:
|
||||
case 1:
|
||||
return l10n.modifierUndernourished(s);
|
||||
default:
|
||||
return '$m$s';
|
||||
}
|
||||
}
|
||||
|
||||
String _young(int m, String s) {
|
||||
switch (-m) {
|
||||
case -5:
|
||||
case 5:
|
||||
return l10n.modifierFoetal(s);
|
||||
case -4:
|
||||
case 4:
|
||||
return l10n.modifierBaby(s);
|
||||
case -3:
|
||||
case 3:
|
||||
return l10n.modifierPreadolescent(s);
|
||||
case -2:
|
||||
case 2:
|
||||
return l10n.modifierTeenage(s);
|
||||
case -1:
|
||||
case 1:
|
||||
return l10n.modifierUnderage(s);
|
||||
default:
|
||||
return '$m$s';
|
||||
}
|
||||
}
|
||||
|
||||
String _big(int m, String s) {
|
||||
switch (m) {
|
||||
case 1:
|
||||
case -1:
|
||||
return l10n.modifierGreater(s);
|
||||
case 2:
|
||||
case -2:
|
||||
return l10n.modifierMassive(s);
|
||||
case 3:
|
||||
case -3:
|
||||
return l10n.modifierEnormous(s);
|
||||
case 4:
|
||||
case -4:
|
||||
return l10n.modifierGiant(s);
|
||||
case 5:
|
||||
case -5:
|
||||
return l10n.modifierTitanic(s);
|
||||
default:
|
||||
return s;
|
||||
}
|
||||
}
|
||||
|
||||
String _special(int m, String s) {
|
||||
switch (-m) {
|
||||
case 1:
|
||||
case -1:
|
||||
return s.contains(' ') ? l10n.modifierVeteran(s) : l10n.modifierBattle(s);
|
||||
case 2:
|
||||
case -2:
|
||||
return l10n.modifierCursed(s);
|
||||
case 3:
|
||||
case -3:
|
||||
return s.contains(' ') ? l10n.modifierWarrior(s) : l10n.modifierWere(s);
|
||||
case 4:
|
||||
case -4:
|
||||
return l10n.modifierUndead(s);
|
||||
case 5:
|
||||
case -5:
|
||||
return l10n.modifierDemon(s);
|
||||
default:
|
||||
return s;
|
||||
}
|
||||
}
|
||||
283
lib/src/core/util/pq_quest.dart
Normal file
283
lib/src/core/util/pq_quest.dart
Normal file
@@ -0,0 +1,283 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:asciineverdie/data/game_text_l10n.dart' as l10n;
|
||||
import 'package:asciineverdie/src/core/model/game_state.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_item.dart';
|
||||
import 'package:asciineverdie/src/core/util/pq_monster.dart';
|
||||
import 'package:asciineverdie/src/core/util/pq_random.dart';
|
||||
import 'package:asciineverdie/src/core/util/roman.dart';
|
||||
|
||||
/// 퀘스트/Act/시네마틱 관련 함수들
|
||||
/// 원본 Main.pas의 퀘스트 로직을 포팅
|
||||
|
||||
/// 보상 종류
|
||||
enum RewardKind { spell, equip, stat, item }
|
||||
|
||||
/// 퀘스트 결과
|
||||
class QuestResult {
|
||||
const QuestResult({
|
||||
required this.caption,
|
||||
required this.reward,
|
||||
this.monsterName,
|
||||
this.monsterLevel,
|
||||
this.monsterIndex,
|
||||
});
|
||||
|
||||
final String caption;
|
||||
final RewardKind reward;
|
||||
|
||||
/// 몬스터 전체 데이터 (예: "Rat|5|tail") - 원본 fQuest.Caption
|
||||
final String? monsterName;
|
||||
|
||||
/// 몬스터 레벨 (파싱된 값)
|
||||
final int? monsterLevel;
|
||||
|
||||
/// 몬스터 인덱스 (config.monsters에서의 위치) - 원본 fQuest.Tag
|
||||
final int? monsterIndex;
|
||||
}
|
||||
|
||||
/// Act 결과
|
||||
class ActResult {
|
||||
const ActResult({
|
||||
required this.actTitle,
|
||||
required this.plotBarMaxSeconds,
|
||||
required this.rewards,
|
||||
});
|
||||
|
||||
final String actTitle;
|
||||
final int plotBarMaxSeconds;
|
||||
final List<RewardKind> rewards;
|
||||
}
|
||||
|
||||
/// 몬스터 문자열에서 레벨 파싱
|
||||
int _monsterLevel(String entry) {
|
||||
final parts = entry.split('|');
|
||||
if (parts.length < 2) return 0;
|
||||
return int.tryParse(parts[1]) ?? 0;
|
||||
}
|
||||
|
||||
/// 퀘스트 완료 처리 (원본 Main.pas:930-984)
|
||||
QuestResult completeQuest(PqConfig config, DeterministicRandom rng, int level) {
|
||||
final rewardRoll = rng.nextInt(4);
|
||||
final reward = switch (rewardRoll) {
|
||||
0 => RewardKind.spell,
|
||||
1 => RewardKind.equip,
|
||||
2 => RewardKind.stat,
|
||||
_ => RewardKind.item,
|
||||
};
|
||||
|
||||
final questRoll = rng.nextInt(5);
|
||||
switch (questRoll) {
|
||||
case 0:
|
||||
// Exterminate: 4번 시도하여 레벨에 가장 가까운 몬스터 선택
|
||||
var best = '';
|
||||
var bestLevel = 0;
|
||||
var bestIndex = 0;
|
||||
for (var i = 0; i < 4; i++) {
|
||||
final monsterIndex = rng.nextInt(config.monsters.length);
|
||||
final m = config.monsters[monsterIndex];
|
||||
final l = _monsterLevel(m);
|
||||
if (i == 0 || (l - level).abs() < (bestLevel - level).abs()) {
|
||||
best = m;
|
||||
bestLevel = l;
|
||||
bestIndex = monsterIndex;
|
||||
}
|
||||
}
|
||||
final nameEn = best.split('|').first;
|
||||
final name = l10n.translateMonster(nameEn);
|
||||
return QuestResult(
|
||||
caption: l10n.questPatch(l10n.definiteL10n(name, 2)),
|
||||
reward: reward,
|
||||
monsterName: best,
|
||||
monsterLevel: bestLevel,
|
||||
monsterIndex: bestIndex,
|
||||
);
|
||||
case 1:
|
||||
// interestingItem: attrib + special 조합 후 번역
|
||||
final attr = pick(config.itemAttrib, rng);
|
||||
final special = pick(config.specials, rng);
|
||||
final item = l10n.translateInterestingItem(attr, special);
|
||||
return QuestResult(
|
||||
caption: l10n.questLocate(l10n.definiteL10n(item, 1)),
|
||||
reward: reward,
|
||||
);
|
||||
case 2:
|
||||
final itemEn = boringItem(config, rng);
|
||||
final item = l10n.translateBoringItem(itemEn);
|
||||
return QuestResult(caption: l10n.questTransfer(item), reward: reward);
|
||||
case 3:
|
||||
final itemEn = boringItem(config, rng);
|
||||
final item = l10n.translateBoringItem(itemEn);
|
||||
return QuestResult(
|
||||
caption: l10n.questDownload(l10n.indefiniteL10n(item, 1)),
|
||||
reward: reward,
|
||||
);
|
||||
default:
|
||||
// Stabilize: 2번 시도하여 레벨에 가장 가까운 몬스터 선택
|
||||
var best = '';
|
||||
var bestLevel = 0;
|
||||
for (var i = 0; i < 2; i++) {
|
||||
final m = pick(config.monsters, rng);
|
||||
final l = _monsterLevel(m);
|
||||
if (i == 0 || (l - level).abs() < (bestLevel - level).abs()) {
|
||||
best = m;
|
||||
bestLevel = l;
|
||||
}
|
||||
}
|
||||
final nameEn = best.split('|').first;
|
||||
final name = l10n.translateMonster(nameEn);
|
||||
return QuestResult(
|
||||
caption: l10n.questStabilize(l10n.definiteL10n(name, 2)),
|
||||
reward: reward,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Act별 Plot Bar 최대값 (초) - 10시간 완주 목표
|
||||
const _actPlotBarSeconds = [
|
||||
300, // Prologue: 5분
|
||||
7200, // Act I: 2시간
|
||||
10800, // Act II: 3시간
|
||||
10800, // Act III: 3시간
|
||||
5400, // Act IV: 1.5시간
|
||||
1800, // Act V: 30분
|
||||
];
|
||||
|
||||
/// Act 완료 처리
|
||||
ActResult completeAct(int existingActCount) {
|
||||
final nextActIndex = existingActCount;
|
||||
final title = l10n.actTitle(intToRoman(nextActIndex));
|
||||
final plotBarMax = existingActCount < _actPlotBarSeconds.length
|
||||
? _actPlotBarSeconds[existingActCount]
|
||||
: 3600;
|
||||
|
||||
final rewards = <RewardKind>[];
|
||||
if (existingActCount >= 1) {
|
||||
rewards.add(RewardKind.equip);
|
||||
}
|
||||
if (existingActCount > 1) {
|
||||
rewards.add(RewardKind.item);
|
||||
}
|
||||
|
||||
return ActResult(
|
||||
actTitle: title,
|
||||
plotBarMaxSeconds: plotBarMax,
|
||||
rewards: rewards,
|
||||
);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 시네마틱 관련 함수들 (Main.pas:456-521)
|
||||
// =============================================================================
|
||||
|
||||
/// NPC 이름 생성 (ImpressiveGuy, Main.pas:514-521)
|
||||
String impressiveGuy(PqConfig config, DeterministicRandom rng) {
|
||||
final titleEn = pick(config.impressiveTitles, rng);
|
||||
final title = l10n.translateImpressiveTitle(titleEn);
|
||||
switch (rng.nextInt(2)) {
|
||||
case 0:
|
||||
final raceEn = pick(config.races, rng).split('|').first;
|
||||
final race = l10n.translateRace(raceEn);
|
||||
return l10n.impressiveGuyPattern1(title, race);
|
||||
case 1:
|
||||
final name1 = generateName(rng);
|
||||
final name2 = generateName(rng);
|
||||
return l10n.impressiveGuyPattern2(title, name1, name2);
|
||||
default:
|
||||
return title;
|
||||
}
|
||||
}
|
||||
|
||||
/// 플롯 간 시네마틱 이벤트 생성 (InterplotCinematic, Main.pas:456-495)
|
||||
List<QueueEntry> interplotCinematic(
|
||||
PqConfig config,
|
||||
DeterministicRandom rng,
|
||||
int level,
|
||||
int plotCount,
|
||||
) {
|
||||
final entries = <QueueEntry>[];
|
||||
|
||||
void q(QueueKind kind, int seconds, String caption) {
|
||||
entries.add(
|
||||
QueueEntry(kind: kind, durationMillis: seconds * 1000, caption: caption),
|
||||
);
|
||||
}
|
||||
|
||||
switch (rng.nextInt(3)) {
|
||||
case 0:
|
||||
// 시나리오 1: 안전한 캐시 영역 도착
|
||||
q(QueueKind.task, 1, l10n.cinematicCacheZone1());
|
||||
q(QueueKind.task, 2, l10n.cinematicCacheZone2());
|
||||
q(QueueKind.task, 2, l10n.cinematicCacheZone3());
|
||||
q(QueueKind.task, 1, l10n.cinematicCacheZone4());
|
||||
break;
|
||||
|
||||
case 1:
|
||||
// 시나리오 2: 강력한 버그와의 전투
|
||||
q(QueueKind.task, 1, l10n.cinematicCombat1());
|
||||
final nemesis = namedMonster(config, rng, level + 3);
|
||||
q(QueueKind.task, 4, l10n.cinematicCombat2(nemesis));
|
||||
|
||||
var s = rng.nextInt(3);
|
||||
final combatRounds = rng.nextInt(1 + plotCount);
|
||||
for (var i = 0; i < combatRounds; i++) {
|
||||
s += 1 + rng.nextInt(2);
|
||||
switch (s % 3) {
|
||||
case 0:
|
||||
q(QueueKind.task, 2, l10n.cinematicCombatLocked(nemesis));
|
||||
break;
|
||||
case 1:
|
||||
q(QueueKind.task, 2, l10n.cinematicCombatCorrupts(nemesis));
|
||||
break;
|
||||
case 2:
|
||||
q(QueueKind.task, 2, l10n.cinematicCombatWorking(nemesis));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
q(QueueKind.task, 3, l10n.cinematicCombatVictory(nemesis));
|
||||
q(QueueKind.task, 2, l10n.cinematicCombatWakeUp());
|
||||
break;
|
||||
|
||||
case 2:
|
||||
// 시나리오 3: 내부자 위협 발견
|
||||
final guy = impressiveGuy(config, rng);
|
||||
final itemEn = boringItem(config, rng);
|
||||
final item = l10n.translateBoringItem(itemEn);
|
||||
q(QueueKind.task, 2, l10n.cinematicBetrayal1(guy));
|
||||
q(QueueKind.task, 3, l10n.cinematicBetrayal2(guy));
|
||||
q(QueueKind.task, 2, l10n.cinematicBetrayal3(item));
|
||||
q(QueueKind.task, 2, l10n.cinematicBetrayal4());
|
||||
q(QueueKind.task, 2, l10n.cinematicBetrayal5(guy));
|
||||
q(QueueKind.task, 3, l10n.cinematicBetrayal6());
|
||||
break;
|
||||
}
|
||||
|
||||
q(QueueKind.plot, 2, l10n.taskCompiling);
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 스펠 관련 함수
|
||||
// =============================================================================
|
||||
|
||||
/// 스펠 획득 (원본 Main.pas:770-774)
|
||||
String winSpell(
|
||||
PqConfig config,
|
||||
DeterministicRandom rng,
|
||||
int wisdom,
|
||||
int level,
|
||||
) {
|
||||
final maxIndex = math.min(wisdom + level, config.spells.length);
|
||||
if (maxIndex <= 0) return '';
|
||||
final index = randomLow(rng, maxIndex);
|
||||
final entry = config.spells[index];
|
||||
final parts = entry.split('|');
|
||||
final name = parts[0];
|
||||
final currentRank = romanToInt(parts.length > 1 ? parts[1] : 'I');
|
||||
final nextRank = math.max(1, currentRank + 1);
|
||||
return '$name|${intToRoman(nextRank)}';
|
||||
}
|
||||
61
lib/src/core/util/pq_random.dart
Normal file
61
lib/src/core/util/pq_random.dart
Normal file
@@ -0,0 +1,61 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:asciineverdie/src/core/util/deterministic_random.dart';
|
||||
|
||||
/// 랜덤/확률 관련 유틸리티 함수들
|
||||
/// 원본 Main.pas의 랜덤 헬퍼 함수들을 포팅
|
||||
|
||||
/// 두 번의 랜덤 중 작은 값 반환 (낮은 인덱스 선호)
|
||||
int randomLow(DeterministicRandom rng, int below) {
|
||||
return math.min(rng.nextInt(below), rng.nextInt(below));
|
||||
}
|
||||
|
||||
/// 리스트에서 랜덤 선택
|
||||
String pick(List<String> values, DeterministicRandom rng) {
|
||||
if (values.isEmpty) return '';
|
||||
return values[rng.nextInt(values.length)];
|
||||
}
|
||||
|
||||
/// 리스트에서 낮은 인덱스 선호하여 선택
|
||||
String pickLow(List<String> values, DeterministicRandom rng) {
|
||||
if (values.isEmpty) return '';
|
||||
return values[randomLow(rng, values.length)];
|
||||
}
|
||||
|
||||
/// 64비트 범위 랜덤 (큰 범위용)
|
||||
int random64Below(DeterministicRandom rng, int below) {
|
||||
if (below <= 0) return 0;
|
||||
final hi = rng.nextUint32();
|
||||
final lo = rng.nextUint32();
|
||||
final combined = (hi << 32) | lo;
|
||||
return (combined % below).toInt();
|
||||
}
|
||||
|
||||
/// 3d6 스탯 롤
|
||||
int rollStat(DeterministicRandom rng) {
|
||||
return 3 + rng.nextInt(6) + rng.nextInt(6) + rng.nextInt(6);
|
||||
}
|
||||
|
||||
/// 캐릭터 이름 생성 (원본 NewGuy.pas 기반)
|
||||
String generateName(DeterministicRandom rng) {
|
||||
const kParts = [
|
||||
'br|cr|dr|fr|gr|j|kr|l|m|n|pr||||r|sh|tr|v|wh|x|y|z',
|
||||
'a|a|e|e|i|i|o|o|u|u|ae|ie|oo|ou',
|
||||
'b|ck|d|g|k|m|n|p|t|v|x|z',
|
||||
];
|
||||
|
||||
var result = '';
|
||||
for (var i = 0; i <= 5; i++) {
|
||||
result += _pickFromPipe(kParts[i % 3], rng);
|
||||
}
|
||||
if (result.isEmpty) return result;
|
||||
return '${result[0].toUpperCase()}${result.substring(1)}';
|
||||
}
|
||||
|
||||
/// 파이프(|)로 구분된 문자열에서 랜덤 선택
|
||||
String _pickFromPipe(String pipeSeparated, DeterministicRandom rng) {
|
||||
final parts = pipeSeparated.split('|');
|
||||
if (parts.isEmpty) return '';
|
||||
final idx = rng.nextInt(parts.length);
|
||||
return parts[idx];
|
||||
}
|
||||
64
lib/src/core/util/pq_stat.dart
Normal file
64
lib/src/core/util/pq_stat.dart
Normal file
@@ -0,0 +1,64 @@
|
||||
import 'package:asciineverdie/src/core/model/game_state.dart';
|
||||
import 'package:asciineverdie/src/core/util/deterministic_random.dart';
|
||||
import 'package:asciineverdie/src/core/util/pq_random.dart';
|
||||
|
||||
/// 스탯 관련 함수들
|
||||
/// 원본 Main.pas의 스탯 처리 로직을 포팅
|
||||
|
||||
/// 스탯 증가 인덱스 결정 (원본 Main.pas:870-883)
|
||||
int winStatIndex(DeterministicRandom rng, List<int> statValues) {
|
||||
// 50%: 모든 8개 스탯 중 랜덤
|
||||
// 50%: 첫 6개(STR~CHA)만 제곱 가중치로 선택
|
||||
if (rng.nextInt(2) == 0) {
|
||||
return rng.nextInt(statValues.length);
|
||||
}
|
||||
|
||||
// 첫 6개(STR, CON, DEX, INT, WIS, CHA)만 제곱 가중치 적용
|
||||
const firstSixCount = 6;
|
||||
var total = 0;
|
||||
for (var i = 0; i < firstSixCount && i < statValues.length; i++) {
|
||||
total += statValues[i] * statValues[i];
|
||||
}
|
||||
if (total == 0) return rng.nextInt(firstSixCount);
|
||||
var pickValue = random64Below(rng, total);
|
||||
for (var i = 0; i < firstSixCount; i++) {
|
||||
pickValue -= statValues[i] * statValues[i];
|
||||
if (pickValue < 0) return i;
|
||||
}
|
||||
return firstSixCount - 1;
|
||||
}
|
||||
|
||||
/// 스탯 증가 적용
|
||||
Stats winStat(Stats stats, DeterministicRandom rng) {
|
||||
final values = <int>[
|
||||
stats.str,
|
||||
stats.con,
|
||||
stats.dex,
|
||||
stats.intelligence,
|
||||
stats.wis,
|
||||
stats.cha,
|
||||
stats.hpMax,
|
||||
stats.mpMax,
|
||||
];
|
||||
final idx = winStatIndex(rng, values);
|
||||
switch (idx) {
|
||||
case 0:
|
||||
return stats.copyWith(str: stats.str + 1);
|
||||
case 1:
|
||||
return stats.copyWith(con: stats.con + 1);
|
||||
case 2:
|
||||
return stats.copyWith(dex: stats.dex + 1);
|
||||
case 3:
|
||||
return stats.copyWith(intelligence: stats.intelligence + 1);
|
||||
case 4:
|
||||
return stats.copyWith(wis: stats.wis + 1);
|
||||
case 5:
|
||||
return stats.copyWith(cha: stats.cha + 1);
|
||||
case 6:
|
||||
return stats.copyWith(hpMax: stats.hpMax + 1);
|
||||
case 7:
|
||||
return stats.copyWith(mpMax: stats.mpMax + 1);
|
||||
default:
|
||||
return stats;
|
||||
}
|
||||
}
|
||||
55
lib/src/core/util/pq_string.dart
Normal file
55
lib/src/core/util/pq_string.dart
Normal file
@@ -0,0 +1,55 @@
|
||||
import 'package:asciineverdie/data/game_text_l10n.dart' as l10n;
|
||||
|
||||
/// 문자열 유틸리티 함수들
|
||||
/// 원본 Main.pas의 문자열 처리 함수들을 포팅
|
||||
|
||||
/// 단어를 복수형으로 변환 (영문)
|
||||
String pluralize(String s) {
|
||||
if (_ends(s, 'y')) return '${s.substring(0, s.length - 1)}ies';
|
||||
if (_ends(s, 'us')) return '${s.substring(0, s.length - 2)}i';
|
||||
if (_ends(s, 'ch') || _ends(s, 'x') || _ends(s, 's')) return '${s}es';
|
||||
if (_ends(s, 'f')) return '${s.substring(0, s.length - 1)}ves';
|
||||
if (_ends(s, 'man') || _ends(s, 'Man')) {
|
||||
return '${s.substring(0, s.length - 2)}en';
|
||||
}
|
||||
return '${s}s';
|
||||
}
|
||||
|
||||
/// 부정관사 + 명사 (a/an + 단수 또는 수량 + 복수)
|
||||
String indefinite(String s, int qty) {
|
||||
if (qty == 1) {
|
||||
const vowels = 'AEIOUÜaeiouü';
|
||||
final first = s.isNotEmpty ? s[0] : 'a';
|
||||
final article = vowels.contains(first) ? 'an' : 'a';
|
||||
return '$article $s';
|
||||
}
|
||||
return '$qty ${pluralize(s)}';
|
||||
}
|
||||
|
||||
/// 정관사 + 명사 (the + 명사)
|
||||
String definite(String s, int qty) {
|
||||
if (qty > 1) {
|
||||
s = pluralize(s);
|
||||
}
|
||||
return 'the $s';
|
||||
}
|
||||
|
||||
/// 초 단위 시간을 사람이 읽기 쉬운 형태로 변환 (원본 Main.pas:1265-1271)
|
||||
/// l10n 지원
|
||||
String roughTime(int seconds) {
|
||||
if (seconds < 120) {
|
||||
return l10n.roughTimeSeconds(seconds);
|
||||
} else if (seconds < 60 * 120) {
|
||||
return l10n.roughTimeMinutes(seconds ~/ 60);
|
||||
} else if (seconds < 60 * 60 * 48) {
|
||||
return l10n.roughTimeHours(seconds ~/ 3600);
|
||||
} else {
|
||||
return l10n.roughTimeDays(seconds ~/ (3600 * 24));
|
||||
}
|
||||
}
|
||||
|
||||
/// 문자열이 특정 접미사로 끝나는지 확인
|
||||
bool _ends(String s, String suffix) {
|
||||
return s.length >= suffix.length &&
|
||||
s.substring(s.length - suffix.length) == suffix;
|
||||
}
|
||||
97
lib/src/core/util/pq_task.dart
Normal file
97
lib/src/core/util/pq_task.dart
Normal file
@@ -0,0 +1,97 @@
|
||||
import 'dart:collection';
|
||||
|
||||
import 'package:asciineverdie/src/core/model/game_state.dart';
|
||||
|
||||
/// 태스크/큐/진행 관련 함수들
|
||||
/// 원본 Main.pas의 태스크 처리 로직을 포팅
|
||||
|
||||
/// 레벨업에 필요한 시간 (초)
|
||||
int levelUpTimeSeconds(int level) {
|
||||
// Act 진행과 레벨업 동기화 (10시간 완주 목표)
|
||||
if (level <= 20) {
|
||||
// Act I: 레벨 1-20, 2시간 (7200초) → 평균 360초/레벨
|
||||
return 300 + (level * 6);
|
||||
} else if (level <= 40) {
|
||||
// Act II: 레벨 21-40, 3시간 (10800초) → 평균 540초/레벨
|
||||
return 400 + ((level - 20) * 10);
|
||||
} else if (level <= 60) {
|
||||
// Act III: 레벨 41-60, 3시간 (10800초) → 평균 540초/레벨
|
||||
return 400 + ((level - 40) * 10);
|
||||
} else if (level <= 80) {
|
||||
// Act IV: 레벨 61-80, 1.5시간 (5400초) → 평균 270초/레벨
|
||||
return 200 + ((level - 60) * 5);
|
||||
} else {
|
||||
// Act V: 레벨 81-100, 30분 (1800초) → 평균 90초/레벨
|
||||
return 60 + ((level - 80) * 3);
|
||||
}
|
||||
}
|
||||
|
||||
/// levelUpTimeSeconds의 별칭 (호환성 유지)
|
||||
int levelUpTime(int level) => levelUpTimeSeconds(level);
|
||||
|
||||
/// 태스크 결과
|
||||
class TaskResult {
|
||||
const TaskResult({
|
||||
required this.caption,
|
||||
required this.durationMillis,
|
||||
required this.progress,
|
||||
});
|
||||
|
||||
final String caption;
|
||||
final int durationMillis;
|
||||
final ProgressState progress;
|
||||
}
|
||||
|
||||
/// 태스크 시작: 태스크 바 리셋 및 캡션 설정
|
||||
TaskResult startTask(
|
||||
ProgressState progress,
|
||||
String caption,
|
||||
int durationMillis,
|
||||
) {
|
||||
final updated = progress.copyWith(
|
||||
task: ProgressBarState(position: 0, max: durationMillis),
|
||||
);
|
||||
return TaskResult(
|
||||
caption: '$caption...',
|
||||
durationMillis: durationMillis,
|
||||
progress: updated,
|
||||
);
|
||||
}
|
||||
|
||||
/// 큐 처리 결과
|
||||
class DequeueResult {
|
||||
const DequeueResult({
|
||||
required this.progress,
|
||||
required this.queue,
|
||||
required this.caption,
|
||||
required this.taskType,
|
||||
required this.kind,
|
||||
});
|
||||
|
||||
final ProgressState progress;
|
||||
final QueueState queue;
|
||||
final String caption;
|
||||
final TaskType taskType;
|
||||
final QueueKind kind;
|
||||
}
|
||||
|
||||
/// 큐에서 다음 태스크 꺼내기
|
||||
/// 현재 태스크가 완료되면 큐에서 다음 태스크를 가져옴
|
||||
DequeueResult? dequeue(ProgressState progress, QueueState queue) {
|
||||
// 태스크 바가 완료되지 않으면 null 반환
|
||||
if (progress.task.position < progress.task.max) return null;
|
||||
if (queue.entries.isEmpty) return null;
|
||||
|
||||
final entries = Queue<QueueEntry>.from(queue.entries);
|
||||
if (entries.isEmpty) return null;
|
||||
|
||||
final next = entries.removeFirst();
|
||||
final taskResult = startTask(progress, next.caption, next.durationMillis);
|
||||
return DequeueResult(
|
||||
progress: taskResult.progress,
|
||||
queue: QueueState(entries: entries.toList()),
|
||||
caption: taskResult.caption,
|
||||
taskType: next.taskType,
|
||||
kind: next.kind,
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user