diff --git a/src/main/kotlin/org/openbase/planetsudo/game/strategy/AbstractStrategy.kt b/src/main/kotlin/org/openbase/planetsudo/game/strategy/AbstractStrategy.kt index 2b5d4d86..36aa68b5 100644 --- a/src/main/kotlin/org/openbase/planetsudo/game/strategy/AbstractStrategy.kt +++ b/src/main/kotlin/org/openbase/planetsudo/game/strategy/AbstractStrategy.kt @@ -29,6 +29,8 @@ abstract class AbstractStrategy>(val agent: Agen private val strategyOwner: Agent by lazy { agent as Agent } val mothership: MothershipInterface by lazy { strategyOwner.mothership } + + val tower: TowerInterface by lazy { strategyOwner.mothership.tower } val enemyAgent get() = strategyOwner.enemyAgent val teamAgent get() = strategyOwner.teamAgent private val mothershipInternal: Mothership by lazy { strategyOwner.mothership } diff --git a/src/main/kotlin/org/openbase/planetsudo/game/strategy/DivineStrategy.kt b/src/main/kotlin/org/openbase/planetsudo/game/strategy/DivineStrategy.kt index 1c1ade6e..327c2a92 100644 --- a/src/main/kotlin/org/openbase/planetsudo/game/strategy/DivineStrategy.kt +++ b/src/main/kotlin/org/openbase/planetsudo/game/strategy/DivineStrategy.kt @@ -1,6 +1,7 @@ package org.openbase.planetsudo.game.strategy import org.openbase.planetsudo.game.SwatTeam +import org.openbase.planetsudo.level.LevelSize import org.openbase.planetsudo.level.levelobjects.AgentInterface import org.openbase.planetsudo.level.levelobjects.Resource.ResourceType import org.openbase.planetsudo.level.levelobjects.Tower @@ -37,18 +38,17 @@ class DivineStrategy(agent: AgentInterface) : StrategyLevelLegacy(agent) { } }, ) - // --------------------------------------------> - createRule( - object : Rule("Discover", SwatTeam.COMMANDER) { - override fun constraint(): Boolean { - return true - } - override fun action() { - agent.goRight(4) - } - }, - ) + "Discover" commander inCase { true } then { agent.goRight(4) } + + "Scan" commander inCase { tower.type == Tower.TowerType.ObservationTower && tower.levelSize == LevelSize.UNKNOWN } then { + tower.scanLevelSize() + } + + "Place Observation Tower" commander inCase { agent.hasTower } then { + agent.constructTower(Tower.TowerType.ObservationTower) + } + // --------------------------------------------> // createRule( // object : Rule("Avoid Agents", SwatTeam.COMMANDER) { @@ -498,19 +498,6 @@ class DivineStrategy(agent: AgentInterface) : StrategyLevelLegacy(agent) { } }, ) - // --------------------------------------------> - createRule( - object : Rule("Construct Tower", SwatTeam.COMMANDER) { - override fun constraint(): Boolean { - return agent.hasTower && agent.seeResource - } - - override fun action() { - agent.constructTower(Tower.TowerType.DefenceTower) - } - }, - ) - // --------------------------------------------> createRule( object : Rule("Follow Wall", SwatTeam.ALPHA) { override fun constraint(): Boolean { diff --git a/src/main/kotlin/org/openbase/planetsudo/level/AbstractLevel.kt b/src/main/kotlin/org/openbase/planetsudo/level/AbstractLevel.kt index 90ba225b..dc710792 100644 --- a/src/main/kotlin/org/openbase/planetsudo/level/AbstractLevel.kt +++ b/src/main/kotlin/org/openbase/planetsudo/level/AbstractLevel.kt @@ -33,6 +33,7 @@ abstract class AbstractLevel : AbstractGameObject, Runnable { private val RESOURCES_LOCK: Any = SyncObject("ResourcesLock") private val TOWER_LOCK: Any = SyncObject("TowerLock") private val base: Point2D + val size: LevelSize by lazy { LevelSize.fromLevel(this) } @JvmField val levelBorderPolygon: Polygon @@ -102,11 +103,11 @@ abstract class AbstractLevel : AbstractGameObject, Runnable { this.base = calculateBasePosition(levelBorderPolygon) levelBorderPolygon.translate(-base.x.toInt(), -base.y.toInt()) - if (levelWallPolygons != null) { - for (p in levelWallPolygons) { - p.translate(-base.x.toInt(), -base.y.toInt()) - } + + for (p in levelWallPolygons) { + p.translate(-base.x.toInt(), -base.y.toInt()) } + for (homepos in homePositions) { homepos.translateIntoBase(base) } diff --git a/src/main/kotlin/org/openbase/planetsudo/level/LevelSize.kt b/src/main/kotlin/org/openbase/planetsudo/level/LevelSize.kt new file mode 100644 index 00000000..3c01be3e --- /dev/null +++ b/src/main/kotlin/org/openbase/planetsudo/level/LevelSize.kt @@ -0,0 +1,22 @@ +package org.openbase.planetsudo.level + +import kotlin.math.max + +enum class LevelSize(val label: String) { + SMALL("Klein"), + MEDIUM("Mittel"), + LARGE("Groß"), + UNKNOWN("?"), + ; + + companion object { + fun fromLevel(level: AbstractLevel): LevelSize { + val size = max(level.levelBorderPolygon.bounds2D.width, level.levelBorderPolygon.bounds2D.height) + return when { + size <= 1000 -> SMALL + size <= 1500 -> MEDIUM + else -> LARGE + } + } + } +} diff --git a/src/main/kotlin/org/openbase/planetsudo/level/levelobjects/Tower.kt b/src/main/kotlin/org/openbase/planetsudo/level/levelobjects/Tower.kt index 7fcc4b74..af13ddc5 100644 --- a/src/main/kotlin/org/openbase/planetsudo/level/levelobjects/Tower.kt +++ b/src/main/kotlin/org/openbase/planetsudo/level/levelobjects/Tower.kt @@ -9,6 +9,7 @@ import org.openbase.planetsudo.game.GameSound import org.openbase.planetsudo.geometry.Direction2D import org.openbase.planetsudo.geometry.Point2D import org.openbase.planetsudo.level.AbstractLevel +import org.openbase.planetsudo.level.LevelSize import org.openbase.planetsudo.level.ResourcePlacement import org.slf4j.Logger import org.slf4j.LoggerFactory @@ -34,23 +35,34 @@ class Tower(id: Int, level: AbstractLevel, @JvmField val mothership: Mothership) SIZE.toDouble(), ObjectShape.Rec, ), - ActionListener { + ActionListener, + TowerInterface { enum class TowerType { - DefenceTower, ObservationTower + Unknown, DefenceTower, ObservationTower } - var type: TowerType? = null + override var type: TowerType = TowerType.Unknown private set + private val lock = ReentrantLock() private val condition = lock.newCondition() private val placement: ResourcePlacement? = null private val timer: Timer + override var levelSize: LevelSize = LevelSize.UNKNOWN + var shieldForce: Int = 0 private set - var fuel: Int = 0 + + override var fuel: Int = 0 private set + override val fuelVolume: Int + get() = TOWER_FUEL_VOLUME + + override val fuelInPercent: Int + get() = (fuel * 100) / fuelVolume + @JvmField val direction: Direction2D = Direction2D(0) val isAttacked: Boolean = false @@ -70,7 +82,7 @@ class Tower(id: Int, level: AbstractLevel, @JvmField val mothership: Mothership) Thread.sleep(1000) continue } - direction.angle = direction.angle + 10 + direction.angle += 10 Thread.sleep(100) } } catch (ex: InterruptedException) { @@ -82,7 +94,7 @@ class Tower(id: Int, level: AbstractLevel, @JvmField val mothership: Mothership) } @Throws(CouldNotPerformException::class) - fun construct(type: TowerType?, commander: Agent) { + fun construct(type: TowerType, commander: Agent) { if (!commander.isCommander) { commander.kill() throw CouldNotPerformException("Only the commander can construct a tower!") @@ -114,7 +126,7 @@ class Tower(id: Int, level: AbstractLevel, @JvmField val mothership: Mothership) isConstructed = false } - fun seeTower(agent: Agent): Boolean { + override fun seeTower(agent: Agent): Boolean { if (!isConstructed) { return false } @@ -122,12 +134,12 @@ class Tower(id: Int, level: AbstractLevel, @JvmField val mothership: Mothership) return bounds.intersects(agent.bounds) } - fun orderFuel(fuel: Int, agent: Agent?): Int { + override fun orderFuel(fuel: Int, agent: Agent?): Int { var fuel = fuel if (agent == null || bounds.contains(agent.bounds)) { try { val oldFuel = this.fuel - if (fuel <= 0) { // fuel emty + if (fuel <= 0) { // fuel empty fuel = 0 } else if (this.fuel < fuel) { // use last fuel fuel = this.fuel @@ -148,7 +160,7 @@ class Tower(id: Int, level: AbstractLevel, @JvmField val mothership: Mothership) return fuel } - fun hasFuel(): Boolean { + override fun hasFuel(): Boolean { return fuel > 0 } @@ -162,7 +174,7 @@ class Tower(id: Int, level: AbstractLevel, @JvmField val mothership: Mothership) } @Synchronized - fun attack() { + override fun attack() { logger.debug("Attack Mothership") if (shieldForce > 0) { shieldForce-- @@ -177,7 +189,7 @@ class Tower(id: Int, level: AbstractLevel, @JvmField val mothership: Mothership) } @Synchronized - fun repair() { + override fun repair() { if (shieldForce < 100) { shieldForce++ if (shieldForce > Mothership.Companion.BURNING_TOWER && timer.isRunning) { @@ -187,16 +199,24 @@ class Tower(id: Int, level: AbstractLevel, @JvmField val mothership: Mothership) } } - val isBurning: Boolean + override fun scanLevelSize() { + if (!isConstructed) { + return + } + levelSize = level.size + changes.firePropertyChange(TOWER_SCAN_LEVEL, null, null) + } + + override val isBurning: Boolean get() = shieldForce < Mothership.BURNING_TOWER && hasFuel() - val shieldPoints: Int + override val shieldPoints: Int get() = shieldForce / 2 - val isMaxDamaged: Boolean + override val isMaxDamaged: Boolean get() = shieldForce == 0 - val isDamaged: Boolean + override val isDamaged: Boolean get() = shieldForce < 100 override fun actionPerformed(ex: ActionEvent) { @@ -210,6 +230,7 @@ class Tower(id: Int, level: AbstractLevel, @JvmField val mothership: Mothership) const val TOWER_FUEL_VOLUME: Int = 500 const val TOWER_FUEL_STATE_CHANGE: String = "FuelStateChange" const val TOWER_SHIELD_STATE_CHANGE: String = "ShieldStateChange" + const val TOWER_SCAN_LEVEL: String = "ScanLevel" const val TOWER_CONSTRUCT: String = "construct tower" const val TOWER_DECONSTRUCT: String = "deconstruct tower" diff --git a/src/main/kotlin/org/openbase/planetsudo/level/levelobjects/TowerInterface.kt b/src/main/kotlin/org/openbase/planetsudo/level/levelobjects/TowerInterface.kt new file mode 100644 index 00000000..0f9a8bbc --- /dev/null +++ b/src/main/kotlin/org/openbase/planetsudo/level/levelobjects/TowerInterface.kt @@ -0,0 +1,108 @@ +package org.openbase.planetsudo.level.levelobjects + +import org.openbase.planetsudo.level.LevelSize + +/*- + * #%L + * PlanetSudo GameEngine + * %% + * Copyright (C) 2009 - 2024 openbase.org + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ +/** + * + * @author Divine Threepwood + */ +interface TowerInterface { + + val de get() = TowerInterfaceGermanWrapper(this) + + /** + * Checks whether the given agent can see the tower. + */ + fun seeTower(agent: Agent): Boolean + + /** + * Orders fuel from the tower. Returns the amount actually provided. + */ + fun orderFuel(fuel: Int, agent: Agent?): Int + + /** + * Returns whether the tower has any fuel left. + */ + fun hasFuel(): Boolean + + /** + * Current fuel amount of the tower. + */ + val fuel: Int + + /** + * Maximum fuel capacity of the tower. + */ + val fuelVolume: Int + + /** + * Current fuel level as percentage. + */ + val fuelInPercent: Int + + /** + * Makes the tower perform an attack. + */ + fun attack() + + /** + * Repairs the tower (increases shield strength). + */ + fun repair() + + /** + * Returns whether the tower is burning (losing fuel). + */ + val isBurning: Boolean + + /** + * Shield points of the tower. + */ + val shieldPoints: Int + + /** + * Returns whether the tower is completely destroyed (no shield left). + */ + val isMaxDamaged: Boolean + + /** + * Returns whether the tower is damaged (shield not full). + */ + val isDamaged: Boolean + + /** + * Current tower type (DefenceTower or ObservationTower). Unknown if no tower is constructed. + */ + val type: Tower.TowerType + + /** + * Scans and updates the currently detected level size. + */ + fun scanLevelSize() + + /** + * The last scanned level size. + */ + val levelSize: LevelSize +} diff --git a/src/main/kotlin/org/openbase/planetsudo/level/levelobjects/TowerInterfaceGermanWrapper.kt b/src/main/kotlin/org/openbase/planetsudo/level/levelobjects/TowerInterfaceGermanWrapper.kt new file mode 100644 index 00000000..ee9c8c8e --- /dev/null +++ b/src/main/kotlin/org/openbase/planetsudo/level/levelobjects/TowerInterfaceGermanWrapper.kt @@ -0,0 +1,104 @@ +package org.openbase.planetsudo.level.levelobjects + +/*- + * #%L + * PlanetSudo GameEngine + * %% + * Copyright (C) 2009 - 2024 openbase.org + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ +/** + * + * @author Divine Threepwood + */ +class TowerInterfaceGermanWrapper(private val tower: TowerInterface) { + + /** + * Prüft ob der Agent den Turm sehen kann. + */ + fun siehtTurm(agent: Agent) = tower.seeTower(agent) + + /** + * Bestellt Treibstoff vom Turm. + */ + fun bestelleTreibstoff(menge: Int, agent: Agent?) = tower.orderFuel(menge, agent) + + /** + * Gibt zurück, ob der Turm Treibstoff hat. + */ + fun hatTreibstoff() = tower.hasFuel() + + /** + * Aktueller Treibstoff des Turms. + */ + val treibstoff get() = tower.fuel + + /** + * Maximales Treibstoffvolumen des Turms. + */ + val treibstoffVolumen get() = tower.fuelVolume + + /** + * Treibstoff in Prozent. + */ + val treibstoffInProzent get() = tower.fuelInPercent + + /** + * Lässt den Turm angreifen. + */ + fun greifeAn() = tower.attack() + + /** + * Repariert den Turm. + */ + fun repariere() = tower.repair() + + /** + * Gibt zurück, ob der Turm brennt. + */ + val istAmBrennen get() = tower.isBurning + + /** + * Gibt die Schildpunkte des Turms zurück. + */ + val schildPunkte get() = tower.shieldPoints + + /** + * Gibt zurück, ob der Turm maximal beschädigt ist. + */ + val istMaximalBeschaedigt get() = tower.isMaxDamaged + + /** + * Gibt zurück, ob der Turm beschädigt ist. + */ + val istBeschaedigt get() = tower.isDamaged + + /** + * Scannt die Level-Größe und aktualisiert die interne Größe. + */ + fun scanneLevelGroesse() = tower.scanLevelSize() + + /** + * Zuletzt gescannte Level-Größe. + */ + val levelGroesse get() = tower.levelSize + + /** + * Typ des Turms (DefenceTower oder ObservationTower). Null wenn nicht konstruiert. + */ + val turmTyp get() = tower.type +} 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 a3de66ec..e99ee49e 100644 --- a/src/main/kotlin/org/openbase/planetsudo/view/configuration/ConfigurationPanel.kt +++ b/src/main/kotlin/org/openbase/planetsudo/view/configuration/ConfigurationPanel.kt @@ -695,6 +695,8 @@ class ConfigurationPanel : JPanel() { } val level = getInstance()!!.loadLevel(levelName) gameManager.setLevel(level!!) + levelPreviewPanel!!.border = + BorderFactory.createTitledBorder("Nächstes Level: ${level.name} \tGröße: ${level.size.label}") levelPreviewDisplayPanel!!.setLevel(level) levelPreviewDisplayPanel!!.isOpaque = true levelPreviewDisplayPanel!!.background = level.color diff --git a/src/main/kotlin/org/openbase/planetsudo/view/level/LevelDisplayPanel.kt b/src/main/kotlin/org/openbase/planetsudo/view/level/LevelDisplayPanel.kt index be324d15..bfa5680e 100644 --- a/src/main/kotlin/org/openbase/planetsudo/view/level/LevelDisplayPanel.kt +++ b/src/main/kotlin/org/openbase/planetsudo/view/level/LevelDisplayPanel.kt @@ -14,6 +14,7 @@ import org.openbase.planetsudo.level.AbstractLevel import org.slf4j.Logger import org.slf4j.LoggerFactory import javax.swing.GroupLayout +import javax.swing.JLabel /** * @@ -41,6 +42,8 @@ class LevelDisplayPanel : ResourceDisplayPanel(), Runnable { setVisibleResourcePanel(LevelPanel(level, this)) } + private val level get() = visibleResourcePanel?.resource + fun displayLevelObjects() { if (visibleResourcePanel != null) { visibleResourcePanel?.loadLevelObjects() @@ -89,6 +92,9 @@ class LevelDisplayPanel : ResourceDisplayPanel(), Runnable { layout.createParallelGroup(GroupLayout.Alignment.LEADING) .addGap(0, 300, Short.MAX_VALUE.toInt()), ) + level?.let { + parent.add("Label", JLabel("Name: ${level?.name} (${level?.size}) }")) + } } // //GEN-END:initComponents override fun run() { diff --git a/src/main/kotlin/org/openbase/planetsudo/view/level/levelobjects/TowerTopPanel.kt b/src/main/kotlin/org/openbase/planetsudo/view/level/levelobjects/TowerTopPanel.kt index 397cce40..4a548443 100644 --- a/src/main/kotlin/org/openbase/planetsudo/view/level/levelobjects/TowerTopPanel.kt +++ b/src/main/kotlin/org/openbase/planetsudo/view/level/levelobjects/TowerTopPanel.kt @@ -6,10 +6,14 @@ import org.openbase.planetsudo.level.levelobjects.Tower import org.openbase.planetsudo.level.levelobjects.Tower.TowerType import org.slf4j.Logger import org.slf4j.LoggerFactory +import java.awt.BasicStroke +import java.awt.Color import java.awt.Graphics2D import java.awt.event.MouseEvent +import java.awt.geom.Arc2D import java.beans.PropertyChangeEvent import java.beans.PropertyChangeListener +import javax.swing.Timer /** * @@ -29,10 +33,40 @@ class TowerTopPanel(tower: Tower, parentPanel: TowerPanel) : private var gg2: Graphics2D? = null private var direction: Direction2D? = null + // scanning animation state: expanding wave rings + private var scanning: Boolean = false + private val scanRings: MutableList = ArrayList() + private var scanTimer: Timer? = null + private var scanMaxRadius: Double = 0.0 + private val ringSpeed: Double = 12.0 // pixels per tick + private val ringSpacing: Double = 40.0 // spacing between consecutive rings + init { LOGGER.info("Create $this") this.tower = tower tower.addPropertyChangeListener(this) + + // Timer advances rings outward + scanTimer = Timer(40) { + if (!scanning) return@Timer + val it = scanRings.listIterator() + while (it.hasNext()) { + val idx = it.nextIndex() + val v = it.next() + val nv = v + ringSpeed + it.set(nv) + // remove ring if beyond max radius + spacing + if (nv > scanMaxRadius + ringSpacing * 2) { + it.remove() + } + } + // stop when no rings left + if (scanRings.isEmpty()) { + scanning = false + scanTimer?.stop() + } + parentPanel.repaint() + } } override fun paintComponent(g2: Graphics2D, gl: Graphics2D) { @@ -43,17 +77,76 @@ class TowerTopPanel(tower: Tower, parentPanel: TowerPanel) : boundingBox = resource.bounds direction = resource.direction + gg2 = g2.create() as Graphics2D paintImageRotated(tower.direction, gg2!!) + + // draw scanning waves if active + if (scanning) { + try { + val overlay = g2.create() as Graphics2D + overlay.composite = java.awt.AlphaComposite.getInstance(java.awt.AlphaComposite.SRC_OVER, 1.0f) + overlay.stroke = BasicStroke(3.0f) + + val cx = boundingBox.centerX + val cy = boundingBox.centerY + + for (r in scanRings) { + if (r <= 0) continue + val progress = (r / scanMaxRadius).coerceIn(0.0, 1.0) + val alpha = (1.0 - progress) * 0.7 + val color = Color(0, 220, 255, (alpha * 255).toInt().coerceIn(0, 255)) + overlay.color = color + val ir = r + overlay.draw(Arc2D.Double(cx - ir, cy - ir, ir * 2, ir * 2, 0.0, 360.0, Arc2D.OPEN)) + } + + overlay.dispose() + } catch (ex: Exception) { + // ignore painting errors + } + } + gg2!!.dispose() } override fun propertyChange(evt: PropertyChangeEvent) { -// if (evt.getPropertyName().equals(Tower.REMOVE_TOWER)) { -// if (((Tower) evt.getNewValue()).equals(resource)) { -// parentResourcePanel.removeChild(this); -// } -// } + // if (evt.getPropertyName().equals(Tower.REMOVE_TOWER)) { + // if (((Tower) evt.getNewValue()).equals(resource)) { + // parentResourcePanel.removeChild(this); + // } + // } + if (evt.propertyName == Tower.TOWER_SCAN_LEVEL) { + // start wave animation: compute max radius to level borders + val levelBounds = tower.level.levelBorderPolygon.bounds2D + val cx = tower.position.x + val cy = tower.position.y + // compute distance to all 4 corners and take max + val corners = arrayOf( + doubleArrayOf(levelBounds.minX, levelBounds.minY), + doubleArrayOf(levelBounds.minX + levelBounds.width, levelBounds.minY), + doubleArrayOf(levelBounds.minX, levelBounds.minY + levelBounds.height), + doubleArrayOf(levelBounds.minX + levelBounds.width, levelBounds.minY + levelBounds.height), + ) + var maxd = 0.0 + for (c in corners) { + val dx = c[0] - cx + val dy = c[1] - cy + val d = Math.hypot(dx, dy) + if (d > maxd) maxd = d + } + scanMaxRadius = maxd + + // prime a few rings so the wave looks continuous + scanRings.clear() + scanRings.add(0.0) + scanRings.add(-ringSpacing) + scanRings.add(-ringSpacing * 2) + scanning = true + scanTimer?.restart() + return + } + // other property changes can be handled here } override val isFocusable: Boolean