Skip to content

Commit c921906

Browse files
Merge pull request #161 from jandurovec/feature/live-indicator
Update round/turn indicator in arena panel
2 parents 85a030d + dec275b commit c921906

File tree

1 file changed

+127
-8
lines changed
  • gui-app/src/main/kotlin/dev/robocode/tankroyale/gui/ui/arena

1 file changed

+127
-8
lines changed

gui-app/src/main/kotlin/dev/robocode/tankroyale/gui/ui/arena/ArenaPanel.kt

Lines changed: 127 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package dev.robocode.tankroyale.gui.ui.arena
33
import dev.robocode.tankroyale.client.model.*
44
import dev.robocode.tankroyale.gui.client.Client
55
import dev.robocode.tankroyale.gui.client.ClientEvents
6+
import dev.robocode.tankroyale.gui.player.ReplayBattlePlayer
67
import dev.robocode.tankroyale.gui.ui.ResultsFrame
78
import dev.robocode.tankroyale.gui.ui.extensions.ColorExt.hsl
89
import dev.robocode.tankroyale.gui.ui.extensions.ColorExt.lightness
@@ -15,20 +16,37 @@ import dev.robocode.tankroyale.gui.util.Graphics2DState
1516
import dev.robocode.tankroyale.gui.util.HslColor
1617
import java.awt.*
1718
import 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.*
2220
import java.util.concurrent.CopyOnWriteArrayList
2321
import java.util.concurrent.atomic.AtomicBoolean
2422
import javax.swing.JPanel
23+
import kotlin.math.sin
2524
import kotlin.math.sqrt
2625

2726

2827
object 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

Comments
 (0)