feat(core): 장비 시스템 및 게임 상태 모델 확장
- Equipment 클래스를 11개 슬롯으로 확장 (원본 Main.dfm 충실) - TaskInfo에 몬스터 정보(baseName, part) 추가 - Stats에 현재 HP/MP 필드 추가 - 히스토리 기능 구현 (plotHistory, questHistory) - pq_logic winEquip/winStatIndex 원본 로직 개선 - 퀘스트 몬스터 처리 로직 구현 - SaveData 직렬화 확장
This commit is contained in:
@@ -140,7 +140,6 @@ int _parseLevel(String entry) {
|
||||
return int.tryParse(parts[1].replaceAll('+', '')) ?? 0;
|
||||
}
|
||||
|
||||
|
||||
String addModifier(
|
||||
DeterministicRandom rng,
|
||||
String baseName,
|
||||
@@ -211,19 +210,27 @@ int random64Below(DeterministicRandom rng, int below) {
|
||||
return (combined % below).toInt();
|
||||
}
|
||||
|
||||
/// 장비 생성 (원본 Main.pas:791-830 WinEquip)
|
||||
/// [slotIndex]: 0=Weapon, 1=Shield, 2-10=Armor 계열
|
||||
String winEquip(
|
||||
PqConfig config,
|
||||
DeterministicRandom rng,
|
||||
int level,
|
||||
EquipmentSlot slot,
|
||||
int slotIndex,
|
||||
) {
|
||||
// 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,
|
||||
};
|
||||
// 원본 로직:
|
||||
// posn = 0: Weapon → K.Weapons, OffenseAttrib
|
||||
// posn = 1: Shield → K.Shields, DefenseAttrib
|
||||
// posn >= 2: Armor → K.Armors, DefenseAttrib
|
||||
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;
|
||||
|
||||
@@ -239,21 +246,38 @@ String winEquip(
|
||||
return addModifier(rng, baseName, modifiers, plus);
|
||||
}
|
||||
|
||||
/// EquipmentSlot enum을 사용하는 편의 함수
|
||||
String winEquipBySlot(
|
||||
PqConfig config,
|
||||
DeterministicRandom rng,
|
||||
int level,
|
||||
EquipmentSlot slot,
|
||||
) {
|
||||
return winEquip(config, rng, level, slot.index);
|
||||
}
|
||||
|
||||
int winStatIndex(DeterministicRandom rng, List<int> statValues) {
|
||||
// 원본 Main.pas:870-883: 50% 확률로 완전 랜덤, 50% 확률로 제곱 가중치
|
||||
// 원본 Main.pas:870-883
|
||||
// 50%: 모든 8개 스탯 중 랜덤
|
||||
// 50%: 첫 6개(STR~CHA)만 제곱 가중치로 선택
|
||||
if (rng.nextInt(2) == 0) {
|
||||
// Odds(1,2): 완전 랜덤 선택
|
||||
// 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);
|
||||
// 원본: for i := 0 to 5 do Inc(t, Square(GetI(Stats,i)));
|
||||
// 첫 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 < statValues.length; i++) {
|
||||
for (var i = 0; i < firstSixCount; i++) {
|
||||
pickValue -= statValues[i] * statValues[i];
|
||||
if (pickValue < 0) return i;
|
||||
}
|
||||
return statValues.length - 1;
|
||||
return firstSixCount - 1;
|
||||
}
|
||||
|
||||
Stats winStat(Stats stats, DeterministicRandom rng) {
|
||||
@@ -290,7 +314,7 @@ Stats winStat(Stats stats, DeterministicRandom rng) {
|
||||
}
|
||||
}
|
||||
|
||||
String monsterTask(
|
||||
MonsterTaskResult monsterTask(
|
||||
PqConfig config,
|
||||
DeterministicRandom rng,
|
||||
int level,
|
||||
@@ -308,6 +332,7 @@ String monsterTask(
|
||||
|
||||
String monster;
|
||||
int monsterLevel;
|
||||
String part;
|
||||
bool definite = false;
|
||||
|
||||
// 원본 Main.pas:537-547: 가끔 NPC를 몬스터로 사용
|
||||
@@ -324,11 +349,13 @@ String monsterTask(
|
||||
definite = true;
|
||||
}
|
||||
monsterLevel = targetLevel;
|
||||
monster = '$monster|$monsterLevel|*';
|
||||
part = '*'; // NPC는 WinItem 호출
|
||||
} else if (questMonster != null && rng.nextInt(4) == 0) {
|
||||
// Use quest monster.
|
||||
monster = questMonster;
|
||||
final parts = questMonster.split('|');
|
||||
monsterLevel = questLevel ?? targetLevel;
|
||||
part = parts.length > 2 ? parts[2] : '';
|
||||
} else {
|
||||
// Pick closest level among random samples.
|
||||
monster = pick(config.monsters, rng);
|
||||
@@ -342,12 +369,18 @@ String monsterTask(
|
||||
monsterLevel = candLevel;
|
||||
}
|
||||
}
|
||||
// 몬스터 데이터에서 부위 정보 추출 (예: "Rat|0|tail")
|
||||
final monsterParts = monster.split('|');
|
||||
part = monsterParts.length > 2 ? monsterParts[2] : '';
|
||||
}
|
||||
|
||||
// 기본 몬스터 이름 (부위 정보 제외)
|
||||
final baseName = monster.split('|').first;
|
||||
|
||||
// Adjust quantity and adjectives based on level delta.
|
||||
var qty = 1;
|
||||
final levelDiff = targetLevel - monsterLevel;
|
||||
var name = monster.split('|').first;
|
||||
var name = baseName;
|
||||
|
||||
if (levelDiff > 10) {
|
||||
qty =
|
||||
@@ -385,7 +418,34 @@ String monsterTask(
|
||||
name = indefinite(name, qty);
|
||||
}
|
||||
|
||||
return name;
|
||||
return MonsterTaskResult(
|
||||
displayName: name,
|
||||
baseName: baseName,
|
||||
level: monsterLevel * qty,
|
||||
part: part,
|
||||
);
|
||||
}
|
||||
|
||||
/// monsterTask의 반환 타입 (원본 fTask.Caption 정보 포함)
|
||||
class MonsterTaskResult {
|
||||
const MonsterTaskResult({
|
||||
required this.displayName,
|
||||
required this.baseName,
|
||||
required this.level,
|
||||
required this.part,
|
||||
});
|
||||
|
||||
/// 화면에 표시할 몬스터 이름 (형용사 포함, 예: "a sick Goblin")
|
||||
final String displayName;
|
||||
|
||||
/// 기본 몬스터 이름 (형용사 제외, 예: "Goblin")
|
||||
final String baseName;
|
||||
|
||||
/// 몬스터 레벨
|
||||
final int level;
|
||||
|
||||
/// 전리품 부위 (예: "claw", "tail", "*"는 WinItem 호출)
|
||||
final String part;
|
||||
}
|
||||
|
||||
enum RewardKind { spell, equip, stat, item }
|
||||
@@ -396,12 +456,17 @@ class QuestResult {
|
||||
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;
|
||||
}
|
||||
|
||||
QuestResult completeQuest(PqConfig config, DeterministicRandom rng, int level) {
|
||||
@@ -416,14 +481,19 @@ QuestResult completeQuest(PqConfig config, DeterministicRandom rng, int level) {
|
||||
final questRoll = rng.nextInt(5);
|
||||
switch (questRoll) {
|
||||
case 0:
|
||||
// Exterminate: 4번 시도하여 레벨에 가장 가까운 몬스터 선택
|
||||
// 원본 Main.pas:936-954
|
||||
var best = '';
|
||||
var bestLevel = 0;
|
||||
var bestIndex = 0;
|
||||
for (var i = 0; i < 4; i++) {
|
||||
final m = pick(config.monsters, rng);
|
||||
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 name = best.split('|').first;
|
||||
@@ -432,6 +502,7 @@ QuestResult completeQuest(PqConfig config, DeterministicRandom rng, int level) {
|
||||
reward: reward,
|
||||
monsterName: best,
|
||||
monsterLevel: bestLevel,
|
||||
monsterIndex: bestIndex,
|
||||
);
|
||||
case 1:
|
||||
final item = interestingItem(config, rng);
|
||||
@@ -446,6 +517,8 @@ QuestResult completeQuest(PqConfig config, DeterministicRandom rng, int level) {
|
||||
reward: reward,
|
||||
);
|
||||
default:
|
||||
// Placate: 2번 시도하여 레벨에 가장 가까운 몬스터 선택
|
||||
// 원본 Main.pas:971-984 (fQuest.Caption := '' 처리됨)
|
||||
var best = '';
|
||||
var bestLevel = 0;
|
||||
for (var i = 0; i < 2; i++) {
|
||||
@@ -457,11 +530,10 @@ QuestResult completeQuest(PqConfig config, DeterministicRandom rng, int level) {
|
||||
}
|
||||
}
|
||||
final name = best.split('|').first;
|
||||
// Placate는 fQuest.Caption := '' 로 비움 → monsterIndex 미저장
|
||||
return QuestResult(
|
||||
caption: 'Placate ${definite(name, 2)}',
|
||||
reward: reward,
|
||||
monsterName: best,
|
||||
monsterLevel: bestLevel,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user