diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index f3d5c41..989bbbe 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,38 +1,25 @@ --- name: Bug report -about: Create a report to help us improve -title: '' +about: Report a bug in Canopy +title: "[Bug]: " labels: bug -assignees: '' - --- -**Describe the bug** -A clear and concise description of what the bug is. - -**To Reproduce** -Steps to reproduce the behavior: -1. Go to '...' -2. Click on '....' -3. Scroll down to '....' -4. See error - -**Expected behavior** -A clear and concise description of what you expected to happen. +**Description** +Briefly describe the bug. -**Screenshots** -If applicable, add screenshots to help explain your problem. +**Steps to Reproduce** +1. +2. +3. -**Desktop (please complete the following information):** - - OS: [e.g. iOS] - - Browser [e.g. chrome, safari] - - Version [e.g. 22] +**Expected Behavior** +What should happen instead? -**Smartphone (please complete the following information):** - - Device: [e.g. iPhone6] - - OS: [e.g. iOS8.1] - - Browser [e.g. stock browser, safari] - - Version [e.g. 22] +**Environment** +- Canopy version: +- OS: +- JVM / Kotlin version (if relevant): -**Additional context** -Add any other context about the problem here. +**Additional Context** +Logs, stack traces, or screenshots if applicable. diff --git a/.github/ISSUE_TEMPLATE/feature_proposal.md b/.github/ISSUE_TEMPLATE/feature_proposal.md new file mode 100644 index 0000000..2300d56 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_proposal.md @@ -0,0 +1,21 @@ +--- +name: Feature proposal +about: Propose a new feature or improvement +title: "[Proposal]: " +labels: enhancement, proposal +--- + +**Summary** +Describe the feature or improvement you would like. + +**Motivation** +What problem does this solve? Why is it useful? + +**Proposed Solution** +Describe how you think this could work. + +**Alternatives** +Any alternative approaches or solutions considered. + +**Additional Context** +Links, examples, or related discussions. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index 11fc491..0000000 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -name: Feature request -about: Suggest an idea for this project -title: '' -labels: enhancement -assignees: '' - ---- - -**Is your feature request related to a problem? Please describe.** -A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] - -**Describe the solution you'd like** -A clear and concise description of what you want to happen. - -**Describe alternatives you've considered** -A clear and concise description of any alternative solutions or features you've considered. - -**Additional context** -Add any other context or screenshots about the feature request here. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index a2f7ad9..4a8413b 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,44 +1,32 @@ -**What does this PR do, and why** - +**Summary** +Briefly describe what this PR changes and why. -**What changes were made** - - -- Example change 1 -- Example change 2 +**Changes** +- +- +- **Related Issues** - - Closes # **Type of Change** - - [ ] Bug fix -- [ ] New feature -- [ ] Breaking change +- [ ] Feature - [ ] Refactor -- [ ] Documentation update -- [ ] Test update +- [ ] Documentation +- [ ] Tests - [ ] Chore +- [ ] Breaking change **How to Test** - - -1. -2. -3. +Steps to verify the change: -**Screenshots / Recordings** - +1. +2. +3. **Checklist** - -- [ ] My code follows the project style guidelines -- [ ] I have self-reviewed my code -- [ ] I have added or updated tests where needed -- [ ] I have updated documentation where needed -- [ ] This PR is ready for review - -**Notes for Reviewers** - +- [ ] Code follows project style +- [ ] Self-reviewed +- [ ] Tests added/updated (if applicable) +- [ ] Docs updated (if applicable) diff --git a/engine/core/src/main/kotlin/io/canopy/engine/core/reactive/Context.kt b/engine/core/src/main/kotlin/io/canopy/engine/core/flow/Context.kt similarity index 98% rename from engine/core/src/main/kotlin/io/canopy/engine/core/reactive/Context.kt rename to engine/core/src/main/kotlin/io/canopy/engine/core/flow/Context.kt index 9bd0f7e..c221c9a 100644 --- a/engine/core/src/main/kotlin/io/canopy/engine/core/reactive/Context.kt +++ b/engine/core/src/main/kotlin/io/canopy/engine/core/flow/Context.kt @@ -1,4 +1,4 @@ -package io.canopy.engine.core.reactive +package io.canopy.engine.core.flow import io.canopy.engine.core.nodes.Node diff --git a/engine/core/src/main/kotlin/io/canopy/engine/core/reactive/Event.kt b/engine/core/src/main/kotlin/io/canopy/engine/core/flow/Event.kt similarity index 99% rename from engine/core/src/main/kotlin/io/canopy/engine/core/reactive/Event.kt rename to engine/core/src/main/kotlin/io/canopy/engine/core/flow/Event.kt index fa3e736..f9d342a 100644 --- a/engine/core/src/main/kotlin/io/canopy/engine/core/reactive/Event.kt +++ b/engine/core/src/main/kotlin/io/canopy/engine/core/flow/Event.kt @@ -1,4 +1,4 @@ -package io.canopy.engine.core.reactive +package io.canopy.engine.core.flow import java.lang.ref.WeakReference import java.util.concurrent.CopyOnWriteArrayList diff --git a/engine/core/src/main/kotlin/io/canopy/engine/core/reactive/Signal.kt b/engine/core/src/main/kotlin/io/canopy/engine/core/flow/Signal.kt similarity index 94% rename from engine/core/src/main/kotlin/io/canopy/engine/core/reactive/Signal.kt rename to engine/core/src/main/kotlin/io/canopy/engine/core/flow/Signal.kt index 84502b7..546b32a 100644 --- a/engine/core/src/main/kotlin/io/canopy/engine/core/reactive/Signal.kt +++ b/engine/core/src/main/kotlin/io/canopy/engine/core/flow/Signal.kt @@ -1,4 +1,4 @@ -package io.canopy.engine.core.reactive +package io.canopy.engine.core.flow import kotlin.properties.Delegates import kotlinx.coroutines.flow.MutableSharedFlow @@ -73,6 +73,13 @@ class Signal(initial: T) { /** Removes all listeners registered via [connect]. */ fun clear() = valueChanged.clear() + + /** + * Allows value updates based on previous value + */ + fun update(handler: (T) -> T) { + value = handler(value) + } } /* ------------------------------------------------------------------ diff --git a/engine/core/src/main/kotlin/io/canopy/engine/core/managers/SceneManager.kt b/engine/core/src/main/kotlin/io/canopy/engine/core/managers/SceneManager.kt index 1543fb2..eae3874 100644 --- a/engine/core/src/main/kotlin/io/canopy/engine/core/managers/SceneManager.kt +++ b/engine/core/src/main/kotlin/io/canopy/engine/core/managers/SceneManager.kt @@ -2,10 +2,10 @@ package io.canopy.engine.core.managers import kotlin.reflect.KClass import com.badlogic.gdx.math.Vector2 +import io.canopy.engine.core.flow.asSignal +import io.canopy.engine.core.flow.event import io.canopy.engine.core.nodes.Node import io.canopy.engine.core.nodes.TreeSystem -import io.canopy.engine.core.reactive.asSignal -import io.canopy.engine.core.reactive.event import io.canopy.engine.logging.EngineLogs import io.canopy.engine.logging.LogContext diff --git a/engine/core/src/main/kotlin/io/canopy/engine/core/nodes/Node.kt b/engine/core/src/main/kotlin/io/canopy/engine/core/nodes/Node.kt index 86ea83f..cab34e3 100644 --- a/engine/core/src/main/kotlin/io/canopy/engine/core/nodes/Node.kt +++ b/engine/core/src/main/kotlin/io/canopy/engine/core/nodes/Node.kt @@ -1,10 +1,10 @@ package io.canopy.engine.core.nodes import kotlin.reflect.KClass +import io.canopy.engine.core.flow.Context import io.canopy.engine.core.managers.SceneManager import io.canopy.engine.core.managers.lazyManager import io.canopy.engine.core.managers.manager -import io.canopy.engine.core.reactive.Context import io.canopy.engine.logging.EngineLogs import io.canopy.engine.logging.LogContext diff --git a/engine/core/src/main/kotlin/io/canopy/engine/core/reactive/NodeRef.kt b/engine/core/src/main/kotlin/io/canopy/engine/core/reactive/NodeRef.kt deleted file mode 100644 index 58f0a40..0000000 --- a/engine/core/src/main/kotlin/io/canopy/engine/core/reactive/NodeRef.kt +++ /dev/null @@ -1,73 +0,0 @@ -package io.canopy.engine.core.reactive - -import java.lang.ref.WeakReference -import io.canopy.engine.core.nodes.Node - -/** - * A reference to a [Node] that can be resolved later. - * - * This is useful when: - * - you want to point to a node that might not exist yet (path-based lookup) - * - you want to avoid retaining a node strongly (weak reference) - * - * Inspired by Godot's `$"path/to/node"` style lookups. - * - * Resolution: - * - [DirectRef] resolves to a specific node instance (stored as a [WeakReference]) - * - [PathRef] resolves by calling [Node.getNode] on the provided owner - * - * Note: - * - [DirectRef] may become null if the node is garbage-collected. - * - [PathRef] depends on the node tree structure and may fail if nodes are renamed/moved. - */ -sealed class NodeRef> { - - /** - * Resolves the referenced node relative to [owner]. - * - * @throws IllegalStateException / IllegalArgumentException if resolution fails - */ - abstract fun get(owner: Node<*>): T - - /** - * Direct reference to a node instance using a [WeakReference]. - * - * This avoids keeping the node alive unintentionally, but resolution may fail - * if the node has been garbage-collected. - */ - class DirectRef>(node: T) : NodeRef() { - private val reference = WeakReference(node) - - override fun get(owner: Node<*>): T = reference.get() - ?: throw IllegalStateException( - "Direct node reference is no longer valid (node was garbage-collected)." - ) - } - - /** - * Path-based reference resolved from an [owner] node. - * - * This is flexible (works even if the target node is created later), - * but it depends on the stability of node paths (names/structure). - */ - class PathRef>(private val path: String) : NodeRef() { - override fun get(owner: Node<*>): T = owner.getNode(path) - } - - operator fun > NodeRef.invoke(owner: Node<*>): T = get(owner) -} - -/** - * Creates a direct (weak) reference to an existing node instance. - */ -fun > nodeRef(node: T): NodeRef = NodeRef.DirectRef(node) - -/** - * Creates a path reference (resolved relative to the owner passed to [NodeRef.get]). - * - * Example: - * ``` - * val weaponRef = nodeRef>("$/Player/Weapon") - * ``` - */ -fun > nodeRef(path: String): NodeRef = NodeRef.PathRef(path) diff --git a/engine/core/src/test/kotlin/io/canopy/engine/core/reactive/ContextTests.kt b/engine/core/src/test/kotlin/io/canopy/engine/core/flow/ContextTests.kt similarity index 99% rename from engine/core/src/test/kotlin/io/canopy/engine/core/reactive/ContextTests.kt rename to engine/core/src/test/kotlin/io/canopy/engine/core/flow/ContextTests.kt index 1eb068f..5c41aef 100644 --- a/engine/core/src/test/kotlin/io/canopy/engine/core/reactive/ContextTests.kt +++ b/engine/core/src/test/kotlin/io/canopy/engine/core/flow/ContextTests.kt @@ -1,4 +1,4 @@ -package io.canopy.engine.core.reactive +package io.canopy.engine.core.flow import io.canopy.engine.core.managers.ManagersRegistry import io.canopy.engine.core.managers.SceneManager diff --git a/engine/core/src/test/kotlin/io/canopy/engine/core/reactive/EventTests.kt b/engine/core/src/test/kotlin/io/canopy/engine/core/flow/EventTests.kt similarity index 99% rename from engine/core/src/test/kotlin/io/canopy/engine/core/reactive/EventTests.kt rename to engine/core/src/test/kotlin/io/canopy/engine/core/flow/EventTests.kt index bcd755f..f13d519 100644 --- a/engine/core/src/test/kotlin/io/canopy/engine/core/reactive/EventTests.kt +++ b/engine/core/src/test/kotlin/io/canopy/engine/core/flow/EventTests.kt @@ -1,4 +1,4 @@ -package io.canopy.engine.core.reactive +package io.canopy.engine.core.flow import kotlin.concurrent.atomics.AtomicInt import kotlin.concurrent.atomics.ExperimentalAtomicApi diff --git a/engine/core/src/test/kotlin/io/canopy/engine/core/reactive/SignalTests.kt b/engine/core/src/test/kotlin/io/canopy/engine/core/flow/SignalTests.kt similarity index 88% rename from engine/core/src/test/kotlin/io/canopy/engine/core/reactive/SignalTests.kt rename to engine/core/src/test/kotlin/io/canopy/engine/core/flow/SignalTests.kt index 534a703..f27b7b2 100644 --- a/engine/core/src/test/kotlin/io/canopy/engine/core/reactive/SignalTests.kt +++ b/engine/core/src/test/kotlin/io/canopy/engine/core/flow/SignalTests.kt @@ -1,4 +1,4 @@ -package io.canopy.engine.core.reactive +package io.canopy.engine.core.flow import kotlin.test.Test import kotlinx.coroutines.launch @@ -28,7 +28,7 @@ class SignalTests { signal connect callback // Mutate the signal - signal.value = 42 + signal.value += 42 // Listener should receive the new value assert(receivedValue == 42) { "Listener should have received the emitted value." } @@ -107,9 +107,9 @@ class SignalTests { } // Update values - signal.value = 42 + signal.update { 42 } signal.value = 100 - signal.value = 100 // duplicate -> should not be collected (distinctUntilChanged) + signal.update { 100 } // duplicate -> should not be collected (distinctUntilChanged) // Give collector a chance to run yield() @@ -120,13 +120,13 @@ class SignalTests { @Test fun `asSignal should wrap a value and allow updates`() { - val signalVal = 10.asSignal() + val signal = 10.asSignal() // Initial value should be preserved - assertEquals(10, signalVal.value) { "Wrapped value should match the initial value." } + assertEquals(10, signal.value) { "Wrapped value should match the initial value." } // Updating the signal should update its stored value - signalVal.value = 20 - assertEquals(20, signalVal.value) { "Wrapped value should update when assigned." } + signal.update { 20 } + assertEquals(20, signal.value) { "Wrapped value should update when assigned." } } } diff --git a/engine/core/src/test/kotlin/io/canopy/engine/core/nodes/NodeRefTests.kt b/engine/core/src/test/kotlin/io/canopy/engine/core/nodes/NodeRefTests.kt deleted file mode 100644 index d1730a6..0000000 --- a/engine/core/src/test/kotlin/io/canopy/engine/core/nodes/NodeRefTests.kt +++ /dev/null @@ -1,63 +0,0 @@ -package io.canopy.engine.core.nodes - -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertNotNull -import io.canopy.engine.core.managers.ManagersRegistry -import io.canopy.engine.core.managers.SceneManager -import io.canopy.engine.core.nodes.types.empty.EmptyNode -import io.canopy.engine.core.reactive.NodeRef -import io.canopy.engine.core.reactive.nodeRef -import org.junit.jupiter.api.BeforeAll - -class NodeRefTests { - - /** - * Node that stores an external node reference. - * - * In real usage this pattern is common for "wiring" nodes together without - * requiring the target to be constructed first. - */ - private class NeedsRef(name: String, val external: NodeRef<*>, block: NeedsRef.() -> Unit = {}) : - Node(name, block) - - companion object { - @BeforeAll - @JvmStatic - fun setup() { - // Tests run in a shared JVM; reset manager state to avoid cross-test contamination. - ManagersRegistry.withScope { - register(SceneManager()) - } - } - } - - @Test - fun `path nodeRef should resolve from scene root`() { - var referencedNodeName: String? = null - - // Build a tree where "referrer" holds a reference to "$/external". - val tree = - EmptyNode(name = "root") { - NeedsRef( - name = "referrer", - external = nodeRef("$/external") // `$` means "resolve from current scene root" - ) { - behavior( - onReady = { - // Resolve the reference relative to this node. - referencedNodeName = external.get(this).name - } - ) - } - - EmptyNode(name = "external") - }.asSceneRoot() - - // Triggers enterTree + ready; behaviors run in ready. - tree.buildTree() - - assertNotNull(referencedNodeName, "Expected reference to be resolved during onReady()") - assertEquals("external", referencedNodeName) - } -}