Compare commits
2 Commits
08054d97c1
...
b450bf2600
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b450bf2600 | ||
|
|
b512fde1fb |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -44,3 +44,4 @@ app.*.map.json
|
||||
/android/app/debug
|
||||
/android/app/profile
|
||||
/android/app/release
|
||||
example/pq
|
||||
|
||||
@@ -9,27 +9,22 @@ class GameMutations {
|
||||
|
||||
final PqConfig config;
|
||||
|
||||
GameState winEquip(GameState state, int level, EquipmentSlot slot) {
|
||||
/// 장비 획득 (원본 Main.pas:791-830 WinEquip)
|
||||
/// [slotIndex]: 0-10 (원본 Equips.Items.Count = 11)
|
||||
GameState winEquipByIndex(GameState state, int level, int slotIndex) {
|
||||
final rng = state.rng;
|
||||
final name = pq_logic.winEquip(config, rng, level, slot);
|
||||
final equip = state.equipment;
|
||||
final updatedEquip = switch (slot) {
|
||||
EquipmentSlot.weapon => equip.copyWith(
|
||||
weapon: name,
|
||||
bestIndex: EquipmentSlot.weapon.index,
|
||||
),
|
||||
EquipmentSlot.shield => equip.copyWith(
|
||||
shield: name,
|
||||
bestIndex: EquipmentSlot.shield.index,
|
||||
),
|
||||
EquipmentSlot.armor => equip.copyWith(
|
||||
armor: name,
|
||||
bestIndex: EquipmentSlot.armor.index,
|
||||
),
|
||||
};
|
||||
final name = pq_logic.winEquip(config, rng, level, slotIndex);
|
||||
final updatedEquip = state.equipment
|
||||
.setByIndex(slotIndex, name)
|
||||
.copyWith(bestIndex: slotIndex);
|
||||
return state.copyWith(rng: rng, equipment: updatedEquip);
|
||||
}
|
||||
|
||||
/// EquipmentSlot enum을 사용하는 편의 함수
|
||||
GameState winEquip(GameState state, int level, EquipmentSlot slot) {
|
||||
return winEquipByIndex(state, level, slot.index);
|
||||
}
|
||||
|
||||
GameState winStat(GameState state) {
|
||||
final updatedStats = pq_logic.winStat(state.stats, state.rng);
|
||||
return state.copyWith(rng: state.rng, stats: updatedStats);
|
||||
|
||||
@@ -2,7 +2,6 @@ import 'dart:math' as math;
|
||||
|
||||
import 'package:askiineverdie/src/core/engine/game_mutations.dart';
|
||||
import 'package:askiineverdie/src/core/engine/reward_service.dart';
|
||||
import 'package:askiineverdie/src/core/model/equipment_slot.dart';
|
||||
import 'package:askiineverdie/src/core/model/game_state.dart';
|
||||
import 'package:askiineverdie/src/core/model/pq_config.dart';
|
||||
import 'package:askiineverdie/src/core/util/pq_logic.dart' as pq_logic;
|
||||
@@ -49,21 +48,24 @@ class ProgressService {
|
||||
const QueueEntry(
|
||||
kind: QueueKind.task,
|
||||
durationMillis: 6 * 1000,
|
||||
caption: "Much is revealed about that wise old bastard you'd "
|
||||
caption:
|
||||
"Much is revealed about that wise old bastard you'd "
|
||||
'underestimated',
|
||||
taskType: TaskType.load,
|
||||
),
|
||||
const QueueEntry(
|
||||
kind: QueueKind.task,
|
||||
durationMillis: 6 * 1000,
|
||||
caption: 'A shocking series of events leaves you alone and bewildered, '
|
||||
caption:
|
||||
'A shocking series of events leaves you alone and bewildered, '
|
||||
'but resolute',
|
||||
taskType: TaskType.load,
|
||||
),
|
||||
const QueueEntry(
|
||||
kind: QueueKind.task,
|
||||
durationMillis: 4 * 1000,
|
||||
caption: 'Drawing upon an unexpected reserve of determination, '
|
||||
caption:
|
||||
'Drawing upon an unexpected reserve of determination, '
|
||||
'you set out on a long and dangerous journey',
|
||||
taskType: TaskType.load,
|
||||
),
|
||||
@@ -76,17 +78,10 @@ class ProgressService {
|
||||
];
|
||||
|
||||
// 첫 번째 태스크 'Loading' 시작 (원본 752줄)
|
||||
final taskResult = pq_logic.startTask(
|
||||
state.progress,
|
||||
'Loading',
|
||||
2 * 1000,
|
||||
);
|
||||
final taskResult = pq_logic.startTask(state.progress, 'Loading', 2 * 1000);
|
||||
|
||||
// ExpBar 초기화 (원본 743-746줄)
|
||||
final expBar = ProgressBarState(
|
||||
position: 0,
|
||||
max: pq_logic.levelUpTime(1),
|
||||
);
|
||||
final expBar = ProgressBarState(position: 0, max: pq_logic.levelUpTime(1));
|
||||
|
||||
// PlotBar 초기화 (원본 759줄)
|
||||
final plotBar = const ProgressBarState(position: 0, max: 26 * 1000);
|
||||
@@ -97,6 +92,8 @@ class ProgressService {
|
||||
currentTask: const TaskInfo(caption: 'Loading...', type: TaskType.load),
|
||||
plotStageCount: 1, // Prologue
|
||||
questCount: 0,
|
||||
plotHistory: const [HistoryEntry(caption: 'Prologue', isComplete: false)],
|
||||
questHistory: const [],
|
||||
);
|
||||
|
||||
return _recalculateEncumbrance(
|
||||
@@ -348,12 +345,21 @@ class ProgressService {
|
||||
|
||||
// 3. MonsterTask 실행 (원본 678-684줄)
|
||||
final level = state.traits.level;
|
||||
final monster = pq_logic.monsterTask(
|
||||
|
||||
// 원본 Main.pas:548-551: 25% 확률로 Quest Monster 사용
|
||||
// fQuest.Caption이 비어있지 않으면 해당 몬스터 데이터 전달
|
||||
final questMonster = state.progress.currentQuestMonster;
|
||||
final questMonsterData = questMonster?.monsterData;
|
||||
final questLevel = questMonsterData != null
|
||||
? int.tryParse(questMonsterData.split('|').elementAtOrNull(1) ?? '') ?? 0
|
||||
: null;
|
||||
|
||||
final monsterResult = pq_logic.monsterTask(
|
||||
config,
|
||||
state.rng,
|
||||
level,
|
||||
null, // questMonster
|
||||
null, // questLevel
|
||||
questMonsterData,
|
||||
questLevel,
|
||||
);
|
||||
|
||||
// 태스크 지속시간 계산 (원본 682줄)
|
||||
@@ -364,7 +370,7 @@ class ProgressService {
|
||||
|
||||
final taskResult = pq_logic.startTask(
|
||||
progress,
|
||||
'Executing $monster',
|
||||
'Executing ${monsterResult.displayName}',
|
||||
durationMillis,
|
||||
);
|
||||
|
||||
@@ -372,6 +378,8 @@ class ProgressService {
|
||||
currentTask: TaskInfo(
|
||||
caption: taskResult.caption,
|
||||
type: TaskType.kill,
|
||||
monsterBaseName: monsterResult.baseName,
|
||||
monsterPart: monsterResult.part,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -389,6 +397,23 @@ class ProgressService {
|
||||
var nextState = _applyReward(state, result.reward);
|
||||
final questCount = nextState.progress.questCount + 1;
|
||||
|
||||
// 퀘스트 히스토리 업데이트: 이전 퀘스트 완료 표시, 새 퀘스트 추가
|
||||
final updatedQuestHistory = [
|
||||
...nextState.progress.questHistory.map(
|
||||
(e) => e.isComplete ? e : e.copyWith(isComplete: true),
|
||||
),
|
||||
HistoryEntry(caption: result.caption, isComplete: false),
|
||||
];
|
||||
|
||||
// 퀘스트 몬스터 정보 저장 (Exterminate 타입용)
|
||||
// 원본 fQuest.Caption = monsterData, fQuest.Tag = monsterIndex
|
||||
final questMonster = result.monsterIndex != null
|
||||
? QuestMonsterInfo(
|
||||
monsterData: result.monsterName!,
|
||||
monsterIndex: result.monsterIndex!,
|
||||
)
|
||||
: null;
|
||||
|
||||
// Append quest entry to queue (task kind).
|
||||
final updatedQueue = QueueState(
|
||||
entries: [
|
||||
@@ -409,6 +434,8 @@ class ProgressService {
|
||||
max: 50 + nextState.rng.nextInt(100),
|
||||
),
|
||||
questCount: questCount,
|
||||
questHistory: updatedQuestHistory,
|
||||
currentQuestMonster: questMonster,
|
||||
);
|
||||
|
||||
return _recalculateEncumbrance(
|
||||
@@ -425,9 +452,19 @@ class ProgressService {
|
||||
}
|
||||
|
||||
final plotStages = nextState.progress.plotStageCount + 1;
|
||||
|
||||
// 플롯 히스토리 업데이트: 이전 플롯 완료 표시, 새 플롯 추가
|
||||
final updatedPlotHistory = [
|
||||
...nextState.progress.plotHistory.map(
|
||||
(e) => e.isComplete ? e : e.copyWith(isComplete: true),
|
||||
),
|
||||
HistoryEntry(caption: actResult.actTitle, isComplete: false),
|
||||
];
|
||||
|
||||
var updatedProgress = nextState.progress.copyWith(
|
||||
plot: ProgressBarState(position: 0, max: actResult.plotBarMaxSeconds),
|
||||
plotStageCount: plotStages,
|
||||
plotHistory: updatedPlotHistory,
|
||||
);
|
||||
|
||||
nextState = nextState.copyWith(progress: updatedProgress);
|
||||
@@ -455,6 +492,20 @@ class ProgressService {
|
||||
max: 50 + state.rng.nextInt(100),
|
||||
);
|
||||
|
||||
// 첫 퀘스트 히스토리 추가
|
||||
final questHistory = [
|
||||
HistoryEntry(caption: result.caption, isComplete: false),
|
||||
];
|
||||
|
||||
// 퀘스트 몬스터 정보 저장 (Exterminate 타입용)
|
||||
// 원본 fQuest.Caption = monsterData, fQuest.Tag = monsterIndex
|
||||
final questMonster = result.monsterIndex != null
|
||||
? QuestMonsterInfo(
|
||||
monsterData: result.monsterName!,
|
||||
monsterIndex: result.monsterIndex!,
|
||||
)
|
||||
: null;
|
||||
|
||||
// 첫 퀘스트 추가
|
||||
final updatedQueue = QueueState(
|
||||
entries: [
|
||||
@@ -471,6 +522,8 @@ class ProgressService {
|
||||
final progress = state.progress.copyWith(
|
||||
quest: questBar,
|
||||
questCount: 1,
|
||||
questHistory: questHistory,
|
||||
currentQuestMonster: questMonster,
|
||||
);
|
||||
|
||||
return state.copyWith(progress: progress, queue: updatedQueue);
|
||||
@@ -550,28 +603,25 @@ class ProgressService {
|
||||
|
||||
/// 킬 태스크 완료 시 전리품 획득 (원본 Main.pas:625-630)
|
||||
GameState _winLoot(GameState state) {
|
||||
final taskCaption = state.progress.currentTask.caption;
|
||||
final taskInfo = state.progress.currentTask;
|
||||
final monsterPart = taskInfo.monsterPart ?? '';
|
||||
final monsterBaseName = taskInfo.monsterBaseName ?? '';
|
||||
|
||||
// 부위가 '*'이면 WinItem 호출 (특수 아이템)
|
||||
if (monsterPart == '*') {
|
||||
return mutations.winItem(state);
|
||||
}
|
||||
|
||||
// 부위가 비어있으면 전리품 없음
|
||||
if (monsterPart.isEmpty || monsterBaseName.isEmpty) {
|
||||
return state;
|
||||
}
|
||||
|
||||
// 몬스터 이름에서 전리품 아이템 생성
|
||||
// 원본: Add(Inventory, LowerCase(Split(fTask.Caption,1) + ' ' +
|
||||
// ProperCase(Split(fTask.Caption,3))), 1);
|
||||
// 예: "Executing a Goblin..." -> "goblin ear" 등의 아이템
|
||||
|
||||
// 태스크 캡션에서 몬스터 이름 추출 ("Executing ..." 형태)
|
||||
String monsterName = taskCaption;
|
||||
if (monsterName.startsWith('Executing ')) {
|
||||
monsterName = monsterName.substring('Executing '.length);
|
||||
}
|
||||
if (monsterName.endsWith('...')) {
|
||||
monsterName = monsterName.substring(0, monsterName.length - 3);
|
||||
}
|
||||
|
||||
// 몬스터 부위 선택 (원본에서는 몬스터별로 다르지만, 간단히 랜덤 선택)
|
||||
final parts = ['Skin', 'Tooth', 'Claw', 'Ear', 'Eye', 'Tail', 'Scale'];
|
||||
final part = pq_logic.pick(parts, state.rng);
|
||||
|
||||
// 아이템 이름 생성 (예: "Goblin Ear")
|
||||
final itemName = '${_extractBaseName(monsterName)} $part';
|
||||
// 예: "goblin Claw" 형태로 인벤토리 추가
|
||||
final itemName =
|
||||
'${monsterBaseName.toLowerCase()} ${_properCase(monsterPart)}';
|
||||
|
||||
// 인벤토리에 추가
|
||||
final items = [...state.inventory.items];
|
||||
@@ -584,32 +634,13 @@ class ProgressService {
|
||||
items.add(InventoryEntry(name: itemName, count: 1));
|
||||
}
|
||||
|
||||
return state.copyWith(
|
||||
inventory: state.inventory.copyWith(items: items),
|
||||
);
|
||||
return state.copyWith(inventory: state.inventory.copyWith(items: items));
|
||||
}
|
||||
|
||||
/// 몬스터 이름에서 기본 이름 추출 (형용사 제거)
|
||||
String _extractBaseName(String name) {
|
||||
// "a Goblin", "an Orc", "2 Goblins" 등에서 기본 이름 추출
|
||||
final words = name.split(' ');
|
||||
if (words.isEmpty) return name;
|
||||
|
||||
// 관사나 숫자 제거
|
||||
var startIndex = 0;
|
||||
if (words[0] == 'a' || words[0] == 'an' || words[0] == 'the') {
|
||||
startIndex = 1;
|
||||
} else if (int.tryParse(words[0]) != null) {
|
||||
startIndex = 1;
|
||||
}
|
||||
|
||||
if (startIndex >= words.length) return name;
|
||||
|
||||
// 마지막 단어가 몬스터 이름 (형용사들 건너뛰기)
|
||||
final baseName = words.last;
|
||||
// 첫 글자 대문자로
|
||||
if (baseName.isEmpty) return name;
|
||||
return baseName[0].toUpperCase() + baseName.substring(1).toLowerCase();
|
||||
/// 첫 글자만 대문자로 변환 (원본 ProperCase)
|
||||
String _properCase(String s) {
|
||||
if (s.isEmpty) return s;
|
||||
return s[0].toUpperCase() + s.substring(1);
|
||||
}
|
||||
|
||||
/// 인벤토리에서 Gold 수량 반환
|
||||
@@ -635,11 +666,9 @@ class ProgressService {
|
||||
);
|
||||
|
||||
// 장비 획득 (WinEquip)
|
||||
nextState = mutations.winEquip(
|
||||
nextState,
|
||||
level,
|
||||
EquipmentSlot.values[nextState.rng.nextInt(EquipmentSlot.values.length)],
|
||||
);
|
||||
// 원본 Main.pas:797 - posn := Random(Equips.Items.Count); (11개 슬롯)
|
||||
final slotIndex = nextState.rng.nextInt(Equipment.slotCount);
|
||||
nextState = mutations.winEquipByIndex(nextState, level, slotIndex);
|
||||
|
||||
return nextState;
|
||||
}
|
||||
@@ -662,7 +691,8 @@ class ProgressService {
|
||||
|
||||
// " of " 포함 시 보너스 (원본 639-640)
|
||||
if (item.name.contains(' of ')) {
|
||||
price = price *
|
||||
price =
|
||||
price *
|
||||
(1 + pq_logic.randomLow(state.rng, 10)) *
|
||||
(1 + pq_logic.randomLow(state.rng, level));
|
||||
}
|
||||
@@ -687,10 +717,7 @@ class ProgressService {
|
||||
1 * 1000,
|
||||
);
|
||||
final progress = taskResult.progress.copyWith(
|
||||
currentTask: TaskInfo(
|
||||
caption: taskResult.caption,
|
||||
type: TaskType.sell,
|
||||
),
|
||||
currentTask: TaskInfo(caption: taskResult.caption, type: TaskType.sell),
|
||||
);
|
||||
return (
|
||||
state: state.copyWith(
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import 'package:askiineverdie/src/core/engine/game_mutations.dart';
|
||||
import 'package:askiineverdie/src/core/model/equipment_slot.dart';
|
||||
import 'package:askiineverdie/src/core/model/game_state.dart';
|
||||
import 'package:askiineverdie/src/core/util/pq_logic.dart';
|
||||
|
||||
@@ -14,9 +13,9 @@ class RewardService {
|
||||
case RewardKind.spell:
|
||||
return mutations.winSpell(state, state.stats.wis, state.traits.level);
|
||||
case RewardKind.equip:
|
||||
final slot = EquipmentSlot
|
||||
.values[state.rng.nextInt(EquipmentSlot.values.length)];
|
||||
return mutations.winEquip(state, state.traits.level, slot);
|
||||
// 원본 Main.pas:797 - Random(Equips.Items.Count) (11개 슬롯)
|
||||
final slotIndex = state.rng.nextInt(Equipment.slotCount);
|
||||
return mutations.winEquipByIndex(state, state.traits.level, slotIndex);
|
||||
case RewardKind.stat:
|
||||
return mutations.winStat(state);
|
||||
case RewardKind.item:
|
||||
|
||||
@@ -1 +1,29 @@
|
||||
enum EquipmentSlot { weapon, shield, armor }
|
||||
/// 장비 슬롯 (원본 Main.dfm Equips ListView 순서)
|
||||
/// 0=Weapon, 1=Shield, 2-10=Armor 계열
|
||||
enum EquipmentSlot {
|
||||
weapon, // 0: 무기 → K.Weapons
|
||||
shield, // 1: 방패 → K.Shields
|
||||
helm, // 2: 투구 → K.Armors
|
||||
hauberk, // 3: 사슬갑옷 → K.Armors
|
||||
brassairts, // 4: 상완갑 → K.Armors
|
||||
vambraces, // 5: 전완갑 → K.Armors
|
||||
gauntlets, // 6: 건틀릿 → K.Armors
|
||||
gambeson, // 7: 갬비슨(누비옷) → K.Armors
|
||||
cuisses, // 8: 허벅지갑 → K.Armors
|
||||
greaves, // 9: 정강이갑 → K.Armors
|
||||
sollerets, // 10: 철제신발 → K.Armors
|
||||
}
|
||||
|
||||
extension EquipmentSlotX on EquipmentSlot {
|
||||
/// 무기 슬롯 여부
|
||||
bool get isWeapon => this == EquipmentSlot.weapon;
|
||||
|
||||
/// 방패 슬롯 여부
|
||||
bool get isShield => this == EquipmentSlot.shield;
|
||||
|
||||
/// 방어구(armor) 슬롯 여부 (2-10)
|
||||
bool get isArmor => index >= 2;
|
||||
|
||||
/// 슬롯 인덱스 (0-10)
|
||||
int get slotIndex => index;
|
||||
}
|
||||
|
||||
@@ -90,16 +90,37 @@ enum TaskType {
|
||||
}
|
||||
|
||||
class TaskInfo {
|
||||
const TaskInfo({required this.caption, required this.type});
|
||||
const TaskInfo({
|
||||
required this.caption,
|
||||
required this.type,
|
||||
this.monsterBaseName,
|
||||
this.monsterPart,
|
||||
});
|
||||
|
||||
final String caption;
|
||||
final TaskType type;
|
||||
|
||||
/// 킬 태스크의 몬스터 기본 이름 (형용사 제외, 예: "Goblin")
|
||||
final String? monsterBaseName;
|
||||
|
||||
/// 킬 태스크의 전리품 부위 (예: "claw", "tail", "*"는 WinItem)
|
||||
final String? monsterPart;
|
||||
|
||||
factory TaskInfo.empty() =>
|
||||
const TaskInfo(caption: '', type: TaskType.neutral);
|
||||
|
||||
TaskInfo copyWith({String? caption, TaskType? type}) {
|
||||
return TaskInfo(caption: caption ?? this.caption, type: type ?? this.type);
|
||||
TaskInfo copyWith({
|
||||
String? caption,
|
||||
TaskType? type,
|
||||
String? monsterBaseName,
|
||||
String? monsterPart,
|
||||
}) {
|
||||
return TaskInfo(
|
||||
caption: caption ?? this.caption,
|
||||
type: type ?? this.type,
|
||||
monsterBaseName: monsterBaseName ?? this.monsterBaseName,
|
||||
monsterPart: monsterPart ?? this.monsterPart,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,6 +179,8 @@ class Stats {
|
||||
required this.cha,
|
||||
required this.hpMax,
|
||||
required this.mpMax,
|
||||
this.hpCurrent,
|
||||
this.mpCurrent,
|
||||
});
|
||||
|
||||
final int str;
|
||||
@@ -169,6 +192,18 @@ class Stats {
|
||||
final int hpMax;
|
||||
final int mpMax;
|
||||
|
||||
/// 현재 HP (null이면 hpMax와 동일)
|
||||
final int? hpCurrent;
|
||||
|
||||
/// 현재 MP (null이면 mpMax와 동일)
|
||||
final int? mpCurrent;
|
||||
|
||||
/// 실제 현재 HP 값
|
||||
int get hp => hpCurrent ?? hpMax;
|
||||
|
||||
/// 실제 현재 MP 값
|
||||
int get mp => mpCurrent ?? mpMax;
|
||||
|
||||
factory Stats.empty() => const Stats(
|
||||
str: 0,
|
||||
con: 0,
|
||||
@@ -189,6 +224,8 @@ class Stats {
|
||||
int? cha,
|
||||
int? hpMax,
|
||||
int? mpMax,
|
||||
int? hpCurrent,
|
||||
int? mpCurrent,
|
||||
}) {
|
||||
return Stats(
|
||||
str: str ?? this.str,
|
||||
@@ -199,6 +236,8 @@ class Stats {
|
||||
cha: cha ?? this.cha,
|
||||
hpMax: hpMax ?? this.hpMax,
|
||||
mpMax: mpMax ?? this.mpMax,
|
||||
hpCurrent: hpCurrent ?? this.hpCurrent,
|
||||
mpCurrent: mpCurrent ?? this.mpCurrent,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -227,38 +266,118 @@ class Inventory {
|
||||
}
|
||||
}
|
||||
|
||||
/// 장비 (원본 Main.dfm Equips ListView, 11개 슬롯)
|
||||
class Equipment {
|
||||
const Equipment({
|
||||
required this.weapon,
|
||||
required this.shield,
|
||||
required this.armor,
|
||||
required this.helm,
|
||||
required this.hauberk,
|
||||
required this.brassairts,
|
||||
required this.vambraces,
|
||||
required this.gauntlets,
|
||||
required this.gambeson,
|
||||
required this.cuisses,
|
||||
required this.greaves,
|
||||
required this.sollerets,
|
||||
required this.bestIndex,
|
||||
});
|
||||
|
||||
final String weapon;
|
||||
final String shield;
|
||||
final String armor;
|
||||
final String weapon; // 0: 무기
|
||||
final String shield; // 1: 방패
|
||||
final String helm; // 2: 투구
|
||||
final String hauberk; // 3: 사슬갑옷
|
||||
final String brassairts; // 4: 상완갑
|
||||
final String vambraces; // 5: 전완갑
|
||||
final String gauntlets; // 6: 건틀릿
|
||||
final String gambeson; // 7: 갬비슨
|
||||
final String cuisses; // 8: 허벅지갑
|
||||
final String greaves; // 9: 정강이갑
|
||||
final String sollerets; // 10: 철제신발
|
||||
|
||||
/// Tracks best slot index (mirror of Equips.Tag in original code; 0=weapon,1=shield,2=armor).
|
||||
/// 최고 아이템 슬롯 인덱스 (원본 Equips.Tag, 0-10)
|
||||
final int bestIndex;
|
||||
|
||||
/// 슬롯 개수
|
||||
static const slotCount = 11;
|
||||
|
||||
factory Equipment.empty() => const Equipment(
|
||||
weapon: 'Sharp Stick',
|
||||
shield: '',
|
||||
armor: '',
|
||||
helm: '',
|
||||
hauberk: '',
|
||||
brassairts: '',
|
||||
vambraces: '',
|
||||
gauntlets: '',
|
||||
gambeson: '',
|
||||
cuisses: '',
|
||||
greaves: '',
|
||||
sollerets: '',
|
||||
bestIndex: 0,
|
||||
);
|
||||
|
||||
/// 인덱스로 슬롯 값 가져오기
|
||||
String getByIndex(int index) {
|
||||
return switch (index) {
|
||||
0 => weapon,
|
||||
1 => shield,
|
||||
2 => helm,
|
||||
3 => hauberk,
|
||||
4 => brassairts,
|
||||
5 => vambraces,
|
||||
6 => gauntlets,
|
||||
7 => gambeson,
|
||||
8 => cuisses,
|
||||
9 => greaves,
|
||||
10 => sollerets,
|
||||
_ => '',
|
||||
};
|
||||
}
|
||||
|
||||
/// 인덱스로 슬롯 값 설정한 새 Equipment 반환
|
||||
Equipment setByIndex(int index, String value) {
|
||||
return switch (index) {
|
||||
0 => copyWith(weapon: value),
|
||||
1 => copyWith(shield: value),
|
||||
2 => copyWith(helm: value),
|
||||
3 => copyWith(hauberk: value),
|
||||
4 => copyWith(brassairts: value),
|
||||
5 => copyWith(vambraces: value),
|
||||
6 => copyWith(gauntlets: value),
|
||||
7 => copyWith(gambeson: value),
|
||||
8 => copyWith(cuisses: value),
|
||||
9 => copyWith(greaves: value),
|
||||
10 => copyWith(sollerets: value),
|
||||
_ => this,
|
||||
};
|
||||
}
|
||||
|
||||
Equipment copyWith({
|
||||
String? weapon,
|
||||
String? shield,
|
||||
String? armor,
|
||||
String? helm,
|
||||
String? hauberk,
|
||||
String? brassairts,
|
||||
String? vambraces,
|
||||
String? gauntlets,
|
||||
String? gambeson,
|
||||
String? cuisses,
|
||||
String? greaves,
|
||||
String? sollerets,
|
||||
int? bestIndex,
|
||||
}) {
|
||||
return Equipment(
|
||||
weapon: weapon ?? this.weapon,
|
||||
shield: shield ?? this.shield,
|
||||
armor: armor ?? this.armor,
|
||||
helm: helm ?? this.helm,
|
||||
hauberk: hauberk ?? this.hauberk,
|
||||
brassairts: brassairts ?? this.brassairts,
|
||||
vambraces: vambraces ?? this.vambraces,
|
||||
gauntlets: gauntlets ?? this.gauntlets,
|
||||
gambeson: gambeson ?? this.gambeson,
|
||||
cuisses: cuisses ?? this.cuisses,
|
||||
greaves: greaves ?? this.greaves,
|
||||
sollerets: sollerets ?? this.sollerets,
|
||||
bestIndex: bestIndex ?? this.bestIndex,
|
||||
);
|
||||
}
|
||||
@@ -304,6 +423,40 @@ class ProgressBarState {
|
||||
}
|
||||
}
|
||||
|
||||
/// 히스토리 엔트리 (Plot/Quest 진행 기록)
|
||||
class HistoryEntry {
|
||||
const HistoryEntry({required this.caption, required this.isComplete});
|
||||
|
||||
/// 표시 텍스트 (예: "Prologue", "Act I", "Exterminate the Goblins")
|
||||
final String caption;
|
||||
|
||||
/// 완료 여부 (원본 StateIndex: 0=진행중, 1=완료)
|
||||
final bool isComplete;
|
||||
|
||||
HistoryEntry copyWith({String? caption, bool? isComplete}) {
|
||||
return HistoryEntry(
|
||||
caption: caption ?? this.caption,
|
||||
isComplete: isComplete ?? this.isComplete,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 현재 퀘스트 몬스터 정보 (원본 fQuest)
|
||||
class QuestMonsterInfo {
|
||||
const QuestMonsterInfo({
|
||||
required this.monsterData,
|
||||
required this.monsterIndex,
|
||||
});
|
||||
|
||||
/// 몬스터 데이터 문자열 (예: "Goblin|3|ear")
|
||||
final String monsterData;
|
||||
|
||||
/// 몬스터 인덱스 (Config.monsters에서의 인덱스)
|
||||
final int monsterIndex;
|
||||
|
||||
static const empty = QuestMonsterInfo(monsterData: '', monsterIndex: -1);
|
||||
}
|
||||
|
||||
class ProgressState {
|
||||
const ProgressState({
|
||||
required this.task,
|
||||
@@ -314,6 +467,9 @@ class ProgressState {
|
||||
required this.currentTask,
|
||||
required this.plotStageCount,
|
||||
required this.questCount,
|
||||
this.plotHistory = const [],
|
||||
this.questHistory = const [],
|
||||
this.currentQuestMonster,
|
||||
});
|
||||
|
||||
final ProgressBarState task;
|
||||
@@ -325,6 +481,15 @@ class ProgressState {
|
||||
final int plotStageCount;
|
||||
final int questCount;
|
||||
|
||||
/// 플롯 히스토리 (Prologue, Act I, Act II, ...)
|
||||
final List<HistoryEntry> plotHistory;
|
||||
|
||||
/// 퀘스트 히스토리 (완료된/진행중인 퀘스트 목록)
|
||||
final List<HistoryEntry> questHistory;
|
||||
|
||||
/// 현재 퀘스트 몬스터 정보 (Exterminate 타입용)
|
||||
final QuestMonsterInfo? currentQuestMonster;
|
||||
|
||||
factory ProgressState.empty() => ProgressState(
|
||||
task: ProgressBarState.empty(),
|
||||
quest: ProgressBarState.empty(),
|
||||
@@ -334,6 +499,9 @@ class ProgressState {
|
||||
currentTask: TaskInfo.empty(),
|
||||
plotStageCount: 1, // Prologue
|
||||
questCount: 0,
|
||||
plotHistory: const [HistoryEntry(caption: 'Prologue', isComplete: false)],
|
||||
questHistory: const [],
|
||||
currentQuestMonster: null,
|
||||
);
|
||||
|
||||
ProgressState copyWith({
|
||||
@@ -345,6 +513,9 @@ class ProgressState {
|
||||
TaskInfo? currentTask,
|
||||
int? plotStageCount,
|
||||
int? questCount,
|
||||
List<HistoryEntry>? plotHistory,
|
||||
List<HistoryEntry>? questHistory,
|
||||
QuestMonsterInfo? currentQuestMonster,
|
||||
}) {
|
||||
return ProgressState(
|
||||
task: task ?? this.task,
|
||||
@@ -355,6 +526,9 @@ class ProgressState {
|
||||
currentTask: currentTask ?? this.currentTask,
|
||||
plotStageCount: plotStageCount ?? this.plotStageCount,
|
||||
questCount: questCount ?? this.questCount,
|
||||
plotHistory: plotHistory ?? this.plotHistory,
|
||||
questHistory: questHistory ?? this.questHistory,
|
||||
currentQuestMonster: currentQuestMonster ?? this.currentQuestMonster,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,6 +63,8 @@ class GameSave {
|
||||
'cha': stats.cha,
|
||||
'hpMax': stats.hpMax,
|
||||
'mpMax': stats.mpMax,
|
||||
'hpCurrent': stats.hpCurrent,
|
||||
'mpCurrent': stats.mpCurrent,
|
||||
},
|
||||
'inventory': {
|
||||
'gold': inventory.gold,
|
||||
@@ -73,7 +75,15 @@ class GameSave {
|
||||
'equipment': {
|
||||
'weapon': equipment.weapon,
|
||||
'shield': equipment.shield,
|
||||
'armor': equipment.armor,
|
||||
'helm': equipment.helm,
|
||||
'hauberk': equipment.hauberk,
|
||||
'brassairts': equipment.brassairts,
|
||||
'vambraces': equipment.vambraces,
|
||||
'gauntlets': equipment.gauntlets,
|
||||
'gambeson': equipment.gambeson,
|
||||
'cuisses': equipment.cuisses,
|
||||
'greaves': equipment.greaves,
|
||||
'sollerets': equipment.sollerets,
|
||||
'bestIndex': equipment.bestIndex,
|
||||
},
|
||||
'spells': spellBook.spells
|
||||
@@ -88,9 +98,23 @@ class GameSave {
|
||||
'taskInfo': {
|
||||
'caption': progress.currentTask.caption,
|
||||
'type': progress.currentTask.type.name,
|
||||
'monsterBaseName': progress.currentTask.monsterBaseName,
|
||||
'monsterPart': progress.currentTask.monsterPart,
|
||||
},
|
||||
'plotStages': progress.plotStageCount,
|
||||
'questCount': progress.questCount,
|
||||
'plotHistory': progress.plotHistory
|
||||
.map((e) => {'caption': e.caption, 'complete': e.isComplete})
|
||||
.toList(),
|
||||
'questHistory': progress.questHistory
|
||||
.map((e) => {'caption': e.caption, 'complete': e.isComplete})
|
||||
.toList(),
|
||||
'questMonster': progress.currentQuestMonster != null
|
||||
? {
|
||||
'data': progress.currentQuestMonster!.monsterData,
|
||||
'index': progress.currentQuestMonster!.monsterIndex,
|
||||
}
|
||||
: null,
|
||||
},
|
||||
'queue': queue.entries
|
||||
.map(
|
||||
@@ -134,6 +158,8 @@ class GameSave {
|
||||
cha: statsJson['cha'] as int? ?? 0,
|
||||
hpMax: statsJson['hpMax'] as int? ?? 0,
|
||||
mpMax: statsJson['mpMax'] as int? ?? 0,
|
||||
hpCurrent: statsJson['hpCurrent'] as int?,
|
||||
mpCurrent: statsJson['mpCurrent'] as int?,
|
||||
),
|
||||
inventory: Inventory(
|
||||
gold: inventoryJson['gold'] as int? ?? 0,
|
||||
@@ -149,7 +175,15 @@ class GameSave {
|
||||
equipment: Equipment(
|
||||
weapon: equipmentJson['weapon'] as String? ?? 'Sharp Stick',
|
||||
shield: equipmentJson['shield'] as String? ?? '',
|
||||
armor: equipmentJson['armor'] as String? ?? '',
|
||||
helm: equipmentJson['helm'] as String? ?? '',
|
||||
hauberk: equipmentJson['hauberk'] as String? ?? '',
|
||||
brassairts: equipmentJson['brassairts'] as String? ?? '',
|
||||
vambraces: equipmentJson['vambraces'] as String? ?? '',
|
||||
gauntlets: equipmentJson['gauntlets'] as String? ?? '',
|
||||
gambeson: equipmentJson['gambeson'] as String? ?? '',
|
||||
cuisses: equipmentJson['cuisses'] as String? ?? '',
|
||||
greaves: equipmentJson['greaves'] as String? ?? '',
|
||||
sollerets: equipmentJson['sollerets'] as String? ?? '',
|
||||
bestIndex: equipmentJson['bestIndex'] as int? ?? 0,
|
||||
),
|
||||
spellBook: SpellBook(
|
||||
@@ -178,6 +212,15 @@ class GameSave {
|
||||
),
|
||||
plotStageCount: progressJson['plotStages'] as int? ?? 1,
|
||||
questCount: progressJson['questCount'] as int? ?? 0,
|
||||
plotHistory: _historyListFromJson(
|
||||
progressJson['plotHistory'] as List<dynamic>?,
|
||||
),
|
||||
questHistory: _historyListFromJson(
|
||||
progressJson['questHistory'] as List<dynamic>?,
|
||||
),
|
||||
currentQuestMonster: _questMonsterFromJson(
|
||||
progressJson['questMonster'] as Map<String, dynamic>?,
|
||||
),
|
||||
),
|
||||
queue: QueueState(
|
||||
entries: Queue<QueueEntry>.from(
|
||||
@@ -233,5 +276,29 @@ TaskInfo _taskInfoFromJson(Map<String, dynamic> json) {
|
||||
(t) => t.name == typeName,
|
||||
orElse: () => TaskType.neutral,
|
||||
);
|
||||
return TaskInfo(caption: json['caption'] as String? ?? '', type: type);
|
||||
return TaskInfo(
|
||||
caption: json['caption'] as String? ?? '',
|
||||
type: type,
|
||||
monsterBaseName: json['monsterBaseName'] as String?,
|
||||
monsterPart: json['monsterPart'] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
List<HistoryEntry> _historyListFromJson(List<dynamic>? json) {
|
||||
if (json == null || json.isEmpty) return [];
|
||||
return json.map((e) {
|
||||
final m = e as Map<String, dynamic>;
|
||||
return HistoryEntry(
|
||||
caption: m['caption'] as String? ?? '',
|
||||
isComplete: m['complete'] as bool? ?? false,
|
||||
);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
QuestMonsterInfo? _questMonsterFromJson(Map<String, dynamic>? json) {
|
||||
if (json == null) return null;
|
||||
return QuestMonsterInfo(
|
||||
monsterData: json['data'] as String? ?? '',
|
||||
monsterIndex: json['index'] as int? ?? -1,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -301,7 +301,9 @@ class _GamePlayScreenState extends State<GamePlayScreen>
|
||||
child: Text(
|
||||
'${speed}x',
|
||||
style: TextStyle(
|
||||
fontWeight: speed > 1 ? FontWeight.bold : FontWeight.normal,
|
||||
fontWeight: speed > 1
|
||||
? FontWeight.bold
|
||||
: FontWeight.normal,
|
||||
color: speed > 1
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: null,
|
||||
@@ -468,11 +470,19 @@ class _GamePlayScreenState extends State<GamePlayScreen>
|
||||
}
|
||||
|
||||
Widget _buildEquipmentList(GameState state) {
|
||||
// 원본에는 11개 슬롯이 있지만, 현재 모델은 3개만 구현
|
||||
// 원본 Main.dfm Equips ListView - 11개 슬롯
|
||||
final equipment = [
|
||||
('Weapon', state.equipment.weapon),
|
||||
('Shield', state.equipment.shield),
|
||||
('Armor', state.equipment.armor),
|
||||
('Helm', state.equipment.helm),
|
||||
('Hauberk', state.equipment.hauberk),
|
||||
('Brassairts', state.equipment.brassairts),
|
||||
('Vambraces', state.equipment.vambraces),
|
||||
('Gauntlets', state.equipment.gauntlets),
|
||||
('Gambeson', state.equipment.gambeson),
|
||||
('Cuisses', state.equipment.cuisses),
|
||||
('Greaves', state.equipment.greaves),
|
||||
('Sollerets', state.equipment.sollerets),
|
||||
];
|
||||
|
||||
return ListView.builder(
|
||||
|
||||
@@ -38,7 +38,8 @@ class _NewCharacterScreenState extends State<NewCharacterScreen> {
|
||||
int _wis = 0;
|
||||
int _cha = 0;
|
||||
|
||||
// 롤 이력 (Unroll 기능용)
|
||||
// 롤 이력 (Unroll 기능용) - 원본 OldRolls TListBox
|
||||
static const int _maxRollHistory = 20; // 최대 저장 개수
|
||||
final List<int> _rollHistory = [];
|
||||
|
||||
// 현재 RNG 시드 (Re-Roll 전 저장)
|
||||
@@ -93,6 +94,11 @@ class _NewCharacterScreenState extends State<NewCharacterScreen> {
|
||||
// 현재 시드를 이력에 저장
|
||||
_rollHistory.insert(0, _currentSeed);
|
||||
|
||||
// 최대 개수 초과 시 가장 오래된 항목 제거
|
||||
if (_rollHistory.length > _maxRollHistory) {
|
||||
_rollHistory.removeLast();
|
||||
}
|
||||
|
||||
// 새 시드로 굴림
|
||||
_currentSeed = math.Random().nextInt(0x7FFFFFFF);
|
||||
_rollStats();
|
||||
@@ -132,6 +138,7 @@ class _NewCharacterScreenState extends State<NewCharacterScreen> {
|
||||
}
|
||||
|
||||
/// Sold! 버튼 클릭 - 캐릭터 생성 완료
|
||||
/// 원본 Main.pas:1371-1388 RollCharacter 로직
|
||||
void _onSold() {
|
||||
final name = _nameController.text.trim();
|
||||
if (name.isEmpty) {
|
||||
@@ -144,22 +151,23 @@ class _NewCharacterScreenState extends State<NewCharacterScreen> {
|
||||
// 게임에 사용할 새 RNG 생성
|
||||
final gameSeed = math.Random().nextInt(0x7FFFFFFF);
|
||||
|
||||
// 종족/직업의 보너스 스탯 파싱
|
||||
final raceEntry = _config.races[_selectedRaceIndex];
|
||||
final klassEntry = _config.klasses[_selectedKlassIndex];
|
||||
final raceBonus = _parseStatBonus(raceEntry);
|
||||
final klassBonus = _parseStatBonus(klassEntry);
|
||||
// 원본 Main.pas:1380-1381 - 기본 롤 값(CON.Tag, INT.Tag)만 사용
|
||||
// 종족/직업 보너스는 스탯에 적용되지 않음 (UI 힌트용)
|
||||
// Put(Stats,'HP Max',Random(8) + CON.Tag div 6);
|
||||
// Put(Stats,'MP Max',Random(8) + INT.Tag div 6);
|
||||
final hpMax = math.Random().nextInt(8) + _con ~/ 6;
|
||||
final mpMax = math.Random().nextInt(8) + _int ~/ 6;
|
||||
|
||||
// 최종 스탯 계산 (기본 + 종족 보너스 + 직업 보너스)
|
||||
// 원본 Main.pas:1375-1379 - 기본 롤 값 그대로 저장 (보너스 없음)
|
||||
final finalStats = Stats(
|
||||
str: _str + (raceBonus['STR'] ?? 0) + (klassBonus['STR'] ?? 0),
|
||||
con: _con + (raceBonus['CON'] ?? 0) + (klassBonus['CON'] ?? 0),
|
||||
dex: _dex + (raceBonus['DEX'] ?? 0) + (klassBonus['DEX'] ?? 0),
|
||||
intelligence: _int + (raceBonus['INT'] ?? 0) + (klassBonus['INT'] ?? 0),
|
||||
wis: _wis + (raceBonus['WIS'] ?? 0) + (klassBonus['WIS'] ?? 0),
|
||||
cha: _cha + (raceBonus['CHA'] ?? 0) + (klassBonus['CHA'] ?? 0),
|
||||
hpMax: _con + (raceBonus['CON'] ?? 0) + (klassBonus['CON'] ?? 0),
|
||||
mpMax: _int + (raceBonus['INT'] ?? 0) + (klassBonus['INT'] ?? 0),
|
||||
str: _str,
|
||||
con: _con,
|
||||
dex: _dex,
|
||||
intelligence: _int,
|
||||
wis: _wis,
|
||||
cha: _cha,
|
||||
hpMax: hpMax,
|
||||
mpMax: mpMax,
|
||||
);
|
||||
|
||||
final traits = Traits(
|
||||
@@ -186,24 +194,6 @@ class _NewCharacterScreenState extends State<NewCharacterScreen> {
|
||||
widget.onCharacterCreated?.call(initialState);
|
||||
}
|
||||
|
||||
/// 종족/직업 보너스 파싱 (예: "Half Orc|STR+2,INT-1")
|
||||
Map<String, int> _parseStatBonus(String entry) {
|
||||
final parts = entry.split('|');
|
||||
if (parts.length < 2) return {};
|
||||
|
||||
final bonuses = <String, int>{};
|
||||
final bonusPart = parts[1];
|
||||
|
||||
// STR+2,INT-1 형식 파싱
|
||||
final regex = RegExp(r'([A-Z]+)([+-]\d+)');
|
||||
for (final match in regex.allMatches(bonusPart)) {
|
||||
final stat = match.group(1)!;
|
||||
final value = int.parse(match.group(2)!);
|
||||
bonuses[stat] = value;
|
||||
}
|
||||
return bonuses;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
|
||||
@@ -35,9 +35,7 @@ void main() {
|
||||
),
|
||||
inventory: const Inventory(
|
||||
gold: 5,
|
||||
items: [
|
||||
InventoryEntry(name: 'Rock', count: 3),
|
||||
],
|
||||
items: [InventoryEntry(name: 'Rock', count: 3)],
|
||||
),
|
||||
progress: const ProgressState(
|
||||
task: ProgressBarState(position: 0, max: 80),
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import 'package:askiineverdie/src/core/model/equipment_slot.dart';
|
||||
import 'package:askiineverdie/src/core/model/game_state.dart';
|
||||
import 'package:askiineverdie/src/core/model/pq_config.dart';
|
||||
import 'package:askiineverdie/src/core/util/deterministic_random.dart';
|
||||
@@ -65,7 +64,7 @@ void main() {
|
||||
config,
|
||||
DeterministicRandom(12),
|
||||
5,
|
||||
EquipmentSlot.weapon,
|
||||
0, // weapon slot
|
||||
),
|
||||
'Baselard',
|
||||
);
|
||||
@@ -74,7 +73,7 @@ void main() {
|
||||
config,
|
||||
DeterministicRandom(15),
|
||||
2,
|
||||
EquipmentSlot.armor,
|
||||
2, // helm slot (armor category)
|
||||
),
|
||||
'-2 Canvas',
|
||||
);
|
||||
@@ -86,18 +85,35 @@ void main() {
|
||||
});
|
||||
|
||||
test('monsterTask picks level-appropriate monsters with modifiers', () {
|
||||
expect(
|
||||
pq_logic.monsterTask(config, DeterministicRandom(99), 5, null, null),
|
||||
'an underage Rakshasa',
|
||||
final result1 = pq_logic.monsterTask(
|
||||
config,
|
||||
DeterministicRandom(99),
|
||||
5,
|
||||
null,
|
||||
null,
|
||||
);
|
||||
expect(
|
||||
pq_logic.monsterTask(config, DeterministicRandom(7), 10, null, null),
|
||||
'a greater Sphinx',
|
||||
expect(result1.displayName, 'an underage Rakshasa');
|
||||
expect(result1.baseName, 'Rakshasa');
|
||||
expect(result1.part, isNotEmpty);
|
||||
|
||||
final result2 = pq_logic.monsterTask(
|
||||
config,
|
||||
DeterministicRandom(7),
|
||||
10,
|
||||
null,
|
||||
null,
|
||||
);
|
||||
expect(
|
||||
pq_logic.monsterTask(config, DeterministicRandom(5), 6, 'Goblin|3', 3),
|
||||
'a Barbed Devil',
|
||||
expect(result2.displayName, 'a greater Sphinx');
|
||||
expect(result2.baseName, 'Sphinx');
|
||||
|
||||
final result3 = pq_logic.monsterTask(
|
||||
config,
|
||||
DeterministicRandom(5),
|
||||
6,
|
||||
'Goblin|3|ear',
|
||||
3,
|
||||
);
|
||||
expect(result3.displayName, 'a Barbed Devil');
|
||||
});
|
||||
|
||||
test('completeQuest and completeAct return deterministic results', () {
|
||||
|
||||
@@ -7,7 +7,6 @@ library;
|
||||
import 'package:askiineverdie/src/core/engine/game_mutations.dart';
|
||||
import 'package:askiineverdie/src/core/engine/progress_service.dart';
|
||||
import 'package:askiineverdie/src/core/engine/reward_service.dart';
|
||||
import 'package:askiineverdie/src/core/model/equipment_slot.dart';
|
||||
import 'package:askiineverdie/src/core/model/game_state.dart';
|
||||
import 'package:askiineverdie/src/core/model/pq_config.dart';
|
||||
import 'package:askiineverdie/src/core/util/deterministic_random.dart';
|
||||
@@ -41,71 +40,59 @@ void main() {
|
||||
test('monsterTask produces consistent monster names', () {
|
||||
// 시드 42, 레벨 5에서의 몬스터 이름
|
||||
expect(
|
||||
pq_logic.monsterTask(
|
||||
config,
|
||||
DeterministicRandom(testSeed),
|
||||
5,
|
||||
null,
|
||||
null,
|
||||
),
|
||||
pq_logic
|
||||
.monsterTask(config, DeterministicRandom(testSeed), 5, null, null)
|
||||
.displayName,
|
||||
'an underage Su-monster',
|
||||
);
|
||||
|
||||
// 시드 42, 레벨 10에서의 몬스터 이름
|
||||
expect(
|
||||
pq_logic.monsterTask(
|
||||
config,
|
||||
DeterministicRandom(testSeed),
|
||||
10,
|
||||
null,
|
||||
null,
|
||||
),
|
||||
pq_logic
|
||||
.monsterTask(config, DeterministicRandom(testSeed), 10, null, null)
|
||||
.displayName,
|
||||
'a cursed Troll',
|
||||
);
|
||||
|
||||
// 시드 42, 레벨 1에서의 몬스터 이름
|
||||
expect(
|
||||
pq_logic.monsterTask(
|
||||
config,
|
||||
DeterministicRandom(testSeed),
|
||||
1,
|
||||
null,
|
||||
null,
|
||||
),
|
||||
pq_logic
|
||||
.monsterTask(config, DeterministicRandom(testSeed), 1, null, null)
|
||||
.displayName,
|
||||
'a greater Crayfish',
|
||||
);
|
||||
});
|
||||
|
||||
test('winEquip produces consistent equipment', () {
|
||||
// 시드 42에서 무기 획득
|
||||
// 시드 42에서 무기 획득 (슬롯 0)
|
||||
expect(
|
||||
pq_logic.winEquip(
|
||||
config,
|
||||
DeterministicRandom(testSeed),
|
||||
5,
|
||||
EquipmentSlot.weapon,
|
||||
0, // weapon slot
|
||||
),
|
||||
'Longiron',
|
||||
);
|
||||
|
||||
// 시드 42에서 방어구 획득
|
||||
// 시드 42에서 방어구 획득 (슬롯 2 = helm, armor 카테고리)
|
||||
expect(
|
||||
pq_logic.winEquip(
|
||||
config,
|
||||
DeterministicRandom(testSeed),
|
||||
5,
|
||||
EquipmentSlot.armor,
|
||||
2, // helm slot (armor category)
|
||||
),
|
||||
'-1 Holey Mildewed Bearskin',
|
||||
);
|
||||
|
||||
// 시드 42에서 방패 획득
|
||||
// 시드 42에서 방패 획득 (슬롯 1)
|
||||
expect(
|
||||
pq_logic.winEquip(
|
||||
config,
|
||||
DeterministicRandom(testSeed),
|
||||
5,
|
||||
EquipmentSlot.shield,
|
||||
1, // shield slot
|
||||
),
|
||||
'Round Shield',
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user