diff --git a/lib/src/core/model/death_info.dart b/lib/src/core/model/death_info.dart new file mode 100644 index 0000000..e159181 --- /dev/null +++ b/lib/src/core/model/death_info.dart @@ -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 lastCombatEvents; + + DeathInfo copyWith({ + DeathCause? cause, + String? killerName, + int? lostEquipmentCount, + String? lostItemName, + EquipmentSlot? lostItemSlot, + ItemRarity? lostItemRarity, + EquipmentItem? lostItem, + int? goldAtDeath, + int? levelAtDeath, + int? timestamp, + List? 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, + ); + } +} diff --git a/lib/src/core/model/equipment_container.dart b/lib/src/core/model/equipment_container.dart new file mode 100644 index 0000000..f13bac4 --- /dev/null +++ b/lib/src/core/model/equipment_container.dart @@ -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 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 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.from(items); + newItems[index] = item; + return Equipment(items: newItems, bestIndex: bestIndex); + } + + Equipment copyWith({List? items, int? bestIndex}) { + return Equipment( + items: items ?? List.from(this.items), + bestIndex: bestIndex ?? this.bestIndex, + ); + } +} diff --git a/lib/src/core/model/game_state.dart b/lib/src/core/model/game_state.dart index d3acd71..9f5242a 100644 --- a/lib/src/core/model/game_state.dart +++ b/lib/src/core/model/game_state.dart @@ -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 lastCombatEvents; - - DeathInfo copyWith({ - DeathCause? cause, - String? killerName, - int? lostEquipmentCount, - String? lostItemName, - EquipmentSlot? lostItemSlot, - ItemRarity? lostItemRarity, - EquipmentItem? lostItem, - int? goldAtDeath, - int? levelAtDeath, - int? timestamp, - List? 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 skillStates; - - /// 현재 활성화된 버프 목록 - final List 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 = {}; - 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? skillStates, - List? 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 items; - - /// 초기 골드 1000 지급 (캐릭터 생성 시) - factory Inventory.empty() => const Inventory(gold: 1000, items: []); - - Inventory copyWith({int? gold, List? 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 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 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.from(items); - newItems[index] = item; - return Equipment(items: newItems, bestIndex: bestIndex); - } - - Equipment copyWith({List? items, int? bestIndex}) { - return Equipment( - items: items ?? List.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 skills; - - factory SkillBook.empty() => const SkillBook(skills: []); - - SkillBook copyWith({List? 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 plotHistory; - - /// 퀘스트 히스토리 (완료된/진행중인 퀘스트 목록) - final List 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? plotHistory, - List? 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? entries}) - : entries = Queue.from(entries ?? const []); - - final Queue entries; - - factory QueueState.empty() => QueueState(entries: const []); - - QueueState copyWith({Iterable? entries}) { - return QueueState(entries: Queue.from(entries ?? this.entries)); - } -} diff --git a/lib/src/core/model/inventory.dart b/lib/src/core/model/inventory.dart new file mode 100644 index 0000000..976aa0f --- /dev/null +++ b/lib/src/core/model/inventory.dart @@ -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 items; + + /// 초기 골드 1000 지급 (캐릭터 생성 시) + factory Inventory.empty() => const Inventory(gold: 1000, items: []); + + Inventory copyWith({int? gold, List? items}) { + return Inventory(gold: gold ?? this.gold, items: items ?? this.items); + } +} diff --git a/lib/src/core/model/progress_state.dart b/lib/src/core/model/progress_state.dart new file mode 100644 index 0000000..abcee9f --- /dev/null +++ b/lib/src/core/model/progress_state.dart @@ -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 plotHistory; + + /// 퀘스트 히스토리 (완료된/진행중인 퀘스트 목록) + final List 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? plotHistory, + List? 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!; + } +} diff --git a/lib/src/core/model/queue_state.dart b/lib/src/core/model/queue_state.dart new file mode 100644 index 0000000..fbede27 --- /dev/null +++ b/lib/src/core/model/queue_state.dart @@ -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? entries}) + : entries = Queue.from(entries ?? const []); + + final Queue entries; + + factory QueueState.empty() => QueueState(entries: const []); + + QueueState copyWith({Iterable? entries}) { + return QueueState(entries: Queue.from(entries ?? this.entries)); + } +} diff --git a/lib/src/core/model/skill_book.dart b/lib/src/core/model/skill_book.dart new file mode 100644 index 0000000..f193c0a --- /dev/null +++ b/lib/src/core/model/skill_book.dart @@ -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 skills; + + factory SkillBook.empty() => const SkillBook(skills: []); + + SkillBook copyWith({List? skills}) { + return SkillBook(skills: skills ?? this.skills); + } +} diff --git a/lib/src/core/model/skill_system_state.dart b/lib/src/core/model/skill_system_state.dart new file mode 100644 index 0000000..71e03bf --- /dev/null +++ b/lib/src/core/model/skill_system_state.dart @@ -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 skillStates; + + /// 현재 활성화된 버프 목록 + final List 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 = {}; + 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? skillStates, + List? 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); + } +} diff --git a/lib/src/core/model/stats.dart b/lib/src/core/model/stats.dart new file mode 100644 index 0000000..c62d050 --- /dev/null +++ b/lib/src/core/model/stats.dart @@ -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, + ); + } +} diff --git a/lib/src/core/model/task_info.dart b/lib/src/core/model/task_info.dart new file mode 100644 index 0000000..089a2a6 --- /dev/null +++ b/lib/src/core/model/task_info.dart @@ -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, + ); + } +} diff --git a/lib/src/core/model/traits.dart b/lib/src/core/model/traits.dart new file mode 100644 index 0000000..95d7621 --- /dev/null +++ b/lib/src/core/model/traits.dart @@ -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, + ); + } +}