refactor: 使用flutter对原先的web项目重构,正在进行中

This commit is contained in:
cancerai committed 2026-10-25 00:06:51 +08:00
1 parent 273224f5cf
commit 18017f819a
205 files changed
+13563

No files matched your search

+45
View File
@@ -0,0 +1,45 @@
# Miscellaneous
*.class
*.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-dependencies
.pub-cache/
.pub/
/build/
/coverage/
# 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
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: "c9a6c484230f8b5e408ec57be1ef71dee1e77020"
channel: "stable"
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: c9a6c484230f8b5e408ec57be1ef71dee1e77020
base_revision: c9a6c484230f8b5e408ec57be1ef71dee1e77020
- platform: android
create_revision: c9a6c484230f8b5e408ec57be1ef71dee1e77020
base_revision: c9a6c484230f8b5e408ec57be1ef71dee1e77020
- platform: ios
create_revision: c9a6c484230f8b5e408ec57be1ef71dee1e77020
base_revision: c9a6c484230f8b5e408ec57be1ef71dee1e77020
- platform: linux
create_revision: c9a6c484230f8b5e408ec57be1ef71dee1e77020
base_revision: c9a6c484230f8b5e408ec57be1ef71dee1e77020
- platform: macos
create_revision: c9a6c484230f8b5e408ec57be1ef71dee1e77020
base_revision: c9a6c484230f8b5e408ec57be1ef71dee1e77020
- platform: web
create_revision: c9a6c484230f8b5e408ec57be1ef71dee1e77020
base_revision: c9a6c484230f8b5e408ec57be1ef71dee1e77020
- platform: windows
create_revision: c9a6c484230f8b5e408ec57be1ef71dee1e77020
base_revision: c9a6c484230f8b5e408ec57be1ef71dee1e77020
# 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'
+17
View File
@@ -0,0 +1,17 @@
# iwbtc
A new Flutter project.
## Getting Started
This project is a starting point for a Flutter application.
A few resources to get you started if this is your first Flutter project:
- [Learn Flutter](https://docs.flutter.dev/get-started/learn-flutter)
- [Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
- [Flutter learning resources](https://docs.flutter.dev/reference/learning-resources)
For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev/), which offers tutorials,
samples, guidance on mobile development, and a full API reference.
+28
View File
@@ -0,0 +1,28 @@
# 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
linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at https://dart.dev/lints.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options
+14
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
+45
View File
@@ -0,0 +1,45 @@
plugins {
id("com.android.application")
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id("dev.flutter.flutter-gradle-plugin")
}
android {
namespace = "com.iwbtc.iwbtc"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.iwbtc.iwbtc"
// 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")
}
}
}
kotlin {
compilerOptions {
jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17
}
}
flutter {
source = "../.."
}
@@ -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>
@@ -0,0 +1,51 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:label="iwbtc"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<!-- 屏幕方向:游戏画面是 800×608 的横向舞台,竖屏下信箱缩放后画面极小、
虚拟手柄也会挤在一起,故安卓端锁定横屏。
sensorLandscape:左横/右横都允许,跟随重力(只保留一个方向就写 landscape)。
Dart 侧 SystemChrome 也会再锁一次,两者共同保证冷启动首帧即为横屏。 -->
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:screenOrientation="sensorLandscape"
android:resizeableActivity="false"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
</queries>
</manifest>
@@ -0,0 +1,5 @@
package com.iwbtc.iwbtc
import io.flutter.embedding.android.FlutterActivity
class MainActivity : FlutterActivity()
@@ -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>
@@ -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

@@ -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>
@@ -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>
@@ -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
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)
}
+6
View File
@@ -0,0 +1,6 @@
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true
# This newDsl flag was added by the Flutter template
android.newDsl=false
# This builtInKotlin flag was added by the Flutter template
android.builtInKotlin=false
+5
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-9.1.0-all.zip
+26
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 "9.0.1" apply false
id("org.jetbrains.kotlin.android") version "2.3.20" apply false
}
include(":app")
File diff suppressed because it is too large. Load diff
+34
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
+24
View File
@@ -0,0 +1,24 @@
<?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>
</dict>
</plist>
+1
View File
@@ -0,0 +1 @@
#include "Generated.xcconfig"
+1
View File
@@ -0,0 +1 @@
#include "Generated.xcconfig"
+644
View File
@@ -0,0 +1,644 @@
// !$*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 */; };
7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */; };
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; };
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>"; };
7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; 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 = (
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */,
);
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 = (
78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */,
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 */,
7884E8672EC3CC0400C636F2 /* SceneDelegate.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;
packageProductDependencies = (
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */,
);
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;
packageReferences = (
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */,
);
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 */,
7884E8682EC3CC0700C636F2 /* SceneDelegate.swift 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.iwbtc.iwbtc;
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.iwbtc.iwbtc.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.iwbtc.iwbtc.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.iwbtc.iwbtc.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.iwbtc.iwbtc;
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.iwbtc.iwbtc;
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 */
/* Begin XCLocalSwiftPackageReference section */
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = {
isa = XCLocalSwiftPackageReference;
relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage;
};
/* End XCLocalSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = {
isa = XCSwiftPackageProductDependency;
productName = FlutterGeneratedPluginSwiftPackage;
};
/* End XCSwiftPackageProductDependency section */
};
rootObject = 97C146E61CF9000F007C117D /* Project object */;
}
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>
@@ -0,0 +1,119 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1510"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<PreActions>
<ExecutionAction
ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction">
<ActionContent
title = "Run Prepare Flutter Framework Script"
scriptText = "/bin/sh &quot;$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh&quot; prepare&#10;">
<EnvironmentBuildable>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</EnvironmentBuildable>
</ActionContent>
</ExecutionAction>
</PreActions>
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
shouldUseLaunchSchemeArgsEnv = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</MacroExpansion>
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "331C8080294A63A400263BE5"
BuildableName = "RunnerTests.xctest"
BlueprintName = "RunnerTests"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
enableGPUValidationMode = "1"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Profile"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
+7
View File
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
</Workspace>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>
+16
View File
@@ -0,0 +1,16 @@
import Flutter
import UIKit
@main
@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
}
}
@@ -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

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

