Wall hitbox and action system
This commit is contained in:
parent
34f9b418ff
commit
4806ac3170
13 changed files with 194 additions and 6 deletions
|
@ -1,19 +1,20 @@
|
||||||
package group.ouroboros.potrogue.blocks
|
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.FLOOR
|
||||||
import group.ouroboros.potrogue.builders.GameTileRepository.PLAYER
|
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.GameEntity
|
||||||
|
import group.ouroboros.potrogue.extensions.occupiesBlock
|
||||||
import group.ouroboros.potrogue.extensions.tile
|
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.amethyst.api.entity.EntityType
|
||||||
|
import org.hexworks.cobalt.datatypes.Maybe
|
||||||
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(
|
class GameBlock(
|
||||||
private var defaultTile: Tile = FLOOR,
|
private var defaultTile: Tile = WALL,
|
||||||
// We added currentEntities which is just a mutable list of Entity objects which is empty by default
|
// We added currentEntities which is just a mutable list of Entity objects which is empty by default
|
||||||
private val currentEntities: MutableList<GameEntity<EntityType>> = mutableListOf(),
|
private val currentEntities: MutableList<GameEntity<EntityType>> = mutableListOf(),
|
||||||
) : BaseBlock<Tile>(
|
) : BaseBlock<Tile>(
|
||||||
|
@ -21,6 +22,13 @@ class GameBlock(
|
||||||
tiles = persistentMapOf(BlockTileType.CONTENT to defaultTile)
|
tiles = persistentMapOf(BlockTileType.CONTENT to defaultTile)
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
fun createWith(entity: GameEntity<EntityType>) = GameBlock(
|
||||||
|
currentEntities = mutableListOf(entity)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
val isFloor: Boolean
|
val isFloor: Boolean
|
||||||
get() = defaultTile == FLOOR
|
get() = defaultTile == FLOOR
|
||||||
|
|
||||||
|
@ -31,6 +39,13 @@ class GameBlock(
|
||||||
val isEmptyFloor: Boolean
|
val isEmptyFloor: Boolean
|
||||||
get() = currentEntities.isEmpty()
|
get() = currentEntities.isEmpty()
|
||||||
|
|
||||||
|
// occupier will return the first entity which has the BlockOccupier flag or an empty Maybe if there is none
|
||||||
|
val occupier: Maybe<GameEntity<EntityType>>
|
||||||
|
get() = Maybe.ofNullable(currentEntities.firstOrNull { it.occupiesBlock })
|
||||||
|
|
||||||
|
val isOccupied: Boolean
|
||||||
|
get() = occupier.isPresent // Note how we tell whether a block is occupied by checking for the presence of an occupier
|
||||||
|
|
||||||
// Exposed a getter for entities which takes a snapshot (defensive copy) of the current entities and returns them.
|
// 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
|
// 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>>
|
val entities: Iterable<GameEntity<EntityType>>
|
||||||
|
@ -60,4 +75,7 @@ class GameBlock(
|
||||||
else -> defaultTile
|
else -> defaultTile
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,9 @@ package group.ouroboros.potrogue.builders
|
||||||
|
|
||||||
import group.ouroboros.potrogue.entity.attributes.EntityPosition
|
import group.ouroboros.potrogue.entity.attributes.EntityPosition
|
||||||
import group.ouroboros.potrogue.entity.attributes.EntityTile
|
import group.ouroboros.potrogue.entity.attributes.EntityTile
|
||||||
|
import group.ouroboros.potrogue.entity.attributes.flags.BlockOccupier
|
||||||
import group.ouroboros.potrogue.entity.attributes.types.Player
|
import group.ouroboros.potrogue.entity.attributes.types.Player
|
||||||
|
import group.ouroboros.potrogue.entity.attributes.types.Wall
|
||||||
import group.ouroboros.potrogue.entity.systems.CameraMover
|
import group.ouroboros.potrogue.entity.systems.CameraMover
|
||||||
import group.ouroboros.potrogue.entity.systems.InputReceiver
|
import group.ouroboros.potrogue.entity.systems.InputReceiver
|
||||||
import group.ouroboros.potrogue.entity.systems.Movable
|
import group.ouroboros.potrogue.entity.systems.Movable
|
||||||
|
@ -20,10 +22,24 @@ fun <T : EntityType> newGameEntityOfType(
|
||||||
// We define our factory as an object since we’ll only ever have a single instance of it.
|
// We define our factory as an object since we’ll only ever have a single instance of it.
|
||||||
object EntityFactory {
|
object EntityFactory {
|
||||||
|
|
||||||
|
// WALLS!
|
||||||
|
fun newWall() = newGameEntityOfType(Wall) {
|
||||||
|
attributes(
|
||||||
|
EntityPosition(),
|
||||||
|
BlockOccupier,
|
||||||
|
EntityTile(GameTileRepository.WALL)
|
||||||
|
)
|
||||||
|
//facets(Diggable)
|
||||||
|
}
|
||||||
|
|
||||||
// We add a function for creating a newPlayer and call newGameEntityOfType with our previously created Player type.
|
// We add a function for creating a newPlayer and call newGameEntityOfType with our previously created Player type.
|
||||||
fun newPlayer() = newGameEntityOfType(Player) {
|
fun newPlayer() = newGameEntityOfType(Player) {
|
||||||
// We specify our Attributes, Behaviors and Facets. We only have Attributes so far though.
|
// We specify our Attributes, Behaviors and Facets. We only have Attributes so far though.
|
||||||
attributes(EntityPosition(), EntityTile(GameTileRepository.PLAYER))
|
attributes(
|
||||||
|
EntityPosition(),
|
||||||
|
EntityTile(GameTileRepository.PLAYER),
|
||||||
|
//EntityActions(Dig::class)
|
||||||
|
)
|
||||||
behaviors(InputReceiver)
|
behaviors(InputReceiver)
|
||||||
facets(Movable, CameraMover)
|
facets(Movable, CameraMover)
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,5 +5,5 @@ import group.ouroboros.potrogue.blocks.GameBlock
|
||||||
object GameBlockFactory {
|
object GameBlockFactory {
|
||||||
fun floor() = GameBlock(GameTileRepository.FLOOR)
|
fun floor() = GameBlock(GameTileRepository.FLOOR)
|
||||||
|
|
||||||
fun wall() = GameBlock(GameTileRepository.WALL)
|
fun wall() = GameBlock.createWith(EntityFactory.newWall())
|
||||||
}
|
}
|
|
@ -30,7 +30,7 @@ object GameTileRepository {
|
||||||
.buildCharacterTile()
|
.buildCharacterTile()
|
||||||
|
|
||||||
//Player Tile
|
//Player Tile
|
||||||
val PLAYER = Tile.newBuilder()
|
val PLAYER: CharacterTile = Tile.newBuilder()
|
||||||
.withCharacter('☺')
|
.withCharacter('☺')
|
||||||
.withBackgroundColor(FLOOR_BACKGROUND)
|
.withBackgroundColor(FLOOR_BACKGROUND)
|
||||||
.withForegroundColor(ACCENT_COLOR)
|
.withForegroundColor(ACCENT_COLOR)
|
||||||
|
|
|
@ -0,0 +1,40 @@
|
||||||
|
package group.ouroboros.potrogue.entity.attributes
|
||||||
|
|
||||||
|
import group.ouroboros.potrogue.entity.messages.EntityAction
|
||||||
|
import group.ouroboros.potrogue.extensions.GameEntity
|
||||||
|
import group.ouroboros.potrogue.world.GameContext
|
||||||
|
import org.hexworks.amethyst.api.base.BaseAttribute
|
||||||
|
import org.hexworks.amethyst.api.entity.EntityType
|
||||||
|
import kotlin.reflect.KClass
|
||||||
|
|
||||||
|
class EntityActions (
|
||||||
|
// This Attribute is capable of holding classes of any kind of EntityAction.
|
||||||
|
// We use vararg here which is similar to how varargs work in Java:
|
||||||
|
// we can create the EntityActions object with any number of constructor parameters like this:
|
||||||
|
// EntityActions(Dig::class, Look::class).
|
||||||
|
// We need to use the class objects (KClass) here instead of the actual EntityAction objects because each time we perform an action
|
||||||
|
// a new EntityAction has to be created.
|
||||||
|
// So you can think about actions here as templates.
|
||||||
|
private vararg val actions: KClass<out EntityAction<out EntityType, out EntityType>>
|
||||||
|
) : BaseAttribute() {
|
||||||
|
|
||||||
|
// This function can be used to create the actual EntityAction objects by using the given context, source and target
|
||||||
|
fun createActionsFor(
|
||||||
|
context: GameContext,
|
||||||
|
source: GameEntity<EntityType>,
|
||||||
|
target: GameEntity<EntityType>
|
||||||
|
): Iterable<EntityAction<out EntityType, out EntityType>> {
|
||||||
|
return actions.map {
|
||||||
|
try {
|
||||||
|
// When we create the actions we just call the first constructor of the class and hope for the best.
|
||||||
|
// There is no built-in way in Kotlin (nor in Java) to make sure that a class has a specific constructor in compile time so that’s why
|
||||||
|
it.constructors.first().call(context, source, target)
|
||||||
|
|
||||||
|
// We catch any exceptions and rethrow them here stating that the operation failed.
|
||||||
|
// We just have to remember that whenever we create an EntityAction it has a constructor for the 3 mandatory fields.
|
||||||
|
} catch (e: Exception) {
|
||||||
|
throw IllegalArgumentException("Can't create EntityAction. Does it have the proper constructor?")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
package group.ouroboros.potrogue.entity.attributes.flags
|
||||||
|
|
||||||
|
import org.hexworks.amethyst.api.base.BaseAttribute
|
||||||
|
|
||||||
|
object BlockOccupier : BaseAttribute()
|
|
@ -5,3 +5,7 @@ import org.hexworks.amethyst.api.base.BaseEntityType
|
||||||
object Player : BaseEntityType(
|
object Player : BaseEntityType(
|
||||||
name = "player"
|
name = "player"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
object Wall : BaseEntityType(
|
||||||
|
name = "wall"
|
||||||
|
)
|
|
@ -0,0 +1,27 @@
|
||||||
|
package group.ouroboros.potrogue.entity.messages
|
||||||
|
|
||||||
|
import group.ouroboros.potrogue.extensions.GameEntity
|
||||||
|
import group.ouroboros.potrogue.extensions.GameMessage
|
||||||
|
import org.hexworks.amethyst.api.entity.EntityType
|
||||||
|
|
||||||
|
// Our EntityAction is different from a regular GameMessage in a way that it also has a target.
|
||||||
|
// So an EntityAction represents source trying to perform an action on target.
|
||||||
|
|
||||||
|
// We have two generic type parameters, S and T.
|
||||||
|
// S is the EntityType of the source, T is the EntityType of the target.
|
||||||
|
// This will be useful later on as we’ll see.
|
||||||
|
interface EntityAction <S : EntityType, T : EntityType> : GameMessage {
|
||||||
|
|
||||||
|
// We save the reference to target in all EntityActions
|
||||||
|
val target: GameEntity<T>
|
||||||
|
|
||||||
|
// The component1, component2 … componentN methods implement destructuring in Kotlin.
|
||||||
|
// Since destructuring is positional as we’ve seen previously by implementing the
|
||||||
|
// component* functions we can control how an EntityAction can be destructured.
|
||||||
|
// In our case with these 3 operator functions we can destructure any EntityActions like this:
|
||||||
|
//
|
||||||
|
//val (context, source, target) = entityAction
|
||||||
|
operator fun component1() = context
|
||||||
|
operator fun component2() = source
|
||||||
|
operator fun component3() = target
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
package group.ouroboros.potrogue.entity.systems
|
||||||
|
|
||||||
|
object Diggable /*: BaseFacet<GameContext, Dig>(Dig::class)*/ {
|
||||||
|
/*override suspend fun receive(message: Dig): Response {
|
||||||
|
val (context, _, target) = message
|
||||||
|
context.world.removeEntity(target)
|
||||||
|
return Consumed
|
||||||
|
}*/
|
||||||
|
}
|
|
@ -24,11 +24,13 @@ object InputReceiver : BaseBehavior<GameContext>() {
|
||||||
// We use when which is similar to switch in Java to check which key was pressed.
|
// 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,
|
// 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.
|
// and not a statement so it returns a value. We can change it into our newPosition variable.
|
||||||
|
|
||||||
val newPosition = when (uiEvent.code) {
|
val newPosition = when (uiEvent.code) {
|
||||||
KeyCode.KEY_W -> currentPos.withRelativeY(-1)
|
KeyCode.KEY_W -> currentPos.withRelativeY(-1)
|
||||||
KeyCode.KEY_A -> currentPos.withRelativeX(-1)
|
KeyCode.KEY_A -> currentPos.withRelativeX(-1)
|
||||||
KeyCode.KEY_S -> currentPos.withRelativeY(1)
|
KeyCode.KEY_S -> currentPos.withRelativeY(1)
|
||||||
KeyCode.KEY_D -> currentPos.withRelativeX(1)
|
KeyCode.KEY_D -> currentPos.withRelativeX(1)
|
||||||
|
|
||||||
KeyCode.ESCAPE -> exitProcess(0)
|
KeyCode.ESCAPE -> exitProcess(0)
|
||||||
else -> {
|
else -> {
|
||||||
// If some key is pressed other than WASD, then we just return the current position, so no movement will happen
|
// If some key is pressed other than WASD, then we just return the current position, so no movement will happen
|
||||||
|
|
|
@ -4,6 +4,7 @@ import group.ouroboros.potrogue.entity.attributes.types.Player
|
||||||
import group.ouroboros.potrogue.entity.messages.MoveCamera
|
import group.ouroboros.potrogue.entity.messages.MoveCamera
|
||||||
import group.ouroboros.potrogue.entity.messages.MoveTo
|
import group.ouroboros.potrogue.entity.messages.MoveTo
|
||||||
import group.ouroboros.potrogue.extensions.position
|
import group.ouroboros.potrogue.extensions.position
|
||||||
|
import group.ouroboros.potrogue.extensions.tryActionsOn
|
||||||
import group.ouroboros.potrogue.world.GameContext
|
import group.ouroboros.potrogue.world.GameContext
|
||||||
import org.hexworks.amethyst.api.Consumed
|
import org.hexworks.amethyst.api.Consumed
|
||||||
import org.hexworks.amethyst.api.MessageResponse
|
import org.hexworks.amethyst.api.MessageResponse
|
||||||
|
@ -39,6 +40,7 @@ object Movable : BaseFacet<GameContext, MoveTo>(MoveTo::class) {
|
||||||
val previousPosition = entity.position
|
val previousPosition = entity.position
|
||||||
// Here we say that we’ll return Pass as a default
|
// Here we say that we’ll return Pass as a default
|
||||||
var result: Response = Pass
|
var result: Response = Pass
|
||||||
|
/*
|
||||||
// Then we check whether moving the entity was successful or not (remember the success return value?)
|
// Then we check whether moving the entity was successful or not (remember the success return value?)
|
||||||
if (world.moveEntity(entity, position)) {
|
if (world.moveEntity(entity, position)) {
|
||||||
// If the move was successful and the entity we moved is the player
|
// If the move was successful and the entity we moved is the player
|
||||||
|
@ -56,6 +58,30 @@ object Movable : BaseFacet<GameContext, MoveTo>(MoveTo::class) {
|
||||||
}
|
}
|
||||||
// Finally we return the result
|
// Finally we return the result
|
||||||
return result
|
return result
|
||||||
}
|
}*/
|
||||||
|
|
||||||
|
// We will only do anything if there is a block at the given position.
|
||||||
|
// It is possible that there are no blocks at the edge of the map for example (if we want to move off the map)
|
||||||
|
world.fetchBlockAtOrNull(position)?.let { block ->
|
||||||
|
if (block.isOccupied) {
|
||||||
|
// If the block is occupied we try our actions on the block
|
||||||
|
result = entity.tryActionsOn(context, block.occupier.get())
|
||||||
|
} else {
|
||||||
|
//Otherwise we do what we were doing before
|
||||||
|
if (world.moveEntity(entity, position)) {
|
||||||
|
result = Consumed
|
||||||
|
if (entity.type == Player) {
|
||||||
|
result = MessageResponse(
|
||||||
|
MoveCamera(
|
||||||
|
context = context,
|
||||||
|
source = entity,
|
||||||
|
previousPosition = previousPosition
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
package group.ouroboros.potrogue.extensions
|
||||||
|
|
||||||
|
import group.ouroboros.potrogue.entity.attributes.EntityActions
|
||||||
|
import group.ouroboros.potrogue.entity.attributes.flags.BlockOccupier
|
||||||
|
import group.ouroboros.potrogue.world.GameContext
|
||||||
|
import org.hexworks.amethyst.api.Consumed
|
||||||
|
import org.hexworks.amethyst.api.Pass
|
||||||
|
import org.hexworks.amethyst.api.Response
|
||||||
|
|
||||||
|
val AnyGameEntity.occupiesBlock: Boolean
|
||||||
|
get() = findAttribute(BlockOccupier::class).isPresent
|
||||||
|
|
||||||
|
// We define this function as an extension function on AnyGameEntity.
|
||||||
|
// This means that from now on we can call tryActionsOn on any of our entities!
|
||||||
|
// It is also a suspend fun because the receiveMessage function we call later is also a suspending function.
|
||||||
|
// Suspending is part of the Kotlin Coroutines API and it is a deep topic.
|
||||||
|
// We’re not going to cover it here as we don’t take advantage of it
|
||||||
|
suspend fun AnyGameEntity.tryActionsOn(context: GameContext, target: AnyGameEntity): Response {
|
||||||
|
var result: Response = Pass
|
||||||
|
// We can only try the actions of an entity which has at least one, so we try to find the attribute.
|
||||||
|
findAttributeOrNull(EntityActions::class)?.let {
|
||||||
|
// if we find the attribute we just create the actions for our context/source/target combination
|
||||||
|
it.createActionsFor(context, this, target).forEach { action ->
|
||||||
|
// And we then send the message to the target for immediate processing and if the message is Consumed it means that
|
||||||
|
if (target.receiveMessage(action) is Consumed) {
|
||||||
|
result = Consumed
|
||||||
|
// We can break out of the forEach block.
|
||||||
|
return@forEach
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
|
@ -60,6 +60,14 @@ class World (
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun removeEntity(entity: Entity<EntityType, GameContext>) {
|
||||||
|
fetchBlockAt(entity.position).map {
|
||||||
|
it.removeEntity(entity)
|
||||||
|
}
|
||||||
|
engine.removeEntity(entity)
|
||||||
|
entity.position = Position3D.unknown()
|
||||||
|
}
|
||||||
|
|
||||||
// Added a function for adding an Entity at an empty position.
|
// Added a function for adding an Entity at an empty position.
|
||||||
// This function needs a little explanation though.
|
// 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).
|
// What happens here is that we try to find and empty position in our World within the given bounds (offset and size).
|
||||||
|
|
Loading…
Reference in a new issue