inventory and its ui

Former-commit-id: b1a073c5636ac4516e6c9cf41bb97a844057de3f
This commit is contained in:
Song Minjae
2017-03-17 03:28:47 +09:00
parent 3d91023011
commit 2491a03c99
15 changed files with 364 additions and 63 deletions

View File

@@ -13,5 +13,5 @@
"GAME_INVENTORY_BLOCKS" : "블록", "GAME_INVENTORY_BLOCKS" : "블록",
"GAME_INVENTORY_WALLS" : "벽지", "GAME_INVENTORY_WALLS" : "벽지",
"CONTEXT_ITEM_EQUIPMENT_PLURAL" : "장비", "CONTEXT_ITEM_EQUIPMENT_PLURAL" : "장비",
"GAME_INVENTORY_FAVORITES" : "중요" "GAME_INVENTORY_FAVORITES" : "등록"
} }

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,55 @@
package net.torvald.terrarum
import net.torvald.terrarum.gamecontroller.Key
import net.torvald.terrarum.gamecontroller.KeyToggler
import net.torvald.terrarum.gameworld.fmod
import org.newdawn.slick.Color
import org.newdawn.slick.GameContainer
import org.newdawn.slick.Graphics
import org.newdawn.slick.state.BasicGameState
import org.newdawn.slick.state.StateBasedGame
/**
* Created by SKYHi14 on 2017-03-15.
*/
class StateControllerRumbleTest : BasicGameState() {
override fun init(container: GameContainer?, game: StateBasedGame?) {
}
override fun update(container: GameContainer, game: StateBasedGame, delta: Int) {
Terrarum.appgc.setTitle("${GAME_NAME} — Do not pull out the controller!")
KeyToggler.update(container.input)
if (Terrarum.controller != null) {
for (i in 0..minOf(rumblerCount - 1, 9)) {
Terrarum.controller!!.setRumblerStrength(i, if (KeyToggler.isOn(2 + i)) 1f else 0f)
}
}
}
private var rumblerCount = Terrarum.controller?.rumblerCount ?: 0
override fun getID() = Terrarum.STATE_ID_TOOL_RUMBLE_DIAGNOSIS
override fun render(gc: GameContainer, game: StateBasedGame, g: Graphics) {
g.font = Terrarum.fontGame
g.color = Color.white
if (Terrarum.controller != null) {
g.drawString("Controller: ${Terrarum.controller!!.name}", 10f, 10f)
g.drawString("Rumbler count: ${rumblerCount}", 10f, 30f)
g.drawString("Rumblers", 10f, 70f)
for (i in 0..minOf(rumblerCount - 1, 9)) {
g.color = if (KeyToggler.isOn(2 + i)) Color(0x55ff55) else Color(0x808080)
//g.drawString("$i", 10f + i * 16f, 90f)
g.drawString("$i${Terrarum.controller!!.getRumblerName(i)}", 10f, 90f + 20 * i)
}
}
else {
g.drawString("Controller not found.", 10f, 10f)
}
}
}

View File

