This commit is contained in:
nelle 2024-02-12 18:45:21 -07:00
parent d1e55aeac7
commit 2fbb50151a
49 changed files with 1635 additions and 4 deletions

View file

@ -1,8 +1,6 @@
# PotRogue # PotRogue
### A WIP, opensource, roguelike project built in [Kotlin](https://kotlinlang.org/), utilizing [Zircon](https://hexworks.org/projects/zircon/). ### A WIP, opensource, roguelike project built in [Kotlin](https://kotlinlang.org/), utilizing [Zircon](https://hexworks.org/projects/zircon/).
### For now, please make issues on [The mirror repo](https://next.forgejo.org/Ouroboros/potrogue/issues) as ForgeFed has not yet been implemented in mainline ForgeJo, and this instance does not have an open registration
## Installation ## Installation
1. Make sure you have installed Java 20/21 1. Make sure you have installed Java 20/21
2. Clone this repo and run `./gradlew clean build` || ~~Download the latest .jar in the Releases page [Self-Hosted ForgeJo](https://git.ouroboros.group/Ouroboros/potrogue/releases) | ~~[Mirror](https://next.forgejo.org/Ouroboros/potrogue/releases)~~~~ 2. Clone this repo and run `./gradlew clean build` || ~~Download the latest .jar in the Releases page [Self-Hosted ForgeJo](https://git.ouroboros.group/Ouroboros/potrogue/releases) | ~~[Mirror](https://next.forgejo.org/Ouroboros/potrogue/releases)~~~~

BIN
bin/main/assets/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 724 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

View file

@ -0,0 +1 @@
## CURRENTLY PLACEHOLDER TO SERVE AS A REMINDER FOR AN IDEA

View file

@ -0,0 +1,86 @@
package group.ouroboros.potrogue.blocks
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,
// 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)
) {
init {
updateContent()
}
val isWall: Boolean
get() = defaultTile == WALL
val isFloor: Boolean
get() = defaultTile == FLOOR
// We add a property which tells whether this block is just a floor (similar to isWall)
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>>
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
entityTiles.contains(WALL) -> WALL
// Otherwise, the first Entity is displayed if present
entityTiles.isNotEmpty() -> entityTiles.first()
// Or the default tile if not
else -> defaultTile
}
}
companion object {
fun createWith(entity: GameEntity<EntityType>) = GameBlock(
currentEntities = mutableListOf(entity)
)
}
}

View file

@ -0,0 +1,64 @@
package group.ouroboros.potrogue.builders
import group.ouroboros.potrogue.entity.attributes.CreatureSpread
import group.ouroboros.potrogue.entity.attributes.EntityActions
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.Creature
import group.ouroboros.potrogue.entity.attributes.types.Player
import group.ouroboros.potrogue.entity.attributes.types.Wall
import group.ouroboros.potrogue.entity.messages.Attack
import group.ouroboros.potrogue.entity.messages.Dig
import group.ouroboros.potrogue.entity.systems.*
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 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),
EntityActions(Dig::class, Attack::class)
)
behaviors(InputReceiver)
facets(Movable, CameraMover)
}
// We added the creatureSpread as a parameter to newCreature and it also has a default value.
// This enables us to call it with a CreatureSpread object when The Creature grows and use the default when we create the first one in the builder
fun newCreature(creatureSpread: CreatureSpread = CreatureSpread()) = newGameEntityOfType(Creature) {
attributes(
BlockOccupier,
EntityPosition(),
EntityTile(GameTileRepository.CREATURE),
// We pass the creatureSPread parameter to our builder so it will use whatever we supplied instead of creating one by hand
creatureSpread
)
facets(Attackable)
behaviors(CreatureGrowth)
}
}

View file

@ -0,0 +1,9 @@
package group.ouroboros.potrogue.builders
import group.ouroboros.potrogue.blocks.GameBlock
object GameBlockFactory {
fun floor() = GameBlock(GameTileRepository.FLOOR)
fun wall() = GameBlock.createWith(EntityFactory.newWall())
}

View file

@ -0,0 +1,18 @@
package group.ouroboros.potrogue.builders
import org.hexworks.zircon.api.color.TileColor
object GameColors {
//We set some colors for tiles
val wallForegroundColor = TileColor.fromString("#1e1e2e")
val wallBackgroundColor = TileColor.fromString("#cba6f7")
val floorForegroundColor = TileColor.fromString("#1e1e2e")
val floorBackgroundColor = TileColor.fromString("#11111b")
// Player Color?
val accentColor = TileColor.fromString("#94e2d5")
//The Creature Color
val creatureColor = TileColor.fromString("#f9e2af")
}

View file

