Many changes

Add player actor substates
Added color logic to hud stats bars
This commit is contained in:
Rohan Sircar 2021-03-05 00:39:57 +05:30
parent 1422d91b14
commit 67201c8f7e
10 changed files with 301 additions and 152 deletions

View File

@ -0,0 +1,11 @@
.red-bar > .bar {
-fx-background-color: red;
}
.green-bar > .bar {
-fx-background-color: green;
}
.yellow-bar > .bar {
-fx-background-color: yellow;
}

View File

@ -2,6 +2,7 @@ package wow.doge.mygame
import java.util.concurrent.TimeoutException
import scala.annotation.switch
import scala.concurrent.duration._
import akka.actor.typed.ActorRef
@ -35,6 +36,7 @@ import monix.eval.Coeval
import monix.execution.cancelables.CompositeCancelable
import monix.execution.exceptions.DummyException
import monix.reactive.Observable
import monix.{eval => me}
import scalafx.scene.control.Label
import scalafx.scene.control.TextArea
import scalafx.scene.layout.HBox
@ -50,8 +52,8 @@ 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.PlayerActorSupervisor
import wow.doge.mygame.game.entities.PlayerController
import wow.doge.mygame.game.entities.player.PlayerActorSupervisor
import wow.doge.mygame.game.entities.player.PlayerController
import wow.doge.mygame.game.subsystems.input.GameInputHandler
import wow.doge.mygame.game.subsystems.input.PlayerCameraInput
import wow.doge.mygame.game.subsystems.level.DefaultGameLevel
@ -386,6 +388,8 @@ class MainAppDelegate(
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
@ -398,10 +402,29 @@ class MainAppDelegate(
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 {
pprint.log(show"Received $a $b")
pprint.log(show"${styleClass.toString}")
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()
}
)
},
@ -419,9 +442,32 @@ class MainAppDelegate(
.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 {
pprint.log(show"Received $a $b")
pprint.log(show"${styleClass.toString}")
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()
}
)
}

View File

