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 cats.syntax.show._ 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.utils.GenericTimerActor object NpcActorSupervisor { type Ref = ActorRef[Command] sealed trait Command final case class Move(pos: ImVector3f) extends Command private final case class InternalMove( move: Move, signal: CancelableFuture[NpcMovementActor.DoneMoving.type] ) extends Command private case object DoneMoving extends Command private case class LogError(err: Throwable) extends Command private case class MovementFailed(err: Throwable) extends Command class Props( val npcMovementActorBehavior: Behavior[NpcMovementActor.Command], val npcName: String, val initialPos: ImVector3f ) { def behavior = Behaviors.withMdc[Command](Map("actorName" -> npcName))( Behaviors.setup[Command] { ctx => val npcMovementActor = ctx.spawn( (npcMovementActorBehavior), s"npc-${npcName}-NpcMovementActor" ) new NpcActorSupervisor(ctx, this, Children(npcMovementActor)) .idle(State()) } ) } final case class State() final case class Children( npcMovementActor: ActorRef[NpcMovementActor.Command] ) } class NpcActorSupervisor( ctx: ActorContext[NpcActorSupervisor.Command], props: NpcActorSupervisor.Props, children: NpcActorSupervisor.Children ) { import NpcActorSupervisor._ implicit val timeout = Timeout(1.second) val movementTimer = ctx.spawn( GenericTimerActor .Props( children.npcMovementActor, NpcMovementActor.MovementTick, 100.millis ) .behavior, s"npc-${props.npcName}-NpcActorTimer" ) def idle(state: State): Behavior[NpcActorSupervisor.Command] = Behaviors.setup { _ => ctx.log.debugP(s"npcActor-${props.npcName}: Entered Idle State") Behaviors.receiveMessage[Command] { case m @ Move(pos) => ctx.ask( children.npcMovementActor, NpcMovementActor.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) => logError(err) Behaviors.same case _ => Behaviors.unhandled } } def moving( state: State, targetPos: ImVector3f, signal: CancelableFuture[NpcMovementActor.DoneMoving.type] ): Behavior[NpcActorSupervisor.Command] = Behaviors.setup { _ => ctx.log.debugP(s"npcActor-${props.npcName}: Entered Moving State") movementTimer ! GenericTimerActor.Start ctx.pipeToSelf(signal) { case Success(value) => DoneMoving case Failure(exception) => MovementFailed(exception) } Behaviors.receiveMessagePartial[Command] { case LogError(err) => logError(err) Behaviors.same case MovementFailed(err) => ctx.self ! LogError(err) movementTimer ! GenericTimerActor.Stop idle(state) case m @ Move(pos) => movementTimer ! GenericTimerActor.Stop children.npcMovementActor ! NpcMovementActor.StopMoving // new movement request received, cancel previous request signal.cancel() ctx.ask( children.npcMovementActor, NpcMovementActor.MoveTo(pos, _) ) { case Success(signal) => InternalMove(m, signal) case Failure(exception) => MovementFailed(exception) } Behaviors.same case InternalMove(move, signal) => moving(state, targetPos, signal) case DoneMoving => movementTimer ! GenericTimerActor.Stop idle(state) } } def logError(err: Throwable) = ctx.log.errorP(s"npcActor-${props.npcName}: " + err.getMessage) } object NpcMovementActor { 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 MoveTo( target: ImVector3f, doneSignal: ActorRef[CancelableFuture[DoneMoving.type]] ) extends Command class Props[T: CanMove]( val enqueueR: Function1[() => Unit, Unit], val initialPos: ImVector3f, // val tickEventBus: GameEventBus[TickEvent], val npcName: String, val movable: T ) { def behavior = Behaviors.setup[Command] { ctx => new NpcMovementActor(ctx, this).receive(State()) } } final case class State() } class NpcMovementActor[T]( ctx: ActorContext[NpcMovementActor.Command], props: NpcMovementActor.Props[T] )(implicit cm: CanMove[T]) { import NpcMovementActor._ def location = cm.location(props.movable) def receive(state: State): Behavior[NpcMovementActor.Command] = Behaviors.receiveMessagePartial { case AskPosition(replyTo) => replyTo ! location Behaviors.same case MoveTo( target: ImVector3f, replyTo: ActorRef[CancelableFuture[DoneMoving.type]] ) => props.enqueueR(() => cm.move(props.movable, target - location, 20f)) val p = CancelablePromise[DoneMoving.type]() replyTo ! p.future ticking(p, target, state) } def ticking( reachDestination: CancelablePromise[DoneMoving.type], targetPos: ImVector3f, state: State ): Behavior[NpcMovementActor.Command] = Behaviors.receiveMessagePartial { case StopMoving => ctx.log.debugP( show"npcActor-${props.npcName}: Position at Stop = " + location ) props.enqueueR(() => cm.stop(props.movable)) receive(state) case MovementTick => val dst = ImVector3f.manhattanDst(targetPos, location) if (dst <= 10f) { ctx.self ! StopMoving reachDestination.success(DoneMoving) } else { // format:off ctx.log.traceP( show"npcActor-${props.npcName}: Difference = ${dst.formatted("%.2f")}" ) ctx.log.traceP( show"npcActor-${props.npcName}: Current pos = $location" ) } Behaviors.same } } object NpcMovementActorNotUsed { sealed trait Command class Props( imMovementActorBehavior: Behavior[ImMovementActor.Command], npcName: String, // movementActor: ActorRef[ImMovementActor.Command], mainEventBus: ActorRef[ EventBus.Command[Event] ], tickEventBus: GameEventBus[TickEvent] ) { def behavior: Behavior[Command] = Behaviors.setup { ctx => val movementActor = ctx.spawn( Behaviors .supervise(imMovementActorBehavior) .onFailure[Exception]( SupervisorStrategy.restart.withLimit(2, 100.millis) ), s"npc-${npcName}-MovementActorChild" ) val npcMovementElBehavior = Behaviors.receiveMessagePartial[Event] { case event: EntityMovementEvent => event match { case MovedLeft(name, pressed) => if (name == npcName) movementActor ! ImMovementActor.MoveLeft(pressed) Behaviors.same case MovedUp(name, pressed) => if (name == npcName) movementActor ! ImMovementActor.MoveUp(pressed) Behaviors.same case MovedRight(name, pressed) => if (name == npcName) movementActor ! ImMovementActor.MoveRight(pressed) Behaviors.same case MovedDown(name, pressed) => if (name == npcName) movementActor ! ImMovementActor.MoveDown(pressed) Behaviors.same } } val npcMovementEl = ctx.spawn( Behaviors .supervise(npcMovementElBehavior) .onFailure[Exception]( SupervisorStrategy.restart.withLimit(2, 100.millis) ), s"npc-${npcName}-MovementEventHandler" ) val renderTickElBehavior = Behaviors.receiveMessage[RenderTick.type] { case RenderTick => movementActor ! ImMovementActor.Tick Behaviors.same } val renderTickEl = ctx.spawn( renderTickElBehavior, s"npc-${npcName}-MovementTickListener" ) mainEventBus ! EventBus.Subscribe(npcMovementEl) tickEventBus ! EventBus.Subscribe(renderTickEl) Behaviors.receiveMessage(msg => Behaviors.same) } } }