@@ -1,16 +1,14 @@
package net.torvald.terrarum package net.torvald.terrarum
import net.torvald.terrarum.gameactors.ActorInventory import net.torvald.terrarum.gameactors.ActorInventory
import net.torvald.terrarum.gameactors.InventoryPair
import net.torvald.terrarum.gameitem.InventoryItem import net.torvald.terrarum.gameitem.InventoryItem
import net.torvald.terrarum.mapdrawer.MapCamera import net.torvald.terrarum.mapdrawer.MapCamera
import net.torvald.terrarum.ui.UICanvas import net.torvald.terrarum.ui.UICanvas
import net.torvald.terrarum.ui.UIHandler import net.torvald.terrarum.ui.UIHandler
import net.torvald.terrarum.ui.UIItemTextButton import net.torvald.terrarum.ui.UIItemTextButton
import net.torvald.terrarum.ui.UIItemTextButtonList import net.torvald.terrarum.ui.UIItemTextButtonList
import org.newdawn.slick.Color import org.newdawn.slick.*
import org.newdawn.slick.GameContainer
import org.newdawn.slick.Graphics
import org.newdawn.slick.Input
import org.newdawn.slick.state.BasicGameState import org.newdawn.slick.state.BasicGameState
import org.newdawn.slick.state.StateBasedGame import org.newdawn.slick.state.StateBasedGame
@@ -19,11 +17,13 @@ import org.newdawn.slick.state.StateBasedGame
*/ */
class StateUITest : BasicGameState() { class StateUITest : BasicGameState() {
val ui = UIHandler(SimpleUI()) val ui: UIHandler
val inventory = ActorInventory() val inventory = ActorInventory()
init { init {
ui = UIHandler(SimpleUI(inventory))
ui.posX = 50 ui.posX = 50
ui.posY = 30 ui.posY = 30
ui.isVisible = true ui.isVisible = true
@@ -31,13 +31,18 @@ class StateUITest : BasicGameState() {
inventory.add(object : InventoryItem() { inventory.add(object : InventoryItem() {
override val id: Int = 5656 override val id: Int = 5656
override var originalName: String = "Test tool"
override var baseMass: Double = 12.0 override var baseMass: Double = 12.0
override var baseToolSize: Double? = 8.0 override var baseToolSize: Double? = 8.0
override var category: String = "tool" override var category: String = "tool"
override var maxDurability: Double = 10.0
override var durability: Double = 10.0
}) })
inventory.getByID(5656)!!.item.name = "Test tool"
inventory.add(object : InventoryItem() { inventory.add(object : InventoryItem() {
override val id: Int = 4633 override val id: Int = 4633
override var originalName: String = "CONTEXT_ITEM_QUEST_NOUN"
override var baseMass: Double = 1.4 override var baseMass: Double = 1.4
override var baseToolSize: Double? = null override var baseToolSize: Double? = null
override var category: String = "bulk" override var category: String = "bulk"
@@ -64,12 +69,14 @@ class StateUITest : BasicGameState() {
private class SimpleUI : UICanvas { private class SimpleUI(val inventory: ActorInventory) : UICanvas {
override var width = 700 override var width = 700
override var height = 440 // multiple of 40 (2 * font.lineHeight) override var height = 480 // multiple of 40 (2 * font.lineHeight)
override var handler: UIHandler? = null override var handler: UIHandler? = null
override var openCloseTime: Int = UICanvas.OPENCLOSE_GENERIC override var openCloseTime: Int = UICanvas.OPENCLOSE_GENERIC
val itemImage = Image("./assets/item_kari_24.tga")
val buttons = UIItemTextButtonList( val buttons = UIItemTextButtonList(
this, this,
arrayOf( arrayOf(
@@ -93,10 +100,48 @@ private class SimpleUI : UICanvas {
kinematic = true kinematic = true
) )
val itemStripGutterV = 4
val itemStripGutterH = 48
val itemsStripWidth = width - buttons.width - 2 * itemStripGutterH
val items = Array(height / (UIItemInventoryElem.height + itemStripGutterV), { UIItemInventoryElem(
parentUI = this,
posX = buttons.width + itemStripGutterH,
posY = it * (UIItemInventoryElem.height + itemStripGutterV),
width = itemsStripWidth,
item = null,
amount = UIItemInventoryElem.UNIQUE_ITEM_HAS_NO_AMOUNT,
itemImage = null,
backCol = Color(255, 255, 255, 0x30)
) })
val itemsScrollOffset = 0
var inventorySortList = ArrayList<InventoryPair>()
var rebuildList = true
override fun update(gc: GameContainer, delta: Int) { override fun update(gc: GameContainer, delta: Int) {
Terrarum.gameLocale = "fiFI" // hot swap this to test Terrarum.gameLocale = "en" // hot swap this to test
buttons.update(gc, delta) buttons.update(gc, delta)
// test fill: just copy the inventory, fuck sorting
if (rebuildList) {
inventorySortList = ArrayList<InventoryPair>()
inventory.forEach { inventorySortList.add(it) }
rebuildList = false
// sort if needed //
inventorySortList.forEachIndexed { index, pair ->
if (index - itemsScrollOffset >= 0 && index < items.size + itemsScrollOffset) {
items[index - itemsScrollOffset].item = pair.item
items[index - itemsScrollOffset].amount = pair.amount
items[index - itemsScrollOffset].itemImage = itemImage
}
}
}
} }
override fun render(gc: GameContainer, g: Graphics) { override fun render(gc: GameContainer, g: Graphics) {
@@ -104,6 +149,11 @@ private class SimpleUI : UICanvas {
g.fillRect(0f, 0f, width.toFloat(), height.toFloat()) g.fillRect(0f, 0f, width.toFloat(), height.toFloat())
buttons.render(gc, g) buttons.render(gc, g)
items.forEach {
it.render(gc, g)
}
} }
override fun processInput(gc: GameContainer, delta: Int, input: Input) { override fun processInput(gc: GameContainer, delta: Int, input: Input) {
@@ -121,7 +171,7 @@ private class SimpleUI : UICanvas {
UICanvas.endOpeningFade(handler) UICanvas.endOpeningFade(handler)
} }
override fun endClosing(gc: GameContainer, delta: Int) { override fun endClosing(gc: GameContainer, delta: Int) {7
UICanvas.endClosingFade(handler) UICanvas.endClosingFade(handler)
} }
} }

View File

@@ -138,6 +138,7 @@ object Terrarum : StateBasedGame(GAME_NAME) {
val STATE_ID_TEST_UI = 0x105 val STATE_ID_TEST_UI = 0x105
val STATE_ID_TOOL_NOISEGEN = 0x200 val STATE_ID_TOOL_NOISEGEN = 0x200
val STATE_ID_TOOL_RUMBLE_DIAGNOSIS = 0x201
var controller: org.lwjgl.input.Controller? = null var controller: org.lwjgl.input.Controller? = null
private set private set
@@ -284,9 +285,9 @@ object Terrarum : StateBasedGame(GAME_NAME) {
//addState(StateShaderTest()) //addState(StateShaderTest())
//addState(StateNoiseTester()) //addState(StateNoiseTester())
addState(StateUITest()) addState(StateUITest())
//addState(StateControllerRumbleTest())
//ingame = StateInGame() //ingame = StateInGame(); addState(ingame)
//addState(ingame)
// foolproof // foolproof

View File

@@ -0,0 +1,101 @@
package net.torvald.terrarum
import net.torvald.terrarum.gameitem.InventoryItem
import net.torvald.terrarum.ui.UICanvas
import net.torvald.terrarum.ui.UIItem
import net.torvald.terrarum.ui.UIItemTextButton
import org.newdawn.slick.Color
import org.newdawn.slick.GameContainer
import org.newdawn.slick.Graphics
import org.newdawn.slick.Image
/**
* @param amount: set to -1 (UIItemInventoryElem.UNIQUE_ITEM_HAS_NO_AMOUNT) for unique item (does not show item count)
*
* Note that the UI will not render if either item or itemImage is null.
*
* Created by SKYHi14 on 2017-03-16.
*/
class UIItemInventoryElem(
parentUI: UICanvas,
override var posX: Int,
override var posY: Int,
override val width: Int,
var item: InventoryItem?,
var amount: Int,
var itemImage: Image?,
val backCol: Color = Color(0,0,0,0),
val backColBlendMode: String = BlendMode.NORMAL
) : UIItem(parentUI) {
companion object {
val height = 48
val UNIQUE_ITEM_HAS_NO_AMOUNT = -1
}
override val height = UIItemInventoryElem.height
private val imgOffset: Float
get() = (this.height - itemImage!!.height).div(2).toFloat() // to snap to the pixel grid
private val textOffsetX = 52f
override fun update(gc: GameContainer, delta: Int) {
if (item != null) {
}
}
override fun render(gc: GameContainer, g: Graphics) {
if (item != null && itemImage != null) {
g.font = Terrarum.fontGame
if (mouseUp) {
BlendMode.resolve(backColBlendMode)
g.color = backCol
g.fillRect(posX.toFloat(), posY.toFloat(), width.toFloat(), height.toFloat())
}
blendNormal()
g.drawImage(itemImage!!, posX + imgOffset, posY + imgOffset)
// if mouse is over, text lights up
g.color = item!!.nameColour * if (mouseUp) Color(0xffffff) else UIItemTextButton.defaultInactiveCol
g.drawString(item!!.name, posX + textOffsetX, posY + 0f)
if (item!!.maxDurability > 0.0) {
// TODO durability gauge
}
}
}
override fun keyPressed(key: Int, c: Char) {
}
override fun keyReleased(key: Int, c: Char) {
}
override fun mouseMoved(oldx: Int, oldy: Int, newx: Int, newy: Int) {
}
override fun mouseDragged(oldx: Int, oldy: Int, newx: Int, newy: Int) {
}
override fun mousePressed(button: Int, x: Int, y: Int) {
}
override fun mouseReleased(button: Int, x: Int, y: Int) {
}
override fun mouseWheelMoved(change: Int) {
}
override fun controllerButtonPressed(controller: Int, button: Int) {
}
override fun controllerButtonReleased(controller: Int, button: Int) {
}
}

View File

@@ -35,11 +35,11 @@ internal object Inventory : ConsoleCommand {
Echo("(inventory empty)") Echo("(inventory empty)")
} }
else { else {
target.inventory.forEach { refId, amount -> target.inventory.forEach {
if (amount == 0) { if (it.amount == 0) {
EchoError("Unexpected zero-amounted item: ID $refId") EchoError("Unexpected zero-amounted item: ID ${it.item.id}")
} }
Echo("ID $refId${if (amount > 1) " ($amount)" else ""}") Echo("ID ${it.item.id}${if (it.amount > 1) " ($it.second)" else ""}")
} }
} }
} }
@@ -62,7 +62,7 @@ internal object Inventory : ConsoleCommand {
val item = ItemCodex[refId] val item = ItemCodex[refId]
// if the item does not exist, add it first // if the item does not exist, add it first
if (!target.inventory.contains(item)) { if (!target.inventory.hasItem(item)) {
target.inventory.add(item) target.inventory.add(item)
} }

View File

@@ -140,6 +140,7 @@ open class ActorHumanoid(birth: GameDate, death: GameDate? = null)
override var baseMass: Double = 0.0 override var baseMass: Double = 0.0
override var baseToolSize: Double? = null override var baseToolSize: Double? = null
override var category = "should_not_be_seen" override var category = "should_not_be_seen"
override val originalName: String = actorValue.getAsString(AVKey.NAME) ?: "(no name)"
} }
override fun update(gc: GameContainer, delta: Int) { override fun update(gc: GameContainer, delta: Int) {
@@ -168,12 +169,12 @@ open class ActorHumanoid(birth: GameDate, death: GameDate? = null)
} }
// update inventory items // update inventory items
inventory.forEach { item, amount -> inventory.forEach {
if (!itemEquipped.contains(item)) { // unequipped if (!itemEquipped.contains(it.item)) { // unequipped
item.effectWhileInPocket(gc, delta) it.item.effectWhileInPocket(gc, delta)
} }
else { // equipped else { // equipped
item.effectWhenEquipped(gc, delta) it.item.effectWhenEquipped(gc, delta)
} }
} }
} }

View File

@@ -4,6 +4,8 @@ import net.torvald.terrarum.Terrarum
import net.torvald.terrarum.gameitem.InventoryItem import net.torvald.terrarum.gameitem.InventoryItem
import net.torvald.terrarum.itemproperties.ItemCodex import net.torvald.terrarum.itemproperties.ItemCodex
import java.util.* import java.util.*
import java.util.concurrent.locks.Lock
import java.util.concurrent.locks.ReentrantLock
/** /**
* Created by minjaesong on 16-03-15. * Created by minjaesong on 16-03-15.
@@ -22,9 +24,9 @@ class ActorInventory() {
private var capacityMode: Int private var capacityMode: Int
/** /**
* HashMap<ReferenceID, Amounts> * Sorted by referenceID.
*/ */
private val itemList: HashMap<InventoryItem, Int> = HashMap() private val itemList = ArrayList<InventoryPair>()
/** /**
* Default constructor with no encumbrance. * Default constructor with no encumbrance.
@@ -62,32 +64,42 @@ class ActorInventory() {
// If we already have the item, increment the amount // If we already have the item, increment the amount
// If not, add item with specified amount // If not, add item with specified amount
itemList.put(item, itemList[item] ?: 0 + count) val existingItem = getByID(item.id)
if (existingItem != null) { // if the item already exists
val newCount = getByID(item.id)!!.amount + count
itemList.remove(existingItem)
itemList.add(InventoryPair(existingItem.item, newCount))
}
else {
itemList.add(InventoryPair(item, count))
}
insertionSortLastElem(itemList)
} }
fun remove(itemID: Int, count: Int = 1) = remove(ItemCodex[itemID], count) fun remove(itemID: Int, count: Int = 1) = remove(ItemCodex[itemID], count)
fun remove(item: InventoryItem, count: Int = 1) { fun remove(item: InventoryItem, count: Int = 1) {
// check if the item does NOT exist val existingItem = getByID(item.id)
if (itemList[item] == null) { if (existingItem != null) { // if the item already exists
return val newCount = getByID(item.id)!!.amount - count
if (newCount < 0) {
throw Error("Tried to remove $count of $item, but the inventory only contains ${getByID(item.id)!!.amount} of them.")
}
else if (newCount > 0) {
add(item, -count)
}
else {
itemList.remove(existingItem)
}
} }
else { else {
// remove the existence of the item if count <= 0 throw Error("Tried to remove $item, but the inventory does not have it.")
if (itemList[item]!! - count <= 0) {
itemList.remove(item)
}
// else, decrement the item count
else {
itemList.put(item, itemList[item]!! - count)
}
} }
} }
/**
fun contains(item: InventoryItem) = itemList.containsKey(item) * HashMap<InventoryItem, Amounts>
fun contains(itemID: Int) = itemList.containsKey(ItemCodex[itemID]) */
fun forEach(consumer: (InventoryPair) -> Unit) = itemList.forEach(consumer)
fun forEach(consumer: (InventoryItem, Int) -> Unit) = itemList.forEach(consumer)
/** /**
* Get capacity of inventory * Get capacity of inventory
@@ -109,26 +121,9 @@ class ActorInventory() {
return capacityMode return capacityMode
} }
/**
* Get reference to the itemList
* @return
*/
fun getItemList(): Map<InventoryItem, Int>? {
return itemList
}
/**
* Get clone of the itemList
* @return
*/
@Suppress("UNCHECKED_CAST")
fun getCopyOfItemList(): Map<InventoryItem, Int>? {
return itemList.clone() as Map<InventoryItem, Int>
}
fun getTotalWeight(): Double { fun getTotalWeight(): Double {
var weight = 0.0 var weight = 0.0
itemList.forEach { item, i -> weight += item.mass * i } itemList.forEach { weight += it.item.mass * it.amount }
return weight return weight
} }
@@ -138,7 +133,7 @@ class ActorInventory() {
*/ */
fun getTotalCount(): Int { fun getTotalCount(): Int {
var count = 0 var count = 0
itemList.forEach { item, i -> count += i } itemList.forEach { count += it.amount }
return count return count
} }
@@ -158,10 +153,72 @@ class ActorInventory() {
if (getCapacityMode() == CAPACITY_MODE_WEIGHT) { if (getCapacityMode() == CAPACITY_MODE_WEIGHT) {
return capacityByWeight < getTotalWeight() return capacityByWeight < getTotalWeight()
} else if (getCapacityMode() == CAPACITY_MODE_COUNT) { } else if (getCapacityMode() == CAPACITY_MODE_COUNT) {
return capacityByCount < getTotalWeight() return capacityByCount < getTotalCount()
} else { } else {
return false return false
} }
} }
}
fun hasItem(item: InventoryItem) = hasItem(item.id)
fun hasItem(id: Int) =
if (itemList.size == 0)
false
else
itemList.binarySearch(id) >= 0
fun getByID(id: Int): InventoryPair? {
if (itemList.size == 0)
return null
val index = itemList.binarySearch(id)
if (index < 0)
return null
else
return itemList[index]
}
private fun insertionSortLastElem(arr: ArrayList<InventoryPair>) {
lock(ReentrantLock()) {
var j = arr.lastIndex - 1
val x = arr.last()
while (j >= 0 && arr[j].item > x.item) {
arr[j + 1] = arr[j]
j -= 1
}
arr[j + 1] = x
}
}
private fun ArrayList<InventoryPair>.binarySearch(ID: Int): Int {
// code from collections/Collections.kt
var low = 0
var high = this.size - 1
while (low <= high) {
val mid = (low + high).ushr(1) // safe from overflows
val midVal = get(mid).item
if (ID > midVal.id)
low = mid + 1
else if (ID < midVal.id)
high = mid - 1
else
return mid // key found
}
return -(low + 1) // key not found
}
inline fun lock(lock: Lock, body: () -> Unit) {
lock.lock()
try {
body()
}
finally {
lock.unlock()
}
}
}
data class InventoryPair(val item: InventoryItem, val amount: Int)

View File

@@ -53,6 +53,7 @@ open class HumanoidNPC(
actorValue[AVKey.SCALE] = value actorValue[AVKey.SCALE] = value
} }
override var category = "npc" override var category = "npc"
override val originalName: String = actorValue.getAsString(AVKey.NAME) ?: "NPC"
override fun secondaryUse(gc: GameContainer, delta: Int) { override fun secondaryUse(gc: GameContainer, delta: Int) {
// TODO place this Actor to the world // TODO place this Actor to the world

View File

@@ -1,12 +1,14 @@
package net.torvald.terrarum.gameitem package net.torvald.terrarum.gameitem
import net.torvald.terrarum.itemproperties.Material import net.torvald.terrarum.itemproperties.Material
import net.torvald.terrarum.langpack.Lang
import org.newdawn.slick.Color
import org.newdawn.slick.GameContainer import org.newdawn.slick.GameContainer
/** /**
* Created by minjaesong on 16-01-16. * Created by minjaesong on 16-01-16.
*/ */
abstract class InventoryItem { abstract class InventoryItem : Comparable<InventoryItem> {
/** /**
* Internal ID of an Item, Long * Internal ID of an Item, Long
* 0-4095: Tiles * 0-4095: Tiles
@@ -16,6 +18,21 @@ abstract class InventoryItem {
*/ */
abstract val id: Int abstract val id: Int
abstract protected val originalName: String
private var newName: String = "SET THE NAME!!"
var name: String
set(value) {
newName = value
isCustomName = true
}
get() = if (isCustomName) newName else Lang[originalName]
open var isCustomName = false // true: reads from lang
var nameColour = Color.white
abstract var baseMass: Double abstract var baseMass: Double
abstract var baseToolSize: Double? abstract var baseToolSize: Double?
@@ -59,6 +76,12 @@ abstract class InventoryItem {
*/ */
open var scale: Double = 1.0 open var scale: Double = 1.0
/**
* Set to zero if durability not applicable
*/
open var maxDurability: Double = 0.0
open var durability: Double = maxDurability
/** /**
* Effects applied continuously while in pocket * Effects applied continuously while in pocket
*/ */
@@ -107,6 +130,16 @@ abstract class InventoryItem {
if (other == null) return false if (other == null) return false
return id == (other as InventoryItem).id return id == (other as InventoryItem).id
} }
fun unsetCustomName() {
name = originalName
isCustomName = false
nameColour = Color.white
}
override fun compareTo(other: InventoryItem): Int = (this.id - other.id).sign()
fun Int.sign(): Int = if (this > 0) 1 else if (this < 0) -1 else 0
} }
object EquipPosition { object EquipPosition {

View File

@@ -42,6 +42,7 @@ object ItemCodex {
override var baseToolSize: Double? = null override var baseToolSize: Double? = null
override var equipPosition = EquipPosition.HAND_GRIP override var equipPosition = EquipPosition.HAND_GRIP
override var category = "block" override var category = "block"
override val originalName = TileCodex[i].nameKey
override fun primaryUse(gc: GameContainer, delta: Int) { override fun primaryUse(gc: GameContainer, delta: Int) {
// TODO base punch attack // TODO base punch attack

View File

@@ -24,12 +24,13 @@ class UIItemTextButton(
val highlightCol: Color = Color(0x00f8ff), val highlightCol: Color = Color(0x00f8ff),
val highlightBackCol: Color = Color(0xb0b0b0), val highlightBackCol: Color = Color(0xb0b0b0),
val highlightBackBlendMode: String = BlendMode.MULTIPLY, val highlightBackBlendMode: String = BlendMode.MULTIPLY,
val inactiveCol: Color = Color(0xc8c8c8) val inactiveCol: Color = UIItemTextButton.defaultInactiveCol
) : UIItem(parentUI) { ) : UIItem(parentUI) {
companion object { companion object {
val font = Terrarum.fontGame!! val font = Terrarum.fontGame!!
val height = font.lineHeight * 2 val height = font.lineHeight * 2
val defaultInactiveCol: Color = Color(0xc8c8c8)
} }
private val label: String private val label: String