@ -0,0 +1,46 @@
package group.ouroboros.potrogue.builders
import group.ouroboros.potrogue.builders.GameColors.accentColor
import group.ouroboros.potrogue.builders.GameColors.floorBackgroundColor
import group.ouroboros.potrogue.builders.GameColors.floorForegroundColor
import group.ouroboros.potrogue.builders.GameColors.wallBackgroundColor
import group.ouroboros.potrogue.builders.GameColors.wallForegroundColor
import org.hexworks.zircon.api.data.CharacterTile
import org.hexworks.zircon.api.data.Tile
import org.hexworks.zircon.api.graphics.Symbols
object GameTileRepository {
// Factory for creating tile objects, we use basic CharacterTiles here,
// but Zircon can indeed use GraphicalTiles(textured) which will come later.
//Empty Tile
val EMPTY: CharacterTile = Tile.empty()
//Floor Tile
val FLOOR: CharacterTile = Tile.newBuilder()
.withCharacter(Symbols.INTERPUNCT)
.withForegroundColor(floorForegroundColor)
.withBackgroundColor(floorBackgroundColor)
.buildCharacterTile()
//Wall Tile
val WALL: CharacterTile = Tile.newBuilder()
.withCharacter('▒')
.withForegroundColor(wallForegroundColor)
.withBackgroundColor(wallBackgroundColor)
.buildCharacterTile()
//Player Tile
val PLAYER: CharacterTile = Tile.newBuilder()
.withCharacter('☺')
.withBackgroundColor(floorBackgroundColor)
.withForegroundColor(accentColor)
.buildCharacterTile()
//The Creature Tile
val CREATURE = Tile.newBuilder()
.withCharacter('☻')
.withBackgroundColor(GameColors.floorBackgroundColor)
.withForegroundColor(GameColors.creatureColor)
.buildCharacterTile()
}

View file

@ -0,0 +1,74 @@
package group.ouroboros.potrogue.builders
import group.ouroboros.potrogue.blocks.GameBlock
import group.ouroboros.potrogue.extensions.sameLevelNeighborsShuffled
import group.ouroboros.potrogue.world.World
import org.hexworks.zircon.api.data.Position3D
import org.hexworks.zircon.api.data.Size3D
// We take the worldSize from the outside world. This is useful because later it can be parameterized.
class WorldBuilder (private val worldSize: Size3D) {
private val width = worldSize.xLength
private val height = worldSize.zLength
// We maintain a Map of Blocks which we will use when we build the World
private var blocks: MutableMap<Position3D, GameBlock> = mutableMapOf()
// With makeCaves we create a fluent interface so that the users of WorldBuilder can use it in a similar manner as we build Tiles and Components in Zircon.
fun makeCaves(): WorldBuilder {
return randomizeTiles()
.smooth(8)
}
// When we build the World we take a visible size which will be used by the GameArea.
fun build(visibleSize: Size3D): World = World(blocks, visibleSize, worldSize)
private fun randomizeTiles(): WorldBuilder {
forAllPositions { pos ->
// In Kotlin if is not a statement but an expression. This means that it returns a value so we can assign it to our Map.
blocks[pos] = if (Math.random() < 0.5) {
GameBlockFactory.floor()
} else GameBlockFactory.wall()
}
return this
}
private fun smooth(iterations: Int): WorldBuilder {
// We are going to need a new Map of blocks for our smoothing because we cant do it in place. Modifying the original Map would render our cellular automata algorithm useless because it needs to calculate the new state from the old state.
val newBlocks = mutableMapOf<Position3D, GameBlock>()
repeat(iterations) {
forAllPositions { pos ->
// We create a 3D world, so we need not only x and y, but also z. What you see here is called destructuring
val (x, y, z) = pos
var floors = 0
var rocks = 0
// Here we iterate over a list of the current position and all its neighbors
pos.sameLevelNeighborsShuffled().plus(pos).forEach { neighbor ->
// And we only care about the positions which have a corresponding block (when they are not outside the game world)
blocks.whenPresent(neighbor) { block ->
if (block.isEmptyFloor) {
floors++
} else rocks++
}
}
newBlocks[Position3D.create(x, y, z)] =
if (floors >= rocks) GameBlockFactory.floor() else GameBlockFactory.wall()
}
// When were done with smoothing we replace the old Map with the new one.
blocks = newBlocks
}
return this
}
// This is just a convenience function for iterating over all of the worlds positions which I added as a demonstration of how functions with lambdas work. Here you can pass any function which takes a Position3D and returns Unit (Unit is the equivalent of Javas Void).
private fun forAllPositions(fn: (Position3D) -> Unit) {
worldSize.fetchPositions().forEach(fn)
}
// This function is an example of defining an extension function which takes a function as a parameter. What the header of the function means here is:
//
// Augment all MutableMaps which are holding Position3D to GameBlock mappings to have a function named “whenPresent” which takes a position and a function.
private fun MutableMap<Position3D, GameBlock>.whenPresent(pos: Position3D, fn: (GameBlock) -> Unit) {
this[pos]?.let(fn)
}
}

View file

