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
|
||||
|
||||
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 don’t 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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -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 we’ll 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)
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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(
|
||||
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.
|
||||
// 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
|
||||
|
|
|
@ -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 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
|
||||
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
// 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).
|
||||
|
|
Loading…
Reference in a new issue