Player move!
This commit is contained in:
parent
f2a68b708b
commit
ccba59e2ba
22 changed files with 575 additions and 18 deletions
|
@ -3,3 +3,5 @@
|
|||
|
||||
The plan is to be able to self-contain the entire game on a DVD/Blue-Ray once "completed." thatd be so cool and so epic.
|
||||
|
||||
Big help from [The Hexworks Tutorial](https://hexworks.org/posts/tutorials/2018/12/04/how-to-make-a-roguelike.html), which helped me understand the basics of zircon,
|
||||
much of this code will be heavily transformed once complete.
|
|
@ -1,6 +1,7 @@
|
|||
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
|
||||
|
||||
val zircon_version: String by project
|
||||
val amethyst_version: String by project
|
||||
val slf4j_version: String by project
|
||||
val junit_version: String by project
|
||||
val mockito_version: String by project
|
||||
|
@ -31,6 +32,8 @@ dependencies {
|
|||
implementation("org.hexworks.zircon:zircon.core-jvm:$zircon_version")
|
||||
implementation("org.hexworks.zircon:zircon.jvm.swing:$zircon_version")
|
||||
|
||||
implementation("org.hexworks.amethyst:amethyst.core-jvm:$amethyst_version")
|
||||
|
||||
implementation(kotlin("stdlib-jdk8"))
|
||||
|
||||
testImplementation("junit:junit:$junit_version")
|
||||
|
|
|
@ -3,6 +3,7 @@ org.gradle.parallel=true
|
|||
org.gradle.jvmargs=-Xmx2048M
|
||||
org.gradle.daemon=true
|
||||
|
||||
amethyst_version=2020.1.1-RELEASE
|
||||
zircon_version=2021.1.0-RELEASE
|
||||
junit_version=4.12
|
||||
mockito_version=1.10.19
|
||||
|
|
|
@ -17,10 +17,12 @@ object GameConfig {
|
|||
const val LOG_AREA_HEIGHT = 12
|
||||
|
||||
// sizing
|
||||
const val BORDERLESS_WINDOW_WIDTH = 120
|
||||
const val BORDERLESS_WINDOW_HEIGHT = 65
|
||||
const val WINDOW_WIDTH = 80
|
||||
const val WINDOW_HEIGHT = 50
|
||||
|
||||
val WORLD_SIZE = Size3D.create(WINDOW_WIDTH, WINDOW_HEIGHT, DUNGEON_LEVELS)
|
||||
val WORLD_SIZE = Size3D.create(WINDOW_WIDTH * 2, WINDOW_HEIGHT * 2 , DUNGEON_LEVELS)
|
||||
val GAME_AREA_SIZE = Size3D.create(
|
||||
xLength = WINDOW_WIDTH - SIDEBAR_WIDTH,
|
||||
yLength = WINDOW_HEIGHT - LOG_AREA_HEIGHT,
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
package group.ouroboros.potrogue.attributes
|
||||
|
||||
import org.hexworks.amethyst.api.base.BaseAttribute
|
||||
import org.hexworks.cobalt.databinding.api.extension.toProperty
|
||||
import org.hexworks.zircon.api.data.Position3D
|
||||
class EntityPosition(
|
||||
// We add initialPosition as a constructor parameter to our class and its default value is unknown.
|
||||
// What’s this? Position3D comes from Zircon and can be used to represent a point in 3D space (as we have discussed before),
|
||||
// and unknown impelments the Null Object Pattern for us.
|
||||
initialPosition: Position3D = Position3D.unknown()
|
||||
) : BaseAttribute() {
|
||||
|
||||
// Here we create a private Property from the initialPosition.
|
||||
// What’s a Property you might ask? Well, it is used for data binding.
|
||||
// A Property is a wrapper for a value that can change over time.
|
||||
// It can be bound to other Property objects so their values change together and you can also add change listeners to them.
|
||||
// Property comes from the Cobalt library we use and it works in a very similar way as properties work in JavaFX.
|
||||
private val positionProperty = initialPosition.toProperty()
|
||||
|
||||
// We create a Kotlin delegate from our Property.
|
||||
// This means that position will be accessible to the outside world as if it was a simple field, but it takes its value from our Property under the hood.
|
||||
var position: Position3D by positionProperty.asDelegate()
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
package group.ouroboros.potrogue.attributes
|
||||
|
||||
import org.hexworks.amethyst.api.base.BaseAttribute
|
||||
import org.hexworks.zircon.api.data.Tile
|
||||
|
||||
// EntityTile is an Attribute which holds the Tile of an Entity we use to display it in our world
|
||||
data class EntityTile(val tile: Tile = Tile.empty()) : BaseAttribute()
|
|
@ -0,0 +1,7 @@
|
|||
package group.ouroboros.potrogue.attributes.types
|
||||
|
||||
import org.hexworks.amethyst.api.base.BaseEntityType
|
||||
|
||||
object Player : BaseEntityType(
|
||||
name = "player"
|
||||
)
|
|
@ -2,20 +2,62 @@ package group.ouroboros.potrogue.blocks
|
|||
|
||||
import group.ouroboros.potrogue.builders.GameTileRepository.EMPTY
|
||||
import group.ouroboros.potrogue.builders.GameTileRepository.FLOOR
|
||||
import group.ouroboros.potrogue.builders.GameTileRepository.PLAYER
|
||||
import group.ouroboros.potrogue.builders.GameTileRepository.WALL
|
||||
import group.ouroboros.potrogue.extensions.GameEntity
|
||||
import group.ouroboros.potrogue.extensions.tile
|
||||
import kotlinx.collections.immutable.persistentMapOf
|
||||
import org.hexworks.amethyst.api.entity.EntityType
|
||||
import org.hexworks.zircon.api.data.BlockTileType
|
||||
import org.hexworks.zircon.api.data.Tile
|
||||
import org.hexworks.zircon.api.data.base.BaseBlock
|
||||
|
||||
class GameBlock (content: Tile = FLOOR) : BaseBlock<Tile>(
|
||||
emptyTile = EMPTY,
|
||||
tiles = persistentMapOf(BlockTileType.CONTENT to content)
|
||||
class GameBlock(
|
||||
private var defaultTile: Tile = FLOOR,
|
||||
// We added currentEntities which is just a mutable list of Entity objects which is empty by default
|
||||
private val currentEntities: MutableList<GameEntity<EntityType>> = mutableListOf(),
|
||||
) : BaseBlock<Tile>(
|
||||
emptyTile = Tile.empty(),
|
||||
tiles = persistentMapOf(BlockTileType.CONTENT to defaultTile)
|
||||
) {
|
||||
|
||||
val isFloor: Boolean
|
||||
get() = content == FLOOR
|
||||
get() = defaultTile == FLOOR
|
||||
|
||||
val isWall: Boolean
|
||||
get() = content == WALL
|
||||
get() = defaultTile == WALL
|
||||
|
||||
// We add a property which tells whether this block is just a floor (similar to isWall)
|
||||
val isEmptyFloor: Boolean
|
||||
get() = currentEntities.isEmpty()
|
||||
|
||||
// Exposed a getter for entities which takes a snapshot (defensive copy) of the current entities and returns them.
|
||||
// We do this because we don’t want to expose the internals of GameBlock which would make currentEntities mutable to the outside world
|
||||
val entities: Iterable<GameEntity<EntityType>>
|
||||
get() = currentEntities.toList()
|
||||
|
||||
// We expose a function for adding an Entity to our block
|
||||
fun addEntity(entity: GameEntity<EntityType>) {
|
||||
currentEntities.add(entity)
|
||||
updateContent()
|
||||
}
|
||||
|
||||
// And also for removing one
|
||||
fun removeEntity(entity: GameEntity<EntityType>) {
|
||||
currentEntities.remove(entity)
|
||||
updateContent()
|
||||
}
|
||||
|
||||
// Incorporated our entities to how we display a block by
|
||||
private fun updateContent() {
|
||||
val entityTiles = currentEntities.map { it.tile }
|
||||
content = when {
|
||||
// Checking if the player is at this block. If yes it is displayed on top
|
||||
entityTiles.contains(PLAYER) -> PLAYER
|
||||
// Otherwise the first Entity is displayed if present
|
||||
entityTiles.isNotEmpty() -> entityTiles.first()
|
||||
// Or the default tile if not
|
||||
else -> defaultTile
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
package group.ouroboros.potrogue.builders
|
||||
|
||||
import group.ouroboros.potrogue.attributes.EntityPosition
|
||||
import group.ouroboros.potrogue.attributes.EntityTile
|
||||
import group.ouroboros.potrogue.attributes.types.Player
|
||||
import group.ouroboros.potrogue.systems.CameraMover
|
||||
import group.ouroboros.potrogue.systems.InputReceiver
|
||||
import group.ouroboros.potrogue.systems.Movable
|
||||
import group.ouroboros.potrogue.world.GameContext
|
||||
import org.hexworks.amethyst.api.builder.EntityBuilder
|
||||
import org.hexworks.amethyst.api.entity.EntityType
|
||||
import org.hexworks.amethyst.api.newEntityOfType
|
||||
|
||||
// We add a function which calls Entities.newEntityOfType and pre-fills the generic type parameter for Context with GameContext.
|
||||
fun <T : EntityType> newGameEntityOfType(
|
||||
type: T,
|
||||
init: EntityBuilder<T, GameContext>.() -> Unit
|
||||
) = newEntityOfType(type, init)
|
||||
|
||||
// We define our factory as an object since we’ll only ever have a single instance of it.
|
||||
object EntityFactory {
|
||||
|
||||
// We add a function for creating a newPlayer and call newGameEntityOfType with our previously created Player type.
|
||||
fun newPlayer() = newGameEntityOfType(Player) {
|
||||
// We specify our Attributes, Behaviors and Facets. We only have Attributes so far though.
|
||||
attributes(EntityPosition(), EntityTile(GameTileRepository.PLAYER))
|
||||
behaviors(InputReceiver)
|
||||
facets(Movable, CameraMover)
|
||||
}
|
||||
}
|
|
@ -10,4 +10,7 @@ object GameColors {
|
|||
|
||||
val FLOOR_FOREGROUND = TileColor.fromString("#75715E")
|
||||
val FLOOR_BACKGROUND = TileColor.fromString("#1e2320")
|
||||
|
||||
// Player Color?
|
||||
val ACCENT_COLOR = TileColor.fromString("#FFCD22")
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
package group.ouroboros.potrogue.builders
|
||||
|
||||
import group.ouroboros.potrogue.builders.GameColors.ACCENT_COLOR
|
||||
import group.ouroboros.potrogue.builders.GameColors.FLOOR_BACKGROUND
|
||||
import group.ouroboros.potrogue.builders.GameColors.FLOOR_FOREGROUND
|
||||
import group.ouroboros.potrogue.builders.GameColors.WALL_BACKGROUND
|
||||
|
@ -28,4 +29,11 @@ object GameTileRepository {
|
|||
.withBackgroundColor(WALL_BACKGROUND)
|
||||
.buildCharacterTile()
|
||||
|
||||
//Player Tile
|
||||
val PLAYER = Tile.newBuilder()
|
||||
.withCharacter('☺')
|
||||
.withBackgroundColor(FLOOR_BACKGROUND)
|
||||
.withForegroundColor(ACCENT_COLOR)
|
||||
.buildCharacterTile()
|
||||
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
package group.ouroboros.potrogue.extensions
|
||||
|
||||
import group.ouroboros.potrogue.attributes.EntityPosition
|
||||
import group.ouroboros.potrogue.attributes.EntityTile
|
||||
import group.ouroboros.potrogue.world.GameContext
|
||||
import org.hexworks.amethyst.api.Attribute
|
||||
import org.hexworks.amethyst.api.entity.Entity
|
||||
import org.hexworks.amethyst.api.entity.EntityType
|
||||
import org.hexworks.zircon.api.data.Tile
|
||||
import kotlin.reflect.KClass
|
||||
import org.hexworks.amethyst.api.Message
|
||||
|
||||
typealias AnyGameEntity = GameEntity<EntityType>
|
||||
typealias GameEntity<T> = Entity<T, GameContext>
|
||||
typealias GameMessage = Message<GameContext>
|
||||
|
||||
// Create an extension property (works the same way as an extension function) on AnyGameEntity.
|
||||
var AnyGameEntity.position
|
||||
// Define a getter for it which tries to find the EntityPosition attribute in our Entity and throws and exception if the Entity has no position.
|
||||
get() = tryToFindAttribute(EntityPosition::class).position
|
||||
// We also define a setter for it which sets the Property we defined before
|
||||
set(value) {
|
||||
findAttribute(EntityPosition::class).map {
|
||||
it.position = value
|
||||
}
|
||||
}
|
||||
|
||||
val AnyGameEntity.tile: Tile
|
||||
get() = this.tryToFindAttribute(EntityTile::class).tile
|
||||
|
||||
// Define a function which implements the “try to find or throw an exception” logic for both of our properties.
|
||||
fun <T : Attribute> AnyGameEntity.tryToFindAttribute(klass: KClass<T>): T = findAttribute(klass).orElseThrow {
|
||||
NoSuchElementException("Entity '$this' has no property with type '${klass.simpleName}'.")
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
package group.ouroboros.potrogue.messages
|
||||
|
||||
import group.ouroboros.potrogue.extensions.GameEntity
|
||||
import group.ouroboros.potrogue.extensions.GameMessage
|
||||
import group.ouroboros.potrogue.world.GameContext
|
||||
import org.hexworks.amethyst.api.entity.EntityType
|
||||
import org.hexworks.zircon.api.data.Position3D
|
||||
|
||||
data class MoveCamera(
|
||||
override val context: GameContext,
|
||||
override val source: GameEntity<EntityType>,
|
||||
val previousPosition: Position3D
|
||||
) : GameMessage
|
13
src/main/kotlin/group/ouroboros/potrogue/messages/MoveTo.kt
Normal file
13
src/main/kotlin/group/ouroboros/potrogue/messages/MoveTo.kt
Normal file
|
@ -0,0 +1,13 @@
|
|||
package group.ouroboros.potrogue.messages
|
||||
|
||||
import group.ouroboros.potrogue.extensions.GameEntity
|
||||
import group.ouroboros.potrogue.extensions.GameMessage
|
||||
import group.ouroboros.potrogue.world.GameContext
|
||||
import org.hexworks.amethyst.api.entity.EntityType
|
||||
import org.hexworks.zircon.api.data.Position3D
|
||||
|
||||
data class MoveTo(
|
||||
override val context: GameContext,
|
||||
override val source: GameEntity<EntityType>,
|
||||
val position: Position3D
|
||||
) : GameMessage
|
|
@ -0,0 +1,40 @@
|
|||
package group.ouroboros.potrogue.systems
|
||||
|
||||
import group.ouroboros.potrogue.extensions.position
|
||||
import group.ouroboros.potrogue.messages.MoveCamera
|
||||
import group.ouroboros.potrogue.world.GameContext
|
||||
import org.hexworks.amethyst.api.Consumed
|
||||
import org.hexworks.amethyst.api.Response
|
||||
import org.hexworks.amethyst.api.base.BaseFacet
|
||||
|
||||
object CameraMover : BaseFacet<GameContext, MoveCamera>(MoveCamera::class) {
|
||||
|
||||
override suspend fun receive(message: MoveCamera): Response {
|
||||
val (context, source, previousPosition) = message
|
||||
val world = context.world
|
||||
// The player’s position on the screen can be calculated by subtracting the World’s visibleOffset from the player’s position.
|
||||
// The visibleOffset is the top left position of the visible part of the World relative to the top left corner of the whole World (which is 0, 0).
|
||||
val screenPos = source.position - world.visibleOffset
|
||||
// We calculate the center position of the visible part of the world here
|
||||
val halfHeight = world.visibleSize.yLength / 2
|
||||
val halfWidth = world.visibleSize.xLength / 2
|
||||
val currentPosition = source.position
|
||||
// And we only move the camera if we moved in a certain direction (left for example) and the Entity’s position on the screen is left of the middle position.
|
||||
// The logic is the same for all directions, but we use the corresponding x or y coordinate
|
||||
when {
|
||||
previousPosition.y > currentPosition.y && screenPos.y < halfHeight -> {
|
||||
world.scrollOneBackward()
|
||||
}
|
||||
previousPosition.y < currentPosition.y && screenPos.y > halfHeight -> {
|
||||
world.scrollOneForward()
|
||||
}
|
||||
previousPosition.x > currentPosition.x && screenPos.x < halfWidth -> {
|
||||
world.scrollOneLeft()
|
||||
}
|
||||
previousPosition.x < currentPosition.x && screenPos.x > halfWidth -> {
|
||||
world.scrollOneRight()
|
||||
}
|
||||
}
|
||||
return Consumed
|
||||
}
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
package group.ouroboros.potrogue.systems
|
||||
|
||||
import group.ouroboros.potrogue.extensions.position
|
||||
import group.ouroboros.potrogue.messages.MoveTo
|
||||
import group.ouroboros.potrogue.world.GameContext
|
||||
import org.hexworks.amethyst.api.base.BaseBehavior
|
||||
import org.hexworks.amethyst.api.entity.Entity
|
||||
import org.hexworks.amethyst.api.entity.EntityType
|
||||
import org.hexworks.zircon.api.uievent.KeyCode
|
||||
import org.hexworks.zircon.api.uievent.KeyboardEvent
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
// InputReceiver is pretty simple, it just checks for WASD, and acts accordingly
|
||||
object InputReceiver : BaseBehavior<GameContext>() {
|
||||
|
||||
override suspend fun update(entity: Entity<EntityType, GameContext>, context: GameContext): Boolean {
|
||||
// We destructure our context object so its properties are easy to access.
|
||||
// Destructuring is positional, so here _ means that we don’t care about that specific property.
|
||||
val (_, _, uiEvent, player) = context
|
||||
val currentPos = player.position
|
||||
// We only want KeyboardEvents for now so we check with the is operator.
|
||||
// This is similar as the instanceof operator in Java but a bit more useful.
|
||||
if (uiEvent is KeyboardEvent) {
|
||||
// We use when which is similar to switch in Java to check which key was pressed.
|
||||
// Zircon has a KeyCode for all keys which can be pressed. when in Kotlin is also an expression,
|
||||
// and not a statement so it returns a value. We can change it into our newPosition variable.
|
||||
val newPosition = when (uiEvent.code) {
|
||||
KeyCode.KEY_W -> currentPos.withRelativeY(-1)
|
||||
KeyCode.KEY_A -> currentPos.withRelativeX(-1)
|
||||
KeyCode.KEY_S -> currentPos.withRelativeY(1)
|
||||
KeyCode.KEY_D -> currentPos.withRelativeX(1)
|
||||
KeyCode.ESCAPE -> exitProcess(0)
|
||||
else -> {
|
||||
// If some key is pressed other than WASD, then we just return the current position, so no movement will happen
|
||||
currentPos
|
||||
}
|
||||
}
|
||||
// We receive the MoveTo message on our player here.
|
||||
player.receiveMessage(MoveTo(context, player, newPosition))
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
61
src/main/kotlin/group/ouroboros/potrogue/systems/Movable.kt
Normal file
61
src/main/kotlin/group/ouroboros/potrogue/systems/Movable.kt
Normal file
|
@ -0,0 +1,61 @@
|
|||
package group.ouroboros.potrogue.systems
|
||||
|
||||
import group.ouroboros.potrogue.messages.MoveTo
|
||||
import group.ouroboros.potrogue.world.GameContext
|
||||
import org.hexworks.amethyst.api.Consumed
|
||||
import org.hexworks.amethyst.api.Pass
|
||||
import org.hexworks.amethyst.api.Response
|
||||
import org.hexworks.amethyst.api.base.BaseFacet
|
||||
import group.ouroboros.potrogue.attributes.types.Player
|
||||
import group.ouroboros.potrogue.extensions.position
|
||||
import group.ouroboros.potrogue.messages.MoveCamera
|
||||
import org.hexworks.amethyst.api.MessageResponse
|
||||
|
||||
/*
|
||||
* Hey, what’s Pass and Consumed?
|
||||
* Why do we have to return anything? Good question! When an Entity receives a Message it tries to send the given message to its Facets in order.
|
||||
* Each Facet has to return a Response. There are 3 kinds: Pass, Consumed and MessageResponse. If we return Pass, the loop continues and the entity tries the next Facet.
|
||||
* If we return Consumed, the loop stops. MessageResponse is special, we can return a new message using it and the entity will continue the loop using the new Message!
|
||||
* This is useful for implementing complex interactions between entities
|
||||
*/
|
||||
|
||||
// A Facet accepts only a specific message, so we have to indicate that we only handle MoveTo.
|
||||
object Movable : BaseFacet<GameContext, MoveTo>(MoveTo::class) {
|
||||
|
||||
override suspend fun receive(message: MoveTo): Response {
|
||||
/*
|
||||
* This funky (context, entity, position) code is called Destructuring.
|
||||
* This might be familiar for Python folks and what it does is that it unpacks the values from an object which supports it. So writing this:
|
||||
* val (context, entity, position) = myObj
|
||||
*
|
||||
* is the equivalent of writing this:
|
||||
* val context = myObj.context
|
||||
* val entity = myObj.entity
|
||||
* val position = myObj.position
|
||||
*/
|
||||
val (context, entity, position) = message
|
||||
val world = context.world
|
||||
// we save the previous position before we change it
|
||||
val previousPosition = entity.position
|
||||
// Here we say that we’ll return Pass as a default
|
||||
var result: Response = Pass
|
||||
// Then we check whether moving the entity was successful or not (remember the success return value?)
|
||||
if (world.moveEntity(entity, position)) {
|
||||
// If the move was successful and the entity we moved is the player
|
||||
result = if (entity.type == Player) {
|
||||
MessageResponse(
|
||||
// We return the MessageResponse
|
||||
MoveCamera(
|
||||
context = context,
|
||||
source = entity,
|
||||
previousPosition = previousPosition
|
||||
)
|
||||
)
|
||||
// Otherwise we keep the Consumed response
|
||||
} else Consumed
|
||||
}
|
||||
// Finally we return the result
|
||||
return result
|
||||
}
|
||||
|
||||
}
|
|
@ -2,10 +2,10 @@ package group.ouroboros.potrogue.view
|
|||
|
||||
import group.ouroboros.potrogue.GameConfig
|
||||
import group.ouroboros.potrogue.GameConfig.LOG_AREA_HEIGHT
|
||||
import group.ouroboros.potrogue.GameConfig.SIDEBAR_WIDTH
|
||||
import group.ouroboros.potrogue.GameConfig.WINDOW_WIDTH
|
||||
import group.ouroboros.potrogue.builders.GameTileRepository
|
||||
import group.ouroboros.potrogue.world.Game
|
||||
import group.ouroboros.potrogue.world.GameBuilder
|
||||
import org.hexworks.cobalt.databinding.api.extension.toProperty
|
||||
import org.hexworks.zircon.api.ComponentDecorations.box
|
||||
import org.hexworks.zircon.api.Components
|
||||
|
@ -15,9 +15,11 @@ import org.hexworks.zircon.api.game.ProjectionMode
|
|||
import org.hexworks.zircon.api.grid.TileGrid
|
||||
import org.hexworks.zircon.api.view.base.BaseView
|
||||
import org.hexworks.zircon.internal.game.impl.GameAreaComponentRenderer
|
||||
import org.hexworks.zircon.api.uievent.KeyboardEventType
|
||||
import org.hexworks.zircon.api.uievent.Processed
|
||||
|
||||
|
||||
class PlayView (private val grid: TileGrid, private val game: Game = Game.create(), theme: ColorTheme = GameConfig.THEME) : BaseView(grid, theme) {
|
||||
class PlayView (private val grid: TileGrid, private val game: Game = GameBuilder.create(), theme: ColorTheme = GameConfig.THEME) : BaseView(grid, theme) {
|
||||
init {
|
||||
//Create Sidebar
|
||||
val sidebar = Components.panel()
|
||||
|
@ -47,5 +49,11 @@ class PlayView (private val grid: TileGrid, private val game: Game = Game.create
|
|||
|
||||
screen.addComponents(sidebar, logArea, gameComponent)
|
||||
|
||||
// modify our PlayView to update our world whenever the user presses a key
|
||||
screen.handleKeyboardEvents(KeyboardEventType.KEY_PRESSED) {event, _ ->
|
||||
game.world.update(screen, event, game)
|
||||
Processed
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -1,21 +1,29 @@
|
|||
package group.ouroboros.potrogue.world
|
||||
|
||||
import group.ouroboros.potrogue.GameConfig.GAME_AREA_SIZE
|
||||
import group.ouroboros.potrogue.GameConfig.WORLD_SIZE
|
||||
import group.ouroboros.potrogue.builders.WorldBuilder
|
||||
import org.hexworks.zircon.api.data.Size3D
|
||||
import group.ouroboros.potrogue.attributes.types.Player
|
||||
import group.ouroboros.potrogue.extensions.GameEntity
|
||||
|
||||
class Game (val world: World) {
|
||||
/*
|
||||
* The TL;DR for DIP is this: By stating what we need (the World here) but not how we get it we let the outside world decide how to provide it for us.
|
||||
* This is also called “Wishful Thinking”.
|
||||
* This kind of dependency inversion lets the users of our program inject any kind of object that corresponds to the World contract.
|
||||
* For example we can create an in-memory world, one which is stored in a database or one which is generated on the fly. Game won’t care!
|
||||
* This is in stark contrast to what we had before: an explicit instantiation of World by using the WorldBuilder.
|
||||
*/
|
||||
|
||||
class Game (
|
||||
val world: World,
|
||||
val player: GameEntity<Player>
|
||||
) {
|
||||
|
||||
companion object {
|
||||
|
||||
fun create(
|
||||
worldSize: Size3D = WORLD_SIZE,
|
||||
visibleSize: Size3D = GAME_AREA_SIZE
|
||||
player: GameEntity<Player>,
|
||||
world: World
|
||||
) = Game(
|
||||
WorldBuilder(worldSize)
|
||||
.makeCaves()
|
||||
.build(visibleSize)
|
||||
world = world,
|
||||
player = player
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,67 @@
|
|||
package group.ouroboros.potrogue.world
|
||||
|
||||
import group.ouroboros.potrogue.GameConfig
|
||||
import group.ouroboros.potrogue.GameConfig.LOG_AREA_HEIGHT
|
||||
import group.ouroboros.potrogue.GameConfig.SIDEBAR_WIDTH
|
||||
import group.ouroboros.potrogue.GameConfig.WINDOW_HEIGHT
|
||||
import group.ouroboros.potrogue.GameConfig.WINDOW_WIDTH
|
||||
import group.ouroboros.potrogue.GameConfig.WORLD_SIZE
|
||||
import group.ouroboros.potrogue.attributes.types.Player
|
||||
import group.ouroboros.potrogue.builders.EntityFactory
|
||||
import group.ouroboros.potrogue.builders.WorldBuilder
|
||||
import group.ouroboros.potrogue.extensions.GameEntity
|
||||
import org.hexworks.zircon.api.data.Position3D
|
||||
import org.hexworks.zircon.api.data.Size3D
|
||||
|
||||
// Take the size of the World as a parameter
|
||||
class GameBuilder (val worldSize: Size3D) {
|
||||
|
||||
// We define the visible size which is our viewport of the world
|
||||
private val visibleSize = Size3D.create(
|
||||
xLength = WINDOW_WIDTH - SIDEBAR_WIDTH,
|
||||
yLength = WINDOW_HEIGHT - LOG_AREA_HEIGHT,
|
||||
zLength = 1
|
||||
)
|
||||
|
||||
// We build our World here as part of the Game
|
||||
val world = WorldBuilder(worldSize)
|
||||
.makeCaves()
|
||||
.build(visibleSize = visibleSize)
|
||||
|
||||
fun buildGame(): Game {
|
||||
|
||||
prepareWorld()
|
||||
|
||||
val player = addPlayer()
|
||||
|
||||
return Game.create(
|
||||
player = player,
|
||||
world = world
|
||||
)
|
||||
}
|
||||
|
||||
// prepareWorld can be called with method chaining here, since also will return the GameBuilder object
|
||||
private fun prepareWorld() = also {
|
||||
world.scrollUpBy(world.actualSize.zLength)
|
||||
}
|
||||
|
||||
private fun addPlayer(): GameEntity<Player> {
|
||||
// We create the player entity here since we’re going to pass it as a parameter to other objects
|
||||
val player = EntityFactory.newPlayer()
|
||||
world.addAtEmptyPosition(
|
||||
// We immediately add the player to the World which takes an offset and a size as a parameter
|
||||
player,
|
||||
// offset determines the position where the search for empty positions will start. Here we specify that the top level will be searched starting at (0, 0)
|
||||
offset = Position3D.create(0, 0, GameConfig.DUNGEON_LEVELS - 1),
|
||||
size = world.visibleSize.copy(zLength = 0)
|
||||
) // And we also determine that we should search only the throughout the viewport. This ensures that the player will be visible on the screen when we start the game
|
||||
return player
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
fun create() = GameBuilder(
|
||||
worldSize = WORLD_SIZE
|
||||
).buildGame()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
package group.ouroboros.potrogue.world
|
||||
|
||||
import group.ouroboros.potrogue.extensions.GameEntity
|
||||
import org.hexworks.amethyst.api.Context
|
||||
import org.hexworks.zircon.api.screen.Screen
|
||||
import org.hexworks.zircon.api.uievent.UIEvent
|
||||
import group.ouroboros.potrogue.attributes.types.Player
|
||||
|
||||
data class GameContext(
|
||||
// The world itself
|
||||
val world: World,
|
||||
// The Screen object which we can use to open dialogs and interact with the UI in general
|
||||
val screen: Screen,
|
||||
// The UIEvent which caused the update of the world (a key press for example)
|
||||
val uiEvent: UIEvent,
|
||||
// The object representing the player. This is optional, but because we use the player in a lot of places it makes sense to add it here
|
||||
val player: GameEntity<Player>
|
||||
|
||||
) : Context
|
|
@ -1,11 +1,20 @@
|
|||
package group.ouroboros.potrogue.world
|
||||
|
||||
import group.ouroboros.potrogue.blocks.GameBlock
|
||||
import group.ouroboros.potrogue.extensions.GameEntity
|
||||
import group.ouroboros.potrogue.extensions.position
|
||||
import org.hexworks.amethyst.api.Engine
|
||||
import org.hexworks.amethyst.api.entity.Entity
|
||||
import org.hexworks.amethyst.api.entity.EntityType
|
||||
import org.hexworks.amethyst.internal.TurnBasedEngine
|
||||
import org.hexworks.cobalt.datatypes.Maybe
|
||||
import org.hexworks.zircon.api.builder.game.GameAreaBuilder
|
||||
import org.hexworks.zircon.api.data.Position3D
|
||||
import org.hexworks.zircon.api.data.Size3D
|
||||
import org.hexworks.zircon.api.data.Tile
|
||||
import org.hexworks.zircon.api.game.GameArea
|
||||
import org.hexworks.zircon.api.screen.Screen
|
||||
import org.hexworks.zircon.api.uievent.UIEvent
|
||||
|
||||
class World (
|
||||
// A World object is about holding the world data in memory, but it is not about generating it, so we take the initial state of the world as a parameter.
|
||||
|
@ -20,10 +29,124 @@ class World (
|
|||
.withActualSize(actualSize)
|
||||
.build() {
|
||||
|
||||
// We added the Engine to the world which handles our entities.
|
||||
// We could have used dependency inversion here, but this is not likely to change in the future so we’re keeping it simple.
|
||||
private val engine: TurnBasedEngine<GameContext> = Engine.create()
|
||||
|
||||
init {
|
||||
startingBlocks.forEach { (pos, block) ->
|
||||
// a World takes a Map of GameBlocks, so we need to add them to the GameArea. Where these blocks come from? We’ll see soon enough wen we implement the WorldBuilder!
|
||||
setBlockAt(pos, block)
|
||||
block.entities.forEach { entity ->
|
||||
// Also added the Entities in the starting blocks to our engine
|
||||
engine.addEntity(entity)
|
||||
// Saved their position
|
||||
entity.position = pos
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Adds the given [Entity] at the given [Position3D].
|
||||
* Has no effect if this world already contains the
|
||||
* given [Entity].
|
||||
*/
|
||||
// Added a function for adding new entities
|
||||
fun addEntity(entity: Entity<EntityType, GameContext>, position: Position3D) {
|
||||
entity.position = position
|
||||
engine.addEntity(entity)
|
||||
fetchBlockAt(position).map {
|
||||
it.addEntity(entity)
|
||||
}
|
||||
}
|
||||
|
||||
// Added a function for adding an Entity at an empty position.
|
||||
// This function needs a little explanation though.
|
||||
// What happens here is that we try to find and empty position in our World within the given bounds (offset and size).
|
||||
// Using this function we can limit the search for empty positions to a single level or multiple levels, and also within a given level.
|
||||
// This will be very useful later.
|
||||
fun addAtEmptyPosition(
|
||||
entity: GameEntity<EntityType>,
|
||||
offset: Position3D = Position3D.create(0, 0, 0),
|
||||
size: Size3D = actualSize
|
||||
): Boolean {
|
||||
return findEmptyLocationWithin(offset, size).fold(
|
||||
// If we didn’t find an empty position, then we return with false indicating that we were not successful
|
||||
whenEmpty = {
|
||||
false
|
||||
},
|
||||
// Otherwise we add the Entity at the position which was found.
|
||||
whenPresent = { location ->
|
||||
addEntity(entity, location)
|
||||
true
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds an empty location within the given area (offset and size) on this [World].
|
||||
*/
|
||||
// This function performs a random serach for an empty position.
|
||||
// To prevent seraching endlessly in a World which has none, we limit the maximum number of tries to 10.
|
||||
fun findEmptyLocationWithin(offset: Position3D, size: Size3D): Maybe<Position3D> {
|
||||
var position = Maybe.empty<Position3D>()
|
||||
val maxTries = 10
|
||||
var currentTry = 0
|
||||
while (position.isPresent.not() && currentTry < maxTries) {
|
||||
val pos = Position3D.create(
|
||||
x = (Math.random() * size.xLength).toInt() + offset.x,
|
||||
y = (Math.random() * size.yLength).toInt() + offset.y,
|
||||
z = (Math.random() * size.zLength).toInt() + offset.z
|
||||
)
|
||||
fetchBlockAt(pos).map {
|
||||
if (it.isEmptyFloor) {
|
||||
position = Maybe.of(pos)
|
||||
}
|
||||
}
|
||||
currentTry++
|
||||
}
|
||||
return position
|
||||
}
|
||||
|
||||
// Create the function update which takes all the necessary objects as parameters
|
||||
fun update(screen: Screen, uiEvent: UIEvent, game: Game) {
|
||||
// We use the context object which we created before to update the engine.
|
||||
// If you were wondering before why this class will be necessary now you know:
|
||||
// a Context object holds all the information which might be necessary to update the entity objects within our world
|
||||
engine.executeTurn(GameContext(
|
||||
world = this,
|
||||
// We pass the screen because we’ll be using it to display dialogs and similar things
|
||||
screen = screen,
|
||||
// We’ll inspect the UIEvent to determine what the user wants to do (like moving around).
|
||||
// We’re using UIEvent instead of KeyboardEvent here because it is possible that at some time we also want to use mouse events.
|
||||
uiEvent = uiEvent,
|
||||
// Adding the player entity to the context is not mandatory,
|
||||
// but since we use it almost everywhere this little optimization will make our life easier.
|
||||
player = game.player))
|
||||
}
|
||||
|
||||
// We pass the entity we want to move and the position where we want to move it.
|
||||
fun moveEntity(entity: GameEntity<EntityType>, position: Position3D): Boolean {
|
||||
// We create a success variable which holds a Boolean value representing whether the operation was successful
|
||||
var success = false
|
||||
// We fetch both blocks
|
||||
val oldBlock = fetchBlockAt(entity.position)
|
||||
val newBlock = fetchBlockAt(position)
|
||||
|
||||
// We only proceed if both blocks are present
|
||||
if (bothBlocksPresent(oldBlock, newBlock)) {
|
||||
// In that case success is true
|
||||
success = true
|
||||
oldBlock.get().removeEntity(entity)
|
||||
entity.position = position
|
||||
newBlock.get().addEntity(entity)
|
||||
}
|
||||
//Then we return success
|
||||
return success
|
||||
}
|
||||
|
||||
// This is an example of giving a name to a logical operation.
|
||||
// In this case it is very simple but sometimes logical operations become very complex and it makes sense to give them a name like this (“both blocks present?”)
|
||||
// so they are easy to reason about.
|
||||
private fun bothBlocksPresent(oldBlock: Maybe<GameBlock>, newBlock: Maybe<GameBlock>) =
|
||||
oldBlock.isPresent && newBlock.isPresent
|
||||
}
|
Loading…
Reference in a new issue