From 8ad9f45d41a5bee6c2da1a8cdee15854596a88c6 Mon Sep 17 00:00:00 2001 From: adam-arold Date: Wed, 26 Jun 2019 23:05:30 +0200 Subject: [PATCH] Add experience and leveling up --- .../cavesofzircon/attributes/CombatStats.kt | 2 +- .../cavesofzircon/attributes/Experience.kt | 36 +++++++++ .../cavesofzircon/attributes/Vision.kt | 2 +- .../attributes/types/EntityTypes.kt | 2 +- .../attributes/types/ExperienceGainer.kt | 15 ++++ .../cavesofzircon/builders/EntityFactory.kt | 5 +- .../cavesofzircon/commands/EntityDestroyed.kt | 10 +++ .../cavesofzircon/events/PlayerGainedLevel.kt | 5 ++ .../cavesofzircon/systems/Destructible.kt | 5 +- .../systems/ExperienceAccumulator.kt | 50 ++++++++++++ .../hexworks/cavesofzircon/view/PlayView.kt | 5 ++ .../view/dialog/CloseButtonFragment.kt | 23 ++++++ .../cavesofzircon/view/dialog/Dialog.kt | 28 +++++++ .../view/dialog/LevelUpDialog.kt | 78 +++++++++++++++++++ 14 files changed, 260 insertions(+), 6 deletions(-) create mode 100644 src/main/kotlin/org/hexworks/cavesofzircon/attributes/Experience.kt create mode 100644 src/main/kotlin/org/hexworks/cavesofzircon/attributes/types/ExperienceGainer.kt create mode 100644 src/main/kotlin/org/hexworks/cavesofzircon/commands/EntityDestroyed.kt create mode 100644 src/main/kotlin/org/hexworks/cavesofzircon/events/PlayerGainedLevel.kt create mode 100644 src/main/kotlin/org/hexworks/cavesofzircon/systems/ExperienceAccumulator.kt create mode 100644 src/main/kotlin/org/hexworks/cavesofzircon/view/dialog/CloseButtonFragment.kt create mode 100644 src/main/kotlin/org/hexworks/cavesofzircon/view/dialog/Dialog.kt create mode 100644 src/main/kotlin/org/hexworks/cavesofzircon/view/dialog/LevelUpDialog.kt diff --git a/src/main/kotlin/org/hexworks/cavesofzircon/attributes/CombatStats.kt b/src/main/kotlin/org/hexworks/cavesofzircon/attributes/CombatStats.kt index a5a39bf..c0eaaa8 100644 --- a/src/main/kotlin/org/hexworks/cavesofzircon/attributes/CombatStats.kt +++ b/src/main/kotlin/org/hexworks/cavesofzircon/attributes/CombatStats.kt @@ -16,7 +16,7 @@ data class CombatStats(val maxHpProperty: Property, val defenseValue: Int by defenseValueProperty.asDelegate() override fun toComponent(width: Int) = Components.vbox() - .withSize(width, 5) + .withSize(width, 4) .build().apply { val hpLabel = Components.label() .withSize(width, 1) diff --git a/src/main/kotlin/org/hexworks/cavesofzircon/attributes/Experience.kt b/src/main/kotlin/org/hexworks/cavesofzircon/attributes/Experience.kt new file mode 100644 index 0000000..0109dac --- /dev/null +++ b/src/main/kotlin/org/hexworks/cavesofzircon/attributes/Experience.kt @@ -0,0 +1,36 @@ +package org.hexworks.cavesofzircon.attributes + +import org.hexworks.cobalt.databinding.api.createPropertyFrom +import org.hexworks.cobalt.databinding.api.expression.concat +import org.hexworks.cobalt.databinding.api.property.Property +import org.hexworks.zircon.api.Components + +data class Experience(val currentXPProperty: Property = createPropertyFrom(0), + val currentLevelProperty: Property = createPropertyFrom(1)) : DisplayableAttribute { + + var currentXP: Int by currentXPProperty.asDelegate() + var currentLevel: Int by currentLevelProperty.asDelegate() + + override fun toComponent(width: Int) = Components.vbox() + .withSize(width, 3) + .build().apply { + val xpLabel = Components.label() + .withSize(width, 1) + .build() + val levelLabel = Components.label() + .withSize(width, 1) + .build() + + xpLabel.textProperty.updateFrom(createPropertyFrom("XP: ") + .concat(currentXPProperty)) + + levelLabel.textProperty.updateFrom(createPropertyFrom("Lvl: ") + .concat(currentLevelProperty)) + + addComponent(Components.textBox() + .withContentWidth(width) + .addHeader("Experience", false)) + addComponent(xpLabel) + addComponent(levelLabel) + } +} diff --git a/src/main/kotlin/org/hexworks/cavesofzircon/attributes/Vision.kt b/src/main/kotlin/org/hexworks/cavesofzircon/attributes/Vision.kt index ebae09c..438a2e7 100644 --- a/src/main/kotlin/org/hexworks/cavesofzircon/attributes/Vision.kt +++ b/src/main/kotlin/org/hexworks/cavesofzircon/attributes/Vision.kt @@ -2,4 +2,4 @@ package org.hexworks.cavesofzircon.attributes import org.hexworks.amethyst.api.Attribute -data class Vision(val radius: Int) : Attribute +data class Vision(var radius: Int) : Attribute diff --git a/src/main/kotlin/org/hexworks/cavesofzircon/attributes/types/EntityTypes.kt b/src/main/kotlin/org/hexworks/cavesofzircon/attributes/types/EntityTypes.kt index 597f42c..bce0383 100644 --- a/src/main/kotlin/org/hexworks/cavesofzircon/attributes/types/EntityTypes.kt +++ b/src/main/kotlin/org/hexworks/cavesofzircon/attributes/types/EntityTypes.kt @@ -3,7 +3,7 @@ package org.hexworks.cavesofzircon.attributes.types import org.hexworks.amethyst.api.base.BaseEntityType object Player : BaseEntityType( - name = "player"), Combatant, ItemHolder, EnergyUser, EquipmentHolder + name = "player"), Combatant, ItemHolder, EnergyUser, EquipmentHolder, ExperienceGainer object Wall : BaseEntityType( name = "wall") diff --git a/src/main/kotlin/org/hexworks/cavesofzircon/attributes/types/ExperienceGainer.kt b/src/main/kotlin/org/hexworks/cavesofzircon/attributes/types/ExperienceGainer.kt new file mode 100644 index 0000000..f286c5f --- /dev/null +++ b/src/main/kotlin/org/hexworks/cavesofzircon/attributes/types/ExperienceGainer.kt @@ -0,0 +1,15 @@ +package org.hexworks.cavesofzircon.attributes.types + +import org.hexworks.amethyst.api.entity.EntityType +import org.hexworks.cavesofzircon.attributes.CombatStats +import org.hexworks.cavesofzircon.attributes.Experience +import org.hexworks.cavesofzircon.extensions.GameEntity + +interface ExperienceGainer : EntityType + +val GameEntity.experience: Experience + get() = findAttribute(Experience::class).get() + +val GameEntity.combatStats: CombatStats + get() = findAttribute(CombatStats::class).get() + diff --git a/src/main/kotlin/org/hexworks/cavesofzircon/builders/EntityFactory.kt b/src/main/kotlin/org/hexworks/cavesofzircon/builders/EntityFactory.kt index 49db99a..c6563b6 100644 --- a/src/main/kotlin/org/hexworks/cavesofzircon/builders/EntityFactory.kt +++ b/src/main/kotlin/org/hexworks/cavesofzircon/builders/EntityFactory.kt @@ -9,6 +9,7 @@ import org.hexworks.cavesofzircon.attributes.EntityActions import org.hexworks.cavesofzircon.attributes.EntityPosition import org.hexworks.cavesofzircon.attributes.EntityTile import org.hexworks.cavesofzircon.attributes.Equipment +import org.hexworks.cavesofzircon.attributes.Experience import org.hexworks.cavesofzircon.attributes.FungusSpread import org.hexworks.cavesofzircon.attributes.Inventory import org.hexworks.cavesofzircon.attributes.ItemCombatStats @@ -46,6 +47,7 @@ import org.hexworks.cavesofzircon.systems.Destructible import org.hexworks.cavesofzircon.systems.DigestiveSystem import org.hexworks.cavesofzircon.systems.Diggable import org.hexworks.cavesofzircon.systems.EnergyExpender +import org.hexworks.cavesofzircon.systems.ExperienceAccumulator import org.hexworks.cavesofzircon.systems.FungusGrowth import org.hexworks.cavesofzircon.systems.HunterSeeker import org.hexworks.cavesofzircon.systems.InputReceiver @@ -91,6 +93,7 @@ object EntityFactory { fun newPlayer() = newGameEntityOfType(Player) { attributes( + Experience(), Vision(9), EntityPosition(), BlockOccupier, @@ -106,7 +109,7 @@ object EntityFactory { initialWeapon = newClub(), initialArmor = newJacket())) behaviors(InputReceiver, EnergyExpender) - facets(Movable, CameraMover, StairClimber, StairDescender, Attackable, Destructible, + facets(Movable, CameraMover, StairClimber, StairDescender, Attackable, ExperienceAccumulator, Destructible, ItemPicker, InventoryInspector, ItemDropper, EnergyExpender, DigestiveSystem) } diff --git a/src/main/kotlin/org/hexworks/cavesofzircon/commands/EntityDestroyed.kt b/src/main/kotlin/org/hexworks/cavesofzircon/commands/EntityDestroyed.kt new file mode 100644 index 0000000..81d6e77 --- /dev/null +++ b/src/main/kotlin/org/hexworks/cavesofzircon/commands/EntityDestroyed.kt @@ -0,0 +1,10 @@ +package org.hexworks.cavesofzircon.commands + +import org.hexworks.amethyst.api.entity.EntityType +import org.hexworks.cavesofzircon.extensions.GameCommand +import org.hexworks.cavesofzircon.extensions.GameEntity +import org.hexworks.cavesofzircon.world.GameContext + +data class EntityDestroyed(override val context: GameContext, + override val source: GameEntity, + val destroyer: GameEntity) : GameCommand diff --git a/src/main/kotlin/org/hexworks/cavesofzircon/events/PlayerGainedLevel.kt b/src/main/kotlin/org/hexworks/cavesofzircon/events/PlayerGainedLevel.kt new file mode 100644 index 0000000..e292442 --- /dev/null +++ b/src/main/kotlin/org/hexworks/cavesofzircon/events/PlayerGainedLevel.kt @@ -0,0 +1,5 @@ +package org.hexworks.cavesofzircon.events + +import org.hexworks.cobalt.events.api.Event + +object PlayerGainedLevel : Event diff --git a/src/main/kotlin/org/hexworks/cavesofzircon/systems/Destructible.kt b/src/main/kotlin/org/hexworks/cavesofzircon/systems/Destructible.kt index dfea149..3dec41f 100644 --- a/src/main/kotlin/org/hexworks/cavesofzircon/systems/Destructible.kt +++ b/src/main/kotlin/org/hexworks/cavesofzircon/systems/Destructible.kt @@ -4,15 +4,16 @@ import org.hexworks.amethyst.api.Consumed import org.hexworks.amethyst.api.base.BaseFacet import org.hexworks.amethyst.api.entity.EntityType import org.hexworks.cavesofzircon.commands.Destroy +import org.hexworks.cavesofzircon.commands.EntityDestroyed import org.hexworks.cavesofzircon.extensions.GameCommand import org.hexworks.cavesofzircon.functions.logGameEvent import org.hexworks.cavesofzircon.world.GameContext object Destructible : BaseFacet() { - override fun executeCommand(command: GameCommand) = command.responseWhenCommandIs(Destroy::class) { (context, attacker, target, cause) -> + override fun executeCommand(command: GameCommand) = command.responseWhenCommandIs(Destroy::class) { (context, destroyer, target, cause) -> context.world.removeEntity(target) - + destroyer.executeCommand(EntityDestroyed(context, target, destroyer)) logGameEvent("$target dies $cause.") Consumed } diff --git a/src/main/kotlin/org/hexworks/cavesofzircon/systems/ExperienceAccumulator.kt b/src/main/kotlin/org/hexworks/cavesofzircon/systems/ExperienceAccumulator.kt new file mode 100644 index 0000000..906a6f4 --- /dev/null +++ b/src/main/kotlin/org/hexworks/cavesofzircon/systems/ExperienceAccumulator.kt @@ -0,0 +1,50 @@ +package org.hexworks.cavesofzircon.systems + +import org.hexworks.amethyst.api.Consumed +import org.hexworks.amethyst.api.Pass +import org.hexworks.amethyst.api.Response +import org.hexworks.amethyst.api.base.BaseFacet +import org.hexworks.amethyst.api.entity.EntityType +import org.hexworks.cavesofzircon.attributes.CombatStats +import org.hexworks.cavesofzircon.attributes.types.ExperienceGainer +import org.hexworks.cavesofzircon.attributes.types.combatStats +import org.hexworks.cavesofzircon.attributes.types.experience +import org.hexworks.cavesofzircon.commands.EntityDestroyed +import org.hexworks.cavesofzircon.events.PlayerGainedLevel +import org.hexworks.cavesofzircon.extensions.GameCommand +import org.hexworks.cavesofzircon.extensions.attackValue +import org.hexworks.cavesofzircon.extensions.defenseValue +import org.hexworks.cavesofzircon.extensions.isPlayer +import org.hexworks.cavesofzircon.extensions.whenTypeIs +import org.hexworks.cavesofzircon.functions.logGameEvent +import org.hexworks.cavesofzircon.world.GameContext +import org.hexworks.cobalt.datatypes.extensions.map +import org.hexworks.zircon.internal.Zircon +import kotlin.math.min + +object ExperienceAccumulator : BaseFacet() { + + override fun executeCommand(command: GameCommand) = command.responseWhenCommandIs(EntityDestroyed::class) { (_, defender, attacker) -> + var response: Response = Pass + attacker.whenTypeIs { experienceGainer -> + response = Consumed + val xp = experienceGainer.experience + val stats = experienceGainer.combatStats + val defenderHp = defender.findAttribute(CombatStats::class).map { it.maxHp }.orElse(0) + val amount = (defenderHp + defender.attackValue + defender.defenseValue) - xp.currentLevel * 2 + if (amount > 0) { + xp.currentXP += amount + while (xp.currentXP > Math.pow(xp.currentLevel.toDouble(), 1.5) * 20) { + xp.currentLevel++ + logGameEvent("$attacker advanced to level ${xp.currentLevel}.") + stats.hpProperty.value = min(stats.hp + xp.currentLevel * 2, stats.maxHp) + if (attacker.isPlayer) { + Zircon.eventBus.publish(PlayerGainedLevel) + } + } + } + + } + response + } +} diff --git a/src/main/kotlin/org/hexworks/cavesofzircon/view/PlayView.kt b/src/main/kotlin/org/hexworks/cavesofzircon/view/PlayView.kt index ced5702..27df160 100644 --- a/src/main/kotlin/org/hexworks/cavesofzircon/view/PlayView.kt +++ b/src/main/kotlin/org/hexworks/cavesofzircon/view/PlayView.kt @@ -3,6 +3,8 @@ package org.hexworks.cavesofzircon.view import org.hexworks.cavesofzircon.GameConfig import org.hexworks.cavesofzircon.blocks.GameBlock import org.hexworks.cavesofzircon.events.GameLogEvent +import org.hexworks.cavesofzircon.events.PlayerGainedLevel +import org.hexworks.cavesofzircon.view.dialog.LevelUpDialog import org.hexworks.cavesofzircon.view.fragment.PlayerStatsFragment import org.hexworks.cavesofzircon.world.Game import org.hexworks.cavesofzircon.world.GameBuilder @@ -55,6 +57,9 @@ class PlayView(private val game: Game = GameBuilder.defaultGame()) : BaseView() withNewLine = false, withTypingEffectSpeedInMs = 10) } + Zircon.eventBus.subscribe { + screen.openModal(LevelUpDialog(screen, game.player)) + } val gameComponent = GameComponents.newGameComponentBuilder() .withGameArea(game.world) diff --git a/src/main/kotlin/org/hexworks/cavesofzircon/view/dialog/CloseButtonFragment.kt b/src/main/kotlin/org/hexworks/cavesofzircon/view/dialog/CloseButtonFragment.kt new file mode 100644 index 0000000..7822344 --- /dev/null +++ b/src/main/kotlin/org/hexworks/cavesofzircon/view/dialog/CloseButtonFragment.kt @@ -0,0 +1,23 @@ +package org.hexworks.cavesofzircon.view.dialog + +import org.hexworks.zircon.api.Components +import org.hexworks.zircon.api.component.ComponentAlignment +import org.hexworks.zircon.api.component.Container +import org.hexworks.zircon.api.component.Fragment +import org.hexworks.zircon.api.component.modal.Modal +import org.hexworks.zircon.api.extensions.onComponentEvent +import org.hexworks.zircon.api.uievent.ComponentEventType +import org.hexworks.zircon.api.uievent.Processed +import org.hexworks.zircon.internal.component.modal.EmptyModalResult + +class CloseButtonFragment(modal: Modal, parent: Container) : Fragment { + + override val root = Components.button().withText("Close") + .withAlignmentWithin(parent, ComponentAlignment.BOTTOM_RIGHT) + .build().apply { + onComponentEvent(ComponentEventType.ACTIVATED) { + modal.close(EmptyModalResult) + Processed + } + } +} diff --git a/src/main/kotlin/org/hexworks/cavesofzircon/view/dialog/Dialog.kt b/src/main/kotlin/org/hexworks/cavesofzircon/view/dialog/Dialog.kt new file mode 100644 index 0000000..8918293 --- /dev/null +++ b/src/main/kotlin/org/hexworks/cavesofzircon/view/dialog/Dialog.kt @@ -0,0 +1,28 @@ +package org.hexworks.cavesofzircon.view.dialog + +import org.hexworks.cavesofzircon.GameConfig +import org.hexworks.zircon.api.builder.component.ModalBuilder +import org.hexworks.zircon.api.component.Container +import org.hexworks.zircon.api.component.modal.Modal +import org.hexworks.zircon.api.component.modal.ModalFragment +import org.hexworks.zircon.api.screen.Screen +import org.hexworks.zircon.internal.component.modal.EmptyModalResult + +abstract class Dialog(private val screen: Screen, + withClose: Boolean = true) : ModalFragment { + + abstract val container: Container // 1 + + final override val root: Modal by lazy { // 2 + ModalBuilder.newBuilder() + .withComponent(container) + .withParentSize(screen.size) + .withCenteredDialog(true) // 3 + .build().also { + if (withClose) { // 4 + container.addFragment(CloseButtonFragment(it, container)) + } + container.applyColorTheme(GameConfig.THEME) + } + } +} diff --git a/src/main/kotlin/org/hexworks/cavesofzircon/view/dialog/LevelUpDialog.kt b/src/main/kotlin/org/hexworks/cavesofzircon/view/dialog/LevelUpDialog.kt new file mode 100644 index 0000000..7d6dea1 --- /dev/null +++ b/src/main/kotlin/org/hexworks/cavesofzircon/view/dialog/LevelUpDialog.kt @@ -0,0 +1,78 @@ +package org.hexworks.cavesofzircon.view.dialog + +import org.hexworks.cavesofzircon.attributes.CombatStats +import org.hexworks.cavesofzircon.attributes.Vision +import org.hexworks.cavesofzircon.attributes.types.Player +import org.hexworks.cavesofzircon.extensions.GameEntity +import org.hexworks.cavesofzircon.extensions.tryToFindAttribute +import org.hexworks.cavesofzircon.functions.logGameEvent +import org.hexworks.zircon.api.Components +import org.hexworks.zircon.api.extensions.onComponentEvent +import org.hexworks.zircon.api.graphics.BoxType +import org.hexworks.zircon.api.screen.Screen +import org.hexworks.zircon.api.uievent.ComponentEventType +import org.hexworks.zircon.api.uievent.Processed +import org.hexworks.zircon.internal.component.modal.EmptyModalResult + +class LevelUpDialog(screen: Screen, player: GameEntity) : Dialog(screen, false) { + + override val container = Components.vbox() + .withTitle("Ding!") + .withSize(30, 15) + .withBoxType(BoxType.TOP_BOTTOM_DOUBLE) + .wrapWithBox() + .build().apply { + val stats = player.tryToFindAttribute(CombatStats::class) + val vision = player.tryToFindAttribute(Vision::class) + + addComponent(Components.textBox() + .withContentWidth(27) + .addHeader("Congratulations, you leveled up!") + .addParagraph("Pick an improvement from the options below:")) + + addComponent(Components.button() + .withText("Max HP") + .build().apply { + onComponentEvent(ComponentEventType.ACTIVATED) { + stats.maxHpProperty.value += 10 + logGameEvent("You look healthier.") + root.close(EmptyModalResult) + Processed + } + }) + + + addComponent(Components.button() + .withText("Attack") + .build().apply { + onComponentEvent(ComponentEventType.ACTIVATED) { + stats.attackValueProperty.value += 2 + logGameEvent("You look stronger.") + root.close(EmptyModalResult) + Processed + } + }) + + addComponent(Components.button() + .withText("Defense") + .build().apply { + onComponentEvent(ComponentEventType.ACTIVATED) { + stats.defenseValueProperty.value += 2 + logGameEvent("You look tougher.") + root.close(EmptyModalResult) + Processed + } + }) + + addComponent(Components.button() + .withText("Vision") + .build().apply { + onComponentEvent(ComponentEventType.ACTIVATED) { + vision.radius++ + logGameEvent("You look more perceptive.") + root.close(EmptyModalResult) + Processed + } + }) + } +}