diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b9af08..6b30ece 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,23 @@ ### Added +- **Coin bounce animation**: Coins now bounce upward when collected, providing satisfying + visual feedback with a parabolic arc animation (0.4s, 2 tiles high) + +### Fixed + +- **Java plugin dependency**: Removed Java plugin as a required dependency to improve + compatibility + +### Changed + +- **Coordinate calculations**: Reduced code duplication in rendering coordinate + calculations + +## [0.2.2] - 2026-04-06 + +### Added + - **Debug render mode**: Added an optional in-editor debug overlay for game rendering - **FPS counter**: Show current renderer FPS inside debug mode diff --git a/src/main/kotlin/dev/stillya/vpet/game/WorldUpdate.kt b/src/main/kotlin/dev/stillya/vpet/game/WorldUpdate.kt index dcd3e07..4cf679b 100644 --- a/src/main/kotlin/dev/stillya/vpet/game/WorldUpdate.kt +++ b/src/main/kotlin/dev/stillya/vpet/game/WorldUpdate.kt @@ -5,12 +5,14 @@ import dev.stillya.vpet.game.ecs.Physics import dev.stillya.vpet.game.ecs.SpatialGrid import dev.stillya.vpet.game.ecs.World import dev.stillya.vpet.game.ecs.components.Collectible +import dev.stillya.vpet.game.ecs.components.Collecting import dev.stillya.vpet.game.ecs.components.PhaseState import dev.stillya.vpet.game.ecs.components.PhysicsState import dev.stillya.vpet.game.ecs.components.SpriteState import dev.stillya.vpet.game.ecs.components.Transform import dev.stillya.vpet.game.ecs.components.Velocity import dev.stillya.vpet.game.ecs.systems.AnimationSystem +import dev.stillya.vpet.game.ecs.systems.CollectionSystem import dev.stillya.vpet.game.ecs.systems.CollisionSystem import dev.stillya.vpet.game.input.InputState import dev.stillya.vpet.game.physics.PhysicsBody @@ -68,9 +70,15 @@ object WorldUpdate { var scoreGain = 0 for (id in collected) { val collectible = reg.get(id) - if (collectible != null) scoreGain += collectible.value - reg.markForRemoval(id) + val entityTransform = reg.get(id) + if (collectible != null && entityTransform != null) { + scoreGain += collectible.value + reg.add(id, Collecting(startY = entityTransform.y)) + reg.remove(id) + } } + + CollectionSystem.updateCollecting(reg, dt) reg.flushRemovals() AnimationSystem.updateAnimations(reg, dt) diff --git a/src/main/kotlin/dev/stillya/vpet/game/ecs/EntityRegistry.kt b/src/main/kotlin/dev/stillya/vpet/game/ecs/EntityRegistry.kt index 86bd449..8f07e9e 100644 --- a/src/main/kotlin/dev/stillya/vpet/game/ecs/EntityRegistry.kt +++ b/src/main/kotlin/dev/stillya/vpet/game/ecs/EntityRegistry.kt @@ -37,6 +37,14 @@ class EntityRegistry { fun has(id: EntityID, type: KClass): Boolean = components[id]?.containsKey(type) == true + inline fun remove(id: EntityID) { + remove(id, T::class) + } + + fun remove(id: EntityID, type: KClass) { + components[id]?.remove(type) + } + fun allWith(vararg types: KClass<*>): List = components.entries .filter { (_, comps) -> types.all { it in comps } } diff --git a/src/main/kotlin/dev/stillya/vpet/game/ecs/components/Collecting.kt b/src/main/kotlin/dev/stillya/vpet/game/ecs/components/Collecting.kt new file mode 100644 index 0000000..f8d30ff --- /dev/null +++ b/src/main/kotlin/dev/stillya/vpet/game/ecs/components/Collecting.kt @@ -0,0 +1,6 @@ +package dev.stillya.vpet.game.ecs.components + +data class Collecting( + val startY: Float, + val elapsed: Float = 0f +) diff --git a/src/main/kotlin/dev/stillya/vpet/game/ecs/systems/CollectionSystem.kt b/src/main/kotlin/dev/stillya/vpet/game/ecs/systems/CollectionSystem.kt new file mode 100644 index 0000000..e8aaab1 --- /dev/null +++ b/src/main/kotlin/dev/stillya/vpet/game/ecs/systems/CollectionSystem.kt @@ -0,0 +1,32 @@ +package dev.stillya.vpet.game.ecs.systems + +import dev.stillya.vpet.game.ecs.EntityRegistry +import dev.stillya.vpet.game.ecs.components.Collecting +import dev.stillya.vpet.game.ecs.components.Transform +import kotlin.math.pow + +object CollectionSystem { + private const val BOUNCE_DURATION = 0.4f + private const val BOUNCE_HEIGHT = 2.0f + + fun updateCollecting(registry: EntityRegistry, dt: Float) { + val collectingEntities = registry.allWith(Collecting::class, Transform::class) + + for (id in collectingEntities) { + val collecting = registry.get(id) ?: continue + val transform = registry.get(id) ?: continue + + val newElapsed = collecting.elapsed + dt + + if (newElapsed >= BOUNCE_DURATION) { + registry.markForRemoval(id) + } else { + val progress = newElapsed / BOUNCE_DURATION + val bounceOffset = BOUNCE_HEIGHT * (1 - (2 * progress - 1).pow(2)) + + registry.add(id, transform.copy(y = collecting.startY - bounceOffset)) + registry.add(id, collecting.copy(elapsed = newElapsed)) + } + } + } +} diff --git a/src/main/kotlin/dev/stillya/vpet/game/rendering/RenderSystem.kt b/src/main/kotlin/dev/stillya/vpet/game/rendering/RenderSystem.kt index 7f18685..8b9f672 100644 --- a/src/main/kotlin/dev/stillya/vpet/game/rendering/RenderSystem.kt +++ b/src/main/kotlin/dev/stillya/vpet/game/rendering/RenderSystem.kt @@ -96,6 +96,8 @@ class RenderSystem( if (frames.isEmpty()) continue val coinLine = t.y.toTileInt() + if (coinLine < 0) continue + val lineFrac = t.y - coinLine val baseY = editor.logicalPositionToXY(LogicalPosition(coinLine, 0)).y val pixelY = baseY + (lineFrac * lineHeight).toInt() diff --git a/src/test/kotlin/dev/stillya/vpet/game/WorldUpdateTest.kt b/src/test/kotlin/dev/stillya/vpet/game/WorldUpdateTest.kt index 9b16852..dcab3ac 100644 --- a/src/test/kotlin/dev/stillya/vpet/game/WorldUpdateTest.kt +++ b/src/test/kotlin/dev/stillya/vpet/game/WorldUpdateTest.kt @@ -11,6 +11,7 @@ import dev.stillya.vpet.game.ecs.Physics import dev.stillya.vpet.game.ecs.World import dev.stillya.vpet.game.ecs.components.AnimationComponent import dev.stillya.vpet.game.ecs.components.Collectible +import dev.stillya.vpet.game.ecs.components.Collecting import dev.stillya.vpet.game.ecs.components.PhaseState import dev.stillya.vpet.game.ecs.components.PhysicsState import dev.stillya.vpet.game.ecs.components.SpriteState @@ -131,7 +132,9 @@ class WorldUpdateTest { val (frame, _) = WorldUpdate.tick(world, InputState(), 0.016f, character, tileMap, 0..10) assertEquals(initialScore + 1, frame.world.score) - assertTrue("Collected bug should be removed", !frame.world.registry.exists(bug)) + assertTrue("Collected bug should still exist during bounce", frame.world.registry.exists(bug)) + assertTrue("Collected bug should have Collecting component", frame.world.registry.has(bug)) + assertTrue("Collected bug should not have Collectible component", !frame.world.registry.has(bug)) } @Test