@ -0,0 +1,69 @@
package group.ouroboros.potrogue.data.config
import dev.dirs.ProjectDirectories
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.io.OutputStream
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths
import java.util.*
val prop = Properties()
class Config {
val confDir = ProjectDirectories.from("xyz", "limepot", "potrogue")
val runDir = File(confDir.configDir)
val confFile = File(confDir.configDir + "/potrogue.conf")
private val prop = Properties()
private var runDirExists = runDir.exists()
init {
//Check if the directories and files exist, if not, create them. Also check if config version is incorrect.
//TODO: DataPacks and Advanced configuration system (see values.conf in jar)
//Files.createDirectories(Paths.get("./run/data"))
if(!runDirExists){
Files.createDirectories(Paths.get(confDir.configDir))
}
if(confFile.exists()) {
FileInputStream(confFile).use { prop.load(it) }
}
//Otherwise create the necessary directories
else{
Files.createFile(Path.of(confDir.configDir + "/potrogue.conf"))
FileInputStream(confFile).use {
prop.load(it)
prop.setProperty("configVersion", "1")
prop.setProperty("windowWidth", "80")
prop.setProperty("windowHeight", "54")
prop.setProperty("dungeonLevels", "2")
prop.setProperty("sidebarWidth", "18")
prop.setProperty("logAreaHeight", "12")
prop.setProperty("helpTipHeight", "3")
prop.setProperty("creaturesPerLevel", "15")
prop.setProperty("creatureMaxSpread", "20")
}
val out: OutputStream = FileOutputStream(confFile)
prop.store(out, "PotRogue Configuration File, restart game if changed value. HERE BE DRAGONS.")
}
}
//Convert values from the config file to in-code variables,
// so we can use them later, also make them public because I said so.
val windowWidth: Int = (prop.getProperty("windowWidth")).toInt()
val windowHeight: Int = (prop.getProperty("windowHeight")).toInt()
val dungeonLevels: Int = (prop.getProperty("dungeonLevels")).toInt()
val sidebarWidth: Int = (prop.getProperty("sidebarWidth")).toInt()
val logAreaHeight: Int = (prop.getProperty("logAreaHeight")).toInt()
val helpTipHeight: Int = (prop.getProperty("helpTipHeight")).toInt()
val creaturesPerLevel: Int = (prop.getProperty("creaturesPerLevel")).toInt()
val creatureMaxSpread: Int = (prop.getProperty("creatureMaxSpread")).toInt()
val configVersion: Int = (prop.getProperty("configVersion")).toInt()
}

View file

@ -0,0 +1,39 @@
package group.ouroboros.potrogue.data.config
import group.ouroboros.potrogue.GAME_ID
import group.ouroboros.potrogue.GAME_VER
import org.hexworks.zircon.api.CP437TilesetResources
import org.hexworks.zircon.api.ColorThemes.newBuilder
import org.hexworks.zircon.api.application.AppConfig
import org.hexworks.zircon.api.color.TileColor
import org.hexworks.zircon.api.data.Size3D
object GameConfig {
// look & feel
var TILESET = CP437TilesetResources.loadTilesetFromJar(16, 16, "/assets/tilesets/potrogue_grunge_16x16.png")
val WORLD_SIZE = Size3D.create(Config().windowWidth * 3, Config().windowHeight * 3 , Config().dungeonLevels)
val GAME_AREA_SIZE = Size3D.create(
xLength = Config().windowWidth - Config().sidebarWidth,
yLength = Config().windowHeight - Config().logAreaHeight,
zLength = Config().dungeonLevels
)
fun buildAppConfig() = AppConfig.newBuilder()
.withDefaultTileset(TILESET)
.withSize(Config().windowWidth, Config().windowHeight)
.withTitle("$GAME_ID | $GAME_VER")
.withIcon("assets/icon.png")
.build()
var catppuccinMocha = newBuilder()
.withAccentColor(TileColor.fromString("#b4befe"))
.withPrimaryForegroundColor(TileColor.fromString("#f5c2e7"))
.withSecondaryForegroundColor(TileColor.fromString("#cba6f7"))
.withPrimaryBackgroundColor(TileColor.fromString("#1e1e2e"))
.withSecondaryBackgroundColor(TileColor.fromString("#11111b"))
.build()
val THEME = catppuccinMocha
}

View file

@ -0,0 +1,9 @@
package group.ouroboros.potrogue.entity.attributes
import group.ouroboros.potrogue.data.config.Config
import org.hexworks.amethyst.api.base.BaseAttribute
data class CreatureSpread(
var spreadCount: Int = 0,
val maximumSpread: Int = Config().creatureMaxSpread
) : BaseAttribute()

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,26 @@
package group.ouroboros.potrogue.entity.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.
// Whats this? Position3D comes from Zircon
// and can be used to represent a point in 3D space (as we have discussed before),
// and unknown implements the Null Object Pattern for us.
initialPosition: Position3D = Position3D.unknown()
) : BaseAttribute() {
// Here we create a private Property from the initialPosition.
// Whats 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()
}

View file

@ -0,0 +1,7 @@
package group.ouroboros.potrogue.entity.attributes
import org.hexworks.amethyst.api.base.BaseAttribute
import org.hexworks.zircon.api.data.Tile
// EntityTile is an Attribute that holds the Tile of an Entity we use to display it in our world
data class EntityTile(val tile: Tile = Tile.empty()) : BaseAttribute()

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

@ -0,0 +1,15 @@
package group.ouroboros.potrogue.entity.attributes.types
import org.hexworks.amethyst.api.base.BaseEntityType
object Player : BaseEntityType(
name = "player"
)
object Wall : BaseEntityType(
name = "wall"
)
object Creature : BaseEntityType(
name = "creature"
)

