feat: 초기 커밋
- Progress Quest 6.4 Flutter 포팅 프로젝트 - 게임 루프, 상태 관리, UI 구현 - 캐릭터 생성, 인벤토리, 장비, 주문 시스템 - 시장/판매/구매 메커니즘
This commit is contained in:
820
lib/src/core/util/pq_logic.dart
Normal file
820
lib/src/core/util/pq_logic.dart
Normal file
@@ -0,0 +1,820 @@
|
||||
import 'dart:collection';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:askiineverdie/src/core/util/deterministic_random.dart';
|
||||
import 'package:askiineverdie/src/core/model/pq_config.dart';
|
||||
import 'package:askiineverdie/src/core/util/roman.dart';
|
||||
import 'package:askiineverdie/src/core/model/equipment_slot.dart';
|
||||
import 'package:askiineverdie/src/core/model/game_state.dart';
|
||||
|
||||
// Mirrors core utility functions from the original Delphi sources (Main.pas / NewGuy.pas).
|
||||
|
||||
int levelUpTimeSeconds(int level) {
|
||||
// ~20 minutes for level 1, then exponential growth (same as LevelUpTime in Main.pas).
|
||||
final seconds = (20.0 + math.pow(1.15, level)) * 60.0;
|
||||
return seconds.round();
|
||||
}
|
||||
|
||||
/// 초 단위 시간을 사람이 읽기 쉬운 형태로 변환 (원본 Main.pas:1265-1271)
|
||||
String roughTime(int seconds) {
|
||||
if (seconds < 120) {
|
||||
return '$seconds seconds';
|
||||
} else if (seconds < 60 * 120) {
|
||||
return '${seconds ~/ 60} minutes';
|
||||
} else if (seconds < 60 * 60 * 48) {
|
||||
return '${seconds ~/ 3600} hours';
|
||||
} else {
|
||||
return '${seconds ~/ (3600 * 24)} days';
|
||||
}
|
||||
}
|
||||
|
||||
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';
|
||||
}
|
||||
|
||||
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)}';
|
||||
}
|
||||
|
||||
String definite(String s, int qty) {
|
||||
if (qty > 1) {
|
||||
s = pluralize(s);
|
||||
}
|
||||
return 'the $s';
|
||||
}
|
||||
|
||||
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 += _pick(kParts[i % 3], rng);
|
||||
}
|
||||
if (result.isEmpty) return result;
|
||||
return '${result[0].toUpperCase()}${result.substring(1)}';
|
||||
}
|
||||
|
||||
// Random helpers
|
||||
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)];
|
||||
}
|
||||
|
||||
// Item name generators (match Main.pas)
|
||||
String boringItem(PqConfig config, DeterministicRandom rng) {
|
||||
return pick(config.boringItems, rng);
|
||||
}
|
||||
|
||||
String interestingItem(PqConfig config, DeterministicRandom rng) {
|
||||
final attr = pick(config.itemAttrib, rng);
|
||||
final special = pick(config.specials, rng);
|
||||
return '$attr $special';
|
||||
}
|
||||
|
||||
String specialItem(PqConfig config, DeterministicRandom rng) {
|
||||
return '${interestingItem(config, rng)} of ${pick(config.itemOfs, 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;
|
||||
}
|
||||
|
||||
// Character/stat growth
|
||||
int levelUpTime(int level) => levelUpTimeSeconds(level);
|
||||
|
||||
String winSpell(
|
||||
PqConfig config,
|
||||
DeterministicRandom rng,
|
||||
int wisdom,
|
||||
int level,
|
||||
) {
|
||||
// 원본 Main.pas:770-774: RandomLow로 인덱스 선택 (리스트 앞쪽 선호)
|
||||
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)}';
|
||||
}
|
||||
|
||||
String winItem(PqConfig config, DeterministicRandom rng, int inventoryCount) {
|
||||
// If inventory is already very large, signal caller to duplicate an existing item.
|
||||
final threshold = math.max(250, rng.nextInt(999));
|
||||
if (inventoryCount > threshold) return '';
|
||||
return specialItem(config, rng);
|
||||
}
|
||||
|
||||
int rollStat(DeterministicRandom rng) {
|
||||
// 3d6 roll.
|
||||
return 3 + rng.nextInt(6) + rng.nextInt(6) + rng.nextInt(6);
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
String winEquip(
|
||||
PqConfig config,
|
||||
DeterministicRandom rng,
|
||||
int level,
|
||||
EquipmentSlot slot,
|
||||
) {
|
||||
// Decide item set and modifiers based on slot.
|
||||
final bool isWeapon = slot == EquipmentSlot.weapon;
|
||||
final items = switch (slot) {
|
||||
EquipmentSlot.weapon => config.weapons,
|
||||
EquipmentSlot.shield => config.shields,
|
||||
EquipmentSlot.armor => 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 modifiers = plus >= 0 ? better : worse;
|
||||
return addModifier(rng, baseName, modifiers, plus);
|
||||
}
|
||||
|
||||
int winStatIndex(DeterministicRandom rng, List<int> statValues) {
|
||||
// 원본 Main.pas:870-883: 50% 확률로 완전 랜덤, 50% 확률로 제곱 가중치
|
||||
if (rng.nextInt(2) == 0) {
|
||||
// Odds(1,2): 완전 랜덤 선택
|
||||
return rng.nextInt(statValues.length);
|
||||
}
|
||||
// 제곱 가중치로 높은 스탯 선호
|
||||
final total = statValues.fold<int>(0, (sum, v) => sum + v * v);
|
||||
if (total == 0) return rng.nextInt(statValues.length);
|
||||
var pickValue = random64Below(rng, total);
|
||||
for (var i = 0; i < statValues.length; i++) {
|
||||
pickValue -= statValues[i] * statValues[i];
|
||||
if (pickValue < 0) return i;
|
||||
}
|
||||
return statValues.length - 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;
|
||||
}
|
||||
}
|
||||
|
||||
String monsterTask(
|
||||
PqConfig config,
|
||||
DeterministicRandom rng,
|
||||
int level,
|
||||
String? questMonster, // optional monster name from quest
|
||||
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;
|
||||
bool definite = false;
|
||||
|
||||
// 원본 Main.pas:537-547: 가끔 NPC를 몬스터로 사용
|
||||
if (rng.nextInt(25) == 0) {
|
||||
final race = pick(config.races, rng).split('|').first;
|
||||
if (rng.nextInt(2) == 0) {
|
||||
// 'passing Race Class' 형태
|
||||
final klass = pick(config.klasses, rng).split('|').first;
|
||||
monster = 'passing $race $klass';
|
||||
} else {
|
||||
// 'Title Name the Race' 형태 (원본은 PickLow(Titles) 사용)
|
||||
final title = pickLow(config.titles, rng);
|
||||
monster = '$title ${generateName(rng)} the $race';
|
||||
definite = true;
|
||||
}
|
||||
monsterLevel = targetLevel;
|
||||
monster = '$monster|$monsterLevel|*';
|
||||
} else if (questMonster != null && rng.nextInt(4) == 0) {
|
||||
// Use quest monster.
|
||||
monster = questMonster;
|
||||
monsterLevel = questLevel ?? targetLevel;
|
||||
} else {
|
||||
// Pick closest level among random samples.
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Adjust quantity and adjectives based on level delta.
|
||||
var qty = 1;
|
||||
final levelDiff = targetLevel - monsterLevel;
|
||||
var name = monster.split('|').first;
|
||||
|
||||
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 = 'imaginary $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 = 'messianic $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 = indefinite(name, qty);
|
||||
}
|
||||
|
||||
return name;
|
||||
}
|
||||
|
||||
enum RewardKind { spell, equip, stat, item }
|
||||
|
||||
class QuestResult {
|
||||
const QuestResult({
|
||||
required this.caption,
|
||||
required this.reward,
|
||||
this.monsterName,
|
||||
this.monsterLevel,
|
||||
});
|
||||
|
||||
final String caption;
|
||||
final RewardKind reward;
|
||||
final String? monsterName;
|
||||
final int? monsterLevel;
|
||||
}
|
||||
|
||||
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:
|
||||
var best = '';
|
||||
var bestLevel = 0;
|
||||
for (var i = 0; i < 4; 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 name = best.split('|').first;
|
||||
return QuestResult(
|
||||
caption: 'Exterminate ${definite(name, 2)}',
|
||||
reward: reward,
|
||||
monsterName: best,
|
||||
monsterLevel: bestLevel,
|
||||
);
|
||||
case 1:
|
||||
final item = interestingItem(config, rng);
|
||||
return QuestResult(caption: 'Seek ${definite(item, 1)}', reward: reward);
|
||||
case 2:
|
||||
final item = boringItem(config, rng);
|
||||
return QuestResult(caption: 'Deliver this $item', reward: reward);
|
||||
case 3:
|
||||
final item = boringItem(config, rng);
|
||||
return QuestResult(
|
||||
caption: 'Fetch me ${indefinite(item, 1)}',
|
||||
reward: reward,
|
||||
);
|
||||
default:
|
||||
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 name = best.split('|').first;
|
||||
return QuestResult(
|
||||
caption: 'Placate ${definite(name, 2)}',
|
||||
reward: reward,
|
||||
monsterName: best,
|
||||
monsterLevel: bestLevel,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ActResult {
|
||||
const ActResult({
|
||||
required this.actTitle,
|
||||
required this.plotBarMaxSeconds,
|
||||
required this.rewards,
|
||||
});
|
||||
|
||||
final String actTitle;
|
||||
final int plotBarMaxSeconds;
|
||||
final List<RewardKind> rewards;
|
||||
}
|
||||
|
||||
ActResult completeAct(int existingActCount) {
|
||||
final nextActIndex = existingActCount;
|
||||
final title = 'Act ${intToRoman(nextActIndex)}';
|
||||
final plotBarMax = 60 * 60 * (1 + 5 * existingActCount);
|
||||
|
||||
final rewards = <RewardKind>[];
|
||||
if (existingActCount > 1) {
|
||||
rewards.add(RewardKind.item);
|
||||
}
|
||||
if (existingActCount > 2) {
|
||||
rewards.add(RewardKind.equip);
|
||||
}
|
||||
|
||||
return ActResult(
|
||||
actTitle: title,
|
||||
plotBarMaxSeconds: plotBarMax,
|
||||
rewards: rewards,
|
||||
);
|
||||
}
|
||||
|
||||
class TaskResult {
|
||||
const TaskResult({
|
||||
required this.caption,
|
||||
required this.durationMillis,
|
||||
required this.progress,
|
||||
});
|
||||
|
||||
final String caption;
|
||||
final int durationMillis;
|
||||
final ProgressState progress;
|
||||
}
|
||||
|
||||
/// Starts a task: resets task bar and sets caption.
|
||||
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;
|
||||
}
|
||||
|
||||
/// Process the queue when current task is done. Returns null if nothing to do.
|
||||
DequeueResult? dequeue(ProgressState progress, QueueState queue) {
|
||||
// Only act when the task bar is finished.
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
||||
int _monsterLevel(String entry) {
|
||||
final parts = entry.split('|');
|
||||
if (parts.length < 2) return 0;
|
||||
return int.tryParse(parts[1]) ?? 0;
|
||||
}
|
||||
|
||||
String _sick(int m, String s) {
|
||||
switch (m) {
|
||||
case -5:
|
||||
case 5:
|
||||
return 'dead $s';
|
||||
case -4:
|
||||
case 4:
|
||||
return 'comatose $s';
|
||||
case -3:
|
||||
case 3:
|
||||
return 'crippled $s';
|
||||
case -2:
|
||||
case 2:
|
||||
return 'sick $s';
|
||||
case -1:
|
||||
case 1:
|
||||
return 'undernourished $s';
|
||||
default:
|
||||
return '$m$s';
|
||||
}
|
||||
}
|
||||
|
||||
String _young(int m, String s) {
|
||||
switch (-m) {
|
||||
case -5:
|
||||
case 5:
|
||||
return 'foetal $s';
|
||||
case -4:
|
||||
case 4:
|
||||
return 'baby $s';
|
||||
case -3:
|
||||
case 3:
|
||||
return 'preadolescent $s';
|
||||
case -2:
|
||||
case 2:
|
||||
return 'teenage $s';
|
||||
case -1:
|
||||
case 1:
|
||||
return 'underage $s';
|
||||
default:
|
||||
return '$m$s';
|
||||
}
|
||||
}
|
||||
|
||||
String _big(int m, String s) {
|
||||
switch (m) {
|
||||
case 1:
|
||||
case -1:
|
||||
return 'greater $s';
|
||||
case 2:
|
||||
case -2:
|
||||
return 'massive $s';
|
||||
case 3:
|
||||
case -3:
|
||||
return 'enormous $s';
|
||||
case 4:
|
||||
case -4:
|
||||
return 'giant $s';
|
||||
case 5:
|
||||
case -5:
|
||||
return 'titanic $s';
|
||||
default:
|
||||
return s;
|
||||
}
|
||||
}
|
||||
|
||||
String _special(int m, String s) {
|
||||
switch (-m) {
|
||||
case 1:
|
||||
case -1:
|
||||
return s.contains(' ') ? 'veteran $s' : 'Battle-$s';
|
||||
case 2:
|
||||
case -2:
|
||||
return 'cursed $s';
|
||||
case 3:
|
||||
case -3:
|
||||
return s.contains(' ') ? 'warrior $s' : 'Were-$s';
|
||||
case 4:
|
||||
case -4:
|
||||
return 'undead $s';
|
||||
case 5:
|
||||
case -5:
|
||||
return 'demon $s';
|
||||
default:
|
||||
return s;
|
||||
}
|
||||
}
|
||||
|
||||
bool _ends(String s, String suffix) {
|
||||
return s.length >= suffix.length &&
|
||||
s.substring(s.length - suffix.length) == suffix;
|
||||
}
|
||||
|
||||
String _pick(String pipeSeparated, DeterministicRandom rng) {
|
||||
final parts = pipeSeparated.split('|');
|
||||
if (parts.isEmpty) return '';
|
||||
final idx = rng.nextInt(parts.length);
|
||||
return parts[idx];
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// InterplotCinematic 관련 함수들 (Main.pas:456-521)
|
||||
// =============================================================================
|
||||
|
||||
/// NPC 이름 생성 (ImpressiveGuy, Main.pas:514-521)
|
||||
/// 인상적인 타이틀 + 종족 또는 이름 조합
|
||||
String impressiveGuy(PqConfig config, DeterministicRandom rng) {
|
||||
var result = pick(config.impressiveTitles, rng);
|
||||
switch (rng.nextInt(2)) {
|
||||
case 0:
|
||||
// "the King of the Elves" 형태
|
||||
final race = pick(config.races, rng).split('|').first;
|
||||
result = 'the $result of the ${pluralize(race)}';
|
||||
break;
|
||||
case 1:
|
||||
// "King Vrognak of Zoxzik" 형태
|
||||
result = '$result ${generateName(rng)} of ${generateName(rng)}';
|
||||
break;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// 이름 있는 몬스터 생성 (NamedMonster, Main.pas:498-512)
|
||||
/// 레벨에 맞는 몬스터 선택 후 GenerateName으로 이름 붙이기
|
||||
String namedMonster(PqConfig config, DeterministicRandom rng, int level) {
|
||||
String best = '';
|
||||
int bestLevel = 0;
|
||||
|
||||
// 5번 시도해서 레벨에 가장 가까운 몬스터 선택
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
return '${generateName(rng)} the $best';
|
||||
}
|
||||
|
||||
/// 플롯 간 시네마틱 이벤트 생성 (InterplotCinematic, Main.pas:456-495)
|
||||
/// 3가지 시나리오 중 하나를 랜덤 선택
|
||||
List<QueueEntry> interplotCinematic(
|
||||
PqConfig config,
|
||||
DeterministicRandom rng,
|
||||
int level,
|
||||
int plotCount,
|
||||
) {
|
||||
final entries = <QueueEntry>[];
|
||||
|
||||
// 헬퍼: 큐 엔트리 추가 (원본의 Q 함수 역할)
|
||||
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,
|
||||
'Exhausted, you arrive at a friendly oasis in a hostile land',
|
||||
);
|
||||
q(QueueKind.task, 2, 'You greet old friends and meet new allies');
|
||||
q(QueueKind.task, 2, 'You are privy to a council of powerful do-gooders');
|
||||
q(QueueKind.task, 1, 'There is much to be done. You are chosen!');
|
||||
break;
|
||||
|
||||
case 1:
|
||||
// 시나리오 2: 강력한 적과의 전투
|
||||
q(
|
||||
QueueKind.task,
|
||||
1,
|
||||
'Your quarry is in sight, but a mighty enemy bars your path!',
|
||||
);
|
||||
final nemesis = namedMonster(config, rng, level + 3);
|
||||
q(QueueKind.task, 4, 'A desperate struggle commences with $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, 'Locked in grim combat with $nemesis');
|
||||
break;
|
||||
case 1:
|
||||
q(QueueKind.task, 2, '$nemesis seems to have the upper hand');
|
||||
break;
|
||||
case 2:
|
||||
q(
|
||||
QueueKind.task,
|
||||
2,
|
||||
'You seem to gain the advantage over $nemesis',
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
q(
|
||||
QueueKind.task,
|
||||
3,
|
||||
'Victory! $nemesis is slain! Exhausted, you lose conciousness',
|
||||
);
|
||||
q(
|
||||
QueueKind.task,
|
||||
2,
|
||||
'You awake in a friendly place, but the road awaits',
|
||||
);
|
||||
break;
|
||||
|
||||
case 2:
|
||||
// 시나리오 3: 배신 발견
|
||||
final guy = impressiveGuy(config, rng);
|
||||
q(
|
||||
QueueKind.task,
|
||||
2,
|
||||
"Oh sweet relief! You've reached the kind protection of $guy",
|
||||
);
|
||||
q(
|
||||
QueueKind.task,
|
||||
3,
|
||||
'There is rejoicing, and an unnerving encouter with $guy in private',
|
||||
);
|
||||
q(
|
||||
QueueKind.task,
|
||||
2,
|
||||
'You forget your ${boringItem(config, rng)} and go back to get it',
|
||||
);
|
||||
q(QueueKind.task, 2, "What's this!? You overhear something shocking!");
|
||||
q(QueueKind.task, 2, 'Could $guy be a dirty double-dealer?');
|
||||
q(
|
||||
QueueKind.task,
|
||||
3,
|
||||
'Who can possibly be trusted with this news!? -- Oh yes, of course',
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
// 마지막에 plot|2|Loading 추가
|
||||
q(QueueKind.plot, 2, 'Loading');
|
||||
|
||||
return entries;
|
||||
}
|
||||
Reference in New Issue
Block a user