From eba0521ffecf79b9b1df1b16af612eea0e9ee5cd Mon Sep 17 00:00:00 2001 From: JiWoong Sul Date: Wed, 14 Jan 2026 02:26:18 +0900 Subject: [PATCH] =?UTF-8?q?refactor(core):=20=EC=95=A0=EB=8B=88=EB=A9=94?= =?UTF-8?q?=EC=9D=B4=EC=85=98=20=EC=BB=B4=ED=8F=AC=EC=A0=80=20=EB=B0=8F=20?= =?UTF-8?q?=EC=A0=80=EC=9E=A5=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CanvasWalkingComposer 정리 - SaveData 모델 확장 --- .../canvas/canvas_walking_composer.dart | 16 +--- lib/src/core/model/save_data.dart | 92 +++++++++++++------ 2 files changed, 70 insertions(+), 38 deletions(-) diff --git a/lib/src/core/animation/canvas/canvas_walking_composer.dart b/lib/src/core/animation/canvas/canvas_walking_composer.dart index fb617ce..a2ba285 100644 --- a/lib/src/core/animation/canvas/canvas_walking_composer.dart +++ b/lib/src/core/animation/canvas/canvas_walking_composer.dart @@ -89,19 +89,11 @@ class CanvasWalkingComposer { } /// idle 프레임 기반 걷기 애니메이션 생성 - /// 머리와 몸통은 유지, 다리만 걷는 동작으로 변경 + /// 종족별 다리 모양을 유지 (idle 프레임이 4개라 자연스럽게 변화) List _animateWalking(List idleLines, int frameIndex) { - if (idleLines.length < 3) return idleLines; - - // 머리(0)와 몸통(1)은 그대로 유지 - final head = idleLines[0]; - final body = idleLines[1]; - - // 다리 애니메이션 (4프레임) - 걷기 동작 - const legFrames = [' /| ', ' |\\ ', ' /| ', ' |\\ ']; - final legs = legFrames[frameIndex % legFrames.length]; - - return [head, body, legs]; + // idle 프레임을 그대로 사용 (종족별 다리 모양 유지) + // frameIndex에 따라 idle[0~3] 중 하나가 선택되어 자연스럽게 애니메이션됨 + return idleLines; } /// 문자열 스프라이트를 AsciiCell 2D 배열로 변환 diff --git a/lib/src/core/model/save_data.dart b/lib/src/core/model/save_data.dart index 9f3aaa5..eb16d81 100644 --- a/lib/src/core/model/save_data.dart +++ b/lib/src/core/model/save_data.dart @@ -1,9 +1,15 @@ import 'dart:collection'; +import 'package:asciineverdie/src/core/model/equipment_item.dart'; +import 'package:asciineverdie/src/core/model/equipment_slot.dart'; +import 'package:asciineverdie/src/core/model/monster_grade.dart'; import 'package:asciineverdie/src/core/util/deterministic_random.dart'; import 'package:asciineverdie/src/core/model/game_state.dart'; -const int kSaveVersion = 2; +/// 세이브 파일 버전 +/// - v2: 장비 이름만 저장 (레거시) +/// - v3: 장비 전체 정보 저장 (level, rarity, stats 포함) +const int kSaveVersion = 3; class GameSave { GameSave({ @@ -79,17 +85,7 @@ class GameSave { .toList(), }, 'equipment': { - 'weapon': equipment.weapon, - 'shield': equipment.shield, - 'helm': equipment.helm, - 'hauberk': equipment.hauberk, - 'brassairts': equipment.brassairts, - 'vambraces': equipment.vambraces, - 'gauntlets': equipment.gauntlets, - 'gambeson': equipment.gambeson, - 'cuisses': equipment.cuisses, - 'greaves': equipment.greaves, - 'sollerets': equipment.sollerets, + 'items': equipment.items.map((e) => e.toJson()).toList(), 'bestIndex': equipment.bestIndex, }, 'skills': skillBook.skills @@ -106,6 +102,8 @@ class GameSave { 'type': progress.currentTask.type.name, 'monsterBaseName': progress.currentTask.monsterBaseName, 'monsterPart': progress.currentTask.monsterPart, + 'monsterLevel': progress.currentTask.monsterLevel, + 'monsterGrade': progress.currentTask.monsterGrade?.name, }, 'plotStages': progress.plotStageCount, 'questCount': progress.questCount, @@ -183,20 +181,7 @@ class GameSave { ) .toList(), ), - equipment: Equipment.fromStrings( - weapon: equipmentJson['weapon'] as String? ?? 'Keyboard', - shield: equipmentJson['shield'] as String? ?? '', - helm: equipmentJson['helm'] as String? ?? '', - hauberk: equipmentJson['hauberk'] as String? ?? '', - brassairts: equipmentJson['brassairts'] as String? ?? '', - vambraces: equipmentJson['vambraces'] as String? ?? '', - gauntlets: equipmentJson['gauntlets'] as String? ?? '', - gambeson: equipmentJson['gambeson'] as String? ?? '', - cuisses: equipmentJson['cuisses'] as String? ?? '', - greaves: equipmentJson['greaves'] as String? ?? '', - sollerets: equipmentJson['sollerets'] as String? ?? '', - bestIndex: equipmentJson['bestIndex'] as int? ?? 0, - ), + equipment: _equipmentFromJson(equipmentJson, json['version'] as int? ?? 2), skillBook: SkillBook( skills: skillsJson .map( @@ -289,11 +274,23 @@ TaskInfo _taskInfoFromJson(Map json) { (t) => t.name == typeName, orElse: () => TaskType.neutral, ); + + // monsterGrade 파싱 + final gradeName = json['monsterGrade'] as String?; + final monsterGrade = gradeName != null + ? MonsterGrade.values.firstWhere( + (g) => g.name == gradeName, + orElse: () => MonsterGrade.normal, + ) + : null; + return TaskInfo( caption: json['caption'] as String? ?? '', type: type, monsterBaseName: json['monsterBaseName'] as String?, monsterPart: json['monsterPart'] as String?, + monsterLevel: json['monsterLevel'] as int?, + monsterGrade: monsterGrade, ); } @@ -315,3 +312,46 @@ QuestMonsterInfo? _questMonsterFromJson(Map? json) { monsterIndex: json['index'] as int? ?? -1, ); } + +/// 장비 데이터 역직렬화 (버전별 분기 처리) +/// +/// - v2 이하: 레거시 문자열 기반 (이름만 저장) +/// - v3 이상: 전체 EquipmentItem 정보 저장 +Equipment _equipmentFromJson(Map json, int version) { + // v3 이상: 새로운 형식 (items 배열) + if (version >= 3 && json['items'] != null) { + final itemsList = json['items'] as List; + final items = []; + + for (var i = 0; i < Equipment.slotCount; i++) { + if (i < itemsList.length) { + final itemJson = itemsList[i] as Map; + items.add(EquipmentItem.fromJson(itemJson)); + } else { + // 누락된 슬롯은 빈 아이템으로 채움 + items.add(EquipmentItem.empty(EquipmentSlot.values[i])); + } + } + + return Equipment( + items: items, + bestIndex: json['bestIndex'] as int? ?? 0, + ); + } + + // v2 이하: 레거시 형식 (문자열 기반) + return Equipment.fromStrings( + weapon: json['weapon'] as String? ?? 'Keyboard', + shield: json['shield'] as String? ?? '', + helm: json['helm'] as String? ?? '', + hauberk: json['hauberk'] as String? ?? '', + brassairts: json['brassairts'] as String? ?? '', + vambraces: json['vambraces'] as String? ?? '', + gauntlets: json['gauntlets'] as String? ?? '', + gambeson: json['gambeson'] as String? ?? '', + cuisses: json['cuisses'] as String? ?? '', + greaves: json['greaves'] as String? ?? '', + sollerets: json['sollerets'] as String? ?? '', + bestIndex: json['bestIndex'] as int? ?? 0, + ); +}