View file

@ -0,0 +1,11 @@
package group.ouroboros.potrogue.entity.messages
import group.ouroboros.potrogue.extensions.GameEntity
import group.ouroboros.potrogue.world.GameContext
import org.hexworks.amethyst.api.entity.EntityType
data class Attack(
override val context: GameContext,
override val source: GameEntity<EntityType>,
override val target: GameEntity<EntityType>
) : EntityAction<EntityType, EntityType>

View file

@ -0,0 +1,11 @@
package group.ouroboros.potrogue.entity.messages
import group.ouroboros.potrogue.extensions.GameEntity
import group.ouroboros.potrogue.world.GameContext
import org.hexworks.amethyst.api.entity.EntityType
data class Dig(
override val context: GameContext,
override val source: GameEntity<EntityType>,
override val target: GameEntity<EntityType>
) : EntityAction<EntityType, EntityType>

View file

@ -0,0 +1,34 @@
package group.ouroboros.potrogue.entity.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
// Our EntityAction is different from a regular GameMessage in a way that it also has a target.
// So an EntityAction represents a 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
data class Attack(
override val context: GameContext,
override val source: GameEntity<EntityType>,
override val target: GameEntity<EntityType>
) : EntityAction<EntityType, EntityType>
}

View file

@ -0,0 +1,13 @@
package group.ouroboros.potrogue.entity.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

View file

@ -0,0 +1,13 @@
package group.ouroboros.potrogue.entity.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

View file

@ -0,0 +1,15 @@
package group.ouroboros.potrogue.entity.systems
import group.ouroboros.potrogue.entity.messages.Attack
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 Attackable : BaseFacet<GameContext, Attack>(Attack::class) {
override suspend fun receive(message: Attack): Response {
val (context, _, target) = message
context.world.removeEntity(target)
return Consumed
}
}

View file

@ -0,0 +1,44 @@
package group.ouroboros.potrogue.entity.systems
import group.ouroboros.potrogue.entity.messages.MoveCamera
import group.ouroboros.potrogue.extensions.position
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 players position on the screen can be calculated
// by subtracting the Worlds visibleOffset from the players 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 Entitys 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
}
}

View file

@ -0,0 +1,42 @@
package group.ouroboros.potrogue.entity.systems
import group.ouroboros.potrogue.builders.EntityFactory
import group.ouroboros.potrogue.entity.attributes.CreatureSpread
import group.ouroboros.potrogue.extensions.position
import group.ouroboros.potrogue.extensions.tryToFindAttribute
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.data.Size3D
// We create a Behavior and supply CreatureSpread as a mandatory Attribute to it
object CreatureGrowth : BaseBehavior<GameContext>(CreatureSpread::class) {
override suspend fun update(entity: Entity<EntityType, GameContext>, context: GameContext): Boolean {
val world = context.world
// When update is called with an entity we try to find its CreatureSpread Attribute.
// We know that it is there so we dont have to use the findAttribute method.
val creatureSpread = entity.tryToFindAttribute(CreatureSpread::class)
// Destructuring works for CreatureSpread because it is a data class
val (spreadCount, maxSpread) = creatureSpread
// You can specify any probability here.
// It will have a direct effect on how often The Creature spreads.
// Feel free to tinker with this number but dont be surprised if you find yourself in a creaturesplosion!
return if (spreadCount < maxSpread && Math.random() < 0.015) {
world.findEmptyLocationWithin(
offset = entity.position
.withRelativeX(-1)
.withRelativeY(-1),
size = Size3D.create(3, 3, 0)
).map { emptyLocation ->
// Note that we pass creatureSpread as a parameter to newCreature
// so that all Creatures in the same Creature colony can share this Attribute.
// This makes sure that Creatures wont spread all over the place and the size of a colony is controlled
world.addEntity(EntityFactory.newCreature(creatureSpread), emptyLocation)
creatureSpread.spreadCount++
}
true
} else false
}
}

View file

@ -0,0 +1,15 @@
package group.ouroboros.potrogue.entity.systems
import group.ouroboros.potrogue.entity.messages.Dig
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 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

@ -0,0 +1,43 @@
package group.ouroboros.potrogue.entity.systems
import group.ouroboros.potrogue.entity.messages.MoveTo
import group.ouroboros.potrogue.extensions.position
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
// InputReceiver 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 easier to access.
// Destructuring is positional, so here _ means that we dont 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)
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
}
}

View file

@ -0,0 +1,87 @@
package group.ouroboros.potrogue.entity.systems
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
import org.hexworks.amethyst.api.Pass
import org.hexworks.amethyst.api.Response
import org.hexworks.amethyst.api.base.BaseFacet
/*
* Hey, whats 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 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
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
}*/
// 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,34 @@
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
// 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 suspending 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
}
val AnyGameEntity.occupiesBlock: Boolean
get() = findAttribute(BlockOccupier::class).isPresent

View file

