feat(core): 장비 시스템 및 게임 상태 모델 확장
- Equipment 클래스를 11개 슬롯으로 확장 (원본 Main.dfm 충실) - TaskInfo에 몬스터 정보(baseName, part) 추가 - Stats에 현재 HP/MP 필드 추가 - 히스토리 기능 구현 (plotHistory, questHistory) - pq_logic winEquip/winStatIndex 원본 로직 개선 - 퀘스트 몬스터 처리 로직 구현 - SaveData 직렬화 확장
This commit is contained in:
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user