Verified Commit c36d9987 authored by Mathias O. Myklebust's avatar Mathias O. Myklebust 🦆
Browse files

Add BattleView

parent 55366de4
Pipeline #175799 failed with stages
in 35 minutes and 33 seconds
package se.battlegoo.battlegoose.controllers
import com.badlogic.gdx.Gdx
import com.badlogic.gdx.graphics.g2d.SpriteBatch
import com.badlogic.gdx.math.Rectangle
import com.badlogic.gdx.utils.Logger
import se.battlegoo.battlegoose.Game
import se.battlegoo.battlegoose.datamodels.ActionData
......@@ -23,24 +21,36 @@ import se.battlegoo.battlegoose.models.units.DelinquentDuck
import se.battlegoo.battlegoose.models.units.GuardGoose
import se.battlegoo.battlegoose.models.units.PrivatePenguin
import se.battlegoo.battlegoose.models.units.SpitfireSeagull
import se.battlegoo.battlegoose.models.units.UnitModel
import se.battlegoo.battlegoose.network.MultiplayerService
import se.battlegoo.battlegoose.utils.Modal
import se.battlegoo.battlegoose.utils.ModalType
import se.battlegoo.battlegoose.views.BattleMapView
import se.battlegoo.battlegoose.views.BattleView
import se.battlegoo.battlegoose.views.BattleViewObserver
import se.battlegoo.battlegoose.views.FacingDirection
import se.battlegoo.battlegoose.views.UnitSprite
import se.battlegoo.battlegoose.views.UnitStatsView
import se.battlegoo.battlegoose.views.UnitView
import java.util.concurrent.TimeUnit
import kotlin.math.abs
import kotlin.random.Random
class BattleController(
val battle: Battle,
private val view: UnitView,
private val battle: Battle,
private val view: BattleView,
private val playerID: String
) : ControllerBase(view) {
) : ControllerBase(view), BattleMapObserver, BattleViewObserver {
companion object {
// Relative values in percentage (0f - 1f) to place elements atop the background
private const val BATTLE_MAP_WIDTH_RATIO = 0.8f
private const val BATTLE_MAP_HEIGHT_RATIO = 1f
private const val STATS_VIEW_WIDTH_RATIO = 0.2f
// private const val STATS_VIEW_HEIGHT_RATIO = 1f
fun getRandom(battleId: String): Random {
return Random(battleId.hashCode())
}
......@@ -48,23 +58,37 @@ class BattleController(
private val random: Random = getRandom(battle.battleId)
private val mapSize = ScreenVector(
Game.WIDTH * 0.8f,
Game.HEIGHT * 0.9f
)
private val battleMapWidth = BATTLE_MAP_WIDTH_RATIO * view.maxSize.x
private val battleMapHeight = BATTLE_MAP_HEIGHT_RATIO * view.maxSize.y
private val battleMapX =
view.position.x + view.maxSize.x * (1f - BATTLE_MAP_WIDTH_RATIO)
private val battleMapY = // Centered
view.position.y + view.maxSize.y * (1f - BATTLE_MAP_HEIGHT_RATIO) / 2
private val statsWidth = (STATS_VIEW_WIDTH_RATIO * view.maxSize.x)
private val battleMapController = BattleMapController(
battle.hero1,
battle.battleMap,
BattleMapView(
battle.battleMap.background,
ScreenVector((Game.WIDTH - mapSize.x) / 2f, (Game.HEIGHT - mapSize.y) / 2f),
mapSize
ScreenVector(battleMapX, battleMapY),
ScreenVector(battleMapWidth, battleMapHeight)
),
::onAttackUnit,
::onMoveUnit
)
private val unitStatsController: UnitStatsController = UnitStatsController(
null,
UnitStatsView(
ScreenVector(
view.position.x,
view.position.y + view.maxSize.y * 0.3f
),
statsWidth
)
)
private val maxTurnTimeSeconds = 60
private var turnStartMillis: Long? = null
private var turnElapsedMillis: Long? = null
......@@ -72,8 +96,9 @@ class BattleController(
private var battleOver = false
init {
view.position = ScreenVector(50f, 50f)
view.size = ScreenVector(200f, 200f)
view.subscribe(this)
battleMapController.subscribe(this)
unitStatsController.showView = true
placeUnits()
placeObstacles()
battle.yourTurn = random.nextBoolean() xor battle.isHost
......@@ -81,12 +106,8 @@ class BattleController(
startTurn()
}
override fun render(sb: SpriteBatch) {
battleMapController.render(sb)
view.render(sb)
}
override fun update(dt: Float) {
view.registerInput()
if (battle.yourTurn) {
turnStartMillis?.let { startTime ->
(TimeUnit.NANOSECONDS.toMillis(System.nanoTime()) - startTime).let { elapsed ->
......@@ -98,29 +119,15 @@ class BattleController(
}
}
// Example spell cast button
if (Gdx.input.justTouched()) {
val touchPoint = Game.unproject(Gdx.input.x.toFloat(), Gdx.input.y.toFloat())
if (Rectangle(view.position.x, view.position.y, view.size.x, view.size.y)
.contains(touchPoint)
) {
onCastSpell()
view.position = ScreenVector(view.position.x, view.position.y + view.size.y)
}
}
if (!battle.yourTurn) {
checkForOpposingMoves()
}
battleMapController.update(dt)
}
override fun dispose() {
battleMapController.dispose()
}
private fun startTurn() {
battleMapController.yourTurn = battle.yourTurn
view.yourTurn = battle.yourTurn
Logger(Game.LOGGER_TAG, Logger.INFO)
.info((if (battle.yourTurn) "My" else "Opponent's") + " turn")
applySpells(if (battle.yourTurn) battle.activeSpells.first else battle.activeSpells.second)
......@@ -282,7 +289,7 @@ class BattleController(
}
}
private fun onCastSpell() {
override fun onCastSpell() {
if (battle.yourTurn) {
val (spellData, activeSpell) =
when (battle.hero1.spell) {
......@@ -314,12 +321,12 @@ class BattleController(
}
}
private fun onForfeit() {
override fun onForfeit() {
doAction(ActionData.Forfeit(playerID))
resolveGame(BattleOutcome.DEFEAT)
}
private fun onPass() {
override fun onPass() {
doAction(ActionData.Pass(playerID))
subtractActionPoints(battle.hero1, battle.hero1.currentStats.actionPoints)
}
......@@ -399,4 +406,25 @@ class BattleController(
activeSpell.apply(battle)
battle.getCurrentOutcome()?.let(::resolveGame)
}
override fun render(sb: SpriteBatch) {
battleMapController.render(sb)
view.render(sb)
unitStatsController.render(sb)
}
override fun dispose() {
battleMapController.dispose()
view.dispose()
unitStatsController.dispose()
}
override fun onSelectUnit(unit: UnitModel) {
unitStatsController.unit = unit
unitStatsController.showView = true
}
override fun onDeselectUnit() {
unitStatsController.unit = null
}
}
......@@ -24,6 +24,16 @@ sealed class ActionState {
val tileController: BattleMapTileController,
val unit: UnitModel
) : ActionState()
data class SelectingEnemy(val pos: GridVector) : ActionState()
}
interface BattleMapObserver {
fun onSelectUnit(unit: UnitModel)
fun onDeselectUnit()
}
interface BattleMapObservable {
fun subscribe(observer: BattleMapObserver)
}
class BattleMapController(
......@@ -32,7 +42,18 @@ class BattleMapController(
private val view: BattleMapView,
private val onMoveUnit: (fromPosition: GridVector, toPosition: GridVector) -> Unit,
private val onAttackUnit: (attackerPosition: GridVector, targetPosition: GridVector) -> Unit
) : ControllerBase(view) {
) : ControllerBase(view), BattleMapObservable {
companion object {
// Relative values in percentage (0f - 1f) to place elements atop the background
private const val TILE_HEXES_WIDTH_RATIO = 0.92f
private const val TILE_HEXES_HEIGHT_RATIO = 0.92f
}
private val tileHexesWidth = TILE_HEXES_WIDTH_RATIO * view.size.x
private val tileHexesHeight = TILE_HEXES_HEIGHT_RATIO * view.size.y
private var observer: BattleMapObserver? = null
val mapSize by model::gridSize
var yourTurn = false
......@@ -40,9 +61,9 @@ class BattleMapController(
// Radius of a hexagon is the distance from center to vertex
private val tileHexRadius = min(
// Width of single tile is sqrt(3) radii, total +.5 tile to make space for the offset rows
view.size.x / (sqrt(3f) * (mapSize.x + 0.5f)),
tileHexesWidth / (sqrt(3f) * (mapSize.x + 0.5f)),
// Height of single tile is 2 radii, but each overlaps the previous 1/4, except the first one adding 1/2 radius
view.size.y / (2 * mapSize.y * 0.75f + 0.5f)
tileHexesHeight / (2 * mapSize.y * 0.75f + 0.5f)
)
private val tilesSize = ScreenVector(
tileHexRadius * sqrt(3f) * mapSize.x,
......@@ -142,10 +163,17 @@ class BattleMapController(
when (tileController.state) {
BattleMapTileState.NORMAL -> {
model.getUnit(gridPosition).let { unitModel ->
if (unitModel == null || unitModel.allegiance != hero) {
if (unitModel == null) {
if (actionState is ActionState.SelectingEnemy) {
actionState = ActionState.Idle
observer?.onDeselectUnit()
return
}
registerInvalidSelection()
} else {
registerValidSelection()
return
}
registerValidSelection()
if (unitModel.allegiance == hero) {
clearTileStates()
actionState = ActionState.Selecting(gridPosition, tileController, unitModel)
showMoveAndAttackOptionsForSelectedUnit()
......@@ -153,24 +181,42 @@ class BattleMapController(
val unitController = getUnitControllerAt(gridPosition)
?: throw IllegalStateException("Missing unit controller")
unitController.selected = true
} else {
actionState.also {
if (it is ActionState.Selecting) {
clearTileStates()
observer?.onDeselectUnit()
} else if (it is ActionState.SelectingEnemy) {
if (it.pos == gridPosition) {
actionState = ActionState.Idle
observer?.onDeselectUnit()
return
}
}
}
actionState = ActionState.SelectingEnemy(gridPosition)
}
observer?.onSelectUnit(unitModel)
}
}
BattleMapTileState.FOCUSED -> {
registerValidSelection()
clearTileStates()
observer?.onDeselectUnit()
actionState = ActionState.Idle
}
BattleMapTileState.MOVE_TARGET -> {
registerValidSelection()
clearTileStates()
moveSelectedUnit(gridPosition)
observer?.onDeselectUnit()
actionState = ActionState.Idle
}
BattleMapTileState.ATTACK_TARGET -> {
registerValidSelection()
clearTileStates()
attackWithSelectedUnit(gridPosition)
observer?.onDeselectUnit()
actionState = ActionState.Idle
}
}
......@@ -180,6 +226,7 @@ class BattleMapController(
actionState.let { state ->
if (state is ActionState.Selecting) {
clearTileStates()
observer?.onDeselectUnit()
actionState = ActionState.Idle
if (state.unit == model.getUnit(state.pos)) {
selectTile(state.pos, state.tileController)
......@@ -229,7 +276,7 @@ class BattleMapController(
}
val hintTiles = actionState.let { state ->
when (state) {
ActionState.Idle -> {
ActionState.Idle, is ActionState.SelectingEnemy -> {
model.filter { pos ->
model.getUnit(pos).let { it != null && it.allegiance == hero }
}
......@@ -370,4 +417,8 @@ class BattleMapController(
uc.dispose()
}
}
override fun subscribe(observer: BattleMapObserver) {
this.observer = observer
}
}
package se.battlegoo.battlegoose.gamestates
import com.badlogic.gdx.graphics.g2d.SpriteBatch
import se.battlegoo.battlegoose.Game
import se.battlegoo.battlegoose.controllers.BattleController
import se.battlegoo.battlegoose.datamodels.GridVector
import se.battlegoo.battlegoose.datamodels.ScreenVector
import se.battlegoo.battlegoose.models.Battle
import se.battlegoo.battlegoose.models.BattleMap
import se.battlegoo.battlegoose.models.BattleMapBackground
import se.battlegoo.battlegoose.models.heroes.Hero
import se.battlegoo.battlegoose.views.FacingDirection
import se.battlegoo.battlegoose.views.UnitSprite
import se.battlegoo.battlegoose.views.UnitView
import se.battlegoo.battlegoose.views.BattleView
class BattleState(
playerID: String,
......@@ -18,19 +18,28 @@ class BattleState(
otherHero: Hero,
isHost: Boolean
) : GameState() {
private val battle = Battle(
if (isHost) hostHero else otherHero,
if (isHost) otherHero else hostHero,
BattleMap(
BattleMapBackground.values().random(BattleController.getRandom(battleID)),
GridVector(10, 6)
),
battleID,
isHost
)
private val battleController: BattleController = BattleController(
Battle(
if (isHost) hostHero else otherHero,
if (isHost) otherHero else hostHero,
BattleMap(
BattleMapBackground.values().random(BattleController.getRandom(battleID)),
GridVector(10, 6)
),
battleID,
isHost
battle,
BattleView(
battle.battleMap.background,
ScreenVector(0f, 0f),
ScreenVector(Game.WIDTH, Game.HEIGHT),
battle.hero1,
battle.hero2,
stage
),
UnitView(UnitSprite.DELINQUENT_DUCK, FacingDirection.LEFT), // TODO: Change to
// BattleView
playerID
)
......
......@@ -25,8 +25,8 @@ class BattleMapView(
override fun render(sb: SpriteBatch) {
sb.draw(
backgroundTextureRegion,
0f, 0f,
Game.WIDTH, Game.HEIGHT
pos.x, pos.y,
size.x, size.y
)
}
......
package se.battlegoo.battlegoose.views
import com.badlogic.gdx.Gdx
import com.badlogic.gdx.graphics.Texture
import com.badlogic.gdx.graphics.g2d.Sprite
import com.badlogic.gdx.graphics.g2d.SpriteBatch
import com.badlogic.gdx.scenes.scene2d.Stage
import com.badlogic.gdx.scenes.scene2d.ui.Skin
import com.badlogic.gdx.scenes.scene2d.ui.Table
import se.battlegoo.battlegoose.Game
import se.battlegoo.battlegoose.datamodels.ScreenVector
import se.battlegoo.battlegoose.models.BattleMapBackground
import se.battlegoo.battlegoose.models.heroes.Hero
import se.battlegoo.battlegoose.utils.Modal
import se.battlegoo.battlegoose.utils.ModalType
interface BattleViewObserver {
fun onCastSpell()
fun onPass()
fun onForfeit()
}
class BattleView(
background: BattleMapBackground,
val position: ScreenVector,
val maxSize: ScreenVector,
val hero: Hero,
enemyHero: Hero,
val stage: Stage
) :
ViewBase() {
companion object {
// Relative values in percentage (0f - 1f) to place elements atop the background
private const val HERO_ICON_WIDTH_RATIO = 0.16f // used to determine height to
private const val HERO_ICON_MARGIN_UP_LEFT_RATIO = 0.04f // based on Height
private const val INFO_BACKGROUND_WIDTH_RATIO = 0.205f
private const val INFO_BACKGROUND_HEIGHT_RATIO = 1f
private const val STANDARD_BUTTON_WIDTH_RATIO = 0.14f
private const val STANDARD_BUTTON_MARGIN_RATIO = 0.025f
private const val PREF_MODAL_HEIGHT = Game.HEIGHT * 0.6f
private const val PREF_MODAL_WIDTH = Game.WIDTH * 0.4f
}
private val heroIconWidth = HERO_ICON_WIDTH_RATIO * maxSize.y
private val heroIconMarginUpLeft = HERO_ICON_MARGIN_UP_LEFT_RATIO * maxSize.y
private val infoBackgroundWidth = INFO_BACKGROUND_WIDTH_RATIO * maxSize.x
private val infoBackgroundHeight = INFO_BACKGROUND_HEIGHT_RATIO * maxSize.y
private val standardButtonWidth = STANDARD_BUTTON_WIDTH_RATIO * maxSize.x
private val standardButtonMargin = STANDARD_BUTTON_MARGIN_RATIO * maxSize.y
private val backgroundTexture = Texture(
when (background) {
BattleMapBackground.DUNES -> "maps/sidebar/beachTowelCropped.png"
BattleMapBackground.DIRT_ROAD -> "maps/sidebar/grassPatchCropped.png"
BattleMapBackground.ICE_RINK -> "maps/sidebar/snowPatchCropped.png"
}
)
private val backgroundSprite = Sprite(backgroundTexture)
private val heroIconView = HeroIconView(
ScreenVector(
position.x + heroIconMarginUpLeft,
position.y + maxSize.y - heroIconWidth - heroIconMarginUpLeft
),
ScreenVector(
heroIconWidth,
heroIconWidth
),
hero.heroSprite
)
private val enemyIconView = HeroIconView(
ScreenVector(
position.x + maxSize.x - heroIconWidth - heroIconMarginUpLeft,
position.y + maxSize.y - heroIconWidth - heroIconMarginUpLeft
),
ScreenVector(
heroIconWidth,
heroIconWidth
),
enemyHero.heroSprite
)
private val spellButtonView: ButtonView = ButtonView(
"spellBtn.png",
position.x + (infoBackgroundWidth - standardButtonWidth) / 2,
position.y + standardButtonMargin * 5,
standardButtonWidth.toInt(), ::onClickSpellButton
)
private val endTurnButtonView: ButtonView = ButtonView(
"endTurnBtn.png",
position.x + (infoBackgroundWidth - standardButtonWidth) / 2,
position.y + standardButtonMargin,
standardButtonWidth.toInt(), ::onClickPassButton
)
private val surrenderButtonView: ButtonView = ButtonView(
"surrenderBtn.png",
position.x + maxSize.x - standardButtonWidth -
(infoBackgroundWidth - standardButtonWidth) / 2,
position.y + standardButtonMargin,
standardButtonWidth.toInt(), ::onClickSurrenderButton
)
private val mainSkin: Skin = Skin(Gdx.files.internal(Skins.STAR_SOLDIER.filepath))
private val textTable: Table = Table(mainSkin)
private lateinit var spellModal: Modal
private lateinit var surrenderModal: Modal
private var observer: BattleViewObserver? = null
var yourTurn: Boolean = false
set(yourTurn) {
heroIconView.showMyTurn = yourTurn
enemyIconView.showMyTurn = !yourTurn
field = yourTurn
}
init {
backgroundSprite.setPosition(position.x, position.y)
backgroundSprite.setSize(infoBackgroundWidth, infoBackgroundHeight)
}
fun subscribe(observer: BattleViewObserver) {
this.observer = observer
}
private fun newSpellModal(): Modal {
return Modal(
hero.spell.title,
"${hero.spell.description}\nCooldown: ${hero.spell.cooldown}",
// TODO: Fix auto linebreak
ModalType.Question(
onYes = { observer?.onCastSpell() }
),
stage,
contentActors = listOf(textTable),
prefHeight = PREF_MODAL_HEIGHT,
prefWidth = PREF_MODAL_WIDTH
)
}
private fun newSurrenderModal(): Modal {
return Modal(
"Surrender",
"Are you sure you want to surrender?",
ModalType.Question(
onYes = { observer?.onForfeit() }
),