Many changes
Enhanced NPC state machine Made player movement direction depend on camera direction
This commit is contained in:
parent
73d657952f
commit
88293cebde
1
.gitignore
vendored
1
.gitignore
vendored
@ -14,6 +14,7 @@ project/plugins/project/
|
||||
metals.sbt
|
||||
.metals
|
||||
.bloop
|
||||
.ammonite
|
||||
|
||||
# Scala-IDE specific
|
||||
.scala_dependencies
|
||||
|
@ -10,21 +10,28 @@ import com.jme3.app.state.AppStateManager
|
||||
import com.jme3.asset.AssetManager
|
||||
import com.jme3.asset.plugins.ZipLocator
|
||||
import com.jme3.bullet.BulletAppState
|
||||
import com.jme3.bullet.control.BetterCharacterControl
|
||||
import com.jme3.input.InputManager
|
||||
import com.jme3.renderer.Camera
|
||||
import com.jme3.renderer.RenderManager
|
||||
import com.jme3.renderer.ViewPort
|
||||
import com.jme3.scene.Node
|
||||
import com.jme3.scene.control.AbstractControl
|
||||
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.execution.exceptions.DummyException
|
||||
import scalafx.scene.control.TextArea
|
||||
import wow.doge.mygame.executors.Schedulers
|
||||
import wow.doge.mygame.game.GameApp
|
||||
import wow.doge.mygame.game.GameAppActor
|
||||
import wow.doge.mygame.game.GameAppTags
|
||||
import wow.doge.mygame.game.entities.EntityIds
|
||||
import wow.doge.mygame.game.entities.NpcActorSupervisor
|
||||
import wow.doge.mygame.game.entities.NpcMovementActor2
|
||||
import wow.doge.mygame.game.entities.PlayerController
|
||||
import wow.doge.mygame.game.entities.PlayerControllerTags
|
||||
import wow.doge.mygame.game.subsystems.input.GameInputHandler
|
||||
@ -42,12 +49,9 @@ import wow.doge.mygame.subsystems.scriptsystem.ScriptInitMode
|
||||
import wow.doge.mygame.subsystems.scriptsystem.ScriptSystemResource
|
||||
import wow.doge.mygame.utils.AkkaUtils
|
||||
import wow.doge.mygame.utils.GenericConsoleStream
|
||||
import wow.doge.mygame.utils.IOUtils
|
||||
|
||||
import EventsModule.GameEventBus
|
||||
import wow.doge.mygame.game.entities.NpcMovementActor2
|
||||
import wow.doge.mygame.game.entities.NpcActorSupervisor
|
||||
import monix.execution.exceptions.DummyException
|
||||
import com.jme3.bullet.control.BetterCharacterControl
|
||||
|
||||
class MainApp(
|
||||
logger: Logger[Task],
|
||||
@ -189,9 +193,23 @@ class MainAppDelegate(
|
||||
_ <- createPlayerController(appScheduler)
|
||||
.absorbWith(e => DummyException("boom"))
|
||||
.onErrorRestart(3)
|
||||
johnActor <- createNpc(appScheduler, "John").executeOn(appScheduler)
|
||||
_ <- johnActor !! NpcActorSupervisor.Move(ImVector3f(0, 0, 20))
|
||||
_ <- wire[GameInputHandler.Props].begin.onErrorRestart(3)
|
||||
// johnActor <- createTestNpc(appScheduler, "John").executeOn(appScheduler)
|
||||
// _ <- johnActor !! NpcActorSupervisor.Move(ImVector3f(0, 0, 20))
|
||||
|
||||
// _ <- (johnActor !! NpcActorSupervisor.Move(
|
||||
// ImVector3f(-80, 0, 100)
|
||||
// )).executeAsync.delayExecution(2.seconds)
|
||||
_ <-
|
||||
IOUtils
|
||||
.toIO(
|
||||
rootNode
|
||||
.observableBreadthFirst()
|
||||
.doOnNext(spat => IOUtils.toTask(loggerL.debug(spat.getName())))
|
||||
.completedL
|
||||
)
|
||||
.executeOn(appScheduler)
|
||||
.startAndForget
|
||||
} yield ()
|
||||
|
||||
def createPlayerController(
|
||||
@ -199,28 +217,49 @@ class MainAppDelegate(
|
||||
): IO[PlayerController.Error, Unit] = {
|
||||
val playerPos = ImVector3f.ZERO
|
||||
val modelPath = os.rel / "Models" / "Jaime" / "Jaime.j3o"
|
||||
val playerPhysicsControl =
|
||||
lazy val playerPhysicsControl =
|
||||
PlayerController.Defaults.defaultPlayerPhysicsControl
|
||||
.taggedWith[PlayerControllerTags.PlayerTag]
|
||||
val camNode =
|
||||
PlayerController.Defaults
|
||||
.defaultCamerNode(camera, playerPos)
|
||||
.taggedWith[PlayerControllerTags.PlayerCameraNode]
|
||||
val mbPlayerNode = PlayerController.Defaults
|
||||
// lazy val camNode =
|
||||
// PlayerController.Defaults
|
||||
// .defaultCamerNode(camera, playerPos)
|
||||
// .taggedWith[PlayerControllerTags.PlayerCameraNode]
|
||||
lazy val mbPlayerNode = PlayerController.Defaults
|
||||
.defaultPlayerNode(
|
||||
assetManager,
|
||||
modelPath,
|
||||
playerPos,
|
||||
camNode,
|
||||
// camNode
|
||||
playerPhysicsControl
|
||||
)
|
||||
lazy val cameraPivotNode = new Node(EntityIds.CameraPivot.value)
|
||||
.taggedWith[PlayerControllerTags.PlayerCameraPivotNode]
|
||||
|
||||
for {
|
||||
playerNode <- IO.fromEither(mbPlayerNode)
|
||||
_ <- IO(cameraPivotNode.addControl(new FollowControl(playerNode)))
|
||||
.onErrorHandleWith(e =>
|
||||
IO.raiseError(PlayerController.GenericError(e.getMessage()))
|
||||
)
|
||||
camNode <- IO(
|
||||
PlayerController.Defaults
|
||||
.defaultCamerNode(camera, cameraPivotNode, playerNode, playerPos)
|
||||
).onErrorHandleWith(e =>
|
||||
IO.raiseError(PlayerController.GenericError(e.getMessage()))
|
||||
).map(_.taggedWith[PlayerControllerTags.PlayerCameraNode])
|
||||
// _ <- Task {
|
||||
// val chaseCam = new ChaseCamera(camera, playerNode, inputManager)
|
||||
// chaseCam.setSmoothMotion(false)
|
||||
// chaseCam.setLookAtOffset(new Vector3f(0, 1.5f, 10))
|
||||
// chaseCam
|
||||
// }
|
||||
// .onErrorHandleWith(e =>
|
||||
// IO.raiseError(PlayerController.GenericError(e.getMessage()))
|
||||
// )
|
||||
_ <- wire[PlayerController.Props].create
|
||||
} yield ()
|
||||
}
|
||||
def createNpc(
|
||||
def createTestNpc(
|
||||
appScheduler: monix.execution.Scheduler,
|
||||
npcName: String
|
||||
) =
|
||||
@ -243,7 +282,7 @@ class MainAppDelegate(
|
||||
assetManager,
|
||||
initialPos,
|
||||
npcPhysicsControl,
|
||||
"John"
|
||||
npcName
|
||||
)
|
||||
val npcActorTask = AkkaUtils.spawnActorL2(
|
||||
NpcActorSupervisor
|
||||
@ -251,14 +290,14 @@ class MainAppDelegate(
|
||||
new NpcMovementActor2.Props(
|
||||
enqueueR,
|
||||
initialPos,
|
||||
tickEventBus,
|
||||
// tickEventBus,
|
||||
npcPhysicsControl
|
||||
).create,
|
||||
npcName,
|
||||
initialPos
|
||||
)
|
||||
.create,
|
||||
s"${npcName}-npcMovementActorSupervisor"
|
||||
s"${npcName}-npcActorSupervisor"
|
||||
)
|
||||
// .taggedWith[PlayerControllerTags.PlayerTag]
|
||||
|
||||
@ -274,3 +313,13 @@ class MainAppDelegate(
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class FollowControl(playerNode: Node) extends AbstractControl {
|
||||
override def controlUpdate(tpf: Float): Unit = {
|
||||
this.spatial.setLocalTranslation(playerNode.getLocalTranslation())
|
||||
}
|
||||
override def controlRender(
|
||||
rm: RenderManager,
|
||||
vp: ViewPort
|
||||
): Unit = {}
|
||||
}
|
||||
|
@ -67,10 +67,7 @@ class SimpleAppExt(
|
||||
|
||||
object JMEExecutorService extends GUIExecutorService {
|
||||
override def execute(command: Runnable): Unit =
|
||||
enqueueScala(() => command.run())
|
||||
// enqueue(command)
|
||||
// new SingleThreadEventExecutor()
|
||||
// sys.addShutdownHook(JMEExecutorService.shutdown())
|
||||
enqueue(command)
|
||||
}
|
||||
|
||||
lazy val scheduler = Scheduler(JMEExecutorService)
|
||||
|
@ -0,0 +1,6 @@
|
||||
package wow.doge.mygame.game.entities
|
||||
|
||||
class EntityId(val value: String) extends AnyVal
|
||||
object EntityIds {
|
||||
val CameraPivot = new EntityId("CameraPivot")
|
||||
}
|
@ -1,35 +1,43 @@
|
||||
package wow.doge.mygame.game.entities
|
||||
|
||||
import scala.concurrent.duration._
|
||||
import scala.util.Failure
|
||||
import scala.util.Success
|
||||
|
||||
import akka.actor.typed.ActorRef
|
||||
import akka.actor.typed.Behavior
|
||||
import akka.actor.typed.SupervisorStrategy
|
||||
import akka.actor.typed.scaladsl.ActorContext
|
||||
import akka.actor.typed.scaladsl.Behaviors
|
||||
import akka.util.Timeout
|
||||
import monix.execution.CancelableFuture
|
||||
import monix.execution.CancelablePromise
|
||||
import wow.doge.mygame.game.subsystems.movement.CanMove
|
||||
import wow.doge.mygame.implicits._
|
||||
import wow.doge.mygame.math.ImVector3f
|
||||
import wow.doge.mygame.subsystems.events.EntityMovementEvent
|
||||
import wow.doge.mygame.subsystems.events.EntityMovementEvent.MovedDown
|
||||
import wow.doge.mygame.subsystems.events.EntityMovementEvent.MovedLeft
|
||||
import wow.doge.mygame.subsystems.events.EntityMovementEvent.MovedRight
|
||||
import wow.doge.mygame.subsystems.events.EntityMovementEvent.MovedUp
|
||||
import wow.doge.mygame.subsystems.events.Event
|
||||
import wow.doge.mygame.subsystems.events.EventBus
|
||||
import wow.doge.mygame.subsystems.events.EventsModule.GameEventBus
|
||||
import wow.doge.mygame.subsystems.events.TickEvent
|
||||
import wow.doge.mygame.subsystems.events.TickEvent.RenderTick
|
||||
import wow.doge.mygame.subsystems.movement.ImMovementActor
|
||||
import wow.doge.mygame.subsystems.events.EntityMovementEvent
|
||||
import wow.doge.mygame.subsystems.events.EntityMovementEvent.MovedLeft
|
||||
import wow.doge.mygame.subsystems.events.EntityMovementEvent.MovedUp
|
||||
import wow.doge.mygame.subsystems.events.EntityMovementEvent.MovedRight
|
||||
import wow.doge.mygame.subsystems.events.EntityMovementEvent.MovedDown
|
||||
import wow.doge.mygame.math.ImVector3f
|
||||
import wow.doge.mygame.game.subsystems.movement.CanMove
|
||||
import wow.doge.mygame.implicits._
|
||||
import akka.util.Timeout
|
||||
import scala.concurrent.duration._
|
||||
import scala.util.Success
|
||||
import scala.util.Failure
|
||||
|
||||
object NpcActorSupervisor {
|
||||
sealed trait Command
|
||||
case class Move(pos: ImVector3f) extends Command
|
||||
private case class UpdatePosition(pos: ImVector3f) extends Command
|
||||
final case class Move(pos: ImVector3f) extends Command
|
||||
private final case class InternalMove(
|
||||
move: Move,
|
||||
signal: CancelableFuture[NpcMovementActor2.DoneMoving.type]
|
||||
) extends Command
|
||||
private case object DoneMoving extends Command
|
||||
// private case class MovementResponse(response: CancelableFuture[_]) extends Command
|
||||
private case class LogError(err: Throwable) extends Command
|
||||
case object MovementTick extends Command
|
||||
private case object NoOp extends Command
|
||||
|
||||
final case class Props(
|
||||
npcMovementActorBehavior: Behavior[NpcMovementActor2.Command],
|
||||
@ -43,115 +51,186 @@ object NpcActorSupervisor {
|
||||
s"npc-${npcName}-NpcMovementActor"
|
||||
)
|
||||
|
||||
new NpcActorSupervisor(ctx, this)
|
||||
.idle(State(npcMovementActor, initialPos))
|
||||
new NpcActorSupervisor(ctx, this, Children(npcMovementActor))
|
||||
.idle(State())
|
||||
}
|
||||
}
|
||||
final case class State(
|
||||
npcMovementActor: ActorRef[NpcMovementActor2.Command],
|
||||
currentPos: ImVector3f
|
||||
)
|
||||
final case class Children(
|
||||
npcMovementActor: ActorRef[NpcMovementActor2.Command]
|
||||
)
|
||||
}
|
||||
class NpcActorSupervisor(
|
||||
ctx: ActorContext[NpcActorSupervisor.Command],
|
||||
props: NpcActorSupervisor.Props
|
||||
props: NpcActorSupervisor.Props,
|
||||
children: NpcActorSupervisor.Children
|
||||
) {
|
||||
import NpcActorSupervisor._
|
||||
implicit val timeout = Timeout(1.second)
|
||||
def idle(state: State): Behavior[NpcActorSupervisor.Command] =
|
||||
Behaviors.receiveMessage[Command] {
|
||||
case Move(pos) => {
|
||||
state.npcMovementActor ! NpcMovementActor2.Move(pos)
|
||||
val movementTimer = ctx.spawn(
|
||||
GenericTimerActor.Props(ctx.self, MovementTick, 100.millis).create,
|
||||
s"npc-${props.npcName}-NpcActorTimer"
|
||||
|
||||
private val movementTimer = ctx.spawn(
|
||||
GenericTimerActor
|
||||
.Props(
|
||||
children.npcMovementActor,
|
||||
NpcMovementActor2.MovementTick,
|
||||
100.millis
|
||||
)
|
||||
movementTimer ! GenericTimerActor.Start
|
||||
moving(state, pos, movementTimer)
|
||||
.create,
|
||||
s"npc-John-NpcActorTimer"
|
||||
)
|
||||
|
||||
def idle(state: State): Behavior[NpcActorSupervisor.Command] =
|
||||
Behaviors.setup { _ =>
|
||||
ctx.log.info("Inside Idle State")
|
||||
Behaviors.receiveMessage[Command] {
|
||||
case m @ Move(pos) =>
|
||||
ctx.ask(
|
||||
children.npcMovementActor,
|
||||
NpcMovementActor2.MoveTo(pos, _)
|
||||
) {
|
||||
case Success(signal) => InternalMove(m, signal)
|
||||
case Failure(exception) => LogError(exception)
|
||||
}
|
||||
Behaviors.same
|
||||
case InternalMove(move, signal) =>
|
||||
moving(state, move.pos, signal)
|
||||
|
||||
case LogError(err) =>
|
||||
ctx.log.warn(err.getMessage())
|
||||
Behaviors.same
|
||||
case _ => Behaviors.unhandled
|
||||
}
|
||||
}
|
||||
|
||||
def moving(
|
||||
state: State,
|
||||
targetPos: ImVector3f,
|
||||
movementTimer: ActorRef[GenericTimerActor.Command]
|
||||
signal: CancelableFuture[NpcMovementActor2.DoneMoving.type]
|
||||
): Behavior[NpcActorSupervisor.Command] =
|
||||
Behaviors.receiveMessagePartial[Command] {
|
||||
case LogError(err) =>
|
||||
ctx.log.warn(err.getMessage())
|
||||
Behaviors.same
|
||||
case Move(pos) => moving(state, pos, movementTimer)
|
||||
case UpdatePosition(pos) =>
|
||||
ctx.log.trace("Current pos = " + state.currentPos.toString())
|
||||
moving(state.copy(currentPos = pos), targetPos, movementTimer)
|
||||
case MovementTick =>
|
||||
val dst = ImVector3f.dst(targetPos, state.currentPos)
|
||||
if (dst <= 10f) {
|
||||
state.npcMovementActor ! NpcMovementActor2.StopMoving
|
||||
movementTimer ! GenericTimerActor.Stop
|
||||
idle(state)
|
||||
} else {
|
||||
// ctx.log.debug("Difference = " + dst.toString())
|
||||
// ctx.log.debug("Current pos = " + state.currentPos.toString())
|
||||
Behaviors.setup { _ =>
|
||||
movementTimer ! GenericTimerActor.Start
|
||||
|
||||
ctx.ask(state.npcMovementActor, NpcMovementActor2.AskPosition(_)) {
|
||||
case Success(value) =>
|
||||
UpdatePosition(value)
|
||||
// ctx
|
||||
// .ask(state.npcMovementActor, NpcMovementActor2.MoveTo(targetPos, _))(
|
||||
// _.fold(LogError(_), MovementResponse(_))
|
||||
// )
|
||||
ctx.pipeToSelf(signal) {
|
||||
case Success(value) => DoneMoving
|
||||
case Failure(exception) => LogError(exception)
|
||||
}
|
||||
// Behaviors.same
|
||||
moving(state, targetPos, movementTimer)
|
||||
Behaviors.receiveMessagePartial[Command] {
|
||||
case LogError(err) =>
|
||||
ctx.log.error(err.getMessage())
|
||||
Behaviors.same
|
||||
case m @ Move(pos) =>
|
||||
movementTimer ! GenericTimerActor.Stop
|
||||
children.npcMovementActor ! NpcMovementActor2.StopMoving
|
||||
signal.cancel()
|
||||
ctx.ask(
|
||||
children.npcMovementActor,
|
||||
NpcMovementActor2.MoveTo(pos, _)
|
||||
) {
|
||||
case Success(signal) => InternalMove(m, signal)
|
||||
case Failure(exception) => LogError(exception)
|
||||
}
|
||||
Behaviors.same
|
||||
case InternalMove(move, signal) =>
|
||||
moving(state, targetPos, signal)
|
||||
case NoOp => Behaviors.same
|
||||
// case MovementResponse(x: CancelableFuture[_]) =>
|
||||
// // ctx.pipeToSelf(x)(_.)
|
||||
case DoneMoving =>
|
||||
movementTimer ! GenericTimerActor.Stop
|
||||
idle(state)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object NpcMovementActor2 {
|
||||
|
||||
case object DoneMoving
|
||||
|
||||
sealed trait Command
|
||||
case class AskPosition(replyTo: ActorRef[ImVector3f]) extends Command
|
||||
case object MovementTick extends Command
|
||||
case object StopMoving extends Command
|
||||
case class Move(target: ImVector3f) extends Command
|
||||
case class MoveTo(
|
||||
target: ImVector3f,
|
||||
doneSignal: ActorRef[CancelableFuture[DoneMoving.type]]
|
||||
) extends Command
|
||||
|
||||
final class Props[T: CanMove](
|
||||
val enqueueR: Function1[() => Unit, Unit],
|
||||
val initialPos: ImVector3f,
|
||||
val tickEventBus: GameEventBus[TickEvent],
|
||||
// val tickEventBus: GameEventBus[TickEvent],
|
||||
val movable: T
|
||||
) {
|
||||
def create =
|
||||
Behaviors.setup[Command] { ctx =>
|
||||
new NpcMovementActor2(ctx, this).receive(State(initialPos))
|
||||
new NpcMovementActor2(ctx, this).receive(State())
|
||||
}
|
||||
}
|
||||
|
||||
final case class State(currentPos: ImVector3f)
|
||||
final case class State()
|
||||
}
|
||||
class NpcMovementActor2[T](
|
||||
ctx: ActorContext[NpcMovementActor2.Command],
|
||||
props: NpcMovementActor2.Props[T]
|
||||
) {
|
||||
)(implicit cm: CanMove[T]) {
|
||||
import NpcMovementActor2._
|
||||
|
||||
def location = cm.location(props.movable)
|
||||
|
||||
def receive(
|
||||
state: State
|
||||
)(implicit cm: CanMove[T]): Behavior[NpcMovementActor2.Command] =
|
||||
Behaviors.receiveMessage[Command] {
|
||||
): Behavior[NpcMovementActor2.Command] =
|
||||
Behaviors.receiveMessagePartial {
|
||||
case AskPosition(replyTo) =>
|
||||
replyTo ! cm.location(props.movable)
|
||||
replyTo ! location
|
||||
Behaviors.same
|
||||
case Move(target: ImVector3f) =>
|
||||
props.enqueueR(() =>
|
||||
cm.move(props.movable, (target - state.currentPos) * 0.005f)
|
||||
)
|
||||
receive(state = state.copy(currentPos = cm.location(props.movable)))
|
||||
case StopMoving =>
|
||||
ctx.log.debug(
|
||||
"Position at Stop = " + cm.location(props.movable).toString
|
||||
"Position at Stop = " + location.toString
|
||||
)
|
||||
props.enqueueR(() => cm.stop(props.movable))
|
||||
receive(state = state.copy(currentPos = cm.location(props.movable)))
|
||||
receive(state)
|
||||
case MoveTo(
|
||||
target: ImVector3f,
|
||||
replyTo: ActorRef[CancelableFuture[DoneMoving.type]]
|
||||
) =>
|
||||
props.enqueueR(() => cm.move(props.movable, target - location))
|
||||
val p = CancelablePromise[DoneMoving.type]()
|
||||
replyTo ! p.future
|
||||
ticking(p, target, state)
|
||||
|
||||
}
|
||||
|
||||
def ticking(
|
||||
reachDestination: CancelablePromise[DoneMoving.type],
|
||||
targetPos: ImVector3f,
|
||||
state: State
|
||||
): Behavior[NpcMovementActor2.Command] =
|
||||
Behaviors.receiveMessagePartial {
|
||||
case StopMoving =>
|
||||
ctx.log.debug(
|
||||
"Position at Stop = " + location.toString
|
||||
)
|
||||
props.enqueueR(() => cm.stop(props.movable))
|
||||
receive(state)
|
||||
|
||||
case MovementTick =>
|
||||
val dst = ImVector3f.dst(targetPos, location)
|
||||
if (dst <= 10f) {
|
||||
ctx.self ! StopMoving
|
||||
reachDestination.success(DoneMoving)
|
||||
receive(state)
|
||||
} else {
|
||||
ctx.log.trace("Difference = " + dst.toString())
|
||||
ctx.log.trace("Current pos = " + location.toString())
|
||||
Behaviors.same
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -30,7 +30,7 @@ object PlayerActorSupervisor {
|
||||
playerCameraEventBus: ActorRef[EventBus.Command[PlayerCameraEvent]],
|
||||
tickEventBus: GameEventBus[TickEvent],
|
||||
imMovementActorBehavior: Behavior[ImMovementActor.Command],
|
||||
playerCamELBehavior: Behavior[PlayerCameraEvent]
|
||||
playerCameraActorBehavior: Behavior[PlayerCameraActor.Command]
|
||||
) {
|
||||
def create[T: CanMove](movable: T) =
|
||||
Behaviors.logMessages(
|
||||
@ -50,23 +50,29 @@ object PlayerActorSupervisor {
|
||||
.onFailure[Exception](SupervisorStrategy.restart),
|
||||
"playerMovementActorChild"
|
||||
)
|
||||
|
||||
val playerCameraActor =
|
||||
ctx.spawn(playerCameraActorBehavior, "playerCameraActor")
|
||||
|
||||
val playerCameraEl = ctx.spawn(
|
||||
PlayerCameraEventListener(playerCameraActor),
|
||||
"playerCameraActorEl"
|
||||
)
|
||||
|
||||
ctx.spawn(
|
||||
PlayerMovementActor
|
||||
.Props(movementActor, playerMovementEventBus, tickEventBus)
|
||||
.Props(
|
||||
movementActor,
|
||||
playerCameraActor,
|
||||
playerMovementEventBus,
|
||||
tickEventBus
|
||||
)
|
||||
.create,
|
||||
"playerMovementAcor"
|
||||
)
|
||||
lazy val playerCameraHandler = {
|
||||
ctx.spawn(
|
||||
Behaviors
|
||||
.supervise(playerCamELBehavior)
|
||||
.onFailure[Exception](SupervisorStrategy.restart),
|
||||
"playerCameraHandler"
|
||||
)
|
||||
}
|
||||
|
||||
//init actors
|
||||
playerCameraEventBus ! EventBus.Subscribe(playerCameraHandler)
|
||||
playerCameraEventBus ! EventBus.Subscribe(playerCameraEl)
|
||||
|
||||
new PlayerActorSupervisor(
|
||||
ctx,
|
||||
@ -100,6 +106,7 @@ object PlayerMovementActor {
|
||||
sealed trait Command
|
||||
final case class Props(
|
||||
movementActor: ActorRef[ImMovementActor.Command],
|
||||
playerCameraActor: ActorRef[PlayerCameraActor.Command],
|
||||
playerMovementEventBus: ActorRef[
|
||||
EventBus.Command[PlayerMovementEvent]
|
||||
],
|
||||
@ -118,14 +125,13 @@ object PlayerMovementActor {
|
||||
Behaviors.receiveMessage[RenderTick.type] {
|
||||
case RenderTick =>
|
||||
movementActor ! ImMovementActor.Tick
|
||||
// playerCameraActor ! PlayerCameraActor.Tick
|
||||
Behaviors.same
|
||||
}
|
||||
val renderTickEl =
|
||||
ctx.spawn(renderTickElBehavior, "playerMovementTickListener")
|
||||
|
||||
playerMovementEventBus ! EventBus.Subscribe(
|
||||
playerMovementEl
|
||||
)
|
||||
playerMovementEventBus ! EventBus.Subscribe(playerMovementEl)
|
||||
tickEventBus ! EventBus.Subscribe(renderTickEl)
|
||||
Behaviors.receiveMessage { msg => Behaviors.same }
|
||||
}
|
||||
|
@ -1,16 +1,40 @@
|
||||
package wow.doge.mygame.game.entities
|
||||
|
||||
import akka.actor.typed.Behavior
|
||||
import akka.actor.typed.LogOptions
|
||||
import akka.actor.typed.scaladsl.ActorContext
|
||||
import akka.actor.typed.scaladsl.Behaviors
|
||||
import com.jme3.math.FastMath
|
||||
import com.jme3.math.Quaternion
|
||||
import com.jme3.math.Vector3f
|
||||
import com.jme3.scene.Node
|
||||
import com.typesafe.scalalogging.Logger
|
||||
import org.slf4j.event.Level
|
||||
|
||||
object PlayerCameraActor {
|
||||
sealed trait Command
|
||||
case object RotateLeft extends Command
|
||||
case object RotateRight extends Command
|
||||
case object RotateUp extends Command
|
||||
case object RotateDown extends Command
|
||||
case object Tick extends Command
|
||||
|
||||
class Props() {
|
||||
class Props(
|
||||
val cameraPivotNode: Node,
|
||||
val enqueueR: Function1[() => Unit, Unit],
|
||||
val getPlayerLocation: Function0[Vector3f]
|
||||
) {
|
||||
def create =
|
||||
Behaviors.logMessages(
|
||||
LogOptions()
|
||||
.withLevel(Level.TRACE)
|
||||
.withLogger(
|
||||
Logger[PlayerCameraActor].underlying
|
||||
),
|
||||
Behaviors.setup[Command] { ctx =>
|
||||
new PlayerCameraActor(ctx, this).receive(State.empty)
|
||||
new PlayerCameraActor(ctx, this).receive()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
case class State()
|
||||
@ -23,8 +47,36 @@ class PlayerCameraActor(
|
||||
props: PlayerCameraActor.Props
|
||||
) {
|
||||
import PlayerCameraActor._
|
||||
def receive(state: State) =
|
||||
Behaviors.receiveMessage[Command] {
|
||||
case _ => Behaviors.same
|
||||
def receive(
|
||||
rotationBuf: Quaternion = new Quaternion(),
|
||||
state: State = State.empty
|
||||
): Behavior[Command] =
|
||||
Behaviors.receiveMessage {
|
||||
case RotateLeft =>
|
||||
val rot = rotationBuf
|
||||
.fromAngleAxis(1 * FastMath.DEG_TO_RAD, Vector3f.UNIT_Y)
|
||||
props.enqueueR(() => props.cameraPivotNode.rotate(rot))
|
||||
Behaviors.same
|
||||
case RotateRight =>
|
||||
val rot = rotationBuf
|
||||
.fromAngleAxis(-1 * FastMath.DEG_TO_RAD, Vector3f.UNIT_Y)
|
||||
props.enqueueR(() => props.cameraPivotNode.rotate(rot))
|
||||
Behaviors.same
|
||||
case RotateUp =>
|
||||
val rot = rotationBuf
|
||||
.fromAngleAxis(-1 * FastMath.DEG_TO_RAD, Vector3f.UNIT_X)
|
||||
props.enqueueR(() => props.cameraPivotNode.rotate(rot))
|
||||
Behaviors.same
|
||||
case RotateDown =>
|
||||
val rot = rotationBuf
|
||||
.fromAngleAxis(1 * FastMath.DEG_TO_RAD, Vector3f.UNIT_X)
|
||||
props.enqueueR(() => props.cameraPivotNode.rotate(rot))
|
||||
Behaviors.same
|
||||
case Tick =>
|
||||
props.enqueueR(() => {
|
||||
val location = props.getPlayerLocation()
|
||||
props.cameraPivotNode.setLocalTranslation(location)
|
||||
})
|
||||
Behaviors.same
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
package wow.doge.mygame.game.entities
|
||||
|
||||
import akka.actor.typed.ActorRef
|
||||
import akka.actor.typed.Props
|
||||
import akka.actor.typed.Scheduler
|
||||
import akka.actor.typed.SpawnProtocol
|
||||
import akka.util.Timeout
|
||||
@ -15,9 +14,7 @@ import com.jme3.renderer.Camera
|
||||
import com.jme3.scene.CameraNode
|
||||
import com.jme3.scene.Geometry
|
||||
import com.jme3.scene.Node
|
||||
import com.jme3.scene.control.CameraControl.ControlDirection
|
||||
import com.jme3.scene.shape.Box
|
||||
import com.softwaremill.macwire._
|
||||
import com.softwaremill.tagging._
|
||||
import io.odin.Logger
|
||||
import monix.bio.IO
|
||||
@ -38,6 +35,7 @@ import wow.doge.mygame.utils.AkkaUtils
|
||||
object PlayerControllerTags {
|
||||
sealed trait PlayerTag
|
||||
sealed trait PlayerCameraNode
|
||||
sealed trait PlayerCameraPivotNode
|
||||
}
|
||||
|
||||
object PlayerController {
|
||||
@ -60,7 +58,9 @@ object PlayerController {
|
||||
appScheduler: monix.execution.Scheduler,
|
||||
playerNode: Node @@ PlayerControllerTags.PlayerTag,
|
||||
cameraNode: CameraNode @@ PlayerControllerTags.PlayerCameraNode,
|
||||
tickEventBus: GameEventBus[TickEvent]
|
||||
cameraPivotNode: Node @@ PlayerControllerTags.PlayerCameraPivotNode,
|
||||
tickEventBus: GameEventBus[TickEvent],
|
||||
camera: Camera
|
||||
)(implicit timeout: Timeout, scheduler: Scheduler) {
|
||||
val create: IO[Error, Unit] =
|
||||
(for {
|
||||
@ -71,17 +71,31 @@ object PlayerController {
|
||||
playerMovementEventBus,
|
||||
playerCameraEventBus,
|
||||
tickEventBus,
|
||||
ImMovementActor
|
||||
.Props(enqueueR, playerPhysicsControl)
|
||||
.create,
|
||||
wireWith(PlayerCameraEventListener.apply _)
|
||||
new ImMovementActor.Props(
|
||||
enqueueR,
|
||||
playerPhysicsControl,
|
||||
camera
|
||||
).create,
|
||||
// wireWith(PlayerCameraEventListener.apply _)
|
||||
// PlayerCameraEventListener()
|
||||
new PlayerCameraActor.Props(
|
||||
cameraPivotNode,
|
||||
enqueueR,
|
||||
playerNode.getWorldTranslation _
|
||||
).create
|
||||
).create(playerPhysicsControl)
|
||||
)
|
||||
_ <- Task(rootNode += playerNode)
|
||||
_ <- IO {
|
||||
physicsSpace += playerNode
|
||||
physicsSpace += playerPhysicsControl
|
||||
// rootNode += cameraNode
|
||||
cameraPivotNode += cameraNode
|
||||
// playerNode += cameraPivotNode
|
||||
rootNode += cameraPivotNode
|
||||
|
||||
}
|
||||
_ <- Task(rootNode += playerNode)
|
||||
|
||||
} yield ())
|
||||
.onErrorHandleWith(e => IO.raiseError(GenericError(e.getMessage())))
|
||||
.executeOn(appScheduler)
|
||||
@ -129,9 +143,27 @@ object PlayerController {
|
||||
path = os.rel / "Common" / "MatDefs" / "Misc" / "Unshaded.j3md"
|
||||
)
|
||||
|
||||
def defaultCamerNode(cam: Camera, playerPos: ImVector3f) =
|
||||
new CameraNode("CameraNode", cam)
|
||||
.withControlDir(ControlDirection.SpatialToCamera)
|
||||
// new CameraControl(cam) {
|
||||
// override def controlUpdate(tpf: Float) = {
|
||||
// this.getCamera().setRotation(spatial.getWorldRotation())
|
||||
// cameraPivotNode.setLocalTranslation(
|
||||
// playerNode.getWorldTranslation()
|
||||
// )
|
||||
// this.getCamera().setLocation(spatial.getWorldTranslation())
|
||||
// }
|
||||
// }
|
||||
|
||||
def defaultCamerNode(
|
||||
cam: Camera,
|
||||
playerNode: Node,
|
||||
cameraPivotNode: Node,
|
||||
playerPos: ImVector3f
|
||||
) =
|
||||
new CameraNode(
|
||||
"CameraNode",
|
||||
cam
|
||||
)
|
||||
// .withControlDir(ControlDirection.SpatialToCamera)
|
||||
.withLocalTranslation(ImVector3f(0, 1.5f, 10))
|
||||
.withLookAt(playerPos, ImVector3f.UNIT_Y)
|
||||
|
||||
@ -139,14 +171,14 @@ object PlayerController {
|
||||
assetManager: AssetManager,
|
||||
modelPath: os.RelPath,
|
||||
playerPos: ImVector3f,
|
||||
camNode: CameraNode,
|
||||
// camNode: CameraNode,
|
||||
playerPhysicsControl: BetterCharacterControl
|
||||
) =
|
||||
Either
|
||||
.catchNonFatal(
|
||||
Node("PlayerNode")
|
||||
.withChildren(
|
||||
camNode,
|
||||
// camNode,
|
||||
assetManager
|
||||
.loadModel(modelPath)
|
||||
.asInstanceOf[Node]
|
||||
@ -187,45 +219,7 @@ object PlayerController {
|
||||
}
|
||||
}
|
||||
|
||||
object Methods {
|
||||
def spawnMovementActor(
|
||||
enqueueR: Function1[() => Unit, Unit],
|
||||
spawnProtocol: ActorRef[SpawnProtocol.Command],
|
||||
movable: BetterCharacterControl @@ PlayerControllerTags.PlayerTag,
|
||||
playerMovementEventBus: ActorRef[
|
||||
EventBus.Command[PlayerMovementEvent]
|
||||
],
|
||||
loggerL: Logger[Task]
|
||||
)(implicit timeout: Timeout, scheduler: Scheduler) =
|
||||
spawnProtocol.askL[ActorRef[ImMovementActor.Command]](
|
||||
SpawnProtocol.Spawn(
|
||||
ImMovementActor.Props(enqueueR, movable).create,
|
||||
"imMovementActor",
|
||||
Props.empty,
|
||||
_
|
||||
)
|
||||
)
|
||||
// def spawnPlayerActor(
|
||||
// app: GameApp,
|
||||
// spawnProtocol: ActorRef[SpawnProtocol.Command],
|
||||
// movable: BetterCharacterControl @@ Player,
|
||||
// playerMovementEventBus: ActorRef[
|
||||
// EventBus.Command[PlayerMovementEvent]
|
||||
// ]
|
||||
// )(implicit timeout: Timeout, scheduler: Scheduler) =
|
||||
// spawnProtocol.askL[ActorRef[PlayerActorSupervisor.Command]](
|
||||
// SpawnProtocol.Spawn(
|
||||
// new PlayerActorSupervisor.Props(
|
||||
// app,
|
||||
// movable,
|
||||
// playerMovementEventBus
|
||||
// ).create,
|
||||
// "playerActor",
|
||||
// Props.empty,
|
||||
// _
|
||||
// )
|
||||
// )
|
||||
}
|
||||
object Methods {}
|
||||
|
||||
// camNode <- IO(
|
||||
// _cameraNode.getOrElse(defaultCamerNode(camera, initialPlayerPos))
|
||||
|
@ -3,18 +3,17 @@ package wow.doge.mygame.game.entities
|
||||
import akka.actor.typed.ActorRef
|
||||
import akka.actor.typed.LogOptions
|
||||
import akka.actor.typed.scaladsl.Behaviors
|
||||
import com.jme3.scene.CameraNode
|
||||
import com.typesafe.scalalogging.Logger
|
||||
import org.slf4j.event.Level
|
||||
import wow.doge.mygame.subsystems.events.PlayerCameraEvent
|
||||
import wow.doge.mygame.subsystems.events.PlayerCameraEvent.CameraMovedDown
|
||||
import wow.doge.mygame.subsystems.events.PlayerCameraEvent.CameraMovedUp
|
||||
import wow.doge.mygame.subsystems.events.PlayerMovementEvent
|
||||
import wow.doge.mygame.subsystems.movement.ImMovementActor
|
||||
|
||||
object PlayerMovementEventListener {
|
||||
import PlayerMovementEvent._
|
||||
def apply(movementActor: ActorRef[ImMovementActor.Command]) =
|
||||
def apply(
|
||||
movementActor: ActorRef[ImMovementActor.Command]
|
||||
) =
|
||||
Behaviors.logMessages(
|
||||
LogOptions()
|
||||
.withLevel(Level.TRACE)
|
||||
@ -22,7 +21,7 @@ object PlayerMovementEventListener {
|
||||
Logger[PlayerMovementEventListener.type].underlying
|
||||
),
|
||||
Behaviors.setup[PlayerMovementEvent](ctx =>
|
||||
Behaviors.receiveMessagePartial {
|
||||
Behaviors.receiveMessage {
|
||||
case PlayerMovedLeft(pressed) =>
|
||||
movementActor ! ImMovementActor.MovedLeft(pressed)
|
||||
Behaviors.same
|
||||
@ -38,27 +37,21 @@ object PlayerMovementEventListener {
|
||||
case PlayerJumped =>
|
||||
movementActor ! ImMovementActor.Jump
|
||||
Behaviors.same
|
||||
case PlayerRotatedRight =>
|
||||
movementActor ! ImMovementActor.RotateRight
|
||||
Behaviors.same
|
||||
case PlayerRotatedLeft =>
|
||||
movementActor ! ImMovementActor.RotateLeft
|
||||
Behaviors.same
|
||||
case PlayerCameraUp =>
|
||||
ctx.log.warn("camera up not implemented yet")
|
||||
Behaviors.same
|
||||
case PlayerCameraDown =>
|
||||
ctx.log.warn("camera down not implemented yet")
|
||||
Behaviors.same
|
||||
// case PlayerTurnedRight =>
|
||||
// movementActor ! ImMovementActor.RotateRight
|
||||
// Behaviors.same
|
||||
// case PlayerTurnedLeft =>
|
||||
// movementActor ! ImMovementActor.RotateLeft
|
||||
// Behaviors.same
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
object PlayerCameraEventListener {
|
||||
import PlayerCameraEvent._
|
||||
def apply(
|
||||
camNode: CameraNode,
|
||||
enqueueR: Function1[() => Unit, Unit]
|
||||
playerCameraActor: ActorRef[PlayerCameraActor.Command]
|
||||
) =
|
||||
Behaviors.logMessages(
|
||||
LogOptions()
|
||||
@ -69,16 +62,17 @@ object PlayerCameraEventListener {
|
||||
Behaviors.setup[PlayerCameraEvent](ctx =>
|
||||
Behaviors.receiveMessagePartial {
|
||||
case CameraMovedUp =>
|
||||
enqueueR(() => {
|
||||
playerCameraActor ! PlayerCameraActor.RotateUp
|
||||
|
||||
camNode.move(0, 1, 0)
|
||||
})
|
||||
Behaviors.same
|
||||
case CameraMovedDown =>
|
||||
enqueueR(() => {
|
||||
|
||||
camNode.move(0, -1, 0)
|
||||
})
|
||||
playerCameraActor ! PlayerCameraActor.RotateDown
|
||||
Behaviors.same
|
||||
case CameraLeft =>
|
||||
playerCameraActor ! PlayerCameraActor.RotateLeft
|
||||
Behaviors.same
|
||||
case CameraRight =>
|
||||
playerCameraActor ! PlayerCameraActor.RotateRight
|
||||
Behaviors.same
|
||||
}
|
||||
)
|
||||
|
@ -3,11 +3,11 @@ package wow.doge.mygame.game.subsystems.ai
|
||||
import scala.collection.immutable.ArraySeq
|
||||
|
||||
import com.badlogic.gdx.ai.pfa.Connection
|
||||
import wow.doge.mygame.game.subsystems.ai.gdx.MyIndexedGraph
|
||||
import com.badlogic.gdx.ai.steer.Steerable
|
||||
import com.badlogic.gdx.math.Vector3
|
||||
import com.badlogic.gdx.ai.utils.Location
|
||||
import com.badlogic.gdx.ai.steer.behaviors.Arrive
|
||||
import com.badlogic.gdx.ai.utils.Location
|
||||
import com.badlogic.gdx.math.Vector3
|
||||
import wow.doge.mygame.game.subsystems.ai.gdx.MyIndexedGraph
|
||||
// import com.badlogic.gdx.ai.pfa.indexed.IndexedGraph
|
||||
// import scala.jdk.javaapi.CollectionConverters._
|
||||
|
||||
|
@ -3,12 +3,14 @@ package wow.doge.mygame.game.subsystems.input
|
||||
import scala.concurrent.duration._
|
||||
|
||||
import akka.actor.typed.ActorRef
|
||||
import cats.effect.concurrent.Ref
|
||||
import com.jme3.input.InputManager
|
||||
import com.jme3.input.KeyInput
|
||||
import com.jme3.input.MouseInput
|
||||
import com.jme3.input.controls.KeyTrigger
|
||||
import com.jme3.input.controls.MouseAxisTrigger
|
||||
import monix.bio.UIO
|
||||
import monix.{eval => me}
|
||||
import wow.doge.mygame.implicits._
|
||||
import wow.doge.mygame.subsystems.events.EventBus
|
||||
import wow.doge.mygame.subsystems.events.EventsModule.GameEventBus
|
||||
@ -18,7 +20,7 @@ import wow.doge.mygame.utils.IOUtils._
|
||||
|
||||
object GameInputHandler {
|
||||
|
||||
final case class Props(
|
||||
final class Props(
|
||||
inputManager: InputManager,
|
||||
playerMovementEventBus: GameEventBus[PlayerMovementEvent],
|
||||
playerCameraEventBus: GameEventBus[PlayerCameraEvent]
|
||||
@ -27,7 +29,8 @@ object GameInputHandler {
|
||||
def begin =
|
||||
for {
|
||||
_ <- UIO(setupMovementKeys(inputManager))
|
||||
_ <- UIO(setupKeys(inputManager))
|
||||
// _ <- UIO(setupAnalogMovementKeys)
|
||||
_ <- UIO(setupCameraKeys())
|
||||
_ <- toIO(
|
||||
generateMovementInputEvents(
|
||||
inputManager,
|
||||
@ -35,7 +38,7 @@ object GameInputHandler {
|
||||
).completedL.startAndForget
|
||||
)
|
||||
_ <- toIO(
|
||||
generateRotateEvents(
|
||||
generateAnalogMovementEvents(
|
||||
inputManager,
|
||||
playerMovementEventBus
|
||||
).completedL.startAndForget
|
||||
@ -46,8 +49,10 @@ object GameInputHandler {
|
||||
playerCameraEventBus
|
||||
).completedL.startAndForget
|
||||
)
|
||||
_ <- toIO(
|
||||
Ref.of[me.Task, Boolean](false).flatMap(value => cursorToggle(value))
|
||||
)
|
||||
} yield ()
|
||||
}
|
||||
|
||||
def setupMovementKeys(inputManager: InputManager) =
|
||||
inputManager.withEnumMappings(PlayerMovementInput) {
|
||||
@ -63,28 +68,65 @@ object GameInputHandler {
|
||||
Seq(new KeyTrigger(KeyInput.KEY_SPACE))
|
||||
}
|
||||
|
||||
def setupKeys(inputManager: InputManager) =
|
||||
inputManager
|
||||
.withMapping(
|
||||
PlayerAnalogMovementInput.TurnRight.entryName,
|
||||
new KeyTrigger(KeyInput.KEY_RIGHT),
|
||||
new MouseAxisTrigger(MouseInput.AXIS_X, true)
|
||||
)
|
||||
.withMapping(
|
||||
PlayerAnalogMovementInput.TurnLeft.entryName,
|
||||
def setupAnalogMovementKeys() =
|
||||
inputManager.withEnumMappings(PlayerAnalogMovementInput) {
|
||||
case PlayerAnalogMovementInput.TurnRight =>
|
||||
Seq(new KeyTrigger(KeyInput.KEY_D))
|
||||
case PlayerAnalogMovementInput.TurnLeft =>
|
||||
Seq(new KeyTrigger(KeyInput.KEY_A))
|
||||
}
|
||||
|
||||
def setupCameraKeys() =
|
||||
inputManager.withEnumMappings(PlayerCameraInput) {
|
||||
case PlayerCameraInput.CameraRotateLeft =>
|
||||
Seq(
|
||||
new KeyTrigger(KeyInput.KEY_LEFT),
|
||||
new MouseAxisTrigger(MouseInput.AXIS_X, false)
|
||||
)
|
||||
.withMapping(
|
||||
"CAMERA_UP",
|
||||
// new KeyTrigger(KeyInput.KEY_LEFT),
|
||||
case PlayerCameraInput.CameraRotateRight =>
|
||||
Seq(
|
||||
new KeyTrigger(KeyInput.KEY_RIGHT),
|
||||
new MouseAxisTrigger(MouseInput.AXIS_X, true)
|
||||
)
|
||||
case PlayerCameraInput.CameraRotateUp =>
|
||||
Seq(
|
||||
new KeyTrigger(KeyInput.KEY_UP),
|
||||
new MouseAxisTrigger(MouseInput.AXIS_Y, false)
|
||||
)
|
||||
.withMapping(
|
||||
"CAMERA_DOWN",
|
||||
// new KeyTrigger(KeyInput.KEY_LEFT),
|
||||
case PlayerCameraInput.CameraRotateDown =>
|
||||
Seq(
|
||||
new KeyTrigger(KeyInput.KEY_DOWN),
|
||||
new MouseAxisTrigger(MouseInput.AXIS_Y, true)
|
||||
)
|
||||
}
|
||||
|
||||
def cursorToggle(toggleRef: Ref[me.Task, Boolean]) =
|
||||
for {
|
||||
_ <- me.Task(
|
||||
inputManager.withMapping(
|
||||
MiscInput.ToggleCursor,
|
||||
new KeyTrigger(KeyInput.KEY_Z)
|
||||
)
|
||||
)
|
||||
_ <-
|
||||
inputManager
|
||||
.enumEntryObservableAction(MiscInput.ToggleCursor)
|
||||
.doOnNext(action =>
|
||||
action.binding match {
|
||||
case MiscInput.ToggleCursor =>
|
||||
if (action.value) for {
|
||||
value <- toggleRef.getAndUpdate(!_)
|
||||
_ <- me.Task(inputManager.setCursorVisible(value))
|
||||
} yield ()
|
||||
else me.Task.unit
|
||||
// case _ => me.Task.unit
|
||||
}
|
||||
)
|
||||
.completedL
|
||||
.startAndForget
|
||||
} yield ()
|
||||
|
||||
}
|
||||
|
||||
def generateMovementInputEvents(
|
||||
inputManager: InputManager,
|
||||
@ -99,74 +141,74 @@ object GameInputHandler {
|
||||
.doOnNext { action =>
|
||||
action.binding match {
|
||||
case PlayerMovementInput.WalkLeft =>
|
||||
toTask(
|
||||
playerMovementEventBus !! EventBus.Publish(
|
||||
me.Task(
|
||||
playerMovementEventBus ! EventBus.Publish(
|
||||
PlayerMovementEvent.PlayerMovedLeft(pressed = action.value),
|
||||
name
|
||||
)
|
||||
)
|
||||
case PlayerMovementInput.WalkRight =>
|
||||
toTask(
|
||||
playerMovementEventBus !! EventBus.Publish(
|
||||
me.Task(
|
||||
playerMovementEventBus ! EventBus.Publish(
|
||||
PlayerMovementEvent.PlayerMovedRight(pressed = action.value),
|
||||
name
|
||||
)
|
||||
)
|
||||
case PlayerMovementInput.WalkForward =>
|
||||
toTask(
|
||||
playerMovementEventBus !! EventBus.Publish(
|
||||
me.Task(
|
||||
playerMovementEventBus ! EventBus.Publish(
|
||||
PlayerMovementEvent.PlayerMovedForward(pressed = action.value),
|
||||
name
|
||||
)
|
||||
)
|
||||
case PlayerMovementInput.WalkBackward =>
|
||||
toTask(
|
||||
playerMovementEventBus !! EventBus.Publish(
|
||||
me.Task(
|
||||
playerMovementEventBus ! EventBus.Publish(
|
||||
PlayerMovementEvent.PlayerMovedBackward(pressed = action.value),
|
||||
name
|
||||
)
|
||||
)
|
||||
case PlayerMovementInput.Jump =>
|
||||
if (action.value) {
|
||||
toTask(
|
||||
playerMovementEventBus !! EventBus.Publish(
|
||||
me.Task(
|
||||
playerMovementEventBus ! EventBus.Publish(
|
||||
PlayerMovementEvent.PlayerJumped,
|
||||
name
|
||||
)
|
||||
)
|
||||
} else monix.eval.Task.unit
|
||||
} else me.Task.unit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def generateRotateEvents(
|
||||
def generateAnalogMovementEvents(
|
||||
inputManager: InputManager,
|
||||
playerMovementEventBus: ActorRef[
|
||||
EventBus.Command[PlayerMovementEvent]
|
||||
]
|
||||
) = {
|
||||
val name = "rotateMovementEventsGenerator"
|
||||
// val name = "analogMovementEventsGenerator"
|
||||
inputManager
|
||||
.enumAnalogObservable(PlayerAnalogMovementInput)
|
||||
.sample(1.millis)
|
||||
.doOnNext(analogEvent =>
|
||||
analogEvent.binding match {
|
||||
case PlayerAnalogMovementInput.TurnRight =>
|
||||
toTask(
|
||||
playerMovementEventBus !! EventBus.Publish(
|
||||
PlayerMovementEvent.PlayerRotatedRight,
|
||||
name
|
||||
)
|
||||
)
|
||||
case PlayerAnalogMovementInput.TurnLeft =>
|
||||
toTask(
|
||||
playerMovementEventBus !! EventBus.Publish(
|
||||
PlayerMovementEvent.PlayerRotatedLeft,
|
||||
name
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
// .doOnNext(analogEvent =>
|
||||
// analogEvent.binding match {
|
||||
// case PlayerAnalogMovementInput.TurnRight =>
|
||||
// me.Task(
|
||||
// playerMovementEventBus ! EventBus.Publish(
|
||||
// PlayerMovementEvent.PlayerTurnedRight,
|
||||
// name
|
||||
// )
|
||||
// )
|
||||
// case PlayerAnalogMovementInput.TurnLeft =>
|
||||
// me.Task(
|
||||
// playerMovementEventBus ! EventBus.Publish(
|
||||
// PlayerMovementEvent.PlayerTurnedLeft,
|
||||
// name
|
||||
// )
|
||||
// )
|
||||
// }
|
||||
// )
|
||||
}
|
||||
|
||||
def generateCameraEvents(
|
||||
@ -175,25 +217,38 @@ object GameInputHandler {
|
||||
) = {
|
||||
val name = "cameraMovementEventsGenerator"
|
||||
inputManager
|
||||
.analogObservable("CAMERA_UP", "CAMERA_DOWN")
|
||||
.enumAnalogObservable(PlayerCameraInput)
|
||||
.sample(1.millis)
|
||||
.doOnNext(analogEvent =>
|
||||
analogEvent.binding.name match {
|
||||
case "CAMERA_UP" =>
|
||||
toTask(
|
||||
playerCameraEventBus !! EventBus.Publish(
|
||||
analogEvent.binding match {
|
||||
case PlayerCameraInput.CameraRotateLeft =>
|
||||
me.Task(
|
||||
playerCameraEventBus ! EventBus.Publish(
|
||||
PlayerCameraEvent.CameraLeft,
|
||||
name
|
||||
)
|
||||
)
|
||||
case PlayerCameraInput.CameraRotateRight =>
|
||||
me.Task(
|
||||
playerCameraEventBus ! EventBus.Publish(
|
||||
PlayerCameraEvent.CameraRight,
|
||||
name
|
||||
)
|
||||
)
|
||||
case PlayerCameraInput.CameraRotateUp =>
|
||||
me.Task(
|
||||
playerCameraEventBus ! EventBus.Publish(
|
||||
PlayerCameraEvent.CameraMovedUp,
|
||||
name
|
||||
)
|
||||
)
|
||||
case "CAMERA_DOWN" =>
|
||||
toTask(
|
||||
playerCameraEventBus !! EventBus.Publish(
|
||||
case PlayerCameraInput.CameraRotateDown =>
|
||||
me.Task(
|
||||
playerCameraEventBus ! EventBus.Publish(
|
||||
PlayerCameraEvent.CameraMovedDown,
|
||||
name
|
||||
)
|
||||
)
|
||||
case _ => monix.eval.Task.unit
|
||||
}
|
||||
)
|
||||
}
|
||||
|
@ -3,18 +3,33 @@ import enumeratum.EnumEntry._
|
||||
import enumeratum._
|
||||
|
||||
sealed trait PlayerMovementInput extends EnumEntry with UpperSnakecase
|
||||
object PlayerMovementInput extends Enum[PlayerMovementInput] {
|
||||
final object PlayerMovementInput extends Enum[PlayerMovementInput] {
|
||||
val values = findValues
|
||||
case object WalkForward extends PlayerMovementInput
|
||||
case object WalkRight extends PlayerMovementInput
|
||||
case object WalkLeft extends PlayerMovementInput
|
||||
case object WalkBackward extends PlayerMovementInput
|
||||
case object Jump extends PlayerMovementInput
|
||||
final case object WalkForward extends PlayerMovementInput
|
||||
final case object WalkRight extends PlayerMovementInput
|
||||
final case object WalkLeft extends PlayerMovementInput
|
||||
final case object WalkBackward extends PlayerMovementInput
|
||||
final case object Jump extends PlayerMovementInput
|
||||
}
|
||||
|
||||
sealed trait PlayerAnalogMovementInput extends EnumEntry with UpperSnakecase
|
||||
object PlayerAnalogMovementInput extends Enum[PlayerAnalogMovementInput] {
|
||||
final object PlayerAnalogMovementInput extends Enum[PlayerAnalogMovementInput] {
|
||||
val values = findValues
|
||||
case object TurnRight extends PlayerAnalogMovementInput
|
||||
case object TurnLeft extends PlayerAnalogMovementInput
|
||||
final case object TurnRight extends PlayerAnalogMovementInput
|
||||
final case object TurnLeft extends PlayerAnalogMovementInput
|
||||
}
|
||||
|
||||
sealed trait PlayerCameraInput extends EnumEntry with UpperSnakecase
|
||||
final object PlayerCameraInput extends Enum[PlayerCameraInput] {
|
||||
val values = findValues
|
||||
final case object CameraRotateLeft extends PlayerCameraInput
|
||||
final case object CameraRotateRight extends PlayerCameraInput
|
||||
final case object CameraRotateUp extends PlayerCameraInput
|
||||
final case object CameraRotateDown extends PlayerCameraInput
|
||||
}
|
||||
|
||||
sealed trait MiscInput extends EnumEntry with UpperSnakecase
|
||||
final object MiscInput extends Enum[MiscInput] {
|
||||
val values = findValues
|
||||
final case object ToggleCursor extends MiscInput
|
||||
}
|
||||
|
@ -12,7 +12,7 @@ import wow.doge.mygame.subsystems.movement.RotateDir
|
||||
|
||||
trait CanMove[-A] {
|
||||
// def getDirection(cam: Camera, cardinalDir: CardinalDirection): ImVector3f
|
||||
def move(inst: A, direction: ImVector3f): Unit
|
||||
def move(inst: A, direction: ImVector3f, speedFactor: Float = 20f): Unit
|
||||
def location(inst: A): ImVector3f
|
||||
def jump(inst: A): Unit
|
||||
def stop(inst: A): Unit
|
||||
@ -24,12 +24,12 @@ object CanMove {
|
||||
new CanMove[BetterCharacterControl] {
|
||||
override def move(
|
||||
inst: BetterCharacterControl,
|
||||
direction: ImVector3f
|
||||
direction: ImVector3f,
|
||||
speedFactor: Float = 20f
|
||||
): Unit = {
|
||||
// val dir = direction.mutable
|
||||
// inst.setViewDirection(dir)
|
||||
// inst.setViewDirection(direction.mutable)
|
||||
inst.setWalkDirection(direction.mutable.multLocal(50f))
|
||||
val dir = direction.mutable.normalizeLocal()
|
||||
inst.setViewDirection(dir.negate())
|
||||
inst.setWalkDirection(dir.mult(speedFactor))
|
||||
}
|
||||
override def location(inst: BetterCharacterControl) =
|
||||
inst.getSpatial().getLocalTranslation().immutable
|
||||
@ -42,10 +42,10 @@ object CanMove {
|
||||
rotateDir match {
|
||||
case RotateDir.Left =>
|
||||
new Quaternion()
|
||||
.fromAngleAxis(-5 * FastMath.DEG_TO_RAD, Vector3f.UNIT_Y)
|
||||
.fromAngleNormalAxis(5 * FastMath.DEG_TO_RAD, Vector3f.UNIT_Y)
|
||||
case RotateDir.Right =>
|
||||
new Quaternion()
|
||||
.fromAngleAxis(5 * FastMath.DEG_TO_RAD, Vector3f.UNIT_Y)
|
||||
.fromAngleAxis(-5 * FastMath.DEG_TO_RAD, Vector3f.UNIT_Y)
|
||||
}
|
||||
|
||||
val tmp = new Vector3f()
|
||||
@ -57,8 +57,12 @@ object CanMove {
|
||||
}
|
||||
|
||||
implicit val implCanMoveForGeom = new CanMove[Spatial] with LazyLogging {
|
||||
override def move(inst: Spatial, direction: ImVector3f): Unit = {
|
||||
inst.move(direction.mutable)
|
||||
override def move(
|
||||
inst: Spatial,
|
||||
direction: ImVector3f,
|
||||
speedFactor: Float = 1f
|
||||
): Unit = {
|
||||
inst.move(direction.mutable multLocal speedFactor)
|
||||
}
|
||||
override def location(inst: Spatial) =
|
||||
inst.getLocalTranslation().immutable
|
||||
|
@ -4,6 +4,7 @@ import akka.actor.typed.Behavior
|
||||
import akka.actor.typed.scaladsl.ActorContext
|
||||
import akka.actor.typed.scaladsl.Behaviors
|
||||
import com.jme3.math.Vector3f
|
||||
import com.jme3.renderer.Camera
|
||||
import com.softwaremill.quicklens._
|
||||
import wow.doge.mygame.game.subsystems.movement.CanMove
|
||||
import wow.doge.mygame.implicits._
|
||||
@ -27,15 +28,16 @@ object ImMovementActor {
|
||||
final case class MovedRight(pressed: Boolean) extends Movement
|
||||
final case class MovedDown(pressed: Boolean) extends Movement
|
||||
final case object Jump extends Movement
|
||||
final case object RotateRight extends Movement
|
||||
final case object RotateLeft extends Movement
|
||||
// final case object RotateRight extends Movement
|
||||
// final case object RotateLeft extends Movement
|
||||
|
||||
final case class Props[T: CanMove](
|
||||
enqueueR: Function1[() => Unit, Unit],
|
||||
movable: T
|
||||
final class Props[T: CanMove](
|
||||
val enqueueR: Function1[() => Unit, Unit],
|
||||
val movable: T,
|
||||
// playerMovementEventBus: ActorRef[
|
||||
// EventBus.Command[PlayerMovementEvent]
|
||||
// ]
|
||||
val camera: Camera
|
||||
) {
|
||||
def create: Behavior[Command] =
|
||||
Behaviors.setup(ctx => new ImMovementActor(ctx, this).receive(State()))
|
||||
@ -78,25 +80,47 @@ class ImMovementActor[T](
|
||||
case Jump =>
|
||||
props.enqueueR(() => cm.jump(props.movable))
|
||||
Behaviors.same
|
||||
case RotateLeft =>
|
||||
props.enqueueR(() => cm.rotate(props.movable, RotateDir.Left))
|
||||
Behaviors.same
|
||||
case RotateRight =>
|
||||
props.enqueueR(() => cm.rotate(props.movable, RotateDir.Right))
|
||||
Behaviors.same
|
||||
}
|
||||
|
||||
case Tick =>
|
||||
val walkDir =
|
||||
getDirection(state.cardinalDir, ctx.log.trace)
|
||||
if (walkDir != ImVector3f.ZERO) {
|
||||
getDirection2(state.cardinalDir, ctx.log.trace)
|
||||
// if (walkDir != ImVector3f.ZERO) {
|
||||
val tmp = walkDir * 25f * (1f / 144)
|
||||
props.enqueueR { () =>
|
||||
cm.move(props.movable, tmp)
|
||||
}
|
||||
}
|
||||
// }
|
||||
Behaviors.same
|
||||
}
|
||||
|
||||
def getDirection2(cardinalDir: CardinalDirection, debug: String => Unit) = {
|
||||
val camDir =
|
||||
props.camera.getDirection().clone().normalizeLocal.multLocal(0.6f)
|
||||
val camLeft = props.camera.getLeft().clone().normalizeLocal.multLocal(0.4f)
|
||||
val dir = cardinalDir
|
||||
val walkDir = {
|
||||
val mutWalkDir = new Vector3f()
|
||||
if (dir.up) {
|
||||
debug("up")
|
||||
mutWalkDir += camDir
|
||||
}
|
||||
if (dir.left) {
|
||||
debug("left")
|
||||
mutWalkDir += camLeft
|
||||
}
|
||||
if (dir.right) {
|
||||
debug("right")
|
||||
mutWalkDir += -camLeft
|
||||
}
|
||||
if (dir.down) {
|
||||
debug("down")
|
||||
mutWalkDir += -camDir
|
||||
}
|
||||
mutWalkDir.immutable
|
||||
}
|
||||
walkDir
|
||||
}
|
||||
}
|
||||
object Methods {
|
||||
def getDirection(cardinalDir: CardinalDirection, trace: String => Unit) = {
|
||||
|
@ -26,6 +26,7 @@ import com.jme3.input.controls.AnalogListener
|
||||
import com.jme3.input.controls.InputListener
|
||||
import com.jme3.input.controls.Trigger
|
||||
import com.jme3.light.Light
|
||||
import com.jme3.material.Material
|
||||
import com.jme3.math.Vector3f
|
||||
import com.jme3.scene.CameraNode
|
||||
import com.jme3.scene.Geometry
|
||||
@ -50,7 +51,6 @@ import monix.reactive.OverflowStrategy
|
||||
import monix.reactive.observers.Subscriber
|
||||
import wow.doge.mygame.math.ImVector3f
|
||||
import wow.doge.mygame.state.MyBaseState
|
||||
import com.jme3.material.Material
|
||||
|
||||
final case class ActionEvent(binding: Action, value: Boolean, tpf: Float)
|
||||
final case class EnumActionEvent[T <: EnumEntry](
|
||||
@ -397,16 +397,68 @@ package object implicits {
|
||||
|
||||
implicit final class InputManagerExt(private val inputManager: InputManager)
|
||||
extends AnyVal {
|
||||
|
||||
/**
|
||||
* Create a new mapping to the given triggers.
|
||||
*
|
||||
* <p>
|
||||
* The given mapping will be assigned to the given triggers, when
|
||||
* any of the triggers given raise an event, the listeners
|
||||
* registered to the mappings will receive appropriate events.
|
||||
*
|
||||
* @param mappingName The mapping name to assign.
|
||||
* @param triggers The triggers to which the mapping is to be registered.
|
||||
*/
|
||||
def withMapping(mapping: String, triggers: Trigger*): InputManager = {
|
||||
inputManager.addMapping(mapping, triggers: _*)
|
||||
inputManager
|
||||
}
|
||||
|
||||
def withMapping[T <: EnumEntry](
|
||||
mapping: T,
|
||||
triggers: Trigger*
|
||||
): InputManager = {
|
||||
inputManager.addMapping(mapping.entryName, triggers: _*)
|
||||
inputManager
|
||||
}
|
||||
|
||||
def withListener(listener: InputListener, mappings: String*) = {
|
||||
inputManager.addListener(listener, mappings: _*)
|
||||
inputManager
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates new mappings from the values of the given Enum
|
||||
*
|
||||
* <p>
|
||||
* The given mapping will be assigned to the given triggers, when
|
||||
* any of the triggers given raise an event, the listeners
|
||||
* registered to the mappings will receive appropriate events.
|
||||
*
|
||||
* @param mappingName The mapping name to assign.
|
||||
* @param mappingFn Function from enum values to the sequence of trigers.
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* {{{
|
||||
*
|
||||
* sealed trait PlayerAnalogMovementInput extends EnumEntry with UpperSnakecase
|
||||
* object PlayerAnalogMovementInput extends Enum[PlayerAnalogMovementInput] {
|
||||
* val values = findValues
|
||||
* case object TurnRight extends PlayerAnalogMovementInput
|
||||
* case object TurnLeft extends PlayerAnalogMovementInput
|
||||
* }
|
||||
*
|
||||
* {
|
||||
* inputManager.withEnumMappings(PlayerAnalogMovementInput) {
|
||||
* case PlayerAnalogMovementInput.TurnRight =>
|
||||
* Seq(new KeyTrigger(KeyInput.KEY_RIGHT))
|
||||
* case PlayerAnalogMovementInput.TurnLeft =>
|
||||
* Seq(new KeyTrigger(KeyInput.KEY_LEFT))
|
||||
* }
|
||||
* }
|
||||
* }}}
|
||||
*/
|
||||
def withEnumMappings[T <: EnumEntry](
|
||||
mappingEnum: Enum[T]
|
||||
)(mappingFn: T => Seq[Trigger]): InputManager = {
|
||||
@ -417,6 +469,15 @@ package object implicits {
|
||||
inputManager
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an observable which emits the given mappings as elements of an observable
|
||||
*
|
||||
* @param mappingNames
|
||||
* @return Observable of action events
|
||||
*
|
||||
* @see [[ActionEvent]]
|
||||
* @see [[enumObservableAction]]
|
||||
*/
|
||||
def observableAction(mappingNames: String*): Observable[ActionEvent] = {
|
||||
|
||||
Observable.create(OverflowStrategy.DropOld(10)) { sub =>
|
||||
@ -443,6 +504,27 @@ package object implicits {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* Create an observable which emits the values of the given
|
||||
* enum as elements of an observable
|
||||
*
|
||||
* @param mappingNames
|
||||
* @return Observable of enum values
|
||||
*
|
||||
* @example {{{
|
||||
* inputManager
|
||||
* .enumObservableAction(PlayerMovementInput)
|
||||
* .doOnNext { action =>
|
||||
* action.binding match {
|
||||
* case PlayerMovementInput.WalkLeft => Task {/* your actions */}
|
||||
* }
|
||||
* }
|
||||
* }}}
|
||||
*
|
||||
* @see [[EnumActionEvent]]
|
||||
* @see [[enumAnalogObservable]]
|
||||
*/
|
||||
def enumObservableAction[T <: EnumEntry](
|
||||
mappingEnum: Enum[T]
|
||||
): Observable[EnumActionEvent[T]] = {
|
||||
@ -472,6 +554,36 @@ package object implicits {
|
||||
}
|
||||
}
|
||||
|
||||
def enumEntryObservableAction[T <: EnumEntry](
|
||||
mappingEnumEntry: T
|
||||
): Observable[EnumActionEvent[T]] = {
|
||||
|
||||
Observable.create(OverflowStrategy.DropOld(10)) { sub =>
|
||||
val c = SingleAssignCancelable()
|
||||
val al = new ActionListener {
|
||||
override def onAction(
|
||||
binding: String,
|
||||
value: Boolean,
|
||||
tpf: Float
|
||||
): Unit = {
|
||||
if (
|
||||
sub.onNext(
|
||||
EnumActionEvent(mappingEnumEntry, value, tpf)
|
||||
) == Ack.Stop
|
||||
) {
|
||||
sub.onComplete()
|
||||
c.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inputManager.addListener(al, mappingEnumEntry.entryName)
|
||||
|
||||
c := Cancelable(() => inputManager.removeListener(al))
|
||||
c
|
||||
}
|
||||
}
|
||||
|
||||
def analogObservable(mappingNames: String*): Observable[AnalogEvent] = {
|
||||
|
||||
Observable.create(OverflowStrategy.DropOld(50)) { sub =>
|
||||
@ -666,6 +778,11 @@ package object implicits {
|
||||
def /(f: Float): ImVector3f = v.copy(v.x / f, v.y / f, v.z / f)
|
||||
def unary_- = v.copy(-v.x, -v.y, -v.z)
|
||||
def mutable = new Vector3f(v.x, v.y, v.z)
|
||||
|
||||
// /**
|
||||
// * alias for [[cross]] product
|
||||
// */
|
||||
// def |*|() = ()
|
||||
}
|
||||
|
||||
// val TasktoUIO = new FunctionK[Task, UIO] {
|
||||
|
@ -1,6 +1,6 @@
|
||||
package wow.doge.mygame.math;
|
||||
|
||||
import Math.{abs, sqrt, pow}
|
||||
import Math.{sqrt, pow}
|
||||
|
||||
case class ImVector3f(x: Float = 0f, y: Float = 0f, z: Float = 0f)
|
||||
|
||||
|
@ -1,7 +1,9 @@
|
||||
package wow.doge.mygame.subsystems.events
|
||||
|
||||
sealed trait PlayerMovementEvent
|
||||
object PlayerMovementEvent {
|
||||
sealed trait PlayerEvent
|
||||
|
||||
sealed trait PlayerMovementEvent extends PlayerEvent
|
||||
final object PlayerMovementEvent {
|
||||
final case class PlayerMovedLeft(pressed: Boolean) extends PlayerMovementEvent
|
||||
final case class PlayerMovedRight(pressed: Boolean)
|
||||
extends PlayerMovementEvent
|
||||
@ -10,15 +12,15 @@ object PlayerMovementEvent {
|
||||
final case class PlayerMovedBackward(pressed: Boolean)
|
||||
extends PlayerMovementEvent
|
||||
final case object PlayerJumped extends PlayerMovementEvent
|
||||
final case object PlayerRotatedRight extends PlayerMovementEvent
|
||||
final case object PlayerRotatedLeft extends PlayerMovementEvent
|
||||
final case object PlayerCameraUp extends PlayerMovementEvent
|
||||
final case object PlayerCameraDown extends PlayerMovementEvent
|
||||
// final case object PlayerTurnedRight extends PlayerMovementEvent
|
||||
// final case object PlayerTurnedLeft extends PlayerMovementEvent
|
||||
}
|
||||
|
||||
sealed trait PlayerCameraEvent
|
||||
sealed trait PlayerCameraEvent extends PlayerEvent
|
||||
|
||||
object PlayerCameraEvent {
|
||||
final object PlayerCameraEvent {
|
||||
final case object CameraLeft extends PlayerCameraEvent
|
||||
final case object CameraRight extends PlayerCameraEvent
|
||||
final case object CameraMovedUp extends PlayerCameraEvent
|
||||
final case object CameraMovedDown extends PlayerCameraEvent
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user