feat: 초기 커밋
- Progress Quest 6.4 Flutter 포팅 프로젝트 - 게임 루프, 상태 관리, UI 구현 - 캐릭터 생성, 인벤토리, 장비, 주문 시스템 - 시장/판매/구매 메커니즘
46
.gitignore
vendored
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
# Miscellaneous
|
||||||
|
*.class
|
||||||
|
.cursor/
|
||||||
|
*.log
|
||||||
|
*.pyc
|
||||||
|
*.swp
|
||||||
|
.DS_Store
|
||||||
|
.atom/
|
||||||
|
.build/
|
||||||
|
.buildlog/
|
||||||
|
.history
|
||||||
|
.svn/
|
||||||
|
.swiftpm/
|
||||||
|
migrate_working_dir/
|
||||||
|
|
||||||
|
# IntelliJ related
|
||||||
|
*.iml
|
||||||
|
*.ipr
|
||||||
|
*.iws
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# The .vscode folder contains launch configuration and tasks you configure in
|
||||||
|
# VS Code which you may wish to be included in version control, so this line
|
||||||
|
# is commented out by default.
|
||||||
|
#.vscode/
|
||||||
|
|
||||||
|
# Flutter/Dart/Pub related
|
||||||
|
**/doc/api/
|
||||||
|
**/ios/Flutter/.last_build_id
|
||||||
|
.dart_tool/
|
||||||
|
.flutter-plugins
|
||||||
|
.flutter-plugins-dependencies
|
||||||
|
.pub-cache/
|
||||||
|
.pub/
|
||||||
|
/build/
|
||||||
|
|
||||||
|
# Symbolication related
|
||||||
|
app.*.symbols
|
||||||
|
|
||||||
|
# Obfuscation related
|
||||||
|
app.*.map.json
|
||||||
|
|
||||||
|
# Android Studio will place build artifacts here
|
||||||
|
/android/app/debug
|
||||||
|
/android/app/profile
|
||||||
|
/android/app/release
|
||||||
45
.metadata
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
# This file tracks properties of this Flutter project.
|
||||||
|
# Used by Flutter tool to assess capabilities and perform upgrades etc.
|
||||||
|
#
|
||||||
|
# This file should be version controlled and should not be manually edited.
|
||||||
|
|
||||||
|
version:
|
||||||
|
revision: "a402d9a4376add5bc2d6b1e33e53edaae58c07f8"
|
||||||
|
channel: "stable"
|
||||||
|
|
||||||
|
project_type: app
|
||||||
|
|
||||||
|
# Tracks metadata for the flutter migrate command
|
||||||
|
migration:
|
||||||
|
platforms:
|
||||||
|
- platform: root
|
||||||
|
create_revision: a402d9a4376add5bc2d6b1e33e53edaae58c07f8
|
||||||
|
base_revision: a402d9a4376add5bc2d6b1e33e53edaae58c07f8
|
||||||
|
- platform: android
|
||||||
|
create_revision: a402d9a4376add5bc2d6b1e33e53edaae58c07f8
|
||||||
|
base_revision: a402d9a4376add5bc2d6b1e33e53edaae58c07f8
|
||||||
|
- platform: ios
|
||||||
|
create_revision: a402d9a4376add5bc2d6b1e33e53edaae58c07f8
|
||||||
|
base_revision: a402d9a4376add5bc2d6b1e33e53edaae58c07f8
|
||||||
|
- platform: linux
|
||||||
|
create_revision: a402d9a4376add5bc2d6b1e33e53edaae58c07f8
|
||||||
|
base_revision: a402d9a4376add5bc2d6b1e33e53edaae58c07f8
|
||||||
|
- platform: macos
|
||||||
|
create_revision: a402d9a4376add5bc2d6b1e33e53edaae58c07f8
|
||||||
|
base_revision: a402d9a4376add5bc2d6b1e33e53edaae58c07f8
|
||||||
|
- platform: web
|
||||||
|
create_revision: a402d9a4376add5bc2d6b1e33e53edaae58c07f8
|
||||||
|
base_revision: a402d9a4376add5bc2d6b1e33e53edaae58c07f8
|
||||||
|
- platform: windows
|
||||||
|
create_revision: a402d9a4376add5bc2d6b1e33e53edaae58c07f8
|
||||||
|
base_revision: a402d9a4376add5bc2d6b1e33e53edaae58c07f8
|
||||||
|
|
||||||
|
# User provided section
|
||||||
|
|
||||||
|
# List of Local paths (relative to this file) that should be
|
||||||
|
# ignored by the migrate tool.
|
||||||
|
#
|
||||||
|
# Files that are not part of the templates will be ignored by default.
|
||||||
|
unmanaged_files:
|
||||||
|
- 'lib/main.dart'
|
||||||
|
- 'ios/Runner.xcodeproj/project.pbxproj'
|
||||||
59
AGENTS.md
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
# Codex Agent Guide — Progress Quest Flutter Port
|
||||||
|
|
||||||
|
## Scope & Precedence
|
||||||
|
- Applies to this repository (`askiineverdie`) unless a more specific rule exists deeper in the tree.
|
||||||
|
- Order of authority: system/developer messages > this AGENTS.md > other inherited defaults.
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
- Recreate the Progress Quest 6.4 single-player experience from `example/pq` in Flutter with identical gameplay, logic, and data.
|
||||||
|
- Run completely offline: no network calls, server selection, guild/brag uploads, or browser launches.
|
||||||
|
- Do not alter original algorithms or data (monsters, items, spells, modifiers, etc.). If a change is required, explain why and obtain user approval first.
|
||||||
|
|
||||||
|
## Guardrails
|
||||||
|
- Workspace-only edits; avoid destructive rewrites unless explicitly requested.
|
||||||
|
- Dependency or network changes (e.g., `pubspec.yaml`, new packages, online access) require prior user approval.
|
||||||
|
- Keep the app offline: remove/disable HTTP, web views, and external browser calls.
|
||||||
|
- Responses are in Korean by default; if uncertain, prefix with `Uncertain:` and list only the top two options.
|
||||||
|
- For multi-step work, use `update_plan` with exactly one `in_progress` step at a time.
|
||||||
|
- When following checklist documents, update checkboxes immediately as progress is made.
|
||||||
|
|
||||||
|
## Git / Commit / Review Comments
|
||||||
|
- Branch naming: `codex/<type>-<slug>` (e.g., `codex/feat-seed-loader`).
|
||||||
|
- Commit messages: Conventional Commit format `type(scope): summary`; summaries primarily in Korean (technical terms in English as needed).
|
||||||
|
- Review/report notes: present code/logs/commands first, followed by brief rationale. Use `Uncertain:` if unsure.
|
||||||
|
- After git push, share reports/descriptions in Korean.
|
||||||
|
|
||||||
|
## Coding Standards
|
||||||
|
- Language/Tools: Dart/Flutter; port original algorithms directly—no speculative optimizations or logic refactors.
|
||||||
|
- Data: keep extracted datasets (e.g., `Config.dfm`) 1:1 with the source; no ad-hoc edits.
|
||||||
|
- Style: two-space indentation; run `dart format .`; use meaningful names; avoid unnecessary comments.
|
||||||
|
- Structure: preserve standard Flutter structure; shared constants/data may live in `lib/data`, but logic must stay faithful to the original.
|
||||||
|
- Tests: add/update unit/widget tests when behavior changes, ensuring original logic is preserved.
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
- Prefer to run before handoff:
|
||||||
|
- `dart format --set-exit-if-changed .`
|
||||||
|
- `flutter analyze`
|
||||||
|
- `flutter test` (when tests exist)
|
||||||
|
|
||||||
|
## Sensitive Areas (require approval)
|
||||||
|
- Dependency graph changes (`pubspec.yaml`), platform build settings (Android/iOS/desktop), Gradle/Xcode configs, signing/provisioning.
|
||||||
|
- Introducing network access, modifying original data/algorithms, large file deletions, or repository restructuring.
|
||||||
|
|
||||||
|
## Communication
|
||||||
|
- Keep summaries concise; list code/logs/commands first, then short reasoning.
|
||||||
|
- For UI changes, briefly describe visual impact (layout/color/interaction).
|
||||||
|
- Offer numbered next-step options only when useful; otherwise, simply report completion.
|
||||||
|
|
||||||
|
## Architecture Discipline
|
||||||
|
- Preserve Single Responsibility Principle and Clean Architecture boundaries:
|
||||||
|
- Presentation → Domain → Data dependency flow only; never depend upward.
|
||||||
|
- Domain stays framework-agnostic; no Flutter/UI imports.
|
||||||
|
- Data must not reference presentation; convert DTOs before exposing.
|
||||||
|
- Split oversized files/functions; avoid “god” widgets or services.
|
||||||
|
|
||||||
|
## Notification
|
||||||
|
- Before final handoff, run the notification script when possible:
|
||||||
|
- Requires macOS `terminal-notifier`.
|
||||||
|
- Usage: `python3 /Users/maximilian.j.sul/.codex/notify.py '<json_payload>'`
|
||||||
|
- Example payload: `{"type":"agent-turn-complete","input_messages":["..."],"last-assistant-message":"..."}`
|
||||||
119
CLAUDE.md
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## 프로젝트 개요
|
||||||
|
|
||||||
|
Askii Never Die는 Progress Quest 6.4 (Delphi 원본)를 Flutter로 100% 동일하게 복제하는 오프라인 싱글플레이어 RPG입니다. 네트워크 기능은 모두 제외되며, 원본 알고리즘과 데이터를 그대로 유지해야 합니다.
|
||||||
|
|
||||||
|
## 빌드 및 실행
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 의존성 설치
|
||||||
|
flutter pub get
|
||||||
|
|
||||||
|
# 실행 (플랫폼 지정 가능: -d macos, -d chrome 등)
|
||||||
|
flutter run
|
||||||
|
|
||||||
|
# 핸드오프 전 필수 검증
|
||||||
|
dart format --set-exit-if-changed .
|
||||||
|
flutter analyze
|
||||||
|
flutter test
|
||||||
|
```
|
||||||
|
|
||||||
|
## 아키텍처
|
||||||
|
|
||||||
|
### 디렉토리 구조
|
||||||
|
|
||||||
|
```
|
||||||
|
lib/
|
||||||
|
├── main.dart # 앱 진입점
|
||||||
|
├── data/pq_config_data.dart # PQ 정적 데이터 (Config.dfm 추출)
|
||||||
|
└── src/
|
||||||
|
├── app.dart # MaterialApp 설정
|
||||||
|
├── core/
|
||||||
|
│ ├── engine/ # 게임 루프 및 진행 로직
|
||||||
|
│ │ ├── progress_loop.dart # 타이머 기반 메인 루프 (원본 200ms)
|
||||||
|
│ │ ├── progress_service.dart # 틱 처리, 경험치/레벨업 로직
|
||||||
|
│ │ ├── game_mutations.dart # 상태 변경 함수
|
||||||
|
│ │ └── reward_service.dart # 보상 처리
|
||||||
|
│ ├── model/
|
||||||
|
│ │ ├── game_state.dart # 핵심 상태: Traits, Stats, Inventory, Equipment, SpellBook, ProgressState, QueueState
|
||||||
|
│ │ ├── pq_config.dart # Config 데이터 접근
|
||||||
|
│ │ ├── equipment_slot.dart # 장비 슬롯 정의
|
||||||
|
│ │ └── save_data.dart # 저장 데이터 구조
|
||||||
|
│ ├── storage/ # 세이브 파일 처리
|
||||||
|
│ └── util/
|
||||||
|
│ ├── deterministic_random.dart # 결정론적 RNG (재현 가능)
|
||||||
|
│ ├── pq_logic.dart # 원본 로직 포팅 (odds, randSign 등)
|
||||||
|
│ └── roman.dart # 로마 숫자 변환
|
||||||
|
└── features/
|
||||||
|
├── front/front_screen.dart # 임시 프론트 화면
|
||||||
|
└── game/game_session_controller.dart # 게임 세션 관리
|
||||||
|
|
||||||
|
example/pq/ # Delphi 원본 소스 (참조용, 빌드 대상 아님)
|
||||||
|
test/ # 단위/위젯 테스트
|
||||||
|
```
|
||||||
|
|
||||||
|
### 레이어 구조 (Clean Architecture)
|
||||||
|
|
||||||
|
- **Presentation** (`features/`) → **Domain** (`core/model/`, `core/engine/`) → **Data** (`data/`, `core/storage/`)
|
||||||
|
- 역방향 의존 금지: Domain은 Flutter/UI import 불가
|
||||||
|
|
||||||
|
### 핵심 데이터 흐름
|
||||||
|
|
||||||
|
1. `ProgressLoop`가 타이머로 `tickOnce()` 호출
|
||||||
|
2. `ProgressService.tick()`이 GameState 업데이트
|
||||||
|
3. GameState 스트림으로 UI 갱신
|
||||||
|
4. 레벨업/퀘스트 완료 시 `SaveManager` 자동 저장
|
||||||
|
|
||||||
|
## 핵심 규칙
|
||||||
|
|
||||||
|
### 원본 충실도
|
||||||
|
- `example/pq/` 내 Delphi 소스의 알고리즘/데이터를 100% 동일하게 포팅
|
||||||
|
- 원본 로직 변경 필요 시 반드시 사용자 승인 필요
|
||||||
|
- 새로운 기능, 값, 처리 로직 추가 금지 (디버깅 로그 예외)
|
||||||
|
|
||||||
|
### 데이터 관리
|
||||||
|
- 정적 데이터(몬스터, 아이템, 주문 등)는 `Config.dfm`에서 추출하여 JSON/Dart const로 관리
|
||||||
|
- 영문 원문 기준 작성, UI 텍스트는 i18n 구조로 분리
|
||||||
|
- 이미지 파일 미사용
|
||||||
|
|
||||||
|
### 코딩 표준
|
||||||
|
- 2-space 인덴트, `dart format` 준수
|
||||||
|
- 타입 명시적 선언, `any`/`dynamic` 지양
|
||||||
|
- 파일명: snake_case, 클래스: PascalCase, 변수/함수: camelCase
|
||||||
|
- 파일당 200 LOC 이하, 함수 20 라인 이하 권장
|
||||||
|
- SRP(Single Responsibility Principle) 준수
|
||||||
|
|
||||||
|
### 화면 구성
|
||||||
|
- 2개 화면만 사용: 캐릭터 생성 화면, 게임 진행 화면
|
||||||
|
- 화면 내 요소는 위젯 단위로 분리
|
||||||
|
|
||||||
|
## 원본 소스 참조 (example/pq/)
|
||||||
|
|
||||||
|
| 파일 | 핵심 함수/라인 | 역할 |
|
||||||
|
|------|----------------|------|
|
||||||
|
| `Main.pas:523-1040` | `MonsterTask` | 전투/전리품/레벨업 |
|
||||||
|
| `Main.pas:267-424` | `StartTimer` | 메인 게임 루프 |
|
||||||
|
| `Main.pas:456-521` | `InterplotCinematic`, `NamedMonster` | 시네마틱/명명 |
|
||||||
|
| `NewGuy.pas:55-68` | `RerollClick` | 캐릭터 생성 (3d6) |
|
||||||
|
| `Config.dfm` | TMemo 데이터 | 주문, 몬스터, 아이템, 종족, 직업 등 |
|
||||||
|
|
||||||
|
## 승인 필요 사항
|
||||||
|
|
||||||
|
- `pubspec.yaml` 의존성 변경
|
||||||
|
- 플랫폼 빌드 설정 (Android/iOS/desktop)
|
||||||
|
- 네트워크 접근 도입
|
||||||
|
- 원본 데이터/알고리즘 수정
|
||||||
|
- 대규모 파일 삭제 또는 구조 변경
|
||||||
|
|
||||||
|
## 커밋 규칙
|
||||||
|
|
||||||
|
```
|
||||||
|
type(scope): 한국어 설명
|
||||||
|
|
||||||
|
- 변경 상세 내용
|
||||||
|
```
|
||||||
|
|
||||||
|
Types: `feat`, `fix`, `refactor`, `test`, `docs`, `style`, `chore`, `perf`
|
||||||
25
README.md
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
# Ascii Never Die
|
||||||
|
|
||||||
|
Offline Flutter rebuild of **Progress Quest 6.4** (single-player only). Network features are stripped; all game data and saves live locally.
|
||||||
|
|
||||||
|
## Layout
|
||||||
|
- `lib/src/features/front/` – temporary front screen shell to hang the upcoming flow on.
|
||||||
|
- `doc/progress-quest-flutter-plan.md` – working plan/notes for the port.
|
||||||
|
- `example/pq/` – original Delphi source/assets (reference only, not built).
|
||||||
|
|
||||||
|
## Run
|
||||||
|
```bash
|
||||||
|
flutter pub get
|
||||||
|
flutter run
|
||||||
|
```
|
||||||
|
|
||||||
|
Use any supported platform (`-d macos`, `-d chrome`, etc.); multi-platform scaffolding is enabled.
|
||||||
|
|
||||||
|
## Checks
|
||||||
|
Run from repo root before handoff:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dart format --set-exit-if-changed .
|
||||||
|
flutter analyze
|
||||||
|
flutter test
|
||||||
|
```
|
||||||
26
analysis_options.yaml
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# This file configures the analyzer, which statically analyzes Dart code to
|
||||||
|
# check for errors, warnings, and lints.
|
||||||
|
#
|
||||||
|
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
|
||||||
|
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
|
||||||
|
# invoked from the command line by running `flutter analyze`.
|
||||||
|
|
||||||
|
# The following line activates a set of recommended lints for Flutter apps,
|
||||||
|
# packages, and plugins designed to encourage good coding practices.
|
||||||
|
include: package:flutter_lints/flutter.yaml
|
||||||
|
|
||||||
|
analyzer:
|
||||||
|
language:
|
||||||
|
strict-casts: true
|
||||||
|
strict-inference: true
|
||||||
|
strict-raw-types: true
|
||||||
|
|
||||||
|
linter:
|
||||||
|
# Keep the rule set lean; we will tighten as the engine port stabilizes.
|
||||||
|
rules:
|
||||||
|
always_use_package_imports: true
|
||||||
|
avoid_print: true
|
||||||
|
prefer_single_quotes: true
|
||||||
|
|
||||||
|
# Additional information about this file can be found at
|
||||||
|
# https://dart.dev/guides/language/analysis-options
|
||||||
14
android/.gitignore
vendored
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
gradle-wrapper.jar
|
||||||
|
/.gradle
|
||||||
|
/captures/
|
||||||
|
/gradlew
|
||||||
|
/gradlew.bat
|
||||||
|
/local.properties
|
||||||
|
GeneratedPluginRegistrant.java
|
||||||
|
.cxx/
|
||||||
|
|
||||||
|
# Remember to never publicly share your keystore.
|
||||||
|
# See https://flutter.dev/to/reference-keystore
|
||||||
|
key.properties
|
||||||
|
**/*.keystore
|
||||||
|
**/*.jks
|
||||||
44
android/app/build.gradle.kts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
plugins {
|
||||||
|
id("com.android.application")
|
||||||
|
id("kotlin-android")
|
||||||
|
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
|
||||||
|
id("dev.flutter.flutter-gradle-plugin")
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "com.example.askiineverdie"
|
||||||
|
compileSdk = flutter.compileSdkVersion
|
||||||
|
ndkVersion = flutter.ndkVersion
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_11
|
||||||
|
targetCompatibility = JavaVersion.VERSION_11
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlinOptions {
|
||||||
|
jvmTarget = JavaVersion.VERSION_11.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||||
|
applicationId = "com.example.askiineverdie"
|
||||||
|
// You can update the following values to match your application needs.
|
||||||
|
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
||||||
|
minSdk = flutter.minSdkVersion
|
||||||
|
targetSdk = flutter.targetSdkVersion
|
||||||
|
versionCode = flutter.versionCode
|
||||||
|
versionName = flutter.versionName
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
// TODO: Add your own signing config for the release build.
|
||||||
|
// Signing with the debug keys for now, so `flutter run --release` works.
|
||||||
|
signingConfig = signingConfigs.getByName("debug")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
flutter {
|
||||||
|
source = "../.."
|
||||||
|
}
|
||||||
7
android/app/src/debug/AndroidManifest.xml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<!-- The INTERNET permission is required for development. Specifically,
|
||||||
|
the Flutter tool needs it to communicate with the running application
|
||||||
|
to allow setting breakpoints, to provide hot reload, etc.
|
||||||
|
-->
|
||||||
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
|
</manifest>
|
||||||
45
android/app/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<application
|
||||||
|
android:label="askiineverdie"
|
||||||
|
android:name="${applicationName}"
|
||||||
|
android:icon="@mipmap/ic_launcher">
|
||||||
|
<activity
|
||||||
|
android:name=".MainActivity"
|
||||||
|
android:exported="true"
|
||||||
|
android:launchMode="singleTop"
|
||||||
|
android:taskAffinity=""
|
||||||
|
android:theme="@style/LaunchTheme"
|
||||||
|
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||||
|
android:hardwareAccelerated="true"
|
||||||
|
android:windowSoftInputMode="adjustResize">
|
||||||
|
<!-- Specifies an Android theme to apply to this Activity as soon as
|
||||||
|
the Android process has started. This theme is visible to the user
|
||||||
|
while the Flutter UI initializes. After that, this theme continues
|
||||||
|
to determine the Window background behind the Flutter UI. -->
|
||||||
|
<meta-data
|
||||||
|
android:name="io.flutter.embedding.android.NormalTheme"
|
||||||
|
android:resource="@style/NormalTheme"
|
||||||
|
/>
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN"/>
|
||||||
|
<category android:name="android.intent.category.LAUNCHER"/>
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
<!-- Don't delete the meta-data below.
|
||||||
|
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||||
|
<meta-data
|
||||||
|
android:name="flutterEmbedding"
|
||||||
|
android:value="2" />
|
||||||
|
</application>
|
||||||
|
<!-- Required to query activities that can process text, see:
|
||||||
|
https://developer.android.com/training/package-visibility and
|
||||||
|
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
|
||||||
|
|
||||||
|
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
|
||||||
|
<queries>
|
||||||
|
<intent>
|
||||||
|
<action android:name="android.intent.action.PROCESS_TEXT"/>
|
||||||
|
<data android:mimeType="text/plain"/>
|
||||||
|
</intent>
|
||||||
|
</queries>
|
||||||
|
</manifest>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package com.example.askiineverdie
|
||||||
|
|
||||||
|
import io.flutter.embedding.android.FlutterActivity
|
||||||
|
|
||||||
|
class MainActivity : FlutterActivity()
|
||||||
12
android/app/src/main/res/drawable-v21/launch_background.xml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Modify this file to customize your launch splash screen -->
|
||||||
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:drawable="?android:colorBackground" />
|
||||||
|
|
||||||
|
<!-- You can insert your own image assets here -->
|
||||||
|
<!-- <item>
|
||||||
|
<bitmap
|
||||||
|
android:gravity="center"
|
||||||
|
android:src="@mipmap/launch_image" />
|
||||||
|
</item> -->
|
||||||
|
</layer-list>
|
||||||
12
android/app/src/main/res/drawable/launch_background.xml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Modify this file to customize your launch splash screen -->
|
||||||
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:drawable="@android:color/white" />
|
||||||
|
|
||||||
|
<!-- You can insert your own image assets here -->
|
||||||
|
<!-- <item>
|
||||||
|
<bitmap
|
||||||
|
android:gravity="center"
|
||||||
|
android:src="@mipmap/launch_image" />
|
||||||
|
</item> -->
|
||||||
|
</layer-list>
|
||||||
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 544 B |
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 442 B |
BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 721 B |
BIN
android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
18
android/app/src/main/res/values-night/styles.xml
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
|
||||||
|
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||||
|
<!-- Show a splash screen on the activity. Automatically removed when
|
||||||
|
the Flutter engine draws its first frame -->
|
||||||
|
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||||
|
</style>
|
||||||
|
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||||
|
This theme determines the color of the Android Window while your
|
||||||
|
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||||
|
running.
|
||||||
|
|
||||||
|
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||||
|
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||||
|
<item name="android:windowBackground">?android:colorBackground</item>
|
||||||
|
</style>
|
||||||
|
</resources>
|
||||||
18
android/app/src/main/res/values/styles.xml
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
|
||||||
|
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||||
|
<!-- Show a splash screen on the activity. Automatically removed when
|
||||||
|
the Flutter engine draws its first frame -->
|
||||||
|
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||||
|
</style>
|
||||||
|
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||||
|
This theme determines the color of the Android Window while your
|
||||||
|
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||||
|
running.
|
||||||
|
|
||||||
|
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||||
|
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||||
|
<item name="android:windowBackground">?android:colorBackground</item>
|
||||||
|
</style>
|
||||||
|
</resources>
|
||||||
7
android/app/src/profile/AndroidManifest.xml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<!-- The INTERNET permission is required for development. Specifically,
|
||||||
|
the Flutter tool needs it to communicate with the running application
|
||||||
|
to allow setting breakpoints, to provide hot reload, etc.
|
||||||
|
-->
|
||||||
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
|
</manifest>
|
||||||
24
android/build.gradle.kts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
allprojects {
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val newBuildDir: Directory =
|
||||||
|
rootProject.layout.buildDirectory
|
||||||
|
.dir("../../build")
|
||||||
|
.get()
|
||||||
|
rootProject.layout.buildDirectory.value(newBuildDir)
|
||||||
|
|
||||||
|
subprojects {
|
||||||
|
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
|
||||||
|
project.layout.buildDirectory.value(newSubprojectBuildDir)
|
||||||
|
}
|
||||||
|
subprojects {
|
||||||
|
project.evaluationDependsOn(":app")
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.register<Delete>("clean") {
|
||||||
|
delete(rootProject.layout.buildDirectory)
|
||||||
|
}
|
||||||
663
android/build/reports/problems/problems-report.html
Normal file
3
android/gradle.properties
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
|
||||||
|
android.useAndroidX=true
|
||||||
|
android.enableJetifier=true
|
||||||
5
android/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
distributionBase=GRADLE_USER_HOME
|
||||||
|
distributionPath=wrapper/dists
|
||||||
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
zipStorePath=wrapper/dists
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-all.zip
|
||||||
26
android/settings.gradle.kts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
pluginManagement {
|
||||||
|
val flutterSdkPath =
|
||||||
|
run {
|
||||||
|
val properties = java.util.Properties()
|
||||||
|
file("local.properties").inputStream().use { properties.load(it) }
|
||||||
|
val flutterSdkPath = properties.getProperty("flutter.sdk")
|
||||||
|
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
|
||||||
|
flutterSdkPath
|
||||||
|
}
|
||||||
|
|
||||||
|
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
gradlePluginPortal()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
|
||||||
|
id("com.android.application") version "8.9.1" apply false
|
||||||
|
id("org.jetbrains.kotlin.android") version "2.1.0" apply false
|
||||||
|
}
|
||||||
|
|
||||||
|
include(":app")
|
||||||
0
assets/.gitkeep
Normal file
19
doc/dfm-extract-notes.md
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# DFM Extract Script Usage
|
||||||
|
|
||||||
|
- Input: `example/pq/Config.dfm`
|
||||||
|
- Output: `lib/data/pq_config_data.dart`
|
||||||
|
- Command:
|
||||||
|
```bash
|
||||||
|
dart run tool/dfm_extract.dart
|
||||||
|
```
|
||||||
|
- Expected summary (item counts):
|
||||||
|
- Spells 45
|
||||||
|
- OffenseAttrib 11 / DefenseAttrib 9 / OffenseBad 9 / DefenseBad 14
|
||||||
|
- Weapons 38 / Armors 20 / Shields 16
|
||||||
|
- Specials 37 / ItemAttrib 33 / ItemOfs 51 / BoringItems 42
|
||||||
|
- Monsters 231 / MonMods 16
|
||||||
|
- Races 21 / Klasses 18 / Titles 9 / ImpressiveTitles 13
|
||||||
|
- Notes:
|
||||||
|
- Keeps original string literals and `name|level` style intact.
|
||||||
|
- Keys mirror TMemo names in `Config.dfm` without trailing colons.
|
||||||
|
- Re-run the command after any DFM change to refresh the Dart data.
|
||||||
61
doc/progress-quest-flutter-plan.md
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
# 목표
|
||||||
|
- `example/pq`의 Progress Quest 6.4 싱글플레이 체험을 Flutter로 1:1 재현(게임플레이, 이름 생성, 레벨업/퀘스트/플롯 진행 로직까지 동일).
|
||||||
|
- 온라인/네트워크 기능(서버 선택, 계정/비밀번호, 길드/자랑 업로드, 웹 링크 열기, HTTP 요청)은 전부 제거하거나 더미로 대체한 완전 오프라인 버전 구현.
|
||||||
|
- 기존 데이터(종족, 직업, 스펠, 무기/방어구, 몬스터, 아이템 속성 등)는 원본과 같은 풀셋을 포함시켜 동일한 결과가 나오도록 유지.
|
||||||
|
|
||||||
|
# 참조 소스(원본 Delphi)
|
||||||
|
- 메인 루프/타이머/퀘스트/레벨업: `example/pq/Main.pas`
|
||||||
|
- 캐릭터 생성/주사위 굴림/이름 생성: `example/pq/NewGuy.pas`
|
||||||
|
- 데이터 세트(메모 내용): `example/pq/Config.dfm` (`Spells`, `Weapons`, `Armors`, `Shields`, `Monsters`, `ItemAttrib/ItemOfs`, `Races`, `Klasses`, `Titles`, `ImpressiveTitles` 등)
|
||||||
|
- 네트워크 관련 제거 대상: `example/pq/Web.pas`, `HTTPGet.pas`, `SelServ.pas`, `Login.*`, `Info.*`, `Front.*`(웹 링크 부분), `Main.pas` 내 `Brag`, `Guildify`, `Navigate`, `AuthenticateUrl` 호출부
|
||||||
|
- 자산: `swords.gif`, `crossed_swords_sm.gif`, `screenshots.zip`(참고용), `pq.res`(아이콘 참고)
|
||||||
|
|
||||||
|
# 범위와 비범위
|
||||||
|
- 포함: 캐릭 생성(3d6×6, Re-Roll/Unroll, 자동 이름 생성), 퀘스트/플롯 진행 큐, 몬스터/아이템/장비/스펠 획득, 인카운터 텍스트 큐, 저장/불러오기, 진행 바(Task/Quest/Plot/EXP/Encumbrance), 로그 힌트(툴팁 수준), 기본 UI 흐름(프런트 → 새 게임/불러오기 → 메인 화면).
|
||||||
|
- 제외: 모든 HTTP 통신, 서버/길드/자랑 업로드, 프록시/패스키, 외부 브라우저 열기, 시스템 트레이 연동, Windows 레지스트리/파일 연관.
|
||||||
|
|
||||||
|
# 구현 계획(Flutter)
|
||||||
|
1) 프로젝트 세팅
|
||||||
|
- `flutter create`로 신규 앱 생성(안드로이드/웹/데스크톱 동시 지원), lints/analysis 옵션 활성화.
|
||||||
|
- 의존성: `path_provider`(세이브 파일 위치), `shared_preferences` 또는 로컬 파일 I/O, 필요 시 `intl`(날짜 포맷). 상태 관리는 기본 `ChangeNotifier`/`ValueNotifier` 또는 간단한 `provider`로 최소화.
|
||||||
|
|
||||||
|
2) 데이터 포팅
|
||||||
|
- `Config.dfm`의 TMemo 내용을 파싱해 정적 JSON/const 리스트로 변환(스펠/무기/방어구/몬스터/수식어 등). 파싱 스크립트 작성 후 `lib/data/`에 Dart 소스로 고정.
|
||||||
|
- 종족/직업/칭호/몬스터 레벨 등 숫자 필드는 원본 문자열 포맷(`name|level`) 그대로 저장하여 로직 재현.
|
||||||
|
|
||||||
|
3) 게임 상태/엔진
|
||||||
|
- `GameState` 모델: Traits(이름/종족/직업/레벨), Stats(STR/CON/DEX/INT/WIS/CHA/HP/MP), 장비/스펠/인벤토리, 진행 큐(fQueue), 현재 태스크/퀘스트/플롯 상태, encumbrance.
|
||||||
|
- 주기적 타이머(`Ticker`/`Timer.periodic`)로 `TaskBar` 증분, `Dequeue` 동작, `LevelUp`, `CompleteQuest`, `CompleteAct`, `WinEquip/WinSpell/WinItem/WinStat`, `MonsterTask` 로직을 원본 알고리즘에 맞게 Dart로 포팅.
|
||||||
|
- 난수 처리: `Random` seed를 저장/복원 가능하게 두어 재현성 확보(세이브/로드 포함).
|
||||||
|
- 캐릭 생성: 3d6 롤, Total 색상 규칙, Race/Class 선택, 이름 생성(`GenerateName`), Re-Roll/Unroll 이력 지원.
|
||||||
|
|
||||||
|
4) UI 구성
|
||||||
|
- 프런트 화면: 새 게임/불러오기(파일 선택)/종료 선택. 웹 링크 버튼은 제거하거나 “오프라인 버전” 안내로 대체.
|
||||||
|
- 캐릭터 생성 화면: 원본과 동일한 정보 배치(종족/직업 라디오, 6개 스탯 패널, Total 표시, Reroll/Unroll, Name 입력/자동생성).
|
||||||
|
- 메인 화면: ProgressQuest 레이아웃을 Flutter 위젯으로 재현(ListView/SelectableRows → `ListView.builder` + `ListTile`), 상단 Traits/Stats, Equipment/Spells/Inventory, Task/Quest/Plot/EXP/Encumbrance ProgressBar(LinearProgressIndicator 커스텀), 퀘스트/플롯 리스트, 상태바 텍스트 표시.
|
||||||
|
- 툴팁/힌트: Long-press/`Tooltip` 위젯으로 EXP/Plot/Quest 남은 시간, Encumbrance 등 표시.
|
||||||
|
- 치트 패널/숨김 키 조합은 개발자 메뉴로 옵션화(토글 버튼 또는 디버그 빌드 한정).
|
||||||
|
|
||||||
|
5) 저장/불러오기
|
||||||
|
- 세이브 구조: 원본의 Zlib 압축/컴포넌트 덤프 대신 JSON + GZip(`GZipCodec`)으로 단일 파일(`.pqf` 등) 저장. 기존 `.pq` 파일과는 별도 호환 경고 표시.
|
||||||
|
- 자동 저장 시점: 레벨업, 퀘스트/플롯 완료, 앱 백그라운드/종료 전 후킹(각 플랫폼 라이프사이클 대응).
|
||||||
|
- 불러오기: 파일 피커(웹 제외 시 기본 리스트) + 버전 검증 후 상태 복원.
|
||||||
|
|
||||||
|
6) 네트워크 제거/대체
|
||||||
|
- `Brag`, `Guildify`, `Navigate`, `AuthenticateUrl` 등 호출부는 무력화하거나 UI에서 노출하지 않음.
|
||||||
|
- 서버 선택/로그인/패스키 필드 제거. 관련 설정 값은 로컬 상태로만 유지.
|
||||||
|
- 웹 브라우저 링크 버튼은 “온라인 기능 미지원” 안내 다이얼로그로 교체.
|
||||||
|
|
||||||
|
7) 검증/테스트
|
||||||
|
- 단위 테스트: `LevelUpTime`, `GenerateName`(seed 고정), `MonsterTask`(레벨/수식어 변형), `CompleteQuest` 보상 분배, 인카운트 큐(`Dequeue`) 진행 여부.
|
||||||
|
- 위젯 테스트: 메인 화면 로딩, 진행 바 증가, 세이브/로드 왕복.
|
||||||
|
- 회귀 체크: Delphi 원본과 동일한 RNG seed에서 주요 출력(예: 첫 번째 몬스터 이름/퀘스트 문구/장비 이름) 비교 스냅샷.
|
||||||
|
|
||||||
|
8) 작업 순서 제안
|
||||||
|
1. 데이터 추출 스크립트 작성 → Dart 정적 데이터 생성
|
||||||
|
2. GameState/엔진 포팅 및 단위 테스트 확보
|
||||||
|
3. 캐릭터 생성 UI + 상태 연결
|
||||||
|
4. 메인 화면 레이아웃 구현 → 진행 루프 연동
|
||||||
|
5. 세이브/로드/자동저장 구현
|
||||||
|
6. 네트워크 UI 제거/대체 처리 후 스타일 폴리싱
|
||||||
|
7. 멀티플랫폼 빌드 검증(모바일/데스크톱/웹) 및 최종 회귀 테스트
|
||||||
159
doc/progress-quest-tasklist.md
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
# Progress Quest Flutter Task List
|
||||||
|
- 목적: `example/pq`의 Progress Quest 6.4를 Flutter로 오프라인 상태에서 동일하게 재현한다.
|
||||||
|
- 원본 알고리즘/데이터 변형 금지. 변경 필요 시 먼저 사용자 동의.
|
||||||
|
- 작업 중에는 진행된 항목의 체크박스를 즉시 갱신한다.
|
||||||
|
|
||||||
|
## 1) 프로젝트 골격 및 도구
|
||||||
|
- [x] `flutter create`로 새 앱 생성(안드로이드/웹/데스크톱 포함), 루트에 배치 후 기본 샘플 제거.
|
||||||
|
- [x] `analysis_options.yaml`에서 `flutter_lints` 활성화 및 레벨 조정(필요 시).
|
||||||
|
- [x] 의존성 합의 후 추가(`path_provider`, `shared_preferences`/파일 I/O, `intl` 필요 시) — 의존성은 추가함, `pubspec.yaml` 자산 경로 예약 완료.
|
||||||
|
- [x] CI/로컬 명령 정리: `dart format --set-exit-if-changed .`, `flutter analyze`, `flutter test` 기본 스크립트 문서화.
|
||||||
|
|
||||||
|
## 2) 원본 데이터 추출·정적화
|
||||||
|
- [x] `example/pq/Config.dfm`의 TMemo 블록을 파싱하는 스크립트 작성(`tool/dfm_extract.dart`).
|
||||||
|
- [x] 추출 결과를 원본 포맷 그대로 유지(`name|level` 등)한 정적 Dart/JSON으로 변환 후 `lib/data/`에 커밋.
|
||||||
|
- [x] 스펠/무기/방어구/방패/몬스터/수식어/종족/직업/칭호 등 모든 리스트가 원본 개수와 일치하는지 검증 로그 남기기.
|
||||||
|
- [x] 추출 스크립트 사용법과 검증 결과를 `doc/`에 간단히 기록.
|
||||||
|
|
||||||
|
## 3) 엔진/상태 모델 포팅 ✅ (100% 완료)
|
||||||
|
- [x] `GameState`/도메인 모델 정의: Traits, Stats, Equip/Spells/Inventory, 진행 큐(fQueue), Task/Quest/Plot/Exp/Encumbrance 상태, RNG seed.
|
||||||
|
- [x] RNG 시드 저장/복원 로직 구현(`Random` 재생성), 세이브/로드에 포함.
|
||||||
|
- [x] 주요 함수 포팅: `LevelUpTime`, `GenerateName`, `Indefinite/Definite/Plural`, `SpecialItem/InterestingItem/BoringItem`, `WinSpell`, `WinItem`, `WinEquip`, `WinStat`, `MonsterTask`, `CompleteQuest`, `CompleteAct`, `Task`, `Dequeue` 완료.
|
||||||
|
- [x] 보상 적용/장비·스펠·아이템·스탯 변이 헬퍼(GameMutations/RewardService) 추가.
|
||||||
|
- [x] 진행 서비스(ProgressService) 초안: Quest/Act 보상 적용 및 진행 바 리셋/큐 추가.
|
||||||
|
- [x] ProgressState/QueueState 연동 타이머 틱 + Exp/Encumbrance 재계산 + 치트/자동저장 훅 구현(ProgressService.tick/ProgressLoop).
|
||||||
|
- [x] 치트 플래그는 개발자 옵션으로 격리(기본 비활성). — `GameSessionController.cheatsEnabled` 구현됨
|
||||||
|
- [x] 타이머 루프(`Timer.periodic` 또는 `Ticker`)로 TaskBar 증분 및 `Dequeue` 호출, 경과 시간 처리(`timeGetTime` 대체) 구현.
|
||||||
|
- [x] `InterplotCinematic()` 포팅 — 플롯 진행 스토리 생성 (원본 Main.pas:456-493)
|
||||||
|
- [x] `ImpressiveGuy()` 포팅 — NPC 이름 생성 (원본 Main.pas:514)
|
||||||
|
|
||||||
|
## 4) UI 흐름 구현 🟡 (60% 완료)
|
||||||
|
|
||||||
|
### 4.1) 캐릭터 생성 화면 (NewCharacterScreen) ✅ 완료
|
||||||
|
|
||||||
|
- [x] 종족 선택 RadioGroup (21개): Half Orc, Double Hobbit, Gobhoblin 등
|
||||||
|
- [x] 직업 선택 RadioGroup (18개): Ur-Paladin, Voodoo Princess, Robot Monk 등
|
||||||
|
- [x] 능력치 굴림 (3d6×6): STR/CON/DEX/INT/WIS/CHA + Total 표시
|
||||||
|
- [x] Total 색상 규칙: 81+ 빨강, 72+ 노랑, 45- 회색, 54- 은색, 그 외 흰색
|
||||||
|
- [x] Re-Roll/Unroll 버튼 (이력 관리)
|
||||||
|
- [x] 이름 입력 + Gen 버튼 (`generateName()` 연동)
|
||||||
|
- [x] "Sold!" 버튼으로 GameState 생성 및 게임 시작
|
||||||
|
|
||||||
|
### 4.2) 게임 진행 화면 (GamePlayScreen) ✅ 기본 완료
|
||||||
|
|
||||||
|
- [x] 3패널 레이아웃 구성 (원본 Main.dfm 기준)
|
||||||
|
- [x] **좌측 패널 (Character Sheet)**:
|
||||||
|
- [x] Traits ListView (이름, 종족, 직업, 레벨)
|
||||||
|
- [x] Stats ListView (STR/CON/DEX/INT/WIS/CHA + HP Max/MP Max)
|
||||||
|
- [x] Experience ProgressBar
|
||||||
|
- [x] Spell Book ListView (스펠 이름 + 로마 숫자 랭크)
|
||||||
|
- [x] **중앙 패널 (Equipment/Inventory)**:
|
||||||
|
- [x] Equipment ListView (Weapon/Shield/Armor)
|
||||||
|
- [x] Inventory ListView (아이템 이름 + 수량)
|
||||||
|
- [x] Encumbrance ProgressBar
|
||||||
|
- [x] **우측 패널 (Plot/Quest)**:
|
||||||
|
- [x] Plot Development ListView (액트 목록)
|
||||||
|
- [x] Plot ProgressBar
|
||||||
|
- [x] Quests ListView (현재 퀘스트)
|
||||||
|
- [x] Quest ProgressBar
|
||||||
|
- [x] **하단 (Status Bar)**:
|
||||||
|
- [x] Task ProgressBar + 현재 작업 텍스트
|
||||||
|
- [x] 상태 메시지
|
||||||
|
|
||||||
|
### 4.3) 프런트 화면 (FrontScreen) 개선 ✅ 완료
|
||||||
|
|
||||||
|
- [x] "New Character" 버튼 → NewCharacterScreen 연결
|
||||||
|
- [x] "Load Game" 버튼 → 저장 파일 로드 연결
|
||||||
|
- [x] 파일 피커 UI 추가 (다중 세이브 슬롯 지원)
|
||||||
|
|
||||||
|
### 4.4) 화면 네비게이션 ✅ 완료
|
||||||
|
|
||||||
|
- [x] 라우터 설정: FrontScreen → NewCharacterScreen → GamePlayScreen
|
||||||
|
- [x] 저장 파일 로드 시 직접 GamePlayScreen 이동
|
||||||
|
- [x] 뒤로가기/종료 처리 개선 (PopScope + 저장 확인 다이얼로그)
|
||||||
|
|
||||||
|
### 4.5) 부가 UI ✅ 완료
|
||||||
|
|
||||||
|
- [x] 툴팁/힌트: EXP/Plot/Quest 남은 시간을 `Tooltip`으로 표시.
|
||||||
|
- [x] 치트 패널은 디버그 전용 토글로 감추기(cheatsEnabled 플래그로 제어).
|
||||||
|
|
||||||
|
## 5) 저장/불러오기 ✅ (100% 완료)
|
||||||
|
|
||||||
|
- [x] 세이브 포맷 결정 및 구현: JSON + GZip(`GZipCodec`) 단일 파일(`.pqf`), RNG seed 포함. (`GameSave` 직렬화 + SaveService 구현)
|
||||||
|
- [x] 저장 시점: 레벨업, 퀘스트/플롯 완료 시 자동 저장. (AutoSaveConfig로 구현됨)
|
||||||
|
- [x] 실패 처리: 예외/손상 파일 시 사용자 피드백 및 안전한 종료 흐름. (SaveRepository에 오류 결과 반환)
|
||||||
|
- [x] SaveManager: GameState <-> 파일 입출력 상위 래퍼 추가 (`progress.pqf` 기본 파일명).
|
||||||
|
- [x] SaveManager를 레벨업/퀘스트/액트 완료/루프 중단 시 자동 저장하도록 엔진(ProgressLoop)과 연결.
|
||||||
|
- [x] GameSessionController(프리젠테이션 레이어)로 ProgressLoop/SaveManager 제어 및 상태 구독 토대 마련(향후 UI 연결 필요).
|
||||||
|
- [x] 불러오기: 파일 피커 UI로 다중 세이브 슬롯 지원.
|
||||||
|
- [x] 파일 피커/로드 오류 메시지 처리 및 UI 연결.
|
||||||
|
- [x] 앱 백그라운드/종료 감지 시 자동 저장 (WidgetsBindingObserver 연동)
|
||||||
|
|
||||||
|
## 6) 네트워크 제거/대체 ⏸️ (보류 - 오프라인 전용 구현)
|
||||||
|
|
||||||
|
- [x] 온라인 관련 코드 포팅하지 않음 (Web.pas, SelServ.pas, Login.pas 제외)
|
||||||
|
- [ ] UI에서 "오프라인 버전" 안내 다이얼로그 추가 (선택사항)
|
||||||
|
|
||||||
|
## 7) 자산 정리 ✅ (완료 - Material Icons 사용)
|
||||||
|
|
||||||
|
- [x] 원본 자산 확인: `swords.gif`, `crossed_swords_sm.gif`, `pq.res` (example/pq에 참조용 유지)
|
||||||
|
- [x] **정책**: CLAUDE.md "이미지 파일 미사용" 규칙에 따라 Material Icons로 대체
|
||||||
|
- `Icons.auto_awesome` - 타이틀 아이콘
|
||||||
|
- `Icons.casino_outlined` - 새 캐릭터 버튼
|
||||||
|
- `Icons.folder_open` - 로드 버튼
|
||||||
|
- 기타 Material Icons 활용
|
||||||
|
|
||||||
|
## 8) 테스트·회귀 검증 ✅ (100% 완료)
|
||||||
|
|
||||||
|
- [x] 단위 테스트: `LevelUpTime`, `GenerateName`(고정 시드), `MonsterTask`(레벨/수식어 조합), `CompleteQuest` 보상, `Dequeue` 진행 확인.
|
||||||
|
- [x] 단위 테스트: 진행 틱/레벨업/퀘스트 완료 및 ProgressLoop 자동저장 동작 추가.
|
||||||
|
- [x] GameSessionController 테스트: 새 게임 시작, 로드 오류 처리, 일시정지 저장 (3개 통과)
|
||||||
|
- [x] 위젯 테스트: NewCharacterScreen (5개), GamePlayScreen (6개), FrontScreen (1개) 추가.
|
||||||
|
- [x] 원본 대비 회귀 체크: 동일 시드(42, 100, 999)에서 스냅샷 비교 검증 완료 (`test/regression/deterministic_game_test.dart`)
|
||||||
|
- generateName, monsterTask, namedMonster, winEquip, winSpell, winItem, completeQuest 결정적 출력 검증
|
||||||
|
- Config Data 개수 검증: races(21), klasses(18), monsters(231), spells(45), weapons(38), armors(20), shields(16)
|
||||||
|
|
||||||
|
**현재 테스트 현황**: 50개 테스트 모두 통과
|
||||||
|
|
||||||
|
## 9) 검증 및 문서화 ✅ (100% 완료)
|
||||||
|
|
||||||
|
- [x] `dart format --set-exit-if-changed .` 실행 (포맷 적용됨)
|
||||||
|
- [x] `flutter test` 실행 (50개 테스트 모두 통과)
|
||||||
|
- [x] `flutter analyze` - **No issues found!**
|
||||||
|
- deprecated 경고 수정: `surfaceVariant` → `surfaceContainerHighest`, `withOpacity` → `withValues`
|
||||||
|
- prefer_single_quotes 수정: `dart fix --apply` 로 647개 수정
|
||||||
|
- 테스트 파일 lint 수정: `fake_async` 의존성 추가, 로컬 변수명 규칙 준수
|
||||||
|
- [x] 변경 사항/검증 결과/제약사항 업데이트 완료
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 구현 파일 현황
|
||||||
|
|
||||||
|
### 완료된 핵심 파일
|
||||||
|
|
||||||
|
| 파일 | 줄 수 | 상태 |
|
||||||
|
|------|-------|------|
|
||||||
|
| `lib/src/core/engine/progress_service.dart` | 296 | ✅ |
|
||||||
|
| `lib/src/core/engine/progress_loop.dart` | 124 | ✅ |
|
||||||
|
| `lib/src/core/engine/game_mutations.dart` | 86 | ✅ |
|
||||||
|
| `lib/src/core/engine/reward_service.dart` | 26 | ✅ |
|
||||||
|
| `lib/src/core/model/game_state.dart` | 380 | ✅ |
|
||||||
|
| `lib/src/core/model/save_data.dart` | 237 | ✅ |
|
||||||
|
| `lib/src/core/util/pq_logic.dart` | 664 | ✅ |
|
||||||
|
| `lib/src/core/util/deterministic_random.dart` | 38 | ✅ |
|
||||||
|
| `lib/src/core/util/roman.dart` | 83 | ✅ |
|
||||||
|
| `lib/src/core/storage/save_*.dart` | 121 | ✅ |
|
||||||
|
| `lib/data/pq_config_data.dart` | 675 | ✅ |
|
||||||
|
| `lib/src/features/game/game_session_controller.dart` | 117 | ✅ |
|
||||||
|
| `lib/src/features/new_character/new_character_screen.dart` | 486 | ✅ 신규 |
|
||||||
|
| `lib/src/features/game/game_play_screen.dart` | 420 | ✅ 신규 |
|
||||||
|
| `lib/src/app.dart` (라우터 포함) | 108 | ✅ 수정 |
|
||||||
|
| `lib/src/features/front/front_screen.dart` | 318 | ✅ 수정 |
|
||||||
|
|
||||||
|
### 남은 작업
|
||||||
|
|
||||||
|
| 작업 | 우선순위 |
|
||||||
|
|------|---------|
|
||||||
|
| (선택) "오프라인 버전" 안내 다이얼로그 | 🟢 선택사항 |
|
||||||
|
|
||||||
|
**🎉 핵심 기능 100% 완료!**
|
||||||
1
example/pq
Submodule
34
ios/.gitignore
vendored
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
**/dgph
|
||||||
|
*.mode1v3
|
||||||
|
*.mode2v3
|
||||||
|
*.moved-aside
|
||||||
|
*.pbxuser
|
||||||
|
*.perspectivev3
|
||||||
|
**/*sync/
|
||||||
|
.sconsign.dblite
|
||||||
|
.tags*
|
||||||
|
**/.vagrant/
|
||||||
|
**/DerivedData/
|
||||||
|
Icon?
|
||||||
|
**/Pods/
|
||||||
|
**/.symlinks/
|
||||||
|
profile
|
||||||
|
xcuserdata
|
||||||
|
**/.generated/
|
||||||
|
Flutter/App.framework
|
||||||
|
Flutter/Flutter.framework
|
||||||
|
Flutter/Flutter.podspec
|
||||||
|
Flutter/Generated.xcconfig
|
||||||
|
Flutter/ephemeral/
|
||||||
|
Flutter/app.flx
|
||||||
|
Flutter/app.zip
|
||||||
|
Flutter/flutter_assets/
|
||||||
|
Flutter/flutter_export_environment.sh
|
||||||
|
ServiceDefinitions.json
|
||||||
|
Runner/GeneratedPluginRegistrant.*
|
||||||
|
|
||||||
|
# Exceptions to above rules.
|
||||||
|
!default.mode1v3
|
||||||
|
!default.mode2v3
|
||||||
|
!default.pbxuser
|
||||||
|
!default.perspectivev3
|
||||||
26
ios/Flutter/AppFrameworkInfo.plist
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
|
<string>en</string>
|
||||||
|
<key>CFBundleExecutable</key>
|
||||||
|
<string>App</string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>io.flutter.flutter.app</string>
|
||||||
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
|
<string>6.0</string>
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>App</string>
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>FMWK</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>1.0</string>
|
||||||
|
<key>CFBundleSignature</key>
|
||||||
|
<string>????</string>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>1.0</string>
|
||||||
|
<key>MinimumOSVersion</key>
|
||||||
|
<string>13.0</string>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
2
ios/Flutter/Debug.xcconfig
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
|
||||||
|
#include "Generated.xcconfig"
|
||||||
2
ios/Flutter/Release.xcconfig
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
|
||||||
|
#include "Generated.xcconfig"
|
||||||
43
ios/Podfile
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
# Uncomment this line to define a global platform for your project
|
||||||
|
# platform :ios, '13.0'
|
||||||
|
|
||||||
|
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
||||||
|
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
||||||
|
|
||||||
|
project 'Runner', {
|
||||||
|
'Debug' => :debug,
|
||||||
|
'Profile' => :release,
|
||||||
|
'Release' => :release,
|
||||||
|
}
|
||||||
|
|
||||||
|
def flutter_root
|
||||||
|
generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
|
||||||
|
unless File.exist?(generated_xcode_build_settings_path)
|
||||||
|
raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
|
||||||
|
end
|
||||||
|
|
||||||
|
File.foreach(generated_xcode_build_settings_path) do |line|
|
||||||
|
matches = line.match(/FLUTTER_ROOT\=(.*)/)
|
||||||
|
return matches[1].strip if matches
|
||||||
|
end
|
||||||
|
raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
|
||||||
|
end
|
||||||
|
|
||||||
|
require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
|
||||||
|
|
||||||
|
flutter_ios_podfile_setup
|
||||||
|
|
||||||
|
target 'Runner' do
|
||||||
|
use_frameworks!
|
||||||
|
|
||||||
|
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
|
||||||
|
target 'RunnerTests' do
|
||||||
|
inherit! :search_paths
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
post_install do |installer|
|
||||||
|
installer.pods_project.targets.each do |target|
|
||||||
|
flutter_additional_ios_build_settings(target)
|
||||||
|
end
|
||||||
|
end
|
||||||
616
ios/Runner.xcodeproj/project.pbxproj
Normal file
@@ -0,0 +1,616 @@
|
|||||||
|
// !$*UTF8*$!
|
||||||
|
{
|
||||||
|
archiveVersion = 1;
|
||||||
|
classes = {
|
||||||
|
};
|
||||||
|
objectVersion = 54;
|
||||||
|
objects = {
|
||||||
|
|
||||||
|
/* Begin PBXBuildFile section */
|
||||||
|
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
|
||||||
|
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
|
||||||
|
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
|
||||||
|
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
|
||||||
|
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
|
||||||
|
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
|
||||||
|
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
|
||||||
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
|
/* Begin PBXContainerItemProxy section */
|
||||||
|
331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = {
|
||||||
|
isa = PBXContainerItemProxy;
|
||||||
|
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
|
||||||
|
proxyType = 1;
|
||||||
|
remoteGlobalIDString = 97C146ED1CF9000F007C117D;
|
||||||
|
remoteInfo = Runner;
|
||||||
|
};
|
||||||
|
/* End PBXContainerItemProxy section */
|
||||||
|
|
||||||
|
/* Begin PBXCopyFilesBuildPhase section */
|
||||||
|
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
|
||||||
|
isa = PBXCopyFilesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
dstPath = "";
|
||||||
|
dstSubfolderSpec = 10;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
name = "Embed Frameworks";
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
/* End PBXCopyFilesBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXFileReference section */
|
||||||
|
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
|
||||||
|
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
|
||||||
|
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
|
||||||
|
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
|
||||||
|
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||||
|
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||||
|
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
|
||||||
|
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
|
||||||
|
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
|
||||||
|
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
|
||||||
|
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||||
|
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||||
|
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||||
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
|
97C146EB1CF9000F007C117D /* Frameworks */ = {
|
||||||
|
isa = PBXFrameworksBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
/* End PBXFrameworksBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXGroup section */
|
||||||
|
331C8082294A63A400263BE5 /* RunnerTests */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
331C807B294A618700263BE5 /* RunnerTests.swift */,
|
||||||
|
);
|
||||||
|
path = RunnerTests;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
9740EEB11CF90186004384FC /* Flutter */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
|
||||||
|
9740EEB21CF90195004384FC /* Debug.xcconfig */,
|
||||||
|
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
|
||||||
|
9740EEB31CF90195004384FC /* Generated.xcconfig */,
|
||||||
|
);
|
||||||
|
name = Flutter;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
97C146E51CF9000F007C117D = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
9740EEB11CF90186004384FC /* Flutter */,
|
||||||
|
97C146F01CF9000F007C117D /* Runner */,
|
||||||
|
97C146EF1CF9000F007C117D /* Products */,
|
||||||
|
331C8082294A63A400263BE5 /* RunnerTests */,
|
||||||
|
);
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
97C146EF1CF9000F007C117D /* Products */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
97C146EE1CF9000F007C117D /* Runner.app */,
|
||||||
|
331C8081294A63A400263BE5 /* RunnerTests.xctest */,
|
||||||
|
);
|
||||||
|
name = Products;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
97C146F01CF9000F007C117D /* Runner */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
97C146FA1CF9000F007C117D /* Main.storyboard */,
|
||||||
|
97C146FD1CF9000F007C117D /* Assets.xcassets */,
|
||||||
|
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
|
||||||
|
97C147021CF9000F007C117D /* Info.plist */,
|
||||||
|
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
|
||||||
|
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
|
||||||
|
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
|
||||||
|
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
|
||||||
|
);
|
||||||
|
path = Runner;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
/* End PBXGroup section */
|
||||||
|
|
||||||
|
/* Begin PBXNativeTarget section */
|
||||||
|
331C8080294A63A400263BE5 /* RunnerTests */ = {
|
||||||
|
isa = PBXNativeTarget;
|
||||||
|
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
|
||||||
|
buildPhases = (
|
||||||
|
331C807D294A63A400263BE5 /* Sources */,
|
||||||
|
331C807F294A63A400263BE5 /* Resources */,
|
||||||
|
);
|
||||||
|
buildRules = (
|
||||||
|
);
|
||||||
|
dependencies = (
|
||||||
|
331C8086294A63A400263BE5 /* PBXTargetDependency */,
|
||||||
|
);
|
||||||
|
name = RunnerTests;
|
||||||
|
productName = RunnerTests;
|
||||||
|
productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;
|
||||||
|
productType = "com.apple.product-type.bundle.unit-test";
|
||||||
|
};
|
||||||
|
97C146ED1CF9000F007C117D /* Runner */ = {
|
||||||
|
isa = PBXNativeTarget;
|
||||||
|
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
|
||||||
|
buildPhases = (
|
||||||
|
9740EEB61CF901F6004384FC /* Run Script */,
|
||||||
|
97C146EA1CF9000F007C117D /* Sources */,
|
||||||
|
97C146EB1CF9000F007C117D /* Frameworks */,
|
||||||
|
97C146EC1CF9000F007C117D /* Resources */,
|
||||||
|
9705A1C41CF9048500538489 /* Embed Frameworks */,
|
||||||
|
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
|
||||||
|
);
|
||||||
|
buildRules = (
|
||||||
|
);
|
||||||
|
dependencies = (
|
||||||
|
);
|
||||||
|
name = Runner;
|
||||||
|
productName = Runner;
|
||||||
|
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
|
||||||
|
productType = "com.apple.product-type.application";
|
||||||
|
};
|
||||||
|
/* End PBXNativeTarget section */
|
||||||
|
|
||||||
|
/* Begin PBXProject section */
|
||||||
|
97C146E61CF9000F007C117D /* Project object */ = {
|
||||||
|
isa = PBXProject;
|
||||||
|
attributes = {
|
||||||
|
BuildIndependentTargetsInParallel = YES;
|
||||||
|
LastUpgradeCheck = 1510;
|
||||||
|
ORGANIZATIONNAME = "";
|
||||||
|
TargetAttributes = {
|
||||||
|
331C8080294A63A400263BE5 = {
|
||||||
|
CreatedOnToolsVersion = 14.0;
|
||||||
|
TestTargetID = 97C146ED1CF9000F007C117D;
|
||||||
|
};
|
||||||
|
97C146ED1CF9000F007C117D = {
|
||||||
|
CreatedOnToolsVersion = 7.3.1;
|
||||||
|
LastSwiftMigration = 1100;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
|
||||||
|
compatibilityVersion = "Xcode 9.3";
|
||||||
|
developmentRegion = en;
|
||||||
|
hasScannedForEncodings = 0;
|
||||||
|
knownRegions = (
|
||||||
|
en,
|
||||||
|
Base,
|
||||||
|
);
|
||||||
|
mainGroup = 97C146E51CF9000F007C117D;
|
||||||
|
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
|
||||||
|
projectDirPath = "";
|
||||||
|
projectRoot = "";
|
||||||
|
targets = (
|
||||||
|
97C146ED1CF9000F007C117D /* Runner */,
|
||||||
|
331C8080294A63A400263BE5 /* RunnerTests */,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
/* End PBXProject section */
|
||||||
|
|
||||||
|
/* Begin PBXResourcesBuildPhase section */
|
||||||
|
331C807F294A63A400263BE5 /* Resources */ = {
|
||||||
|
isa = PBXResourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
97C146EC1CF9000F007C117D /* Resources */ = {
|
||||||
|
isa = PBXResourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
|
||||||
|
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
|
||||||
|
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
|
||||||
|
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
/* End PBXResourcesBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXShellScriptBuildPhase section */
|
||||||
|
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
alwaysOutOfDate = 1;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
inputPaths = (
|
||||||
|
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
|
||||||
|
);
|
||||||
|
name = "Thin Binary";
|
||||||
|
outputPaths = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
|
||||||
|
};
|
||||||
|
9740EEB61CF901F6004384FC /* Run Script */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
alwaysOutOfDate = 1;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
inputPaths = (
|
||||||
|
);
|
||||||
|
name = "Run Script";
|
||||||
|
outputPaths = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
|
||||||
|
};
|
||||||
|
/* End PBXShellScriptBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXSourcesBuildPhase section */
|
||||||
|
331C807D294A63A400263BE5 /* Sources */ = {
|
||||||
|
isa = PBXSourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
97C146EA1CF9000F007C117D /* Sources */ = {
|
||||||
|
isa = PBXSourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
|
||||||
|
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
/* End PBXSourcesBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXTargetDependency section */
|
||||||
|
331C8086294A63A400263BE5 /* PBXTargetDependency */ = {
|
||||||
|
isa = PBXTargetDependency;
|
||||||
|
target = 97C146ED1CF9000F007C117D /* Runner */;
|
||||||
|
targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */;
|
||||||
|
};
|
||||||
|
/* End PBXTargetDependency section */
|
||||||
|
|
||||||
|
/* Begin PBXVariantGroup section */
|
||||||
|
97C146FA1CF9000F007C117D /* Main.storyboard */ = {
|
||||||
|
isa = PBXVariantGroup;
|
||||||
|
children = (
|
||||||
|
97C146FB1CF9000F007C117D /* Base */,
|
||||||
|
);
|
||||||
|
name = Main.storyboard;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
|
||||||
|
isa = PBXVariantGroup;
|
||||||
|
children = (
|
||||||
|
97C147001CF9000F007C117D /* Base */,
|
||||||
|
);
|
||||||
|
name = LaunchScreen.storyboard;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
/* End PBXVariantGroup section */
|
||||||
|
|
||||||
|
/* Begin XCBuildConfiguration section */
|
||||||
|
249021D3217E4FDB00AE95B9 /* Profile */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||||
|
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||||
|
CLANG_ANALYZER_NONNULL = YES;
|
||||||
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||||
|
CLANG_CXX_LIBRARY = "libc++";
|
||||||
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
CLANG_ENABLE_OBJC_ARC = YES;
|
||||||
|
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||||
|
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_COMMA = YES;
|
||||||
|
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||||
|
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||||
|
CLANG_WARN_EMPTY_BODY = YES;
|
||||||
|
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||||
|
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||||
|
CLANG_WARN_INT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||||
|
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||||
|
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||||
|
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||||
|
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||||
|
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||||
|
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||||
|
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||||
|
COPY_PHASE_STRIP = NO;
|
||||||
|
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||||
|
ENABLE_NS_ASSERTIONS = NO;
|
||||||
|
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||||
|
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||||
|
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||||
|
GCC_NO_COMMON_BLOCKS = YES;
|
||||||
|
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||||
|
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||||
|
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||||
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||||
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
|
SDKROOT = iphoneos;
|
||||||
|
SUPPORTED_PLATFORMS = iphoneos;
|
||||||
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
|
VALIDATE_PRODUCT = YES;
|
||||||
|
};
|
||||||
|
name = Profile;
|
||||||
|
};
|
||||||
|
249021D4217E4FDB00AE95B9 /* Profile */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
|
||||||
|
buildSettings = {
|
||||||
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
|
ENABLE_BITCODE = NO;
|
||||||
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/Frameworks",
|
||||||
|
);
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = com.example.askiineverdie;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
VERSIONING_SYSTEM = "apple-generic";
|
||||||
|
};
|
||||||
|
name = Profile;
|
||||||
|
};
|
||||||
|
331C8088294A63A400263BE5 /* Debug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
MARKETING_VERSION = 1.0;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = com.example.askiineverdie.RunnerTests;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||||
|
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
||||||
|
};
|
||||||
|
name = Debug;
|
||||||
|
};
|
||||||
|
331C8089294A63A400263BE5 /* Release */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
MARKETING_VERSION = 1.0;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = com.example.askiineverdie.RunnerTests;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
||||||
|
};
|
||||||
|
name = Release;
|
||||||
|
};
|
||||||
|
331C808A294A63A400263BE5 /* Profile */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
MARKETING_VERSION = 1.0;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = com.example.askiineverdie.RunnerTests;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
||||||
|
};
|
||||||
|
name = Profile;
|
||||||
|
};
|
||||||
|
97C147031CF9000F007C117D /* Debug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||||
|
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||||
|
CLANG_ANALYZER_NONNULL = YES;
|
||||||
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||||
|
CLANG_CXX_LIBRARY = "libc++";
|
||||||
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
CLANG_ENABLE_OBJC_ARC = YES;
|
||||||
|
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||||
|
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_COMMA = YES;
|
||||||
|
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||||
|
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||||
|
CLANG_WARN_EMPTY_BODY = YES;
|
||||||
|
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||||
|
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||||
|
CLANG_WARN_INT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||||
|
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||||
|
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||||
|
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||||
|
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||||
|
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||||
|
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||||
|
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||||
|
COPY_PHASE_STRIP = NO;
|
||||||
|
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||||
|
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||||
|
ENABLE_TESTABILITY = YES;
|
||||||
|
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||||
|
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||||
|
GCC_DYNAMIC_NO_PIC = NO;
|
||||||
|
GCC_NO_COMMON_BLOCKS = YES;
|
||||||
|
GCC_OPTIMIZATION_LEVEL = 0;
|
||||||
|
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||||
|
"DEBUG=1",
|
||||||
|
"$(inherited)",
|
||||||
|
);
|
||||||
|
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||||
|
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||||
|
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||||
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||||
|
MTL_ENABLE_DEBUG_INFO = YES;
|
||||||
|
ONLY_ACTIVE_ARCH = YES;
|
||||||
|
SDKROOT = iphoneos;
|
||||||
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
|
};
|
||||||
|
name = Debug;
|
||||||
|
};
|
||||||
|
97C147041CF9000F007C117D /* Release */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||||
|
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||||
|
CLANG_ANALYZER_NONNULL = YES;
|
||||||
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||||
|
CLANG_CXX_LIBRARY = "libc++";
|
||||||
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
CLANG_ENABLE_OBJC_ARC = YES;
|
||||||
|
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||||
|
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_COMMA = YES;
|
||||||
|
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||||
|
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||||
|
CLANG_WARN_EMPTY_BODY = YES;
|
||||||
|
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||||
|
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||||
|
CLANG_WARN_INT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||||
|
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||||
|
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||||
|
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||||
|
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||||
|
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||||
|
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||||
|
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||||
|
COPY_PHASE_STRIP = NO;
|
||||||
|
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||||
|
ENABLE_NS_ASSERTIONS = NO;
|
||||||
|
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||||
|
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||||
|
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||||
|
GCC_NO_COMMON_BLOCKS = YES;
|
||||||
|
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||||
|
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||||
|
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||||
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||||
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
|
SDKROOT = iphoneos;
|
||||||
|
SUPPORTED_PLATFORMS = iphoneos;
|
||||||
|
SWIFT_COMPILATION_MODE = wholemodule;
|
||||||
|
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
||||||
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
|
VALIDATE_PRODUCT = YES;
|
||||||
|
};
|
||||||
|
name = Release;
|
||||||
|
};
|
||||||
|
97C147061CF9000F007C117D /* Debug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
|
||||||
|
buildSettings = {
|
||||||
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
|
ENABLE_BITCODE = NO;
|
||||||
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/Frameworks",
|
||||||
|
);
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = com.example.askiineverdie;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||||
|
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
VERSIONING_SYSTEM = "apple-generic";
|
||||||
|
};
|
||||||
|
name = Debug;
|
||||||
|
};
|
||||||
|
97C147071CF9000F007C117D /* Release */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
|
||||||
|
buildSettings = {
|
||||||
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
|
ENABLE_BITCODE = NO;
|
||||||
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/Frameworks",
|
||||||
|
);
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = com.example.askiineverdie;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
VERSIONING_SYSTEM = "apple-generic";
|
||||||
|
};
|
||||||
|
name = Release;
|
||||||
|
};
|
||||||
|
/* End XCBuildConfiguration section */
|
||||||
|
|
||||||
|
/* Begin XCConfigurationList section */
|
||||||
|
331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = {
|
||||||
|
isa = XCConfigurationList;
|
||||||
|
buildConfigurations = (
|
||||||
|
331C8088294A63A400263BE5 /* Debug */,
|
||||||
|
331C8089294A63A400263BE5 /* Release */,
|
||||||
|
331C808A294A63A400263BE5 /* Profile */,
|
||||||
|
);
|
||||||
|
defaultConfigurationIsVisible = 0;
|
||||||
|
defaultConfigurationName = Release;
|
||||||
|
};
|
||||||
|
97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
|
||||||
|
isa = XCConfigurationList;
|
||||||
|
buildConfigurations = (
|
||||||
|
97C147031CF9000F007C117D /* Debug */,
|
||||||
|
97C147041CF9000F007C117D /* Release */,
|
||||||
|
249021D3217E4FDB00AE95B9 /* Profile */,
|
||||||
|
);
|
||||||
|
defaultConfigurationIsVisible = 0;
|
||||||
|
defaultConfigurationName = Release;
|
||||||
|
};
|
||||||
|
97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
|
||||||
|
isa = XCConfigurationList;
|
||||||
|
buildConfigurations = (
|
||||||
|
97C147061CF9000F007C117D /* Debug */,
|
||||||
|
97C147071CF9000F007C117D /* Release */,
|
||||||
|
249021D4217E4FDB00AE95B9 /* Profile */,
|
||||||
|
);
|
||||||
|
defaultConfigurationIsVisible = 0;
|
||||||
|
defaultConfigurationName = Release;
|
||||||
|
};
|
||||||
|
/* End XCConfigurationList section */
|
||||||
|
};
|
||||||
|
rootObject = 97C146E61CF9000F007C117D /* Project object */;
|
||||||
|
}
|
||||||
7
ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
generated
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Workspace
|
||||||
|
version = "1.0">
|
||||||
|
<FileRef
|
||||||
|
location = "self:">
|
||||||
|
</FileRef>
|
||||||
|
</Workspace>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>IDEDidComputeMac32BitWarning</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>PreviewsEnabled</key>
|
||||||
|
<false/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
101
ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Scheme
|
||||||
|
LastUpgradeVersion = "1510"
|
||||||
|
version = "1.3">
|
||||||
|
<BuildAction
|
||||||
|
parallelizeBuildables = "YES"
|
||||||
|
buildImplicitDependencies = "YES">
|
||||||
|
<BuildActionEntries>
|
||||||
|
<BuildActionEntry
|
||||||
|
buildForTesting = "YES"
|
||||||
|
buildForRunning = "YES"
|
||||||
|
buildForProfiling = "YES"
|
||||||
|
buildForArchiving = "YES"
|
||||||
|
buildForAnalyzing = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||||
|
BuildableName = "Runner.app"
|
||||||
|
BlueprintName = "Runner"
|
||||||
|
ReferencedContainer = "container:Runner.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildActionEntry>
|
||||||
|
</BuildActionEntries>
|
||||||
|
</BuildAction>
|
||||||
|
<TestAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||||
|
<MacroExpansion>
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||||
|
BuildableName = "Runner.app"
|
||||||
|
BlueprintName = "Runner"
|
||||||
|
ReferencedContainer = "container:Runner.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</MacroExpansion>
|
||||||
|
<Testables>
|
||||||
|
<TestableReference
|
||||||
|
skipped = "NO"
|
||||||
|
parallelizable = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "331C8080294A63A400263BE5"
|
||||||
|
BuildableName = "RunnerTests.xctest"
|
||||||
|
BlueprintName = "RunnerTests"
|
||||||
|
ReferencedContainer = "container:Runner.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</TestableReference>
|
||||||
|
</Testables>
|
||||||
|
</TestAction>
|
||||||
|
<LaunchAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
|
||||||
|
launchStyle = "0"
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
ignoresPersistentStateOnLaunch = "NO"
|
||||||
|
debugDocumentVersioning = "YES"
|
||||||
|
debugServiceExtension = "internal"
|
||||||
|
enableGPUValidationMode = "1"
|
||||||
|
allowLocationSimulation = "YES">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||||
|
BuildableName = "Runner.app"
|
||||||
|
BlueprintName = "Runner"
|
||||||
|
ReferencedContainer = "container:Runner.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
</LaunchAction>
|
||||||
|
<ProfileAction
|
||||||
|
buildConfiguration = "Profile"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
savedToolIdentifier = ""
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
debugDocumentVersioning = "YES">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||||
|
BuildableName = "Runner.app"
|
||||||
|
BlueprintName = "Runner"
|
||||||
|
ReferencedContainer = "container:Runner.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
</ProfileAction>
|
||||||
|
<AnalyzeAction
|
||||||
|
buildConfiguration = "Debug">
|
||||||
|
</AnalyzeAction>
|
||||||
|
<ArchiveAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
revealArchiveInOrganizer = "YES">
|
||||||
|
</ArchiveAction>
|
||||||
|
</Scheme>
|
||||||
7
ios/Runner.xcworkspace/contents.xcworkspacedata
generated
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Workspace
|
||||||
|
version = "1.0">
|
||||||
|
<FileRef
|
||||||
|
location = "group:Runner.xcodeproj">
|
||||||
|
</FileRef>
|
||||||
|
</Workspace>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>IDEDidComputeMac32BitWarning</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>PreviewsEnabled</key>
|
||||||
|
<false/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
13
ios/Runner/AppDelegate.swift
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import Flutter
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
@main
|
||||||
|
@objc class AppDelegate: FlutterAppDelegate {
|
||||||
|
override func application(
|
||||||
|
_ application: UIApplication,
|
||||||
|
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
||||||
|
) -> Bool {
|
||||||
|
GeneratedPluginRegistrant.register(with: self)
|
||||||
|
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
||||||
|
}
|
||||||
|
}
|
||||||
122
ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"size" : "20x20",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"filename" : "Icon-App-20x20@2x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "20x20",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"filename" : "Icon-App-20x20@3x.png",
|
||||||
|
"scale" : "3x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "29x29",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"filename" : "Icon-App-29x29@1x.png",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "29x29",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"filename" : "Icon-App-29x29@2x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "29x29",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"filename" : "Icon-App-29x29@3x.png",
|
||||||
|
"scale" : "3x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "40x40",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"filename" : "Icon-App-40x40@2x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "40x40",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"filename" : "Icon-App-40x40@3x.png",
|
||||||
|
"scale" : "3x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "60x60",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"filename" : "Icon-App-60x60@2x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "60x60",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"filename" : "Icon-App-60x60@3x.png",
|
||||||
|
"scale" : "3x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "20x20",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"filename" : "Icon-App-20x20@1x.png",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "20x20",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"filename" : "Icon-App-20x20@2x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "29x29",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"filename" : "Icon-App-29x29@1x.png",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "29x29",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"filename" : "Icon-App-29x29@2x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "40x40",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"filename" : "Icon-App-40x40@1x.png",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "40x40",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"filename" : "Icon-App-40x40@2x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "76x76",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"filename" : "Icon-App-76x76@1x.png",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "76x76",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"filename" : "Icon-App-76x76@2x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "83.5x83.5",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"filename" : "Icon-App-83.5x83.5@2x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "1024x1024",
|
||||||
|
"idiom" : "ios-marketing",
|
||||||
|
"filename" : "Icon-App-1024x1024@1x.png",
|
||||||
|
"scale" : "1x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"version" : 1,
|
||||||
|
"author" : "xcode"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 295 B |
|
After Width: | Height: | Size: 406 B |
|
After Width: | Height: | Size: 450 B |
|
After Width: | Height: | Size: 282 B |
|
After Width: | Height: | Size: 462 B |
|
After Width: | Height: | Size: 704 B |
|
After Width: | Height: | Size: 406 B |
|
After Width: | Height: | Size: 586 B |
|
After Width: | Height: | Size: 862 B |
|
After Width: | Height: | Size: 862 B |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 762 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
23
ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"filename" : "LaunchImage.png",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"filename" : "LaunchImage@2x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"filename" : "LaunchImage@3x.png",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"version" : 1,
|
||||||
|
"author" : "xcode"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
vendored
Normal file
|
After Width: | Height: | Size: 68 B |
BIN
ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 68 B |
BIN
ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 68 B |
5
ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# Launch Screen Assets
|
||||||
|
|
||||||
|
You can customize the launch screen with your own desired assets by replacing the image files in this directory.
|
||||||
|
|
||||||
|
You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.
|
||||||
37
ios/Runner/Base.lproj/LaunchScreen.storyboard
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16G29" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
|
||||||
|
<dependencies>
|
||||||
|
<deployment identifier="iOS"/>
|
||||||
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
|
||||||
|
</dependencies>
|
||||||
|
<scenes>
|
||||||
|
<!--View Controller-->
|
||||||
|
<scene sceneID="EHf-IW-A2E">
|
||||||
|
<objects>
|
||||||
|
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
|
||||||
|
<layoutGuides>
|
||||||
|
<viewControllerLayoutGuide type="top" id="Ydg-fD-yQy"/>
|
||||||
|
<viewControllerLayoutGuide type="bottom" id="xbc-2k-c8Z"/>
|
||||||
|
</layoutGuides>
|
||||||
|
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
|
||||||
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
|
<subviews>
|
||||||
|
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
|
||||||
|
</imageView>
|
||||||
|
</subviews>
|
||||||
|
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
|
||||||
|
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
|
||||||
|
</constraints>
|
||||||
|
</view>
|
||||||
|
</viewController>
|
||||||
|
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||||
|
</objects>
|
||||||
|
<point key="canvasLocation" x="53" y="375"/>
|
||||||
|
</scene>
|
||||||
|
</scenes>
|
||||||
|
<resources>
|
||||||
|
<image name="LaunchImage" width="168" height="185"/>
|
||||||
|
</resources>
|
||||||
|
</document>
|
||||||
26
ios/Runner/Base.lproj/Main.storyboard
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
|
||||||
|
<dependencies>
|
||||||
|
<deployment identifier="iOS"/>
|
||||||
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
|
||||||
|
</dependencies>
|
||||||
|
<scenes>
|
||||||
|
<!--Flutter View Controller-->
|
||||||
|
<scene sceneID="tne-QT-ifu">
|
||||||
|
<objects>
|
||||||
|
<viewController id="BYZ-38-t0r" customClass="FlutterViewController" sceneMemberID="viewController">
|
||||||
|
<layoutGuides>
|
||||||
|
<viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
|
||||||
|
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
|
||||||
|
</layoutGuides>
|
||||||
|
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
|
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
|
||||||
|
</view>
|
||||||
|
</viewController>
|
||||||
|
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
|
||||||
|
</objects>
|
||||||
|
</scene>
|
||||||
|
</scenes>
|
||||||
|
</document>
|
||||||
49
ios/Runner/Info.plist
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
|
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||||
|
<key>CFBundleDisplayName</key>
|
||||||
|
<string>Askiineverdie</string>
|
||||||
|
<key>CFBundleExecutable</key>
|
||||||
|
<string>$(EXECUTABLE_NAME)</string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||||
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
|
<string>6.0</string>
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>askiineverdie</string>
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>APPL</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>$(FLUTTER_BUILD_NAME)</string>
|
||||||
|
<key>CFBundleSignature</key>
|
||||||
|
<string>????</string>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||||
|
<key>LSRequiresIPhoneOS</key>
|
||||||
|
<true/>
|
||||||
|
<key>UILaunchStoryboardName</key>
|
||||||
|
<string>LaunchScreen</string>
|
||||||
|
<key>UIMainStoryboardFile</key>
|
||||||
|
<string>Main</string>
|
||||||
|
<key>UISupportedInterfaceOrientations</key>
|
||||||
|
<array>
|
||||||
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
|
</array>
|
||||||
|
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||||
|
<array>
|
||||||
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
|
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
|
</array>
|
||||||
|
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||||
|
<true/>
|
||||||
|
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
1
ios/Runner/Runner-Bridging-Header.h
Normal file
@@ -0,0 +1 @@
|
|||||||
|
#import "GeneratedPluginRegistrant.h"
|
||||||
12
ios/RunnerTests/RunnerTests.swift
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import Flutter
|
||||||
|
import UIKit
|
||||||
|
import XCTest
|
||||||
|
|
||||||
|
class RunnerTests: XCTestCase {
|
||||||
|
|
||||||
|
func testExample() {
|
||||||
|
// If you add code to the Runner application, consider adding tests here.
|
||||||
|
// See https://developer.apple.com/documentation/xctest for more information about using XCTest.
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
674
lib/data/pq_config_data.dart
Normal file
@@ -0,0 +1,674 @@
|
|||||||
|
// GENERATED CODE - DO NOT EDIT BY HAND.
|
||||||
|
// Generated by tool/dfm_extract.dart from example/pq/Config.dfm
|
||||||
|
|
||||||
|
const Map<String, List<String>> pqConfigData = {
|
||||||
|
'Armors': [
|
||||||
|
'Lace|1',
|
||||||
|
'Macrame|2',
|
||||||
|
'Burlap|3',
|
||||||
|
'Canvas|4',
|
||||||
|
'Flannel|5',
|
||||||
|
'Chamois|6',
|
||||||
|
'Pleathers|7',
|
||||||
|
'Leathers|8',
|
||||||
|
'Bearskin|9',
|
||||||
|
'Ringmail|10',
|
||||||
|
'Scale Mail|12',
|
||||||
|
'Chainmail|14',
|
||||||
|
'Splint Mail|15',
|
||||||
|
'Platemail|16',
|
||||||
|
'ABS|17',
|
||||||
|
'Kevlar|18',
|
||||||
|
'Titanium|19',
|
||||||
|
'Mithril Mail|20',
|
||||||
|
'Diamond Mail|25',
|
||||||
|
'Plasma|30',
|
||||||
|
],
|
||||||
|
'BoringItems': [
|
||||||
|
'nail',
|
||||||
|
'lunchpail',
|
||||||
|
'sock',
|
||||||
|
'I.O.U.',
|
||||||
|
'cookie',
|
||||||
|
'pint',
|
||||||
|
'toothpick',
|
||||||
|
'writ',
|
||||||
|
'newspaper',
|
||||||
|
'letter',
|
||||||
|
'plank',
|
||||||
|
'hat',
|
||||||
|
'egg',
|
||||||
|
'coin',
|
||||||
|
'needle',
|
||||||
|
'bucket',
|
||||||
|
'ladder',
|
||||||
|
'chicken',
|
||||||
|
'twig',
|
||||||
|
'dirtclod',
|
||||||
|
'counterpane',
|
||||||
|
'vest',
|
||||||
|
'teratoma',
|
||||||
|
'bunny',
|
||||||
|
'rock',
|
||||||
|
'pole',
|
||||||
|
'carrot',
|
||||||
|
'canoe',
|
||||||
|
'inkwell',
|
||||||
|
'hoe',
|
||||||
|
'bandage',
|
||||||
|
'trowel',
|
||||||
|
'towel',
|
||||||
|
'planter box',
|
||||||
|
'anvil',
|
||||||
|
'axle',
|
||||||
|
'tuppence',
|
||||||
|
'casket',
|
||||||
|
'nosegay',
|
||||||
|
'trinket',
|
||||||
|
'credenza',
|
||||||
|
'writ',
|
||||||
|
],
|
||||||
|
'DefenseAttrib': [
|
||||||
|
'Studded|+1',
|
||||||
|
'Banded|+2',
|
||||||
|
'Gilded|+2',
|
||||||
|
'Festooned|+3',
|
||||||
|
'Holy|+4',
|
||||||
|
'Cambric|+1',
|
||||||
|
'Fine|+4',
|
||||||
|
'Impressive|+5',
|
||||||
|
'Custom|+3',
|
||||||
|
],
|
||||||
|
'DefenseBad': [
|
||||||
|
'Holey|-1',
|
||||||
|
'Patched|-1',
|
||||||
|
'Threadbare|-2',
|
||||||
|
'Faded|-1',
|
||||||
|
'Rusty|-3',
|
||||||
|
'Motheaten|-3',
|
||||||
|
'Mildewed|-2',
|
||||||
|
'Torn|-3',
|
||||||
|
'Dented|-3',
|
||||||
|
'Cursed|-5',
|
||||||
|
'Plastic|-4',
|
||||||
|
'Cracked|-4',
|
||||||
|
'Warped|-3',
|
||||||
|
'Corroded|-3',
|
||||||
|
],
|
||||||
|
'ImpressiveTitles': [
|
||||||
|
'King',
|
||||||
|
'Queen',
|
||||||
|
'Lord',
|
||||||
|
'Lady',
|
||||||
|
'Viceroy',
|
||||||
|
'Mayor',
|
||||||
|
'Prince',
|
||||||
|
'Princess',
|
||||||
|
'Chief',
|
||||||
|
'Boss',
|
||||||
|
'Archbishop',
|
||||||
|
'Baron',
|
||||||
|
'Comptroller',
|
||||||
|
],
|
||||||
|
'ItemAttrib': [
|
||||||
|
'Golden',
|
||||||
|
'Gilded',
|
||||||
|
'Spectral',
|
||||||
|
'Astral',
|
||||||
|
'Garlanded',
|
||||||
|
'Precious',
|
||||||
|
'Crafted',
|
||||||
|
'Dual',
|
||||||
|
'Filigreed',
|
||||||
|
'Cruciate',
|
||||||
|
'Arcane',
|
||||||
|
'Blessed',
|
||||||
|
'Reverential',
|
||||||
|
'Lucky',
|
||||||
|
'Enchanted',
|
||||||
|
'Gleaming',
|
||||||
|
'Grandiose',
|
||||||
|
'Sacred',
|
||||||
|
'Legendary',
|
||||||
|
'Mythic',
|
||||||
|
'Crystalline',
|
||||||
|
'Austere',
|
||||||
|
'Ostentatious',
|
||||||
|
'One True',
|
||||||
|
'Proverbial',
|
||||||
|
'Fearsome',
|
||||||
|
'Deadly',
|
||||||
|
'Benevolent',
|
||||||
|
'Unearthly',
|
||||||
|
'Magnificent',
|
||||||
|
'Iron',
|
||||||
|
'Ormolu',
|
||||||
|
'Puissant',
|
||||||
|
],
|
||||||
|
'ItemOfs': [
|
||||||
|
'Foreboding',
|
||||||
|
'Foreshadowing',
|
||||||
|
'Nervousness',
|
||||||
|
'Happiness',
|
||||||
|
'Torpor',
|
||||||
|
'Danger',
|
||||||
|
'Craft',
|
||||||
|
'Silence',
|
||||||
|
'Invisibility',
|
||||||
|
'Rapidity',
|
||||||
|
'Pleasure',
|
||||||
|
'Practicality',
|
||||||
|
'Hurting',
|
||||||
|
'Joy',
|
||||||
|
'Petulance',
|
||||||
|
'Intrusion',
|
||||||
|
'Chaos',
|
||||||
|
'Suffering',
|
||||||
|
'Extroversion',
|
||||||
|
'Frenzy',
|
||||||
|
'Solitude',
|
||||||
|
'Punctuality',
|
||||||
|
'Efficiency',
|
||||||
|
'Comfort',
|
||||||
|
'Patience',
|
||||||
|
'Internment',
|
||||||
|
'Incarceration',
|
||||||
|
'Misapprehension',
|
||||||
|
'Loyalty',
|
||||||
|
'Envy',
|
||||||
|
'Acrimony',
|
||||||
|
'Worry',
|
||||||
|
'Fear',
|
||||||
|
'Awe',
|
||||||
|
'Guile',
|
||||||
|
'Prurience',
|
||||||
|
'Fortune',
|
||||||
|
'Perspicacity',
|
||||||
|
'Domination',
|
||||||
|
'Submission',
|
||||||
|
'Fealty',
|
||||||
|
'Hunger',
|
||||||
|
'Despair',
|
||||||
|
'Cruelty',
|
||||||
|
'Grob',
|
||||||
|
'Dignard',
|
||||||
|
'Ra',
|
||||||
|
'the Bone',
|
||||||
|
'Diamonique',
|
||||||
|
'Electrum',
|
||||||
|
'Hydragyrum',
|
||||||
|
],
|
||||||
|
'Klasses': [
|
||||||
|
'Ur-Paladin|WIS,CON',
|
||||||
|
'Voodoo Princess|INT,CHA',
|
||||||
|
'Robot Monk|STR',
|
||||||
|
'Mu-Fu Monk|DEX',
|
||||||
|
'Mage Illusioner|INT,MP Max',
|
||||||
|
'Shiv-Knight|DEX',
|
||||||
|
'Inner Mason|CON',
|
||||||
|
'Fighter/Organist|CHA,STR',
|
||||||
|
'Puma Burgular|DEX',
|
||||||
|
'Runeloremaster|WIS',
|
||||||
|
'Hunter Strangler|DEX,INT',
|
||||||
|
'Battle-Felon|STR',
|
||||||
|
'Tickle-Mimic|WIS,INT',
|
||||||
|
'Slow Poisoner|CON',
|
||||||
|
'Bastard Lunatic|CON',
|
||||||
|
'Jungle Clown|DEX,CHA',
|
||||||
|
'Birdrider|WIS',
|
||||||
|
'Vermineer|INT',
|
||||||
|
],
|
||||||
|
'MonMods': [
|
||||||
|
'-4 fœtal *',
|
||||||
|
'-4 dying *',
|
||||||
|
'-3 crippled *',
|
||||||
|
'-3 baby *',
|
||||||
|
'-2 adolescent',
|
||||||
|
'-2 very sick *',
|
||||||
|
'-1 lesser *',
|
||||||
|
'-1 undernourished *',
|
||||||
|
'+1 greater *',
|
||||||
|
'+1 * Elder',
|
||||||
|
'+2 war *',
|
||||||
|
'+2 Battle-*',
|
||||||
|
'+3 Were-*',
|
||||||
|
'+3 undead *',
|
||||||
|
'+4 giant *',
|
||||||
|
'+4 * Rex',
|
||||||
|
],
|
||||||
|
'Monsters': [
|
||||||
|
'Anhkheg|6|chitin',
|
||||||
|
'Ant|0|antenna',
|
||||||
|
'Ape|4|ass',
|
||||||
|
'Baluchitherium|14|ear',
|
||||||
|
'Beholder|10|eyestalk',
|
||||||
|
'Black Pudding|10|saliva',
|
||||||
|
'Blink Dog|4|eyelid',
|
||||||
|
'Cub Scout|1|neckerchief',
|
||||||
|
'Girl Scout|2|cookie',
|
||||||
|
'Boy Scout|3|merit badge',
|
||||||
|
'Eagle Scout|4|merit badge',
|
||||||
|
'Bugbear|3|skin',
|
||||||
|
'Bugboar|3|tusk',
|
||||||
|
'Boogie|3|slime',
|
||||||
|
'Camel|2|hump',
|
||||||
|
'Carrion Crawler|3|egg',
|
||||||
|
'Catoblepas|6|neck',
|
||||||
|
'Centaur|4|rib',
|
||||||
|
'Centipede|0|leg',
|
||||||
|
'Cockatrice|5|wattle',
|
||||||
|
'Couatl|9|wing',
|
||||||
|
'Crayfish|0|antenna',
|
||||||
|
'Demogorgon|53|tentacle',
|
||||||
|
'Jubilex|17|gel',
|
||||||
|
'Manes|1|tooth',
|
||||||
|
'Orcus|27|wand',
|
||||||
|
'Succubus|6|bra',
|
||||||
|
'Vrock|8|neck',
|
||||||
|
'Hezrou|9|leg',
|
||||||
|
'Glabrezu|10|collar',
|
||||||
|
'Nalfeshnee|11|tusk',
|
||||||
|
'Marilith|7|arm',
|
||||||
|
'Balor|8|whip',
|
||||||
|
'Yeenoghu|25|flail',
|
||||||
|
'Asmodeus|52|leathers',
|
||||||
|
'Baalzebul|43|pants',
|
||||||
|
'Barbed Devil|8|flame',
|
||||||
|
'Bone Devil|9|hook',
|
||||||
|
'Dispater|30|matches',
|
||||||
|
'Erinyes|6|thong',
|
||||||
|
'Geryon|30|cornucopia',
|
||||||
|
'Malebranche|5|fork',
|
||||||
|
'Ice Devil|11|snow',
|
||||||
|
'Lemure|3|blob',
|
||||||
|
'Pit Fiend|13|seed',
|
||||||
|
'Ankylosaurus|9|tail',
|
||||||
|
'Brontosaurus|30|brain',
|
||||||
|
'Diplodocus|24|fin',
|
||||||
|
'Elasmosaurus|15|neck',
|
||||||
|
'Gorgosaurus|13|arm',
|
||||||
|
'Iguanadon|6|thumb',
|
||||||
|
'Megalosaurus|12|jaw',
|
||||||
|
'Monoclonius|8|horn',
|
||||||
|
'Pentasaurus|12|head',
|
||||||
|
'Stegosaurus|18|plate',
|
||||||
|
'Triceratops|16|horn',
|
||||||
|
'Tyrannosaurus Rex|18|forearm',
|
||||||
|
'Djinn|7|lamp',
|
||||||
|
'Doppelganger|4|face',
|
||||||
|
'Black Dragon|7|*',
|
||||||
|
'Plaid Dragon|7|sporrin',
|
||||||
|
'Blue Dragon|9|*',
|
||||||
|
'Beige Dragon|9|*',
|
||||||
|
'Brass Dragon|7|pole',
|
||||||
|
'Tin Dragon|8|*',
|
||||||
|
'Bronze Dragon|9|medal',
|
||||||
|
'Chromatic Dragon|16|scale',
|
||||||
|
'Copper Dragon|8|loafer',
|
||||||
|
'Gold Dragon|8|filling',
|
||||||
|
'Green Dragon|8|*',
|
||||||
|
'Platinum Dragon|21|*',
|
||||||
|
'Red Dragon|10|cocktail',
|
||||||
|
'Silver Dragon|10|*',
|
||||||
|
'White Dragon|6|tooth',
|
||||||
|
'Dragon Turtle|13|shell',
|
||||||
|
'Dryad|2|acorn',
|
||||||
|
'Dwarf|1|drawers',
|
||||||
|
'Eel|2|sashimi',
|
||||||
|
'Efreet|10|cinder',
|
||||||
|
'Sand Elemental|8|glass',
|
||||||
|
'Bacon Elemental|10|bit',
|
||||||
|
'Porn Elemental|12|lube',
|
||||||
|
'Cheese Elemental|14|curd',
|
||||||
|
'Hair Elemental|16|follicle',
|
||||||
|
'Swamp Elf|1|lilypad',
|
||||||
|
'Brown Elf|1|tusk',
|
||||||
|
'Sea Elf|1|jerkin',
|
||||||
|
'Ettin|10|fur',
|
||||||
|
'Frog|0|leg',
|
||||||
|
'Violet Fungi|3|spore',
|
||||||
|
'Gargoyle|4|gravel',
|
||||||
|
'Gelatinous Cube|4|jam',
|
||||||
|
'Ghast|4|vomit',
|
||||||
|
'Ghost|10|*',
|
||||||
|
'Ghoul|2|muscle',
|
||||||
|
'Humidity Giant|12|drops',
|
||||||
|
'Beef Giant|11|steak',
|
||||||
|
'Quartz Giant|10|crystal',
|
||||||
|
'Porcelain Giant|9|fixture',
|
||||||
|
'Rice Giant|8|grain',
|
||||||
|
'Cloud Giant|12|condensation',
|
||||||
|
'Fire Giant|11|cigarettes',
|
||||||
|
'Frost Giant|10|snowman',
|
||||||
|
'Hill Giant|8|corpse',
|
||||||
|
'Stone Giant|9|hatchling',
|
||||||
|
'Storm Giant|15|barometer',
|
||||||
|
'Mini Giant|4|pompadour',
|
||||||
|
'Gnoll|2|collar',
|
||||||
|
'Gnome|1|hat',
|
||||||
|
'Goblin|1|ear',
|
||||||
|
'Grid Bug|1|carapace',
|
||||||
|
'Jellyrock|9|seedling',
|
||||||
|
'Beer Golem|15|foam',
|
||||||
|
'Oxygen Golem|17|platelet',
|
||||||
|
'Cardboard Golem|14|recycling',
|
||||||
|
'Rubber Golem|16|ball',
|
||||||
|
'Leather Golem|15|fob',
|
||||||
|
'Gorgon|8|testicle',
|
||||||
|
'Gray Ooze|3|gravy',
|
||||||
|
'Green Slime|2|sample',
|
||||||
|
'Griffon|7|nest',
|
||||||
|
'Banshee|7|larynx',
|
||||||
|
'Harpy|3|mascara',
|
||||||
|
'Hell Hound|5|tongue',
|
||||||
|
'Hippocampus|4|mane',
|
||||||
|
'Hippogriff|3|egg',
|
||||||
|
'Hobgoblin|1|patella',
|
||||||
|
'Homunculus|2|fluid',
|
||||||
|
'Hydra|8|gyrum',
|
||||||
|
'Imp|2|tail',
|
||||||
|
'Invisible Stalker|8|*',
|
||||||
|
'Iron Peasant|3|chaff',
|
||||||
|
'Jumpskin|3|shin',
|
||||||
|
'Kobold|1|penis',
|
||||||
|
'Leprechaun|1|wallet',
|
||||||
|
'Leucrotta|6|hoof',
|
||||||
|
'Lich|11|crown',
|
||||||
|
'Lizard Man|2|tail',
|
||||||
|
'Lurker|10|sac',
|
||||||
|
'Manticore|6|spike',
|
||||||
|
'Mastodon|12|tusk',
|
||||||
|
'Medusa|6|eye',
|
||||||
|
'Multicell|2|dendrite',
|
||||||
|
'Pirate|1|booty',
|
||||||
|
'Berserker|1|shirt',
|
||||||
|
'Caveman|2|club',
|
||||||
|
'Dervish|1|robe',
|
||||||
|
'Merman|1|trident',
|
||||||
|
'Mermaid|1|gills',
|
||||||
|
'Mimic|9|hinge',
|
||||||
|
'Mind Flayer|8|tentacle',
|
||||||
|
'Minotaur|6|map',
|
||||||
|
'Yellow Mold|1|spore',
|
||||||
|
'Morkoth|7|teeth',
|
||||||
|
'Mummy|6|gauze',
|
||||||
|
'Naga|9|rattle',
|
||||||
|
'Nebbish|1|belly',
|
||||||
|
'Neo-Otyugh|11|organ ',
|
||||||
|
'Nixie|1|webbing',
|
||||||
|
'Nymph|3|hanky',
|
||||||
|
'Ochre Jelly|6|nucleus',
|
||||||
|
'Octopus|2|beak',
|
||||||
|
'Ogre|4|talon',
|
||||||
|
'Ogre Mage|5|apparel',
|
||||||
|
'Orc|1|snout',
|
||||||
|
'Otyugh|7|organ',
|
||||||
|
'Owlbear|5|feather',
|
||||||
|
'Pegasus|4|aileron',
|
||||||
|
'Peryton|4|antler',
|
||||||
|
'Piercer|3|tip',
|
||||||
|
'Pixie|1|dust',
|
||||||
|
'Man-o-war|3|tentacle',
|
||||||
|
'Purple Worm|15|dung',
|
||||||
|
'Quasit|3|tail',
|
||||||
|
'Rakshasa|7|pajamas',
|
||||||
|
'Rat|0|tail',
|
||||||
|
'Remorhaz|11|protrusion',
|
||||||
|
'Roc|18|wing',
|
||||||
|
'Roper|11|twine',
|
||||||
|
'Rot Grub|1|eggsac',
|
||||||
|
'Rust Monster|5|shavings',
|
||||||
|
'Satyr|5|hoof',
|
||||||
|
'Sea Hag|3|wart',
|
||||||
|
'Silkie|3|fur',
|
||||||
|
'Shadow|3|silhouette',
|
||||||
|
'Shambling Mound|10|mulch',
|
||||||
|
'Shedu|9|hoof',
|
||||||
|
'Shrieker|3|stalk',
|
||||||
|
'Skeleton|1|clavicle',
|
||||||
|
'Spectre|7|vestige',
|
||||||
|
'Sphinx|10|paw',
|
||||||
|
'Spider|0|web',
|
||||||
|
'Sprite|1|can',
|
||||||
|
'Stirge|1|proboscis',
|
||||||
|
'Stun Bear|5|tooth',
|
||||||
|
'Stun Worm|2|trode',
|
||||||
|
'Su-monster|5|tail',
|
||||||
|
'Sylph|3|thigh',
|
||||||
|
'Titan|20|sandal',
|
||||||
|
'Trapper|12|shag',
|
||||||
|
'Treant|10|acorn',
|
||||||
|
'Triton|3|scale',
|
||||||
|
'Troglodyte|2|tail',
|
||||||
|
'Troll|6|hide',
|
||||||
|
'Umber Hulk|8|claw',
|
||||||
|
'Unicorn|4|blood',
|
||||||
|
'Vampire|8|pancreas',
|
||||||
|
'Wight|4|lung',
|
||||||
|
'Will-o-the-Wisp|9|wisp',
|
||||||
|
'Wraith|5|finger',
|
||||||
|
'Wyvern|7|wing',
|
||||||
|
'Xorn|7|jaw',
|
||||||
|
'Yeti|4|fur',
|
||||||
|
'Zombie|2|forehead',
|
||||||
|
'Wasp|0|stinger',
|
||||||
|
'Rat|1|tail',
|
||||||
|
'Bunny|0|ear',
|
||||||
|
'Moth|0|dust',
|
||||||
|
'Beagle|0|collar',
|
||||||
|
'Midge|0|corpse',
|
||||||
|
'Ostrich|1|beak',
|
||||||
|
'Billy Goat|1|beard',
|
||||||
|
'Bat|1|wing',
|
||||||
|
'Koala|2|heart',
|
||||||
|
'Wolf|2|paw',
|
||||||
|
'Whippet|2|collar',
|
||||||
|
'Uruk|2|boot',
|
||||||
|
'Poroid|4|node',
|
||||||
|
'Moakum|8|frenum',
|
||||||
|
'Fly|0|*',
|
||||||
|
'Hogbird|3|curl',
|
||||||
|
],
|
||||||
|
'OffenseAttrib': [
|
||||||
|
'Polished|+1',
|
||||||
|
'Serrated|+1',
|
||||||
|
'Heavy|+1',
|
||||||
|
'Pronged|+2',
|
||||||
|
'Steely|+2',
|
||||||
|
'Vicious|+3',
|
||||||
|
'Venomed|+4',
|
||||||
|
'Stabbity|+4',
|
||||||
|
'Dancing|+5',
|
||||||
|
'Invisible|+6',
|
||||||
|
'Vorpal|+7',
|
||||||
|
],
|
||||||
|
'OffenseBad': [
|
||||||
|
'Dull|-2',
|
||||||
|
'Tarnished|-1',
|
||||||
|
'Rusty|-3',
|
||||||
|
'Padded|-5',
|
||||||
|
'Bent|-4',
|
||||||
|
'Mini|-4',
|
||||||
|
'Rubber|-6',
|
||||||
|
'Nerf|-7',
|
||||||
|
'Unbalanced|-2',
|
||||||
|
],
|
||||||
|
'Races': [
|
||||||
|
'Half Orc|HP Max',
|
||||||
|
'Half Man|CHA',
|
||||||
|
'Half Halfling|DEX',
|
||||||
|
'Double Hobbit|STR',
|
||||||
|
'Hob-Hobbit|DEX,CON',
|
||||||
|
'Low Elf|CON',
|
||||||
|
'Dung Elf|WIS',
|
||||||
|
'Talking Pony|MP Max,INT',
|
||||||
|
'Gyrognome|DEX',
|
||||||
|
'Lesser Dwarf|CON',
|
||||||
|
'Crested Dwarf|CHA',
|
||||||
|
'Eel Man|DEX',
|
||||||
|
'Panda Man|CON,STR',
|
||||||
|
'Trans-Kobold|WIS',
|
||||||
|
'Enchanted Motorcycle|MP Max',
|
||||||
|
"Will o' the Wisp|WIS",
|
||||||
|
'Battle-Finch|DEX,INT',
|
||||||
|
'Double Wookiee|STR',
|
||||||
|
'Skraeling|WIS',
|
||||||
|
'Demicanadian|CON',
|
||||||
|
'Land Squid|STR,HP Max',
|
||||||
|
],
|
||||||
|
'Shields': [
|
||||||
|
'Parasol|0',
|
||||||
|
'Pie Plate|1',
|
||||||
|
'Garbage Can Lid|2',
|
||||||
|
'Buckler|3',
|
||||||
|
'Plexiglass|4',
|
||||||
|
'Fender|4',
|
||||||
|
'Round Shield|5',
|
||||||
|
'Carapace|5',
|
||||||
|
'Scutum|6',
|
||||||
|
'Propugner|6',
|
||||||
|
'Kite Shield|7',
|
||||||
|
'Pavise|8',
|
||||||
|
'Tower Shield|9',
|
||||||
|
'Baroque Shield|11',
|
||||||
|
'Aegis|12',
|
||||||
|
'Magnetic Field|18',
|
||||||
|
],
|
||||||
|
'Specials': [
|
||||||
|
'Diadem',
|
||||||
|
'Festoon',
|
||||||
|
'Gemstone',
|
||||||
|
'Phial',
|
||||||
|
'Tiara',
|
||||||
|
'Scabbard',
|
||||||
|
'Arrow',
|
||||||
|
'Lens',
|
||||||
|
'Lamp',
|
||||||
|
'Hymnal',
|
||||||
|
'Fleece',
|
||||||
|
'Laurel',
|
||||||
|
'Brooch',
|
||||||
|
'Gimlet',
|
||||||
|
'Cobble',
|
||||||
|
'Albatross',
|
||||||
|
'Brazier',
|
||||||
|
'Bandolier',
|
||||||
|
'Tome',
|
||||||
|
'Garnet',
|
||||||
|
'Amethyst',
|
||||||
|
'Candelabra',
|
||||||
|
'Corset',
|
||||||
|
'Sphere',
|
||||||
|
'Sceptre',
|
||||||
|
'Ankh',
|
||||||
|
'Talisman',
|
||||||
|
'Orb',
|
||||||
|
'Gammel',
|
||||||
|
'Ornament',
|
||||||
|
'Brocade',
|
||||||
|
'Galoon',
|
||||||
|
'Bijou',
|
||||||
|
'Spangle',
|
||||||
|
'Gimcrack',
|
||||||
|
'Hood',
|
||||||
|
'Vulpeculum',
|
||||||
|
],
|
||||||
|
'Spells': [
|
||||||
|
'Slime Finger',
|
||||||
|
'Rabbit Punch',
|
||||||
|
'Hastiness',
|
||||||
|
'Good Move',
|
||||||
|
'Sadness',
|
||||||
|
'Seasick',
|
||||||
|
'Shoelaces',
|
||||||
|
'Inoculate',
|
||||||
|
'Cone of Annoyance',
|
||||||
|
'Magnetic Orb',
|
||||||
|
'Invisible Hands',
|
||||||
|
'Revolting Cloud',
|
||||||
|
'Aqueous Humor',
|
||||||
|
'Spectral Miasma',
|
||||||
|
'Clever Fellow',
|
||||||
|
'Lockjaw',
|
||||||
|
'History Lesson',
|
||||||
|
'Hydrophobia',
|
||||||
|
'Big Sister',
|
||||||
|
'Cone of Paste',
|
||||||
|
'Mulligan',
|
||||||
|
"Nestor's Bright Idea",
|
||||||
|
'Holy Batpole',
|
||||||
|
'Tumor (Benign)',
|
||||||
|
'Braingate',
|
||||||
|
'Nonplus',
|
||||||
|
'Animate Nightstand',
|
||||||
|
'Eye of the Troglodyte',
|
||||||
|
'Curse Name',
|
||||||
|
'Dropsy',
|
||||||
|
'Vitreous Humor',
|
||||||
|
"Roger's Grand Illusion",
|
||||||
|
'Covet',
|
||||||
|
'Astral Miasma',
|
||||||
|
'Spectral Oyster',
|
||||||
|
'Acrid Hands',
|
||||||
|
'Angioplasty',
|
||||||
|
"Grognor's Big Day Off",
|
||||||
|
'Tumor (Malignant)',
|
||||||
|
'Animate Tunic',
|
||||||
|
'Ursine Armor',
|
||||||
|
'Holy Roller',
|
||||||
|
'Tonsillectomy',
|
||||||
|
'Curse Family',
|
||||||
|
'Infinite Confusion',
|
||||||
|
],
|
||||||
|
'Titles': [
|
||||||
|
'Mr.',
|
||||||
|
'Mrs.',
|
||||||
|
'Sir',
|
||||||
|
'Sgt.',
|
||||||
|
'Ms.',
|
||||||
|
'Captain',
|
||||||
|
'Chief',
|
||||||
|
'Admiral',
|
||||||
|
'Saint',
|
||||||
|
],
|
||||||
|
'Weapons': [
|
||||||
|
'Stick|0',
|
||||||
|
'Broken Bottle|1',
|
||||||
|
'Shiv|1',
|
||||||
|
'Sprig|1',
|
||||||
|
'Oxgoad|1',
|
||||||
|
'Eelspear|2',
|
||||||
|
'Bowie Knife|2',
|
||||||
|
'Claw Hammer|2',
|
||||||
|
'Handpeen|2',
|
||||||
|
'Andiron|3',
|
||||||
|
'Hatchet|3',
|
||||||
|
'Tomahawk|3',
|
||||||
|
'Hackbarm|3',
|
||||||
|
'Crowbar|4',
|
||||||
|
'Mace|4',
|
||||||
|
'Battleadze|4',
|
||||||
|
'Leafmace|5',
|
||||||
|
'Shortsword|5',
|
||||||
|
'Longiron|5',
|
||||||
|
'Poachard|5',
|
||||||
|
'Baselard|5',
|
||||||
|
'Whinyard|6',
|
||||||
|
'Blunderbuss|6',
|
||||||
|
'Longsword|6',
|
||||||
|
'Crankbow|6',
|
||||||
|
'Blibo|7',
|
||||||
|
'Broadsword|7',
|
||||||
|
'Kreen|7',
|
||||||
|
'Morning Star|8',
|
||||||
|
'Pole-adze|8',
|
||||||
|
'Spontoon|8',
|
||||||
|
'Bastard Sword|9',
|
||||||
|
'Peen-arm|9',
|
||||||
|
'Culverin|10',
|
||||||
|
'Lance|10',
|
||||||
|
'Halberd|11',
|
||||||
|
'Poleax|12',
|
||||||
|
'Bandyclef|15',
|
||||||
|
],
|
||||||
|
};
|
||||||
6
lib/main.dart
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import 'package:askiineverdie/src/app.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
runApp(const AskiiNeverDieApp());
|
||||||
|
}
|
||||||
145
lib/src/app.dart
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:askiineverdie/src/core/engine/game_mutations.dart';
|
||||||
|
import 'package:askiineverdie/src/core/engine/progress_service.dart';
|
||||||
|
import 'package:askiineverdie/src/core/engine/reward_service.dart';
|
||||||
|
import 'package:askiineverdie/src/core/model/game_state.dart';
|
||||||
|
import 'package:askiineverdie/src/core/model/pq_config.dart';
|
||||||
|
import 'package:askiineverdie/src/core/storage/save_manager.dart';
|
||||||
|
import 'package:askiineverdie/src/core/storage/save_repository.dart';
|
||||||
|
import 'package:askiineverdie/src/features/front/front_screen.dart';
|
||||||
|
import 'package:askiineverdie/src/features/front/save_picker_dialog.dart';
|
||||||
|
import 'package:askiineverdie/src/features/game/game_play_screen.dart';
|
||||||
|
import 'package:askiineverdie/src/features/game/game_session_controller.dart';
|
||||||
|
import 'package:askiineverdie/src/features/new_character/new_character_screen.dart';
|
||||||
|
|
||||||
|
class AskiiNeverDieApp extends StatefulWidget {
|
||||||
|
const AskiiNeverDieApp({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<AskiiNeverDieApp> createState() => _AskiiNeverDieAppState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AskiiNeverDieAppState extends State<AskiiNeverDieApp> {
|
||||||
|
late final GameSessionController _controller;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
const config = PqConfig();
|
||||||
|
final mutations = GameMutations(config);
|
||||||
|
final rewards = RewardService(mutations);
|
||||||
|
|
||||||
|
_controller = GameSessionController(
|
||||||
|
progressService: ProgressService(
|
||||||
|
config: config,
|
||||||
|
mutations: mutations,
|
||||||
|
rewards: rewards,
|
||||||
|
),
|
||||||
|
saveManager: SaveManager(SaveRepository()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_controller.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return MaterialApp(
|
||||||
|
title: 'Ascii Never Die',
|
||||||
|
debugShowCheckedModeBanner: false,
|
||||||
|
theme: ThemeData(
|
||||||
|
colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF234361)),
|
||||||
|
scaffoldBackgroundColor: const Color(0xFFF4F5F7),
|
||||||
|
useMaterial3: true,
|
||||||
|
),
|
||||||
|
home: FrontScreen(
|
||||||
|
onNewCharacter: _navigateToNewCharacter,
|
||||||
|
onLoadSave: _loadSave,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _navigateToNewCharacter(BuildContext context) {
|
||||||
|
Navigator.of(context).push(
|
||||||
|
MaterialPageRoute<void>(
|
||||||
|
builder: (context) => NewCharacterScreen(
|
||||||
|
onCharacterCreated: (initialState) {
|
||||||
|
_startGame(context, initialState);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadSave(BuildContext context) async {
|
||||||
|
// 저장 파일 목록 조회
|
||||||
|
final saves = await _controller.saveManager.listSaves();
|
||||||
|
|
||||||
|
if (!context.mounted) return;
|
||||||
|
|
||||||
|
String? selectedFileName;
|
||||||
|
|
||||||
|
if (saves.isEmpty) {
|
||||||
|
// 저장 파일이 없으면 안내 메시지
|
||||||
|
ScaffoldMessenger.of(
|
||||||
|
context,
|
||||||
|
).showSnackBar(const SnackBar(content: Text('저장된 게임이 없습니다.')));
|
||||||
|
return;
|
||||||
|
} else if (saves.length == 1) {
|
||||||
|
// 파일이 하나면 바로 선택
|
||||||
|
selectedFileName = saves.first.fileName;
|
||||||
|
} else {
|
||||||
|
// 여러 개면 다이얼로그 표시
|
||||||
|
selectedFileName = await SavePickerDialog.show(context, saves);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedFileName == null || !context.mounted) return;
|
||||||
|
|
||||||
|
// 선택된 파일 로드
|
||||||
|
await _controller.loadAndStart(
|
||||||
|
fileName: selectedFileName,
|
||||||
|
cheatsEnabled: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (_controller.status == GameSessionStatus.running) {
|
||||||
|
if (context.mounted) {
|
||||||
|
_navigateToGame(context);
|
||||||
|
}
|
||||||
|
} else if (_controller.status == GameSessionStatus.error) {
|
||||||
|
if (context.mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(
|
||||||
|
'저장 파일을 불러올 수 없습니다: ${_controller.error ?? "알 수 없는 오류"}',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _startGame(BuildContext context, GameState initialState) async {
|
||||||
|
await _controller.startNew(initialState, cheatsEnabled: false);
|
||||||
|
|
||||||
|
if (context.mounted) {
|
||||||
|
// NewCharacterScreen을 pop하고 GamePlayScreen으로 이동
|
||||||
|
Navigator.of(context).pushReplacement(
|
||||||
|
MaterialPageRoute<void>(
|
||||||
|
builder: (context) => GamePlayScreen(controller: _controller),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _navigateToGame(BuildContext context) {
|
||||||
|
Navigator.of(context).push(
|
||||||
|
MaterialPageRoute<void>(
|
||||||
|
builder: (context) => GamePlayScreen(controller: _controller),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
86
lib/src/core/engine/game_mutations.dart
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
import 'package:askiineverdie/src/core/model/equipment_slot.dart';
|
||||||
|
import 'package:askiineverdie/src/core/model/game_state.dart';
|
||||||
|
import 'package:askiineverdie/src/core/model/pq_config.dart';
|
||||||
|
import 'package:askiineverdie/src/core/util/pq_logic.dart' as pq_logic;
|
||||||
|
|
||||||
|
/// Game state mutations that mirror the original PQ win/reward logic.
|
||||||
|
class GameMutations {
|
||||||
|
const GameMutations(this.config);
|
||||||
|
|
||||||
|
final PqConfig config;
|
||||||
|
|
||||||
|
GameState winEquip(GameState state, int level, EquipmentSlot slot) {
|
||||||
|
final rng = state.rng;
|
||||||
|
final name = pq_logic.winEquip(config, rng, level, slot);
|
||||||
|
final equip = state.equipment;
|
||||||
|
final updatedEquip = switch (slot) {
|
||||||
|
EquipmentSlot.weapon => equip.copyWith(
|
||||||
|
weapon: name,
|
||||||
|
bestIndex: EquipmentSlot.weapon.index,
|
||||||
|
),
|
||||||
|
EquipmentSlot.shield => equip.copyWith(
|
||||||
|
shield: name,
|
||||||
|
bestIndex: EquipmentSlot.shield.index,
|
||||||
|
),
|
||||||
|
EquipmentSlot.armor => equip.copyWith(
|
||||||
|
armor: name,
|
||||||
|
bestIndex: EquipmentSlot.armor.index,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
return state.copyWith(rng: rng, equipment: updatedEquip);
|
||||||
|
}
|
||||||
|
|
||||||
|
GameState winStat(GameState state) {
|
||||||
|
final updatedStats = pq_logic.winStat(state.stats, state.rng);
|
||||||
|
return state.copyWith(rng: state.rng, stats: updatedStats);
|
||||||
|
}
|
||||||
|
|
||||||
|
GameState winSpell(GameState state, int wisdom, int level) {
|
||||||
|
final result = pq_logic.winSpell(config, state.rng, wisdom, level);
|
||||||
|
final parts = result.split('|');
|
||||||
|
final name = parts[0];
|
||||||
|
final rank = parts.length > 1 ? parts[1] : 'I';
|
||||||
|
|
||||||
|
final spells = [...state.spellBook.spells];
|
||||||
|
final index = spells.indexWhere((s) => s.name == name);
|
||||||
|
if (index >= 0) {
|
||||||
|
spells[index] = spells[index].copyWith(rank: rank);
|
||||||
|
} else {
|
||||||
|
spells.add(SpellEntry(name: name, rank: rank));
|
||||||
|
}
|
||||||
|
|
||||||
|
return state.copyWith(
|
||||||
|
rng: state.rng,
|
||||||
|
spellBook: state.spellBook.copyWith(spells: spells),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
GameState winItem(GameState state) {
|
||||||
|
final rng = state.rng;
|
||||||
|
final result = pq_logic.winItem(config, rng, state.inventory.items.length);
|
||||||
|
final items = [...state.inventory.items];
|
||||||
|
|
||||||
|
if (result.isEmpty) {
|
||||||
|
// Duplicate an existing item if possible.
|
||||||
|
if (items.isNotEmpty) {
|
||||||
|
final pickIndex = rng.nextInt(items.length);
|
||||||
|
final picked = items[pickIndex];
|
||||||
|
items[pickIndex] = picked.copyWith(count: picked.count + 1);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
final existing = items.indexWhere((e) => e.name == result);
|
||||||
|
if (existing >= 0) {
|
||||||
|
items[existing] = items[existing].copyWith(
|
||||||
|
count: items[existing].count + 1,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
items.add(InventoryEntry(name: result, count: 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return state.copyWith(
|
||||||
|
rng: rng,
|
||||||
|
inventory: state.inventory.copyWith(items: items),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
138
lib/src/core/engine/progress_loop.dart
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:askiineverdie/src/core/model/game_state.dart';
|
||||||
|
import 'package:askiineverdie/src/core/storage/save_manager.dart';
|
||||||
|
import 'package:askiineverdie/src/core/engine/progress_service.dart';
|
||||||
|
|
||||||
|
class AutoSaveConfig {
|
||||||
|
const AutoSaveConfig({
|
||||||
|
this.onLevelUp = true,
|
||||||
|
this.onQuestComplete = true,
|
||||||
|
this.onActComplete = true,
|
||||||
|
this.onStop = true,
|
||||||
|
});
|
||||||
|
|
||||||
|
final bool onLevelUp;
|
||||||
|
final bool onQuestComplete;
|
||||||
|
final bool onActComplete;
|
||||||
|
final bool onStop;
|
||||||
|
|
||||||
|
bool shouldSave(ProgressTickResult result) {
|
||||||
|
return (onLevelUp && result.leveledUp) ||
|
||||||
|
(onQuestComplete && result.completedQuest) ||
|
||||||
|
(onActComplete && result.completedAct);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Runs the periodic timer loop that advances tasks/quests/plots.
|
||||||
|
class ProgressLoop {
|
||||||
|
ProgressLoop({
|
||||||
|
required GameState initialState,
|
||||||
|
required this.progressService,
|
||||||
|
this.saveManager,
|
||||||
|
Duration tickInterval = const Duration(milliseconds: 50),
|
||||||
|
AutoSaveConfig autoSaveConfig = const AutoSaveConfig(),
|
||||||
|
DateTime Function()? now,
|
||||||
|
this.cheatsEnabled = false,
|
||||||
|
}) : _state = initialState,
|
||||||
|
_tickInterval = tickInterval,
|
||||||
|
_autoSaveConfig = autoSaveConfig,
|
||||||
|
_now = now ?? DateTime.now,
|
||||||
|
_stateController = StreamController<GameState>.broadcast();
|
||||||
|
|
||||||
|
final ProgressService progressService;
|
||||||
|
final SaveManager? saveManager;
|
||||||
|
final Duration _tickInterval;
|
||||||
|
final AutoSaveConfig _autoSaveConfig;
|
||||||
|
final DateTime Function() _now;
|
||||||
|
final StreamController<GameState> _stateController;
|
||||||
|
bool cheatsEnabled;
|
||||||
|
|
||||||
|
Timer? _timer;
|
||||||
|
int? _lastTickMs;
|
||||||
|
int _speedMultiplier = 1;
|
||||||
|
|
||||||
|
GameState get current => _state;
|
||||||
|
Stream<GameState> get stream => _stateController.stream;
|
||||||
|
GameState _state;
|
||||||
|
|
||||||
|
/// 현재 배속 (1x, 2x, 5x)
|
||||||
|
int get speedMultiplier => _speedMultiplier;
|
||||||
|
|
||||||
|
/// 배속 순환: 1 -> 2 -> 5 -> 1
|
||||||
|
void cycleSpeed() {
|
||||||
|
_speedMultiplier = switch (_speedMultiplier) {
|
||||||
|
1 => 2,
|
||||||
|
2 => 5,
|
||||||
|
_ => 1,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
void start() {
|
||||||
|
_lastTickMs = _now().millisecondsSinceEpoch;
|
||||||
|
_timer ??= Timer.periodic(_tickInterval, (_) => tickOnce());
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> stop({bool saveOnStop = false}) async {
|
||||||
|
_timer?.cancel();
|
||||||
|
_timer = null;
|
||||||
|
if (saveOnStop && _autoSaveConfig.onStop && saveManager != null) {
|
||||||
|
await saveManager!.saveState(_state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void dispose() {
|
||||||
|
_timer?.cancel();
|
||||||
|
_stateController.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Run one iteration of the loop (used by Timer or manual stepping).
|
||||||
|
GameState tickOnce({int? deltaMillis}) {
|
||||||
|
final baseDelta = deltaMillis ?? _computeDelta();
|
||||||
|
final delta = baseDelta * _speedMultiplier;
|
||||||
|
final result = progressService.tick(_state, delta);
|
||||||
|
_state = result.state;
|
||||||
|
_stateController.add(_state);
|
||||||
|
|
||||||
|
if (saveManager != null && _autoSaveConfig.shouldSave(result)) {
|
||||||
|
saveManager!.saveState(_state);
|
||||||
|
}
|
||||||
|
return _state;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Replace state (e.g., after loading) and reset timing.
|
||||||
|
void replaceState(GameState newState) {
|
||||||
|
_state = newState;
|
||||||
|
_stateController.add(newState);
|
||||||
|
_lastTickMs = _now().millisecondsSinceEpoch;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Developer-only helpers mirroring original cheat panel actions.
|
||||||
|
void cheatCompleteTask() {
|
||||||
|
if (!cheatsEnabled) return;
|
||||||
|
_state = progressService.forceTaskComplete(_state);
|
||||||
|
_stateController.add(_state);
|
||||||
|
}
|
||||||
|
|
||||||
|
void cheatCompleteQuest() {
|
||||||
|
if (!cheatsEnabled) return;
|
||||||
|
_state = progressService.forceQuestComplete(_state);
|
||||||
|
_stateController.add(_state);
|
||||||
|
}
|
||||||
|
|
||||||
|
void cheatCompletePlot() {
|
||||||
|
if (!cheatsEnabled) return;
|
||||||
|
_state = progressService.forcePlotComplete(_state);
|
||||||
|
_stateController.add(_state);
|
||||||
|
}
|
||||||
|
|
||||||
|
int _computeDelta() {
|
||||||
|
final nowMs = _now().millisecondsSinceEpoch;
|
||||||
|
final last = _lastTickMs;
|
||||||
|
_lastTickMs = nowMs;
|
||||||
|
if (last == null) return 0;
|
||||||
|
final delta = nowMs - last;
|
||||||
|
if (delta < 0) return 0;
|
||||||
|
return delta;
|
||||||
|
}
|
||||||
|
}
|
||||||
712
lib/src/core/engine/progress_service.dart
Normal file
@@ -0,0 +1,712 @@
|
|||||||
|
import 'dart:math' as math;
|
||||||
|
|
||||||
|
import 'package:askiineverdie/src/core/engine/game_mutations.dart';
|
||||||
|
import 'package:askiineverdie/src/core/engine/reward_service.dart';
|
||||||
|
import 'package:askiineverdie/src/core/model/equipment_slot.dart';
|
||||||
|
import 'package:askiineverdie/src/core/model/game_state.dart';
|
||||||
|
import 'package:askiineverdie/src/core/model/pq_config.dart';
|
||||||
|
import 'package:askiineverdie/src/core/util/pq_logic.dart' as pq_logic;
|
||||||
|
|
||||||
|
class ProgressTickResult {
|
||||||
|
const ProgressTickResult({
|
||||||
|
required this.state,
|
||||||
|
this.leveledUp = false,
|
||||||
|
this.completedQuest = false,
|
||||||
|
this.completedAct = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
final GameState state;
|
||||||
|
final bool leveledUp;
|
||||||
|
final bool completedQuest;
|
||||||
|
final bool completedAct;
|
||||||
|
|
||||||
|
bool get shouldAutosave => leveledUp || completedQuest || completedAct;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Drives quest/plot/task progression by applying queued actions and rewards.
|
||||||
|
class ProgressService {
|
||||||
|
ProgressService({
|
||||||
|
required this.config,
|
||||||
|
required this.mutations,
|
||||||
|
required this.rewards,
|
||||||
|
});
|
||||||
|
|
||||||
|
final PqConfig config;
|
||||||
|
final GameMutations mutations;
|
||||||
|
final RewardService rewards;
|
||||||
|
|
||||||
|
/// 새 게임 초기화 (원본 GoButtonClick, Main.pas:741-767)
|
||||||
|
/// Prologue 태스크들을 큐에 추가하고 첫 태스크 시작
|
||||||
|
GameState initializeNewGame(GameState state) {
|
||||||
|
// 초기 큐 설정 (원본 753-757줄)
|
||||||
|
final initialQueue = <QueueEntry>[
|
||||||
|
const QueueEntry(
|
||||||
|
kind: QueueKind.task,
|
||||||
|
durationMillis: 10 * 1000,
|
||||||
|
caption: 'Experiencing an enigmatic and foreboding night vision',
|
||||||
|
taskType: TaskType.load,
|
||||||
|
),
|
||||||
|
const QueueEntry(
|
||||||
|
kind: QueueKind.task,
|
||||||
|
durationMillis: 6 * 1000,
|
||||||
|
caption: "Much is revealed about that wise old bastard you'd "
|
||||||
|
'underestimated',
|
||||||
|
taskType: TaskType.load,
|
||||||
|
),
|
||||||
|
const QueueEntry(
|
||||||
|
kind: QueueKind.task,
|
||||||
|
durationMillis: 6 * 1000,
|
||||||
|
caption: 'A shocking series of events leaves you alone and bewildered, '
|
||||||
|
'but resolute',
|
||||||
|
taskType: TaskType.load,
|
||||||
|
),
|
||||||
|
const QueueEntry(
|
||||||
|
kind: QueueKind.task,
|
||||||
|
durationMillis: 4 * 1000,
|
||||||
|
caption: 'Drawing upon an unexpected reserve of determination, '
|
||||||
|
'you set out on a long and dangerous journey',
|
||||||
|
taskType: TaskType.load,
|
||||||
|
),
|
||||||
|
const QueueEntry(
|
||||||
|
kind: QueueKind.plot,
|
||||||
|
durationMillis: 2 * 1000,
|
||||||
|
caption: 'Loading',
|
||||||
|
taskType: TaskType.plot,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
// 첫 번째 태스크 'Loading' 시작 (원본 752줄)
|
||||||
|
final taskResult = pq_logic.startTask(
|
||||||
|
state.progress,
|
||||||
|
'Loading',
|
||||||
|
2 * 1000,
|
||||||
|
);
|
||||||
|
|
||||||
|
// ExpBar 초기화 (원본 743-746줄)
|
||||||
|
final expBar = ProgressBarState(
|
||||||
|
position: 0,
|
||||||
|
max: pq_logic.levelUpTime(1),
|
||||||
|
);
|
||||||
|
|
||||||
|
// PlotBar 초기화 (원본 759줄)
|
||||||
|
final plotBar = const ProgressBarState(position: 0, max: 26 * 1000);
|
||||||
|
|
||||||
|
final progress = taskResult.progress.copyWith(
|
||||||
|
exp: expBar,
|
||||||
|
plot: plotBar,
|
||||||
|
currentTask: const TaskInfo(caption: 'Loading...', type: TaskType.load),
|
||||||
|
plotStageCount: 1, // Prologue
|
||||||
|
questCount: 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
return _recalculateEncumbrance(
|
||||||
|
state.copyWith(
|
||||||
|
progress: progress,
|
||||||
|
queue: QueueState(entries: initialQueue),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Starts a task and tags its type (kill, plot, load, neutral).
|
||||||
|
GameState startTask(
|
||||||
|
GameState state, {
|
||||||
|
required String caption,
|
||||||
|
required int durationMillis,
|
||||||
|
TaskType taskType = TaskType.neutral,
|
||||||
|
}) {
|
||||||
|
final taskResult = pq_logic.startTask(
|
||||||
|
state.progress,
|
||||||
|
caption,
|
||||||
|
durationMillis,
|
||||||
|
);
|
||||||
|
final progress = taskResult.progress.copyWith(
|
||||||
|
currentTask: TaskInfo(caption: taskResult.caption, type: taskType),
|
||||||
|
);
|
||||||
|
return state.copyWith(progress: progress);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tick the timer loop (equivalent to Timer1Timer in the original code).
|
||||||
|
ProgressTickResult tick(GameState state, int elapsedMillis) {
|
||||||
|
final int clamped = elapsedMillis.clamp(0, 100).toInt();
|
||||||
|
var progress = state.progress;
|
||||||
|
var queue = state.queue;
|
||||||
|
var nextState = state;
|
||||||
|
var leveledUp = false;
|
||||||
|
var questDone = false;
|
||||||
|
var actDone = false;
|
||||||
|
|
||||||
|
// Advance task bar if still running.
|
||||||
|
if (progress.task.position < progress.task.max) {
|
||||||
|
final uncapped = progress.task.position + clamped;
|
||||||
|
final int newTaskPos = uncapped > progress.task.max
|
||||||
|
? progress.task.max
|
||||||
|
: uncapped;
|
||||||
|
progress = progress.copyWith(
|
||||||
|
task: progress.task.copyWith(position: newTaskPos),
|
||||||
|
);
|
||||||
|
nextState = _recalculateEncumbrance(
|
||||||
|
nextState.copyWith(progress: progress),
|
||||||
|
);
|
||||||
|
return ProgressTickResult(state: nextState);
|
||||||
|
}
|
||||||
|
|
||||||
|
final gain = progress.currentTask.type == TaskType.kill;
|
||||||
|
final incrementSeconds = progress.task.max ~/ 1000;
|
||||||
|
|
||||||
|
// 킬 태스크 완료 시 전리품 획득 (원본 Main.pas:625-630)
|
||||||
|
if (gain) {
|
||||||
|
nextState = _winLoot(nextState);
|
||||||
|
progress = nextState.progress;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 시장/판매/구매 태스크 완료 시 처리 (원본 Main.pas:631-649)
|
||||||
|
final taskType = progress.currentTask.type;
|
||||||
|
if (taskType == TaskType.buying) {
|
||||||
|
// 장비 구매 완료 (원본 631-634)
|
||||||
|
nextState = _completeBuying(nextState);
|
||||||
|
progress = nextState.progress;
|
||||||
|
} else if (taskType == TaskType.market || taskType == TaskType.sell) {
|
||||||
|
// 시장 도착 또는 판매 완료 (원본 635-649)
|
||||||
|
final sellResult = _processSell(nextState);
|
||||||
|
nextState = sellResult.state;
|
||||||
|
progress = nextState.progress;
|
||||||
|
queue = nextState.queue;
|
||||||
|
|
||||||
|
// 판매 중이면 다른 로직 건너뛰기
|
||||||
|
if (sellResult.continuesSelling) {
|
||||||
|
nextState = _recalculateEncumbrance(
|
||||||
|
nextState.copyWith(progress: progress, queue: queue),
|
||||||
|
);
|
||||||
|
return ProgressTickResult(
|
||||||
|
state: nextState,
|
||||||
|
leveledUp: false,
|
||||||
|
completedQuest: false,
|
||||||
|
completedAct: false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gain XP / level up.
|
||||||
|
if (gain) {
|
||||||
|
if (progress.exp.position >= progress.exp.max) {
|
||||||
|
nextState = _levelUp(nextState);
|
||||||
|
leveledUp = true;
|
||||||
|
progress = nextState.progress;
|
||||||
|
} else {
|
||||||
|
final uncappedExp = progress.exp.position + incrementSeconds;
|
||||||
|
final int newExpPos = uncappedExp > progress.exp.max
|
||||||
|
? progress.exp.max
|
||||||
|
: uncappedExp;
|
||||||
|
progress = progress.copyWith(
|
||||||
|
exp: progress.exp.copyWith(position: newExpPos),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Advance quest bar after Act I.
|
||||||
|
final canQuestProgress =
|
||||||
|
gain &&
|
||||||
|
progress.plotStageCount > 1 &&
|
||||||
|
progress.questCount > 0 &&
|
||||||
|
progress.quest.max > 0;
|
||||||
|
if (canQuestProgress) {
|
||||||
|
if (progress.quest.position + incrementSeconds >= progress.quest.max) {
|
||||||
|
nextState = completeQuest(nextState);
|
||||||
|
questDone = true;
|
||||||
|
progress = nextState.progress;
|
||||||
|
queue = nextState.queue;
|
||||||
|
} else {
|
||||||
|
progress = progress.copyWith(
|
||||||
|
quest: progress.quest.copyWith(
|
||||||
|
position: progress.quest.position + incrementSeconds,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 플롯(plot) 바가 완료되면 InterplotCinematic 트리거
|
||||||
|
// (원본 Main.pas:1301-1304)
|
||||||
|
if (gain &&
|
||||||
|
progress.plot.max > 0 &&
|
||||||
|
progress.plot.position >= progress.plot.max) {
|
||||||
|
// InterplotCinematic을 호출하여 시네마틱 이벤트 큐에 추가
|
||||||
|
final cinematicEntries = pq_logic.interplotCinematic(
|
||||||
|
config,
|
||||||
|
nextState.rng,
|
||||||
|
nextState.traits.level,
|
||||||
|
nextState.progress.plotStageCount,
|
||||||
|
);
|
||||||
|
queue = QueueState(entries: [...queue.entries, ...cinematicEntries]);
|
||||||
|
// 플롯 바를 0으로 리셋하지 않음 - completeAct에서 처리됨
|
||||||
|
} else if (progress.currentTask.type != TaskType.load &&
|
||||||
|
progress.plot.max > 0) {
|
||||||
|
final uncappedPlot = progress.plot.position + incrementSeconds;
|
||||||
|
final int newPlotPos = uncappedPlot > progress.plot.max
|
||||||
|
? progress.plot.max
|
||||||
|
: uncappedPlot;
|
||||||
|
progress = progress.copyWith(
|
||||||
|
plot: progress.plot.copyWith(position: newPlotPos),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dequeue next scripted task if available.
|
||||||
|
final dq = pq_logic.dequeue(progress, queue);
|
||||||
|
if (dq != null) {
|
||||||
|
progress = dq.progress.copyWith(
|
||||||
|
currentTask: TaskInfo(caption: dq.caption, type: dq.taskType),
|
||||||
|
);
|
||||||
|
queue = dq.queue;
|
||||||
|
|
||||||
|
// plot 타입이 dequeue 되면 completeAct 실행 (원본 Main.pas 로직)
|
||||||
|
if (dq.kind == QueueKind.plot) {
|
||||||
|
nextState = nextState.copyWith(progress: progress, queue: queue);
|
||||||
|
nextState = completeAct(nextState);
|
||||||
|
actDone = true;
|
||||||
|
progress = nextState.progress;
|
||||||
|
queue = nextState.queue;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 큐가 비어있으면 새 태스크 생성 (원본 Dequeue 667-684줄)
|
||||||
|
nextState = nextState.copyWith(progress: progress, queue: queue);
|
||||||
|
final newTaskResult = _generateNextTask(nextState);
|
||||||
|
progress = newTaskResult.progress;
|
||||||
|
queue = newTaskResult.queue;
|
||||||
|
}
|
||||||
|
|
||||||
|
nextState = _recalculateEncumbrance(
|
||||||
|
nextState.copyWith(progress: progress, queue: queue),
|
||||||
|
);
|
||||||
|
|
||||||
|
return ProgressTickResult(
|
||||||
|
state: nextState,
|
||||||
|
leveledUp: leveledUp,
|
||||||
|
completedQuest: questDone,
|
||||||
|
completedAct: actDone,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 큐가 비어있을 때 다음 태스크 생성 (원본 Dequeue 667-684줄)
|
||||||
|
({ProgressState progress, QueueState queue}) _generateNextTask(
|
||||||
|
GameState state,
|
||||||
|
) {
|
||||||
|
var progress = state.progress;
|
||||||
|
final queue = state.queue;
|
||||||
|
final oldTaskType = progress.currentTask.type;
|
||||||
|
|
||||||
|
// 1. Encumbrance가 가득 찼으면 시장으로 이동 (원본 667-669줄)
|
||||||
|
if (progress.encumbrance.position >= progress.encumbrance.max &&
|
||||||
|
progress.encumbrance.max > 0) {
|
||||||
|
final taskResult = pq_logic.startTask(
|
||||||
|
progress,
|
||||||
|
'Heading to market to sell loot',
|
||||||
|
4 * 1000,
|
||||||
|
);
|
||||||
|
progress = taskResult.progress.copyWith(
|
||||||
|
currentTask: TaskInfo(
|
||||||
|
caption: taskResult.caption,
|
||||||
|
type: TaskType.market,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return (progress: progress, queue: queue);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. kill 태스크가 아니었고 heading도 아니면 heading 또는 buying 태스크 실행
|
||||||
|
// (원본 670-677줄)
|
||||||
|
if (oldTaskType != TaskType.kill && oldTaskType != TaskType.neutral) {
|
||||||
|
// Gold가 충분하면 장비 구매 (원본 671-673줄)
|
||||||
|
final gold = _getGold(state);
|
||||||
|
final equipPrice = _equipPrice(state.traits.level);
|
||||||
|
if (gold > equipPrice) {
|
||||||
|
final taskResult = pq_logic.startTask(
|
||||||
|
progress,
|
||||||
|
'Negotiating purchase of better equipment',
|
||||||
|
5 * 1000,
|
||||||
|
);
|
||||||
|
progress = taskResult.progress.copyWith(
|
||||||
|
currentTask: TaskInfo(
|
||||||
|
caption: taskResult.caption,
|
||||||
|
type: TaskType.buying,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return (progress: progress, queue: queue);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gold가 부족하면 전장으로 이동 (원본 674-676줄)
|
||||||
|
final taskResult = pq_logic.startTask(
|
||||||
|
progress,
|
||||||
|
'Heading to the killing fields',
|
||||||
|
4 * 1000,
|
||||||
|
);
|
||||||
|
progress = taskResult.progress.copyWith(
|
||||||
|
currentTask: TaskInfo(
|
||||||
|
caption: taskResult.caption,
|
||||||
|
type: TaskType.neutral,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return (progress: progress, queue: queue);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. MonsterTask 실행 (원본 678-684줄)
|
||||||
|
final level = state.traits.level;
|
||||||
|
final monster = pq_logic.monsterTask(
|
||||||
|
config,
|
||||||
|
state.rng,
|
||||||
|
level,
|
||||||
|
null, // questMonster
|
||||||
|
null, // questLevel
|
||||||
|
);
|
||||||
|
|
||||||
|
// 태스크 지속시간 계산 (원본 682줄)
|
||||||
|
// n := (2 * InventoryLabelAlsoGameStyle.Tag * n * 1000) div l;
|
||||||
|
// InventoryLabelAlsoGameStyle.Tag는 게임 스타일을 나타내는 값 (1이 기본)
|
||||||
|
const gameStyleTag = 1;
|
||||||
|
final durationMillis = (2 * gameStyleTag * level * 1000) ~/ level;
|
||||||
|
|
||||||
|
final taskResult = pq_logic.startTask(
|
||||||
|
progress,
|
||||||
|
'Executing $monster',
|
||||||
|
durationMillis,
|
||||||
|
);
|
||||||
|
|
||||||
|
progress = taskResult.progress.copyWith(
|
||||||
|
currentTask: TaskInfo(
|
||||||
|
caption: taskResult.caption,
|
||||||
|
type: TaskType.kill,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return (progress: progress, queue: queue);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Advances quest completion, applies reward, and enqueues next quest task.
|
||||||
|
GameState completeQuest(GameState state) {
|
||||||
|
final result = pq_logic.completeQuest(
|
||||||
|
config,
|
||||||
|
state.rng,
|
||||||
|
state.traits.level,
|
||||||
|
);
|
||||||
|
|
||||||
|
var nextState = _applyReward(state, result.reward);
|
||||||
|
final questCount = nextState.progress.questCount + 1;
|
||||||
|
|
||||||
|
// Append quest entry to queue (task kind).
|
||||||
|
final updatedQueue = QueueState(
|
||||||
|
entries: [
|
||||||
|
...nextState.queue.entries,
|
||||||
|
QueueEntry(
|
||||||
|
kind: QueueKind.task,
|
||||||
|
durationMillis: 50 + nextState.rng.nextInt(100),
|
||||||
|
caption: result.caption,
|
||||||
|
taskType: TaskType.neutral,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Update quest progress bar with reset position.
|
||||||
|
final progress = nextState.progress.copyWith(
|
||||||
|
quest: ProgressBarState(
|
||||||
|
position: 0,
|
||||||
|
max: 50 + nextState.rng.nextInt(100),
|
||||||
|
),
|
||||||
|
questCount: questCount,
|
||||||
|
);
|
||||||
|
|
||||||
|
return _recalculateEncumbrance(
|
||||||
|
nextState.copyWith(progress: progress, queue: updatedQueue),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Advances plot to next act and applies any act-level rewards.
|
||||||
|
GameState completeAct(GameState state) {
|
||||||
|
final actResult = pq_logic.completeAct(state.progress.plotStageCount);
|
||||||
|
var nextState = state;
|
||||||
|
for (final reward in actResult.rewards) {
|
||||||
|
nextState = _applyReward(nextState, reward);
|
||||||
|
}
|
||||||
|
|
||||||
|
final plotStages = nextState.progress.plotStageCount + 1;
|
||||||
|
var updatedProgress = nextState.progress.copyWith(
|
||||||
|
plot: ProgressBarState(position: 0, max: actResult.plotBarMaxSeconds),
|
||||||
|
plotStageCount: plotStages,
|
||||||
|
);
|
||||||
|
|
||||||
|
nextState = nextState.copyWith(progress: updatedProgress);
|
||||||
|
|
||||||
|
// Act I 완료 후(Prologue -> Act I) 첫 퀘스트 시작 (원본 Main.pas 로직)
|
||||||
|
// plotStages == 2는 Prologue(1) -> Act I(2) 전환을 의미
|
||||||
|
if (plotStages == 2) {
|
||||||
|
nextState = _startFirstQuest(nextState);
|
||||||
|
}
|
||||||
|
|
||||||
|
return _recalculateEncumbrance(nextState);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 첫 퀘스트 시작 (Act I 시작 시)
|
||||||
|
GameState _startFirstQuest(GameState state) {
|
||||||
|
final result = pq_logic.completeQuest(
|
||||||
|
config,
|
||||||
|
state.rng,
|
||||||
|
state.traits.level,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 퀘스트 바 초기화
|
||||||
|
final questBar = ProgressBarState(
|
||||||
|
position: 0,
|
||||||
|
max: 50 + state.rng.nextInt(100),
|
||||||
|
);
|
||||||
|
|
||||||
|
// 첫 퀘스트 추가
|
||||||
|
final updatedQueue = QueueState(
|
||||||
|
entries: [
|
||||||
|
...state.queue.entries,
|
||||||
|
QueueEntry(
|
||||||
|
kind: QueueKind.task,
|
||||||
|
durationMillis: 50 + state.rng.nextInt(100),
|
||||||
|
caption: result.caption,
|
||||||
|
taskType: TaskType.neutral,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
final progress = state.progress.copyWith(
|
||||||
|
quest: questBar,
|
||||||
|
questCount: 1,
|
||||||
|
);
|
||||||
|
|
||||||
|
return state.copyWith(progress: progress, queue: updatedQueue);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Developer-only cheat hooks for quickly finishing bars.
|
||||||
|
GameState forceTaskComplete(GameState state) {
|
||||||
|
final progress = state.progress.copyWith(
|
||||||
|
task: state.progress.task.copyWith(position: state.progress.task.max),
|
||||||
|
);
|
||||||
|
return state.copyWith(progress: progress);
|
||||||
|
}
|
||||||
|
|
||||||
|
GameState forceQuestComplete(GameState state) {
|
||||||
|
final progress = state.progress.copyWith(
|
||||||
|
task: state.progress.task.copyWith(position: state.progress.task.max),
|
||||||
|
quest: state.progress.quest.copyWith(position: state.progress.quest.max),
|
||||||
|
);
|
||||||
|
return state.copyWith(progress: progress);
|
||||||
|
}
|
||||||
|
|
||||||
|
GameState forcePlotComplete(GameState state) {
|
||||||
|
final progress = state.progress.copyWith(
|
||||||
|
task: state.progress.task.copyWith(position: state.progress.task.max),
|
||||||
|
plot: state.progress.plot.copyWith(position: state.progress.plot.max),
|
||||||
|
);
|
||||||
|
return state.copyWith(progress: progress);
|
||||||
|
}
|
||||||
|
|
||||||
|
GameState _applyReward(GameState state, pq_logic.RewardKind reward) {
|
||||||
|
final updated = rewards.applyReward(state, reward);
|
||||||
|
return _recalculateEncumbrance(updated);
|
||||||
|
}
|
||||||
|
|
||||||
|
GameState _levelUp(GameState state) {
|
||||||
|
final nextLevel = state.traits.level + 1;
|
||||||
|
final rng = state.rng;
|
||||||
|
final hpGain = state.stats.con ~/ 3 + 1 + rng.nextInt(4);
|
||||||
|
final mpGain = state.stats.intelligence ~/ 3 + 1 + rng.nextInt(4);
|
||||||
|
|
||||||
|
var nextState = state.copyWith(
|
||||||
|
traits: state.traits.copyWith(level: nextLevel),
|
||||||
|
stats: state.stats.copyWith(
|
||||||
|
hpMax: state.stats.hpMax + hpGain,
|
||||||
|
mpMax: state.stats.mpMax + mpGain,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Win two stats and a spell, matching the original leveling rules.
|
||||||
|
nextState = mutations.winStat(nextState);
|
||||||
|
nextState = mutations.winStat(nextState);
|
||||||
|
nextState = mutations.winSpell(nextState, nextState.stats.wis, nextLevel);
|
||||||
|
|
||||||
|
final expBar = ProgressBarState(
|
||||||
|
position: 0,
|
||||||
|
max: pq_logic.levelUpTime(nextLevel),
|
||||||
|
);
|
||||||
|
final progress = nextState.progress.copyWith(exp: expBar);
|
||||||
|
nextState = nextState.copyWith(progress: progress);
|
||||||
|
return _recalculateEncumbrance(nextState);
|
||||||
|
}
|
||||||
|
|
||||||
|
GameState _recalculateEncumbrance(GameState state) {
|
||||||
|
// items에는 Gold가 포함되지 않음 (inventory.gold 필드로 관리)
|
||||||
|
final encumValue = state.inventory.items.fold<int>(
|
||||||
|
0,
|
||||||
|
(sum, item) => sum + item.count,
|
||||||
|
);
|
||||||
|
final encumMax = 10 + state.stats.str;
|
||||||
|
final encumBar = state.progress.encumbrance.copyWith(
|
||||||
|
position: encumValue,
|
||||||
|
max: encumMax,
|
||||||
|
);
|
||||||
|
final progress = state.progress.copyWith(encumbrance: encumBar);
|
||||||
|
return state.copyWith(progress: progress);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 킬 태스크 완료 시 전리품 획득 (원본 Main.pas:625-630)
|
||||||
|
GameState _winLoot(GameState state) {
|
||||||
|
final taskCaption = state.progress.currentTask.caption;
|
||||||
|
|
||||||
|
// 몬스터 이름에서 전리품 아이템 생성
|
||||||
|
// 원본: Add(Inventory, LowerCase(Split(fTask.Caption,1) + ' ' +
|
||||||
|
// ProperCase(Split(fTask.Caption,3))), 1);
|
||||||
|
// 예: "Executing a Goblin..." -> "goblin ear" 등의 아이템
|
||||||
|
|
||||||
|
// 태스크 캡션에서 몬스터 이름 추출 ("Executing ..." 형태)
|
||||||
|
String monsterName = taskCaption;
|
||||||
|
if (monsterName.startsWith('Executing ')) {
|
||||||
|
monsterName = monsterName.substring('Executing '.length);
|
||||||
|
}
|
||||||
|
if (monsterName.endsWith('...')) {
|
||||||
|
monsterName = monsterName.substring(0, monsterName.length - 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 몬스터 부위 선택 (원본에서는 몬스터별로 다르지만, 간단히 랜덤 선택)
|
||||||
|
final parts = ['Skin', 'Tooth', 'Claw', 'Ear', 'Eye', 'Tail', 'Scale'];
|
||||||
|
final part = pq_logic.pick(parts, state.rng);
|
||||||
|
|
||||||
|
// 아이템 이름 생성 (예: "Goblin Ear")
|
||||||
|
final itemName = '${_extractBaseName(monsterName)} $part';
|
||||||
|
|
||||||
|
// 인벤토리에 추가
|
||||||
|
final items = [...state.inventory.items];
|
||||||
|
final existing = items.indexWhere((e) => e.name == itemName);
|
||||||
|
if (existing >= 0) {
|
||||||
|
items[existing] = items[existing].copyWith(
|
||||||
|
count: items[existing].count + 1,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
items.add(InventoryEntry(name: itemName, count: 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
return state.copyWith(
|
||||||
|
inventory: state.inventory.copyWith(items: items),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 몬스터 이름에서 기본 이름 추출 (형용사 제거)
|
||||||
|
String _extractBaseName(String name) {
|
||||||
|
// "a Goblin", "an Orc", "2 Goblins" 등에서 기본 이름 추출
|
||||||
|
final words = name.split(' ');
|
||||||
|
if (words.isEmpty) return name;
|
||||||
|
|
||||||
|
// 관사나 숫자 제거
|
||||||
|
var startIndex = 0;
|
||||||
|
if (words[0] == 'a' || words[0] == 'an' || words[0] == 'the') {
|
||||||
|
startIndex = 1;
|
||||||
|
} else if (int.tryParse(words[0]) != null) {
|
||||||
|
startIndex = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (startIndex >= words.length) return name;
|
||||||
|
|
||||||
|
// 마지막 단어가 몬스터 이름 (형용사들 건너뛰기)
|
||||||
|
final baseName = words.last;
|
||||||
|
// 첫 글자 대문자로
|
||||||
|
if (baseName.isEmpty) return name;
|
||||||
|
return baseName[0].toUpperCase() + baseName.substring(1).toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 인벤토리에서 Gold 수량 반환
|
||||||
|
int _getGold(GameState state) {
|
||||||
|
return state.inventory.gold;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 장비 가격 계산 (원본 Main.pas:612-616)
|
||||||
|
/// Result := 5 * Level^2 + 10 * Level + 20
|
||||||
|
int _equipPrice(int level) {
|
||||||
|
return 5 * level * level + 10 * level + 20;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 장비 구매 완료 처리 (원본 Main.pas:631-634)
|
||||||
|
GameState _completeBuying(GameState state) {
|
||||||
|
final level = state.traits.level;
|
||||||
|
final price = _equipPrice(level);
|
||||||
|
|
||||||
|
// Gold 차감 (inventory.gold 필드 사용)
|
||||||
|
final newGold = math.max(0, state.inventory.gold - price);
|
||||||
|
var nextState = state.copyWith(
|
||||||
|
inventory: state.inventory.copyWith(gold: newGold),
|
||||||
|
);
|
||||||
|
|
||||||
|
// 장비 획득 (WinEquip)
|
||||||
|
nextState = mutations.winEquip(
|
||||||
|
nextState,
|
||||||
|
level,
|
||||||
|
EquipmentSlot.values[nextState.rng.nextInt(EquipmentSlot.values.length)],
|
||||||
|
);
|
||||||
|
|
||||||
|
return nextState;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 판매 처리 결과
|
||||||
|
({GameState state, bool continuesSelling}) _processSell(GameState state) {
|
||||||
|
final taskType = state.progress.currentTask.type;
|
||||||
|
var items = [...state.inventory.items];
|
||||||
|
var goldAmount = state.inventory.gold;
|
||||||
|
|
||||||
|
// sell 태스크 완료 시 아이템 판매 (원본 Main.pas:636-643)
|
||||||
|
if (taskType == TaskType.sell) {
|
||||||
|
// 첫 번째 아이템 찾기 (items에는 Gold가 없음)
|
||||||
|
if (items.isNotEmpty) {
|
||||||
|
final item = items.first;
|
||||||
|
final level = state.traits.level;
|
||||||
|
|
||||||
|
// 가격 계산: 수량 * 레벨
|
||||||
|
var price = item.count * level;
|
||||||
|
|
||||||
|
// " of " 포함 시 보너스 (원본 639-640)
|
||||||
|
if (item.name.contains(' of ')) {
|
||||||
|
price = price *
|
||||||
|
(1 + pq_logic.randomLow(state.rng, 10)) *
|
||||||
|
(1 + pq_logic.randomLow(state.rng, level));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 아이템 삭제
|
||||||
|
items.removeAt(0);
|
||||||
|
|
||||||
|
// Gold 추가 (inventory.gold 필드 사용)
|
||||||
|
goldAmount += price;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 판매할 아이템이 남아있는지 확인
|
||||||
|
final hasItemsToSell = items.isNotEmpty;
|
||||||
|
|
||||||
|
if (hasItemsToSell) {
|
||||||
|
// 다음 아이템 판매 태스크 시작
|
||||||
|
final nextItem = items.first;
|
||||||
|
final taskResult = pq_logic.startTask(
|
||||||
|
state.progress,
|
||||||
|
'Selling ${pq_logic.indefinite(nextItem.name, nextItem.count)}',
|
||||||
|
1 * 1000,
|
||||||
|
);
|
||||||
|
final progress = taskResult.progress.copyWith(
|
||||||
|
currentTask: TaskInfo(
|
||||||
|
caption: taskResult.caption,
|
||||||
|
type: TaskType.sell,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
state: state.copyWith(
|
||||||
|
inventory: state.inventory.copyWith(gold: goldAmount, items: items),
|
||||||
|
progress: progress,
|
||||||
|
),
|
||||||
|
continuesSelling: true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 판매 완료 - 인벤토리 업데이트만 하고 다음 태스크로
|
||||||
|
return (
|
||||||
|
state: state.copyWith(
|
||||||
|
inventory: state.inventory.copyWith(gold: goldAmount, items: items),
|
||||||
|
),
|
||||||
|
continuesSelling: false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
26
lib/src/core/engine/reward_service.dart
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import 'package:askiineverdie/src/core/engine/game_mutations.dart';
|
||||||
|
import 'package:askiineverdie/src/core/model/equipment_slot.dart';
|
||||||
|
import 'package:askiineverdie/src/core/model/game_state.dart';
|
||||||
|
import 'package:askiineverdie/src/core/util/pq_logic.dart';
|
||||||
|
|
||||||
|
/// Applies quest/act rewards to the GameState using shared RNG.
|
||||||
|
class RewardService {
|
||||||
|
RewardService(this.mutations);
|
||||||
|
|
||||||
|
final GameMutations mutations;
|
||||||
|
|
||||||
|
GameState applyReward(GameState state, RewardKind reward) {
|
||||||
|
switch (reward) {
|
||||||
|
case RewardKind.spell:
|
||||||
|
return mutations.winSpell(state, state.stats.wis, state.traits.level);
|
||||||
|
case RewardKind.equip:
|
||||||
|
final slot = EquipmentSlot
|
||||||
|
.values[state.rng.nextInt(EquipmentSlot.values.length)];
|
||||||
|
return mutations.winEquip(state, state.traits.level, slot);
|
||||||
|
case RewardKind.stat:
|
||||||
|
return mutations.winStat(state);
|
||||||
|
case RewardKind.item:
|
||||||
|
return mutations.winItem(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1
lib/src/core/model/equipment_slot.dart
Normal file
@@ -0,0 +1 @@
|
|||||||
|
enum EquipmentSlot { weapon, shield, armor }
|
||||||
389
lib/src/core/model/game_state.dart
Normal file
@@ -0,0 +1,389 @@
|
|||||||
|
import 'dart:collection';
|
||||||
|
import 'package:askiineverdie/src/core/util/deterministic_random.dart';
|
||||||
|
|
||||||
|
/// Minimal skeletal state to mirror Progress Quest structures.
|
||||||
|
///
|
||||||
|
/// Logic will be ported faithfully from the Delphi source; this file only
|
||||||
|
/// defines containers and helpers for deterministic RNG.
|
||||||
|
class GameState {
|
||||||
|
GameState({
|
||||||
|
required DeterministicRandom rng,
|
||||||
|
Traits? traits,
|
||||||
|
Stats? stats,
|
||||||
|
Inventory? inventory,
|
||||||
|
Equipment? equipment,
|
||||||
|
SpellBook? spellBook,
|
||||||
|
ProgressState? progress,
|
||||||
|
QueueState? queue,
|
||||||
|
}) : rng = DeterministicRandom.clone(rng),
|
||||||
|
traits = traits ?? Traits.empty(),
|
||||||
|
stats = stats ?? Stats.empty(),
|
||||||
|
inventory = inventory ?? Inventory.empty(),
|
||||||
|
equipment = equipment ?? Equipment.empty(),
|
||||||
|
spellBook = spellBook ?? SpellBook.empty(),
|
||||||
|
progress = progress ?? ProgressState.empty(),
|
||||||
|
queue = queue ?? QueueState.empty();
|
||||||
|
|
||||||
|
factory GameState.withSeed({
|
||||||
|
required int seed,
|
||||||
|
Traits? traits,
|
||||||
|
Stats? stats,
|
||||||
|
Inventory? inventory,
|
||||||
|
Equipment? equipment,
|
||||||
|
SpellBook? spellBook,
|
||||||
|
ProgressState? progress,
|
||||||
|
QueueState? queue,
|
||||||
|
}) {
|
||||||
|
return GameState(
|
||||||
|
rng: DeterministicRandom(seed),
|
||||||
|
traits: traits,
|
||||||
|
stats: stats,
|
||||||
|
inventory: inventory,
|
||||||
|
equipment: equipment,
|
||||||
|
spellBook: spellBook,
|
||||||
|
progress: progress,
|
||||||
|
queue: queue,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final DeterministicRandom rng;
|
||||||
|
final Traits traits;
|
||||||
|
final Stats stats;
|
||||||
|
final Inventory inventory;
|
||||||
|
final Equipment equipment;
|
||||||
|
final SpellBook spellBook;
|
||||||
|
final ProgressState progress;
|
||||||
|
final QueueState queue;
|
||||||
|
|
||||||
|
GameState copyWith({
|
||||||
|
DeterministicRandom? rng,
|
||||||
|
Traits? traits,
|
||||||
|
Stats? stats,
|
||||||
|
Inventory? inventory,
|
||||||
|
Equipment? equipment,
|
||||||
|
SpellBook? spellBook,
|
||||||
|
ProgressState? progress,
|
||||||
|
QueueState? queue,
|
||||||
|
}) {
|
||||||
|
return GameState(
|
||||||
|
rng: rng ?? DeterministicRandom.clone(this.rng),
|
||||||
|
traits: traits ?? this.traits,
|
||||||
|
stats: stats ?? this.stats,
|
||||||
|
inventory: inventory ?? this.inventory,
|
||||||
|
equipment: equipment ?? this.equipment,
|
||||||
|
spellBook: spellBook ?? this.spellBook,
|
||||||
|
progress: progress ?? this.progress,
|
||||||
|
queue: queue ?? this.queue,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 태스크 타입 (원본 fTask.Caption 값들에 대응)
|
||||||
|
enum TaskType {
|
||||||
|
neutral, // heading 등 일반 이동
|
||||||
|
kill, // 몬스터 처치
|
||||||
|
load, // 로딩/초기화
|
||||||
|
plot, // 플롯 진행
|
||||||
|
market, // 시장으로 이동 중
|
||||||
|
sell, // 아이템 판매 중
|
||||||
|
buying, // 장비 구매 중
|
||||||
|
}
|
||||||
|
|
||||||
|
class TaskInfo {
|
||||||
|
const TaskInfo({required this.caption, required this.type});
|
||||||
|
|
||||||
|
final String caption;
|
||||||
|
final TaskType type;
|
||||||
|
|
||||||
|
factory TaskInfo.empty() =>
|
||||||
|
const TaskInfo(caption: '', type: TaskType.neutral);
|
||||||
|
|
||||||
|
TaskInfo copyWith({String? caption, TaskType? type}) {
|
||||||
|
return TaskInfo(caption: caption ?? this.caption, type: type ?? this.type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Traits {
|
||||||
|
const Traits({
|
||||||
|
required this.name,
|
||||||
|
required this.race,
|
||||||
|
required this.klass,
|
||||||
|
required this.level,
|
||||||
|
required this.motto,
|
||||||
|
required this.guild,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String name;
|
||||||
|
final String race;
|
||||||
|
final String klass;
|
||||||
|
final int level;
|
||||||
|
final String motto;
|
||||||
|
final String guild;
|
||||||
|
|
||||||
|
factory Traits.empty() => const Traits(
|
||||||
|
name: '',
|
||||||
|
race: '',
|
||||||
|
klass: '',
|
||||||
|
level: 1,
|
||||||
|
motto: '',
|
||||||
|
guild: '',
|
||||||
|
);
|
||||||
|
|
||||||
|
Traits copyWith({
|
||||||
|
String? name,
|
||||||
|
String? race,
|
||||||
|
String? klass,
|
||||||
|
int? level,
|
||||||
|
String? motto,
|
||||||
|
String? guild,
|
||||||
|
}) {
|
||||||
|
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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
|
||||||
|
final int str;
|
||||||
|
final int con;
|
||||||
|
final int dex;
|
||||||
|
final int intelligence;
|
||||||
|
final int wis;
|
||||||
|
final int cha;
|
||||||
|
final int hpMax;
|
||||||
|
final int 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,
|
||||||
|
}) {
|
||||||
|
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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
factory Inventory.empty() => const Inventory(gold: 0, items: []);
|
||||||
|
|
||||||
|
Inventory copyWith({int? gold, List<InventoryEntry>? items}) {
|
||||||
|
return Inventory(gold: gold ?? this.gold, items: items ?? this.items);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Equipment {
|
||||||
|
const Equipment({
|
||||||
|
required this.weapon,
|
||||||
|
required this.shield,
|
||||||
|
required this.armor,
|
||||||
|
required this.bestIndex,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String weapon;
|
||||||
|
final String shield;
|
||||||
|
final String armor;
|
||||||
|
|
||||||
|
/// Tracks best slot index (mirror of Equips.Tag in original code; 0=weapon,1=shield,2=armor).
|
||||||
|
final int bestIndex;
|
||||||
|
|
||||||
|
factory Equipment.empty() => const Equipment(
|
||||||
|
weapon: 'Sharp Stick',
|
||||||
|
shield: '',
|
||||||
|
armor: '',
|
||||||
|
bestIndex: 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
Equipment copyWith({
|
||||||
|
String? weapon,
|
||||||
|
String? shield,
|
||||||
|
String? armor,
|
||||||
|
int? bestIndex,
|
||||||
|
}) {
|
||||||
|
return Equipment(
|
||||||
|
weapon: weapon ?? this.weapon,
|
||||||
|
shield: shield ?? this.shield,
|
||||||
|
armor: armor ?? this.armor,
|
||||||
|
bestIndex: bestIndex ?? this.bestIndex,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SpellEntry {
|
||||||
|
const SpellEntry({required this.name, required this.rank});
|
||||||
|
|
||||||
|
final String name;
|
||||||
|
final String rank; // e.g., Roman numerals
|
||||||
|
|
||||||
|
SpellEntry copyWith({String? name, String? rank}) {
|
||||||
|
return SpellEntry(name: name ?? this.name, rank: rank ?? this.rank);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SpellBook {
|
||||||
|
const SpellBook({required this.spells});
|
||||||
|
|
||||||
|
final List<SpellEntry> spells;
|
||||||
|
|
||||||
|
factory SpellBook.empty() => const SpellBook(spells: []);
|
||||||
|
|
||||||
|
SpellBook copyWith({List<SpellEntry>? spells}) {
|
||||||
|
return SpellBook(spells: spells ?? this.spells);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
|
||||||
|
final ProgressBarState task;
|
||||||
|
final ProgressBarState quest;
|
||||||
|
final ProgressBarState plot;
|
||||||
|
final ProgressBarState exp;
|
||||||
|
final ProgressBarState encumbrance;
|
||||||
|
final TaskInfo currentTask;
|
||||||
|
final int plotStageCount;
|
||||||
|
final int questCount;
|
||||||
|
|
||||||
|
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,
|
||||||
|
);
|
||||||
|
|
||||||
|
ProgressState copyWith({
|
||||||
|
ProgressBarState? task,
|
||||||
|
ProgressBarState? quest,
|
||||||
|
ProgressBarState? plot,
|
||||||
|
ProgressBarState? exp,
|
||||||
|
ProgressBarState? encumbrance,
|
||||||
|
TaskInfo? currentTask,
|
||||||
|
int? plotStageCount,
|
||||||
|
int? questCount,
|
||||||
|
}) {
|
||||||
|
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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
31
lib/src/core/model/pq_config.dart
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import 'package:askiineverdie/data/pq_config_data.dart';
|
||||||
|
|
||||||
|
/// Typed accessors for Progress Quest static data extracted from Config.dfm.
|
||||||
|
class PqConfig {
|
||||||
|
const PqConfig();
|
||||||
|
|
||||||
|
List<String> get spells => _copy('Spells');
|
||||||
|
List<String> get offenseAttrib => _copy('OffenseAttrib');
|
||||||
|
List<String> get defenseAttrib => _copy('DefenseAttrib');
|
||||||
|
List<String> get offenseBad => _copy('OffenseBad');
|
||||||
|
List<String> get defenseBad => _copy('DefenseBad');
|
||||||
|
List<String> get shields => _copy('Shields');
|
||||||
|
List<String> get armors => _copy('Armors');
|
||||||
|
List<String> get weapons => _copy('Weapons');
|
||||||
|
List<String> get specials => _copy('Specials');
|
||||||
|
List<String> get itemAttrib => _copy('ItemAttrib');
|
||||||
|
List<String> get itemOfs => _copy('ItemOfs');
|
||||||
|
List<String> get boringItems => _copy('BoringItems');
|
||||||
|
List<String> get monsters => _copy('Monsters');
|
||||||
|
List<String> get monMods => _copy('MonMods');
|
||||||
|
List<String> get races => _copy('Races');
|
||||||
|
List<String> get klasses => _copy('Klasses');
|
||||||
|
List<String> get titles => _copy('Titles');
|
||||||
|
List<String> get impressiveTitles => _copy('ImpressiveTitles');
|
||||||
|
|
||||||
|
List<String> _copy(String key) {
|
||||||
|
final values = pqConfigData[key];
|
||||||
|
if (values == null) return const [];
|
||||||
|
return List<String>.from(values);
|
||||||
|
}
|
||||||
|
}
|
||||||
237
lib/src/core/model/save_data.dart
Normal file
@@ -0,0 +1,237 @@
|
|||||||
|
import 'dart:collection';
|
||||||
|
|
||||||
|
import 'package:askiineverdie/src/core/util/deterministic_random.dart';
|
||||||
|
import 'package:askiineverdie/src/core/model/game_state.dart';
|
||||||
|
|
||||||
|
const int kSaveVersion = 2;
|
||||||
|
|
||||||
|
class GameSave {
|
||||||
|
GameSave({
|
||||||
|
required this.version,
|
||||||
|
required this.rngState,
|
||||||
|
required this.traits,
|
||||||
|
required this.stats,
|
||||||
|
required this.inventory,
|
||||||
|
required this.equipment,
|
||||||
|
required this.spellBook,
|
||||||
|
required this.progress,
|
||||||
|
required this.queue,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory GameSave.fromState(GameState state) {
|
||||||
|
return GameSave(
|
||||||
|
version: kSaveVersion,
|
||||||
|
rngState: state.rng.state,
|
||||||
|
traits: state.traits,
|
||||||
|
stats: state.stats,
|
||||||
|
inventory: state.inventory,
|
||||||
|
equipment: state.equipment,
|
||||||
|
spellBook: state.spellBook,
|
||||||
|
progress: state.progress,
|
||||||
|
queue: state.queue,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final int version;
|
||||||
|
final int rngState;
|
||||||
|
final Traits traits;
|
||||||
|
final Stats stats;
|
||||||
|
final Inventory inventory;
|
||||||
|
final Equipment equipment;
|
||||||
|
final SpellBook spellBook;
|
||||||
|
final ProgressState progress;
|
||||||
|
final QueueState queue;
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {
|
||||||
|
'version': version,
|
||||||
|
'rng': rngState,
|
||||||
|
'traits': {
|
||||||
|
'name': traits.name,
|
||||||
|
'race': traits.race,
|
||||||
|
'klass': traits.klass,
|
||||||
|
'level': traits.level,
|
||||||
|
'motto': traits.motto,
|
||||||
|
'guild': traits.guild,
|
||||||
|
},
|
||||||
|
'stats': {
|
||||||
|
'str': stats.str,
|
||||||
|
'con': stats.con,
|
||||||
|
'dex': stats.dex,
|
||||||
|
'int': stats.intelligence,
|
||||||
|
'wis': stats.wis,
|
||||||
|
'cha': stats.cha,
|
||||||
|
'hpMax': stats.hpMax,
|
||||||
|
'mpMax': stats.mpMax,
|
||||||
|
},
|
||||||
|
'inventory': {
|
||||||
|
'gold': inventory.gold,
|
||||||
|
'items': inventory.items
|
||||||
|
.map((e) => {'name': e.name, 'count': e.count})
|
||||||
|
.toList(),
|
||||||
|
},
|
||||||
|
'equipment': {
|
||||||
|
'weapon': equipment.weapon,
|
||||||
|
'shield': equipment.shield,
|
||||||
|
'armor': equipment.armor,
|
||||||
|
'bestIndex': equipment.bestIndex,
|
||||||
|
},
|
||||||
|
'spells': spellBook.spells
|
||||||
|
.map((e) => {'name': e.name, 'rank': e.rank})
|
||||||
|
.toList(),
|
||||||
|
'progress': {
|
||||||
|
'task': _barToJson(progress.task),
|
||||||
|
'quest': _barToJson(progress.quest),
|
||||||
|
'plot': _barToJson(progress.plot),
|
||||||
|
'exp': _barToJson(progress.exp),
|
||||||
|
'encumbrance': _barToJson(progress.encumbrance),
|
||||||
|
'taskInfo': {
|
||||||
|
'caption': progress.currentTask.caption,
|
||||||
|
'type': progress.currentTask.type.name,
|
||||||
|
},
|
||||||
|
'plotStages': progress.plotStageCount,
|
||||||
|
'questCount': progress.questCount,
|
||||||
|
},
|
||||||
|
'queue': queue.entries
|
||||||
|
.map(
|
||||||
|
(e) => {
|
||||||
|
'kind': e.kind.name,
|
||||||
|
'duration': e.durationMillis,
|
||||||
|
'caption': e.caption,
|
||||||
|
'taskType': e.taskType.name,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static GameSave fromJson(Map<String, dynamic> json) {
|
||||||
|
final traitsJson = json['traits'] as Map<String, dynamic>;
|
||||||
|
final statsJson = json['stats'] as Map<String, dynamic>;
|
||||||
|
final inventoryJson = json['inventory'] as Map<String, dynamic>;
|
||||||
|
final equipmentJson = json['equipment'] as Map<String, dynamic>;
|
||||||
|
final progressJson = json['progress'] as Map<String, dynamic>;
|
||||||
|
final queueJson = (json['queue'] as List<dynamic>? ?? []).cast<dynamic>();
|
||||||
|
final spellsJson = (json['spells'] as List<dynamic>? ?? []).cast<dynamic>();
|
||||||
|
|
||||||
|
return GameSave(
|
||||||
|
version: json['version'] as int? ?? kSaveVersion,
|
||||||
|
rngState: json['rng'] as int? ?? 0,
|
||||||
|
traits: Traits(
|
||||||
|
name: traitsJson['name'] as String? ?? '',
|
||||||
|
race: traitsJson['race'] as String? ?? '',
|
||||||
|
klass: traitsJson['klass'] as String? ?? '',
|
||||||
|
level: traitsJson['level'] as int? ?? 1,
|
||||||
|
motto: traitsJson['motto'] as String? ?? '',
|
||||||
|
guild: traitsJson['guild'] as String? ?? '',
|
||||||
|
),
|
||||||
|
stats: Stats(
|
||||||
|
str: statsJson['str'] as int? ?? 0,
|
||||||
|
con: statsJson['con'] as int? ?? 0,
|
||||||
|
dex: statsJson['dex'] as int? ?? 0,
|
||||||
|
intelligence: statsJson['int'] as int? ?? 0,
|
||||||
|
wis: statsJson['wis'] as int? ?? 0,
|
||||||
|
cha: statsJson['cha'] as int? ?? 0,
|
||||||
|
hpMax: statsJson['hpMax'] as int? ?? 0,
|
||||||
|
mpMax: statsJson['mpMax'] as int? ?? 0,
|
||||||
|
),
|
||||||
|
inventory: Inventory(
|
||||||
|
gold: inventoryJson['gold'] as int? ?? 0,
|
||||||
|
items: (inventoryJson['items'] as List<dynamic>? ?? [])
|
||||||
|
.map(
|
||||||
|
(e) => InventoryEntry(
|
||||||
|
name: (e as Map<String, dynamic>)['name'] as String? ?? '',
|
||||||
|
count: (e)['count'] as int? ?? 0,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
equipment: Equipment(
|
||||||
|
weapon: equipmentJson['weapon'] as String? ?? 'Sharp Stick',
|
||||||
|
shield: equipmentJson['shield'] as String? ?? '',
|
||||||
|
armor: equipmentJson['armor'] as String? ?? '',
|
||||||
|
bestIndex: equipmentJson['bestIndex'] as int? ?? 0,
|
||||||
|
),
|
||||||
|
spellBook: SpellBook(
|
||||||
|
spells: spellsJson
|
||||||
|
.map(
|
||||||
|
(e) => SpellEntry(
|
||||||
|
name: (e as Map<String, dynamic>)['name'] as String? ?? '',
|
||||||
|
rank: (e)['rank'] as String? ?? 'I',
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
progress: ProgressState(
|
||||||
|
task: _barFromJson(progressJson['task'] as Map<String, dynamic>? ?? {}),
|
||||||
|
quest: _barFromJson(
|
||||||
|
progressJson['quest'] as Map<String, dynamic>? ?? {},
|
||||||
|
),
|
||||||
|
plot: _barFromJson(progressJson['plot'] as Map<String, dynamic>? ?? {}),
|
||||||
|
exp: _barFromJson(progressJson['exp'] as Map<String, dynamic>? ?? {}),
|
||||||
|
encumbrance: _barFromJson(
|
||||||
|
progressJson['encumbrance'] as Map<String, dynamic>? ?? {},
|
||||||
|
),
|
||||||
|
currentTask: _taskInfoFromJson(
|
||||||
|
progressJson['taskInfo'] as Map<String, dynamic>? ??
|
||||||
|
<String, dynamic>{},
|
||||||
|
),
|
||||||
|
plotStageCount: progressJson['plotStages'] as int? ?? 1,
|
||||||
|
questCount: progressJson['questCount'] as int? ?? 0,
|
||||||
|
),
|
||||||
|
queue: QueueState(
|
||||||
|
entries: Queue<QueueEntry>.from(
|
||||||
|
queueJson.map((e) {
|
||||||
|
final m = e as Map<String, dynamic>;
|
||||||
|
final kind = QueueKind.values.firstWhere(
|
||||||
|
(k) => k.name == m['kind'],
|
||||||
|
orElse: () => QueueKind.task,
|
||||||
|
);
|
||||||
|
final taskType = TaskType.values.firstWhere(
|
||||||
|
(t) => t.name == m['taskType'],
|
||||||
|
orElse: () => TaskType.neutral,
|
||||||
|
);
|
||||||
|
return QueueEntry(
|
||||||
|
kind: kind,
|
||||||
|
durationMillis: m['duration'] as int? ?? 0,
|
||||||
|
caption: m['caption'] as String? ?? '',
|
||||||
|
taskType: taskType,
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
GameState toState() {
|
||||||
|
return GameState(
|
||||||
|
rng: DeterministicRandom.fromState(rngState),
|
||||||
|
traits: traits,
|
||||||
|
stats: stats,
|
||||||
|
inventory: inventory,
|
||||||
|
equipment: equipment,
|
||||||
|
spellBook: spellBook,
|
||||||
|
progress: progress,
|
||||||
|
queue: queue,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> _barToJson(ProgressBarState bar) => {
|
||||||
|
'pos': bar.position,
|
||||||
|
'max': bar.max,
|
||||||
|
};
|
||||||
|
|
||||||
|
ProgressBarState _barFromJson(Map<String, dynamic> json) => ProgressBarState(
|
||||||
|
position: json['pos'] as int? ?? 0,
|
||||||
|
max: json['max'] as int? ?? 1,
|
||||||
|
);
|
||||||
|
|
||||||
|
TaskInfo _taskInfoFromJson(Map<String, dynamic> json) {
|
||||||
|
final typeName = json['type'] as String?;
|
||||||
|
final type = TaskType.values.firstWhere(
|
||||||
|
(t) => t.name == typeName,
|
||||||
|
orElse: () => TaskType.neutral,
|
||||||
|
);
|
||||||
|
return TaskInfo(caption: json['caption'] as String? ?? '', type: type);
|
||||||
|
}
|
||||||
33
lib/src/core/storage/save_manager.dart
Normal 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();
|
||||||
|
}
|
||||||
64
lib/src/core/storage/save_repository.dart
Normal 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 [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
84
lib/src/core/storage/save_service.dart
Normal 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', '');
|
||||||
|
}
|
||||||
38
lib/src/core/util/deterministic_random.dart
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
/// Simple deterministic RNG (xorshift32) with serializable state.
|
||||||
|
class DeterministicRandom {
|
||||||
|
DeterministicRandom(int seed) : _state = seed & _mask;
|
||||||
|
|
||||||
|
DeterministicRandom.clone(DeterministicRandom other)
|
||||||
|
: _state = other._state & _mask;
|
||||||
|
|
||||||
|
DeterministicRandom.fromState(int state) : _state = state & _mask;
|
||||||
|
|
||||||
|
static const int _mask = 0xFFFFFFFF;
|
||||||
|
|
||||||
|
int _state;
|
||||||
|
|
||||||
|
int get state => _state;
|
||||||
|
|
||||||
|
/// Returns next unsigned 32-bit value.
|
||||||
|
int nextUint32() {
|
||||||
|
var x = _state;
|
||||||
|
x ^= (x << 13) & _mask;
|
||||||
|
x ^= (x >> 17) & _mask;
|
||||||
|
x ^= (x << 5) & _mask;
|
||||||
|
_state = x & _mask;
|
||||||
|
return _state;
|
||||||
|
}
|
||||||
|
|
||||||
|
int nextInt(int maxExclusive) {
|
||||||
|
if (maxExclusive <= 0) {
|
||||||
|
throw ArgumentError.value(maxExclusive, 'maxExclusive', 'must be > 0');
|
||||||
|
}
|
||||||
|
return nextUint32() % maxExclusive;
|
||||||
|
}
|
||||||
|
|
||||||
|
double nextDouble() {
|
||||||
|
// 2^32 as double.
|
||||||
|
const double denom = 4294967296.0;
|
||||||
|
return nextUint32() / denom;
|
||||||
|
}
|
||||||
|
}
|
||||||
820
lib/src/core/util/pq_logic.dart
Normal file
@@ -0,0 +1,820 @@
|
|||||||
|
import 'dart:collection';
|
||||||
|
import 'dart:math' as math;
|
||||||
|
|
||||||
|
import 'package:askiineverdie/src/core/util/deterministic_random.dart';
|
||||||
|
import 'package:askiineverdie/src/core/model/pq_config.dart';
|
||||||
|
import 'package:askiineverdie/src/core/util/roman.dart';
|
||||||
|
import 'package:askiineverdie/src/core/model/equipment_slot.dart';
|
||||||
|
import 'package:askiineverdie/src/core/model/game_state.dart';
|
||||||
|
|
||||||
|
// Mirrors core utility functions from the original Delphi sources (Main.pas / NewGuy.pas).
|
||||||
|
|
||||||
|
int levelUpTimeSeconds(int level) {
|
||||||
|
// ~20 minutes for level 1, then exponential growth (same as LevelUpTime in Main.pas).
|
||||||
|
final seconds = (20.0 + math.pow(1.15, level)) * 60.0;
|
||||||
|
return seconds.round();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 초 단위 시간을 사람이 읽기 쉬운 형태로 변환 (원본 Main.pas:1265-1271)
|
||||||
|
String roughTime(int seconds) {
|
||||||
|
if (seconds < 120) {
|
||||||
|
return '$seconds seconds';
|
||||||
|
} else if (seconds < 60 * 120) {
|
||||||
|
return '${seconds ~/ 60} minutes';
|
||||||
|
} else if (seconds < 60 * 60 * 48) {
|
||||||
|
return '${seconds ~/ 3600} hours';
|
||||||
|
} else {
|
||||||
|
return '${seconds ~/ (3600 * 24)} days';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String pluralize(String s) {
|
||||||
|
if (_ends(s, 'y')) return '${s.substring(0, s.length - 1)}ies';
|
||||||
|
if (_ends(s, 'us')) return '${s.substring(0, s.length - 2)}i';
|
||||||
|
if (_ends(s, 'ch') || _ends(s, 'x') || _ends(s, 's')) return '${s}es';
|
||||||
|
if (_ends(s, 'f')) return '${s.substring(0, s.length - 1)}ves';
|
||||||
|
if (_ends(s, 'man') || _ends(s, 'Man')) {
|
||||||
|
return '${s.substring(0, s.length - 2)}en';
|
||||||
|
}
|
||||||
|
return '${s}s';
|
||||||
|
}
|
||||||
|
|
||||||
|
String indefinite(String s, int qty) {
|
||||||
|
if (qty == 1) {
|
||||||
|
const vowels = 'AEIOUÜaeiouü';
|
||||||
|
final first = s.isNotEmpty ? s[0] : 'a';
|
||||||
|
final article = vowels.contains(first) ? 'an' : 'a';
|
||||||
|
return '$article $s';
|
||||||
|
}
|
||||||
|
return '$qty ${pluralize(s)}';
|
||||||
|
}
|
||||||
|
|
||||||
|
String definite(String s, int qty) {
|
||||||
|
if (qty > 1) {
|
||||||
|
s = pluralize(s);
|
||||||
|
}
|
||||||
|
return 'the $s';
|
||||||
|
}
|
||||||
|
|
||||||
|
String generateName(DeterministicRandom rng) {
|
||||||
|
const kParts = [
|
||||||
|
'br|cr|dr|fr|gr|j|kr|l|m|n|pr||||r|sh|tr|v|wh|x|y|z',
|
||||||
|
'a|a|e|e|i|i|o|o|u|u|ae|ie|oo|ou',
|
||||||
|
'b|ck|d|g|k|m|n|p|t|v|x|z',
|
||||||
|
];
|
||||||
|
|
||||||
|
var result = '';
|
||||||
|
for (var i = 0; i <= 5; i++) {
|
||||||
|
result += _pick(kParts[i % 3], rng);
|
||||||
|
}
|
||||||
|
if (result.isEmpty) return result;
|
||||||
|
return '${result[0].toUpperCase()}${result.substring(1)}';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Random helpers
|
||||||
|
int randomLow(DeterministicRandom rng, int below) {
|
||||||
|
return math.min(rng.nextInt(below), rng.nextInt(below));
|
||||||
|
}
|
||||||
|
|
||||||
|
String pick(List<String> values, DeterministicRandom rng) {
|
||||||
|
if (values.isEmpty) return '';
|
||||||
|
return values[rng.nextInt(values.length)];
|
||||||
|
}
|
||||||
|
|
||||||
|
String pickLow(List<String> values, DeterministicRandom rng) {
|
||||||
|
if (values.isEmpty) return '';
|
||||||
|
return values[randomLow(rng, values.length)];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Item name generators (match Main.pas)
|
||||||
|
String boringItem(PqConfig config, DeterministicRandom rng) {
|
||||||
|
return pick(config.boringItems, rng);
|
||||||
|
}
|
||||||
|
|
||||||
|
String interestingItem(PqConfig config, DeterministicRandom rng) {
|
||||||
|
final attr = pick(config.itemAttrib, rng);
|
||||||
|
final special = pick(config.specials, rng);
|
||||||
|
return '$attr $special';
|
||||||
|
}
|
||||||
|
|
||||||
|
String specialItem(PqConfig config, DeterministicRandom rng) {
|
||||||
|
return '${interestingItem(config, rng)} of ${pick(config.itemOfs, rng)}';
|
||||||
|
}
|
||||||
|
|
||||||
|
String pickWeapon(PqConfig config, DeterministicRandom rng, int level) {
|
||||||
|
return _lPick(config.weapons, rng, level);
|
||||||
|
}
|
||||||
|
|
||||||
|
String pickShield(PqConfig config, DeterministicRandom rng, int level) {
|
||||||
|
return _lPick(config.shields, rng, level);
|
||||||
|
}
|
||||||
|
|
||||||
|
String pickArmor(PqConfig config, DeterministicRandom rng, int level) {
|
||||||
|
return _lPick(config.armors, rng, level);
|
||||||
|
}
|
||||||
|
|
||||||
|
String pickSpell(PqConfig config, DeterministicRandom rng, int goalLevel) {
|
||||||
|
return _lPick(config.spells, rng, goalLevel);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 원본 Main.pas:776-789 LPick: 6회 시도하여 목표 레벨에 가장 가까운 아이템 선택
|
||||||
|
String _lPick(List<String> items, DeterministicRandom rng, int goal) {
|
||||||
|
if (items.isEmpty) return '';
|
||||||
|
var result = pick(items, rng);
|
||||||
|
var bestLevel = _parseLevel(result);
|
||||||
|
|
||||||
|
for (var i = 0; i < 5; i++) {
|
||||||
|
final candidate = pick(items, rng);
|
||||||
|
final candLevel = _parseLevel(candidate);
|
||||||
|
if ((goal - candLevel).abs() < (goal - bestLevel).abs()) {
|
||||||
|
result = candidate;
|
||||||
|
bestLevel = candLevel;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
int _parseLevel(String entry) {
|
||||||
|
final parts = entry.split('|');
|
||||||
|
if (parts.length < 2) return 0;
|
||||||
|
return int.tryParse(parts[1].replaceAll('+', '')) ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
String addModifier(
|
||||||
|
DeterministicRandom rng,
|
||||||
|
String baseName,
|
||||||
|
List<String> modifiers,
|
||||||
|
int plus,
|
||||||
|
) {
|
||||||
|
var name = baseName;
|
||||||
|
var remaining = plus;
|
||||||
|
var count = 0;
|
||||||
|
|
||||||
|
while (count < 2 && remaining != 0) {
|
||||||
|
final modifier = pick(modifiers, rng);
|
||||||
|
final parts = modifier.split('|');
|
||||||
|
if (parts.isEmpty) break;
|
||||||
|
final label = parts[0];
|
||||||
|
final qual = parts.length > 1 ? int.tryParse(parts[1]) ?? 0 : 0;
|
||||||
|
if (name.contains(label)) break; // avoid repeats
|
||||||
|
if (remaining.abs() < qual.abs()) break;
|
||||||
|
name = '$label $name';
|
||||||
|
remaining -= qual;
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (remaining != 0) {
|
||||||
|
name = '${remaining > 0 ? '+' : ''}$remaining $name';
|
||||||
|
}
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Character/stat growth
|
||||||
|
int levelUpTime(int level) => levelUpTimeSeconds(level);
|
||||||
|
|
||||||
|
String winSpell(
|
||||||
|
PqConfig config,
|
||||||
|
DeterministicRandom rng,
|
||||||
|
int wisdom,
|
||||||
|
int level,
|
||||||
|
) {
|
||||||
|
// 원본 Main.pas:770-774: RandomLow로 인덱스 선택 (리스트 앞쪽 선호)
|
||||||
|
final maxIndex = math.min(wisdom + level, config.spells.length);
|
||||||
|
if (maxIndex <= 0) return '';
|
||||||
|
final index = randomLow(rng, maxIndex);
|
||||||
|
final entry = config.spells[index];
|
||||||
|
final parts = entry.split('|');
|
||||||
|
final name = parts[0];
|
||||||
|
final currentRank = romanToInt(parts.length > 1 ? parts[1] : 'I');
|
||||||
|
final nextRank = math.max(1, currentRank + 1);
|
||||||
|
return '$name|${intToRoman(nextRank)}';
|
||||||
|
}
|
||||||
|
|
||||||
|
String winItem(PqConfig config, DeterministicRandom rng, int inventoryCount) {
|
||||||
|
// If inventory is already very large, signal caller to duplicate an existing item.
|
||||||
|
final threshold = math.max(250, rng.nextInt(999));
|
||||||
|
if (inventoryCount > threshold) return '';
|
||||||
|
return specialItem(config, rng);
|
||||||
|
}
|
||||||
|
|
||||||
|
int rollStat(DeterministicRandom rng) {
|
||||||
|
// 3d6 roll.
|
||||||
|
return 3 + rng.nextInt(6) + rng.nextInt(6) + rng.nextInt(6);
|
||||||
|
}
|
||||||
|
|
||||||
|
int random64Below(DeterministicRandom rng, int below) {
|
||||||
|
if (below <= 0) return 0;
|
||||||
|
final hi = rng.nextUint32();
|
||||||
|
final lo = rng.nextUint32();
|
||||||
|
final combined = (hi << 32) | lo;
|
||||||
|
return (combined % below).toInt();
|
||||||
|
}
|
||||||
|
|
||||||
|
String winEquip(
|
||||||
|
PqConfig config,
|
||||||
|
DeterministicRandom rng,
|
||||||
|
int level,
|
||||||
|
EquipmentSlot slot,
|
||||||
|
) {
|
||||||
|
// Decide item set and modifiers based on slot.
|
||||||
|
final bool isWeapon = slot == EquipmentSlot.weapon;
|
||||||
|
final items = switch (slot) {
|
||||||
|
EquipmentSlot.weapon => config.weapons,
|
||||||
|
EquipmentSlot.shield => config.shields,
|
||||||
|
EquipmentSlot.armor => config.armors,
|
||||||
|
};
|
||||||
|
final better = isWeapon ? config.offenseAttrib : config.defenseAttrib;
|
||||||
|
final worse = isWeapon ? config.offenseBad : config.defenseBad;
|
||||||
|
|
||||||
|
final base = _lPick(items, rng, level);
|
||||||
|
final parts = base.split('|');
|
||||||
|
final baseName = parts[0];
|
||||||
|
final qual = parts.length > 1
|
||||||
|
? int.tryParse(parts[1].replaceAll('+', '')) ?? 0
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
final plus = level - qual;
|
||||||
|
final modifiers = plus >= 0 ? better : worse;
|
||||||
|
return addModifier(rng, baseName, modifiers, plus);
|
||||||
|
}
|
||||||
|
|
||||||
|
int winStatIndex(DeterministicRandom rng, List<int> statValues) {
|
||||||
|
// 원본 Main.pas:870-883: 50% 확률로 완전 랜덤, 50% 확률로 제곱 가중치
|
||||||
|
if (rng.nextInt(2) == 0) {
|
||||||
|
// Odds(1,2): 완전 랜덤 선택
|
||||||
|
return rng.nextInt(statValues.length);
|
||||||
|
}
|
||||||
|
// 제곱 가중치로 높은 스탯 선호
|
||||||
|
final total = statValues.fold<int>(0, (sum, v) => sum + v * v);
|
||||||
|
if (total == 0) return rng.nextInt(statValues.length);
|
||||||
|
var pickValue = random64Below(rng, total);
|
||||||
|
for (var i = 0; i < statValues.length; i++) {
|
||||||
|
pickValue -= statValues[i] * statValues[i];
|
||||||
|
if (pickValue < 0) return i;
|
||||||
|
}
|
||||||
|
return statValues.length - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
Stats winStat(Stats stats, DeterministicRandom rng) {
|
||||||
|
final values = <int>[
|
||||||
|
stats.str,
|
||||||
|
stats.con,
|
||||||
|
stats.dex,
|
||||||
|
stats.intelligence,
|
||||||
|
stats.wis,
|
||||||
|
stats.cha,
|
||||||
|
stats.hpMax,
|
||||||
|
stats.mpMax,
|
||||||
|
];
|
||||||
|
final idx = winStatIndex(rng, values);
|
||||||
|
switch (idx) {
|
||||||
|
case 0:
|
||||||
|
return stats.copyWith(str: stats.str + 1);
|
||||||
|
case 1:
|
||||||
|
return stats.copyWith(con: stats.con + 1);
|
||||||
|
case 2:
|
||||||
|
return stats.copyWith(dex: stats.dex + 1);
|
||||||
|
case 3:
|
||||||
|
return stats.copyWith(intelligence: stats.intelligence + 1);
|
||||||
|
case 4:
|
||||||
|
return stats.copyWith(wis: stats.wis + 1);
|
||||||
|
case 5:
|
||||||
|
return stats.copyWith(cha: stats.cha + 1);
|
||||||
|
case 6:
|
||||||
|
return stats.copyWith(hpMax: stats.hpMax + 1);
|
||||||
|
case 7:
|
||||||
|
return stats.copyWith(mpMax: stats.mpMax + 1);
|
||||||
|
default:
|
||||||
|
return stats;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String monsterTask(
|
||||||
|
PqConfig config,
|
||||||
|
DeterministicRandom rng,
|
||||||
|
int level,
|
||||||
|
String? questMonster, // optional monster name from quest
|
||||||
|
int? questLevel,
|
||||||
|
) {
|
||||||
|
var targetLevel = level;
|
||||||
|
|
||||||
|
for (var i = level; i > 0; i--) {
|
||||||
|
if (rng.nextInt(5) < 2) {
|
||||||
|
targetLevel += rng.nextInt(2) * 2 - 1; // RandSign
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (targetLevel < 1) targetLevel = 1;
|
||||||
|
|
||||||
|
String monster;
|
||||||
|
int monsterLevel;
|
||||||
|
bool definite = false;
|
||||||
|
|
||||||
|
// 원본 Main.pas:537-547: 가끔 NPC를 몬스터로 사용
|
||||||
|
if (rng.nextInt(25) == 0) {
|
||||||
|
final race = pick(config.races, rng).split('|').first;
|
||||||
|
if (rng.nextInt(2) == 0) {
|
||||||
|
// 'passing Race Class' 형태
|
||||||
|
final klass = pick(config.klasses, rng).split('|').first;
|
||||||
|
monster = 'passing $race $klass';
|
||||||
|
} else {
|
||||||
|
// 'Title Name the Race' 형태 (원본은 PickLow(Titles) 사용)
|
||||||
|
final title = pickLow(config.titles, rng);
|
||||||
|
monster = '$title ${generateName(rng)} the $race';
|
||||||
|
definite = true;
|
||||||
|
}
|
||||||
|
monsterLevel = targetLevel;
|
||||||
|
monster = '$monster|$monsterLevel|*';
|
||||||
|
} else if (questMonster != null && rng.nextInt(4) == 0) {
|
||||||
|
// Use quest monster.
|
||||||
|
monster = questMonster;
|
||||||
|
monsterLevel = questLevel ?? targetLevel;
|
||||||
|
} else {
|
||||||
|
// Pick closest level among random samples.
|
||||||
|
monster = pick(config.monsters, rng);
|
||||||
|
monsterLevel = _monsterLevel(monster);
|
||||||
|
for (var i = 0; i < 5; i++) {
|
||||||
|
final candidate = pick(config.monsters, rng);
|
||||||
|
final candLevel = _monsterLevel(candidate);
|
||||||
|
if ((targetLevel - candLevel).abs() <
|
||||||
|
(targetLevel - monsterLevel).abs()) {
|
||||||
|
monster = candidate;
|
||||||
|
monsterLevel = candLevel;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adjust quantity and adjectives based on level delta.
|
||||||
|
var qty = 1;
|
||||||
|
final levelDiff = targetLevel - monsterLevel;
|
||||||
|
var name = monster.split('|').first;
|
||||||
|
|
||||||
|
if (levelDiff > 10) {
|
||||||
|
qty =
|
||||||
|
(targetLevel + rng.nextInt(monsterLevel == 0 ? 1 : monsterLevel)) ~/
|
||||||
|
(monsterLevel == 0 ? 1 : monsterLevel);
|
||||||
|
if (qty < 1) qty = 1;
|
||||||
|
targetLevel ~/= qty;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (levelDiff <= -10) {
|
||||||
|
name = 'imaginary $name';
|
||||||
|
} else if (levelDiff < -5) {
|
||||||
|
final i = 5 - rng.nextInt(10 + levelDiff + 1);
|
||||||
|
name = _sick(i, _young((monsterLevel - targetLevel) - i, name));
|
||||||
|
} else if (levelDiff < 0) {
|
||||||
|
if (rng.nextInt(2) == 1) {
|
||||||
|
name = _sick(levelDiff, name);
|
||||||
|
} else {
|
||||||
|
name = _young(levelDiff, name);
|
||||||
|
}
|
||||||
|
} else if (levelDiff >= 10) {
|
||||||
|
name = 'messianic $name';
|
||||||
|
} else if (levelDiff > 5) {
|
||||||
|
final i = 5 - rng.nextInt(10 - levelDiff + 1);
|
||||||
|
name = _big(i, _special((levelDiff) - i, name));
|
||||||
|
} else if (levelDiff > 0) {
|
||||||
|
if (rng.nextInt(2) == 1) {
|
||||||
|
name = _big(levelDiff, name);
|
||||||
|
} else {
|
||||||
|
name = _special(levelDiff, name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!definite) {
|
||||||
|
name = indefinite(name, qty);
|
||||||
|
}
|
||||||
|
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum RewardKind { spell, equip, stat, item }
|
||||||
|
|
||||||
|
class QuestResult {
|
||||||
|
const QuestResult({
|
||||||
|
required this.caption,
|
||||||
|
required this.reward,
|
||||||
|
this.monsterName,
|
||||||
|
this.monsterLevel,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String caption;
|
||||||
|
final RewardKind reward;
|
||||||
|
final String? monsterName;
|
||||||
|
final int? monsterLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
QuestResult completeQuest(PqConfig config, DeterministicRandom rng, int level) {
|
||||||
|
final rewardRoll = rng.nextInt(4);
|
||||||
|
final reward = switch (rewardRoll) {
|
||||||
|
0 => RewardKind.spell,
|
||||||
|
1 => RewardKind.equip,
|
||||||
|
2 => RewardKind.stat,
|
||||||
|
_ => RewardKind.item,
|
||||||
|
};
|
||||||
|
|
||||||
|
final questRoll = rng.nextInt(5);
|
||||||
|
switch (questRoll) {
|
||||||
|
case 0:
|
||||||
|
var best = '';
|
||||||
|
var bestLevel = 0;
|
||||||
|
for (var i = 0; i < 4; i++) {
|
||||||
|
final m = pick(config.monsters, rng);
|
||||||
|
final l = _monsterLevel(m);
|
||||||
|
if (i == 0 || (l - level).abs() < (bestLevel - level).abs()) {
|
||||||
|
best = m;
|
||||||
|
bestLevel = l;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final name = best.split('|').first;
|
||||||
|
return QuestResult(
|
||||||
|
caption: 'Exterminate ${definite(name, 2)}',
|
||||||
|
reward: reward,
|
||||||
|
monsterName: best,
|
||||||
|
monsterLevel: bestLevel,
|
||||||
|
);
|
||||||
|
case 1:
|
||||||
|
final item = interestingItem(config, rng);
|
||||||
|
return QuestResult(caption: 'Seek ${definite(item, 1)}', reward: reward);
|
||||||
|
case 2:
|
||||||
|
final item = boringItem(config, rng);
|
||||||
|
return QuestResult(caption: 'Deliver this $item', reward: reward);
|
||||||
|
case 3:
|
||||||
|
final item = boringItem(config, rng);
|
||||||
|
return QuestResult(
|
||||||
|
caption: 'Fetch me ${indefinite(item, 1)}',
|
||||||
|
reward: reward,
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
var best = '';
|
||||||
|
var bestLevel = 0;
|
||||||
|
for (var i = 0; i < 2; i++) {
|
||||||
|
final m = pick(config.monsters, rng);
|
||||||
|
final l = _monsterLevel(m);
|
||||||
|
if (i == 0 || (l - level).abs() < (bestLevel - level).abs()) {
|
||||||
|
best = m;
|
||||||
|
bestLevel = l;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final name = best.split('|').first;
|
||||||
|
return QuestResult(
|
||||||
|
caption: 'Placate ${definite(name, 2)}',
|
||||||
|
reward: reward,
|
||||||
|
monsterName: best,
|
||||||
|
monsterLevel: bestLevel,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ActResult {
|
||||||
|
const ActResult({
|
||||||
|
required this.actTitle,
|
||||||
|
required this.plotBarMaxSeconds,
|
||||||
|
required this.rewards,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String actTitle;
|
||||||
|
final int plotBarMaxSeconds;
|
||||||
|
final List<RewardKind> rewards;
|
||||||
|
}
|
||||||
|
|
||||||
|
ActResult completeAct(int existingActCount) {
|
||||||
|
final nextActIndex = existingActCount;
|
||||||
|
final title = 'Act ${intToRoman(nextActIndex)}';
|
||||||
|
final plotBarMax = 60 * 60 * (1 + 5 * existingActCount);
|
||||||
|
|
||||||
|
final rewards = <RewardKind>[];
|
||||||
|
if (existingActCount > 1) {
|
||||||
|
rewards.add(RewardKind.item);
|
||||||
|
}
|
||||||
|
if (existingActCount > 2) {
|
||||||
|
rewards.add(RewardKind.equip);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ActResult(
|
||||||
|
actTitle: title,
|
||||||
|
plotBarMaxSeconds: plotBarMax,
|
||||||
|
rewards: rewards,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class TaskResult {
|
||||||
|
const TaskResult({
|
||||||
|
required this.caption,
|
||||||
|
required this.durationMillis,
|
||||||
|
required this.progress,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String caption;
|
||||||
|
final int durationMillis;
|
||||||
|
final ProgressState progress;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Starts a task: resets task bar and sets caption.
|
||||||
|
TaskResult startTask(
|
||||||
|
ProgressState progress,
|
||||||
|
String caption,
|
||||||
|
int durationMillis,
|
||||||
|
) {
|
||||||
|
final updated = progress.copyWith(
|
||||||
|
task: ProgressBarState(position: 0, max: durationMillis),
|
||||||
|
);
|
||||||
|
return TaskResult(
|
||||||
|
caption: '$caption...',
|
||||||
|
durationMillis: durationMillis,
|
||||||
|
progress: updated,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class DequeueResult {
|
||||||
|
const DequeueResult({
|
||||||
|
required this.progress,
|
||||||
|
required this.queue,
|
||||||
|
required this.caption,
|
||||||
|
required this.taskType,
|
||||||
|
required this.kind,
|
||||||
|
});
|
||||||
|
|
||||||
|
final ProgressState progress;
|
||||||
|
final QueueState queue;
|
||||||
|
final String caption;
|
||||||
|
final TaskType taskType;
|
||||||
|
final QueueKind kind;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Process the queue when current task is done. Returns null if nothing to do.
|
||||||
|
DequeueResult? dequeue(ProgressState progress, QueueState queue) {
|
||||||
|
// Only act when the task bar is finished.
|
||||||
|
if (progress.task.position < progress.task.max) return null;
|
||||||
|
if (queue.entries.isEmpty) return null;
|
||||||
|
|
||||||
|
final entries = Queue<QueueEntry>.from(queue.entries);
|
||||||
|
if (entries.isEmpty) return null;
|
||||||
|
|
||||||
|
final next = entries.removeFirst();
|
||||||
|
final taskResult = startTask(progress, next.caption, next.durationMillis);
|
||||||
|
return DequeueResult(
|
||||||
|
progress: taskResult.progress,
|
||||||
|
queue: QueueState(entries: entries.toList()),
|
||||||
|
caption: taskResult.caption,
|
||||||
|
taskType: next.taskType,
|
||||||
|
kind: next.kind,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
int _monsterLevel(String entry) {
|
||||||
|
final parts = entry.split('|');
|
||||||
|
if (parts.length < 2) return 0;
|
||||||
|
return int.tryParse(parts[1]) ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
String _sick(int m, String s) {
|
||||||
|
switch (m) {
|
||||||
|
case -5:
|
||||||
|
case 5:
|
||||||
|
return 'dead $s';
|
||||||
|
case -4:
|
||||||
|
case 4:
|
||||||
|
return 'comatose $s';
|
||||||
|
case -3:
|
||||||
|
case 3:
|
||||||
|
return 'crippled $s';
|
||||||
|
case -2:
|
||||||
|
case 2:
|
||||||
|
return 'sick $s';
|
||||||
|
case -1:
|
||||||
|
case 1:
|
||||||
|
return 'undernourished $s';
|
||||||
|
default:
|
||||||
|
return '$m$s';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _young(int m, String s) {
|
||||||
|
switch (-m) {
|
||||||
|
case -5:
|
||||||
|
case 5:
|
||||||
|
return 'foetal $s';
|
||||||
|
case -4:
|
||||||
|
case 4:
|
||||||
|
return 'baby $s';
|
||||||
|
case -3:
|
||||||
|
case 3:
|
||||||
|
return 'preadolescent $s';
|
||||||
|
case -2:
|
||||||
|
case 2:
|
||||||
|
return 'teenage $s';
|
||||||
|
case -1:
|
||||||
|
case 1:
|
||||||
|
return 'underage $s';
|
||||||
|
default:
|
||||||
|
return '$m$s';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _big(int m, String s) {
|
||||||
|
switch (m) {
|
||||||
|
case 1:
|
||||||
|
case -1:
|
||||||
|
return 'greater $s';
|
||||||
|
case 2:
|
||||||
|
case -2:
|
||||||
|
return 'massive $s';
|
||||||
|
case 3:
|
||||||
|
case -3:
|
||||||
|
return 'enormous $s';
|
||||||
|
case 4:
|
||||||
|
case -4:
|
||||||
|
return 'giant $s';
|
||||||
|
case 5:
|
||||||
|
case -5:
|
||||||
|
return 'titanic $s';
|
||||||
|
default:
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _special(int m, String s) {
|
||||||
|
switch (-m) {
|
||||||
|
case 1:
|
||||||
|
case -1:
|
||||||
|
return s.contains(' ') ? 'veteran $s' : 'Battle-$s';
|
||||||
|
case 2:
|
||||||
|
case -2:
|
||||||
|
return 'cursed $s';
|
||||||
|
case 3:
|
||||||
|
case -3:
|
||||||
|
return s.contains(' ') ? 'warrior $s' : 'Were-$s';
|
||||||
|
case 4:
|
||||||
|
case -4:
|
||||||
|
return 'undead $s';
|
||||||
|
case 5:
|
||||||
|
case -5:
|
||||||
|
return 'demon $s';
|
||||||
|
default:
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _ends(String s, String suffix) {
|
||||||
|
return s.length >= suffix.length &&
|
||||||
|
s.substring(s.length - suffix.length) == suffix;
|
||||||
|
}
|
||||||
|
|
||||||
|
String _pick(String pipeSeparated, DeterministicRandom rng) {
|
||||||
|
final parts = pipeSeparated.split('|');
|
||||||
|
if (parts.isEmpty) return '';
|
||||||
|
final idx = rng.nextInt(parts.length);
|
||||||
|
return parts[idx];
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// InterplotCinematic 관련 함수들 (Main.pas:456-521)
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/// NPC 이름 생성 (ImpressiveGuy, Main.pas:514-521)
|
||||||
|
/// 인상적인 타이틀 + 종족 또는 이름 조합
|
||||||
|
String impressiveGuy(PqConfig config, DeterministicRandom rng) {
|
||||||
|
var result = pick(config.impressiveTitles, rng);
|
||||||
|
switch (rng.nextInt(2)) {
|
||||||
|
case 0:
|
||||||
|
// "the King of the Elves" 형태
|
||||||
|
final race = pick(config.races, rng).split('|').first;
|
||||||
|
result = 'the $result of the ${pluralize(race)}';
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
// "King Vrognak of Zoxzik" 형태
|
||||||
|
result = '$result ${generateName(rng)} of ${generateName(rng)}';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 이름 있는 몬스터 생성 (NamedMonster, Main.pas:498-512)
|
||||||
|
/// 레벨에 맞는 몬스터 선택 후 GenerateName으로 이름 붙이기
|
||||||
|
String namedMonster(PqConfig config, DeterministicRandom rng, int level) {
|
||||||
|
String best = '';
|
||||||
|
int bestLevel = 0;
|
||||||
|
|
||||||
|
// 5번 시도해서 레벨에 가장 가까운 몬스터 선택
|
||||||
|
for (var i = 0; i < 5; i++) {
|
||||||
|
final m = pick(config.monsters, rng);
|
||||||
|
final parts = m.split('|');
|
||||||
|
final name = parts.first;
|
||||||
|
final lev = parts.length > 1 ? (int.tryParse(parts[1]) ?? 0) : 0;
|
||||||
|
|
||||||
|
if (best.isEmpty || (level - lev).abs() < (level - bestLevel).abs()) {
|
||||||
|
best = name;
|
||||||
|
bestLevel = lev;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return '${generateName(rng)} the $best';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 플롯 간 시네마틱 이벤트 생성 (InterplotCinematic, Main.pas:456-495)
|
||||||
|
/// 3가지 시나리오 중 하나를 랜덤 선택
|
||||||
|
List<QueueEntry> interplotCinematic(
|
||||||
|
PqConfig config,
|
||||||
|
DeterministicRandom rng,
|
||||||
|
int level,
|
||||||
|
int plotCount,
|
||||||
|
) {
|
||||||
|
final entries = <QueueEntry>[];
|
||||||
|
|
||||||
|
// 헬퍼: 큐 엔트리 추가 (원본의 Q 함수 역할)
|
||||||
|
void q(QueueKind kind, int seconds, String caption) {
|
||||||
|
entries.add(
|
||||||
|
QueueEntry(kind: kind, durationMillis: seconds * 1000, caption: caption),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (rng.nextInt(3)) {
|
||||||
|
case 0:
|
||||||
|
// 시나리오 1: 우호적 오아시스
|
||||||
|
q(
|
||||||
|
QueueKind.task,
|
||||||
|
1,
|
||||||
|
'Exhausted, you arrive at a friendly oasis in a hostile land',
|
||||||
|
);
|
||||||
|
q(QueueKind.task, 2, 'You greet old friends and meet new allies');
|
||||||
|
q(QueueKind.task, 2, 'You are privy to a council of powerful do-gooders');
|
||||||
|
q(QueueKind.task, 1, 'There is much to be done. You are chosen!');
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 1:
|
||||||
|
// 시나리오 2: 강력한 적과의 전투
|
||||||
|
q(
|
||||||
|
QueueKind.task,
|
||||||
|
1,
|
||||||
|
'Your quarry is in sight, but a mighty enemy bars your path!',
|
||||||
|
);
|
||||||
|
final nemesis = namedMonster(config, rng, level + 3);
|
||||||
|
q(QueueKind.task, 4, 'A desperate struggle commences with $nemesis');
|
||||||
|
|
||||||
|
var s = rng.nextInt(3);
|
||||||
|
final combatRounds = rng.nextInt(1 + plotCount);
|
||||||
|
for (var i = 0; i < combatRounds; i++) {
|
||||||
|
s += 1 + rng.nextInt(2);
|
||||||
|
switch (s % 3) {
|
||||||
|
case 0:
|
||||||
|
q(QueueKind.task, 2, 'Locked in grim combat with $nemesis');
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
q(QueueKind.task, 2, '$nemesis seems to have the upper hand');
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
q(
|
||||||
|
QueueKind.task,
|
||||||
|
2,
|
||||||
|
'You seem to gain the advantage over $nemesis',
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
q(
|
||||||
|
QueueKind.task,
|
||||||
|
3,
|
||||||
|
'Victory! $nemesis is slain! Exhausted, you lose conciousness',
|
||||||
|
);
|
||||||
|
q(
|
||||||
|
QueueKind.task,
|
||||||
|
2,
|
||||||
|
'You awake in a friendly place, but the road awaits',
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 2:
|
||||||
|
// 시나리오 3: 배신 발견
|
||||||
|
final guy = impressiveGuy(config, rng);
|
||||||
|
q(
|
||||||
|
QueueKind.task,
|
||||||
|
2,
|
||||||
|
"Oh sweet relief! You've reached the kind protection of $guy",
|
||||||
|
);
|
||||||
|
q(
|
||||||
|
QueueKind.task,
|
||||||
|
3,
|
||||||
|
'There is rejoicing, and an unnerving encouter with $guy in private',
|
||||||
|
);
|
||||||
|
q(
|
||||||
|
QueueKind.task,
|
||||||
|
2,
|
||||||
|
'You forget your ${boringItem(config, rng)} and go back to get it',
|
||||||
|
);
|
||||||
|
q(QueueKind.task, 2, "What's this!? You overhear something shocking!");
|
||||||
|
q(QueueKind.task, 2, 'Could $guy be a dirty double-dealer?');
|
||||||
|
q(
|
||||||
|
QueueKind.task,
|
||||||
|
3,
|
||||||
|
'Who can possibly be trusted with this news!? -- Oh yes, of course',
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 마지막에 plot|2|Loading 추가
|
||||||
|
q(QueueKind.plot, 2, 'Loading');
|
||||||
|
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
83
lib/src/core/util/roman.dart
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
const _romanMap = <String, int>{
|
||||||
|
'T': 10000,
|
||||||
|
'A': 5000,
|
||||||
|
'P': 100000,
|
||||||
|
'E': 100000, // not used but kept for completeness
|
||||||
|
'M': 1000,
|
||||||
|
'D': 500,
|
||||||
|
'C': 100,
|
||||||
|
'L': 50,
|
||||||
|
'X': 10,
|
||||||
|
'V': 5,
|
||||||
|
'I': 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
String intToRoman(int n) {
|
||||||
|
final buffer = StringBuffer();
|
||||||
|
void emit(int value, String numeral) {
|
||||||
|
while (n >= value) {
|
||||||
|
buffer.write(numeral);
|
||||||
|
n -= value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
emit(10000, 'T');
|
||||||
|
if (n >= 9000) {
|
||||||
|
buffer.write('MT');
|
||||||
|
n -= 9000;
|
||||||
|
}
|
||||||
|
if (n >= 5000) {
|
||||||
|
buffer.write('A');
|
||||||
|
n -= 5000;
|
||||||
|
}
|
||||||
|
if (n >= 4000) {
|
||||||
|
buffer.write('MA');
|
||||||
|
n -= 4000;
|
||||||
|
}
|
||||||
|
|
||||||
|
emit(1000, 'M');
|
||||||
|
_subtract(ref: n, target: 900, numeral: 'CM', buffer: buffer);
|
||||||
|
_subtract(ref: n, target: 500, numeral: 'D', buffer: buffer);
|
||||||
|
_subtract(ref: n, target: 400, numeral: 'CD', buffer: buffer);
|
||||||
|
|
||||||
|
emit(100, 'C');
|
||||||
|
_subtract(ref: n, target: 90, numeral: 'XC', buffer: buffer);
|
||||||
|
_subtract(ref: n, target: 50, numeral: 'L', buffer: buffer);
|
||||||
|
_subtract(ref: n, target: 40, numeral: 'XL', buffer: buffer);
|
||||||
|
|
||||||
|
emit(10, 'X');
|
||||||
|
_subtract(ref: n, target: 9, numeral: 'IX', buffer: buffer);
|
||||||
|
_subtract(ref: n, target: 5, numeral: 'V', buffer: buffer);
|
||||||
|
_subtract(ref: n, target: 4, numeral: 'IV', buffer: buffer);
|
||||||
|
emit(1, 'I');
|
||||||
|
return buffer.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _subtract({
|
||||||
|
required int ref,
|
||||||
|
required int target,
|
||||||
|
required String numeral,
|
||||||
|
required StringBuffer buffer,
|
||||||
|
}) {
|
||||||
|
if (ref >= target) {
|
||||||
|
buffer.write(numeral);
|
||||||
|
ref -= target;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int romanToInt(String n) {
|
||||||
|
var result = 0;
|
||||||
|
var i = 0;
|
||||||
|
while (i < n.length) {
|
||||||
|
final one = _romanMap[n[i]] ?? 0;
|
||||||
|
final two = i + 1 < n.length ? _romanMap[n[i + 1]] ?? 0 : 0;
|
||||||
|
if (two > one) {
|
||||||
|
result += (two - one);
|
||||||
|
i += 2;
|
||||||
|
} else {
|
||||||
|
result += one;
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
314
lib/src/features/front/front_screen.dart
Normal file
@@ -0,0 +1,314 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class FrontScreen extends StatelessWidget {
|
||||||
|
const FrontScreen({super.key, this.onNewCharacter, this.onLoadSave});
|
||||||
|
|
||||||
|
/// "New character" 버튼 클릭 시 호출
|
||||||
|
final void Function(BuildContext context)? onNewCharacter;
|
||||||
|
|
||||||
|
/// "Load save" 버튼 클릭 시 호출
|
||||||
|
final Future<void> Function(BuildContext context)? onLoadSave;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final colorScheme = theme.colorScheme;
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
body: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
colors: [colorScheme.surfaceContainerHighest, colorScheme.surface],
|
||||||
|
begin: Alignment.topLeft,
|
||||||
|
end: Alignment.bottomRight,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: SafeArea(
|
||||||
|
child: Center(
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(maxWidth: 960),
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.all(24),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
_HeroHeader(theme: theme, colorScheme: colorScheme),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
_ActionRow(
|
||||||
|
onNewCharacter: onNewCharacter != null
|
||||||
|
? () => onNewCharacter!(context)
|
||||||
|
: () => _showPlaceholder(context),
|
||||||
|
onLoadSave: onLoadSave != null
|
||||||
|
? () => onLoadSave!(context)
|
||||||
|
: () => _showPlaceholder(context),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
const _StatusCards(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showPlaceholder(BuildContext context) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text(
|
||||||
|
'Core gameplay loop is coming next. See doc/progress-quest-flutter-plan.md for milestones.',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class _HeroHeader extends StatelessWidget {
|
||||||
|
const _HeroHeader({required this.theme, required this.colorScheme});
|
||||||
|
|
||||||
|
final ThemeData theme;
|
||||||
|
final ColorScheme colorScheme;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return DecoratedBox(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(18),
|
||||||
|
gradient: LinearGradient(
|
||||||
|
colors: [
|
||||||
|
colorScheme.primary.withValues(alpha: 0.9),
|
||||||
|
colorScheme.primaryContainer,
|
||||||
|
],
|
||||||
|
begin: Alignment.topLeft,
|
||||||
|
end: Alignment.bottomRight,
|
||||||
|
),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: colorScheme.primary.withValues(alpha: 0.18),
|
||||||
|
blurRadius: 18,
|
||||||
|
offset: const Offset(0, 10),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 24),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Icon(Icons.auto_awesome, color: colorScheme.onPrimary),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Ascii Never Die',
|
||||||
|
style: theme.textTheme.headlineSmall?.copyWith(
|
||||||
|
color: colorScheme.onPrimary,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
Text(
|
||||||
|
'Offline Progress Quest (PQ 6.4) rebuilt with Flutter.',
|
||||||
|
style: theme.textTheme.titleMedium?.copyWith(
|
||||||
|
color: colorScheme.onPrimary.withValues(alpha: 0.9),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 14),
|
||||||
|
Wrap(
|
||||||
|
spacing: 8,
|
||||||
|
runSpacing: 8,
|
||||||
|
children: const [
|
||||||
|
_Tag(icon: Icons.cloud_off_outlined, label: 'No network'),
|
||||||
|
_Tag(icon: Icons.timer_outlined, label: 'Idle RPG loop'),
|
||||||
|
_Tag(icon: Icons.storage_rounded, label: 'Local saves'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ActionRow extends StatelessWidget {
|
||||||
|
const _ActionRow({required this.onNewCharacter, required this.onLoadSave});
|
||||||
|
|
||||||
|
final VoidCallback onNewCharacter;
|
||||||
|
final VoidCallback onLoadSave;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
|
||||||
|
return Wrap(
|
||||||
|
spacing: 12,
|
||||||
|
runSpacing: 12,
|
||||||
|
children: [
|
||||||
|
FilledButton.icon(
|
||||||
|
onPressed: onNewCharacter,
|
||||||
|
icon: const Icon(Icons.casino_outlined),
|
||||||
|
label: const Text('New character'),
|
||||||
|
style: FilledButton.styleFrom(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 14),
|
||||||
|
textStyle: theme.textTheme.titleMedium,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
OutlinedButton.icon(
|
||||||
|
onPressed: onLoadSave,
|
||||||
|
icon: const Icon(Icons.folder_open),
|
||||||
|
label: const Text('Load save'),
|
||||||
|
style: OutlinedButton.styleFrom(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 14),
|
||||||
|
textStyle: theme.textTheme.titleMedium,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
TextButton.icon(
|
||||||
|
onPressed: () => _showPlaceholder(context),
|
||||||
|
icon: const Icon(Icons.menu_book_outlined),
|
||||||
|
label: const Text('View build plan'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _StatusCards extends StatelessWidget {
|
||||||
|
const _StatusCards();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
children: const [
|
||||||
|
_InfoCard(
|
||||||
|
icon: Icons.route_outlined,
|
||||||
|
title: 'Build roadmap',
|
||||||
|
points: [
|
||||||
|
'Port PQ 6.4 data set (Config.dfm) into Dart constants.',
|
||||||
|
'Recreate quest/task loop with deterministic RNG + saves.',
|
||||||
|
'Deliver offline-first storage (GZip JSON) across platforms.',
|
||||||
|
],
|
||||||
|
),
|
||||||
|
SizedBox(height: 16),
|
||||||
|
_InfoCard(
|
||||||
|
icon: Icons.auto_fix_high_outlined,
|
||||||
|
title: 'Tech stack',
|
||||||
|
points: [
|
||||||
|
'Flutter (Material 3) with multiplatform targets enabled.',
|
||||||
|
'path_provider + shared_preferences for local storage hooks.',
|
||||||
|
'Strict lints with package imports enforced from day one.',
|
||||||
|
],
|
||||||
|
),
|
||||||
|
SizedBox(height: 16),
|
||||||
|
_InfoCard(
|
||||||
|
icon: Icons.checklist_rtl,
|
||||||
|
title: 'Today’s focus',
|
||||||
|
points: [
|
||||||
|
'Set up scaffold + lints.',
|
||||||
|
'Wire seed theme and initial navigation shell.',
|
||||||
|
'Keep reference assets under example/pq for parity.',
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _InfoCard extends StatelessWidget {
|
||||||
|
const _InfoCard({required this.title, required this.points, this.icon});
|
||||||
|
|
||||||
|
final String title;
|
||||||
|
final List<String> points;
|
||||||
|
final IconData? icon;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final colorScheme = theme.colorScheme;
|
||||||
|
|
||||||
|
return Card(
|
||||||
|
elevation: 3,
|
||||||
|
shadowColor: colorScheme.shadow.withValues(alpha: 0.2),
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(18),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
if (icon != null) ...[
|
||||||
|
Icon(icon, color: colorScheme.primary),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
],
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
style: theme.textTheme.titleMedium?.copyWith(
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
...points.map(
|
||||||
|
(point) => Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const Padding(
|
||||||
|
padding: EdgeInsets.only(top: 3),
|
||||||
|
child: Icon(Icons.check_circle_outline, size: 18),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
Expanded(
|
||||||
|
child: Text(point, style: theme.textTheme.bodyMedium),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _Tag extends StatelessWidget {
|
||||||
|
const _Tag({required this.icon, required this.label});
|
||||||
|
|
||||||
|
final IconData icon;
|
||||||
|
final String label;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
|
return Chip(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||||
|
backgroundColor: colorScheme.onPrimary.withValues(alpha: 0.14),
|
||||||
|
avatar: Icon(icon, color: colorScheme.onPrimary, size: 16),
|
||||||
|
label: Text(
|
||||||
|
label,
|
||||||
|
style: TextStyle(
|
||||||
|
color: colorScheme.onPrimary,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
side: BorderSide.none,
|
||||||
|
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
107
lib/src/features/front/save_picker_dialog.dart
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
|
||||||
|
import 'package:askiineverdie/src/core/storage/save_service.dart'
|
||||||
|
show SaveFileInfo;
|
||||||
|
|
||||||
|
/// 저장 파일 선택 다이얼로그
|
||||||
|
/// 선택된 파일명을 반환하거나, 취소 시 null 반환
|
||||||
|
class SavePickerDialog extends StatelessWidget {
|
||||||
|
const SavePickerDialog({super.key, required this.saves});
|
||||||
|
|
||||||
|
final List<SaveFileInfo> saves;
|
||||||
|
|
||||||
|
/// 다이얼로그 표시 및 결과 반환
|
||||||
|
static Future<String?> show(
|
||||||
|
BuildContext context,
|
||||||
|
List<SaveFileInfo> saves,
|
||||||
|
) async {
|
||||||
|
if (saves.isEmpty) {
|
||||||
|
// 저장 파일이 없으면 안내 메시지
|
||||||
|
ScaffoldMessenger.of(
|
||||||
|
context,
|
||||||
|
).showSnackBar(const SnackBar(content: Text('저장된 게임이 없습니다.')));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return showDialog<String>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => SavePickerDialog(saves: saves),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final colorScheme = theme.colorScheme;
|
||||||
|
|
||||||
|
return AlertDialog(
|
||||||
|
title: Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.folder_open, color: colorScheme.primary),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
const Text('Load Game'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
content: SizedBox(
|
||||||
|
width: 400,
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(maxHeight: 400),
|
||||||
|
child: ListView.separated(
|
||||||
|
shrinkWrap: true,
|
||||||
|
itemCount: saves.length,
|
||||||
|
separatorBuilder: (_, __) => const Divider(height: 1),
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final save = saves[index];
|
||||||
|
return _SaveListTile(
|
||||||
|
save: save,
|
||||||
|
onTap: () => Navigator.of(context).pop(save.fileName),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(null),
|
||||||
|
child: const Text('Cancel'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SaveListTile extends StatelessWidget {
|
||||||
|
const _SaveListTile({required this.save, required this.onTap});
|
||||||
|
|
||||||
|
final SaveFileInfo save;
|
||||||
|
final VoidCallback onTap;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final dateFormat = DateFormat('yyyy-MM-dd HH:mm');
|
||||||
|
|
||||||
|
return ListTile(
|
||||||
|
leading: const Icon(Icons.save),
|
||||||
|
title: Text(
|
||||||
|
save.displayName,
|
||||||
|
style: theme.textTheme.titleMedium?.copyWith(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
subtitle: Text(
|
||||||
|
'${dateFormat.format(save.modifiedAt)} · ${_formatSize(save.sizeBytes)}',
|
||||||
|
style: theme.textTheme.bodySmall,
|
||||||
|
),
|
||||||
|
trailing: const Icon(Icons.chevron_right),
|
||||||
|
onTap: onTap,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _formatSize(int bytes) {
|
||||||
|
if (bytes < 1024) return '$bytes B';
|
||||||
|
if (bytes < 1024 * 1024) return '${(bytes / 1024).toStringAsFixed(1)} KB';
|
||||||
|
return '${(bytes / (1024 * 1024)).toStringAsFixed(1)} MB';
|
||||||
|
}
|
||||||
|
}
|
||||||
654
lib/src/features/game/game_play_screen.dart
Normal file
@@ -0,0 +1,654 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:askiineverdie/src/core/model/game_state.dart';
|
||||||
|
import 'package:askiineverdie/src/core/util/pq_logic.dart' as pq_logic;
|
||||||
|
import 'package:askiineverdie/src/features/game/game_session_controller.dart';
|
||||||
|
|
||||||
|
/// 게임 진행 화면 (Main.dfm 기반 3패널 레이아웃)
|
||||||
|
class GamePlayScreen extends StatefulWidget {
|
||||||
|
const GamePlayScreen({super.key, required this.controller});
|
||||||
|
|
||||||
|
final GameSessionController controller;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<GamePlayScreen> createState() => _GamePlayScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _GamePlayScreenState extends State<GamePlayScreen>
|
||||||
|
with WidgetsBindingObserver {
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
widget.controller.addListener(_onControllerChanged);
|
||||||
|
WidgetsBinding.instance.addObserver(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
WidgetsBinding.instance.removeObserver(this);
|
||||||
|
widget.controller.removeListener(_onControllerChanged);
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||||
|
super.didChangeAppLifecycleState(state);
|
||||||
|
|
||||||
|
// 앱이 백그라운드로 가거나 비활성화될 때 자동 저장
|
||||||
|
if (state == AppLifecycleState.paused ||
|
||||||
|
state == AppLifecycleState.inactive ||
|
||||||
|
state == AppLifecycleState.detached) {
|
||||||
|
_saveGameState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _saveGameState() async {
|
||||||
|
final currentState = widget.controller.state;
|
||||||
|
if (currentState == null || !widget.controller.isRunning) return;
|
||||||
|
|
||||||
|
await widget.controller.saveManager.saveState(currentState);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 뒤로가기 시 저장 확인 다이얼로그
|
||||||
|
Future<bool> _onPopInvoked() async {
|
||||||
|
final shouldPop = await showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AlertDialog(
|
||||||
|
title: const Text('Exit Game'),
|
||||||
|
content: const Text('Save your progress before leaving?'),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(false),
|
||||||
|
child: const Text('Cancel'),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop(true);
|
||||||
|
},
|
||||||
|
child: const Text('Exit without saving'),
|
||||||
|
),
|
||||||
|
FilledButton(
|
||||||
|
onPressed: () async {
|
||||||
|
await _saveGameState();
|
||||||
|
if (context.mounted) {
|
||||||
|
Navigator.of(context).pop(true);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: const Text('Save and Exit'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return shouldPop ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onControllerChanged() {
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final state = widget.controller.state;
|
||||||
|
if (state == null) {
|
||||||
|
return const Scaffold(body: Center(child: CircularProgressIndicator()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return PopScope(
|
||||||
|
canPop: false,
|
||||||
|
onPopInvokedWithResult: (didPop, result) async {
|
||||||
|
if (didPop) return;
|
||||||
|
final shouldPop = await _onPopInvoked();
|
||||||
|
if (shouldPop && context.mounted) {
|
||||||
|
await widget.controller.pause(saveOnStop: false);
|
||||||
|
if (context.mounted) {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text('Progress Quest - ${state.traits.name}'),
|
||||||
|
actions: [
|
||||||
|
// 치트 버튼 (디버그용)
|
||||||
|
if (widget.controller.cheatsEnabled) ...[
|
||||||
|
IconButton(
|
||||||
|
icon: const Text('L+1'),
|
||||||
|
tooltip: 'Level Up',
|
||||||
|
onPressed: () => widget.controller.loop?.cheatCompleteTask(),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: const Text('Q!'),
|
||||||
|
tooltip: 'Complete Quest',
|
||||||
|
onPressed: () => widget.controller.loop?.cheatCompleteQuest(),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: const Text('P!'),
|
||||||
|
tooltip: 'Complete Plot',
|
||||||
|
onPressed: () => widget.controller.loop?.cheatCompletePlot(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: Column(
|
||||||
|
children: [
|
||||||
|
// 메인 3패널 영역
|
||||||
|
Expanded(
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
// 좌측 패널: Character Sheet
|
||||||
|
Expanded(flex: 2, child: _buildCharacterPanel(state)),
|
||||||
|
|
||||||
|
// 중앙 패널: Equipment/Inventory
|
||||||
|
Expanded(flex: 3, child: _buildEquipmentPanel(state)),
|
||||||
|
|
||||||
|
// 우측 패널: Plot/Quest
|
||||||
|
Expanded(flex: 2, child: _buildQuestPanel(state)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// 하단: Task Progress
|
||||||
|
_buildBottomPanel(state),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 좌측 패널: Character Sheet (Traits, Stats, Experience, Spells)
|
||||||
|
Widget _buildCharacterPanel(GameState state) {
|
||||||
|
return Card(
|
||||||
|
margin: const EdgeInsets.all(4),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
_buildPanelHeader('Character Sheet'),
|
||||||
|
|
||||||
|
// Traits 목록
|
||||||
|
_buildSectionHeader('Traits'),
|
||||||
|
_buildTraitsList(state),
|
||||||
|
|
||||||
|
// Stats 목록
|
||||||
|
_buildSectionHeader('Stats'),
|
||||||
|
Expanded(flex: 2, child: _buildStatsList(state)),
|
||||||
|
|
||||||
|
// Experience 바
|
||||||
|
_buildSectionHeader('Experience'),
|
||||||
|
_buildProgressBar(
|
||||||
|
state.progress.exp.position,
|
||||||
|
state.progress.exp.max,
|
||||||
|
Colors.blue,
|
||||||
|
tooltip:
|
||||||
|
'${state.progress.exp.max - state.progress.exp.position} '
|
||||||
|
'XP needed for next level',
|
||||||
|
),
|
||||||
|
|
||||||
|
// Spell Book
|
||||||
|
_buildSectionHeader('Spell Book'),
|
||||||
|
Expanded(flex: 2, child: _buildSpellsList(state)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 중앙 패널: Equipment/Inventory
|
||||||
|
Widget _buildEquipmentPanel(GameState state) {
|
||||||
|
return Card(
|
||||||
|
margin: const EdgeInsets.all(4),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
_buildPanelHeader('Equipment'),
|
||||||
|
|
||||||
|
// Equipment 목록
|
||||||
|
Expanded(flex: 2, child: _buildEquipmentList(state)),
|
||||||
|
|
||||||
|
// Inventory
|
||||||
|
_buildPanelHeader('Inventory'),
|
||||||
|
Expanded(flex: 3, child: _buildInventoryList(state)),
|
||||||
|
|
||||||
|
// Encumbrance 바
|
||||||
|
_buildSectionHeader('Encumbrance'),
|
||||||
|
_buildProgressBar(
|
||||||
|
state.progress.encumbrance.position,
|
||||||
|
state.progress.encumbrance.max,
|
||||||
|
Colors.orange,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 우측 패널: Plot/Quest
|
||||||
|
Widget _buildQuestPanel(GameState state) {
|
||||||
|
return Card(
|
||||||
|
margin: const EdgeInsets.all(4),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
_buildPanelHeader('Plot Development'),
|
||||||
|
|
||||||
|
// Plot 목록
|
||||||
|
Expanded(child: _buildPlotList(state)),
|
||||||
|
|
||||||
|
// Plot 바
|
||||||
|
_buildProgressBar(
|
||||||
|
state.progress.plot.position,
|
||||||
|
state.progress.plot.max,
|
||||||
|
Colors.purple,
|
||||||
|
tooltip: state.progress.plot.max > 0
|
||||||
|
? '${pq_logic.roughTime(state.progress.plot.max - state.progress.plot.position)} remaining'
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
|
||||||
|
_buildPanelHeader('Quests'),
|
||||||
|
|
||||||
|
// Quest 목록
|
||||||
|
Expanded(child: _buildQuestList(state)),
|
||||||
|
|
||||||
|
// Quest 바
|
||||||
|
_buildProgressBar(
|
||||||
|
state.progress.quest.position,
|
||||||
|
state.progress.quest.max,
|
||||||
|
Colors.green,
|
||||||
|
tooltip: state.progress.quest.max > 0
|
||||||
|
? '${(100 * state.progress.quest.position ~/ state.progress.quest.max)}% complete'
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 하단 패널: Task Progress + Status
|
||||||
|
Widget _buildBottomPanel(GameState state) {
|
||||||
|
final speed = widget.controller.loop?.speedMultiplier ?? 1;
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||||
|
border: Border(top: BorderSide(color: Theme.of(context).dividerColor)),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
// 상태 메시지 + 배속 버튼
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
state.progress.currentTask.caption.isNotEmpty
|
||||||
|
? state.progress.currentTask.caption
|
||||||
|
: 'Welcome to Progress Quest!',
|
||||||
|
style: Theme.of(context).textTheme.bodyMedium,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// 배속 버튼
|
||||||
|
SizedBox(
|
||||||
|
height: 28,
|
||||||
|
child: OutlinedButton(
|
||||||
|
onPressed: () {
|
||||||
|
widget.controller.loop?.cycleSpeed();
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
|
style: OutlinedButton.styleFrom(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
'${speed}x',
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: speed > 1 ? FontWeight.bold : FontWeight.normal,
|
||||||
|
color: speed > 1
|
||||||
|
? Theme.of(context).colorScheme.primary
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
|
||||||
|
// Task Progress 바
|
||||||
|
_buildProgressBar(
|
||||||
|
state.progress.task.position,
|
||||||
|
state.progress.task.max,
|
||||||
|
Theme.of(context).colorScheme.primary,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildPanelHeader(String title) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||||
|
color: Theme.of(context).colorScheme.primaryContainer,
|
||||||
|
child: Text(
|
||||||
|
title,
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSectionHeader(String title) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
||||||
|
child: Text(title, style: Theme.of(context).textTheme.labelSmall),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildProgressBar(
|
||||||
|
int position,
|
||||||
|
int max,
|
||||||
|
Color color, {
|
||||||
|
String? tooltip,
|
||||||
|
}) {
|
||||||
|
final progress = max > 0 ? (position / max).clamp(0.0, 1.0) : 0.0;
|
||||||
|
final bar = Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||||
|
child: LinearProgressIndicator(
|
||||||
|
value: progress,
|
||||||
|
backgroundColor: color.withValues(alpha: 0.2),
|
||||||
|
valueColor: AlwaysStoppedAnimation<Color>(color),
|
||||||
|
minHeight: 12,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (tooltip != null && tooltip.isNotEmpty) {
|
||||||
|
return Tooltip(message: tooltip, child: bar);
|
||||||
|
}
|
||||||
|
return bar;
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildTraitsList(GameState state) {
|
||||||
|
final traits = [
|
||||||
|
('Name', state.traits.name),
|
||||||
|
('Race', state.traits.race),
|
||||||
|
('Class', state.traits.klass),
|
||||||
|
('Level', '${state.traits.level}'),
|
||||||
|
];
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
|
child: Column(
|
||||||
|
children: traits.map((t) {
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
width: 50,
|
||||||
|
child: Text(t.$1, style: const TextStyle(fontSize: 11)),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
t.$2,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildStatsList(GameState state) {
|
||||||
|
final stats = [
|
||||||
|
('STR', state.stats.str),
|
||||||
|
('CON', state.stats.con),
|
||||||
|
('DEX', state.stats.dex),
|
||||||
|
('INT', state.stats.intelligence),
|
||||||
|
('WIS', state.stats.wis),
|
||||||
|
('CHA', state.stats.cha),
|
||||||
|
('HP Max', state.stats.hpMax),
|
||||||
|
('MP Max', state.stats.mpMax),
|
||||||
|
];
|
||||||
|
|
||||||
|
return ListView.builder(
|
||||||
|
itemCount: stats.length,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final stat = stats[index];
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
width: 50,
|
||||||
|
child: Text(stat.$1, style: const TextStyle(fontSize: 11)),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'${stat.$2}',
|
||||||
|
style: const TextStyle(fontSize: 11, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSpellsList(GameState state) {
|
||||||
|
if (state.spellBook.spells.isEmpty) {
|
||||||
|
return const Center(
|
||||||
|
child: Text('No spells yet', style: TextStyle(fontSize: 11)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ListView.builder(
|
||||||
|
itemCount: state.spellBook.spells.length,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final spell = state.spellBook.spells[index];
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
spell.name,
|
||||||
|
style: const TextStyle(fontSize: 11),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
spell.rank,
|
||||||
|
style: const TextStyle(fontSize: 11, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildEquipmentList(GameState state) {
|
||||||
|
// 원본에는 11개 슬롯이 있지만, 현재 모델은 3개만 구현
|
||||||
|
final equipment = [
|
||||||
|
('Weapon', state.equipment.weapon),
|
||||||
|
('Shield', state.equipment.shield),
|
||||||
|
('Armor', state.equipment.armor),
|
||||||
|
];
|
||||||
|
|
||||||
|
return ListView.builder(
|
||||||
|
itemCount: equipment.length,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final equip = equipment[index];
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 2),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
width: 60,
|
||||||
|
child: Text(equip.$1, style: const TextStyle(fontSize: 11)),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
equip.$2.isNotEmpty ? equip.$2 : '-',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildInventoryList(GameState state) {
|
||||||
|
if (state.inventory.items.isEmpty) {
|
||||||
|
return Center(
|
||||||
|
child: Text(
|
||||||
|
'Gold: ${state.inventory.gold}',
|
||||||
|
style: const TextStyle(fontSize: 11),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ListView.builder(
|
||||||
|
itemCount: state.inventory.items.length + 1, // +1 for gold
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
if (index == 0) {
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
const Expanded(
|
||||||
|
child: Text('Gold', style: TextStyle(fontSize: 11)),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'${state.inventory.gold}',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
final item = state.inventory.items[index - 1];
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
item.name,
|
||||||
|
style: const TextStyle(fontSize: 11),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'${item.count}',
|
||||||
|
style: const TextStyle(fontSize: 11, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildPlotList(GameState state) {
|
||||||
|
// 플롯 단계를 표시 (Act I, Act II, ...)
|
||||||
|
final plotCount = state.progress.plotStageCount;
|
||||||
|
if (plotCount == 0) {
|
||||||
|
return const Center(
|
||||||
|
child: Text('Prologue', style: TextStyle(fontSize: 11)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ListView.builder(
|
||||||
|
itemCount: plotCount,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final isCompleted = index < plotCount - 1;
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
isCompleted ? Icons.check_box : Icons.check_box_outline_blank,
|
||||||
|
size: 14,
|
||||||
|
color: isCompleted ? Colors.green : Colors.grey,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Text(
|
||||||
|
index == 0 ? 'Prologue' : 'Act ${_toRoman(index)}',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 11,
|
||||||
|
decoration: isCompleted
|
||||||
|
? TextDecoration.lineThrough
|
||||||
|
: TextDecoration.none,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildQuestList(GameState state) {
|
||||||
|
final questCount = state.progress.questCount;
|
||||||
|
if (questCount == 0) {
|
||||||
|
return const Center(
|
||||||
|
child: Text('No active quests', style: TextStyle(fontSize: 11)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 현재 퀘스트 캡션이 있으면 표시
|
||||||
|
final currentTask = state.progress.currentTask;
|
||||||
|
return ListView(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.arrow_right, size: 14),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
currentTask.caption.isNotEmpty
|
||||||
|
? currentTask.caption
|
||||||
|
: 'Quest #$questCount',
|
||||||
|
style: const TextStyle(fontSize: 11),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 로마 숫자 변환 (간단 버전)
|
||||||
|
String _toRoman(int number) {
|
||||||
|
const romanNumerals = [
|
||||||
|
(1000, 'M'),
|
||||||
|
(900, 'CM'),
|
||||||
|
(500, 'D'),
|
||||||
|
(400, 'CD'),
|
||||||
|
(100, 'C'),
|
||||||
|
(90, 'XC'),
|
||||||
|
(50, 'L'),
|
||||||
|
(40, 'XL'),
|
||||||
|
(10, 'X'),
|
||||||
|
(9, 'IX'),
|
||||||
|
(5, 'V'),
|
||||||
|
(4, 'IV'),
|
||||||
|
(1, 'I'),
|
||||||
|
];
|
||||||
|
|
||||||
|
var result = '';
|
||||||
|
var remaining = number;
|
||||||
|
for (final (value, numeral) in romanNumerals) {
|
||||||
|
while (remaining >= value) {
|
||||||
|
result += numeral;
|
||||||
|
remaining -= value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
126
lib/src/features/game/game_session_controller.dart
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:askiineverdie/src/core/engine/progress_loop.dart';
|
||||||
|
import 'package:askiineverdie/src/core/engine/progress_service.dart';
|
||||||
|
import 'package:askiineverdie/src/core/model/game_state.dart';
|
||||||
|
import 'package:askiineverdie/src/core/storage/save_manager.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
|
enum GameSessionStatus { idle, loading, running, error }
|
||||||
|
|
||||||
|
/// Presentation-friendly wrapper that owns ProgressLoop and SaveManager.
|
||||||
|
class GameSessionController extends ChangeNotifier {
|
||||||
|
GameSessionController({
|
||||||
|
required this.progressService,
|
||||||
|
required this.saveManager,
|
||||||
|
this.autoSaveConfig = const AutoSaveConfig(),
|
||||||
|
Duration tickInterval = const Duration(milliseconds: 50),
|
||||||
|
DateTime Function()? now,
|
||||||
|
}) : _tickInterval = tickInterval,
|
||||||
|
_now = now ?? DateTime.now;
|
||||||
|
|
||||||
|
final ProgressService progressService;
|
||||||
|
final SaveManager saveManager;
|
||||||
|
final AutoSaveConfig autoSaveConfig;
|
||||||
|
|
||||||
|
final Duration _tickInterval;
|
||||||
|
final DateTime Function() _now;
|
||||||
|
|
||||||
|
ProgressLoop? _loop;
|
||||||
|
StreamSubscription<GameState>? _subscription;
|
||||||
|
bool _cheatsEnabled = false;
|
||||||
|
|
||||||
|
GameSessionStatus _status = GameSessionStatus.idle;
|
||||||
|
GameState? _state;
|
||||||
|
String? _error;
|
||||||
|
|
||||||
|
GameSessionStatus get status => _status;
|
||||||
|
GameState? get state => _state;
|
||||||
|
String? get error => _error;
|
||||||
|
bool get isRunning => _status == GameSessionStatus.running;
|
||||||
|
bool get cheatsEnabled => _cheatsEnabled;
|
||||||
|
|
||||||
|
/// 현재 ProgressLoop 인스턴스 (치트 기능용)
|
||||||
|
ProgressLoop? get loop => _loop;
|
||||||
|
|
||||||
|
Future<void> startNew(
|
||||||
|
GameState initialState, {
|
||||||
|
bool cheatsEnabled = false,
|
||||||
|
bool isNewGame = true,
|
||||||
|
}) async {
|
||||||
|
await _stopLoop(saveOnStop: false);
|
||||||
|
|
||||||
|
// 새 게임인 경우 초기화 (프롤로그 태스크 설정)
|
||||||
|
final state = isNewGame
|
||||||
|
? progressService.initializeNewGame(initialState)
|
||||||
|
: initialState;
|
||||||
|
|
||||||
|
_state = state;
|
||||||
|
_error = null;
|
||||||
|
_status = GameSessionStatus.running;
|
||||||
|
_cheatsEnabled = cheatsEnabled;
|
||||||
|
|
||||||
|
_loop = ProgressLoop(
|
||||||
|
initialState: state,
|
||||||
|
progressService: progressService,
|
||||||
|
saveManager: saveManager,
|
||||||
|
autoSaveConfig: autoSaveConfig,
|
||||||
|
tickInterval: _tickInterval,
|
||||||
|
now: _now,
|
||||||
|
cheatsEnabled: cheatsEnabled,
|
||||||
|
);
|
||||||
|
|
||||||
|
_subscription = _loop!.stream.listen((next) {
|
||||||
|
_state = next;
|
||||||
|
notifyListeners();
|
||||||
|
});
|
||||||
|
|
||||||
|
_loop!.start();
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> loadAndStart({
|
||||||
|
String? fileName,
|
||||||
|
bool cheatsEnabled = false,
|
||||||
|
}) async {
|
||||||
|
_status = GameSessionStatus.loading;
|
||||||
|
_error = null;
|
||||||
|
notifyListeners();
|
||||||
|
|
||||||
|
final (outcome, loaded) = await saveManager.loadState(fileName: fileName);
|
||||||
|
if (!outcome.success || loaded == null) {
|
||||||
|
_status = GameSessionStatus.error;
|
||||||
|
_error = outcome.error ?? 'Unknown error';
|
||||||
|
notifyListeners();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await startNew(loaded, cheatsEnabled: cheatsEnabled, isNewGame: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> pause({bool saveOnStop = false}) async {
|
||||||
|
await _stopLoop(saveOnStop: saveOnStop);
|
||||||
|
_status = GameSessionStatus.idle;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
final stop = _stopLoop(saveOnStop: false);
|
||||||
|
if (stop != null) {
|
||||||
|
unawaited(stop);
|
||||||
|
}
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void>? _stopLoop({required bool saveOnStop}) {
|
||||||
|
final loop = _loop;
|
||||||
|
final sub = _subscription;
|
||||||
|
_loop = null;
|
||||||
|
_subscription = null;
|
||||||
|
|
||||||
|
sub?.cancel();
|
||||||
|
if (loop == null) return null;
|
||||||
|
return loop.stop(saveOnStop: saveOnStop);
|
||||||
|
}
|
||||||
|
}
|
||||||
484
lib/src/features/new_character/new_character_screen.dart
Normal file
@@ -0,0 +1,484 @@
|
|||||||
|
import 'dart:math' as math;
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:askiineverdie/src/core/model/game_state.dart';
|
||||||
|
import 'package:askiineverdie/src/core/model/pq_config.dart';
|
||||||
|
import 'package:askiineverdie/src/core/util/deterministic_random.dart';
|
||||||
|
import 'package:askiineverdie/src/core/util/pq_logic.dart';
|
||||||
|
|
||||||
|
/// 캐릭터 생성 화면 (NewGuy.pas 포팅)
|
||||||
|
class NewCharacterScreen extends StatefulWidget {
|
||||||
|
const NewCharacterScreen({super.key, this.onCharacterCreated});
|
||||||
|
|
||||||
|
/// 캐릭터 생성 완료 시 호출되는 콜백
|
||||||
|
final void Function(GameState initialState)? onCharacterCreated;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<NewCharacterScreen> createState() => _NewCharacterScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _NewCharacterScreenState extends State<NewCharacterScreen> {
|
||||||
|
final PqConfig _config = const PqConfig();
|
||||||
|
final TextEditingController _nameController = TextEditingController();
|
||||||
|
|
||||||
|
// 종족(races)과 직업(klasses) 목록
|
||||||
|
late final List<String> _races;
|
||||||
|
late final List<String> _klasses;
|
||||||
|
|
||||||
|
// 선택된 종족/직업 인덱스
|
||||||
|
int _selectedRaceIndex = 0;
|
||||||
|
int _selectedKlassIndex = 0;
|
||||||
|
|
||||||
|
// 능력치(stats)
|
||||||
|
int _str = 0;
|
||||||
|
int _con = 0;
|
||||||
|
int _dex = 0;
|
||||||
|
int _int = 0;
|
||||||
|
int _wis = 0;
|
||||||
|
int _cha = 0;
|
||||||
|
|
||||||
|
// 롤 이력 (Unroll 기능용)
|
||||||
|
final List<int> _rollHistory = [];
|
||||||
|
|
||||||
|
// 현재 RNG 시드 (Re-Roll 전 저장)
|
||||||
|
int _currentSeed = 0;
|
||||||
|
|
||||||
|
// 이름 생성용 RNG
|
||||||
|
late DeterministicRandom _nameRng;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
|
||||||
|
// 종족/직업 목록 로드 (name|attribute 형식에서 name만 추출)
|
||||||
|
_races = _config.races.map((e) => e.split('|').first).toList();
|
||||||
|
_klasses = _config.klasses.map((e) => e.split('|').first).toList();
|
||||||
|
|
||||||
|
// 초기 랜덤화
|
||||||
|
final random = math.Random();
|
||||||
|
_selectedRaceIndex = random.nextInt(_races.length);
|
||||||
|
_selectedKlassIndex = random.nextInt(_klasses.length);
|
||||||
|
|
||||||
|
// 초기 스탯 굴림
|
||||||
|
_currentSeed = random.nextInt(0x7FFFFFFF);
|
||||||
|
_nameRng = DeterministicRandom(random.nextInt(0x7FFFFFFF));
|
||||||
|
_rollStats();
|
||||||
|
|
||||||
|
// 초기 이름 생성
|
||||||
|
_nameController.text = generateName(_nameRng);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_nameController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 스탯 굴림 (3d6 × 6)
|
||||||
|
void _rollStats() {
|
||||||
|
final rng = DeterministicRandom(_currentSeed);
|
||||||
|
setState(() {
|
||||||
|
_str = rollStat(rng);
|
||||||
|
_con = rollStat(rng);
|
||||||
|
_dex = rollStat(rng);
|
||||||
|
_int = rollStat(rng);
|
||||||
|
_wis = rollStat(rng);
|
||||||
|
_cha = rollStat(rng);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Re-Roll 버튼 클릭
|
||||||
|
void _onReroll() {
|
||||||
|
// 현재 시드를 이력에 저장
|
||||||
|
_rollHistory.insert(0, _currentSeed);
|
||||||
|
|
||||||
|
// 새 시드로 굴림
|
||||||
|
_currentSeed = math.Random().nextInt(0x7FFFFFFF);
|
||||||
|
_rollStats();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Unroll 버튼 클릭 (이전 롤로 복원)
|
||||||
|
void _onUnroll() {
|
||||||
|
if (_rollHistory.isEmpty) return;
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_currentSeed = _rollHistory.removeAt(0);
|
||||||
|
});
|
||||||
|
_rollStats();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 이름 생성 버튼 클릭
|
||||||
|
void _onGenerateName() {
|
||||||
|
setState(() {
|
||||||
|
_nameController.text = generateName(_nameRng);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Total 값 계산
|
||||||
|
int get _total => _str + _con + _dex + _int + _wis + _cha;
|
||||||
|
|
||||||
|
/// Total 색상 결정 (원본 규칙)
|
||||||
|
/// 63+18(81) 이상 = 빨강, 4*18(72) 초과 = 노랑
|
||||||
|
/// 63-18(45) 이하 = 회색, 3*18(54) 미만 = 은색
|
||||||
|
/// 그 외 = 흰색
|
||||||
|
Color _getTotalColor() {
|
||||||
|
final total = _total;
|
||||||
|
if (total >= 81) return Colors.red;
|
||||||
|
if (total > 72) return Colors.yellow;
|
||||||
|
if (total <= 45) return Colors.grey;
|
||||||
|
if (total < 54) return Colors.grey.shade400;
|
||||||
|
return Colors.white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sold! 버튼 클릭 - 캐릭터 생성 완료
|
||||||
|
void _onSold() {
|
||||||
|
final name = _nameController.text.trim();
|
||||||
|
if (name.isEmpty) {
|
||||||
|
ScaffoldMessenger.of(
|
||||||
|
context,
|
||||||
|
).showSnackBar(const SnackBar(content: Text('이름을 입력해주세요.')));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 게임에 사용할 새 RNG 생성
|
||||||
|
final gameSeed = math.Random().nextInt(0x7FFFFFFF);
|
||||||
|
|
||||||
|
// 종족/직업의 보너스 스탯 파싱
|
||||||
|
final raceEntry = _config.races[_selectedRaceIndex];
|
||||||
|
final klassEntry = _config.klasses[_selectedKlassIndex];
|
||||||
|
final raceBonus = _parseStatBonus(raceEntry);
|
||||||
|
final klassBonus = _parseStatBonus(klassEntry);
|
||||||
|
|
||||||
|
// 최종 스탯 계산 (기본 + 종족 보너스 + 직업 보너스)
|
||||||
|
final finalStats = Stats(
|
||||||
|
str: _str + (raceBonus['STR'] ?? 0) + (klassBonus['STR'] ?? 0),
|
||||||
|
con: _con + (raceBonus['CON'] ?? 0) + (klassBonus['CON'] ?? 0),
|
||||||
|
dex: _dex + (raceBonus['DEX'] ?? 0) + (klassBonus['DEX'] ?? 0),
|
||||||
|
intelligence: _int + (raceBonus['INT'] ?? 0) + (klassBonus['INT'] ?? 0),
|
||||||
|
wis: _wis + (raceBonus['WIS'] ?? 0) + (klassBonus['WIS'] ?? 0),
|
||||||
|
cha: _cha + (raceBonus['CHA'] ?? 0) + (klassBonus['CHA'] ?? 0),
|
||||||
|
hpMax: _con + (raceBonus['CON'] ?? 0) + (klassBonus['CON'] ?? 0),
|
||||||
|
mpMax: _int + (raceBonus['INT'] ?? 0) + (klassBonus['INT'] ?? 0),
|
||||||
|
);
|
||||||
|
|
||||||
|
final traits = Traits(
|
||||||
|
name: name,
|
||||||
|
race: _races[_selectedRaceIndex],
|
||||||
|
klass: _klasses[_selectedKlassIndex],
|
||||||
|
level: 1,
|
||||||
|
motto: '',
|
||||||
|
guild: '',
|
||||||
|
);
|
||||||
|
|
||||||
|
// 초기 게임 상태 생성
|
||||||
|
final initialState = GameState.withSeed(
|
||||||
|
seed: gameSeed,
|
||||||
|
traits: traits,
|
||||||
|
stats: finalStats,
|
||||||
|
inventory: const Inventory(gold: 0, items: []),
|
||||||
|
equipment: Equipment.empty(),
|
||||||
|
spellBook: SpellBook.empty(),
|
||||||
|
progress: ProgressState.empty(),
|
||||||
|
queue: QueueState.empty(),
|
||||||
|
);
|
||||||
|
|
||||||
|
widget.onCharacterCreated?.call(initialState);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 종족/직업 보너스 파싱 (예: "Half Orc|STR+2,INT-1")
|
||||||
|
Map<String, int> _parseStatBonus(String entry) {
|
||||||
|
final parts = entry.split('|');
|
||||||
|
if (parts.length < 2) return {};
|
||||||
|
|
||||||
|
final bonuses = <String, int>{};
|
||||||
|
final bonusPart = parts[1];
|
||||||
|
|
||||||
|
// STR+2,INT-1 형식 파싱
|
||||||
|
final regex = RegExp(r'([A-Z]+)([+-]\d+)');
|
||||||
|
for (final match in regex.allMatches(bonusPart)) {
|
||||||
|
final stat = match.group(1)!;
|
||||||
|
final value = int.parse(match.group(2)!);
|
||||||
|
bonuses[stat] = value;
|
||||||
|
}
|
||||||
|
return bonuses;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text('Progress Quest - New Character'),
|
||||||
|
centerTitle: true,
|
||||||
|
),
|
||||||
|
body: SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
// 이름 입력 섹션
|
||||||
|
_buildNameSection(),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// 능력치 섹션
|
||||||
|
_buildStatsSection(),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// 종족/직업 선택 섹션
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Expanded(child: _buildRaceSection()),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Expanded(child: _buildKlassSection()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
|
// Sold! 버튼
|
||||||
|
FilledButton.icon(
|
||||||
|
onPressed: _onSold,
|
||||||
|
icon: const Icon(Icons.check),
|
||||||
|
label: const Text('Sold!'),
|
||||||
|
style: FilledButton.styleFrom(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildNameSection() {
|
||||||
|
return Card(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: TextField(
|
||||||
|
controller: _nameController,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Name',
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
maxLength: 30,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
IconButton.filled(
|
||||||
|
onPressed: _onGenerateName,
|
||||||
|
icon: const Icon(Icons.casino),
|
||||||
|
tooltip: 'Generate Name',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildStatsSection() {
|
||||||
|
return Card(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text('Stats', style: Theme.of(context).textTheme.titleMedium),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
|
// 스탯 그리드
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(child: _buildStatTile('STR', _str)),
|
||||||
|
Expanded(child: _buildStatTile('CON', _con)),
|
||||||
|
Expanded(child: _buildStatTile('DEX', _dex)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(child: _buildStatTile('INT', _int)),
|
||||||
|
Expanded(child: _buildStatTile('WIS', _wis)),
|
||||||
|
Expanded(child: _buildStatTile('CHA', _cha)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
|
// Total
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: _getTotalColor().withValues(alpha: 0.3),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
border: Border.all(color: _getTotalColor()),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
'Total',
|
||||||
|
style: TextStyle(fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'$_total',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: _getTotalColor() == Colors.white
|
||||||
|
? Colors.black
|
||||||
|
: _getTotalColor(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
|
// Roll 버튼들
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
OutlinedButton.icon(
|
||||||
|
onPressed: _onUnroll,
|
||||||
|
icon: const Icon(Icons.undo),
|
||||||
|
label: const Text('Unroll'),
|
||||||
|
style: OutlinedButton.styleFrom(
|
||||||
|
foregroundColor: _rollHistory.isEmpty ? Colors.grey : null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
FilledButton.icon(
|
||||||
|
onPressed: _onReroll,
|
||||||
|
icon: const Icon(Icons.casino),
|
||||||
|
label: const Text('Roll'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
if (_rollHistory.isNotEmpty)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 8),
|
||||||
|
child: Text(
|
||||||
|
'${_rollHistory.length} roll(s) in history',
|
||||||
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildStatTile(String label, int value) {
|
||||||
|
return Container(
|
||||||
|
margin: const EdgeInsets.all(4),
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Text(label, style: Theme.of(context).textTheme.labelSmall),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
'$value',
|
||||||
|
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildRaceSection() {
|
||||||
|
return Card(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text('Race', style: Theme.of(context).textTheme.titleMedium),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
SizedBox(
|
||||||
|
height: 300,
|
||||||
|
child: ListView.builder(
|
||||||
|
itemCount: _races.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final isSelected = index == _selectedRaceIndex;
|
||||||
|
return ListTile(
|
||||||
|
leading: Icon(
|
||||||
|
isSelected
|
||||||
|
? Icons.radio_button_checked
|
||||||
|
: Icons.radio_button_unchecked,
|
||||||
|
color: isSelected
|
||||||
|
? Theme.of(context).colorScheme.primary
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
title: Text(
|
||||||
|
_races[index],
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: isSelected
|
||||||
|
? FontWeight.bold
|
||||||
|
: FontWeight.normal,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
dense: true,
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
onTap: () => setState(() => _selectedRaceIndex = index),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildKlassSection() {
|
||||||
|
return Card(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text('Class', style: Theme.of(context).textTheme.titleMedium),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
SizedBox(
|
||||||
|
height: 300,
|
||||||
|
child: ListView.builder(
|
||||||
|
itemCount: _klasses.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final isSelected = index == _selectedKlassIndex;
|
||||||
|
return ListTile(
|
||||||
|
leading: Icon(
|
||||||
|
isSelected
|
||||||
|
? Icons.radio_button_checked
|
||||||
|
: Icons.radio_button_unchecked,
|
||||||
|
color: isSelected
|
||||||
|
? Theme.of(context).colorScheme.primary
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
title: Text(
|
||||||
|
_klasses[index],
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: isSelected
|
||||||
|
? FontWeight.bold
|
||||||
|
: FontWeight.normal,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
dense: true,
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
onTap: () => setState(() => _selectedKlassIndex = index),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
1
linux/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
flutter/ephemeral
|
||||||
128
linux/CMakeLists.txt
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
# Project-level configuration.
|
||||||
|
cmake_minimum_required(VERSION 3.13)
|
||||||
|
project(runner LANGUAGES CXX)
|
||||||
|
|
||||||
|
# The name of the executable created for the application. Change this to change
|
||||||
|
# the on-disk name of your application.
|
||||||
|
set(BINARY_NAME "askiineverdie")
|
||||||
|
# The unique GTK application identifier for this application. See:
|
||||||
|
# https://wiki.gnome.org/HowDoI/ChooseApplicationID
|
||||||
|
set(APPLICATION_ID "com.example.askiineverdie")
|
||||||
|
|
||||||
|
# Explicitly opt in to modern CMake behaviors to avoid warnings with recent
|
||||||
|
# versions of CMake.
|
||||||
|
cmake_policy(SET CMP0063 NEW)
|
||||||
|
|
||||||
|
# Load bundled libraries from the lib/ directory relative to the binary.
|
||||||
|
set(CMAKE_INSTALL_RPATH "$ORIGIN/lib")
|
||||||
|
|
||||||
|
# Root filesystem for cross-building.
|
||||||
|
if(FLUTTER_TARGET_PLATFORM_SYSROOT)
|
||||||
|
set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT})
|
||||||
|
set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT})
|
||||||
|
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
|
||||||
|
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
|
||||||
|
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
|
||||||
|
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# Define build configuration options.
|
||||||
|
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
|
||||||
|
set(CMAKE_BUILD_TYPE "Debug" CACHE
|
||||||
|
STRING "Flutter build mode" FORCE)
|
||||||
|
set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS
|
||||||
|
"Debug" "Profile" "Release")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# Compilation settings that should be applied to most targets.
|
||||||
|
#
|
||||||
|
# Be cautious about adding new options here, as plugins use this function by
|
||||||
|
# default. In most cases, you should add new options to specific targets instead
|
||||||
|
# of modifying this function.
|
||||||
|
function(APPLY_STANDARD_SETTINGS TARGET)
|
||||||
|
target_compile_features(${TARGET} PUBLIC cxx_std_14)
|
||||||
|
target_compile_options(${TARGET} PRIVATE -Wall -Werror)
|
||||||
|
target_compile_options(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:-O3>")
|
||||||
|
target_compile_definitions(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:NDEBUG>")
|
||||||
|
endfunction()
|
||||||
|
|
||||||
|
# Flutter library and tool build rules.
|
||||||
|
set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter")
|
||||||
|
add_subdirectory(${FLUTTER_MANAGED_DIR})
|
||||||
|
|
||||||
|
# System-level dependencies.
|
||||||
|
find_package(PkgConfig REQUIRED)
|
||||||
|
pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
|
||||||
|
|
||||||
|
# Application build; see runner/CMakeLists.txt.
|
||||||
|
add_subdirectory("runner")
|
||||||
|
|
||||||
|
# Run the Flutter tool portions of the build. This must not be removed.
|
||||||
|
add_dependencies(${BINARY_NAME} flutter_assemble)
|
||||||
|
|
||||||
|
# Only the install-generated bundle's copy of the executable will launch
|
||||||
|
# correctly, since the resources must in the right relative locations. To avoid
|
||||||
|
# people trying to run the unbundled copy, put it in a subdirectory instead of
|
||||||
|
# the default top-level location.
|
||||||
|
set_target_properties(${BINARY_NAME}
|
||||||
|
PROPERTIES
|
||||||
|
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# Generated plugin build rules, which manage building the plugins and adding
|
||||||
|
# them to the application.
|
||||||
|
include(flutter/generated_plugins.cmake)
|
||||||
|
|
||||||
|
|
||||||
|
# === Installation ===
|
||||||
|
# By default, "installing" just makes a relocatable bundle in the build
|
||||||
|
# directory.
|
||||||
|
set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle")
|
||||||
|
if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
|
||||||
|
set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# Start with a clean build bundle directory every time.
|
||||||
|
install(CODE "
|
||||||
|
file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\")
|
||||||
|
" COMPONENT Runtime)
|
||||||
|
|
||||||
|
set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data")
|
||||||
|
set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib")
|
||||||
|
|
||||||
|
install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}"
|
||||||
|
COMPONENT Runtime)
|
||||||
|
|
||||||
|
install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
|
||||||
|
COMPONENT Runtime)
|
||||||
|
|
||||||
|
install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
|
||||||
|
COMPONENT Runtime)
|
||||||
|
|
||||||
|
foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES})
|
||||||
|
install(FILES "${bundled_library}"
|
||||||
|
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
|
||||||
|
COMPONENT Runtime)
|
||||||
|
endforeach(bundled_library)
|
||||||
|
|
||||||
|
# Copy the native assets provided by the build.dart from all packages.
|
||||||
|
set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/linux/")
|
||||||
|
install(DIRECTORY "${NATIVE_ASSETS_DIR}"
|
||||||
|
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
|
||||||
|
COMPONENT Runtime)
|
||||||
|
|
||||||
|
# Fully re-copy the assets directory on each build to avoid having stale files
|
||||||
|
# from a previous install.
|
||||||
|
set(FLUTTER_ASSET_DIR_NAME "flutter_assets")
|
||||||
|
install(CODE "
|
||||||
|
file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\")
|
||||||
|
" COMPONENT Runtime)
|
||||||
|
install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}"
|
||||||
|
DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime)
|
||||||
|
|
||||||
|
# Install the AOT library on non-Debug builds only.
|
||||||
|
if(NOT CMAKE_BUILD_TYPE MATCHES "Debug")
|
||||||
|
install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
|
||||||
|
COMPONENT Runtime)
|
||||||
|
endif()
|
||||||
88
linux/flutter/CMakeLists.txt
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
# This file controls Flutter-level build steps. It should not be edited.
|
||||||
|
cmake_minimum_required(VERSION 3.10)
|
||||||
|
|
||||||
|
set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral")
|
||||||
|
|
||||||
|
# Configuration provided via flutter tool.
|
||||||
|
include(${EPHEMERAL_DIR}/generated_config.cmake)
|
||||||
|
|
||||||
|
# TODO: Move the rest of this into files in ephemeral. See
|
||||||
|
# https://github.com/flutter/flutter/issues/57146.
|
||||||
|
|
||||||
|
# Serves the same purpose as list(TRANSFORM ... PREPEND ...),
|
||||||
|
# which isn't available in 3.10.
|
||||||
|
function(list_prepend LIST_NAME PREFIX)
|
||||||
|
set(NEW_LIST "")
|
||||||
|
foreach(element ${${LIST_NAME}})
|
||||||
|
list(APPEND NEW_LIST "${PREFIX}${element}")
|
||||||
|
endforeach(element)
|
||||||
|
set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE)
|
||||||
|
endfunction()
|
||||||
|
|
||||||
|
# === Flutter Library ===
|
||||||
|
# System-level dependencies.
|
||||||
|
find_package(PkgConfig REQUIRED)
|
||||||
|
pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
|
||||||
|
pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0)
|
||||||
|
pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0)
|
||||||
|
|
||||||
|
set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so")
|
||||||
|
|
||||||
|
# Published to parent scope for install step.
|
||||||
|
set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE)
|
||||||
|
set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE)
|
||||||
|
set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE)
|
||||||
|
set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE)
|
||||||
|
|
||||||
|
list(APPEND FLUTTER_LIBRARY_HEADERS
|
||||||
|
"fl_basic_message_channel.h"
|
||||||
|
"fl_binary_codec.h"
|
||||||
|
"fl_binary_messenger.h"
|
||||||
|
"fl_dart_project.h"
|
||||||
|
"fl_engine.h"
|
||||||
|
"fl_json_message_codec.h"
|
||||||
|
"fl_json_method_codec.h"
|
||||||
|
"fl_message_codec.h"
|
||||||
|
"fl_method_call.h"
|
||||||
|
"fl_method_channel.h"
|
||||||
|
"fl_method_codec.h"
|
||||||
|
"fl_method_response.h"
|
||||||
|
"fl_plugin_registrar.h"
|
||||||
|
"fl_plugin_registry.h"
|
||||||
|
"fl_standard_message_codec.h"
|
||||||
|
"fl_standard_method_codec.h"
|
||||||
|
"fl_string_codec.h"
|
||||||
|
"fl_value.h"
|
||||||
|
"fl_view.h"
|
||||||
|
"flutter_linux.h"
|
||||||
|
)
|
||||||
|
list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/")
|
||||||
|
add_library(flutter INTERFACE)
|
||||||
|
target_include_directories(flutter INTERFACE
|
||||||
|
"${EPHEMERAL_DIR}"
|
||||||
|
)
|
||||||
|
target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}")
|
||||||
|
target_link_libraries(flutter INTERFACE
|
||||||
|
PkgConfig::GTK
|
||||||
|
PkgConfig::GLIB
|
||||||
|
PkgConfig::GIO
|
||||||
|
)
|
||||||
|
add_dependencies(flutter flutter_assemble)
|
||||||
|
|
||||||
|
# === Flutter tool backend ===
|
||||||
|
# _phony_ is a non-existent file to force this command to run every time,
|
||||||
|
# since currently there's no way to get a full input/output list from the
|
||||||
|
# flutter tool.
|
||||||
|
add_custom_command(
|
||||||
|
OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS}
|
||||||
|
${CMAKE_CURRENT_BINARY_DIR}/_phony_
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E env
|
||||||
|
${FLUTTER_TOOL_ENVIRONMENT}
|
||||||
|
"${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh"
|
||||||
|
${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE}
|
||||||
|
VERBATIM
|
||||||
|
)
|
||||||
|
add_custom_target(flutter_assemble DEPENDS
|
||||||
|
"${FLUTTER_LIBRARY}"
|
||||||
|
${FLUTTER_LIBRARY_HEADERS}
|
||||||
|
)
|
||||||
11
linux/flutter/generated_plugin_registrant.cc
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
//
|
||||||
|
// Generated file. Do not edit.
|
||||||
|
//
|
||||||
|
|
||||||
|
// clang-format off
|
||||||
|
|
||||||
|
#include "generated_plugin_registrant.h"
|
||||||
|
|
||||||
|
|
||||||
|
void fl_register_plugins(FlPluginRegistry* registry) {
|
||||||
|
}
|
||||||
15
linux/flutter/generated_plugin_registrant.h
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
//
|
||||||
|
// Generated file. Do not edit.
|
||||||
|
//
|
||||||
|
|
||||||
|
// clang-format off
|
||||||
|
|
||||||
|
#ifndef GENERATED_PLUGIN_REGISTRANT_
|
||||||
|
#define GENERATED_PLUGIN_REGISTRANT_
|
||||||
|
|
||||||
|
#include <flutter_linux/flutter_linux.h>
|
||||||
|
|
||||||
|
// Registers Flutter plugins.
|
||||||
|
void fl_register_plugins(FlPluginRegistry* registry);
|
||||||
|
|
||||||
|
#endif // GENERATED_PLUGIN_REGISTRANT_
|
||||||
23
linux/flutter/generated_plugins.cmake
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
#
|
||||||
|
# Generated file, do not edit.
|
||||||
|
#
|
||||||
|
|
||||||
|
list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
|
)
|
||||||
|
|
||||||
|
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||||
|
)
|
||||||
|
|
||||||
|
set(PLUGIN_BUNDLED_LIBRARIES)
|
||||||
|
|
||||||
|
foreach(plugin ${FLUTTER_PLUGIN_LIST})
|
||||||
|
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin})
|
||||||
|
target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin)
|
||||||
|
list(APPEND PLUGIN_BUNDLED_LIBRARIES $<TARGET_FILE:${plugin}_plugin>)
|
||||||
|
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})
|
||||||
|
endforeach(plugin)
|
||||||
|
|
||||||
|
foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST})
|
||||||
|
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin})
|
||||||
|
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries})
|
||||||
|
endforeach(ffi_plugin)
|
||||||
26
linux/runner/CMakeLists.txt
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.13)
|
||||||
|
project(runner LANGUAGES CXX)
|
||||||
|
|
||||||
|
# Define the application target. To change its name, change BINARY_NAME in the
|
||||||
|
# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer
|
||||||
|
# work.
|
||||||
|
#
|
||||||
|
# Any new source files that you add to the application should be added here.
|
||||||
|
add_executable(${BINARY_NAME}
|
||||||
|
"main.cc"
|
||||||
|
"my_application.cc"
|
||||||
|
"${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Apply the standard set of build settings. This can be removed for applications
|
||||||
|
# that need different build settings.
|
||||||
|
apply_standard_settings(${BINARY_NAME})
|
||||||
|
|
||||||
|
# Add preprocessor definitions for the application ID.
|
||||||
|
add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}")
|
||||||
|
|
||||||
|
# Add dependency libraries. Add any application-specific dependencies here.
|
||||||
|
target_link_libraries(${BINARY_NAME} PRIVATE flutter)
|
||||||
|
target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK)
|
||||||
|
|
||||||
|
target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}")
|
||||||