refactor(model): game_state.dart 분할 (SRP 개선)

- Stats, Traits, Inventory, Equipment 등 11개 파일로 분리
- 단일 책임 원칙 적용으로 유지보수성 향상
This commit is contained in:
JiWoong Sul
2026-01-21 17:33:30 +09:00
parent 7b9f1f87a6
commit e516076ce8
11 changed files with 940 additions and 890 deletions

View 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,
);
}
}

View 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,
);
}
}

View File

@@ -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));
}
}

View 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);
}
}

View 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!;
}
}

View 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));
}
}

View 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);
}
}

View 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);
}
}

View 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,
);
}
}

View 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,
);
}
}

View 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,
);
}
}