packages feed

swarm-0.7.0.0: src/swarm-tui/Swarm/TUI/Controller/EventHandlers/Main.hs

{-# LANGUAGE OverloadedStrings #-}

-- |
-- SPDX-License-Identifier: BSD-3-Clause
--
-- Here is the main player configurable key event handler while playing the game.
module Swarm.TUI.Controller.EventHandlers.Main (
  mainEventHandlers,
  isRunning,
  whenRunningAppState,
  whenRunningPlayState,
  runSingleTick,
  adjustTPS,
  toggleREPLVisibility,
  showCESKDebug,
  hideRobots,
  toggleDiscoveryNotificationModal,
  viewGoal,
  toggleMessagesModal,
) where

import Brick
import Brick.Keybindings
import Control.Lens as Lens
import Control.Monad (unless, void, when)
import Control.Monad.IO.Class (liftIO)
import Swarm.Game.Scenario.Topography.Structure.Recognition.Type (originalStructureDefinitions)
import Swarm.Game.State
import Swarm.Game.State.Landscape
import Swarm.Game.State.Robot
import Swarm.Game.State.Substate
import Swarm.Game.Step (finishGameTick)
import Swarm.TUI.Controller.EventHandlers.Frame (runGameTickUI)
import Swarm.TUI.Controller.UpdateUI (updateUI)
import Swarm.TUI.Controller.Util
import Swarm.TUI.Editor.Model (isWorldEditorEnabled, worldOverdraw)
import Swarm.TUI.Model
import Swarm.TUI.Model.DebugOption (DebugOption (ToggleCreative, ToggleWorldEditor))
import Swarm.TUI.Model.Dialog.Goal
import Swarm.TUI.Model.Event (MainEvent (..), SwarmEvent (..))
import Swarm.TUI.Model.Menu
import Swarm.TUI.Model.UI
import Swarm.TUI.Model.UI.Gameplay
import System.Clock (Clock (..), TimeSpec (..), getTime)

-- | Main keybindings event handler while running the game itself.
--
-- See 'Swarm.TUI.Controller.handleMainEvent'.
mainEventHandlers :: [KeyEventHandler SwarmEvent (EventM Name AppState)]
mainEventHandlers = allHandlers Main $ \case
  QuitEvent -> ("Open quit game dialog", toggleQuitGameDialog)
  ViewHelpEvent ->
    ( "View Help screen"
    , Brick.zoom (playState . scenarioState) $ toggleMidScenarioModal HelpModal
    )
  ViewRobotsEvent ->
    ( "View Robots screen"
    , Brick.zoom (playState . scenarioState) $ toggleMidScenarioModal RobotsModal
    )
  ViewRecipesEvent ->
    ( "View Recipes screen"
    , Brick.zoom (playState . scenarioState) $ toggleDiscoveryNotificationModal RecipesModal availableRecipes
    )
  ViewCommandsEvent ->
    ( "View Commands screen"
    , Brick.zoom (playState . scenarioState) $ toggleDiscoveryNotificationModal CommandsModal availableCommands
    )
  ViewMessagesEvent ->
    ( "View Messages screen"
    , Brick.zoom (playState . scenarioState) toggleMessagesModal
    )
  ViewStructuresEvent ->
    ( "View Structures screen"
    , Brick.zoom (playState . scenarioState) $
        toggleStructuresModal (recognizerAutomatons . originalStructureDefinitions)
    )
  ViewGoalEvent ->
    ( "View scenario goal description"
    , Brick.zoom (playState . scenarioState) viewGoal
    )
  HideRobotsEvent -> ("Hide robots for a few ticks", Brick.zoom (playState . scenarioState . uiGameplay) hideRobots)
  ShowCESKDebugEvent -> ("Show active robot CESK machine debugging line", showCESKDebug)
  PauseEvent -> ("Pause or unpause the game", Brick.zoom (playState . scenarioState) $ whenRunningPlayState safeTogglePause)
  RunSingleTickEvent -> ("Run game for a single tick", whenRunningAppState runSingleTick)
  IncreaseTpsEvent -> ("Double game speed", Brick.zoom (playState . scenarioState) $ whenRunningPlayState . modify $ adjustTPS (+))
  DecreaseTpsEvent -> ("Halve game speed", Brick.zoom (playState . scenarioState) $ whenRunningPlayState . modify $ adjustTPS (-))
  FocusWorldEvent -> ("Set focus on the World panel", Brick.zoom (playState . scenarioState) $ setFocus WorldPanel)
  FocusRobotEvent -> ("Set focus on the Robot panel", Brick.zoom (playState . scenarioState) $ setFocus RobotPanel)
  FocusREPLEvent -> ("Set focus on the REPL panel", Brick.zoom (playState . scenarioState) $ setFocus REPLPanel)
  FocusInfoEvent -> ("Set focus on the Info panel", Brick.zoom (playState . scenarioState) $ setFocus InfoPanel)
  ToggleCreativeModeEvent -> ("Toggle creative mode", whenDebug ToggleCreative $ Brick.zoom (playState . scenarioState) toggleCreativeMode)
  ToggleWorldEditorEvent -> ("Toggle world editor mode", whenDebug ToggleWorldEditor $ Brick.zoom (playState . scenarioState) toggleWorldEditor)
  ToggleREPLVisibilityEvent -> ("Collapse/Expand REPL panel", Brick.zoom (playState . scenarioState) toggleREPLVisibility)
  ViewBaseEvent -> ("View the base robot", Brick.zoom (playState . scenarioState) viewBase)
  ToggleFPSEvent -> ("Toggle the FPS display", Brick.zoom (playState . scenarioState) toggleFPS)

toggleQuitGameDialog :: EventM Name AppState ()
toggleQuitGameDialog = do
  s <- get
  let whichModal = case s ^. playState . scenarioState . gameState . winCondition of
        WinConditions (Won _ _) _ -> ScenarioFinishModal WinModal
        WinConditions (Unwinnable _) _ -> ScenarioFinishModal LoseModal
        _ -> QuitModal
  Brick.zoom playState $ toggleEndScenarioModal whichModal $ s ^. uiState . uiMenu

toggleGameModal ::
  Foldable t =>
  MidScenarioModalType ->
  Getter GameState (t a) ->
  EventM Name ScenarioState Bool
toggleGameModal m l = do
  s <- get
  let nothingToShow = null $ s ^. gameState . l

  unless nothingToShow $
    toggleMidScenarioModal m
  return nothingToShow

toggleStructuresModal ::
  Foldable t =>
  Lens' Landscape (t a) ->
  EventM Name ScenarioState ()
toggleStructuresModal l = void $ toggleGameModal StructuresModal (landscape . l)

toggleDiscoveryNotificationModal ::
  MidScenarioModalType ->
  Lens' Discovery (Notifications a) ->
  EventM Name ScenarioState ()
toggleDiscoveryNotificationModal mt l = do
  nothingToShow <- toggleGameModal mt $ discovery . l . notificationsContent
  unless nothingToShow $ gameState . discovery . l . notificationsCount .= 0

toggleMessagesModal :: EventM Name ScenarioState ()
toggleMessagesModal = do
  s <- get
  nothingToShow <- toggleGameModal MessagesModal $ messageNotifications . notificationsContent
  unless nothingToShow $ gameState . messageInfo . lastSeenMessageTime .= s ^. gameState . temporal . ticks

viewGoal :: EventM Name ScenarioState ()
viewGoal = do
  s <- get
  if hasAnythingToShow $ s ^. uiGameplay . uiDialogs . uiGoal . goalsContent
    then toggleMidScenarioModal GoalModal
    else continueWithoutRedraw

hideRobots :: EventM Name UIGameplay ()
hideRobots = do
  t <- liftIO $ getTime Monotonic
  h <- use uiHideRobotsUntil
  case h >= t of
    -- ignore repeated keypresses
    True -> continueWithoutRedraw
    -- hide for two seconds
    False -> do
      uiHideRobotsUntil .= t + TimeSpec 2 0
      invalidateCacheEntry WorldCache

showCESKDebug :: EventM Name AppState ()
showCESKDebug = do
  s <- get
  let isPaused = s ^. playState . scenarioState . gameState . temporal . paused
  let isCreative = s ^. playState . scenarioState . gameState . creativeMode
  let hasDebug = hasDebugCapability isCreative $ s ^. playState . scenarioState . gameState
  when (isPaused && hasDebug) $ do
    debug <- playState . scenarioState . uiGameplay . uiShowDebug Lens.<%= not
    if debug
      then playState . scenarioState . gameState . temporal . gameStep .= RobotStep SBefore
      else zoomGameStateFromAppState finishGameTick >> void updateUI

runSingleTick :: EventM Name AppState ()
runSingleTick = do
  playState . scenarioState . gameState . temporal . runStatus .= ManualPause
  runGameTickUI

-- | Adjust the ticks per second speed.
adjustTPS :: (Int -> Int -> Int) -> ScenarioState -> ScenarioState
adjustTPS (+/-) = uiGameplay . uiTiming . lgTicksPerSecond %~ (+/- 1)

toggleCreativeMode :: EventM Name ScenarioState ()
toggleCreativeMode = gameState . creativeMode %= not

toggleWorldEditor :: EventM Name ScenarioState ()
toggleWorldEditor = do
  uiGameplay . uiWorldEditor . worldOverdraw . isWorldEditorEnabled %= not
  setFocus WorldEditorPanel

toggleREPLVisibility :: EventM Name ScenarioState ()
toggleREPLVisibility = do
  invalidateCacheEntry WorldCache
  uiGameplay . uiShowREPL %= not

viewBase :: EventM Name ScenarioState ()
viewBase = do
  invalidateCacheEntry WorldCache
  gameState . robotInfo . viewCenterRule .= VCRobot 0

toggleFPS :: EventM Name ScenarioState ()
toggleFPS = uiGameplay . uiTiming . uiShowFPS %= not

-- ----------------------------------------------
--                 HELPER UTILS
-- ----------------------------------------------

isRunning :: EventM Name ScenarioState Bool
isRunning = do
  mt <- preuse $ uiGameplay . uiDialogs . uiModal . _Just . modalType
  return $ maybe True isRunningModal mt

whenRunningAppState :: EventM Name AppState () -> EventM Name AppState ()
whenRunningAppState a = Brick.zoom (playState . scenarioState) isRunning >>= \r -> when r a

whenRunningPlayState :: EventM Name ScenarioState () -> EventM Name ScenarioState ()
whenRunningPlayState a = isRunning >>= \r -> when r a

whenDebug :: DebugOption -> EventM Name AppState () -> EventM Name AppState ()
whenDebug d a = do
  debug <- use $ uiState . uiDebugOptions . contains d
  when debug a