@@ -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.
@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16G29" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="Ydg-fD-yQy"/>
<viewControllerLayoutGuide type="bottom" id="xbc-2k-c8Z"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
</imageView>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
</constraints>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
</scene>
</scenes>
<resources>
<image name="LaunchImage" width="168" height="185"/>
</resources>
</document>
+26
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>
+70
View File
@@ -0,0 +1,70 @@
<?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>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Iwbtc</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>iwbtc</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>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<false/>
<key>UISceneConfigurations</key>
<dict>
<key>UIWindowSceneSessionRoleApplication</key>
<array>
<dict>
<key>UISceneClassName</key>
<string>UIWindowScene</string>
<key>UISceneConfigurationName</key>
<string>flutter</string>
<key>UISceneDelegateClassName</key>
<string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
<key>UISceneStoryboardFile</key>
<string>Main</string>
</dict>
</array>
</dict>
</dict>
<key>UIApplicationSupportsIndirectInputEvents</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>
</dict>
</plist>
+1
View File
@@ -0,0 +1 @@
#import "GeneratedPluginRegistrant.h"
+6
View File
@@ -0,0 +1,6 @@
import Flutter
import UIKit
class SceneDelegate: FlutterSceneDelegate {
}
+12
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.
}
}
+57
View File
@@ -0,0 +1,57 @@
// 入口:安装全局错误钩子后用标准 runApp 启动。
// 关键点:release 包里框架/build 抛出的异常不会被自动展示,只会留黑屏;
// 这里统一接管 FlutterError.onError、Zone 未捕获异常与 ErrorWidget.builder。
// ensureInitialized 与 runApp 都放进同一个 zone,避免触发 debugCheckZone 的
// "Zone mismatch"。runApp 内部会做 wrapWithDefaultView,注入 View 祖先。
import 'dart:async' show runZonedGuarded;
import 'package:flutter/foundation.dart'
show FlutterErrorDetails, defaultTargetPlatform, TargetPlatform;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart'
show DeviceOrientation, SystemChrome;
import 'package:iwbtc/src/core/app_error.dart';
import 'package:iwbtc/src/app_root.dart' show AppRoot;
Future<void> main() async {
runZonedGuarded(
() async {
WidgetsFlutterBinding.ensureInitialized();
// 框架级错误(build / layout / paint / 手势回调等)。
FlutterError.onError = (FlutterErrorDetails d) {
FlutterError.presentError(d); // 保持控制台/日志里的完整输出
AppError.reportFlutter(d);
};
// Widget 抛错时的占位界面 —— 换成可读文案,而不是默认的红屏/黑屏。
ErrorWidget.builder = (FlutterErrorDetails d) =>
AppError.buildScreen(d.exceptionAsString());
await _configureMobile();
runApp(const AppRoot());
},
(Object e, StackTrace st) {
AppError.reportZone(e, st);
},
);
}
/// 移动端专属的显示适配:锁定横屏。
///
/// 画面是 800×608 的横向虚拟舞台,横屏下信箱缩放才能拿到可用的可视比例;
/// 竖屏时上下留白巨大、画面极小,虚拟手柄也会挤成一团。桌面端保持原样不干预。
/// (安卓另有 AndroidManifest 的 `screenOrientation="sensorLandscape"`,
/// 保证冷启动第一帧就是横屏,避免先竖后横的闪烁。)
Future<void> _configureMobile() async {
final t = defaultTargetPlatform;
if (t != TargetPlatform.android && t != TargetPlatform.iOS) return;
try {
await SystemChrome.setPreferredOrientations(<DeviceOrientation>[
DeviceOrientation.landscapeLeft,
DeviceOrientation.landscapeRight,
]);
} catch (e) {
// 某些设备/系统策略不允许锁定方向,属可容忍情况,不影响游戏运行。
AppError.warn('锁定横屏失败(保持系统默认方向)', '$e');
}
}
+319
View File
@@ -0,0 +1,319 @@
// 应用根:实现 GameHost,拥有 World,驱动 Ticker 循环,处理键盘,组装界面栈。
import 'dart:ui' show Color;
import 'dart:async' show Timer;
import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart'
show debugPrint, defaultTargetPlatform, TargetPlatform;
import 'package:flutter/scheduler.dart' show Ticker;
import 'package:flutter/services.dart'
show RawKeyboard, RawKeyEvent, RawKeyDownEvent;
import 'package:iwbtc/src/core/app_error.dart' show AppError;
import 'package:iwbtc/src/core/save.dart';
import 'package:iwbtc/src/core/input.dart' show Input;
import 'package:iwbtc/src/game/levels.dart' show LEVELS, loadLevels;
import 'package:iwbtc/src/game/world.dart' show World, GameHost;
import 'package:iwbtc/src/audio/sfx.dart' show Sfx;
import 'package:iwbtc/src/ui/game_canvas.dart' show GameCanvas;
import 'package:iwbtc/src/ui/overlays.dart' show HudOverlay;
import 'package:iwbtc/src/ui/screens.dart'
show TitleScreen, LevelSelectScreen, PauseScreen, ResultScreen, SettingsScreen;
import 'package:iwbtc/src/ui/touch_controls.dart' show TouchControls;
import 'package:iwbtc/src/ui/editor_panel.dart' show EditorPanel, EditorController;
class AppRoot extends StatefulWidget {
const AppRoot({super.key});
@override
State<AppRoot> createState() => _AppRootState();
}
class _AppRootState extends State<AppRoot>
with SingleTickerProviderStateMixin
implements GameHost {
late World world;
late EditorController editor;
bool _ready = false;
bool _worldOk = false;
bool _useTouch = false;
bool _audioUnlocked = false;
final ValueNotifier<String> screen = ValueNotifier<String>('scr-title');
final ValueNotifier<Map<String, dynamic>> hud =
ValueNotifier<Map<String, dynamic>>(<String, dynamic>{});
final ValueNotifier<dynamic> boss = ValueNotifier<dynamic>(null);
final ValueNotifier<String> toast = ValueNotifier<String>('');
final ValueNotifier<Map<String, dynamic>> result =
ValueNotifier<Map<String, dynamic>>(<String, dynamic>{});
final ValueNotifier<bool> touchVisible = ValueNotifier<bool>(false);
final ValueNotifier<bool> touchGun = ValueNotifier<bool>(false);
final ValueNotifier<String> pauseQuit = ValueNotifier<String>('返回关卡选择');
final ValueNotifier<int> levelsReload = ValueNotifier<int>(0);
final ValueNotifier<int> frame = ValueNotifier<int>(0);
late Ticker _ticker;
int _lastMs = 0;
Timer? _watchdog;
bool _loggedTick = false;
bool _loggedBuild = false;
@override
void initState() {
super.initState();
_useTouch = defaultTargetPlatform == TargetPlatform.android;
// 看门狗:初始化迟迟不完成时报出可诊断信息,避免永远停在加载态。
_watchdog = Timer(const Duration(seconds: 12), () {
if (mounted && !_ready) {
AppError.report(
'初始化超时(12 秒未完成)',
'ready=$_ready worldOk=$_worldOk\n'
'请用 adb logcat 查看 DEBUG 输出中的 [AppError] / [AppWarn] 行。',
);
}
});
_init();
}
/// 分阶段初始化:任何一环失败都记录错误并尽力继续,
/// 避免一处异常导致 _ready 永远为 false —— 那正是"永久黑屏"的成因。
Future<void> _init() async {
debugPrint('[boot] init start');
// 1) 存档读取失败:退化为内存存档,游戏仍可玩(仅记日志)。
try {
await Save.load();
debugPrint('[boot] save loaded ok');
} catch (e, s) {
AppError.warn('存档读取失败(已退化为内存存档)', '$e\n$s');
}
// 2) 内置关卡加载失败:退化为空清单,不影响标题与其他界面(仅记日志)。
try {
await loadLevels();
debugPrint('[boot] levels loaded: ${LEVELS.length}');
} catch (e, s) {
AppError.warn('内置关卡加载失败(levels.json 未能读取)', '$e\n$s');
}
// 3) 世界与输入装配:失败则无法继续,交给错误界面展示。
try {
Sfx.enabled = Save.data['audio'] != false;
world = World(this);
world.useTouch = _useTouch;
editor = EditorController();
editor.runner = world;
RawKeyboard.instance.addListener(_rawKey);
_ticker = createTicker(_onTick);
_ticker.start();
_worldOk = true;
debugPrint('[boot] world assembled');
} catch (e, s) {
AppError.report('游戏初始化失败', '$e\n$s');
}
if (mounted) setState(() => _ready = true);
debugPrint('[boot] init done: ready=$_ready worldOk=$_worldOk');
}
void _onTick(Duration elapsed) {
if (!_loggedTick) {
_loggedTick = true;
debugPrint('[boot] first tick fired (game loop running)');
}
final dt = elapsed.inMilliseconds - _lastMs;
_lastMs = elapsed.inMilliseconds;
world.tick(dt);
frame.value++;
}
void _rawKey(RawKeyEvent e) {
if (world.state != 'playing' && world.state != 'paused') return;
if (!_audioUnlocked) {
_audioUnlocked = true;
Sfx.instance.unlock();
}
final code = e.logicalKey.debugName?.replaceAll(' ', '') ?? '';
Input.onKey(code, e is RawKeyDownEvent);
}
@override
void dispose() {
_watchdog?.cancel();
RawKeyboard.instance.removeListener(_rawKey);
_ticker.dispose();
super.dispose();
}
// ---------------- GameHost ----------------
@override
void onShowScreen(String name) {
screen.value = name;
if (name == 'scr-editor' ||
name == 'scr-title' ||
name == 'scr-levels' ||
name == 'scr-settings') {
touchVisible.value = false;
}
}
@override
void onHud(Map<String, dynamic> h) => hud.value = h;
@override
void onBossBar(dynamic b) => boss.value = b;
@override
void onToast(String t) => toast.value = t;
@override
void onResult(Map<String, dynamic> r) => result.value = r;
@override
void onPauseMenu(String label) => pauseQuit.value = label;
@override
void editorShow() => editor.showOrOpen();
@override
void touchShow() => touchVisible.value = true;
@override
void touchHide() {
touchVisible.value = false;
Input.clearTouch();
}
@override
void touchSetGun(bool has) => touchGun.value = has;
@override
Widget build(BuildContext context) {
return ValueListenableBuilder<String?>(
valueListenable: AppError.current,
builder: (_, String? err, __) {
// 致命错误优先展示详情 —— release 包里原本只会留下一片黑屏。
if (err != null) {
return MaterialApp(home: Scaffold(body: AppError.buildScreen(err)));
}
// 尚未就绪:显示可见的加载界面(不再是纯黑背景)。
if (!_ready || !_worldOk) return const MaterialApp(home: _BootScreen());
return _buildGame();
},
);
}
/// 真正的游戏界面栈(画布 + 各屏幕 + HUD + 触屏手柄)。
Widget _buildGame() {
if (!_loggedBuild) {
_loggedBuild = true;
debugPrint(
'[boot] buildGame: state=${world.state} screen=${screen.value}');
}
return MaterialApp(
home: Scaffold(
backgroundColor: const Color(0xff000000),
body: Stack(
children: <Widget>[
Positioned.fill(
child: GameCanvas(world: world, repaint: frame),
),
// 注意:Positioned 必须是 Stack 的直接子级。此前把 Positioned 放在
// ValueListenableBuilder 的返回值里(隔着一层),定位数据被丢弃,
// 导致整棵标题树横向塌缩为 0 宽 —— 界面上只剩近黑的画布,即"黑屏"。
Positioned.fill(
child: ValueListenableBuilder<String>(
valueListenable: screen,
builder: (_, String s, __) {
if (s == 'scr-title') {
return TitleScreen(
world: world,
onSettings: () => screen.value = 'scr-settings',
onEditor: () => world.toEditor(),
);
}
if (s == 'scr-levels') {
return LevelSelectScreen(
world: world,
reload: levelsReload,
onChanged: () => levelsReload.value++,
);
}
if (s == 'scr-settings') {
return SettingsScreen(
world: world,
isTouch: _useTouch,
onBack: () => world.toTitle(),
);
}
if (s == 'scr-editor') {
return EditorPanel(
controller: editor,
onBack: () => world.toTitle(),
);
}
return const SizedBox();
},
),
),
// 暂停 / 结算(半透明,覆盖在画布之上)
Positioned.fill(
child: ValueListenableBuilder<String>(
valueListenable: screen,
builder: (_, String s, __) {
if (s == 'scr-pause') {
return PauseScreen(world: world, quitLabel: pauseQuit.value);
}
if (s == 'scr-result') {
return ResultScreen(world: world, result: result.value);
}
return const SizedBox();
},
),
),
// HUD:显隐必须随 world.state 变化而重建。
// 早先把它写成 build() 里读 world.state 的 if 分支 —— 但 startLevel()
// 只改 World 字段、不会触发 AppRoot 重建,于是进关卡后 HUD 永远不显示。
// 这里改为监听 stateN。(Positioned 仍保持 Stack 直接子级,监听在内。)
Positioned.fill(
child: ValueListenableBuilder<String>(
valueListenable: world.stateN,
builder: (_, String s, __) =>
(s == 'playing' || s == 'paused')
? HudOverlay(hud: hud, boss: boss, toast: toast)
: const SizedBox(),
),
),
if (_useTouch)
Positioned.fill(
child: TouchControls(
world: world,
visible: touchVisible,
hasGun: touchGun,
),
),
],
),
),
);
}
}
/// 启动加载界面。取代原先近黑的空 Scaffold,
/// 这样即使初始化较慢或出错,用户也能看到明确反馈而不是"黑屏"。
class _BootScreen extends StatelessWidget {
const _BootScreen();
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xff08080f),
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: const <Widget>[
CircularProgressIndicator(color: Color(0xffff5c7a)),
SizedBox(height: 18),
Text(
'正在加载关卡…',
style: TextStyle(color: Color(0xffc8c8d8), fontSize: 14),
),
],
),
),
);
}
}
+278
View File
@@ -0,0 +1,278 @@
// 纯合成音效(对应 web 版 audio.js 的 WebAudio 合成),无外部资源。
// 在 Dart 侧生成 16-bit PCM WAV 字节,通过 audioplayers 播放(自带播放池以支持重叠)。
import 'dart:math';
import 'dart:typed_data';
import 'package:audioplayers/audioplayers.dart';
class _Voice {
final Float32List samples; // 已含包络、相对自身 0 起点
final int delaySamples;
_Voice(this.samples, this.delaySamples);
}
class Sfx {
static final Sfx instance = Sfx._();
factory Sfx() => instance;
Sfx._();
static bool enabled = true;
static double volume = 0.5;
/// 音频不可用时永久静默。
///
/// 音效纯属装饰,但 audioplayers 的异常会在**别的 Zone 上下文**里冒出来,
/// 现有的 try/catch 抓不到;一旦冒泡到全局错误钩子,整个界面会被切换成错误详情。
/// 因此只要音频子系统出过一次错,就彻底关掉,保证「音频坏掉 ≠ 游戏坏掉」。
static bool broken = false;
final int rate = 44100;
final List<AudioPlayer> _pool = [];
final Map<String, Uint8List> _cache = {};
bool _init = false;
void init() {
if (_init || broken) return;
_init = true;
try {
for (int i = 0; i < 12; i++) {
final p = AudioPlayer();
p.setVolume(volume);
_pool.add(p);
}
} catch (_) {
broken = true;
}
}
void setVolume(double v) {
volume = v;
for (final p in _pool) p.setVolume(v);
}
/// 首次用户手势时解锁音频(移动端要求用户手势后才能播放)。
/// 音效被关闭时不创建播放器,省资源也避免插件噪声。
void unlock() {
if (!enabled || broken) return;
init();
}
Uint8List _render(List<_Voice> voices) {
int maxLen = 0;
for (final v in voices) {
maxLen = max(maxLen, v.delaySamples + v.samples.length);
}
final mix = Float32List(maxLen);
for (final v in voices) {
for (int i = 0; i < v.samples.length; i++) {
mix[v.delaySamples + i] += v.samples[i];
}
}
// 限幅
for (int i = 0; i < mix.length; i++) {
if (mix[i] > 1) mix[i] = 1;
if (mix[i] < -1) mix[i] = -1;
}
return _encodeWav(mix);
}
Float32List _genTone({required double f, double f2 = 0, required double d, String type = 'square', double vol = 0.25, double delay = 0, double decay = 0}) {
final total = d + decay + 0.02;
final n = (total * rate).ceil();
final out = Float32List(n);
final t0 = (delay * rate).round();
final len = (d * rate).round();
final end = ((d + decay) * rate).round();
final k = decay > 0 ? -log(0.0001) / max(1e-4, (d + decay - 0.008)) : 0.0;
double phase = 0;
for (int i = 0; i < n; i++) {
final t = i / rate;
final fr = f2 > 0 ? f * pow(f2 / f, t / max(1e-6, d)) : f;
phase += 2 * pi * fr / rate;
if (phase > 1e9) phase = phase % (2 * pi);
double e;
if (t < 0.008) {
e = t / 0.008;
} else {
e = exp(-k * (t - 0.008));
}
final w = _wave(type, phase);
out[i] = w * vol * e;
}
return out;
}
Float32List _genNoise({required double d, double vol = 0.25, double delay = 0, double hp = 400}) {
final total = d + 0.02;
final n = (total * rate).ceil();
final out = Float32List(n);
final t0 = (delay * rate).round();
final end = (d * rate).round();
final a = hp / (hp + rate);
double hpState = 0, prevX = 0, prevOut = 0;
for (int i = 0; i < n; i++) {
final t = i / rate;
final x = (Random().nextDouble() * 2 - 1) * (1 - i / max(1, n));
final e = (i < end) ? 1.0 : 0.0;
hpState = 0.99 * hpState + x - prevX;
prevX = x;
prevOut = a * (prevOut + hpState);
out[i] = prevOut * vol * e;
}
return out;
}
static double _wave(String type, double phase) {
switch (type) {
case 'sine':
return sin(phase);
case 'triangle':
final f = (phase / (2 * pi)) % 1;
return 4 * (f - 0.5).abs() - 1;
case 'sawtooth':
final f = (phase / (2 * pi)) % 1;
return 2 * f - 1;
case 'square':
default:
return sin(phase) >= 0 ? 1 : -1;
}
}
Uint8List _encodeWav(Float32List samples) {
final n = samples.length;
final bytes = ByteData(44 + n * 2);
final bc = bytes.buffer.asUint8List();
// RIFF
bc.setRange(0, 4, [0x52, 0x49, 0x46, 0x46]); // RIFF
bytes.setUint32(4, 36 + n * 2, Endian.little);
bc.setRange(8, 12, [0x57, 0x41, 0x56, 0x45]); // WAVE
bc.setRange(12, 16, [0x66, 0x6d, 0x74, 0x20]); // fmt
bytes.setUint32(16, 16, Endian.little);
bytes.setUint16(20, 1, Endian.little); // PCM
bytes.setUint16(22, 1, Endian.little); // mono
bytes.setUint32(24, rate, Endian.little);
bytes.setUint32(28, rate * 2, Endian.little);
bytes.setUint16(32, 2, Endian.little);
bytes.setUint16(34, 16, Endian.little);
bc.setRange(36, 40, [0x64, 0x61, 0x74, 0x61]); // data
bytes.setUint32(40, n * 2, Endian.little);
for (int i = 0; i < n; i++) {
int s = (samples[i] * 32767).round().clamp(-32768, 32767);
bytes.setInt16(44 + i * 2, s, Endian.little);
}
return bc;
}
Uint8List _build(String name) {
late List<_Voice> voices;
switch (name) {
case 'jump':
voices = [_v(_genTone(f: 380, f2: 700, d: 0.09, type: 'square', vol: 0.16))];
break;
case 'djump':
voices = [_v(_genTone(f: 520, f2: 900, d: 0.09, type: 'square', vol: 0.14))];
break;
case 'shoot':
voices = [
_v(_genTone(f: 900, f2: 260, d: 0.07, type: 'square', vol: 0.14)),
_v(_genNoise(d: 0.05, vol: 0.06, hp: 2000), 0),
];
break;
case 'death':
voices = [
_v(_genTone(f: 620, f2: 60, d: 0.55, type: 'sawtooth', vol: 0.26)),
_v(_genNoise(d: 0.35, vol: 0.16, hp: 300), (0.02 * rate).round()),
];
break;
case 'save':
voices = [
_v(_genTone(f: 700, d: 0.08, type: 'triangle', vol: 0.2)),
_v(_genTone(f: 1050, d: 0.13, type: 'triangle', vol: 0.18), (0.07 * rate).round()),
];
break;
case 'goal':
voices = [
for (int i = 0; i < 4; i++)
_v(_genTone(f: [523.0, 659.0, 784.0, 1047.0][i], d: 0.16, type: 'triangle', vol: 0.2), (i * 0.1 * rate).round()),
];
break;
case 'warp':
voices = [_v(_genTone(f: 300, f2: 1400, d: 0.2, type: 'sine', vol: 0.18))];
break;
case 'hit':
voices = [_v(_genTone(f: 200, f2: 90, d: 0.08, type: 'square', vol: 0.16))];
break;
case 'bossHit':
voices = [_v(_genTone(f: 260, f2: 140, d: 0.1, type: 'sawtooth', vol: 0.14))];
break;
case 'bossDie':
voices = [
for (int i = 0; i < 5; i++)
_v(_genTone(f: [400.0, 330.0, 260.0, 180.0, 110.0][i], d: 0.22, type: 'sawtooth', vol: 0.22), (i * 0.11 * rate).round()),
_v(_genNoise(d: 0.8, vol: 0.2, hp: 150), (0.1 * rate).round()),
];
break;
case 'menu':
voices = [_v(_genTone(f: 620, d: 0.05, type: 'square', vol: 0.12))];
break;
case 'vanish':
voices = [_v(_genTone(f: 700, f2: 300, d: 0.12, type: 'sine', vol: 0.1))];
break;
case 'troll':
voices = [_v(_genTone(f: 180, f2: 900, d: 0.3, type: 'sine', vol: 0.18))];
break;
case 'splash':
voices = [_v(_genNoise(d: 0.18, vol: 0.12, hp: 800))];
break;
default:
voices = [_v(_genTone(f: 440, d: 0.05, type: 'square', vol: 0.1))];
}
return _render(voices);
}
_Voice _v(Float32List s, [int delaySamples = 0]) => _Voice(s, delaySamples);
void _play(String name) {
if (!enabled || broken) return;
try {
init();
final bytes = _cache.putIfAbsent(name, () => _build(name));
// 找一个空闲播放器
AudioPlayer? free;
for (final p in _pool) {
if (p.state == PlayerState.stopped || p.state == PlayerState.completed) {
free = p;
break;
}
}
free ??= _pool[_lastIdx = (_lastIdx + 1) % _pool.length];
free.stop().catchError((Object _) {
broken = true;
});
free
.play(BytesSource(bytes, mimeType: 'audio/wav'))
.catchError((Object _) {
broken = true;
});
} catch (_) {
// 任何失败都只静音,不影响玩法。
broken = true;
}
}
int _lastIdx = 0;
void jump() => _play('jump');
void djump() => _play('djump');
void shoot() => _play('shoot');
void death() => _play('death');
void save() => _play('save');
void goal() => _play('goal');
void warp() => _play('warp');
void hit() => _play('hit');
void bossHit() => _play('bossHit');
void bossDie() => _play('bossDie');
void menu() => _play('menu');
void vanish() => _play('vanish');
void troll() => _play('troll');
void splash() => _play('splash');
}
+110
View File
@@ -0,0 +1,110 @@
// 全局错误捕获与展示。
// 目的:release 包里任何未捕获异常都会被静默吞掉,只留下一片黑屏。
// 这里把所有异常统一收拢到一个 ValueNotifier,并由 AppRoot 优先渲染成
// 可读的错误详情界面,方便在真机上直接看到堆栈(配合 adb logcat 更好)。
import 'package:flutter/foundation.dart'
show debugPrint, FlutterErrorDetails, ValueNotifier;
import 'package:flutter/widgets.dart';
class AppError {
AppError._();
/// 首个被捕获的错误详情;非空时整个应用改为显示错误界面。
static final ValueNotifier<String?> current = ValueNotifier<String?>(null);
static bool get hasError => current.value != null;
static void _log(String tag, String head, String body) {
final trimmed = body.trim();
final text = trimmed.isEmpty ? head : '$head\n\n$trimmed';
debugPrint('[$tag] $text');
}
/// 记录致命错误(占屏展示;只保留第一个,避免刷屏把界面刷乱)。
static void report(String head, [String body = '']) {
_log('AppError', head, body);
if (current.value != null) return;
final trimmed = body.trim();
current.value = trimmed.isEmpty ? head : '$head\n\n$trimmed';
}
/// 记录 Flutter 框架级错误(build / layout / paint 抛出的)。
static void reportFlutter(FlutterErrorDetails d) {
report(d.exceptionAsString(), d.stack?.toString() ?? '');
}
/// 记录 Zone 内未捕获的异步错误。
static void reportZone(Object e, StackTrace st) {
// 音效插件的缺失/未初始化会以异步错误的形式冒出来,届时已错过 Sfx 内部的
// try/catch。音频只是装饰性能力,不能因为设备不支持就把整局游戏判死刑,
// 因此这类错误降级为日志。其余错误照常占屏,保证真机上仍看得见堆栈。
if (isAudioNoise(e)) {
warn('音频子系统不可用(已静音,游戏继续)', '$e');
return;
}
report(e.toString(), st.toString());
}
/// 是否为已知的音频插件噪声错误。
static bool isAudioNoise(Object e) {
final s = e.toString().toLowerCase();
return s.contains('missingpluginexception') &&
(s.contains('audioplayers') || s.contains('xyz.luan'));
}
/// 非致命问题:仅写日志(adb logcat 可查),不占用界面,游戏继续运行。
static void warn(String head, [String body = '']) {
_log('AppWarn', head, body);
}
/// 错误详情界面(替代黑屏)。
static Widget buildScreen(String text) {
// 显式提供 Directionality:ErrorWidget 可能在很早期替换出错 widget,
// 那时树里未必有 Directionality,缺失会引发二次报错盖住真正原因。
return Directionality(
textDirection: TextDirection.ltr,
child: Container(
color: const Color(0xff14060a),
padding: const EdgeInsets.all(24),
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
const SizedBox(height: 40),
const Text(
'启动失败 / 运行错误',
style: TextStyle(
color: Color(0xffff8fa3),
fontSize: 20,
fontWeight: FontWeight.bold,
decoration: TextDecoration.none,
),
),
const SizedBox(height: 6),
const Text(
'请把下面内容截图发给开发者,或用 adb logcat 抓取完整堆栈。',
style: TextStyle(
color: Color(0xff9a9ab0),
fontSize: 12,
decoration: TextDecoration.none,
),
),
const SizedBox(height: 16),
Text(
text,
style: const TextStyle(
color: Color(0xffe8e8f0),
fontSize: 11,
height: 1.4,
fontFamily: 'monospace',
decoration: TextDecoration.none,
),
),
const SizedBox(height: 40),
],
),
),
),
);
}
}
+124
View File
@@ -0,0 +1,124 @@
// 输入:IWBT 经典键位(Shift 跳 / Z 跳 / X 射击 / R 重来)。
// 与 web 版 input.js 1:1 对应。键码沿用 Web 的 KeyboardEvent.code 字符串,
// 由 UI 层的键盘监听器把 Flutter 按键翻译成 code 后喂给 onKey()。
import 'dart:convert';
class Input {
/// 动作 -> 默认绑定的按键 code 列表。
static final Map<String, List<String>> bind = {
'left': ['ArrowLeft', 'KeyA'],
'right': ['ArrowRight', 'KeyD'],
'up': ['ArrowUp', 'KeyW'],
'down': ['ArrowDown', 'KeyS'],
'jump': ['ShiftLeft', 'ShiftRight', 'Space', 'KeyZ', 'KeyN'],
'shoot': ['KeyX', 'KeyJ', 'KeyC', 'KeyM'],
'suicide': ['KeyR'], // IWBT 标准:自杀并回到最近存档点
'restart': ['Backspace'], // 整关从头再来(放弃所有存档点)
'pause': ['Escape', 'KeyP'],
'menu': ['KeyQ'],
};
static final List<String> actions =
bind.keys.toList(); // ['left','right',...]
static final Map<String, List<String>> _defaultBind = {
for (final e in bind.entries) e.key: List<String>.from(e.value),
};
static final Set<String> down = {}; // 当前按住的原始 code
static final Map<String, bool> now = {for (final a in bind.keys) a: false};
static final Map<String, bool> prev = {for (final a in bind.keys) a: false};
static final Map<String, bool> touch = {}; // 触屏虚拟按键 action -> bool
static bool enabled = true;
static bool held(String a) => now[a] ?? false;
static bool hit(String a) => (now[a] ?? false) && !(prev[a] ?? false);
static bool released(String a) => !(now[a] ?? false) && (prev[a] ?? false);
static bool raw(String c) => down.contains(c);
static bool anyHit() {
for (final k in now.keys) {
if (now[k]! && !(prev[k] ?? false)) return true;
}
return false;
}
static void clear() {
down.clear();
for (final k in now.keys) now[k] = false;
for (final k in prev.keys) prev[k] = false;
}
/// 每逻辑帧末尾调用。
static void endFrame() {
for (final k in now.keys) prev[k] = now[k]!;
}
static void _refresh() {
for (final act in bind.keys) {
bool v = false;
for (final c in bind[act]!) {
if (down.contains(c)) {
v = true;
break;
}
}
if (touch[act] ?? false) v = true;
now[act] = enabled ? v : false;
}
}
/// 原始键盘事件(Web code 字符串)。
static void onKey(String code, bool isDown) {
if (isDown) {
down.add(code);
} else {
down.remove(code);
}
_refresh();
}
// ---------------- 触屏虚拟按键 ----------------
static void setTouch(String action, bool on) {
touch.putIfAbsent(action, () => false);
if (touch[action] == on) return;
touch[action] = on;
_refresh();
}
static void clearTouch() {
bool any = false;
for (final k in touch.keys) {
if (touch[k]!) {
touch[k] = false;
any = true;
}
}
if (any) _refresh();
}
// ---------------- 键位自定义(设置界面调用)----------------
static Map<String, List<String>> defaultBindings() =>
{for (final e in bind.entries) e.key: List<String>.from(e.value)};
static Map<String, List<String>> snapshotBindings() =>
{for (final e in bind.entries) e.key: List<String>.from(e.value)};
static void applyBindings(Map<String, List<String>>? map) {
if (map == null) return;
for (final act in map.keys) {
if (bind.containsKey(act)) bind[act] = List<String>.from(map[act]!);
}
}
/// 持久化用:转 JSON 字符串。
static String toJson() => jsonEncode(bind);
static void fromJson(String? s) {
if (s == null) return;
try {
final m = (jsonDecode(s) as Map).cast<String, dynamic>();
final decoded = m.map((k, v) => MapEntry(k, List<String>.from(v as List)));
applyBindings(decoded);
} catch (_) {}
}
}
+112
View File
@@ -0,0 +1,112 @@
// 本地存档:通关记录、最佳成绩、自制关卡。对应 web 版 save.js(localStorage)。
// 跨平台使用 shared_preferences;内存中保留 data 镜像,写盘异步进行。
import 'dart:convert';
import 'package:shared_preferences/shared_preferences.dart';
class Save {
static Map<String, dynamic> data = _blank();
static late SharedPreferences _sp;
static bool _ready = false;
static Map<String, dynamic> _blank() => {
'cleared': <String, dynamic>{},
'customs': <dynamic>[],
'audio': true,
'settings': <String, dynamic>{},
};
/// 设置对象(用户名 / 触屏布局 / 键位)。不存在时自动创建。
static Map<String, dynamic> settings() {
if (data['settings'] is! Map) data['settings'] = <String, dynamic>{};
return data['settings'] as Map<String, dynamic>;
}
static Future<void> load() async {
_sp = await SharedPreferences.getInstance();
_ready = true;
final s = _sp.getString('iwbtc_fan_v1');
if (s != null) {
try {
final m = (jsonDecode(s) as Map).cast<String, dynamic>();
data = {..._blank(), ...m};
data['cleared'] = (data['cleared'] as Map?)?.cast<String, dynamic>() ??
<String, dynamic>{};
data['customs'] = (data['customs'] as List?)?.cast<dynamic>() ??
<dynamic>[];
} catch (_) {
data = _blank();
}
}
}
static void _flush() {
if (!_ready) return;
// 异步写盘,不阻塞游戏循环。
_sp.setString('iwbtc_fan_v1', jsonEncode(data));
}
/// 公开写盘入口(设置界面「确定」时调用)。
static void flush() => _flush();
static Map<String, dynamic>? get(String id) => data['cleared'][id] as Map<String, dynamic>?;
static bool isCleared(String id) => data['cleared'][id] != null;
/// 记录通关,返回是否刷新纪录。
static bool clear(String id, int deaths, int ms, int apples) {
final old = data['cleared'][id] as Map<String, dynamic>?;
final rec = {'deaths': deaths, 'time': ms, 'apples': apples, 'at': DateTime.now().millisecondsSinceEpoch};
bool better = false;
if (old == null) {
data['cleared'][id] = rec;
better = true;
} else {
final nd = min(old['deaths'] as int, deaths);
final nt = min(old['time'] as int, ms);
better = deaths < (old['deaths'] as int) || ms < (old['time'] as int);
data['cleared'][id] = {
...old,
'deaths': nd,
'time': nt,
'apples': max((old['apples'] as int? ?? 0), apples),
};
}
_flush();
return better;
}
static void wipe() {
data = _blank();
_flush();
}
static List<dynamic> customs() => (data['customs'] as List?)?.cast<dynamic>() ?? <dynamic>[];
static void addCustom(Map<String, dynamic> lv) {
(data['customs'] as List).add(lv);
_flush();
}
/// 更新已保存的自制关卡(按 id 就地替换,不新增);id 不存在则追加。
static void updateCustom(Map<String, dynamic> lv) {
final list = (data['customs'] as List).cast<dynamic>();
final i = list.indexWhere((l) => l['id'] == lv['id']);
if (i >= 0) {
list[i] = lv;
} else {
list.add(lv);
}
_flush();
}
static void removeCustom(String id) {
(data['customs'] as List).removeWhere((l) => l['id'] == id);
_flush();
}
static void setAudio(bool on) {
data['audio'] = on;
_flush();
}
}
int min(int a, int b) => a < b ? a : b;
int max(int a, int b) => a > b ? a : b;
+69
View File
@@ -0,0 +1,69 @@
// 通用工具:常量、数学、AABB、颜色、时间格式、确定性噪声。
// 与 web 版 util.js 1:1 对应,保证逻辑一致。
import 'dart:math';
import 'dart:ui' show Color;
const double TILE = 32.0;
const double ROOM_W = 800.0;
const double ROOM_H = 608.0;
const int GRID_W = 25;
const int GRID_H = 19;
final Random _rng = Random();
double clamp(double v, double a, double b) => v < a ? a : v > b ? b : v;
double lerp(double a, double b, double t) => a + (b - a) * t;
int sgn(double v) => v < 0 ? -1 : v > 0 ? 1 : 0;
double rand(double a, double b) => a + _rng.nextDouble() * (b - a);
int randInt(int a, int b) => (a + _rng.nextDouble() * (b - a + 1)).floor();
T pick<T>(List<T> arr) => arr[_rng.nextInt(arr.length)];
/// 轴对齐矩形(顶-左 x,y,w,h)。
class Box {
final double x, y, w, h;
const Box(this.x, this.y, this.w, this.h);
double get right => x + w;
double get bottom => y + h;
}
bool overlap(Box a, Box b) =>
a.x < b.x + b.w && a.x + a.w > b.x && a.y < b.y + b.h && a.y + a.h > b.y;
bool pointIn(double px, double py, Box r) =>
px >= r.x && px <= r.x + r.w && py >= r.y && py <= r.y + r.h;
// ---------------- 颜色 ----------------
Color hex(String s) {
var h = s.replaceFirst('#', '');
if (h.length == 3) {
h = h.split('').map((c) => c + c).join('');
}
final v = int.parse(h, radix: 16);
return Color.fromARGB(255, (v >> 16) & 255, (v >> 8) & 255, v & 255);
}
Color shade(Color c, int n) {
final r = (c.red + n).clamp(0, 255).toInt();
final g = (c.green + n).clamp(0, 255).toInt();
final b = (c.blue + n).clamp(0, 255).toInt();
return Color.fromARGB(255, r, g, b);
}
// ---------------- 时间 ----------------
String fmtTime(int ms) {
final t = max(0, ms);
final m = (t / 60000).floor();
final s = ((t % 60000) / 1000).floor();
final c = ((t % 1000) / 10).floor();
final ss = s.toString().padLeft(2, '0');
final cc = c.toString().padLeft(2, '0');
return '$m:$ss.$cc';
}
// ---------------- 平滑确定性伪随机(仅装饰,不影响帧同步)----------------
double hashNoise(int x, int y) {
int h = (x * 374761393 + y * 668265263).toUnsigned(32);
h = ((h ^ (h ~/ 8192)) * 1274126177).toUnsigned(32);
h = (h ^ (h ~/ 65536)).toUnsigned(32);
return h / 4294967295.0;
}
+287
View File
@@ -0,0 +1,287 @@
// BOSS 战系统:多阶段弹幕 / 冲撞 / 刺雨。对应 web 版 boss.js。
import 'dart:ui' show Canvas, Paint, Path, Color, Rect, Offset, PaintingStyle;
import 'dart:math' show sin, cos, atan2, pi, min, max;
import 'package:iwbtc/src/core/util.dart';
import 'package:iwbtc/src/audio/sfx.dart';
import 'package:iwbtc/src/game/level.dart' show Level, Entity, EBullet;
import 'package:iwbtc/src/game/views.dart' show PlayerView;
const Map<String, dynamic> defs = {
'guy': {'hp': 42, 'w': 64, 'h': 74, 'name': 'THE GUY'},
'mecha': {'hp': 62, 'w': 78, 'h': 62, 'name': 'MECHA KID'},
'devil': {'hp': 84, 'w': 74, 'h': 90, 'name': 'THE DEVIL'},
};
class Boss {
final Level level;
final String kind;
final String name;
double x, y, w, h;
double maxHp;
double hp;
int flash = 0;
bool dead = false;
int deathT = 0;
bool bodyKill = true;
int t = 0;
int phase = 1;
double vx = 0, vy = 0;
String state = 'idle';
int stateT = 0;
late double homeX, homeY;
double wing = 0;
late Map bounds;
void Function()? onDefeat;
Boss(this.level, Entity e)
: kind = e.kind,
name = (defs[e.kind] ?? defs['guy'])['name'],
x = e.x, y = e.y,
w = (defs[e.kind] ?? defs['guy'])['w'].toDouble(),
h = (defs[e.kind] ?? defs['guy'])['h'].toDouble(),
maxHp = (e.hp ?? (defs[e.kind] ?? defs['guy'])['hp']).toDouble(),
hp = (e.hp ?? (defs[e.kind] ?? defs['guy'])['hp']).toDouble() {
bounds = {
'x0': 40.0, 'x1': level.pxW - 40 - w,
'y0': 40.0, 'y1': level.pxH - 32 - h,
};
homeY = clamp(e.y, bounds['y0'], bounds['y1']);
y = homeY;
homeX = e.x;
}
Box get rect => Box(x, y, w, h);
void shoot(double x2, double y2, double vx, double vy, [String col = '#ff5c7a', bool big = false]) {
level.ebullets.add(EBullet(x: x2 - 4, y: y2 - 4, w: big ? 12 : 8, h: big ? 12 : 8, vx: vx, vy: vy, col: col, big: big));
}
void hit([int dmg = 1]) {
if (dead || flash > 6) return;
hp -= dmg;
flash = 10;
Sfx.instance.bossHit();
level.particles.spawn(x + w / 2, y + h / 2, 6, colors: const ['#fff', '#ffc63a'], speed: 2.6, life: 18);
final r = hp / maxHp;
phase = r > 0.6 ? 1 : r > 0.3 ? 2 : 3;
if (hp <= 0) die();
}
void die() {
dead = true; hp = 0; bodyKill = false;
Sfx.instance.bossDie();
level.shake = 16;
level.particles.burst(x + w / 2, y + h / 2,
n: 40, colors: const ['#ff5c7a', '#ffc63a', '#fff', '#e03050'], speed: 5, life: 60, size: 4);
}
void update(int dt, PlayerView player) {
if (dead) {
deathT++;
if (deathT % 8 == 0 && deathT < 90) {
level.particles.burst(x + rand(0, w), y + rand(0, h),
n: 6, colors: const ['#ffc63a', '#ff5c7a', '#fff'], speed: 3, life: 30, size: 3);
Sfx.instance.hit();
}
if (deathT == 90 && onDefeat != null) onDefeat!();
return;
}
t++;
stateT++;
if (flash > 0) flash--;
if (kind == 'guy') updateGuy(player);
else if (kind == 'mecha') updateMecha(player);
else updateDevil(player);
x += vx; y += vy;
x = clamp(x, bounds['x0'], max(bounds['x0'], bounds['x1']));
y = clamp(y, bounds['y0'], max(bounds['y0'], bounds['y1']));
for (final b in level.bullets) {
if (b.dead) continue;
if (overlap(Box(b.x, b.y, b.w, b.h), rect)) {
b.dead = true;
hit(1);
}
}
}
void updateGuy(PlayerView player) {
final L = level;
if (phase == 1) {
vx = sin(t / 60) * 1.5;
y = homeY + sin(t / 45) * 8;
if (t % 96 == 0) {
final cx = x + w / 2, cy = y + h / 2;
for (int i = -1; i <= 1; i++) {
final a = atan2(player.cy - cy, player.cx - cx) + i * 0.28;
shoot(cx, cy, cos(a) * 2.8, sin(a) * 2.8);
}
Sfx.instance.shoot();
}
} else {
final dir = sgn(player.cx - (x + w / 2));
if (state == 'idle') {
vx = dir * 1.9;
y = homeY + sin(t / 30) * 6;
if (stateT > 70) { state = 'slam'; stateT = 0; vy = 0; }
} else if (state == 'slam') {
vx *= 0.9;
vy = min(vy + 0.7, 11);
if (y + h >= bounds['y1'] + h * 0.2 || y >= homeY + 150) {
state = 'idle'; stateT = 0; vy = -3;
L.shake = 10; Sfx.instance.hit();
for (int i = 0; i < 4; i++) {
shoot(x + w / 2, y + h - 6, -2.6 - i * 0.4, -1.2 - i * 0.15, '#ffc63a');
shoot(x + w / 2, y + h - 6, 2.6 + i * 0.4, -1.2 - i * 0.15, '#ffc63a');
}
}
}
}
}
void updateMecha(PlayerView player) {
final L = level;
if (state == 'idle') {
vx *= 0.92;
y = homeY + sin(t / 40) * 5;
if (stateT > 100 - phase * 18) {
state = 'dash'; stateT = 0;
vx = sgn(player.cx - (x + w / 2)) * (5 + phase * 0.8);
Sfx.instance.shoot();
}
if (t % 150 == 0) {
for (int i = 0; i < 4 + phase; i++) {
final tx = randInt(1, L.w - 2);
L.entities.add(Entity('dropspike', 0, 0, 0, tx * TILE, -TILE, TILE, TILE)
..falling = true
..vy = 0
..homeY = -TILE
..temp = true);
}
L.showToast('刺雨来了!', 80);
Sfx.instance.troll();
}
} else if (state == 'dash') {
vy = 0;
if (x <= bounds['x0'] + 1 || x >= bounds['x1'] - 1 || stateT > 90) {
state = 'idle'; stateT = 0; vx = 0;
L.shake = 6;
L.particles.spawn(x + w / 2, y + h / 2, 8, colors: const ['#9ad8ff', '#fff'], speed: 3, life: 20);
}
}
}
void updateDevil(PlayerView player) {
final L = level;
wing += 0.16;
final cx = x + w / 2, cy = y + h / 2;
final p2 = phase;
if (p2 == 1) { vx = sin(t / 70) * 1.6; y = homeY + sin(t / 50) * 8; }
else if (p2 == 2) { vx = sin(t / 40) * 2.6; y = homeY + sin(t / 28) * 11; }
else { vx = sin(t / 25) * 3.4; y = homeY + sin(t / 20) * 14; }
if (p2 == 1) {
if (t % 60 == 0) {
final a = atan2(player.cy - cy, player.cx - cx);
for (int i = -1; i <= 1; i++) shoot(cx, cy, cos(a + i * 0.2) * 3, sin(a + i * 0.2) * 3, '#ff9a3a');
Sfx.instance.shoot();
}
} else if (p2 == 2) {
if (t % 110 == 0) { state = 'laser'; stateT = 0; }
if (state == 'laser' && stateT < 40 && stateT % 5 == 0) {
final yy = 40 + (stateT / 40) * (L.pxH - 90);
shoot(8, yy, 6.5, 0, '#ff4060', true);
shoot(L.pxW - 12, L.pxH - yy, -6.5, 0, '#ff4060', true);
Sfx.instance.shoot();
}
if (stateT > 44) state = 'idle';
} else {
if (t % 34 == 0) {
const n = 10;
final off = t / 200;
for (int i = 0; i < n; i++) {
final a = off + (i / n) * 2 * pi;
shoot(cx, cy, cos(a) * 2.6, sin(a) * 2.6, '#ff5c7a');
}
Sfx.instance.shoot();
}
if (t % 170 == 0) {
final a = atan2(player.cy - cy, player.cx - cx);
shoot(cx, cy, cos(a) * 4.4, sin(a) * 4.4, '#ffc63a', true);
}
}
}
// ---------------- 绘制 ----------------
void draw(Canvas c, int t) {
if (dead && deathT > 96) return;
final x = this.x, y = this.y, w = this.w, h = this.h;
final fl = flash > 0;
c.save();
if (kind == 'guy') drawGuy(c, x, y, w, h, fl, t);
else if (kind == 'mecha') drawMecha(c, x, y, w, h, fl, t);
else drawDevil(c, x, y, w, h, fl, t);
c.restore();
}
void drawGuy(Canvas c, double x, double y, double w, double h, bool fl, int t) {
final c1 = fl ? const Color(0xffffffff) : const Color(0xff2a1020);
final c2 = fl ? const Color(0xffffffff) : const Color(0xff5a1a34);
c.drawCircle(Offset(x + w / 2, y + h * 0.62), w * 0.42, Paint()..color = c1);
c.drawCircle(Offset(x + w / 2, y + h * 0.3), w * 0.36, Paint()..color = c2);
final horn = fl ? const Color(0xffffffff) : const Color(0xffc8305a);
_tri(c, x + w * 0.2, y + h * 0.16, x + w * 0.05, y - h * 0.12, x + w * 0.34, y + h * 0.06, horn);
_tri(c, x + w * 0.8, y + h * 0.16, x + w * 0.95, y - h * 0.12, x + w * 0.66, y + h * 0.06, horn);
c.drawRect(Rect.fromLTWH(x + w * 0.3, y + h * 0.28, w * 0.14, h * 0.1), Paint()..color = fl ? const Color(0xff000000) : const Color(0xffffe14a));
c.drawRect(Rect.fromLTWH(x + w * 0.56, y + h * 0.28, w * 0.14, h * 0.1), Paint()..color = fl ? const Color(0xff000000) : const Color(0xffffe14a));
c.drawRect(Rect.fromLTWH(x + w * 0.34, y + h * 0.46, w * 0.32, h * 0.06), Paint()..color = fl ? const Color(0xff000000) : const Color(0xff801025));
if (phase >= 2) {
final sp = Paint()
..color = fl ? const Color(0xff000000) : const Color(0xffff4060)
..style = PaintingStyle.stroke
..strokeWidth = 2;
c.drawCircle(Offset(x + w / 2, y + h / 2), w * 0.62 + sin(t / 90) * 3, sp);
}
}
void drawMecha(Canvas c, double x, double y, double w, double h, bool fl, int t) {
c.drawRect(Rect.fromLTWH(x + 4, y + 6, w - 8, h - 10), Paint()..color = fl ? const Color(0xffffffff) : const Color(0xff3a4258));
c.drawRect(Rect.fromLTWH(x + 8, y + 10, w - 16, h - 20), Paint()..color = fl ? const Color(0xffffffff) : const Color(0xff68728e));
final blink = (t ~/ 200) % 5 == 0;
c.drawRect(Rect.fromLTWH(x + w * 0.22, y + h * 0.34, w * 0.18, h * 0.14), Paint()..color = fl ? const Color(0xff000000) : (state == 'dash' ? const Color(0xffff4060) : const Color(0xff7ae0ff)));
c.drawRect(Rect.fromLTWH(x + w * 0.6, y + h * 0.34, w * 0.18, h * 0.14), Paint()..color = fl ? const Color(0xff000000) : (state == 'dash' ? const Color(0xffff4060) : const Color(0xff7ae0ff)));
c.drawRect(Rect.fromLTWH(x + w * 0.42, y + h - 8, w * 0.16, 10), Paint()..color = fl ? const Color(0xffffffff) : const Color(0xff2a2f40));
c.drawRect(Rect.fromLTWH(x + w / 2 - 1, y - 6, 2, 8), Paint()..color = fl ? const Color(0xffffffff) : const Color(0xff8a94ad));
c.drawCircle(Offset(x + w / 2, y - 8), 3, Paint()..color = (t ~/ 120) % 2 == 0 ? const Color(0xffff4060) : const Color(0xff601020));
if (state == 'dash') {
c.drawRect(Rect.fromLTWH(x - 6, y + h * 0.4, 8, h * 0.2), Paint()..color = const Color(0xffffb040));
c.drawRect(Rect.fromLTWH(x + w - 2, y + h * 0.4, 8, h * 0.2), Paint()..color = const Color(0xffffb040));
}
}
void drawDevil(Canvas c, double x, double y, double w, double h, bool fl, int t) {
final fl2 = sin(this.wing) * 8;
final wingCol = fl ? const Color(0xffffffff) : const Color(0xff4a0f22);
_tri(c, x + 6, y + h * 0.4, x - 26, y + h * 0.1 + fl2, x - 4, y + h * 0.72 + fl2, wingCol);
_tri(c, x + w - 6, y + h * 0.4, x + w + 26, y + h * 0.1 + fl2, x + w + 4, y + h * 0.72 + fl2, wingCol);
c.drawCircle(Offset(x + w / 2, y + h * 0.6), w * 0.4, Paint()..color = fl ? const Color(0xffffffff) : const Color(0xff8e1030));
c.drawCircle(Offset(x + w / 2, y + h * 0.32), w * 0.34, Paint()..color = fl ? const Color(0xffffffff) : const Color(0xffc01a3a));
final horn = fl ? const Color(0xffffffff) : const Color(0xffffc63a);
_tri(c, x + w * 0.18, y + h * 0.14, x - w * 0.06, y - h * 0.16, x + w * 0.32, y + h * 0.04, horn);
_tri(c, x + w * 0.82, y + h * 0.14, x + w * 1.06, y - h * 0.16, x + w * 0.68, y + h * 0.04, horn);
final ec = phase == 3 ? const Color(0xff7ae0ff) : phase == 2 ? const Color(0xffffb040) : const Color(0xffffe14a);
c.drawRect(Rect.fromLTWH(x + w * 0.26, y + h * 0.26, w * 0.16, h * 0.12), Paint()..color = fl ? const Color(0xff000000) : ec);
c.drawRect(Rect.fromLTWH(x + w * 0.58, y + h * 0.26, w * 0.16, h * 0.12), Paint()..color = fl ? const Color(0xff000000) : ec);
for (int i = 0; i < 4; i++) c.drawRect(Rect.fromLTWH(x + w * 0.32 + i * w * 0.1, y + h * 0.46, 4, 6), Paint()..color = fl ? const Color(0xff000000) : const Color(0xffffffff));
}
void _tri(Canvas c, double ax, double ay, double bx, double by, double cx, double cy, Color col) {
final p = Path();
p.moveTo(ax, ay); p.lineTo(bx, by); p.lineTo(cx, cy); p.close();
c.drawPath(p, Paint()..color = col);
}
}
String bossName(String kind) => (defs[kind] ?? defs['guy'])['name'];
+124
View File
@@ -0,0 +1,124 @@
// 摄像机:跟随 / 锁定 / 缩放(坑向关的"摄像头"道具靠它搞事)。
// 与 web 版 camera.js 1:1 对应。渲染变换通过 apply()。
import 'dart:ui' show Canvas;
import 'dart:math' show min, max;
import 'package:iwbtc/src/core/util.dart';
import 'package:iwbtc/src/game/views.dart' show PlayerView;
class Camera {
double w, h;
double x = 0, y = 0;
double zoom = 1;
double tz = 1;
String mode = 'follow';
double lockX = 0, lockY = 0;
double shakeX = 0, shakeY = 0;
Camera([this.w = 800, this.h = 608]);
void reset(LevelLike level, PlayerView player) {
zoom = 1;
tz = 1;
mode = 'follow';
snapTo(player, level);
}
double viewW() => w / zoom;
double viewH() => h / zoom;
void snapTo(PlayerView player, LevelLike level) {
x = clamp(player.cx - viewW() / 2, 0, max(0, level.pxW - viewW()));
y = clamp(player.cy - viewH() / 2, 0, max(0, level.pxH - viewH()));
mode = 'follow';
zoom = 1;
tz = 1;
}
void update(LevelLike level, PlayerView player, int dt) {
final zone = level.camZone;
double? tx, ty, tz = 1;
double speed = 0.16;
if (zone != null) {
if (zone.mode == 'lock') {
mode = 'lock';
tx = zone.lx != null ? zone.lx! : zone.x;
ty = zone.ly != null ? zone.ly! : zone.y;
tz = zone.zoom ?? 1;
speed = 0.05;
} else if (zone.mode == 'zoom') {
mode = 'zoom';
tz = zone.zoom ?? 1;
tx = player.cx - w / tz / 2;
ty = player.cy - h / tz / 2;
speed = 0.14;
} else if (zone.mode == 'off') {
mode = 'off';
tx = x;
ty = y;
tz = zone.zoom ?? zoom;
speed = 0;
}
} else {
mode = 'follow';
tx = player.cx - w / zoom / 2;
ty = player.cy - h / zoom / 2;
tz = 1;
speed = 0.16;
}
this.tz = lerp(this.tz, tz, 0.06);
if ((this.tz - zoom).abs() > 0.002) zoom = this.tz;
if (speed > 0) {
x = lerp(x, tx!, speed);
y = lerp(y, ty!, speed);
}
_clampToLevel(level);
final sh = level.shake;
if (sh > 0.2) {
shakeX = (rand(-0.5, 0.5)) * sh;
shakeY = (rand(-0.5, 0.5)) * sh;
} else {
shakeX = 0;
shakeY = 0;
}
}
void _clampToLevel(LevelLike level) {
final vw = viewW(), vh = viewH();
if (level.pxW <= vw) {
x = (level.pxW - vw) / 2;
} else {
x = clamp(x, 0, level.pxW - vw);
}
if (level.pxH <= vh) {
y = (level.pxH - vh) / 2;
} else {
y = clamp(y, 0, level.pxH - vh);
}
}
void apply(Canvas canvas) {
canvas.translate(shakeX, shakeY);
if (zoom != 1) canvas.scale(zoom, zoom);
canvas.translate(-x, -y);
}
}
/// 摄像机只关心关卡/玩家的少量字段,用抽象接口解耦避免循环依赖。
abstract class LevelLike {
double get pxW;
double get pxH;
double get shake;
CamZoneLike? get camZone;
}
abstract class CamZoneLike {
String get mode;
double? get zoom;
double get x;
double get y;
double? get lx;
double? get ly;
}
+785
View File
@@ -0,0 +1,785 @@
// 关卡解析与运行时:ASCII 网格 -> tiles + 实体 + 事件。
// 与 web 版 level.js 1:1 对应(含三角刺的 SAT 判定)。
import 'dart:ui' show Canvas, Paint, Color, Rect, Offset;
import 'dart:math' show min, max, sqrt;
import 'package:flutter/widgets.dart';
import 'package:iwbtc/src/core/util.dart';
import 'package:iwbtc/src/game/sprites.dart';
import 'package:iwbtc/src/game/particles.dart';
import 'package:iwbtc/src/game/boss.dart';
import 'package:iwbtc/src/game/camera.dart' show CamZoneLike, LevelLike, Camera;
import 'package:iwbtc/src/game/views.dart';
import 'package:iwbtc/src/audio/sfx.dart';
import 'package:iwbtc/src/game/player.dart' show Player;
enum Tile {
empty, block, grass, brick, metal, ice, water, oneway, killer, vanish,
spikeU, spikeD, spikeL, spikeR,
}
const Map<String, Tile> char2tile = {
'#': Tile.block, '@': Tile.grass, '=': Tile.brick, 'M': Tile.metal,
'%': Tile.ice, '~': Tile.water, '-': Tile.oneway, '*': Tile.killer, 'V': Tile.vanish,
'^': Tile.spikeU, 'v': Tile.spikeD, '<': Tile.spikeL, '>': Tile.spikeR,
};
const Map<String, String> char2ent = {
'S': 'spawn', 'G': 'save', 'X': 'goal', 'P': 'gun', 'F': 'apple', 'J': 'jumpref',
'W': 'warp', 'T': 'sign', 'O': 'turret', 'B': 'boss', 'D': 'dropspike',
'C': 'camzone', '!': 'trigger',
};
const Map<String, List<int>> esize = {
'spawn': [16, 26], 'save': [20, 22], 'goal': [30, 42], 'gun': [28, 24], 'apple': [18, 20],
'jumpref': [26, 26], 'warp': [28, 28], 'sign': [30, 26], 'turret': [32, 32],
'boss': [64, 64], 'dropspike': [32, 32], 'camzone': [96, 96], 'trigger': [32, 32],
};
const Set<Tile> solidTiles = {
Tile.block, Tile.grass, Tile.brick, Tile.metal, Tile.ice, Tile.vanish,
};
const Set<Tile> deadlyTiles = {
Tile.killer, Tile.spikeU, Tile.spikeD, Tile.spikeL, Tile.spikeR,
};
class Entity implements EntityView, CamZoneLike {
final String type;
int idx;
int tx, ty;
double x, y, w, h;
bool active = false;
bool dead = false;
bool taken = false;
int t = 0;
int? no;
bool fake = false;
String? text;
String dir = 'left';
int period = 90;
double speed = 2.4;
int timer = 0;
String kind = 'ball';
String? doorId;
int pair = 0;
dynamic to;
int cool = 0;
bool hidden = false;
String mode = 'lock';
double? zoom;
double? lx;
double? ly;
bool falling = false;
double vy = 0;
double homeY = 0;
bool landed = false;
int landT = 0;
bool temp = false;
double? hp;
Entity(this.type, this.idx, this.tx, this.ty, this.x, this.y, this.w, this.h);
}
class Mover implements MoverView {
double x, y, w, h;
List<Cell> path;
int i = 0;
int dir = 1;
double speed;
int t = 0;
String mode;
double dx = 0, dy = 0;
double ax, ay, aw, ah;
double bx, by;
bool gone = false;
Mover(this.x, this.y, this.w, this.h, this.path, this.speed, this.mode, this.ax, this.ay, this.aw, this.ah, this.bx, this.by);
}
class Cell {
final double x, y;
Cell(this.x, this.y);
}
class Bullet implements BulletView {
double x, y, w, h, vx, vy;
int life;
bool dead = false;
String col = '#fff6a0';
bool big = false;
Bullet({required this.x, required this.y, required this.w, required this.h, required this.vx, required this.vy, required this.life});
}
class EBullet implements BulletView {
double x, y, w, h, vx, vy;
int life;
bool dead = false;
String col;
bool big;
EBullet({required this.x, required this.y, required this.w, required this.h, required this.vx, required this.vy, this.life = 500, this.col = '#ff5c7a', this.big = false});
}
class Level implements LevelLike {
Map<String, dynamic> data;
String name;
String type;
late int w, h;
late double pxW, pxH;
late List<List<Tile>> tiles;
late List<Entity> entities;
late List<Mover> movers;
late List<Boss> bosses;
late List<dynamic> events;
late Particles particles;
late List<DeathMark> deathMarks;
late List<DeathFX> deathFX;
late Map<int, int> vanish;
Map<int, int>? vanishT;
late List<Bullet> bullets;
late List<EBullet> ebullets;
late List<dynamic> srcEntities;
late List<dynamic> moversSrc;
late List<dynamic> eventSrc;
late List<dynamic> msgs;
late List<dynamic> bossSrc;
late List<dynamic> warpSrc;
late List<dynamic> turretSrc;
late List<dynamic> camSrc;
dynamic spawnExtra;
String hint = '';
String bgTheme = 'jump';
int time = 0;
String? toast;
int toastT = 0;
double shake = 0;
bool beat = false;
CamZoneLike? camZone;
late Cell spawn;
int totalApples = 0;
int totalSaves = 0;
int? faceIndex;
Level(this.data) : name = data['name'] ?? 'STAGE', type = data['type'] ?? 'jump' {
parse(data);
particles = Particles();
reset(true);
}
// ---------------- 解析 ----------------
void parse(Map<String, dynamic> data) {
final rows = (data['grid'] as List?)?.map((e) => e as String).toList() ?? [];
h = rows.length;
w = (data['width'] as int?) ?? rows.map((r) => r.length).fold(0, max);
pxW = w * TILE;
pxH = h * TILE;
tiles = [];
final srcEnts = <Map<String, dynamic>>[];
final order = <String, int>{};
for (int y = 0; y < h; y++) {
final line = rows[y];
final row = <Tile>[];
for (int x = 0; x < w; x++) {
final ch = x < line.length ? line[x] : ' ';
if (char2tile.containsKey(ch)) {
row.add(char2tile[ch]!);
} else if (char2ent.containsKey(ch)) {
final type = char2ent[ch]!;
final ew = esize[type]![0], eh = esize[type]![1];
final idx = (order[type] = (order[type] ?? 0) + 1) - 1;
srcEnts.add({'type': type, 'idx': idx, 'tx': x, 'ty': y, 'ch': ch});
row.add(Tile.empty);
} else {
row.add(Tile.empty);
}
}
tiles.add(row);
}
srcEntities = srcEnts;
moversSrc = (data['movers'] as List?) ?? [];
eventSrc = (data['events'] as List?) ?? [];
msgs = (data['messages'] as List?) ?? [];
bossSrc = (data['bosses'] as List?) ?? [];
warpSrc = (data['warps'] as List?) ?? [];
turretSrc = (data['turrets'] as List?) ?? [];
camSrc = (data['camzones'] as List?) ?? [];
spawnExtra = data['spawn'];
hint = data['hint'] ?? '';
bgTheme = data['bg'] ??
(type == 'boss' ? 'boss' : type == 'troll' ? 'troll' : type == 'endur' ? 'endur' : 'jump');
}
void reset([bool full = false]) {
if (full) {
deathMarks = [];
deathFX = [];
}
vanish = {};
bullets = [];
ebullets = [];
entities = [];
movers = [];
bosses = [];
events = [];
time = 0;
toast = null;
toastT = 0;
particles.clear();
shake = 0;
beat = false;
int saveNo = 0;
for (final s in srcEntities) {
final type = s['type'] as String;
final ew = esize[type]![0], eh = esize[type]![1];
final idx = s['idx'] as int;
final tx = s['tx'] as int, ty = s['ty'] as int;
final e = Entity(type, idx, tx, ty,
tx * TILE + (TILE - ew) / 2,
ty * TILE + (TILE - eh),
ew.toDouble(), eh.toDouble());
e.t = 0;
if (type == 'goal') e.y = ty * TILE + TILE - eh;
if (type == 'save') { e.fake = false; e.no = ++saveNo; }
if (type == 'sign') e.text = (msgs.length > idx ? msgs[idx] : '...') as String?;
if (type == 'turret') {
final cfg = (idx < turretSrc.length ? turretSrc[idx] : {}) as Map? ?? {};
e.dir = cfg['dir'] ?? 'left';
e.period = cfg['period'] ?? 90;
e.speed = (cfg['speed'] ?? 2.4).toDouble();
e.timer = cfg['offset'] ?? 0;
e.kind = cfg['kind'] ?? 'ball';
}
if (type == 'warp') {
final cfg = (idx < warpSrc.length ? warpSrc[idx] : {}) as Map? ?? {};
final door = (data['doors'] as List?)?.cast<Map>()
.firstWhere((d) => d['tx'] == tx && d['ty'] == ty, orElse: () => <String, dynamic>{});
e.doorId = (door != null && door.isNotEmpty) ? door['id'] : idx;
e.pair = cfg['pair'] ?? (idx % 2 == 0 ? idx + 1 : idx - 1);
e.to = (door != null && door.isNotEmpty && door['to'] != null) ? door['to'] : (cfg['to']);
e.cool = 0;
e.hidden = cfg['hidden'] ?? false;
}
if (type == 'camzone') {
final cfg = (idx < camSrc.length ? camSrc[idx] : {}) as Map? ?? {};
e.w = ((cfg['w'] ?? 3) * TILE).toDouble();
e.h = ((cfg['h'] ?? 3) * TILE).toDouble();
e.x = tx * TILE;
e.y = ty * TILE;
e.mode = cfg['mode'] ?? 'lock';
e.zoom = cfg['zoom']?.toDouble();
e.lx = cfg['lx']?.toDouble();
e.ly = cfg['ly']?.toDouble();
}
if (type == 'dropspike') {
e.falling = false; e.vy = 0; e.homeY = e.y;
}
if (type == 'boss') {
final cfg = (idx < bossSrc.length ? bossSrc[idx] : {}) as Map? ?? {'kind': 'guy'};
e.kind = cfg['kind'] ?? 'guy';
e.hp = (cfg['hp'] ?? 40).toDouble();
e.w = ((cfg['w'] ?? 64)).toDouble();
e.h = ((cfg['h'] ?? 64)).toDouble();
e.x = tx * TILE + TILE / 2 - e.w / 2;
e.y = ty * TILE + TILE - e.h;
}
entities.add(e);
}
for (final m in moversSrc) {
final pathRaw = (m['path'] as List?)?.map((p) => Cell((p[0] as num) * TILE, (p[1] as num) * TILE)).toList() ?? [];
final a = pathRaw.isNotEmpty ? pathRaw[0] : Cell((m['x'] as num) * TILE, (m['y'] as num) * TILE);
movers.add(Mover(
a.x, a.y,
((m['w'] ?? 3) as num) * TILE, ((m['h'] ?? 1) as num) * TILE,
pathRaw,
(m['speed'] ?? 1.2).toDouble(),
m['mode'] ?? 'loop',
a.x, a.y, ((m['w'] ?? 3) as num) * TILE, ((m['h'] ?? 1) as num) * TILE,
(pathRaw.length > 1 ? pathRaw[1] : a).x, (pathRaw.length > 1 ? pathRaw[1] : a).y,
));
}
for (final ev in eventSrc) {
events.add({
'x': (ev['x'] as num) * TILE, 'y': (ev['y'] as num) * TILE,
'w': ((ev['w'] ?? 1) as num) * TILE, 'h': ((ev['h'] ?? 1) as num) * TILE,
'once': ev['once'] ?? true, 'fired': false,
'actions': ev['actions'] ?? [], 'hidden': ev['hidden'] ?? true,
});
}
final camPlaced = srcEntities.where((s) => s['type'] == 'camzone').length;
for (int i = camPlaced; i < camSrc.length; i++) {
final cfg = camSrc[i] as Map;
entities.add(Entity('camzone', i, cfg['x'] as int, cfg['y'] as int,
(cfg['x'] as num) * TILE, (cfg['y'] as num) * TILE,
((cfg['w'] ?? 3) as num) * TILE, ((cfg['h'] ?? 3) as num) * TILE)
..mode = cfg['mode'] ?? 'lock'
..zoom = (cfg['zoom'] as num?)?.toDouble()
..lx = (cfg['lx'] as num?)?.toDouble()
..ly = (cfg['ly'] as num?)?.toDouble());
}
for (final e in entities) {
if (e.type == 'boss') bosses.add(Boss(this, e));
}
final sp = entities.firstWhere((e) => e.type == 'spawn', orElse: () => Entity('spawn', 0, 0, 0, 0, 0, 0, 0));
if (sp.type == 'spawn') {
spawn = Cell(sp.x + sp.w / 2 - 5.5, sp.y + sp.h - 21);
} else if (spawnExtra != null) {
spawn = Cell((spawnExtra[0] as num) * TILE, (spawnExtra[1] as num) * TILE);
} else {
spawn = Cell(40, 40);
}
totalApples = entities.where((e) => e.type == 'apple').length;
totalSaves = saveNo;
}
/// 死亡后的软重置:保留存档点与已拾取道具,其余复位(不清粒子/震屏)。
void softReset() {
vanish.clear();
vanishT?.clear();
bullets.clear();
ebullets.clear();
for (final m in movers) {
final a = m.path.isNotEmpty ? m.path[0] : Cell(m.x, m.y);
m.x = a.x; m.y = a.y; m.i = 0;
m.dx = 0; m.dy = 0;
final nx = m.path.length > 1 ? m.path[1] : a;
m.ax = m.x; m.ay = m.y; m.bx = nx.x; m.by = nx.y;
}
for (final e in entities) {
if (e.type == 'dropspike') {
if (e.temp) { e.dead = true; continue; }
e.y = e.homeY; e.falling = false; e.vy = 0; e.landed = false; e.landT = 0;
}
if (e.type == 'warp') e.cool = 0;
}
entities = entities.where((e) => !e.dead).toList();
for (final ev in events) ev['fired'] = false;
for (final b in bosses) {
b.hp = b.maxHp; b.dead = false; b.deathT = 0; b.bodyKill = true;
b.t = 0; b.state = 'idle'; b.stateT = 0; b.phase = 1;
b.x = b.homeX; b.y = b.homeY; b.vx = 0; b.vy = 0;
}
}
void addDeathMark(double x, double y) {
deathMarks.clear();
deathMarks.add(DeathMark(x, y));
}
void addDeathFX(double x, double y) {
deathFX.add(DeathFX(x, y, 0));
}
// ---------------- 查询 ----------------
Tile tileAt(int tx, int ty) {
if (tx < 0 || ty < 0 || tx >= w || ty >= h) return ty < 0 ? Tile.empty : Tile.block;
return tiles[ty][tx];
}
bool isSolidTile(Tile t, int tx, int ty) {
if (!solidTiles.contains(t)) return false;
if (t == Tile.vanish) {
final st = vanishState(tx, ty);
return st != 2;
}
return true;
}
int vanishState(int tx, int ty) => vanish[ty * 1000 + tx] ?? 0;
void setVanish(int tx, int ty, int st) => vanish[ty * 1000 + tx] = st;
bool solidRect(double x, double y, double ww, double hh, {bool dropThrough = false, double? prevBottom, Mover? moverExclude}) {
final x0 = (x / TILE).floor();
final x1 = ((x + ww - 0.001) / TILE).floor();
final y0 = (y / TILE).floor();
final y1 = ((y + hh - 0.001) / TILE).floor();
for (int ty = y0; ty <= y1; ty++) {
for (int tx = x0; tx <= x1; tx++) {
final t = tileAt(tx, ty);
if (t == Tile.oneway) {
if (!dropThrough && prevBottom != null && prevBottom <= ty * TILE + 6) {
if (y + hh <= ty * TILE + 8) continue;
if (prevBottom > ty * TILE + 2) continue;
return true;
}
continue;
}
if (isSolidTile(t, tx, ty)) return true;
}
}
for (final m in movers) {
if (m.gone) continue;
if (x < m.x + m.w && x + ww > m.x && y < m.y + m.h && y + hh > m.y) {
if (moverExclude == m) continue;
return true;
}
}
return false;
}
bool deadlyRect(double x, double y, double ww, double hh) {
final x0 = (x / TILE).floor();
final x1 = ((x + ww - 0.001) / TILE).floor();
final y0 = (y / TILE).floor();
final y1 = ((y + hh - 0.001) / TILE).floor();
for (int ty = y0; ty <= y1; ty++) {
for (int tx = x0; tx <= x1; tx++) {
final t = tileAt(tx, ty);
if (!deadlyTiles.contains(t)) continue;
final tr = spikeTri(t, tx * TILE, ty * TILE);
if (triRectOverlap(tr.ax, tr.ay, tr.bx, tr.by, tr.cx, tr.cy, x, y, ww, hh)) return true;
}
}
for (final e in entities) {
if (e.type == 'dropspike' && e.falling) {
final tr = spikeTri(Tile.spikeL, e.x, e.y);
if (triRectOverlap(tr.ax, tr.ay, tr.bx, tr.by, tr.cx, tr.cy, x, y, ww, hh)) return true;
}
}
for (final b in ebullets) {
if (b.dead) continue;
if (overlap(Box(x, y, ww, hh), Box(b.x, b.y, b.w, b.h))) return true;
}
for (final bs in bosses) {
if (bs.dead || !bs.bodyKill) continue;
if (overlap(Box(x, y, ww, hh), Box(bs.x, bs.y, bs.w, bs.h))) return true;
}
return false;
}
bool waterRect(double x, double y, double ww, double hh) {
final x0 = (x / TILE).floor();
final x1 = ((x + ww - 0.001) / TILE).floor();
final y0 = (y / TILE).floor();
final y1 = ((y + hh - 0.001) / TILE).floor();
for (int ty = y0; ty <= y1; ty++)
for (int tx = x0; tx <= x1; tx++)
if (tileAt(tx, ty) == Tile.water) return true;
return false;
}
bool iceAtBottom(double x, double y, double ww, double hh) {
final tx0 = (x / TILE).floor();
final tx1 = ((x + ww - 1) / TILE).floor();
final ty = ((y + hh + 1) / TILE).floor();
for (int tx = tx0; tx <= tx1; tx++) if (tileAt(tx, ty) == Tile.ice) return true;
return false;
}
// ---------------- 更新 ----------------
void update(int dt, Player player, Camera cam) {
time += dt;
final t = time;
for (int i = deathFX.length - 1; i >= 0; i--) {
if (++deathFX[i].t > 14) deathFX.removeAt(i);
}
for (final m in movers) {
if (m.path.isEmpty) continue;
final tgt = m.path[(m.i + 1) % m.path.length];
final ox = m.x, oy = m.y;
final dx = tgt.x - m.x, dy = tgt.y - m.y;
final dist = sqrt(dx * dx + dy * dy);
if (dist <= m.speed) {
m.x = tgt.x; m.y = tgt.y;
m.i = (m.i + 1) % m.path.length;
} else {
m.x += dx / dist * m.speed;
m.y += dy / dist * m.speed;
}
m.dx = m.x - ox; m.dy = m.y - oy;
final next = m.path[(m.i + 1) % m.path.length];
m.ax = m.x; m.ay = m.y; m.bx = next.x; m.by = next.y;
}
for (final key in List<int>.from(vanish.keys)) {
final st = vanish[key]!;
if (st == 0) continue;
final tt = (vanishT?[key] ?? 0) + 1;
vanishT ??= {};
vanishT![key] = tt;
if (st == 1 && tt > 55) { vanish[key] = 2; vanishT![key] = 0; }
else if (st == 2 && tt > 110) { vanish[key] = 0; vanishT![key] = 0; }
}
for (final e in entities) {
if (e.type != 'dropspike') continue;
if (!e.falling) {
final px = player.x + player.w / 2;
if ((px - (e.x + e.w / 2)).abs() < 26 && (player.y - e.y).abs() < 260 && player.y > e.y) {
e.falling = true; e.vy = 0;
Sfx.instance.vanish();
}
} else if (e.landed) {
e.landT++;
if (e.landT > 80) { e.dead = true; e.falling = false; }
} else {
e.vy = min(e.vy + 0.5, 11);
e.y += e.vy;
final ty = ((e.y + e.h) / TILE).floor();
final tx = ((e.x + e.w / 2) / TILE).floor();
if (isSolidTile(tileAt(tx, ty), tx, ty)) {
e.y = ty * TILE - e.h; e.vy = 0; e.landed = true; e.landT = 0;
shake = max(shake, 5);
}
if (e.y > pxH + 100) e.dead = true;
}
}
for (final e in entities) {
if (e.type != 'turret') continue;
e.t++;
if (e.t % (e.period) == 0) {
final cx = e.x + e.w / 2, cy = e.y + e.h / 2;
double vx = 0, vy = 0;
if (e.dir == 'left') vx = -e.speed;
else if (e.dir == 'right') vx = e.speed;
else if (e.dir == 'up') vy = -e.speed;
else vy = e.speed;
if (e.kind == 'aim' && player.alive) {
final dx = (player.x + player.w / 2) - cx, dy = (player.y + player.h / 2) - cy;
final d = max(sqrt(dx * dx + dy * dy), 1e-6);
vx = dx / d * e.speed; vy = dy / d * e.speed;
}
ebullets.add(EBullet(x: cx - 4, y: cy - 4, w: 8, h: 8, vx: vx, vy: vy, col: '#ff5c7a'));
Sfx.instance.shoot();
}
}
for (final b in ebullets) {
b.x += b.vx; b.y += b.vy; b.life--;
if (b.life <= 0) b.dead = true;
final tx = ((b.x + b.w / 2) / TILE).floor(), ty = ((b.y + b.h / 2) / TILE).floor();
if (isSolidTile(tileAt(tx, ty), tx, ty)) {
b.dead = true;
particles.spawn(b.x + 4, b.y + 4, 4, colors: const ['#ff5c7a', '#fff'], speed: 2, life: 14);
}
}
ebullets.removeWhere((b) => b.dead);
for (final b in bullets) {
b.x += b.vx; b.y += b.vy; b.life--;
if (b.life <= 0) b.dead = true;
final tx = ((b.x + b.w / 2) / TILE).floor(), ty = ((b.y + b.h / 2) / TILE).floor();
final tt = tileAt(tx, ty);
if (tt == Tile.block || tt == Tile.grass || tt == Tile.brick || tt == Tile.metal) {
b.dead = true;
particles.spawn(b.x + 3, b.y + 3, 5, colors: const ['#fff6a0', '#ffb040'], speed: 2.4, life: 16);
}
}
bullets.removeWhere((b) => b.dead);
for (final ev in events) {
if (ev['fired'] && ev['once']) continue;
if (overlap(Box(player.x, player.y, player.w, player.h), Box(ev['x'], ev['y'], ev['w'], ev['h']))) {
fireEvent(ev, player);
}
}
camZone = null;
for (final e in entities) {
if (e.type != 'camzone') continue;
if (overlap(Box(player.x + player.w / 2 - 1, player.y + player.h / 2 - 1, 2, 2), Box(e.x, e.y, e.w, e.h))) {
camZone = e;
break;
}
}
if (shake > 0) shake *= 0.86;
if (toastT > 0 && --toastT == 0) toast = null;
particles.update();
}
void fireEvent(Map ev, Player player) {
ev['fired'] = true;
for (final a in (ev['actions'] as List)) {
final type = a['type'];
if (type == 'msg') {
showToast(a['text'] ?? '', a['dur'] ?? 150);
} else if (type == 'spikes') {
for (int i = 0; i < (a['count'] ?? 5); i++) {
final x = ((a['x'] + i * (a['gap'] ?? 1)) as num) * TILE;
entities.add(Entity('dropspike', 0, 0, 0, x, ((a['y'] ?? 0) as num) * TILE, TILE, TILE)
..falling = true
..vy = 0
..homeY = ((a['y'] ?? 0) as num) * TILE
..temp = true);
}
Sfx.instance.troll();
} else if (type == 'vanish') {
for (int i = 0; i < (a['count'] ?? 3); i++) {
final tx = (a['x'] + i) as int, ty = a['y'] as int;
if (tileAt(tx, ty) == Tile.empty) continue;
setVanish(tx, ty, 1);
vanishT ??= {};
vanishT![ty * 1000 + tx] = 0;
}
Sfx.instance.vanish();
} else if (type == 'warp') {
player.x = (a['x'] as num) * TILE;
player.y = (a['y'] as num) * TILE;
player.vx = 0; player.vy = 0;
Sfx.instance.warp();
} else if (type == 'kill') {
player.kill();
} else if (type == 'shake') {
shake = (a['v'] ?? 10).toDouble();
}
}
}
void showToast(String text, [int dur = 150]) {
toast = text;
toastT = dur;
}
// ---------------- 绘制 ----------------
void drawBg(Canvas c, Camera cam, int t) {
final th = {
'jump': ['#1a1830', '#0a0a14'],
'endur': ['#101f24', '#080c10'],
'boss': ['#2a1018', '#0d0810'],
'troll': ['#221432', '#0b0812'],
'custom': ['#141428', '#08080f'],
}[bgTheme] ?? ['#1a1830', '#0a0a14'];
// 主题双色近似(顶/底)
c.drawRect(Rect.fromLTWH(0, 0, cam.w, cam.h), Paint()..color = hex(th[1]));
c.drawRect(Rect.fromLTWH(0, 0, cam.w, cam.h * 0.5), Paint()..color = hex(th[0]));
final px = -cam.x * 0.25, py = -cam.y * 0.15;
final sp = Paint()..color = const Color(0x29ffffff);
for (int i = 0; i < 60; i++) {
final nx = hashNoise(i, 7) * 2000, ny = hashNoise(i, 13) * 900;
final sx = ((nx + px) % (cam.w + 200)) - 100;
final sy = ((ny + py) % (cam.h + 120)) - 60;
c.drawRect(Rect.fromLTWH(sx, sy, 2, 2), sp);
}
final gp = Paint()..color = const Color(0x0affffff);
final off = TILE * 2;
final sx0 = -((cam.x % off));
final sy0 = -((cam.y % off));
for (double x = sx0; x < cam.w; x += off) {
c.drawLine(Offset(x + 0.5, 0), Offset(x + 0.5, cam.h), gp);
}
for (double y = sy0; y < cam.h; y += off) {
c.drawLine(Offset(0, y + 0.5), Offset(cam.w, y + 0.5), gp);
}
}
void draw(Canvas c, Camera cam, int t) {
final vx0 = max(0, (cam.x / TILE).floor() - 1);
final vx1 = min(w - 1, (((cam.x + cam.w / (cam.zoom)) / TILE).ceil() + 1));
final vy0 = max(0, (cam.y / TILE).floor() - 1);
final vy1 = min(h - 1, (((cam.y + cam.h / (cam.zoom)) / TILE).ceil() + 1));
for (int ty = vy0; ty <= vy1; ty++) {
for (int tx = vx0; tx <= vx1; tx++) {
final v = tiles[ty][tx];
if (v == Tile.empty) continue;
final x = tx * TILE, y = ty * TILE;
switch (v) {
case Tile.block: drawBlock(c, x, y, 0); break;
case Tile.grass: drawBlock(c, x, y, 1); break;
case Tile.brick: drawBlock(c, x, y, 2); break;
case Tile.metal: drawBlock(c, x, y, 3); break;
case Tile.ice: drawIce(c, x, y); break;
case Tile.water: drawWater(c, x, y, t); break;
case Tile.oneway: drawOneWay(c, x, y); break;
case Tile.killer: drawKiller(c, x, y, t); break;
case Tile.vanish: drawVanish(c, x, y, vanishState(tx, ty), t); break;
case Tile.spikeU: drawSpike(c, x, y, 0, t); break;
case Tile.spikeD: drawSpike(c, x, y, 1, t); break;
case Tile.spikeL: drawSpike(c, x, y, 2, t); break;
case Tile.spikeR: drawSpike(c, x, y, 3, t); break;
default: break;
}
}
}
for (final m in movers) drawMovingPlatform(c, m);
for (final e in entities) {
if (e.dead) continue;
switch (e.type) {
case 'save': drawSave(c, e, t); break;
case 'goal': drawGoal(c, e, t); break;
case 'gun': if (!e.taken) drawGun(c, e, t); break;
case 'apple': if (!e.taken) drawApple(c, e, t); break;
case 'jumpref': drawJumpRef(c, e.x, e.y, t); break;
case 'warp': if (!e.hidden) drawWarp(c, e, t); break;
case 'sign': drawSign(c, e, t); break;
case 'turret': drawTurret(c, e, t); break;
case 'camzone': drawCamera(c, e, t); break;
case 'dropspike': drawDropSpike(c, e, t); break;
default: break;
}
}
for (final b in bullets) drawBullet(c, b);
for (final b in ebullets) drawEBullet(c, b, t);
for (final dm in deathMarks) drawDeathMark(c, dm);
for (final fx in deathFX) drawDeathRing(c, fx);
particles.draw(c);
}
void drawOverlay(Canvas c, Camera cam, int t) {
for (final e in entities) {
if (e.type != 'sign') continue;
final sx = (e.x + e.w / 2 - cam.x) * (cam.zoom);
final sy = (e.y - 6 - cam.y) * (cam.zoom);
if (sx < -80 || sx > cam.w + 80) continue;
final txt = e.text ?? '';
final tp = TextPainter(
text: TextSpan(text: txt, style: TextStyle(color: const Color(0xffffe9a8), fontSize: 11, fontFamily: 'Courier')),
textDirection: TextDirection.ltr,
);
tp.layout();
c.drawRect(Rect.fromLTWH(sx - tp.width / 2 - 5, sy - 13, tp.width + 10, 16), Paint()..color = rgba(0, 0, 0, 0.72));
tp.paint(c, Offset(sx - tp.width / 2, sy));
}
}
}
// ---------------- 三角刺顶点与 SAT 判定 ----------------
class Tri {
final double ax, ay, bx, by, cx, cy;
Tri(this.ax, this.ay, this.bx, this.by, this.cx, this.cy);
}
Tri spikeTri(Tile t, double x, double y) {
const s = TILE;
if (t == Tile.spikeU) return Tri(x + s / 2, y, x, y + s, x + s, y + s);
if (t == Tile.spikeD) return Tri(x + s / 2, y + s, x, y, x + s, y);
if (t == Tile.spikeL) return Tri(x, y + s / 2, x + s, y, x + s, y + s);
if (t == Tile.spikeR) return Tri(x + s, y + s / 2, x, y, x, y + s);
return Tri(x, y, x + s, y, x, y + s);
}
bool triRectOverlap(double ax, double ay, double bx, double by, double cx, double cy,
double rx, double ry, double rw, double rh) {
final rMinX = rx, rMaxX = rx + rw, rMinY = ry, rMaxY = ry + rh;
final tMinX = min(ax, min(bx, cx)), tMaxX = max(ax, max(bx, cx));
final tMinY = min(ay, min(by, cy)), tMaxY = max(ay, max(by, cy));
if (tMaxX < rMinX || tMinX > rMaxX || tMaxY < rMinY || tMinY > rMaxY) return false;
final edges = [[bx - ax, by - ay], [cx - bx, cy - by], [ax - cx, ay - cy]];
for (final e in edges) {
final ex = e[0] as double, ey = e[1] as double;
final nx = -ey, ny = ex;
final t0 = nx * ax + ny * ay, t1 = nx * bx + ny * by, t2 = nx * cx + ny * cy;
final tMin = min(t0, min(t1, t2)), tMax = max(t0, max(t1, t2));
final p0 = nx * rMinX + ny * rMinY, p1 = nx * rMaxX + ny * rMinY;
final p2 = nx * rMinX + ny * rMaxY, p3 = nx * rMaxX + ny * rMaxY;
final rMin = min(p0, min(p1, min(p2, p3))), rMax = max(p0, max(p1, max(p2, p3)));
if (tMax < rMin || rMax < tMin) return false;
}
return true;
}
+66
View File
@@ -0,0 +1,66 @@
// 关卡清单:从 assets/levels.json 加载内置关卡,并提供与 web 版
// levels/index.js 一致的 TYPE_LABEL / TYPE_CLASS / makeCustomLevel / findLevel。
import 'dart:convert';
import 'package:flutter/services.dart' show rootBundle;
import 'package:iwbtc/src/core/save.dart';
const Map<String, String> TYPE_LABEL = {
'jump': '跳刺',
'endur': '耐久',
'boss': 'BOSS',
'troll': '坑向',
'custom': '自制',
};
const Map<String, String> TYPE_CLASS = {
'jump': 't-jump',
'endur': 't-endur',
'boss': 't-boss',
'troll': 't-troll',
'custom': 't-custom',
};
List<Map<String, dynamic>> _builtin = <Map<String, dynamic>>[];
bool _loaded = false;
/// 加载内置关卡(assets/levels.json)。重复调用安全。
Future<void> loadLevels() async {
if (_loaded) return;
final s = await rootBundle.loadString('assets/levels.json');
final m = (jsonDecode(s) as Map).cast<String, dynamic>();
final list = ((m['levels'] as List?) ?? <dynamic>[])
.cast<Map<String, dynamic>>()
.toList();
for (int i = 0; i < list.length; i++) {
list[i] = <String, dynamic>{...list[i], 'index': i, 'builtin': true};
}
_builtin = list;
_loaded = true;
}
/// 内置关卡清单(已标注 index / builtin)。
List<Map<String, dynamic>> get LEVELS => _builtin;
/// 合成一张自制关卡数据对象(与 web makeCustomLevel 一致)。
Map<String, dynamic> makeCustomLevel(Map<String, dynamic> data) {
return <String, dynamic>{
...data,
'id': 'u' + (DateTime.now().millisecondsSinceEpoch % 100000).toString() + (_uid++).toString(),
'type': 'custom',
'builtin': false,
'index': -1,
};
}
int _uid = 0;
/// 按 id 查找关卡(内置优先,其次自制)。
Map<String, dynamic>? findLevel(String id) {
for (final l in _builtin) {
if (l['id'] == id) return l;
}
for (final l in Save.customs()) {
if (l['id'] == id) return l as Map<String, dynamic>;
}
return null;
}
+66
View File
@@ -0,0 +1,66 @@
// 粒子系统。与 web 版 particles.js 1:1 对应。
import 'dart:ui' show Canvas, Paint, Color, Rect;
import 'dart:math' show cos, sin, min, max, sqrt, atan2, pi;
import 'package:iwbtc/src/core/util.dart';
class Particle {
double x, y, vx, vy;
int life, max;
Color c;
double s, g;
Particle(this.x, this.y, this.vx, this.vy, this.life, this.max, this.c, this.s, this.g);
}
class Particles {
final List<Particle> list = [];
void clear() => list.clear();
void spawn(double x, double y, int n,
{List<String> colors = const ['#fff', '#ff5c7a', '#ffc63a'],
double speed = 3,
int life = 40,
double size = 3,
double grav = 0.12,
double spread = 6.283185307179586,
double dir = 0}) {
for (int i = 0; i < n; i++) {
final a = dir + (rand(-0.5, 0.5)) * spread;
final s = rand(speed * 0.35, speed);
list.add(Particle(
x,
y,
cos(a) * s,
sin(a) * s,
(life * rand(0.6, 1.25)).round(),
life,
hex(colors[randInt(0, colors.length - 1)]),
size * rand(0.6, 1.3),
grav,
));
}
}
void burst(double x, double y, {int n = 18, double speed = 3, int life = 40, double size = 3, double grav = 0.12, List<String> colors = const ['#fff', '#ff5c7a', '#ffc63a']}) {
spawn(x, y, n, colors: colors, speed: speed, life: life, size: size, grav: grav);
}
void update() {
for (int i = list.length - 1; i >= 0; i--) {
final p = list[i];
p.x += p.vx;
p.y += p.vy;
p.vy += p.g;
p.vx *= 0.985;
if (--p.life <= 0) list.removeAt(i);
}
}
void draw(Canvas canvas) {
final paint = Paint();
for (final p in list) {
paint.color = p.c.withAlpha((min(1.0, p.life / (p.max * 0.5)) * 255).round().clamp(0, 255));
canvas.drawRect(Rect.fromLTWH(p.x.floorToDouble(), p.y.floorToDouble(), p.s.floorToDouble(), p.s.floorToDouble()), paint);
}
}
}
+383
View File
@@ -0,0 +1,383 @@
// 玩家:还原 IWBT 的硬核手感 —— 松手即停、无加速、可变跳跃高度、二段跳。
// 与 web 版 player.js 1:1 对应(逐帧积分完全一致,保证手感)。
import 'dart:ui' show Canvas;
import 'dart:math' show pi, max, min;
import 'package:iwbtc/src/core/util.dart';
import 'package:iwbtc/src/core/input.dart';
import 'package:iwbtc/src/audio/sfx.dart';
import 'package:iwbtc/src/game/sprites.dart';
import 'package:iwbtc/src/game/views.dart';
import 'package:iwbtc/src/game/level.dart' show Level, Tile, Mover, Cell, Bullet;
class SavePoint {
double x, y;
int face;
SavePoint(this.x, this.y, this.face);
}
class PHY {
static const double GRAV = 0.25;
static const double GRAV_FALL = 0.32;
static const double GRAV_DJ = 0.35;
static const double MAXFALL = 9;
static const double JUMP = 6.6;
static const double DJUMP = 6.7;
static const double DJ_INERTIA = 0;
static const double HSPD = 2.6;
static const double WATER_GRAV = 0.16;
static const double WATER_FALL = 2.6;
static const double WATER_RISE = 0.42;
static const double ICE_ACC = 0.18;
static const double ICE_FRIC = 0.07;
static const double SLIDE_FRIC = 0.12;
static const double SLIDE_AIR = 0.02;
static const double SLIDE_EASE = 0.14;
static const double SLIDE_BRAKE = 0.45;
static const double SLIDE_MIN = 0.35;
static const double SLIDE_STOP = 0.05;
static const double ICE_TOP = 1.6;
}
class Player implements PlayerView {
Level level;
double w = 11, h = 21;
double x = 0, y = 0;
double vx = 0, vy = 0;
int jumps = 0;
bool djUp = false;
bool grounded = false;
int facing = 1;
bool alive = true;
bool dead = false;
int deadT = 0;
double deathX = 0, deathY = 0;
bool inWater = false;
int animT = 0;
int shootCool = 0;
Mover? mover;
bool sliding = false;
bool justRespawned = false;
bool hasGun = false;
int apples = 0;
SavePoint? savePoint;
int saveIdx = 0;
bool clearing = false;
// 回调(由 World 设置)
void Function()? onDeath;
void Function()? onClear;
void Function(dynamic e)? onPortal;
void Function()? onRespawnFace;
Player(this.level) {
reset(true);
}
double get cx => x + w / 2;
double get cy => y + h / 2;
void reset(bool full) {
final sp = (full || savePoint == null)
? SavePoint(level.spawn.x, level.spawn.y, level.faceIndex ?? 0)
: savePoint!;
x = sp.x; y = sp.y;
vx = 0; vy = 0;
jumps = 0;
djUp = false;
grounded = false;
facing = 1;
alive = true;
dead = false;
deadT = 0;
deathX = 0; deathY = 0;
inWater = false;
animT = 0;
shootCool = 0;
mover = null;
sliding = false;
justRespawned = false;
if (full) {
hasGun = level.data['startGun'] == true;
apples = 0;
savePoint = null;
saveIdx = 0;
}
}
/// 纯死亡演出(不进死亡状态):粒子爆开 + 屏幕轻震 + 死亡点扩散冲击环。
void deathBurst() {
final L = level;
final dx = clamp(cx, 10, max(10, L.pxW - 10));
final dy = clamp(cy, 10, max(10, L.pxH - 10));
Sfx.instance.death();
L.particles.spawn(dx, dy, 16, colors: const ['#ff5c7a', '#ff8095', '#fff', '#ffc63a'], speed: 3.4, life: 34, size: 3, grav: 0.16);
L.shake = max(L.shake, 6);
L.addDeathFX(dx, dy);
deathX = dx; deathY = dy;
}
void kill([Map opts = const {}]) {
if (dead || !alive) return;
dead = true; alive = false; deadT = 0;
vx = 0; vy = 0; sliding = false;
deathBurst();
if (opts['mark'] != false) level.addDeathMark(deathX, deathY);
if (opts['count'] != false && onDeath != null) onDeath!();
}
void respawn() {
dead = false; alive = true;
final L = level;
final sp = savePoint ?? SavePoint(L.spawn.x, L.spawn.y, 0);
x = sp.x; y = sp.y;
vx = 0; vy = 0; jumps = 0;
djUp = false;
shootCool = 0; sliding = false;
deadT = 0;
int guard = 0;
while (L.solidRect(x, y, w, h) && guard++ < 48) y -= 2;
L.softReset();
justRespawned = true;
}
void update(int dt) {
final L = level;
animT++;
if (shootCool > 0) shootCool--;
if (dead) { deadT++; return; }
final inWater = L.waterRect(x + 2, y + 4, w - 4, h - 4);
if (inWater && !this.inWater) Sfx.instance.splash();
this.inWater = inWater;
final moveL = Input.held('left'), moveR = Input.held('right');
final onIce = grounded && L.iceAtBottom(x, y, w, h);
double target = 0;
if (moveL && !moveR) target = -PHY.HSPD;
else if (moveR && !moveL) target = PHY.HSPD;
final ICE_MAX = PHY.HSPD * PHY.ICE_TOP;
final decel = (double f) {
if (vx.abs() <= f) vx = 0;
else vx -= sgn(vx) * f;
};
if (onIce) {
if (target != 0) vx += sgn(target) * PHY.ICE_ACC;
else decel(PHY.ICE_FRIC);
vx = clamp(vx, -ICE_MAX, ICE_MAX);
if (vx.abs() > PHY.SLIDE_MIN) sliding = true;
} else if (sliding && vx.abs() > PHY.SLIDE_STOP) {
if (target != 0) {
final dir = sgn(vx);
if (dir == 0 || sgn(target) == dir) {
if (vx.abs() >= PHY.HSPD) {
vx -= dir * PHY.SLIDE_EASE;
if (vx.abs() < PHY.HSPD) { vx = target; sliding = false; }
} else {
vx = target; sliding = false;
}
} else {
vx -= dir * PHY.SLIDE_BRAKE;
if (sgn(vx) == sgn(target)) { vx = target; sliding = false; }
}
vx = clamp(vx, -ICE_MAX, ICE_MAX);
} else {
decel(grounded ? PHY.SLIDE_FRIC : PHY.SLIDE_AIR);
if (vx == 0) sliding = false;
}
if (vx != 0 && vx.abs() < PHY.SLIDE_STOP) { vx = 0; sliding = false; }
} else {
vx = target;
sliding = false;
}
if (target != 0) facing = sgn(target).toInt();
if (inWater) {
if (Input.held('jump')) vy -= PHY.WATER_RISE;
if (Input.held('down')) vy += 0.25;
vy = clamp(vy, -3.2, PHY.WATER_FALL);
jumps = 0; djUp = false;
} else {
if (Input.hit('jump')) {
if (grounded || jumps == 0) {
vy = -PHY.JUMP; jumps = 1; grounded = false;
Sfx.instance.jump();
L.particles.spawn(cx, y + h, 5, colors: const ['#fff', '#cfcfe0'], speed: 1.6, life: 16, size: 2, dir: -pi / 2, spread: 2);
} else if (jumps == 1) {
vy = max(vy, 0) * PHY.DJ_INERTIA - PHY.DJUMP; jumps = 2;
djUp = vy < 0;
Sfx.instance.djump();
L.particles.spawn(cx, cy, 10, colors: const ['#fff', '#9ad8ff'], speed: 2.2, life: 20, size: 2, spread: pi * 2);
}
}
if (!Input.held('jump') && vy < -3) vy = -3;
vy = min(vy + (vy < 0 ? (djUp ? PHY.GRAV_DJ : PHY.GRAV) : PHY.GRAV_FALL), PHY.MAXFALL);
if (djUp && vy >= 0) djUp = false;
}
if (mover != null && !(mover!.gone)) {
x += mover!.dx;
y += mover!.dy;
}
// X 轴移动
final prevBottom = y + h;
mover = null;
double steps = vx.abs();
final dx = sgn(vx).toDouble();
while (steps > 0) {
final m = min(1, steps);
if (!L.solidRect(x + dx * m, y, w, h)) { x += dx * m; steps -= m; }
else break;
}
if (L.solidRect(x + dx, y, w, h) && vx.abs() > 0) vx = 0;
// Y 轴移动
final dropThrough = Input.held('down');
steps = vy.abs();
final dy = sgn(vy).toDouble();
bool hitGround = false;
while (steps > 0) {
final m = min(1, steps);
final ny = y + dy * m;
final solidHere = L.solidRect(x, ny, w, h, prevBottom: prevBottom, dropThrough: dropThrough);
if (!solidHere) { y = ny; steps -= m; }
else {
if (dy > 0) hitGround = true;
vy = 0;
break;
}
}
for (final m in L.movers) {
if (m.gone) continue;
if (x + w > m.x && x < m.x + m.w) {
if (y + h >= m.y - 2 && y + h <= m.y + m.h) {
mover = m; hitGround = true;
}
}
}
final wasAir = !grounded;
grounded = hitGround || (vy == 0 && L.solidRect(x, y + 1, w, h));
if (grounded) {
jumps = 0; djUp = false;
}
if (sliding && grounded && vx.abs() > PHY.HSPD && animT % 5 == 0) {
L.particles.spawn(cx - sgn(vx) * 5, y + h - 2, 2, colors: const ['#dff4ff', '#8fd8f0', '#fff'], speed: 1.1, life: 16, size: 2, dir: -pi / 2, spread: 1.7, grav: 0.1);
}
x = clamp(x, 0, L.pxW - w);
if (y > L.pxH + 200) { kill(); return; }
if (y < -300) { y = -300; vy = 0; }
if (L.deadlyRect(x, y, w, h)) { kill(); return; }
if (hasGun && Input.hit('shoot') && shootCool == 0 && L.bullets.length < 5) {
shoot();
}
touchEntities();
if (grounded) {
final ty = ((y + h + 2) / TILE).floor();
for (int tx = (x / TILE).floor(); tx <= ((x + w - 1) / TILE).floor(); tx++) {
if (ty < L.tiles.length && tx >= 0 && tx < L.tiles[0].length && L.tiles[ty][tx] == Tile.vanish && L.vanishState(tx, ty) == 0) {
L.setVanish(tx, ty, 1);
L.vanishT ??= {};
L.vanishT![ty * 1000 + tx] = 0;
}
}
}
}
void shoot() {
final L = level;
shootCool = 9;
L.bullets.add(Bullet(
x: facing > 0 ? x + w - 2 : x - 6,
y: y + 8, w: 10.0, h: 5.0,
vx: 9.0 * facing, vy: 0.0, life: 130,
));
Sfx.instance.shoot();
L.particles.spawn(facing > 0 ? x + w : x, y + 10, 3, colors: const ['#fff6a0'], speed: 1.5, life: 10, size: 2);
}
void touchEntities() {
final L = level;
for (final e in L.entities) {
if (e.dead) continue;
final hit = e.type == 'save'
? overlap(Box(x - 6, y - 6, w + 12, h + 12), Box(e.x, e.y, e.w, e.h))
: overlap(Box(x, y, w, h), Box(e.x, e.y, e.w, e.h));
if (!hit) continue;
switch (e.type) {
case 'save':
if (!e.active) {
e.active = true;
savePoint = SavePoint(e.x + e.w / 2 - w / 2, e.y + e.h - h, level.faceIndex ?? 0);
saveIdx = e.no ?? 1;
Sfx.instance.save();
L.particles.spawn(e.x + e.w / 2, e.y + e.h / 2, 18, colors: const ['#b06bff', '#fff', '#e5d0ff'], speed: 2.6, life: 30);
if (e.fake) { L.showToast('假的。', 90); kill(); return; }
}
break;
case 'goal':
if (!clearing) { clearing = true; if (onClear != null) onClear!(); }
break;
case 'gun':
if (!e.taken) {
e.taken = true; hasGun = true;
L.particles.spawn(e.x + e.w / 2, e.y + e.h / 2, 12, colors: const ['#dfe0f0', '#fff'], speed: 2.2, life: 24);
L.showToast('获得手枪!按 X 射击', 120);
Sfx.instance.save();
}
break;
case 'apple':
if (!e.taken) { e.taken = true; apples++; Sfx.instance.save(); }
break;
case 'jumpref':
if (animT % 4 == 0) jumps = 0;
break;
case 'warp':
if (e.cool > 0) break;
e.cool = 40;
final to = e.to;
if (to != null && to is Map && !to.containsKey('x') && !to.containsKey('y') && to['face'] is int) {
if (onPortal != null) onPortal!(e);
break;
}
double? nx, ny;
if (to == 'spawn') {
nx = L.spawn.x; ny = L.spawn.y;
} else if (to != null && to is Map && to.containsKey('x')) {
nx = (to['x'] as num) * TILE; ny = (to['y'] as num) * TILE;
} else {
final p = L.entities.firstWhere((o) => o.type == 'warp' && o.idx == e.pair, orElse: () => e);
if (p != e) {
nx = p.x + p.w / 2 - w / 2; ny = p.y + p.h - h;
}
}
if (nx != null && ny != null) {
x = nx; y = ny;
vx = 0; vy = 0;
final partner = L.entities.firstWhere((o) => o.type == 'warp' && o.idx == e.pair, orElse: () => e);
if (partner != e) partner.cool = 40;
Sfx.instance.warp();
L.particles.spawn(cx, cy, 12, colors: const ['#a86bff', '#fff'], speed: 2.5, life: 24);
}
break;
case 'sign':
if (animT % 30 == 0) L.showToast(e.text ?? '', 90);
break;
}
}
}
void draw(Canvas c) {
if (dead) return;
drawKid(c, this, animT);
}
}
+448
View File
@@ -0,0 +1,448 @@
// 程序化像素绘制(无外部图片资源)。对应 web 版 sprites.js。
// 全部使用 Flutter dart:ui 的 Canvas / Paint / Path,逐函数镜像。
import 'dart:ui' show Canvas, Paint, Path, Rect, Color, Offset, PaintingStyle, StrokeCap, TextAlign;
import 'dart:math' show sin, cos, pi, max, min, sqrt, atan2;
import 'package:flutter/widgets.dart';
import 'package:iwbtc/src/core/util.dart';
import 'package:iwbtc/src/game/views.dart';
Color rgba(int r, int g, int b, double a) =>
Color.fromARGB((a * 255).round().clamp(0, 255), r, g, b);
class C {
static const Color stone = Color(0xff6b6b7e);
static const Color stoneDark = Color(0xff3a3a48);
static const Color stoneLite = Color(0xff8f8fa6);
static const Color grass = Color(0xff4f9e46);
static const Color grassDark = Color(0xff2c5c2b);
static const Color brick = Color(0xffa8543c);
static const Color brickDark = Color(0xff632a1d);
static const Color metal = Color(0xff5a6478);
static const Color metalDark = Color(0xff333a4a);
static const Color ice = Color(0xff8fd8f0);
static const Color iceDark = Color(0xff4a90b8);
static const Color water = Color(0xff2a6fd8);
static const Color spike = Color(0xffcfdde9);
static const Color spikeDark = Color(0xffa8bccf);
static const Color kid = Color(0xffd94a55);
static const Color kidDark = Color(0xff8e2530);
static const Color skin = Color(0xfff5cfa8);
static const Color hair = Color(0xff5a3a22);
static const Color pant = Color(0xff2c3a7a);
static const Color save = Color(0xffb06bff);
static const Color goal = Color(0xffffc63a);
static const Color gun = Color(0xffb8b8c8);
static const Color apple = Color(0xffe03050);
}
void px(Canvas c, double x, double y, double w, double h, Color col) {
final p = Paint()..color = col;
c.drawRect(Rect.fromLTWH(x, y, w, h), p);
}
// ---------------- 地形 ----------------
void drawBlock(Canvas c, double x, double y, int variant, [bool lit = false]) {
const s = TILE;
Color base = C.stone, dark = C.stoneDark, lite = C.stoneLite;
if (variant == 1) {
base = C.grass; dark = C.grassDark; lite = const Color(0xff7fd06a);
} else if (variant == 2) {
base = C.brick; dark = C.brickDark; lite = const Color(0xffd1785c);
} else if (variant == 3) {
base = C.metal; dark = C.metalDark; lite = const Color(0xff8a94ad);
}
px(c, x, y, s, s, dark);
px(c, x + 1, y + 1, s - 2, s - 2, base);
if (variant == 2) {
px(c, x + 1, y + 8, s - 2, 1, dark);
px(c, x + 1, y + 17, s - 2, 1, dark);
px(c, x + 10, y + 1, 1, 7, dark);
px(c, x + 20, y + 9, 1, 8, dark);
px(c, x + 6, y + 18, 1, 7, dark);
} else if (variant == 1) {
px(c, x + 1, y + 1, s - 2, 5, lite);
for (int i = 0; i < 4; i++) px(c, x + 3 + i * 7, y + 2, 1, 4, dark);
} else {
final n = hashNoise(x.round(), y.round());
px(c, x + 2, y + 2, 6, 2, lite);
if (n > 0.5) px(c, x + 18, y + 12, 8, 2, dark);
if (n < 0.35) px(c, x + 6, y + 22, 7, 2, dark);
}
px(c, x, y, s, 1, lite);
px(c, x, y, 1, s, lite);
px(c, x, y + s - 1, s, 1, dark);
px(c, x + s - 1, y, 1, s, dark);
if (lit) px(c, x, y, s, s, rgba(255, 255, 255, 0.09));
}
void drawIce(Canvas c, double x, double y) {
const s = TILE;
px(c, x, y, s, s, C.iceDark);
px(c, x + 1, y + 1, s - 2, s - 2, C.ice);
px(c, x + 2, y + 3, 10, 3, const Color(0xffe6fbff));
px(c, x + 18, y + 16, 8, 2, rgba(255, 255, 255, 0.7));
px(c, x, y, s, 1, const Color(0xffe6fbff));
px(c, x, y + s - 1, s, 1, C.iceDark);
}
void drawWater(Canvas c, double x, double y, int t) {
const s = TILE;
px(c, x, y, s, s, rgba(40, 110, 215, 0.55));
final off = sin(t / 260 + x * 0.09) * 3;
px(c, x, y + 4 + off, s, 3, rgba(120, 200, 255, 0.35));
px(c, x, y + 16 - off, s, 2, rgba(120, 200, 255, 0.35));
}
void drawOneWay(Canvas c, double x, double y) {
const s = TILE;
px(c, x, y, s, 6, C.metalDark);
px(c, x, y, s, 3, const Color(0xff9aa6bd));
for (int i = 0; i < 8; i++) px(c, x + i * 4, y + 6, 2, 3, C.metalDark);
}
void drawSpike(Canvas c, double x, double y, int dir, int t) {
const s = TILE;
late double ax, ay, bx, by, cx2, cy2;
if (dir == 0) {
ax = x + s / 2; ay = y; bx = x; by = y + s; cx2 = x + s; cy2 = y + s;
} else if (dir == 1) {
ax = x + s / 2; ay = y + s; bx = x; by = y; cx2 = x + s; cy2 = y;
} else if (dir == 2) {
ax = x; ay = y + s / 2; bx = x + s; by = y; cx2 = x + s; cy2 = y + s;
} else {
ax = x + s; ay = y + s / 2; bx = x; by = y; cx2 = x; cy2 = y + s;
}
late Offset from, to;
if (dir == 0) { from = Offset(x, y); to = Offset(x, y + s); }
else if (dir == 1) { from = Offset(x, y + s); to = Offset(x, y); }
else if (dir == 2) { from = Offset(x, y); to = Offset(x + s, y); }
else { from = Offset(x + s, y); to = Offset(x, y); }
final rect = Rect.fromLTWH(x, y, s, s);
final g = LinearGradient(
begin: _gradAlign(from, rect), end: _gradAlign(to, rect),
colors: const [Color(0xfff4f9fd), Color(0xffcfdde9), Color(0xffa8bccf)],
);
final p = Path();
p.moveTo(ax, ay); p.lineTo(bx, by); p.lineTo(cx2, cy2); p.close();
final paint = Paint()..shader = g.createShader(rect);
c.drawPath(p, paint);
final stroke = Paint()
..color = rgba(150, 175, 200, 0.9)
..style = PaintingStyle.stroke
..strokeWidth = 1;
c.drawPath(p, stroke);
}
void drawKiller(Canvas c, double x, double y, int t) {
const s = TILE;
final bob = sin(t / 300 + x) * 1.5;
c.save();
c.translate(x + s / 2, y + s / 2 + bob);
final p = Paint()..color = C.apple;
c.drawCircle(Offset(0, 2), 11, p);
final p2 = Paint()..color = const Color(0xffff7a90);
c.drawCircle(Offset(-4, -2), 4, p2);
px(c, -1, -13, 2, 6, const Color(0xff6b4423));
final leaf = Paint()..color = const Color(0xff3ddc84);
c.drawOval(Rect.fromLTWH(1, -13, 10, 5.2), leaf);
px(c, -5, 0, 2, 2, const Color(0xff000000));
px(c, 3, 0, 2, 2, const Color(0xff000000));
c.restore();
}
void drawVanish(Canvas c, double x, double y, int state, int t) {
const s = TILE;
final alpha = state == 0 ? 1.0 : state == 1 ? (sin(t / 60) > 0 ? 0.55 : 0.9) : 0.14;
drawBlock(c, x, y, 3);
if (alpha < 1) {
// 模拟 globalAlpha:用半透明黑覆盖来压暗
px(c, x, y, s, s, Color.fromARGB(((1 - alpha) * 255).round().clamp(0, 255), 0, 0, 0));
}
if (state != 2) {
final sp = Paint()
..color = rgba(255, 200, 60, 0.55)
..style = PaintingStyle.stroke
..strokeWidth = 1;
c.drawRect(Rect.fromLTWH(x + 2.5, y + 2.5, s - 5, s - 5), sp);
}
}
void drawJumpRef(Canvas c, double x, double y, int t) {
const s = TILE;
final b = sin(t / 200 + x) * 2;
final p = Path();
p.moveTo(x + s / 2, y + 8 + b);
p.lineTo(x + s - 6, y + s - 10 + b);
p.lineTo(x + 6, y + s - 10 + b);
p.close();
c.drawPath(p, Paint()..color = const Color(0xff3ddc84));
final sp = Paint()
..color = const Color(0xff0d5c30)
..style = PaintingStyle.stroke
..strokeWidth = 1;
c.drawPath(p, sp);
}
// ---------------- 玩家 ----------------
void drawKid(Canvas c, PlayerView p, int t) {
final x = p.x.roundToDouble(), y = p.y.roundToDouble();
final w = p.w, h = p.h;
final air = !p.grounded;
c.save();
c.translate(x, y);
// 身体(红衣)
px(c, 2, 7, w - 4, 7, C.kid);
px(c, 2, 7, w - 4, 1, shade(C.kid, 40));
if (air) {
px(c, p.facing > 0 ? w - 3 : 0, 5, 3, 5, C.skin);
} else {
px(c, p.facing > 0 ? w - 3 : 0, 9, 3, 5, C.skin);
}
if (air) {
px(c, 3, 14, 4, 4, C.pant);
px(c, w - 7, 14, 4, 3, C.pant);
} else {
px(c, 2, 14, 4, 4, C.pant);
px(c, w - 6, 14, 4, 4, C.pant);
}
px(c, 1, 0, w - 2, 8, C.skin);
px(c, 1, 0, w - 2, 3, C.hair);
px(c, p.facing > 0 ? w - 5 : 3, 1, 3, 2, C.hair);
px(c, p.facing > 0 ? w - 6 : 3, 4, 2, 2, const Color(0xff101018));
px(c, p.facing > 0 ? w - 3 : 0, 4, 1, 2, const Color(0xff101018));
c.restore();
if (p.hasGun && !p.dead) {
final gx = p.facing > 0 ? x + w - 1 : x - 7;
final gy = y + 9;
px(c, gx, gy, 8, 3, C.gun);
px(c, p.facing > 0 ? gx + 6 : gx, gy + 1, 3, 2, const Color(0xff7a7a8c));
px(c, gx + 1, gy + 3, 3, 3, const Color(0xff6a6a7c));
}
}
// ---------------- 实体 ----------------
void drawSave(Canvas c, EntityView e, int t) {
final cx = e.x + e.w / 2, cy = e.y + e.h / 2;
final on = e.active;
final pulse = 1 + sin(t / 180) * 0.09;
if (on) {
final p = Paint()..color = rgba(176, 107, 255, 0.28);
c.drawCircle(Offset(cx, cy), 15 * pulse, p);
} else {
final r = 13 + sin(t / 26) * 2.2;
final sp = Paint()
..color = rgba(138, 106, 184, 0.30 + sin(t / 26) * 0.12)
..style = PaintingStyle.stroke
..strokeWidth = 1.5;
c.drawCircle(Offset(cx, cy), r, sp);
}
final col = on ? C.save : const Color(0xff8f74b5);
final p = Paint()..color = col;
c.drawCircle(Offset(cx - 3, cy - 1), 4.2, p);
c.drawCircle(Offset(cx + 4, cy + 1), 4.2, p);
c.drawCircle(Offset(cx, cy + 5), 4.2, p);
c.drawCircle(Offset(cx + 1, cy - 5), 4.2, p);
if (on) {
c.drawCircle(Offset(cx - 1, cy - 2), 1.6, Paint()..color = const Color(0xffe5d0ff));
}
px(c, cx + 1, cy - 9, 2, 4, const Color(0xff4a8a3a));
}
void drawGoal(Canvas c, EntityView e, int t) {
final x = e.x, y = e.y, w = e.w, h = e.h;
px(c, x + 3, y + 2, w - 6, h - 2, const Color(0xff2a1e3a));
px(c, x + 5, y + 4, w - 10, h - 6, const Color(0xff140d20));
final gr = Rect.fromLTWH(x + 5, y + 4, w - 10, h - 6);
final g = LinearGradient(
begin: _gradAlign(Offset(x, y + h), gr), end: _gradAlign(Offset(x, y), gr),
colors: [rgba(255, 200, 60, 0.55), rgba(255, 200, 60, 0)],
);
c.drawRect(gr, Paint()..shader = g.createShader(gr));
for (int i = 0; i < 3; i++) {
final pp = (((t / 700) + i / 3) % 1);
px(c, x + 7 + ((i * 9) % (w - 12)), y + h - 4 - pp * (h - 8), 2, 2, C.goal);
}
px(c, x + 3, y + 2, w - 6, 2, C.goal);
px(c, x + 3, y + 2, 2, h - 2, const Color(0xffc99a1e));
px(c, x + w - 5, y + 2, 2, h - 2, const Color(0xffc99a1e));
}
void drawGun(Canvas c, EntityView e, int t) {
final x = e.x, y = e.y + sin(t / 250) * 2;
px(c, x + 2, y + 6, 20, 7, const Color(0xff4a4a5c));
px(c, x + 2, y + 6, 20, 2, const Color(0xff8a8aa0));
px(c, x + 18, y + 8, 9, 3, C.gun);
px(c, x + 6, y + 13, 5, 8, const Color(0xff3a3a4c));
px(c, x + 8, y + 19, 3, 3, const Color(0xff2a2a3a));
px(c, x + 24, y + 7, 2, 5, const Color(0xffd0d0e0));
}
void drawApple(Canvas c, EntityView e, int t) {
final cx = e.x + e.w / 2, cy = e.y + e.h / 2 + sin(t / 320 + e.x) * 1.5;
c.drawCircle(Offset(cx, cy), 8, Paint()..color = C.apple);
c.drawCircle(Offset(cx - 3, cy - 3), 3, Paint()..color = const Color(0xffff8095));
px(c, cx - 1, cy - 11, 2, 4, const Color(0xff6b4423));
c.drawOval(Rect.fromLTWH(cx + 4 - 4, cy - 9 - 2, 8, 4), Paint()..color = const Color(0xff3ddc84));
}
void drawWarp(Canvas c, EntityView e, int t) {
final cx = e.x + e.w / 2, cy = e.y + e.h / 2;
for (int i = 0; i < 3; i++) {
final r = 14 - i * 4;
final a = t / (300 + i * 120);
final sp = Paint()
..color = Color.fromARGB(((0.85 - i * 0.2) * 255).round().clamp(0, 255), (170 - i * 30).clamp(0, 255), (90 + i * 40).clamp(0, 255), 255)
..style = PaintingStyle.stroke
..strokeWidth = 2;
c.drawArc(Rect.fromLTWH(cx - r, cy - r, r * 2, r * 2), a, pi * 1.4, false, sp);
}
c.drawCircle(Offset(cx, cy), 5, Paint()..color = rgba(120, 60, 200, 0.35));
}
void drawCamera(Canvas c, EntityView e, int t) {
final x = e.x, y = e.y, w = e.w, h = e.h;
_dashedRect(c, Rect.fromLTWH(x + 1, y + 1, w - 2, h - 2),
rgba(80, 220, 255, 0.35 + sin(t / 300) * 0.15), 1, 4, 4);
final ix = x + w / 2 - 9, iy = y + 6;
px(c, ix, iy + 3, 18, 11, const Color(0xff1e1e2a));
px(c, ix + 2, iy + 5, 14, 7, const Color(0xff3a3a4c));
c.drawCircle(Offset(ix + 9, iy + 8.5), 3.5, Paint()..color = const Color(0xff7ae0ff));
px(c, ix + 12, iy, 5, 3, const Color(0xff1e1e2a));
px(c, ix + 13, iy - 1, 3, 2, const Color(0xffff4060));
}
void drawSign(Canvas c, EntityView e, int t) {
final x = e.x, y = e.y, w = e.w, h = e.h;
px(c, x + w / 2 - 1, y + h - 8, 3, 8, const Color(0xff6b4423));
px(c, x, y + 2, w, h - 10, const Color(0xffc9a86b));
px(c, x + 2, y + 4, w - 4, h - 14, const Color(0xffe8d5a8));
px(c, x + 4, y + 8, w - 8, 2, const Color(0xff8a7550));
px(c, x + 4, y + 13, w - 12, 2, const Color(0xff8a7550));
}
void drawTurret(Canvas c, EntityView e, int t) {
final x = e.x, y = e.y;
const s = TILE;
px(c, x + 2, y + 6, s - 4, s - 6, const Color(0xff4a4a5e));
px(c, x + 4, y + 8, s - 8, s - 12, const Color(0xff6a6a80));
final dirx = e.dir == 'left' ? -1 : e.dir == 'right' ? 1 : 0;
final diry = e.dir == 'up' ? -1 : e.dir == 'down' ? 1 : 0;
final cx = x + s / 2, cy = y + s / 2;
px(c, cx + dirx * 8 - 3, cy + diry * 8 - 3, 7, 7, const Color(0xff2a2a38));
final blink = (t ~/ 300) % 3 == 0;
px(c, cx + dirx * 12 - 2, cy + diry * 12 - 2, 4, 4, blink ? const Color(0xffff4060) : const Color(0xff8a2030));
}
void drawMovingPlatform(Canvas c, MoverView m) {
final x = m.x, y = m.y, w = m.w, h = m.h;
px(c, x, y, w, h, const Color(0xff3a3a4c));
px(c, x + 1, y + 1, w - 2, h - 2, const Color(0xff6a6a84));
px(c, x + 1, y + 1, w - 2, 2, const Color(0xff9a9ab4));
for (double i = 0; i < w; i += 8) px(c, x + i + 2, y + h - 4, 4, 2, const Color(0xff3a3a4c));
_dashedLine(c, Offset(m.ax + m.aw / 2, m.ay + m.ah / 2), Offset(m.bx + m.aw / 2, m.by + m.ah / 2),
rgba(255, 255, 255, 0.10), 1, 3, 5);
}
void drawDropSpike(Canvas c, EntityView e, int t) {
const s = TILE;
if (!e.falling) {
px(c, e.x + 4, e.y + 4, s - 8, s - 8, rgba(74, 74, 92, 0.85));
px(c, e.x + 6, e.y + 6, s - 12, s - 12, rgba(106, 106, 128, 0.85));
} else {
drawSpike(c, e.x, e.y, 2, t);
}
}
void drawBullet(Canvas c, BulletView b) {
px(c, b.x - 1, b.y - 1, b.w + 2, b.h + 2, rgba(255, 240, 120, 0.35));
px(c, b.x, b.y, b.w, b.h, const Color(0xfffff6a0));
px(c, b.x, b.y + b.h - 2, b.w, 2, const Color(0xffffb040));
}
void drawEBullet(Canvas c, BulletView b, int t) {
final cx = b.x + b.w / 2, cy = b.y + b.h / 2;
c.drawCircle(Offset(cx, cy), b.w / 2 + 1, Paint()..color = hex(b.col));
c.drawCircle(Offset(cx - 1.5, cy - 1.5), b.w / 4, Paint()..color = const Color(0xffffffff));
if (b.big) {
final sp = Paint()
..color = rgba(255, 255, 255, 0.4)
..style = PaintingStyle.stroke;
c.drawCircle(Offset(cx, cy), b.w / 2 + 4 + sin(t / 90) * 2, sp);
}
}
// ---------------- 死亡特效 ----------------
void drawDeathRing(Canvas c, DeathFX fx) {
final t = fx.t;
if (t > 14) return;
final r = 4 + t * 1.8;
final a = max(0.0, 1 - t / 14);
final sp = Paint()
..color = rgba(255, 143, 163, a * 0.85)
..style = PaintingStyle.stroke
..strokeWidth = 2;
c.drawCircle(Offset(fx.x, fx.y), r, sp);
c.drawCircle(Offset(fx.x, fx.y), r * 0.55, Paint()..color = rgba(255, 92, 122, a * 0.3));
}
void drawDeathMark(Canvas c, DeathMark dm) {
const s = 4.0;
final sp = Paint()
..color = rgba(0, 0, 0, 0.55)
..style = PaintingStyle.stroke
..strokeWidth = 3
..strokeCap = StrokeCap.round;
_line(c, sp, dm.x - s, dm.y - s, dm.x + s, dm.y + s);
_line(c, sp, dm.x + s, dm.y - s, dm.x - s, dm.y + s);
final sp2 = Paint()
..color = const Color(0xffff2535)
..style = PaintingStyle.stroke
..strokeWidth = 2
..strokeCap = StrokeCap.round;
_line(c, sp2, dm.x - s, dm.y - s, dm.x + s, dm.y + s);
_line(c, sp2, dm.x + s, dm.y - s, dm.x - s, dm.y + s);
}
// ---------------- 文本 ----------------
void drawText(Canvas c, String text, double x, double y,
{Color color = const Color(0xffffffff), double size = 11, TextAlign align = TextAlign.center}) {
final tp = TextPainter(
text: TextSpan(text: text, style: TextStyle(color: color, fontSize: size, fontFamily: 'Courier')),
textAlign: align,
textDirection: TextDirection.ltr,
);
tp.layout();
tp.paint(c, Offset(x - (align == TextAlign.center ? tp.width / 2 : 0), y));
}
// ---------------- 虚线辅助 ----------------
void _dashedLine(Canvas c, Offset a, Offset b, Color col, double width, double dash, double gap) {
final p = Paint()..color = col..strokeWidth = width;
final dx = b.dx - a.dx, dy = b.dy - a.dy;
final dist = sqrt(dx * dx + dy * dy);
if (dist == 0) return;
final ux = dx / dist, uy = dy / dist;
double d = 0;
while (d < dist) {
final d2 = min(d + dash, dist);
c.drawLine(Offset(a.dx + ux * d, a.dy + uy * d), Offset(a.dx + ux * d2, a.dy + uy * d2), p);
d += dash + gap;
}
}
void _dashedRect(Canvas c, Rect r, Color col, double width, double dash, double gap) {
_dashedLine(c, r.topLeft, r.topRight, col, width, dash, gap);
_dashedLine(c, r.topRight, r.bottomRight, col, width, dash, gap);
_dashedLine(c, r.bottomRight, r.bottomLeft, col, width, dash, gap);
_dashedLine(c, r.bottomLeft, r.topLeft, col, width, dash, gap);
}
void _line(Canvas c, Paint p, double x1, double y1, double x2, double y2) {
c.drawLine(Offset(x1, y1), Offset(x2, y2), p);
}
// 把像素空间的绝对 Offset 换算成 Flutter LinearGradient 所需的 Alignment。
Alignment _gradAlign(Offset abs, Rect r) =>
Alignment(((abs.dx - r.left) - r.width / 2) / (r.width / 2),
((abs.dy - r.top) - r.height / 2) / (r.height / 2));
+58
View File
@@ -0,0 +1,58 @@
// 渲染层只读的"视图"接口:让 sprites.dart 不依赖完整游戏类,避免循环依赖。
import 'dart:ui' show Color;
abstract class PlayerView {
double get x;
double get y;
double get w;
double get h;
double get cx;
double get cy;
bool get grounded;
int get facing;
bool get hasGun;
bool get dead;
}
abstract class EntityView {
double get x;
double get y;
double get w;
double get h;
bool get active;
bool get falling;
String get dir;
}
abstract class MoverView {
double get x;
double get y;
double get w;
double get h;
double get ax;
double get ay;
double get aw;
double get ah;
double get bx;
double get by;
}
abstract class BulletView {
double get x;
double get y;
double get w;
double get h;
String get col;
bool get big;
}
class DeathFX {
double x, y;
int t;
DeathFX(this.x, this.y, this.t);
}
class DeathMark {
double x, y;
DeathMark(this.x, this.y);
}
+469
View File
@@ -0,0 +1,469 @@
// 游戏循环 / 状态机(对应 web 版 main.js)。
// 用固定 60Hz 时间步长驱动已移植的纯 Canvas 游戏逻辑,保证手感 1:1。
// 渲染在 800×608 虚拟分辨率下完成,由 GameCanvas 做信箱(letterbox)缩放。
import 'dart:ui' show Canvas, Color, Paint, Path, Rect, Offset, TextAlign, TextDirection;
import 'dart:math' show sin;
import 'package:flutter/widgets.dart'
show TextPainter, TextSpan, TextStyle, LinearGradient, Alignment, FontWeight, ValueNotifier;
import 'package:iwbtc/src/core/input.dart';
import 'package:iwbtc/src/core/save.dart';
import 'package:iwbtc/src/core/util.dart';
import 'package:iwbtc/src/audio/sfx.dart';
import 'package:iwbtc/src/game/level.dart';
import 'package:iwbtc/src/game/player.dart';
import 'package:iwbtc/src/game/camera.dart';
import 'package:iwbtc/src/game/levels.dart' show LEVELS;
const double W = 800.0;
const double H = 608.0;
const double STEP = 1000 / 60;
/// 编辑器试玩回调(避免 World <-> EditorController 循环依赖)。
abstract class LevelTestRunner {
void startLevel(Map<String, dynamic> data, int idx);
}
/// 渲染宿主回调:World 通过它把界面状态推给 Flutter 层。
abstract class GameHost {
/// 切换可见界面:''=游戏中,'scr-title'|'scr-levels'|'scr-settings'|
/// 'scr-pause'|'scr-result'|'scr-editor'。
void onShowScreen(String name);
void onHud(Map<String, dynamic> hud);
void onBossBar(dynamic boss);
void onToast(String text);
void onResult(Map<String, dynamic> r);
/// 暂停菜单「退出」按钮文案随来源变化。
void onPauseMenu(String label);
/// 进入编辑器界面。
void editorShow();
void touchShow();
void touchHide();
void touchSetGun(bool has);
}
class World implements LevelTestRunner {
final GameHost host;
/// 游戏状态:menu | playing | paused | result | editor
///
/// 用 ValueNotifier 承载,是因为 Flutter 侧界面(HUD 显隐)必须能监听它重建:
/// 若只存普通字段,`startLevel()` 改完状态不会触发 AppRoot 重建,HUD 永远不出现。
/// (web 版由主循环每帧直接改 DOM,不存在这个问题。)
final ValueNotifier<String> stateN = ValueNotifier<String>('menu');
String get state => stateN.value;
set state(String v) {
if (stateN.value != v) stateN.value = v;
}
String screen = 'scr-title';
Level? level;
Player? player;
Camera? cam;
Map<String, dynamic>? data;
int idx = -1;
List<Level>? map;
int face = 0;
bool fromEditor = false;
int deaths = 0;
int ms = 0;
int hudTick = 0;
int time = 0;
String? lastLevelId;
bool useTouch = false;
double _acc = 0;
String _lastToast = '';
World(this.host);
// ---------------- 关卡流程 ----------------
Map<String, dynamic> faceData(Map<String, dynamic> map, Map<String, dynamic> f, int i) {
final grid = (f['grid'] as List?)?.cast<String>() ?? <String>[];
return <String, dynamic>{
'id': (map['id'] ?? 'map').toString() + '-' + i.toString(),
'name': f['name'] ?? ('面' + (i + 1).toString()),
'type': 'custom',
'width': f['width'] ?? (grid.isNotEmpty ? grid[0].length : 25),
'grid': f['grid'],
'doors': f['doors'] ?? <dynamic>[],
'bg': 'custom',
'hint': '',
'messages': <dynamic>[],
};
}
void startLevel(Map<String, dynamic> data, int idx) {
Sfx.instance.unlock();
this.data = data;
this.idx = idx;
lastLevelId = data['id']?.toString();
final faces = data['faces'];
if (faces is List && faces.isNotEmpty) {
map = <Level>[];
for (int i = 0; i < faces.length; i++) {
final L = Level(faceData(data, faces[i] as Map<String, dynamic>, i));
L.faceIndex = i;
map!.add(L);
}
face = ((data['startFace'] as int?) ?? 0).clamp(0, map!.length - 1);
} else {
map = <Level>[Level(data)];
face = 0;
}
level = map![face];
player = Player(level!);
cam = Camera(W, H);
cam!.reset(level!, player!);
player!.onDeath = () {
deaths++;
};
player!.onClear = () {
finish();
};
player!.onPortal = (e) {
doPortal(e as Entity);
};
player!.onRespawnFace = () {
doRespawnFace();
};
for (final b in level!.bosses) {
b.onDefeat = () {
finish();
};
}
deaths = 0;
ms = 0;
hudTick = 0;
state = 'playing';
host.onShowScreen('');
if (useTouch) host.touchShow();
host.onToast('');
final hint = data['hint'];
if (hint is String && hint.isNotEmpty) host.onToast(hint);
}
void placeOnDoor(Entity dest) {
final P = player!;
P.x = dest.x + dest.w / 2 - P.w / 2;
P.y = dest.y + dest.h - P.h;
P.vx = 0;
P.vy = 0;
P.jumps = 0;
}
void doPortal(Entity e) {
final to = e.to;
if (to is! Map) return;
final tp = (to['face'] as int? ?? face).clamp(0, map!.length - 1);
if (tp == face) {
final dest = level!.entities
.where((o) => o.type == 'warp' && o.doorId == to['door'])
.firstOrNull;
if (dest != null) {
placeOnDoor(dest);
dest.cool = 40;
}
Sfx.instance.warp();
} else {
switchFace(tp, to['door']);
}
}
void switchFace(int f, dynamic doorId) {
face = f;
level = map![f];
level!.reset(true);
player!.level = level!;
final dest = level!.entities
.where((o) => o.type == 'warp' && o.doorId == doorId)
.firstOrNull;
if (dest != null) {
placeOnDoor(dest);
dest.cool = 40;
} else {
final sp = level!.spawn;
player!.x = sp.x;
player!.y = sp.y;
player!.vx = 0;
player!.vy = 0;
}
cam!.reset(level!, player!);
Sfx.instance.warp();
level!.particles.spawn(player!.cx, player!.cy, 12,
colors: const ['#a86bff', '#fff'], speed: 2.5, life: 24);
}
void doRespawnFace() {
final sp = player!.savePoint;
if (sp != null && sp.face != face) {
face = sp.face;
level = map![face];
level!.reset(true);
player!.level = level!;
cam!.reset(level!, player!);
}
}
void restart() {
if (data == null) return;
final d = deaths;
startLevel(data!, idx);
deaths = d;
}
void suicide() {
final P = player;
if (P == null || state != 'playing') return;
if (P.dead) {
P.respawn();
} else {
P.deathBurst();
P.respawn();
}
}
void shoot() {
final P = player;
if (state != 'playing' || P == null || P.dead || !P.hasGun || P.shootCool != 0) return;
P.shoot();
}
void finish() {
if (state != 'playing') return;
state = 'result';
host.touchHide();
Sfx.instance.goal();
final apples = player!.apples;
final total = level!.totalApples;
final better = Save.clear(data!['id'].toString(), deaths, ms, apples);
final hasNext = idx >= 0 && idx + 1 < LEVELS.length;
host.onResult(<String, dynamic>{
'name': data!['name'],
'ms': ms,
'deaths': deaths,
'apples': apples,
'totalApples': total,
'better': better,
'hasNext': hasNext,
});
host.onShowScreen('scr-result');
}
void nextLevel() {
if (idx >= 0 && idx + 1 < LEVELS.length) {
startLevel(LEVELS[idx + 1], idx + 1);
} else {
toLevels();
}
}
void toLevels() {
state = 'menu';
screen = 'scr-levels';
host.onShowScreen('scr-levels');
host.touchHide();
}
void toTitle() {
state = 'menu';
screen = 'scr-title';
host.onShowScreen('scr-title');
host.touchHide();
}
void toEditor() {
state = 'editor';
screen = 'scr-editor';
host.onShowScreen('scr-editor');
host.touchHide();
host.editorShow();
}
void pause() {
if (state != 'playing') return;
state = 'paused';
// 暂停菜单末项:从编辑器试玩进来 → 「返回编辑器」;否则 → 「返回关卡选择」
host.onPauseMenu(fromEditor ? '返回编辑器' : '返回关卡选择');
host.onShowScreen('scr-pause');
host.touchHide();
}
void resume() {
if (state == 'paused') {
state = 'playing';
host.onShowScreen('');
if (useTouch) host.touchShow();
}
}
// ---------------- 更新 ----------------
void update() {
final P = player!;
if (Input.hit('suicide')) {
suicide();
return;
}
if (Input.hit('restart')) {
restart();
return;
}
if (Input.hit('pause')) {
pause();
return;
}
level!.update(1, P, cam!);
for (final b in level!.bosses) b.update(1, P);
P.update(1);
if (P.justRespawned) {
if (P.savePoint != null && P.savePoint!.face != face) doRespawnFace();
cam!.snapTo(P, level!);
P.justRespawned = false;
}
cam!.update(level!, P, 1);
ms += STEP.round();
time++;
if (++hudTick % 5 == 0) {
final L = level!;
host.onHud(<String, dynamic>{
'name': (data!['name'] ?? 'STAGE') +
(map!.length > 1 ? ' · 面${face + 1}/${map!.length}' : ''),
'deaths': deaths,
'ms': ms,
'apples': P.apples,
'totalApples': L.totalApples,
'hasGun': P.hasGun,
'save': P.saveIdx,
'totalSaves': L.totalSaves,
});
host.touchSetGun(P.hasGun);
final boss = L.bosses.where((b) => !b.dead).firstOrNull ?? L.bosses.firstOrNull;
host.onBossBar(L.bosses.isNotEmpty ? boss : null);
_pushToast(L.toast ?? '');
}
}
void _pushToast(String t) {
if (t != _lastToast) {
_lastToast = t;
host.onToast(t);
}
}
// ---------------- 循环驱动(由 Ticker 调用) ----------------
void tick(int dtMs) {
int dt = dtMs;
if (dt > 200) dt = 200;
_acc += dt;
int guard = 0;
while (_acc >= STEP && guard++ < 6) {
_acc -= STEP;
if (state == 'playing') {
update();
} else if (state == 'paused' && Input.hit('pause')) {
resume();
}
}
Input.endFrame();
// toast 推送必须在 Ticker 回调阶段(transientCallbacks)完成,
// 不能放进 render() —— paint 阶段改被 ValueListenableBuilder 监听的
// notifier 会触发 "Build scheduled during frame"(晚一帧且刷警告)。
_pushToast(level?.toast ?? '');
if (state != 'playing') time++;
}
// ---------------- 渲染(800×608 虚拟空间) ----------------
void render(Canvas c) {
final t = time;
if (state == 'playing' || state == 'paused' || state == 'result') {
final L = level!;
final P = player!;
final C = cam!;
c.drawRect(Rect.fromLTWH(0, 0, W, H), Paint()..color = hex('#0a0a14'));
L.drawBg(c, C, t);
c.save();
C.apply(c);
L.draw(c, C, t);
for (final b in L.bosses) b.draw(c, t);
P.draw(c);
c.restore();
L.drawOverlay(c, C, t);
// 死亡中:提示手动重生
if (state == 'playing' && P.dead) {
final blink = sin(t / 12) > -0.3;
final tp = TextPainter(
text: TextSpan(
text: useTouch ? '按右下角重生键' : '按 R 重生',
style: TextStyle(
color: const Color(0xffff8fa3).withValues(alpha: (blink ? 1.0 : 0.3) * 255),
fontSize: 20,
fontWeight: FontWeight.bold,
fontFamily: 'monospace',
),
),
textAlign: TextAlign.center,
textDirection: TextDirection.ltr,
);
tp.layout();
tp.paint(c, Offset(W / 2 - tp.width / 2, H / 2 - 36 - tp.height / 2));
final sub = TextPainter(
text: TextSpan(
text: 'Backspace 整关重来',
style: TextStyle(
color: const Color(0xffa8a8c0).withValues(alpha: 0.7 * 255),
fontSize: 12,
fontFamily: 'monospace',
),
),
textAlign: TextAlign.center,
textDirection: TextDirection.ltr,
);
sub.layout();
sub.paint(c, Offset(W / 2 - sub.width / 2, H / 2 - 12 - sub.height / 2));
}
} else {
drawTitleBg(c);
}
}
void drawTitleBg(Canvas c) {
final g = LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: <Color>[hex('#181828'), hex('#08080f')],
).createShader(Rect.fromLTWH(0, 0, W, H));
c.drawRect(Rect.fromLTWH(0, 0, W, H), Paint()..shader = g);
for (int i = 0; i < 26; i++) {
final double x = ((i * 137 + time * 0.15) % (W + 60)) - 30;
final double y = 120 + ((i * 71) % (H - 160)).toDouble();
final col = i % 3 == 0 ? hex('#3a3a4c') : hex('#2a2a38');
final p = Path()
..moveTo(x, y + 18)
..lineTo(x + 11, y)
..lineTo(x + 22, y + 18)
..close();
c.drawPath(p, Paint()..color = col.withValues(alpha: 0.5 * 255));
}
c.drawRect(Rect.fromLTWH(0, H - 30, W, 30), Paint()..color = hex('#1a1a26'));
final spikePaint = Paint()..color = hex('#c8c8d8');
for (double x = 0; x < W; x += 22) {
final p = Path()
..moveTo(x, H - 4)
..lineTo(x + 11, H - 26)
..lineTo(x + 22, H - 4)
..close();
c.drawPath(p, spikePaint);
}
}
}
File diff suppressed because it is too large. Load diff
+95
View File
@@ -0,0 +1,95 @@
// 游戏画布:把 World 在 800×608 虚拟分辨率下的渲染做信箱(letterbox)缩放。
import 'dart:ui' show Canvas, Rect, Paint, Color;
import 'dart:math' show min;
import 'package:flutter/widgets.dart';
import 'package:flutter/foundation.dart' show debugPrint;
import 'package:iwbtc/src/core/app_error.dart' show AppError;
import 'package:iwbtc/src/game/world.dart' show World, W, H;
/// 计算 800×608 虚拟画面在给定屏幕尺寸下的信箱矩形(居中、等比缩放)。
///
/// 画布和虚拟手柄都用它:横屏手机宽高比远大于画面比例,两侧会出现黑边,
/// 手柄按钮必须锚定在**画面区域**内,否则会漂到黑边上(手指按到的位置和
/// 屏幕上看到的游戏内容对不上)。尺寸非法(尚未布局完成)时返回 Rect.zero。
Rect letterboxRect(Size size) {
if (!size.width.isFinite || !size.height.isFinite) return Rect.zero;
if (size.width <= 0 || size.height <= 0) return Rect.zero;
final s = min(size.width / W, size.height / H);
if (!s.isFinite || s <= 0) return Rect.zero;
final double w = W * s;
final double h = H * s;
return Rect.fromLTWH(
(size.width - w) / 2, (size.height - h) / 2, w, h);
}
class GameCanvas extends StatelessWidget {
final World world;
final Listenable repaint;
const GameCanvas({super.key, required this.world, required this.repaint});
@override
Widget build(BuildContext context) {
// 用 SizedBox.expand 明确占满父级,避免 CustomPaint 使用 Size.infinite
// 时在部分平台上拿到零尺寸/无穷尺寸,算出 scale = 0 而整屏消失(表现为黑屏)。
return SizedBox.expand(
child: CustomPaint(
painter: _GamePainter(world, repaint),
),
);
}
}
class _GamePainter extends CustomPainter {
final World world;
_GamePainter(this.world, Listenable repaint) : super(repaint: repaint);
static bool _loggedFirstPaint = false;
static int _renderErrors = 0;
@override
void paint(Canvas canvas, Size size) {
// 信箱外框(黑色)
canvas.drawRect(
Rect.fromLTWH(0, 0, size.width, size.height),
Paint()..color = const Color(0xff000000));
// 首帧可能尚未完成布局,尺寸为 0 时不要绘制内容(letterboxRect 返回 zero)。
final rect = letterboxRect(size);
if (rect.isEmpty) return;
canvas.save();
canvas.translate(rect.left, rect.top);
canvas.scale(rect.width / W, rect.width / W);
canvas.clipRect(Rect.fromLTWH(0, 0, W, H));
canvas.drawRect(
Rect.fromLTWH(0, 0, W, H), Paint()..color = const Color(0xff0a0a14));
// 渲染异常原本会被静默吞掉,画面变成纯黑。
// 偶发的一帧异常不该让整局报废 —— 只记录日志;但若连续多帧都失败
// (比如某张地图/某种渲染路径真有问题),才升级成占屏错误以便真机诊断。
try {
world.render(canvas);
} catch (e, s) {
_renderErrors++;
if (_renderErrors <= 3 || _renderErrors == 60) {
AppError.warn('游戏渲染异常(第 $_renderErrors 次,已跳过该帧)', '$e\n$s');
}
if (_renderErrors == 60) {
AppError.report('游戏渲染持续失败(连续 60 帧)', '$e\n$s');
}
} finally {
canvas.restore();
}
// 以有效尺寸完成一次真实渲染后才记录,便于真机确认渲染管线在跑。
if (!_loggedFirstPaint) {
_loggedFirstPaint = true;
debugPrint(
'[boot] canvas rendered: screen=${size.width}x${size.height} '
'stage=${rect.width}x${rect.height} at (${rect.left},${rect.top}) '
'state=${world.state}');
}
}
@override
bool shouldRepaint(_GamePainter old) => true;
}
+147
View File
@@ -0,0 +1,147 @@
// HUD / BOSS 血条 / Toast 覆盖层(对应 web 版 ui.js 的 hud/bossBar/toast)。
import 'package:flutter/widgets.dart';
import 'package:iwbtc/src/core/util.dart' show fmtTime;
import 'package:iwbtc/src/game/boss.dart' show Boss;
/// 顶部 HUD + BOSS 血条 + Toast。
class HudOverlay extends StatelessWidget {
final ValueNotifier<Map<String, dynamic>> hud;
final ValueNotifier<dynamic> boss;
final ValueNotifier<String> toast;
const HudOverlay(
{super.key, required this.hud, required this.boss, required this.toast});
@override
Widget build(BuildContext context) {
// 注意:Positioned 必须是 Stack 的**直接子级**。
// 此前把它写在 ValueListenableBuilder 的返回值里(隔着一层),定位数据被丢弃,
// 覆盖层会像真机黑屏那次一样横向塌缩。故此处一律 Positioned 在外、监听在内。
return Stack(
children: <Widget>[
Positioned(
top: 8,
left: 8,
right: 8,
child: ValueListenableBuilder<Map<String, dynamic>>(
valueListenable: hud,
builder: (_, Map<String, dynamic> h, __) {
final totalApples = h['totalApples'] ?? 0;
final totalSaves = h['totalSaves'] ?? 0;
final save = h['save'] ?? 0;
return Row(
children: <Widget>[
_box(h['name'] ?? '', flex: 1),
_box('DEATH ' + (h['deaths'] ?? 0).toString()),
_box(fmtTime(h['ms'] ?? 0)),
// 文案与 web 版 ui.js 保持一致(中文)。
if (totalApples > 0) _box('苹果 ${h['apples'] ?? 0}/$totalApples'),
if (totalSaves > 0) _box('存档 $save/$totalSaves', on: save > 0),
],
);
},
),
),
Positioned(
top: 40,
left: 24,
right: 24,
child: ValueListenableBuilder<dynamic>(
valueListenable: boss,
builder: (_, dynamic b, __) =>
b == null ? const SizedBox() : _BossBar(boss: b as Boss),
),
),
Positioned(
top: 66,
left: 0,
right: 0,
child: ValueListenableBuilder<String>(
valueListenable: toast,
builder: (_, String t, __) => t.isEmpty
? const SizedBox()
: Center(
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 14, vertical: 6),
decoration: BoxDecoration(
color: const Color(0xb0000000),
borderRadius: BorderRadius.circular(6),
),
child: Text(t,
style: const TextStyle(
color: Color(0xffffe9a8),
fontSize: 14,
fontFamily: 'monospace')),
),
),
),
),
],
);
}
static Widget _box(String text, {double flex = 0, bool on = false}) {
final w = Expanded(
flex: flex > 0 ? 1 : 0,
child: Container(
margin: const EdgeInsets.only(right: 6),
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: const Color(0x99000000),
borderRadius: BorderRadius.circular(4),
border: on
? Border.all(color: const Color(0xffb06bff))
: null,
),
child: Text(text,
style: const TextStyle(
color: Color(0xffd8d8e8),
fontSize: 12,
fontFamily: 'monospace')),
),
);
return w;
}
}
class _BossBar extends StatelessWidget {
final Boss boss;
const _BossBar({required this.boss});
@override
Widget build(BuildContext context) {
final ratio = (boss.hp / boss.maxHp).clamp(0.0, 1.0);
// 只负责内容,定位由外层 Stack 的 Positioned 提供。
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text(boss.name,
style: const TextStyle(
color: Color(0xffffffff),
fontSize: 13,
fontFamily: 'monospace',
fontWeight: FontWeight.bold)),
const SizedBox(height: 4),
Container(
height: 10,
decoration: BoxDecoration(
color: const Color(0x66000000),
borderRadius: BorderRadius.circular(5),
border: Border.all(color: const Color(0xffffffff)),
),
child: FractionallySizedBox(
alignment: Alignment.centerLeft,
widthFactor: ratio,
child: Container(
decoration: BoxDecoration(
color: const Color(0xffff5c7a),
borderRadius: BorderRadius.circular(5),
),
),
),
),
],
);
}
}
File diff suppressed because it is too large. Load diff
+190
View File
@@ -0,0 +1,190 @@
// 移动端虚拟手柄(对应 web 版 touch.js):方向 + 跳跃 + 射击/下蹲 + 暂停/重生。
import 'dart:async';
import 'package:flutter/widgets.dart';
import 'package:iwbtc/src/core/input.dart' show Input;
import 'package:iwbtc/src/ui/touch_layout.dart'
show BUTTONS, metaSettings;
import 'package:iwbtc/src/game/world.dart' show World, W;
import 'package:iwbtc/src/ui/game_canvas.dart' show letterboxRect;
class TouchControls extends StatelessWidget {
final World world;
final ValueNotifier<bool> visible;
final ValueNotifier<bool> hasGun;
const TouchControls(
{super.key,
required this.world,
required this.visible,
required this.hasGun});
@override
Widget build(BuildContext context) {
final touch = metaSettings()['touch'] as Map;
return ValueListenableBuilder<bool>(
valueListenable: visible,
builder: (_, bool v, __) {
if (!v) return const SizedBox();
// 按钮锚定在「信箱缩放后的画面矩形」内,而不是整块屏幕:
// 横屏时两侧是黑边,若按全屏百分比定位,左/右/跳跃键会飘到黑边上。
return LayoutBuilder(
builder: (_, BoxConstraints cons) {
final area = letterboxRect(Size(cons.maxWidth, cons.maxHeight));
return Stack(
children: <Widget>[
for (final b in BUTTONS)
_TouchButton(
world: world,
keyName: b.key,
label: b.label,
label2: b.label2,
shape: b.shape,
cx: (touch[b.key]?['cx'] ?? 50).toDouble(),
cy: (touch[b.key]?['cy'] ?? 50).toDouble(),
size: (touch[b.key]?['size'] ?? 50).toDouble(),
hasGun: hasGun,
area: area,
),
],
);
},
);
},
);
}
}
class _TouchButton extends StatefulWidget {
final World world;
final String keyName;
final String label;
final String? label2;
final String shape;
final double cx;
final double cy;
final double size;
final ValueNotifier<bool> hasGun;
/// 画面区域(信箱缩放后的矩形);按钮在其中按百分比定位。
/// 空 Rect 表示尚未完成布局,此时不渲染。
final Rect area;
const _TouchButton(
{required this.world,
required this.keyName,
required this.label,
this.label2,
required this.shape,
required this.cx,
required this.cy,
required this.size,
required this.hasGun,
required this.area});
@override
State<_TouchButton> createState() => _TouchButtonState();
}
class _TouchButtonState extends State<_TouchButton> {
Timer? _shootTimer;
bool _pressed = false;
@override
void dispose() {
_shootTimer?.cancel();
super.dispose();
}
void _press() {
if (_pressed) return;
_pressed = true;
final a = widget.keyName;
if (a == 'shoot') {
widget.world.shoot();
_shootTimer = Timer.periodic(const Duration(milliseconds: 140), (_) {
widget.world.shoot();
});
} else if (a == 'pause') {
widget.world.pause();
} else if (a == 'suicide') {
widget.world.suicide();
} else {
Input.setTouch(a, true);
}
setState(() {});
}
void _release() {
if (!_pressed) return;
_pressed = false;
_shootTimer?.cancel();
_shootTimer = null;
final a = widget.keyName;
if (a == 'shoot' || a == 'pause' || a == 'suicide') {
// 瞬时动作,无需松开处理
} else {
Input.setTouch(a, false);
}
setState(() {});
}
@override
Widget build(BuildContext context) {
final Rect area = widget.area;
if (area.isEmpty) return const SizedBox.shrink();
// 按钮尺寸随画面缩放等比放大:默认数值是按 800 宽的画面设计的,
// 大屏/高 DPI 下直接套用会显得过小(反之亦然);限制在合理区间避免失控。
final double k = (area.width / W).clamp(0.6, 2.6);
final double sz = widget.size * k;
final left = area.left + widget.cx / 100 * area.width - sz / 2;
final top = area.top + widget.cy / 100 * area.height - sz / 2;
final isJump = widget.shape == 'jump';
return Positioned(
left: left,
top: top,
width: sz,
height: sz,
child: Listener(
onPointerDown: (_) => _press(),
onPointerUp: (_) => _release(),
onPointerCancel: (_) => _release(),
child: ValueListenableBuilder<bool>(
valueListenable: widget.hasGun,
builder: (_, __, ___) {
final off = widget.keyName == 'shoot' && !widget.hasGun.value;
return Opacity(
opacity: off ? 0.3 : 1,
child: Container(
decoration: BoxDecoration(
color: _pressed
? const Color(0x88ffffff)
: const Color(0x444a5a7a),
shape: isJump ? BoxShape.circle : BoxShape.rectangle,
borderRadius: isJump ? null : BorderRadius.circular(sz * 0.3),
border: Border.all(
color: const Color(0xff7ae0ff), width: 2),
),
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text(widget.label,
style: TextStyle(
color: const Color(0xffffffff),
fontSize: sz * 0.32,
fontWeight: FontWeight.bold)),
if (widget.label2 != null)
Text(widget.label2!,
style: TextStyle(
color: const Color(0xffcfe8ff),
fontSize: sz * 0.16)),
],
),
),
),
);
},
),
),
);
}
}
+104
View File
@@ -0,0 +1,104 @@
// 触屏手柄按钮定义与默认布局(对应 web 版 settings.js 的 BUTTONS / DEFAULT_TOUCH)。
// 坐标 cx/cy 为相对舞台中心的百分比,size 为按钮边长(px)。
import 'package:iwbtc/src/core/save.dart';
class TouchBtn {
final String key;
final String label;
final String shape;
final String? label2;
final double cx;
final double cy;
final double size;
const TouchBtn(this.key, this.label, this.shape, this.cx, this.cy, this.size,
[this.label2]);
}
const List<TouchBtn> BUTTONS = <TouchBtn>[
TouchBtn('left', '◀', 'round', 12, 80, 62),
TouchBtn('right', '▶', 'round', 24, 80, 62),
TouchBtn('jump', '跳', 'jump', 88, 84, 94),
TouchBtn('shoot', '🔫', 'item', 79, 72, 54, '射击'),
TouchBtn('down', '▼', 'item', 90, 72, 54, '下蹲'),
TouchBtn('pause', '❚❚', 'sm', 81, 58, 42),
TouchBtn('suicide', '↺', 'sm', 90, 58, 42),
];
const List<List<String>> KEY_ACTIONS = <List<String>>[
<String>['left', '左移'],
<String>['right', '右移'],
<String>['up', '上(上浮)'],
<String>['jump', '跳跃'],
<String>['shoot', '射击'],
<String>['suicide', '自杀 / 重生'],
<String>['restart', '整关重来'],
<String>['pause', '暂停'],
];
Map<String, dynamic> defaultTouch() {
final m = <String, dynamic>{};
for (final b in BUTTONS) {
m[b.key] = <String, dynamic>{
'cx': b.cx,
'cy': b.cy,
'size': b.size,
};
}
return m;
}
Map<String, dynamic> defaultSettings() => <String, dynamic>{
'username': '',
'touch': defaultTouch(),
'keys': defaultBindings(),
};
Map<String, List<String>> defaultBindings() => <String, List<String>>{
'left': <String>['ArrowLeft', 'KeyA'],
'right': <String>['ArrowRight', 'KeyD'],
'up': <String>['ArrowUp', 'KeyW'],
'down': <String>['ArrowDown', 'KeyS'],
'jump': <String>['ShiftLeft', 'ShiftRight', 'Space', 'KeyZ', 'KeyN'],
'shoot': <String>['KeyX', 'KeyJ', 'KeyC', 'KeyM'],
'suicide': <String>['KeyR'],
'restart': <String>['Backspace'],
'pause': <String>['Escape', 'KeyP'],
};
/// 读取/补全存档里的设置对象(保证 touch / keys 字段齐全)。
Map<String, dynamic> metaSettings() {
final s = Save.settings();
if (s['username'] is! String) s['username'] = '';
if (s['touch'] is! Map) s['touch'] = defaultTouch();
if (s['keys'] is! Map) s['keys'] = defaultBindings();
return s;
}
String keyLabel(String code) {
const Map<String, String> M = <String, String>{
'Space': '空格',
'ShiftLeft': 'Shift',
'ShiftRight': 'Shift',
'ControlLeft': 'Ctrl',
'ControlRight': 'Ctrl',
'AltLeft': 'Alt',
'AltRight': 'Alt',
'Tab': 'Tab',
'Backspace': '⌫',
'Escape': 'Esc',
'Enter': 'Enter',
'ArrowLeft': '←',
'ArrowRight': '→',
'ArrowUp': '↑',
'ArrowDown': '↓',
'MetaLeft': 'Win',
'MetaRight': 'Win',
'ContextMenu': '菜单',
};
if (M.containsKey(code)) return M[code]!;
if (code.startsWith('Key')) return code.substring(3);
if (code.startsWith('Digit')) return code.substring(5);
if (code.startsWith('Numpad')) return '小键' + code.substring(6);
if (RegExp(r'^F\d{1,2}$').hasMatch(code)) return code;
return code;
}
+1
View File
@@ -0,0 +1 @@
flutter/ephemeral
+128
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 "iwbtc")
# The unique GTK application identifier for this application. See:
# https://wiki.gnome.org/HowDoI/ChooseApplicationID
set(APPLICATION_ID "com.iwbtc.iwbtc")
# Explicitly opt in to modern CMake behaviors to avoid warnings with recent
# versions of CMake.
cmake_policy(SET CMP0063 NEW)
# Load bundled libraries from the lib/ directory relative to the binary.
set(CMAKE_INSTALL_RPATH "$ORIGIN/lib")
# Root filesystem for cross-building.
if(FLUTTER_TARGET_PLATFORM_SYSROOT)
set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT})
set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT})
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
endif()
# Define build configuration options.
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
set(CMAKE_BUILD_TYPE "Debug" CACHE
STRING "Flutter build mode" FORCE)
set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS
"Debug" "Profile" "Release")
endif()
# Compilation settings that should be applied to most targets.
#
# Be cautious about adding new options here, as plugins use this function by
# default. In most cases, you should add new options to specific targets instead
# of modifying this function.
function(APPLY_STANDARD_SETTINGS TARGET)
target_compile_features(${TARGET} PUBLIC cxx_std_14)
target_compile_options(${TARGET} PRIVATE -Wall -Werror)
target_compile_options(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:-O3>")
target_compile_definitions(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:NDEBUG>")
endfunction()
# Flutter library and tool build rules.
set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter")
add_subdirectory(${FLUTTER_MANAGED_DIR})
# System-level dependencies.
find_package(PkgConfig REQUIRED)
pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
# Application build; see runner/CMakeLists.txt.
add_subdirectory("runner")
# Run the Flutter tool portions of the build. This must not be removed.
add_dependencies(${BINARY_NAME} flutter_assemble)
# Only the install-generated bundle's copy of the executable will launch
# correctly, since the resources must in the right relative locations. To avoid
# people trying to run the unbundled copy, put it in a subdirectory instead of
# the default top-level location.
set_target_properties(${BINARY_NAME}
PROPERTIES
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run"
)
# Generated plugin build rules, which manage building the plugins and adding
# them to the application.
include(flutter/generated_plugins.cmake)
# === Installation ===
# By default, "installing" just makes a relocatable bundle in the build
# directory.
set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle")
if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE)
endif()
# Start with a clean build bundle directory every time.
install(CODE "
file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\")
" COMPONENT Runtime)
set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data")
set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib")
install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}"
COMPONENT Runtime)
install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
COMPONENT Runtime)
install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES})
install(FILES "${bundled_library}"
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
endforeach(bundled_library)
# Copy the native assets provided by the build.dart from all packages.
set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/linux/")
install(DIRECTORY "${NATIVE_ASSETS_DIR}"
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
# Fully re-copy the assets directory on each build to avoid having stale files
# from a previous install.
set(FLUTTER_ASSET_DIR_NAME "flutter_assets")
install(CODE "
file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\")
" COMPONENT Runtime)
install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}"
DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime)
# Install the AOT library on non-Debug builds only.
if(NOT CMAKE_BUILD_TYPE MATCHES "Debug")
install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
endif()
+88
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}
)
@@ -0,0 +1,15 @@
//
// Generated file. Do not edit.
//
// clang-format off
#include "generated_plugin_registrant.h"
#include <audioplayers_linux/audioplayers_linux_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) audioplayers_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "AudioplayersLinuxPlugin");
audioplayers_linux_plugin_register_with_registrar(audioplayers_linux_registrar);
}
@@ -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_
+25
View File
@@ -0,0 +1,25 @@
#
# Generated file, do not edit.
#
list(APPEND FLUTTER_PLUGIN_LIST
audioplayers_linux
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST
jni
)
set(PLUGIN_BUNDLED_LIBRARIES)
foreach(plugin ${FLUTTER_PLUGIN_LIST})
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin})
target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin)
list(APPEND PLUGIN_BUNDLED_LIBRARIES $<TARGET_FILE:${plugin}_plugin>)
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})
endforeach(plugin)
foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST})
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin})
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries})
endforeach(ffi_plugin)
+26
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}")
+6
View File
@@ -0,0 +1,6 @@
#include "my_application.h"
int main(int argc, char** argv) {
g_autoptr(MyApplication) app = my_application_new();
return g_application_run(G_APPLICATION(app), argc, argv);
}
+148
View File
@@ -0,0 +1,148 @@
#include "my_application.h"
#include <flutter_linux/flutter_linux.h>
#ifdef GDK_WINDOWING_X11
#include <gdk/gdkx.h>
#endif
#include "flutter/generated_plugin_registrant.h"
struct _MyApplication {
GtkApplication parent_instance;
char** dart_entrypoint_arguments;
};
G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION)
// Called when first Flutter frame received.
static void first_frame_cb(MyApplication* self, FlView* view) {
gtk_widget_show(gtk_widget_get_toplevel(GTK_WIDGET(view)));
}
// Implements GApplication::activate.
static void my_application_activate(GApplication* application) {
MyApplication* self = MY_APPLICATION(application);
GtkWindow* window =
GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application)));
// Use a header bar when running in GNOME as this is the common style used
// by applications and is the setup most users will be using (e.g. Ubuntu
// desktop).
// If running on X and not using GNOME then just use a traditional title bar
// in case the window manager does more exotic layout, e.g. tiling.
// If running on Wayland assume the header bar will work (may need changing
// if future cases occur).
gboolean use_header_bar = TRUE;
#ifdef GDK_WINDOWING_X11
GdkScreen* screen = gtk_window_get_screen(window);
if (GDK_IS_X11_SCREEN(screen)) {
const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen);
if (g_strcmp0(wm_name, "GNOME Shell") != 0) {
use_header_bar = FALSE;
}
}
#endif
if (use_header_bar) {
GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new());
gtk_widget_show(GTK_WIDGET(header_bar));
gtk_header_bar_set_title(header_bar, "iwbtc");
gtk_header_bar_set_show_close_button(header_bar, TRUE);
gtk_window_set_titlebar(window, GTK_WIDGET(header_bar));
} else {
gtk_window_set_title(window, "iwbtc");
}
gtk_window_set_default_size(window, 1280, 720);
g_autoptr(FlDartProject) project = fl_dart_project_new();
fl_dart_project_set_dart_entrypoint_arguments(
project, self->dart_entrypoint_arguments);
FlView* view = fl_view_new(project);
GdkRGBA background_color;
// Background defaults to black, override it here if necessary, e.g. #00000000
// for transparent.
gdk_rgba_parse(&background_color, "#000000");
fl_view_set_background_color(view, &background_color);
gtk_widget_show(GTK_WIDGET(view));
gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view));
// Show the window when Flutter renders.
// Requires the view to be realized so we can start rendering.
g_signal_connect_swapped(view, "first-frame", G_CALLBACK(first_frame_cb),
self);
gtk_widget_realize(GTK_WIDGET(view));
fl_register_plugins(FL_PLUGIN_REGISTRY(view));
gtk_widget_grab_focus(GTK_WIDGET(view));
}
// Implements GApplication::local_command_line.
static gboolean my_application_local_command_line(GApplication* application,
gchar*** arguments,
int* exit_status) {
MyApplication* self = MY_APPLICATION(application);
// Strip out the first argument as it is the binary name.
self->dart_entrypoint_arguments = g_strdupv(*arguments + 1);
g_autoptr(GError) error = nullptr;
if (!g_application_register(application, nullptr, &error)) {
g_warning("Failed to register: %s", error->message);
*exit_status = 1;
return TRUE;
}
g_application_activate(application);
*exit_status = 0;
return TRUE;
}
// Implements GApplication::startup.
static void my_application_startup(GApplication* application) {
// MyApplication* self = MY_APPLICATION(object);
// Perform any actions required at application startup.
G_APPLICATION_CLASS(my_application_parent_class)->startup(application);
}
// Implements GApplication::shutdown.
static void my_application_shutdown(GApplication* application) {
// MyApplication* self = MY_APPLICATION(object);
// Perform any actions required at application shutdown.
G_APPLICATION_CLASS(my_application_parent_class)->shutdown(application);
}
// Implements GObject::dispose.
static void my_application_dispose(GObject* object) {
MyApplication* self = MY_APPLICATION(object);
g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev);
G_OBJECT_CLASS(my_application_parent_class)->dispose(object);
}
static void my_application_class_init(MyApplicationClass* klass) {
G_APPLICATION_CLASS(klass)->activate = my_application_activate;
G_APPLICATION_CLASS(klass)->local_command_line =
my_application_local_command_line;
G_APPLICATION_CLASS(klass)->startup = my_application_startup;
G_APPLICATION_CLASS(klass)->shutdown = my_application_shutdown;
G_OBJECT_CLASS(klass)->dispose = my_application_dispose;
}
static void my_application_init(MyApplication* self) {}
MyApplication* my_application_new() {
// Set the program name to the application ID, which helps various systems
// like GTK and desktop environments map this running application to its
// corresponding .desktop file. This ensures better integration by allowing
// the application to be recognized beyond its binary name.
g_set_prgname(APPLICATION_ID);
return MY_APPLICATION(g_object_new(my_application_get_type(),
"application-id", APPLICATION_ID, "flags",
G_APPLICATION_NON_UNIQUE, nullptr));
}
+21
View File
@@ -0,0 +1,21 @@
#ifndef FLUTTER_MY_APPLICATION_H_
#define FLUTTER_MY_APPLICATION_H_
#include <gtk/gtk.h>
G_DECLARE_FINAL_TYPE(MyApplication,
my_application,
MY,
APPLICATION,
GtkApplication)
/**
* my_application_new:
*
* Creates a new Flutter-based application.
*
* Returns: a new #MyApplication.
*/
MyApplication* my_application_new();
#endif // FLUTTER_MY_APPLICATION_H_
+7
View File
@@ -0,0 +1,7 @@
# Flutter-related
**/Flutter/ephemeral/
**/Pods/
# Xcode-related
**/dgph
**/xcuserdata/
+1
View File
@@ -0,0 +1 @@
#include "ephemeral/Flutter-Generated.xcconfig"
@@ -0,0 +1 @@
#include "ephemeral/Flutter-Generated.xcconfig"
@@ -0,0 +1,14 @@
//
// Generated file. Do not edit.
//
import FlutterMacOS
import Foundation
import audioplayers_darwin
import shared_preferences_foundation
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
AudioplayersDarwinPlugin.register(with: registry.registrar(forPlugin: "AudioplayersDarwinPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
}
Loaded 100 of 205 files, more files were not shown because too many files have changed in this diff. Show more