feat: 초기 커밋

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

46
.gitignore vendored Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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

View 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 = "../.."
}

View 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>

View 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>

View File

@@ -0,0 +1,5 @@
package com.example.askiineverdie
import io.flutter.embedding.android.FlutterActivity
class MainActivity : FlutterActivity()

View 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>

View 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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View 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>

View 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>

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

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,3 @@
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true
android.enableJetifier=true

View 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

View 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
View File

19
doc/dfm-extract-notes.md Normal file
View 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.

View 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. 멀티플랫폼 빌드 검증(모바일/데스크톱/웹) 및 최종 회귀 테스트

View 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

Submodule example/pq added at 342a0ef59f

34
ios/.gitignore vendored Normal file
View 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

View 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>

View File

@@ -0,0 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
#include "Generated.xcconfig"

View File

@@ -0,0 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
#include "Generated.xcconfig"

43
ios/Podfile Normal file
View 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

View 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 */;
}

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>

View File

@@ -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>

View File

@@ -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>

View 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>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
</Workspace>

View File

@@ -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>

View File

@@ -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>

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

View 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"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 450 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 462 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 704 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 586 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 762 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View 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"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

View 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.

View 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>

View 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
View 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>

View File

@@ -0,0 +1 @@
#import "GeneratedPluginRegistrant.h"

View 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.
}
}

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

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

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

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

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

View File

@@ -0,0 +1 @@
enum EquipmentSlot { weapon, shield, armor }

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

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

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

View File

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

View File

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

View File

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

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

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

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

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

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

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

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

View 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
View File

@@ -0,0 +1 @@
flutter/ephemeral

128
linux/CMakeLists.txt Normal file
View 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()

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

View File

@@ -0,0 +1,11 @@
//
// Generated file. Do not edit.
//
// clang-format off
#include "generated_plugin_registrant.h"
void fl_register_plugins(FlPluginRegistry* registry) {
}

View 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_

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

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

Some files were not shown because too many files have changed in this diff Show More