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
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.occupiesBlock
import group.ouroboros.potrogue.extensions.tile
import kotlinx.collections.immutable.persistentMapOf
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.Tile
import org.hexworks.zircon.api.data.base.BaseBlock
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
private val currentEntities: MutableList<GameEntity<EntityType>> = mutableListOf(),
) : BaseBlock<Tile>(
@ -21,6 +22,13 @@ class GameBlock(
tiles = persistentMapOf(BlockTileType.CONTENT to defaultTile)
) {
companion object {
fun createWith(entity: GameEntity<EntityType>) = GameBlock(
currentEntities = mutableListOf(entity)
)
}
val isFloor: Boolean
get() = defaultTile == FLOOR
@ -31,6 +39,13 @@ class GameBlock(
val isEmptyFloor: Boolean
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.
// 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>>
@ -60,4 +75,7 @@ class GameBlock(
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.EntityTile
import group.ouroboros.potrogue.entity.attributes.flags.BlockOccupier
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.InputReceiver
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.
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.
fun newPlayer() = newGameEntityOfType(Player) {
// 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)
facets(Movable, CameraMover)
}

View file

@ -5,5 +5,5 @@ import group.ouroboros.potrogue.blocks.GameBlock
object GameBlockFactory {
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()
//Player Tile
val PLAYER = Tile.newBuilder()
val PLAYER: CharacterTile = Tile.newBuilder()
.withCharacter('☺')
.withBackgroundColor(FLOOR_BACKGROUND)
.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

@ -4,4 +4,8 @@ import org.hexworks.amethyst.api.base.BaseEntityType
object Player : BaseEntityType(
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.
// 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

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.MoveTo
import group.ouroboros.potrogue.extensions.position
import group.ouroboros.potrogue.extensions.tryActionsOn
import group.ouroboros.potrogue.world.GameContext
import org.hexworks.amethyst.api.Consumed
import org.hexworks.amethyst.api.MessageResponse
@ -39,6 +40,7 @@ object Movable : BaseFacet<GameContext, MoveTo>(MoveTo::class) {
val previousPosition = entity.position
// Here we say that well 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
@ -56,6 +58,30 @@ object Movable : BaseFacet<GameContext, MoveTo>(MoveTo::class) {
}
// Finally we return the 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.
// 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).