diff --git a/src/main/kotlin/org/openbase/planetsudo/game/GameManager.kt b/src/main/kotlin/org/openbase/planetsudo/game/GameManager.kt index 923d2233..c553312d 100644 --- a/src/main/kotlin/org/openbase/planetsudo/game/GameManager.kt +++ b/src/main/kotlin/org/openbase/planetsudo/game/GameManager.kt @@ -50,6 +50,11 @@ class GameManager : Runnable { private var gameSpeedFactor: Double var gameOverSoon: Boolean + private var gameEndDurationInMs: Long = 0 + private var gameEndCalcDurationInMs: Long = 0 + private var gameEndTimeout: GameTimeout? = null + private var gameEndCalcTimeout: GameTimeout? = null + init { LOGGER.info("Create $this.") this.gameThread = Thread(this, "GameThread") @@ -116,7 +121,15 @@ class GameManager : Runnable { LOGGER.info("Set $level as new level.") } - fun startGame() { + fun setGameEndDuration(durationInMs: Long) { + gameEndDurationInMs = durationInMs + } + + fun setGameEndCalcDuration(durationInMs: Long) { + gameEndCalcDurationInMs = durationInMs + } + + fun startGame(gameEndFunction: () -> Unit) { val gameStartThread: Thread = object : Thread("Gamestart Thread") { override fun run() { LOGGER.info("Init game start...") @@ -146,11 +159,24 @@ class GameManager : Runnable { } } level?.reset() + + gameEndTimeout = null + gameEndCalcTimeout = null + + if (gameEndDurationInMs > 0) { + gameEndTimeout = GameTimeout(level!!, gameEndDurationInMs) { setGameOverSoon() } + if (gameEndCalcDurationInMs > 0) { + gameEndCalcTimeout = GameTimeout(level!!, gameEndDurationInMs + gameEndCalcDurationInMs) { gameEndFunction() } + } + } + setGameState(GameState.Running) Thread(level, "Levelrunner").start() lock.withLock { condition.signalAll() } + gameEndTimeout?.startTimer() + gameEndCalcTimeout?.startTimer() LOGGER.info("Game is Running.") } } @@ -171,11 +197,23 @@ class GameManager : Runnable { private fun setGameState(state: GameState) { GUIController.setEvent(PropertyChangeEvent(this, GUIController.GAME_STATE_CHANGE, gameState, state)) this.gameState = state - isPause = state == GameState.Break + + if (state == GameState.Break) { + isPause = true + gameEndCalcTimeout?.stopTimer() + gameEndTimeout?.stopTimer() + } else if (isPause && gameState == GameState.Running) { + isPause = false + gameEndCalcTimeout?.startTimer() + gameEndTimeout?.startTimer() + } + if (state == GameState.Running) { isGameOver = false } else if (state == GameState.Configuration) { isGameOver = true + gameEndCalcTimeout?.stopTimer() + gameEndTimeout?.stopTimer() } } diff --git a/src/main/kotlin/org/openbase/planetsudo/game/GameTimeout.kt b/src/main/kotlin/org/openbase/planetsudo/game/GameTimeout.kt new file mode 100644 index 00000000..5bc59a30 --- /dev/null +++ b/src/main/kotlin/org/openbase/planetsudo/game/GameTimeout.kt @@ -0,0 +1,57 @@ +package org.openbase.planetsudo.game + +import org.openbase.planetsudo.level.AbstractLevel +import java.awt.event.ActionEvent +import java.awt.event.ActionListener +import javax.swing.Timer + +// This still de-syncs slightly from the timer in LevelMenuPanel +class GameTimeout(level: AbstractLevel, durationInMs: Long, val function: () -> Unit) : ActionListener { + private val timer: Timer = Timer(calcTimerDelay(level.getGameSpeedFactor()), this) + private var secondsRemaining = durationInMs / 1000 + private var fired: Boolean = false + + init { // This is a blatant copy of LevelMenuPanel, thus knowingly violating single source of truth + level.addPropertyChangeListener { + if (it.propertyName == AbstractLevel.GAME_SPEED_FACTOR_CHANGED) { + timer.isRunning.let { running -> + updateDelay(it.newValue as Double) + if (running) { + timer.restart() + } + } + } + } + } + + private fun updateDelay(gameSpeed: Double) { + val delay = calcTimerDelay(gameSpeed) + timer.delay = delay + timer.initialDelay = delay + } + + private fun calcTimerDelay(gameSpeed: Double): Int { + return (1000.0 / gameSpeed).toInt() + } + + fun startTimer() { + if (!timer.isRunning && !fired) { + timer.start() + } + } + + fun stopTimer() { + if (timer.isRunning) { + timer.stop() + } + } + + override fun actionPerformed(ex: ActionEvent) { + secondsRemaining-- + if (secondsRemaining <= 0 && !fired) { + function() + fired = true + stopTimer() + } + } +} diff --git a/src/main/kotlin/org/openbase/planetsudo/view/MainGUI.kt b/src/main/kotlin/org/openbase/planetsudo/view/MainGUI.kt index 320cc280..8a9dfe58 100644 --- a/src/main/kotlin/org/openbase/planetsudo/view/MainGUI.kt +++ b/src/main/kotlin/org/openbase/planetsudo/view/MainGUI.kt @@ -260,7 +260,7 @@ class MainGUI : JFrame, PropertyChangeListener { private var finished = false - private fun finalizeGame() { + fun finalizeGame() { finished = true gameManager.switchGameState(GameState.Break) @@ -460,7 +460,7 @@ class MainGUI : JFrame, PropertyChangeListener { private fun startPauseMenuItemActionPerformed(evt: ActionEvent) { // GEN-FIRST:event_startPauseMenuItemActionPerformed when (gameManager.gameState) { - GameState.Configuration -> gameManager.startGame() + GameState.Configuration -> gameManager.startGame { finalizeGame() } GameState.Running -> gameManager.switchGameState(GameState.Break) GameState.Break -> gameManager.switchGameState(GameState.Running) else -> {} diff --git a/src/main/kotlin/org/openbase/planetsudo/view/configuration/ConfigurationPanel.kt b/src/main/kotlin/org/openbase/planetsudo/view/configuration/ConfigurationPanel.kt index e99ee49e..b86cb5cf 100644 --- a/src/main/kotlin/org/openbase/planetsudo/view/configuration/ConfigurationPanel.kt +++ b/src/main/kotlin/org/openbase/planetsudo/view/configuration/ConfigurationPanel.kt @@ -10,6 +10,7 @@ package org.openbase.planetsudo.view.configuration import org.apache.commons.io.FileUtils +import org.apache.commons.lang3.time.TimeZones import org.openbase.jul.exception.CouldNotPerformException import org.openbase.jul.exception.printer.ExceptionPrinter import org.openbase.jul.visual.swing.image.ImageLoader @@ -32,8 +33,10 @@ import java.awt.Color import java.io.File import java.io.FileInputStream import java.io.FileOutputStream +import java.text.SimpleDateFormat import java.util.* import javax.swing.* +import javax.swing.JSpinner.DateEditor import javax.swing.border.BevelBorder import javax.swing.border.SoftBevelBorder import javax.swing.event.ListSelectionEvent @@ -157,6 +160,11 @@ class ConfigurationPanel : JPanel() { levelChooserPanel = JPanel() levelChooserComboBox = JComboBox() startGameButton = JButton() + gameEndTimeSpinner = JSpinner() + gameEndCalcTimeSpinner = JSpinner() + gameEndTimeLabel = JLabel() + gameEndCalcTimeLabel = JLabel() + automaticEndPanel = JPanel() networkPanel = JPanel() networkTeamPanel = JPanel() defaultTeamComboBox = JComboBox() @@ -218,6 +226,8 @@ class ConfigurationPanel : JPanel() { gameSettingsPanel!!.border = BorderFactory.createTitledBorder("Spiel Einstellungen") + automaticEndPanel!!.border = BorderFactory.createTitledBorder("Automatisches Ende") + teamsPanel!!.border = BorderFactory.createTitledBorder("Teams") versusLabel!!.horizontalAlignment = SwingConstants.CENTER @@ -418,6 +428,65 @@ class ConfigurationPanel : JPanel() { startGameButton!!.text = "Spiel Starten" startGameButton!!.addActionListener { _ -> startGameButtonActionPerformed() } + configureDateSpinner(gameEndTimeSpinner!!) + configureDateSpinner(gameEndCalcTimeSpinner!!) + + gameEndTimeSpinner!!.addChangeListener { e -> gameManager.setGameEndDuration(((e.source as JSpinner).value as Date).time) } + gameEndCalcTimeSpinner!!.addChangeListener { e -> gameManager.setGameEndCalcDuration(((e.source as JSpinner).value as Date).time) } + + gameEndTimeLabel!!.horizontalAlignment = SwingConstants.LEADING + gameEndTimeLabel!!.text = "Automatisches Spielende einleiten" + + gameEndCalcTimeLabel!!.horizontalAlignment = SwingConstants.LEADING + gameEndCalcTimeLabel!!.text = "danach automatische Endabrechnung" + + val automaticEndPanelLayout = GroupLayout(automaticEndPanel) + automaticEndPanel!!.layout = automaticEndPanelLayout + automaticEndPanelLayout.setHorizontalGroup( + automaticEndPanelLayout.createParallelGroup(GroupLayout.Alignment.LEADING) + .addGroup( + automaticEndPanelLayout.createSequentialGroup() + .addContainerGap() + .addComponent(gameEndTimeLabel) + .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED) + .addComponent(gameEndTimeSpinner) + .addContainerGap(), + ) + .addGroup( + automaticEndPanelLayout.createSequentialGroup() + .addContainerGap() + .addComponent(gameEndCalcTimeLabel) + .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED) + .addComponent(gameEndCalcTimeSpinner) + .addContainerGap(), + ), + ) + + automaticEndPanelLayout.setVerticalGroup( + automaticEndPanelLayout.createParallelGroup(GroupLayout.Alignment.LEADING) + .addGroup( + automaticEndPanelLayout.createSequentialGroup() + .addGroup( + automaticEndPanelLayout.createParallelGroup(GroupLayout.Alignment.LEADING) + .addComponent( + gameEndTimeSpinner, + ) + .addComponent( + gameEndTimeLabel, + ), + ) + .addGroup( + automaticEndPanelLayout.createParallelGroup(GroupLayout.Alignment.LEADING) + .addComponent( + gameEndCalcTimeSpinner, + ) + .addComponent( + gameEndCalcTimeLabel, + ), + ), + ), + ) + val gameSettingsPanelLayout = GroupLayout(gameSettingsPanel) gameSettingsPanel!!.layout = gameSettingsPanelLayout gameSettingsPanelLayout.setHorizontalGroup( @@ -439,6 +508,12 @@ class ConfigurationPanel : JPanel() { GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE.toInt(), ) + .addComponent( + automaticEndPanel, + GroupLayout.DEFAULT_SIZE, + GroupLayout.DEFAULT_SIZE, + Short.MAX_VALUE.toInt(), + ) .addComponent( startGameButton, GroupLayout.Alignment.TRAILING, @@ -459,6 +534,8 @@ class ConfigurationPanel : JPanel() { .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED) .addComponent(levelChooserPanel, GroupLayout.PREFERRED_SIZE, 62, GroupLayout.PREFERRED_SIZE) .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED) + .addComponent(automaticEndPanel) + .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED) .addComponent(startGameButton) .addContainerGap(GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE.toInt()), ), @@ -668,6 +745,22 @@ class ConfigurationPanel : JPanel() { ) } // //GEN-END:initComponents + private fun configureDateSpinner(dateSpinner: JSpinner) { + val dateModel = SpinnerDateModel() + dateModel.calendarField = Calendar.MINUTE + dateSpinner.model = dateModel + dateModel.start = Date(0) + dateModel.end = Date(60 * 60 * 1000 - 1) // 59:59 in ms + + dateSpinner.editor = DateEditor(dateSpinner) + val dateFormat: SimpleDateFormat = (dateSpinner.editor as DateEditor).format + dateFormat.applyLocalizedPattern("mm:ss") + dateFormat.timeZone = TimeZones.GMT // GMT because that's where a displayed value of "00:00" is 0 ms + + // Update value here because of https://stackoverflow.com/a/37444974 + dateModel.value = Date(0) // TODO keep last value as default? + } + private fun levelChooserComboBoxActionPerformed() { // GEN-FIRST:event_levelChooserComboBoxActionPerformed object : SwingWorker() { @Throws(Exception::class) @@ -716,7 +809,7 @@ class ConfigurationPanel : JPanel() { private fun startGameButtonActionPerformed() { // GEN-FIRST:event_startGameButtonActionPerformed MainGUI.instance!!.showLoadingPanel() - gameManager.startGame() + gameManager.startGame { MainGUI.instance!!.finalizeGame() } } // GEN-LAST:event_startGameButtonActionPerformed private fun teamAComboBoxActionPerformed() { // GEN-FIRST:event_teamAComboBoxActionPerformed @@ -794,6 +887,11 @@ class ConfigurationPanel : JPanel() { private var connectionStateLabel: JLabel? = null private var defaultTeamComboBox: JComboBox? = null private var startGameButton: JButton? = null + private var gameEndTimeSpinner: JSpinner? = null + private var gameEndCalcTimeSpinner: JSpinner? = null + private var gameEndTimeLabel: JLabel? = null + private var gameEndCalcTimeLabel: JLabel? = null + private var automaticEndPanel: JPanel? = null private var teamALabel: JLabel? = null private var teamBLabel: JLabel? = null private var versusLabel: JLabel? = null diff --git a/src/main/kotlin/org/openbase/planetsudo/view/menu/LevelMenuPanel.kt b/src/main/kotlin/org/openbase/planetsudo/view/menu/LevelMenuPanel.kt index 443cce77..26986078 100644 --- a/src/main/kotlin/org/openbase/planetsudo/view/menu/LevelMenuPanel.kt +++ b/src/main/kotlin/org/openbase/planetsudo/view/menu/LevelMenuPanel.kt @@ -26,17 +26,30 @@ class LevelMenuPanel : JPanel(), ActionListener { fun setLevel(level: AbstractLevel) { levelName = level.name + updateDelay(level.getGameSpeedFactor()) level.addPropertyChangeListener { if (it.propertyName == AbstractLevel.GAME_SPEED_FACTOR_CHANGED) { timer.isRunning.let { running -> - timer.delay = (1000.0 * (1 / (it.newValue as Double))).toInt() - if (running) timer.restart() + updateDelay(it.newValue as Double) + if (running) { + timer.restart() + } } } } updateTitle() } + private fun updateDelay(gameSpeed: Double) { + val delay = calcTimerDelay(gameSpeed) + timer.delay = delay + timer.initialDelay = delay + } + + private fun calcTimerDelay(gameSpeed: Double): Int { + return (1000.0 / gameSpeed).toInt() + } + private var text: String? = null private fun updateTitle() { text = "$levelName [" @@ -52,7 +65,9 @@ class LevelMenuPanel : JPanel(), ActionListener { } fun startTimer() { - if (!timer.isRunning) timer.start() + if (!timer.isRunning) { + timer.start() + } } fun stopTimer() {