@@ -3,6 +3,7 @@ package dev.robocode.tankroyale.gui.ui.arena
33import dev.robocode.tankroyale.client.model.*
44import dev.robocode.tankroyale.gui.client.Client
55import dev.robocode.tankroyale.gui.client.ClientEvents
6+ import dev.robocode.tankroyale.gui.player.ReplayBattlePlayer
67import dev.robocode.tankroyale.gui.ui.ResultsFrame
78import dev.robocode.tankroyale.gui.ui.extensions.ColorExt.hsl
89import dev.robocode.tankroyale.gui.ui.extensions.ColorExt.lightness
@@ -15,20 +16,37 @@ import dev.robocode.tankroyale.gui.util.Graphics2DState
1516import dev.robocode.tankroyale.gui.util.HslColor
1617import java.awt.*
1718import java.awt.event.*
18- import java.awt.geom.AffineTransform
19- import java.awt.geom.Arc2D
20- import java.awt.geom.Area
21- import java.awt.geom.Ellipse2D
19+ import java.awt.geom.*
2220import java.util.concurrent.CopyOnWriteArrayList
2321import java.util.concurrent.atomic.AtomicBoolean
2422import javax.swing.JPanel
23+ import kotlin.math.sin
2524import kotlin.math.sqrt
2625
2726
2827object ArenaPanel : JPanel() {
2928
3029 private val circleShape = Area (Ellipse2D .Double (- 0.5 , - 0.5 , 1.0 , 1.0 ))
3130
31+ // Game status indicator constants
32+ private const val INDICATOR_BACKGROUND_OPACITY = 0.8f
33+ private const val INDICATOR_SHADOW_OPACITY = 0.3f
34+ private val INDICATOR_LIVE_BG_COLOR = Color (220 , 20 , 60 ) // Crimson red (solid)
35+ private val INDICATOR_REPLAY_BG_COLOR = Color (40 , 40 , 40 ) // Dark gray (solid)
36+ private val INDICATOR_ROUND_BG_COLOR = Color (60 , 60 , 60 , (255 * INDICATOR_BACKGROUND_OPACITY ).toInt()) // Medium dark gray
37+ private val INDICATOR_TURN_BG_COLOR = Color (80 , 80 , 80 , (255 * INDICATOR_BACKGROUND_OPACITY ).toInt()) // Lighter gray
38+ private val INDICATOR_SHADOW_COLOR = Color .BLACK
39+ private val INDICATOR_STATUS_COLOR = Color .WHITE
40+ private val INDICATOR_TEXT_COLOR = Color .WHITE
41+ private val INDICATOR_STATUS_FONT = Font (Font .SANS_SERIF , Font .BOLD , 14 )
42+ private val INDICATOR_INFO_FONT = Font (Font .SANS_SERIF , Font .PLAIN , 12 )
43+ private const val INDICATOR_X_OFFSET = 20.0
44+ private const val INDICATOR_Y_OFFSET = 20.0
45+ private const val INDICATOR_HEIGHT = 25.0
46+ private const val INDICATOR_CORNER_RADIUS = 8
47+ private const val INDICATOR_TEXT_PADDING = 8
48+
49+
3250 private val explosions = CopyOnWriteArrayList <Animation >()
3351
3452 private var arenaWidth: Int = Client .currentGameSetup?.arenaWidth ? : 800
@@ -39,6 +57,9 @@ object ArenaPanel : JPanel() {
3957 private var bots: Set <BotState > = HashSet ()
4058 private var bullets: Set <BulletState > = HashSet ()
4159
60+ // Battle mode state
61+ private var isLiveMode: Boolean = true
62+
4263 private val tick = AtomicBoolean (false )
4364
4465 private var scale = 1.0
@@ -69,13 +90,19 @@ object ArenaPanel : JPanel() {
6990 onGameEnded.subscribe(ArenaPanel ) { onGameEnded(it) }
7091 onTickEvent.subscribe(ArenaPanel ) { onTick(it) }
7192 onGameStarted.subscribe(ArenaPanel ) { onGameStarted(it) }
93+ onPlayerChanged.subscribe(ArenaPanel ) { onPlayerChanged(it) }
7294 }
7395 }
7496
7597 private fun onGameEnded (gameEndedEvent : GameEndedEvent ) {
7698 ResultsFrame (gameEndedEvent.results).isVisible = true
7799 }
78100
101+ private fun onPlayerChanged (player : dev.robocode.tankroyale.gui.player.BattlePlayer ) {
102+ isLiveMode = player !is ReplayBattlePlayer // Default to LIVE mode for other player types
103+ repaint() // Refresh the display to show correct indicator
104+ }
105+
79106 private fun onTick (tickEvent : TickEvent ) {
80107 if (tick.get()) return
81108 tick.set(true )
@@ -215,6 +242,9 @@ object ArenaPanel : JPanel() {
215242 val marginX = (size.width - arenaWidth * scale) / 2
216243 val marginY = (size.height - arenaHeight * scale) / 2
217244
245+ // Save original transform to restore for indicator rendering
246+ val originalTransform = g.transform
247+
218248 // Move the offset of the arena
219249 g.translate(marginX + deltaX, marginY + deltaY)
220250
@@ -225,8 +255,11 @@ object ArenaPanel : JPanel() {
225255 drawBots(g)
226256 drawExplosions(g)
227257 drawBullets(g)
228- drawRoundInfo(g)
229258 drawDebugGraphics(g)
259+
260+ // Restore original transform and draw indicator last (so it's not overwritten)
261+ g.transform = originalTransform
262+ drawRoundInfo(g)
230263 }
231264
232265 private fun drawBots (g : Graphics2D ) {
@@ -321,9 +354,83 @@ object ArenaPanel : JPanel() {
321354 private fun drawRoundInfo (g : Graphics2D ) {
322355 val oldState = Graphics2DState (g)
323356
324- g.scale(1.0 , - 1.0 )
325- g.color = Color .YELLOW
326- g.drawString(" Round $round , Turn: $time " , 10 , 20 - arenaHeight)
357+ val x = INDICATOR_X_OFFSET
358+ val y = INDICATOR_Y_OFFSET
359+
360+ // Calculate section widths
361+ val statusText = if (isLiveMode) " LIVE" else " REPLAY"
362+ val roundText = " ROUND $round "
363+ val turnText = " TURN $time "
364+
365+ g.font = INDICATOR_STATUS_FONT
366+ val statusFontMetrics = g.fontMetrics
367+ val statusBounds = statusFontMetrics.getStringBounds(statusText, g)
368+ val statusWidth = (statusBounds.width + 2 * INDICATOR_TEXT_PADDING ).toInt()
369+
370+ g.font = INDICATOR_INFO_FONT
371+ val infoFontMetrics = g.fontMetrics
372+ val roundBounds = infoFontMetrics.getStringBounds(roundText, g)
373+ val turnBounds = infoFontMetrics.getStringBounds(turnText, g)
374+ val roundWidth = (roundBounds.width + 2 * INDICATOR_TEXT_PADDING ).toInt()
375+ val turnWidth = (turnBounds.width + 2 * INDICATOR_TEXT_PADDING ).toInt()
376+
377+ val totalWidth = statusWidth + roundWidth + turnWidth
378+
379+ // Draw drop shadow
380+ g.composite = AlphaComposite .getInstance(AlphaComposite .SRC_OVER , INDICATOR_SHADOW_OPACITY )
381+ g.color = INDICATOR_SHADOW_COLOR
382+ g.fillRoundRect((x + 2 ).toInt(), (y + 2 ).toInt(), totalWidth, INDICATOR_HEIGHT .toInt(), INDICATOR_CORNER_RADIUS , INDICATOR_CORNER_RADIUS )
383+ g.composite = AlphaComposite .getInstance(AlphaComposite .SRC_OVER , 1.0f )
384+
385+ // Draw all sections pixel-perfect without overlaps
386+ val roundX = x + statusWidth.toDouble()
387+ val turnX = roundX + roundWidth.toDouble()
388+
389+ // Draw stackable blocks from right
390+
391+ // Draw TURN section - right side rounded, left side inversely rounded for ROUND to fit in
392+ g.color = INDICATOR_TURN_BG_COLOR
393+ g.clip = createStackableRoundedCornersShape(turnX - INDICATOR_CORNER_RADIUS , y, (turnWidth + INDICATOR_CORNER_RADIUS ).toDouble(), INDICATOR_HEIGHT , INDICATOR_CORNER_RADIUS .toDouble())
394+ g.fillRect(turnX.toInt() - INDICATOR_CORNER_RADIUS , y.toInt(), turnWidth + INDICATOR_CORNER_RADIUS , INDICATOR_HEIGHT .toInt())
395+
396+ // Draw ROUND section - right side rounded, left side inversely rounded for STATUS to fit in
397+ g.color = INDICATOR_ROUND_BG_COLOR
398+ g.clip = createStackableRoundedCornersShape(roundX - INDICATOR_CORNER_RADIUS , y, (roundWidth + INDICATOR_CORNER_RADIUS ).toDouble(), INDICATOR_HEIGHT , INDICATOR_CORNER_RADIUS .toDouble())
399+ g.fillRect(roundX.toInt() - INDICATOR_CORNER_RADIUS , y.toInt(), roundWidth + INDICATOR_CORNER_RADIUS , INDICATOR_HEIGHT .toInt())
400+ g.clip = null
401+
402+ // Draw status section (LIVE/REPLAY) - both sides rounded
403+ val statusColor = if (isLiveMode) INDICATOR_LIVE_BG_COLOR else INDICATOR_REPLAY_BG_COLOR
404+ g.color = statusColor
405+ g.fillRoundRect(x.toInt(), y.toInt(), statusWidth, INDICATOR_HEIGHT .toInt(), INDICATOR_CORNER_RADIUS , INDICATOR_CORNER_RADIUS )
406+
407+ // Draw status text (LIVE/REPLAY) (properly centered)
408+ g.font = INDICATOR_STATUS_FONT
409+
410+ if (isLiveMode) {
411+ g.color = INDICATOR_STATUS_COLOR
412+ } else {
413+ // Pulsating effect for REPLAY text (opacity between 60% and 100%)
414+ val replayOpacity = (0.6 + 0.4 * sin(System .currentTimeMillis() * 0.004 )).toFloat()
415+ g.color = Color (INDICATOR_STATUS_COLOR .red, INDICATOR_STATUS_COLOR .green, INDICATOR_STATUS_COLOR .blue, (255 * replayOpacity).toInt())
416+ }
417+
418+ val statusTextX = x + INDICATOR_TEXT_PADDING
419+ val statusTextY = y + (INDICATOR_HEIGHT - statusFontMetrics.height) / 2 + statusFontMetrics.ascent
420+ g.drawString(statusText, statusTextX.toInt(), statusTextY.toInt())
421+
422+ // Draw round text (properly centered)
423+ g.font = INDICATOR_INFO_FONT
424+ g.color = INDICATOR_TEXT_COLOR
425+ val roundTextX = roundX + INDICATOR_TEXT_PADDING
426+ val roundTextY = y + (INDICATOR_HEIGHT - infoFontMetrics.height) / 2 + infoFontMetrics.ascent
427+ g.drawString(roundText, roundTextX.toInt(), roundTextY.toInt())
428+
429+ // Draw turn text (properly centered)
430+ g.color = INDICATOR_TEXT_COLOR
431+ val turnTextX = turnX + INDICATOR_TEXT_PADDING
432+ val turnTextY = y + (INDICATOR_HEIGHT - infoFontMetrics.height) / 2 + infoFontMetrics.ascent
433+ g.drawString(turnText, turnTextX.toInt(), turnTextY.toInt())
327434
328435 oldState.restore(g)
329436 }
@@ -433,4 +540,16 @@ object ArenaPanel : JPanel() {
433540 y = origY
434541 }
435542 }
543+
544+ /* *
545+ * Draws a rectangle with rounded corners on the right side and negative rounded corners (for stacking) on the
546+ * left side.
547+ */
548+ private fun createStackableRoundedCornersShape (x : Double , y : Double , width : Double , height : Double , cornerRadius : Double ): Area {
549+ // start with the rounded rectangle
550+ val area = Area (RoundRectangle2D .Double (x - cornerRadius, y, width + cornerRadius, height, cornerRadius, cornerRadius))
551+ area.subtract(Area (RoundRectangle2D .Double (x - cornerRadius, y, 2 * cornerRadius, height, cornerRadius, cornerRadius)))
552+ return area
553+ }
554+
436555}
0 commit comments