summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore19
-rw-r--r--CLAUDE.md88
-rw-r--r--README.md48
-rw-r--r--build.gradle.kts10
-rw-r--r--composeApp/build.gradle.kts101
-rw-r--r--composeApp/src/androidMain/AndroidManifest.xml22
-rw-r--r--composeApp/src/androidMain/kotlin/coffee/liz/abstractionengine/MainActivity.kt25
-rw-r--r--composeApp/src/androidMain/kotlin/coffee/liz/abstractionengine/Platform.android.kt9
-rw-r--r--composeApp/src/androidMain/res/drawable-v24/ic_launcher_foreground.xml30
-rw-r--r--composeApp/src/androidMain/res/drawable/ic_launcher_background.xml170
-rw-r--r--composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher.xml6
-rw-r--r--composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_round.xml5
-rw-r--r--composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher.pngbin0 -> 5868 bytes
-rw-r--r--composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_background.pngbin0 -> 13455 bytes
-rw-r--r--composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_foreground.pngbin0 -> 5047 bytes
-rw-r--r--composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_monochrome.pngbin0 -> 3371 bytes
-rw-r--r--composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_round.pngbin0 -> 5339 bytes
-rw-r--r--composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher.pngbin0 -> 3110 bytes
-rw-r--r--composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_background.pngbin0 -> 6898 bytes
-rw-r--r--composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_foreground.pngbin0 -> 3161 bytes
-rw-r--r--composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_monochrome.pngbin0 -> 2325 bytes
-rw-r--r--composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_round.pngbin0 -> 3388 bytes
-rw-r--r--composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher.pngbin0 -> 8513 bytes
-rw-r--r--composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_background.pngbin0 -> 23364 bytes
-rw-r--r--composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_foreground.pngbin0 -> 7854 bytes
-rw-r--r--composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_monochrome.pngbin0 -> 5276 bytes
-rw-r--r--composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_round.pngbin0 -> 7472 bytes
-rw-r--r--composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher.pngbin0 -> 16812 bytes
-rw-r--r--composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_background.pngbin0 -> 50329 bytes
-rw-r--r--composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_foreground.pngbin0 -> 12807 bytes
-rw-r--r--composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_monochrome.pngbin0 -> 7962 bytes
-rw-r--r--composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_round.pngbin0 -> 11873 bytes
-rw-r--r--composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher.pngbin0 -> 27062 bytes
-rw-r--r--composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_background.pngbin0 -> 90310 bytes
-rw-r--r--composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_foreground.pngbin0 -> 19592 bytes
-rw-r--r--composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_monochrome.pngbin0 -> 12600 bytes
-rw-r--r--composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_round.pngbin0 -> 16570 bytes
-rw-r--r--composeApp/src/androidMain/res/play_store_512.pngbin0 -> 142862 bytes
-rw-r--r--composeApp/src/androidMain/res/values/strings.xml3
-rw-r--r--composeApp/src/commonMain/composeResources/drawable/compose-multiplatform.xml44
-rw-r--r--composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/AbstractionEngine.kt5
-rw-r--r--composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/App.kt28
-rw-r--r--composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/Greeting.kt9
-rw-r--r--composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/Platform.kt7
-rw-r--r--composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/game/Game.kt77
-rw-r--r--composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/game/ImageCache.kt23
-rw-r--r--composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/game/RenderSystem.kt70
-rw-r--r--composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/ui/ArcadeButton.kt52
-rw-r--r--composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/ui/ArcadeControls.kt130
-rw-r--r--composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/ui/DPad.kt105
-rw-r--r--composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/ui/PCBBackground.kt85
-rw-r--r--composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/ui/Theme.kt32
-rw-r--r--composeApp/src/commonMain/kotlin/coffee/liz/ecs/Component.kt7
-rw-r--r--composeApp/src/commonMain/kotlin/coffee/liz/ecs/DAGWorld.kt127
-rw-r--r--composeApp/src/commonMain/kotlin/coffee/liz/ecs/Entity.kt70
-rw-r--r--composeApp/src/commonMain/kotlin/coffee/liz/ecs/System.kt22
-rw-r--r--composeApp/src/commonMain/kotlin/coffee/liz/ecs/World.kt42
-rw-r--r--composeApp/src/commonMain/kotlin/coffee/liz/ecs/animation/AnimationComponents.kt114
-rw-r--r--composeApp/src/commonMain/kotlin/coffee/liz/ecs/animation/AnimationSystem.kt77
-rw-r--r--composeApp/src/commonTest/kotlin/coffee/liz/abstractionengine/ComposeAppCommonTest.kt12
-rw-r--r--composeApp/src/commonTest/kotlin/coffee/liz/ecs/DAGWorldTest.kt242
-rw-r--r--composeApp/src/commonTest/kotlin/coffee/liz/ecs/EntityTest.kt116
-rw-r--r--composeApp/src/commonTest/kotlin/coffee/liz/ecs/animation/AnimationSystemTest.kt238
-rw-r--r--composeApp/src/iosMain/kotlin/coffee/liz/abstractionengine/MainViewController.kt5
-rw-r--r--composeApp/src/iosMain/kotlin/coffee/liz/abstractionengine/Platform.ios.kt9
-rw-r--r--composeApp/src/jvmMain/kotlin/coffee/liz/abstractionengine/Platform.jvm.kt7
-rw-r--r--composeApp/src/jvmMain/kotlin/coffee/liz/abstractionengine/main.kt13
-rw-r--r--gradle.properties12
-rw-r--r--gradle/libs.versions.toml37
-rw-r--r--gradle/wrapper/gradle-wrapper.jarbin0 -> 43764 bytes
-rw-r--r--gradle/wrapper/gradle-wrapper.properties7
-rwxr-xr-xgradlew251
-rw-r--r--gradlew.bat94
-rw-r--r--iosApp/Configuration/Config.xcconfig7
-rw-r--r--iosApp/iosApp.xcodeproj/project.pbxproj373
-rw-r--r--iosApp/iosApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata7
-rw-r--r--iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-20@2x.pngbin0 -> 2261 bytes
-rw-r--r--iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-20@2x~ipad.pngbin0 -> 2261 bytes
-rw-r--r--iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-20@3x.pngbin0 -> 3810 bytes
-rw-r--r--iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-20~ipad.pngbin0 -> 868 bytes
-rw-r--r--iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-29.pngbin0 -> 1521 bytes
-rw-r--r--iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-29@2x.pngbin0 -> 3826 bytes
-rw-r--r--iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-29@2x~ipad.pngbin0 -> 3826 bytes
-rw-r--r--iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-29@3x.pngbin0 -> 6546 bytes
-rw-r--r--iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-29~ipad.pngbin0 -> 1521 bytes
-rw-r--r--iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-40@2x.pngbin0 -> 5979 bytes
-rw-r--r--iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-40@2x~ipad.pngbin0 -> 5979 bytes
-rw-r--r--iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-40@3x.pngbin0 -> 10746 bytes
-rw-r--r--iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-40~ipad.pngbin0 -> 2261 bytes
-rw-r--r--iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-60@2x~car.pngbin0 -> 10746 bytes
-rw-r--r--iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-60@3x~car.pngbin0 -> 21865 bytes
-rw-r--r--iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-83.5@2x~ipad.pngbin0 -> 20545 bytes
-rw-r--r--iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon@2x.pngbin0 -> 10746 bytes
-rw-r--r--iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon@2x~ipad.pngbin0 -> 16679 bytes
-rw-r--r--iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon@3x.pngbin0 -> 21865 bytes
-rw-r--r--iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon~ios-marketing.pngbin0 -> 460496 bytes
-rw-r--r--iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon~ipad.pngbin0 -> 5490 bytes
-rw-r--r--iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json134
-rw-r--r--iosApp/iosApp/Assets.xcassets/Contents.json6
-rw-r--r--iosApp/iosApp/ContentView.swift21
-rw-r--r--iosApp/iosApp/Info.plist8
-rw-r--r--iosApp/iosApp/Preview Content/Preview Assets.xcassets/Contents.json6
-rw-r--r--iosApp/iosApp/iOSApp.swift10
-rw-r--r--settings.gradle.kts35
104 files changed, 3312 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..adfa9bf
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,19 @@
+*.iml
+.kotlin
+.gradle
+**/build/
+xcuserdata
+!src/**/build/
+local.properties
+.idea
+.DS_Store
+captures
+.externalNativeBuild
+.cxx
+*.xcodeproj/*
+!*.xcodeproj/project.pbxproj
+!*.xcodeproj/xcshareddata/
+!*.xcodeproj/project.xcworkspace/
+!*.xcworkspace/contents.xcworkspacedata
+**/xcshareddata/WorkspaceSettings.xcsettings
+node_modules/
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 0000000..c0da46a
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,88 @@
+# CLAUDE.md
+
+This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
+
+## Project Overview
+
+This is a Kotlin Multiplatform project using Compose Multiplatform, targeting Android, iOS, and Desktop (JVM). The project is called "The Abstraction Engine" with the package namespace `coffee.liz.abstractionengine`.
+
+## Build Commands
+
+### Building
+- **Android Debug Build**: `./gradlew :composeApp:assembleDebug`
+- **Android Release Build**: `./gradlew :composeApp:assembleRelease`
+- **Desktop Build**: `./gradlew :composeApp:packageDistributionForCurrentOS`
+
+### Running
+- **Desktop App**: `./gradlew :composeApp:run`
+- **iOS App**: Open `iosApp/` directory in Xcode and run from there
+- **Android App**: Use Android Studio run configurations or `./gradlew :composeApp:installDebug`
+
+### Testing
+- **Run Common Tests**: `./gradlew :composeApp:testDebugUnitTest`
+- **Run All Tests**: `./gradlew test`
+
+### Clean
+- **Clean Build**: `./gradlew clean`
+
+## Architecture
+
+### Source Structure
+
+The project follows Kotlin Multiplatform's platform-specific source set structure:
+
+- **composeApp/src/commonMain/kotlin**: Shared code for all platforms
+ - Contains the main `App.kt` composable and shared business logic
+ - Uses Compose Multiplatform for cross-platform UI
+ - Platform-agnostic interface definitions (e.g., `Platform.kt`)
+
+- **composeApp/src/androidMain/kotlin**: Android-specific implementations
+ - `MainActivity.kt`: Android app entry point
+ - `Platform.android.kt`: Android platform implementations
+
+- **composeApp/src/iosMain/kotlin**: iOS-specific implementations
+ - `MainViewController.kt`: iOS app entry point
+ - `Platform.ios.kt`: iOS platform implementations
+
+- **composeApp/src/jvmMain/kotlin**: Desktop (JVM) specific implementations
+ - `main.kt`: Desktop app entry point with window configuration
+ - `Platform.jvm.kt`: JVM platform implementations
+
+- **iosApp/**: Native iOS app wrapper that hosts the Compose Multiplatform framework
+
+### Platform Abstraction Pattern
+
+The codebase uses the expect/actual pattern for platform-specific implementations:
+1. Define an `expect` declaration in `commonMain` (e.g., `Platform.kt`)
+2. Provide `actual` implementations in each platform source set (androidMain, iosMain, jvmMain)
+
+### Key Dependencies
+
+- **Compose Multiplatform 1.9.0**: UI framework
+- **Kotlin 2.2.20**: Language version
+- **AndroidX Lifecycle 2.9.4**: ViewModel and lifecycle support
+- **Compose Hot Reload**: Development feature for faster iteration
+
+### Build Configuration
+
+- Uses Gradle version catalogs (`gradle/libs.versions.toml`) for dependency management
+- Android targets API 36, minimum API 24
+- JVM target is Java 11
+- iOS targets arm64 and simulator arm64
+- Desktop distribution formats: DMG (macOS), MSI (Windows), DEB (Linux)
+
+## Development Notes
+
+### Entry Points
+- **Desktop**: `coffee.liz.abstractionengine.MainKt` (composeApp/src/jvmMain/kotlin/coffee/liz/abstractionengine/main.kt:6)
+- **Android**: `MainActivity` (composeApp/src/androidMain/kotlin/coffee/liz/abstractionengine/MainActivity.kt)
+- **iOS**: `MainViewController` (composeApp/src/iosMain/kotlin/coffee/liz/abstractionengine/MainViewController.kt)
+
+### Adding Platform-Specific Code
+When adding functionality that requires platform APIs:
+1. Add the interface/expect declaration in `commonMain/kotlin/coffee/liz/abstractionengine`
+2. Implement actual platform-specific versions in the corresponding platform source sets
+3. Platform implementations can use native APIs (Android SDK, iOS UIKit, JVM Swing)
+
+### Resource Management
+Compose resources are stored in `composeApp/src/commonMain/composeResources/` and are accessible via the generated `Res` object.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..3cc70ff
--- /dev/null
+++ b/README.md
@@ -0,0 +1,48 @@
+This is a Kotlin Multiplatform project targeting Android, iOS, Desktop (JVM).
+
+* [/composeApp](./composeApp/src) is for code that will be shared across your Compose Multiplatform applications.
+ It contains several subfolders:
+ - [commonMain](./composeApp/src/commonMain/kotlin) is for code that’s common for all targets.
+ - Other folders are for Kotlin code that will be compiled for only the platform indicated in the folder name.
+ For example, if you want to use Apple’s CoreCrypto for the iOS part of your Kotlin app,
+ the [iosMain](./composeApp/src/iosMain/kotlin) folder would be the right place for such calls.
+ Similarly, if you want to edit the Desktop (JVM) specific part, the [jvmMain](./composeApp/src/jvmMain/kotlin)
+ folder is the appropriate location.
+
+* [/iosApp](./iosApp/iosApp) contains iOS applications. Even if you’re sharing your UI with Compose Multiplatform,
+ you need this entry point for your iOS app. This is also where you should add SwiftUI code for your project.
+
+### Build and Run Android Application
+
+To build and run the development version of the Android app, use the run configuration from the run widget
+in your IDE’s toolbar or build it directly from the terminal:
+- on macOS/Linux
+ ```shell
+ ./gradlew :composeApp:assembleDebug
+ ```
+- on Windows
+ ```shell
+ .\gradlew.bat :composeApp:assembleDebug
+ ```
+
+### Build and Run Desktop (JVM) Application
+
+To build and run the development version of the desktop app, use the run configuration from the run widget
+in your IDE’s toolbar or run it directly from the terminal:
+- on macOS/Linux
+ ```shell
+ ./gradlew :composeApp:run
+ ```
+- on Windows
+ ```shell
+ .\gradlew.bat :composeApp:run
+ ```
+
+### Build and Run iOS Application
+
+To build and run the development version of the iOS app, use the run configuration from the run widget
+in your IDE’s toolbar or open the [/iosApp](./iosApp) directory in Xcode and run it from there.
+
+---
+
+Learn more about [Kotlin Multiplatform](https://www.jetbrains.com/help/kotlin-multiplatform-dev/get-started.html)… \ No newline at end of file
diff --git a/build.gradle.kts b/build.gradle.kts
new file mode 100644
index 0000000..98ddb8c
--- /dev/null
+++ b/build.gradle.kts
@@ -0,0 +1,10 @@
+plugins {
+ // this is necessary to avoid the plugins to be loaded multiple times
+ // in each subproject's classloader
+ alias(libs.plugins.androidApplication) apply false
+ alias(libs.plugins.androidLibrary) apply false
+ alias(libs.plugins.composeHotReload) apply false
+ alias(libs.plugins.composeMultiplatform) apply false
+ alias(libs.plugins.composeCompiler) apply false
+ alias(libs.plugins.kotlinMultiplatform) apply false
+} \ No newline at end of file
diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts
new file mode 100644
index 0000000..9e17fc5
--- /dev/null
+++ b/composeApp/build.gradle.kts
@@ -0,0 +1,101 @@
+import org.jetbrains.compose.desktop.application.dsl.TargetFormat
+import org.jetbrains.kotlin.gradle.dsl.JvmTarget
+
+plugins {
+ alias(libs.plugins.kotlinMultiplatform)
+ alias(libs.plugins.androidApplication)
+ alias(libs.plugins.composeMultiplatform)
+ alias(libs.plugins.composeCompiler)
+ alias(libs.plugins.composeHotReload)
+}
+
+kotlin {
+ compilerOptions {
+ freeCompilerArgs.add("-opt-in=kotlin.time.ExperimentalTime")
+ }
+
+ androidTarget {
+ compilerOptions {
+ jvmTarget.set(JvmTarget.JVM_11)
+ }
+ }
+
+ listOf(
+ iosArm64(),
+ iosSimulatorArm64()
+ ).forEach { iosTarget ->
+ iosTarget.binaries.framework {
+ baseName = "ComposeApp"
+ isStatic = true
+ }
+ }
+
+ jvm()
+
+ sourceSets {
+ androidMain.dependencies {
+ implementation(compose.preview)
+ implementation(libs.androidx.activity.compose)
+ }
+ commonMain.dependencies {
+ implementation(compose.runtime)
+ implementation(compose.foundation)
+ implementation(compose.material3)
+ implementation(compose.ui)
+ implementation(compose.components.resources)
+ implementation(compose.components.uiToolingPreview)
+ implementation(libs.androidx.lifecycle.viewmodelCompose)
+ implementation(libs.androidx.lifecycle.runtimeCompose)
+ }
+ commonTest.dependencies {
+ implementation(libs.kotlin.test)
+ }
+ jvmMain.dependencies {
+ implementation(compose.desktop.currentOs)
+ implementation(libs.kotlinx.coroutinesSwing)
+ }
+ }
+}
+
+android {
+ namespace = "coffee.liz.abstractionengine"
+ compileSdk = libs.versions.android.compileSdk.get().toInt()
+
+ defaultConfig {
+ applicationId = "coffee.liz.abstractionengine"
+ minSdk = libs.versions.android.minSdk.get().toInt()
+ targetSdk = libs.versions.android.targetSdk.get().toInt()
+ versionCode = 1
+ versionName = "1.0"
+ }
+ packaging {
+ resources {
+ excludes += "/META-INF/{AL2.0,LGPL2.1}"
+ }
+ }
+ buildTypes {
+ getByName("release") {
+ isMinifyEnabled = false
+ }
+ }
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_11
+ targetCompatibility = JavaVersion.VERSION_11
+ }
+}
+
+dependencies {
+ debugImplementation(compose.uiTooling)
+}
+
+compose.desktop {
+ application {
+ mainClass = "coffee.liz.abstractionengine.MainKt"
+
+ nativeDistributions {
+ targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
+ packageName = "coffee.liz.abstractionengine"
+ packageVersion = "1.0.0"
+ }
+ }
+}
diff --git a/composeApp/src/androidMain/AndroidManifest.xml b/composeApp/src/androidMain/AndroidManifest.xml
new file mode 100644
index 0000000..26403a7
--- /dev/null
+++ b/composeApp/src/androidMain/AndroidManifest.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <application
+ android:allowBackup="true"
+ android:icon="@mipmap/ic_launcher"
+ android:label="@string/app_name"
+ android:roundIcon="@mipmap/ic_launcher_round"
+ android:supportsRtl="true"
+ android:theme="@android:style/Theme.Material.Light.NoActionBar">
+ <activity
+ android:exported="true"
+ android:name=".MainActivity">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity>
+ </application>
+
+</manifest> \ No newline at end of file
diff --git a/composeApp/src/androidMain/kotlin/coffee/liz/abstractionengine/MainActivity.kt b/composeApp/src/androidMain/kotlin/coffee/liz/abstractionengine/MainActivity.kt
new file mode 100644
index 0000000..49d0dfa
--- /dev/null
+++ b/composeApp/src/androidMain/kotlin/coffee/liz/abstractionengine/MainActivity.kt
@@ -0,0 +1,25 @@
+package coffee.liz.abstractionengine
+
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.activity.enableEdgeToEdge
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.tooling.preview.Preview
+
+class MainActivity : ComponentActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ enableEdgeToEdge()
+ super.onCreate(savedInstanceState)
+
+ setContent {
+ App()
+ }
+ }
+}
+
+@Preview
+@Composable
+fun AppAndroidPreview() {
+ App()
+} \ No newline at end of file
diff --git a/composeApp/src/androidMain/kotlin/coffee/liz/abstractionengine/Platform.android.kt b/composeApp/src/androidMain/kotlin/coffee/liz/abstractionengine/Platform.android.kt
new file mode 100644
index 0000000..3ab6a6a
--- /dev/null
+++ b/composeApp/src/androidMain/kotlin/coffee/liz/abstractionengine/Platform.android.kt
@@ -0,0 +1,9 @@
+package coffee.liz.abstractionengine
+
+import android.os.Build
+
+class AndroidPlatform : Platform {
+ override val name: String = "Android ${Build.VERSION.SDK_INT}"
+}
+
+actual fun getPlatform(): Platform = AndroidPlatform() \ No newline at end of file
diff --git a/composeApp/src/androidMain/res/drawable-v24/ic_launcher_foreground.xml b/composeApp/src/androidMain/res/drawable-v24/ic_launcher_foreground.xml
new file mode 100644
index 0000000..2b068d1
--- /dev/null
+++ b/composeApp/src/androidMain/res/drawable-v24/ic_launcher_foreground.xml
@@ -0,0 +1,30 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:aapt="http://schemas.android.com/aapt"
+ android:width="108dp"
+ android:height="108dp"
+ android:viewportWidth="108"
+ android:viewportHeight="108">
+ <path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
+ <aapt:attr name="android:fillColor">
+ <gradient
+ android:endX="85.84757"
+ android:endY="92.4963"
+ android:startX="42.9492"
+ android:startY="49.59793"
+ android:type="linear">
+ <item
+ android:color="#44000000"
+ android:offset="0.0" />
+ <item
+ android:color="#00000000"
+ android:offset="1.0" />
+ </gradient>
+ </aapt:attr>
+ </path>
+ <path
+ android:fillColor="#FFFFFF"
+ android:fillType="nonZero"
+ android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
+ android:strokeWidth="1"
+ android:strokeColor="#00000000" />
+</vector> \ No newline at end of file
diff --git a/composeApp/src/androidMain/res/drawable/ic_launcher_background.xml b/composeApp/src/androidMain/res/drawable/ic_launcher_background.xml
new file mode 100644
index 0000000..e93e11a
--- /dev/null
+++ b/composeApp/src/androidMain/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,170 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="108dp"
+ android:height="108dp"
+ android:viewportWidth="108"
+ android:viewportHeight="108">
+ <path
+ android:fillColor="#3DDC84"
+ android:pathData="M0,0h108v108h-108z" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M9,0L9,108"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M19,0L19,108"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M29,0L29,108"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M39,0L39,108"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M49,0L49,108"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M59,0L59,108"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M69,0L69,108"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M79,0L79,108"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M89,0L89,108"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M99,0L99,108"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,9L108,9"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,19L108,19"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,29L108,29"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,39L108,39"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,49L108,49"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,59L108,59"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,69L108,69"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,79L108,79"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,89L108,89"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,99L108,99"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M19,29L89,29"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M19,39L89,39"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M19,49L89,49"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M19,59L89,59"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M19,69L89,69"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M19,79L89,79"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M29,19L29,89"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M39,19L39,89"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M49,19L49,89"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M59,19L59,89"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M69,19L69,89"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M79,19L79,89"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+</vector> \ No newline at end of file
diff --git a/composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher.xml b/composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000..345888d
--- /dev/null
+++ b/composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+ <background android:drawable="@mipmap/ic_launcher_background"/>
+ <foreground android:drawable="@mipmap/ic_launcher_foreground"/>
+ <monochrome android:drawable="@mipmap/ic_launcher_monochrome"/>
+</adaptive-icon> \ No newline at end of file
diff --git a/composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_round.xml b/composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 0000000..eca70cf
--- /dev/null
+++ b/composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+ <background android:drawable="@drawable/ic_launcher_background" />
+ <foreground android:drawable="@drawable/ic_launcher_foreground" />
+</adaptive-icon> \ No newline at end of file
diff --git a/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher.png b/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..579c9e1
--- /dev/null
+++ b/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher.png
Binary files differ
diff --git a/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_background.png b/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_background.png
new file mode 100644
index 0000000..d96f4cd
--- /dev/null
+++ b/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_background.png
Binary files differ
diff --git a/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_foreground.png b/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..8a44498
--- /dev/null
+++ b/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_foreground.png
Binary files differ
diff --git a/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_monochrome.png b/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_monochrome.png
new file mode 100644
index 0000000..1f56bb1
--- /dev/null
+++ b/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_monochrome.png
Binary files differ
diff --git a/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_round.png b/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_round.png
new file mode 100644
index 0000000..61da551
--- /dev/null
+++ b/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_round.png
Binary files differ
diff --git a/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher.png b/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..85fae95
--- /dev/null
+++ b/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher.png
Binary files differ
diff --git a/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_background.png b/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_background.png
new file mode 100644
index 0000000..d22fcbb
--- /dev/null
+++ b/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_background.png
Binary files differ
diff --git a/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_foreground.png b/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..344dfe0
--- /dev/null
+++ b/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_foreground.png
Binary files differ
diff --git a/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_monochrome.png b/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_monochrome.png
new file mode 100644
index 0000000..f942995
--- /dev/null
+++ b/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_monochrome.png
Binary files differ
diff --git a/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_round.png b/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_round.png
new file mode 100644
index 0000000..db5080a
--- /dev/null
+++ b/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_round.png
Binary files differ
diff --git a/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher.png b/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..e0d00ef
--- /dev/null
+++ b/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher.png
Binary files differ
diff --git a/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_background.png b/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_background.png
new file mode 100644
index 0000000..7b4d63b
--- /dev/null
+++ b/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_background.png
Binary files differ
diff --git a/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_foreground.png b/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..3f51c64
--- /dev/null
+++ b/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_foreground.png
Binary files differ
diff --git a/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_monochrome.png b/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_monochrome.png
new file mode 100644
index 0000000..baddf51
--- /dev/null
+++ b/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_monochrome.png
Binary files differ
diff --git a/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_round.png b/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..da31a87
--- /dev/null
+++ b/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_round.png
Binary files differ
diff --git a/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher.png b/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..a8aae0a
--- /dev/null
+++ b/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher.png
Binary files differ
diff --git a/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_background.png b/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_background.png
new file mode 100644
index 0000000..762d687
--- /dev/null
+++ b/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_background.png
Binary files differ
diff --git a/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_foreground.png b/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..dde1f38
--- /dev/null
+++ b/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_foreground.png
Binary files differ
diff --git a/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_monochrome.png b/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_monochrome.png
new file mode 100644
index 0000000..75429fe
--- /dev/null
+++ b/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_monochrome.png
Binary files differ
diff --git a/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_round.png b/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..b216f2d
--- /dev/null
+++ b/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_round.png
Binary files differ
diff --git a/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher.png b/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..ed1272b
--- /dev/null
+++ b/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher.png
Binary files differ
diff --git a/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_background.png b/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_background.png
new file mode 100644
index 0000000..b3af25d
--- /dev/null
+++ b/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_background.png
Binary files differ
diff --git a/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..e92bedb
--- /dev/null
+++ b/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_foreground.png
Binary files differ
diff --git a/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_monochrome.png b/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_monochrome.png
new file mode 100644
index 0000000..ac0eee1
--- /dev/null
+++ b/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_monochrome.png
Binary files differ
diff --git a/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_round.png b/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..e96783c
--- /dev/null
+++ b/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_round.png
Binary files differ
diff --git a/composeApp/src/androidMain/res/play_store_512.png b/composeApp/src/androidMain/res/play_store_512.png
new file mode 100644
index 0000000..e44c917
--- /dev/null
+++ b/composeApp/src/androidMain/res/play_store_512.png
Binary files differ
diff --git a/composeApp/src/androidMain/res/values/strings.xml b/composeApp/src/androidMain/res/values/strings.xml
new file mode 100644
index 0000000..ffcea6e
--- /dev/null
+++ b/composeApp/src/androidMain/res/values/strings.xml
@@ -0,0 +1,3 @@
+<resources>
+ <string name="app_name">The Abstraction Engine</string>
+</resources> \ No newline at end of file
diff --git a/composeApp/src/commonMain/composeResources/drawable/compose-multiplatform.xml b/composeApp/src/commonMain/composeResources/drawable/compose-multiplatform.xml
new file mode 100644
index 0000000..1ffc948
--- /dev/null
+++ b/composeApp/src/commonMain/composeResources/drawable/compose-multiplatform.xml
@@ -0,0 +1,44 @@
+<vector
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:aapt="http://schemas.android.com/aapt"
+ android:width="450dp"
+ android:height="450dp"
+ android:viewportWidth="64"
+ android:viewportHeight="64">
+ <path
+ android:pathData="M56.25,18V46L32,60 7.75,46V18L32,4Z"
+ android:fillColor="#6075f2"/>
+ <path
+ android:pathData="m41.5,26.5v11L32,43V60L56.25,46V18Z"
+ android:fillColor="#6b57ff"/>
+ <path
+ android:pathData="m32,43 l-9.5,-5.5v-11L7.75,18V46L32,60Z">
+ <aapt:attr name="android:fillColor">
+ <gradient
+ android:centerX="23.131"
+ android:centerY="18.441"
+ android:gradientRadius="42.132"
+ android:type="radial">
+ <item android:offset="0" android:color="#FF5383EC"/>
+ <item android:offset="0.867" android:color="#FF7F52FF"/>
+ </gradient>
+ </aapt:attr>
+ </path>
+ <path
+ android:pathData="M22.5,26.5 L32,21 41.5,26.5 56.25,18 32,4 7.75,18Z">
+ <aapt:attr name="android:fillColor">
+ <gradient
+ android:startX="44.172"
+ android:startY="4.377"
+ android:endX="17.973"
+ android:endY="34.035"
+ android:type="linear">
+ <item android:offset="0" android:color="#FF33C3FF"/>
+ <item android:offset="0.878" android:color="#FF5383EC"/>
+ </gradient>
+ </aapt:attr>
+ </path>
+ <path
+ android:pathData="m32,21 l9.526,5.5v11L32,43 22.474,37.5v-11z"
+ android:fillColor="#000000"/>
+</vector> \ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/AbstractionEngine.kt b/composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/AbstractionEngine.kt
new file mode 100644
index 0000000..d557fd8
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/AbstractionEngine.kt
@@ -0,0 +1,5 @@
+package coffee.liz.abstractionengine
+
+class AbstractionEngine {
+
+} \ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/App.kt b/composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/App.kt
new file mode 100644
index 0000000..a69e711
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/App.kt
@@ -0,0 +1,28 @@
+package coffee.liz.abstractionengine
+
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.runtime.*
+import androidx.compose.ui.Modifier
+import coffee.liz.abstractionengine.ui.ArcadeControls
+import coffee.liz.abstractionengine.ui.GameBoyTheme
+import org.jetbrains.compose.ui.tooling.preview.Preview
+
+@Composable
+@Preview
+fun App() {
+ MaterialTheme(colorScheme = GameBoyTheme) {
+ ArcadeControls(
+ onDirectionPressed = { direction ->
+ println("Direction pressed: $direction")
+ },
+ onActionA = {
+ println("Action A pressed!")
+ },
+ onActionB = {
+ println("Action B pressed!")
+ },
+ modifier = Modifier.fillMaxSize()
+ )
+ }
+} \ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/Greeting.kt b/composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/Greeting.kt
new file mode 100644
index 0000000..6f23320
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/Greeting.kt
@@ -0,0 +1,9 @@
+package coffee.liz.abstractionengine
+
+class Greeting {
+ private val platform = getPlatform()
+
+ fun greet(): String {
+ return "Hello, ${platform.name}!"
+ }
+} \ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/Platform.kt b/composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/Platform.kt
new file mode 100644
index 0000000..91faf62
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/Platform.kt
@@ -0,0 +1,7 @@
+package coffee.liz.abstractionengine
+
+interface Platform {
+ val name: String
+}
+
+expect fun getPlatform(): Platform \ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/game/Game.kt b/composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/game/Game.kt
new file mode 100644
index 0000000..9490f83
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/game/Game.kt
@@ -0,0 +1,77 @@
+package coffee.liz.abstractionengine.game
+
+import androidx.compose.foundation.Canvas
+import androidx.compose.runtime.*
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import coffee.liz.ecs.World
+import kotlinx.coroutines.isActive
+
+/**
+ * Manages the game loop for an ECS world.
+ * Syncs with Compose's frame rate (like requestAnimationFrame) and provides
+ * the world to child content for custom rendering/UI.
+ *
+ * @param world The ECS world containing entities and systems
+ * @param content Composable content that can use the world for rendering
+ */
+@Composable
+fun Game(
+ world: World,
+ content: @Composable () -> Unit
+) {
+ // Trigger recomposition when we need to redraw
+ var frameCount by remember { mutableStateOf(0) }
+
+ // Game loop - syncs with Compose's frame rate like requestAnimationFrame
+ LaunchedEffect(world) {
+ var lastFrameTimeNanos = 0L
+
+ while (isActive) {
+ // Wait for next frame (like requestAnimationFrame)
+ withFrameNanos { frameTimeNanos ->
+ val deltaTime = if (lastFrameTimeNanos != 0L) {
+ (frameTimeNanos - lastFrameTimeNanos) / 1_000_000_000f
+ } else {
+ 0f
+ }
+ lastFrameTimeNanos = frameTimeNanos
+
+ // Update ECS world (runs all systems)
+ if (deltaTime > 0f) {
+ world.update(deltaTime)
+ }
+
+ // Trigger recomposition to redraw
+ frameCount++
+ }
+ }
+ }
+
+ // Render content
+ content()
+}
+
+/**
+ * A Canvas that renders sprites from an ECS world using a RenderSystem.
+ *
+ * @param world The ECS world containing entities to render
+ * @param renderSystem The system responsible for rendering sprites
+ * @param backgroundColor Background color for the canvas
+ * @param modifier Modifier for the Canvas
+ */
+@Composable
+fun GameCanvas(
+ world: World,
+ renderSystem: RenderSystem,
+ backgroundColor: Color = Color.Transparent,
+ modifier: Modifier = Modifier
+) {
+ Canvas(modifier = modifier) {
+ // Clear background
+ drawRect(backgroundColor)
+
+ // Render all sprites
+ renderSystem.render(world, this)
+ }
+}
diff --git a/composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/game/ImageCache.kt b/composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/game/ImageCache.kt
new file mode 100644
index 0000000..0722535
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/game/ImageCache.kt
@@ -0,0 +1,23 @@
+package coffee.liz.abstractionengine.game
+
+import androidx.compose.ui.graphics.ImageBitmap
+
+/**
+ * Simple cache for loaded images.
+ * In a real game, you'd want more sophisticated resource management.
+ */
+class ImageCache {
+ private val images = mutableMapOf<String, ImageBitmap>()
+
+ fun loadImage(path: String, image: ImageBitmap) {
+ images[path] = image
+ }
+
+ fun getImage(path: String): ImageBitmap? {
+ return images[path]
+ }
+
+ fun clear() {
+ images.clear()
+ }
+}
diff --git a/composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/game/RenderSystem.kt b/composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/game/RenderSystem.kt
new file mode 100644
index 0000000..1d22ae6
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/game/RenderSystem.kt
@@ -0,0 +1,70 @@
+package coffee.liz.abstractionengine.game
+
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.graphics.drawscope.DrawScope
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.IntSize
+import coffee.liz.ecs.System
+import coffee.liz.ecs.World
+import coffee.liz.ecs.animation.AnimationSystem
+import coffee.liz.ecs.animation.Animator
+import coffee.liz.ecs.animation.Position
+import coffee.liz.ecs.animation.SpriteSheet
+import kotlin.reflect.KClass
+
+/**
+ * System that renders sprites to a DrawScope.
+ *
+ * This system queries all entities with Position, SpriteSheet, and Animator components,
+ * then draws their current animation frame to the provided DrawScope.
+ *
+ * Depends on AnimationSystem to ensure animations are updated before rendering.
+ */
+class RenderSystem(
+ private val imageCache: ImageCache
+) : System {
+
+ override val dependencies: Set<KClass<out System>> = setOf(AnimationSystem::class)
+
+ /**
+ * The update method is required by the System interface, but rendering
+ * happens synchronously during Compose's draw phase via the render() method.
+ */
+ override fun update(world: World, deltaTime: Float) {
+ // Rendering happens in render() method, not here
+ }
+
+ /**
+ * Renders all entities with sprites to the given DrawScope.
+ * This should be called from a Compose Canvas during the draw phase.
+ */
+ fun render(world: World, drawScope: DrawScope) {
+ // Query all entities that can be rendered
+ world.query(Position::class, SpriteSheet::class, Animator::class).forEach { entity ->
+ val position = entity.get(Position::class) ?: return@forEach
+ val spriteSheet = entity.get(SpriteSheet::class) ?: return@forEach
+ val animator = entity.get(Animator::class) ?: return@forEach
+
+ // Get the current frame name from the animator
+ val frameName = animator.getCurrentFrameName() ?: return@forEach
+
+ // Look up the frame rectangle in the sprite sheet
+ val frameRect = spriteSheet.frames[frameName] ?: return@forEach
+
+ // Get the image from cache
+ val image = imageCache.getImage(spriteSheet.imagePath) ?: return@forEach
+
+ // Draw the sprite
+ with(drawScope) {
+ drawImage(
+ image = image,
+ srcOffset = IntOffset(frameRect.x, frameRect.y),
+ srcSize = IntSize(frameRect.width, frameRect.height),
+ dstOffset = IntOffset(position.x.toInt(), position.y.toInt()),
+ dstSize = IntSize(frameRect.width, frameRect.height)
+ )
+ }
+ }
+ }
+}
diff --git a/composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/ui/ArcadeButton.kt b/composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/ui/ArcadeButton.kt
new file mode 100644
index 0000000..9d50192
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/ui/ArcadeButton.kt
@@ -0,0 +1,52 @@
+package coffee.liz.abstractionengine.ui
+
+import androidx.compose.foundation.border
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+
+enum class ArcadeButtonColor {
+ RED, YELLOW
+}
+
+@Composable
+fun ArcadeButton(
+ label: String,
+ color: ArcadeButtonColor = ArcadeButtonColor.RED,
+ onClick: () -> Unit,
+ modifier: Modifier = Modifier
+) {
+ val buttonColor = when (color) {
+ ArcadeButtonColor.RED -> GameBoyColors.ButtonRed
+ ArcadeButtonColor.YELLOW -> GameBoyColors.ButtonYellow
+ }
+
+ Button(
+ onClick = onClick,
+ modifier = modifier
+ .size(65.dp)
+ .border(1.dp, MaterialTheme.colorScheme.outline, CircleShape),
+ shape = CircleShape,
+ colors = ButtonDefaults.buttonColors(
+ containerColor = buttonColor,
+ contentColor = MaterialTheme.colorScheme.onPrimary
+ ),
+ contentPadding = PaddingValues(0.dp)
+ ) {
+ Text(
+ text = label,
+ fontSize = 20.sp,
+ fontWeight = FontWeight.Bold,
+ color = MaterialTheme.colorScheme.onPrimary
+ )
+ }
+}
diff --git a/composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/ui/ArcadeControls.kt b/composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/ui/ArcadeControls.kt
new file mode 100644
index 0000000..f5b4839
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/ui/ArcadeControls.kt
@@ -0,0 +1,130 @@
+package coffee.liz.abstractionengine.ui
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.*
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+
+@Composable
+fun ArcadeControls(
+ onDirectionPressed: (Direction) -> Unit,
+ onActionA: () -> Unit,
+ onActionB: () -> Unit,
+ modifier: Modifier = Modifier,
+ gameContent: @Composable BoxScope.() -> Unit = {
+ // Default placeholder content
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.spacedBy(16.dp)
+ ) {
+ Text(
+ text = "GAME AREA",
+ fontSize = 18.sp,
+ fontWeight = FontWeight.Bold,
+ fontFamily = FontFamily.Monospace,
+ color = MaterialTheme.colorScheme.onPrimary
+ )
+ }
+ }
+) {
+ var lastDirection by remember { mutableStateOf<Direction?>(null) }
+ var lastAction by remember { mutableStateOf<String?>(null) }
+
+ Box(modifier = modifier.fillMaxSize()) {
+ // PCB background layer
+ PCBBackground()
+
+ // Transparent casing overlay
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .background(Color(0x33000000))
+ )
+
+ // Content
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(16.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.spacedBy(16.dp)
+ ) {
+ // Game area (square)
+ Box(
+ modifier = Modifier
+ .fillMaxWidth(0.95f)
+ .aspectRatio(1f)
+ .background(
+ color = GameBoyColors.ScreenGreen.copy(alpha = 0.85f),
+ shape = RoundedCornerShape(8.dp)
+ )
+ .border(1.dp, MaterialTheme.colorScheme.outline, RoundedCornerShape(8.dp))
+ .padding(8.dp),
+ contentAlignment = Alignment.Center,
+ content = gameContent
+ )
+
+ // Control panel
+ Box(
+ modifier = Modifier
+ .fillMaxWidth(0.95f)
+ .background(
+ color = MaterialTheme.colorScheme.surface.copy(alpha = 0.7f),
+ shape = RoundedCornerShape(16.dp)
+ )
+ .border(1.dp, MaterialTheme.colorScheme.outline, RoundedCornerShape(16.dp))
+ .padding(horizontal = 16.dp, vertical = 16.dp),
+ contentAlignment = Alignment.Center
+ ) {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceEvenly,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ // D-Pad on the left
+ DPad(
+ onDirectionPressed = { direction ->
+ lastDirection = direction
+ lastAction = null
+ onDirectionPressed(direction)
+ }
+ )
+
+ // Action buttons on the right
+ Row(
+ horizontalArrangement = Arrangement.spacedBy(12.dp)
+ ) {
+ ArcadeButton(
+ label = "B",
+ color = ArcadeButtonColor.YELLOW,
+ onClick = {
+ lastAction = "B"
+ lastDirection = null
+ onActionB()
+ }
+ )
+ ArcadeButton(
+ label = "A",
+ color = ArcadeButtonColor.RED,
+ onClick = {
+ lastAction = "A"
+ lastDirection = null
+ onActionA()
+ }
+ )
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/ui/DPad.kt b/composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/ui/DPad.kt
new file mode 100644
index 0000000..52f5866
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/ui/DPad.kt
@@ -0,0 +1,105 @@
+package coffee.liz.abstractionengine.ui
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.*
+import androidx.compose.runtime.*
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+
+enum class Direction {
+ UP, DOWN, LEFT, RIGHT
+}
+
+@Composable
+fun DPad(
+ onDirectionPressed: (Direction) -> Unit,
+ modifier: Modifier = Modifier
+) {
+ Box(
+ modifier = modifier
+ .size(140.dp)
+ .background(
+ color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.6f),
+ shape = CircleShape
+ )
+ .border(1.dp, MaterialTheme.colorScheme.outline, CircleShape)
+ ) {
+ // Up button
+ DirectionButton(
+ text = "▲",
+ onClick = { onDirectionPressed(Direction.UP) },
+ modifier = Modifier
+ .align(Alignment.TopCenter)
+ .offset(y = 20.dp)
+ )
+
+ // Down button
+ DirectionButton(
+ text = "▼",
+ onClick = { onDirectionPressed(Direction.DOWN) },
+ modifier = Modifier
+ .align(Alignment.BottomCenter)
+ .offset(y = (-20).dp)
+ )
+
+ // Left button
+ DirectionButton(
+ text = "◀",
+ onClick = { onDirectionPressed(Direction.LEFT) },
+ modifier = Modifier
+ .align(Alignment.CenterStart)
+ .offset(x = 20.dp)
+ )
+
+ // Right button
+ DirectionButton(
+ text = "▶",
+ onClick = { onDirectionPressed(Direction.RIGHT) },
+ modifier = Modifier
+ .align(Alignment.CenterEnd)
+ .offset(x = (-20).dp)
+ )
+
+ // Center circle
+ Box(
+ modifier = Modifier
+ .align(Alignment.Center)
+ .size(38.dp)
+ .background(
+ color = MaterialTheme.colorScheme.surface.copy(alpha = 0.5f),
+ shape = CircleShape
+ )
+ .border(1.dp, MaterialTheme.colorScheme.outline, CircleShape)
+ )
+ }
+}
+
+@Composable
+private fun DirectionButton(
+ text: String,
+ onClick: () -> Unit,
+ modifier: Modifier = Modifier
+) {
+ Button(
+ onClick = onClick,
+ modifier = modifier.size(38.dp),
+ shape = RoundedCornerShape(4.dp),
+ colors = ButtonDefaults.buttonColors(
+ containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.6f),
+ contentColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.3f)
+ ),
+ contentPadding = PaddingValues(0.dp)
+ ) {
+ Text(
+ text = text,
+ fontSize = 14.sp,
+ style = MaterialTheme.typography.headlineMedium
+ )
+ }
+}
diff --git a/composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/ui/PCBBackground.kt b/composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/ui/PCBBackground.kt
new file mode 100644
index 0000000..be35a1a
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/ui/PCBBackground.kt
@@ -0,0 +1,85 @@
+package coffee.liz.abstractionengine.ui
+
+import androidx.compose.foundation.Canvas
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.PathEffect
+
+@Composable
+fun PCBBackground(modifier: Modifier = Modifier) {
+ val pcbGreen = Color(0xFF0D5F0D)
+ val traceColor = Color(0xFF1A7F1A)
+ val padColor = Color(0xFFD4AF37)
+
+ Canvas(modifier = modifier.fillMaxSize().background(pcbGreen)) {
+ val width = size.width
+ val height = size.height
+
+ // Draw circuit traces (horizontal and vertical lines)
+ val pathEffect = PathEffect.dashPathEffect(floatArrayOf(10f, 5f), 0f)
+
+ // Horizontal traces
+ for (i in 0..15) {
+ val y = (i * height / 15)
+ drawLine(
+ color = traceColor,
+ start = Offset(0f, y),
+ end = Offset(width, y),
+ strokeWidth = 2f,
+ pathEffect = pathEffect
+ )
+ }
+
+ // Vertical traces
+ for (i in 0..10) {
+ val x = (i * width / 10)
+ drawLine(
+ color = traceColor,
+ start = Offset(x, 0f),
+ end = Offset(x, height),
+ strokeWidth = 2f,
+ pathEffect = pathEffect
+ )
+ }
+
+ // Draw pads/components (small circles at intersections)
+ for (i in 0..10 step 2) {
+ for (j in 0..15 step 3) {
+ val x = i * width / 10
+ val y = j * height / 15
+ drawCircle(
+ color = padColor,
+ radius = 4f,
+ center = Offset(x, y)
+ )
+ }
+ }
+
+ // Draw some resistor-like rectangles
+ for (i in 1..9 step 4) {
+ val x = i * width / 10
+ val y = height / 2
+ drawRect(
+ color = Color(0xFF2A4A2A),
+ topLeft = Offset(x - 15f, y - 8f),
+ size = androidx.compose.ui.geometry.Size(30f, 16f)
+ )
+ // Resistor contacts
+ drawCircle(
+ color = padColor,
+ radius = 3f,
+ center = Offset(x - 15f, y)
+ )
+ drawCircle(
+ color = padColor,
+ radius = 3f,
+ center = Offset(x + 15f, y)
+ )
+ }
+ }
+}
diff --git a/composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/ui/Theme.kt b/composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/ui/Theme.kt
new file mode 100644
index 0000000..661cb09
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/ui/Theme.kt
@@ -0,0 +1,32 @@
+package coffee.liz.abstractionengine.ui
+
+import androidx.compose.material3.darkColorScheme
+import androidx.compose.ui.graphics.Color
+
+// GameBoy-inspired color palette
+object GameBoyColors {
+ val DarkestGreen = Color(0xFF0F380F)
+ val DarkGreen = Color(0xFF306230)
+ val MediumGreen = Color(0xFF8BAC0F)
+ val LightGreen = Color(0xFF9BBC0F)
+ val ScreenGreen = Color(0xFF8BAC0F)
+
+ // Accent colors for buttons (still retro but with more variety)
+ val ButtonRed = Color(0xFFE76F51)
+ val ButtonYellow = Color(0xFFF4A261)
+ val DPadGray = Color(0xFF4A5759)
+ val DPadLight = Color(0xFF6B7F82)
+}
+
+val GameBoyTheme = darkColorScheme(
+ primary = GameBoyColors.MediumGreen,
+ onPrimary = GameBoyColors.DarkestGreen,
+ secondary = GameBoyColors.LightGreen,
+ onSecondary = GameBoyColors.DarkestGreen,
+ background = GameBoyColors.DarkestGreen,
+ onBackground = GameBoyColors.LightGreen,
+ surface = GameBoyColors.DarkGreen,
+ onSurface = GameBoyColors.LightGreen,
+ surfaceVariant = GameBoyColors.DPadGray,
+ outline = GameBoyColors.DarkestGreen,
+)
diff --git a/composeApp/src/commonMain/kotlin/coffee/liz/ecs/Component.kt b/composeApp/src/commonMain/kotlin/coffee/liz/ecs/Component.kt
new file mode 100644
index 0000000..c64a863
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/coffee/liz/ecs/Component.kt
@@ -0,0 +1,7 @@
+package coffee.liz.ecs
+
+/**
+ * Marker interface for all components.
+ * Components are pure data containers with no behavior.
+ */
+interface Component
diff --git a/composeApp/src/commonMain/kotlin/coffee/liz/ecs/DAGWorld.kt b/composeApp/src/commonMain/kotlin/coffee/liz/ecs/DAGWorld.kt
new file mode 100644
index 0000000..d314065
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/coffee/liz/ecs/DAGWorld.kt
@@ -0,0 +1,127 @@
+package coffee.liz.ecs
+
+import kotlin.reflect.KClass
+
+/**
+ * World implementation that executes systems in dependency order using a DAG.
+ */
+class DAGWorld(systems: List<System>) : World {
+ private val entities = mutableSetOf<Entity>()
+ private val componentCache = mutableMapOf<KClass<out Component>, MutableSet<Entity>>()
+ private val systemExecutionOrder: List<System>
+ private var nextEntityId = 0
+
+ init {
+ systemExecutionOrder = buildExecutionOrder(systems)
+ }
+
+ override fun createEntity(): Entity {
+ val entity = Entity(nextEntityId++)
+ entities.add(entity)
+ return entity
+ }
+
+ override fun destroyEntity(entity: Entity) {
+ // Remove from all component caches
+ entity.componentTypes().forEach { componentType ->
+ componentCache[componentType]?.remove(entity)
+ }
+ entities.remove(entity)
+ }
+
+ override fun query(vararg componentTypes: KClass<out Component>): Set<Entity> {
+ if (componentTypes.isEmpty()) return entities.toSet()
+
+ // Start with entities that have the first component type
+ val firstType = componentTypes[0]
+ val candidates = componentCache[firstType] ?: return emptySet()
+
+ // Filter to entities that have all component types
+ return candidates.filter { entity ->
+ entity.hasAll(*componentTypes)
+ }.toSet()
+ }
+
+ override fun update(deltaTime: Float) {
+ // Update component cache based on current entity state
+ updateComponentCache()
+
+ // Execute systems in dependency order
+ systemExecutionOrder.forEach { system ->
+ system.update(this, deltaTime)
+ }
+ }
+
+ /**
+ * Rebuild the component cache based on current entity components.
+ * Call this after components have been added/removed from entities.
+ */
+ private fun updateComponentCache() {
+ componentCache.clear()
+ entities.forEach { entity ->
+ entity.componentTypes().forEach { componentType ->
+ componentCache.getOrPut(componentType) { mutableSetOf() }.add(entity)
+ }
+ }
+ }
+
+ /**
+ * Build a topologically sorted execution order from system dependencies.
+ * Uses Kahn's algorithm for topological sorting.
+ */
+ private fun buildExecutionOrder(systems: List<System>): List<System> {
+ if (systems.isEmpty()) return emptyList()
+
+ val systemMap = systems.associateBy { it::class }
+ val inDegree = mutableMapOf<KClass<out System>, Int>()
+ val adjacencyList = mutableMapOf<KClass<out System>, MutableSet<KClass<out System>>>()
+
+ // Initialize graph
+ systems.forEach { system ->
+ val systemClass = system::class
+ inDegree[systemClass] = 0
+ adjacencyList[systemClass] = mutableSetOf()
+ }
+
+ // Build dependency graph (reversed - dependencies point to dependents)
+ systems.forEach { system ->
+ system.dependencies.forEach { dependency ->
+ if (systemMap.containsKey(dependency)) {
+ adjacencyList[dependency]!!.add(system::class)
+ inDegree[system::class] = inDegree[system::class]!! + 1
+ }
+ }
+ }
+
+ // Kahn's algorithm
+ val queue = ArrayDeque<KClass<out System>>()
+ val result = mutableListOf<System>()
+
+ // Add all systems with no dependencies to queue
+ inDegree.forEach { (systemClass, degree) ->
+ if (degree == 0) {
+ queue.add(systemClass)
+ }
+ }
+
+ while (queue.isNotEmpty()) {
+ val currentClass = queue.removeFirst()
+ result.add(systemMap[currentClass]!!)
+
+ // Process dependents
+ adjacencyList[currentClass]?.forEach { dependent ->
+ inDegree[dependent] = inDegree[dependent]!! - 1
+ if (inDegree[dependent] == 0) {
+ queue.add(dependent)
+ }
+ }
+ }
+
+ // Check for cycles
+ if (result.size != systems.size) {
+ throw IllegalArgumentException("Circular dependency detected in systems")
+ }
+
+ return result
+ }
+}
diff --git a/composeApp/src/commonMain/kotlin/coffee/liz/ecs/Entity.kt b/composeApp/src/commonMain/kotlin/coffee/liz/ecs/Entity.kt
new file mode 100644
index 0000000..2ceac47
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/coffee/liz/ecs/Entity.kt
@@ -0,0 +1,70 @@
+package coffee.liz.ecs
+
+import kotlin.reflect.KClass
+
+/**
+ * An entity is a unique identifier with a collection of components.
+ */
+class Entity(val id: Int) {
+ private val components = mutableMapOf<KClass<out Component>, Component>()
+
+ /**
+ * Add a component to this entity.
+ */
+ fun <T : Component> add(component: T): Entity {
+ components[component::class] = component
+ return this
+ }
+
+ /**
+ * Remove a component from this entity.
+ */
+ fun <T : Component> remove(type: KClass<T>): Entity {
+ components.remove(type)
+ return this
+ }
+
+ /**
+ * Get a component by type.
+ */
+ @Suppress("UNCHECKED_CAST")
+ fun <T : Component> get(type: KClass<T>): T? {
+ return components[type] as? T
+ }
+
+ /**
+ * Check if entity has a component.
+ */
+ fun <T : Component> has(type: KClass<T>): Boolean {
+ return components.containsKey(type)
+ }
+
+ /**
+ * Check if entity has all specified components.
+ */
+ fun hasAll(vararg types: KClass<out Component>): Boolean {
+ return types.all { components.containsKey(it) }
+ }
+
+ /**
+ * Get all component types on this entity.
+ */
+ fun componentTypes(): Set<KClass<out Component>> {
+ return components.keys.toSet()
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is Entity) return false
+ return id == other.id
+ }
+
+ override fun hashCode(): Int = id
+
+ override fun toString(): String = "Entity($id)"
+}
+
+// Convenience extensions for cleaner syntax
+inline fun <reified T : Component> Entity.get(): T? = get(T::class)
+inline fun <reified T : Component> Entity.has(): Boolean = has(T::class)
+inline fun <reified T : Component> Entity.remove(): Entity = remove(T::class)
diff --git a/composeApp/src/commonMain/kotlin/coffee/liz/ecs/System.kt b/composeApp/src/commonMain/kotlin/coffee/liz/ecs/System.kt
new file mode 100644
index 0000000..0f8ef2a
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/coffee/liz/ecs/System.kt
@@ -0,0 +1,22 @@
+package coffee.liz.ecs
+
+import kotlin.reflect.KClass
+
+/**
+ * Systems contain the logic that operates on entities with specific components.
+ */
+interface System {
+ /**
+ * Systems that must run before this system in the update loop.
+ * Used to construct a dependency DAG.
+ */
+ val dependencies: Set<KClass<out System>>
+ get() = emptySet()
+
+ /**
+ * Update this system. Called once per frame.
+ * @param world The world containing entities and systems
+ * @param deltaTime Time elapsed since last update in seconds
+ */
+ fun update(world: World, deltaTime: Float)
+}
diff --git a/composeApp/src/commonMain/kotlin/coffee/liz/ecs/World.kt b/composeApp/src/commonMain/kotlin/coffee/liz/ecs/World.kt
new file mode 100644
index 0000000..fd3b6df
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/coffee/liz/ecs/World.kt
@@ -0,0 +1,42 @@
+package coffee.liz.ecs
+
+import kotlin.jvm.JvmName
+import kotlin.reflect.KClass
+
+/**
+ * The World manages entities and systems.
+ */
+interface World {
+ /**
+ * Create a new entity with a unique ID.
+ */
+ fun createEntity(): Entity
+
+ /**
+ * Destroy an entity and remove it from all caches.
+ */
+ fun destroyEntity(entity: Entity)
+
+ /**
+ * Get all entities that have all the specified component types.
+ */
+ fun query(vararg componentTypes: KClass<out Component>): Set<Entity>
+
+ /**
+ * Update all systems in dependency order.
+ * @param deltaTime Time elapsed since last update in seconds
+ */
+ fun update(deltaTime: Float)
+}
+
+// Convenience extension for queries
+@JvmName("query1")
+inline fun <reified T : Component> World.query(): Set<Entity> = query(T::class)
+
+@JvmName("query2")
+inline fun <reified T1 : Component, reified T2 : Component> World.query(): Set<Entity> =
+ query(T1::class, T2::class)
+
+@JvmName("query3")
+inline fun <reified T1 : Component, reified T2 : Component, reified T3 : Component> World.query(): Set<Entity> =
+ query(T1::class, T2::class, T3::class)
diff --git a/composeApp/src/commonMain/kotlin/coffee/liz/ecs/animation/AnimationComponents.kt b/composeApp/src/commonMain/kotlin/coffee/liz/ecs/animation/AnimationComponents.kt
new file mode 100644
index 0000000..4289eec
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/coffee/liz/ecs/animation/AnimationComponents.kt
@@ -0,0 +1,114 @@
+package coffee.liz.ecs.animation
+
+import coffee.liz.ecs.Component
+import kotlin.jvm.JvmInline
+
+/**
+ * Loop modes for animation playback.
+ */
+enum class LoopMode {
+ /** Play once and stop on last frame */
+ ONCE,
+ /** Repeat from beginning */
+ LOOP,
+ /** Play forward, then backward, repeat */
+ PING_PONG
+}
+
+/**
+ * Type-safe wrapper for animation names.
+ */
+@JvmInline
+value class AnimationName(val value: String)
+
+/**
+ * Type-safe wrapper for frame names.
+ */
+@JvmInline
+value class FrameName(val value: String)
+
+/**
+ * Represents a rectangle region in a sprite sheet.
+ */
+data class Rect(
+ val x: Int,
+ val y: Int,
+ val width: Int,
+ val height: Int
+)
+
+/**
+ * Defines a single animation clip with frames and playback settings.
+ */
+data class AnimationClip(
+ val frameNames: List<FrameName>,
+ val frameDuration: Float, // seconds per frame
+ val loopMode: LoopMode = LoopMode.LOOP
+)
+
+/**
+ * Component containing sprite sheet data - the image and frame definitions.
+ * This is immutable data that can be shared across entities.
+ */
+data class SpriteSheet(
+ val imagePath: String,
+ val frames: Map<FrameName, Rect>
+) : Component
+
+/**
+ * Component for animation playback state.
+ * Contains animation clips and current playback state.
+ */
+data class Animator(
+ val clips: Map<AnimationName, AnimationClip>,
+ var currentClip: AnimationName,
+ var frameIndex: Int = 0,
+ var elapsed: Float = 0f,
+ var playing: Boolean = true,
+ var direction: Int = 1 // 1 for forward, -1 for backward (used in PING_PONG)
+) : Component {
+
+ /**
+ * Play a specific animation clip by name.
+ * Resets playback state when switching clips.
+ */
+ fun play(clipName: AnimationName, restart: Boolean = true) {
+ if (currentClip != clipName || restart) {
+ currentClip = clipName
+ frameIndex = 0
+ elapsed = 0f
+ direction = 1
+ playing = true
+ }
+ }
+
+ /**
+ * Pause the current animation.
+ */
+ fun pause() {
+ playing = false
+ }
+
+ /**
+ * Resume the current animation.
+ */
+ fun resume() {
+ playing = true
+ }
+
+ /**
+ * Get the current frame name being displayed.
+ */
+ fun getCurrentFrameName(): FrameName? {
+ val clip = clips[currentClip] ?: return null
+ return clip.frameNames.getOrNull(frameIndex)
+ }
+}
+
+/**
+ * Component for entity position in 2D space.
+ */
+data class Position(
+ var x: Float,
+ var y: Float
+) : Component
diff --git a/composeApp/src/commonMain/kotlin/coffee/liz/ecs/animation/AnimationSystem.kt b/composeApp/src/commonMain/kotlin/coffee/liz/ecs/animation/AnimationSystem.kt
new file mode 100644
index 0000000..7ae405e
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/coffee/liz/ecs/animation/AnimationSystem.kt
@@ -0,0 +1,77 @@
+package coffee.liz.ecs.animation
+
+import coffee.liz.ecs.System
+import coffee.liz.ecs.World
+
+/**
+ * System that updates animation playback state for all entities with an Animator component.
+ *
+ * This system:
+ * - Updates elapsed time for playing animations
+ * - Advances frame indices based on frame duration
+ * - Handles loop modes (ONCE, LOOP, PING_PONG)
+ * - Stops animations when they complete (for ONCE mode)
+ */
+class AnimationSystem : System {
+ override fun update(world: World, deltaTime: Float) {
+ // Query all entities that have both Animator and SpriteSheet
+ world.query(Animator::class, SpriteSheet::class).forEach { entity ->
+ val animator = entity.get(Animator::class) ?: return@forEach
+
+ // Skip if animation is not playing
+ if (!animator.playing) return@forEach
+
+ // Get the current animation clip
+ val clip = animator.clips[animator.currentClip] ?: return@forEach
+
+ // Accumulate elapsed time
+ animator.elapsed += deltaTime
+
+ // Advance frames while we have enough elapsed time
+ while (animator.elapsed >= clip.frameDuration) {
+ animator.elapsed -= clip.frameDuration
+ advanceFrame(animator, clip)
+ }
+ }
+ }
+
+ /**
+ * Advances the animation to the next frame based on loop mode.
+ */
+ private fun advanceFrame(animator: Animator, clip: AnimationClip) {
+ animator.frameIndex += animator.direction
+
+ when (clip.loopMode) {
+ LoopMode.ONCE -> {
+ // Play once and stop on last frame
+ if (animator.frameIndex >= clip.frameNames.size) {
+ animator.frameIndex = clip.frameNames.size - 1
+ animator.playing = false
+ }
+ }
+
+ LoopMode.LOOP -> {
+ // Loop back to beginning
+ if (animator.frameIndex >= clip.frameNames.size) {
+ animator.frameIndex = 0
+ } else if (animator.frameIndex < 0) {
+ // Handle negative wrap (shouldn't happen in LOOP but be safe)
+ animator.frameIndex = clip.frameNames.size - 1
+ }
+ }
+
+ LoopMode.PING_PONG -> {
+ // Bounce back and forth
+ if (animator.frameIndex >= clip.frameNames.size) {
+ // Hit the end, reverse direction
+ animator.frameIndex = (clip.frameNames.size - 2).coerceAtLeast(0)
+ animator.direction = -1
+ } else if (animator.frameIndex < 0) {
+ // Hit the beginning, reverse direction
+ animator.frameIndex = 1.coerceAtMost(clip.frameNames.size - 1)
+ animator.direction = 1
+ }
+ }
+ }
+ }
+}
diff --git a/composeApp/src/commonTest/kotlin/coffee/liz/abstractionengine/ComposeAppCommonTest.kt b/composeApp/src/commonTest/kotlin/coffee/liz/abstractionengine/ComposeAppCommonTest.kt
new file mode 100644
index 0000000..78e642f
--- /dev/null
+++ b/composeApp/src/commonTest/kotlin/coffee/liz/abstractionengine/ComposeAppCommonTest.kt
@@ -0,0 +1,12 @@
+package coffee.liz.abstractionengine
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+
+class ComposeAppCommonTest {
+
+ @Test
+ fun example() {
+ assertEquals(3, 1 + 2)
+ }
+} \ No newline at end of file
diff --git a/composeApp/src/commonTest/kotlin/coffee/liz/ecs/DAGWorldTest.kt b/composeApp/src/commonTest/kotlin/coffee/liz/ecs/DAGWorldTest.kt
new file mode 100644
index 0000000..737e386
--- /dev/null
+++ b/composeApp/src/commonTest/kotlin/coffee/liz/ecs/DAGWorldTest.kt
@@ -0,0 +1,242 @@
+package coffee.liz.ecs
+
+import kotlin.reflect.KClass
+import kotlin.test.*
+
+// Test systems for circular dependency test
+class CircularSystemA : System {
+ override val dependencies: Set<KClass<out System>>
+ get() = setOf(CircularSystemB::class)
+ override fun update(world: World, deltaTime: Float) {}
+}
+
+class CircularSystemB : System {
+ override val dependencies: Set<KClass<out System>>
+ get() = setOf(CircularSystemA::class)
+ override fun update(world: World, deltaTime: Float) {}
+}
+
+class DAGWorldTest {
+
+ @Test
+ fun `can create entities with unique ids`() {
+ val world = DAGWorld(emptyList())
+
+ val entity1 = world.createEntity()
+ val entity2 = world.createEntity()
+
+ assertNotEquals(entity1.id, entity2.id)
+ }
+
+ @Test
+ fun `can destroy entity`() {
+ val world = DAGWorld(emptyList())
+ val entity = world.createEntity()
+ entity.add(Position(10f, 20f))
+
+ world.destroyEntity(entity)
+
+ val results = world.query<Position>()
+ assertFalse(results.contains(entity))
+ }
+
+ @Test
+ fun `query returns entities with matching components`() {
+ val world = DAGWorld(emptyList())
+ val entity1 = world.createEntity().add(Position(10f, 20f))
+ val entity2 = world.createEntity().add(Position(30f, 40f))
+ val entity3 = world.createEntity().add(Velocity(1f, 2f))
+
+ world.update(0f) // Trigger cache update
+
+ val results = world.query<Position>()
+
+ assertEquals(2, results.size)
+ assertTrue(results.contains(entity1))
+ assertTrue(results.contains(entity2))
+ assertFalse(results.contains(entity3))
+ }
+
+ @Test
+ fun `query with multiple components returns intersection`() {
+ val world = DAGWorld(emptyList())
+ val entity1 = world.createEntity()
+ .add(Position(10f, 20f))
+ .add(Velocity(1f, 2f))
+ val entity2 = world.createEntity()
+ .add(Position(30f, 40f))
+ val entity3 = world.createEntity()
+ .add(Velocity(3f, 4f))
+
+ world.update(0f) // Trigger cache update
+
+ val results = world.query<Position, Velocity>()
+
+ assertEquals(1, results.size)
+ assertTrue(results.contains(entity1))
+ }
+
+ @Test
+ fun `systems execute in dependency order`() {
+ val executionOrder = mutableListOf<String>()
+
+ val systemA = object : System {
+ override fun update(world: World, deltaTime: Float) {
+ executionOrder.add("A")
+ }
+ }
+
+ val systemB = object : System {
+ override val dependencies = setOf(systemA::class)
+ override fun update(world: World, deltaTime: Float) {
+ executionOrder.add("B")
+ }
+ }
+
+ val systemC = object : System {
+ override val dependencies = setOf(systemB::class)
+ override fun update(world: World, deltaTime: Float) {
+ executionOrder.add("C")
+ }
+ }
+
+ val world = DAGWorld(listOf(systemC, systemA, systemB))
+ world.update(0f)
+
+ assertEquals(listOf("A", "B", "C"), executionOrder)
+ }
+
+ @Test
+ fun `systems with no dependencies can execute in any order`() {
+ val executionOrder = mutableListOf<String>()
+
+ val systemA = object : System {
+ override fun update(world: World, deltaTime: Float) {
+ executionOrder.add("A")
+ }
+ }
+
+ val systemB = object : System {
+ override fun update(world: World, deltaTime: Float) {
+ executionOrder.add("B")
+ }
+ }
+
+ val world = DAGWorld(listOf(systemA, systemB))
+ world.update(0f)
+
+ assertEquals(2, executionOrder.size)
+ assertTrue(executionOrder.contains("A"))
+ assertTrue(executionOrder.contains("B"))
+ }
+
+ @Test
+ fun `circular dependency throws exception`() {
+ val systemA = CircularSystemA()
+ val systemB = CircularSystemB()
+
+ assertFailsWith<IllegalArgumentException> {
+ DAGWorld(listOf(systemA, systemB))
+ }
+ }
+
+ @Test
+ fun `system receives correct deltaTime`() {
+ var receivedDelta = 0f
+
+ val system = object : System {
+ override fun update(world: World, deltaTime: Float) {
+ receivedDelta = deltaTime
+ }
+ }
+
+ val world = DAGWorld(listOf(system))
+ world.update(0.016f)
+
+ assertEquals(0.016f, receivedDelta)
+ }
+
+ @Test
+ fun `systems can query entities during update`() {
+ var queriedEntities: Set<Entity>? = null
+
+ val system = object : System {
+ override fun update(world: World, deltaTime: Float) {
+ queriedEntities = world.query<Position>()
+ }
+ }
+
+ val world = DAGWorld(listOf(system))
+ val entity = world.createEntity().add(Position(10f, 20f))
+
+ world.update(0f)
+
+ assertNotNull(queriedEntities)
+ assertEquals(1, queriedEntities!!.size)
+ assertTrue(queriedEntities!!.contains(entity))
+ }
+
+ @Test
+ fun `component cache updates after entity changes`() {
+ val world = DAGWorld(emptyList())
+ val entity = world.createEntity()
+
+ world.update(0f)
+ assertEquals(0, world.query<Position>().size)
+
+ entity.add(Position(10f, 20f))
+ world.update(0f)
+ assertEquals(1, world.query<Position>().size)
+
+ entity.remove<Position>()
+ world.update(0f)
+ assertEquals(0, world.query<Position>().size)
+ }
+
+ @Test
+ fun `diamond dependency resolves correctly`() {
+ val executionOrder = mutableListOf<String>()
+
+ // A
+ // / \
+ // B C
+ // \ /
+ // D
+
+ val systemA = object : System {
+ override fun update(world: World, deltaTime: Float) {
+ executionOrder.add("A")
+ }
+ }
+
+ val systemB = object : System {
+ override val dependencies = setOf(systemA::class)
+ override fun update(world: World, deltaTime: Float) {
+ executionOrder.add("B")
+ }
+ }
+
+ val systemC = object : System {
+ override val dependencies = setOf(systemA::class)
+ override fun update(world: World, deltaTime: Float) {
+ executionOrder.add("C")
+ }
+ }
+
+ val systemD = object : System {
+ override val dependencies = setOf(systemB::class, systemC::class)
+ override fun update(world: World, deltaTime: Float) {
+ executionOrder.add("D")
+ }
+ }
+
+ val world = DAGWorld(listOf(systemD, systemC, systemB, systemA))
+ world.update(0f)
+
+ // A must come first, D must come last, B and C in between
+ assertEquals("A", executionOrder.first())
+ assertEquals("D", executionOrder.last())
+ assertTrue(executionOrder.indexOf("B") < executionOrder.indexOf("D"))
+ assertTrue(executionOrder.indexOf("C") < executionOrder.indexOf("D"))
+ }
+}
diff --git a/composeApp/src/commonTest/kotlin/coffee/liz/ecs/EntityTest.kt b/composeApp/src/commonTest/kotlin/coffee/liz/ecs/EntityTest.kt
new file mode 100644
index 0000000..e807bd2
--- /dev/null
+++ b/composeApp/src/commonTest/kotlin/coffee/liz/ecs/EntityTest.kt
@@ -0,0 +1,116 @@
+package coffee.liz.ecs
+
+import kotlin.test.*
+
+// Test components
+data class Position(val x: Float, val y: Float) : Component
+data class Velocity(val dx: Float, val dy: Float) : Component
+data class Health(val value: Int) : Component
+
+class EntityTest {
+
+ @Test
+ fun `entity has unique id`() {
+ val entity1 = Entity(1)
+ val entity2 = Entity(2)
+
+ assertNotEquals(entity1.id, entity2.id)
+ }
+
+ @Test
+ fun `can add component to entity`() {
+ val entity = Entity(0)
+ val position = Position(10f, 20f)
+
+ entity.add(position)
+
+ assertTrue(entity.has<Position>())
+ assertEquals(position, entity.get<Position>())
+ }
+
+ @Test
+ fun `can remove component from entity`() {
+ val entity = Entity(0)
+ entity.add(Position(10f, 20f))
+
+ entity.remove<Position>()
+
+ assertFalse(entity.has<Position>())
+ assertNull(entity.get<Position>())
+ }
+
+ @Test
+ fun `add returns entity for chaining`() {
+ val entity = Entity(0)
+
+ val result = entity
+ .add(Position(10f, 20f))
+ .add(Velocity(1f, 2f))
+
+ assertSame(entity, result)
+ assertTrue(entity.has<Position>())
+ assertTrue(entity.has<Velocity>())
+ }
+
+ @Test
+ fun `can replace component by adding same type`() {
+ val entity = Entity(0)
+ entity.add(Position(10f, 20f))
+ entity.add(Position(30f, 40f))
+
+ val position = entity.get<Position>()
+
+ assertNotNull(position)
+ assertEquals(30f, position.x)
+ assertEquals(40f, position.y)
+ }
+
+ @Test
+ fun `hasAll returns true when entity has all components`() {
+ val entity = Entity(0)
+ entity.add(Position(10f, 20f))
+ entity.add(Velocity(1f, 2f))
+
+ assertTrue(entity.hasAll(Position::class, Velocity::class))
+ }
+
+ @Test
+ fun `hasAll returns false when entity missing component`() {
+ val entity = Entity(0)
+ entity.add(Position(10f, 20f))
+
+ assertFalse(entity.hasAll(Position::class, Velocity::class))
+ }
+
+ @Test
+ fun `componentTypes returns all component types`() {
+ val entity = Entity(0)
+ entity.add(Position(10f, 20f))
+ entity.add(Velocity(1f, 2f))
+ entity.add(Health(100))
+
+ val types = entity.componentTypes()
+
+ assertEquals(3, types.size)
+ assertTrue(types.contains(Position::class))
+ assertTrue(types.contains(Velocity::class))
+ assertTrue(types.contains(Health::class))
+ }
+
+ @Test
+ fun `entities with same id are equal`() {
+ val entity1 = Entity(1)
+ val entity2 = Entity(1)
+
+ assertEquals(entity1, entity2)
+ assertEquals(entity1.hashCode(), entity2.hashCode())
+ }
+
+ @Test
+ fun `entities with different id are not equal`() {
+ val entity1 = Entity(1)
+ val entity2 = Entity(2)
+
+ assertNotEquals(entity1, entity2)
+ }
+}
diff --git a/composeApp/src/commonTest/kotlin/coffee/liz/ecs/animation/AnimationSystemTest.kt b/composeApp/src/commonTest/kotlin/coffee/liz/ecs/animation/AnimationSystemTest.kt
new file mode 100644
index 0000000..bb7298e
--- /dev/null
+++ b/composeApp/src/commonTest/kotlin/coffee/liz/ecs/animation/AnimationSystemTest.kt
@@ -0,0 +1,238 @@
+package coffee.liz.ecs.animation
+
+import coffee.liz.ecs.DAGWorld
+import kotlin.test.*
+
+class AnimationSystemTest {
+
+ private fun createTestSpriteSheet(): SpriteSheet {
+ return SpriteSheet(
+ imagePath = "test.png",
+ frames = mapOf(
+ FrameName("idle_0") to Rect(0, 0, 32, 32),
+ FrameName("idle_1") to Rect(32, 0, 32, 32),
+ FrameName("walk_0") to Rect(0, 32, 32, 32),
+ FrameName("walk_1") to Rect(32, 32, 32, 32),
+ FrameName("walk_2") to Rect(64, 32, 32, 32)
+ )
+ )
+ }
+
+ private fun createTestAnimator(): Animator {
+ return Animator(
+ clips = mapOf(
+ AnimationName("idle") to AnimationClip(
+ frameNames = listOf(FrameName("idle_0"), FrameName("idle_1")),
+ frameDuration = 0.1f,
+ loopMode = LoopMode.LOOP
+ ),
+ AnimationName("walk") to AnimationClip(
+ frameNames = listOf(
+ FrameName("walk_0"),
+ FrameName("walk_1"),
+ FrameName("walk_2")
+ ),
+ frameDuration = 0.15f,
+ loopMode = LoopMode.LOOP
+ )
+ ),
+ currentClip = AnimationName("idle")
+ )
+ }
+
+ @Test
+ fun `animation advances frame after frame duration`() {
+ val world = DAGWorld(listOf(AnimationSystem()))
+ val entity = world.createEntity()
+ val animator = createTestAnimator()
+ entity.add(createTestSpriteSheet())
+ entity.add(animator)
+
+ assertEquals(0, animator.frameIndex)
+ assertEquals(0f, animator.elapsed)
+
+ // Update with less than frame duration - should not advance
+ world.update(0.05f)
+ assertEquals(0, animator.frameIndex)
+ assertEquals(0.05f, animator.elapsed)
+
+ // Update to exceed frame duration - should advance to frame 1
+ world.update(0.06f)
+ assertEquals(1, animator.frameIndex)
+ assertTrue(animator.elapsed < 0.1f)
+ }
+
+ @Test
+ fun `LOOP mode wraps to beginning`() {
+ val world = DAGWorld(listOf(AnimationSystem()))
+ val entity = world.createEntity()
+ val animator = createTestAnimator()
+ entity.add(createTestSpriteSheet())
+ entity.add(animator)
+
+ // Idle animation has 2 frames at 0.1s each
+ world.update(0.1f) // Frame 1
+ assertEquals(1, animator.frameIndex)
+
+ world.update(0.1f) // Should wrap to Frame 0
+ assertEquals(0, animator.frameIndex)
+ assertTrue(animator.playing) // Still playing
+ }
+
+ @Test
+ fun `ONCE mode stops at last frame`() {
+ val world = DAGWorld(listOf(AnimationSystem()))
+ val entity = world.createEntity()
+ val animator = Animator(
+ clips = mapOf(
+ AnimationName("once") to AnimationClip(
+ frameNames = listOf(FrameName("idle_0"), FrameName("idle_1")),
+ frameDuration = 0.1f,
+ loopMode = LoopMode.ONCE
+ )
+ ),
+ currentClip = AnimationName("once")
+ )
+ entity.add(createTestSpriteSheet())
+ entity.add(animator)
+
+ world.update(0.1f) // Frame 1
+ assertEquals(1, animator.frameIndex)
+ assertTrue(animator.playing)
+
+ world.update(0.1f) // Should stop at frame 1
+ assertEquals(1, animator.frameIndex)
+ assertFalse(animator.playing)
+
+ // Further updates should not change anything
+ world.update(0.1f)
+ assertEquals(1, animator.frameIndex)
+ assertFalse(animator.playing)
+ }
+
+ @Test
+ fun `PING_PONG mode bounces back and forth`() {
+ val world = DAGWorld(listOf(AnimationSystem()))
+ val entity = world.createEntity()
+ val animator = Animator(
+ clips = mapOf(
+ AnimationName("pingpong") to AnimationClip(
+ frameNames = listOf(
+ FrameName("walk_0"),
+ FrameName("walk_1"),
+ FrameName("walk_2")
+ ),
+ frameDuration = 0.1f,
+ loopMode = LoopMode.PING_PONG
+ )
+ ),
+ currentClip = AnimationName("pingpong")
+ )
+ entity.add(createTestSpriteSheet())
+ entity.add(animator)
+
+ // Forward: 0 -> 1 -> 2
+ world.update(0.1f)
+ assertEquals(1, animator.frameIndex)
+ assertEquals(1, animator.direction)
+
+ world.update(0.1f)
+ assertEquals(2, animator.frameIndex)
+ assertEquals(1, animator.direction)
+
+ // Hit end, should reverse: 2 -> 1
+ world.update(0.1f)
+ assertEquals(1, animator.frameIndex)
+ assertEquals(-1, animator.direction)
+
+ // Continue backward: 1 -> 0
+ world.update(0.1f)
+ assertEquals(0, animator.frameIndex)
+ assertEquals(-1, animator.direction)
+
+ // Hit beginning, should reverse: 0 -> 1
+ world.update(0.1f)
+ assertEquals(1, animator.frameIndex)
+ assertEquals(1, animator.direction)
+ }
+
+ @Test
+ fun `play() switches animation and resets state`() {
+ val animator = createTestAnimator()
+ assertEquals(AnimationName("idle"), animator.currentClip)
+ assertEquals(0, animator.frameIndex)
+
+ // Manually advance
+ animator.frameIndex = 1
+ animator.elapsed = 0.05f
+
+ // Switch to walk animation
+ animator.play(AnimationName("walk"))
+ assertEquals(AnimationName("walk"), animator.currentClip)
+ assertEquals(0, animator.frameIndex)
+ assertEquals(0f, animator.elapsed)
+ assertEquals(1, animator.direction)
+ assertTrue(animator.playing)
+ }
+
+ @Test
+ fun `pause() and resume() control playback`() {
+ val world = DAGWorld(listOf(AnimationSystem()))
+ val entity = world.createEntity()
+ val animator = createTestAnimator()
+ entity.add(createTestSpriteSheet())
+ entity.add(animator)
+
+ animator.pause()
+ assertFalse(animator.playing)
+
+ // Update should not advance animation
+ world.update(0.1f)
+ assertEquals(0, animator.frameIndex)
+
+ animator.resume()
+ assertTrue(animator.playing)
+
+ // Now it should advance
+ world.update(0.1f)
+ assertEquals(1, animator.frameIndex)
+ }
+
+ @Test
+ fun `getCurrentFrameName returns correct frame`() {
+ val animator = createTestAnimator()
+
+ assertEquals(FrameName("idle_0"), animator.getCurrentFrameName())
+
+ animator.frameIndex = 1
+ assertEquals(FrameName("idle_1"), animator.getCurrentFrameName())
+
+ animator.play(AnimationName("walk"))
+ assertEquals(FrameName("walk_0"), animator.getCurrentFrameName())
+ }
+
+ @Test
+ fun `multiple updates in single frame work correctly`() {
+ val world = DAGWorld(listOf(AnimationSystem()))
+ val entity = world.createEntity()
+ val animator = createTestAnimator()
+ entity.add(createTestSpriteSheet())
+ entity.add(animator)
+
+ // Large delta time should advance multiple frames
+ world.update(0.25f) // Should advance 2 frames and have 0.05s remainder
+ assertEquals(0, animator.frameIndex) // Wrapped back to 0 (LOOP mode)
+ assertTrue(animator.elapsed >= 0.04f && animator.elapsed <= 0.06f)
+ }
+
+ @Test
+ fun `entities without SpriteSheet are skipped`() {
+ val world = DAGWorld(listOf(AnimationSystem()))
+ val entity = world.createEntity()
+ val animator = createTestAnimator()
+ entity.add(animator) // No SpriteSheet
+
+ // Should not crash
+ world.update(0.1f)
+ }
+}
diff --git a/composeApp/src/iosMain/kotlin/coffee/liz/abstractionengine/MainViewController.kt b/composeApp/src/iosMain/kotlin/coffee/liz/abstractionengine/MainViewController.kt
new file mode 100644
index 0000000..4f98625
--- /dev/null
+++ b/composeApp/src/iosMain/kotlin/coffee/liz/abstractionengine/MainViewController.kt
@@ -0,0 +1,5 @@
+package coffee.liz.abstractionengine
+
+import androidx.compose.ui.window.ComposeUIViewController
+
+fun MainViewController() = ComposeUIViewController { App() } \ No newline at end of file
diff --git a/composeApp/src/iosMain/kotlin/coffee/liz/abstractionengine/Platform.ios.kt b/composeApp/src/iosMain/kotlin/coffee/liz/abstractionengine/Platform.ios.kt
new file mode 100644
index 0000000..a6362e3
--- /dev/null
+++ b/composeApp/src/iosMain/kotlin/coffee/liz/abstractionengine/Platform.ios.kt
@@ -0,0 +1,9 @@
+package coffee.liz.abstractionengine
+
+import platform.UIKit.UIDevice
+
+class IOSPlatform: Platform {
+ override val name: String = UIDevice.currentDevice.systemName() + " " + UIDevice.currentDevice.systemVersion
+}
+
+actual fun getPlatform(): Platform = IOSPlatform() \ No newline at end of file
diff --git a/composeApp/src/jvmMain/kotlin/coffee/liz/abstractionengine/Platform.jvm.kt b/composeApp/src/jvmMain/kotlin/coffee/liz/abstractionengine/Platform.jvm.kt
new file mode 100644
index 0000000..e1487e0
--- /dev/null
+++ b/composeApp/src/jvmMain/kotlin/coffee/liz/abstractionengine/Platform.jvm.kt
@@ -0,0 +1,7 @@
+package coffee.liz.abstractionengine
+
+class JVMPlatform: Platform {
+ override val name: String = "Java ${System.getProperty("java.version")}"
+}
+
+actual fun getPlatform(): Platform = JVMPlatform() \ No newline at end of file
diff --git a/composeApp/src/jvmMain/kotlin/coffee/liz/abstractionengine/main.kt b/composeApp/src/jvmMain/kotlin/coffee/liz/abstractionengine/main.kt
new file mode 100644
index 0000000..a426fb4
--- /dev/null
+++ b/composeApp/src/jvmMain/kotlin/coffee/liz/abstractionengine/main.kt
@@ -0,0 +1,13 @@
+package coffee.liz.abstractionengine
+
+import androidx.compose.ui.window.Window
+import androidx.compose.ui.window.application
+
+fun main() = application {
+ Window(
+ onCloseRequest = ::exitApplication,
+ title = "The Abstraction Engine",
+ ) {
+ App()
+ }
+} \ No newline at end of file
diff --git a/gradle.properties b/gradle.properties
new file mode 100644
index 0000000..6f8e6ea
--- /dev/null
+++ b/gradle.properties
@@ -0,0 +1,12 @@
+#Kotlin
+kotlin.code.style=official
+kotlin.daemon.jvmargs=-Xmx3072M
+
+#Gradle
+org.gradle.jvmargs=-Xmx4096M -Dfile.encoding=UTF-8
+org.gradle.configuration-cache=true
+org.gradle.caching=true
+
+#Android
+android.nonTransitiveRClass=true
+android.useAndroidX=true \ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
new file mode 100644
index 0000000..595245f
--- /dev/null
+++ b/gradle/libs.versions.toml
@@ -0,0 +1,37 @@
+[versions]
+agp = "8.11.2"
+android-compileSdk = "36"
+android-minSdk = "24"
+android-targetSdk = "36"
+androidx-activity = "1.11.0"
+androidx-appcompat = "1.7.1"
+androidx-core = "1.17.0"
+androidx-espresso = "3.7.0"
+androidx-lifecycle = "2.9.4"
+androidx-testExt = "1.3.0"
+composeHotReload = "1.0.0-beta07"
+composeMultiplatform = "1.9.0"
+junit = "4.13.2"
+kotlin = "2.2.20"
+kotlinx-coroutines = "1.10.2"
+
+[libraries]
+kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
+kotlin-testJunit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" }
+junit = { module = "junit:junit", version.ref = "junit" }
+androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "androidx-core" }
+androidx-testExt-junit = { module = "androidx.test.ext:junit", version.ref = "androidx-testExt" }
+androidx-espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "androidx-espresso" }
+androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidx-appcompat" }
+androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activity" }
+androidx-lifecycle-viewmodelCompose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "androidx-lifecycle" }
+androidx-lifecycle-runtimeCompose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-runtime-compose", version.ref = "androidx-lifecycle" }
+kotlinx-coroutinesSwing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" }
+
+[plugins]
+androidApplication = { id = "com.android.application", version.ref = "agp" }
+androidLibrary = { id = "com.android.library", version.ref = "agp" }
+composeHotReload = { id = "org.jetbrains.compose.hot-reload", version.ref = "composeHotReload" }
+composeMultiplatform = { id = "org.jetbrains.compose", version.ref = "composeMultiplatform" }
+composeCompiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
+kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } \ No newline at end of file
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..1b33c55
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.jar
Binary files differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..d4081da
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,7 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
+networkTimeout=10000
+validateDistributionUrl=true
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/gradlew b/gradlew
new file mode 100755
index 0000000..23d15a9
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,251 @@
+#!/bin/sh
+
+#
+# Copyright © 2015-2021 the original authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# SPDX-License-Identifier: Apache-2.0
+#
+
+##############################################################################
+#
+# Gradle start up script for POSIX generated by Gradle.
+#
+# Important for running:
+#
+# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+# noncompliant, but you have some other compliant shell such as ksh or
+# bash, then to run this script, type that shell name before the whole
+# command line, like:
+#
+# ksh Gradle
+#
+# Busybox and similar reduced shells will NOT work, because this script
+# requires all of these POSIX shell features:
+# * functions;
+# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
+# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
+# * compound commands having a testable exit status, especially «case»;
+# * various built-in commands including «command», «set», and «ulimit».
+#
+# Important for patching:
+#
+# (2) This script targets any POSIX shell, so it avoids extensions provided
+# by Bash, Ksh, etc; in particular arrays are avoided.
+#
+# The "traditional" practice of packing multiple parameters into a
+# space-separated string is a well documented source of bugs and security
+# problems, so this is (mostly) avoided, by progressively accumulating
+# options in "$@", and eventually passing that to Java.
+#
+# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+# see the in-line comments for details.
+#
+# There are tweaks for specific operating systems such as AIX, CygWin,
+# Darwin, MinGW, and NonStop.
+#
+# (3) This script is generated from the Groovy template
+# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+# within the Gradle project.
+#
+# You can find Gradle at https://github.com/gradle/gradle/.
+#
+##############################################################################
+
+# Attempt to set APP_HOME
+
+# Resolve links: $0 may be a link
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+ APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
+ [ -h "$app_path" ]
+do
+ ls=$( ls -ld "$app_path" )
+ link=${ls#*' -> '}
+ case $link in #(
+ /*) app_path=$link ;; #(
+ *) app_path=$APP_HOME$link ;;
+ esac
+done
+
+# This is normally unused
+# shellcheck disable=SC2034
+APP_BASE_NAME=${0##*/}
+# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
+APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD=maximum
+
+warn () {
+ echo "$*"
+} >&2
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+} >&2
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "$( uname )" in #(
+ CYGWIN* ) cygwin=true ;; #(
+ Darwin* ) darwin=true ;; #(
+ MSYS* | MINGW* ) msys=true ;; #(
+ NONSTOP* ) nonstop=true ;;
+esac
+
+CLASSPATH="\\\"\\\""
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD=$JAVA_HOME/jre/sh/java
+ else
+ JAVACMD=$JAVA_HOME/bin/java
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD=java
+ if ! command -v java >/dev/null 2>&1
+ then
+ die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+fi
+
+# Increase the maximum file descriptors if we can.
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+ case $MAX_FD in #(
+ max*)
+ # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ MAX_FD=$( ulimit -H -n ) ||
+ warn "Could not query maximum file descriptor limit"
+ esac
+ case $MAX_FD in #(
+ '' | soft) :;; #(
+ *)
+ # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ ulimit -n "$MAX_FD" ||
+ warn "Could not set maximum file descriptor limit to $MAX_FD"
+ esac
+fi
+
+# Collect all arguments for the java command, stacking in reverse order:
+# * args from the command line
+# * the main class name
+# * -classpath
+# * -D...appname settings
+# * --module-path (only if needed)
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if "$cygwin" || "$msys" ; then
+ APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+ CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
+
+ JAVACMD=$( cygpath --unix "$JAVACMD" )
+
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ for arg do
+ if
+ case $arg in #(
+ -*) false ;; # don't mess with options #(
+ /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
+ [ -e "$t" ] ;; #(
+ *) false ;;
+ esac
+ then
+ arg=$( cygpath --path --ignore --mixed "$arg" )
+ fi
+ # Roll the args list around exactly as many times as the number of
+ # args, so each arg winds up back in the position where it started, but
+ # possibly modified.
+ #
+ # NB: a `for` loop captures its iteration list before it begins, so
+ # changing the positional parameters here affects neither the number of
+ # iterations, nor the values presented in `arg`.
+ shift # remove old arg
+ set -- "$@" "$arg" # push replacement arg
+ done
+fi
+
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Collect all arguments for the java command:
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
+# and any embedded shellness will be escaped.
+# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
+# treated as '${Hostname}' itself on the command line.
+
+set -- \
+ "-Dorg.gradle.appname=$APP_BASE_NAME" \
+ -classpath "$CLASSPATH" \
+ -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
+ "$@"
+
+# Stop when "xargs" is not available.
+if ! command -v xargs >/dev/null 2>&1
+then
+ die "xargs is not available"
+fi
+
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+# set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
+
+eval "set -- $(
+ printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+ xargs -n1 |
+ sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+ tr '\n' ' '
+ )" '"$@"'
+
+exec "$JAVACMD" "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 0000000..5eed7ee
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,94 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+@rem SPDX-License-Identifier: Apache-2.0
+@rem
+
+@if "%DEBUG%"=="" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%"=="" set DIRNAME=.
+@rem This is normally unused
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if %ERRORLEVEL% equ 0 goto execute
+
+echo. 1>&2
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo. 1>&2
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if %ERRORLEVEL% equ 0 goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+set EXIT_CODE=%ERRORLEVEL%
+if %EXIT_CODE% equ 0 set EXIT_CODE=1
+if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
+exit /b %EXIT_CODE%
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/iosApp/Configuration/Config.xcconfig b/iosApp/Configuration/Config.xcconfig
new file mode 100644
index 0000000..e9ee188
--- /dev/null
+++ b/iosApp/Configuration/Config.xcconfig
@@ -0,0 +1,7 @@
+TEAM_ID=
+
+PRODUCT_NAME=The Abstraction Engine
+PRODUCT_BUNDLE_IDENTIFIER=coffee.liz.abstractionengine.TheAbstractionEngine$(TEAM_ID)
+
+CURRENT_PROJECT_VERSION=1
+MARKETING_VERSION=1.0 \ No newline at end of file
diff --git a/iosApp/iosApp.xcodeproj/project.pbxproj b/iosApp/iosApp.xcodeproj/project.pbxproj
new file mode 100644
index 0000000..1b11e19
--- /dev/null
+++ b/iosApp/iosApp.xcodeproj/project.pbxproj
@@ -0,0 +1,373 @@
+// !$*UTF8*$!
+{
+ archiveVersion = 1;
+ classes = {
+ };
+ objectVersion = 77;
+ objects = {
+
+/* Begin PBXFileReference section */
+ F5FF5B23FF9DADAFFC41EC5B /* The Abstraction Engine.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "The Abstraction Engine.app"; sourceTree = BUILT_PRODUCTS_DIR; };
+/* End PBXFileReference section */
+
+/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
+ B68B0CBE2FD019FBB9ADC062 /* Exceptions for "iosApp" folder in "iosApp" target */ = {
+ isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
+ membershipExceptions = (
+ Info.plist,
+ );
+ target = 84468865F64DFD79AC9E2C2F /* iosApp */;
+ };
+/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
+
+/* Begin PBXFileSystemSynchronizedRootGroup section */
+ 61BC5DB5739A5B0768C43C39 /* iosApp */ = {
+ isa = PBXFileSystemSynchronizedRootGroup;
+ exceptions = (
+ B68B0CBE2FD019FBB9ADC062 /* Exceptions for "iosApp" folder in "iosApp" target */,
+ );
+ path = iosApp;
+ sourceTree = "<group>";
+ };
+ 831D5F81A482AA3D6A68517F /* Configuration */ = {
+ isa = PBXFileSystemSynchronizedRootGroup;
+ path = Configuration;
+ sourceTree = "<group>";
+ };
+/* End PBXFileSystemSynchronizedRootGroup section */
+
+/* Begin PBXFrameworksBuildPhase section */
+ AC6111C758B22DDF1138CFB9 /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+ F5D8D61979EE6CAF6A1A0602 = {
+ isa = PBXGroup;
+ children = (
+ 831D5F81A482AA3D6A68517F /* Configuration */,
+ 61BC5DB5739A5B0768C43C39 /* iosApp */,
+ 495300AED579013BD62BC6B8 /* Products */,
+ );
+ sourceTree = "<group>";
+ };
+ 495300AED579013BD62BC6B8 /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ F5FF5B23FF9DADAFFC41EC5B /* The Abstraction Engine.app */,
+ );
+ name = Products;
+ sourceTree = "<group>";
+ };
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+ 84468865F64DFD79AC9E2C2F /* iosApp */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = E2F0DFE8FC41CBCBBDA48BEE /* Build configuration list for PBXNativeTarget "iosApp" */;
+ buildPhases = (
+ 5A110A979046F24C7C9E7A26 /* Compile Kotlin Framework */,
+ 0AE70916E8C6CBBEDFAFD3AD /* Sources */,
+ AC6111C758B22DDF1138CFB9 /* Frameworks */,
+ B7ABC5FA67E48B4046A73E06 /* Resources */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ );
+ fileSystemSynchronizedGroups = (
+ 61BC5DB5739A5B0768C43C39 /* iosApp */,
+ );
+ name = iosApp;
+ packageProductDependencies = (
+ );
+ productName = iosApp;
+ productReference = F5FF5B23FF9DADAFFC41EC5B /* The Abstraction Engine.app */;
+ productType = "com.apple.product-type.application";
+ };
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+ 482A5064A76EC02255085D66 /* Project object */ = {
+ isa = PBXProject;
+ attributes = {
+ BuildIndependentTargetsInParallel = 1;
+ LastSwiftUpdateCheck = 1620;
+ LastUpgradeCheck = 1620;
+ TargetAttributes = {
+ 84468865F64DFD79AC9E2C2F = {
+ CreatedOnToolsVersion = 16.2;
+ };
+ };
+ };
+ buildConfigurationList = BC12BB3CA5431F80864A9FB6 /* Build configuration list for PBXProject "iosApp" */;
+ developmentRegion = en;
+ hasScannedForEncodings = 0;
+ knownRegions = (
+ en,
+ Base,
+ );
+ mainGroup = F5D8D61979EE6CAF6A1A0602;
+ minimizedProjectReferenceProxies = 1;
+ preferredProjectObjectVersion = 77;
+ productRefGroup = 495300AED579013BD62BC6B8 /* Products */;
+ projectDirPath = "";
+ projectRoot = "";
+ targets = (
+ 84468865F64DFD79AC9E2C2F /* iosApp */,
+ );
+ };
+/* End PBXProject section */
+
+/* Begin PBXResourcesBuildPhase section */
+ B7ABC5FA67E48B4046A73E06 /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXShellScriptBuildPhase section */
+ 5A110A979046F24C7C9E7A26 /* Compile Kotlin Framework */ = {
+ isa = PBXShellScriptBuildPhase;
+ alwaysOutOfDate = 1;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputFileListPaths = (
+ );
+ inputPaths = (
+ );
+ name = "Compile Kotlin Framework";
+ outputFileListPaths = (
+ );
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "if [ \"YES\" = \"$OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED\" ]; then\n echo \"Skipping Gradle build task invocation due to OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED environment variable set to \\\"YES\\\"\"\n exit 0\nfi\ncd \"$SRCROOT/..\"\n./gradlew :composeApp:embedAndSignAppleFrameworkForXcode\n";
+ };
+/* End PBXShellScriptBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+ 0AE70916E8C6CBBEDFAFD3AD /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXSourcesBuildPhase section */
+
+/* Begin XCBuildConfiguration section */
+ D94EDDF3060BE1195839DB3B /* Debug */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReferenceAnchor = 831D5F81A482AA3D6A68517F /* Configuration */;
+ baseConfigurationReferenceRelativePath = Config.xcconfig;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_ENABLE_OBJC_WEAK = 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_DOCUMENTATION_COMMENTS = YES;
+ 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_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ 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 = gnu17;
+ 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 = 18.2;
+ LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
+ MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
+ MTL_FAST_MATH = YES;
+ ONLY_ACTIVE_ARCH = YES;
+ SDKROOT = iphoneos;
+ SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
+ SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+ };
+ name = Debug;
+ };
+ 82793F83896C4AA8A17CB990 /* Release */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReferenceAnchor = 831D5F81A482AA3D6A68517F /* Configuration */;
+ baseConfigurationReferenceRelativePath = Config.xcconfig;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_ENABLE_OBJC_WEAK = 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_DOCUMENTATION_COMMENTS = YES;
+ 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_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ 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 = gnu17;
+ 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 = 18.2;
+ LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
+ MTL_ENABLE_DEBUG_INFO = NO;
+ MTL_FAST_MATH = YES;
+ SDKROOT = iphoneos;
+ SWIFT_COMPILATION_MODE = wholemodule;
+ VALIDATE_PRODUCT = YES;
+ };
+ name = Release;
+ };
+ 5C40F620F8F710C457E29100 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ARCHS = arm64;
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+ CODE_SIGN_IDENTITY = "Apple Development";
+ CODE_SIGN_STYLE = Automatic;
+ DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\"";
+ DEVELOPMENT_TEAM = "${TEAM_ID}";
+ ENABLE_PREVIEWS = YES;
+ GENERATE_INFOPLIST_FILE = YES;
+ INFOPLIST_FILE = iosApp/Info.plist;
+ INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
+ INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
+ INFOPLIST_KEY_UILaunchScreen_Generation = YES;
+ INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
+ INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
+ SWIFT_EMIT_LOC_STRINGS = YES;
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Debug;
+ };
+ 6F527A73C2B3BB7B74541389 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ARCHS = arm64;
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+ CODE_SIGN_IDENTITY = "Apple Development";
+ CODE_SIGN_STYLE = Automatic;
+ DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\"";
+ DEVELOPMENT_TEAM = "${TEAM_ID}";
+ ENABLE_PREVIEWS = YES;
+ GENERATE_INFOPLIST_FILE = YES;
+ INFOPLIST_FILE = iosApp/Info.plist;
+ INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
+ INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
+ INFOPLIST_KEY_UILaunchScreen_Generation = YES;
+ INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
+ INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
+ SWIFT_EMIT_LOC_STRINGS = YES;
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Release;
+ };
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+ BC12BB3CA5431F80864A9FB6 /* Build configuration list for PBXProject "iosApp" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ D94EDDF3060BE1195839DB3B /* Debug */,
+ 82793F83896C4AA8A17CB990 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ E2F0DFE8FC41CBCBBDA48BEE /* Build configuration list for PBXNativeTarget "iosApp" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 5C40F620F8F710C457E29100 /* Debug */,
+ 6F527A73C2B3BB7B74541389 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+/* End XCConfigurationList section */
+ };
+ rootObject = 482A5064A76EC02255085D66 /* Project object */;
+} \ No newline at end of file
diff --git a/iosApp/iosApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/iosApp/iosApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 0000000..919434a
--- /dev/null
+++ b/iosApp/iosApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Workspace
+ version = "1.0">
+ <FileRef
+ location = "self:">
+ </FileRef>
+</Workspace>
diff --git a/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-20@2x.png b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-20@2x.png
new file mode 100644
index 0000000..1ad3abc
--- /dev/null
+++ b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-20@2x.png
Binary files differ
diff --git a/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-20@2x~ipad.png b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-20@2x~ipad.png
new file mode 100644
index 0000000..1ad3abc
--- /dev/null
+++ b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-20@2x~ipad.png
Binary files differ
diff --git a/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-20@3x.png b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-20@3x.png
new file mode 100644
index 0000000..d8d4a9d
--- /dev/null
+++ b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-20@3x.png
Binary files differ
diff --git a/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-20~ipad.png b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-20~ipad.png
new file mode 100644
index 0000000..a678b1d
--- /dev/null
+++ b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-20~ipad.png
Binary files differ
diff --git a/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-29.png b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-29.png
new file mode 100644
index 0000000..0b7c640
--- /dev/null
+++ b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-29.png
Binary files differ
diff --git a/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-29@2x.png b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-29@2x.png
new file mode 100644
index 0000000..ba13984
--- /dev/null
+++ b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-29@2x.png
Binary files differ
diff --git a/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-29@2x~ipad.png b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-29@2x~ipad.png
new file mode 100644
index 0000000..ba13984
--- /dev/null
+++ b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-29@2x~ipad.png
Binary files differ
diff --git a/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-29@3x.png b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-29@3x.png
new file mode 100644
index 0000000..4b730e5
--- /dev/null
+++ b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-29@3x.png
Binary files differ
diff --git a/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-29~ipad.png b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-29~ipad.png
new file mode 100644
index 0000000..0b7c640
--- /dev/null
+++ b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-29~ipad.png
Binary files differ
diff --git a/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-40@2x.png b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-40@2x.png
new file mode 100644
index 0000000..b9d09fa
--- /dev/null
+++ b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-40@2x.png
Binary files differ
diff --git a/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-40@2x~ipad.png b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-40@2x~ipad.png
new file mode 100644
index 0000000..b9d09fa
--- /dev/null
+++ b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-40@2x~ipad.png
Binary files differ
diff --git a/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-40@3x.png b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-40@3x.png
new file mode 100644
index 0000000..e645dd9
--- /dev/null
+++ b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-40@3x.png
Binary files differ
diff --git a/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-40~ipad.png b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-40~ipad.png
new file mode 100644
index 0000000..1ad3abc
--- /dev/null
+++ b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-40~ipad.png
Binary files differ
diff --git a/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-60@2x~car.png b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-60@2x~car.png
new file mode 100644
index 0000000..e645dd9
--- /dev/null
+++ b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-60@2x~car.png
Binary files differ
diff --git a/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-60@3x~car.png b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-60@3x~car.png
new file mode 100644
index 0000000..2440719
--- /dev/null
+++ b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-60@3x~car.png
Binary files differ
diff --git a/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-83.5@2x~ipad.png b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-83.5@2x~ipad.png
new file mode 100644
index 0000000..3067194
--- /dev/null
+++ b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-83.5@2x~ipad.png
Binary files differ
diff --git a/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon@2x.png b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon@2x.png
new file mode 100644
index 0000000..e645dd9
--- /dev/null
+++ b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon@2x.png
Binary files differ
diff --git a/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon@2x~ipad.png b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon@2x~ipad.png
new file mode 100644
index 0000000..e7778b4
--- /dev/null
+++ b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon@2x~ipad.png
Binary files differ
diff --git a/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon@3x.png b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon@3x.png
new file mode 100644
index 0000000..2440719
--- /dev/null
+++ b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon@3x.png
Binary files differ
diff --git a/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon~ios-marketing.png b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon~ios-marketing.png
new file mode 100644
index 0000000..26c3552
--- /dev/null
+++ b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon~ios-marketing.png
Binary files differ
diff --git a/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon~ipad.png b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon~ipad.png
new file mode 100644
index 0000000..e0428a2
--- /dev/null
+++ b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon~ipad.png
Binary files differ
diff --git a/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 0000000..bd04914
--- /dev/null
+++ b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,134 @@
+{
+ "images": [
+ {
+ "filename": "AppIcon@2x.png",
+ "idiom": "iphone",
+ "scale": "2x",
+ "size": "60x60"
+ },
+ {
+ "filename": "AppIcon@3x.png",
+ "idiom": "iphone",
+ "scale": "3x",
+ "size": "60x60"
+ },
+ {
+ "filename": "AppIcon~ipad.png",
+ "idiom": "ipad",
+ "scale": "1x",
+ "size": "76x76"
+ },
+ {
+ "filename": "AppIcon@2x~ipad.png",
+ "idiom": "ipad",
+ "scale": "2x",
+ "size": "76x76"
+ },
+ {
+ "filename": "AppIcon-83.5@2x~ipad.png",
+ "idiom": "ipad",
+ "scale": "2x",
+ "size": "83.5x83.5"
+ },
+ {
+ "filename": "AppIcon-40@2x.png",
+ "idiom": "iphone",
+ "scale": "2x",
+ "size": "40x40"
+ },
+ {
+ "filename": "AppIcon-40@3x.png",
+ "idiom": "iphone",
+ "scale": "3x",
+ "size": "40x40"
+ },
+ {
+ "filename": "AppIcon-40~ipad.png",
+ "idiom": "ipad",
+ "scale": "1x",
+ "size": "40x40"
+ },
+ {
+ "filename": "AppIcon-40@2x~ipad.png",
+ "idiom": "ipad",
+ "scale": "2x",
+ "size": "40x40"
+ },
+ {
+ "filename": "AppIcon-20@2x.png",
+ "idiom": "iphone",
+ "scale": "2x",
+ "size": "20x20"
+ },
+ {
+ "filename": "AppIcon-20@3x.png",
+ "idiom": "iphone",
+ "scale": "3x",
+ "size": "20x20"
+ },
+ {
+ "filename": "AppIcon-20~ipad.png",
+ "idiom": "ipad",
+ "scale": "1x",
+ "size": "20x20"
+ },
+ {
+ "filename": "AppIcon-20@2x~ipad.png",
+ "idiom": "ipad",
+ "scale": "2x",
+ "size": "20x20"
+ },
+ {
+ "filename": "AppIcon-29.png",
+ "idiom": "iphone",
+ "scale": "1x",
+ "size": "29x29"
+ },
+ {
+ "filename": "AppIcon-29@2x.png",
+ "idiom": "iphone",
+ "scale": "2x",
+ "size": "29x29"
+ },
+ {
+ "filename": "AppIcon-29@3x.png",
+ "idiom": "iphone",
+ "scale": "3x",
+ "size": "29x29"
+ },
+ {
+ "filename": "AppIcon-29~ipad.png",
+ "idiom": "ipad",
+ "scale": "1x",
+ "size": "29x29"
+ },
+ {
+ "filename": "AppIcon-29@2x~ipad.png",
+ "idiom": "ipad",
+ "scale": "2x",
+ "size": "29x29"
+ },
+ {
+ "filename": "AppIcon-60@2x~car.png",
+ "idiom": "car",
+ "scale": "2x",
+ "size": "60x60"
+ },
+ {
+ "filename": "AppIcon-60@3x~car.png",
+ "idiom": "car",
+ "scale": "3x",
+ "size": "60x60"
+ },
+ {
+ "filename": "AppIcon~ios-marketing.png",
+ "idiom": "ios-marketing",
+ "scale": "1x",
+ "size": "1024x1024"
+ }
+ ],
+ "info": {
+ "author": "iconkitchen",
+ "version": 1
+ }
+} \ No newline at end of file
diff --git a/iosApp/iosApp/Assets.xcassets/Contents.json b/iosApp/iosApp/Assets.xcassets/Contents.json
new file mode 100644
index 0000000..73c0059
--- /dev/null
+++ b/iosApp/iosApp/Assets.xcassets/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/iosApp/iosApp/ContentView.swift b/iosApp/iosApp/ContentView.swift
new file mode 100644
index 0000000..c765ff2
--- /dev/null
+++ b/iosApp/iosApp/ContentView.swift
@@ -0,0 +1,21 @@
+import UIKit
+import SwiftUI
+import ComposeApp
+
+struct ComposeView: UIViewControllerRepresentable {
+ func makeUIViewController(context: Context) -> UIViewController {
+ MainViewControllerKt.MainViewController()
+ }
+
+ func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}
+}
+
+struct ContentView: View {
+ var body: some View {
+ ComposeView()
+ .ignoresSafeArea()
+ }
+}
+
+
+
diff --git a/iosApp/iosApp/Info.plist b/iosApp/iosApp/Info.plist
new file mode 100644
index 0000000..11845e1
--- /dev/null
+++ b/iosApp/iosApp/Info.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>CADisableMinimumFrameDurationOnPhone</key>
+ <true/>
+</dict>
+</plist>
diff --git a/iosApp/iosApp/Preview Content/Preview Assets.xcassets/Contents.json b/iosApp/iosApp/Preview Content/Preview Assets.xcassets/Contents.json
new file mode 100644
index 0000000..73c0059
--- /dev/null
+++ b/iosApp/iosApp/Preview Content/Preview Assets.xcassets/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/iosApp/iosApp/iOSApp.swift b/iosApp/iosApp/iOSApp.swift
new file mode 100644
index 0000000..d83dca6
--- /dev/null
+++ b/iosApp/iosApp/iOSApp.swift
@@ -0,0 +1,10 @@
+import SwiftUI
+
+@main
+struct iOSApp: App {
+ var body: some Scene {
+ WindowGroup {
+ ContentView()
+ }
+ }
+} \ No newline at end of file
diff --git a/settings.gradle.kts b/settings.gradle.kts
new file mode 100644
index 0000000..0c54e50
--- /dev/null
+++ b/settings.gradle.kts
@@ -0,0 +1,35 @@
+rootProject.name = "TheAbstractionEngine"
+enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")
+
+pluginManagement {
+ repositories {
+ google {
+ mavenContent {
+ includeGroupAndSubgroups("androidx")
+ includeGroupAndSubgroups("com.android")
+ includeGroupAndSubgroups("com.google")
+ }
+ }
+ mavenCentral()
+ gradlePluginPortal()
+ }
+}
+
+dependencyResolutionManagement {
+ repositories {
+ google {
+ mavenContent {
+ includeGroupAndSubgroups("androidx")
+ includeGroupAndSubgroups("com.android")
+ includeGroupAndSubgroups("com.google")
+ }
+ }
+ mavenCentral()
+ }
+}
+
+plugins {
+ id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0"
+}
+
+include(":composeApp") \ No newline at end of file