summaryrefslogtreecommitdiff
path: root/composeApp
diff options
context:
space:
mode:
Diffstat (limited to 'composeApp')
-rw-r--r--composeApp/build.gradle.kts2
-rw-r--r--composeApp/src/androidMain/kotlin/coffee/liz/abstractionengine/MainActivity.kt1
-rw-r--r--composeApp/src/androidMain/kotlin/coffee/liz/abstractionengine/Platform.android.kt4
-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.kt2
-rw-r--r--composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/app/App.kt24
-rw-r--r--composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/app/ui/Theme.kt (renamed from composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/ui/Theme.kt)2
-rw-r--r--composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/game/AbstractionEngine.kt5
-rw-r--r--composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/game/Game.kt37
-rw-r--r--composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/game/Renderer.kt (renamed from composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/game/RenderSystem.kt)11
-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/ecs/Component.kt4
-rw-r--r--composeApp/src/commonMain/kotlin/coffee/liz/ecs/DAGWorld.kt61
-rw-r--r--composeApp/src/commonMain/kotlin/coffee/liz/ecs/Entity.kt28
-rw-r--r--composeApp/src/commonMain/kotlin/coffee/liz/ecs/GameLoop.kt25
-rw-r--r--composeApp/src/commonMain/kotlin/coffee/liz/ecs/System.kt17
-rw-r--r--composeApp/src/commonMain/kotlin/coffee/liz/ecs/TickedSystem.kt17
-rw-r--r--composeApp/src/commonMain/kotlin/coffee/liz/ecs/Vec.kt33
-rw-r--r--composeApp/src/commonMain/kotlin/coffee/liz/ecs/World.kt32
-rw-r--r--composeApp/src/commonMain/kotlin/coffee/liz/ecs/animation/AnimationComponents.kt67
-rw-r--r--composeApp/src/commonMain/kotlin/coffee/liz/ecs/animation/AnimationSystem.kt41
-rw-r--r--composeApp/src/commonMain/kotlin/coffee/liz/ecs/input/Controlled.kt7
-rw-r--r--composeApp/src/commonMain/kotlin/coffee/liz/ecs/input/InputProvider.kt7
-rw-r--r--composeApp/src/commonMain/kotlin/coffee/liz/ecs/input/InputSystem.kt23
-rw-r--r--composeApp/src/commonMain/kotlin/coffee/liz/ecs/physics/CollisionTick.kt28
-rw-r--r--composeApp/src/commonMain/kotlin/coffee/liz/ecs/physics/Components.kt15
-rw-r--r--composeApp/src/commonMain/kotlin/coffee/liz/ecs/physics/IntegrationTick.kt27
-rw-r--r--composeApp/src/commonMain/kotlin/coffee/liz/ecs/physics/PhysicsSystem.kt26
-rw-r--r--composeApp/src/commonTest/kotlin/coffee/liz/ecs/DAGWorldTest.kt2
-rw-r--r--composeApp/src/commonTest/kotlin/coffee/liz/ecs/animation/AnimationSystemTest.kt20
-rw-r--r--composeApp/src/iosMain/kotlin/coffee/liz/abstractionengine/MainViewController.kt1
-rw-r--r--composeApp/src/iosMain/kotlin/coffee/liz/abstractionengine/Platform.ios.kt4
-rw-r--r--composeApp/src/jvmMain/kotlin/coffee/liz/abstractionengine/Platform.jvm.kt2
-rw-r--r--composeApp/src/jvmMain/kotlin/coffee/liz/abstractionengine/main.kt1
39 files changed, 357 insertions, 633 deletions
diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts
index 9e17fc5..2c6e273 100644
--- a/composeApp/build.gradle.kts
+++ b/composeApp/build.gradle.kts
@@ -11,7 +11,7 @@ plugins {
kotlin {
compilerOptions {
- freeCompilerArgs.add("-opt-in=kotlin.time.ExperimentalTime")
+ freeCompilerArgs.add("-opt-in=kotlin.time.ExperimentalTime -opt-in=kotlin.concurrent.atomics.ExperimentalAtomicApi")
}
androidTarget {
diff --git a/composeApp/src/androidMain/kotlin/coffee/liz/abstractionengine/MainActivity.kt b/composeApp/src/androidMain/kotlin/coffee/liz/abstractionengine/MainActivity.kt
index 49d0dfa..acb292e 100644
--- a/composeApp/src/androidMain/kotlin/coffee/liz/abstractionengine/MainActivity.kt
+++ b/composeApp/src/androidMain/kotlin/coffee/liz/abstractionengine/MainActivity.kt
@@ -6,6 +6,7 @@ import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
+import coffee.liz.abstractionengine.app.App
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
diff --git a/composeApp/src/androidMain/kotlin/coffee/liz/abstractionengine/Platform.android.kt b/composeApp/src/androidMain/kotlin/coffee/liz/abstractionengine/Platform.android.kt
index 3ab6a6a..c9dd6cb 100644
--- a/composeApp/src/androidMain/kotlin/coffee/liz/abstractionengine/Platform.android.kt
+++ b/composeApp/src/androidMain/kotlin/coffee/liz/abstractionengine/Platform.android.kt
@@ -1,9 +1,7 @@
package coffee.liz.abstractionengine
-import android.os.Build
-
class AndroidPlatform : Platform {
- override val name: String = "Android ${Build.VERSION.SDK_INT}"
+ override val needsTouchscreenControls = true
}
actual fun getPlatform(): Platform = AndroidPlatform() \ 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
deleted file mode 100644
index d557fd8..0000000
--- a/composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/AbstractionEngine.kt
+++ /dev/null
@@ -1,5 +0,0 @@
-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
deleted file mode 100644
index a69e711..0000000
--- a/composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/App.kt
+++ /dev/null
@@ -1,28 +0,0 @@
-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
deleted file mode 100644
index 6f23320..0000000
--- a/composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/Greeting.kt
+++ /dev/null
@@ -1,9 +0,0 @@
-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
index 91faf62..17ca317 100644
--- a/composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/Platform.kt
+++ b/composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/Platform.kt
@@ -1,7 +1,7 @@
package coffee.liz.abstractionengine
interface Platform {
- val name: String
+ val needsTouchscreenControls: Boolean
}
expect fun getPlatform(): Platform \ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/app/App.kt b/composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/app/App.kt
new file mode 100644
index 0000000..50e72f0
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/app/App.kt
@@ -0,0 +1,24 @@
+package coffee.liz.abstractionengine.app
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.*
+import androidx.compose.ui.Modifier
+import coffee.liz.abstractionengine.app.ui.GameBoyTheme
+import coffee.liz.abstractionengine.getPlatform
+import org.jetbrains.compose.ui.tooling.preview.Preview
+
+@Composable
+@Preview
+fun App() {
+
+ MaterialTheme(colorScheme = GameBoyTheme) {
+ Box(modifier = Modifier.fillMaxSize()) {
+ if (getPlatform().needsTouchscreenControls) {
+ Text("NEEDS TOUCHSCREEN CONTROLS")
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/ui/Theme.kt b/composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/app/ui/Theme.kt
index 661cb09..be6c3ad 100644
--- a/composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/ui/Theme.kt
+++ b/composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/app/ui/Theme.kt
@@ -1,4 +1,4 @@
-package coffee.liz.abstractionengine.ui
+package coffee.liz.abstractionengine.app.ui
import androidx.compose.material3.darkColorScheme
import androidx.compose.ui.graphics.Color
diff --git a/composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/game/AbstractionEngine.kt b/composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/game/AbstractionEngine.kt
new file mode 100644
index 0000000..18349c3
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/game/AbstractionEngine.kt
@@ -0,0 +1,5 @@
+package coffee.liz.abstractionengine.game
+
+class AbstractionEngine {
+
+} \ 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
index 9490f83..0c3a007 100644
--- a/composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/game/Game.kt
+++ b/composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/game/Game.kt
@@ -6,64 +6,35 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import coffee.liz.ecs.World
import kotlinx.coroutines.isActive
+import kotlin.time.Duration.Companion.nanoseconds
/**
- * 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
+ * Manages and paints the [World].
*/
@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
+ var lastUpdate: Long? = null
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,
+ renderSystem: Renderer,
backgroundColor: Color = Color.Transparent,
modifier: Modifier = Modifier
) {
diff --git a/composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/game/RenderSystem.kt b/composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/game/Renderer.kt
index 1d22ae6..57511f3 100644
--- a/composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/game/RenderSystem.kt
+++ b/composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/game/Renderer.kt
@@ -1,7 +1,5 @@
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
@@ -12,6 +10,7 @@ import coffee.liz.ecs.animation.Animator
import coffee.liz.ecs.animation.Position
import coffee.liz.ecs.animation.SpriteSheet
import kotlin.reflect.KClass
+import kotlin.time.Duration
/**
* System that renders sprites to a DrawScope.
@@ -21,17 +20,13 @@ import kotlin.reflect.KClass
*
* Depends on AnimationSystem to ensure animations are updated before rendering.
*/
-class RenderSystem(
+class Renderer(
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) {
+ override fun update(world: World, duration: Duration) {
// Rendering happens in render() method, not here
}
diff --git a/composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/ui/ArcadeButton.kt b/composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/ui/ArcadeButton.kt
deleted file mode 100644
index 9d50192..0000000
--- a/composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/ui/ArcadeButton.kt
+++ /dev/null
@@ -1,52 +0,0 @@
-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
deleted file mode 100644
index f5b4839..0000000
--- a/composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/ui/ArcadeControls.kt
+++ /dev/null
@@ -1,130 +0,0 @@
-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
deleted file mode 100644
index 52f5866..0000000
--- a/composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/ui/DPad.kt
+++ /dev/null
@@ -1,105 +0,0 @@
-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
deleted file mode 100644
index be35a1a..0000000
--- a/composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/ui/PCBBackground.kt
+++ /dev/null
@@ -1,85 +0,0 @@
-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/ecs/Component.kt b/composeApp/src/commonMain/kotlin/coffee/liz/ecs/Component.kt
index c64a863..cfb01b1 100644
--- a/composeApp/src/commonMain/kotlin/coffee/liz/ecs/Component.kt
+++ b/composeApp/src/commonMain/kotlin/coffee/liz/ecs/Component.kt
@@ -1,7 +1,3 @@
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
index d314065..77cb30b 100644
--- a/composeApp/src/commonMain/kotlin/coffee/liz/ecs/DAGWorld.kt
+++ b/composeApp/src/commonMain/kotlin/coffee/liz/ecs/DAGWorld.kt
@@ -1,28 +1,27 @@
package coffee.liz.ecs
+import kotlin.concurrent.atomics.AtomicInt
+import kotlin.concurrent.atomics.ExperimentalAtomicApi
+import kotlin.concurrent.atomics.incrementAndFetch
import kotlin.reflect.KClass
+import kotlin.time.Duration
/**
- * World implementation that executes systems in dependency order using a DAG.
+ * [World] that updates in [System.dependencies] DAG order.
*/
-class DAGWorld(systems: List<System>) : World {
+@OptIn(ExperimentalAtomicApi::class)
+class DAGWorld<Outside>(systems: List<System<Outside>>) : World<Outside> {
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)
- }
+ private val systemExecutionOrder: List<System<Outside>> = buildExecutionOrder(systems)
+ private val nextEntityId = AtomicInt(0)
override fun createEntity(): Entity {
- val entity = Entity(nextEntityId++)
- entities.add(entity)
- return entity
+ return Entity(nextEntityId.incrementAndFetch())
+ .also { entities.add(it) }
}
- override fun destroyEntity(entity: Entity) {
- // Remove from all component caches
+ override fun removeEntity(entity: Entity) {
entity.componentTypes().forEach { componentType ->
componentCache[componentType]?.remove(entity)
}
@@ -38,25 +37,18 @@ class DAGWorld(systems: List<System>) : World {
// Filter to entities that have all component types
return candidates.filter { entity ->
- entity.hasAll(*componentTypes)
+ componentTypes.all { entity.has(it) }
}.toSet()
}
- override fun update(deltaTime: Float) {
- // Update component cache based on current entity state
- updateComponentCache()
-
- // Execute systems in dependency order
+ override fun update(state: Outside, deltaTime: Duration) {
+ refreshComponentCache()
systemExecutionOrder.forEach { system ->
- system.update(this, deltaTime)
+ system.update(this, state, deltaTime)
}
}
- /**
- * Rebuild the component cache based on current entity components.
- * Call this after components have been added/removed from entities.
- */
- private fun updateComponentCache() {
+ private fun refreshComponentCache() {
componentCache.clear()
entities.forEach { entity ->
entity.componentTypes().forEach { componentType ->
@@ -67,14 +59,13 @@ class DAGWorld(systems: List<System>) : World {
/**
* Build a topologically sorted execution order from system dependencies.
- * Uses Kahn's algorithm for topological sorting.
*/
- private fun buildExecutionOrder(systems: List<System>): List<System> {
+ private fun buildExecutionOrder(systems: List<System<Outside>>): List<System<Outside>> {
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>>>()
+ val inDegree = mutableMapOf<KClass<out System<Outside>>, Int>()
+ val adjacencyList = mutableMapOf<KClass<out System<Outside>>, MutableSet<KClass<out System<Outside>>>>()
// Initialize graph
systems.forEach { system ->
@@ -94,15 +85,11 @@ class DAGWorld(systems: List<System>) : World {
}
// Kahn's algorithm
- val queue = ArrayDeque<KClass<out System>>()
- val result = mutableListOf<System>()
+ val queue = ArrayDeque<KClass<out System<Outside>>>()
+ val result = mutableListOf<System<Outside>>()
// Add all systems with no dependencies to queue
- inDegree.forEach { (systemClass, degree) ->
- if (degree == 0) {
- queue.add(systemClass)
- }
- }
+ queue.addAll(inDegree.filterValues { it == 0 }.keys)
while (queue.isNotEmpty()) {
val currentClass = queue.removeFirst()
@@ -119,7 +106,7 @@ class DAGWorld(systems: List<System>) : World {
// Check for cycles
if (result.size != systems.size) {
- throw IllegalArgumentException("Circular dependency detected in systems")
+ error("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
index 2ceac47..fcc39fb 100644
--- a/composeApp/src/commonMain/kotlin/coffee/liz/ecs/Entity.kt
+++ b/composeApp/src/commonMain/kotlin/coffee/liz/ecs/Entity.kt
@@ -9,7 +9,7 @@ class Entity(val id: Int) {
private val components = mutableMapOf<KClass<out Component>, Component>()
/**
- * Add a component to this entity.
+ * Add [Component].
*/
fun <T : Component> add(component: T): Entity {
components[component::class] = component
@@ -17,7 +17,7 @@ class Entity(val id: Int) {
}
/**
- * Remove a component from this entity.
+ * Remove [Component].
*/
fun <T : Component> remove(type: KClass<T>): Entity {
components.remove(type)
@@ -25,29 +25,22 @@ class Entity(val id: Int) {
}
/**
- * Get a component by type.
+ * Get [Component]
*/
@Suppress("UNCHECKED_CAST")
- fun <T : Component> get(type: KClass<T>): T? {
- return components[type] as? T
+ fun <T : Component> get(type: KClass<T>): T {
+ return components[type] as T
}
/**
- * Check if entity has a component.
+ * Has [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.
+ * [Component]s of this entity.
*/
fun componentTypes(): Set<KClass<out Component>> {
return components.keys.toSet()
@@ -58,13 +51,6 @@ class Entity(val id: Int) {
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/GameLoop.kt b/composeApp/src/commonMain/kotlin/coffee/liz/ecs/GameLoop.kt
new file mode 100644
index 0000000..713586d
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/coffee/liz/ecs/GameLoop.kt
@@ -0,0 +1,25 @@
+package coffee.liz.ecs
+
+import kotlin.time.Duration.Companion.nanoseconds
+
+abstract class GameLoop<Outside> {
+ private var world: World<Outside>? = null
+ private var lastUpdate: Long? = null
+
+ fun load(world: World<Outside>) {
+ this.world = world
+ this.lastUpdate = null
+ }
+
+ abstract fun render(world: World<Outside>)
+ abstract fun outsideState(): Outside
+
+ fun loop(now: Long) {
+ val world = this.world ?: error("World not loaded")
+ val outsideState = outsideState()
+ val dt = (now - (lastUpdate ?: now)).nanoseconds
+ world.update(outsideState, dt)
+ render(world)
+ lastUpdate = now
+ }
+} \ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/coffee/liz/ecs/System.kt b/composeApp/src/commonMain/kotlin/coffee/liz/ecs/System.kt
index 0f8ef2a..e763c9c 100644
--- a/composeApp/src/commonMain/kotlin/coffee/liz/ecs/System.kt
+++ b/composeApp/src/commonMain/kotlin/coffee/liz/ecs/System.kt
@@ -1,22 +1,23 @@
package coffee.liz.ecs
import kotlin.reflect.KClass
+import kotlin.time.Duration
/**
- * Systems contain the logic that operates on entities with specific components.
+ * System is a component that updates the world state.
+ * @param Outside is the state of the stuff outside the world (input, etc.).
*/
-interface System {
+interface System<Outside> {
/**
* Systems that must run before this system in the update loop.
- * Used to construct a dependency DAG.
*/
- val dependencies: Set<KClass<out System>>
+ 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
+ * Integrate this system over [deltaTime].
+ * @param world [World]
+ * @param deltaTime [Duration] since last update
*/
- fun update(world: World, deltaTime: Float)
+ fun update(world: World<Outside>, state: Outside, deltaTime: Duration)
}
diff --git a/composeApp/src/commonMain/kotlin/coffee/liz/ecs/TickedSystem.kt b/composeApp/src/commonMain/kotlin/coffee/liz/ecs/TickedSystem.kt
new file mode 100644
index 0000000..f512ca7
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/coffee/liz/ecs/TickedSystem.kt
@@ -0,0 +1,17 @@
+package coffee.liz.ecs
+
+import kotlin.time.Duration
+
+abstract class TickedSystem<Outside>(
+ protected val tickRate: Duration,
+ private var lastTick: Duration = Duration.ZERO
+): System<Outside> {
+ abstract fun update(world: World<Outside>, state: Outside, ticks: Int)
+
+ override fun update(world: World<Outside>, state: Outside, deltaTime: Duration) {
+ val ticks = ((deltaTime - lastTick) / tickRate).toInt()
+ lastTick = if (ticks == 0) (lastTick + deltaTime).also {
+ update(world, state, ticks)
+ } else Duration.ZERO
+ }
+} \ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/coffee/liz/ecs/Vec.kt b/composeApp/src/commonMain/kotlin/coffee/liz/ecs/Vec.kt
new file mode 100644
index 0000000..0b771e4
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/coffee/liz/ecs/Vec.kt
@@ -0,0 +1,33 @@
+package coffee.liz.ecs
+
+/**
+ * Rectangle in cartesian space.
+ */
+data class Rect(
+ val topLeft: Vec2,
+ val dimensions: Vec2
+) {
+ fun overlaps(other: Rect): Boolean {
+ val xOverlap = topLeft.x <= other.topLeft.x + other.dimensions.x &&
+ topLeft.x + dimensions.x >= other.topLeft.x
+ val yOverlap = topLeft.y <= other.topLeft.y + other.dimensions.y &&
+ topLeft.y + dimensions.y >= other.topLeft.y
+ return xOverlap && yOverlap
+ }
+}
+
+/**
+ * Cartesian point.
+ */
+data class Vec2(
+ val x: Float,
+ val y: Float
+) {
+ fun plus(other: Vec2): Vec2 = Vec2(x + other.x, y + other.y)
+ fun minus(other: Vec2): Vec2 = Vec2(x - other.x, y - other.y)
+ fun times(scalar: Float): Vec2 = Vec2(x * scalar, y * scalar)
+ fun div(scalar: Float): Vec2 = Vec2(x / scalar, y / scalar)
+ fun distanceTo(other: Vec2): Float = (this.minus(other)).length()
+ fun normalize(): Vec2 = this.div(length())
+ fun length(): Float = kotlin.math.sqrt(x * x + y * y)
+} \ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/coffee/liz/ecs/World.kt b/composeApp/src/commonMain/kotlin/coffee/liz/ecs/World.kt
index fd3b6df..452517f 100644
--- a/composeApp/src/commonMain/kotlin/coffee/liz/ecs/World.kt
+++ b/composeApp/src/commonMain/kotlin/coffee/liz/ecs/World.kt
@@ -1,42 +1,30 @@
package coffee.liz.ecs
-import kotlin.jvm.JvmName
import kotlin.reflect.KClass
+import kotlin.time.Duration
/**
- * The World manages entities and systems.
+ * [World] is the state of the world.
+ * @param Outside is the state of the stuff outside the world (input, etc.).
*/
-interface World {
+interface World<Outside> {
/**
- * Create a new entity with a unique ID.
+ * Create unique [Entity] in the [World].
*/
fun createEntity(): Entity
/**
- * Destroy an entity and remove it from all caches.
+ * Remove [entity] from [World].
*/
- fun destroyEntity(entity: Entity)
+ fun removeEntity(entity: Entity)
/**
- * Get all entities that have all the specified component types.
+ * Get entities with a [Component] type.
*/
fun query(vararg componentTypes: KClass<out Component>): Set<Entity>
/**
- * Update all systems in dependency order.
- * @param deltaTime Time elapsed since last update in seconds
+ * Integrate the [World].
*/
- fun update(deltaTime: Float)
+ fun update(state: Outside, deltaTime: Duration)
}
-
-// 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
index 4289eec..cb3708f 100644
--- a/composeApp/src/commonMain/kotlin/coffee/liz/ecs/animation/AnimationComponents.kt
+++ b/composeApp/src/commonMain/kotlin/coffee/liz/ecs/animation/AnimationComponents.kt
@@ -1,85 +1,77 @@
package coffee.liz.ecs.animation
import coffee.liz.ecs.Component
+import coffee.liz.ecs.Rect
import kotlin.jvm.JvmInline
/**
* Loop modes for animation playback.
*/
enum class LoopMode {
- /** Play once and stop on last frame */
+ /** Play once **/
ONCE,
- /** Repeat from beginning */
+ /** Repeat **/
LOOP,
- /** Play forward, then backward, repeat */
+ /** Play forward, then backward, repeat **/
PING_PONG
}
/**
- * Type-safe wrapper for animation names.
+ * Name of an animation clip.
*/
@JvmInline
value class AnimationName(val value: String)
/**
- * Type-safe wrapper for frame names.
+ * Name of a frame.
*/
@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.
+ * Animation Clip details.
*/
data class AnimationClip(
val frameNames: List<FrameName>,
- val frameDuration: Float, // seconds per frame
+ val frameTicks: Int,
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
+enum class AnimationDirection(val step: Int) {
+ /** Play in forward direction **/
+ FORWARD(step = 1),
+ /** Play in reverse direction **/
+ BACKWARD(step = -1);
+}
/**
- * Component for animation playback state.
- * Contains animation clips and current playback state.
+ * Current state of an animation.
*/
data class Animator(
val clips: Map<AnimationName, AnimationClip>,
var currentClip: AnimationName,
var frameIndex: Int = 0,
- var elapsed: Float = 0f,
+ var elapsedTicks: Int = 0,
var playing: Boolean = true,
- var direction: Int = 1 // 1 for forward, -1 for backward (used in PING_PONG)
+ var direction: AnimationDirection = AnimationDirection.FORWARD
) : 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
- }
+ clipName.takeIf { currentClip != it || restart }
+ .run {
+ currentClip = clipName
+ reset()
+ }
}
/**
@@ -103,12 +95,11 @@ data class Animator(
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
+ private fun reset() {
+ frameIndex = 0
+ elapsedTicks = 0
+ direction = AnimationDirection.FORWARD
+ playing = true
+ }
+}
diff --git a/composeApp/src/commonMain/kotlin/coffee/liz/ecs/animation/AnimationSystem.kt b/composeApp/src/commonMain/kotlin/coffee/liz/ecs/animation/AnimationSystem.kt
index 7ae405e..7968250 100644
--- a/composeApp/src/commonMain/kotlin/coffee/liz/ecs/animation/AnimationSystem.kt
+++ b/composeApp/src/commonMain/kotlin/coffee/liz/ecs/animation/AnimationSystem.kt
@@ -1,35 +1,27 @@
package coffee.liz.ecs.animation
-import coffee.liz.ecs.System
+import coffee.liz.ecs.TickedSystem
import coffee.liz.ecs.World
+import kotlin.time.Duration
/**
- * 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)
+ * Updates animation playback state for all [Animator] components in [World].
*/
-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
+class AnimationSystem(animationTickRate: Duration) : TickedSystem(animationTickRate) {
+ override fun update(world: World, ticks: Int) {
+ world.query(Animator::class).forEach { entity ->
+ val animator = entity.get(Animator::class)
if (!animator.playing) return@forEach
// Get the current animation clip
val clip = animator.clips[animator.currentClip] ?: return@forEach
// Accumulate elapsed time
- animator.elapsed += deltaTime
+ animator.elapsedTicks += ticks
// Advance frames while we have enough elapsed time
- while (animator.elapsed >= clip.frameDuration) {
- animator.elapsed -= clip.frameDuration
+ while (animator.elapsedTicks >= clip.frameTicks) {
+ animator.elapsedTicks -= clip.frameTicks
advanceFrame(animator, clip)
}
}
@@ -39,11 +31,10 @@ class AnimationSystem : System {
* Advances the animation to the next frame based on loop mode.
*/
private fun advanceFrame(animator: Animator, clip: AnimationClip) {
- animator.frameIndex += animator.direction
+ animator.frameIndex += animator.direction.step
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
@@ -51,25 +42,21 @@ class AnimationSystem : System {
}
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
+ // Go to (endFrame - 1) or (beginFrame + 1) ping ponging
if (animator.frameIndex >= clip.frameNames.size) {
- // Hit the end, reverse direction
animator.frameIndex = (clip.frameNames.size - 2).coerceAtLeast(0)
- animator.direction = -1
+ animator.direction = AnimationDirection.BACKWARD
} else if (animator.frameIndex < 0) {
- // Hit the beginning, reverse direction
animator.frameIndex = 1.coerceAtMost(clip.frameNames.size - 1)
- animator.direction = 1
+ animator.direction = AnimationDirection.FORWARD
}
}
}
diff --git a/composeApp/src/commonMain/kotlin/coffee/liz/ecs/input/Controlled.kt b/composeApp/src/commonMain/kotlin/coffee/liz/ecs/input/Controlled.kt
new file mode 100644
index 0000000..8c54bfe
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/coffee/liz/ecs/input/Controlled.kt
@@ -0,0 +1,7 @@
+package coffee.liz.ecs.input
+
+import coffee.liz.ecs.Component
+
+interface Controlled: Component {
+ fun handleInput(inputState: InputState)
+} \ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/coffee/liz/ecs/input/InputProvider.kt b/composeApp/src/commonMain/kotlin/coffee/liz/ecs/input/InputProvider.kt
new file mode 100644
index 0000000..5735665
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/coffee/liz/ecs/input/InputProvider.kt
@@ -0,0 +1,7 @@
+package coffee.liz.ecs.input
+
+data class InputState(val activeInputs: Set<String>)
+
+interface InputProvider {
+ val inputState: InputState
+} \ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/coffee/liz/ecs/input/InputSystem.kt b/composeApp/src/commonMain/kotlin/coffee/liz/ecs/input/InputSystem.kt
new file mode 100644
index 0000000..6dc8256
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/coffee/liz/ecs/input/InputSystem.kt
@@ -0,0 +1,23 @@
+package coffee.liz.ecs.input
+
+import coffee.liz.ecs.System
+import coffee.liz.ecs.TickedSystem
+import coffee.liz.ecs.World
+import kotlin.time.Duration
+
+class InputSystem<Outside : InputProvider>(
+ pollRate: Duration
+) : TickedSystem<Outside>(pollRate) {
+ private val activeInputs = mutableMapOf<String, Int>()
+
+ override fun update(world: World<Outside>, state: Outside, ticks: Int) {
+ state.inputState.activeInputs.forEach {
+ activeInputs[it] = (activeInputs[it] ?: 0) + ticks
+ }
+ activeInputs.keys.retainAll(state.inputState.activeInputs)
+
+ world.query(Controlled::class).forEach {
+ it.get(Controlled::class).handleInput(activeInputs.toMap())
+ }
+ }
+} \ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/coffee/liz/ecs/physics/CollisionTick.kt b/composeApp/src/commonMain/kotlin/coffee/liz/ecs/physics/CollisionTick.kt
new file mode 100644
index 0000000..bc34ac1
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/coffee/liz/ecs/physics/CollisionTick.kt
@@ -0,0 +1,28 @@
+package coffee.liz.ecs.physics
+
+import coffee.liz.ecs.Rect
+import coffee.liz.ecs.World
+
+internal class CollisionTick<Outside>(
+ private val collisionResolver: CollisionResolver<Outside>
+) {
+ fun runTick(world: World<Outside>) {
+ // Eh, fast enough for now. Don't need to do any fancy collision detection. There's always later to improve if
+ // it's that bad.
+ world.query(Collidable::class, Position::class).forEach { a ->
+ world.query(Collidable::class, Position::class).forEach { b ->
+ val aHitBoxes = a.get(Position::class).let { pos -> a.get(Collidable::class).hitboxes.map {
+ Rect(pos.vec2, it.dimensions)
+ }}
+ val bHitBoxes = b.get(Position::class).let { pos -> b.get(Collidable::class).hitboxes.map {
+ Rect(pos.vec2, it.dimensions)
+ }}
+
+ val collisionDetected = aHitBoxes.any { a -> bHitBoxes.any { b -> a.overlaps(b) } }
+ if (collisionDetected) {
+ collisionResolver.resolveCollision(world, a, b)
+ }
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/coffee/liz/ecs/physics/Components.kt b/composeApp/src/commonMain/kotlin/coffee/liz/ecs/physics/Components.kt
new file mode 100644
index 0000000..ffae10b
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/coffee/liz/ecs/physics/Components.kt
@@ -0,0 +1,15 @@
+package coffee.liz.ecs.physics
+
+import coffee.liz.ecs.Component
+import coffee.liz.ecs.Rect
+import coffee.liz.ecs.Vec2
+
+data class Position(val vec2: Vec2): Component
+data class Velocity(val vec2: Vec2) : Component
+data class Acceleration(val vec2: Vec2) : Component
+
+/**
+ * @param hitboxes a collection of hitboxes to check collisions against relative to [Rect.topLeft] as the top left of
+ * [Position].
+ */
+data class Collidable(val hitboxes: Collection<Rect>): Component
diff --git a/composeApp/src/commonMain/kotlin/coffee/liz/ecs/physics/IntegrationTick.kt b/composeApp/src/commonMain/kotlin/coffee/liz/ecs/physics/IntegrationTick.kt
new file mode 100644
index 0000000..03af835
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/coffee/liz/ecs/physics/IntegrationTick.kt
@@ -0,0 +1,27 @@
+package coffee.liz.ecs.physics
+
+import coffee.liz.ecs.World
+
+internal class IntegrationTick<Outside> {
+ fun runTick(world: World<Outside>) {
+ world.query(Velocity::class, Acceleration::class).forEach { entity ->
+ val velocity = entity.get(Velocity::class)
+ val acceleration = entity.get(Acceleration::class)
+ entity.add(
+ Velocity(
+ vec2 = velocity.vec2.plus(acceleration.vec2)
+ )
+ )
+ }
+
+ world.query(Position::class, Velocity::class).forEach { entity ->
+ val position = entity.get(Position::class)
+ val velocity = entity.get(Acceleration::class)
+ entity.add(
+ Position(
+ vec2 = position.vec2.plus(velocity.vec2)
+ )
+ )
+ }
+ }
+} \ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/coffee/liz/ecs/physics/PhysicsSystem.kt b/composeApp/src/commonMain/kotlin/coffee/liz/ecs/physics/PhysicsSystem.kt
new file mode 100644
index 0000000..703e614
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/coffee/liz/ecs/physics/PhysicsSystem.kt
@@ -0,0 +1,26 @@
+package coffee.liz.ecs.physics
+
+import coffee.liz.ecs.Entity
+import coffee.liz.ecs.TickedSystem
+import coffee.liz.ecs.World
+import kotlin.time.Duration
+
+internal fun interface CollisionResolver<Outside> {
+ fun resolveCollision(world: World<Outside>, a: Entity, b: Entity)
+}
+
+abstract class PhysicsSystem<Outside>(
+ physicsTickRate: Duration
+): TickedSystem<Outside>(physicsTickRate), CollisionResolver<Outside> {
+ private val integrationTick = IntegrationTick<Outside>()
+ private val collisionTick = CollisionTick(this)
+
+ abstract override fun resolveCollision(world: World<Outside>, a: Entity, b: Entity)
+
+ override fun update(world: World<Outside>, state: Outside, ticks: Int) {
+ for (tick in 1..ticks) {
+ collisionTick.runTick(world)
+ integrationTick.runTick(world)
+ }
+ }
+} \ 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
index 737e386..bdf7e99 100644
--- a/composeApp/src/commonTest/kotlin/coffee/liz/ecs/DAGWorldTest.kt
+++ b/composeApp/src/commonTest/kotlin/coffee/liz/ecs/DAGWorldTest.kt
@@ -34,7 +34,7 @@ class DAGWorldTest {
val entity = world.createEntity()
entity.add(Position(10f, 20f))
- world.destroyEntity(entity)
+ world.removeEntity(entity)
val results = world.query<Position>()
assertFalse(results.contains(entity))
diff --git a/composeApp/src/commonTest/kotlin/coffee/liz/ecs/animation/AnimationSystemTest.kt b/composeApp/src/commonTest/kotlin/coffee/liz/ecs/animation/AnimationSystemTest.kt
index bb7298e..588e245 100644
--- a/composeApp/src/commonTest/kotlin/coffee/liz/ecs/animation/AnimationSystemTest.kt
+++ b/composeApp/src/commonTest/kotlin/coffee/liz/ecs/animation/AnimationSystemTest.kt
@@ -23,7 +23,7 @@ class AnimationSystemTest {
clips = mapOf(
AnimationName("idle") to AnimationClip(
frameNames = listOf(FrameName("idle_0"), FrameName("idle_1")),
- frameDuration = 0.1f,
+ frameTicks = 0.1f,
loopMode = LoopMode.LOOP
),
AnimationName("walk") to AnimationClip(
@@ -32,7 +32,7 @@ class AnimationSystemTest {
FrameName("walk_1"),
FrameName("walk_2")
),
- frameDuration = 0.15f,
+ frameTicks = 0.15f,
loopMode = LoopMode.LOOP
)
),
@@ -49,17 +49,17 @@ class AnimationSystemTest {
entity.add(animator)
assertEquals(0, animator.frameIndex)
- assertEquals(0f, animator.elapsed)
+ assertEquals(0f, animator.elapsedTicks)
// Update with less than frame duration - should not advance
world.update(0.05f)
assertEquals(0, animator.frameIndex)
- assertEquals(0.05f, animator.elapsed)
+ assertEquals(0.05f, animator.elapsedTicks)
// Update to exceed frame duration - should advance to frame 1
world.update(0.06f)
assertEquals(1, animator.frameIndex)
- assertTrue(animator.elapsed < 0.1f)
+ assertTrue(animator.elapsedTicks < 0.1f)
}
@Test
@@ -87,7 +87,7 @@ class AnimationSystemTest {
clips = mapOf(
AnimationName("once") to AnimationClip(
frameNames = listOf(FrameName("idle_0"), FrameName("idle_1")),
- frameDuration = 0.1f,
+ frameTicks = 0.1f,
loopMode = LoopMode.ONCE
)
),
@@ -122,7 +122,7 @@ class AnimationSystemTest {
FrameName("walk_1"),
FrameName("walk_2")
),
- frameDuration = 0.1f,
+ frameTicks = 0.1f,
loopMode = LoopMode.PING_PONG
)
),
@@ -164,13 +164,13 @@ class AnimationSystemTest {
// Manually advance
animator.frameIndex = 1
- animator.elapsed = 0.05f
+ animator.elapsedTicks = 0.05f
// Switch to walk animation
animator.play(AnimationName("walk"))
assertEquals(AnimationName("walk"), animator.currentClip)
assertEquals(0, animator.frameIndex)
- assertEquals(0f, animator.elapsed)
+ assertEquals(0f, animator.elapsedTicks)
assertEquals(1, animator.direction)
assertTrue(animator.playing)
}
@@ -222,7 +222,7 @@ class AnimationSystemTest {
// 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)
+ assertTrue(animator.elapsedTicks >= 0.04f && animator.elapsedTicks <= 0.06f)
}
@Test
diff --git a/composeApp/src/iosMain/kotlin/coffee/liz/abstractionengine/MainViewController.kt b/composeApp/src/iosMain/kotlin/coffee/liz/abstractionengine/MainViewController.kt
index 4f98625..84b004a 100644
--- a/composeApp/src/iosMain/kotlin/coffee/liz/abstractionengine/MainViewController.kt
+++ b/composeApp/src/iosMain/kotlin/coffee/liz/abstractionengine/MainViewController.kt
@@ -1,5 +1,6 @@
package coffee.liz.abstractionengine
import androidx.compose.ui.window.ComposeUIViewController
+import coffee.liz.abstractionengine.app.App
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
index a6362e3..d52c60f 100644
--- a/composeApp/src/iosMain/kotlin/coffee/liz/abstractionengine/Platform.ios.kt
+++ b/composeApp/src/iosMain/kotlin/coffee/liz/abstractionengine/Platform.ios.kt
@@ -1,9 +1,7 @@
package coffee.liz.abstractionengine
-import platform.UIKit.UIDevice
-
class IOSPlatform: Platform {
- override val name: String = UIDevice.currentDevice.systemName() + " " + UIDevice.currentDevice.systemVersion
+ override val needsTouchscreenControls = true
}
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
index e1487e0..69c011f 100644
--- a/composeApp/src/jvmMain/kotlin/coffee/liz/abstractionengine/Platform.jvm.kt
+++ b/composeApp/src/jvmMain/kotlin/coffee/liz/abstractionengine/Platform.jvm.kt
@@ -1,7 +1,7 @@
package coffee.liz.abstractionengine
class JVMPlatform: Platform {
- override val name: String = "Java ${System.getProperty("java.version")}"
+ override val needsTouchscreenControls = false
}
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
index a426fb4..d786d9b 100644
--- a/composeApp/src/jvmMain/kotlin/coffee/liz/abstractionengine/main.kt
+++ b/composeApp/src/jvmMain/kotlin/coffee/liz/abstractionengine/main.kt
@@ -2,6 +2,7 @@ package coffee.liz.abstractionengine
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application
+import coffee.liz.abstractionengine.app.App
fun main() = application {
Window(