@ -0,0 +1,20 @@
package group.ouroboros.potrogue.extensions
import org.hexworks.zircon.api.data.Position3D
// We add the extension function to Position3D.
// We do it by defining a function not with a simple name, but by the format:
// fun <target class>.<function name>: return type { // ....
fun Position3D.sameLevelNeighborsShuffled(): List<Position3D> {
return (-1..1).flatMap { x ->
// We use functional programming here.
// flatMap and map work in a similar way as you might've been used to it in Java 8s Stream API.
(-1..1).map { y ->
// When you write extension functions, this will be bound to the class being extended.
// So this here will point to the Position3D instance on which sameLevelNeighborsShuffled is called.
this.withRelativeX(x).withRelativeY(y)
}
// minus here will remove this position from the List and return a new List.
// shuffled will also return a new list which contains the same elements but shuffled.
}.minus(this).shuffled()
}

View file

@ -0,0 +1,35 @@
package group.ouroboros.potrogue.extensions
import group.ouroboros.potrogue.entity.attributes.EntityPosition
import group.ouroboros.potrogue.entity.attributes.EntityTile
import group.ouroboros.potrogue.world.GameContext
import org.hexworks.amethyst.api.Attribute
import org.hexworks.amethyst.api.Message
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
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}'.")
}

View file

@ -0,0 +1,22 @@
package group.ouroboros.potrogue
import group.ouroboros.potrogue.data.config.Config
import group.ouroboros.potrogue.data.config.GameConfig
import group.ouroboros.potrogue.view.StartView
import org.hexworks.zircon.api.SwingApplications
import org.hexworks.zircon.api.VirtualApplications
// Important Values
const val GAME_ID = "PotRogue"
const val GAME_VER = "0.1.0-DEV"
const val confVers = 1
fun main() {
Config()
if (Config().configVersion != confVers){
Config().confFile.delete()
}
// Start Application
val grid = SwingApplications.startTileGrid(GameConfig.buildAppConfig())
StartView(grid).dock()
}

View file

@ -0,0 +1,14 @@
package group.ouroboros.potrogue.util
import java.net.URL
import java.nio.file.Files
import java.nio.file.Paths
class ResourceGetter {
fun downloadFile(url: URL, fileName: String) {
url.openStream().use { Files.copy(it, Paths.get(fileName)) }
}
//EXAMPLE USAGE
// ResourceGetter().downloadFile(URL("https://url.to/resource.txt"), "location/to/store/resource.txt")
}

View file

@ -0,0 +1,56 @@
package group.ouroboros.potrogue.view
import group.ouroboros.potrogue.data.config.GameConfig
import org.hexworks.zircon.api.CP437TilesetResources
import org.hexworks.zircon.api.ComponentDecorations
import org.hexworks.zircon.api.Components
import org.hexworks.zircon.api.component.ColorTheme
import org.hexworks.zircon.api.component.ComponentAlignment
import org.hexworks.zircon.api.grid.TileGrid
import org.hexworks.zircon.api.view.base.BaseView
class ConfigView (private val grid: TileGrid, theme: ColorTheme = GameConfig.THEME) : BaseView(grid, theme) {
init {
val msg = "Pre-Game Configuration"
// a text box can hold headers, paragraphs and list items
// `contentWidth = ` here is a so-called keyword parameter
// using them you can pass parameters not by their order
// but by their name.
// this might be familiar for Python programmers
val header = Components.textBox(contentWidth = msg.length)
// we add a header
.addHeader(msg)
// and a new line
.addNewLine()
// and align it to center
.withAlignmentWithin(screen, ComponentAlignment.TOP_CENTER)
.build() // finally, we build the component
//TODO: Options: world size, character tile (smiley, @, &), character customizations (class, looks, stats, start),
val tilesetButton = Components.button()
.withAlignmentWithin(screen, ComponentAlignment.CENTER)
.withText("CHANGE TILESET")
.withDecorations(ComponentDecorations.box(), ComponentDecorations.shadow())
.build()
val backButton = Components.button()
.withAlignmentWithin(screen, ComponentAlignment.BOTTOM_CENTER)
.withText("BACK")
.withDecorations(ComponentDecorations.box(), ComponentDecorations.shadow())
.build()
tilesetButton.onActivated {
GameConfig.TILESET = CP437TilesetResources.anikki16x16()
}
//Once the back button is activated, go back to startView
backButton.onActivated {
replaceWith(StartView(grid))
}
// We can add multiple components at once
//Bake The Cake
screen.addComponents(header,backButton,tilesetButton)
}}

View file

@ -0,0 +1,49 @@
package group.ouroboros.potrogue.view
import group.ouroboros.potrogue.data.config.GameConfig
import org.hexworks.zircon.api.ComponentDecorations.box
import org.hexworks.zircon.api.Components
import org.hexworks.zircon.api.component.ColorTheme
import org.hexworks.zircon.api.component.ComponentAlignment
import org.hexworks.zircon.api.grid.TileGrid
import org.hexworks.zircon.api.view.base.BaseView
import kotlin.system.exitProcess
class LoseView (private val grid: TileGrid, theme: ColorTheme = GameConfig.THEME) : BaseView(grid, theme) {
init {
//Title
val header = Components.header()
.withText("Game Over")
.withAlignmentWithin(screen, ComponentAlignment.CENTER)
.build()
//Reset Button
val restartButton = Components.button()
.withAlignmentAround(header, ComponentAlignment.BOTTOM_LEFT)
.withText("Restart")
.withDecorations(box())
.build()
//Quit Button
val exitButton = Components.button()
.withAlignmentAround(header, ComponentAlignment.BOTTOM_RIGHT)
.withText("Quit")
.withDecorations(box())
.build()
//On Reset Button activated, move back to PlayView
restartButton.onActivated {
replaceWith(PlayView(grid))
}
//On Quit BButton activated, exit program
exitButton.onActivated {
exitProcess(0)
}
//Bake the cake
screen.addComponents(header, restartButton, exitButton)
}
}

