Table of Contents
- Items
- Overview
- Item System Architecture
- Item Types
- Creating Custom Items
- Step 1: Define Item Class
- Step 2: Register in CSV
- Step 3: Add Translation
- Step 4: Create Sprite
- Step 5: Load in Module
- Item Properties
- Item Effects
- Item Tags
- Dynamic Item System
- Example Items
- Best Practises
- Common Pitfalls
- See Also
Items
Audience: Module developers creating tools, consumables, equipment, and custom items.
Items are objects that actors can carry, use, equip, and interact with. This guide covers the item system, properties, effects, dynamic items, and creating custom items.
Overview
Items provide:
- Tools — Pickaxes, axes, shovels for resource gathering
- Weapons — Swords, bows, guns for combat
- Armour — Helmets, chestplates, boots for protection
- Consumables — Food, potions, ammunition
- Blocks — Placeable terrain and walls
- Fixtures — Workbenches, chests, decorations
- Special items — Keys, quest items, books
Item System Architecture
GameItem Abstract Class
All items extend the GameItem abstract class:
abstract class GameItem(val originalID: ItemID) : Comparable<GameItem>, Cloneable, TaggedProp {
// Identity
open var dynamicID: ItemID = originalID
open var originalName: String = "" // Translation key
var newName: String = "" // Custom name (renamed items)
var isCustomName = false
// Physical properties
abstract var baseMass: Double // Mass in kg
abstract var baseToolSize: Double? // Tool size/weight
abstract var inventoryCategory: String // "tool", "weapon", "armor", etc.
abstract val materialId: String // 4-letter material code
// Item type
abstract val canBeDynamic: Boolean // Can have unique instances?
var stackable: Boolean = true // Can stack in inventory?
val isConsumable: Boolean // Consumed on use?
get() = stackable && !canBeDynamic
// Durability
open var maxDurability: Int = 0 // Max durability (0 = none)
open var durability: Float = 0f // Current durability
// Equipment
open var equipPosition: Int = EquipPosition.NULL
// Appearance
var itemImage: TextureRegion? // Item sprite
var itemImagePixmap: Pixmap? // Pixmap data
open val itemImageGlow: TextureRegion? = null
open val itemImageEmissive: TextureRegion? = null
// Tags and data
var tags = HashSet<String>() // Item tags
var modifiers = HashSet<String>() // Dynamic modifiers
open val extra = Codex() // Custom module data
var itemProperties = ItemValue() // Item-specific values
// Effects (override these)
open fun effectWhileInPocket(actor: ActorWithBody, delta: Float) { }
open fun effectOnPickup(actor: ActorWithBody) { }
open fun effectWhileEquipped(actor: ActorWithBody, delta: Float) { }
open fun effectOnUnequip(actor: ActorWithBody) { }
open fun effectOnThrow(actor: ActorWithBody) { }
open fun startPrimaryUse(actor: ActorWithBody, delta: Float): Long = -1
open fun startSecondaryUse(actor: ActorWithBody, delta: Float): Long = -1
open fun endPrimaryUse(actor: ActorWithBody, delta: Float): Boolean = false
open fun endSecondaryUse(actor: ActorWithBody, delta: Float): Boolean = false
}
ItemCodex Singleton
All items are registered in the global ItemCodex:
object ItemCodex {
val itemCodex = ItemTable() // Static items
val dynamicItemInventory = ItemTable() // Dynamic items
val dynamicToStaticTable = HashMap<ItemID, ItemID>()
operator fun get(id: ItemID?): GameItem?
fun registerNewDynamicItem(dynamicID: ItemID, item: GameItem)
}
Access pattern:
val pickaxe = ItemCodex["basegame:1"] // Copper pickaxe
println("Mass: ${pickaxe?.mass} kg")
println("Tool size: ${pickaxe?.toolSize}")
Item Types
Static Items
Static items are shared instances defined in module files:
id;classname;tags
1;net.torvald.terrarum.modulebasegame.gameitems.PickaxeCopper;TOOL,PICK
Characteristics:
- Single shared instance
- Cannot be modified per-instance
- Stackable
- ID range:
item@module:id
Examples:
- Blocks (stone, dirt, wood)
- Consumables (food, potions)
- Ammunition (arrows, bullets)
Dynamic Items
Dynamic items are unique instances created at runtime:
// Create from static template
val baseSword = ItemCodex["basegame:sword_iron"]
val customSword = baseSword.copy()
// Customise
customSword.name = "§o§Excalibur§.§" // Formatted name
customSword.durability = customSword.maxDurability * 0.5f // Half durability
customSword.tags.add("LEGENDARY")
// Register with unique ID
val dynamicID = "dyn:${System.nanoTime()}"
ItemCodex.registerNewDynamicItem(dynamicID, customSword)
Characteristics:
- Unique instance per item
- Can be modified (durability, enchantments, names)
- NOT stackable
- ID range:
dyn:*
Examples:
- Tools (worn pickaxes)
- Weapons (enchanted swords)
- Armour (damaged chestplate)
- Named items (signed books)
Item Categories
Items have categories for organisation:
abstract var inventoryCategory: String
Standard categories:
"tool"— Pickaxes, axes, shovels"weapon"— Swords, bows, guns"armor"— Helmets, chestplates, boots, shields"block"— Placeable blocks"fixture"— Workbenches, chests, doors"consumable"— Food, potions, single-use items"material"— Raw materials, ingots, gems"misc"— Uncategorised items
Creating Custom Items
Step 1: Define Item Class
Create a Kotlin class extending GameItem:
package net.torvald.mymod.items
import net.torvald.terrarum.gameitems.GameItem
import net.torvald.terrarum.gameitems.ItemID
import net.torvald.terrarum.gameactors.ActorWithBody
import net.torvald.terrarum.gameactors.AVKey
class FireSword(originalID: ItemID) : GameItem(originalID) {
override var baseMass = 2.5
override var baseToolSize: Double? = 2.0
override val canBeDynamic = true
override var inventoryCategory = "weapon"
override val materialId = "IRON"
override var maxDurability = 1000
init {
originalName = "ITEM_FIRE_SWORD"
tags.add("WEAPON")
tags.add("SWORD")
tags.add("FIRE")
durability = maxDurability.toFloat()
equipPosition = EquipPosition.HAND_GRIP
}
override fun effectWhileEquipped(actor: ActorWithBody, delta: Float) {
// Give fire resistance while equipped
if (actor.actorValue.getAsBoolean("mymod:fire_sword.buffGiven") != true) {
actor.actorValue[AVKey.FIRE_RESISTANCE] =
(actor.actorValue.getAsDouble(AVKey.FIRE_RESISTANCE) ?: 0.0) + 50.0
actor.actorValue["mymod:fire_sword.buffGiven"] = true
}
}
override fun effectOnUnequip(actor: ActorWithBody) {
// Remove fire resistance when unequipped
if (actor.actorValue.getAsBoolean("mymod:fire_sword.buffGiven") == true) {
actor.actorValue[AVKey.FIRE_RESISTANCE] =
(actor.actorValue.getAsDouble(AVKey.FIRE_RESISTANCE) ?: 0.0) - 50.0
actor.actorValue.remove("mymod:fire_sword.buffGiven")
}
}
override fun startPrimaryUse(actor: ActorWithBody, delta: Float): Long {
// Attack with fire damage
val target = findTargetInRange(actor)
if (target != null) {
target.takeDamage(15.0) // Normal damage
target.setOnFire(5.0) // 5 seconds of fire
// Reduce durability
durability -= 1f
if (durability <= 0f) {
// Item breaks
return 1 // Remove from inventory
}
return 0 // Successfully used, don't remove
}
return -1 // Failed to use
}
}
Step 2: Register in CSV
Add to items/itemid.csv:
id;classname;tags
fire_sword;net.torvald.mymod.items.FireSword;WEAPON,SWORD,FIRE
Step 3: Add Translation
Add to locales/en/items.txt:
ITEM_FIRE_SWORD=Fire Sword
ITEM_FIRE_SWORD_DESC=A blazing blade that sets enemies aflame.
Step 4: Create Sprite
Add sprite to items/items.tga spritesheet at the appropriate index.
Step 5: Load in Module
In your EntryPoint:
override fun invoke() {
// Load item sprites
CommonResourcePool.addToLoadingList("mymod.items") {
ItemSheet(ModMgr.getGdxFile("mymod", "items/items.tga"))
}
CommonResourcePool.loadAll()
// Load items
ModMgr.GameItemLoader.invoke("mymod")
}
Item Properties
Mass and Scale
Items have mass that scales with size:
override var baseMass = 2.5 // Base mass at scale 1.0
// Apparent mass scales cubically
open var mass: Double
get() = baseMass * scale * scale * scale
Typical masses:
- Small items (arrows): 0.02 kg
- Tools (pickaxe): 2-5 kg
- Blocks (stone): 10-50 kg
- Heavy equipment: 20+ kg
Tool Size
Tools have a size/weight property:
override var baseToolSize: Double? = 2.0 // null for non-tools
// Scales cubically like mass
open var toolSize: Double?
get() = if (baseToolSize != null) baseToolSize!! * scale * scale * scale else null
Uses:
- Mining speed calculation
- Attack damage
- Stamina drain
- Tool tier comparison
Durability
Items can wear down with use:
override var maxDurability: Int = 1000
open var durability: Float = 1000f
Durability values:
0— Infinite durability (blocks)100-500— Fragile (glass tools)500-2000— Normal (iron tools)2000+— Durable (diamond tools)
Reducing durability:
override fun startPrimaryUse(actor: ActorWithBody, delta: Float): Long {
// Use tool
performAction()
// Reduce durability
durability -= 1f
if (durability <= 0f) {
// Item breaks
playBreakSound()
return 1 // Remove 1 from inventory
}
return 0 // Used successfully
}
Materials
Every item has a material:
override val materialId = "IRON" // 4-letter code
Material affects:
- Tool effectiveness vs. blocks
- Durability calculations
- Sound effects
- Value/rarity
- Thermal properties
Common materials:
WOOD— Wooden tools (tier 0)ROCK— Stone tools (tier 1)COPR— Copper tools (tier 2)IRON— Iron tools (tier 3)GOLD— Gold tools (tier 4)DIAM— Diamond tools (tier 5)
Equipment Positions
Items can be equipped to specific slots:
open var equipPosition: Int = EquipPosition.HAND_GRIP
Equipment positions:
EquipPosition.NULL— Cannot be equippedEquipPosition.HAND_GRIP— Main hand (tools, weapons)EquipPosition.HAND_GRIP_SECONDARY— Off-handEquipPosition.HEAD— HelmetEquipPosition.BODY— ChestplateEquipPosition.LEGS— LeggingsEquipPosition.FEET— Boots
Equipping items:
item equipTo actor // Infix notation
actor.equipItem(item)
Item Effects
Effect Hooks
Items can react to various events:
effectWhileInPocket
Called every frame while item is in inventory:
override fun effectWhileInPocket(actor: ActorWithBody, delta: Float) {
// Passive effects just from having item
if (actor.actorValue.getAsBoolean("mymod:amulet.protection") != true) {
actor.actorValue[AVKey.DEFENCEBUFF] += 10.0
actor.actorValue["mymod:amulet.protection"] = true
}
}
Uses:
- Passive buffs
- Curses
- Background effects
effectOnPickup
Called once when item is picked up:
override fun effectOnPickup(actor: ActorWithBody) {
// Immediate pickup effects
actor.heal(20.0)
playPickupSound()
}
Uses:
- Immediate healing
- Buffs
- Achievements/triggers
effectWhileEquipped
Called every frame while item is equipped:
override fun effectWhileEquipped(actor: ActorWithBody, delta: Float) {
// Active equipment effects
if (actor.actorValue.getAsBoolean("mymod:ring.speedGiven") != true) {
actor.actorValue[AVKey.SPEEDBUFF] += 2.0
actor.actorValue["mymod:ring.speedGiven"] = true
}
}
Uses:
- Stat boosts
- Continuous effects
- Visual effects
Important: Use a custom flag to apply effects only once:
if (actor.actorValue.getAsBoolean("mymod:item.buffGiven") != true) {
// Apply buff
actor.actorValue["mymod:item.buffGiven"] = true
}
effectOnUnequip
Called once when item is unequipped:
override fun effectOnUnequip(actor: ActorWithBody) {
// Remove equipment effects
if (actor.actorValue.getAsBoolean("mymod:ring.speedGiven") == true) {
actor.actorValue[AVKey.SPEEDBUFF] -= 2.0
actor.actorValue.remove("mymod:ring.speedGiven")
}
}
Uses:
- Remove buffs
- Clean up state
- Penalties
effectOnThrow
Called once when item is discarded:
override fun effectOnThrow(actor: ActorWithBody) {
// Throwing effects
if (hasTag("EXPLOSIVE")) {
createExplosion(actor.position)
}
}
Uses:
- Grenades
- Throwable potions
- Item-specific discard behaviour
Use Handlers
Items can respond to player input:
startPrimaryUse
Called while primary button (left mouse/RT) is held:
override fun startPrimaryUse(actor: ActorWithBody, delta: Float): Long {
// Perform action
val success = doAction(actor)
if (success) {
durability -= 1f
if (durability <= 0f) {
return 1 // Remove 1 from inventory (item consumed/broken)
}
return 0 // Successfully used, keep item
}
return -1 // Failed to use
}
Return values:
0or greater — Amount to remove from inventory (success)-1— Failed to use (no removal)
Uses:
- Mining blocks (pickaxe)
- Attacking (sword)
- Placing blocks
- Using consumables
startSecondaryUse
Called while secondary button (right mouse) is held:
override fun startSecondaryUse(actor: ActorWithBody, delta: Float): Long {
// Alternative action
if (canPerformSecondary(actor)) {
performSecondaryAction()
return 0
}
return -1
}
Note: Secondary use is less commonly used due to control scheme limitations.
endPrimaryUse / endSecondaryUse
Called when button is released:
override fun endPrimaryUse(actor: ActorWithBody, delta: Float): Boolean {
// Finish action (e.g., shoot charged bow)
if (chargeTime > 1.0f) {
shootArrow(actor, chargeTime)
chargeTime = 0f
return true
}
return false
}
Uses:
- Charged attacks
- Bow drawing
- Cleanup actions
Item Tags
Tags enable flexible categorisation:
tags.add("WEAPON")
tags.add("SWORD")
tags.add("LEGENDARY")
Common Tags
Item types:
TOOL— General toolsWEAPON— Combat itemsARMOR— Protection gearCONSUMABLE— Single-use itemsBLOCK— Placeable blocks
Tool subtypes:
PICK— PickaxesAXE— AxesSHOVEL— ShovelsHAMMER— Hammers
Weapon subtypes:
SWORD— SwordsBOW— BowsGUN— FirearmsMAGIC— Magic weapons
Materials:
WOOD— Wooden itemsSTONE— Stone itemsMETAL— Metal itemsGEM— Gem items
Special:
LEGENDARY— Rare/unique itemsQUEST— Quest itemsKEY— Key itemsCOMBUSTIBLE— Can be used as fuel
Usage:
// Check tags
if (item.hasTag("WEAPON")) {
println("This is a weapon")
}
// Query items by tag
val allWeapons = ItemCodex.itemCodex.values.filter { it.hasTag("WEAPON") }
val allLegendary = ItemCodex.itemCodex.values.filter { it.hasTag("LEGENDARY") }
Dynamic Item System
Creating Dynamic Items
Transform static items into unique instances:
// Get static template
val ironSword = ItemCodex["basegame:sword_iron"]
// Create dynamic copy
val enchantedSword = ironSword.copy()
// Customise
enchantedSword.name = "Sword of Flames"
enchantedSword.nameColour = Color.RED
enchantedSword.durability = enchantedSword.maxDurability * 0.75f
enchantedSword.modifiers.add("FIRE_DAMAGE")
enchantedSword.itemProperties["enchantment_level"] = 3
// Register
val dynamicID = "dyn:${System.nanoTime()}"
enchantedSword.dynamicID = dynamicID
ItemCodex.registerNewDynamicItem(dynamicID, enchantedSword)
Dynamic Item Lifecycle
- Creation — Copy from static template
- Customisation — Modify properties
- Registration — Add to
dynamicItemInventory - Serialisation — Save to disk
- Deserialisation — Reload from disk
- Reload — Re-sync properties after loading
Reload hook:
override fun reload() {
// Called after loading from save
// Re-sync derived values
if (durability > maxDurability) {
durability = maxDurability.toFloat()
}
}
Dynamic Item Storage
Dynamic items are stored separately:
object ItemCodex {
val itemCodex = ItemTable() // Static items
val dynamicItemInventory = ItemTable() // Dynamic items
val dynamicToStaticTable = HashMap<ItemID, ItemID>() // Dynamic → Static mapping
}
Lookup:
val item = ItemCodex["dyn:123456789"] // Checks dynamic inventory first
val originalID = ItemCodex.dynamicToStaticTable["dyn:123456789"] // "basegame:sword_iron"
Example Items
Simple Consumable
class HealthPotion(originalID: ItemID) : GameItem(originalID) {
override var baseMass = 0.2
override var baseToolSize: Double? = null
override val canBeDynamic = false
override var inventoryCategory = "consumable"
override val materialId = "GLAS"
init {
originalName = "ITEM_HEALTH_POTION"
tags.add("CONSUMABLE")
tags.add("POTION")
stackable = true
}
override fun startPrimaryUse(actor: ActorWithBody, delta: Float): Long {
actor.heal(50.0)
playDrinkSound()
return 1 // Consume one potion
}
}
Durable Tool
class DiamondPickaxe(originalID: ItemID) : GameItem(originalID) {
override var baseMass = 3.0
override var baseToolSize: Double? = 3.5
override val canBeDynamic = true
override var inventoryCategory = "tool"
override val materialId = "DIAM"
override var maxDurability = 5000
init {
originalName = "ITEM_PICKAXE_DIAMOND"
tags.add("TOOL")
tags.add("PICK")
durability = maxDurability.toFloat()
equipPosition = EquipPosition.HAND_GRIP
}
override fun startPrimaryUse(actor: ActorWithBody, delta: Float): Long {
// Mining handled by PickaxeCore
val result = PickaxeCore.startPrimaryUse(actor, delta, this, mouseX, mouseY)
if (result >= 0) {
// Successfully mined
durability -= 1f
if (durability <= 0f) {
playBreakSound()
return 1 // Tool breaks
}
}
return result
}
}
Equipment with Buffs
class IronHelmet(originalID: ItemID) : GameItem(originalID) {
override var baseMass = 1.5
override var baseToolSize: Double? = null
override val canBeDynamic = true
override var inventoryCategory = "armor"
override val materialId = "IRON"
override var maxDurability = 2000
init {
originalName = "ITEM_HELMET_IRON"
tags.add("ARMOR")
tags.add("HELMET")
durability = maxDurability.toFloat()
equipPosition = EquipPosition.HEAD
}
override fun effectWhileEquipped(actor: ActorWithBody, delta: Float) {
if (actor.actorValue.getAsBoolean("armor:iron_helmet.defenceGiven") != true) {
actor.actorValue[AVKey.DEFENCEBUFF] += 15.0
actor.actorValue["armor:iron_helmet.defenceGiven"] = true
}
}
override fun effectOnUnequip(actor: ActorWithBody) {
if (actor.actorValue.getAsBoolean("armor:iron_helmet.defenceGiven") == true) {
actor.actorValue[AVKey.DEFENCEBUFF] -= 15.0
actor.actorValue.remove("armor:iron_helmet.defenceGiven")
}
}
}
Best Practises
- Use appropriate mass values — Match real-world weights
- Set correct durability — Balance tool lifespans
- Tag comprehensively — Enable flexible queries
- Implement effect cleanup — Always remove buffs in effectOnUnequip
- Use flags for one-time effects — Prevent duplicate buff application
- Return correct values from use handlers — Respect the return value contract
- Handle edge cases — Check for null, zero durability, etc.
- Test dynamic items — Verify serialisation/deserialisation
- Balance tool sizes — Affects mining/attack speed
- Namespace IDs — Use
modulename:itemnameformat
Common Pitfalls
- Forgetting effectOnUnequip — Buffs persist after unequipping
- Not using flags — Buffs applied multiple times per frame
- Wrong return values — Items disappear or duplicate unexpectedly
- Infinite durability — Set maxDurability > 0 for tools
- Null tool size — Set baseToolSize for tools
- Missing sprites — Items render as missing texture
- Wrong equipment position — Items can't be equipped
- Not checking actor validity — Crashes when actor is null
- Forgetting stackable flag — Consumables don't stack
- Hardcoded IDs — Use string constants or config
See Also
- Modules-Codex-Systems#ItemCodex — ItemCodex reference
- Modules-Setup — Creating modules with items
- Blocks — Block items and placement
- Actors — Actor inventory system
- Fixtures — Fixture items