You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
613 lines
21 KiB
613 lines
21 KiB
package wow.doge.mygame
|
|
|
|
import java.util.concurrent.TimeoutException
|
|
|
|
import scala.annotation.switch
|
|
import scala.concurrent.duration._
|
|
|
|
import akka.actor.typed.ActorRef
|
|
import akka.actor.typed.SpawnProtocol
|
|
import akka.util.Timeout
|
|
import cats.effect.Resource
|
|
import cats.effect.concurrent.Deferred
|
|
import cats.syntax.eq._
|
|
import cats.syntax.show._
|
|
import com.jayfella.jme.jfx.JavaFxUI
|
|
import com.jme3.asset.plugins.ZipLocator
|
|
import com.jme3.bullet.control.BetterCharacterControl
|
|
import com.jme3.input.InputManager
|
|
import com.jme3.material.Material
|
|
import com.jme3.material.MaterialDef
|
|
import com.jme3.math.ColorRGBA
|
|
import com.jme3.math.FastMath
|
|
import com.jme3.math.Quaternion
|
|
import com.jme3.math.Vector3f
|
|
import com.jme3.renderer.Camera
|
|
import com.jme3.renderer.ViewPort
|
|
import com.jme3.scene.Node
|
|
import com.softwaremill.macwire._
|
|
import com.softwaremill.tagging._
|
|
import io.odin.Logger
|
|
import monix.bio.Fiber
|
|
import monix.bio.IO
|
|
import monix.bio.Task
|
|
import monix.bio.UIO
|
|
import monix.eval.Coeval
|
|
import monix.execution.cancelables.CompositeCancelable
|
|
import monix.reactive.Observable
|
|
import monix.{eval => me}
|
|
import scalafx.scene.control.Label
|
|
import scalafx.scene.control.TextArea
|
|
import scalafx.scene.layout.HBox
|
|
import scalafx.scene.layout.Priority
|
|
import scalafx.scene.layout.VBox
|
|
import scalafx.scene.paint.Color
|
|
import wow.doge.mygame.AppError.TimeoutError
|
|
import wow.doge.mygame.executors.Schedulers
|
|
import wow.doge.mygame.game.GameApp
|
|
import wow.doge.mygame.game.GameAppResource
|
|
import wow.doge.mygame.game.controls.FollowControl
|
|
import wow.doge.mygame.game.entities.CharacterStats
|
|
import wow.doge.mygame.game.entities.EntityIds
|
|
import wow.doge.mygame.game.entities.NpcActorSupervisor
|
|
import wow.doge.mygame.game.entities.NpcMovementActor
|
|
import wow.doge.mygame.game.entities.player.PlayerActor
|
|
import wow.doge.mygame.game.entities.player.PlayerController
|
|
import wow.doge.mygame.game.entities.player.PlayerMovementReducer
|
|
import wow.doge.mygame.game.subsystems.input.InputMappings
|
|
import wow.doge.mygame.game.subsystems.input.PlayerCameraInput
|
|
import wow.doge.mygame.game.subsystems.input.PlayerMovementInput
|
|
import wow.doge.mygame.game.subsystems.level.DefaultGameLevel
|
|
import wow.doge.mygame.implicits._
|
|
import wow.doge.mygame.launcher.Launcher
|
|
import wow.doge.mygame.launcher.Launcher.LauncherResult
|
|
import wow.doge.mygame.math.ImVector3f
|
|
import wow.doge.mygame.subsystems.events.Event
|
|
import wow.doge.mygame.subsystems.events.EventBus
|
|
import wow.doge.mygame.subsystems.events.EventBus.ObservableSubscription
|
|
import wow.doge.mygame.subsystems.events.EventsModule
|
|
import wow.doge.mygame.subsystems.events.EventsModule.GameEventBus
|
|
import wow.doge.mygame.subsystems.events.PlayerCameraEvent
|
|
import wow.doge.mygame.subsystems.events.PlayerEvent
|
|
import wow.doge.mygame.subsystems.events.PlayerMovementEvent
|
|
import wow.doge.mygame.subsystems.events.StatsEvent.DamageEvent
|
|
import wow.doge.mygame.subsystems.events.TickEvent
|
|
import wow.doge.mygame.subsystems.scriptsystem.ScriptCompiler
|
|
import wow.doge.mygame.subsystems.scriptsystem.ScriptInitMode
|
|
import wow.doge.mygame.subsystems.scriptsystem.ScriptSystemResource
|
|
import wow.doge.mygame.types._
|
|
import wow.doge.mygame.utils.AkkaUtils
|
|
import wow.doge.mygame.utils.GenericConsoleStream
|
|
import wow.doge.mygame.utils.IOUtils
|
|
import wow.doge.mygame.utils.MonixDirectoryWatcher
|
|
import wow.doge.mygame.utils.MonixDirectoryWatcher.ModifyEvent
|
|
import wow.doge.mygame.utils.controls.JFXProgressBar
|
|
import wow.doge.mygame.utils.wrappers.jme
|
|
import wow.doge.mygame.utils.wrappers.jme.AssetManager
|
|
import wow.doge.mygame.utils.wrappers.jme.PhysicsSpace
|
|
|
|
class MainApp(
|
|
logger: Logger[Task],
|
|
jmeThread: JmeScheduler,
|
|
schedulers: Schedulers,
|
|
consoleStream: GenericConsoleStream[TextArea]
|
|
)(implicit
|
|
spawnProtocol: ActorRef[SpawnProtocol.Command],
|
|
timeout: Timeout,
|
|
scheduler: AkkaScheduler
|
|
) {
|
|
implicit val as = scheduler.value
|
|
|
|
val scriptSystemResource: Resource[UIO, ScriptCompiler] =
|
|
new ScriptSystemResource(os.pwd, logger, ScriptInitMode.Eager).init2
|
|
|
|
val eventsModule = new EventsModule(scheduler, spawnProtocol)
|
|
|
|
def eval(
|
|
tickEventBus: GameEventBus[TickEvent],
|
|
gameApp: GameApp,
|
|
fib: Fiber[Nothing, Unit]
|
|
) =
|
|
for {
|
|
// g <- UIO.pure(gameApp)
|
|
playerEventBus <- eventsModule.playerEventBus
|
|
mainEventBus <- eventsModule.mainEventBus
|
|
obs <-
|
|
playerEventBus
|
|
.askL[Observable[PlayerMovementEvent]](ObservableSubscription(_))
|
|
.onErrorHandleWith(TimeoutError.from)
|
|
_ <-
|
|
IOUtils
|
|
.toIO(
|
|
obs
|
|
.doOnNextF(pme =>
|
|
logger.trace(show"Received event $pme").toTask.void
|
|
)
|
|
.completedL
|
|
.startAndForget
|
|
)
|
|
.hideErrors
|
|
inputManager <- gameApp.inputManager
|
|
assetManager <- UIO.pure(gameApp.assetManager)
|
|
camera <- gameApp.camera
|
|
rootNode <- UIO.pure(gameApp.rootNode)
|
|
enqueueR <- UIO(gameApp.enqueue _)
|
|
viewPort <- gameApp.viewPort
|
|
physicsSpace <- UIO.pure(gameApp.physicsSpace)
|
|
_ <- logger.infoU("before")
|
|
jfxUI <- gameApp.jfxUI.hideErrors
|
|
consoleTextArea <- UIO(new TextArea {
|
|
text = "hello \n"
|
|
editable = false
|
|
wrapText = true
|
|
// maxHeight = 150
|
|
// maxWidth = 300
|
|
})
|
|
|
|
// _ <- Task(consoleStream := consoleTextArea)
|
|
// _ <- Task(jfxUI += consoleTextArea)
|
|
_ <- logger.infoU("after")
|
|
_ <- logger.infoU("Initializing console stream")
|
|
_ <-
|
|
wire[MainAppDelegate]
|
|
.init()
|
|
.executeOn(gameApp.scheduler.value)
|
|
} yield fib
|
|
|
|
def gameInit(
|
|
tickEventBus: GameEventBus[TickEvent]
|
|
): Resource[UIO, Either[AppError, Fiber[Nothing, Unit]]] =
|
|
for {
|
|
r1 <- wire[GameAppResource].resource.evalMap(e =>
|
|
IO.fromEither(e)
|
|
.flatMap {
|
|
case (gameApp -> gameAppFib) =>
|
|
eval(tickEventBus, gameApp, gameAppFib)
|
|
}
|
|
.attempt
|
|
)
|
|
dirWatcher <- Resource.liftF(
|
|
MonixDirectoryWatcher(
|
|
os.pwd / "assets" / "scripts"
|
|
).hideErrors
|
|
)
|
|
sc <- scriptSystemResource
|
|
obs = dirWatcher.doOnNext {
|
|
case ModifyEvent(file, count) =>
|
|
sc.request(ScriptCompiler.GetScript(os.Path(file.path), _, true))(
|
|
15.seconds
|
|
).toTask
|
|
.void
|
|
case _ => me.Task.unit
|
|
}
|
|
_ <- Resource.make(obs.completedL.toIO.hideErrors.start)(_.cancel)
|
|
// _ <-
|
|
// dirWatcher
|
|
// .doOnNextF(event =>
|
|
// Coeval(pprint.log(show"Received file event $event")).void
|
|
// )
|
|
// .completedL
|
|
// .executeOn(schedulers.io.value)
|
|
// .startAndForget
|
|
// .toIO
|
|
// .hideErrors
|
|
} yield r1
|
|
|
|
val program = for {
|
|
// scriptSystem <- scriptSystemInit
|
|
launchSignal <- Deferred[Task, Launcher.LauncherResult].hideErrors
|
|
launcher <- new Launcher.Props(schedulers.fx, launchSignal).create
|
|
launchResult <-
|
|
launcher.init
|
|
.use(_ => launchSignal.get)
|
|
.hideErrors
|
|
tickEventBus <-
|
|
eventsModule.tickEventBus.hideErrorsWith(e => new Exception(e.toString))
|
|
_ <-
|
|
/**
|
|
* User chose to quit
|
|
*/
|
|
if (launchResult === LauncherResult.Exit)
|
|
logger.infoU("Exiting")
|
|
/**
|
|
* User chose launch. Wait for game window to close
|
|
*/
|
|
else
|
|
gameInit(tickEventBus).use {
|
|
case Right(fib) => fib.join >> Task.unit
|
|
case Left(error) => IO.terminate(new Exception(error.toString))
|
|
}.hideErrors
|
|
|
|
} yield ()
|
|
}
|
|
|
|
/**
|
|
* Class with all dependencies in one place for easy wiring
|
|
*/
|
|
class MainAppDelegate(
|
|
gameApp: GameApp,
|
|
loggerL: Logger[Task],
|
|
mainEventBus: GameEventBus[Event],
|
|
playerEventBus: GameEventBus[PlayerEvent],
|
|
tickEventBus: GameEventBus[TickEvent],
|
|
inputManager: InputManager,
|
|
assetManager: AssetManager,
|
|
physicsSpace: PhysicsSpace,
|
|
camera: Camera,
|
|
viewPort: ViewPort,
|
|
enqueueR: Function1[() => Unit, Unit],
|
|
rootNode: RootNode,
|
|
schedulers: Schedulers,
|
|
jfxUI: JavaFxUI
|
|
)(implicit
|
|
spawnProtocol: ActorRef[SpawnProtocol.Command],
|
|
timeout: Timeout,
|
|
scheduler: AkkaScheduler
|
|
) {
|
|
implicit val as = scheduler.value
|
|
|
|
def init(
|
|
// appScheduler: monix.execution.Scheduler
|
|
// consoleStream: GenericConsoleStream[TextArea]
|
|
): IO[AppError, Unit] =
|
|
for {
|
|
_ <- loggerL.infoU("Initializing Systems")
|
|
_ <- assetManager.registerLocator(
|
|
os.rel / "assets" / "town.zip",
|
|
classOf[ZipLocator]
|
|
)
|
|
|
|
// _ <- Task(consoleStream.println("text"))
|
|
level <- DefaultGameLevel(assetManager, viewPort)
|
|
_ <- level.addToGame(rootNode, physicsSpace)
|
|
playerActor <- createPlayerController()
|
|
// .onErrorRestart(3)
|
|
// _ <- wire[GameInputHandler.Props].begin
|
|
_ <- new InputMappings(new jme.InputManager(inputManager)).setup
|
|
// .onErrorRestart(3)
|
|
johnActor <- createTestNpc("John")
|
|
// _ <- johnActor !! NpcActorSupervisor.Move(ImVector3f(0, 0, 20))
|
|
|
|
// _ <-
|
|
// johnActor
|
|
// .tellL(NpcActorSupervisor.Move(ImVector3f(-30, 0, 10)))
|
|
// .delayExecution(2.seconds)
|
|
_ <-
|
|
rootNode.depthFirstTraversal
|
|
.doOnNextF(spat => loggerL.debug(spat.getName).toTask)
|
|
.completedL
|
|
.toIO
|
|
.hideErrors
|
|
damageObs <-
|
|
mainEventBus
|
|
.askL[Observable[DamageEvent]](ObservableSubscription(_))
|
|
.onErrorHandleWith(TimeoutError.from)
|
|
_ <-
|
|
damageObs
|
|
.doOnNextF(event =>
|
|
(loggerL.debug(show"Received Damage Event $event") >>
|
|
(if (event.victimName === "PlayerNode")
|
|
playerActor
|
|
.askL(PlayerActor.TakeDamage(event.amount, _))
|
|
.void
|
|
.onErrorHandle { case ex: TimeoutException => () }
|
|
else IO.unit)).toTask
|
|
)
|
|
.completedL
|
|
.toIO
|
|
.hideErrors
|
|
.startAndForget
|
|
// _ <-
|
|
// Observable
|
|
// .interval(1.second)
|
|
// .doOnNextF(_ =>
|
|
// playerActor
|
|
// .askL(PlayerActorSupervisor.GetStatus)
|
|
// .flatMap(s => loggerL.debug(show"Player actor status: $s"))
|
|
|
|
// // .flatMap(s =>
|
|
// // if (s == Status.Alive)
|
|
// // playerActor
|
|
// // .askL(PlayerActorSupervisor.CurrentStats )
|
|
// // .flatMap(s => loggerL.debug(show"Got state $s"))
|
|
// // else IO.unit
|
|
// // )
|
|
// .toTask
|
|
// )
|
|
// // .doOnNextF(_ =>
|
|
// // playerActor
|
|
// // .askL(PlayerActorSupervisor.GetStatus )
|
|
// // .flatMap(s => loggerL.debug(show"Player actor status: $s"))
|
|
// // .toTask
|
|
// // )
|
|
// .completedL
|
|
// .toIO
|
|
// .hideErrors
|
|
// .startAndForget
|
|
_ <-
|
|
physicsSpace.collisionObservable
|
|
// .filter(event =>
|
|
// (for {
|
|
// nodeA <- event.nodeA
|
|
// nodeB <- event.nodeB
|
|
// } yield nodeA.getName === "PlayerNode" && nodeB.getName === "John" ||
|
|
// nodeB.getName === "PlayerNode" && nodeA.getName === "John")
|
|
// .getOrElse(false)
|
|
// )
|
|
// .doOnNextF(event =>
|
|
// loggerL
|
|
// .debug(show"$event ${event.appliedImpulse()}")
|
|
// .toTask
|
|
// )
|
|
.filter(_.nodeA.map(_.getName =!= "main-scene_node").getOrElse(false))
|
|
.filter(_.nodeB.map(_.getName =!= "main-scene_node").getOrElse(false))
|
|
.doOnNextF(event =>
|
|
(for {
|
|
victim <- Coeval(for {
|
|
nodeA <- event.nodeA
|
|
nodeB <- event.nodeB
|
|
} yield if (nodeB.getName === "John") nodeA else nodeB)
|
|
_ <- Coeval(
|
|
victim.foreach { v =>
|
|
pprint.log(show"emitted event ${v.getName}")
|
|
mainEventBus ! EventBus.Publish(
|
|
DamageEvent(
|
|
"John",
|
|
v.getName,
|
|
CharacterStats.DamageHealth(10)
|
|
),
|
|
"damageHandler"
|
|
)
|
|
}
|
|
)
|
|
} yield ()).void
|
|
)
|
|
.completedL
|
|
.toIO
|
|
.hideErrors
|
|
.startAndForget
|
|
// _ <-
|
|
// IOUtils
|
|
// .toIO(
|
|
// rootNode
|
|
// .observableBreadthFirst()
|
|
// .doOnNext(spat => IOUtils.toTask(loggerL.debug(spat.getName())))
|
|
// .completedL
|
|
// )
|
|
// .executeOn(appScheduler)
|
|
// .startAndForget
|
|
statsObs <-
|
|
playerActor
|
|
.askL(PlayerActor.GetStatsObservable(_))
|
|
.onErrorHandleWith(TimeoutError.from)
|
|
.flatten
|
|
playerHud <-
|
|
UIO
|
|
.deferAction(implicit s =>
|
|
UIO(new VBox {
|
|
implicit val c = CompositeCancelable()
|
|
hgrow = Priority.Always
|
|
spacing = 10
|
|
style = """-fx-background-color: rgba(0,0,0,0.7);"""
|
|
stylesheets = Seq((os.rel / "main.css").toString)
|
|
|
|
children = List(
|
|
new HBox {
|
|
spacing = 5
|
|
children = List(
|
|
new Label("Health") { textFill = Color.White },
|
|
new Label("100") {
|
|
text <-- statsObs
|
|
.doOnNextF(i => loggerL.trace(show"Received stats: $i"))
|
|
.map(_.hp.toInt.toString)
|
|
textFill = Color.White
|
|
},
|
|
new JFXProgressBar {
|
|
|
|
progress = 100
|
|
minHeight = 10
|
|
progress <-- statsObs
|
|
.map(_.hp.toInt.toDouble / 100)
|
|
c += statsObs
|
|
.scanEval(me.Task.pure("green-bar")) {
|
|
case (a, b) =>
|
|
me.Task(styleClass.removeAll(a)) >>
|
|
me.Task.pure(
|
|
(b.hp.toInt: @switch) match {
|
|
case v if v > 80 => "green-bar"
|
|
case v if v > 20 && v <= 80 => "yellow-bar"
|
|
case _ => "red-bar"
|
|
}
|
|
)
|
|
}
|
|
.doOnNext(cls => me.Task(styleClass += cls))
|
|
.subscribe()
|
|
}
|
|
)
|
|
},
|
|
new HBox {
|
|
spacing = 5
|
|
children = List(
|
|
new Label("Stamina") {
|
|
textFill = Color.White
|
|
|
|
},
|
|
new Label("100") {
|
|
textFill = Color.White
|
|
text <-- statsObs
|
|
.doOnNextF(i => loggerL.trace(show"Received stats: $i"))
|
|
.map(_.stamina.toInt.toString)
|
|
},
|
|
new JFXProgressBar {
|
|
|
|
progress = 100
|
|
minHeight = 10
|
|
progress <-- statsObs.map(_.stamina.toInt.toDouble / 100)
|
|
|
|
styleClass ++= Seq("green-bar")
|
|
|
|
c += statsObs
|
|
.scanEval(me.Task.pure("green-bar")) {
|
|
case (a, b) =>
|
|
me.Task(styleClass.removeAll(a)) >>
|
|
me.Task.pure(
|
|
(b.stamina.toInt: @switch) match {
|
|
case v if v > 80 => "green-bar"
|
|
case v if v > 20 && v <= 40 => "yellow-bar"
|
|
case _ => "red-bar"
|
|
}
|
|
)
|
|
}
|
|
.doOnNext(cls => me.Task(styleClass += cls))
|
|
.subscribe()
|
|
|
|
}
|
|
)
|
|
}
|
|
)
|
|
})
|
|
)
|
|
.executeOn(schedulers.fx.value)
|
|
_ <- UIO(jfxUI += playerHud)
|
|
} yield ()
|
|
|
|
def createPlayerController(
|
|
// appScheduler: monix.execution.Scheduler
|
|
): IO[AppError, PlayerActor.Ref] = {
|
|
val playerPos = ImVector3f.Zero
|
|
val modelPath = os.rel / "Models" / "Jaime" / "Jaime.j3o"
|
|
// val modelPath = os.rel / "Models" / "Oto" / "Oto.mesh.xml"
|
|
val playerPhysicsControl =
|
|
PlayerController.Defaults.defaultPlayerPhysicsControl
|
|
|
|
for {
|
|
playerModel <-
|
|
assetManager
|
|
.loadModelAs[Node](modelPath)
|
|
.map(_.withRotate(0, FastMath.PI, 0))
|
|
.tapEval(m => UIO(m.center()))
|
|
.mapError(AppError.AssetManagerError)
|
|
playerNode <- UIO(
|
|
PlayerController.Defaults
|
|
.defaultPlayerNode(
|
|
playerPos,
|
|
playerModel,
|
|
playerPhysicsControl
|
|
)
|
|
)
|
|
cameraPivotNode <- UIO(
|
|
new Node(EntityIds.CameraPivot.value)
|
|
.withControl(new FollowControl(playerNode))
|
|
.taggedWith[PlayerController.Tags.PlayerCameraPivotNode]
|
|
)
|
|
camNode <- UIO(
|
|
PlayerController.Defaults
|
|
.defaultCamerNode(camera, playerPos)
|
|
.taggedWith[PlayerController.Tags.PlayerCameraNode]
|
|
)
|
|
playerCameraEvents <-
|
|
playerEventBus
|
|
.askL[Observable[PlayerCameraEvent]](ObservableSubscription(_))
|
|
.onErrorHandleWith(TimeoutError.from)
|
|
_ <-
|
|
inputManager
|
|
.enumAnalogObservable(PlayerCameraInput)
|
|
.sample(1.millis)
|
|
.scan(new Quaternion) {
|
|
case (rotationBuf, action) =>
|
|
action.binding match {
|
|
case PlayerCameraInput.CameraRotateLeft =>
|
|
val rot = rotationBuf
|
|
.fromAngleAxis(1 * FastMath.DEG_TO_RAD, Vector3f.UNIT_Y)
|
|
cameraPivotNode.rotate(rot)
|
|
rotationBuf
|
|
case PlayerCameraInput.CameraRotateRight =>
|
|
val rot = rotationBuf
|
|
.fromAngleAxis(-1 * FastMath.DEG_TO_RAD, Vector3f.UNIT_Y)
|
|
cameraPivotNode.rotate(rot)
|
|
rotationBuf
|
|
case PlayerCameraInput.CameraRotateUp =>
|
|
val rot = rotationBuf
|
|
.fromAngleAxis(-1 * FastMath.DEG_TO_RAD, Vector3f.UNIT_X)
|
|
cameraPivotNode.rotate(rot)
|
|
rotationBuf
|
|
case PlayerCameraInput.CameraRotateDown =>
|
|
val rot = rotationBuf
|
|
.fromAngleAxis(1 * FastMath.DEG_TO_RAD, Vector3f.UNIT_X)
|
|
cameraPivotNode.rotate(rot)
|
|
rotationBuf
|
|
}
|
|
}
|
|
.completedL
|
|
.toIO
|
|
.hideErrors
|
|
.startAndForget
|
|
|
|
sched <- UIO.pure(schedulers.async)
|
|
fxSched <- UIO.pure(schedulers.fx)
|
|
playerActor <- wire[PlayerController.Props].create
|
|
playerMovementReducer = new PlayerMovementReducer(playerActor, loggerL)
|
|
_ <-
|
|
inputManager
|
|
.enumObservableAction(PlayerMovementInput)
|
|
.sample(1.millis)
|
|
.scanEval(me.Task.pure(PlayerMovementReducer.State.empty))(
|
|
playerMovementReducer.value
|
|
)
|
|
.completedL
|
|
.toIO
|
|
.hideErrors
|
|
.startAndForget
|
|
} yield playerActor
|
|
}
|
|
|
|
def createTestNpc(
|
|
// appScheduler: monix.execution.Scheduler,
|
|
npcName: String
|
|
): IO[AppError, NpcActorSupervisor.Ref] = {
|
|
val initialPos = ImVector3f(50, 5, 0)
|
|
val npcPhysicsControl = new BetterCharacterControl(1.5f, 6f, 1f)
|
|
// (1f, 2.1f, 10f)
|
|
.withJumpForce(ImVector3f(0, 5f, 0))
|
|
|
|
val npcActorTask = AkkaUtils.spawnActorL(
|
|
new NpcActorSupervisor.Props(
|
|
new NpcMovementActor.Props(
|
|
enqueueR,
|
|
initialPos,
|
|
npcName,
|
|
npcPhysicsControl
|
|
).behavior,
|
|
npcName,
|
|
initialPos
|
|
).behavior,
|
|
actorName = Some(show"${npcName}-npcActorSupervisor")
|
|
)
|
|
|
|
(for {
|
|
materialDef <-
|
|
assetManager
|
|
.loadAssetAs[MaterialDef](
|
|
os.rel / "Common" / "MatDefs" / "Misc" / "Unshaded.j3md"
|
|
)
|
|
.mapError(AppError.AssetManagerError)
|
|
material = new Material(materialDef)
|
|
_ = material.setColor("Color", ColorRGBA.Blue)
|
|
mesh = PlayerController.Defaults.defaultMesh.withMaterial(material)
|
|
npcNode = PlayerController.Defaults.defaultNpcNode(
|
|
mesh,
|
|
initialPos,
|
|
npcPhysicsControl,
|
|
npcName
|
|
)
|
|
_ <- (for {
|
|
_ <- physicsSpace += npcPhysicsControl
|
|
_ <- physicsSpace += npcNode
|
|
_ <- rootNode += npcNode
|
|
} yield ()).mapError(AppError.AppNodeError)
|
|
npcActor <- npcActorTask
|
|
} yield npcActor)
|
|
|
|
}
|
|
|
|
}
|