View file

@ -0,0 +1,54 @@
package group.ouroboros.potrogue.view
import group.ouroboros.potrogue.data.config.GameConfig
import org.hexworks.zircon.api.ComponentDecorations
import org.hexworks.zircon.api.Components
import org.hexworks.zircon.api.component.ColorTheme
import org.hexworks.zircon.api.component.ComponentAlignment
import org.hexworks.zircon.api.grid.TileGrid
import org.hexworks.zircon.api.view.base.BaseView
class PauseView(private val grid: TileGrid, theme: ColorTheme = GameConfig.THEME) : BaseView(grid, theme) {
init {
val msg = "Pre-Game Configuration"
// a text box can hold headers, paragraphs and list items
// `contentWidth = ` here is a so-called keyword parameter
// using them you can pass parameters not by their order
// but by their name.
// this might be familiar for Python programmers
val header = Components.textBox(contentWidth = msg.length)
// we add a header
.addHeader(msg)
// and a new line
.addNewLine()
// and align it to center
.withAlignmentWithin(screen, ComponentAlignment.TOP_CENTER)
.build() // finally, we build the component
val backButton = Components.button()
.withAlignmentWithin(screen, ComponentAlignment.BOTTOM_CENTER)
.withText("RESUME")
.withDecorations(ComponentDecorations.box(), ComponentDecorations.shadow())
.build()
val resumeButton = Components.button()
.withAlignmentWithin(screen, ComponentAlignment.BOTTOM_CENTER)
.withText("RESUME")
.withDecorations(ComponentDecorations.box(), ComponentDecorations.shadow())
.build()
//Once the back button is activated, go back to startView
backButton.onActivated {
replaceWith(StartView(grid))
}
resumeButton.onActivated {
replaceWith(PlayView(grid))
}
// We can add multiple components at once
//Bake The Cake
screen.addComponents(header,backButton)
}
}

View file

@ -0,0 +1,77 @@
package group.ouroboros.potrogue.view
import group.ouroboros.potrogue.builders.GameTileRepository
import group.ouroboros.potrogue.data.config.Config
import group.ouroboros.potrogue.data.config.GameConfig
import group.ouroboros.potrogue.world.Game
import group.ouroboros.potrogue.world.GameBuilder
import org.hexworks.cobalt.databinding.api.extension.toProperty
import org.hexworks.zircon.api.ComponentDecorations.box
import org.hexworks.zircon.api.Components
import org.hexworks.zircon.api.component.ColorTheme
import org.hexworks.zircon.api.component.ComponentAlignment
import org.hexworks.zircon.api.game.ProjectionMode
import org.hexworks.zircon.api.grid.TileGrid
import org.hexworks.zircon.api.uievent.*
import org.hexworks.zircon.api.view.base.BaseView
import org.hexworks.zircon.internal.game.impl.GameAreaComponentRenderer
class PlayView (private val grid: TileGrid, private val game: Game = GameBuilder.create(), theme: ColorTheme = GameConfig.THEME) : BaseView(grid, theme) {
init {
//Create Sidebar
val sidebar = Components.panel()
.withPreferredSize(Config().sidebarWidth, Config().windowHeight - Config().logAreaHeight)
.withDecorations(box())
.build()
//Create area for logging
val logArea = Components.logArea()
.withDecorations(box(title = "Log"))
.withPreferredSize(Config().windowWidth, Config().logAreaHeight)
.withAlignmentWithin(screen, ComponentAlignment.BOTTOM_RIGHT)
.build()
//Create help tooltip
val helpTip = Components.panel()
.withPreferredSize(Config().windowWidth - Config().sidebarWidth, Config().helpTipHeight)
.withPosition(Config().sidebarWidth, 42 - Config().helpTipHeight)
.withDecorations(box(title = "Help"))
.build()
//Create Game view
val gameComponent = Components.panel()
.withPreferredSize(game.world.visibleSize.to2DSize())
.withComponentRenderer(
GameAreaComponentRenderer(
gameArea = game.world,
projectionMode = ProjectionMode.TOP_DOWN.toProperty(),
fillerTile = GameTileRepository.FLOOR
)
)
.withAlignmentWithin(screen, ComponentAlignment.TOP_RIGHT)
.build()
screen.addComponents(sidebar, logArea, helpTip, 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
}
grid.handleKeyboardEvents(KeyboardEventType.KEY_PRESSED) label@{ event: KeyboardEvent, phase: UIEventPhase? ->
// we filter for KeyCode.ESCAPE only
if (event.code == KeyCode.ESCAPE) {
// only prints it when we press Arrow Up
replaceWith(PauseView(grid))
return@label UIEventResponse.processed()
} else {
// otherwise we just pass on it
return@label UIEventResponse.pass() // we didn't handle it so we pass on the event
}
}
}
}

