Skip to content

Commit

Permalink
Merge pull request #16 from benpollarduk/attributes
Browse files Browse the repository at this point in the history
Attributes
  • Loading branch information
benpollarduk authored Jan 30, 2024
2 parents 6e25267 + 9124fd5 commit d296ba6
Show file tree
Hide file tree
Showing 16 changed files with 638 additions and 9 deletions.
71 changes: 71 additions & 0 deletions docs/mkdocs/docs/attributes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# Attributes

## Overview
All examinable objects can have attributes. Attributes provide a way of adding a lot of depth to games. For example,
attributes could be used to buy and sell items, contain a characters XP or HP or even provide a way to add durability
to items.

## Use
To add to an existing attribute or to create a new one use the **add** function.

```kotlin
var player = PlayableCharacter("Player", "")
player.attributes.add("$", 10)
```

To subtract from an existing attribute use the **subtract** function.

```kotlin
player.attributes.subtract("$", 10)
```

Attributes values can be capped. In this example the $ attribute is limited to a range of 0 - 100. Adding or
subtracting will not cause the value of the attribute to change outside of this range.

```kotlin
var cappedAttribute = Attribute("$", "Dollars.", 0, 100)
player.attributes.add(cappedAttribute, 50)
```

## An example - buying an Item from a NonPlayableCharacter.
The following is an example of buying an Item from NonPlayableCharacter. Here a trader has a spade. The player can only
buy the spade if they have at least $5. The conversation will jump to the correct paragraph based on if they choose to
buy the spade or not. If the player chooses to buy the spade and has enough $ the transaction is made and the spade
changes hands.

