From 2d3fea4fd88fb69368f2759877dbb24383bb0356 Mon Sep 17 00:00:00 2001 From: Rohan Sircar Date: Thu, 26 Nov 2020 10:40:31 +0530 Subject: [PATCH] added javafx ui in game --- build.sbt | 20 +- lib/jme-jfx-11-1.1.5.jar | Bin 0 -> 71570 bytes .../scala/com/jayfella/jme/jfx/package.scala | 10 + .../scala/com/jme3/animation/package.scala | 98 ++- src/main/scala/com/jme3/app/package.scala | 5 +- .../com/jme3/input/controls/package.scala | 36 +- src/main/scala/com/jme3/input/package.scala | 3 +- src/main/scala/com/jme3/scene/package.scala | 4 +- .../org/slf4j/impl/StaticLoggerBuilder.scala | 3 + src/main/scala/wow/doge/mygame/Main.scala | 94 +-- src/main/scala/wow/doge/mygame/MainApp.scala | 145 +++- .../scala/wow/doge/mygame/game/GameApp.scala | 142 ++-- .../scala/wow/doge/mygame/game/GameApp2.scala | 38 +- .../wow/doge/mygame/game/GameModule.scala | 13 +- .../mygame/game/GameSystemsInitializer.scala | 4 +- .../mygame/game/nodes/PlayerController.scala | 8 +- .../game/nodes/PlayerEventListeners.scala | 4 +- .../mygame/game/subsystems/ai/GdxAiTest.scala | 33 + .../mygame/game/subsystems/ai/gdx/Graph.java | 43 + .../ai/gdx/IndexedAStarPathFinder.java | 371 +++++++++ .../subsystems/ai/gdx/MyIndexedGraph.java | 41 + .../subsystems/input/GameInputHandler.scala | 24 - .../subsystems/level/DefaultGameLevel.scala | 6 +- .../level/{Level.scala => GameLevel.scala} | 12 +- .../mygame/game/subsystems/ui/JmeJfx.scala | 43 + .../implicits/JavaFXMonixObservables.scala | 88 ++ .../wow/doge/mygame/implicits/TestEnum.scala | 43 - .../implicits/observables/package.scala | 18 + .../wow/doge/mygame/implicits/package.scala | 69 +- .../wow/doge/mygame/launcher/DefaultUI.scala | 102 +++ .../wow/doge/mygame/launcher/Launcher.scala | 153 ++++ .../subsystems/events/MovementEvents.scala | 2 +- .../moddingsystem/ModdingSystem.scala | 3 - .../subsystems/scriptsystem/ScriptActor.scala | 47 +- .../scriptsystem/ScriptCachingActor.scala | 73 ++ .../scriptsystem/ScriptSystemModule.scala | 1 + .../doge/mygame/utils/BorderlessScene.scala | 750 ++++++++++++++++++ .../mygame/utils/GenericConsoleStream.scala | 104 +++ .../doge/mygame/utils/JFXConsoleStream.scala | 71 -- .../wow/doge/mygame/utils/ResizeHelper.java | 130 +++ .../wow/doge/mygame/utils/TreeTest.scala | 24 + 41 files changed, 2415 insertions(+), 463 deletions(-) create mode 100644 lib/jme-jfx-11-1.1.5.jar create mode 100644 src/main/scala/com/jayfella/jme/jfx/package.scala create mode 100644 src/main/scala/wow/doge/mygame/game/subsystems/ai/GdxAiTest.scala create mode 100644 src/main/scala/wow/doge/mygame/game/subsystems/ai/gdx/Graph.java create mode 100644 src/main/scala/wow/doge/mygame/game/subsystems/ai/gdx/IndexedAStarPathFinder.java create mode 100644 src/main/scala/wow/doge/mygame/game/subsystems/ai/gdx/MyIndexedGraph.java rename src/main/scala/wow/doge/mygame/game/subsystems/level/{Level.scala => GameLevel.scala} (79%) create mode 100644 src/main/scala/wow/doge/mygame/game/subsystems/ui/JmeJfx.scala create mode 100644 src/main/scala/wow/doge/mygame/implicits/JavaFXMonixObservables.scala delete mode 100644 src/main/scala/wow/doge/mygame/implicits/TestEnum.scala create mode 100644 src/main/scala/wow/doge/mygame/implicits/observables/package.scala create mode 100644 src/main/scala/wow/doge/mygame/launcher/DefaultUI.scala create mode 100644 src/main/scala/wow/doge/mygame/launcher/Launcher.scala create mode 100644 src/main/scala/wow/doge/mygame/utils/BorderlessScene.scala create mode 100644 src/main/scala/wow/doge/mygame/utils/GenericConsoleStream.scala delete mode 100644 src/main/scala/wow/doge/mygame/utils/JFXConsoleStream.scala create mode 100644 src/main/scala/wow/doge/mygame/utils/ResizeHelper.java create mode 100644 src/main/scala/wow/doge/mygame/utils/TreeTest.scala diff --git a/build.sbt b/build.sbt index 6072f63..d0f09ae 100644 --- a/build.sbt +++ b/build.sbt @@ -21,6 +21,7 @@ scalaVersion := "2.13.3" resolvers += "Jcenter" at "https://jcenter.bintray.com/" resolvers += "JME Bintray" at "https://bintray.com/jmonkeyengine/com.jme3" +resolvers += "Jitpack" at "https://jitpack.io" resolvers += Resolver.mavenLocal resolvers += Resolver.sonatypeRepo("snapshots") @@ -48,7 +49,7 @@ lazy val root = (project in file(".")).settings( organization := "wow.doge", version := "1.0-SNAPSHOT", libraryDependencies ++= Seq( - "org.scala-lang.modules" %% "scala-parser-combinators" % "1.1.2", + // "org.scala-lang.modules" %% "scala-parser-combinators" % "1.1.2", // https://mvnrepository.com/artifact/org.jmonkeyengine/jme3-core "org.jmonkeyengine" % "jme3-core" % jmeVersion, // https://mvnrepository.com/artifact/org.jmonkeyengine/jme3-desktop @@ -58,23 +59,18 @@ lazy val root = (project in file(".")).settings( "org.jmonkeyengine" % "jme3-effects" % jmeVersion, "org.jmonkeyengine" % "jme3-plugins" % jmeVersion, "org.jmonkeyengine" % "jme3-blender" % jmeVersion, -// https://mvnrepository.com/artifact/com.github.stephengold/Minie "com.github.stephengold" % "Minie" % "3.0.0", -// https://mvnrepository.com/artifact/com.simsilica/zay-es "com.simsilica" % "zay-es" % "1.2.1", "org.typelevel" %% "cats-core" % "2.1.1", "com.lihaoyi" % "ammonite" % "2.2.0" cross CrossVersion.full, "org.jetbrains.kotlin" % "kotlin-main-kts" % "1.4.10", "org.jetbrains.kotlin" % "kotlin-scripting-jsr223" % "1.4.10", "org.codehaus.groovy" % "groovy-all" % "3.0.6" pomOnly (), - // "wow.doge" % "game" % "1.0-SNAPSHOT", "org.scalafx" %% "scalafx" % "14-R19", "com.typesafe.akka" %% "akka-actor-typed" % "2.6.10", - // "ch.qos.logback" % "logback-classic" % "1.2.3", "org.typelevel" %% "cats-core" % "2.1.1", "org.typelevel" %% "cats-effect" % "2.1.4", "io.monix" %% "monix" % "3.2.2", - // "io.monix" %% "monix-bio" % "1.0.0", "io.monix" %% "monix-bio" % "1.1.0", "io.circe" %% "circe-core" % "0.13.0", "io.circe" %% "circe-generic" % "0.13.0", @@ -85,15 +81,21 @@ lazy val root = (project in file(".")).settings( "com.github.valskalla" %% "odin-monix" % "0.8.1", "com.github.valskalla" %% "odin-json" % "0.9.1", "com.softwaremill.macwire" %% "util" % "2.3.7", - "com.softwaremill.macwire" %% "macros" % "2.3.6" % "provided", - "com.softwaremill.macwire" %% "macrosakka" % "2.3.6" % "provided", + "com.softwaremill.macwire" %% "macros" % "2.3.7" % "provided", + // "com.softwaremill.macwire" %% "macrosakka" % "2.3.6" % "provided", "com.github.valskalla" %% "odin-slf4j" % "0.8.1", "com.softwaremill.quicklens" %% "quicklens" % "1.6.1", "org.scala-lang.modules" %% "scala-parallel-collections" % "1.0.0-RC1", "com.typesafe.scala-logging" %% "scala-logging" % "3.9.2", "io.circe" %% "circe-config" % "0.8.0", "com.beachape" %% "enumeratum-circe" % "1.6.1", - "com.lihaoyi" %% "os-lib" % "0.7.1" + "com.lihaoyi" %% "os-lib" % "0.7.1", + // "com.jayfella" % "jme-jfx-11" % "1.1.5", + "com.github.goxr3plus" % "FX-BorderlessScene" % "4.4.0", + "com.github.Oshan96" % "CustomStage" % "v1.3.1", + "com.badlogicgames.gdx" % "gdx-ai" % "1.8.2", + "org.recast4j" % "recast" % "1.2.5", + "org.recast4j" % "detour" % "1.2.5" ), // Determine OS version of JavaFX binaries diff --git a/lib/jme-jfx-11-1.1.5.jar b/lib/jme-jfx-11-1.1.5.jar new file mode 100644 index 0000000000000000000000000000000000000000..35efab9c38992c5fbf12fb58cb23ce212cd6c60e GIT binary patch literal 71570 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 - * See {@link #setAnim(java.lang.String, float)}. - * The blendTime argument by default is 150 milliseconds. - * - * @param action The action (name) of the animation to play - */ - def setAnim(action: Action): Unit = uval.setAnim(action.name) - - /** - * Set the current animation that is played by this AnimChannel. - *

- * This resets the time to zero, and optionally blends the animation - * over blendTime seconds with the currently playing animation. - * Notice that this method will reset the control's speed to 1.0. - * - * @param action The action (name) of the animation to play - * @param blendTime The blend time over which to blend the new animation - * with the old one. If zero, then no blending will occur and the new - * animation will be applied instantly. - */ - def setAnim(action: Action, blendTime: Float): Unit = uval.setAnim(action.name, blendTime) + * Set the current animation that is played by this AnimChannel. + *

+ * See {@link #setAnim(java.lang.String, float)}. + * The blendTime argument by default is 150 milliseconds. + * + * @param action The action (name) of the animation to play + */ + def setAnim(action: Action): Unit = uval.setAnim(action.name) + /** + * Set the current animation that is played by this AnimChannel. + *

+ * This resets the time to zero, and optionally blends the animation + * over blendTime seconds with the currently playing animation. + * Notice that this method will reset the control's speed to 1.0. + * + * @param action The action (name) of the animation to play + * @param blendTime The blend time over which to blend the new animation + * with the old one. If zero, then no blending will occur and the new + * animation will be applied instantly. + */ + def setAnim(action: Action, blendTime: Float): Unit = + uval.setAnim(action.name, blendTime) } - implicit class AnimEventListenerWrap(private val uval: AnimEventListener) extends AnyVal { - + implicit final class AnimEventListenerWrap( + private val uval: AnimEventListener + ) extends AnyVal { + /** - * Invoked when an animation "cycle" is done. For non-looping animations, - * this event is invoked when the animation is finished playing. For - * looping animations, this even is invoked each time the animation is restarted. - * - * @param control The control to which the listener is assigned. - * @param channel The channel being altered - * @param action The new animation action that is done. - */ - def onAnimCycleDone(control: AnimControl, channel: AnimChannel, action: Action): Unit = + * Invoked when an animation "cycle" is done. For non-looping animations, + * this event is invoked when the animation is finished playing. For + * looping animations, this even is invoked each time the animation is restarted. + * + * @param control The control to which the listener is assigned. + * @param channel The channel being altered + * @param action The new animation action that is done. + */ + def onAnimCycleDone( + control: AnimControl, + channel: AnimChannel, + action: Action + ): Unit = uval.onAnimCycleDone(control, channel, action.name) /** - * Invoked when a animation is set to play by the user on the given channel. - * - * @param control The control to which the listener is assigned. - * @param channel The channel being altered - * @param action The new animation action set. - */ - def onAnimChange(control: AnimControl, channel: AnimChannel, action: Action): Unit = - uval.onAnimChange(control, channel, action.name) + * Invoked when a animation is set to play by the user on the given channel. + * + * @param control The control to which the listener is assigned. + * @param channel The channel being altered + * @param action The new animation action set. + */ + def onAnimChange( + control: AnimControl, + channel: AnimChannel, + action: Action + ): Unit = + uval.onAnimChange(control, channel, action.name) } - -} \ No newline at end of file +} diff --git a/src/main/scala/com/jme3/app/package.scala b/src/main/scala/com/jme3/app/package.scala index 658ea99..027b473 100644 --- a/src/main/scala/com/jme3/app/package.scala +++ b/src/main/scala/com/jme3/app/package.scala @@ -5,12 +5,13 @@ package com.jme3 */ package object app { - implicit class SimpleApplicationWrap(private val uval: SimpleApplication) extends AnyVal { + implicit final class SimpleApplicationWrap( + private val uval: SimpleApplication + ) extends AnyVal { //FIXME: proof of concept, remove later def testWrap: String = uval.hashCode().toString - } } diff --git a/src/main/scala/com/jme3/input/controls/package.scala b/src/main/scala/com/jme3/input/controls/package.scala index ac29ed3..803f425 100644 --- a/src/main/scala/com/jme3/input/controls/package.scala +++ b/src/main/scala/com/jme3/input/controls/package.scala @@ -5,31 +5,33 @@ package com.jme3.input */ package object controls { - implicit class ActionListenerWrap(private val uval: ActionListener) extends AnyVal { + implicit final class ActionListenerWrap(private val uval: ActionListener) + extends AnyVal { /** - * Called when an input to which this listener is registered to is invoked. - * - * @param action The action (name) of the mapping that was invoked - * @param isPressed True if the action is "pressed", false otherwise - * @param tpf The time per frame value. - */ + * Called when an input to which this listener is registered to is invoked. + * + * @param action The action (name) of the mapping that was invoked + * @param isPressed True if the action is "pressed", false otherwise + * @param tpf The time per frame value. + */ def onAction(action: Action, keyPressed: Boolean, tpf: Float): Unit = uval.onAction(action.name, keyPressed, tpf) } - implicit class AnalogListenerWrap(private val uval: AnalogListener) extends AnyVal { + implicit final class AnalogListenerWrap(private val uval: AnalogListener) + extends AnyVal { /** - * Called to notify the implementation that an analog event has occurred. - * - * The results of KeyTrigger and MouseButtonTrigger events will have tpf - * == value. - * - * @param action The action (name) of the mapping that was invoked - * @param value Value of the axis, from 0 to 1. - * @param tpf The time per frame value. - */ + * Called to notify the implementation that an analog event has occurred. + * + * The results of KeyTrigger and MouseButtonTrigger events will have tpf + * == value. + * + * @param action The action (name) of the mapping that was invoked + * @param value Value of the axis, from 0 to 1. + * @param tpf The time per frame value. + */ def onAnalog(action: Action, value: Float, tpf: Float): Unit = uval.onAnalog(action.name, value, tpf) } diff --git a/src/main/scala/com/jme3/input/package.scala b/src/main/scala/com/jme3/input/package.scala index fff855f..aa51000 100644 --- a/src/main/scala/com/jme3/input/package.scala +++ b/src/main/scala/com/jme3/input/package.scala @@ -8,7 +8,8 @@ import com.jme3.input.controls.Trigger */ package object input { - implicit class InputManagerWrap(private val uval: InputManager) extends AnyVal { + implicit final class InputManagerWrap(private val uval: InputManager) + extends AnyVal { def addMapping(action: Action, triggers: Trigger*): Unit = uval.addMapping(action.name, triggers: _*) def addListener(listener: InputListener, actions: Action*): Unit = diff --git a/src/main/scala/com/jme3/scene/package.scala b/src/main/scala/com/jme3/scene/package.scala index 3b6698b..59db398 100644 --- a/src/main/scala/com/jme3/scene/package.scala +++ b/src/main/scala/com/jme3/scene/package.scala @@ -33,14 +33,14 @@ package object scene { def apply(name: String): Node = new Node(name) } - implicit class NodeWrap(private val uval: Node) extends AnyVal { + implicit final class NodeWrap(private val uval: Node) extends AnyVal { def getControlMaybe[T <: Control](controlType: Class[T]): Option[T] = Option(uval.getControl(controlType)) } - implicit class SpatialWrap(private val uval: Spatial) extends AnyVal { + implicit final class SpatialWrap(private val uval: Spatial) extends AnyVal { def toNode: Either[ClassCastException, Node] = uval match { diff --git a/src/main/scala/org/slf4j/impl/StaticLoggerBuilder.scala b/src/main/scala/org/slf4j/impl/StaticLoggerBuilder.scala index 529ee98..0cb63ea 100644 --- a/src/main/scala/org/slf4j/impl/StaticLoggerBuilder.scala +++ b/src/main/scala/org/slf4j/impl/StaticLoggerBuilder.scala @@ -69,6 +69,9 @@ class StaticLoggerBinder extends OdinLoggerBinder[IO] { case asyncHttpClient if asyncHttpClient.startsWith("org.asynchttpclient.netty") => defaultConsoleLogger.withMinimalLevel(Level.Warn) + case s if s.startsWith("com.jayfella.jme.jfx.util.JfxPlatform") => + defaultConsoleLogger.withMinimalLevel(Level.Info) + // case s // if s.startsWith( // "wow.doge.mygame.subsystems.movement.PlayerMovementEventHandler" diff --git a/src/main/scala/wow/doge/mygame/Main.scala b/src/main/scala/wow/doge/mygame/Main.scala index e4c89d1..4a0a3b6 100644 --- a/src/main/scala/wow/doge/mygame/Main.scala +++ b/src/main/scala/wow/doge/mygame/Main.scala @@ -2,7 +2,6 @@ package wow.doge.mygame import scala.concurrent.duration._ -import _root_.monix.bio.Task import akka.util.Timeout import cats.effect.ExitCode import cats.implicits._ @@ -11,17 +10,19 @@ import io.odin._ import io.odin.json.Formatter import io.odin.syntax._ import wow.doge.mygame.game.GameAppResource -import wow.doge.mygame.subsystems.scriptsystem.ScriptSystemResource import _root_.monix.bio.BIOApp +import _root_.monix.bio.Task import _root_.monix.bio.UIO import cats.effect.Resource +import scalafx.scene.control.TextArea +import wow.doge.mygame.utils.GenericConsoleStream object Main extends BIOApp with MainModule { import java.util.logging.{Logger => JLogger, Level} JLogger.getLogger("").setLevel(Level.SEVERE) implicit val timeout = Timeout(1.second) - def appResource = + def appResource(consoleStream: GenericConsoleStream[TextArea]) = for { logger <- consoleLogger().withAsync(timeWindow = 1.milliseconds) |+| @@ -31,89 +32,38 @@ object Main extends BIOApp with MainModule { ).withAsync(timeWindow = 1.milliseconds) jmeScheduler <- jMESchedulerResource actorSystem <- actorSystemResource2(logger) - scriptCacheActor <- new ScriptSystemResource(os.pwd, actorSystem)( - timeout, - actorSystem.scheduler - ).make - // akkaScheduler = actorSystemResource2.scheduler // consoleTextArea <- Resource.liftF(Task(new TextArea())) // consoleStream <- wireWith(JFXConsoleStream.textAreaStream _) - (gameApp) <- { + gameApp <- { // new BulletAppState() // bas.setThreadingType(Thr) // gameAppResource(new StatsAppState()) - wire[GameAppResource].get2 + wire[GameAppResource].get } _ <- Resource.liftF( - new MainApp(logger, gameApp, actorSystem, jmeScheduler)( + new MainApp( + logger, + gameApp, + actorSystem, + jmeScheduler, + schedulers, + consoleStream + )( timeout, actorSystem.scheduler - ).gameInit + ).program ) - // fib <- Resource.liftF( - // gameApp - // .enqueueL(() => - // new MainApp(logger, gameApp, actorSystem, jmeScheduler)( - // timeout, - // actorSystem.scheduler - // ) - // ) - // .start - // ) - // _ <- Resource.liftF(fib.join.flatMap(_.gameInit) - // app = gameApp - // inputManager = gameApp.inputManager - // assetManager = gameApp.assetManager - // bulletAppState = new BulletAppState() - // (playerMovementEventBus, playerCameraEventBus) <- new EventsModule2( - // actorSystem - // ).resource - // b1 = playerMovementEventBus - // b2 = playerCameraEventBus - - // playerPos = ImVector3f.ZERO - // playerNode = None.taggedWith[Player] - // modelPath = os.rel / "Models" / "Jaime" / "Jaime.j3o".taggedWith[Player] - // playerController <- Resource.liftF { - // implicit val s = actorSystem.scheduler - // wire[PlayerController.Props].create.onErrorHandle(err => - // logger.error(err.toString()) - // ) - // } - - // gameSystemsInitializerFib <- Resource.make( - // logger.info("creating game systems initializer") >> - // gameApp - // .enqueueL(() => wire[GameSystemsInitializer]) - // .start - // )(c => logger.info("destroying game systems initializer") >> c.cancel) - // _ <- Resource.liftF(gameSystemsInitializerFib.join.flatMap(_.init)) } yield () - // def createPlayerController( - // playerMovementEventBus: ActorRef[ - // EventBus.Command[PlayerMovementEvent] - // ], - // playerCameraEventBus: ActorRef[EventBus.Command[PlayerCameraEvent]] - // ): IO[PlayerController.Error, Unit] = { - // val playerPos = ImVector3f.ZERO - // val playerNode = None.taggedWith[Player] - // val modelPath = os.rel / "Models" / "Jaime" / "Jaime.j3o" - // wire[PlayerController.Props].create - // } - def run(args: List[String]): UIO[ExitCode] = { - // Console.withOut( - // new JFXConsoleStream( - // new scalafx.scene.control.TextArea(), - // new ByteArrayOutputStream(35) - // ) - // )(()) - appResource - .use(_ => Task.unit) - .onErrorHandle(_.printStackTrace()) - .as(ExitCode.Success) + lazy val consoleStream = GenericConsoleStream.textAreaStream() + Console.withOut(consoleStream)( + appResource(consoleStream) + .use(_ => Task.unit) + .onErrorHandle(_.printStackTrace()) + .as(ExitCode.Success) + ) } } diff --git a/src/main/scala/wow/doge/mygame/MainApp.scala b/src/main/scala/wow/doge/mygame/MainApp.scala index 5bed4f7..b287386 100644 --- a/src/main/scala/wow/doge/mygame/MainApp.scala +++ b/src/main/scala/wow/doge/mygame/MainApp.scala @@ -20,7 +20,7 @@ import monix.bio.IO import monix.bio.Task import wow.doge.mygame.events.EventBus import wow.doge.mygame.game.GameApp2 -import wow.doge.mygame.game.nodes.Player +import wow.doge.mygame.game.nodes.PlayerTag import wow.doge.mygame.game.nodes.PlayerController import wow.doge.mygame.game.subsystems.input.GameInputHandler import wow.doge.mygame.implicits._ @@ -32,12 +32,32 @@ import wow.doge.mygame.game.subsystems.level.DefaultGameLevel import com.jme3.renderer.ViewPort import wow.doge.mygame.subsystems.scriptsystem.ScriptSystemResource import wow.doge.mygame.subsystems.scriptsystem.ScriptInitMode +import wow.doge.mygame.launcher.Launcher +import wow.doge.mygame.executors.Schedulers +import scalafx.application.JFXApp.PrimaryStage +import scalafx.geometry.Insets +import scalafx.scene.Scene +import scalafx.scene.control.Button +import scalafx.scene.layout.StackPane +import scalafx.scene.paint.Color +import scalafx.scene.shape.Rectangle +import scalafx.Includes._ +import scala.concurrent.duration._ +import cats.effect.concurrent.Deferred +import monix.bio.Fiber +import wow.doge.mygame.launcher.Launcher.LauncherResult +import scalafx.scene.control.TextArea +import com.jayfella.jme.jfx.JavaFxUI +import wow.doge.mygame.utils.GenericConsoleStream +import java.io.PrintStream class MainApp( logger: Logger[Task], gameApp: GameApp2, spawnProtocol: ActorSystem[SpawnProtocol.Command], - jmeThread: monix.execution.Scheduler + jmeThread: monix.execution.Scheduler, + schedulers: Schedulers, + consoleStream: GenericConsoleStream[TextArea] )(implicit @annotation.unused timeout: Timeout, @annotation.unused scheduler: Scheduler @@ -46,39 +66,96 @@ class MainApp( lazy val scriptSystemInit = new ScriptSystemResource(os.pwd, spawnProtocol, ScriptInitMode.Eager).init - lazy val gameInit: Task[Unit] = for { - eventsModule <- Task(new EventsModule2(spawnProtocol)) - playerMovementEventBus <- eventsModule.playerMovementEventBusTask - playerCameraEventBus <- eventsModule.playerCameraEventBusTask - gameAppFib <- gameApp.start.executeOn(jmeThread).start - /** - * schedule a fiber to run on the JME thread and wait for it's completion - * before proceeding forward, as a signal that JME thread has been - * initialized, otherwise we'll get NPEs trying to access the fields - * of the game app - */ - initFib <- gameApp.enqueueL(() => Task("done")).start - _ <- initFib.join - inputManager <- gameApp.inputManager - assetManager <- gameApp.assetManager - stateManager <- gameApp.stateManager - camera <- gameApp.camera - rootNode <- gameApp.rootNode - enqueueR <- Task(gameApp.enqueue _) - viewPort <- gameApp.viewPort - bulletAppState <- Task(new BulletAppState()) - appScheduler <- Task(gameApp.scheduler) - // enqueueL <- Task(gameApp.enqueueL _) - _ <- wire[MainAppDelegate].init(gameApp.scheduler) - _ <- gameAppFib.join - } yield () + def gameInit: Task[Fiber[Throwable, Unit]] = + for { + eventsModule <- Task(new EventsModule2(spawnProtocol)) + playerMovementEventBus <- eventsModule.playerMovementEventBusTask + playerCameraEventBus <- eventsModule.playerCameraEventBusTask + gameAppFib <- gameApp.start.executeOn(jmeThread).start + /** + * schedule a task to run on the JME thread and wait for it's completion + * before proceeding forward, as a signal that JME thread has been + * initialized, otherwise we'll get NPEs trying to access the fields + * of the game app + */ + res <- gameApp.enqueueL(() => Task("done")).flatten + + _ <- logger.info(s"Result = $res") + inputManager <- gameApp.inputManager + assetManager <- gameApp.assetManager + stateManager <- gameApp.stateManager + camera <- gameApp.camera + rootNode <- gameApp.rootNode + enqueueR <- Task(gameApp.enqueue _) + viewPort <- gameApp.viewPort + _ <- logger.info("before") + // jfxUI <- Task(JavaFxUI.initialize(gameApp.app)) + // .executeOn(gameApp.scheduler) >> Task.sleep(500.millis) >> Task( + // JavaFxUI.getInstance() + // ) + // .start + jfxUI <- gameApp.jfxUI + consoleTextArea <- Task(new TextArea { + text = "hello" + editable = false + wrapText = true + // maxHeight = 150 + // maxWidth = 300 + }) + _ <- Task(consoleStream := consoleTextArea) + _ <- Task(jfxUI += consoleTextArea) + // consoleStream <- Task( + // GenericConsoleStream.textAreaStream(consoleTextArea) + // ) + _ <- logger.info("after") + bulletAppState <- Task(new BulletAppState()) + _ <- Task(stateManager.attach(bulletAppState)) + _ <- logger.info("Initializing console stream") + // _ <- Task(GenericConsoleStream.textAreaStream(consoleTextArea)).bracket( + // consoleStream => + // Task { System.setOut(consoleStream) } >> wire[MainAppDelegate] + // .init(gameApp.scheduler, consoleStream) + // // consoleLogger + // // Console.withOut(consoleStream)( + // // wire[MainAppDelegate].init(gameApp.scheduler, consoleStream) + // // ) + // )(stream => Task(stream.close()).onErrorHandle(_.printStackTrace())) + // _ <- Task { System.setOut(new PrintStream(consoleStream, true)) } + // _ <- Task { + // Console.withOut(consoleStream)(println("hello")) + // } + _ <- wire[MainAppDelegate].init(gameApp.scheduler) + } yield (gameAppFib) lazy val program = for { scriptSystem <- scriptSystemInit - game <- gameInit + /** + * Signal for synchronization between the JavaFX launcher and the in-game JavaFX GUI + * Without this, we get a "Toolkit already initialized" excResult. The launch button + * in the launcher completes the signal. The game init process which listens for this + * signal can then continue + */ + launchSignal <- Deferred[Task, Launcher.LauncherResult] + launcher <- new Launcher.Props(schedulers, launchSignal).create + cancelToken <- launcher.init() + launchResult <- launchSignal.get + _ <- cancelToken.cancel + _ <- + if (launchResult == LauncherResult.Exit) + logger.info("Exiting") + else gameInit.flatMap(_.join) + // _ <- Task.sleep(2000.millis) + // gameAppFib <- gameInit + /** + * Wait for game window to close + */ + // _ <- gameAppFib.join } yield () } +/** + * Class with all dependencies in one place for easy wiring + */ class MainAppDelegate( gameApp: GameApp2, spawnProtocol: ActorSystem[SpawnProtocol.Command], @@ -101,16 +178,20 @@ class MainAppDelegate( @annotation.unused scheduler: Scheduler ) { - def init(appScheduler: monix.execution.Scheduler) = + def init( + appScheduler: monix.execution.Scheduler + // consoleStream: GenericConsoleStream[TextArea] + ) = for { _ <- loggerL.info("Initializing Systems") - _ <- Task(stateManager.attach(bulletAppState)) _ <- Task( assetManager.registerLocator( (os.rel / "assets" / "town.zip"), classOf[ZipLocator] ) ) + _ <- loggerL.info("test hmm") + // _ <- Task(consoleStream.println("text")) _ <- DefaultGameLevel(assetManager, viewPort) .addToGame( rootNode, @@ -131,7 +212,7 @@ class MainAppDelegate( @annotation.unused val playerPos = ImVector3f.ZERO @annotation.unused - val playerNode = None.taggedWith[Player] + val playerNode = None.taggedWith[PlayerTag] @annotation.unused val modelPath = os.rel / "Models" / "Jaime" / "Jaime.j3o" wire[PlayerController.Props].create diff --git a/src/main/scala/wow/doge/mygame/game/GameApp.scala b/src/main/scala/wow/doge/mygame/game/GameApp.scala index b364403..fc29785 100644 --- a/src/main/scala/wow/doge/mygame/game/GameApp.scala +++ b/src/main/scala/wow/doge/mygame/game/GameApp.scala @@ -16,81 +16,26 @@ import wow.doge.mygame.executors.GUIExecutorService import wow.doge.mygame.executors.Schedulers class GameApp( - // actorSystem: ActorSystem[SpawnProtocol.Command], schedulers: Schedulers, appStates: AppState* ) extends SimpleApplication(appStates: _*) { import GameApp._ - // implicit val timeout = Timeout(10.seconds) - // implicit val scheduler = actorSystem.scheduler - // private lazy val taskQueueS = new ConcurrentLinkedQueue[MyTask[_]]() + /** + * A non blocking synchronized queue using an immutable scala queue and monix's atomic class + */ private lazy val taskQueue2 = Atomic(Queue.empty[MyTask[_]]) private val tickSubject = ConcurrentSubject[Float](multicast = MulticastStrategy.publish)( schedulers.async ) - // (scheduler) - - override def simpleInitApp(): Unit = { - - println("gameapp" + Thread.currentThread().getName()) - - // val ship = ed.createEntity() - // val mbState = stateManager().state[EntityDataState]().map(_.getEntityData()) - // val mbState = Try( - // stateManager() - // .state[TestAppState]() - // .entity - // ).toOption.flatten.toRight(println("empty")) - // // .flatMap(_.entity) - // val x = mbState.flatMap( - // _.query - // .filter[TestComponent]("name", new Object()) - // // .filterOr[TestEntity]( - // // Filters - // // .fieldEquals(classOf[TestEntity], "", null) - // // ) - // .component[Tag]() - // .component[TestComponent]() - // .result - // .toRight(println("failed")) - // ) - - // rootNode - // .child(geom) - // .child(geom) - // .child(geom) - // .child(geom) - // .child(geom) - // .child(geom) - // .child(geom) - // .child(geom) - // Future(println("hello"))(jmeEC(this)) - // val wbActor: Future[ActorRef[Greeter.Greet]] = actorSystem.ask( - // SpawnProtocol.Spawn( - // behavior = Greeter(), - // name = "listener", - // DispatcherSelector.fromConfig("jme-dispatcher"), - // _ - // ) - // ) - - // wbActor.map(a => a.ask(Greeter.Greet("hello", _)).map(println)) - - } def tickObservable: Observable[Float] = tickSubject - override def simpleUpdate(tpf: Float): Unit = { - // val rot2 = rot.fromAngleAxis(FastMath.PI, new Vector3f(0, 0, 1)) - // val rotation = geom.getLocalRotation() - // rotation.add(rot2) - // geom.rotate(rot2) + override def simpleInitApp(): Unit = {} - // geom.updateModelBound() - // geom.updateGeometricState() + override def simpleUpdate(tpf: Float): Unit = { tickSubject.onNext(tpf) } @@ -101,32 +46,14 @@ class GameApp( def enqueueScala[T](cb: () => T): CancelableFuture[T] = { val p = Promise[T]() - // p.success(cb()) - // taskQueueS.add(MyTask(p, cb)) taskQueue2.transform(_ :+ MyTask(p, cb)) p.future } -// taskQueue2.transform(_ :+ MyTask(p, cb)) -// p + def enqueueL[T](cb: () => T): Task[T] = - // Task(Promise[T]()).flatMap { p => - // Task(taskQueue2.transform(_ :+ MyTask(p, cb))) >> - // Task.fromCancelablePromise(p) - // } - // Task.fromCancelablePromise { - // val p = Promise[T]() - // taskQueue2.transform(_ :+ MyTask(p, cb)) - // p - // } Task.deferFuture(enqueueScala(cb)) - // taskQueueS.add(MyTask(p, cb)) - override protected def runQueuedTasks(): Unit = { - // Option(taskQueueS.poll()).foreach { - // case MyTask(p, cb) => - // p.success(cb()) - // } taskQueue2.transform { current => current.dequeueOption.fold(current) { case (MyTask(p, cb), updated) => @@ -139,8 +66,9 @@ class GameApp( } object JMEExecutorService extends GUIExecutorService { - override def execute(command: Runnable) = - enqueue(command) + override def execute(command: Runnable): Unit = + enqueueScala(() => command.run()) + // enqueue(command) // new SingleThreadEventExecutor() // sys.addShutdownHook(JMEExecutorService.shutdown()) } @@ -150,3 +78,55 @@ class GameApp( object GameApp { private[game] case class MyTask[T](p: Promise[T], cb: () => T) } + +// val ship = ed.createEntity() +// val mbState = stateManager().state[EntityDataState]().map(_.getEntityData()) +// val mbState = Try( +// stateManager() +// .state[TestAppState]() +// .entity +// ).toOption.flatten.toRight(println("empty")) +// // .flatMap(_.entity) +// val x = mbState.flatMap( +// _.query +// .filter[TestComponent]("name", new Object()) +// // .filterOr[TestEntity]( +// // Filters +// // .fieldEquals(classOf[TestEntity], "", null) +// // ) +// .component[Tag]() +// .component[TestComponent]() +// .result +// .toRight(println("failed")) +// ) + +// rootNode +// .child(geom) +// .child(geom) +// .child(geom) +// .child(geom) +// .child(geom) +// .child(geom) +// .child(geom) +// .child(geom) +// Future(println("hello"))(jmeEC(this)) +// val wbActor: Future[ActorRef[Greeter.Greet]] = actorSystem.ask( +// SpawnProtocol.Spawn( +// behavior = Greeter(), +// name = "listener", +// DispatcherSelector.fromConfig("jme-dispatcher"), +// _ +// ) +// ) + +// wbActor.map(a => a.ask(Greeter.Greet("hello", _)).map(println)) + +// Task(Promise[T]()).flatMap { p => +// Task(taskQueue2.transform(_ :+ MyTask(p, cb))) >> +// Task.fromCancelablePromise(p) +// } +// Task.fromCancelablePromise { +// val p = Promise[T]() +// taskQueue2.transform(_ :+ MyTask(p, cb)) +// p +// } diff --git a/src/main/scala/wow/doge/mygame/game/GameApp2.scala b/src/main/scala/wow/doge/mygame/game/GameApp2.scala index e3c231f..54a9f28 100644 --- a/src/main/scala/wow/doge/mygame/game/GameApp2.scala +++ b/src/main/scala/wow/doge/mygame/game/GameApp2.scala @@ -6,11 +6,16 @@ import com.jme3.asset.AssetManager import com.jme3.input.InputManager import monix.bio.IO import monix.bio.Task +import com.jme3.scene.Node +import monix.catnap.Semaphore +import com.jme3.scene.Spatial +import wow.doge.mygame.game.GameApp2.SynchedObject +import wow.doge.mygame.game.subsystems.ui.JFxUI sealed trait Error case object FlyCamNotExists extends Error -class GameApp2(app: GameApp) { +class GameApp2(val app: GameApp) { def stateManager: Task[AppStateManager] = Task(app.getStateManager()) def inputManager: Task[InputManager] = Task(app.getInputManager()) def assetManager: Task[AssetManager] = Task(app.getAssetManager()) @@ -22,6 +27,7 @@ class GameApp2(app: GameApp) { def camera = Task(app.getCamera()) def viewPort = Task(app.getViewPort()) def rootNode = Ref[Task].of(app.getRootNode()) + def rootNode2 = SynchedObject(app.getRootNode()) def enqueue(cb: () => Unit) = app.enqueue(new Runnable { override def run() = cb() @@ -31,5 +37,35 @@ class GameApp2(app: GameApp) { def start = Task(app.start()) def stop = Task(app.stop()) def scheduler = app.scheduler + def jfxUI = JFxUI(app) } + +object GameApp2 { + + class WrappedNode(node: Node, lock: Semaphore[Task]) { + + def +=(spat: Spatial) = lock.withPermit(Task(node.attachChild(spat))) + } + + /** + * Synchronization wrapper for a mutable object + * + * @param obj the mutable object + * @param lock lock for synchronization + */ + class SynchedObject[A](obj: A, lock: Semaphore[Task]) { + def modify(f: A => Unit): Task[Unit] = + lock.withPermit(Task(f(obj))) + + def flatModify(f: A => Task[Unit]): Task[Unit] = + lock.withPermit(f(obj)) + + def get: Task[A] = lock.withPermit(Task(obj)) + } + + object SynchedObject { + def apply[A](obj: A) = + Semaphore[Task](1).flatMap(lock => Task(new SynchedObject(obj, lock))) + } +} diff --git a/src/main/scala/wow/doge/mygame/game/GameModule.scala b/src/main/scala/wow/doge/mygame/game/GameModule.scala index 7aabcdc..c7378e2 100644 --- a/src/main/scala/wow/doge/mygame/game/GameModule.scala +++ b/src/main/scala/wow/doge/mygame/game/GameModule.scala @@ -17,7 +17,7 @@ class GameAppResource( jmeScheduler: Scheduler, schedulers: Schedulers ) { - def get: Resource[Task, (GameApp2, Fiber[Throwable, Unit])] = + def get2: Resource[Task, (GameApp2, Fiber[Throwable, Unit])] = Resource.make( for { _ <- logger.info("Creating game app") @@ -37,7 +37,7 @@ class GameAppResource( } yield (app2 -> fib) )(logger.info("Closing game app") >> _._2.cancel) - def get2: Resource[Task, GameApp2] = + def get: Resource[Task, GameApp2] = Resource.make( for { _ <- logger.info("Creating game app") @@ -45,9 +45,12 @@ class GameAppResource( app2 <- Task { val settings = new AppSettings(true) settings.setVSync(true) - settings.setUseInput(true) - // new FlyCamAppState - // settings.setFrameRate(250) + + /** + * disables the launcher + * We'll be making our own launcher anyway + */ + app.setShowSettings(false) app.setSettings(settings) // JMERunner.runner = app new GameApp2(app) diff --git a/src/main/scala/wow/doge/mygame/game/GameSystemsInitializer.scala b/src/main/scala/wow/doge/mygame/game/GameSystemsInitializer.scala index 90f1dc7..ead2fc0 100644 --- a/src/main/scala/wow/doge/mygame/game/GameSystemsInitializer.scala +++ b/src/main/scala/wow/doge/mygame/game/GameSystemsInitializer.scala @@ -22,7 +22,7 @@ import monix.bio.IO import monix.bio.Task import monix.reactive.Consumer import wow.doge.mygame.events.EventBus -import wow.doge.mygame.game.nodes.Player +import wow.doge.mygame.game.nodes.PlayerTag import wow.doge.mygame.game.nodes.PlayerController import wow.doge.mygame.game.subsystems.input.GameInputHandler import wow.doge.mygame.implicits._ @@ -86,7 +86,7 @@ class GameSystemsInitializer( @annotation.unused val playerPos = ImVector3f.ZERO @annotation.unused - val playerNode = None.taggedWith[Player] + val playerNode = None.taggedWith[PlayerTag] @annotation.unused val modelPath = os.rel / "Models" / "Jaime" / "Jaime.j3o" wire[PlayerController.Props].create diff --git a/src/main/scala/wow/doge/mygame/game/nodes/PlayerController.scala b/src/main/scala/wow/doge/mygame/game/nodes/PlayerController.scala index 7a69d92..2dcc339 100644 --- a/src/main/scala/wow/doge/mygame/game/nodes/PlayerController.scala +++ b/src/main/scala/wow/doge/mygame/game/nodes/PlayerController.scala @@ -32,7 +32,7 @@ import wow.doge.mygame.subsystems.movement.ImMovementActor import wow.doge.mygame.utils.AkkaUtils // class PlayerNode(val name: String) extends Node(name) {} -sealed trait Player +sealed trait PlayerTag sealed trait PlayerCameraNode object PlayerController { @@ -54,7 +54,7 @@ object PlayerController { ], playerCameraEventBus: ActorRef[EventBus.Command[PlayerCameraEvent]], _playerPhysicsControl: Option[BetterCharacterControl], - _playerNode: Option[Node with Player] = None, + _playerNode: Option[Node with PlayerTag] = None, _cameraNode: Option[CameraNode with PlayerCameraNode] = None, appScheduler: monix.execution.Scheduler )(implicit timeout: Timeout, scheduler: Scheduler) { @@ -68,7 +68,7 @@ object PlayerController { playerPhysicsControl <- IO( _playerPhysicsControl .getOrElse(defaultPlayerPhysicsControl) - .taggedWith[Player] + .taggedWith[PlayerTag] ) playerNode <- IO.fromEither( _playerNode.fold( @@ -179,7 +179,7 @@ object Methods { def spawnMovementActor( enqueueR: Function1[() => Unit, Unit], spawnProtocol: ActorRef[SpawnProtocol.Command], - movable: BetterCharacterControl @@ Player, + movable: BetterCharacterControl @@ PlayerTag, playerMovementEventBus: ActorRef[ EventBus.Command[PlayerMovementEvent] ], diff --git a/src/main/scala/wow/doge/mygame/game/nodes/PlayerEventListeners.scala b/src/main/scala/wow/doge/mygame/game/nodes/PlayerEventListeners.scala index 1b9f7f6..2b49910 100644 --- a/src/main/scala/wow/doge/mygame/game/nodes/PlayerEventListeners.scala +++ b/src/main/scala/wow/doge/mygame/game/nodes/PlayerEventListeners.scala @@ -22,7 +22,7 @@ object PlayerMovementEventListener { Logger[PlayerMovementEventListener.type].underlying ), Behaviors.setup[PlayerMovementEvent](ctx => - Behaviors.receiveMessage { + Behaviors.receiveMessagePartial { case PlayerMovedLeft(pressed) => movementActor ! ImMovementActor.MovedLeft(pressed) Behaviors.same @@ -69,7 +69,7 @@ object PlayerCameraEventListener { Logger[PlayerCameraEventListener.type].underlying ), Behaviors.setup[PlayerCameraEvent](ctx => - Behaviors.receiveMessage { + Behaviors.receiveMessagePartial { case CameraMovedUp => enqueueR(() => { diff --git a/src/main/scala/wow/doge/mygame/game/subsystems/ai/GdxAiTest.scala b/src/main/scala/wow/doge/mygame/game/subsystems/ai/GdxAiTest.scala new file mode 100644 index 0000000..124a72d --- /dev/null +++ b/src/main/scala/wow/doge/mygame/game/subsystems/ai/GdxAiTest.scala @@ -0,0 +1,33 @@ +package wow.doge.mygame.game.subsystems.ai + +import com.badlogic.gdx.ai.pfa.Connection +import wow.doge.mygame.game.subsystems.ai.gdx.MyIndexedGraph +import scala.collection.immutable.ArraySeq +// import com.badlogic.gdx.ai.pfa.indexed.IndexedGraph +// import scala.jdk.javaapi.CollectionConverters._ + +case class City(x: Float, y: Float, name: String, index: Int) +case class Street(fromNode: City, toNode: City, cost: Float) + extends Connection[City] { + + override def getCost(): Float = cost + + override def getFromNode(): City = fromNode + + override def getToNode(): City = toNode + +} + +class CityGraph extends MyIndexedGraph[City] { + + override def getConnections( + city: City + ): IndexedSeq[Connection[City]] = + ArraySeq(Street(City(0f, 0f, "egw", 0), City(0f, 0f, "egw", 0), 1)) + // or Vector(Street(City(0f, 0f, "egw", 0), City(0f, 0f, "egw", 0), 1)) + + override def getIndex(city: City): Int = ??? + + override def getNodeCount(): Int = ??? + +} diff --git a/src/main/scala/wow/doge/mygame/game/subsystems/ai/gdx/Graph.java b/src/main/scala/wow/doge/mygame/game/subsystems/ai/gdx/Graph.java new file mode 100644 index 0000000..e62df12 --- /dev/null +++ b/src/main/scala/wow/doge/mygame/game/subsystems/ai/gdx/Graph.java @@ -0,0 +1,43 @@ +/******************************************************************************* + * Copyright 2014 See AUTHORS file. + * + * 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 + * + * http://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. + ******************************************************************************/ + +package wow.doge.mygame.game.subsystems.ai.gdx; + +import com.badlogic.gdx.ai.pfa.Connection; + +import com.badlogic.gdx.utils.Array; +// import java.lang.Iterable; +import java.util.List; +import scala.collection.immutable.IndexedSeq; + +/** + * A graph is a collection of nodes, each one having a collection of outgoing + * {@link Connection connections}. + * + * @param Type of node + * + * @author davebaol + */ +public interface Graph { + + /** + * Returns the connections outgoing from the given node. + * + * @param fromNode the node whose outgoing connections will be returned + * @return the array of connections outgoing from the given node. + */ + public IndexedSeq> getConnections(N fromNode); +} diff --git a/src/main/scala/wow/doge/mygame/game/subsystems/ai/gdx/IndexedAStarPathFinder.java b/src/main/scala/wow/doge/mygame/game/subsystems/ai/gdx/IndexedAStarPathFinder.java new file mode 100644 index 0000000..85e7e8a --- /dev/null +++ b/src/main/scala/wow/doge/mygame/game/subsystems/ai/gdx/IndexedAStarPathFinder.java @@ -0,0 +1,371 @@ +/******************************************************************************* + * Copyright 2014 See AUTHORS file. + * + * 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 + * + * http://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. + ******************************************************************************/ + +package wow.doge.mygame.game.subsystems.ai.gdx; + +import com.badlogic.gdx.ai.pfa.Connection; +import com.badlogic.gdx.ai.pfa.GraphPath; +import com.badlogic.gdx.ai.pfa.Heuristic; +import com.badlogic.gdx.ai.pfa.PathFinder; +import com.badlogic.gdx.ai.pfa.PathFinderQueue; +import com.badlogic.gdx.ai.pfa.PathFinderRequest; +import com.badlogic.gdx.utils.Array; +import com.badlogic.gdx.utils.BinaryHeap; +import com.badlogic.gdx.utils.TimeUtils; +import java.util.List; +import wow.doge.mygame.game.subsystems.ai.gdx.Graph; +import wow.doge.mygame.game.subsystems.ai.gdx.MyIndexedGraph; +import scala.collection.immutable.IndexedSeq; + +/** + * A fully implemented {@link PathFinder} that can perform both interruptible + * and non-interruptible pathfinding. + *

+ * This implementation is a common variation of the A* algorithm that is faster + * than the general A*. + *

+ * In the general A* implementation, data are held for each node in the open or + * closed lists, and these data are held as a NodeRecord instance. Records are + * created when a node is first considered and then moved between the open and + * closed lists, as required. There is a key step in the algorithm where the + * lists are searched for a node record corresponding to a particular node. This + * operation is something time-consuming. + *

+ * The indexed A* algorithm improves execution speed by using an array of all + * the node records for every node in the graph. Nodes must be numbered using + * sequential integers (see {@link MyIndexedGraph#getIndex(Object)}), so we + * don't need to search for a node in the two lists at all. We can simply use + * the node index to look up its record in the array (creating it if it is + * missing). This means that the close list is no longer needed. To know whether + * a node is open or closed, we use the {@link NodeRecord#category category} of + * the node record. This makes the search step very fast indeed (in fact, there + * is no search, and we can go straight to the information we need). + * Unfortunately, we can't get rid of the open list because we still need to be + * able to retrieve the element with the lowest cost. However, we use a + * {@link BinaryHeap} for the open list in order to keep performance as high as + * possible. + * + * @param Type of node + * + * @author davebaol + */ +public class IndexedAStarPathFinder implements PathFinder { + MyIndexedGraph graph; + NodeRecord[] nodeRecords; + BinaryHeap> openList; + NodeRecord current; + public Metrics metrics; + + /** The unique ID for each search run. Used to mark nodes. */ + private int searchId; + + private static final int UNVISITED = 0; + private static final int OPEN = 1; + private static final int CLOSED = 2; + + public IndexedAStarPathFinder(MyIndexedGraph graph) { + this(graph, false); + } + + @SuppressWarnings("unchecked") + public IndexedAStarPathFinder(MyIndexedGraph graph, boolean calculateMetrics) { + this.graph = graph; + this.nodeRecords = (NodeRecord[]) new NodeRecord[graph.getNodeCount()]; + this.openList = new BinaryHeap>(); + if (calculateMetrics) + this.metrics = new Metrics(); + } + + @Override + public boolean searchConnectionPath(N startNode, N endNode, Heuristic heuristic, + GraphPath> outPath) { + + // Perform AStar + boolean found = search(startNode, endNode, heuristic); + + if (found) { + // Create a path made of connections + generateConnectionPath(startNode, outPath); + } + + return found; + } + + @Override + public boolean searchNodePath(N startNode, N endNode, Heuristic heuristic, GraphPath outPath) { + + // Perform AStar + boolean found = search(startNode, endNode, heuristic); + + if (found) { + // Create a path made of nodes + generateNodePath(startNode, outPath); + } + + return found; + } + + protected boolean search(N startNode, N endNode, Heuristic heuristic) { + + initSearch(startNode, endNode, heuristic); + + // Iterate through processing each node + do { + // Retrieve the node with smallest estimated total cost from the open list + current = openList.pop(); + current.category = CLOSED; + + // Terminate if we reached the goal node + if (current.node == endNode) + return true; + + visitChildren(endNode, heuristic); + + } while (openList.size > 0); + + // We've run out of nodes without finding the goal, so there's no solution + return false; + } + + @Override + public boolean search(PathFinderRequest request, long timeToRun) { + + long lastTime = TimeUtils.nanoTime(); + + // We have to initialize the search if the status has just changed + if (request.statusChanged) { + initSearch(request.startNode, request.endNode, request.heuristic); + request.statusChanged = false; + } + + // Iterate through processing each node + do { + + // Check the available time + long currentTime = TimeUtils.nanoTime(); + timeToRun -= currentTime - lastTime; + if (timeToRun <= PathFinderQueue.TIME_TOLERANCE) + return false; + + // Retrieve the node with smallest estimated total cost from the open list + current = openList.pop(); + current.category = CLOSED; + + // Terminate if we reached the goal node; we've found a path. + if (current.node == request.endNode) { + request.pathFound = true; + + generateNodePath(request.startNode, request.resultPath); + + return true; + } + + // Visit current node's children + visitChildren(request.endNode, request.heuristic); + + // Store the current time + lastTime = currentTime; + + } while (openList.size > 0); + + // The open list is empty and we've not found a path. + request.pathFound = false; + return true; + } + + protected void initSearch(N startNode, N endNode, Heuristic heuristic) { + if (metrics != null) + metrics.reset(); + + // Increment the search id + if (++searchId < 0) + searchId = 1; + + // Initialize the open list + openList.clear(); + + // Initialize the record for the start node and add it to the open list + NodeRecord startRecord = getNodeRecord(startNode); + startRecord.node = startNode; + startRecord.connection = null; + startRecord.costSoFar = 0; + addToOpenList(startRecord, heuristic.estimate(startNode, endNode)); + + current = null; + } + + protected void visitChildren(N endNode, Heuristic heuristic) { + // Get current node's outgoing connections + IndexedSeq> connections = graph.getConnections(current.node); + + // Loop through each connection in turn + for (int i = 0; i < connections.size(); i++) { + if (metrics != null) + metrics.visitedNodes++; + + Connection connection = connections.apply(i); + + // Get the cost estimate for the node + N node = connection.getToNode(); + float nodeCost = current.costSoFar + connection.getCost(); + + float nodeHeuristic; + NodeRecord nodeRecord = getNodeRecord(node); + if (nodeRecord.category == CLOSED) { // The node is closed + + // If we didn't find a shorter route, skip + if (nodeRecord.costSoFar <= nodeCost) + continue; + + // We can use the node's old cost values to calculate its heuristic + // without calling the possibly expensive heuristic function + nodeHeuristic = nodeRecord.getEstimatedTotalCost() - nodeRecord.costSoFar; + } else if (nodeRecord.category == OPEN) { // The node is open + + // If our route is no better, then skip + if (nodeRecord.costSoFar <= nodeCost) + continue; + + // Remove it from the open list (it will be re-added with the new cost) + openList.remove(nodeRecord); + + // We can use the node's old cost values to calculate its heuristic + // without calling the possibly expensive heuristic function + nodeHeuristic = nodeRecord.getEstimatedTotalCost() - nodeRecord.costSoFar; + } else { // the node is unvisited + + // We'll need to calculate the heuristic value using the function, + // since we don't have a node record with a previously calculated value + nodeHeuristic = heuristic.estimate(node, endNode); + } + + // Update node record's cost and connection + nodeRecord.costSoFar = nodeCost; + nodeRecord.connection = connection; + + // Add it to the open list with the estimated total cost + addToOpenList(nodeRecord, nodeCost + nodeHeuristic); + } + + } + + protected void generateConnectionPath(N startNode, GraphPath> outPath) { + + // Work back along the path, accumulating connections + // outPath.clear(); + while (current.node != startNode) { + outPath.add(current.connection); + current = nodeRecords[graph.getIndex(current.connection.getFromNode())]; + } + + // Reverse the path + outPath.reverse(); + } + + protected void generateNodePath(N startNode, GraphPath outPath) { + + // Work back along the path, accumulating nodes + // outPath.clear(); + while (current.connection != null) { + outPath.add(current.node); + current = nodeRecords[graph.getIndex(current.connection.getFromNode())]; + } + outPath.add(startNode); + + // Reverse the path + outPath.reverse(); + } + + protected void addToOpenList(NodeRecord nodeRecord, float estimatedTotalCost) { + openList.add(nodeRecord, estimatedTotalCost); + nodeRecord.category = OPEN; + if (metrics != null) { + metrics.openListAdditions++; + metrics.openListPeak = Math.max(metrics.openListPeak, openList.size); + } + } + + protected NodeRecord getNodeRecord(N node) { + int index = graph.getIndex(node); + NodeRecord nr = nodeRecords[index]; + if (nr != null) { + if (nr.searchId != searchId) { + nr.category = UNVISITED; + nr.searchId = searchId; + } + return nr; + } + nr = nodeRecords[index] = new NodeRecord(); + nr.node = node; + nr.searchId = searchId; + return nr; + } + + /** + * This nested class is used to keep track of the information we need for each + * node during the search. + * + * @param Type of node + * + * @author davebaol + */ + static class NodeRecord extends BinaryHeap.Node { + /** The reference to the node. */ + N node; + + /** The incoming connection to the node */ + Connection connection; + + /** The actual cost from the start node. */ + float costSoFar; + + /** The node category: {@link #UNVISITED}, {@link #OPEN} or {@link #CLOSED}. */ + int category; + + /** ID of the current search. */ + int searchId; + + /** Creates a {@code NodeRecord}. */ + public NodeRecord() { + super(0); + } + + /** Returns the estimated total cost. */ + public float getEstimatedTotalCost() { + return getValue(); + } + } + + /** + * A class used by {@link IndexedAStarPathFinder} to collect search metrics. + * + * @author davebaol + */ + public static class Metrics { + public int visitedNodes; + public int openListAdditions; + public int openListPeak; + + public Metrics() { + } + + public void reset() { + visitedNodes = 0; + openListAdditions = 0; + openListPeak = 0; + } + } +} diff --git a/src/main/scala/wow/doge/mygame/game/subsystems/ai/gdx/MyIndexedGraph.java b/src/main/scala/wow/doge/mygame/game/subsystems/ai/gdx/MyIndexedGraph.java new file mode 100644 index 0000000..652700b --- /dev/null +++ b/src/main/scala/wow/doge/mygame/game/subsystems/ai/gdx/MyIndexedGraph.java @@ -0,0 +1,41 @@ +/******************************************************************************* + * Copyright 2014 See AUTHORS file. + * + * 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 + * + * http://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. + ******************************************************************************/ + +package wow.doge.mygame.game.subsystems.ai.gdx; + +import wow.doge.mygame.game.subsystems.ai.gdx.Graph; + +/** + * A graph for the {@link IndexedAStarPathFinder}. + * + * @param Type of node + * + * @author davebaol + */ +public interface MyIndexedGraph extends Graph { + + /** + * Returns the unique index of the given node. + * + * @param node the node whose index will be returned + * @return the unique index of the given node. + */ + public int getIndex(N node); + + /** Returns the number of nodes in this graph. */ + public int getNodeCount(); + +} diff --git a/src/main/scala/wow/doge/mygame/game/subsystems/input/GameInputHandler.scala b/src/main/scala/wow/doge/mygame/game/subsystems/input/GameInputHandler.scala index ca82781..b8457d7 100644 --- a/src/main/scala/wow/doge/mygame/game/subsystems/input/GameInputHandler.scala +++ b/src/main/scala/wow/doge/mygame/game/subsystems/input/GameInputHandler.scala @@ -65,30 +65,6 @@ object GameInputHandler { def setupKeys(inputManager: InputManager) = inputManager - // .withMapping( - // PlayerMovementInput.WalkLeft.entryName, - // new KeyTrigger(KeyInput.KEY_A) - // // new KeyTrigger(KeyInput.KEY_LEFT) - // ) - // .withMapping( - // PlayerMovementInput.WalkRight.entryName, - // new KeyTrigger(KeyInput.KEY_D) - // // new KeyTrigger(KeyInput.KEY_RIGHT) - // ) - // .withMapping( - // PlayerMovementInput.WalkForward.entryName, - // new KeyTrigger(KeyInput.KEY_W) - // // new KeyTrigger(KeyInput.KEY_UP) - // ) - // .withMapping( - // PlayerMovementInput.WalkBackward.entryName, - // new KeyTrigger(KeyInput.KEY_S) - // // new KeyTrigger(KeyInput.KEY_DOWN) - // ) - // .withMapping( - // PlayerMovementInput.Jump.entryName, - // new KeyTrigger(KeyInput.KEY_SPACE) - // ) .withMapping( PlayerAnalogInput.TurnRight.entryName, new KeyTrigger(KeyInput.KEY_RIGHT), diff --git a/src/main/scala/wow/doge/mygame/game/subsystems/level/DefaultGameLevel.scala b/src/main/scala/wow/doge/mygame/game/subsystems/level/DefaultGameLevel.scala index fb55178..d1eeacc 100644 --- a/src/main/scala/wow/doge/mygame/game/subsystems/level/DefaultGameLevel.scala +++ b/src/main/scala/wow/doge/mygame/game/subsystems/level/DefaultGameLevel.scala @@ -17,8 +17,8 @@ object DefaultGameLevel { lazy val sceneModel: Spatial = assetManager.loadModel("main.scene") lazy val sceneShape = CollisionShapeFactory.createMeshShape( sceneModel.toNode match { - case util.Right(node) => node - case util.Left(ex) => + case Right(node) => node + case Left(ex) => throw new NotImplementedError("No fallback sceneshape") } ) @@ -40,7 +40,7 @@ object DefaultGameLevel { dl.setColor(ColorRGBA.White); dl.setDirection(new Vector3f(2.8f, -2.8f, -2.8f).normalizeLocal()); // app.rootNode.addLight(dl); - new Level( + new GameLevel( model = sceneModel, physicsControl = landscape, ambientLight = al, diff --git a/src/main/scala/wow/doge/mygame/game/subsystems/level/Level.scala b/src/main/scala/wow/doge/mygame/game/subsystems/level/GameLevel.scala similarity index 79% rename from src/main/scala/wow/doge/mygame/game/subsystems/level/Level.scala rename to src/main/scala/wow/doge/mygame/game/subsystems/level/GameLevel.scala index a5ca94e..c260071 100644 --- a/src/main/scala/wow/doge/mygame/game/subsystems/level/Level.scala +++ b/src/main/scala/wow/doge/mygame/game/subsystems/level/GameLevel.scala @@ -10,7 +10,7 @@ import monix.bio.Task import com.jme3.scene.Node import wow.doge.mygame.implicits._ -class Level( +class GameLevel( model: Spatial, physicsControl: RigidBodyControl, ambientLight: AmbientLight, @@ -19,14 +19,8 @@ class Level( def addToGame(rootNode: Ref[Task, Node], physicsSpace: PhysicsSpace) = { for { _ <- rootNode.update(_ :+ model) - _ <- rootNode.update { r => - r.addLight(ambientLight) - r - } - _ <- rootNode.update { r => - r.addLight(directionalLight) - r - } + _ <- rootNode.update(_ :+ ambientLight) + _ <- rootNode.update(_ :+ directionalLight) _ <- Task(physicsSpace += model) _ <- Task(physicsSpace += physicsControl) } yield () diff --git a/src/main/scala/wow/doge/mygame/game/subsystems/ui/JmeJfx.scala b/src/main/scala/wow/doge/mygame/game/subsystems/ui/JmeJfx.scala new file mode 100644 index 0000000..5bf3cfc --- /dev/null +++ b/src/main/scala/wow/doge/mygame/game/subsystems/ui/JmeJfx.scala @@ -0,0 +1,43 @@ +package wow.doge.mygame.game.subsystems.ui + +import com.jme3.app.Application +import com.jayfella.jme.jfx.JavaFxUI +import scalafx.application.Platform +import monix.execution.CancelablePromise +import monix.bio.Task +import cats.effect.concurrent.Deferred +import scala.concurrent.duration._ +import wow.doge.mygame.game.GameApp + +object JFxUI { + def apply(app: GameApp) = + Task(JavaFxUI.initialize(app)) + .executeOn(app.scheduler) >> Task.sleep(500.millis) >> Task( + JavaFxUI.getInstance() + ) + // Task { + // Platform.runLater(() => { + // println("here jfx") + // JavaFxUI.initialize(app) + // println("here2 jfx2") + // val inst = JavaFxUI.getInstance() + // println(inst) + // }) + // } + // Task.fromFuture { + // val p = CancelablePromise[JavaFxUI]() + // Platform.runLater(() => { + // println("here") + // JavaFxUI.initialize(app) + // println("here2") + // val inst = JavaFxUI.getInstance() + // println(inst) + // p.success(inst) + // }) + // p.future + // } +// for { +// d <- Deferred[Task, JavaFxUI] +// _ <- Task(JavaFxUI.initialize(app)) +// } yield () +} diff --git a/src/main/scala/wow/doge/mygame/implicits/JavaFXMonixObservables.scala b/src/main/scala/wow/doge/mygame/implicits/JavaFXMonixObservables.scala new file mode 100644 index 0000000..34b69d6 --- /dev/null +++ b/src/main/scala/wow/doge/mygame/implicits/JavaFXMonixObservables.scala @@ -0,0 +1,88 @@ +package wow.doge.mygame.implicits + +import javafx.{ + collections => jfxc, + event => jfxe, + geometry => jfxg, + scene => jfxs, + util => jfxu +} +import javafx.scene.{input => jfxsi, layout => jfxsl, paint => jfxsp} +import scalafx.scene.Scene +import monix.execution.Cancelable +import monix.reactive.OverflowStrategy +import monix.reactive.Observable +import monix.execution.Ack +import scalafx.scene.control.ButtonBase + +object JavaFXMonixObservables { + + implicit final class SceneObservables(private val scene: Scene) + extends AnyVal { + def observableMousePressed(): Observable[jfxsi.MouseEvent] = { + import monix.execution.cancelables.SingleAssignCancelable + Observable.create(OverflowStrategy.Unbounded) { sub => + val c = SingleAssignCancelable() + val l = new jfxe.EventHandler[jfxsi.MouseEvent] { + override def handle(event: jfxsi.MouseEvent): Unit = { + if (sub.onNext(event) == Ack.Stop) c.cancel() + } + } + + scene.onMousePressed = l + c := Cancelable(() => + scene.removeEventHandler( + jfxsi.MouseEvent.MOUSE_PRESSED, + l + ) + ) + c + } + } + def observableMouseDragged(): Observable[jfxsi.MouseEvent] = { + import monix.execution.cancelables.SingleAssignCancelable + Observable.create(OverflowStrategy.Unbounded) { sub => + val c = SingleAssignCancelable() + val l = new jfxe.EventHandler[jfxsi.MouseEvent] { + override def handle(event: jfxsi.MouseEvent): Unit = { + if (sub.onNext(event) == Ack.Stop) c.cancel() + } + } + + scene.onMouseDragged = l + c := Cancelable(() => + scene.removeEventHandler( + jfxsi.MouseEvent.MOUSE_DRAGGED, + l + ) + ) + c + } + } + } + + implicit final class OnActionObservable( + private val button: ButtonBase + ) extends AnyVal { + def observableAction(): Observable[jfxe.ActionEvent] = { + import monix.execution.cancelables.SingleAssignCancelable + Observable.create(OverflowStrategy.Unbounded) { sub => + val c = SingleAssignCancelable() + val l = new jfxe.EventHandler[jfxe.ActionEvent] { + override def handle(event: jfxe.ActionEvent): Unit = { + if (sub.onNext(event) == Ack.Stop) c.cancel() + } + } + + button.onAction = l + c := Cancelable(() => + button.removeEventHandler( + jfxe.ActionEvent.ACTION, + l + ) + ) + c + } + } + } +} diff --git a/src/main/scala/wow/doge/mygame/implicits/TestEnum.scala b/src/main/scala/wow/doge/mygame/implicits/TestEnum.scala deleted file mode 100644 index d568b2b..0000000 --- a/src/main/scala/wow/doge/mygame/implicits/TestEnum.scala +++ /dev/null @@ -1,43 +0,0 @@ -package wow.doge.mygame.implicits - -import enumeratum._ - -sealed trait TestEnum extends EnumEntry - -object TestEnum extends Enum[TestEnum] { - val values = findValues - case object Test2 extends TestEnum -} - -sealed trait Greeting extends EnumEntry - -object Greeting extends Enum[Greeting] { - - /* - `findValues` is a protected method that invokes a macro to find all `Greeting` object declarations inside an `Enum` - - You use it to implement the `val values` member - */ - val values = findValues - - case object Hello extends Greeting - case object GoodBye extends Greeting - case object Hi extends Greeting - case object Bye extends Greeting - -} -object ObsTest {} - -sealed trait PlayerMovementEnum extends EnumEntry { - def test: String -} - -object PlayerMovementEnum extends Enum[PlayerMovementEnum] { - val values = findValues - case object MOVE_RIGHT extends PlayerMovementEnum { - val test = "hmm" - } - case object MOVE_LEFT extends PlayerMovementEnum { - val test = "mmh" - } -} diff --git a/src/main/scala/wow/doge/mygame/implicits/observables/package.scala b/src/main/scala/wow/doge/mygame/implicits/observables/package.scala new file mode 100644 index 0000000..d587ffe --- /dev/null +++ b/src/main/scala/wow/doge/mygame/implicits/observables/package.scala @@ -0,0 +1,18 @@ +package wow.doge.mygame.implicits + +import javafx.{ + collections => jfxc, + event => jfxe, + geometry => jfxg, + scene => jfxs, + util => jfxu +} +import javafx.scene.{input => jfxsi, layout => jfxsl, paint => jfxsp} +import scalafx.scene.Scene +import monix.execution.Cancelable +import monix.reactive.OverflowStrategy +import monix.reactive.Observable +import monix.execution.Ack +import scalafx.scene.control.Button + +package object observables {} diff --git a/src/main/scala/wow/doge/mygame/implicits/package.scala b/src/main/scala/wow/doge/mygame/implicits/package.scala index 0d76f9b..e670a14 100644 --- a/src/main/scala/wow/doge/mygame/implicits/package.scala +++ b/src/main/scala/wow/doge/mygame/implicits/package.scala @@ -6,7 +6,6 @@ import scala.reflect.ClassTag import akka.actor.typed.ActorRef import akka.actor.typed.Scheduler import akka.util.Timeout -import cats.effect.concurrent.Ref import com.jme3.app.Application import com.jme3.app.SimpleApplication import com.jme3.app.state.AppState @@ -49,6 +48,8 @@ import monix.reactive.OverflowStrategy import monix.reactive.observers.Subscriber import wow.doge.mygame.math.ImVector3f import wow.doge.mygame.state.MyBaseState +import com.jme3.light.Light +import com.jayfella.jme.jfx.JavaFxUI case class ActionEvent(binding: Action, value: Boolean, tpf: Float) case class EnumActionEvent[T <: EnumEntry]( @@ -70,7 +71,7 @@ package object implicits { type PhysicsTickObservable = Observable[Either[PrePhysicsTickEvent, PhysicsTickEvent]] - implicit class JMEAppExt(private val app: Application) extends AnyVal { + implicit final class JMEAppExt(private val app: Application) extends AnyVal { def enqueueR(cb: () => Unit) = app.enqueue(new Runnable { @@ -78,7 +79,7 @@ package object implicits { }) } - implicit class StateManagerExt(private val asm: AppStateManager) + implicit final class StateManagerExt(private val asm: AppStateManager) extends AnyVal { def state[S <: AppState]()(implicit c: ClassTag[S]): S = asm.getState(c.runtimeClass.asInstanceOf[Class[S]]) @@ -87,8 +88,9 @@ package object implicits { } - implicit class SimpleApplicationExt[T <: SimpleApplication](private val sa: T) - extends AnyVal { + implicit final class SimpleApplicationExt[T <: SimpleApplication]( + private val sa: T + ) extends AnyVal { def stateManager: AppStateManager = sa.getStateManager() def inputManager: InputManager = sa.getInputManager() def assetManager: AssetManager = sa.getAssetManager() @@ -121,7 +123,8 @@ package object implicits { } } - implicit class AssetManagerExt(private val am: AssetManager) extends AnyVal { + implicit final class AssetManagerExt(private val am: AssetManager) + extends AnyVal { def registerLocator( assetPath: os.RelPath, locator: Class[_ <: AssetLocator] @@ -134,13 +137,13 @@ package object implicits { } } - implicit class BulletAppStateExt(private val bas: BulletAppState) + implicit final class BulletAppStateExt(private val bas: BulletAppState) extends AnyVal { def physicsSpace = bas.getPhysicsSpace() def speed = bas.getSpeed() } - implicit class BetterCharacterControlExt( + implicit final class BetterCharacterControlExt( private val bcc: BetterCharacterControl ) { def withJumpForce(force: ImVector3f) = { @@ -149,11 +152,12 @@ package object implicits { } } - implicit class SpatialExt[T <: Spatial](private val spat: T) extends AnyVal { - def asRef = Ref[Task].of(spat) + implicit final class SpatialExt[T <: Spatial](private val spat: T) + extends AnyVal { + // def asRef = Ref[Task].of(spat) } - implicit class NodeExt[T <: Node](private val n: T) extends AnyVal { + implicit final class NodeExt[T <: Node](private val n: T) extends AnyVal { /** * Attaches the given child @@ -167,18 +171,13 @@ package object implicits { } /** - * Gets the list of children as a monix observable - * - * @return + * @return Gets the list of children as a monix observable */ - // def children = n.getChildren().asScala.toSeq def observableChildren = Observable.fromIterable(n.getChildren().asScala) /** - * A copy of the list of children of this node as a lazy list - * - * @return + * @return A copy of the list of children of this node as a lazy list */ def children = LazyList.from(n.getChildren().asScala) @@ -199,6 +198,11 @@ package object implicits { n } + def :+(light: Light) = { + n.addLight(light) + n + } + def -=(spatial: Spatial) = n.detachChild(spatial) def :-(spatial: Spatial) = { @@ -206,6 +210,11 @@ package object implicits { n } + def :-(light: Light) = { + n.removeLight(light) + n + } + def depthFirst(cb: Spatial => Unit) = n.depthFirstTraversal(new SceneGraphVisitor() { override def visit(s: Spatial) = cb(s) @@ -313,7 +322,7 @@ package object implicits { } - implicit class CameraNodeExt(private val cn: CameraNode) { + implicit final class CameraNodeExt(private val cn: CameraNode) { def withControlDir(controlDir: ControlDirection) = { cn.setControlDir(controlDir) cn @@ -325,14 +334,15 @@ package object implicits { } } - implicit class EntityDataExt(private val ed: EntityData) extends AnyVal { + implicit final class EntityDataExt(private val ed: EntityData) + extends AnyVal { def query = new EntityQuery(ed) // def entities[T <: EntityComponent](entities: Seq[T]) } - implicit class EntityExt(private val e: EntityId) extends AnyVal { + implicit final class EntityExt(private val e: EntityId) extends AnyVal { def withComponents(classes: EntityComponent*)(implicit ed: EntityData ): EntityId = { @@ -341,7 +351,8 @@ package object implicits { } } - implicit class ActorRefExt[Req](private val a: ActorRef[Req]) extends AnyVal { + implicit final class ActorRefExt[Req](private val a: ActorRef[Req]) + extends AnyVal { import akka.actor.typed.scaladsl.AskPattern._ /** @@ -374,7 +385,7 @@ package object implicits { // ask(replyTo)(timeout, scheduler) // } - implicit class InputManagerExt(private val inputManager: InputManager) + implicit final class InputManagerExt(private val inputManager: InputManager) extends AnyVal { def withMapping(mapping: String, triggers: Trigger*): InputManager = { inputManager.addMapping(mapping, triggers: _*) @@ -507,7 +518,7 @@ package object implicits { } } - implicit class PhysicsSpaceExt(private val space: PhysicsSpace) + implicit final class PhysicsSpaceExt(private val space: PhysicsSpace) extends AnyVal { def collisionObservable(): Observable[PhysicsCollisionEvent] = { @@ -587,9 +598,11 @@ package object implicits { space } + // def asRef = Ref[Task].of(space) + } - implicit class Vector3fExt(private val v: Vector3f) extends AnyVal { + implicit final class Vector3fExt(private val v: Vector3f) extends AnyVal { //TODO add more operations def +=(that: Vector3f) = v.addLocal(that) def +=(f: Float) = v.addLocal(f, f, f) @@ -608,7 +621,7 @@ package object implicits { def immutable = ImVector3f(v.x, v.y, v.z) } - implicit class ImVector3fExt(private val v: ImVector3f) extends AnyVal { + implicit final class ImVector3fExt(private val v: ImVector3f) extends AnyVal { def +(that: ImVector3f) = v.copy(v.x + that.x, v.y + that.y, v.z + that.z) def +(f: Float): ImVector3f = v.copy(v.x + f, v.y + f, v.z + f) def *(that: ImVector3f) = v.copy(v.x * that.x, v.y * that.y, v.z * that.z) @@ -628,4 +641,8 @@ package object implicits { // f.hideErrors // } + implicit final class JavaFxUIExt(private val jfxui: JavaFxUI) extends AnyVal { + def +=(node: scalafx.scene.Node) = jfxui.attachChild(node) + def -=(node: scalafx.scene.Node) = jfxui.detachChild(node) + } } diff --git a/src/main/scala/wow/doge/mygame/launcher/DefaultUI.scala b/src/main/scala/wow/doge/mygame/launcher/DefaultUI.scala new file mode 100644 index 0000000..7dfb948 --- /dev/null +++ b/src/main/scala/wow/doge/mygame/launcher/DefaultUI.scala @@ -0,0 +1,102 @@ +package wow.doge.mygame.launcher + +import scalafx.geometry.Insets +import scalafx.scene.Scene +import scalafx.scene.effect.DropShadow +import scalafx.scene.layout.HBox +import scalafx.scene.paint.Color._ +import scalafx.scene.paint._ +import scalafx.scene.text.Text +import scalafx.scene.control.Button +import scalafx.scene.layout.VBox +import scalafx.scene.layout.FlowPane +import scalafx.geometry.Orientation +import scalafx.geometry.Pos +import scalafx.stage.Stage + +object DefaultUI { + def scene( + // stage: Stage, + launchButton: Button, + exitButton: Button + ) = + new Scene { + fill = Color.rgb(38, 38, 38) + content = new VBox { + children = Seq( + new HBox { + padding = Insets(50, 80, 50, 80) + children = Seq( + new Text { + text = "JMonkeyEngine" + style = "-fx-font: normal bold 50pt sans-serif" + fill = new LinearGradient(endX = 0, stops = Stops(Red, DarkRed)) + }, + new Text { + text = " Game" + style = "-fx-font: italic bold 50pt sans-serif" + fill = new LinearGradient( + endX = 0, + stops = Stops(White, DarkGray) + ) + effect = new DropShadow { + color = DarkGray + radius = 15 + spread = 0.25 + } + } + ) + }, + new FlowPane { + hgap = 10 + padding = Insets(50, 80, 50, 80) + orientation = Orientation.Horizontal + alignment = Pos.Center + children = Seq(launchButton, exitButton) + } + ) + } + // onMousePressed = (pressEvent) => { + // onMouseDragged = (dragEvent) => { + // stage.setX(dragEvent.getScreenX() - pressEvent.getSceneX()) + // stage.setY(dragEvent.getScreenY() - pressEvent.getSceneY()) + // } + // } + } + + def box(launchButton: Button, exitButton: Button) = + new VBox { + children = Seq( + new HBox { + padding = Insets(50, 80, 50, 80) + children = Seq( + new Text { + text = "JMonkeyEngine" + style = "-fx-font: normal bold 50pt sans-serif" + fill = new LinearGradient(endX = 0, stops = Stops(Red, DarkRed)) + }, + new Text { + text = " Game" + style = "-fx-font: italic bold 50pt sans-serif" + fill = new LinearGradient( + endX = 0, + stops = Stops(White, DarkGray) + ) + effect = new DropShadow { + color = DarkGray + radius = 15 + spread = 0.25 + } + } + ) + }, + new FlowPane { + hgap = 10 + padding = Insets(50, 80, 50, 80) + orientation = Orientation.Horizontal + alignment = Pos.Center + children = Seq(launchButton, exitButton) + } + ) + } +} diff --git a/src/main/scala/wow/doge/mygame/launcher/Launcher.scala b/src/main/scala/wow/doge/mygame/launcher/Launcher.scala new file mode 100644 index 0000000..73671cd --- /dev/null +++ b/src/main/scala/wow/doge/mygame/launcher/Launcher.scala @@ -0,0 +1,153 @@ +package wow.doge.mygame.launcher + +import scala.concurrent.duration.FiniteDuration +import scalafx.application.JFXApp +import scalafx.application.JFXApp.PrimaryStage +import wow.doge.mygame.executors.Schedulers +import cats.effect.Resource +import monix.bio.Task +import scala.concurrent.duration._ +import javafx.application.Platform +import scalafx.scene.control.Button +import cats.effect.concurrent.Deferred +import wow.doge.mygame.utils.IOUtils._ +import monix.eval.{Task => ETask} +import monix.reactive.Observable +import monix.bio.Fiber +import scalafx.stage.StageStyle +import scalafx.Includes._ +import wow.doge.mygame.utils.ResizeHelper +import scalafx.scene.Scene +import scalafx.scene.layout.VBox +import wow.doge.mygame.implicits.JavaFXMonixObservables._ +import monix.catnap.cancelables.SingleAssignCancelableF +import monix.catnap.CancelableF +// import wow.doge.mygame.implicits.JavaFXMonixObservables._ + +// import scala.language.implicitConversions + +// object Stage { +// implicit def sfxStage2jfx(v: Stage): jfxs.Stage = if (v != null) v.delegate else null +// } + +object Launcher { + sealed trait LauncherResult + object LauncherResult { + case object LaunchGame extends LauncherResult + case object Exit extends LauncherResult + } + + class Props( + val schedulers: Schedulers, + val signal: Deferred[Task, LauncherResult] + ) { + // val resource2 + // : Resource[Task, (LauncherApp, Task[Ref[Task, Stage]], Fiber[Unit])] = + // Resource.make(for { + // app <- Task(new LauncherApp(this)) + // fib <- app.init.start + // } yield ((app, app.stageRef, fib)))(_._3.cancel) + val create = Task(new Launcher(this)) + + val resource: Resource[Task, Launcher] = + Resource.make(for { + app <- Task(new Launcher(this)) + // fib <- app.init.start + } yield (app))(_ => Task.unit) + } +} +class Launcher private (props: Launcher.Props) { + import Launcher._ + + private lazy val launchButton = new Button { + text = "Launch" + } + // private lazy val launchButtonObs = + + private lazy val launchAction = + launchButton + .observableAction() + .doOnNext(_ => toTask(props.signal.complete(LauncherResult.LaunchGame))) + + private lazy val exitButton = new Button { + text = "Exit" + } + // private lazy val exitButtonObs = + + private lazy val exitAction = + exitButton + .observableAction() + .doOnNext(_ => toTask(props.signal.complete(LauncherResult.Exit))) + + private lazy val _scene = + // new Scene { + // content = new VBox + // } + DefaultUI.scene(launchButton, exitButton) + + private lazy val _stage = new PrimaryStage { + scene = _scene + } + + private lazy val internal = new JFXApp { + stage = _stage + stage.initStyle(StageStyle.Undecorated) + // ResizeHelper.addResizeListener(stage) + } + + private lazy val sceneDragObservable = { + lazy val mpo = _scene.observableMousePressed() + lazy val mdo = _scene.observableMouseDragged() + + mpo.mergeMap(pressEvent => + mdo.doOnNext(dragEvent => + ETask( + _stage.setX(dragEvent.screenX - pressEvent.sceneX) + ) >> + ETask( + _stage.setY( + dragEvent.screenY - pressEvent.sceneY + ) + ) + ) + ) + } + + // var stage = internal.stage + + // lazy val stageRef = Ref.of[Task, Stage](internal.stage) +// stage: => PrimaryStage + def init(delay: FiniteDuration = 2000.millis) = + for { + _ <- Task(Platform.setImplicitExit(false)) + + fib <- Task(internal.main(Array.empty)).start + _ <- Task.sleep(500.millis) + // _ <- Task { + // // lazy val _stage = new CustomStageBuilder() + // // .setWindowTitle("CustomStage example") + // // .setWindowColor("rgb(34,54,122)") + // // .build() + // internal.stage.scene = + // DefaultUI.scene(internal.stage, launchButton, exitButton) + // // _stage.setScene(DefaultUI.scene(launchButton, exitButton)) + // // JFXApp.Stage = _stage + // }.executeOn(props.schedulers.fx) + // c <- SingleAssignCancelableF[Task] + sceneDragFib <- toIO(sceneDragObservable.completedL).start + fib2 <- toIO( + Observable(launchAction, exitAction).merge + .doOnNext(_ => + ETask(internal.stage.close()).executeOn(props.schedulers.fx) + ) + // .doOnNext(_ => toTask(fib.cancel)) + .completedL + ).start + c <- CancelableF[Task](fib.cancel >> fib2.cancel >> sceneDragFib.cancel) + // _ <- Task { + // internal.stage = stage + // }.executeOn(props.schedulers.fx) + // .delayExecution(delay) + } yield (c) + +} diff --git a/src/main/scala/wow/doge/mygame/subsystems/events/MovementEvents.scala b/src/main/scala/wow/doge/mygame/subsystems/events/MovementEvents.scala index 871c4f6..c6b8e39 100644 --- a/src/main/scala/wow/doge/mygame/subsystems/events/MovementEvents.scala +++ b/src/main/scala/wow/doge/mygame/subsystems/events/MovementEvents.scala @@ -3,7 +3,6 @@ package wow.doge.mygame.subsystems.events import wow.doge.mygame.game.subsystems.movement.CanMove sealed trait EntityMovementEvent - object EntityMovementEvent { final case class MovedLeft[T: CanMove](pressed: Boolean, movable: T) extends EntityMovementEvent @@ -29,5 +28,6 @@ object EntityMovementEvent { final case object PlayerRotatedLeft extends PlayerMovementEvent final case object PlayerCameraUp extends PlayerMovementEvent final case object PlayerCameraDown extends PlayerMovementEvent + } } diff --git a/src/main/scala/wow/doge/mygame/subsystems/moddingsystem/ModdingSystem.scala b/src/main/scala/wow/doge/mygame/subsystems/moddingsystem/ModdingSystem.scala index e225134..f85683d 100644 --- a/src/main/scala/wow/doge/mygame/subsystems/moddingsystem/ModdingSystem.scala +++ b/src/main/scala/wow/doge/mygame/subsystems/moddingsystem/ModdingSystem.scala @@ -23,9 +23,6 @@ final case class Test1(hello1: String, hello2: String) final case class Test2(hello1: String) final case class Plugin(name: String, priority: Int) object Plugin { - // @annotation.nowarn( - // "msg=Block result was adapted via implicit conversion" - // ) implicit val pluginFormat: Decoder[Plugin] = deriveDecoder } diff --git a/src/main/scala/wow/doge/mygame/subsystems/scriptsystem/ScriptActor.scala b/src/main/scala/wow/doge/mygame/subsystems/scriptsystem/ScriptActor.scala index f330603..8027fc0 100644 --- a/src/main/scala/wow/doge/mygame/subsystems/scriptsystem/ScriptActor.scala +++ b/src/main/scala/wow/doge/mygame/subsystems/scriptsystem/ScriptActor.scala @@ -29,6 +29,11 @@ object ScriptActor { result: ActorRef[Either[Error, Any]] ) extends Command + final case class CompileAll( + paths: Seq[os.Path], + result: ActorRef[Map[os.Path, Either[Error, Any]]] + ) extends Command + lazy val defaultScalaRunner = ammonite .Main( @@ -126,15 +131,18 @@ class ScriptActor( import ScriptActor._ def receiveMessage: Behavior[Command] = - Behaviors.receiveMessage { msg => - msg match { - case CompileAny(path, requester) => - context.log.debug(s"Received $path") - val res = getScript(path) - context.log.debug(s"result = $res") - requester ! res - Behaviors.same - } + Behaviors.receiveMessage { + case CompileAny(path, requester) => + context.log.debug(s"Received $path") + val res = getScript(path) + context.log.debug(s"result = $res") + requester ! res + Behaviors.same + + case CompileAll(paths, requester) => + context.log.debug(s"Received $paths") + requester ! compileAll(paths) + Behaviors.same } def getScript(path: os.Path): Either[Error, Any] = @@ -148,4 +156,25 @@ class ScriptActor( case l @ Left(err) => l } + type LOL = Map[os.Path, Either[wow.doge.mygame.state.ScriptActor.Error, Any]] + + def compileAll( + paths: Seq[os.Path] + ): LOL = { + @annotation.tailrec + def loop( + paths: Seq[os.Path], + scriptsMap: Map[ + os.Path, + Either[wow.doge.mygame.state.ScriptActor.Error, Any] + ] + ): LOL = { + paths match { + case head :: next => loop(next, scriptsMap + (head -> getScript(head))) + case Nil => scriptsMap + } + } + loop(paths, Map.empty) + } + } diff --git a/src/main/scala/wow/doge/mygame/subsystems/scriptsystem/ScriptCachingActor.scala b/src/main/scala/wow/doge/mygame/subsystems/scriptsystem/ScriptCachingActor.scala index 7e6e3c6..2f90914 100644 --- a/src/main/scala/wow/doge/mygame/subsystems/scriptsystem/ScriptCachingActor.scala +++ b/src/main/scala/wow/doge/mygame/subsystems/scriptsystem/ScriptCachingActor.scala @@ -37,6 +37,11 @@ object ScriptCachingActor { requester: ActorRef[ScriptResult], force: Boolean = false ) extends Command + // final case class GetAll( + // scriptPaths: Seq[os.Path], + // requester: ActorRef[Map[os.Path, ScriptResult]], + // force: Boolean = false + // ) extends Command final case class GetMap(requester: ActorRef[ScriptsMap]) extends Command final case class Put(scriptPath: os.Path, script: ScriptObject) extends Command @@ -47,6 +52,15 @@ object ScriptCachingActor { scriptPath: os.Path, requester: ActorRef[ScriptResult] ) extends Command + // private[scriptsystem] final case class DelegateAllToChild( + // scriptPaths: Seq[os.Path], + // requester: ActorRef[Map[os.Path, ScriptResult]] + // ) extends Command + + // private[scriptsystem] final case class ReplyWithPrecompiled( + // precompiled: Map[os.Path, ScriptResult], + // requester: ActorRef[Map[os.Path, ScriptResult]] + // ) extends Command // final case class Props( // ctx: ActorContext[Command], @@ -102,6 +116,51 @@ class ScriptCachingActor( ) Behaviors.same + // case GetAll(scriptPaths, requester, force) => + // import scala.concurrent.duration._ + // implicit val timeout = Timeout(15.seconds) + + // /** + // * Holy complexity batman this is getting too complex + // */ + // if (force) { + // scriptPaths + // .sliding( + // if (scriptPaths.length > 3 && scriptPaths.length % 2 == 0) 2 + // else 3 + // ) + // .foreach(lst => + // ctx.self ! DelegateAllToChild(scriptPaths, requester) + // ) + // } else { + // val (failures, successes) = scriptPaths + // .partitionMap(path => + // state.scriptsMap.get(path) match { + // case Some(value) => Right(path -> value) + // case None => Left(path) + // } + // ) match { + // case (failures, successes) => + // import cats.syntax.either._ + // failures -> Map.from(successes.map { + // case (p, obj) => p -> obj.asRight[ScriptActor.Error] + // }) + // } + // ctx.ask(scriptActor, ScriptActor.CompileAll(failures, _)) { + // case Success(value) => + // val total = successes ++ value + // requester ! total + // value.foreach { + // case (path, res) => res.foreach(r => ctx.self ! Put(path, r)) + // } + // NoOp + // case Failure(exception) => NoOp + // } + // } + // // scriptPaths.foreach(p => ctx.self ! Get(p)) + + // Behaviors.same + case DelegateToChild(scriptActor, scriptPath, requester) => import scala.concurrent.duration._ implicit val timeout = Timeout(15.seconds) @@ -114,6 +173,19 @@ class ScriptCachingActor( ) Behaviors.same + // case DelegateAllToChild(scriptPaths, requester) => + // import scala.concurrent.duration._ + // implicit val timeout = Timeout(15.seconds) + // ctx.ask(scriptActor, ScriptActor.CompileAll(scriptPaths, _)) { + // case Success(value) => + // value.foreach { + // case (path, res) => res.foreach(r => ctx.self ! Put(path, r)) + // } + // NoOp + // case Failure(exception) => NoOp + // } + // Behaviors.same + case GetMap(requester) => requester ! state.scriptsMap Behaviors.same @@ -128,6 +200,7 @@ class ScriptCachingActor( case NoOp => Behaviors.same } } + } object ScriptActorPool { diff --git a/src/main/scala/wow/doge/mygame/subsystems/scriptsystem/ScriptSystemModule.scala b/src/main/scala/wow/doge/mygame/subsystems/scriptsystem/ScriptSystemModule.scala index 04e3171..5214c4e 100644 --- a/src/main/scala/wow/doge/mygame/subsystems/scriptsystem/ScriptSystemModule.scala +++ b/src/main/scala/wow/doge/mygame/subsystems/scriptsystem/ScriptSystemModule.scala @@ -35,6 +35,7 @@ class ScriptSystemResource( Resource.liftF(scriptCacheActor) } + // sys.ask(ref => ScriptCachingActor.GetAll(os.pwd/'assets/'scripts/'scala/"hello2.sc",ref, false)) val init = for { scriptFiles <- Task(findScriptFiles(os.pwd / "assets" / "scripts")) diff --git a/src/main/scala/wow/doge/mygame/utils/BorderlessScene.scala b/src/main/scala/wow/doge/mygame/utils/BorderlessScene.scala new file mode 100644 index 0000000..8fe3f0c --- /dev/null +++ b/src/main/scala/wow/doge/mygame/utils/BorderlessScene.scala @@ -0,0 +1,750 @@ +package wow.doge.mygame.utils + +/* + * Copyright (c) 2011-2019, ScalaFX Project + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of the ScalaFX Project nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE SCALAFX PROJECT OR ITS CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED + * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import javafx.scene.{input => jfxsi, layout => jfxsl, paint => jfxsp} +import javafx.{ + collections => jfxc, + event => jfxe, + geometry => jfxg, + scene => jfxs, + util => jfxu +} +import scalafx.Includes._ +import scalafx.beans.property.{ + ObjectProperty, + ReadOnlyDoubleProperty, + ReadOnlyObjectProperty +} +import scalafx.collections._ +import scalafx.delegate.SFXDelegate +import scalafx.geometry.NodeOrientation +import scalafx.scene.image.WritableImage +import scalafx.scene.input.{Dragboard, Mnemonic, TransferMode} +import scalafx.scene.paint.Paint +import com.goxr3plus.fxborderlessscene.borderless.{BorderlessScene => BScene} + +import scala.language.implicitConversions +import scalafx.scene.Cursor +import scalafx.scene._ +import scalafx.stage.Stage +import scalafx.stage.StageStyle + +object BorderlessScene { + implicit def sfxScene2jfx(v: BorderlessScene): Scene = + if (v != null) v.delegate else null +} + +/** + * Wraps [[http://docs.oracle.com/javase/8/javafx/api/javafx/scene/Scene.html]]. + * + * @constructor Create a new ScalaFX Scene with JavaFX Scene as delegate. + * @param delegate JavaFX Scene delegated. Its default value is a JavaFX Scene with a + * [[http://docs.oracle.com/javase/8/javafx/api/javafx/scene/Group.html Group]] as root Node. + */ +class BorderlessScene( + override val delegate: BScene +) extends SFXDelegate[BScene] { + + def this(stage: Stage, stageStyle: StageStyle, parent: Parent) = + this(new BScene(stage, stageStyle, parent)) + + /** + * Returns the root Node of the scene graph + */ + def root: ObjectProperty[jfxs.Parent] = delegate.rootProperty + + /** + * Sets the root Node of the scene graph + */ + def root_=(v: Parent): Unit = { + root() = v + } + + /** + * Returns Nodes children from this Scene's `root`. + */ + def getChildren = + root.value match { + case group: jfxs.Group => group.getChildren + case pane: jfxsl.Pane => pane.getChildren + case _ => + throw new IllegalStateException( + "Cannot access children of root: " + root + "\n" + + "Use a class that extends Group or Pane, or override the getChildren method." + ) + } + + /** + * Returns scene's antialiasing setting. + */ + def antialiasing: SceneAntialiasing = delegate.getAntiAliasing + + /** + * Returns Content's Node children from this Scene's `root`. + */ + def content: jfxc.ObservableList[jfxs.Node] = getChildren + + /** + * Sets the list of Nodes children from this Scene's `root`, replacing the prior content. If you want append to + * current content, use `add` or similar. + * + * @param c list of Nodes children from this Scene's `root` to replace prior content. + */ + def content_=(c: Iterable[Node]): Unit = { + fillSFXCollection(this.content, c) + } + + /** + * Sets a Node child, replacing the prior content. If you want append to current content, use `add` or similar. + * + * @param n Node child to replace prior content. + */ + def content_=(n: Node): Unit = { + fillSFXCollectionWithOne(this.content, n) + } + + /** + * Specifies the type of camera use for rendering this `Scene`. + */ + def camera: ObjectProperty[jfxs.Camera] = delegate.cameraProperty + + def camera_=(v: Camera): Unit = { + camera() = v + } + + /** + * Defines the mouse cursor for this `Scene`. + */ + def cursor: ObjectProperty[jfxs.Cursor] = delegate.cursorProperty + + def cursor_=(v: Cursor): Unit = { + cursor() = v + } + + /** The effective node orientation of a scene resolves the inheritance of node orientation, returning either left-to-right or right-to-left. */ + def effectiveNodeOrientation: ReadOnlyObjectProperty[jfxg.NodeOrientation] = + delegate.effectiveNodeOrientationProperty + + /** + * Specifies the event dispatcher for this scene. + */ + def eventDispatcher: ObjectProperty[jfxe.EventDispatcher] = + delegate.eventDispatcherProperty + + def eventDispatcher_=(v: jfxe.EventDispatcher): Unit = { + eventDispatcher() = v + } + + /** + * Defines the background fill of this Scene. + */ + def fill: ObjectProperty[jfxsp.Paint] = delegate.fillProperty + + def fill_=(v: Paint): Unit = { + fill() = v + } + + /** + * The height of this Scene + */ + def height: ReadOnlyDoubleProperty = delegate.heightProperty + + /** + * The width of this Scene + */ + def width: ReadOnlyDoubleProperty = delegate.widthProperty + + def nodeOrientation: ObjectProperty[jfxg.NodeOrientation] = + delegate.nodeOrientationProperty + + def nodeOrientation_=(v: NodeOrientation): Unit = { + ObjectProperty.fillProperty[jfxg.NodeOrientation](this.nodeOrientation, v) + } + + /** + * Defines a function to be called when a mouse button has been clicked (pressed and released) on this `Scene`. + */ + def onContextMenuRequested = delegate.onContextMenuRequestedProperty + + def onContextMenuRequested_=( + v: jfxe.EventHandler[_ >: jfxsi.ContextMenuEvent] + ): Unit = { + onContextMenuRequested() = v + } + + /** + * Defines a function to be called when drag gesture has been detected. + */ + def onDragDetected = delegate.onDragDetectedProperty + + def onDragDetected_=(v: jfxe.EventHandler[_ >: jfxsi.MouseEvent]): Unit = { + onDragDetected() = v + } + + /** + * Defines a function to be called when this `Scene` is a drag and drop gesture source after its data has been + * dropped on a drop target. + */ + def onDragDone = delegate.onDragDoneProperty + + def onDragDone_=(v: jfxe.EventHandler[_ >: jfxsi.DragEvent]): Unit = { + onDragDone() = v + } + + /** + * Defines a function to be called when the mouse button is released on this `Scene` during drag and drop gesture. + */ + def onDragDropped = delegate.onDragDroppedProperty + + def onDragDropped_=(v: jfxe.EventHandler[_ >: jfxsi.DragEvent]): Unit = { + onDragDropped() = v + } + + /** + * Defines a function to be called when drag gesture enters this Scene. + */ + def onDragEntered = delegate.onDragEnteredProperty + + def onDragEntered_=(v: jfxe.EventHandler[_ >: jfxsi.DragEvent]): Unit = { + onDragEntered() = v + } + + /** + * Defines a function to be called when drag gesture exits this Scene. + */ + def onDragExited = delegate.onDragExitedProperty + + def onDragExited_=(v: jfxe.EventHandler[_ >: jfxsi.DragEvent]): Unit = { + onDragExited() = v + } + + /** + * Defines a function to be called when drag gesture progresses within this `Scene`. + */ + def onDragOver = delegate.onDragOverProperty + + def onDragOver_=(v: jfxe.EventHandler[_ >: jfxsi.DragEvent]): Unit = { + onDragOver() = v + } + + /** + * Defines a function to be called when this `Node` has input focus and the input method text has changed. + */ + def onInputMethodTextChanged = delegate.onInputMethodTextChangedProperty + + def onInputMethodTextChanged_=( + v: jfxe.EventHandler[_ >: jfxsi.InputMethodEvent] + ): Unit = { + onInputMethodTextChanged() = v + } + + /** + * Defines a function to be called when some `Node` of this `Scene` has input focus and a key has been pressed. + */ + def onKeyPressed = delegate.onKeyPressedProperty + + def onKeyPressed_=(v: jfxe.EventHandler[_ >: jfxsi.KeyEvent]): Unit = { + onKeyPressed() = v + } + + /** + * Defines a function to be called when some `Node` of this `Scene` has input focus and a key has been released. + */ + def onKeyReleased = delegate.onKeyReleasedProperty + + def onKeyReleased_=(v: jfxe.EventHandler[_ >: jfxsi.KeyEvent]): Unit = { + onKeyReleased() = v + } + + /** + * Defines a function to be called when some `Node` of this `Scene` has input focus and a key has been typed. + */ + def onKeyTyped = delegate.onKeyTypedProperty + + def onKeyTyped_=(v: jfxe.EventHandler[_ >: jfxsi.KeyEvent]): Unit = { + onKeyTyped() = v + } + + /** + * Defines a function to be called when a mouse button has been clicked (pressed and released) on this `Scene`. + */ + def onMouseClicked = delegate.onMouseClickedProperty + + def onMouseClicked_=(v: jfxe.EventHandler[_ >: jfxsi.MouseEvent]): Unit = { + onMouseClicked() = v + } + + /** + * Defines a function to be called when a mouse button is pressed on this `Scene` and then dragged. + */ + def onMouseDragged = delegate.onMouseDraggedProperty + + def onMouseDragged_=(v: jfxe.EventHandler[_ >: jfxsi.MouseEvent]): Unit = { + onMouseDragged() = v + } + + /** + * Defines a function to be called when a full press-drag-release gesture enters this `Scene`. + */ + def onMouseDragEntered = delegate.onMouseDragEnteredProperty + + def onMouseDragEntered_=( + v: jfxe.EventHandler[_ >: jfxsi.MouseDragEvent] + ): Unit = { + onMouseDragEntered() = v + } + + /** + * Defines a function to be called when a full press-drag-release gesture exits this `Scene`. + */ + def onMouseDragExited = delegate.onMouseDragExitedProperty + + def onMouseDragExited_=( + v: jfxe.EventHandler[_ >: jfxsi.MouseDragEvent] + ): Unit = { + onMouseDragExited() = v + } + + /** + * Defines a function to be called when a full press-drag-release gesture progresses within this `Scene`. + */ + def onMouseDragOver = delegate.onMouseDragOverProperty + + def onMouseDragOver_=( + v: jfxe.EventHandler[_ >: jfxsi.MouseDragEvent] + ): Unit = { + onMouseDragOver() = v + } + + /** + * Defines a function to be called when a full press-drag-release gesture ends within this `Scene`. + */ + def onMouseDragReleased = delegate.onMouseDragReleasedProperty + + def onMouseDragReleased_=( + v: jfxe.EventHandler[_ >: jfxsi.MouseDragEvent] + ): Unit = { + onMouseDragReleased() = v + } + + /** + * Defines a function to be called when the mouse enters this `Scene`. + */ + def onMouseEntered = delegate.onMouseEnteredProperty + + def onMouseEntered_=(v: jfxe.EventHandler[_ >: jfxsi.MouseEvent]): Unit = { + onMouseEntered() = v + } + + /** + * Defines a function to be called when the mouse exits this `Scene`. + */ + def onMouseExited = delegate.onMouseExitedProperty + + def onMouseExited_=(v: jfxe.EventHandler[_ >: jfxsi.MouseEvent]): Unit = { + onMouseExited() = v + } + + /** + * Defines a function to be called when mouse cursor moves within this `Scene` but no buttons have been pushed. + */ + def onMouseMoved = delegate.onMouseMovedProperty + + def onMouseMoved_=(v: jfxe.EventHandler[_ >: jfxsi.MouseEvent]): Unit = { + onMouseMoved() = v + } + + /** + * Defines a function to be called when a mouse button has been pressed on this `Scene`. + */ + def onMousePressed = delegate.onMousePressedProperty + + def onMousePressed_=(v: jfxe.EventHandler[_ >: jfxsi.MouseEvent]): Unit = { + onMousePressed() = v + } + + /** + * Defines a function to be called when a mouse button has been released on this `Scene`. + */ + def onMouseReleased = delegate.onMouseReleasedProperty + + def onMouseReleased_=(v: jfxe.EventHandler[_ >: jfxsi.MouseEvent]): Unit = { + onMouseReleased() = v + } + + /** + * Defines a function to be called when user performs a scrolling action. + */ + def onScroll = delegate.onScrollProperty + + def onScroll_=(v: jfxe.EventHandler[_ >: jfxsi.ScrollEvent]): Unit = { + onScroll() = v + } + + /** + * The URL of the user-agent stylesheet that will be used by this Scene in place of the the platform-default + * user-agent stylesheet. If the URL does not resolve to a valid location, the platform-default user-agent + * stylesheet will be used. + * + * For additional information about using CSS with the scene graph, see the + * [[http://docs.oracle.com/javase/8/javafx/api/javafx/scene/doc-files/cssref.html CSS Reference Guide]]. + * + * @return The URL of the user-agent stylesheet that will be used by this SubScene, or null if has not been set. + */ + def userAgentStylesheet: ObjectProperty[String] = + delegate.userAgentStylesheetProperty + + /** + * Set the URL of the user-agent stylesheet that will be used by this Scene in place of the the platform-default + * user-agent stylesheet. If the URL does not resolve to a valid location, the platform-default user-agent + * stylesheet will be used. + * + * For additional information about using CSS with the scene graph, see the + * [[http://docs.oracle.com/javase/8/javafx/api/javafx/scene/doc-files/cssref.html CSS Reference Guide]]. + * + * @param url The URL is a hierarchical URI of the form `[scheme:][//authority][path]`. + * If the URL does not have a `[scheme:]` component, the URL is considered to be the `[path]` + * component only. Any leading '/' character of the `[path]` is ignored and the `[path]` is + * treated as a path relative to the root of the application's classpath. + */ + def userAgentStylesheet_=(url: String): Unit = { + ObjectProperty.fillProperty[String](userAgentStylesheet, url) + } + + /** + * The `Window` for this Scene + */ + def window: ReadOnlyObjectProperty[javafx.stage.Window] = + delegate.windowProperty + + /** + * The horizontal location of this `Scene` on the `Window`. + */ + def x: ReadOnlyDoubleProperty = delegate.xProperty + + /** + * The vertical location of this `Scene` on the `Window`. + */ + def y: ReadOnlyDoubleProperty = delegate.yProperty + + /** + * Retrieves the depth buffer attribute for this scene. + */ + def depthBuffer = delegate.isDepthBuffer + + /** + * Gets an observable list of string URLs linking to the stylesheets to use with this Parent's contents. + */ + def stylesheets: jfxc.ObservableList[String] = delegate.getStylesheets + + /** + * Sets the list of stylesheets URLs, replacing the prior content. If you want append to current content, use `add` or + * similar. + * + * @param c list of stylesheets URLs to replace prior content. + */ + def stylesheets_=(c: Iterable[String]): Unit = { + fillCollection(stylesheets, c) + } + + /** + * Looks for any node within the scene graph based on the specified CSS selector. + * + * @param selector The css selector to look up + * @return A [[scala.Some]] containing the Node in the scene which matches the CSS selector, or [[scala.None]] + * if none is found. + */ + def lookup(selector: String): Option[Node] = Option(delegate.lookup(selector)) + + /** + * Registers the specified mnemonic. + * + * @param m The Mnemonic + */ + def addMnemonic(m: Mnemonic): Unit = { + delegate.addMnemonic(m) + } + + /** + * Unregisters the specified mnemonic. + * + * @param m The Mnemonic to be removed. + */ + def removeMnemonic(m: Mnemonic): Unit = { + delegate.removeMnemonic(m) + } + + /** + * Gets the list of mnemonics for this `Scene`. + */ + def getMnemonics + : jfxc.ObservableMap[jfxsi.KeyCombination, jfxc.ObservableList[ + jfxsi.Mnemonic + ]] = delegate.getMnemonics + + /** + * Gets the list of accelerators for this Scene. + */ + def accelerators: jfxc.ObservableMap[jfxsi.KeyCombination, Runnable] = + delegate.getAccelerators + + /** + * Confirms a potential drag and drop gesture that is recognized over this `Scene`. + * + * @param transferModes The supported `TransferMode`(s) of this `Node` + * @return A `Dragboard` to place this `Scene`'s data on + */ + def startDragAndDrop(transferModes: TransferMode*): Dragboard = + delegate.startDragAndDrop(transferModes.map(_.delegate): _*) + + /** + * Starts a full press-drag-release gesture with this scene as gesture source. + */ + def startFullDrag(): Unit = { + delegate.startFullDrag() + } + + /** + * The scene's current focus owner node. This node's "focused" variable might be false if this scene has no window, + * or if the window is inactive (window.focused == false). + * + * @since 2.2 + */ + def focusOwner: ReadOnlyObjectProperty[jfxs.Node] = + delegate.focusOwnerProperty() + + /** + * Defines a function to be called when user performs a rotation action. + * + * @since 2.2 + */ + def onRotate = delegate.onRotateProperty + + def onRotate_=(v: jfxe.EventHandler[_ >: jfxsi.RotateEvent]): Unit = { + onRotate() = v + } + + /** + * Defines a function to be called when a rotation gesture ends. + * + * @since 2.2 + */ + def onRotationFinished = delegate.onRotationFinishedProperty() + + def onRotationFinished_=( + v: jfxe.EventHandler[_ >: jfxsi.RotateEvent] + ): Unit = { + onRotationFinished() = v + } + + /** + * Defines a function to be called when a rotation gesture starts. + * + * @since 2.2 + */ + def onRotationStarted = delegate.onRotationFinishedProperty() + + def onRotationStarted_=( + v: jfxe.EventHandler[_ >: jfxsi.RotateEvent] + ): Unit = { + onRotationStarted() = v + } + + /** + * Defines a function to be called when a Scroll gesture ends. + * + * @since 2.2 + */ + def onScrollFinished = delegate.onScrollFinishedProperty() + + def onScrollFinished_=(v: jfxe.EventHandler[_ >: jfxsi.ScrollEvent]): Unit = { + onScrollFinished() = v + } + + /** + * Defines a function to be called when a Scroll gesture starts. + * + * @since 2.2 + */ + def onScrollStarted = delegate.onScrollStartedProperty() + + def onScrollStarted_=(v: jfxe.EventHandler[_ >: jfxsi.ScrollEvent]): Unit = { + onScrollStarted() = v + } + + /** + * Defines a function to be called when a Swipe Down gesture starts. + * + * @since 2.2 + */ + def onSwipeDown = delegate.onSwipeDownProperty() + + def onSwipeDown_=(v: jfxe.EventHandler[_ >: jfxsi.SwipeEvent]): Unit = { + onSwipeDown() = v + } + + /** + * Defines a function to be called when a Swipe Down gesture starts. + * + * @since 2.2 + */ + def onSwipeLeft = delegate.onSwipeLeftProperty() + + def onSwipeLeft_=(v: jfxe.EventHandler[_ >: jfxsi.SwipeEvent]): Unit = { + onSwipeLeft() = v + } + + /** + * Defines a function to be called when a Swipe Up gesture starts. + * + * @since 2.2 + */ + def onSwipeUp = delegate.onSwipeUpProperty() + + def onSwipeUp_=(v: jfxe.EventHandler[_ >: jfxsi.SwipeEvent]): Unit = { + onSwipeUp() = v + } + + /** + * Defines a function to be called when a Swipe Right gesture starts. + * + * @since 2.2 + */ + def onSwipeRight = delegate.onSwipeRightProperty() + + def onSwipeRight_=(v: jfxe.EventHandler[_ >: jfxsi.SwipeEvent]): Unit = { + onSwipeRight() = v + } + + /** + * Defines a function to be called when user performs a Touch action. + * + * @since 2.2 + */ + def onZoom = delegate.onZoomProperty() + + def onZoom_=(v: jfxe.EventHandler[_ >: jfxsi.ZoomEvent]): Unit = { + onZoom() = v + } + + /** + * Defines a function to be called when a Zoom gesture ends. + * + * @since 2.2 + */ + def onZoomFinished = delegate.onZoomFinishedProperty() + + def onZoomFinished_=(v: jfxe.EventHandler[_ >: jfxsi.ZoomEvent]): Unit = { + onZoomFinished() = v + } + + /** + * Defines a function to be called when a Zoom gesture starts. + * + * @since 2.2 + */ + def onZoomStarted = delegate.onZoomStartedProperty() + + def onZoomStarted_=(v: jfxe.EventHandler[_ >: jfxsi.ZoomEvent]): Unit = { + onZoomStarted() = v + } + + /** + * Defines a function to be called when user performs a Touch Moved action. + * + * @since 2.2 + */ + def onTouchMoved = delegate.onTouchMovedProperty() + + def onTouchMoved_=(v: jfxe.EventHandler[_ >: jfxsi.TouchEvent]): Unit = { + onTouchMoved() = v + } + + /** + * Defines a function to be called when user performs a Touch Pressed action. + * + * @since 2.2 + */ + def onTouchPressed = delegate.onTouchPressedProperty() + + def onTouchPressed_=(v: jfxe.EventHandler[_ >: jfxsi.TouchEvent]): Unit = { + onTouchPressed() = v + } + + /** + * Defines a function to be called when user performs a Touch Released action. + * + * @since 2.2 + */ + def onTouchReleased = delegate.onTouchReleasedProperty() + + def onTouchReleased_=(v: jfxe.EventHandler[_ >: jfxsi.TouchEvent]): Unit = { + onTouchReleased() = v + } + + /** + * Defines a function to be called when user performs a Touch Stationary action. + * + * @since 2.2 + */ + def onTouchStationary = delegate.onTouchStationaryProperty() + + def onTouchStationary_=(v: jfxe.EventHandler[_ >: jfxsi.TouchEvent]): Unit = { + onTouchStationary() = v + } + + /** + * Takes a snapshot of this scene and returns the rendered image when it is ready. + * + * @param image The writable image that will be used to hold the rendered scene. + * @return the rendered image + * + * @since 2.2 + */ + def snapshot(image: WritableImage): WritableImage = delegate.snapshot(image) + + /** + * Takes a snapshot of this scene at the next frame and calls the specified callback method when the image is ready. + * + * @param callback A function to be called when the image is ready. + * @param image The writable image that will be used to hold the rendered scene. + * + * @since 2.2 + */ + def snapshot(callback: SnapshotResult => Unit, image: WritableImage): Unit = { + val javaCallback = new jfxu.Callback[jfxs.SnapshotResult, java.lang.Void] { + def call(result: jfxs.SnapshotResult): java.lang.Void = { + callback(new SnapshotResult(result)) + null + } + } + delegate.snapshot(javaCallback, image) + } + +} diff --git a/src/main/scala/wow/doge/mygame/utils/GenericConsoleStream.scala b/src/main/scala/wow/doge/mygame/utils/GenericConsoleStream.scala new file mode 100644 index 0000000..3b1ac28 --- /dev/null +++ b/src/main/scala/wow/doge/mygame/utils/GenericConsoleStream.scala @@ -0,0 +1,104 @@ +package wow.doge.mygame.utils + +import java.io.ByteArrayOutputStream +import java.io.OutputStream +import java.io.PrintStream + +import cats.effect.Resource +import monix.bio.Task +import scalafx.scene.control.TextArea +import scalafx.application.Platform + +trait ConsoleStreamable[T] { + def println(inst: T, text: String): Unit + def print(inst: T, text: String): Unit +} + +class GenericConsoleStream[T]( + outputStream: OutputStream, + val config: GenericConsoleStream.Config = + GenericConsoleStream.Config.default, + private var streamable: Option[T] = None +)(implicit + cs: ConsoleStreamable[T] +) extends PrintStream(outputStream, true) { + private lazy val defaultOut = System.out + + def printToStreamable(stble: Option[T], text: String) = + stble.foreach(s => cs.println(s, text)) + + override def println(text: String): Unit = + if (config.exclusive) { + printToStreamable(streamable, text) + } else { + defaultOut.println(text) + printToStreamable(streamable, text) + } + + override def print(text: String): Unit = + streamable.foreach(s => + if (config.exclusive) { + printToStreamable(streamable, text) + } else { + defaultOut.println(text) + printToStreamable(streamable, text) + } + ) + + def :=(s: T) = { + streamable match { + case Some(value) => + case None => streamable = Some(s) + } + } +} + +object GenericConsoleStream { + + /** + * for future use + */ + case class Config(exclusive: Boolean = false) + object Config { + lazy val default = Config() + } + + implicit val implJFXConsoleStreamForTextArea = + new ConsoleStreamable[scalafx.scene.control.TextArea] { + + override def println( + ta: scalafx.scene.control.TextArea, + text: String + ): Unit = + ta.appendText(text + "\n") + + override def print( + ta: scalafx.scene.control.TextArea, + text: String + ): Unit = + ta.appendText(text) + + } + + // def textAreaStreamResource(ta: TextArea) = + // Resource.make( + // Task( + // new GenericConsoleStream( + // outputStream = new ByteArrayOutputStream(), + // streamable = ta + // ) + // ) + // )(s => Task(s.close())) + + def textAreaStream( + // ta: TextArea + ) = + // Task( + new GenericConsoleStream[TextArea]( + outputStream = new ByteArrayOutputStream() + // streamable = ta + ) + // ) + // (s => Task(s.close())) + +} diff --git a/src/main/scala/wow/doge/mygame/utils/JFXConsoleStream.scala b/src/main/scala/wow/doge/mygame/utils/JFXConsoleStream.scala deleted file mode 100644 index e2c5546..0000000 --- a/src/main/scala/wow/doge/mygame/utils/JFXConsoleStream.scala +++ /dev/null @@ -1,71 +0,0 @@ -package wow.doge.mygame.utils - -import java.io.ByteArrayOutputStream -import java.io.OutputStream -import java.io.PrintStream - -import cats.effect.Resource -import monix.bio.Task -import scalafx.scene.control.TextArea - -trait JFXConsoleStreamable[T] { - def println(inst: T, text: String): Unit - def print(inst: T, text: String): Unit -} - -class JFXConsoleStream[T]( - outputStream: OutputStream, - val config: JFXConsoleStream.Config = JFXConsoleStream.Config.default, - control: T -)(implicit - jcs: JFXConsoleStreamable[T] -) extends PrintStream(outputStream, true) { - private lazy val defaultOut = System.out - override def println(text: String): Unit = - if (config.exclusive) { - jcs.println(control, text + "\n") - } else { - defaultOut.println(text) - jcs.println(control, text + "\n") - } - override def print(text: String): Unit = jcs.println(control, text) -} - -object JFXConsoleStream { - - /** - * for future use - */ - case class Config(exclusive: Boolean = false) - object Config { - lazy val default = Config() - } - - implicit val implJFXConsoleStreamForTextArea = - new JFXConsoleStreamable[scalafx.scene.control.TextArea] { - - override def println( - ta: scalafx.scene.control.TextArea, - text: String - ): Unit = - ta.appendText(text + "\n") - - override def print( - ta: scalafx.scene.control.TextArea, - text: String - ): Unit = - ta.appendText(text) - - } - - def textAreaStream(ta: TextArea) = - Resource.make( - Task( - new JFXConsoleStream( - outputStream = new ByteArrayOutputStream(), - control = ta - ) - ) - )(s => Task(s.close())) - -} diff --git a/src/main/scala/wow/doge/mygame/utils/ResizeHelper.java b/src/main/scala/wow/doge/mygame/utils/ResizeHelper.java new file mode 100644 index 0000000..5bb2086 --- /dev/null +++ b/src/main/scala/wow/doge/mygame/utils/ResizeHelper.java @@ -0,0 +1,130 @@ +package wow.doge.mygame.utils; + +import javafx.event.EventHandler; +import javafx.event.EventType; +import javafx.geometry.Insets; +import javafx.scene.Cursor; +import javafx.scene.Scene; +import javafx.scene.input.MouseEvent; +import javafx.stage.Stage; + +//created by Alexander Berg +public class ResizeHelper { + + public static ResizeListener addResizeListener(Stage stage) { + ResizeListener resizeListener = new ResizeListener(stage); + Scene scene = stage.getScene(); + scene.addEventHandler(MouseEvent.MOUSE_MOVED, resizeListener); + scene.addEventHandler(MouseEvent.MOUSE_PRESSED, resizeListener); + scene.addEventHandler(MouseEvent.MOUSE_DRAGGED, resizeListener); + scene.addEventHandler(MouseEvent.MOUSE_EXITED, resizeListener); + scene.addEventHandler(MouseEvent.MOUSE_EXITED_TARGET, resizeListener); + + return resizeListener; + } + + public static class ResizeListener implements EventHandler { + private Stage stage; + private Scene scene; + private Cursor cursorEvent = Cursor.DEFAULT; + private int border = 4; + private double startX = 0; + private double startY = 0; + private double sceneOffsetX = 0; + private double sceneOffsetY = 0; + private double padTop = 0; + private double padRight = 0; + private double padBottom = 0; + private double padLeft = 0; + + public ResizeListener(Stage stage) { + this.stage = stage; + this.scene = stage.getScene(); + } + + public void setPadding(Insets padding) { + padTop = padding.getTop(); + padRight = padding.getRight(); + padBottom = padding.getBottom(); + padLeft = padding.getLeft(); + } + + @Override + public void handle(MouseEvent mouseEvent) { + EventType mouseEventType = mouseEvent.getEventType(); + + double mouseEventX = mouseEvent.getSceneX(), mouseEventY = mouseEvent.getSceneY(), + viewWidth = stage.getWidth() - padLeft - padRight, + viewHeight = stage.getHeight() - padTop - padBottom; + + if (MouseEvent.MOUSE_MOVED.equals(mouseEventType)) { + if (mouseEventX < border + padLeft && mouseEventY < border + padTop) { + cursorEvent = Cursor.NW_RESIZE; + } else if (mouseEventX < border + padLeft && mouseEventY > viewHeight - border + padTop) { + cursorEvent = Cursor.SW_RESIZE; + } else if (mouseEventX > viewWidth - border + padLeft && mouseEventY < border + padTop) { + cursorEvent = Cursor.NE_RESIZE; + } else if (mouseEventX > viewWidth - border + padLeft && mouseEventY > viewHeight - border + padTop) { + cursorEvent = Cursor.SE_RESIZE; + } else if (mouseEventX < border + padLeft) { + cursorEvent = Cursor.W_RESIZE; + } else if (mouseEventX > viewWidth - border + padLeft) { + cursorEvent = Cursor.E_RESIZE; + } else if (mouseEventY < border + padTop) { + cursorEvent = Cursor.N_RESIZE; + } else if (mouseEventY > viewHeight - border + padTop) { + cursorEvent = Cursor.S_RESIZE; + } else { + cursorEvent = Cursor.DEFAULT; + } + + scene.setCursor(cursorEvent); + } else if (MouseEvent.MOUSE_EXITED.equals(mouseEventType) + || MouseEvent.MOUSE_EXITED_TARGET.equals(mouseEventType)) { + scene.setCursor(Cursor.DEFAULT); + } else if (MouseEvent.MOUSE_PRESSED.equals(mouseEventType)) { + startX = viewWidth - mouseEventX; + startY = viewHeight - mouseEventY; + sceneOffsetX = mouseEvent.getSceneX(); + sceneOffsetY = mouseEvent.getSceneY(); + } else if (MouseEvent.MOUSE_DRAGGED.equals(mouseEventType) && !Cursor.DEFAULT.equals(cursorEvent)) { + if (!Cursor.W_RESIZE.equals(cursorEvent) && !Cursor.E_RESIZE.equals(cursorEvent)) { + double minHeight = stage.getMinHeight() > (border * 2) ? stage.getMinHeight() : (border * 2); + + if (Cursor.NW_RESIZE.equals(cursorEvent) || Cursor.N_RESIZE.equals(cursorEvent) + || Cursor.NE_RESIZE.equals(cursorEvent)) { + if (stage.getHeight() > minHeight || mouseEventY < 0) { + double height = stage.getY() - mouseEvent.getScreenY() + stage.getHeight() + sceneOffsetY; + double y = mouseEvent.getScreenY() - sceneOffsetY; + + stage.setHeight(height); + stage.setY(y); + } + } else { + if (stage.getHeight() > minHeight || mouseEventY + startY - stage.getHeight() > 0) { + stage.setHeight(mouseEventY + startY + padBottom + padTop); + } + } + } + + if (!Cursor.N_RESIZE.equals(cursorEvent) && !Cursor.S_RESIZE.equals(cursorEvent)) { + double minWidth = stage.getMinWidth() > (border * 2) ? stage.getMinWidth() : (border * 2); + if (Cursor.NW_RESIZE.equals(cursorEvent) || Cursor.W_RESIZE.equals(cursorEvent) + || Cursor.SW_RESIZE.equals(cursorEvent)) { + if (stage.getWidth() > minWidth || mouseEventX < 0) { + double width = stage.getX() - mouseEvent.getScreenX() + stage.getWidth() + sceneOffsetX; + double x = mouseEvent.getScreenX() - sceneOffsetX; + + stage.setWidth(width); + stage.setX(x); + } + } else { + if (stage.getWidth() > minWidth || mouseEventX + startX - stage.getWidth() > 0) { + stage.setWidth(mouseEventX + startX + padLeft + padRight); + } + } + } + } + } + } +} \ No newline at end of file diff --git a/src/main/scala/wow/doge/mygame/utils/TreeTest.scala b/src/main/scala/wow/doge/mygame/utils/TreeTest.scala new file mode 100644 index 0000000..2f112ee --- /dev/null +++ b/src/main/scala/wow/doge/mygame/utils/TreeTest.scala @@ -0,0 +1,24 @@ +package wow.doge.mygame.utils + +import monix.execution.atomic.AtomicAny + +/** + * Useless + */ +sealed abstract class Tree[+T] +case class Node[T](data: T, children: AtomicAny[LazyList[Tree[T]]]) + extends Tree[T] { + def add(data: T) = { + children.transform(children => + Node(data, AtomicAny(LazyList[Tree[T]]())) #:: children + ) + } +} +// case object Leaf extends Tree[Nothing] +case class Data(data: Int) + +class TreeManager[T] { +// val root: AtomicAny[Tree[T]] = AtomicAny(Leaf) + + def add(data: T, node: Node[T]) = {} +}