feat: 초기 커밋

- Progress Quest 6.4 Flutter 포팅 프로젝트
- 게임 루프, 상태 관리, UI 구현
- 캐릭터 생성, 인벤토리, 장비, 주문 시스템
- 시장/판매/구매 메커니즘
This commit is contained in:
JiWoong Sul
2025-12-09 17:24:04 +09:00
commit 08054d97c1
168 changed files with 12876 additions and 0 deletions

View File

@@ -0,0 +1,33 @@
import 'package:askiineverdie/src/core/model/game_state.dart';
import 'package:askiineverdie/src/core/model/save_data.dart';
import 'package:askiineverdie/src/core/storage/save_repository.dart';
import 'package:askiineverdie/src/core/storage/save_service.dart'
show SaveFileInfo;
/// Coordinates saving/loading GameState using SaveRepository.
class SaveManager {
SaveManager(this._repo);
final SaveRepository _repo;
static const String defaultFileName = 'progress.pqf';
/// Save current game state to disk. [fileName] may be absolute or relative.
/// Returns outcome with error on failure.
Future<SaveOutcome> saveState(GameState state, {String? fileName}) {
final save = GameSave.fromState(state);
return _repo.save(save, fileName ?? defaultFileName);
}
/// Load game state from disk. [fileName] may be absolute (e.g., file picker).
/// Returns outcome + optional state.
Future<(SaveOutcome, GameState?)> loadState({String? fileName}) async {
final (outcome, save) = await _repo.load(fileName ?? defaultFileName);
if (!outcome.success || save == null) {
return (outcome, null);
}
return (outcome, save.toState());
}
/// 저장 파일 목록 조회
Future<List<SaveFileInfo>> listSaves() => _repo.listSaves();
}

View File

@@ -0,0 +1,64 @@
import 'dart:io';
import 'package:askiineverdie/src/core/model/save_data.dart';
import 'package:askiineverdie/src/core/storage/save_service.dart';
import 'package:path_provider/path_provider.dart';
class SaveOutcome {
const SaveOutcome.success([this.error]) : success = true;
const SaveOutcome.failure(this.error) : success = false;
final bool success;
final String? error;
}
/// High-level save/load wrapper that resolves platform storage paths.
class SaveRepository {
SaveRepository() : _service = null;
SaveService? _service;
Future<void> _ensureService() async {
if (_service != null) return;
final dir = await getApplicationSupportDirectory();
_service = SaveService(baseDir: dir);
}
Future<SaveOutcome> save(GameSave save, String fileName) async {
try {
await _ensureService();
await _service!.save(save, fileName);
return const SaveOutcome.success();
} on FileSystemException catch (e) {
final reason = e.osError?.message ?? e.message;
return SaveOutcome.failure('Unable to save file: $reason');
} catch (e) {
return SaveOutcome.failure(e.toString());
}
}
Future<(SaveOutcome, GameSave?)> load(String fileName) async {
try {
await _ensureService();
final data = await _service!.load(fileName);
return (const SaveOutcome.success(), data);
} on FileSystemException catch (e) {
final reason = e.osError?.message ?? e.message;
return (SaveOutcome.failure('Unable to load save: $reason'), null);
} on FormatException catch (e) {
return (SaveOutcome.failure('Corrupted save file: ${e.message}'), null);
} catch (e) {
return (SaveOutcome.failure(e.toString()), null);
}
}
/// 저장 파일 목록 조회
Future<List<SaveFileInfo>> listSaves() async {
try {
await _ensureService();
return await _service!.listSaves();
} catch (e) {
return [];
}
}
}

View File

@@ -0,0 +1,84 @@
import 'dart:convert';
import 'dart:io';
import 'package:askiineverdie/src/core/model/save_data.dart';
/// Persists GameSave as JSON compressed with GZipCodec.
class SaveService {
SaveService({required this.baseDir});
final Directory baseDir;
final GZipCodec _gzip = GZipCodec();
Future<File> save(GameSave save, String fileName) async {
final path = _resolvePath(fileName);
final file = File(path);
await file.parent.create(recursive: true);
final jsonStr = jsonEncode(save.toJson());
final bytes = utf8.encode(jsonStr);
final compressed = _gzip.encode(bytes);
return file.writeAsBytes(compressed);
}
Future<GameSave> load(String fileName) async {
final path = _resolvePath(fileName);
final file = File(path);
final compressed = await file.readAsBytes();
final decompressed = _gzip.decode(compressed);
final jsonStr = utf8.decode(decompressed);
final map = jsonDecode(jsonStr) as Map<String, dynamic>;
return GameSave.fromJson(map);
}
String _resolvePath(String fileName) {
final normalized = fileName.endsWith('.pqf') ? fileName : '$fileName.pqf';
final file = File(normalized);
if (file.isAbsolute) return file.path;
return '${baseDir.path}/$normalized';
}
/// 저장 디렉토리의 모든 .pqf 파일 목록 반환
Future<List<SaveFileInfo>> listSaves() async {
if (!await baseDir.exists()) {
return [];
}
final files = <SaveFileInfo>[];
await for (final entity in baseDir.list()) {
if (entity is File && entity.path.endsWith('.pqf')) {
final stat = await entity.stat();
final name = entity.uri.pathSegments.last;
files.add(
SaveFileInfo(
fileName: name,
fullPath: entity.path,
modifiedAt: stat.modified,
sizeBytes: stat.size,
),
);
}
}
// 최근 수정된 파일 순으로 정렬
files.sort((a, b) => b.modifiedAt.compareTo(a.modifiedAt));
return files;
}
}
/// 저장 파일 정보
class SaveFileInfo {
const SaveFileInfo({
required this.fileName,
required this.fullPath,
required this.modifiedAt,
required this.sizeBytes,
});
final String fileName;
final String fullPath;
final DateTime modifiedAt;
final int sizeBytes;
/// 확장자 없는 표시용 이름
String get displayName => fileName.replaceAll('.pqf', '');
}