```kotlin
val currency = "$"
val spade = Item("Spade", "")

val player = PlayableCharacter("Player", "").apply {
attributes.add(currency, 10)
}

val trader = NonPlayableCharacter("Trader", "").apply {
acquireItem(spade)
conversation = Conversation(
listOf(
Paragraph("What will you buy").also {
it.responses = listOf(
Response("Spade.", ByCallback {
if (player.attributes.getValue(currency) >= 5) {
ToName("BoughtSpade")
} else {
ToName("NotEnough")
}
}),
Response("Nothing.", Last()),
)
},
Paragraph("Here it is.", First(), "BoughtSpade") {
player.attributes.subtract(currency, 5)
attributes.add(currency, 5)
give(spade, player)
},
Paragraph("You don't have enough money.", First(), "NotEnough"),
Paragraph("Fine.")
),
)
}
```
This is just one example of using attributes to add depth to a game.
1 change: 1 addition & 0 deletions docs/mkdocs/mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ nav:
- 'NonPlayableCharacter': 'non-playable-character.md'
- Conditional Descriptions: 'conditional-descriptions.md'
- Commands: 'commands.md'
- Attributes: 'attributes.md'
- Frame Builders: 'frame-builders.md'
- End Conditions: 'end-conditions.md'
- Api documentation: 'api-documentation.md'
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import com.github.benpollarduk.ktaf.utilities.templates.AssetTemplate
internal class Player : AssetTemplate<PlayableCharacter> {
override fun instantiate(): PlayableCharacter {
return PlayableCharacter(NAME, DESCRIPTION).also {
it.attributes.add("£", 5)
it.acquireItem(Knife().instantiate())
it.interaction = { item ->
when {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.github.benpollarduk.ktaf.assets

import com.github.benpollarduk.ktaf.assets.attributes.AttributeManager
import com.github.benpollarduk.ktaf.commands.CustomCommand

/**
Expand All @@ -21,6 +22,11 @@ public interface Examinable : PlayerVisible {
*/
public val commands: List<CustomCommand>

/**
* An [AttributeManager] that provides management of all [Attribute] for this [Examinable].
*/
public val attributes: AttributeManager

/**
* Examine this object to obtain an [ExaminationResult].
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.github.benpollarduk.ktaf.assets

import com.github.benpollarduk.ktaf.assets.attributes.AttributeManager
import com.github.benpollarduk.ktaf.commands.CustomCommand
import com.github.benpollarduk.ktaf.extensions.ensureFinishedSentence
import com.github.benpollarduk.ktaf.extensions.removeSentenceEnd
Expand All @@ -9,6 +10,8 @@ import com.github.benpollarduk.ktaf.utilities.NEWLINE
* Provides a base implementation for examinable objects.
*/
public abstract class ExaminableObject : Examinable {
public override val attributes: AttributeManager = AttributeManager()

/**
* Provides a callback for handling examination of this object.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.github.benpollarduk.ktaf.assets.attributes

/**
* An attribute with a specified [name], [description] and an optional [minimum] and [maximum] value.
*/
public data class Attribute(
public val name: String,
public val description: String,
public val minimum: Int = Int.MIN_VALUE,
public val maximum: Int = Int.MAX_VALUE
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
package com.github.benpollarduk.ktaf.assets.attributes

import com.github.benpollarduk.ktaf.extensions.insensitiveEquals

/**
* Provides a class for managing [Attribute].
*/
public class AttributeManager {
private val attributes: MutableMap<Attribute, Int> = mutableMapOf()

/**
* The number of [Attribute] managed by this [AttributeManager].
*/
public val count: Int
get() = attributes.size

/**
* Ensure an attribute with a specified [name] exists. If it doesn't it is added.
*/
private fun ensureAttributeExists(name: String) {
val attribute = attributes.keys.firstOrNull { it.name.insensitiveEquals(name) }

if (attribute != null) {
return
}

val newAttribute = Attribute(name, "")
attributes[newAttribute] = 0
}

/**
* Ensure a specified [attribute] exists. If it doesn't it is added.
*/
private fun ensureAttributeExists(attribute: Attribute) {
val match = attributes.keys.firstOrNull { it.name.insensitiveEquals(attribute.name) }

if (match != null) {
return
}

attributes[attribute] = 0
}

/**
* Get an [Attribute] that matches a specified [key].
*/
private fun getMatch(key: Attribute): Attribute {
ensureAttributeExists(key)
return attributes.keys.first { it.name == key.name }
}

/**
* Cap a [value] between a [min] and [max] value.
*/
private fun capValue(value: Int, min: Int, max: Int): Int {
return when {
value < min -> min
value > max -> max
else -> value
}
}

/**
* Get all [Attribute] managed by this [AttributeManager].
*/
public fun getAttributes(): Array<Attribute> {
return attributes.keys.toTypedArray()
}

/**
* Get all [Attribute] and values managed by this [AttributeManager].
*/
public fun toMap(): Map<Attribute, Int> {
return attributes.toMap()
}

/**
* Get the value of an attribute from a specified [attributeName].
*/
public fun getValue(attributeName: String): Int {
val attribute = attributes.keys.firstOrNull { it.name.insensitiveEquals(attributeName) } ?: return 0
return getValue(attribute)
}

/**
* Get the value of an [attribute].
*/
public fun getValue(attribute: Attribute): Int {
return attributes[attribute] ?: 0
}

/**
* Add an attribute with a specified [attributeName] and [value].
*/
public fun add(attributeName: String, value: Int) {
ensureAttributeExists(attributeName)
val attribute = attributes.keys.first { it.name.insensitiveEquals(attributeName) }
attributes[attribute] = capValue(
attributes[attribute]?.plus(value) ?: 0,
attribute.minimum,
attribute.maximum
)
}

/**
* Add an [attribute] with a specified [value].
*/
public fun add(attribute: Attribute, value: Int) {
val matchedAttribute = getMatch(attribute)
attributes[matchedAttribute] = capValue(
attributes[matchedAttribute]?.plus(value) ?: 0,
matchedAttribute.minimum,
matchedAttribute.maximum
)
}

/**
* Subtract a [value] from an attribute with a specified [attributeName].
*/
public fun subtract(attributeName: String, value: Int) {
ensureAttributeExists(attributeName)
val attribute = attributes.keys.first { it.name.insensitiveEquals(attributeName) }
attributes[attribute] = capValue(
attributes[attribute]?.minus(value) ?: 0,
attribute.minimum,
attribute.maximum
)
}

/**
* Subtract a [value] from an [attribute].
*/
public fun subtract(attribute: Attribute, value: Int) {
val matchedAttribute = getMatch(attribute)
attributes[matchedAttribute] = capValue(
attributes[matchedAttribute]?.minus(value) ?: 0,
matchedAttribute.minimum,
matchedAttribute.maximum
)
}

/**
* Remove an attribute with a specified [attributeName].
*/
public fun remove(attributeName: String) {
val attribute = attributes.keys.firstOrNull { it.name.insensitiveEquals(attributeName) }

if (attribute != null) {
attributes.remove(attribute)
}
}

/**
* Remove an [attribute].
*/
public fun remove(attribute: Attribute) {
val matchedAttribute = getMatch(attribute)
attributes.remove(matchedAttribute)
}

/**
* Remove all [attributes].
*/
public fun removeAll() {
attributes.clear()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.github.benpollarduk.ktaf.conversations.instructions

import com.github.benpollarduk.ktaf.conversations.Paragraph

/**
* An end of paragraph instruction that shifts paragraphs based on a callback.
*/
public class ByCallback(public val callback: () -> EndOfParagraphInstruction) : EndOfParagraphInstruction {
override fun getIndexOfNext(current: Paragraph, paragraphs: List<Paragraph>): Int {
return callback().getIndexOfNext(current, paragraphs)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,16 @@ public class AnsiSceneFrameBuilder(
)
}

if (playableCharacter.attributes.count > 0) {
lastPosition = ansiGridStringBuilder.drawWrapped(
StringUtilities.getAttributesAsString(playableCharacter.attributes.toMap()),
leftMargin,
lastPosition.y + 2,
availableWidth,
textColor
)
}

if (!displayMessagesInIsolation && !displayMessage) {
ansiGridStringBuilder.drawHorizontalDivider(lastPosition.y + 3, borderColor)
lastPosition = ansiGridStringBuilder.drawWrapped(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.github.benpollarduk.ktaf.rendering.frames.html

import com.github.benpollarduk.ktaf.assets.Item
import com.github.benpollarduk.ktaf.assets.Size
import com.github.benpollarduk.ktaf.assets.attributes.Attribute
import com.github.benpollarduk.ktaf.assets.characters.PlayableCharacter
import com.github.benpollarduk.ktaf.assets.locations.Region
import com.github.benpollarduk.ktaf.assets.locations.Room
Expand Down Expand Up @@ -41,6 +43,18 @@ public class HtmlSceneFrameBuilder(
return false
}

private fun addPlayerItems(items: List<Item>) {
if (items.any()) {
htmlPageBuilder.p("You have: " + StringUtilities.constructExaminablesAsSentence(items))
}
}

private fun addPlayerAttributes(attributes: Map<Attribute, Int>) {
if (attributes.any()) {
htmlPageBuilder.p(StringUtilities.getAttributesAsString(attributes))
}
}

override fun build(
room: Room,
viewPoint: ViewPoint,
Expand Down Expand Up @@ -88,13 +102,8 @@ public class HtmlSceneFrameBuilder(
var map = gridStringBuilder.toString().removeWhitespaceLines()
htmlPageBuilder.pre(map.replace(NEWLINE, "<br>"))

if (playableCharacter.items.any()) {
htmlPageBuilder.p(
"You have: " + StringUtilities.constructExaminablesAsSentence(
playableCharacter.items
)
)
}
addPlayerItems(playableCharacter.items)
addPlayerAttributes(playableCharacter.attributes.toMap())

if (!displayMessagesInIsolation && !displayMessage) {
htmlPageBuilder.p(message.ensureFinishedSentence())
Expand Down
Loading

0 comments on commit d296ba6

Please sign in to comment.