feat(core): 장비 시스템 및 게임 상태 모델 확장
- Equipment 클래스를 11개 슬롯으로 확장 (원본 Main.dfm 충실) - TaskInfo에 몬스터 정보(baseName, part) 추가 - Stats에 현재 HP/MP 필드 추가 - 히스토리 기능 구현 (plotHistory, questHistory) - pq_logic winEquip/winStatIndex 원본 로직 개선 - 퀘스트 몬스터 처리 로직 구현 - SaveData 직렬화 확장
This commit is contained in:
@@ -9,27 +9,22 @@ class GameMutations {
|
|||||||
|
|
||||||
final PqConfig config;
|
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 rng = state.rng;
|
||||||
final name = pq_logic.winEquip(config, rng, level, slot);
|
final name = pq_logic.winEquip(config, rng, level, slotIndex);
|
||||||
final equip = state.equipment;
|
final updatedEquip = state.equipment
|
||||||
final updatedEquip = switch (slot) {
|
.setByIndex(slotIndex, name)
|
||||||
EquipmentSlot.weapon => equip.copyWith(
|
.copyWith(bestIndex: slotIndex);
|
||||||
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,
|
|
||||||
),
|
|
||||||
};
|
|
||||||
return state.copyWith(rng: rng, equipment: updatedEquip);
|
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) {
|
GameState winStat(GameState state) {
|
||||||
final updatedStats = pq_logic.winStat(state.stats, state.rng);
|
final updatedStats = pq_logic.winStat(state.stats, state.rng);
|
||||||
return state.copyWith(rng: state.rng, stats: updatedStats);
|
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/game_mutations.dart';
|
||||||
import 'package:askiineverdie/src/core/engine/reward_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/game_state.dart';
|
||||||
import 'package:askiineverdie/src/core/model/pq_config.dart';
|
import 'package:askiineverdie/src/core/model/pq_config.dart';
|
||||||
import 'package:askiineverdie/src/core/util/pq_logic.dart' as pq_logic;
|
import 'package:askiineverdie/src/core/util/pq_logic.dart' as pq_logic;
|
||||||
@@ -49,21 +48,24 @@ class ProgressService {
|
|||||||
const QueueEntry(
|
const QueueEntry(
|
||||||
kind: QueueKind.task,
|
kind: QueueKind.task,
|
||||||
durationMillis: 6 * 1000,
|
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',
|
'underestimated',
|
||||||
taskType: TaskType.load,
|
taskType: TaskType.load,
|
||||||
),
|
),
|
||||||
const QueueEntry(
|
const QueueEntry(
|
||||||
kind: QueueKind.task,
|
kind: QueueKind.task,
|
||||||
durationMillis: 6 * 1000,
|
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',
|
'but resolute',
|
||||||
taskType: TaskType.load,
|
taskType: TaskType.load,
|
||||||
),
|
),
|
||||||
const QueueEntry(
|
const QueueEntry(
|
||||||
kind: QueueKind.task,
|
kind: QueueKind.task,
|
||||||
durationMillis: 4 * 1000,
|
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',
|
'you set out on a long and dangerous journey',
|
||||||
taskType: TaskType.load,
|
taskType: TaskType.load,
|
||||||
),
|
),
|
||||||
@@ -76,17 +78,10 @@ class ProgressService {
|
|||||||
];
|
];
|
||||||
|
|
||||||
// 첫 번째 태스크 'Loading' 시작 (원본 752줄)
|
// 첫 번째 태스크 'Loading' 시작 (원본 752줄)
|
||||||
final taskResult = pq_logic.startTask(
|
final taskResult = pq_logic.startTask(state.progress, 'Loading', 2 * 1000);
|
||||||
state.progress,
|
|
||||||
'Loading',
|
|
||||||
2 * 1000,
|
|
||||||
);
|
|
||||||
|
|
||||||
// ExpBar 초기화 (원본 743-746줄)
|
// ExpBar 초기화 (원본 743-746줄)
|
||||||
final expBar = ProgressBarState(
|
final expBar = ProgressBarState(position: 0, max: pq_logic.levelUpTime(1));
|
||||||
position: 0,
|
|
||||||
max: pq_logic.levelUpTime(1),
|
|
||||||
);
|
|
||||||
|
|
||||||
// PlotBar 초기화 (원본 759줄)
|
// PlotBar 초기화 (원본 759줄)
|
||||||
final plotBar = const ProgressBarState(position: 0, max: 26 * 1000);
|
final plotBar = const ProgressBarState(position: 0, max: 26 * 1000);
|
||||||
@@ -97,6 +92,8 @@ class ProgressService {
|
|||||||
currentTask: const TaskInfo(caption: 'Loading...', type: TaskType.load),
|
currentTask: const TaskInfo(caption: 'Loading...', type: TaskType.load),
|
||||||
plotStageCount: 1, // Prologue
|
plotStageCount: 1, // Prologue
|
||||||
questCount: 0,
|
questCount: 0,
|
||||||
|
plotHistory: const [HistoryEntry(caption: 'Prologue', isComplete: false)],
|
||||||
|
questHistory: const [],
|
||||||
);
|
);
|
||||||
|
|
||||||
return _recalculateEncumbrance(
|
return _recalculateEncumbrance(
|
||||||
@@ -348,12 +345,21 @@ class ProgressService {
|
|||||||
|
|
||||||
// 3. MonsterTask 실행 (원본 678-684줄)
|
// 3. MonsterTask 실행 (원본 678-684줄)
|
||||||
final level = state.traits.level;
|
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,
|
config,
|
||||||
state.rng,
|
state.rng,
|
||||||
level,
|
level,
|
||||||
null, // questMonster
|
questMonsterData,
|
||||||
null, // questLevel
|
questLevel,
|
||||||
);
|
);
|
||||||
|
|
||||||
// 태스크 지속시간 계산 (원본 682줄)
|
// 태스크 지속시간 계산 (원본 682줄)
|
||||||
@@ -364,7 +370,7 @@ class ProgressService {
|
|||||||
|
|
||||||
final taskResult = pq_logic.startTask(
|
final taskResult = pq_logic.startTask(
|
||||||
progress,
|
progress,
|
||||||
'Executing $monster',
|
'Executing ${monsterResult.displayName}',
|
||||||
durationMillis,
|
durationMillis,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -372,6 +378,8 @@ class ProgressService {
|
|||||||
currentTask: TaskInfo(
|
currentTask: TaskInfo(
|
||||||
caption: taskResult.caption,
|
caption: taskResult.caption,
|
||||||
type: TaskType.kill,
|
type: TaskType.kill,
|
||||||
|
monsterBaseName: monsterResult.baseName,
|
||||||
|
monsterPart: monsterResult.part,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -389,6 +397,23 @@ class ProgressService {
|
|||||||
var nextState = _applyReward(state, result.reward);
|
var nextState = _applyReward(state, result.reward);
|
||||||
final questCount = nextState.progress.questCount + 1;
|
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).
|
// Append quest entry to queue (task kind).
|
||||||
final updatedQueue = QueueState(
|
final updatedQueue = QueueState(
|
||||||
entries: [
|
entries: [
|
||||||
@@ -409,6 +434,8 @@ class ProgressService {
|
|||||||
max: 50 + nextState.rng.nextInt(100),
|
max: 50 + nextState.rng.nextInt(100),
|
||||||
),
|
),
|
||||||
questCount: questCount,
|
questCount: questCount,
|
||||||
|
questHistory: updatedQuestHistory,
|
||||||
|
currentQuestMonster: questMonster,
|
||||||
);
|
);
|
||||||
|
|
||||||
return _recalculateEncumbrance(
|
return _recalculateEncumbrance(
|
||||||
@@ -425,9 +452,19 @@ class ProgressService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final plotStages = nextState.progress.plotStageCount + 1;
|
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(
|
var updatedProgress = nextState.progress.copyWith(
|
||||||
plot: ProgressBarState(position: 0, max: actResult.plotBarMaxSeconds),
|
plot: ProgressBarState(position: 0, max: actResult.plotBarMaxSeconds),
|
||||||
plotStageCount: plotStages,
|
plotStageCount: plotStages,
|
||||||
|
plotHistory: updatedPlotHistory,
|
||||||
);
|
);
|
||||||
|
|
||||||
nextState = nextState.copyWith(progress: updatedProgress);
|
nextState = nextState.copyWith(progress: updatedProgress);
|
||||||
@@ -455,6 +492,20 @@ class ProgressService {
|
|||||||
max: 50 + state.rng.nextInt(100),
|
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(
|
final updatedQueue = QueueState(
|
||||||
entries: [
|
entries: [
|
||||||
@@ -471,6 +522,8 @@ class ProgressService {
|
|||||||
final progress = state.progress.copyWith(
|
final progress = state.progress.copyWith(
|
||||||
quest: questBar,
|
quest: questBar,
|
||||||
questCount: 1,
|
questCount: 1,
|
||||||
|
questHistory: questHistory,
|
||||||
|
currentQuestMonster: questMonster,
|
||||||
);
|
);
|
||||||
|
|
||||||
return state.copyWith(progress: progress, queue: updatedQueue);
|
return state.copyWith(progress: progress, queue: updatedQueue);
|
||||||
@@ -550,28 +603,25 @@ class ProgressService {
|
|||||||
|
|
||||||
/// 킬 태스크 완료 시 전리품 획득 (원본 Main.pas:625-630)
|
/// 킬 태스크 완료 시 전리품 획득 (원본 Main.pas:625-630)
|
||||||
GameState _winLoot(GameState state) {
|
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) + ' ' +
|
// 원본: Add(Inventory, LowerCase(Split(fTask.Caption,1) + ' ' +
|
||||||
// ProperCase(Split(fTask.Caption,3))), 1);
|
// ProperCase(Split(fTask.Caption,3))), 1);
|
||||||
// 예: "Executing a Goblin..." -> "goblin ear" 등의 아이템
|
// 예: "goblin Claw" 형태로 인벤토리 추가
|
||||||
|
final itemName =
|
||||||
// 태스크 캡션에서 몬스터 이름 추출 ("Executing ..." 형태)
|
'${monsterBaseName.toLowerCase()} ${_properCase(monsterPart)}';
|
||||||
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';
|
|
||||||
|
|
||||||
// 인벤토리에 추가
|
// 인벤토리에 추가
|
||||||
final items = [...state.inventory.items];
|
final items = [...state.inventory.items];
|
||||||
@@ -584,32 +634,13 @@ class ProgressService {
|
|||||||
items.add(InventoryEntry(name: itemName, count: 1));
|
items.add(InventoryEntry(name: itemName, count: 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
return state.copyWith(
|
return state.copyWith(inventory: state.inventory.copyWith(items: items));
|
||||||
inventory: state.inventory.copyWith(items: items),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 몬스터 이름에서 기본 이름 추출 (형용사 제거)
|
/// 첫 글자만 대문자로 변환 (원본 ProperCase)
|
||||||
String _extractBaseName(String name) {
|
String _properCase(String s) {
|
||||||
// "a Goblin", "an Orc", "2 Goblins" 등에서 기본 이름 추출
|
if (s.isEmpty) return s;
|
||||||
final words = name.split(' ');
|
return s[0].toUpperCase() + s.substring(1);
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 인벤토리에서 Gold 수량 반환
|
/// 인벤토리에서 Gold 수량 반환
|
||||||
@@ -635,11 +666,9 @@ class ProgressService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// 장비 획득 (WinEquip)
|
// 장비 획득 (WinEquip)
|
||||||
nextState = mutations.winEquip(
|
// 원본 Main.pas:797 - posn := Random(Equips.Items.Count); (11개 슬롯)
|
||||||
nextState,
|
final slotIndex = nextState.rng.nextInt(Equipment.slotCount);
|
||||||
level,
|
nextState = mutations.winEquipByIndex(nextState, level, slotIndex);
|
||||||
EquipmentSlot.values[nextState.rng.nextInt(EquipmentSlot.values.length)],
|
|
||||||
);
|
|
||||||
|
|
||||||
return nextState;
|
return nextState;
|
||||||
}
|
}
|
||||||
@@ -662,7 +691,8 @@ class ProgressService {
|
|||||||
|
|
||||||
// " of " 포함 시 보너스 (원본 639-640)
|
// " of " 포함 시 보너스 (원본 639-640)
|
||||||
if (item.name.contains(' of ')) {
|
if (item.name.contains(' of ')) {
|
||||||
price = price *
|
price =
|
||||||
|
price *
|
||||||
(1 + pq_logic.randomLow(state.rng, 10)) *
|
(1 + pq_logic.randomLow(state.rng, 10)) *
|
||||||
(1 + pq_logic.randomLow(state.rng, level));
|
(1 + pq_logic.randomLow(state.rng, level));
|
||||||
}
|
}
|
||||||
@@ -687,10 +717,7 @@ class ProgressService {
|
|||||||
1 * 1000,
|
1 * 1000,
|
||||||
);
|
);
|
||||||
final progress = taskResult.progress.copyWith(
|
final progress = taskResult.progress.copyWith(
|
||||||
currentTask: TaskInfo(
|
currentTask: TaskInfo(caption: taskResult.caption, type: TaskType.sell),
|
||||||
caption: taskResult.caption,
|
|
||||||
type: TaskType.sell,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
state: state.copyWith(
|
state: state.copyWith(
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import 'package:askiineverdie/src/core/engine/game_mutations.dart';
|
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/model/game_state.dart';
|
||||||
import 'package:askiineverdie/src/core/util/pq_logic.dart';
|
import 'package:askiineverdie/src/core/util/pq_logic.dart';
|
||||||
|
|
||||||
@@ -14,9 +13,9 @@ class RewardService {
|
|||||||
case RewardKind.spell:
|
case RewardKind.spell:
|
||||||
return mutations.winSpell(state, state.stats.wis, state.traits.level);
|
return mutations.winSpell(state, state.stats.wis, state.traits.level);
|
||||||
case RewardKind.equip:
|
case RewardKind.equip:
|
||||||
final slot = EquipmentSlot
|
// 원본 Main.pas:797 - Random(Equips.Items.Count) (11개 슬롯)
|
||||||
.values[state.rng.nextInt(EquipmentSlot.values.length)];
|
final slotIndex = state.rng.nextInt(Equipment.slotCount);
|
||||||
return mutations.winEquip(state, state.traits.level, slot);
|
return mutations.winEquipByIndex(state, state.traits.level, slotIndex);
|
||||||
case RewardKind.stat:
|
case RewardKind.stat:
|
||||||
return mutations.winStat(state);
|
return mutations.winStat(state);
|
||||||
case RewardKind.item:
|
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 {
|
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 String caption;
|
||||||
final TaskType type;
|
final TaskType type;
|
||||||
|
|
||||||
|
/// 킬 태스크의 몬스터 기본 이름 (형용사 제외, 예: "Goblin")
|
||||||
|
final String? monsterBaseName;
|
||||||
|
|
||||||
|
/// 킬 태스크의 전리품 부위 (예: "claw", "tail", "*"는 WinItem)
|
||||||
|
final String? monsterPart;
|
||||||
|
|
||||||
factory TaskInfo.empty() =>
|
factory TaskInfo.empty() =>
|
||||||
const TaskInfo(caption: '', type: TaskType.neutral);
|
const TaskInfo(caption: '', type: TaskType.neutral);
|
||||||
|
|
||||||
TaskInfo copyWith({String? caption, TaskType? type}) {
|
TaskInfo copyWith({
|
||||||
return TaskInfo(caption: caption ?? this.caption, type: type ?? this.type);
|
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.cha,
|
||||||
required this.hpMax,
|
required this.hpMax,
|
||||||
required this.mpMax,
|
required this.mpMax,
|
||||||
|
this.hpCurrent,
|
||||||
|
this.mpCurrent,
|
||||||
});
|
});
|
||||||
|
|
||||||
final int str;
|
final int str;
|
||||||
@@ -169,6 +192,18 @@ class Stats {
|
|||||||
final int hpMax;
|
final int hpMax;
|
||||||
final int mpMax;
|
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(
|
factory Stats.empty() => const Stats(
|
||||||
str: 0,
|
str: 0,
|
||||||
con: 0,
|
con: 0,
|
||||||
@@ -189,6 +224,8 @@ class Stats {
|
|||||||
int? cha,
|
int? cha,
|
||||||
int? hpMax,
|
int? hpMax,
|
||||||
int? mpMax,
|
int? mpMax,
|
||||||
|
int? hpCurrent,
|
||||||
|
int? mpCurrent,
|
||||||
}) {
|
}) {
|
||||||
return Stats(
|
return Stats(
|
||||||
str: str ?? this.str,
|
str: str ?? this.str,
|
||||||
@@ -199,6 +236,8 @@ class Stats {
|
|||||||
cha: cha ?? this.cha,
|
cha: cha ?? this.cha,
|
||||||
hpMax: hpMax ?? this.hpMax,
|
hpMax: hpMax ?? this.hpMax,
|
||||||
mpMax: mpMax ?? this.mpMax,
|
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 {
|
class Equipment {
|
||||||
const Equipment({
|
const Equipment({
|
||||||
required this.weapon,
|
required this.weapon,
|
||||||
required this.shield,
|
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,
|
required this.bestIndex,
|
||||||
});
|
});
|
||||||
|
|
||||||
final String weapon;
|
final String weapon; // 0: 무기
|
||||||
final String shield;
|
final String shield; // 1: 방패
|
||||||
final String armor;
|
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;
|
final int bestIndex;
|
||||||
|
|
||||||
|
/// 슬롯 개수
|
||||||
|
static const slotCount = 11;
|
||||||
|
|
||||||
factory Equipment.empty() => const Equipment(
|
factory Equipment.empty() => const Equipment(
|
||||||
weapon: 'Sharp Stick',
|
weapon: 'Sharp Stick',
|
||||||
shield: '',
|
shield: '',
|
||||||
armor: '',
|
helm: '',
|
||||||
|
hauberk: '',
|
||||||
|
brassairts: '',
|
||||||
|
vambraces: '',
|
||||||
|
gauntlets: '',
|
||||||
|
gambeson: '',
|
||||||
|
cuisses: '',
|
||||||
|
greaves: '',
|
||||||
|
sollerets: '',
|
||||||
bestIndex: 0,
|
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({
|
Equipment copyWith({
|
||||||
String? weapon,
|
String? weapon,
|
||||||
String? shield,
|
String? shield,
|
||||||
String? armor,
|
String? helm,
|
||||||
|
String? hauberk,
|
||||||
|
String? brassairts,
|
||||||
|
String? vambraces,
|
||||||
|
String? gauntlets,
|
||||||
|
String? gambeson,
|
||||||
|
String? cuisses,
|
||||||
|
String? greaves,
|
||||||
|
String? sollerets,
|
||||||
int? bestIndex,
|
int? bestIndex,
|
||||||
}) {
|
}) {
|
||||||
return Equipment(
|
return Equipment(
|
||||||
weapon: weapon ?? this.weapon,
|
weapon: weapon ?? this.weapon,
|
||||||
shield: shield ?? this.shield,
|
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,
|
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 {
|
class ProgressState {
|
||||||
const ProgressState({
|
const ProgressState({
|
||||||
required this.task,
|
required this.task,
|
||||||
@@ -314,6 +467,9 @@ class ProgressState {
|
|||||||
required this.currentTask,
|
required this.currentTask,
|
||||||
required this.plotStageCount,
|
required this.plotStageCount,
|
||||||
required this.questCount,
|
required this.questCount,
|
||||||
|
this.plotHistory = const [],
|
||||||
|
this.questHistory = const [],
|
||||||
|
this.currentQuestMonster,
|
||||||
});
|
});
|
||||||
|
|
||||||
final ProgressBarState task;
|
final ProgressBarState task;
|
||||||
@@ -325,6 +481,15 @@ class ProgressState {
|
|||||||
final int plotStageCount;
|
final int plotStageCount;
|
||||||
final int questCount;
|
final int questCount;
|
||||||
|
|
||||||
|
/// 플롯 히스토리 (Prologue, Act I, Act II, ...)
|
||||||
|
final List<HistoryEntry> plotHistory;
|
||||||
|
|
||||||
|
/// 퀘스트 히스토리 (완료된/진행중인 퀘스트 목록)
|
||||||
|
final List<HistoryEntry> questHistory;
|
||||||
|
|
||||||
|
/// 현재 퀘스트 몬스터 정보 (Exterminate 타입용)
|
||||||
|
final QuestMonsterInfo? currentQuestMonster;
|
||||||
|
|
||||||
factory ProgressState.empty() => ProgressState(
|
factory ProgressState.empty() => ProgressState(
|
||||||
task: ProgressBarState.empty(),
|
task: ProgressBarState.empty(),
|
||||||
quest: ProgressBarState.empty(),
|
quest: ProgressBarState.empty(),
|
||||||
@@ -334,6 +499,9 @@ class ProgressState {
|
|||||||
currentTask: TaskInfo.empty(),
|
currentTask: TaskInfo.empty(),
|
||||||
plotStageCount: 1, // Prologue
|
plotStageCount: 1, // Prologue
|
||||||
questCount: 0,
|
questCount: 0,
|
||||||
|
plotHistory: const [HistoryEntry(caption: 'Prologue', isComplete: false)],
|
||||||
|
questHistory: const [],
|
||||||
|
currentQuestMonster: null,
|
||||||
);
|
);
|
||||||
|
|
||||||
ProgressState copyWith({
|
ProgressState copyWith({
|
||||||
@@ -345,6 +513,9 @@ class ProgressState {
|
|||||||
TaskInfo? currentTask,
|
TaskInfo? currentTask,
|
||||||
int? plotStageCount,
|
int? plotStageCount,
|
||||||
int? questCount,
|
int? questCount,
|
||||||
|
List<HistoryEntry>? plotHistory,
|
||||||
|
List<HistoryEntry>? questHistory,
|
||||||
|
QuestMonsterInfo? currentQuestMonster,
|
||||||
}) {
|
}) {
|
||||||
return ProgressState(
|
return ProgressState(
|
||||||
task: task ?? this.task,
|
task: task ?? this.task,
|
||||||
@@ -355,6 +526,9 @@ class ProgressState {
|
|||||||
currentTask: currentTask ?? this.currentTask,
|
currentTask: currentTask ?? this.currentTask,
|
||||||
plotStageCount: plotStageCount ?? this.plotStageCount,
|
plotStageCount: plotStageCount ?? this.plotStageCount,
|
||||||
questCount: questCount ?? this.questCount,
|
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,
|
'cha': stats.cha,
|
||||||
'hpMax': stats.hpMax,
|
'hpMax': stats.hpMax,
|
||||||
'mpMax': stats.mpMax,
|
'mpMax': stats.mpMax,
|
||||||
|
'hpCurrent': stats.hpCurrent,
|
||||||
|
'mpCurrent': stats.mpCurrent,
|
||||||
},
|
},
|
||||||
'inventory': {
|
'inventory': {
|
||||||
'gold': inventory.gold,
|
'gold': inventory.gold,
|
||||||
@@ -73,7 +75,15 @@ class GameSave {
|
|||||||
'equipment': {
|
'equipment': {
|
||||||
'weapon': equipment.weapon,
|
'weapon': equipment.weapon,
|
||||||
'shield': equipment.shield,
|
'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,
|
'bestIndex': equipment.bestIndex,
|
||||||
},
|
},
|
||||||
'spells': spellBook.spells
|
'spells': spellBook.spells
|
||||||
@@ -88,9 +98,23 @@ class GameSave {
|
|||||||
'taskInfo': {
|
'taskInfo': {
|
||||||
'caption': progress.currentTask.caption,
|
'caption': progress.currentTask.caption,
|
||||||
'type': progress.currentTask.type.name,
|
'type': progress.currentTask.type.name,
|
||||||
|
'monsterBaseName': progress.currentTask.monsterBaseName,
|
||||||
|
'monsterPart': progress.currentTask.monsterPart,
|
||||||
},
|
},
|
||||||
'plotStages': progress.plotStageCount,
|
'plotStages': progress.plotStageCount,
|
||||||
'questCount': progress.questCount,
|
'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
|
'queue': queue.entries
|
||||||
.map(
|
.map(
|
||||||
@@ -134,6 +158,8 @@ class GameSave {
|
|||||||
cha: statsJson['cha'] as int? ?? 0,
|
cha: statsJson['cha'] as int? ?? 0,
|
||||||
hpMax: statsJson['hpMax'] as int? ?? 0,
|
hpMax: statsJson['hpMax'] as int? ?? 0,
|
||||||
mpMax: statsJson['mpMax'] as int? ?? 0,
|
mpMax: statsJson['mpMax'] as int? ?? 0,
|
||||||
|
hpCurrent: statsJson['hpCurrent'] as int?,
|
||||||
|
mpCurrent: statsJson['mpCurrent'] as int?,
|
||||||
),
|
),
|
||||||
inventory: Inventory(
|
inventory: Inventory(
|
||||||
gold: inventoryJson['gold'] as int? ?? 0,
|
gold: inventoryJson['gold'] as int? ?? 0,
|
||||||
@@ -149,7 +175,15 @@ class GameSave {
|
|||||||
equipment: Equipment(
|
equipment: Equipment(
|
||||||
weapon: equipmentJson['weapon'] as String? ?? 'Sharp Stick',
|
weapon: equipmentJson['weapon'] as String? ?? 'Sharp Stick',
|
||||||
shield: equipmentJson['shield'] as String? ?? '',
|
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,
|
bestIndex: equipmentJson['bestIndex'] as int? ?? 0,
|
||||||
),
|
),
|
||||||
spellBook: SpellBook(
|
spellBook: SpellBook(
|
||||||
@@ -178,6 +212,15 @@ class GameSave {
|
|||||||
),
|
),
|
||||||
plotStageCount: progressJson['plotStages'] as int? ?? 1,
|
plotStageCount: progressJson['plotStages'] as int? ?? 1,
|
||||||
questCount: progressJson['questCount'] as int? ?? 0,
|
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(
|
queue: QueueState(
|
||||||
entries: Queue<QueueEntry>.from(
|
entries: Queue<QueueEntry>.from(
|
||||||
@@ -233,5 +276,29 @@ TaskInfo _taskInfoFromJson(Map<String, dynamic> json) {
|
|||||||
(t) => t.name == typeName,
|
(t) => t.name == typeName,
|
||||||
orElse: () => TaskType.neutral,
|
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;
|
return int.tryParse(parts[1].replaceAll('+', '')) ?? 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
String addModifier(
|
String addModifier(
|
||||||
DeterministicRandom rng,
|
DeterministicRandom rng,
|
||||||
String baseName,
|
String baseName,
|
||||||
@@ -211,19 +210,27 @@ int random64Below(DeterministicRandom rng, int below) {
|
|||||||
return (combined % below).toInt();
|
return (combined % below).toInt();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 장비 생성 (원본 Main.pas:791-830 WinEquip)
|
||||||
|
/// [slotIndex]: 0=Weapon, 1=Shield, 2-10=Armor 계열
|
||||||
String winEquip(
|
String winEquip(
|
||||||
PqConfig config,
|
PqConfig config,
|
||||||
DeterministicRandom rng,
|
DeterministicRandom rng,
|
||||||
int level,
|
int level,
|
||||||
EquipmentSlot slot,
|
int slotIndex,
|
||||||
) {
|
) {
|
||||||
// Decide item set and modifiers based on slot.
|
// 원본 로직:
|
||||||
final bool isWeapon = slot == EquipmentSlot.weapon;
|
// posn = 0: Weapon → K.Weapons, OffenseAttrib
|
||||||
final items = switch (slot) {
|
// posn = 1: Shield → K.Shields, DefenseAttrib
|
||||||
EquipmentSlot.weapon => config.weapons,
|
// posn >= 2: Armor → K.Armors, DefenseAttrib
|
||||||
EquipmentSlot.shield => config.shields,
|
final bool isWeapon = slotIndex == 0;
|
||||||
EquipmentSlot.armor => config.armors,
|
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 better = isWeapon ? config.offenseAttrib : config.defenseAttrib;
|
||||||
final worse = isWeapon ? config.offenseBad : config.defenseBad;
|
final worse = isWeapon ? config.offenseBad : config.defenseBad;
|
||||||
|
|
||||||
@@ -239,21 +246,38 @@ String winEquip(
|
|||||||
return addModifier(rng, baseName, modifiers, plus);
|
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) {
|
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) {
|
if (rng.nextInt(2) == 0) {
|
||||||
// Odds(1,2): 완전 랜덤 선택
|
// Odds(1,2): 모든 스탯 중 완전 랜덤 선택
|
||||||
return rng.nextInt(statValues.length);
|
return rng.nextInt(statValues.length);
|
||||||
}
|
}
|
||||||
// 제곱 가중치로 높은 스탯 선호
|
// 원본: for i := 0 to 5 do Inc(t, Square(GetI(Stats,i)));
|
||||||
final total = statValues.fold<int>(0, (sum, v) => sum + v * v);
|
// 첫 6개(STR, CON, DEX, INT, WIS, CHA)만 제곱 가중치 적용
|
||||||
if (total == 0) return rng.nextInt(statValues.length);
|
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);
|
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];
|
pickValue -= statValues[i] * statValues[i];
|
||||||
if (pickValue < 0) return i;
|
if (pickValue < 0) return i;
|
||||||
}
|
}
|
||||||
return statValues.length - 1;
|
return firstSixCount - 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
Stats winStat(Stats stats, DeterministicRandom rng) {
|
Stats winStat(Stats stats, DeterministicRandom rng) {
|
||||||
@@ -290,7 +314,7 @@ Stats winStat(Stats stats, DeterministicRandom rng) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String monsterTask(
|
MonsterTaskResult monsterTask(
|
||||||
PqConfig config,
|
PqConfig config,
|
||||||
DeterministicRandom rng,
|
DeterministicRandom rng,
|
||||||
int level,
|
int level,
|
||||||
@@ -308,6 +332,7 @@ String monsterTask(
|
|||||||
|
|
||||||
String monster;
|
String monster;
|
||||||
int monsterLevel;
|
int monsterLevel;
|
||||||
|
String part;
|
||||||
bool definite = false;
|
bool definite = false;
|
||||||
|
|
||||||
// 원본 Main.pas:537-547: 가끔 NPC를 몬스터로 사용
|
// 원본 Main.pas:537-547: 가끔 NPC를 몬스터로 사용
|
||||||
@@ -324,11 +349,13 @@ String monsterTask(
|
|||||||
definite = true;
|
definite = true;
|
||||||
}
|
}
|
||||||
monsterLevel = targetLevel;
|
monsterLevel = targetLevel;
|
||||||
monster = '$monster|$monsterLevel|*';
|
part = '*'; // NPC는 WinItem 호출
|
||||||
} else if (questMonster != null && rng.nextInt(4) == 0) {
|
} else if (questMonster != null && rng.nextInt(4) == 0) {
|
||||||
// Use quest monster.
|
// Use quest monster.
|
||||||
monster = questMonster;
|
monster = questMonster;
|
||||||
|
final parts = questMonster.split('|');
|
||||||
monsterLevel = questLevel ?? targetLevel;
|
monsterLevel = questLevel ?? targetLevel;
|
||||||
|
part = parts.length > 2 ? parts[2] : '';
|
||||||
} else {
|
} else {
|
||||||
// Pick closest level among random samples.
|
// Pick closest level among random samples.
|
||||||
monster = pick(config.monsters, rng);
|
monster = pick(config.monsters, rng);
|
||||||
@@ -342,12 +369,18 @@ String monsterTask(
|
|||||||
monsterLevel = candLevel;
|
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.
|
// Adjust quantity and adjectives based on level delta.
|
||||||
var qty = 1;
|
var qty = 1;
|
||||||
final levelDiff = targetLevel - monsterLevel;
|
final levelDiff = targetLevel - monsterLevel;
|
||||||
var name = monster.split('|').first;
|
var name = baseName;
|
||||||
|
|
||||||
if (levelDiff > 10) {
|
if (levelDiff > 10) {
|
||||||
qty =
|
qty =
|
||||||
@@ -385,7 +418,34 @@ String monsterTask(
|
|||||||
name = indefinite(name, qty);
|
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 }
|
enum RewardKind { spell, equip, stat, item }
|
||||||
@@ -396,12 +456,17 @@ class QuestResult {
|
|||||||
required this.reward,
|
required this.reward,
|
||||||
this.monsterName,
|
this.monsterName,
|
||||||
this.monsterLevel,
|
this.monsterLevel,
|
||||||
|
this.monsterIndex,
|
||||||
});
|
});
|
||||||
|
|
||||||
final String caption;
|
final String caption;
|
||||||
final RewardKind reward;
|
final RewardKind reward;
|
||||||
|
/// 몬스터 전체 데이터 (예: "Rat|5|tail") - 원본 fQuest.Caption
|
||||||
final String? monsterName;
|
final String? monsterName;
|
||||||
|
/// 몬스터 레벨 (파싱된 값)
|
||||||
final int? monsterLevel;
|
final int? monsterLevel;
|
||||||
|
/// 몬스터 인덱스 (config.monsters에서의 위치) - 원본 fQuest.Tag
|
||||||
|
final int? monsterIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
QuestResult completeQuest(PqConfig config, DeterministicRandom rng, int level) {
|
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);
|
final questRoll = rng.nextInt(5);
|
||||||
switch (questRoll) {
|
switch (questRoll) {
|
||||||
case 0:
|
case 0:
|
||||||
|
// Exterminate: 4번 시도하여 레벨에 가장 가까운 몬스터 선택
|
||||||
|
// 원본 Main.pas:936-954
|
||||||
var best = '';
|
var best = '';
|
||||||
var bestLevel = 0;
|
var bestLevel = 0;
|
||||||
|
var bestIndex = 0;
|
||||||
for (var i = 0; i < 4; i++) {
|
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);
|
final l = _monsterLevel(m);
|
||||||
if (i == 0 || (l - level).abs() < (bestLevel - level).abs()) {
|
if (i == 0 || (l - level).abs() < (bestLevel - level).abs()) {
|
||||||
best = m;
|
best = m;
|
||||||
bestLevel = l;
|
bestLevel = l;
|
||||||
|
bestIndex = monsterIndex;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
final name = best.split('|').first;
|
final name = best.split('|').first;
|
||||||
@@ -432,6 +502,7 @@ QuestResult completeQuest(PqConfig config, DeterministicRandom rng, int level) {
|
|||||||
reward: reward,
|
reward: reward,
|
||||||
monsterName: best,
|
monsterName: best,
|
||||||
monsterLevel: bestLevel,
|
monsterLevel: bestLevel,
|
||||||
|
monsterIndex: bestIndex,
|
||||||
);
|
);
|
||||||
case 1:
|
case 1:
|
||||||
final item = interestingItem(config, rng);
|
final item = interestingItem(config, rng);
|
||||||
@@ -446,6 +517,8 @@ QuestResult completeQuest(PqConfig config, DeterministicRandom rng, int level) {
|
|||||||
reward: reward,
|
reward: reward,
|
||||||
);
|
);
|
||||||
default:
|
default:
|
||||||
|
// Placate: 2번 시도하여 레벨에 가장 가까운 몬스터 선택
|
||||||
|
// 원본 Main.pas:971-984 (fQuest.Caption := '' 처리됨)
|
||||||
var best = '';
|
var best = '';
|
||||||
var bestLevel = 0;
|
var bestLevel = 0;
|
||||||
for (var i = 0; i < 2; i++) {
|
for (var i = 0; i < 2; i++) {
|
||||||
@@ -457,11 +530,10 @@ QuestResult completeQuest(PqConfig config, DeterministicRandom rng, int level) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
final name = best.split('|').first;
|
final name = best.split('|').first;
|
||||||
|
// Placate는 fQuest.Caption := '' 로 비움 → monsterIndex 미저장
|
||||||
return QuestResult(
|
return QuestResult(
|
||||||
caption: 'Placate ${definite(name, 2)}',
|
caption: 'Placate ${definite(name, 2)}',
|
||||||
reward: reward,
|
reward: reward,
|
||||||
monsterName: best,
|
|
||||||
monsterLevel: bestLevel,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -301,7 +301,9 @@ class _GamePlayScreenState extends State<GamePlayScreen>
|
|||||||
child: Text(
|
child: Text(
|
||||||
'${speed}x',
|
'${speed}x',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontWeight: speed > 1 ? FontWeight.bold : FontWeight.normal,
|
fontWeight: speed > 1
|
||||||
|
? FontWeight.bold
|
||||||
|
: FontWeight.normal,
|
||||||
color: speed > 1
|
color: speed > 1
|
||||||
? Theme.of(context).colorScheme.primary
|
? Theme.of(context).colorScheme.primary
|
||||||
: null,
|
: null,
|
||||||
@@ -468,11 +470,19 @@ class _GamePlayScreenState extends State<GamePlayScreen>
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildEquipmentList(GameState state) {
|
Widget _buildEquipmentList(GameState state) {
|
||||||
// 원본에는 11개 슬롯이 있지만, 현재 모델은 3개만 구현
|
// 원본 Main.dfm Equips ListView - 11개 슬롯
|
||||||
final equipment = [
|
final equipment = [
|
||||||
('Weapon', state.equipment.weapon),
|
('Weapon', state.equipment.weapon),
|
||||||
('Shield', state.equipment.shield),
|
('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(
|
return ListView.builder(
|
||||||
|
|||||||
@@ -38,7 +38,8 @@ class _NewCharacterScreenState extends State<NewCharacterScreen> {
|
|||||||
int _wis = 0;
|
int _wis = 0;
|
||||||
int _cha = 0;
|
int _cha = 0;
|
||||||
|
|
||||||
// 롤 이력 (Unroll 기능용)
|
// 롤 이력 (Unroll 기능용) - 원본 OldRolls TListBox
|
||||||
|
static const int _maxRollHistory = 20; // 최대 저장 개수
|
||||||
final List<int> _rollHistory = [];
|
final List<int> _rollHistory = [];
|
||||||
|
|
||||||
// 현재 RNG 시드 (Re-Roll 전 저장)
|
// 현재 RNG 시드 (Re-Roll 전 저장)
|
||||||
@@ -93,6 +94,11 @@ class _NewCharacterScreenState extends State<NewCharacterScreen> {
|
|||||||
// 현재 시드를 이력에 저장
|
// 현재 시드를 이력에 저장
|
||||||
_rollHistory.insert(0, _currentSeed);
|
_rollHistory.insert(0, _currentSeed);
|
||||||
|
|
||||||
|
// 최대 개수 초과 시 가장 오래된 항목 제거
|
||||||
|
if (_rollHistory.length > _maxRollHistory) {
|
||||||
|
_rollHistory.removeLast();
|
||||||
|
}
|
||||||
|
|
||||||
// 새 시드로 굴림
|
// 새 시드로 굴림
|
||||||
_currentSeed = math.Random().nextInt(0x7FFFFFFF);
|
_currentSeed = math.Random().nextInt(0x7FFFFFFF);
|
||||||
_rollStats();
|
_rollStats();
|
||||||
@@ -132,6 +138,7 @@ class _NewCharacterScreenState extends State<NewCharacterScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Sold! 버튼 클릭 - 캐릭터 생성 완료
|
/// Sold! 버튼 클릭 - 캐릭터 생성 완료
|
||||||
|
/// 원본 Main.pas:1371-1388 RollCharacter 로직
|
||||||
void _onSold() {
|
void _onSold() {
|
||||||
final name = _nameController.text.trim();
|
final name = _nameController.text.trim();
|
||||||
if (name.isEmpty) {
|
if (name.isEmpty) {
|
||||||
@@ -144,22 +151,23 @@ class _NewCharacterScreenState extends State<NewCharacterScreen> {
|
|||||||
// 게임에 사용할 새 RNG 생성
|
// 게임에 사용할 새 RNG 생성
|
||||||
final gameSeed = math.Random().nextInt(0x7FFFFFFF);
|
final gameSeed = math.Random().nextInt(0x7FFFFFFF);
|
||||||
|
|
||||||
// 종족/직업의 보너스 스탯 파싱
|
// 원본 Main.pas:1380-1381 - 기본 롤 값(CON.Tag, INT.Tag)만 사용
|
||||||
final raceEntry = _config.races[_selectedRaceIndex];
|
// 종족/직업 보너스는 스탯에 적용되지 않음 (UI 힌트용)
|
||||||
final klassEntry = _config.klasses[_selectedKlassIndex];
|
// Put(Stats,'HP Max',Random(8) + CON.Tag div 6);
|
||||||
final raceBonus = _parseStatBonus(raceEntry);
|
// Put(Stats,'MP Max',Random(8) + INT.Tag div 6);
|
||||||
final klassBonus = _parseStatBonus(klassEntry);
|
final hpMax = math.Random().nextInt(8) + _con ~/ 6;
|
||||||
|
final mpMax = math.Random().nextInt(8) + _int ~/ 6;
|
||||||
|
|
||||||
// 최종 스탯 계산 (기본 + 종족 보너스 + 직업 보너스)
|
// 원본 Main.pas:1375-1379 - 기본 롤 값 그대로 저장 (보너스 없음)
|
||||||
final finalStats = Stats(
|
final finalStats = Stats(
|
||||||
str: _str + (raceBonus['STR'] ?? 0) + (klassBonus['STR'] ?? 0),
|
str: _str,
|
||||||
con: _con + (raceBonus['CON'] ?? 0) + (klassBonus['CON'] ?? 0),
|
con: _con,
|
||||||
dex: _dex + (raceBonus['DEX'] ?? 0) + (klassBonus['DEX'] ?? 0),
|
dex: _dex,
|
||||||
intelligence: _int + (raceBonus['INT'] ?? 0) + (klassBonus['INT'] ?? 0),
|
intelligence: _int,
|
||||||
wis: _wis + (raceBonus['WIS'] ?? 0) + (klassBonus['WIS'] ?? 0),
|
wis: _wis,
|
||||||
cha: _cha + (raceBonus['CHA'] ?? 0) + (klassBonus['CHA'] ?? 0),
|
cha: _cha,
|
||||||
hpMax: _con + (raceBonus['CON'] ?? 0) + (klassBonus['CON'] ?? 0),
|
hpMax: hpMax,
|
||||||
mpMax: _int + (raceBonus['INT'] ?? 0) + (klassBonus['INT'] ?? 0),
|
mpMax: mpMax,
|
||||||
);
|
);
|
||||||
|
|
||||||
final traits = Traits(
|
final traits = Traits(
|
||||||
@@ -186,24 +194,6 @@ class _NewCharacterScreenState extends State<NewCharacterScreen> {
|
|||||||
widget.onCharacterCreated?.call(initialState);
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
|
|||||||
@@ -35,9 +35,7 @@ void main() {
|
|||||||
),
|
),
|
||||||
inventory: const Inventory(
|
inventory: const Inventory(
|
||||||
gold: 5,
|
gold: 5,
|
||||||
items: [
|
items: [InventoryEntry(name: 'Rock', count: 3)],
|
||||||
InventoryEntry(name: 'Rock', count: 3),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
progress: const ProgressState(
|
progress: const ProgressState(
|
||||||
task: ProgressBarState(position: 0, max: 80),
|
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/game_state.dart';
|
||||||
import 'package:askiineverdie/src/core/model/pq_config.dart';
|
import 'package:askiineverdie/src/core/model/pq_config.dart';
|
||||||
import 'package:askiineverdie/src/core/util/deterministic_random.dart';
|
import 'package:askiineverdie/src/core/util/deterministic_random.dart';
|
||||||
@@ -65,7 +64,7 @@ void main() {
|
|||||||
config,
|
config,
|
||||||
DeterministicRandom(12),
|
DeterministicRandom(12),
|
||||||
5,
|
5,
|
||||||
EquipmentSlot.weapon,
|
0, // weapon slot
|
||||||
),
|
),
|
||||||
'Baselard',
|
'Baselard',
|
||||||
);
|
);
|
||||||
@@ -74,7 +73,7 @@ void main() {
|
|||||||
config,
|
config,
|
||||||
DeterministicRandom(15),
|
DeterministicRandom(15),
|
||||||
2,
|
2,
|
||||||
EquipmentSlot.armor,
|
2, // helm slot (armor category)
|
||||||
),
|
),
|
||||||
'-2 Canvas',
|
'-2 Canvas',
|
||||||
);
|
);
|
||||||
@@ -86,18 +85,35 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('monsterTask picks level-appropriate monsters with modifiers', () {
|
test('monsterTask picks level-appropriate monsters with modifiers', () {
|
||||||
expect(
|
final result1 = pq_logic.monsterTask(
|
||||||
pq_logic.monsterTask(config, DeterministicRandom(99), 5, null, null),
|
config,
|
||||||
'an underage Rakshasa',
|
DeterministicRandom(99),
|
||||||
|
5,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
);
|
);
|
||||||
expect(
|
expect(result1.displayName, 'an underage Rakshasa');
|
||||||
pq_logic.monsterTask(config, DeterministicRandom(7), 10, null, null),
|
expect(result1.baseName, 'Rakshasa');
|
||||||
'a greater Sphinx',
|
expect(result1.part, isNotEmpty);
|
||||||
|
|
||||||
|
final result2 = pq_logic.monsterTask(
|
||||||
|
config,
|
||||||
|
DeterministicRandom(7),
|
||||||
|
10,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
);
|
);
|
||||||
expect(
|
expect(result2.displayName, 'a greater Sphinx');
|
||||||
pq_logic.monsterTask(config, DeterministicRandom(5), 6, 'Goblin|3', 3),
|
expect(result2.baseName, 'Sphinx');
|
||||||
'a Barbed Devil',
|
|
||||||
|
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', () {
|
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/game_mutations.dart';
|
||||||
import 'package:askiineverdie/src/core/engine/progress_service.dart';
|
import 'package:askiineverdie/src/core/engine/progress_service.dart';
|
||||||
import 'package:askiineverdie/src/core/engine/reward_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/game_state.dart';
|
||||||
import 'package:askiineverdie/src/core/model/pq_config.dart';
|
import 'package:askiineverdie/src/core/model/pq_config.dart';
|
||||||
import 'package:askiineverdie/src/core/util/deterministic_random.dart';
|
import 'package:askiineverdie/src/core/util/deterministic_random.dart';
|
||||||
@@ -41,71 +40,59 @@ void main() {
|
|||||||
test('monsterTask produces consistent monster names', () {
|
test('monsterTask produces consistent monster names', () {
|
||||||
// 시드 42, 레벨 5에서의 몬스터 이름
|
// 시드 42, 레벨 5에서의 몬스터 이름
|
||||||
expect(
|
expect(
|
||||||
pq_logic.monsterTask(
|
pq_logic
|
||||||
config,
|
.monsterTask(config, DeterministicRandom(testSeed), 5, null, null)
|
||||||
DeterministicRandom(testSeed),
|
.displayName,
|
||||||
5,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
),
|
|
||||||
'an underage Su-monster',
|
'an underage Su-monster',
|
||||||
);
|
);
|
||||||
|
|
||||||
// 시드 42, 레벨 10에서의 몬스터 이름
|
// 시드 42, 레벨 10에서의 몬스터 이름
|
||||||
expect(
|
expect(
|
||||||
pq_logic.monsterTask(
|
pq_logic
|
||||||
config,
|
.monsterTask(config, DeterministicRandom(testSeed), 10, null, null)
|
||||||
DeterministicRandom(testSeed),
|
.displayName,
|
||||||
10,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
),
|
|
||||||
'a cursed Troll',
|
'a cursed Troll',
|
||||||
);
|
);
|
||||||
|
|
||||||
// 시드 42, 레벨 1에서의 몬스터 이름
|
// 시드 42, 레벨 1에서의 몬스터 이름
|
||||||
expect(
|
expect(
|
||||||
pq_logic.monsterTask(
|
pq_logic
|
||||||
config,
|
.monsterTask(config, DeterministicRandom(testSeed), 1, null, null)
|
||||||
DeterministicRandom(testSeed),
|
.displayName,
|
||||||
1,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
),
|
|
||||||
'a greater Crayfish',
|
'a greater Crayfish',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('winEquip produces consistent equipment', () {
|
test('winEquip produces consistent equipment', () {
|
||||||
// 시드 42에서 무기 획득
|
// 시드 42에서 무기 획득 (슬롯 0)
|
||||||
expect(
|
expect(
|
||||||
pq_logic.winEquip(
|
pq_logic.winEquip(
|
||||||
config,
|
config,
|
||||||
DeterministicRandom(testSeed),
|
DeterministicRandom(testSeed),
|
||||||
5,
|
5,
|
||||||
EquipmentSlot.weapon,
|
0, // weapon slot
|
||||||
),
|
),
|
||||||
'Longiron',
|
'Longiron',
|
||||||
);
|
);
|
||||||
|
|
||||||
// 시드 42에서 방어구 획득
|
// 시드 42에서 방어구 획득 (슬롯 2 = helm, armor 카테고리)
|
||||||
expect(
|
expect(
|
||||||
pq_logic.winEquip(
|
pq_logic.winEquip(
|
||||||
config,
|
config,
|
||||||
DeterministicRandom(testSeed),
|
DeterministicRandom(testSeed),
|
||||||
5,
|
5,
|
||||||
EquipmentSlot.armor,
|
2, // helm slot (armor category)
|
||||||
),
|
),
|
||||||
'-1 Holey Mildewed Bearskin',
|
'-1 Holey Mildewed Bearskin',
|
||||||
);
|
);
|
||||||
|
|
||||||
// 시드 42에서 방패 획득
|
// 시드 42에서 방패 획득 (슬롯 1)
|
||||||
expect(
|
expect(
|
||||||
pq_logic.winEquip(
|
pq_logic.winEquip(
|
||||||
config,
|
config,
|
||||||
DeterministicRandom(testSeed),
|
DeterministicRandom(testSeed),
|
||||||
5,
|
5,
|
||||||
EquipmentSlot.shield,
|
1, // shield slot
|
||||||
),
|
),
|
||||||
'Round Shield',
|
'Round Shield',
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user