1
Items
minjaesong edited this page 2025-11-24 21:24:45 +09:00

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 equipped
  • EquipPosition.HAND_GRIP — Main hand (tools, weapons)
  • EquipPosition.HAND_GRIP_SECONDARY — Off-hand
  • EquipPosition.HEAD — Helmet
  • EquipPosition.BODY — Chestplate
  • EquipPosition.LEGS — Leggings
  • EquipPosition.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:

  • 0 or 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 tools
  • WEAPON — Combat items
  • ARMOR — Protection gear
  • CONSUMABLE — Single-use items
  • BLOCK — Placeable blocks

Tool subtypes:

  • PICK — Pickaxes
  • AXE — Axes
  • SHOVEL — Shovels
  • HAMMER — Hammers

Weapon subtypes:

  • SWORD — Swords
  • BOW — Bows
  • GUN — Firearms
  • MAGIC — Magic weapons

Materials:

  • WOOD — Wooden items
  • STONE — Stone items
  • METAL — Metal items
  • GEM — Gem items

Special:

  • LEGENDARY — Rare/unique items
  • QUEST — Quest items
  • KEY — Key items
  • COMBUSTIBLE — 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

  1. Creation — Copy from static template
  2. Customisation — Modify properties
  3. Registration — Add to dynamicItemInventory
  4. Serialisation — Save to disk
  5. Deserialisation — Reload from disk
  6. 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

  1. Use appropriate mass values — Match real-world weights
  2. Set correct durability — Balance tool lifespans
  3. Tag comprehensively — Enable flexible queries
  4. Implement effect cleanup — Always remove buffs in effectOnUnequip
  5. Use flags for one-time effects — Prevent duplicate buff application
  6. Return correct values from use handlers — Respect the return value contract
  7. Handle edge cases — Check for null, zero durability, etc.
  8. Test dynamic items — Verify serialisation/deserialisation
  9. Balance tool sizes — Affects mining/attack speed
  10. Namespace IDs — Use modulename:itemname format

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