- CombatStats에 toJson/fromJson 직렬화 메서드 추가 - HallOfFameEntry에 finalStats(CombatStats) 필드 추가 - 명예의 전당 상세 다이얼로그에서 전투 스탯, 장비, 스펠 표시 - GameState에 combatStats 접근자 추가 - game_text_l10n에 명예의 전당 관련 텍스트 추가
853 lines
24 KiB
Dart
853 lines
24 KiB
Dart
import 'dart:collection';
|
|
|
|
import 'package:askiineverdie/src/core/model/combat_event.dart';
|
|
import 'package:askiineverdie/src/core/model/combat_state.dart';
|
|
import 'package:askiineverdie/src/core/model/equipment_item.dart';
|
|
import 'package:askiineverdie/src/core/model/equipment_slot.dart';
|
|
import 'package:askiineverdie/src/core/model/item_stats.dart';
|
|
import 'package:askiineverdie/src/core/model/potion.dart';
|
|
import 'package:askiineverdie/src/core/model/skill.dart';
|
|
import 'package:askiineverdie/src/core/util/deterministic_random.dart';
|
|
|
|
/// Minimal skeletal state to mirror Progress Quest structures.
|
|
///
|
|
/// Logic will be ported faithfully from the Delphi source; this file only
|
|
/// defines containers and helpers for deterministic RNG.
|
|
class GameState {
|
|
GameState({
|
|
required DeterministicRandom rng,
|
|
Traits? traits,
|
|
Stats? stats,
|
|
Inventory? inventory,
|
|
Equipment? equipment,
|
|
SpellBook? spellBook,
|
|
ProgressState? progress,
|
|
QueueState? queue,
|
|
SkillSystemState? skillSystem,
|
|
PotionInventory? potionInventory,
|
|
this.deathInfo,
|
|
}) : rng = DeterministicRandom.clone(rng),
|
|
traits = traits ?? Traits.empty(),
|
|
stats = stats ?? Stats.empty(),
|
|
inventory = inventory ?? Inventory.empty(),
|
|
equipment = equipment ?? Equipment.empty(),
|
|
spellBook = spellBook ?? SpellBook.empty(),
|
|
progress = progress ?? ProgressState.empty(),
|
|
queue = queue ?? QueueState.empty(),
|
|
skillSystem = skillSystem ?? SkillSystemState.empty(),
|
|
potionInventory = potionInventory ?? const PotionInventory();
|
|
|
|
factory GameState.withSeed({
|
|
required int seed,
|
|
Traits? traits,
|
|
Stats? stats,
|
|
Inventory? inventory,
|
|
Equipment? equipment,
|
|
SpellBook? spellBook,
|
|
ProgressState? progress,
|
|
QueueState? queue,
|
|
SkillSystemState? skillSystem,
|
|
PotionInventory? potionInventory,
|
|
DeathInfo? deathInfo,
|
|
}) {
|
|
return GameState(
|
|
rng: DeterministicRandom(seed),
|
|
traits: traits,
|
|
stats: stats,
|
|
inventory: inventory,
|
|
equipment: equipment,
|
|
spellBook: spellBook,
|
|
progress: progress,
|
|
queue: queue,
|
|
skillSystem: skillSystem,
|
|
potionInventory: potionInventory,
|
|
deathInfo: deathInfo,
|
|
);
|
|
}
|
|
|
|
final DeterministicRandom rng;
|
|
final Traits traits;
|
|
final Stats stats;
|
|
final Inventory inventory;
|
|
final Equipment equipment;
|
|
final SpellBook spellBook;
|
|
final ProgressState progress;
|
|
final QueueState queue;
|
|
|
|
/// 스킬 시스템 상태 (Phase 3)
|
|
final SkillSystemState skillSystem;
|
|
|
|
/// 물약 인벤토리
|
|
final PotionInventory potionInventory;
|
|
|
|
/// 사망 정보 (Phase 4, null이면 생존 중)
|
|
final DeathInfo? deathInfo;
|
|
|
|
/// 사망 여부
|
|
bool get isDead => deathInfo != null;
|
|
|
|
GameState copyWith({
|
|
DeterministicRandom? rng,
|
|
Traits? traits,
|
|
Stats? stats,
|
|
Inventory? inventory,
|
|
Equipment? equipment,
|
|
SpellBook? spellBook,
|
|
ProgressState? progress,
|
|
QueueState? queue,
|
|
SkillSystemState? skillSystem,
|
|
PotionInventory? potionInventory,
|
|
DeathInfo? deathInfo,
|
|
bool clearDeathInfo = false,
|
|
}) {
|
|
return GameState(
|
|
rng: rng ?? DeterministicRandom.clone(this.rng),
|
|
traits: traits ?? this.traits,
|
|
stats: stats ?? this.stats,
|
|
inventory: inventory ?? this.inventory,
|
|
equipment: equipment ?? this.equipment,
|
|
spellBook: spellBook ?? this.spellBook,
|
|
progress: progress ?? this.progress,
|
|
queue: queue ?? this.queue,
|
|
skillSystem: skillSystem ?? this.skillSystem,
|
|
potionInventory: potionInventory ?? this.potionInventory,
|
|
deathInfo: clearDeathInfo ? null : (deathInfo ?? this.deathInfo),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// 사망 정보 (Phase 4)
|
|
///
|
|
/// 사망 시점의 정보와 상실한 아이템을 기록
|
|
class DeathInfo {
|
|
const DeathInfo({
|
|
required this.cause,
|
|
required this.killerName,
|
|
required this.lostEquipmentCount,
|
|
required this.goldAtDeath,
|
|
required this.levelAtDeath,
|
|
required this.timestamp,
|
|
this.lostItemName,
|
|
this.lastCombatEvents = const [],
|
|
});
|
|
|
|
/// 사망 원인
|
|
final DeathCause cause;
|
|
|
|
/// 사망시킨 몬스터/원인 이름
|
|
final String killerName;
|
|
|
|
/// 상실한 장비 개수 (0 또는 1)
|
|
final int lostEquipmentCount;
|
|
|
|
/// 제물로 바친 아이템 이름 (null이면 없음)
|
|
final String? lostItemName;
|
|
|
|
/// 사망 시점 골드
|
|
final int goldAtDeath;
|
|
|
|
/// 사망 시점 레벨
|
|
final int levelAtDeath;
|
|
|
|
/// 사망 시각 (밀리초)
|
|
final int timestamp;
|
|
|
|
/// 사망 직전 전투 이벤트 (최대 10개)
|
|
final List<CombatEvent> lastCombatEvents;
|
|
|
|
DeathInfo copyWith({
|
|
DeathCause? cause,
|
|
String? killerName,
|
|
int? lostEquipmentCount,
|
|
String? lostItemName,
|
|
int? goldAtDeath,
|
|
int? levelAtDeath,
|
|
int? timestamp,
|
|
List<CombatEvent>? lastCombatEvents,
|
|
}) {
|
|
return DeathInfo(
|
|
cause: cause ?? this.cause,
|
|
killerName: killerName ?? this.killerName,
|
|
lostEquipmentCount: lostEquipmentCount ?? this.lostEquipmentCount,
|
|
lostItemName: lostItemName ?? this.lostItemName,
|
|
goldAtDeath: goldAtDeath ?? this.goldAtDeath,
|
|
levelAtDeath: levelAtDeath ?? this.levelAtDeath,
|
|
timestamp: timestamp ?? this.timestamp,
|
|
lastCombatEvents: lastCombatEvents ?? this.lastCombatEvents,
|
|
);
|
|
}
|
|
}
|
|
|
|
/// 사망 원인
|
|
enum DeathCause {
|
|
/// 몬스터에 의한 사망
|
|
monster,
|
|
|
|
/// 자해 스킬에 의한 사망
|
|
selfDamage,
|
|
|
|
/// 환경 피해에 의한 사망
|
|
environment,
|
|
}
|
|
|
|
/// 스킬 시스템 상태 (Phase 3)
|
|
///
|
|
/// 스킬 쿨타임, 활성 버프, 게임 경과 시간 등을 관리
|
|
class SkillSystemState {
|
|
const SkillSystemState({
|
|
required this.skillStates,
|
|
required this.activeBuffs,
|
|
required this.elapsedMs,
|
|
});
|
|
|
|
/// 스킬별 쿨타임 상태
|
|
final List<SkillState> skillStates;
|
|
|
|
/// 현재 활성화된 버프 목록
|
|
final List<ActiveBuff> activeBuffs;
|
|
|
|
/// 게임 진행 경과 시간 (밀리초, 스킬 쿨타임 계산용)
|
|
final int elapsedMs;
|
|
|
|
factory SkillSystemState.empty() =>
|
|
const SkillSystemState(skillStates: [], activeBuffs: [], elapsedMs: 0);
|
|
|
|
/// 특정 스킬 상태 가져오기
|
|
SkillState? getSkillState(String skillId) {
|
|
for (final state in skillStates) {
|
|
if (state.skillId == skillId) return state;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/// 버프 효과 합산 (동일 버프는 중복 적용 안 됨)
|
|
({double atkMod, double defMod, double criMod, double evasionMod})
|
|
get totalBuffModifiers {
|
|
double atkMod = 0;
|
|
double defMod = 0;
|
|
double criMod = 0;
|
|
double evasionMod = 0;
|
|
|
|
final seenBuffIds = <String>{};
|
|
for (final buff in activeBuffs) {
|
|
if (seenBuffIds.contains(buff.effect.id)) continue;
|
|
seenBuffIds.add(buff.effect.id);
|
|
|
|
if (!buff.isExpired(elapsedMs)) {
|
|
atkMod += buff.effect.atkModifier;
|
|
defMod += buff.effect.defModifier;
|
|
criMod += buff.effect.criRateModifier;
|
|
evasionMod += buff.effect.evasionModifier;
|
|
}
|
|
}
|
|
|
|
return (
|
|
atkMod: atkMod,
|
|
defMod: defMod,
|
|
criMod: criMod,
|
|
evasionMod: evasionMod,
|
|
);
|
|
}
|
|
|
|
SkillSystemState copyWith({
|
|
List<SkillState>? skillStates,
|
|
List<ActiveBuff>? activeBuffs,
|
|
int? elapsedMs,
|
|
}) {
|
|
return SkillSystemState(
|
|
skillStates: skillStates ?? this.skillStates,
|
|
activeBuffs: activeBuffs ?? this.activeBuffs,
|
|
elapsedMs: elapsedMs ?? this.elapsedMs,
|
|
);
|
|
}
|
|
}
|
|
|
|
/// 태스크 타입 (원본 fTask.Caption 값들에 대응)
|
|
enum TaskType {
|
|
neutral, // heading 등 일반 이동
|
|
kill, // 몬스터 처치
|
|
load, // 로딩/초기화
|
|
plot, // 플롯 진행
|
|
market, // 시장으로 이동 중
|
|
sell, // 아이템 판매 중
|
|
buying, // 장비 구매 중
|
|
}
|
|
|
|
class TaskInfo {
|
|
const TaskInfo({
|
|
required this.caption,
|
|
required this.type,
|
|
this.monsterBaseName,
|
|
this.monsterPart,
|
|
this.monsterLevel,
|
|
});
|
|
|
|
final String caption;
|
|
final TaskType type;
|
|
|
|
/// 킬 태스크의 몬스터 기본 이름 (형용사 제외, 예: "Goblin")
|
|
final String? monsterBaseName;
|
|
|
|
/// 킬 태스크의 전리품 부위 (예: "claw", "tail", "*"는 WinItem)
|
|
final String? monsterPart;
|
|
|
|
/// 킬 태스크의 몬스터 레벨 (애니메이션 크기 결정용)
|
|
final int? monsterLevel;
|
|
|
|
factory TaskInfo.empty() =>
|
|
const TaskInfo(caption: '', type: TaskType.neutral);
|
|
|
|
TaskInfo copyWith({
|
|
String? caption,
|
|
TaskType? type,
|
|
String? monsterBaseName,
|
|
String? monsterPart,
|
|
int? monsterLevel,
|
|
}) {
|
|
return TaskInfo(
|
|
caption: caption ?? this.caption,
|
|
type: type ?? this.type,
|
|
monsterBaseName: monsterBaseName ?? this.monsterBaseName,
|
|
monsterPart: monsterPart ?? this.monsterPart,
|
|
monsterLevel: monsterLevel ?? this.monsterLevel,
|
|
);
|
|
}
|
|
}
|
|
|
|
class Traits {
|
|
const Traits({
|
|
required this.name,
|
|
required this.race,
|
|
required this.klass,
|
|
required this.level,
|
|
required this.motto,
|
|
required this.guild,
|
|
this.raceId = '',
|
|
this.classId = '',
|
|
});
|
|
|
|
final String name;
|
|
|
|
/// 종족 표시 이름 (예: "Kernel Giant")
|
|
final String race;
|
|
|
|
/// 클래스 표시 이름 (예: "Bug Hunter")
|
|
final String klass;
|
|
|
|
final int level;
|
|
final String motto;
|
|
final String guild;
|
|
|
|
/// 종족 ID (Phase 5, 예: "kernel_giant")
|
|
final String raceId;
|
|
|
|
/// 클래스 ID (Phase 5, 예: "bug_hunter")
|
|
final String classId;
|
|
|
|
factory Traits.empty() => const Traits(
|
|
name: '',
|
|
race: '',
|
|
klass: '',
|
|
level: 1,
|
|
motto: '',
|
|
guild: '',
|
|
raceId: '',
|
|
classId: '',
|
|
);
|
|
|
|
Traits copyWith({
|
|
String? name,
|
|
String? race,
|
|
String? klass,
|
|
int? level,
|
|
String? motto,
|
|
String? guild,
|
|
String? raceId,
|
|
String? classId,
|
|
}) {
|
|
return Traits(
|
|
name: name ?? this.name,
|
|
race: race ?? this.race,
|
|
klass: klass ?? this.klass,
|
|
level: level ?? this.level,
|
|
motto: motto ?? this.motto,
|
|
guild: guild ?? this.guild,
|
|
raceId: raceId ?? this.raceId,
|
|
classId: classId ?? this.classId,
|
|
);
|
|
}
|
|
}
|
|
|
|
class Stats {
|
|
const Stats({
|
|
required this.str,
|
|
required this.con,
|
|
required this.dex,
|
|
required this.intelligence,
|
|
required this.wis,
|
|
required this.cha,
|
|
required this.hpMax,
|
|
required this.mpMax,
|
|
this.hpCurrent,
|
|
this.mpCurrent,
|
|
});
|
|
|
|
final int str;
|
|
final int con;
|
|
final int dex;
|
|
final int intelligence;
|
|
final int wis;
|
|
final int cha;
|
|
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,
|
|
dex: 0,
|
|
intelligence: 0,
|
|
wis: 0,
|
|
cha: 0,
|
|
hpMax: 0,
|
|
mpMax: 0,
|
|
);
|
|
|
|
Stats copyWith({
|
|
int? str,
|
|
int? con,
|
|
int? dex,
|
|
int? intelligence,
|
|
int? wis,
|
|
int? cha,
|
|
int? hpMax,
|
|
int? mpMax,
|
|
int? hpCurrent,
|
|
int? mpCurrent,
|
|
}) {
|
|
return Stats(
|
|
str: str ?? this.str,
|
|
con: con ?? this.con,
|
|
dex: dex ?? this.dex,
|
|
intelligence: intelligence ?? this.intelligence,
|
|
wis: wis ?? this.wis,
|
|
cha: cha ?? this.cha,
|
|
hpMax: hpMax ?? this.hpMax,
|
|
mpMax: mpMax ?? this.mpMax,
|
|
hpCurrent: hpCurrent ?? this.hpCurrent,
|
|
mpCurrent: mpCurrent ?? this.mpCurrent,
|
|
);
|
|
}
|
|
}
|
|
|
|
class InventoryEntry {
|
|
const InventoryEntry({required this.name, required this.count});
|
|
|
|
final String name;
|
|
final int count;
|
|
|
|
InventoryEntry copyWith({String? name, int? count}) {
|
|
return InventoryEntry(name: name ?? this.name, count: count ?? this.count);
|
|
}
|
|
}
|
|
|
|
class Inventory {
|
|
const Inventory({required this.gold, required this.items});
|
|
|
|
final int gold;
|
|
final List<InventoryEntry> items;
|
|
|
|
factory Inventory.empty() => const Inventory(gold: 0, items: []);
|
|
|
|
Inventory copyWith({int? gold, List<InventoryEntry>? items}) {
|
|
return Inventory(gold: gold ?? this.gold, items: items ?? this.items);
|
|
}
|
|
}
|
|
|
|
/// 장비 (원본 Main.dfm Equips ListView, 11개 슬롯)
|
|
///
|
|
/// Phase 2에서 EquipmentItem 기반으로 확장됨.
|
|
/// 기존 문자열 API(weapon, shield 등)는 호환성을 위해 유지.
|
|
class Equipment {
|
|
Equipment({required this.items, required this.bestIndex})
|
|
: assert(items.length == slotCount, 'Equipment must have $slotCount items');
|
|
|
|
/// 장비 아이템 목록 (11개 슬롯)
|
|
final List<EquipmentItem> items;
|
|
|
|
/// 최고 아이템 슬롯 인덱스 (원본 Equips.Tag, 0-10)
|
|
final int bestIndex;
|
|
|
|
/// 슬롯 개수
|
|
static const slotCount = 11;
|
|
|
|
// ============================================================================
|
|
// 문자열 API (기존 코드 호환성)
|
|
// ============================================================================
|
|
|
|
String get weapon => items[0].name; // 0: 무기
|
|
String get shield => items[1].name; // 1: 방패
|
|
String get helm => items[2].name; // 2: 투구
|
|
String get hauberk => items[3].name; // 3: 사슬갑옷
|
|
String get brassairts => items[4].name; // 4: 상완갑
|
|
String get vambraces => items[5].name; // 5: 전완갑
|
|
String get gauntlets => items[6].name; // 6: 건틀릿
|
|
String get gambeson => items[7].name; // 7: 갬비슨
|
|
String get cuisses => items[8].name; // 8: 허벅지갑
|
|
String get greaves => items[9].name; // 9: 정강이갑
|
|
String get sollerets => items[10].name; // 10: 철제신발
|
|
|
|
// ============================================================================
|
|
// EquipmentItem API
|
|
// ============================================================================
|
|
|
|
EquipmentItem get weaponItem => items[0];
|
|
EquipmentItem get shieldItem => items[1];
|
|
EquipmentItem get helmItem => items[2];
|
|
EquipmentItem get hauberkItem => items[3];
|
|
EquipmentItem get brassairtsItem => items[4];
|
|
EquipmentItem get vambracesItem => items[5];
|
|
EquipmentItem get gauntletsItem => items[6];
|
|
EquipmentItem get gambesonItem => items[7];
|
|
EquipmentItem get cuissesItem => items[8];
|
|
EquipmentItem get greavesItem => items[9];
|
|
EquipmentItem get solleretsItem => items[10];
|
|
|
|
/// 모든 장비 스탯 합산
|
|
ItemStats get totalStats {
|
|
return items.fold(ItemStats.empty, (sum, item) => sum + item.stats);
|
|
}
|
|
|
|
/// 모든 장비 무게 합산
|
|
int get totalWeight {
|
|
return items.fold(0, (sum, item) => sum + item.weight);
|
|
}
|
|
|
|
/// 장착된 아이템 목록 (빈 슬롯 제외)
|
|
List<EquipmentItem> get equippedItems {
|
|
return items.where((item) => item.isNotEmpty).toList();
|
|
}
|
|
|
|
// ============================================================================
|
|
// 팩토리 메서드
|
|
// ============================================================================
|
|
|
|
factory Equipment.empty() {
|
|
return Equipment(
|
|
items: [
|
|
EquipmentItem.defaultWeapon(), // 0: 무기 (Keyboard)
|
|
EquipmentItem.empty(EquipmentSlot.shield), // 1: 방패
|
|
EquipmentItem.empty(EquipmentSlot.helm), // 2: 투구
|
|
EquipmentItem.empty(EquipmentSlot.hauberk), // 3: 사슬갑옷
|
|
EquipmentItem.empty(EquipmentSlot.brassairts), // 4: 상완갑
|
|
EquipmentItem.empty(EquipmentSlot.vambraces), // 5: 전완갑
|
|
EquipmentItem.empty(EquipmentSlot.gauntlets), // 6: 건틀릿
|
|
EquipmentItem.empty(EquipmentSlot.gambeson), // 7: 갬비슨
|
|
EquipmentItem.empty(EquipmentSlot.cuisses), // 8: 허벅지갑
|
|
EquipmentItem.empty(EquipmentSlot.greaves), // 9: 정강이갑
|
|
EquipmentItem.empty(EquipmentSlot.sollerets), // 10: 철제신발
|
|
],
|
|
bestIndex: 0,
|
|
);
|
|
}
|
|
|
|
/// 레거시 문자열 기반 생성자 (세이브 파일 호환용)
|
|
factory Equipment.fromStrings({
|
|
required String weapon,
|
|
required String shield,
|
|
required String helm,
|
|
required String hauberk,
|
|
required String brassairts,
|
|
required String vambraces,
|
|
required String gauntlets,
|
|
required String gambeson,
|
|
required String cuisses,
|
|
required String greaves,
|
|
required String sollerets,
|
|
required int bestIndex,
|
|
}) {
|
|
return Equipment(
|
|
items: [
|
|
_itemFromString(weapon, EquipmentSlot.weapon),
|
|
_itemFromString(shield, EquipmentSlot.shield),
|
|
_itemFromString(helm, EquipmentSlot.helm),
|
|
_itemFromString(hauberk, EquipmentSlot.hauberk),
|
|
_itemFromString(brassairts, EquipmentSlot.brassairts),
|
|
_itemFromString(vambraces, EquipmentSlot.vambraces),
|
|
_itemFromString(gauntlets, EquipmentSlot.gauntlets),
|
|
_itemFromString(gambeson, EquipmentSlot.gambeson),
|
|
_itemFromString(cuisses, EquipmentSlot.cuisses),
|
|
_itemFromString(greaves, EquipmentSlot.greaves),
|
|
_itemFromString(sollerets, EquipmentSlot.sollerets),
|
|
],
|
|
bestIndex: bestIndex,
|
|
);
|
|
}
|
|
|
|
/// 문자열에서 기본 EquipmentItem 생성 (레거시 호환)
|
|
static EquipmentItem _itemFromString(String name, EquipmentSlot slot) {
|
|
if (name.isEmpty) {
|
|
return EquipmentItem.empty(slot);
|
|
}
|
|
// 레거시 아이템: 레벨 1, Common, 기본 스탯
|
|
return EquipmentItem(
|
|
name: name,
|
|
slot: slot,
|
|
level: 1,
|
|
weight: 5,
|
|
stats: ItemStats.empty,
|
|
rarity: ItemRarity.common,
|
|
);
|
|
}
|
|
|
|
// ============================================================================
|
|
// 유틸리티 메서드
|
|
// ============================================================================
|
|
|
|
/// 인덱스로 슬롯 이름 가져오기 (기존 API 호환)
|
|
String getByIndex(int index) {
|
|
if (index < 0 || index >= slotCount) return '';
|
|
return items[index].name;
|
|
}
|
|
|
|
/// 인덱스로 EquipmentItem 가져오기
|
|
EquipmentItem getItemByIndex(int index) {
|
|
if (index < 0 || index >= slotCount) {
|
|
return EquipmentItem.empty(EquipmentSlot.weapon);
|
|
}
|
|
return items[index];
|
|
}
|
|
|
|
/// 인덱스로 슬롯 값 설정 (문자열, 기존 API 호환)
|
|
Equipment setByIndex(int index, String value) {
|
|
if (index < 0 || index >= slotCount) return this;
|
|
final slot = EquipmentSlot.values[index];
|
|
final newItem = _itemFromString(value, slot);
|
|
return setItemByIndex(index, newItem);
|
|
}
|
|
|
|
/// 인덱스로 EquipmentItem 설정
|
|
Equipment setItemByIndex(int index, EquipmentItem item) {
|
|
if (index < 0 || index >= slotCount) return this;
|
|
final newItems = List<EquipmentItem>.from(items);
|
|
newItems[index] = item;
|
|
return Equipment(items: newItems, bestIndex: bestIndex);
|
|
}
|
|
|
|
Equipment copyWith({List<EquipmentItem>? items, int? bestIndex}) {
|
|
return Equipment(
|
|
items: items ?? List<EquipmentItem>.from(this.items),
|
|
bestIndex: bestIndex ?? this.bestIndex,
|
|
);
|
|
}
|
|
}
|
|
|
|
class SpellEntry {
|
|
const SpellEntry({required this.name, required this.rank});
|
|
|
|
final String name;
|
|
final String rank; // e.g., Roman numerals
|
|
|
|
SpellEntry copyWith({String? name, String? rank}) {
|
|
return SpellEntry(name: name ?? this.name, rank: rank ?? this.rank);
|
|
}
|
|
}
|
|
|
|
class SpellBook {
|
|
const SpellBook({required this.spells});
|
|
|
|
final List<SpellEntry> spells;
|
|
|
|
factory SpellBook.empty() => const SpellBook(spells: []);
|
|
|
|
SpellBook copyWith({List<SpellEntry>? spells}) {
|
|
return SpellBook(spells: spells ?? this.spells);
|
|
}
|
|
}
|
|
|
|
class ProgressBarState {
|
|
const ProgressBarState({required this.position, required this.max});
|
|
|
|
final int position;
|
|
final int max;
|
|
|
|
factory ProgressBarState.empty() =>
|
|
const ProgressBarState(position: 0, max: 1);
|
|
|
|
ProgressBarState copyWith({int? position, int? max}) {
|
|
return ProgressBarState(
|
|
position: position ?? this.position,
|
|
max: max ?? this.max,
|
|
);
|
|
}
|
|
}
|
|
|
|
/// 히스토리 엔트리 (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,
|
|
required this.quest,
|
|
required this.plot,
|
|
required this.exp,
|
|
required this.encumbrance,
|
|
required this.currentTask,
|
|
required this.plotStageCount,
|
|
required this.questCount,
|
|
this.plotHistory = const [],
|
|
this.questHistory = const [],
|
|
this.currentQuestMonster,
|
|
this.currentCombat,
|
|
this.monstersKilled = 0,
|
|
this.deathCount = 0,
|
|
});
|
|
|
|
final ProgressBarState task;
|
|
final ProgressBarState quest;
|
|
final ProgressBarState plot;
|
|
final ProgressBarState exp;
|
|
final ProgressBarState encumbrance;
|
|
final TaskInfo currentTask;
|
|
final int plotStageCount;
|
|
final int questCount;
|
|
|
|
/// 플롯 히스토리 (Prologue, Act I, Act II, ...)
|
|
final List<HistoryEntry> plotHistory;
|
|
|
|
/// 퀘스트 히스토리 (완료된/진행중인 퀘스트 목록)
|
|
final List<HistoryEntry> questHistory;
|
|
|
|
/// 현재 퀘스트 몬스터 정보 (Exterminate 타입용)
|
|
final QuestMonsterInfo? currentQuestMonster;
|
|
|
|
/// 현재 전투 상태 (킬 태스크 진행 중)
|
|
final CombatState? currentCombat;
|
|
|
|
/// 처치한 몬스터 수
|
|
final int monstersKilled;
|
|
|
|
/// 사망 횟수
|
|
final int deathCount;
|
|
|
|
factory ProgressState.empty() => ProgressState(
|
|
task: ProgressBarState.empty(),
|
|
quest: ProgressBarState.empty(),
|
|
plot: ProgressBarState.empty(),
|
|
exp: ProgressBarState.empty(),
|
|
encumbrance: ProgressBarState.empty(),
|
|
currentTask: TaskInfo.empty(),
|
|
plotStageCount: 1, // Prologue
|
|
questCount: 0,
|
|
plotHistory: const [HistoryEntry(caption: 'Prologue', isComplete: false)],
|
|
questHistory: const [],
|
|
currentQuestMonster: null,
|
|
currentCombat: null,
|
|
);
|
|
|
|
ProgressState copyWith({
|
|
ProgressBarState? task,
|
|
ProgressBarState? quest,
|
|
ProgressBarState? plot,
|
|
ProgressBarState? exp,
|
|
ProgressBarState? encumbrance,
|
|
TaskInfo? currentTask,
|
|
int? plotStageCount,
|
|
int? questCount,
|
|
List<HistoryEntry>? plotHistory,
|
|
List<HistoryEntry>? questHistory,
|
|
QuestMonsterInfo? currentQuestMonster,
|
|
CombatState? currentCombat,
|
|
int? monstersKilled,
|
|
int? deathCount,
|
|
}) {
|
|
return ProgressState(
|
|
task: task ?? this.task,
|
|
quest: quest ?? this.quest,
|
|
plot: plot ?? this.plot,
|
|
exp: exp ?? this.exp,
|
|
encumbrance: encumbrance ?? this.encumbrance,
|
|
currentTask: currentTask ?? this.currentTask,
|
|
plotStageCount: plotStageCount ?? this.plotStageCount,
|
|
questCount: questCount ?? this.questCount,
|
|
plotHistory: plotHistory ?? this.plotHistory,
|
|
questHistory: questHistory ?? this.questHistory,
|
|
currentQuestMonster: currentQuestMonster ?? this.currentQuestMonster,
|
|
currentCombat: currentCombat ?? this.currentCombat,
|
|
monstersKilled: monstersKilled ?? this.monstersKilled,
|
|
deathCount: deathCount ?? this.deathCount,
|
|
);
|
|
}
|
|
}
|
|
|
|
class QueueEntry {
|
|
const QueueEntry({
|
|
required this.kind,
|
|
required this.durationMillis,
|
|
required this.caption,
|
|
this.taskType = TaskType.neutral,
|
|
});
|
|
|
|
final QueueKind kind;
|
|
final int durationMillis;
|
|
final String caption;
|
|
final TaskType taskType;
|
|
}
|
|
|
|
enum QueueKind { task, plot }
|
|
|
|
class QueueState {
|
|
QueueState({Iterable<QueueEntry>? entries})
|
|
: entries = Queue<QueueEntry>.from(entries ?? const []);
|
|
|
|
final Queue<QueueEntry> entries;
|
|
|
|
factory QueueState.empty() => QueueState(entries: const []);
|
|
|
|
QueueState copyWith({Iterable<QueueEntry>? entries}) {
|
|
return QueueState(entries: Queue<QueueEntry>.from(entries ?? this.entries));
|
|
}
|
|
}
|