wha?
|
@ -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
After Width: | Height: | Size: 724 B |
BIN
bin/main/assets/tilesets/potrogue_grunge_16x16.png
Normal file
After Width: | Height: | Size: 2.5 KiB |
BIN
bin/main/assets/tilesets/rogue_yun_16x16.png
Normal file
After Width: | Height: | Size: 9.5 KiB |
1
bin/main/data/values.conf
Normal file
|
@ -0,0 +1 @@
|
||||||
|
## CURRENTLY PLACEHOLDER TO SERVE AS A REMINDER FOR AN IDEA
|
86
bin/main/group/ouroboros/potrogue/blocks/GameBlock.kt
Normal 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 don’t 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)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
64
bin/main/group/ouroboros/potrogue/builders/EntityFactory.kt
Normal 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 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),
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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())
|
||||||
|
}
|
18
bin/main/group/ouroboros/potrogue/builders/GameColors.kt
Normal 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")
|
||||||
|
}
|
|
@ -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()
|
||||||
|
}
|
74
bin/main/group/ouroboros/potrogue/builders/WorldBuilder.kt
Normal 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 can’t 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 we’re 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 world’s 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 Java’s 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)
|
||||||
|
}
|
||||||
|
}
|
69
bin/main/group/ouroboros/potrogue/data/config/Config.kt
Normal 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()
|
||||||
|
}
|
39
bin/main/group/ouroboros/potrogue/data/config/GameConfig.kt
Normal 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
|
||||||
|
}
|
|
@ -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()
|
|
@ -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,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.
|
||||||
|
// What’s 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.
|
||||||
|
// What’s 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()
|
||||||
|
}
|
|
@ -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()
|
|
@ -0,0 +1,5 @@
|
||||||
|
package group.ouroboros.potrogue.entity.attributes.flags
|
||||||
|
|
||||||
|
import org.hexworks.amethyst.api.base.BaseAttribute
|
||||||
|
|
||||||
|
object BlockOccupier : BaseAttribute()
|
|
@ -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"
|
||||||
|
)
|
11
bin/main/group/ouroboros/potrogue/entity/messages/Attack.kt
Normal 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>
|
11
bin/main/group/ouroboros/potrogue/entity/messages/Dig.kt
Normal 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>
|
|
@ -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 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
|
||||||
|
|
||||||
|
data class Attack(
|
||||||
|
override val context: GameContext,
|
||||||
|
override val source: GameEntity<EntityType>,
|
||||||
|
override val target: GameEntity<EntityType>
|
||||||
|
) : EntityAction<EntityType, EntityType>
|
||||||
|
}
|
|
@ -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
|
13
bin/main/group/ouroboros/potrogue/entity/messages/MoveTo.kt
Normal 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
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 player’s position on the screen can be calculated
|
||||||
|
// by subtracting the World’s visibleOffset from the player’s 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 Entity’s 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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 don’t 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 don’t 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 won’t spread all over the place and the size of a colony is controlled
|
||||||
|
world.addEntity(EntityFactory.newCreature(creatureSpread), emptyLocation)
|
||||||
|
creatureSpread.spreadCount++
|
||||||
|
}
|
||||||
|
true
|
||||||
|
} else false
|
||||||
|
}
|
||||||
|
}
|
15
bin/main/group/ouroboros/potrogue/entity/systems/Diggable.kt
Normal 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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 don’t 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
|
||||||
|
}
|
||||||
|
}
|
87
bin/main/group/ouroboros/potrogue/entity/systems/Movable.kt
Normal 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, what’s 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 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
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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.
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
val AnyGameEntity.occupiesBlock: Boolean
|
||||||
|
get() = findAttribute(BlockOccupier::class).isPresent
|
|
@ -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 8’s 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()
|
||||||
|
}
|
35
bin/main/group/ouroboros/potrogue/extensions/TypeAliases.kt
Normal 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}'.")
|
||||||
|
}
|
22
bin/main/group/ouroboros/potrogue/main.kt
Normal 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()
|
||||||
|
}
|
14
bin/main/group/ouroboros/potrogue/util/ResourceGetter.kt
Normal 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")
|
||||||
|
}
|
56
bin/main/group/ouroboros/potrogue/view/ConfigView.kt
Normal 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)
|
||||||
|
}}
|
49
bin/main/group/ouroboros/potrogue/view/LoseView.kt
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
54
bin/main/group/ouroboros/potrogue/view/PauseView.kt
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
77
bin/main/group/ouroboros/potrogue/view/PlayView.kt
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
73
bin/main/group/ouroboros/potrogue/view/StartView.kt
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
49
bin/main/group/ouroboros/potrogue/view/WinView.kt
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
28
bin/main/group/ouroboros/potrogue/world/Game.kt
Normal 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 won’t 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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
83
bin/main/group/ouroboros/potrogue/world/GameBuilder.kt
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
19
bin/main/group/ouroboros/potrogue/world/GameContext.kt
Normal 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
|
162
bin/main/group/ouroboros/potrogue/world/World.kt
Normal 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 we’ll 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 we’ll 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 we’re 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? We’ll 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 didn’t 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 we’ll be using it to display dialogs and similar things
|
||||||
|
screen = screen,
|
||||||
|
// We’ll inspect the UIEvent to determine what the user wants to do (like moving around).
|
||||||
|
// We’re 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
|
@ -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>
|
|
@ -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) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.5 KiB |
Before Width: | Height: | Size: 9.3 KiB After Width: | Height: | Size: 9.5 KiB |