@ -5,6 +5,7 @@ import akka.actor.typed.Behavior
import akka.actor.typed.scaladsl.ActorContext
import akka.actor.typed.scaladsl.Behaviors
import cats.Show
import cats.kernel.Eq
import io.estatico.newtype.macros.newtype
import wow.doge.mygame.game.entities.CharacterStats.HealHealth
@ -18,10 +19,6 @@ object CharacterStats {
@newtype final case class Health(toInt: Int)
object Health {
implicit class HealthOps(private val h: Health) extends AnyVal {
// def +(v: Int): Health = Health(h.toInt + v)
// def -(v: Int): Health = Health(h.toInt - v)
// def *(v: Int): Health = Health(h.toInt * v)
// def /(v: Int): Health = Health(h.toInt / v)
def :+(v: HealHealth): Health = Health(h.toInt + v.toInt)
def -(v: DamageHealth): Health = Health(h.toInt - v.toInt)
}
@ -31,40 +28,19 @@ object CharacterStats {
@newtype final case class Stamina(toInt: Int)
object Stamina {
implicit class StaminaOps(private val h: Stamina) extends AnyVal {
// def +(v: Int): Stamina = Stamina(h.toInt + v)
// def -(v: Int): Stamina = Stamina(h.toInt - v)
// def *(v: Int): Stamina = Stamina(h.toInt * v)
// def /(v: Int): Stamina = Stamina(h.toInt / v)
def :+(v: HealStamina): Stamina = Stamina(h.toInt + v.toInt)
def -(v: DamageStamina): Stamina = Stamina(h.toInt - v.toInt)
}
}
// object Stamina {
// implicit class StaminaOps(private val h: Stamina) extends AnyVal {
// def +(v: Health): Stamina = Stamina(h.toInt + v.toInt)
// def -(v: Health): Stamina = Stamina(h.toInt - v.toInt)
// def *(v: Health): Stamina = Stamina(h.toInt * v.toInt)
// def /(v: Health): Stamina = Stamina(h.toInt / v.toInt)
// }
// }
// object Damage {
// implicit class DamageOps(private val h: Damage) extends AnyVal {
// def +(v: Health): Damage = Damage(h.toInt + v.toInt)
// def -(v: Health): Damage = Damage(h.toInt - v.toInt)
// def *(v: Health): Damage = Damage(h.toInt * v.toInt)
// def /(v: Health): Damage = Damage(h.toInt / v.toInt)
// }
// }
implicit val show = Show.fromToString[CharacterStats]
implicit val eq = Eq.fromUniversalEquals[CharacterStats]
}
object StatsActor {
sealed trait Command
// final case class TakeDamage(value: Int) extends Command
final case class TakeDamageResult(
value: CharacterStats.DamageHealth,
replyTo: ActorRef[(Boolean, CharacterStats)]

View File

@ -0,0 +1,31 @@
package wow.doge.mygame.game.entities.character
import cats.Show
import cats.kernel.Eq
object CharacterStates {
sealed trait AliveSubstate
object AliveSubstate {
final case class InCombat(substate: CombatSubstate) extends AliveSubstate
final case class Moving(substate: MovementSubstate) extends AliveSubstate
case object Idle extends AliveSubstate
implicit val eq = Eq.fromUniversalEquals[AliveSubstate]
implicit val show = Show.fromToString[AliveSubstate]
}
sealed trait CombatSubstate
object CombatSubstate {
final case class Moving(substate: MovementSubstate) extends CombatSubstate
final case class Attacking(victimName: String) extends CombatSubstate
}
sealed trait MovementSubstate
case object Walking extends MovementSubstate
case object Running extends MovementSubstate
sealed trait DeadSubstate
object DeadSubstate {
implicit val eq = Eq.fromUniversalEquals[DeadSubstate]
implicit val show = Show.fromToString[DeadSubstate]
}
}

View File

@ -1,8 +1,6 @@
package wow.doge.mygame.game.entities
package wow.doge.mygame.game.entities.player
import scala.concurrent.duration._
import scala.util.Failure
import scala.util.Success
import akka.actor.typed.ActorRef
import akka.actor.typed.Behavior
@ -12,14 +10,18 @@ import akka.actor.typed.SupervisorStrategy
import akka.actor.typed.scaladsl.ActorContext
import akka.actor.typed.scaladsl.Behaviors
import akka.util.Timeout
import cats.syntax.show._
import com.typesafe.scalalogging.Logger
import monix.bio.UIO
import monix.execution.AsyncQueue
import monix.reactive.Observable
import org.slf4j.event.Level
import wow.doge.mygame.Dispatchers
import wow.doge.mygame.executors.Schedulers
import wow.doge.mygame.executors.Schedulers.AsyncScheduler
import wow.doge.mygame.game.entities.CharacterStats
import wow.doge.mygame.game.entities.StatsActor
import wow.doge.mygame.game.entities.character.CharacterStates._
import wow.doge.mygame.game.entities.player.behaviors.IdleBehaviorFactory
import wow.doge.mygame.implicits._
import wow.doge.mygame.subsystems.events.EventBus
import wow.doge.mygame.subsystems.events.EventsModule.GameEventBus
@ -27,8 +29,8 @@ import wow.doge.mygame.subsystems.events.PlayerEvent
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.executors.Schedulers
import monix.bio.UIO
//TODO: Change name to PlayerActor
object PlayerActorSupervisor {
type Ref = ActorRef[PlayerActorSupervisor.Command]
@ -56,26 +58,28 @@ object PlayerActorSupervisor {
replyTo: ActorRef[UIO[Observable[CharacterStats]]]
) extends Command
private case object Die extends Command
private final case class DamageResponse(
sealed trait Movement extends Command
final case class MoveLeft(pressed: Boolean) extends Movement
private[player] case object Die extends Command
private[player] final case class StatsResponse(
response: (Boolean, CharacterStats),
replyTo: ActorRef[Unit]
) extends Command
private final case class LogError(ex: Throwable) extends Command
private[player] final case class LogError(ex: Throwable) extends Command
class Props(
val playerEventBus: GameEventBus[PlayerEvent],
val tickEventBus: GameEventBus[TickEvent],
val imMovementActorBehavior: Behavior[ImMovementActor.Command],
val scheduler: AsyncScheduler,
val fxScheduler: Schedulers.FxScheduler
val fxScheduler: Schedulers.FxScheduler,
val statsQueue: AsyncQueue[CharacterStats]
) {
def behavior =
Behaviors.logMessages(
LogOptions()
.withLevel(Level.DEBUG)
.withLogger(
Logger[PlayerActorSupervisor].underlying
),
.withLogger(Logger[PlayerActorSupervisor].underlying),
Behaviors
.setup[Command] { ctx =>
ctx.log.infoP("Starting PlayerActor")
@ -140,9 +144,8 @@ object PlayerActorSupervisor {
new PlayerActorSupervisor(
ctx,
this,
Children(playerMovementActor, playerStatsActor),
AsyncQueue.bounded(10)(scheduler.value)
).aliveState
Children(playerMovementActor, playerStatsActor)
).aliveState(AliveSubstate.Idle)
}
)
@ -152,77 +155,24 @@ object PlayerActorSupervisor {
movementActor: ActorRef[ImMovementActor.Command],
statsActor: ActorRef[StatsActor.Command]
)
final case class Env(
ctx: ActorContext[PlayerActorSupervisor.Command],
props: PlayerActorSupervisor.Props,
children: PlayerActorSupervisor.Children
)
}
class PlayerActorSupervisor(
ctx: ActorContext[PlayerActorSupervisor.Command],
props: PlayerActorSupervisor.Props,
children: PlayerActorSupervisor.Children,
statsQueue: AsyncQueue[CharacterStats]
children: PlayerActorSupervisor.Children
) {
import PlayerActorSupervisor._
implicit val timeout = Timeout(1.second)
val aliveState =
Behaviors
.receiveMessage[Command] {
case TakeDamage(value, replyTo) =>
ctx.ask(children.statsActor, StatsActor.TakeDamageResult(value, _)) {
case Success(response) => DamageResponse(response, replyTo)
case Failure(ex) => LogError(ex)
}
Behaviors.same
case ConsumeStamina(value, replyTo) =>
ctx.ask(
children.statsActor,
StatsActor.ConsumeStaminaResult(value, _)
) {
case Success(response) => DamageResponse(response, replyTo)
case Failure(ex) => LogError(ex)
}
Behaviors.same
case CurrentStats(replyTo) =>
children.statsActor ! StatsActor.CurrentStats(replyTo)
Behaviors.same
case Heal(value) =>
children.statsActor ! StatsActor.HealResult(value)
Behaviors.same
case GetStatus(replyTo) =>
replyTo ! Status.Alive
Behaviors.same
case GetStatsObservable(replyTo) =>
import monix.{eval => me}
replyTo !
UIO(
Observable
.repeatEvalF(
me.Task.deferFuture(statsQueue.poll())
)
.publish(props.fxScheduler.value)
.refCount
)
Behaviors.same
case DamageResponse(response, replyTo) =>
response match {
case (dead, stats) =>
if (dead) ctx.self ! Die
statsQueue
.offer(stats)
.foreach { _ =>
pprint.log(show"Published stats $stats")
replyTo ! ()
}(props.scheduler.value)
}
Behaviors.same
case Die => deadState
case LogError(ex) =>
ctx.log.error(ex.getMessage)
Behaviors.same
}
.receiveSignal {
case (_, PostStop) =>
ctx.log.infoP("stopped")
Behaviors.same
}
val env = Env(ctx, props, children)
val deadState = Behaviors
.receiveMessage[Command] {
// case TakeDamage(value) =>
@ -249,4 +199,43 @@ class PlayerActorSupervisor(
ctx.log.infoP("stopped")
Behaviors.same
}
def idleBehavior(
nextStateFn: AliveSubstate => Behavior[Command],
consumptionMultiplier: Int => Int
) =
new IdleBehaviorFactory(
env,
nextStateFn,
deadState,
consumptionMultiplier
).behavior
def aliveState(substate: AliveSubstate): Behavior[Command] =
substate match {
case AliveSubstate.InCombat(substate) =>
substate match {
case CombatSubstate.Moving(substate) =>
substate match {
case Walking =>
Behaviors.receiveMessage[Command](_ => Behaviors.same)
case Running =>
Behaviors.receiveMessage[Command](_ => Behaviors.same)
}
case CombatSubstate.Attacking(victimName) =>
Behaviors.receiveMessage[Command](_ => Behaviors.same)
}
case AliveSubstate.Moving(substate) =>
substate match {
case Walking =>
ctx.log.debugP("In Walking State")
idleBehavior(aliveState, _ * 2).value
case Running =>
idleBehavior(aliveState, _ * 3).value
}
case AliveSubstate.Idle =>
ctx.log.debugP("In Idle State")
idleBehavior(aliveState, identity).value
}
}

View File

@ -1,4 +1,4 @@
package wow.doge.mygame.game.entities
package wow.doge.mygame.game.entities.player
import akka.actor.typed.ActorRef
import akka.actor.typed.DispatcherSelector
@ -13,7 +13,9 @@ import com.softwaremill.tagging._
import io.odin.Logger
import monix.bio.IO
import monix.bio.Task
import monix.execution.AsyncQueue
import wow.doge.mygame.AppError
import wow.doge.mygame.executors.Schedulers
import wow.doge.mygame.executors.Schedulers.AsyncScheduler
import wow.doge.mygame.game.GameApp
import wow.doge.mygame.implicits._
@ -24,7 +26,6 @@ import wow.doge.mygame.subsystems.events.TickEvent
import wow.doge.mygame.subsystems.movement.ImMovementActor
import wow.doge.mygame.types._
import wow.doge.mygame.utils.wrappers.jme._
import wow.doge.mygame.executors.Schedulers
object PlayerController {
sealed trait Error
@ -69,7 +70,8 @@ object PlayerController {
tickEventBus,
movementActorBeh,
scheduler,
fxScheduler
fxScheduler,
AsyncQueue.bounded(10)(scheduler.value)
).behavior
}
val playerActor =

View File

@ -1,4 +1,4 @@
package wow.doge.mygame.game.entities
package wow.doge.mygame.game.entities.player
import scala.concurrent.duration._
@ -14,8 +14,8 @@ import monix.execution.CancelableFuture
import monix.reactive.Observable
import org.slf4j.event.Level
import wow.doge.mygame.executors.Schedulers
import wow.doge.mygame.game.entities.CharacterStats
import wow.doge.mygame.implicits._
import wow.doge.mygame.subsystems.events.PlayerCameraEvent
import wow.doge.mygame.subsystems.events.PlayerMovementEvent
import wow.doge.mygame.subsystems.movement.ImMovementActor
@ -24,7 +24,7 @@ object PlayerMovementEventListener {
class Props(
val movementActor: ActorRef[ImMovementActor.Command],
val statsActor: PlayerActorSupervisor.Ref,
val playerActor: PlayerActorSupervisor.Ref,
val asyncScheduler: Schedulers.AsyncScheduler
) {
def behavior =
@ -56,7 +56,7 @@ class PlayerMovementEventListener(
Observable
.interval(250.millis)
.doOnNextF(_ =>
props.statsActor.askL(
props.playerActor.askL(
PlayerActorSupervisor
.ConsumeStamina(CharacterStats.DamageStamina(1), _)
)
@ -64,9 +64,12 @@ class PlayerMovementEventListener(
.completedL
)
@SuppressWarnings(Array("org.wartremover.warts.Equals"))
def handleStamina(pressed: Boolean) =
if (pressed)
if (state.staminaTimer == CancelableFuture.unit)
State(makeStaminaTimer.runToFuture(props.asyncScheduler.value))
else state
else {
state.staminaTimer.cancel()
State(CancelableFuture.unit)
@ -74,7 +77,8 @@ class PlayerMovementEventListener(
Behaviors.receiveMessage {
case PlayerMovedLeft(pressed) =>
props.movementActor ! ImMovementActor.MoveLeft(pressed)
// props.movementActor ! ImMovementActor.MoveLeft(pressed)
props.playerActor ! PlayerActorSupervisor.MoveLeft(pressed)
receive(handleStamina(pressed))
case PlayerMovedRight(pressed) =>
props.movementActor ! ImMovementActor.MoveRight(pressed)
@ -98,32 +102,3 @@ class PlayerMovementEventListener(
}
)
}
//not used
object PlayerCameraEventListener {
import PlayerCameraEvent._
def apply(playerCameraActor: ActorRef[PlayerCameraActor.Command]) =
Behaviors.logMessages(
LogOptions()
.withLevel(Level.TRACE)
.withLogger(
Logger[PlayerCameraEventListener.type].underlying
),
Behaviors.setup[PlayerCameraEvent](ctx =>
Behaviors.receiveMessagePartial {
case CameraMovedUp =>
playerCameraActor ! PlayerCameraActor.RotateUp
Behaviors.same
case CameraMovedDown =>
playerCameraActor ! PlayerCameraActor.RotateDown
Behaviors.same
case CameraLeft =>
playerCameraActor ! PlayerCameraActor.RotateLeft
Behaviors.same
case CameraRight =>
playerCameraActor ! PlayerCameraActor.RotateRight
Behaviors.same
}
)
)
}

View File

@ -0,0 +1,115 @@
package wow.doge.mygame.game.entities.player.behaviors
import scala.concurrent.Future
import akka.actor.typed.Behavior
import akka.actor.typed.PostStop
import akka.actor.typed.scaladsl.AskPattern._
import akka.actor.typed.scaladsl.Behaviors
import akka.util.Timeout
import cats.syntax.show._
import monix.bio.UIO
import monix.reactive.Observable
import wow.doge.mygame.game.entities.CharacterStats
import wow.doge.mygame.game.entities.StatsActor
import wow.doge.mygame.game.entities.character.CharacterStates._
import wow.doge.mygame.game.entities.player.PlayerActorSupervisor
import wow.doge.mygame.implicits._
import wow.doge.mygame.subsystems.movement.ImMovementActor
class IdleBehaviorFactory(
env: PlayerActorSupervisor.Env,
nextStateFn: AliveSubstate => Behavior[PlayerActorSupervisor.Command],
deadState: Behavior[PlayerActorSupervisor.Command],
consumptionMultiplier: Int => Int
)(implicit timeout: Timeout) {
import IdleBehaviorFactory.IdleBehavior
import env._
implicit val sched = ctx.system.scheduler
def behavior =
IdleBehavior(
Behaviors
.receiveMessage[PlayerActorSupervisor.Command] {
case PlayerActorSupervisor.MoveLeft(pressed) =>
children.movementActor ! ImMovementActor.MoveLeft(pressed)
if (pressed)
nextStateFn(AliveSubstate.Moving(Walking))
else nextStateFn(AliveSubstate.Idle)
case PlayerActorSupervisor.TakeDamage(value, replyTo) =>
implicit val ec = props.scheduler.value
for {
res <-
children.statsActor.ask(StatsActor.TakeDamageResult(value, _))
_ <- Future.successful(
ctx.self ! PlayerActorSupervisor.StatsResponse(res, replyTo)
)
} yield ()
Behaviors.same
case PlayerActorSupervisor.ConsumeStamina(value, replyTo) =>
implicit val ec = props.scheduler.value
val newValue =
CharacterStats.DamageStamina(consumptionMultiplier(value.toInt))
for {
response <- children.statsActor.ask(
StatsActor.ConsumeStaminaResult(newValue, _)
)
_ =
ctx.self ! PlayerActorSupervisor
.StatsResponse(response, replyTo)
} yield ()
Behaviors.same
case PlayerActorSupervisor.CurrentStats(replyTo) =>
children.statsActor ! StatsActor.CurrentStats(replyTo)
Behaviors.same
case PlayerActorSupervisor.Heal(value) =>
children.statsActor ! StatsActor.HealResult(value)
Behaviors.same
case PlayerActorSupervisor.GetStatus(replyTo) =>
replyTo ! PlayerActorSupervisor.Status.Alive
Behaviors.same
case PlayerActorSupervisor.GetStatsObservable(replyTo) =>
import monix.{eval => me}
replyTo !
UIO(
Observable
.repeatEvalF(
me.Task.deferFuture(props.statsQueue.poll())
)
.publish(props.fxScheduler.value)
.refCount
)
Behaviors.same
case PlayerActorSupervisor.StatsResponse(response, replyTo) =>
response match {
case (dead, stats) =>
if (dead) ctx.self ! PlayerActorSupervisor.Die
props.statsQueue
.offer(stats)
.foreach { _ =>
pprint.log(show"Published stats $stats")
replyTo ! ()
}(props.scheduler.value)
}
Behaviors.same
// nextStateFn(InCombat(Moving(Walking)))
case PlayerActorSupervisor.Die => deadState
case PlayerActorSupervisor.LogError(ex) =>
ctx.log.error(ex.getMessage)
Behaviors.same
}
.receiveSignal {
case (_, PostStop) =>
ctx.log.infoP("stopped")
Behaviors.same
}
)
}
object IdleBehaviorFactory {
final case class IdleBehavior(
value: Behavior[PlayerActorSupervisor.Command]
)
}

View File

@ -12,11 +12,14 @@ import wow.doge.mygame.game.subsystems.movement.CanMove
import wow.doge.mygame.implicits._
final case class CardinalDirection(
left: Boolean = false,
right: Boolean = false,
up: Boolean = false,
down: Boolean = false
left: Boolean,
right: Boolean,
up: Boolean,
down: Boolean
)
object CardinalDirection {
val default = CardinalDirection(false, false, false, false)
}
sealed trait RotateDir
object RotateDir {
@ -53,7 +56,9 @@ object ImMovementActor {
*
* @param cardinalDir The four directions the character can move
*/
final case class State(cardinalDir: CardinalDirection = CardinalDirection())
final case class State(
cardinalDir: CardinalDirection = CardinalDirection.default
)
}
@ -188,7 +193,7 @@ object MovementActor {
* @param walkDirection scratch space to avoid allocations on every tick. Do not share outside the actor
*/
final case class State(
cardinalDir: CardinalDirection = CardinalDirection(),
cardinalDir: CardinalDirection = CardinalDirection.default,
walkDirection: Vector3f = Vector3f.UNIT_X
)

View File

@ -1,5 +1,4 @@
package wow.doge.mygame.utils.controls
// import com.jfoenix.controls.JFXProgressBar
import com.jfoenix.{controls => jfoenixc}
import scalafx.scene.control.ProgressBar