Wall hitbox and action system

This commit is contained in:
nelle 2023-10-29 20:08:00 -06:00
parent 34f9b418ff
commit 4806ac3170
13 changed files with 194 additions and 6 deletions

View file

@ -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 dont want to expose the internals of GameBlock which would make currentEntities mutable to the outside world // We do this because we dont 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
} }
} }
} }

View file

@ -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 well only ever have a single instance of it. // We define our factory as an object since well 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)
} }

View file

@ -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())
} }

View file

@ -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)

View file

@ -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 thats 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?")
}
}
}
}

View file

@ -0,0 +1,5 @@
package group.ouroboros.potrogue.entity.attributes.flags
import org.hexworks.amethyst.api.base.BaseAttribute
object BlockOccupier : BaseAttribute()

View file

@ -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"
)

View file

@ -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 well 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 weve 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
}

View file

@ -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
}*/
}

View file

@ -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

View file

@ -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 well return Pass as a default // Here we say that well 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
}
} }

View file

@ -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.
// Were not going to cover it here as we dont 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
}

View file

@ -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).