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.
|
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
|
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
|
||||||
|
|
||||||
val zircon_version: String by project
|
val zircon_version: String by project
|
||||||
|
val amethyst_version: String by project
|
||||||
val slf4j_version: String by project
|
val slf4j_version: String by project
|
||||||
val junit_version: String by project
|
val junit_version: String by project
|
||||||
val mockito_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.core-jvm:$zircon_version")
|
||||||
implementation("org.hexworks.zircon:zircon.jvm.swing:$zircon_version")
|
implementation("org.hexworks.zircon:zircon.jvm.swing:$zircon_version")
|
||||||
|
|
||||||
|
implementation("org.hexworks.amethyst:amethyst.core-jvm:$amethyst_version")
|
||||||
|
|
||||||
implementation(kotlin("stdlib-jdk8"))
|
implementation(kotlin("stdlib-jdk8"))
|
||||||
|
|
||||||
testImplementation("junit:junit:$junit_version")
|
testImplementation("junit:junit:$junit_version")
|
||||||
|
|
|
@ -3,6 +3,7 @@ org.gradle.parallel=true
|
||||||
org.gradle.jvmargs=-Xmx2048M
|
org.gradle.jvmargs=-Xmx2048M
|
||||||
org.gradle.daemon=true
|
org.gradle.daemon=true
|
||||||
|
|
||||||
|
amethyst_version=2020.1.1-RELEASE
|
||||||
zircon_version=2021.1.0-RELEASE
|
zircon_version=2021.1.0-RELEASE
|
||||||
junit_version=4.12
|
junit_version=4.12
|
||||||
mockito_version=1.10.19
|
mockito_version=1.10.19
|
||||||
|
|
|
@ -17,10 +17,12 @@ object GameConfig {
|
||||||
const val LOG_AREA_HEIGHT = 12
|
const val LOG_AREA_HEIGHT = 12
|
||||||
|
|
||||||
// sizing
|
// sizing
|
||||||
|
const val BORDERLESS_WINDOW_WIDTH = 120
|
||||||
|
const val BORDERLESS_WINDOW_HEIGHT = 65
|
||||||
const val WINDOW_WIDTH = 80
|
const val WINDOW_WIDTH = 80
|
||||||
const val WINDOW_HEIGHT = 50
|
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(
|
val GAME_AREA_SIZE = Size3D.create(
|
||||||
xLength = WINDOW_WIDTH - SIDEBAR_WIDTH,
|
xLength = WINDOW_WIDTH - SIDEBAR_WIDTH,
|
||||||
yLength = WINDOW_HEIGHT - LOG_AREA_HEIGHT,
|
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.EMPTY
|
||||||
import group.ouroboros.potrogue.builders.GameTileRepository.FLOOR
|
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.builders.GameTileRepository.WALL
|
||||||
|
import group.ouroboros.potrogue.extensions.GameEntity
|
||||||
|
import group.ouroboros.potrogue.extensions.tile
|
||||||
import kotlinx.collections.immutable.persistentMapOf
|
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.BlockTileType
|
||||||
import org.hexworks.zircon.api.data.Tile
|
import org.hexworks.zircon.api.data.Tile
|
||||||
import org.hexworks.zircon.api.data.base.BaseBlock
|
import org.hexworks.zircon.api.data.base.BaseBlock
|
||||||
|
|
||||||
class GameBlock (content: Tile = FLOOR) : BaseBlock<Tile>(
|
class GameBlock(
|
||||||
emptyTile = EMPTY,
|
private var defaultTile: Tile = FLOOR,
|
||||||
tiles = persistentMapOf(BlockTileType.CONTENT to content)
|
// 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
|
val isFloor: Boolean
|
||||||
get() = content == FLOOR
|
get() = defaultTile == FLOOR
|
||||||
|
|
||||||
val isWall: Boolean
|
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_FOREGROUND = TileColor.fromString("#75715E")
|
||||||
val FLOOR_BACKGROUND = TileColor.fromString("#1e2320")
|
val FLOOR_BACKGROUND = TileColor.fromString("#1e2320")
|
||||||
|
|
||||||
|
// Player Color?
|
||||||
|
val ACCENT_COLOR = TileColor.fromString("#FFCD22")
|
||||||
}
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
package group.ouroboros.potrogue.builders
|
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_BACKGROUND
|
||||||
import group.ouroboros.potrogue.builders.GameColors.FLOOR_FOREGROUND
|
import group.ouroboros.potrogue.builders.GameColors.FLOOR_FOREGROUND
|
||||||
import group.ouroboros.potrogue.builders.GameColors.WALL_BACKGROUND
|
import group.ouroboros.potrogue.builders.GameColors.WALL_BACKGROUND
|
||||||
|
@ -28,4 +29,11 @@ object GameTileRepository {
|
||||||
.withBackgroundColor(WALL_BACKGROUND)
|
.withBackgroundColor(WALL_BACKGROUND)
|
||||||
.buildCharacterTile()
|
.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
|
||||||
import group.ouroboros.potrogue.GameConfig.LOG_AREA_HEIGHT
|
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.GameConfig.WINDOW_WIDTH
|
||||||
import group.ouroboros.potrogue.builders.GameTileRepository
|
import group.ouroboros.potrogue.builders.GameTileRepository
|
||||||
import group.ouroboros.potrogue.world.Game
|
import group.ouroboros.potrogue.world.Game
|
||||||
|
import group.ouroboros.potrogue.world.GameBuilder
|
||||||
import org.hexworks.cobalt.databinding.api.extension.toProperty
|
import org.hexworks.cobalt.databinding.api.extension.toProperty
|
||||||
import org.hexworks.zircon.api.ComponentDecorations.box
|
import org.hexworks.zircon.api.ComponentDecorations.box
|
||||||
import org.hexworks.zircon.api.Components
|
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.grid.TileGrid
|
||||||
import org.hexworks.zircon.api.view.base.BaseView
|
import org.hexworks.zircon.api.view.base.BaseView
|
||||||
import org.hexworks.zircon.internal.game.impl.GameAreaComponentRenderer
|
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 {
|
init {
|
||||||
//Create Sidebar
|
//Create Sidebar
|
||||||
val sidebar = Components.panel()
|
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)
|
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
|
package group.ouroboros.potrogue.world
|
||||||
|
|
||||||
import group.ouroboros.potrogue.GameConfig.GAME_AREA_SIZE
|
import group.ouroboros.potrogue.attributes.types.Player
|
||||||
import group.ouroboros.potrogue.GameConfig.WORLD_SIZE
|
import group.ouroboros.potrogue.extensions.GameEntity
|
||||||
import group.ouroboros.potrogue.builders.WorldBuilder
|
|
||||||
import org.hexworks.zircon.api.data.Size3D
|
|
||||||
|
|
||||||
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 {
|
companion object {
|
||||||
|
|
||||||
fun create(
|
fun create(
|
||||||
worldSize: Size3D = WORLD_SIZE,
|
player: GameEntity<Player>,
|
||||||
visibleSize: Size3D = GAME_AREA_SIZE
|
world: World
|
||||||
) = Game(
|
) = Game(
|
||||||
WorldBuilder(worldSize)
|
world = world,
|
||||||
.makeCaves()
|
player = player
|
||||||
.build(visibleSize)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
package group.ouroboros.potrogue.world
|
||||||
|
|
||||||
import group.ouroboros.potrogue.blocks.GameBlock
|
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.builder.game.GameAreaBuilder
|
||||||
import org.hexworks.zircon.api.data.Position3D
|
import org.hexworks.zircon.api.data.Position3D
|
||||||
import org.hexworks.zircon.api.data.Size3D
|
import org.hexworks.zircon.api.data.Size3D
|
||||||
import org.hexworks.zircon.api.data.Tile
|
import org.hexworks.zircon.api.data.Tile
|
||||||
import org.hexworks.zircon.api.game.GameArea
|
import org.hexworks.zircon.api.game.GameArea
|
||||||
|
import org.hexworks.zircon.api.screen.Screen
|
||||||
|
import org.hexworks.zircon.api.uievent.UIEvent
|
||||||
|
|
||||||
class World (
|
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.
|
// 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)
|
.withActualSize(actualSize)
|
||||||
.build() {
|
.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 {
|
init {
|
||||||
startingBlocks.forEach { (pos, block) ->
|
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!
|
// 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)
|
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