refactor(model): game_state.dart 분할 (SRP 개선)
- Stats, Traits, Inventory, Equipment 등 11개 파일로 분리 - 단일 책임 원칙 적용으로 유지보수성 향상
This commit is contained in:
96
lib/src/core/model/death_info.dart
Normal file
96
lib/src/core/model/death_info.dart
Normal file
@@ -0,0 +1,96 @@
|
||||
import 'package:asciineverdie/src/core/model/combat_event.dart';
|
||||
import 'package:asciineverdie/src/core/model/equipment_item.dart';
|
||||
import 'package:asciineverdie/src/core/model/equipment_slot.dart';
|
||||
import 'package:asciineverdie/src/core/model/item_stats.dart';
|
||||
|
||||
/// 사망 원인 (Death Cause)
|
||||
enum DeathCause {
|
||||
/// 몬스터에 의한 사망
|
||||
monster,
|
||||
|
||||
/// 자해 스킬에 의한 사망
|
||||
selfDamage,
|
||||
|
||||
/// 환경 피해에 의한 사망
|
||||
environment,
|
||||
}
|
||||
|
||||
/// 사망 정보 (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.lostItemSlot,
|
||||
this.lostItemRarity,
|
||||
this.lostItem,
|
||||
this.lastCombatEvents = const [],
|
||||
});
|
||||
|
||||
/// 사망 원인
|
||||
final DeathCause cause;
|
||||
|
||||
/// 사망시킨 몬스터/원인 이름
|
||||
final String killerName;
|
||||
|
||||
/// 상실한 장비 개수 (0 또는 1)
|
||||
final int lostEquipmentCount;
|
||||
|
||||
/// 제물로 바친 아이템 이름 (null이면 없음)
|
||||
final String? lostItemName;
|
||||
|
||||
/// 제물로 바친 아이템 슬롯 (null이면 없음)
|
||||
final EquipmentSlot? lostItemSlot;
|
||||
|
||||
/// 제물로 바친 아이템 희귀도 (null이면 없음)
|
||||
final ItemRarity? lostItemRarity;
|
||||
|
||||
/// 상실한 장비 전체 정보 (광고 부활 시 복구용)
|
||||
final EquipmentItem? lostItem;
|
||||
|
||||
/// 사망 시점 골드
|
||||
final int goldAtDeath;
|
||||
|
||||
/// 사망 시점 레벨
|
||||
final int levelAtDeath;
|
||||
|
||||
/// 사망 시각 (밀리초)
|
||||
final int timestamp;
|
||||
|
||||
/// 사망 직전 전투 이벤트 (최대 10개)
|
||||
final List<CombatEvent> lastCombatEvents;
|
||||
|
||||
DeathInfo copyWith({
|
||||
DeathCause? cause,
|
||||
String? killerName,
|
||||
int? lostEquipmentCount,
|
||||
String? lostItemName,
|
||||
EquipmentSlot? lostItemSlot,
|
||||
ItemRarity? lostItemRarity,
|
||||
EquipmentItem? lostItem,
|
||||
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,
|
||||
lostItemSlot: lostItemSlot ?? this.lostItemSlot,
|
||||
lostItemRarity: lostItemRarity ?? this.lostItemRarity,
|
||||
lostItem: lostItem ?? this.lostItem,
|
||||
goldAtDeath: goldAtDeath ?? this.goldAtDeath,
|
||||
levelAtDeath: levelAtDeath ?? this.levelAtDeath,
|
||||
timestamp: timestamp ?? this.timestamp,
|
||||
lastCombatEvents: lastCombatEvents ?? this.lastCombatEvents,
|
||||
);
|
||||
}
|
||||
}
|
||||
220
lib/src/core/model/equipment_container.dart
Normal file
220
lib/src/core/model/equipment_container.dart
Normal file
@@ -0,0 +1,220 @@
|
||||
import 'package:asciineverdie/src/core/model/equipment_item.dart';
|
||||
import 'package:asciineverdie/src/core/model/equipment_slot.dart';
|
||||
import 'package:asciineverdie/src/core/model/item_stats.dart';
|
||||
|
||||
/// 장비 (원본 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,
|
||||
);
|
||||
}
|
||||
|
||||
/// 초보자 장비 세트 (모든 슬롯에 기본 방어구 지급)
|
||||
///
|
||||
/// 원본 PQ에서는 초기 장비가 없지만, 밸런스 개선을 위해
|
||||
/// Act 1 완료 전에도 기본 방어력을 제공.
|
||||
factory Equipment.withStarterGear() {
|
||||
return Equipment(
|
||||
items: [
|
||||
EquipmentItem.defaultWeapon(), // 0: 무기 (Keyboard)
|
||||
_starterArmor('Old Mouse', EquipmentSlot.shield, def: 2),
|
||||
_starterArmor('Cardboard Box', EquipmentSlot.helm, def: 1),
|
||||
_starterArmor('Worn T-Shirt', EquipmentSlot.hauberk, def: 3),
|
||||
_starterArmor('Rubber Band', EquipmentSlot.brassairts, def: 1),
|
||||
_starterArmor('Wristwatch', EquipmentSlot.vambraces, def: 1),
|
||||
_starterArmor('Fingerless Gloves', EquipmentSlot.gauntlets, def: 1),
|
||||
_starterArmor('Hoodie', EquipmentSlot.gambeson, def: 2),
|
||||
_starterArmor('Jeans', EquipmentSlot.cuisses, def: 2),
|
||||
_starterArmor('Knee Pads', EquipmentSlot.greaves, def: 1),
|
||||
_starterArmor('Sneakers', EquipmentSlot.sollerets, def: 1),
|
||||
],
|
||||
bestIndex: 0,
|
||||
);
|
||||
}
|
||||
|
||||
/// 초보자 방어구 생성 헬퍼
|
||||
static EquipmentItem _starterArmor(
|
||||
String name,
|
||||
EquipmentSlot slot, {
|
||||
required int def,
|
||||
}) {
|
||||
return EquipmentItem(
|
||||
name: name,
|
||||
slot: slot,
|
||||
level: 1,
|
||||
weight: 1,
|
||||
stats: ItemStats(def: def),
|
||||
rarity: ItemRarity.common,
|
||||
);
|
||||
}
|
||||
|
||||
/// 레거시 문자열 기반 생성자 (세이브 파일 호환용)
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,35 @@
|
||||
import 'dart:collection';
|
||||
/// 게임 상태 모듈 (Game State Module)
|
||||
///
|
||||
/// 하위 파일들을 re-export하여 기존 import 호환성 유지
|
||||
library;
|
||||
|
||||
import 'package:asciineverdie/src/core/animation/monster_size.dart';
|
||||
import 'package:asciineverdie/src/core/model/combat_event.dart';
|
||||
import 'package:asciineverdie/src/core/model/combat_state.dart';
|
||||
import 'package:asciineverdie/src/core/model/equipment_item.dart';
|
||||
import 'package:asciineverdie/src/core/model/equipment_slot.dart';
|
||||
import 'package:asciineverdie/src/core/model/item_stats.dart';
|
||||
import 'package:asciineverdie/src/core/model/monster_grade.dart';
|
||||
export 'death_info.dart';
|
||||
export 'equipment_container.dart';
|
||||
export 'inventory.dart';
|
||||
export 'progress_state.dart';
|
||||
export 'queue_state.dart';
|
||||
export 'skill_book.dart';
|
||||
export 'skill_system_state.dart';
|
||||
export 'stats.dart';
|
||||
export 'task_info.dart';
|
||||
export 'traits.dart';
|
||||
|
||||
import 'package:asciineverdie/src/core/model/death_info.dart';
|
||||
import 'package:asciineverdie/src/core/model/equipment_container.dart';
|
||||
import 'package:asciineverdie/src/core/model/inventory.dart';
|
||||
import 'package:asciineverdie/src/core/model/potion.dart';
|
||||
import 'package:asciineverdie/src/core/model/skill.dart';
|
||||
import 'package:asciineverdie/src/core/model/skill_slots.dart';
|
||||
import 'package:asciineverdie/src/core/model/progress_state.dart';
|
||||
import 'package:asciineverdie/src/core/model/queue_state.dart';
|
||||
import 'package:asciineverdie/src/core/model/skill_book.dart';
|
||||
import 'package:asciineverdie/src/core/model/skill_system_state.dart';
|
||||
import 'package:asciineverdie/src/core/model/stats.dart';
|
||||
import 'package:asciineverdie/src/core/model/traits.dart';
|
||||
import 'package:asciineverdie/src/core/util/deterministic_random.dart';
|
||||
|
||||
/// Minimal skeletal state to mirror Progress Quest structures.
|
||||
/// 게임 전체 상태 (Game State)
|
||||
///
|
||||
/// Logic will be ported faithfully from the Delphi source; this file only
|
||||
/// defines containers and helpers for deterministic RNG.
|
||||
/// Progress Quest 구조를 미러링하는 최소 스켈레톤 상태.
|
||||
/// 로직은 Delphi 소스에서 충실하게 포팅됨.
|
||||
class GameState {
|
||||
GameState({
|
||||
required DeterministicRandom rng,
|
||||
@@ -118,880 +132,3 @@ class GameState {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 사망 정보 (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.lostItemSlot,
|
||||
this.lostItemRarity,
|
||||
this.lostItem,
|
||||
this.lastCombatEvents = const [],
|
||||
});
|
||||
|
||||
/// 사망 원인
|
||||
final DeathCause cause;
|
||||
|
||||
/// 사망시킨 몬스터/원인 이름
|
||||
final String killerName;
|
||||
|
||||
/// 상실한 장비 개수 (0 또는 1)
|
||||
final int lostEquipmentCount;
|
||||
|
||||
/// 제물로 바친 아이템 이름 (null이면 없음)
|
||||
final String? lostItemName;
|
||||
|
||||
/// 제물로 바친 아이템 슬롯 (null이면 없음)
|
||||
final EquipmentSlot? lostItemSlot;
|
||||
|
||||
/// 제물로 바친 아이템 희귀도 (null이면 없음)
|
||||
final ItemRarity? lostItemRarity;
|
||||
|
||||
/// 상실한 장비 전체 정보 (광고 부활 시 복구용)
|
||||
final EquipmentItem? lostItem;
|
||||
|
||||
/// 사망 시점 골드
|
||||
final int goldAtDeath;
|
||||
|
||||
/// 사망 시점 레벨
|
||||
final int levelAtDeath;
|
||||
|
||||
/// 사망 시각 (밀리초)
|
||||
final int timestamp;
|
||||
|
||||
/// 사망 직전 전투 이벤트 (최대 10개)
|
||||
final List<CombatEvent> lastCombatEvents;
|
||||
|
||||
DeathInfo copyWith({
|
||||
DeathCause? cause,
|
||||
String? killerName,
|
||||
int? lostEquipmentCount,
|
||||
String? lostItemName,
|
||||
EquipmentSlot? lostItemSlot,
|
||||
ItemRarity? lostItemRarity,
|
||||
EquipmentItem? lostItem,
|
||||
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,
|
||||
lostItemSlot: lostItemSlot ?? this.lostItemSlot,
|
||||
lostItemRarity: lostItemRarity ?? this.lostItemRarity,
|
||||
lostItem: lostItem ?? this.lostItem,
|
||||
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,
|
||||
this.equippedSkills = const SkillSlots(),
|
||||
this.globalCooldownEndMs = 0,
|
||||
});
|
||||
|
||||
/// 글로벌 쿨타임 (GCD) 상수: 1500ms
|
||||
static const int globalCooldownDuration = 1500;
|
||||
|
||||
/// 스킬별 쿨타임 상태
|
||||
final List<SkillState> skillStates;
|
||||
|
||||
/// 현재 활성화된 버프 목록
|
||||
final List<ActiveBuff> activeBuffs;
|
||||
|
||||
/// 게임 진행 경과 시간 (밀리초, 스킬 쿨타임 계산용)
|
||||
final int elapsedMs;
|
||||
|
||||
/// 장착된 스킬 슬롯 (타입별 제한 있음)
|
||||
final SkillSlots equippedSkills;
|
||||
|
||||
/// 글로벌 쿨타임 종료 시점 (elapsedMs 기준)
|
||||
final int globalCooldownEndMs;
|
||||
|
||||
/// GCD가 활성화 중인지 확인
|
||||
bool get isGlobalCooldownActive => elapsedMs < globalCooldownEndMs;
|
||||
|
||||
/// 남은 GCD 시간 (ms)
|
||||
int get remainingGlobalCooldown =>
|
||||
isGlobalCooldownActive ? globalCooldownEndMs - elapsedMs : 0;
|
||||
|
||||
factory SkillSystemState.empty() => const SkillSystemState(
|
||||
skillStates: [],
|
||||
activeBuffs: [],
|
||||
elapsedMs: 0,
|
||||
equippedSkills: SkillSlots(),
|
||||
globalCooldownEndMs: 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,
|
||||
SkillSlots? equippedSkills,
|
||||
int? globalCooldownEndMs,
|
||||
}) {
|
||||
return SkillSystemState(
|
||||
skillStates: skillStates ?? this.skillStates,
|
||||
activeBuffs: activeBuffs ?? this.activeBuffs,
|
||||
elapsedMs: elapsedMs ?? this.elapsedMs,
|
||||
equippedSkills: equippedSkills ?? this.equippedSkills,
|
||||
globalCooldownEndMs: globalCooldownEndMs ?? this.globalCooldownEndMs,
|
||||
);
|
||||
}
|
||||
|
||||
/// GCD 시작 (스킬 사용 후 호출)
|
||||
SkillSystemState startGlobalCooldown() {
|
||||
return copyWith(globalCooldownEndMs: elapsedMs + globalCooldownDuration);
|
||||
}
|
||||
}
|
||||
|
||||
/// 태스크 타입 (원본 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,
|
||||
this.monsterGrade,
|
||||
this.monsterSize,
|
||||
});
|
||||
|
||||
final String caption;
|
||||
final TaskType type;
|
||||
|
||||
/// 킬 태스크의 몬스터 기본 이름 (형용사 제외, 예: "Goblin")
|
||||
final String? monsterBaseName;
|
||||
|
||||
/// 킬 태스크의 전리품 부위 (예: "claw", "tail", "*"는 WinItem)
|
||||
final String? monsterPart;
|
||||
|
||||
/// 킬 태스크의 몬스터 레벨 (전투 스탯 계산용)
|
||||
final int? monsterLevel;
|
||||
|
||||
/// 킬 태스크의 몬스터 등급 (Normal/Elite/Boss)
|
||||
final MonsterGrade? monsterGrade;
|
||||
|
||||
/// 킬 태스크의 몬스터 사이즈 (애니메이션 크기 결정용, Act 기반)
|
||||
final MonsterSize? monsterSize;
|
||||
|
||||
factory TaskInfo.empty() =>
|
||||
const TaskInfo(caption: '', type: TaskType.neutral);
|
||||
|
||||
TaskInfo copyWith({
|
||||
String? caption,
|
||||
TaskType? type,
|
||||
String? monsterBaseName,
|
||||
String? monsterPart,
|
||||
int? monsterLevel,
|
||||
MonsterGrade? monsterGrade,
|
||||
MonsterSize? monsterSize,
|
||||
}) {
|
||||
return TaskInfo(
|
||||
caption: caption ?? this.caption,
|
||||
type: type ?? this.type,
|
||||
monsterBaseName: monsterBaseName ?? this.monsterBaseName,
|
||||
monsterPart: monsterPart ?? this.monsterPart,
|
||||
monsterLevel: monsterLevel ?? this.monsterLevel,
|
||||
monsterGrade: monsterGrade ?? this.monsterGrade,
|
||||
monsterSize: monsterSize ?? this.monsterSize,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
/// 초기 골드 1000 지급 (캐릭터 생성 시)
|
||||
factory Inventory.empty() => const Inventory(gold: 1000, 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,
|
||||
);
|
||||
}
|
||||
|
||||
/// 초보자 장비 세트 (모든 슬롯에 기본 방어구 지급)
|
||||
///
|
||||
/// 원본 PQ에서는 초기 장비가 없지만, 밸런스 개선을 위해
|
||||
/// Act 1 완료 전에도 기본 방어력을 제공.
|
||||
factory Equipment.withStarterGear() {
|
||||
return Equipment(
|
||||
items: [
|
||||
EquipmentItem.defaultWeapon(), // 0: 무기 (Keyboard)
|
||||
_starterArmor('Old Mouse', EquipmentSlot.shield, def: 2),
|
||||
_starterArmor('Cardboard Box', EquipmentSlot.helm, def: 1),
|
||||
_starterArmor('Worn T-Shirt', EquipmentSlot.hauberk, def: 3),
|
||||
_starterArmor('Rubber Band', EquipmentSlot.brassairts, def: 1),
|
||||
_starterArmor('Wristwatch', EquipmentSlot.vambraces, def: 1),
|
||||
_starterArmor('Fingerless Gloves', EquipmentSlot.gauntlets, def: 1),
|
||||
_starterArmor('Hoodie', EquipmentSlot.gambeson, def: 2),
|
||||
_starterArmor('Jeans', EquipmentSlot.cuisses, def: 2),
|
||||
_starterArmor('Knee Pads', EquipmentSlot.greaves, def: 1),
|
||||
_starterArmor('Sneakers', EquipmentSlot.sollerets, def: 1),
|
||||
],
|
||||
bestIndex: 0,
|
||||
);
|
||||
}
|
||||
|
||||
/// 초보자 방어구 생성 헬퍼
|
||||
static EquipmentItem _starterArmor(
|
||||
String name,
|
||||
EquipmentSlot slot, {
|
||||
required int def,
|
||||
}) {
|
||||
return EquipmentItem(
|
||||
name: name,
|
||||
slot: slot,
|
||||
level: 1,
|
||||
weight: 1,
|
||||
stats: ItemStats(def: def),
|
||||
rarity: ItemRarity.common,
|
||||
);
|
||||
}
|
||||
|
||||
/// 레거시 문자열 기반 생성자 (세이브 파일 호환용)
|
||||
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 SkillEntry {
|
||||
const SkillEntry({required this.name, required this.rank});
|
||||
|
||||
final String name;
|
||||
final String rank; // e.g., Roman numerals
|
||||
|
||||
SkillEntry copyWith({String? name, String? rank}) {
|
||||
return SkillEntry(name: name ?? this.name, rank: rank ?? this.rank);
|
||||
}
|
||||
}
|
||||
|
||||
class SkillBook {
|
||||
const SkillBook({required this.skills});
|
||||
|
||||
final List<SkillEntry> skills;
|
||||
|
||||
factory SkillBook.empty() => const SkillBook(skills: []);
|
||||
|
||||
SkillBook copyWith({List<SkillEntry>? skills}) {
|
||||
return SkillBook(skills: skills ?? this.skills);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
/// 최종 보스 상태 (Final Boss State)
|
||||
enum FinalBossState {
|
||||
/// 최종 보스 등장 전
|
||||
notSpawned,
|
||||
|
||||
/// 최종 보스 전투 중
|
||||
fighting,
|
||||
|
||||
/// 최종 보스 처치 완료
|
||||
defeated,
|
||||
}
|
||||
|
||||
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,
|
||||
this.finalBossState = FinalBossState.notSpawned,
|
||||
this.pendingActCompletion = false,
|
||||
this.bossLevelingEndTime,
|
||||
});
|
||||
|
||||
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;
|
||||
|
||||
/// 최종 보스 상태 (Act V)
|
||||
final FinalBossState finalBossState;
|
||||
|
||||
/// Act Boss 처치 대기 중 여부 (처치 후 시네마틱 재생 트리거)
|
||||
final bool pendingActCompletion;
|
||||
|
||||
/// 보스 사망 후 레벨링 모드 종료 시간 (milliseconds since epoch)
|
||||
/// null이면 레벨링 모드 아님, 값이 있으면 해당 시간까지 레벨링
|
||||
final int? bossLevelingEndTime;
|
||||
|
||||
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,
|
||||
FinalBossState? finalBossState,
|
||||
bool? pendingActCompletion,
|
||||
int? bossLevelingEndTime,
|
||||
bool clearBossLevelingEndTime = false,
|
||||
}) {
|
||||
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,
|
||||
finalBossState: finalBossState ?? this.finalBossState,
|
||||
pendingActCompletion: pendingActCompletion ?? this.pendingActCompletion,
|
||||
bossLevelingEndTime: clearBossLevelingEndTime
|
||||
? null
|
||||
: (bossLevelingEndTime ?? this.bossLevelingEndTime),
|
||||
);
|
||||
}
|
||||
|
||||
/// 현재 레벨링 모드인지 확인
|
||||
bool get isInBossLevelingMode {
|
||||
if (bossLevelingEndTime == null) return false;
|
||||
return DateTime.now().millisecondsSinceEpoch < bossLevelingEndTime!;
|
||||
}
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
28
lib/src/core/model/inventory.dart
Normal file
28
lib/src/core/model/inventory.dart
Normal file
@@ -0,0 +1,28 @@
|
||||
/// 인벤토리 아이템 엔트리 (Inventory Entry)
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// 인벤토리 (Inventory)
|
||||
///
|
||||
/// 골드와 아이템 목록을 관리
|
||||
class Inventory {
|
||||
const Inventory({required this.gold, required this.items});
|
||||
|
||||
final int gold;
|
||||
final List<InventoryEntry> items;
|
||||
|
||||
/// 초기 골드 1000 지급 (캐릭터 생성 시)
|
||||
factory Inventory.empty() => const Inventory(gold: 1000, items: []);
|
||||
|
||||
Inventory copyWith({int? gold, List<InventoryEntry>? items}) {
|
||||
return Inventory(gold: gold ?? this.gold, items: items ?? this.items);
|
||||
}
|
||||
}
|
||||
192
lib/src/core/model/progress_state.dart
Normal file
192
lib/src/core/model/progress_state.dart
Normal file
@@ -0,0 +1,192 @@
|
||||
import 'package:asciineverdie/src/core/model/combat_state.dart';
|
||||
import 'package:asciineverdie/src/core/model/task_info.dart';
|
||||
|
||||
/// 진행 바 상태 (Progress Bar State)
|
||||
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);
|
||||
}
|
||||
|
||||
/// 최종 보스 상태 (Final Boss State)
|
||||
enum FinalBossState {
|
||||
/// 최종 보스 등장 전
|
||||
notSpawned,
|
||||
|
||||
/// 최종 보스 전투 중
|
||||
fighting,
|
||||
|
||||
/// 최종 보스 처치 완료
|
||||
defeated,
|
||||
}
|
||||
|
||||
/// 진행 상태 (Progress State)
|
||||
///
|
||||
/// 태스크, 퀘스트, 플롯, 경험치, 무게 등의 진행 상태를 관리
|
||||
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,
|
||||
this.finalBossState = FinalBossState.notSpawned,
|
||||
this.pendingActCompletion = false,
|
||||
this.bossLevelingEndTime,
|
||||
});
|
||||
|
||||
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;
|
||||
|
||||
/// 최종 보스 상태 (Act V)
|
||||
final FinalBossState finalBossState;
|
||||
|
||||
/// Act Boss 처치 대기 중 여부 (처치 후 시네마틱 재생 트리거)
|
||||
final bool pendingActCompletion;
|
||||
|
||||
/// 보스 사망 후 레벨링 모드 종료 시간 (milliseconds since epoch)
|
||||
/// null이면 레벨링 모드 아님, 값이 있으면 해당 시간까지 레벨링
|
||||
final int? bossLevelingEndTime;
|
||||
|
||||
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,
|
||||
FinalBossState? finalBossState,
|
||||
bool? pendingActCompletion,
|
||||
int? bossLevelingEndTime,
|
||||
bool clearBossLevelingEndTime = false,
|
||||
}) {
|
||||
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,
|
||||
finalBossState: finalBossState ?? this.finalBossState,
|
||||
pendingActCompletion: pendingActCompletion ?? this.pendingActCompletion,
|
||||
bossLevelingEndTime: clearBossLevelingEndTime
|
||||
? null
|
||||
: (bossLevelingEndTime ?? this.bossLevelingEndTime),
|
||||
);
|
||||
}
|
||||
|
||||
/// 현재 레벨링 모드인지 확인
|
||||
bool get isInBossLevelingMode {
|
||||
if (bossLevelingEndTime == null) return false;
|
||||
return DateTime.now().millisecondsSinceEpoch < bossLevelingEndTime!;
|
||||
}
|
||||
}
|
||||
37
lib/src/core/model/queue_state.dart
Normal file
37
lib/src/core/model/queue_state.dart
Normal file
@@ -0,0 +1,37 @@
|
||||
import 'dart:collection';
|
||||
|
||||
import 'package:asciineverdie/src/core/model/task_info.dart';
|
||||
|
||||
/// 큐 종류 (Queue Kind)
|
||||
enum QueueKind { task, plot }
|
||||
|
||||
/// 큐 엔트리 (Queue Entry)
|
||||
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;
|
||||
}
|
||||
|
||||
/// 큐 상태 (Queue State)
|
||||
///
|
||||
/// 대기 중인 태스크/플롯 이벤트 큐를 관리
|
||||
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));
|
||||
}
|
||||
}
|
||||
26
lib/src/core/model/skill_book.dart
Normal file
26
lib/src/core/model/skill_book.dart
Normal file
@@ -0,0 +1,26 @@
|
||||
/// 스킬 엔트리 (Skill Entry)
|
||||
class SkillEntry {
|
||||
const SkillEntry({required this.name, required this.rank});
|
||||
|
||||
final String name;
|
||||
final String rank; // 예: 로마 숫자 (Roman numerals)
|
||||
|
||||
SkillEntry copyWith({String? name, String? rank}) {
|
||||
return SkillEntry(name: name ?? this.name, rank: rank ?? this.rank);
|
||||
}
|
||||
}
|
||||
|
||||
/// 스킬북 (Skill Book)
|
||||
///
|
||||
/// 캐릭터가 보유한 스킬 목록을 관리
|
||||
class SkillBook {
|
||||
const SkillBook({required this.skills});
|
||||
|
||||
final List<SkillEntry> skills;
|
||||
|
||||
factory SkillBook.empty() => const SkillBook(skills: []);
|
||||
|
||||
SkillBook copyWith({List<SkillEntry>? skills}) {
|
||||
return SkillBook(skills: skills ?? this.skills);
|
||||
}
|
||||
}
|
||||
106
lib/src/core/model/skill_system_state.dart
Normal file
106
lib/src/core/model/skill_system_state.dart
Normal file
@@ -0,0 +1,106 @@
|
||||
import 'package:asciineverdie/src/core/model/skill.dart';
|
||||
import 'package:asciineverdie/src/core/model/skill_slots.dart';
|
||||
|
||||
/// 스킬 시스템 상태 (Phase 3)
|
||||
///
|
||||
/// 스킬 쿨타임, 활성 버프, 게임 경과 시간, 장착 스킬 등을 관리
|
||||
class SkillSystemState {
|
||||
const SkillSystemState({
|
||||
required this.skillStates,
|
||||
required this.activeBuffs,
|
||||
required this.elapsedMs,
|
||||
this.equippedSkills = const SkillSlots(),
|
||||
this.globalCooldownEndMs = 0,
|
||||
});
|
||||
|
||||
/// 글로벌 쿨타임 (GCD) 상수: 1500ms
|
||||
static const int globalCooldownDuration = 1500;
|
||||
|
||||
/// 스킬별 쿨타임 상태
|
||||
final List<SkillState> skillStates;
|
||||
|
||||
/// 현재 활성화된 버프 목록
|
||||
final List<ActiveBuff> activeBuffs;
|
||||
|
||||
/// 게임 진행 경과 시간 (밀리초, 스킬 쿨타임 계산용)
|
||||
final int elapsedMs;
|
||||
|
||||
/// 장착된 스킬 슬롯 (타입별 제한 있음)
|
||||
final SkillSlots equippedSkills;
|
||||
|
||||
/// 글로벌 쿨타임 종료 시점 (elapsedMs 기준)
|
||||
final int globalCooldownEndMs;
|
||||
|
||||
/// GCD가 활성화 중인지 확인
|
||||
bool get isGlobalCooldownActive => elapsedMs < globalCooldownEndMs;
|
||||
|
||||
/// 남은 GCD 시간 (ms)
|
||||
int get remainingGlobalCooldown =>
|
||||
isGlobalCooldownActive ? globalCooldownEndMs - elapsedMs : 0;
|
||||
|
||||
factory SkillSystemState.empty() => const SkillSystemState(
|
||||
skillStates: [],
|
||||
activeBuffs: [],
|
||||
elapsedMs: 0,
|
||||
equippedSkills: SkillSlots(),
|
||||
globalCooldownEndMs: 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,
|
||||
SkillSlots? equippedSkills,
|
||||
int? globalCooldownEndMs,
|
||||
}) {
|
||||
return SkillSystemState(
|
||||
skillStates: skillStates ?? this.skillStates,
|
||||
activeBuffs: activeBuffs ?? this.activeBuffs,
|
||||
elapsedMs: elapsedMs ?? this.elapsedMs,
|
||||
equippedSkills: equippedSkills ?? this.equippedSkills,
|
||||
globalCooldownEndMs: globalCooldownEndMs ?? this.globalCooldownEndMs,
|
||||
);
|
||||
}
|
||||
|
||||
/// GCD 시작 (스킬 사용 후 호출)
|
||||
SkillSystemState startGlobalCooldown() {
|
||||
return copyWith(globalCooldownEndMs: elapsedMs + globalCooldownDuration);
|
||||
}
|
||||
}
|
||||
75
lib/src/core/model/stats.dart
Normal file
75
lib/src/core/model/stats.dart
Normal file
@@ -0,0 +1,75 @@
|
||||
/// 캐릭터 스탯 (Stats)
|
||||
///
|
||||
/// 6대 능력치(STR, CON, DEX, INT, WIS, CHA)와 HP/MP를 관리
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
67
lib/src/core/model/task_info.dart
Normal file
67
lib/src/core/model/task_info.dart
Normal file
@@ -0,0 +1,67 @@
|
||||
import 'package:asciineverdie/src/core/animation/monster_size.dart';
|
||||
import 'package:asciineverdie/src/core/model/monster_grade.dart';
|
||||
|
||||
/// 태스크 타입 (원본 fTask.Caption 값들에 대응)
|
||||
enum TaskType {
|
||||
neutral, // heading 등 일반 이동
|
||||
kill, // 몬스터 처치
|
||||
load, // 로딩/초기화
|
||||
plot, // 플롯 진행
|
||||
market, // 시장으로 이동 중
|
||||
sell, // 아이템 판매 중
|
||||
buying, // 장비 구매 중
|
||||
}
|
||||
|
||||
/// 태스크 정보 (Task Info)
|
||||
class TaskInfo {
|
||||
const TaskInfo({
|
||||
required this.caption,
|
||||
required this.type,
|
||||
this.monsterBaseName,
|
||||
this.monsterPart,
|
||||
this.monsterLevel,
|
||||
this.monsterGrade,
|
||||
this.monsterSize,
|
||||
});
|
||||
|
||||
final String caption;
|
||||
final TaskType type;
|
||||
|
||||
/// 킬 태스크의 몬스터 기본 이름 (형용사 제외, 예: "Goblin")
|
||||
final String? monsterBaseName;
|
||||
|
||||
/// 킬 태스크의 전리품 부위 (예: "claw", "tail", "*"는 WinItem)
|
||||
final String? monsterPart;
|
||||
|
||||
/// 킬 태스크의 몬스터 레벨 (전투 스탯 계산용)
|
||||
final int? monsterLevel;
|
||||
|
||||
/// 킬 태스크의 몬스터 등급 (Normal/Elite/Boss)
|
||||
final MonsterGrade? monsterGrade;
|
||||
|
||||
/// 킬 태스크의 몬스터 사이즈 (애니메이션 크기 결정용, Act 기반)
|
||||
final MonsterSize? monsterSize;
|
||||
|
||||
factory TaskInfo.empty() =>
|
||||
const TaskInfo(caption: '', type: TaskType.neutral);
|
||||
|
||||
TaskInfo copyWith({
|
||||
String? caption,
|
||||
TaskType? type,
|
||||
String? monsterBaseName,
|
||||
String? monsterPart,
|
||||
int? monsterLevel,
|
||||
MonsterGrade? monsterGrade,
|
||||
MonsterSize? monsterSize,
|
||||
}) {
|
||||
return TaskInfo(
|
||||
caption: caption ?? this.caption,
|
||||
type: type ?? this.type,
|
||||
monsterBaseName: monsterBaseName ?? this.monsterBaseName,
|
||||
monsterPart: monsterPart ?? this.monsterPart,
|
||||
monsterLevel: monsterLevel ?? this.monsterLevel,
|
||||
monsterGrade: monsterGrade ?? this.monsterGrade,
|
||||
monsterSize: monsterSize ?? this.monsterSize,
|
||||
);
|
||||
}
|
||||
}
|
||||
66
lib/src/core/model/traits.dart
Normal file
66
lib/src/core/model/traits.dart
Normal file
@@ -0,0 +1,66 @@
|
||||
/// 캐릭터 특성 (Traits)
|
||||
///
|
||||
/// 이름, 종족, 직업, 레벨, 좌우명, 길드 정보를 포함
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user