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