View file

@ -0,0 +1,73 @@
package group.ouroboros.potrogue.view
import group.ouroboros.potrogue.GAME_ID
import group.ouroboros.potrogue.data.config.GameConfig
import org.hexworks.zircon.api.ComponentDecorations.box
import org.hexworks.zircon.api.ComponentDecorations.shadow
import org.hexworks.zircon.api.Components
import org.hexworks.zircon.api.component.ColorTheme
import org.hexworks.zircon.api.component.ComponentAlignment
import org.hexworks.zircon.api.grid.TileGrid
import org.hexworks.zircon.api.view.base.BaseView
import kotlin.system.exitProcess
class StartView (private val grid: TileGrid, theme: ColorTheme = GameConfig.THEME) : BaseView(grid, theme) {
init {
val msg = "Welcome to $GAME_ID."
// a text box can hold headers, paragraphs and list items
// `contentWidth = ` here is a so-called keyword parameter
// using them you can pass parameters not by their order
// but by their name.
// this might be familiar for Python programmers
val header = Components.textBox(contentWidth = msg.length)
// we add a header
.addHeader(msg)
// and a new line
.addNewLine()
// and align it to center
.withAlignmentWithin(screen, ComponentAlignment.CENTER)
.build() // finally, we build the component
val startButton = Components.button()
// we align the button to the bottom center of our header
.withAlignmentAround(header, ComponentAlignment.BOTTOM_CENTER)
// its text is "Start!"
.withText("QUICK PLAY!")
// we want a box and some shadow around it
.withDecorations(box(), shadow())
.build()
val configButton = Components.button()
.withAlignmentAround(startButton, ComponentAlignment.BOTTOM_CENTER)
.withText("PLAY")
.withDecorations(box(), shadow())
.build()
val exitButton = Components.button()
.withAlignmentAround(configButton, ComponentAlignment.BOTTOM_CENTER)
.withText("EXIT")
.withDecorations(box(), shadow())
.build()
//TODO: move this on to a configuration screen for world/player customization before PlayView,
// for now basic gameplay is in order though.
//Once the start button is pressed, move on to the PlayView
startButton.onActivated {
replaceWith(PlayView(grid))
}
configButton.onActivated {
replaceWith(ConfigView(grid))
}
exitButton.onActivated {
exitProcess(0)
}
// We can add multiple components at once
//Bake The Cake
screen.addComponents(header, startButton, configButton, exitButton)
}
}

View file

@ -0,0 +1,49 @@
package group.ouroboros.potrogue.view
import group.ouroboros.potrogue.data.config.GameConfig
import org.hexworks.zircon.api.ComponentDecorations.box
import org.hexworks.zircon.api.Components
import org.hexworks.zircon.api.component.ColorTheme
import org.hexworks.zircon.api.component.ComponentAlignment
import org.hexworks.zircon.api.grid.TileGrid
import org.hexworks.zircon.api.view.base.BaseView
import kotlin.system.exitProcess
// For if winning… just a test.
class WinView(private val grid: TileGrid, theme: ColorTheme = GameConfig.THEME) : BaseView(grid, theme) {
init {
// Title
val header = Components.header()
.withText("You won!")
.withAlignmentWithin(screen, ComponentAlignment.CENTER)
.build()
// Create Reset Button
val restartButton = Components.button()
.withAlignmentAround(header, ComponentAlignment.BOTTOM_LEFT)
.withText("Restart")
.withDecorations(box())
.build()
// Create Quit Button
val exitButton = Components.button()
.withAlignmentAround(header, ComponentAlignment.BOTTOM_RIGHT)
.withText("Quit")
.withDecorations(box())
.build()
// On Reset Button activated, move back to PlayView
restartButton.onActivated {
replaceWith(PlayView(grid))
}
// On Quit Button activated, exit program
exitButton.onActivated {
exitProcess(0)
}
// Bake The Cake
screen.addComponents(header, restartButton, exitButton)
}
}

View file

@ -0,0 +1,28 @@
package group.ouroboros.potrogue.world
import group.ouroboros.potrogue.entity.attributes.types.Player
import group.ouroboros.potrogue.extensions.GameEntity
/*
* 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 wont care!
* This is in stark contrast to what we had before: an explicit instantiation of the World by using the WorldBuilder.
*/
class Game (
val world: World,
val player: GameEntity<Player>
) {
companion object {
fun create(
player: GameEntity<Player>,
world: World
) = Game(
world = world,
player = player
)
}
}

View file

@ -0,0 +1,83 @@
package group.ouroboros.potrogue.world
import group.ouroboros.potrogue.builders.EntityFactory
import group.ouroboros.potrogue.builders.WorldBuilder
import group.ouroboros.potrogue.data.config.Config
import group.ouroboros.potrogue.data.config.GameConfig.WORLD_SIZE
import group.ouroboros.potrogue.entity.attributes.types.Player
import group.ouroboros.potrogue.extensions.GameEntity
import org.hexworks.amethyst.api.entity.EntityType
import org.hexworks.zircon.api.data.Position3D
import org.hexworks.zircon.api.data.Size
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 = Config().windowWidth - Config().sidebarWidth,
yLength = Config().windowHeight - Config().logAreaHeight - Config().helpTipHeight,
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()
addCreature()
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)
}
// Add this extension method to any GameEntity and we use the T generic type parameter to preserve the type in the return value to out function
private fun <T : EntityType> GameEntity<T>.addToWorld(
// atLevel will be used to supply the level at which we want to add the Entity
atLevel: Int,
// atArea specifies the size of the area at which we want to add the Entity this defaults to the actual size of the world (the whole level).
// this function returns the GameEntity which we called this function on which allows us to perform Method Chaining
atArea: Size = world.actualSize.to2DSize()): GameEntity<T> {
world.addAtEmptyPosition(this,
// We call addAtEmptyPosition with the supplied level
offset = Position3D.defaultPosition().withZ(atLevel),
// and we set the size using the supplied Size
size = Size3D.from2DSize(atArea))
return this
}
// Create Player using addToWorld Function
private fun addPlayer(): GameEntity<Player> {
return EntityFactory.newPlayer().addToWorld(
atLevel = Config().dungeonLevels - 1,
atArea = world.visibleSize.to2DSize())
}
private fun addCreature() = also {
repeat(world.actualSize.zLength) { level ->
repeat(Config().creaturesPerLevel) {
EntityFactory.newCreature().addToWorld(level)
}
}
}
companion object {
fun create() = GameBuilder(
worldSize = WORLD_SIZE
).buildGame()
}
}

View file

@ -0,0 +1,19 @@
package group.ouroboros.potrogue.world
import group.ouroboros.potrogue.entity.attributes.types.Player
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
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

View file

@ -0,0 +1,162 @@
package group.ouroboros.potrogue.world
import group.ouroboros.potrogue.blocks.GameBlock
import group.ouroboros.potrogue.extensions.GameEntity
import group.ouroboros.potrogue.extensions.position
import org.hexworks.amethyst.api.entity.Entity
import org.hexworks.amethyst.api.entity.EntityType
import org.hexworks.amethyst.internal.TurnBasedEngine
import org.hexworks.amethyst.platform.Dispatchers
import org.hexworks.cobalt.datatypes.Maybe
import org.hexworks.zircon.api.builder.game.GameAreaBuilder
import org.hexworks.zircon.api.data.Position3D
import org.hexworks.zircon.api.data.Size3D
import org.hexworks.zircon.api.data.Tile
import org.hexworks.zircon.api.game.GameArea
import org.hexworks.zircon.api.screen.Screen
import org.hexworks.zircon.api.uievent.UIEvent
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.
startingBlocks: Map<Position3D, GameBlock>,
visibleSize: Size3D,
actualSize: Size3D
// We implement the GameArea which well use with the GameComponent
) : GameArea<Tile, GameBlock> by GameAreaBuilder.newBuilder<Tile, GameBlock>()
// We set its visibleSize. This is the size of the area which will be visible on our screen
.withVisibleSize(visibleSize)
// We set the actualSize. This is the size of the whole world which can be multiple times bigger than the visible part.
// GameArea supports scrolling so well be able to scroll through our caves soon
.withActualSize(actualSize)
.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 were keeping it simple.
private val engine: TurnBasedEngine<GameContext> = TurnBasedEngine(Dispatchers.Single)
init {
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? Well see soon enough wen we implement the WorldBuilder!
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)
}
}
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).
// 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 didnt 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 well be using it to display dialogs and similar things
screen = screen,
// Well inspect the UIEvent to determine what the user wants to do (like moving around).
// Were 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
}

22
bin/main/logback.xml Normal file
View file

@ -0,0 +1,22 @@
<configuration>
<!-- We only print to the console (stdout) by default using the following format -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!-- Logging is set to info by default for our console logger -->
<root level="info">
<appender-ref ref="STDOUT"/>
</root>
<!-- You can either set logging level for whole packages -->
<!--<logger name="org.hexworks.zircon" level="warn"/>
<logger name="org.hexworks.cobalt" level="warn"/>-->
<!-- Or individual classes -->
<!--<logger name="org.hexworks.zircon.api.component.Button" level="debug"/>-->
</configuration>

View file

@ -7,7 +7,6 @@ val junit_version: String by project
val mockito_version: String by project val mockito_version: String by project
val assertj_version: String by project val assertj_version: String by project
val game_name: String by project val game_name: String by project
val version: String by project
plugins { plugins {
kotlin("jvm") version "1.9.20" kotlin("jvm") version "1.9.20"
@ -46,6 +45,8 @@ dependencies {
implementation("dev.dirs:directories:26") implementation("dev.dirs:directories:26")
} }
tasks { tasks {
named<ShadowJar>("shadowJar") { named<ShadowJar>("shadowJar") {
mergeServiceFiles() mergeServiceFiles()
@ -67,4 +68,3 @@ val jar by tasks.getting(Jar::class) {
} }
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.3 KiB

After

Width:  |  Height:  |  Size: 9.5 KiB