mirror of
https://github.com/curioustorvald/Terrarum.git
synced 2026-03-07 12:21:52 +09:00
wiki update
748
Actor-Values-Reference.md
Normal file
748
Actor-Values-Reference.md
Normal file
@@ -0,0 +1,748 @@
|
||||
# Actor Values Reference
|
||||
|
||||
**Audience:** Module developers working with actor properties and behaviour.
|
||||
|
||||
Actor values (`actorValue`) are key-value pairs that define an actor's properties, stats, and state. This reference documents all standard actor value keys defined in the engine.
|
||||
|
||||
## Overview
|
||||
|
||||
Actor values provide:
|
||||
- **Stats** — Health, strength, speed, defence
|
||||
- **Physical properties** — Mass, height, luminosity
|
||||
- **Movement** — Jump power, flight, climbing
|
||||
- **Combat** — Tool size, reach, action timing
|
||||
- **Identity** — Name, race, species
|
||||
- **State** — Timers, counters, temporary flags
|
||||
- **Special** — Portal dictionary, ore dictionary, game mode
|
||||
|
||||
## Accessing Actor Values
|
||||
|
||||
```kotlin
|
||||
// Get value
|
||||
val speed = actor.actorValue.getAsDouble(AVKey.SPEED)
|
||||
val name = actor.actorValue.getAsString(AVKey.NAME)
|
||||
val health = actor.actorValue.getAsDouble(AVKey.HEALTH)
|
||||
|
||||
// Set value
|
||||
actor.actorValue[AVKey.SPEED] = 5.0
|
||||
actor.actorValue[AVKey.NAME] = "Hero"
|
||||
actor.actorValue[AVKey.HEALTH] = 100.0
|
||||
|
||||
// Check if exists
|
||||
if (actor.actorValue.containsKey(AVKey.MAGIC)) {
|
||||
// Actor has magic
|
||||
}
|
||||
|
||||
// Remove value
|
||||
actor.actorValue.remove(AVKey.__ACTION_TIMER)
|
||||
```
|
||||
|
||||
## Movement and Physics
|
||||
|
||||
### Speed
|
||||
|
||||
**Key:** `AVKey.SPEED` / `AVKey.SPEEDBUFF`
|
||||
|
||||
**Type:** Double
|
||||
|
||||
**Unit:** Pixels per frame
|
||||
|
||||
**Description:** Walking/running speed
|
||||
|
||||
**Typical values:**
|
||||
- Slow: 2-3 px/frame
|
||||
- Normal: 4-6 px/frame
|
||||
- Fast: 7-10 px/frame
|
||||
|
||||
```kotlin
|
||||
actor.actorValue[AVKey.SPEED] = 5.0 // Base speed
|
||||
actor.actorValue[AVKey.SPEEDBUFF] = 2.0 // Speed buff from equipment
|
||||
// Effective speed = 5.0 + 2.0 = 7.0 px/frame
|
||||
```
|
||||
|
||||
### Acceleration
|
||||
|
||||
**Key:** `AVKey.ACCEL` / `AVKey.ACCELBUFF`
|
||||
|
||||
**Type:** Double
|
||||
|
||||
**Unit:** Pixels per frame squared
|
||||
|
||||
**Description:** Acceleration of movement (running, flying, driving)
|
||||
|
||||
```kotlin
|
||||
actor.actorValue[AVKey.ACCEL] = 0.5 // Gradual acceleration
|
||||
```
|
||||
|
||||
### Jump Power
|
||||
|
||||
**Key:** `AVKey.JUMPPOWER` / `AVKey.JUMPPOWERBUFF`
|
||||
|
||||
**Type:** Double
|
||||
|
||||
**Unit:** Pixels per frame
|
||||
|
||||
**Description:** Initial velocity when jumping
|
||||
|
||||
**Typical values:**
|
||||
- Weak jump: 8-10 px/frame
|
||||
- Normal jump: 12-15 px/frame
|
||||
- High jump: 18-25 px/frame
|
||||
|
||||
```kotlin
|
||||
actor.actorValue[AVKey.JUMPPOWER] = 15.0
|
||||
```
|
||||
|
||||
### Vertical Stride
|
||||
|
||||
**Key:** `AVKey.VERTSTRIDE`
|
||||
|
||||
**Type:** Double
|
||||
|
||||
**Unit:** Pixels
|
||||
|
||||
**Description:** Maximum height of a stair the actor can climb
|
||||
|
||||
**Typical values:**
|
||||
- Small creatures: 8-12 pixels
|
||||
- Humanoids: 16-20 pixels
|
||||
- Large creatures: 24-32 pixels
|
||||
|
||||
```kotlin
|
||||
actor.actorValue[AVKey.VERTSTRIDE] = 18.0 // Can climb 18-pixel stairs
|
||||
```
|
||||
|
||||
### Air Jumping
|
||||
|
||||
**Key:** `AVKey.AIRJUMPPOINT` (max) / `AVKey.AIRJUMPCOUNT` (current)
|
||||
|
||||
**Type:** Int
|
||||
|
||||
**Description:** Number of air jumps allowed (double-jump, triple-jump, etc.)
|
||||
|
||||
**Note:** 0 is treated as 1 (one air jump allowed)
|
||||
|
||||
```kotlin
|
||||
actor.actorValue[AVKey.AIRJUMPPOINT] = 2 // Can double-jump
|
||||
actor.actorValue[AVKey.AIRJUMPCOUNT] = 0 // No jumps used yet
|
||||
|
||||
// When air jumping:
|
||||
val current = actor.actorValue.getAsInt(AVKey.AIRJUMPCOUNT) ?: 0
|
||||
val max = actor.actorValue.getAsInt(AVKey.AIRJUMPPOINT) ?: 0
|
||||
if (current < max) {
|
||||
performAirJump()
|
||||
actor.actorValue[AVKey.AIRJUMPCOUNT] = current + 1
|
||||
}
|
||||
|
||||
// Reset on landing:
|
||||
actor.actorValue[AVKey.AIRJUMPCOUNT] = 0
|
||||
```
|
||||
|
||||
### Flight
|
||||
|
||||
**Key:** `AVKey.FLIGHTPOINT` (max) / `AVKey.FLIGHTTIMER` (current)
|
||||
|
||||
**Type:** Double
|
||||
|
||||
**Unit:** Seconds
|
||||
|
||||
**Description:** How long the actor can fly continuously
|
||||
|
||||
```kotlin
|
||||
actor.actorValue[AVKey.FLIGHTPOINT] = 10.0 // Can fly for 10 seconds
|
||||
actor.actorValue[AVKey.FLIGHTTIMER] = 0.0 // Not flying yet
|
||||
|
||||
// During flight:
|
||||
val timer = actor.actorValue.getAsDouble(AVKey.FLIGHTTIMER) ?: 0.0
|
||||
actor.actorValue[AVKey.FLIGHTTIMER] = timer + delta
|
||||
|
||||
// Check if out of flight time:
|
||||
if (timer >= actor.actorValue.getAsDouble(AVKey.FLIGHTPOINT) ?: 0.0) {
|
||||
stopFlying()
|
||||
}
|
||||
```
|
||||
|
||||
### Friction Multiplier
|
||||
|
||||
**Key:** `AVKey.FRICTIONMULT`
|
||||
|
||||
**Type:** Double
|
||||
|
||||
**Description:** Friction multiplier (only effective when noclip=true, e.g., camera actors)
|
||||
|
||||
**Not meant for living creatures**
|
||||
|
||||
```kotlin
|
||||
actor.actorValue[AVKey.FRICTIONMULT] = 0.8 // Less friction
|
||||
```
|
||||
|
||||
### Drag Coefficient
|
||||
|
||||
**Key:** `AVKey.DRAGCOEFF`
|
||||
|
||||
**Type:** Double
|
||||
|
||||
**Description:** Air/water drag resistance
|
||||
|
||||
```kotlin
|
||||
actor.actorValue[AVKey.DRAGCOEFF] = 0.5
|
||||
```
|
||||
|
||||
### Fall Dampen Multiplier
|
||||
|
||||
**Key:** `AVKey.FALLDAMPENMULT`
|
||||
|
||||
**Type:** Double
|
||||
|
||||
**Description:** Multiplier for fall damage reduction
|
||||
|
||||
```kotlin
|
||||
actor.actorValue[AVKey.FALLDAMPENMULT] = 0.5 // 50% fall damage reduction
|
||||
```
|
||||
|
||||
## Physical Properties
|
||||
|
||||
### Base Mass
|
||||
|
||||
**Key:** `AVKey.BASEMASS`
|
||||
|
||||
**Type:** Double
|
||||
|
||||
**Unit:** Kilograms
|
||||
|
||||
**Description:** Actor's body mass
|
||||
|
||||
**Typical values:**
|
||||
- Small animals: 5-20 kg
|
||||
- Humanoids: 50-100 kg
|
||||
- Large creatures: 150-500 kg
|
||||
|
||||
```kotlin
|
||||
actor.actorValue[AVKey.BASEMASS] = 70.0 // 70 kg human
|
||||
```
|
||||
|
||||
### Base Height
|
||||
|
||||
**Key:** `AVKey.BASEHEIGHT`
|
||||
|
||||
**Type:** Double
|
||||
|
||||
**Unit:** Pixels
|
||||
|
||||
**Description:** Actor's standing height
|
||||
|
||||
```kotlin
|
||||
actor.actorValue[AVKey.BASEHEIGHT] = 48.0 // 48 pixels tall
|
||||
```
|
||||
|
||||
### Scale
|
||||
|
||||
**Key:** `AVKey.SCALE` / `AVKey.SCALEBUFF`
|
||||
|
||||
**Type:** Double
|
||||
|
||||
**Description:** Size scale multiplier (aka PHYSIQUE)
|
||||
|
||||
```kotlin
|
||||
actor.actorValue[AVKey.SCALE] = 1.0 // Normal size
|
||||
actor.actorValue[AVKey.SCALE] = 1.5 // 150% size (larger, heavier)
|
||||
```
|
||||
|
||||
## Luminosity and Opacity
|
||||
|
||||
### Luminosity
|
||||
|
||||
**Keys:**
|
||||
- `AVKey.LUMR` — Red channel
|
||||
- `AVKey.LUMG` — Green channel
|
||||
- `AVKey.LUMB` — Blue channel
|
||||
- `AVKey.LUMA` — UV channel
|
||||
|
||||
**Type:** Double
|
||||
|
||||
**Unit:** 0.0-1.0
|
||||
|
||||
**Description:** Light emitted by the actor
|
||||
|
||||
```kotlin
|
||||
// Glowing actor (orange light)
|
||||
actor.actorValue[AVKey.LUMR] = 0.8
|
||||
actor.actorValue[AVKey.LUMG] = 0.4
|
||||
actor.actorValue[AVKey.LUMB] = 0.1
|
||||
actor.actorValue[AVKey.LUMA] = 0.0
|
||||
```
|
||||
|
||||
### Opacity
|
||||
|
||||
**Keys:**
|
||||
- `AVKey.OPAR` — Red channel opacity
|
||||
- `AVKey.OPAG` — Green channel opacity
|
||||
- `AVKey.OPAB` — Blue channel opacity
|
||||
- `AVKey.OPAA` — UV channel opacity
|
||||
|
||||
**Type:** Double
|
||||
|
||||
**Unit:** 0.0-1.0
|
||||
|
||||
**Description:** Light blocked by the actor
|
||||
|
||||
```kotlin
|
||||
// Semi-transparent actor
|
||||
actor.actorValue[AVKey.OPAR] = 0.5
|
||||
actor.actorValue[AVKey.OPAG] = 0.5
|
||||
actor.actorValue[AVKey.OPAB] = 0.5
|
||||
actor.actorValue[AVKey.OPAA] = 1.0
|
||||
```
|
||||
|
||||
## Combat and Actions
|
||||
|
||||
### Strength
|
||||
|
||||
**Key:** `AVKey.STRENGTH` / `AVKey.STRENGTHBUFF`
|
||||
|
||||
**Type:** Int
|
||||
|
||||
**Default:** 1000
|
||||
|
||||
**Description:** Physical strength
|
||||
|
||||
```kotlin
|
||||
actor.actorValue[AVKey.STRENGTH] = 1000 // Base strength
|
||||
actor.actorValue[AVKey.STRENGTHBUFF] = 200 // Buff from equipment
|
||||
// Effective strength = 1200
|
||||
```
|
||||
|
||||
### Defence
|
||||
|
||||
**Key:** `AVKey.DEFENCE` / `AVKey.DEFENCEBUFF`
|
||||
|
||||
**Type:** Double
|
||||
|
||||
**Unit:** TBA
|
||||
|
||||
**Description:** Base defence point of the species
|
||||
|
||||
```kotlin
|
||||
actor.actorValue[AVKey.DEFENCE] = 50.0
|
||||
actor.actorValue[AVKey.DEFENCEBUFF] = 10.0
|
||||
```
|
||||
|
||||
### Armour Defence
|
||||
|
||||
**Key:** `AVKey.ARMOURDEFENCE` / `AVKey.ARMOURDEFENCEBUFF`
|
||||
|
||||
**Type:** Double
|
||||
|
||||
**Unit:** TBA
|
||||
|
||||
**Description:** Defence from equipped armour
|
||||
|
||||
```kotlin
|
||||
actor.actorValue[AVKey.ARMOURDEFENCE] = 75.0 // From equipped armour
|
||||
```
|
||||
|
||||
### Reach
|
||||
|
||||
**Key:** `AVKey.REACH` / `AVKey.REACHBUFF`
|
||||
|
||||
**Type:** Double
|
||||
|
||||
**Unit:** Pixels
|
||||
|
||||
**Description:** Hand reach distance (affects gameplay as player)
|
||||
|
||||
**Typical values:**
|
||||
- Short reach: 32-48 pixels
|
||||
- Normal reach: 64-80 pixels
|
||||
- Long reach: 96-128 pixels
|
||||
|
||||
```kotlin
|
||||
actor.actorValue[AVKey.REACH] = 72.0 // Can interact 72 pixels away
|
||||
```
|
||||
|
||||
### Tool Size
|
||||
|
||||
**Key:** `AVKey.TOOLSIZE`
|
||||
|
||||
**Type:** Double
|
||||
|
||||
**Unit:** Kilograms
|
||||
|
||||
**Description:** Size/weight of equipped tool
|
||||
|
||||
Affects:
|
||||
- Attack strength
|
||||
- Mining speed
|
||||
- Stamina drain
|
||||
|
||||
```kotlin
|
||||
actor.actorValue[AVKey.TOOLSIZE] = 2.5 // 2.5 kg pickaxe
|
||||
```
|
||||
|
||||
### Action Interval
|
||||
|
||||
**Key:** `AVKey.ACTION_INTERVAL`
|
||||
|
||||
**Type:** Double
|
||||
|
||||
**Unit:** Seconds
|
||||
|
||||
**Description:** Time between actions (attack speed, mining speed)
|
||||
|
||||
```kotlin
|
||||
actor.actorValue[AVKey.ACTION_INTERVAL] = 0.5 // 2 actions per second
|
||||
```
|
||||
|
||||
### Action Timer
|
||||
|
||||
**Key:** `AVKey.__ACTION_TIMER`
|
||||
|
||||
**Type:** Double
|
||||
|
||||
**Unit:** Milliseconds
|
||||
|
||||
**Description:** How long action button has been held, or NPC wait time
|
||||
|
||||
**Internal state variable (prefixed with `__`)**
|
||||
|
||||
```kotlin
|
||||
// Track button hold time
|
||||
val timer = actor.actorValue.getAsDouble(AVKey.__ACTION_TIMER) ?: 0.0
|
||||
actor.actorValue[AVKey.__ACTION_TIMER] = timer + delta * 1000
|
||||
|
||||
// Reset on release
|
||||
actor.actorValue.remove(AVKey.__ACTION_TIMER)
|
||||
```
|
||||
|
||||
### Encumbrance
|
||||
|
||||
**Key:** `AVKey.ENCUMBRANCE`
|
||||
|
||||
**Type:** Double
|
||||
|
||||
**Description:** Weight penalty from inventory
|
||||
|
||||
```kotlin
|
||||
val totalWeight = inventory.getTotalWeight()
|
||||
actor.actorValue[AVKey.ENCUMBRANCE] = totalWeight
|
||||
```
|
||||
|
||||
## Health and Magic
|
||||
|
||||
### Health
|
||||
|
||||
**Key:** `AVKey.HEALTH`
|
||||
|
||||
**Type:** Double
|
||||
|
||||
**Description:** Current health points
|
||||
|
||||
```kotlin
|
||||
actor.actorValue[AVKey.HEALTH] = 100.0
|
||||
```
|
||||
|
||||
### Magic
|
||||
|
||||
**Key:** `AVKey.MAGIC`
|
||||
|
||||
**Type:** Double
|
||||
|
||||
**Description:** Current magic/mana points
|
||||
|
||||
```kotlin
|
||||
actor.actorValue[AVKey.MAGIC] = 50.0
|
||||
```
|
||||
|
||||
### Magic Regeneration Rate
|
||||
|
||||
**Key:** `AVKey.MAGICREGENRATE` / `AVKey.MAGICREGENRATEBUFF`
|
||||
|
||||
**Type:** Double
|
||||
|
||||
**Unit:** Points per second
|
||||
|
||||
**Description:** Magic regeneration rate
|
||||
|
||||
```kotlin
|
||||
actor.actorValue[AVKey.MAGICREGENRATE] = 5.0 // Regenerate 5 magic/sec
|
||||
```
|
||||
|
||||
## Identity
|
||||
|
||||
### Name
|
||||
|
||||
**Key:** `AVKey.NAME`
|
||||
|
||||
**Type:** String
|
||||
|
||||
**Description:** Actor's individual name
|
||||
|
||||
```kotlin
|
||||
actor.actorValue[AVKey.NAME] = "Jarppi"
|
||||
```
|
||||
|
||||
### Race Name
|
||||
|
||||
**Key:** `AVKey.RACENAME` (singular) / `AVKey.RACENAMEPLURAL` (plural)
|
||||
|
||||
**Type:** String
|
||||
|
||||
**Description:** Species/race name
|
||||
|
||||
```kotlin
|
||||
actor.actorValue[AVKey.RACENAME] = "Duudsoni"
|
||||
actor.actorValue[AVKey.RACENAMEPLURAL] = "Duudsonit"
|
||||
```
|
||||
|
||||
### Intelligent
|
||||
|
||||
**Key:** `AVKey.INTELLIGENT`
|
||||
|
||||
**Type:** Boolean
|
||||
|
||||
**Description:** Whether the player can talk with this actor
|
||||
|
||||
```kotlin
|
||||
actor.actorValue[AVKey.INTELLIGENT] = true // Can be talked to
|
||||
```
|
||||
|
||||
### UUID
|
||||
|
||||
**Key:** `AVKey.UUID`
|
||||
|
||||
**Type:** String
|
||||
|
||||
**Description:** Unique identifier (for fixtures and special actors)
|
||||
|
||||
```kotlin
|
||||
actor.actorValue[AVKey.UUID] = "550e8400-e29b-41d4-a716-446655440000"
|
||||
```
|
||||
|
||||
## Player-Specific
|
||||
|
||||
### Game Mode
|
||||
|
||||
**Key:** `AVKey.GAMEMODE`
|
||||
|
||||
**Type:** String
|
||||
|
||||
**Description:** Current game mode (only for IngamePlayers)
|
||||
|
||||
**Reserved values:**
|
||||
- `"survival"` — Survival mode (future use)
|
||||
- `""` (empty) — Creative mode
|
||||
|
||||
```kotlin
|
||||
actor.actorValue[AVKey.GAMEMODE] = "survival"
|
||||
```
|
||||
|
||||
### Quick Slot Selection
|
||||
|
||||
**Key:** `AVKey.__PLAYER_QUICKSLOTSEL`
|
||||
|
||||
**Type:** Int
|
||||
|
||||
**Description:** Currently selected quick slot index
|
||||
|
||||
**Internal state variable**
|
||||
|
||||
```kotlin
|
||||
actor.actorValue[AVKey.__PLAYER_QUICKSLOTSEL] = 3 // Slot 3 selected
|
||||
```
|
||||
|
||||
### Wire Cutter Selection
|
||||
|
||||
**Key:** `AVKey.__PLAYER_WIRECUTTERSEL`
|
||||
|
||||
**Type:** Int
|
||||
|
||||
**Description:** Wire cutter mode selection
|
||||
|
||||
**Internal state variable**
|
||||
|
||||
```kotlin
|
||||
actor.actorValue[AVKey.__PLAYER_WIRECUTTERSEL] = 1
|
||||
```
|
||||
|
||||
## Historical Data
|
||||
|
||||
### Born Time
|
||||
|
||||
**Key:** `AVKey.__HISTORICAL_BORNTIME`
|
||||
|
||||
**Type:** Long
|
||||
|
||||
**Unit:** TIME_T (world time seconds)
|
||||
|
||||
**Description:** When the actor was spawned
|
||||
|
||||
```kotlin
|
||||
actor.actorValue[AVKey.__HISTORICAL_BORNTIME] = world.worldTime.TIME_T
|
||||
```
|
||||
|
||||
### Dead Time
|
||||
|
||||
**Key:** `AVKey.__HISTORICAL_DEADTIME`
|
||||
|
||||
**Type:** Long
|
||||
|
||||
**Unit:** TIME_T (world time seconds)
|
||||
|
||||
**Description:** When the actor died (-1 if alive)
|
||||
|
||||
```kotlin
|
||||
actor.actorValue[AVKey.__HISTORICAL_DEADTIME] = -1L // Alive
|
||||
// On death:
|
||||
actor.actorValue[AVKey.__HISTORICAL_DEADTIME] = world.worldTime.TIME_T
|
||||
```
|
||||
|
||||
## Special Dictionaries
|
||||
|
||||
### World Portal Dictionary
|
||||
|
||||
**Key:** `AVKey.WORLD_PORTAL_DICT`
|
||||
|
||||
**Type:** String
|
||||
|
||||
**Format:** Comma-separated UUIDs (Ascii85-encoded, big endian)
|
||||
|
||||
**Description:** List of discovered world portals
|
||||
|
||||
```kotlin
|
||||
// Example value:
|
||||
"SIxM+kGlrjZgLx5Zeqz7,;:UIZ5Q=2WT35SgKpOp.,vvf'fNW3G<ROimy(Y;E<,-mdtr5|^RGOqr0x*T*lC,YABr1oQwErKG)pGC'gUG"
|
||||
|
||||
// Add portal:
|
||||
val currentDict = actor.actorValue.getAsString(AVKey.WORLD_PORTAL_DICT) ?: ""
|
||||
val newPortal = portalUUID.toAscii85()
|
||||
actor.actorValue[AVKey.WORLD_PORTAL_DICT] = if (currentDict.isEmpty()) {
|
||||
newPortal
|
||||
} else {
|
||||
"$currentDict,$newPortal"
|
||||
}
|
||||
```
|
||||
|
||||
### Ore Dictionary
|
||||
|
||||
**Key:** `AVKey.ORE_DICT`
|
||||
|
||||
**Type:** String
|
||||
|
||||
**Format:** Comma-separated ItemIDs
|
||||
|
||||
**Description:** Ores the player has discovered
|
||||
|
||||
**Note:** Uses item IDs (`item@basegame:128`) not ore IDs (`ores@basegame:1`)
|
||||
|
||||
```kotlin
|
||||
// Add discovered ore:
|
||||
val oreDict = actor.actorValue.getAsString(AVKey.ORE_DICT) ?: ""
|
||||
val newOre = "item@basegame:128" // Copper ore item
|
||||
if (!oreDict.contains(newOre)) {
|
||||
actor.actorValue[AVKey.ORE_DICT] = if (oreDict.isEmpty()) {
|
||||
newOre
|
||||
} else {
|
||||
"$oreDict,$newOre"
|
||||
}
|
||||
}
|
||||
|
||||
// Check if ore discovered:
|
||||
fun hasDiscoveredOre(actor: ActorWithBody, oreItemID: ItemID): Boolean {
|
||||
val oreDict = actor.actorValue.getAsString(AVKey.ORE_DICT) ?: ""
|
||||
return oreDict.split(',').contains(oreItemID)
|
||||
}
|
||||
```
|
||||
|
||||
## Bare-Hand Actions
|
||||
|
||||
### Bare-Hand Minimum Height
|
||||
|
||||
**Key:** `AVKey.BAREHAND_MINHEIGHT`
|
||||
|
||||
**Type:** Double
|
||||
|
||||
**Description:** Minimum height to enable bare-hand actions
|
||||
|
||||
```kotlin
|
||||
actor.actorValue[AVKey.BAREHAND_MINHEIGHT] = 16.0
|
||||
```
|
||||
|
||||
### Bare-Hand Base Digging Size
|
||||
|
||||
**Key:** `AVKey.BAREHAND_BASE_DIGSIZE`
|
||||
|
||||
**Type:** Double
|
||||
|
||||
**Description:** Base digging size with bare hands
|
||||
|
||||
```kotlin
|
||||
actor.actorValue[AVKey.BAREHAND_BASE_DIGSIZE] = 1.0 // 1x1 block
|
||||
```
|
||||
|
||||
## Buff System
|
||||
|
||||
Many keys have corresponding `*BUFF` keys for temporary bonuses:
|
||||
|
||||
```kotlin
|
||||
// Base value + buff = effective value
|
||||
val baseSpeed = actor.actorValue.getAsDouble(AVKey.SPEED) ?: 0.0
|
||||
val speedBuff = actor.actorValue.getAsDouble(AVKey.SPEEDBUFF) ?: 0.0
|
||||
val effectiveSpeed = baseSpeed + speedBuff
|
||||
|
||||
// Apply temporary speed boost:
|
||||
actor.actorValue[AVKey.SPEEDBUFF] = 3.0 // +3 px/frame
|
||||
|
||||
// Remove buff:
|
||||
actor.actorValue.remove(AVKey.SPEEDBUFF)
|
||||
```
|
||||
|
||||
**Buffable keys:**
|
||||
- `SPEED` / `SPEEDBUFF`
|
||||
- `ACCEL` / `ACCELBUFF`
|
||||
- `JUMPPOWER` / `JUMPPOWERBUFF`
|
||||
- `SCALE` / `SCALEBUFF`
|
||||
- `STRENGTH` / `STRENGTHBUFF`
|
||||
- `DEFENCE` / `DEFENCEBUFF`
|
||||
- `REACH` / `REACHBUFF`
|
||||
- `ARMOURDEFENCE` / `ARMOURDEFENCEBUFF`
|
||||
- `MAGICREGENRATE` / `MAGICREGENRATEBUFF`
|
||||
|
||||
## Naming Conventions
|
||||
|
||||
- **Regular keys** — Lowercase, descriptive name
|
||||
- **Buff keys** — Base key + `"buff"` suffix
|
||||
- **Internal state** — Prefixed with `__` (double underscore)
|
||||
- **Historical data** — Prefixed with `__HISTORICAL_`
|
||||
- **Player-specific** — Prefixed with `__PLAYER_`
|
||||
|
||||
## Best Practises
|
||||
|
||||
1. **Use AVKey constants** — Never hardcode key strings
|
||||
2. **Check type before casting** — Use `getAsDouble()`, `getAsString()`, etc.
|
||||
3. **Handle null values** — Provide sensible defaults
|
||||
4. **Apply buffs additively** — Base + Buff = Effective
|
||||
5. **Clean up temporary values** — Remove timer/counter keys when done
|
||||
6. **Document custom keys** — Add comments for module-specific keys
|
||||
7. **Namespace custom keys** — Use `"mymod:customkey"` format
|
||||
8. **Serialise carefully** — Not all keys should be saved
|
||||
9. **Use internal prefix** — Mark transient state with `__`
|
||||
10. **Validate ranges** — Clamp values to reasonable bounds
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- **Forgetting to check null** — `getAsDouble()` returns null if missing
|
||||
- **Not removing buffs** — Buffs persist forever unless removed
|
||||
- **Hardcoding key strings** — Use AVKey constants
|
||||
- **Wrong type casting** — Check types before accessing
|
||||
- **Modifying base stats directly** — Use buffs for temporary changes
|
||||
- **Not serialising important data** — Historical data may be lost
|
||||
- **Conflicting custom keys** — Use unique namespaced keys
|
||||
- **Assuming defaults** — Not all actors have all keys
|
||||
- **Not updating timers** — Increment/decrement counters properly
|
||||
- **Overwriting instead of adding** — Buffs should add, not replace
|
||||
|
||||
## See Also
|
||||
|
||||
- [[Actors]] — Actor system overview
|
||||
- [[Items#Item-Effects]] — Item effects on actor values
|
||||
- [[Modules-Setup]] — Creating custom actor values
|
||||
453
Actors.md
Normal file
453
Actors.md
Normal file
@@ -0,0 +1,453 @@
|
||||
# Actors
|
||||
|
||||
Actors are the fundamental interactive entities in the Terrarum engine. Everything that can be updated and rendered in the game world—players, NPCs, creatures, particles, projectiles, furniture, and more—is an Actor.
|
||||
|
||||
## Overview
|
||||
|
||||
The Actor system is built on a hierarchy of classes providing increasing levels of functionality:
|
||||
|
||||
1. **Actor** — Base class for all entities
|
||||
2. **ActorWithBody** — Actors with position, physics, and sprites
|
||||
3. **Specialised Actor Types** — Players, NPCs, fixtures, and more
|
||||
|
||||
## Actor Base Class
|
||||
|
||||
All actors inherit from `net.torvald.terrarum.gameactors.Actor`.
|
||||
|
||||
### Key Properties
|
||||
|
||||
#### Reference ID
|
||||
Every actor has a unique **Reference ID** (ActorID), an integer between 16,777,216 and 0x7FFF_FFFF. This ID is used to track and retrieve actors throughout their lifecycle.
|
||||
|
||||
```kotlin
|
||||
val referenceID: ActorID
|
||||
```
|
||||
|
||||
The player actor always has a special reference ID: `Terrarum.PLAYER_REF_ID` (0x91A7E2).
|
||||
|
||||
#### Render Order
|
||||
Determines which visual layer the actor is drawn on. From back to front:
|
||||
|
||||
- **FAR_BEHIND** — Wires and conduits
|
||||
- **BEHIND** — Tapestries, some particles (obstructed by terrain)
|
||||
- **MIDDLE** — Standard actors (players, NPCs, creatures)
|
||||
- **MIDTOP** — Projectiles, thrown items
|
||||
- **FRONT** — Front walls (blocks that obstruct actors)
|
||||
- **OVERLAY** — Screen overlays, not affected by lightmap
|
||||
|
||||
```kotlin
|
||||
var renderOrder: RenderOrder
|
||||
```
|
||||
|
||||
### Actor Lifecycle
|
||||
|
||||
#### Update Cycle
|
||||
|
||||
Actors implement the `updateImpl(delta: Float)` method, which is called every frame:
|
||||
|
||||
```kotlin
|
||||
abstract fun updateImpl(delta: Float)
|
||||
```
|
||||
|
||||
The base `update()` method automatically handles despawn logic before calling your implementation.
|
||||
|
||||
#### Despawning
|
||||
|
||||
Actors can be flagged for removal from the game world:
|
||||
|
||||
```kotlin
|
||||
var flagDespawn: Boolean
|
||||
val canBeDespawned: Boolean // override to prevent despawning
|
||||
```
|
||||
|
||||
When an actor is despawned, the `despawn()` method is called, which:
|
||||
1. Stops any music tracks associated with the actor
|
||||
2. Calls the despawn hook
|
||||
3. Marks the actor as despawned
|
||||
|
||||
```kotlin
|
||||
@Transient var despawnHook: (Actor) -> Unit = {}
|
||||
```
|
||||
|
||||
#### Serialisation and Reload
|
||||
|
||||
When saving the game, actors are serialised. Fields marked with `@Transient` are not saved and must be reconstructed when loading.
|
||||
|
||||
The `reload()` method is called after deserialisation:
|
||||
|
||||
```kotlin
|
||||
open fun reload() {
|
||||
actorValue.actor = this
|
||||
// Reconstruct transient fields here
|
||||
}
|
||||
```
|
||||
|
||||
### ActorValue System
|
||||
|
||||
Actors have a flexible property system called **ActorValue**, similar to Elder Scrolls' actor value system. It stores various attributes and stats:
|
||||
|
||||
```kotlin
|
||||
var actorValue: ActorValue
|
||||
```
|
||||
|
||||
ActorValue stores key-value pairs and notifies the actor when values change via the event handler:
|
||||
|
||||
```kotlin
|
||||
abstract fun onActorValueChange(key: String, value: Any?)
|
||||
```
|
||||
|
||||
Common ActorValue keys are defined in `AVKey` (e.g., `AVKey.SCALE`, `AVKey.BASEMASS`, `AVKey.NAME`).
|
||||
|
||||
### Audio
|
||||
|
||||
Actors can play music and sound effects with spatial positioning:
|
||||
|
||||
```kotlin
|
||||
// Music tracks associated with this actor
|
||||
val musicTracks: HashMap<MusicContainer, TerrarumAudioMixerTrack>
|
||||
|
||||
// Start playing music
|
||||
fun startMusic(music: MusicContainer)
|
||||
```
|
||||
|
||||
By default, music stops when the actor despawns. Override `stopMusicOnDespawn` to change this behaviour:
|
||||
|
||||
```kotlin
|
||||
open val stopMusicOnDespawn: Boolean = true
|
||||
```
|
||||
|
||||
## ActorWithBody
|
||||
|
||||
`ActorWithBody` extends `Actor` with physical presence, including position, collision, physics, and sprite rendering.
|
||||
|
||||
### Position and Hitbox
|
||||
|
||||
ActorWithBody has a precise pixel-based position system:
|
||||
|
||||
```kotlin
|
||||
val hitbox: Hitbox // position and dimensions in pixels
|
||||
```
|
||||
|
||||
The hitbox defines the actor's:
|
||||
- **Position** (top-left corner in pixels)
|
||||
- **Dimensions** (width and height in pixels)
|
||||
- **Collision area**
|
||||
|
||||
#### Setting Position and Size
|
||||
|
||||
Use these methods to configure the actor's spatial properties:
|
||||
|
||||
```kotlin
|
||||
fun setHitboxDimension(width: Int, height: Int, translateX: Int, translateY: Int)
|
||||
fun setPosition(x: Double, y: Double)
|
||||
fun translatePosition(deltaX: Double, deltaY: Double)
|
||||
```
|
||||
|
||||
#### Tilewise Hitboxes
|
||||
|
||||
For tile-based calculations, ActorWithBody provides tilewise hitbox conversions:
|
||||
|
||||
```kotlin
|
||||
// Half-integer tilewise hitbox (for physics)
|
||||
val hIntTilewiseHitbox: Hitbox
|
||||
|
||||
// Integer tilewise hitbox (for block occupation checks)
|
||||
val intTilewiseHitbox: Hitbox
|
||||
```
|
||||
|
||||
When iterating over tiles occupied by an actor:
|
||||
|
||||
```kotlin
|
||||
for (x in actor.intTilewiseHitbox.startX.toInt()..actor.intTilewiseHitbox.endX.toInt()) {
|
||||
for (y in actor.intTilewiseHitbox.startY.toInt()..actor.intTilewiseHitbox.endY.toInt()) {
|
||||
// Process tile at (x, y)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Physics Properties
|
||||
|
||||
ActorWithBody contains a `PhysProperties` object defining physical characteristics:
|
||||
|
||||
```kotlin
|
||||
var physProp: PhysProperties
|
||||
```
|
||||
|
||||
PhysProperties includes:
|
||||
- **immobile** — Whether the actor can move
|
||||
- **usePhysics** — Whether physics simulation applies
|
||||
- **movementType** — Determines physics behaviour (see PhysicalTypes)
|
||||
|
||||
The **physics simulation** applies acceleration, velocity, and collision. Use the following constants:
|
||||
|
||||
- **METER** — 1 metre = 25 pixels
|
||||
- **PHYS_TIME_FRAME** — Time step for physics simulation
|
||||
|
||||
### Velocity
|
||||
|
||||
ActorWithBody has two velocity systems:
|
||||
|
||||
#### External Velocity
|
||||
For physics-driven movement (gravity, collisions, forces):
|
||||
|
||||
```kotlin
|
||||
internal val externalV: Vector2
|
||||
```
|
||||
|
||||
#### Controller Velocity
|
||||
For player/AI-controlled movement:
|
||||
|
||||
```kotlin
|
||||
var controllerV: Vector2? // non-null only for Controllable actors
|
||||
```
|
||||
|
||||
Velocity units are **pixels per frame** (at 60 FPS). The engine automatically scales velocity for different frame rates using:
|
||||
|
||||
```kotlin
|
||||
val adjustedVelocity = velocity * (Terrarum.PHYS_REF_FPS * delta)
|
||||
```
|
||||
|
||||
### Mass and Scale
|
||||
|
||||
ActorWithBody supports dynamic mass and size:
|
||||
|
||||
```kotlin
|
||||
val mass: Double // Calculated from base mass × scale³
|
||||
val scale: Double // Apparent scale (base scale × scale buffs)
|
||||
```
|
||||
|
||||
Mass is stored in **kilograms**. Default mass is defined in the physics properties.
|
||||
|
||||
### Sprites and Animation
|
||||
|
||||
Actors can have multiple sprite layers:
|
||||
|
||||
```kotlin
|
||||
@Transient var sprite: SpriteAnimation? // Main sprite
|
||||
@Transient var spriteGlow: SpriteAnimation? // Glow layer
|
||||
@Transient var spriteEmissive: SpriteAnimation? // Emissive layer
|
||||
```
|
||||
|
||||
Sprites must be reconstructed in the `reload()` method as they are transient.
|
||||
|
||||
The `drawMode` property controls sprite blending:
|
||||
|
||||
```kotlin
|
||||
var drawMode: BlendMode = BlendMode.NORMAL
|
||||
```
|
||||
|
||||
### Lighting
|
||||
|
||||
ActorWithBody actors can emit and block light:
|
||||
|
||||
```kotlin
|
||||
open var lightBoxList: ArrayList<Lightbox> // Light emission areas
|
||||
open var shadeBoxList: ArrayList<Lightbox> // Light blocking areas
|
||||
```
|
||||
|
||||
Lightboxes are relative to the actor's position and define RGB+UV light values.
|
||||
|
||||
**Important:** These lists must be marked `@Transient` and use `ArrayList` (which has a no-arg constructor for serialisation).
|
||||
|
||||
### Stationary vs. Mobile
|
||||
|
||||
Actors can be flagged as stationary to optimise rendering and physics:
|
||||
|
||||
```kotlin
|
||||
open var isStationary: Boolean = true
|
||||
```
|
||||
|
||||
Stationary actors don't move and can be culled more aggressively.
|
||||
|
||||
## Actor Interfaces
|
||||
|
||||
The engine provides several interfaces to add specific capabilities to actors:
|
||||
|
||||
### Controllable
|
||||
|
||||
For actors that can be controlled by players or AI:
|
||||
|
||||
```kotlin
|
||||
interface Controllable {
|
||||
var controllerV: Vector2?
|
||||
var moveState: MoveState
|
||||
// ... control methods
|
||||
}
|
||||
```
|
||||
|
||||
**AvailableControllers:**
|
||||
- Player input
|
||||
- AI controller
|
||||
- Scripted movement
|
||||
|
||||
### Factionable
|
||||
|
||||
For actors belonging to factions:
|
||||
|
||||
```kotlin
|
||||
interface Factionable {
|
||||
var faction: String?
|
||||
// Faction relationships affect AI behaviour
|
||||
}
|
||||
```
|
||||
|
||||
See also: [[Faction|Development:Faction]]
|
||||
|
||||
### AIControlled
|
||||
|
||||
For actors with AI behaviour:
|
||||
|
||||
```kotlin
|
||||
interface AIControlled {
|
||||
fun aiUpdate(delta: Float)
|
||||
}
|
||||
```
|
||||
|
||||
### Luminous
|
||||
|
||||
For actors that emit light (integrated into ActorWithBody via lightBoxList).
|
||||
|
||||
### NoSerialise
|
||||
|
||||
A marker interface for actors that should not be saved:
|
||||
|
||||
```kotlin
|
||||
interface NoSerialise
|
||||
```
|
||||
|
||||
## Specialised Actor Types
|
||||
|
||||
### BlockMarkerActor
|
||||
|
||||
A special actor that marks block positions without physical interaction. Used internally for debug visualisation and block tracking.
|
||||
|
||||
### WireActor
|
||||
|
||||
Represents wire/conduit connections in the game world. Rendered in the FAR_BEHIND layer.
|
||||
|
||||
### Fixtures
|
||||
|
||||
Fixtures are special ActorWithBody instances representing world objects like:
|
||||
- Doors
|
||||
- Chests
|
||||
- Crafting stations
|
||||
- Furniture
|
||||
- Interactive machinery
|
||||
|
||||
Fixtures typically:
|
||||
1. Implement `FixtureBase` or similar classes
|
||||
2. Have associated UI interactions
|
||||
3. May be immobile (`physProp.immobile = true`)
|
||||
4. Often use `isStationary = true`
|
||||
|
||||
### Particles
|
||||
|
||||
Particles are lightweight actors for visual effects:
|
||||
- Typically have short lifespans
|
||||
- May not use full physics
|
||||
- Often rendered in BEHIND or MIDTOP layers
|
||||
- Usually set `canBeDespawned = true`
|
||||
|
||||
### Projectiles
|
||||
|
||||
Projectiles (arrows, bullets, thrown items):
|
||||
- Extend ActorWithBody
|
||||
- Typically use MIDTOP render order
|
||||
- Implement collision detection
|
||||
- Self-despawn on impact or timeout
|
||||
|
||||
## Creating Custom Actors
|
||||
|
||||
### Basic Actor Example
|
||||
|
||||
```kotlin
|
||||
class MyActor : Actor(RenderOrder.MIDDLE, null) {
|
||||
|
||||
override fun updateImpl(delta: Float) {
|
||||
// Update logic here
|
||||
}
|
||||
|
||||
override fun onActorValueChange(key: String, value: Any?) {
|
||||
// Respond to actor value changes
|
||||
}
|
||||
|
||||
override fun run() {
|
||||
// Thread execution if needed
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### ActorWithBody Example
|
||||
|
||||
```kotlin
|
||||
class MyPhysicalActor : ActorWithBody(
|
||||
RenderOrder.MIDDLE,
|
||||
PhysProperties.HUMANOID_DEFAULT(),
|
||||
null // auto-generate ID
|
||||
) {
|
||||
init {
|
||||
setHitboxDimension(32, 48, 0, 0) // 32×48 pixel hitbox
|
||||
setPosition(100.0, 100.0)
|
||||
}
|
||||
|
||||
override fun updateImpl(delta: Float) {
|
||||
// Physics is handled automatically
|
||||
// Add custom behaviour here
|
||||
}
|
||||
|
||||
override fun onActorValueChange(key: String, value: Any?) {
|
||||
when (key) {
|
||||
AVKey.SCALE -> {
|
||||
// Respond to scale changes
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun reload() {
|
||||
super.reload()
|
||||
// Reconstruct sprite and other transient fields
|
||||
sprite = loadMySprite()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Actor Management in IngameInstance
|
||||
|
||||
Actors are managed by the `IngameInstance` which maintains actor lists and handles updates:
|
||||
|
||||
```kotlin
|
||||
// Get an actor by ID
|
||||
val actor = INGAME.getActorByID(actorID)
|
||||
|
||||
// Add an actor to the world
|
||||
INGAME.addNewActor(myActor)
|
||||
|
||||
// Remove an actor
|
||||
actor.flagDespawn = true
|
||||
```
|
||||
|
||||
## Best Practises
|
||||
|
||||
1. **Always call `super.reload()`** when overriding reload()
|
||||
2. **Mark sprites and UI as `@Transient`** — they cannot be serialised
|
||||
3. **Use ArrayList for lightBoxList/shadeBoxList** — other collections won't serialise properly
|
||||
4. **Set `canBeDespawned = false`** for actors that must persist (like the player)
|
||||
5. **Use ActorValue for dynamic properties** rather than adding many fields
|
||||
6. **Clean up resources in `despawn()`** — stop audio, dispose textures, etc.
|
||||
7. **Keep actor updates efficient** — updateImpl is called every frame for every actor
|
||||
8. **Use appropriate RenderOrder** — incorrect ordering causes visual glitches
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- **Forgetting to reconstruct transient fields in reload()** leads to null sprite crashes
|
||||
- **Modifying hitbox directly** instead of using setPosition/setHitboxDimension causes physics issues
|
||||
- **Not accounting for delta time** makes behaviour frame-rate dependent
|
||||
- **Creating circular actor references** can prevent serialisation
|
||||
|
||||
## See Also
|
||||
|
||||
- [[Glossary]] — Actor-related terminology
|
||||
- [[Development:Actors]] — In-depth actor implementation details
|
||||
- [[Modules:Actors]] — Creating actors in modules
|
||||
- [[Faction|Development:Faction]] — Faction system for actors
|
||||
- [[Creature RAW|Development:RAW]] — Data-driven creature definitions
|
||||
611
Animation-Description-Language.md
Normal file
611
Animation-Description-Language.md
Normal file
@@ -0,0 +1,611 @@
|
||||
# Animation Description Language (ADL)
|
||||
|
||||
**Audience:** Engine maintainers and module developers creating animated humanoid characters.
|
||||
|
||||
The Animation Description Language (ADL) is a declarative system for defining skeletal animations for humanoid actors. It uses `.properties` files to define body parts, skeletons, and frame-by-frame transformations, which are then assembled into animated spritesheets at runtime.
|
||||
|
||||
## Overview
|
||||
|
||||
ADL provides:
|
||||
- **Skeletal animation** — Define joints and transform them per-frame
|
||||
- **Modular body parts** — Separate images for head, torso, limbs, etc.
|
||||
- **Animation sequences** — Run cycles, idle animations, etc.
|
||||
- **Equipment layering** — Headgear, held items, armour overlays
|
||||
- **Runtime assembly** — Build final spritesheet from components
|
||||
|
||||
## ADL File Format
|
||||
|
||||
ADL uses Java `.properties` format with special conventions.
|
||||
|
||||
### File Structure
|
||||
|
||||
```properties
|
||||
SPRITESHEET=mods/basegame/sprites/fofu/fofu_
|
||||
EXTENSION=.tga
|
||||
CONFIG=SIZE 48,56;ORIGINX 29
|
||||
BODYPARTS=HEADGEAR 11,11;\
|
||||
HEAD 11,11;\
|
||||
ARM_REST_RIGHT 4,2;\
|
||||
TORSO 10,4
|
||||
SKELETON_STAND=HEADGEAR 0,32;HEAD 0,32;\
|
||||
ARM_REST_RIGHT -7,23;\
|
||||
TORSO 0,22
|
||||
ANIM_RUN=DELAY 0.15;ROW 2;SKELETON SKELETON_STAND
|
||||
ANIM_RUN_1=LEG_REST_RIGHT 1,1;LEG_REST_LEFT -1,0
|
||||
ANIM_RUN_2=ALL 0,1;LEG_REST_RIGHT 0,-1;LEG_REST_LEFT 0,1
|
||||
```
|
||||
|
||||
### Reserved Keywords
|
||||
|
||||
**Global Properties:**
|
||||
- `SPRITESHEET` — Base path for body part images (required)
|
||||
- `EXTENSION` — File extension for body part images (required)
|
||||
- `CONFIG` — Frame size and origin point (required)
|
||||
- `BODYPARTS` — List of body parts with joint positions (required)
|
||||
|
||||
**Skeleton Definitions:**
|
||||
- `SKELETON_*` — Defines a skeleton (e.g., `SKELETON_STAND`, `SKELETON_CROUCH`)
|
||||
|
||||
**Animation Definitions:**
|
||||
- `ANIM_*` — Defines an animation (e.g., `ANIM_RUN`, `ANIM_IDLE`)
|
||||
- `ANIM_*_N` — Defines frame N of an animation (e.g., `ANIM_RUN_1`, `ANIM_RUN_2`)
|
||||
|
||||
**Special Body Parts:**
|
||||
- `HEADGEAR` — Equipped helmet/hat
|
||||
- `HELD_ITEM` — Item in hand
|
||||
- `BOOT_L`, `BOOT_R` — Left/right boots
|
||||
- `GAUNTLET_L`, `GAUNTLET_R` — Left/right gauntlets
|
||||
- `ARMOUR_*` — Armour layers (e.g., `ARMOUR_0`, `ARMOUR_1`)
|
||||
|
||||
## Property Types
|
||||
|
||||
### SPRITESHEET and EXTENSION
|
||||
|
||||
```properties
|
||||
SPRITESHEET=mods/basegame/sprites/fofu/fofu_
|
||||
EXTENSION=.tga
|
||||
```
|
||||
|
||||
Body part files are constructed as: `SPRITESHEET + bodypart_name + EXTENSION`
|
||||
|
||||
**Example:**
|
||||
- `SPRITESHEET=mods/basegame/sprites/fofu/fofu_`
|
||||
- Body part: `HEAD`
|
||||
- Result: `mods/basegame/sprites/fofu/fofu_head.tga`
|
||||
|
||||
### CONFIG
|
||||
|
||||
```properties
|
||||
CONFIG=SIZE 48,56;ORIGINX 29
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `SIZE w,h` — Frame dimensions (width, height) in pixels (required)
|
||||
- `ORIGINX x` — X-coordinate of origin point (required)
|
||||
|
||||
Origin is always `(originX, 0)` — the top-centre anchor point for the character.
|
||||
|
||||
**Frame dimensions include extra headroom:**
|
||||
```kotlin
|
||||
frameWidth = configWidth + 32 // EXTRA_HEADROOM_X = 32
|
||||
frameHeight = configHeight + 16 // EXTRA_HEADROOM_Y = 16
|
||||
```
|
||||
|
||||
### BODYPARTS
|
||||
|
||||
```properties
|
||||
BODYPARTS=HEAD 11,11;\
|
||||
ARM_REST_RIGHT 4,2;\
|
||||
LEG_REST_LEFT 4,7;\
|
||||
TORSO 10,4;\
|
||||
HELD_ITEM 0,0
|
||||
```
|
||||
|
||||
Defines the list of body parts and their **joint positions** (anchor points within each sprite).
|
||||
|
||||
**Format:** `BODYPART_NAME jointX,jointY`
|
||||
|
||||
**Joint Position:**
|
||||
- Coordinates are **relative to the body part sprite's top-left corner**
|
||||
- Joint is where this body part connects to the skeleton
|
||||
- Example: `HEAD 11,11` — Head sprite's joint is 11 pixels right, 11 pixels down from top-left
|
||||
|
||||
**Paint Order:**
|
||||
Body parts are painted in **reverse order** — last in the list paints first (background), first in the list paints last (foreground).
|
||||
|
||||
```properties
|
||||
# Paint order: TORSO (back) → LEG → ARM → HEAD (front)
|
||||
BODYPARTS=HEAD 8,7;\
|
||||
ARM_REST_RIGHT 3,8;\
|
||||
LEG_REST_RIGHT 3,7;\
|
||||
TORSO 9,4
|
||||
```
|
||||
|
||||
### SKELETON Definitions
|
||||
|
||||
```properties
|
||||
SKELETON_STAND=HEADGEAR 0,32;\
|
||||
HEAD 0,32;\
|
||||
ARM_REST_RIGHT -7,23;\
|
||||
TORSO 0,22;\
|
||||
LEG_REST_RIGHT -2,7
|
||||
```
|
||||
|
||||
Defines joint positions for a skeleton pose.
|
||||
|
||||
**Format:** `SKELETON_NAME=JOINT_NAME offsetX,offsetY;...`
|
||||
|
||||
**Joint Offsets:**
|
||||
- Coordinates are **relative to the character's origin** `(originX, 0)`
|
||||
- Positive X → right, Positive Y → down
|
||||
- Example: `HEAD 0,32` — Head joint is 0 pixels right, 32 pixels down from origin
|
||||
- Example: `ARM_REST_RIGHT -7,23` — Right arm joint is 7 pixels left, 23 pixels down
|
||||
|
||||
**Multiple Skeletons:**
|
||||
|
||||
```properties
|
||||
SKELETON_STAND=HEAD 0,32;TORSO 0,22;LEG_LEFT 2,7
|
||||
SKELETON_CROUCH=HEAD 0,28;TORSO 0,20;LEG_LEFT 3,5
|
||||
SKELETON_JUMP=HEAD 0,30;TORSO 0,21;LEG_LEFT 1,8
|
||||
```
|
||||
|
||||
### ANIM Definitions
|
||||
|
||||
```properties
|
||||
ANIM_RUN=DELAY 0.15;ROW 2;SKELETON SKELETON_STAND
|
||||
```
|
||||
|
||||
Defines an animation sequence.
|
||||
|
||||
**Parameters:**
|
||||
- `DELAY seconds` — Time between frames (float; actors may override delays)
|
||||
- `ROW row_number` — Row in the output spritesheet (starts at 1)
|
||||
- `SKELETON skeleton_name` — Which skeleton this animation uses
|
||||
|
||||
**Frame Count:**
|
||||
Frame count is determined by the highest `ANIM_*_N` suffix:
|
||||
|
||||
```properties
|
||||
ANIM_RUN=DELAY 0.15;ROW 2;SKELETON SKELETON_STAND
|
||||
ANIM_RUN_1=...
|
||||
ANIM_RUN_2=...
|
||||
ANIM_RUN_3=...
|
||||
ANIM_RUN_4=...
|
||||
# This animation has 4 frames
|
||||
```
|
||||
|
||||
### ANIM Frame Definitions
|
||||
|
||||
```properties
|
||||
ANIM_RUN_1=LEG_REST_RIGHT 1,1;LEG_REST_LEFT -1,0
|
||||
ANIM_RUN_2=ALL 0,1;LEG_REST_RIGHT 0,-1;LEG_REST_LEFT 0,1
|
||||
```
|
||||
|
||||
Defines per-joint transformations for a specific frame.
|
||||
|
||||
**Format:** `ANIM_NAME_N=JOINT translateX,translateY;...`
|
||||
|
||||
**Transformations:**
|
||||
- `JOINT offsetX,offsetY` — Translate joint by (offsetX, offsetY) pixels
|
||||
- `ALL offsetX,offsetY` — Translate ALL joints by (offsetX, offsetY) pixels
|
||||
|
||||
**Empty Frame:**
|
||||
```properties
|
||||
ANIM_IDLE_1=
|
||||
# Frame 1: No transformations (use skeleton as-is)
|
||||
```
|
||||
|
||||
**Overlapping Transforms:**
|
||||
```properties
|
||||
ANIM_RUN_2=ALL 0,1;LEG_REST_RIGHT 0,-1
|
||||
# 1. Move everything down 1 pixel
|
||||
# 2. Then move right leg up 1 pixel (net: 0 offset for right leg)
|
||||
```
|
||||
|
||||
## Complete Example
|
||||
|
||||
```properties
|
||||
# File paths
|
||||
SPRITESHEET=mods/basegame/sprites/fofu/fofu_
|
||||
EXTENSION=.tga
|
||||
|
||||
# Frame configuration
|
||||
CONFIG=SIZE 48,56;ORIGINX 29
|
||||
|
||||
# Body parts with joint positions
|
||||
BODYPARTS=HEADGEAR 11,11;\
|
||||
HEAD 11,11;\
|
||||
ARM_REST_RIGHT 4,2;\
|
||||
ARM_REST_LEFT 4,2;\
|
||||
LEG_REST_RIGHT 4,7;\
|
||||
LEG_REST_LEFT 4,7;\
|
||||
TORSO 10,4;\
|
||||
TAIL_0 20,1;\
|
||||
HELD_ITEM 0,0
|
||||
|
||||
# Skeleton: standing pose (paint order: top to bottom)
|
||||
SKELETON_STAND=HEADGEAR 0,32;\
|
||||
HEAD 0,32;\
|
||||
ARM_REST_RIGHT -7,23;\
|
||||
ARM_REST_LEFT 5,24;\
|
||||
TORSO 0,22;\
|
||||
LEG_REST_RIGHT -2,7;\
|
||||
LEG_REST_LEFT 2,7;\
|
||||
TAIL_0 0,13;\
|
||||
HELD_ITEM -6,11
|
||||
|
||||
# Animation: running (4 frames, 0.15s per frame)
|
||||
ANIM_RUN=DELAY 0.15;ROW 2;SKELETON SKELETON_STAND
|
||||
ANIM_RUN_1=LEG_REST_RIGHT 1,1;LEG_REST_LEFT -1,0
|
||||
ANIM_RUN_2=ALL 0,1;LEG_REST_RIGHT 0,-1;LEG_REST_LEFT 0,1
|
||||
ANIM_RUN_3=LEG_REST_RIGHT -1,0;LEG_REST_LEFT 1,1
|
||||
ANIM_RUN_4=ALL 0,1;LEG_REST_RIGHT 0,1;LEG_REST_LEFT 0,-1
|
||||
|
||||
# Animation: idle (2 frames, 2s per frame)
|
||||
ANIM_IDLE=DELAY 2;ROW 1;SKELETON SKELETON_STAND
|
||||
ANIM_IDLE_1=
|
||||
ANIM_IDLE_2=TORSO 0,-1;HEAD 0,-1;HELD_ITEM 0,-1;\
|
||||
ARM_REST_LEFT 0,-1;ARM_REST_RIGHT 0,-1;\
|
||||
HEADGEAR 0,-1
|
||||
```
|
||||
|
||||
## ADProperties Class
|
||||
|
||||
The Kotlin class that parses ADL files.
|
||||
|
||||
### Loading ADL
|
||||
|
||||
```kotlin
|
||||
val adl = ADProperties(gdxFile)
|
||||
```
|
||||
|
||||
**Constructors:**
|
||||
```kotlin
|
||||
constructor(gdxFile: FileHandle)
|
||||
constructor(reader: Reader)
|
||||
constructor(inputStream: InputStream)
|
||||
```
|
||||
|
||||
### Properties
|
||||
|
||||
```kotlin
|
||||
class ADProperties {
|
||||
// File information
|
||||
lateinit var baseFilename: String // From SPRITESHEET
|
||||
lateinit var extension: String // From EXTENSION
|
||||
|
||||
// Frame configuration
|
||||
var frameWidth: Int // From CONFIG SIZE + headroom
|
||||
var frameHeight: Int // From CONFIG SIZE + headroom
|
||||
var originX: Int // From CONFIG ORIGINX
|
||||
|
||||
// Spritesheet dimensions
|
||||
var rows: Int // Max animation row
|
||||
var cols: Int // Max frame count
|
||||
|
||||
// Body parts
|
||||
lateinit var bodyparts: List<String>
|
||||
lateinit var bodypartFiles: List<String>
|
||||
val bodypartJoints: HashMap<String, ADPropertyObject.Vector2i>
|
||||
|
||||
// Animation data
|
||||
internal lateinit var skeletons: HashMap<String, Skeleton>
|
||||
internal lateinit var animations: HashMap<String, Animation>
|
||||
internal lateinit var transforms: HashMap<String, List<Transform>>
|
||||
}
|
||||
```
|
||||
|
||||
### Data Classes
|
||||
|
||||
```kotlin
|
||||
// Joint in a skeleton
|
||||
internal data class Joint(
|
||||
val name: String,
|
||||
val position: ADPropertyObject.Vector2i
|
||||
)
|
||||
|
||||
// Skeleton pose
|
||||
internal data class Skeleton(
|
||||
val name: String,
|
||||
val joints: List<Joint>
|
||||
)
|
||||
|
||||
// Animation sequence
|
||||
internal data class Animation(
|
||||
val name: String,
|
||||
val delay: Float, // Seconds per frame
|
||||
val row: Int, // Row in spritesheet
|
||||
val frames: Int, // Frame count
|
||||
val skeleton: Skeleton
|
||||
)
|
||||
|
||||
// Per-frame joint transformation
|
||||
internal data class Transform(
|
||||
val joint: Joint,
|
||||
val translate: ADPropertyObject.Vector2i
|
||||
)
|
||||
```
|
||||
|
||||
### Vector2i
|
||||
|
||||
```kotlin
|
||||
data class Vector2i(var x: Int, var y: Int) {
|
||||
operator fun plus(other: Vector2i) = Vector2i(x + other.x, y + other.y)
|
||||
operator fun minus(other: Vector2i) = Vector2i(x - other.x, y - other.y)
|
||||
|
||||
fun invertY() = Vector2i(x, -y)
|
||||
fun invertX() = Vector2i(-x, y)
|
||||
fun invertXY() = Vector2i(-x, -y)
|
||||
}
|
||||
```
|
||||
|
||||
## Rendering Process
|
||||
|
||||
ADL animations are rendered **on-the-fly** each frame by `AssembledSpriteAnimation.renderThisAnimation()`.
|
||||
|
||||
### On-the-Fly Rendering
|
||||
|
||||
```kotlin
|
||||
class AssembledSpriteAnimation(
|
||||
val adp: ADProperties,
|
||||
parentActor: ActorWithBody,
|
||||
val disk: SimpleFileSystem?,
|
||||
val isGlow: Boolean,
|
||||
val isEmissive: Boolean
|
||||
) : SpriteAnimation(parentActor) {
|
||||
|
||||
// Body part textures cached in memory
|
||||
@Transient private val res = HashMap<String, TextureRegion?>()
|
||||
|
||||
var currentAnimation = "" // e.g., "ANIM_IDLE", "ANIM_RUN"
|
||||
var currentFrame = 0 // Current frame index (zero-based)
|
||||
|
||||
fun renderThisAnimation(
|
||||
batch: SpriteBatch,
|
||||
posX: Float,
|
||||
posY: Float,
|
||||
scale: Float,
|
||||
animName: String, // e.g., "ANIM_RUN_2"
|
||||
mode: Int = 0
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Rendering Steps (per frame)
|
||||
|
||||
1. **Load body part textures** — Cache `TextureRegion` for each body part
|
||||
2. **Get animation data** — Retrieve skeleton and transforms for current frame
|
||||
3. **Calculate positions** — For each body part:
|
||||
```kotlin
|
||||
val skeleton = animation.skeleton.joints.reversed()
|
||||
val transforms = adp.getTransform("ANIM_RUN_2")
|
||||
val bodypartOrigins = adp.bodypartJoints
|
||||
|
||||
AssembleFrameBase.makeTransformList(skeleton, transforms).forEach { (name, bodypartPos) ->
|
||||
// Calculate final position
|
||||
val drawPos = adp.origin + bodypartPos - bodypartOrigins[name]
|
||||
|
||||
// Draw body part texture at calculated position
|
||||
batch.draw(texture, drawPos.x * scale, drawPos.y * scale)
|
||||
}
|
||||
```
|
||||
4. **Draw equipment** — Render held items and armour at joint positions
|
||||
|
||||
**No pre-assembly required** — Body parts are positioned and drawn directly each frame.
|
||||
|
||||
### Example Rendering: ANIM_RUN_2
|
||||
|
||||
**ADL Definition:**
|
||||
```properties
|
||||
SKELETON_STAND=HEAD 0,32;TORSO 0,22;LEG_RIGHT -2,7
|
||||
ANIM_RUN_2=ALL 0,1;LEG_RIGHT 0,-1
|
||||
BODYPARTS=LEG_REST_RIGHT 4,7
|
||||
CONFIG=SIZE 48,56;ORIGINX 29
|
||||
```
|
||||
|
||||
**Rendering LEG_RIGHT (per frame):**
|
||||
1. **Origin:** `(29, 0)` (from `ORIGINX 29`)
|
||||
2. **Skeleton pose:** `LEG_RIGHT -2,7` → joint offset from origin = `(-2, 7)`
|
||||
3. **ALL transform:** `0,1` → shift all joints by `(0, 1)`
|
||||
4. **LEG_RIGHT transform:** `0,-1` → shift this joint by `(0, -1)`
|
||||
5. **Final joint offset:** `(-2, 7) + (0, 1) + (0, -1)` = `(-2, 7)`
|
||||
6. **Joint in world:** `(29, 0) + (-2, 7)` = `(27, 7)`
|
||||
7. **Body part anchor:** `LEG_REST_RIGHT 4,7` (from BODYPARTS)
|
||||
8. **Draw position:** `(27, 7) - (4, 7)` = `(23, 0)`
|
||||
|
||||
Body part sprite is drawn at `(23, 0)` relative to character origin, with its anchor point at the skeleton joint `(27, 7)`.
|
||||
|
||||
## Practical Usage
|
||||
|
||||
### Creating a New Character
|
||||
|
||||
1. **Draw body parts** as separate images:
|
||||
- `character_head.tga`
|
||||
- `character_torso.tga`
|
||||
- `character_arm_rest_left.tga`
|
||||
- `character_arm_rest_right.tga`
|
||||
- `character_leg_rest_left.tga`
|
||||
- `character_leg_rest_right.tga`
|
||||
|
||||
2. **Mark joint positions** on each sprite (where it connects)
|
||||
|
||||
3. **Write ADL file:**
|
||||
|
||||
```properties
|
||||
SPRITESHEET=mods/mymod/sprites/character/character_
|
||||
EXTENSION=.tga
|
||||
CONFIG=SIZE 48,56;ORIGINX 24
|
||||
|
||||
BODYPARTS=HEAD 10,8;\
|
||||
ARM_REST_RIGHT 5,3;\
|
||||
ARM_REST_LEFT 5,3;\
|
||||
TORSO 12,6;\
|
||||
LEG_REST_RIGHT 4,8;\
|
||||
LEG_REST_LEFT 4,8
|
||||
|
||||
SKELETON_STAND=HEAD 0,30;\
|
||||
ARM_REST_RIGHT -8,24;\
|
||||
ARM_REST_LEFT 8,24;\
|
||||
TORSO 0,22;\
|
||||
LEG_REST_RIGHT -3,8;\
|
||||
LEG_REST_LEFT 3,8
|
||||
|
||||
ANIM_IDLE=DELAY 1;ROW 1;SKELETON SKELETON_STAND
|
||||
ANIM_IDLE_1=
|
||||
```
|
||||
|
||||
4. **Load in code:**
|
||||
|
||||
```kotlin
|
||||
val adl = ADProperties(ModMgr.getGdxFile("mymod", "sprites/character.properties"))
|
||||
actor.sprite = AssembledSpriteAnimation(adl, actor, isGlow = false, isEmissive = false)
|
||||
actor.sprite.currentAnimation = "ANIM_IDLE"
|
||||
```
|
||||
|
||||
### Adding New Animations
|
||||
|
||||
```properties
|
||||
# Walk cycle (8 frames)
|
||||
ANIM_WALK=DELAY 0.1;ROW 3;SKELETON SKELETON_STAND
|
||||
ANIM_WALK_1=LEG_REST_RIGHT 0,1;ARM_REST_LEFT 0,1
|
||||
ANIM_WALK_2=LEG_REST_RIGHT 1,0;ARM_REST_LEFT 1,-1
|
||||
ANIM_WALK_3=LEG_REST_RIGHT 1,-1;ARM_REST_LEFT 2,-2
|
||||
ANIM_WALK_4=ALL 0,1;LEG_REST_RIGHT 0,-1;ARM_REST_LEFT 0,1
|
||||
ANIM_WALK_5=LEG_REST_LEFT 0,1;ARM_REST_RIGHT 0,1
|
||||
ANIM_WALK_6=LEG_REST_LEFT 1,0;ARM_REST_RIGHT 1,-1
|
||||
ANIM_WALK_7=LEG_REST_LEFT 1,-1;ARM_REST_RIGHT 2,-2
|
||||
ANIM_WALK_8=ALL 0,1;LEG_REST_LEFT 0,-1;ARM_REST_RIGHT 0,1
|
||||
```
|
||||
|
||||
### Equipment Layering
|
||||
|
||||
Use special body part names for equipment:
|
||||
|
||||
```properties
|
||||
BODYPARTS=HEADGEAR 11,11;\ # Helmet/hat slot
|
||||
HELD_ITEM 0,0;\ # Item in hand
|
||||
GAUNTLET_L 3,3;\ # Left glove
|
||||
GAUNTLET_R 3,3;\ # Right glove
|
||||
BOOT_L 4,2;\ # Left boot
|
||||
BOOT_R 4,2;\ # Right boot
|
||||
ARMOUR_0 10,4;\ # Armour layer 0
|
||||
ARMOUR_1 10,4 # Armour layer 1
|
||||
```
|
||||
|
||||
These slots are rendered dynamically from actor inventory:
|
||||
|
||||
```kotlin
|
||||
// In AssembledSpriteAnimation.renderThisAnimation()
|
||||
if (name in jointNameToEquipPos) {
|
||||
val item = (parentActor as? Pocketed)?.inventory?.itemEquipped?.get(jointNameToEquipPos[name])
|
||||
fetchItemImage(mode, item)?.let { image ->
|
||||
// Draw equipped item at joint position
|
||||
batch.draw(image, drawPos.x, drawPos.y)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Equipment is rendered **automatically** when actor has items equipped.
|
||||
|
||||
## Best Practises
|
||||
|
||||
1. **Use consistent joint positions** — Same body part across animations should have same joint
|
||||
2. **Paint order matters** — List body parts background-to-foreground
|
||||
3. **Use ALL for body movement** — Move entire character up/down with `ALL`
|
||||
4. **Keep frame delays consistent** — Use same delay for similar animations (e.g., all walks 0.1s)
|
||||
5. **Test with origin marker** — Verify origin is at character's centre-top
|
||||
6. **Use skeletons for poses** — Define `SKELETON_CROUCH`, `SKELETON_JUMP` for clarity
|
||||
7. **Name body parts clearly** — Use `_LEFT`/`_RIGHT`, `_REST`/`_ACTIVE` conventions
|
||||
8. **Add headroom** — ADProperties adds 32×16 pixels automatically; don't pre-add
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- **Wrong paint order** — Arm painting behind torso instead of in front
|
||||
- **Inconsistent joint positions** — Head joint moves between body parts
|
||||
- **Forgetting frame numbers** — `ANIM_RUN_1`, `ANIM_RUN_2`, not `ANIM_RUN_0`
|
||||
- **Missing ALL transform** — Forgot to move entire body up/down
|
||||
- **Wrong coordinate space** — Mixing up joint offsets vs. world positions
|
||||
- **Overlapping transforms** — `ALL` and individual transforms don't add correctly
|
||||
- **Origin misalignment** — Character appears to "slide" when moving
|
||||
- **Negative delays** — Invalid delay value
|
||||
|
||||
## Advanced Techniques
|
||||
|
||||
### Layered Animations
|
||||
|
||||
```properties
|
||||
# Base layer (body)
|
||||
SKELETON_BASE=TORSO 0,22;LEG_LEFT 2,7;LEG_RIGHT -2,7
|
||||
|
||||
# Upper body layer (can animate separately)
|
||||
SKELETON_UPPER=HEAD 0,32;ARM_LEFT 6,24;ARM_RIGHT -6,24
|
||||
|
||||
# Combine in animations
|
||||
ANIM_WALK=DELAY 0.1;ROW 2;SKELETON SKELETON_BASE
|
||||
ANIM_ATTACK=DELAY 0.05;ROW 3;SKELETON SKELETON_UPPER
|
||||
```
|
||||
|
||||
### Dynamic Body Part Swapping
|
||||
|
||||
Body parts are loaded on construction and cached:
|
||||
|
||||
```kotlin
|
||||
class AssembledSpriteAnimation {
|
||||
@Transient private val res = HashMap<String, TextureRegion?>()
|
||||
|
||||
init {
|
||||
// Load all body parts from ADL
|
||||
adp.bodyparts.forEach {
|
||||
res[it] = getPartTexture(fileGetter, it)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
To swap body parts dynamically:
|
||||
|
||||
```kotlin
|
||||
// Load base character
|
||||
val sprite = AssembledSpriteAnimation(adl, actor, false, false)
|
||||
|
||||
// Replace body part texture (requires modifying internal res map)
|
||||
val helmetTexture = loadHelmet("iron_helmet.tga")
|
||||
// Note: res is private; body part swapping typically uses equipment slots instead
|
||||
|
||||
// For equipment, use HELD_ITEM/HEADGEAR slots which render from inventory
|
||||
actor.inventory.itemEquipped[GameItem.EquipPosition.HEAD] = ItemCodex["item:helmet_iron"]
|
||||
// Equipment renders automatically in renderThisAnimation()
|
||||
```
|
||||
|
||||
### Mirror Animations
|
||||
|
||||
`AssembledSpriteAnimation` supports horizontal and vertical flipping:
|
||||
|
||||
```kotlin
|
||||
class AssembledSpriteAnimation {
|
||||
var flipHorizontal = false
|
||||
var flipVertical = false
|
||||
}
|
||||
|
||||
// Mirror character for right-facing
|
||||
sprite.flipHorizontal = true
|
||||
|
||||
// In renderThisAnimation(), flipping is applied:
|
||||
fun renderThisAnimation(...) {
|
||||
if (flipHorizontal) bodypartPos = bodypartPos.invertX()
|
||||
if (flipVertical) bodypartPos = bodypartPos.invertY()
|
||||
|
||||
// Draw with negative width/height for flipping
|
||||
if (flipHorizontal && flipVertical)
|
||||
batch.draw(image, x, y, -w, -h)
|
||||
else if (flipHorizontal)
|
||||
batch.draw(image, x, y, -w, h)
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
## See Also
|
||||
|
||||
- [[Actors]] — Actor system using ADL animations
|
||||
- [[Fixtures]] — Fixture actors (non-animated)
|
||||
- [[Glossary]] — Animation terminology
|
||||
504
Audio-Engine-Internals.md
Normal file
504
Audio-Engine-Internals.md
Normal file
@@ -0,0 +1,504 @@
|
||||
# Audio Engine Internals
|
||||
|
||||
**Audience:** Engine maintainers working on the audio system implementation.
|
||||
|
||||
This document describes the internal architecture of Terrarum's audio engine, including the mixer thread, DSP pipeline, spatial audio calculations, and low-level audio processing.
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
The audio engine is built on a **threaded mixer architecture** with real-time DSP processing:
|
||||
|
||||
```
|
||||
┌─────────────────┐
|
||||
│ Game Thread │
|
||||
│ (Main Loop) │
|
||||
└────────┬────────┘
|
||||
│ Control Commands
|
||||
↓
|
||||
┌─────────────────┐
|
||||
│ AudioMixer │ ← Singleton, runs on separate thread
|
||||
│ (Central Hub) │
|
||||
└────────┬────────┘
|
||||
│
|
||||
┌────┴────┬──────────┬──────────┐
|
||||
↓ ↓ ↓ ↓
|
||||
Track 0 Track 1 Track 2 Track N
|
||||
│ │ │ │
|
||||
↓ ↓ ↓ ↓
|
||||
Filter Filter Filter Filter
|
||||
Chain Chain Chain Chain
|
||||
│ │ │ │
|
||||
└────┬────┴──────────┴──────────┘
|
||||
↓
|
||||
┌────────────┐
|
||||
│ Mixer │ ← Combines all tracks
|
||||
└────┬───────┘
|
||||
↓
|
||||
┌────────────┐
|
||||
│ OpenAL │ ← Hardware audio output
|
||||
│ Output │
|
||||
└────────────┘
|
||||
```
|
||||
|
||||
## Threading Model
|
||||
|
||||
### AudioManagerRunnable
|
||||
|
||||
The audio system runs on a dedicated thread:
|
||||
|
||||
```kotlin
|
||||
class AudioManagerRunnable(
|
||||
val audioMixer: AudioMixer
|
||||
) : Runnable {
|
||||
|
||||
@Volatile var running = true
|
||||
private val updateInterval = 1000 / 60 // 60 Hz update rate
|
||||
|
||||
override fun run() {
|
||||
while (running) {
|
||||
val startTime = System.currentTimeMillis()
|
||||
|
||||
// Update all tracks
|
||||
audioMixer.update()
|
||||
|
||||
// Sleep to maintain update rate
|
||||
val elapsed = System.currentTimeMillis() - startTime
|
||||
val sleep Time = maxOf(0, updateInterval - elapsed)
|
||||
Thread.sleep(sleepTime)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Thread Safety
|
||||
|
||||
All audio operations are thread-safe:
|
||||
|
||||
```kotlin
|
||||
@Volatile var trackVolume: TrackVolume // Atomic updates
|
||||
private val lock = ReentrantLock() // Protects critical sections
|
||||
|
||||
fun setMusic(music: MusicContainer) {
|
||||
lock.withLock {
|
||||
currentMusic?.stop()
|
||||
currentMusic = music
|
||||
music.play()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Update Rate
|
||||
|
||||
Audio updates at **60 Hz** (every ~16.67ms) to match game frame rate:
|
||||
|
||||
- Position updates for spatial audio
|
||||
- Volume envelope processing
|
||||
- Filter parameter interpolation
|
||||
- Crossfade calculations
|
||||
|
||||
## TerrarumAudioMixerTrack
|
||||
|
||||
### Track Structure
|
||||
|
||||
Each track is an independent audio channel:
|
||||
|
||||
```kotlin
|
||||
class TerrarumAudioMixerTrack(val index: Int) {
|
||||
var currentMusic: MusicContainer? = null
|
||||
var trackVolume: TrackVolume = TrackVolume(1.0f, 1.0f)
|
||||
var filters: Array<AudioFilter> = arrayOf(NullFilter, NullFilter)
|
||||
var trackingTarget: ActorWithBody? = null
|
||||
var state: TrackState = TrackState.STOPPED
|
||||
|
||||
val processor: StreamProcessor
|
||||
}
|
||||
```
|
||||
|
||||
### Track State Machine
|
||||
|
||||
```kotlin
|
||||
enum class TrackState {
|
||||
STOPPED, // Not playing
|
||||
PLAYING, // Currently playing
|
||||
PAUSED, // Paused, can resume
|
||||
STOPPING, // Fading out before stop
|
||||
}
|
||||
```
|
||||
|
||||
State transitions:
|
||||
|
||||
```
|
||||
STOPPED ─play()─→ PLAYING
|
||||
↑ ↓
|
||||
stop() pause()
|
||||
↑ ↓
|
||||
STOPPING ←─ PAUSED
|
||||
↑ ↓
|
||||
(fadeout) resume()
|
||||
```
|
||||
|
||||
### Stream Processor
|
||||
|
||||
Handles low-level audio streaming:
|
||||
|
||||
```kotlin
|
||||
class StreamProcessor(val track: TerrarumAudioMixerTrack) {
|
||||
var streamBuf: AudioDevice? // OpenAL audio device
|
||||
private val buffer = FloatArray(BUFFER_SIZE)
|
||||
|
||||
fun fillBuffer(): FloatArray {
|
||||
val source = track.currentMusic ?: return emptyBuffer()
|
||||
|
||||
// Read raw PCM data from source
|
||||
val samples = source.readSamples(BUFFER_SIZE)
|
||||
|
||||
// Apply volume
|
||||
applyStereoVolume(samples, track.trackVolume)
|
||||
|
||||
// Process through filter chain
|
||||
val filtered = applyFilters(samples, track.filters)
|
||||
|
||||
// Apply spatial positioning
|
||||
if (track.trackingTarget != null) {
|
||||
applySpatialAudio(filtered, track.trackingTarget!!)
|
||||
}
|
||||
|
||||
return filtered
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Buffer Size
|
||||
|
||||
Buffer size is **user-configurable** (from `MixerTrackProcessor.kt` and `OpenALBufferedAudioDevice.kt`):
|
||||
|
||||
```kotlin
|
||||
class MixerTrackProcessor(bufferSize: Int, val rate: Int, ...)
|
||||
class OpenALBufferedAudioDevice(..., val bufferSize: Int, ...)
|
||||
```
|
||||
|
||||
**Range:** 128 to 4096 samples per buffer
|
||||
**Sample rate:** 48000 Hz (constant from `TerrarumAudioMixerTrack.SAMPLING_RATE`)
|
||||
|
||||
Example buffer duration calculations:
|
||||
- 128 samples: 128 / 48000 ≈ **2.7ms latency**
|
||||
- 4096 samples: 4096 / 48000 ≈ **85ms latency**
|
||||
|
||||
Trade-off:
|
||||
- **Smaller buffers** → Lower latency, higher CPU usage, risk of audio dropouts
|
||||
- **Larger buffers** → Higher latency, lower CPU usage, more stable playback
|
||||
|
||||
## Audio Streaming
|
||||
|
||||
### MusicContainer
|
||||
|
||||
Wraps audio files for streaming:
|
||||
|
||||
```kotlin
|
||||
class MusicContainer(
|
||||
val file: FileHandle,
|
||||
val format: AudioFormat
|
||||
) {
|
||||
private var decoder: AudioDecoder? = null
|
||||
private var position: Long = 0 // Sample position
|
||||
|
||||
fun readSamples(count: Int): FloatArray {
|
||||
if (decoder == null) {
|
||||
decoder = createDecoder(file, format)
|
||||
}
|
||||
|
||||
val samples = FloatArray(count)
|
||||
val read = decoder!!.read(samples, 0, count)
|
||||
|
||||
if (read < count) {
|
||||
// Reached end of file
|
||||
if (looping) {
|
||||
seek(0)
|
||||
decoder!!.read(samples, read, count - read)
|
||||
} else {
|
||||
// Pad with silence
|
||||
samples.fill(0f, read, count)
|
||||
}
|
||||
}
|
||||
|
||||
position += read
|
||||
return samples
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Audio Decoders
|
||||
|
||||
Support multiple formats:
|
||||
|
||||
```kotlin
|
||||
interface AudioDecoder {
|
||||
fun read(buffer: FloatArray, offset: Int, length: Int): Int
|
||||
fun seek(samplePosition: Long)
|
||||
fun close()
|
||||
}
|
||||
|
||||
class OggVorbisDecoder : AudioDecoder { ... }
|
||||
class WavDecoder : AudioDecoder { ... }
|
||||
class Mp3Decoder : AudioDecoder { ... }
|
||||
```
|
||||
|
||||
### Streaming vs. Loading
|
||||
|
||||
- **Streaming** — Large files (music): read chunks on-demand
|
||||
- **Fully loaded** — Small files (SFX): load entire file into memory
|
||||
|
||||
```kotlin
|
||||
val isTooLargeToLoad = file.length() > 5 * 1024 * 1024 // 5 MB threshold
|
||||
|
||||
val container = if (isTooLargeToLoad) {
|
||||
StreamingMusicContainer(file)
|
||||
} else {
|
||||
LoadedMusicContainer(file)
|
||||
}
|
||||
```
|
||||
|
||||
## DSP Filter Pipeline
|
||||
|
||||
### Filter Interface
|
||||
|
||||
```kotlin
|
||||
interface AudioFilter {
|
||||
fun process(samples: FloatArray, sampleRate: Int)
|
||||
fun reset()
|
||||
}
|
||||
```
|
||||
|
||||
### Filter Chain
|
||||
|
||||
Each track has two filter slots:
|
||||
|
||||
```kotlin
|
||||
fun applyFilters(samples: FloatArray, filters: Array<AudioFilter>): FloatArray {
|
||||
var current = samples.copyOf()
|
||||
|
||||
for (filter in filters) {
|
||||
if (filter !is NullFilter) {
|
||||
filter.process(current, SAMPLE_RATE)
|
||||
}
|
||||
}
|
||||
|
||||
return current
|
||||
}
|
||||
```
|
||||
|
||||
### Built-in Filters
|
||||
|
||||
#### LowPassFilter
|
||||
|
||||
Reduces high frequencies (muffled sound):
|
||||
|
||||
```kotlin
|
||||
class LowPassFilter(var cutoffFreq: Float) : AudioFilter {
|
||||
private var prevSample = 0f
|
||||
|
||||
override fun process(samples: FloatArray, sampleRate: Int) {
|
||||
val rc = 1.0f / (cutoffFreq * 2 * PI)
|
||||
val dt = 1.0f / sampleRate
|
||||
val alpha = dt / (rc + dt)
|
||||
|
||||
for (i in samples.indices) {
|
||||
prevSample = prevSample + alpha * (samples[i] - prevSample)
|
||||
samples[i] = prevSample
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### HighPassFilter
|
||||
|
||||
Reduces low frequencies (tinny sound):
|
||||
|
||||
```kotlin
|
||||
class HighPassFilter(var cutoffFreq: Float) : AudioFilter {
|
||||
private var prevInput = 0f
|
||||
private var prevOutput = 0f
|
||||
|
||||
override fun process(samples: FloatArray, sampleRate: Int) {
|
||||
val rc = 1.0f / (cutoffFreq * 2 * PI)
|
||||
val dt = 1.0f / sampleRate
|
||||
val alpha = rc / (rc + dt)
|
||||
|
||||
for (i in samples.indices) {
|
||||
prevOutput = alpha * (prevOutput + samples[i] - prevInput)
|
||||
prevInput = samples[i]
|
||||
samples[i] = prevOutput
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Convolv
|
||||
|
||||
Convolutional reverb using impulse response (IR) files and FFT-based fast convolution:
|
||||
|
||||
```kotlin
|
||||
class Convolv(
|
||||
irModule: String, // Module containing IR file
|
||||
irPath: String, // Path to IR file
|
||||
val crossfeed: Float, // Stereo crossfeed amount (0.0-1.0)
|
||||
gain: Float = 1f / 256f // Output gain
|
||||
) : TerrarumAudioFilter() {
|
||||
|
||||
private val fftLen: Int
|
||||
private val convFFT: Array<ComplexArray>
|
||||
private val sumbuf: Array<ComplexArray>
|
||||
|
||||
init {
|
||||
convFFT = AudioHelper.getIR(irModule, irPath)
|
||||
fftLen = convFFT[0].size
|
||||
sumbuf = Array(2) { ComplexArray(FloatArray(fftLen * 2)) }
|
||||
}
|
||||
|
||||
override fun thru(inbuf: List<FloatArray>, outbuf: List<FloatArray>) {
|
||||
// Fast convolution using overlap-add method
|
||||
pushSum(gain, inbuf[0], inbuf[1], sumbuf)
|
||||
|
||||
convolve(sumbuf[0], convFFT[0], fftOutL)
|
||||
convolve(sumbuf[1], convFFT[1], fftOutR)
|
||||
|
||||
// Extract final output from FFT results
|
||||
for (i in 0 until App.audioBufferSize) {
|
||||
outbuf[0][i] = fftOutL[fftLen - App.audioBufferSize + i]
|
||||
outbuf[1][i] = fftOutR[fftLen - App.audioBufferSize + i]
|
||||
}
|
||||
}
|
||||
|
||||
private fun convolve(x: ComplexArray, h: ComplexArray, output: FloatArray) {
|
||||
FFT.fftInto(x, fftIn)
|
||||
fftIn.mult(h, fftMult)
|
||||
FFT.ifftAndGetReal(fftMult, output)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Impulse Response (IR) Files:**
|
||||
- Binary files containing mono IR with two channels
|
||||
- Loaded via `AudioHelper.getIR(module, path)`
|
||||
- FFT length determined by IR file size
|
||||
- Supports arbitrary room acoustics via recorded/synthesised IRs
|
||||
|
||||
#### BinoPan
|
||||
|
||||
Binaural panning for stereo positioning:
|
||||
|
||||
```kotlin
|
||||
class BinoPan(var pan: Float) : AudioFilter {
|
||||
// pan: -1.0 (full left) to 1.0 (full right)
|
||||
|
||||
override fun process(samples: FloatArray, sampleRate: Int) {
|
||||
val leftGain = sqrt((1.0f - pan) / 2.0f)
|
||||
val rightGain = sqrt((1.0f + pan) / 2.0f)
|
||||
|
||||
for (i in samples.indices step 2) {
|
||||
val mono = (samples[i] + samples[i + 1]) / 2
|
||||
samples[i] = mono * leftGain // Left channel
|
||||
samples[i + 1] = mono * rightGain // Right channel
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Volume Envelope Processing
|
||||
|
||||
### Fade In/Out
|
||||
|
||||
```kotlin
|
||||
class VolumeEnvelope {
|
||||
var targetVolume: Float = 1.0f
|
||||
var currentVolume: Float = 0.0f
|
||||
var fadeSpeed: Float = 1.0f // Volume units per second
|
||||
|
||||
fun update(delta: Float) {
|
||||
val change = fadeSpeed * delta
|
||||
|
||||
currentVolume = when {
|
||||
currentVolume < targetVolume -> minOf(currentVolume + change, targetVolume)
|
||||
currentVolume > targetVolume -> maxOf(currentVolume - change, targetVolume)
|
||||
else -> currentVolume
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Track Allocation
|
||||
|
||||
### Dynamic Track Pool
|
||||
|
||||
```kotlin
|
||||
class AudioMixer {
|
||||
val dynamicTracks = Array(MAX_TRACKS) { TerrarumAudioMixerTrack(it) }
|
||||
|
||||
fun getAvailableTrack(): TerrarumAudioMixerTrack? {
|
||||
// Find stopped track
|
||||
dynamicTracks.firstOrNull { it.state == TrackState.STOPPED }?.let { return it }
|
||||
|
||||
// Find oldest playing track
|
||||
return dynamicTracks.minByOrNull { it.startTime }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Common Issues
|
||||
|
||||
### Buffer Underrun
|
||||
|
||||
Symptoms: Audio stuttering, crackling
|
||||
|
||||
Causes:
|
||||
- Update thread lagging
|
||||
- Heavy CPU load
|
||||
- Inefficient filters
|
||||
|
||||
Solutions:
|
||||
- Increase buffer size
|
||||
- Optimise filter algorithms
|
||||
- Reduce track count
|
||||
|
||||
### Clipping
|
||||
|
||||
Symptoms: Distorted, harsh sound
|
||||
|
||||
Causes:
|
||||
- Excessive volume
|
||||
- Too many overlapping sounds
|
||||
- Improper mixing
|
||||
|
||||
Solutions:
|
||||
- Normalise audio files
|
||||
- Implement limiter/compressor
|
||||
- Reduce master volume
|
||||
|
||||
### Latency
|
||||
|
||||
Symptoms: Delayed audio response
|
||||
|
||||
Causes:
|
||||
- Large buffer size
|
||||
- Slow update rate
|
||||
|
||||
Solutions:
|
||||
- Reduce buffer size
|
||||
- Increase update rate
|
||||
- Use hardware audio acceleration
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
1. **Hardware-accelerated DSP** — Use OpenAL EFX effects
|
||||
2. **Audio compression** — Reduce memory usage with codecs
|
||||
3. **Dynamic range compression** — Automatic volume normalisation
|
||||
4. **3D HRTF** — Head-related transfer functions for realistic 3D
|
||||
5. **Ambisonics** — Full spherical surround sound
|
||||
6. **Audio streaming from network** — Multiplayer voice chat
|
||||
7. **Procedural audio** — Generate sounds algorithmically
|
||||
|
||||
## See Also
|
||||
|
||||
- [[Audio System]] — User-facing audio API
|
||||
- [[Actors]] — Actor audio integration
|
||||
- [[Glossary]] — Audio terminology
|
||||
449
Audio-System.md
Normal file
449
Audio-System.md
Normal file
@@ -0,0 +1,449 @@
|
||||
# Audio System
|
||||
|
||||
Terrarum features a comprehensive audio engine with spatial sound, mixer buses, real-time effects processing, and advanced music playback capabilities.
|
||||
|
||||
## Overview
|
||||
|
||||
The audio system provides:
|
||||
- **AudioMixer** — Central audio management with mixer buses
|
||||
- **Spatial audio** — 3D positional sound
|
||||
- **Dynamic tracks** — Multiple simultaneous music/sound channels
|
||||
- **Effects processing** — Filters, reverb, panning, and more
|
||||
- **Music streaming** — Efficient playback of large music files
|
||||
- **Volume control** — Master, music, SFX, and UI volume
|
||||
|
||||
## Architecture
|
||||
|
||||
### AudioMixer
|
||||
|
||||
The `AudioMixer` singleton manages all audio output:
|
||||
|
||||
```kotlin
|
||||
object App {
|
||||
val audioMixer: AudioMixer
|
||||
}
|
||||
```
|
||||
|
||||
The mixer runs on a separate thread (`AudioManagerRunnable`) for low-latency audio processing.
|
||||
|
||||
### Audio Codex
|
||||
|
||||
All audio assets are registered in the `AudioCodex`:
|
||||
|
||||
```kotlin
|
||||
object AudioCodex {
|
||||
operator fun get(audioID: String): MusicContainer?
|
||||
}
|
||||
```
|
||||
|
||||
## Dynamic Tracks
|
||||
|
||||
The mixer provides multiple dynamic audio tracks:
|
||||
|
||||
```kotlin
|
||||
val dynamicTracks: Array<TerrarumAudioMixerTrack>
|
||||
```
|
||||
|
||||
Each track can play different audio with independent:
|
||||
- Volume control
|
||||
- Effects processing (filters)
|
||||
- Panning
|
||||
- Pitch shifting
|
||||
|
||||
### Track Count
|
||||
|
||||
The number of available tracks depends on configuration, typically:
|
||||
- **Music tracks:** 4-8 simultaneous music streams
|
||||
- **SFX tracks:** 16-32 sound effects
|
||||
|
||||
## Playing Audio
|
||||
|
||||
### Music Playback
|
||||
|
||||
```kotlin
|
||||
fun startMusic(music: MusicContainer, track: Int = 0) {
|
||||
val mixerTrack = App.audioMixer.dynamicTracks[track]
|
||||
mixerTrack.setMusic(music)
|
||||
mixerTrack.play()
|
||||
}
|
||||
```
|
||||
|
||||
### Sound Effects
|
||||
|
||||
Short sound effects use the same track system:
|
||||
|
||||
```kotlin
|
||||
fun playSound(sound: MusicContainer, volume: Float = 1.0f) {
|
||||
val track = App.audioMixer.getAvailableTrack()
|
||||
track.setMusic(sound)
|
||||
track.trackVolume = TrackVolume(volume, volume)
|
||||
track.play()
|
||||
}
|
||||
```
|
||||
|
||||
## Track Management
|
||||
|
||||
### TerrarumAudioMixerTrack
|
||||
|
||||
```kotlin
|
||||
class TerrarumAudioMixerTrack {
|
||||
var trackVolume: TrackVolume
|
||||
var filters: Array<AudioFilter>
|
||||
var trackingTarget: ActorWithBody? // For spatial audio
|
||||
|
||||
fun play()
|
||||
fun stop()
|
||||
fun pause()
|
||||
fun resume()
|
||||
}
|
||||
```
|
||||
|
||||
### Track States
|
||||
|
||||
- **Stopped** — Not playing
|
||||
- **Playing** — Currently playing
|
||||
- **Paused** — Paused, can resume
|
||||
- **Stopping** — Fading out
|
||||
|
||||
## Volume Control
|
||||
|
||||
### TrackVolume
|
||||
|
||||
Stereo volume control:
|
||||
|
||||
```kotlin
|
||||
class TrackVolume(
|
||||
var left: Float, // 0.0-1.0
|
||||
var right: Float // 0.0-1.0
|
||||
)
|
||||
```
|
||||
|
||||
### Master Volume
|
||||
|
||||
Global volume settings:
|
||||
|
||||
```kotlin
|
||||
App.audioMixer.masterVolume // Overall volume
|
||||
App.audioMixer.musicVolume // Music volume multiplier
|
||||
App.audioMixer.sfxVolume // SFX volume multiplier
|
||||
App.audioMixer.uiVolume // UI sounds volume multiplier
|
||||
```
|
||||
|
||||
Final track volume = `trackVolume * categoryVolume * masterVolume`
|
||||
|
||||
## Spatial Audio
|
||||
|
||||
### Positional Sound
|
||||
|
||||
Attach sound to an actor for 3D positioning:
|
||||
|
||||
```kotlin
|
||||
mixerTrack.trackingTarget = actor
|
||||
```
|
||||
|
||||
The mixer automatically:
|
||||
1. Calculates distance from listener
|
||||
2. Applies distance attenuation
|
||||
3. Calculates stereo panning
|
||||
4. Updates in real-time as actor moves
|
||||
|
||||
### 3D Audio Calculation
|
||||
|
||||
```kotlin
|
||||
val listener = playerActor // Usually the player
|
||||
val source = trackingTarget
|
||||
|
||||
val distance = source.position.dst(listener.position)
|
||||
val angle = atan2(
|
||||
source.position.y - listener.position.y,
|
||||
source.position.x - listener.position.x
|
||||
)
|
||||
|
||||
// Distance attenuation
|
||||
val attenuation = 1.0f / (1.0f + distance / referenceDistance)
|
||||
|
||||
// Stereo panning
|
||||
val pan = sin(angle) // -1.0 (left) to 1.0 (right)
|
||||
trackVolume.left = attenuation * (1.0f - max(0.0f, pan))
|
||||
trackVolume.right = attenuation * (1.0f + min(0.0f, pan))
|
||||
```
|
||||
|
||||
### Listener Position
|
||||
|
||||
The audio listener is typically the player's position:
|
||||
|
||||
```kotlin
|
||||
App.audioMixer.listenerPosition = player.hitbox.center
|
||||
```
|
||||
|
||||
## Audio Effects (DSP)
|
||||
|
||||
### Filter System
|
||||
|
||||
Each track has two filter slots:
|
||||
|
||||
```kotlin
|
||||
val filters: Array<AudioFilter> = arrayOf(NullFilter, NullFilter)
|
||||
```
|
||||
|
||||
### Built-in Filters
|
||||
|
||||
- **NullFilter** — No effect (bypass)
|
||||
- **BinoPan** — Binaural panning
|
||||
- **LowPassFilter** — Reduces high frequencies
|
||||
- **HighPassFilter** — Reduces low frequencies
|
||||
- **ReverbFilter** — Room ambience
|
||||
- **EchoFilter** — Delay effect
|
||||
|
||||
### Applying Filters
|
||||
|
||||
```kotlin
|
||||
import net.torvald.terrarum.audio.dsp.LowPassFilter
|
||||
|
||||
mixerTrack.filters[0] = LowPassFilter(cutoffFreq = 1000.0f)
|
||||
mixerTrack.filters[1] = ReverbFilter(roomSize = 0.5f, damping = 0.7f)
|
||||
```
|
||||
|
||||
### Filter Chaining
|
||||
|
||||
Filters process in order:
|
||||
|
||||
```
|
||||
Audio Source → Filter[0] → Filter[1] → Output
|
||||
```
|
||||
|
||||
### Removing Filters
|
||||
|
||||
```kotlin
|
||||
mixerTrack.filters[0] = NullFilter
|
||||
```
|
||||
|
||||
## Music Containers
|
||||
|
||||
### MusicContainer
|
||||
|
||||
Wraps audio assets for playback:
|
||||
|
||||
```kotlin
|
||||
class MusicContainer(
|
||||
val file: FileHandle,
|
||||
val format: AudioFormat
|
||||
)
|
||||
|
||||
enum class AudioFormat {
|
||||
OGG_VORBIS,
|
||||
WAV,
|
||||
MP3
|
||||
}
|
||||
```
|
||||
|
||||
### Loading Music
|
||||
|
||||
```kotlin
|
||||
val music = MusicContainer(
|
||||
Gdx.files.internal("audio/music/theme.ogg"),
|
||||
AudioFormat.OGG_VORBIS
|
||||
)
|
||||
|
||||
AudioCodex.register("theme_music", music)
|
||||
```
|
||||
|
||||
## Actor Audio Integration
|
||||
|
||||
Actors can manage their own audio:
|
||||
|
||||
```kotlin
|
||||
abstract class Actor {
|
||||
val musicTracks: HashMap<MusicContainer, TerrarumAudioMixerTrack>
|
||||
|
||||
fun startMusic(music: MusicContainer)
|
||||
fun stopMusic(music: MusicContainer)
|
||||
}
|
||||
```
|
||||
|
||||
### Automatic Cleanup
|
||||
|
||||
When actors despawn, their audio stops automatically:
|
||||
|
||||
```kotlin
|
||||
override fun despawn() {
|
||||
if (stopMusicOnDespawn) {
|
||||
musicTracks.forEach { (_, track) ->
|
||||
track.stop()
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Music Streaming
|
||||
|
||||
Large music files are streamed rather than loaded entirely:
|
||||
|
||||
```kotlin
|
||||
class MusicStreamer(
|
||||
val file: FileHandle,
|
||||
val bufferSize: Int = 4096
|
||||
) {
|
||||
fun readNextBuffer(): ByteArray
|
||||
}
|
||||
```
|
||||
|
||||
This allows playback of long music tracks without excessive memory usage.
|
||||
|
||||
## Audio Bank
|
||||
|
||||
The `AudioBank` manages audio asset loading and caching:
|
||||
|
||||
```kotlin
|
||||
object AudioBank {
|
||||
fun loadMusic(path: String): MusicContainer
|
||||
fun unloadMusic(musicID: String)
|
||||
}
|
||||
```
|
||||
|
||||
## Pitch Shifting
|
||||
|
||||
Tracks support pitch adjustment:
|
||||
|
||||
```kotlin
|
||||
mixerTrack.processor.streamBuf?.pitch = 1.5f // 1.5x speed (higher pitch)
|
||||
```
|
||||
|
||||
Pitch range: 0.5 (half speed) to 2.0 (double speed).
|
||||
|
||||
## Music Playlists
|
||||
|
||||
```kotlin
|
||||
class TerrarumMusicPlaylist {
|
||||
val tracks: List<MusicContainer>
|
||||
var currentIndex: Int
|
||||
var shuffleMode: Boolean
|
||||
|
||||
fun next()
|
||||
fun previous()
|
||||
fun shuffle()
|
||||
}
|
||||
```
|
||||
|
||||
Playlists handle music progression and looping, and allows audio processor to fetch the next track in gapless manner.
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Background Music
|
||||
|
||||
```kotlin
|
||||
fun playBackgroundMusic(musicID: String) {
|
||||
val music = AudioCodex[musicID]
|
||||
val track = App.audioMixer.dynamicTracks[0] // Reserve track 0 for BGM
|
||||
|
||||
track.setMusic(music)
|
||||
track.trackVolume = TrackVolume(
|
||||
App.audioMixer.musicVolume,
|
||||
App.audioMixer.musicVolume
|
||||
)
|
||||
track.play()
|
||||
}
|
||||
```
|
||||
|
||||
### UI Click Sound
|
||||
|
||||
```kotlin
|
||||
fun playUIClick() {
|
||||
val sound = AudioCodex["ui_click"]
|
||||
val track = App.audioMixer.getAvailableTrack()
|
||||
|
||||
track.setMusic(sound)
|
||||
track.trackVolume = TrackVolume(
|
||||
App.audioMixer.uiVolume,
|
||||
App.audioMixer.uiVolume
|
||||
)
|
||||
track.play()
|
||||
}
|
||||
```
|
||||
|
||||
### Positional Ambient Sound
|
||||
|
||||
```kotlin
|
||||
fun playAmbientSound(position: Vector2, sound: MusicContainer) {
|
||||
val track = App.audioMixer.getAvailableTrack()
|
||||
|
||||
// Create temporary actor at position for spatial audio
|
||||
val soundSource = object : ActorWithBody() {
|
||||
init {
|
||||
setPosition(position.x, position.y)
|
||||
}
|
||||
}
|
||||
|
||||
track.trackingTarget = soundSource
|
||||
track.setMusic(sound)
|
||||
track.play()
|
||||
}
|
||||
```
|
||||
|
||||
### Muffled Sound (Underwater Effect)
|
||||
|
||||
```kotlin
|
||||
fun applyUnderwaterEffect(track: TerrarumAudioMixerTrack) {
|
||||
track.filters[0] = LowPassFilter(cutoffFreq = 500.0f)
|
||||
track.filters[1] = ReverbFilter(roomSize = 0.8f, damping = 0.9f)
|
||||
}
|
||||
```
|
||||
|
||||
## Audio Format Requirements
|
||||
|
||||
### Music Files
|
||||
|
||||
- **Format:** OGG Vorbis recommended
|
||||
- **Quality:** `-q 10` (highest quality)
|
||||
- **Sample rate:** 48000 Hz
|
||||
- **Channels:** Stereo
|
||||
|
||||
### Sound Effects
|
||||
|
||||
- **Format:** OGG Vorbis or WAV
|
||||
- **Duration:** < 5 seconds (use streaming for longer)
|
||||
- **Sample rate:** 48000 Hz
|
||||
- **Channels:** Mono (will be positioned) or Stereo (will also be positioned but poorly)
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
1. **Limit simultaneous sounds** — Too many tracks cause CPU overhead
|
||||
2. **Use streaming for music** — Don't load entire files
|
||||
3. **Unload unused audio** — Free memory when not needed
|
||||
4. **Pool sound effect tracks** — Reuse tracks instead of creating new ones
|
||||
5. **Reduce filter complexity** — Heavy DSP effects impact performance
|
||||
|
||||
## Best Practises
|
||||
|
||||
1. **Reserve tracks for categories** — E.g., track 0 for BGM, 1-3 for ambient
|
||||
2. **Stop music on despawn** — Prevent audio leaks
|
||||
3. **Use spatial audio sparingly** — Not all sounds need positioning
|
||||
4. **Normalise audio levels** — Ensure consistent volume across files
|
||||
5. **Test with volume at 0** — Game should work without audio
|
||||
6. **Provide audio toggles** — Let users disable categories
|
||||
7. **Crossfade music transitions** — Avoid abrupt cuts
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### No Sound
|
||||
- Check master volume settings
|
||||
- Verify audio files are loaded
|
||||
- Ensure OpenAL is initialised
|
||||
- Check track availability
|
||||
|
||||
### Crackling/Popping
|
||||
- Increase buffer size
|
||||
- Reduce simultaneous tracks
|
||||
- Check for audio file corruption
|
||||
- Verify sample rate consistency
|
||||
|
||||
### Spatial Audio Not Working
|
||||
- Ensure `trackingTarget` is set
|
||||
- Verify listener position is updated
|
||||
- Check distance attenuation settings
|
||||
|
||||
## See Also
|
||||
|
||||
- [[Glossary]] — Audio terminology
|
||||
- [[Actors]] — Actor audio integration
|
||||
- [[Modules]] — Adding audio to modules
|
||||
539
Autotiling-In-Depth.md
Normal file
539
Autotiling-In-Depth.md
Normal file
@@ -0,0 +1,539 @@
|
||||
# Autotiling In-Depth (Engine Internals)
|
||||
|
||||
**Audience:** Engine maintainers implementing or modifying the autotiling algorithm.
|
||||
|
||||
This document describes the internal implementation of Terrarum's autotiling system, including the connection algorithms, lookup tables, and subtiling patterns.
|
||||
|
||||
## Overview
|
||||
|
||||
Terrarum's autotiling system automatically selects appropriate tile sprites based on neighbouring tiles, creating seamless connections for blocks like terrain, ores, and platforms.
|
||||
|
||||
The system supports:
|
||||
- **47-tile autotiling** — Full tileset with all connection variants
|
||||
- **16-tile platforms** — Simplified platform connections
|
||||
- **Subtile autotiling** — 8×8 pixel subdivisions for advanced patterns
|
||||
- **Connection modes** — Self-connection and mutual connection
|
||||
- **Brick patterns** — Alternative tiling modes for brick-like textures
|
||||
|
||||
## Connection Detection
|
||||
|
||||
### 8-Neighbour System
|
||||
|
||||
Autotiling examines the **8 surrounding tiles** (Moore neighbourhood):
|
||||
|
||||
```
|
||||
┌───┬───┬───┐
|
||||
│ 5 │ 6 │ 7 │
|
||||
├───┼───┼───┤
|
||||
│ 4 │ @ │ 0 │ @ = Current tile
|
||||
├───┼───┼───┤
|
||||
│ 3 │ 2 │ 1 │
|
||||
└───┴───┴───┘
|
||||
```
|
||||
|
||||
Each neighbour position maps to a bit in an 8-bit bitmask:
|
||||
|
||||
```kotlin
|
||||
// Bit positions
|
||||
val RIGHT = 0 // bit 0 (LSB)
|
||||
val BOTTOM_RIGHT = 1
|
||||
val BOTTOM = 2
|
||||
val BOTTOM_LEFT = 3
|
||||
val LEFT = 4
|
||||
val TOP_LEFT = 5
|
||||
val TOP = 6
|
||||
val TOP_RIGHT = 7 // bit 7 (MSB)
|
||||
```
|
||||
|
||||
### Bitmask Generation
|
||||
|
||||
```kotlin
|
||||
// Get neighbour positions (8 surrounding tiles)
|
||||
private fun getNearbyTilesPos(x: Int, y: Int): Array<Point2i> {
|
||||
return arrayOf(
|
||||
Point2i(x + 1, y), // 0: RIGHT
|
||||
Point2i(x + 1, y + 1), // 1: BOTTOM_RIGHT
|
||||
Point2i(x, y + 1), // 2: BOTTOM
|
||||
Point2i(x - 1, y + 1), // 3: BOTTOM_LEFT
|
||||
Point2i(x - 1, y), // 4: LEFT
|
||||
Point2i(x - 1, y - 1), // 5: TOP_LEFT
|
||||
Point2i(x, y - 1), // 6: TOP
|
||||
Point2i(x + 1, y - 1) // 7: TOP_RIGHT
|
||||
)
|
||||
}
|
||||
|
||||
// Calculate bitmask for CONNECT_SELF tiles
|
||||
private fun getNearbyTilesInfoConSelf(x: Int, y: Int, mode: Int, mark: ItemID?): Int {
|
||||
val nearbyTiles = getNearbyTilesPos(x, y).map { world.getTileFrom(mode, it.x, it.y) }
|
||||
|
||||
var ret = 0
|
||||
for (i in nearbyTiles.indices) {
|
||||
if (nearbyTiles[i] == mark) {
|
||||
ret += (1 shl i) // add 1, 2, 4, 8, etc. for i = 0, 1, 2, 3...
|
||||
}
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
```
|
||||
|
||||
### Connection Rules
|
||||
|
||||
#### CONNECT_SELF
|
||||
|
||||
Tiles connect only to identical tiles. The actual implementation from `BlocksDrawer.kt`:
|
||||
|
||||
```kotlin
|
||||
private fun getNearbyTilesInfoConSelf(x: Int, y: Int, mode: Int, mark: ItemID?): Int {
|
||||
val nearbyTiles = getNearbyTilesPos(x, y).map { world.getTileFrom(mode, it.x, it.y) }
|
||||
|
||||
var ret = 0
|
||||
for (i in nearbyTiles.indices) {
|
||||
if (nearbyTiles[i] == mark) { // Only connect to identical tiles
|
||||
ret += (1 shl i)
|
||||
}
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
```
|
||||
|
||||
**Use cases:**
|
||||
- Individual block types that don't blend with others
|
||||
- Special decorative blocks
|
||||
- Unique terrain features
|
||||
|
||||
#### CONNECT_MUTUAL
|
||||
|
||||
Tiles connect to any solid tile with the same connection tag. The actual implementation from `BlocksDrawer.kt`:
|
||||
|
||||
```kotlin
|
||||
private fun getNearbyTilesInfoConMutual(x: Int, y: Int, mode: Int): Int {
|
||||
val mode = if (mode == TERRAIN_WALLSTICKER) TERRAIN else mode
|
||||
|
||||
val nearbyTiles: List<ItemID> = getNearbyTilesPos(x, y).map { world.getTileFrom(mode, it.x, it.y) }
|
||||
|
||||
var ret = 0
|
||||
for (i in nearbyTiles.indices) {
|
||||
// Connect to solid tiles that are also marked CONNECT_MUTUAL
|
||||
if (BlockCodex[nearbyTiles[i]].isSolidForTileCnx && isConnectMutual(nearbyTiles[i])) {
|
||||
ret += (1 shl i)
|
||||
}
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
```
|
||||
|
||||
**Use cases:**
|
||||
- Dirt, stone, and similar terrain blocks
|
||||
- All blocks tagged as "connect mutually" blend together
|
||||
- Creates natural-looking transitions
|
||||
|
||||
## 47-Tile Autotiling
|
||||
|
||||
### Tile Positions
|
||||
|
||||
The 112×112 texture atlas stores 47 tile variants in a 7×7 grid:
|
||||
|
||||
```
|
||||
Grid layout (each cell is 16×16 pixels):
|
||||
|
||||
0 1 2 3 4 5 6
|
||||
7 8 9 10 11 12 13
|
||||
14 15 16 17 18 19 20
|
||||
21 22 23 24 25 26 27
|
||||
28 29 30 31 32 33 34
|
||||
35 36 37 38 39 40 BB
|
||||
41 42 43 44 45 46 **
|
||||
```
|
||||
|
||||
Position (6,5) is reserved for the barcode. Positions 47 is unused.
|
||||
|
||||
### Connection Lookup Table
|
||||
|
||||
The `connectLut47` maps 8-bit connection bitmasks to tile indices:
|
||||
|
||||
```kotlin
|
||||
val connectLut47 = intArrayOf(
|
||||
0, 1, 2, 3, 4, 5, 6, 7, // 0x00-0x07
|
||||
8, 9, 10, 11, 12, 13, 14, 15, // 0x08-0x0F
|
||||
// ... 256 entries total
|
||||
)
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```kotlin
|
||||
val bitmask = 0b11010110 // Connected top, top-right, left, bottom-right, bottom
|
||||
val tileIndex = connectLut47[bitmask] // Returns appropriate tile variant
|
||||
```
|
||||
|
||||
### Pre-calculated LUT
|
||||
|
||||
The lookup table is **pre-calculated and hard-coded** in `BlocksDrawer.kt`. The actual 256-entry array (from `BlocksDrawer.kt:188`):
|
||||
|
||||
```kotlin
|
||||
val connectLut47 = intArrayOf(
|
||||
17,1,17,1,2,3,2,14,17,1,17,1,2,3,2,14,9,7,9,7,4,5,4,35,9,7,9,7,16,37,16,15,
|
||||
17,1,17,1,2,3,2,14,17,1,17,1,2,3,2,14,9,7,9,7,4,5,4,35,9,7,9,7,16,37,16,15,
|
||||
8,10,8,10,0,12,0,43,8,10,8,10,0,12,0,43,11,13,11,13,6,20,6,34,11,13,11,13,36,33,36,46,
|
||||
8,10,8,10,0,12,0,43,8,10,8,10,0,12,0,43,30,42,30,42,38,26,38,18,30,42,30,42,23,45,23,31,
|
||||
17,1,17,1,2,3,2,14,17,1,17,1,2,3,2,14,9,7,9,7,4,5,4,35,9,7,9,7,16,37,16,15,
|
||||
17,1,17,1,2,3,2,14,17,1,17,1,2,3,2,14,9,7,9,7,4,5,4,35,9,7,9,7,16,37,16,15,
|
||||
8,28,8,28,0,41,0,21,8,28,8,28,0,41,0,21,11,44,11,44,6,27,6,40,11,44,11,44,36,19,36,32,
|
||||
8,28,8,28,0,41,0,21,8,28,8,28,0,41,0,21,30,29,30,29,38,39,38,25,30,29,30,29,23,24,23,22
|
||||
)
|
||||
```
|
||||
|
||||
Each index (0-255) represents a possible 8-bit connection bitmask, and the value at that index is the tile number (0-46) to use from the 47-tile atlas. The exact mapping was designed manually following the visual design in `work_files/dynamic_shape_2_0.psd`.
|
||||
|
||||
## 16-Tile Platform Autotiling
|
||||
|
||||
### Platform Connections
|
||||
|
||||
Platforms only connect horizontally:
|
||||
|
||||
```
|
||||
Tile layout (128×16 = 8 tiles × 16 pixels):
|
||||
|
||||
0: Middle segment
|
||||
1: Right end
|
||||
2: Left end
|
||||
3: Planted on left (middle)
|
||||
4: Planted on left (end)
|
||||
5: Planted on right (middle)
|
||||
6: Planted on right (end)
|
||||
7: Single piece (isolated)
|
||||
```
|
||||
|
||||
### Platform Connection Algorithm
|
||||
|
||||
```kotlin
|
||||
fun getPlatformTileIndex(x: Int, y: Int, blockID: ItemID): Int {
|
||||
val hasLeft = isConnectedHorizontally(x - 1, y, blockID)
|
||||
val hasRight = isConnectedHorizontally(x + 1, y, blockID)
|
||||
val hasBlockBelow = isSolidBlock(x, y + 1)
|
||||
val hasBlockBelowLeft = isSolidBlock(x - 1, y + 1)
|
||||
val hasBlockBelowRight = isSolidBlock(x + 1, y + 1)
|
||||
|
||||
return when {
|
||||
!hasLeft && !hasRight -> 7 // Single piece
|
||||
hasLeft && !hasRight -> 1 // Right end
|
||||
!hasLeft && hasRight -> 2 // Left end
|
||||
hasBlockBelow -> when {
|
||||
hasLeft && hasRight -> 0 // Middle
|
||||
// ... planted variants
|
||||
}
|
||||
else -> 0 // Middle segment
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Platform LUT
|
||||
|
||||
```kotlin
|
||||
val connectLut16 = intArrayOf(
|
||||
7, 2, 1, 0, // Isolated, left-connected, right-connected, both
|
||||
// ... 256 entries for all combinations
|
||||
)
|
||||
```
|
||||
|
||||
## Subtiling System
|
||||
|
||||
### Subtile Resolution
|
||||
|
||||
Subtiles are **8×8 pixels** (half of standard 16×16 tiles):
|
||||
|
||||
```
|
||||
Standard tile subdivided:
|
||||
┌────┬────┐
|
||||
│ TL │ TR │ 8×8 each
|
||||
├────┼────┤
|
||||
│ BL │ BR │
|
||||
└────┴────┘
|
||||
```
|
||||
|
||||
Subtile indices: **0 = Top-Left, 1 = Top-Right, 2 = Bottom-Right, 3 = Bottom-Left**
|
||||
|
||||
### Subtile Variant LUTs
|
||||
|
||||
Four lookup tables determine subtile variants based on connection patterns:
|
||||
|
||||
```kotlin
|
||||
val subtileVarBaseLuts = arrayOf(
|
||||
// TL subtile variants (47 entries)
|
||||
intArrayOf(10,2,2,2,1,1,3,1,10,1,10,3,10,3,2,1,1,2,0,3,3,10,0,0,0,0,0,3,10,0,0,0,3,3,3,1,3,1,0,0,3,10,0,10,3,0,3),
|
||||
// TR subtile variants (47 entries)
|
||||
intArrayOf(4,1,5,1,5,1,4,1,4,5,6,4,6,6,1,1,5,5,6,0,6,0,0,4,0,0,6,0,0,0,4,6,0,6,6,1,4,1,4,0,0,0,6,6,0,6,6),
|
||||
// BR subtile variants (47 entries)
|
||||
intArrayOf(4,7,4,9,4,9,4,7,8,8,7,8,9,7,0,0,4,8,0,9,9,0,0,4,9,0,9,9,7,7,8,0,0,9,0,0,4,9,4,9,0,9,7,0,7,9,0),
|
||||
// BL subtile variants (47 entries)
|
||||
intArrayOf(10,11,10,10,12,12,12,7,11,7,11,7,10,7,10,0,0,11,12,0,12,10,0,0,0,12,12,12,11,7,7,0,0,0,12,12,0,0,12,12,12,10,7,10,7,0,0),
|
||||
)
|
||||
```
|
||||
|
||||
Each array maps from the 47-tile connection index to a subtile variant (0-12).
|
||||
|
||||
### Subtile Reorientation
|
||||
|
||||
Subtiles can be flipped/rotated to create more variations:
|
||||
|
||||
```kotlin
|
||||
val subtileReorientLUT = arrayOf(
|
||||
// For each subtile position (TL, TR, BR, BL):
|
||||
// Map variant 0-15 to (rotation, flip) commands
|
||||
)
|
||||
```
|
||||
|
||||
### Subtile Rendering
|
||||
|
||||
```kotlin
|
||||
fun renderSubtiledTile(x: Int, y: Int, blockID: ItemID) {
|
||||
val connectionIndex = get47TileIndex(x, y, blockID)
|
||||
|
||||
for (subtileIndex in 0 until 4) {
|
||||
val variantIndex = subtileVarBaseLuts[subtileIndex][connectionIndex]
|
||||
val (rotation, flip) = subtileReorientLUT[subtileIndex][variantIndex]
|
||||
|
||||
val subtileTexture = getSubtileTexture(variantIndex)
|
||||
val (sx, sy) = subtileOffsets[subtileIndex]
|
||||
|
||||
renderSubtile(
|
||||
subtileTexture,
|
||||
x * TILE_SIZE + sx,
|
||||
y * TILE_SIZE + sy,
|
||||
rotation, flip
|
||||
)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This composites four 8×8 subtiles into each 16×16 tile position.
|
||||
|
||||
## Brick Patterns
|
||||
|
||||
### Tiling Modes
|
||||
|
||||
```kotlin
|
||||
const val TILING_FULL = 0 // Standard 47-tile autotiling
|
||||
const val TILING_FULL_NOFLIP = 1 // No flipping (for asymmetric textures)
|
||||
const val TILING_BRICK_SMALL = 2 // Small brick pattern
|
||||
const val TILING_BRICK_SMALL_NOFLIP = 3
|
||||
const val TILING_BRICK_LARGE = 4 // Large brick pattern
|
||||
const val TILING_BRICK_LARGE_NOFLIP = 5
|
||||
```
|
||||
|
||||
### Brick Reorientation
|
||||
|
||||
Brick patterns use different subtile remapping:
|
||||
|
||||
```kotlin
|
||||
val tilingModeReorientLUTs = arrayOf(
|
||||
// TILING_FULL
|
||||
arrayOf(16 to 0, 16 to 0, 16 to 0, 16 to 0),
|
||||
|
||||
// TILING_BRICK_SMALL
|
||||
arrayOf(8 to 0, 8 to 8, 8 to 8, 8 to 0), // (modulo, offset) per subtile
|
||||
|
||||
// TILING_BRICK_LARGE
|
||||
arrayOf(8 to 8, 8 to 0, 8 to 8, 8 to 0),
|
||||
)
|
||||
```
|
||||
|
||||
#### Brick Formula
|
||||
|
||||
```kotlin
|
||||
fun getBrickSubtileVariant(baseVariant: Int, subtilePos: Int, mode: Int): Int {
|
||||
val (modulo, offset) = tilingModeReorientLUTs[mode][subtilePos]
|
||||
return (baseVariant % modulo) + offset
|
||||
}
|
||||
```
|
||||
|
||||
This creates brick-like patterns by cycling through specific subtile ranges.
|
||||
|
||||
## Wall Stickers
|
||||
|
||||
### 4-Tile Format
|
||||
|
||||
Wall stickers (torches, paintings) use a simplified 64×16 atlas:
|
||||
|
||||
```
|
||||
0: Free-floating
|
||||
1: Planted on left wall
|
||||
2: Planted on right wall
|
||||
3: Planted on bottom (ceiling)
|
||||
```
|
||||
|
||||
### Wall Sticker Algorithm
|
||||
|
||||
```kotlin
|
||||
fun getWallStickerTile(x: Int, y: Int): Int {
|
||||
val hasLeft = isSolidWall(x - 1, y)
|
||||
val hasRight = isSolidWall(x + 1, y)
|
||||
val hasBottom = isSolidWall(x, y + 1)
|
||||
|
||||
return when {
|
||||
hasLeft -> 1
|
||||
hasRight -> 2
|
||||
hasBottom -> 3
|
||||
else -> 0 // Free-floating
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Occlusion and Corner Darkening
|
||||
|
||||
**NOTE: Corner occlusion now uses shader-based approach. Following section is obsolete and preserved for historical reasons.**
|
||||
|
||||
### Occlusion Tiles
|
||||
|
||||
Corner occlusion uses a separate tile set stored at a reserved atlas position:
|
||||
|
||||
```kotlin
|
||||
val OCCLUSION_TILE_NUM_BASE = 48 // Starting position in atlas
|
||||
```
|
||||
|
||||
### Occlusion Bitmask
|
||||
|
||||
Similar to regular autotiling, but affects lighting:
|
||||
|
||||
```kotlin
|
||||
fun calculateOcclusionMask(x: Int, y: Int): Int {
|
||||
var mask = 0
|
||||
|
||||
for (i in 0 until 8) {
|
||||
val (nx, ny) = getNeighbourPosition(x, y, i)
|
||||
if (isOccluding(nx, ny)) {
|
||||
mask = mask or (1 shl i)
|
||||
}
|
||||
}
|
||||
|
||||
return connectLut47[mask] // Reuse autotiling LUT
|
||||
}
|
||||
```
|
||||
|
||||
### Occlusion Rendering
|
||||
|
||||
Rendered as a dark overlay using multiplicative blending:
|
||||
|
||||
```kotlin
|
||||
batch.setBlendFunction(GL_DST_COLOR, GL_ZERO) // Multiplicative
|
||||
batch.setColor(0.5f, 0.5f, 0.5f, 1f) // Darken by 50%
|
||||
batch.draw(occlusionTexture, x, y)
|
||||
batch.setBlendFunction(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) // Reset
|
||||
```
|
||||
|
||||
## Performance Optimisations
|
||||
|
||||
### Pre-Calculated Tile Lists
|
||||
|
||||
The engine pre-calculates lists of tile numbers by connection type:
|
||||
|
||||
```kotlin
|
||||
lateinit var connectMutualTiles: Array<Int>
|
||||
lateinit var connectSelfTiles: Array<Int>
|
||||
lateinit var platformTiles: Array<Int>
|
||||
lateinit var wallStickerTiles: Array<Int>
|
||||
```
|
||||
|
||||
**Binary search** determines tile type in O(log n):
|
||||
|
||||
```kotlin
|
||||
fun isConnectMutual(tileNum: Int): Boolean {
|
||||
return connectMutualTiles.binarySearch(tileNum) >= 0
|
||||
}
|
||||
```
|
||||
|
||||
### Caching
|
||||
|
||||
Connection bitmasks are cached per-tile:
|
||||
|
||||
```kotlin
|
||||
private val connectionCache = HashMap<Long, Int>() // BlockAddress -> Bitmask
|
||||
|
||||
fun getCachedConnection(x: Int, y: Int): Int? {
|
||||
val address = (y.toLong() shl 32) or x.toLong()
|
||||
return connectionCache[address]
|
||||
}
|
||||
```
|
||||
|
||||
Cache invalidates when blocks change.
|
||||
|
||||
### Shader-Based Autotiling
|
||||
|
||||
**Future enhancement:** Move autotiling to GPU shaders:
|
||||
|
||||
```glsl
|
||||
// Vertex shader passes world coordinates
|
||||
v_worldPos = vec2(x, y);
|
||||
|
||||
// Fragment shader samples neighbours and selects tile
|
||||
int connections = sampleNeighbours(v_worldPos);
|
||||
int tileIndex = connectLut[connections];
|
||||
vec2 atlasUV = getTileUV(tileIndex);
|
||||
fragColor = texture(u_atlas, atlasUV);
|
||||
```
|
||||
|
||||
Benefits:
|
||||
- Offload CPU work to GPU
|
||||
- Dynamic retiling without rebuilding geometry
|
||||
- Per-pixel precision
|
||||
|
||||
## Debugging
|
||||
|
||||
### Visualise Connection Bitmask
|
||||
|
||||
```kotlin
|
||||
fun debugRenderConnections(x: Int, y: Int) {
|
||||
val mask = calculateConnectionBitmask(x, y, blockID)
|
||||
|
||||
for (i in 0 until 8) {
|
||||
if ((mask and (1 shl i)) != 0) {
|
||||
val (nx, ny) = getNeighbourPosition(x, y, i)
|
||||
shapeRenderer.line(
|
||||
x * TILE_SIZE + 8, y * TILE_SIZE + 8,
|
||||
nx * TILE_SIZE + 8, ny * TILE_SIZE + 8
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Verify LUT Completeness
|
||||
|
||||
All 256 bitmask values must have mappings:
|
||||
|
||||
```kotlin
|
||||
fun verifyLUT(lut: IntArray) {
|
||||
require(lut.size == 256) { "LUT must have 256 entries" }
|
||||
|
||||
for (i in lut.indices) {
|
||||
require(lut[i] in 0 until 47) { "Invalid tile index ${lut[i]} at position $i" }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
1. **Incorrect bit ordering** — Ensure neighbour positions match bit positions
|
||||
2. **Self vs. Mutual confusion** — Check connection type before comparing
|
||||
3. **Cache invalidation** — Update cache when blocks change
|
||||
4. **Off-by-one in LUT** — 256 entries (0-255), not 255
|
||||
5. **Subtile index ordering** — Remember: TL=0, TR=1, BR=2, BL=3
|
||||
6. **Forgetting corners** — Diagonal neighbours affect corner tiles
|
||||
7. **Platform direction** — Platforms only connect horizontally
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
1. **3D autotiling** — Connect blocks in vertical layers
|
||||
2. **Gradient transitions** — Smooth colour blending between terrain types
|
||||
3. **Animated autotiles** — Time-varying tile selection
|
||||
4. **Smart slopes** — Automatic slope generation at edges
|
||||
5. **Isometric support** — Adapt algorithm for isometric views
|
||||
6. **Custom connection rules** — Per-block connection predicates
|
||||
|
||||
## See Also
|
||||
|
||||
- [[Tile Atlas System]] — Atlas generation and seasonal blending
|
||||
- [[Rendering Pipeline]] — How autotiled tiles are rendered
|
||||
- [[Modules:Blocks]] — Creating autotiled block textures
|
||||
655
Blocks.md
Normal file
655
Blocks.md
Normal file
@@ -0,0 +1,655 @@
|
||||
# Blocks
|
||||
|
||||
**Audience:** Module developers creating terrain, walls, and decorative blocks.
|
||||
|
||||
Blocks are the fundamental building units of Terrarum's voxel world. This guide covers the block system, properties, rendering, autotiling, and creating custom blocks.
|
||||
|
||||
## Overview
|
||||
|
||||
Blocks provide:
|
||||
- **Terrain tiles** — Ground, stone, dirt, ores
|
||||
- **Wall tiles** — Backgrounds, structures, buildings
|
||||
- **Physics properties** — Collision, friction, density
|
||||
- **Visual properties** — Colour, luminosity, reflectance
|
||||
- **Material properties** — Strength, material composition
|
||||
- **Autotiling** — Automatic visual connections between adjacent blocks
|
||||
- **Dynamic properties** — Animated/flickering lights, seasonal variation
|
||||
|
||||
## Block System Architecture
|
||||
|
||||
### BlockProp Class
|
||||
|
||||
**Note:** You don't implement `BlockProp` yourself — it's automatically generated by `ModMgr.GameBlockLoader` at runtime from CSV data.
|
||||
|
||||
Each block is represented by a `BlockProp` instance containing all properties:
|
||||
|
||||
```kotlin
|
||||
class BlockProp : TaggedProp {
|
||||
var id: ItemID = ""
|
||||
var numericID: Int = -1
|
||||
var nameKey: String = ""
|
||||
|
||||
// Visual properties
|
||||
var shadeColR = 0f // Red shade (0.0-1.0)
|
||||
var shadeColG = 0f // Green shade
|
||||
var shadeColB = 0f // Blue shade
|
||||
var shadeColA = 0f // UV shade
|
||||
var opacity = Cvec() // Opacity per channel
|
||||
|
||||
// Luminosity
|
||||
internal var baseLumColR = 0f
|
||||
internal var baseLumColG = 0f
|
||||
internal var baseLumColB = 0f
|
||||
internal var baseLumColA = 0f
|
||||
|
||||
// Physical properties
|
||||
var strength: Int = 0 // Mining difficulty (HP)
|
||||
var density: Int = 0 // Mass density (kg/m³)
|
||||
var isSolid: Boolean = false
|
||||
var isPlatform: Boolean = false
|
||||
var friction: Int = 0 // Horizontal friction
|
||||
|
||||
// Material & drops
|
||||
var material: String = "" // 4-letter material code
|
||||
var drop: ItemID = "" // Item dropped when mined
|
||||
var world: ItemID = "" // Item used to place block
|
||||
|
||||
// Advanced
|
||||
var dynamicLuminosityFunction: Int = 0 // 0=static, >0=animated
|
||||
var reflectance = 0f // Light reflectance (0.0-1.0)
|
||||
var maxSupport: Int = -1 // Structural support limit
|
||||
|
||||
var tags = HashSet<String>()
|
||||
val extra = Codex() // Extra module data
|
||||
}
|
||||
```
|
||||
|
||||
### BlockCodex Singleton
|
||||
|
||||
All blocks are registered in the global `BlockCodex`:
|
||||
|
||||
```kotlin
|
||||
class BlockCodex {
|
||||
val blockProps = HashMap<ItemID, BlockProp>()
|
||||
val dynamicLights = SortedArrayList<ItemID>()
|
||||
val tileToVirtual = HashMap<ItemID, List<ItemID>>()
|
||||
val virtualToTile = HashMap<ItemID, ItemID>()
|
||||
|
||||
operator fun get(id: ItemID?): BlockProp
|
||||
fun fromModule(module: String, path: String, blockRegisterHook: (BlockProp) -> Unit)
|
||||
}
|
||||
```
|
||||
|
||||
**Access pattern:**
|
||||
|
||||
```kotlin
|
||||
val stone = BlockCodex["basegame:2"]
|
||||
println("Strength: ${stone.strength}")
|
||||
println("Density: ${stone.density} kg/m³")
|
||||
println("Is solid: ${stone.isSolid}")
|
||||
```
|
||||
|
||||
## Block Types
|
||||
|
||||
### Terrain Tiles
|
||||
|
||||
Solid blocks that make up the ground:
|
||||
|
||||
```kotlin
|
||||
val dirt = BlockCodex["basegame:3"]
|
||||
dirt.isSolid = true
|
||||
dirt.isWall = false // Terrain, not wall
|
||||
dirt.material = "SOIL"
|
||||
```
|
||||
|
||||
**Properties:**
|
||||
- `isSolid = true` — Blocks movement
|
||||
- Wall flag = `0` — Terrain tile
|
||||
- Friction affects horizontal movement
|
||||
- Density determines weight
|
||||
|
||||
### Wall Tiles
|
||||
|
||||
Background blocks for structures:
|
||||
|
||||
```kotlin
|
||||
val stoneWall = BlockCodex["basegame:131"]
|
||||
stoneWall.isSolid = false // Walls don't block movement
|
||||
stoneWall.isWall = true // Render behind terrain
|
||||
stoneWall.material = "ROCK"
|
||||
```
|
||||
|
||||
**Properties:**
|
||||
- `isSolid = false` — Passable
|
||||
- Wall flag = `1` — Background tile
|
||||
- Lower opacity than terrain
|
||||
- No collision physics
|
||||
|
||||
### Platform Blocks
|
||||
|
||||
Special blocks that allow one-way passage:
|
||||
|
||||
```kotlin
|
||||
val woodPlatform = BlockCodex["basegame:45"]
|
||||
woodPlatform.isSolid = true
|
||||
woodPlatform.isPlatform = true // Can jump through from below
|
||||
```
|
||||
|
||||
**Behaviour:**
|
||||
- Players can jump up through platforms
|
||||
- Players stand on top of platforms
|
||||
- Can pass through from below/sides
|
||||
|
||||
### Actor Blocks
|
||||
|
||||
Blocks that are actually actors (trees, plants):
|
||||
|
||||
```kotlin
|
||||
val tree = BlockCodex["basegame:80"]
|
||||
tree.isActorBlock = true // Not a real tile, just looks like one
|
||||
tree.isSolid = false // Actors handle collision
|
||||
```
|
||||
|
||||
**Uses:**
|
||||
- Trees with complex behaviour
|
||||
- Plants that grow
|
||||
- Animated decorations
|
||||
- Blocks that need per-instance state
|
||||
|
||||
## Block Properties
|
||||
|
||||
### Visual Properties
|
||||
|
||||
#### Shade Colour
|
||||
|
||||
Tints the block texture:
|
||||
|
||||
```csv
|
||||
"shdr";"shdg";"shdb";"shduv"
|
||||
"0.6290";"0.6290";"0.6290";"0.6290"
|
||||
```
|
||||
|
||||
- RGB channels for visible light
|
||||
- UV channel for fluorescence
|
||||
- Values 0.0 (black) to 1.0 (full brightness)
|
||||
|
||||
#### Luminosity
|
||||
|
||||
Blocks can emit light:
|
||||
|
||||
```csv
|
||||
"lumr";"lumg";"lumb";"lumuv"
|
||||
"0.7664";"0.2032";"0.0000";"0.0000"
|
||||
```
|
||||
|
||||
**Example: Lava**
|
||||
- Red: 0.7664 (bright orange-red)
|
||||
- Green: 0.2032 (dim)
|
||||
- Blue: 0.0 (none)
|
||||
- UV: 0.0 (none)
|
||||
|
||||
**Dynamic luminosity:**
|
||||
|
||||
```kotlin
|
||||
var dynamicLuminosityFunction: Int = 0
|
||||
```
|
||||
|
||||
- `0` — Static light
|
||||
- `1-7` — Various flicker/pulse patterns
|
||||
- Used for torches, lava, glowstone
|
||||
|
||||
#### Reflectance
|
||||
|
||||
Surface reflectivity for lighting:
|
||||
|
||||
```csv
|
||||
"refl"
|
||||
"0.8"
|
||||
```
|
||||
|
||||
- `0.0` — Fully absorbing (dark surfaces)
|
||||
- `1.0` — Fully reflective (mirrors, metal)
|
||||
- Affects how light bounces off surfaces
|
||||
|
||||
### Physical Properties
|
||||
|
||||
#### Strength (Mining Difficulty)
|
||||
|
||||
Hit points required to break block:
|
||||
|
||||
```csv
|
||||
"str"
|
||||
"120"
|
||||
```
|
||||
|
||||
**Common values:**
|
||||
- `1` — Air (instant)
|
||||
- `50` — Dirt (easy)
|
||||
- `120` — Stone (normal)
|
||||
- `500` — Obsidian (hard)
|
||||
- `10000` — Bedrock (unbreakable)
|
||||
|
||||
#### Density
|
||||
|
||||
Mass per cubic metre:
|
||||
|
||||
```csv
|
||||
"dsty"
|
||||
"2600"
|
||||
```
|
||||
|
||||
**Common densities:**
|
||||
- Air: 1 kg/m³
|
||||
- Wood: 800 kg/m³
|
||||
- Stone: 2600 kg/m³
|
||||
- Iron ore: 7800 kg/m³
|
||||
- Gold ore: 19300 kg/m³
|
||||
|
||||
#### Friction
|
||||
|
||||
Horizontal movement resistance:
|
||||
|
||||
```csv
|
||||
"fr"
|
||||
"16"
|
||||
```
|
||||
|
||||
**Values:**
|
||||
- `0` — Frictionless (ice)
|
||||
- `4` — Low friction (polished)
|
||||
- `16` — Normal friction (stone, dirt)
|
||||
- `32` — High friction (rough)
|
||||
|
||||
### Material Assignment
|
||||
|
||||
Every block has a material ID:
|
||||
|
||||
```csv
|
||||
"mate"
|
||||
"ROCK"
|
||||
```
|
||||
|
||||
Materials define:
|
||||
- Physical properties (hardness, density)
|
||||
- Sound effects (footsteps, impacts)
|
||||
- Tool effectiveness
|
||||
- Thermal properties
|
||||
|
||||
**Common materials:**
|
||||
- `AIIR` — Air
|
||||
- `DIRT` — Dirt, sand
|
||||
- `ROCK` — Stone, ores
|
||||
- `WOOD` — Wooden blocks
|
||||
- `WATR` — Water
|
||||
|
||||
See [[Modules-Codex-Systems#MaterialCodex]] for full material properties.
|
||||
|
||||
## Block Tags
|
||||
|
||||
Tags enable flexible queries and categorisation:
|
||||
|
||||
```csv
|
||||
"tags"
|
||||
"STONE,NATURAL,MINERAL"
|
||||
```
|
||||
|
||||
### Common Tags
|
||||
|
||||
**Terrain types:**
|
||||
- `DIRT` — Dirt, sand, clay
|
||||
- `STONE` — Rock, ores
|
||||
- `WOOD` — Wooden planks
|
||||
- `METAL` — Metal blocks
|
||||
|
||||
**Placement:**
|
||||
- `NATURAL` — Generated naturally
|
||||
- `ARTIFICIAL` — Player-created
|
||||
- `INCONSEQUENTIAL` — Doesn't affect world generation
|
||||
|
||||
**Behaviour:**
|
||||
- `TREE` — Tree blocks (special handling)
|
||||
- `ORE` — Ore blocks
|
||||
- `FLUID` — Liquid blocks
|
||||
- `NORANDTILE` — Disable autotiling randomisation
|
||||
|
||||
**Usage:**
|
||||
|
||||
```kotlin
|
||||
// Find all stone blocks
|
||||
val stoneBlocks = BlockCodex.blockProps.values.filter { it.hasTag("STONE") }
|
||||
|
||||
// Check if block is ore
|
||||
if (block.hasTag("ORE")) {
|
||||
println("This is an ore block!")
|
||||
}
|
||||
|
||||
// Check multiple tags
|
||||
if (block.hasAllTagsOf("TREE", "NATURAL")) {
|
||||
println("Natural tree")
|
||||
}
|
||||
```
|
||||
|
||||
## Creating Custom Blocks
|
||||
|
||||
**Important:** You don't write Kotlin/Java code to create blocks. You simply define block properties in CSV files, and the system automatically generates everything at runtime.
|
||||
|
||||
### CSV Format
|
||||
|
||||
Blocks are defined in `blocks/blocks.csv`:
|
||||
|
||||
```csv
|
||||
"id";"drop";"spawn";"name";"shdr";"shdg";"shdb";"shduv";"str";"dsty";"mate";"solid";"wall";"grav";"dlfn";"fv";"fr";"lumr";"lumg";"lumb";"lumuv";"refl";"tags"
|
||||
"2";"basegame:2";"basegame:2";"BLOCK_STONE";"0.6290";"0.6290";"0.6290";"0.6290";"120";"2600";"ROCK";"1";"0";"N/A";"0";"0";"16";"0.0000";"0.0000";"0.0000";"0.0000";"0.0";"STONE,NATURAL,MINERAL"
|
||||
```
|
||||
|
||||
**Key columns:**
|
||||
- `id` — Numeric tile ID (unique within module)
|
||||
- `drop` — Item ID dropped when mined
|
||||
- `spawn` — Item ID used to place block
|
||||
- `name` — Translation key
|
||||
- `shdr/shdg/shdb/shduv` — Shade colour (RGBA)
|
||||
- `str` — Strength/HP
|
||||
- `dsty` — Density (kg/m³)
|
||||
- `mate` — Material ID (4-letter code)
|
||||
- `solid` — Is solid (1) or passable (0)
|
||||
- `wall` — Is wall (1) or terrain (0)
|
||||
- `fr` — Friction coefficient
|
||||
- `lumr/lumg/lumb/lumuv` — Luminosity (RGBA)
|
||||
- `dlfn` — Dynamic luminosity function (0-7)
|
||||
- `refl` — Reflectance (0.0-1.0)
|
||||
- `tags` — Comma-separated tags
|
||||
|
||||
### Example: Custom Glowing Ore
|
||||
|
||||
```csv
|
||||
"id";"drop";"spawn";"name";"shdr";"shdg";"shdb";"shduv";"str";"dsty";"mate";"solid";"wall";"grav";"dlfn";"fv";"fr";"lumr";"lumg";"lumb";"lumuv";"refl";"tags"
|
||||
"200";"mymod:crystal";"mymod:200";"BLOCK_CRYSTAL_ORE";"0.7";"0.8";"0.9";"0.2";"150";"3200";"ROCK";"1";"0";"N/A";"0";"0";"16";"0.2";"0.5";"0.8";"0.0";"0.3";"STONE,ORE,GLOWING"
|
||||
```
|
||||
|
||||
This creates:
|
||||
- Blue-tinted glowing crystal ore
|
||||
- Strength 150 (harder than normal stone)
|
||||
- Emits cyan light (0.2 red, 0.5 green, 0.8 blue)
|
||||
- Drops `mymod:crystal` item when mined
|
||||
|
||||
### Loading Custom Blocks
|
||||
|
||||
In your module's `EntryPoint`:
|
||||
|
||||
```kotlin
|
||||
override fun invoke() {
|
||||
// Load blocks
|
||||
ModMgr.GameBlockLoader.invoke("mymod")
|
||||
}
|
||||
```
|
||||
|
||||
The loader automatically:
|
||||
1. Reads `blocks/blocks.csv`
|
||||
2. Parses CSV records
|
||||
3. Creates `BlockProp` instances from CSV data
|
||||
4. Registers blocks in `BlockCodex`
|
||||
5. Creates corresponding block items via `makeNewItemObj()` and registers them in `ItemCodex`
|
||||
|
||||
**You never manually create `BlockProp` instances** — the system handles everything. You only define CSV data.
|
||||
|
||||
## Block Rendering
|
||||
|
||||
### Tile Atlas System
|
||||
|
||||
Blocks are rendered from texture atlases with autotiling.
|
||||
|
||||
**Atlas structure:**
|
||||
- 16×16 pixel tiles
|
||||
- Organised in sprite sheets
|
||||
- Multiple tiles per block (autotiling variants)
|
||||
- Six seasonal variations per tile
|
||||
|
||||
See [[Tile Atlas System]] for details on atlas generation and seasonal mixing.
|
||||
|
||||
### Autotiling
|
||||
|
||||
Blocks automatically connect to adjacent similar blocks.
|
||||
|
||||
**Tile connections:**
|
||||
- 4-directional (cardinal)
|
||||
- 8-directional (cardinal + diagonal)
|
||||
- Corner pieces
|
||||
- Edge pieces
|
||||
|
||||
**Subtiling:**
|
||||
|
||||
Some blocks have internal variation using subtiles:
|
||||
- Random patterns (ore veins)
|
||||
- Gradients (stone texture)
|
||||
- Detail variations
|
||||
|
||||
See [[Autotiling in Depth]] for complete autotiling documentation.
|
||||
|
||||
### Rendering Order
|
||||
|
||||
Blocks render in layers:
|
||||
1. **Wall layer** — Background walls
|
||||
2. **Terrain layer** — Solid terrain
|
||||
3. **Overlay layer** — Liquids, effects
|
||||
|
||||
**Depth sorting:**
|
||||
- Walls always behind terrain
|
||||
- Transparent blocks blend correctly
|
||||
- Liquids render with transparency
|
||||
|
||||
## Advanced Features
|
||||
|
||||
### Dynamic Luminosity
|
||||
|
||||
Blocks with `dlfn > 0` have animated lighting:
|
||||
|
||||
```kotlin
|
||||
var dynamicLuminosityFunction: Int = 2 // Torch flicker
|
||||
```
|
||||
|
||||
**Functions:**
|
||||
- `0` — Static (no animation)
|
||||
- `1` — Slow pulse
|
||||
- `2` — Torch flicker
|
||||
- `3` — Fast flicker
|
||||
- `4-7` — Various patterns
|
||||
|
||||
**Implementation:**
|
||||
|
||||
The engine creates virtual tiles for each dynamic light block, pre-generating random luminosity variations. At runtime, blocks sample from these virtual tiles based on their world coordinates to create spatially-coherent animated lighting.
|
||||
|
||||
### Structural Support
|
||||
|
||||
Some blocks have weight limits:
|
||||
|
||||
```kotlin
|
||||
var maxSupport: Int = 100 // Can support 100 blocks above
|
||||
```
|
||||
|
||||
Used for:
|
||||
- Collapsing structures
|
||||
- Realistic building constraints
|
||||
- Puzzle mechanics
|
||||
|
||||
### Tile Connecting
|
||||
|
||||
Control how blocks connect to neighbours:
|
||||
|
||||
```kotlin
|
||||
val isSolidForTileCnx: Boolean
|
||||
get() = if (tags.contains("DORENDER") || !isActorBlock) isSolid else false
|
||||
```
|
||||
|
||||
- Actor blocks normally don't connect
|
||||
- `DORENDER` tag forces connection
|
||||
- Used for trees, decorations
|
||||
|
||||
### Block Extra Data
|
||||
|
||||
Store custom module data:
|
||||
|
||||
```kotlin
|
||||
block.extra["mymod:special_property"] = "custom value"
|
||||
block.extra["mymod:spawn_rate"] = 0.5
|
||||
```
|
||||
|
||||
## Block Items
|
||||
|
||||
Every block automatically becomes a placeable item.
|
||||
|
||||
**Automatic registration:**
|
||||
|
||||
When `ModMgr.GameBlockLoader.invoke()` runs, it:
|
||||
1. Creates `BlockProp` instances from CSV
|
||||
2. Calls `blockRegisterHook()` for each block
|
||||
3. The hook generates a `GameItem` via `makeNewItemObj()`
|
||||
4. Registers the item in `ItemCodex` with the same ID as the block
|
||||
|
||||
```kotlin
|
||||
// Internal process (you don't write this)
|
||||
fun blockRegisterHook(tile: BlockProp) {
|
||||
// makeNewItemObj() creates appropriate block item
|
||||
val item = makeNewItemObj(tile, isWall = tile.isWall)
|
||||
ItemCodex[tile.id] = item
|
||||
}
|
||||
```
|
||||
|
||||
**You never manually create block items** — defining a block in CSV automatically creates its placeable item counterpart.
|
||||
|
||||
**Generated block item properties:**
|
||||
- Same ID as block (`basegame:2` for both block and item)
|
||||
- Mass calculated from block density
|
||||
- Stackable (up to 999)
|
||||
- Primary use places the block in world
|
||||
|
||||
**Placement:**
|
||||
|
||||
```kotlin
|
||||
// Place terrain block
|
||||
INGAME.world.setTileTerrain(x, y, blockID, true)
|
||||
|
||||
// Place wall block
|
||||
INGAME.world.setTileWall(x, y, wallID, true)
|
||||
```
|
||||
|
||||
## Querying Blocks
|
||||
|
||||
### By ID
|
||||
|
||||
```kotlin
|
||||
val stone = BlockCodex["basegame:2"]
|
||||
val customOre = BlockCodex["mymod:200"]
|
||||
```
|
||||
|
||||
### By Tags
|
||||
|
||||
```kotlin
|
||||
// All ore blocks
|
||||
val ores = BlockCodex.blockProps.values.filter { it.hasTag("ORE") }
|
||||
|
||||
// All glowing blocks
|
||||
val glowing = BlockCodex.blockProps.values.filter {
|
||||
it.baseLumColR > 0 || it.baseLumColG > 0 || it.baseLumColB > 0
|
||||
}
|
||||
|
||||
// All natural stone
|
||||
val naturalStone = BlockCodex.blockProps.values.filter {
|
||||
it.hasAllTagsOf("STONE", "NATURAL")
|
||||
}
|
||||
```
|
||||
|
||||
### By Property
|
||||
|
||||
```kotlin
|
||||
// All blocks stronger than iron
|
||||
val hardBlocks = BlockCodex.blockProps.values.filter { it.strength > 150 }
|
||||
|
||||
// All high-density blocks
|
||||
val heavyBlocks = BlockCodex.blockProps.values.filter { it.density > 5000 }
|
||||
|
||||
// All reflective surfaces
|
||||
val mirrors = BlockCodex.blockProps.values.filter { it.reflectance > 0.7 }
|
||||
```
|
||||
|
||||
## World Interaction
|
||||
|
||||
### Getting Blocks
|
||||
|
||||
```kotlin
|
||||
// Get terrain block at position
|
||||
val terrainTile = world.getTileFromTerrain(x, y)
|
||||
|
||||
// Get wall block at position
|
||||
val wallTile = world.getTileFromWall(x, y)
|
||||
|
||||
// Check if solid
|
||||
val isSolid = BlockCodex[terrainTile].isSolid
|
||||
```
|
||||
|
||||
### Setting Blocks
|
||||
|
||||
```kotlin
|
||||
// Place terrain
|
||||
world.setTileTerrain(x, y, "basegame:2", true) // true = update neighbours
|
||||
|
||||
// Place wall
|
||||
world.setTileWall(x, y, "basegame:131", true)
|
||||
|
||||
// Remove block (place air)
|
||||
world.setTileTerrain(x, y, Block.AIR, true)
|
||||
```
|
||||
|
||||
### Mining Blocks
|
||||
|
||||
```kotlin
|
||||
val tile = world.getTileFromTerrain(x, y)
|
||||
val prop = BlockCodex[tile]
|
||||
|
||||
// Calculate mining time
|
||||
val toolStrength = player.toolStrength
|
||||
val miningTime = prop.strength / toolStrength
|
||||
|
||||
// Drop item when destroyed
|
||||
val dropItem = prop.drop
|
||||
if (dropItem.isNotEmpty() && Math.random() < dropProbability) {
|
||||
world.spawnDroppedItem(x, y, dropItem)
|
||||
}
|
||||
|
||||
// Remove block
|
||||
world.setTileTerrain(x, y, Block.AIR, true)
|
||||
```
|
||||
|
||||
## Best Practises
|
||||
|
||||
1. **Use appropriate strength values** — Match mining difficulty to tier
|
||||
2. **Set correct density** — Affects physics and realism
|
||||
3. **Tag comprehensively** — Enable flexible queries
|
||||
4. **Match drops to blocks** — Logical item drops
|
||||
5. **Use real material IDs** — Don't create fake materials
|
||||
6. **Test autotiling** — Verify connections work correctly
|
||||
7. **Balance luminosity** — Don't make everything glow
|
||||
8. **Consider friction** — Ice should be slippery
|
||||
9. **Namespace IDs** — Use `modulename:id` format
|
||||
10. **Test in all seasons** — Verify seasonal atlas variations
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- **Wrong solid/wall flags** — Blocks behave incorrectly
|
||||
- **Missing drop items** — Blocks vanish when mined
|
||||
- **Excessive luminosity** — Blindingly bright blocks
|
||||
- **Zero friction** — Players slide uncontrollably
|
||||
- **Inconsistent density** — Wood heavier than stone
|
||||
- **Missing tags** — Can't query blocks properly
|
||||
- **Wrong material** — Incorrect sounds and behaviour
|
||||
- **Forgetting neighbours** — Autotiling breaks
|
||||
- **Numeric ID conflicts** — Multiple blocks with same ID
|
||||
- **Not testing placement** — Block items don't work
|
||||
|
||||
## See Also
|
||||
|
||||
- [[Modules-Codex-Systems#BlockCodex]] — BlockCodex reference
|
||||
- [[Modules-Setup]] — Creating modules with blocks
|
||||
- [[Items]] — Block items and placement
|
||||
- [[World]] — World generation with blocks
|
||||
- [[Tile Atlas System]] — Block rendering and atlases
|
||||
- [[Autotiling in Depth]] — Autotiling implementation
|
||||
@@ -1,24 +1,374 @@
|
||||
This is a place for the developers, a hub for documentation and code conventions.
|
||||
# Developer Portal
|
||||
|
||||
## Concepts
|
||||
* [[Glossary]]
|
||||
* [[Modules]]
|
||||
This is a place for developers, a hub for documentation and code conventions for the Terrarum game engine.
|
||||
|
||||
## Ingame
|
||||
* [[Actors]]
|
||||
* [[World]]
|
||||
* [[Inventory]]
|
||||
## Getting Started
|
||||
|
||||
## Serialisation
|
||||
* [[Save and Load]]
|
||||
* [[Glossary]] — Essential terminology and concepts
|
||||
* [[Modules]] — Understanding the module system
|
||||
* [[Modules:Setup]] — Setting up your first module
|
||||
|
||||
## Internationalisation and Localisation
|
||||
* [[Languages]]
|
||||
* [[Keyboard Layout and IME]]
|
||||
## Core Engine Systems
|
||||
|
||||
## Considerations for Basegame and Modules
|
||||
* [[OpenGL Considerations]]
|
||||
### Game World
|
||||
* [[World]] — World structure, chunks, and layers
|
||||
* [[Actors]] — The actor system and lifecycle
|
||||
* [[Inventory]] — Items and inventory management
|
||||
* [[Save and Load]] — Serialisation and persistence
|
||||
|
||||
## Child Repositories
|
||||
* [Terrarum Sans Bitmap](https://github.com/minjaesong/Terrarum-sans-bitmap): The font used in this game
|
||||
* [TerranVirtualDisk](https://github.com/minjaesong/TerranVirtualDisk): File archival format used in this game
|
||||
### Rendering
|
||||
* [[Rendering Pipeline]] — Graphics system and tile rendering
|
||||
* [[OpenGL Considerations]] — GL 3.2 requirements and shader syntax
|
||||
|
||||
### Physics
|
||||
* [[Physics Engine]] — AABB collision and Newtonian simulation
|
||||
|
||||
### Audio
|
||||
* [[Audio System]] — Sound, music, and spatial audio
|
||||
|
||||
### User Interface
|
||||
* [[UI Framework]] — Canvas-based UI system and components
|
||||
|
||||
### Internationalisation and Localisation
|
||||
* [[Languages]] — Translation system and multilingual support
|
||||
* [[Keyboard Layout and IME]] — Input methods for complex scripts
|
||||
|
||||
## Engine Internals (For Maintainers)
|
||||
|
||||
**Advanced technical documentation for engine developers:**
|
||||
|
||||
### Rendering & Graphics
|
||||
* [[Tile Atlas System]] — Six-season atlas generation, subtiling, and dynamic texture management
|
||||
* [[Autotiling In-Depth]] — Connection algorithms, lookup tables, and subtiling patterns
|
||||
|
||||
### Audio
|
||||
* [[Audio Engine Internals]] — Mixer architecture, DSP pipeline, spatial audio, and streaming
|
||||
|
||||
### Coming Soon
|
||||
* **Fluid Simulation Internals** — Cellular automata and flow algorithms
|
||||
* **Lighting Engine** — RGB+UV light propagation and transmittance
|
||||
* **Save Format Specification** — TerranVirtualDisk binary format details
|
||||
* **Physics Internals** — AABB collision resolution and spatial partitioning
|
||||
* **Networking** — Multiplayer synchronisation and packet protocol
|
||||
|
||||
## Making Modules
|
||||
|
||||
### Content Creation
|
||||
* [[My Little Dagger|Modules:Items]] — Creating custom items
|
||||
* [[My Precious Block|Modules:Blocks]] — Creating custom blocks
|
||||
* [[Let It Snow|Modules:Weather]] — Custom weather systems
|
||||
* [[Art Forgery|Modules:Tapestries]] — Adding decorative images
|
||||
* [[Let There Be Non-Stationary Thingies|Modules:Actors]] — Creating custom actors
|
||||
* [[Texture Packs|Modules:Retextures]] — Retexturing existing content
|
||||
|
||||
### Advanced Topics
|
||||
* [[Items in-depth|Development:Items]] — Advanced item implementation
|
||||
* [[Actors in-depth|Development:Actors]] — Deep dive into actor systems
|
||||
* [[Creature RAW|Development:RAW]] — Data-driven creature definitions
|
||||
* [[Faction|Development:Faction]] — Faction system and relationships
|
||||
|
||||
## Engine Architecture
|
||||
|
||||
### Core Components
|
||||
* **App.java** — Application entry point and initialisation
|
||||
* **Terrarum.kt** — Engine singleton and codex management
|
||||
* **IngameInstance.kt** — Base class for game screens
|
||||
* **ModMgr.kt** — Module loading and management
|
||||
|
||||
### Code Organisation
|
||||
```
|
||||
src/net/torvald/terrarum/
|
||||
├── gameactors/ # Actor system
|
||||
├── gameworld/ # World structure
|
||||
├── gameitems/ # Item system
|
||||
├── blockproperties/ # Block definitions
|
||||
├── itemproperties/ # Item properties
|
||||
├── worlddrawer/ # Rendering
|
||||
├── audio/ # Audio system
|
||||
├── ui/ # User interface
|
||||
├── serialise/ # Save/load
|
||||
├── savegame/ # Virtual disk
|
||||
├── langpack/ # Localisation
|
||||
├── gamecontroller/ # Input handling
|
||||
├── modulebasegame/ # Base game implementation
|
||||
└── shaders/ # GLSL shaders
|
||||
```
|
||||
|
||||
## Data Structures
|
||||
|
||||
### Codices
|
||||
Codices are global registries for game content:
|
||||
|
||||
* **BlockCodex** — All blocks and terrain types
|
||||
* **ItemCodex** — All items
|
||||
* **WireCodex** — Wire and conduit types
|
||||
* **MaterialCodex** — Material definitions
|
||||
* **FactionCodex** — Faction data
|
||||
* **CraftingCodex** — Crafting recipes
|
||||
* **AudioCodex** — Audio assets
|
||||
* **WeatherCodex** — Weather types
|
||||
* **FluidCodex** — Fluid properties
|
||||
* **OreCodex** — Ore definitions
|
||||
|
||||
### Key Data Types
|
||||
* **ActorID** — Integer identifier for actors
|
||||
* **ItemID** — String identifier for items (`item@module:id`)
|
||||
* **BlockAddress** — Long integer for block positions
|
||||
* **RGBA8888** — 32-bit colour value
|
||||
* **Cvec** — Colour vector with RGBUV channels
|
||||
|
||||
## Rendering Details
|
||||
|
||||
### Tile Atlas Formats
|
||||
* **16×16** — Single tile, no autotiling
|
||||
* **64×16** — Wall stickers (4 variants)
|
||||
* **128×16** — Platforms (8 variants)
|
||||
* **112×112** — Full autotiling (49 variants)
|
||||
* **224×224** — Seasonal autotiling (4 seasons)
|
||||
|
||||
### Render Order Layers
|
||||
1. FAR_BEHIND — Wires and conduits
|
||||
2. BEHIND — Tapestries, background particles
|
||||
3. MIDDLE — Actors (players, NPCs, creatures)
|
||||
4. MIDTOP — Projectiles, thrown items
|
||||
5. FRONT — Front walls and barriers
|
||||
6. OVERLAY — Screen overlays (unaffected by lighting)
|
||||
|
||||
### Lighting System
|
||||
* **RGB+UV** — Four independent light channels
|
||||
* **Transmittance** — Light propagation through blocks
|
||||
* **Dynamic lights** — Time-varying luminosity functions
|
||||
* **Corner occlusion** — Shader-based depth enhancement
|
||||
|
||||
## Physics Constants
|
||||
|
||||
* **TILE_SIZE** — 16 pixels (configurable at compile time)
|
||||
* **METER** — 25 pixels (1 metre in game units)
|
||||
* **PHYS_TIME_FRAME** — 25.0 (physics time step)
|
||||
* **PHYS_REF_FPS** — 60.0 (reference frame rate)
|
||||
|
||||
## Input Handling
|
||||
|
||||
### Keyboard Layouts
|
||||
* **Low Layer** — Base layout (QWERTY, Colemak, etc.)
|
||||
* **High Layer** — Language-specific IME (Hangul, Kana, etc.)
|
||||
|
||||
Keyboard layouts are stored in `assets/keylayout/` as `.key` files (Low Layer) and `.ime` files (High Layer).
|
||||
|
||||
### Controller Support
|
||||
* GDX controllers (via LibGDX)
|
||||
* XInput devices (via JXInput)
|
||||
* Custom controller mappings
|
||||
|
||||
## Modding API Guidelines
|
||||
|
||||
### Module Structure
|
||||
```
|
||||
<module>/
|
||||
├── metadata.properties # Required: module info
|
||||
├── default.json # Optional: default config
|
||||
├── icon.png # Optional: 48×48 icon
|
||||
├── <module>.jar # Optional: compiled code
|
||||
├── blocks/ # Block definitions
|
||||
│ └── blocks.csv
|
||||
├── items/ # Item definitions
|
||||
│ └── itemid.csv
|
||||
├── materials/ # Material definitions
|
||||
│ └── materials.csv
|
||||
├── locales/ # Translations
|
||||
│ ├── en/
|
||||
│ ├── koKR/
|
||||
│ └── ...
|
||||
├── crafting/ # Crafting recipes
|
||||
│ └── *.json
|
||||
└── retextures/ # Texture packs
|
||||
└── ...
|
||||
```
|
||||
|
||||
### Entry Point
|
||||
|
||||
If your module has a JAR file, create an entry point:
|
||||
|
||||
```kotlin
|
||||
class MyModuleEntryPoint : ModuleEntryPoint() {
|
||||
override fun init() {
|
||||
// Load blocks
|
||||
ModMgr.GameBlockLoader.loadAll(moduleInfo)
|
||||
|
||||
// Load items
|
||||
ModMgr.GameItemLoader.loadAll(moduleInfo)
|
||||
|
||||
// Load materials
|
||||
ModMgr.GameMaterialLoader.loadAll(moduleInfo)
|
||||
|
||||
// Load languages
|
||||
ModMgr.GameLanguageLoader.loadAll(moduleInfo)
|
||||
|
||||
// Load crafting recipes
|
||||
ModMgr.GameCraftingRecipeLoader.loadAll(moduleInfo)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Dependency Management
|
||||
|
||||
Specify module dependencies in `metadata.properties`:
|
||||
|
||||
```properties
|
||||
dependency=basegame 0.4.0+;othermod 1.2.*
|
||||
```
|
||||
|
||||
Version syntax:
|
||||
* `a.b.c` — Exact version
|
||||
* `a.b.c+` — Version a.b.c to a.b.65535
|
||||
* `a.b.*` — Any version a.b.x
|
||||
* `a.b+` — Version a.b.0 to a.255.65535
|
||||
* `a.*` — Any version a.x.x
|
||||
* `*` — Any version (testing only!)
|
||||
|
||||
## Code Conventions
|
||||
|
||||
### Naming
|
||||
* **Classes** — PascalCase (`ActorPlayer`, `BlockStone`)
|
||||
* **Functions** — camelCase (`update`, `getTileFromTerrain`)
|
||||
* **Constants** — SCREAMING_SNAKE_CASE (`TILE_SIZE`, `METER`)
|
||||
* **Properties** — camelCase (`hitbox`, `baseMass`)
|
||||
|
||||
### Language Usage
|
||||
* **Kotlin** — Preferred for new game logic and systems
|
||||
* **Java** — Used for performance-critical code and LibGDX integration
|
||||
* **Comments** — Required for public APIs
|
||||
|
||||
### File Organisation
|
||||
* One public class per file
|
||||
* Filename matches class name
|
||||
* Group related classes in packages
|
||||
|
||||
## Asset Guidelines
|
||||
|
||||
### Image Formats
|
||||
* **With transparency** — Export as TGA
|
||||
* **Without transparency** — Export as PNG
|
||||
* **Colour space** — sRGB, white point D65
|
||||
|
||||
### Audio Formats
|
||||
* **Music** — OGG Vorbis, `-q 10` quality
|
||||
* **SFX** — OGG Vorbis or WAV
|
||||
* **Sample rate** — 44100 Hz
|
||||
|
||||
### Text Files
|
||||
* **Encoding** — UTF-8
|
||||
* **Line endings** — Unix (LF)
|
||||
* **JSON** — Properly formatted with indentation
|
||||
|
||||
## Debugging Tools
|
||||
|
||||
### Console Commands
|
||||
Access the in-game console (typically F12) for:
|
||||
* Spawning items and actors
|
||||
* Teleportation
|
||||
* Time manipulation
|
||||
* Debug overlays
|
||||
|
||||
### Debug Flags
|
||||
```kotlin
|
||||
App.IS_DEVELOPMENT_BUILD = true // Enable debug features
|
||||
```
|
||||
|
||||
Debug features include:
|
||||
* Hitbox visualisation
|
||||
* Performance metrics
|
||||
* Verbose logging
|
||||
* Collision testing modes
|
||||
|
||||
### Logging
|
||||
```kotlin
|
||||
import net.torvald.terrarum.App.printdbg
|
||||
|
||||
printdbg(this, "Debug message")
|
||||
printdbgerr(this, "Error message")
|
||||
```
|
||||
|
||||
## Performance Profiling
|
||||
|
||||
### DebugTimers
|
||||
```kotlin
|
||||
DebugTimers.start("MyOperation")
|
||||
// ... code to profile
|
||||
DebugTimers.end("MyOperation")
|
||||
```
|
||||
|
||||
View results in the debug UI or logs.
|
||||
|
||||
### Memory Monitoring
|
||||
* Java heap — Standard JVM memory
|
||||
* Unsafe allocation — Custom memory management
|
||||
* Texture memory — GPU texture usage
|
||||
|
||||
## Testing
|
||||
|
||||
### Manual Testing
|
||||
* Test with multiple modules loaded
|
||||
* Test module loading/unloading
|
||||
* Verify save/load compatibility
|
||||
* Check language switching
|
||||
* Test at different resolutions
|
||||
|
||||
### Asset Validation
|
||||
* Check for missing textures
|
||||
* Verify audio file integrity
|
||||
* Test all translations
|
||||
* Validate JSON syntax
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
1. **Forgetting @Transient** — Sprites and UI cannot serialise
|
||||
2. **Not calling reload()** — Transient fields stay null
|
||||
3. **Hardcoding TILE_SIZE** — Use the constant
|
||||
4. **Ignoring frame rate** — Scale by delta time
|
||||
5. **Creating circular references** — Breaks serialisation
|
||||
6. **Not disposing resources** — Memory leaks
|
||||
7. **Assuming English** — Support all languages
|
||||
|
||||
## Best Practises
|
||||
|
||||
1. **Follow existing patterns** — Check neighbouring code
|
||||
2. **Document public APIs** — Help other developers
|
||||
3. **Test with base game** — Ensure compatibility
|
||||
4. **Handle missing data gracefully** — Don't crash on errors
|
||||
5. **Provide fallbacks** — Always have English translations
|
||||
6. **Version your content** — Use semver strictly
|
||||
7. **Profile performance** — Identify bottlenecks early
|
||||
|
||||
## External Resources
|
||||
|
||||
### Child Repositories
|
||||
* [Terrarum Sans Bitmap](https://github.com/minjaesong/Terrarum-sans-bitmap) — The font used in this game
|
||||
* [TerranVirtualDisk](https://github.com/minjaesong/TerranVirtualDisk) — File archival format used in saves
|
||||
|
||||
### Libraries
|
||||
* [LibGDX](https://libgdx.com/) — Game framework
|
||||
* [LWJGL3](https://www.lwjgl.org/) — OpenGL bindings
|
||||
* [dyn4j](https://dyn4j.org/) — Physics library (for Vector2)
|
||||
* [GraalVM JavaScript](https://www.graalvm.org/) — Scripting engine
|
||||
|
||||
### Documentation
|
||||
* [OpenGL 3.2 Reference](https://www.khronos.org/opengl/wiki/OpenGL_3.2)
|
||||
* [GLSL 1.50 Reference](https://www.khronos.org/opengl/wiki/Core_Language_(GLSL))
|
||||
* [Kotlin Documentation](https://kotlinlang.org/docs/home.html)
|
||||
|
||||
## Sample Projects
|
||||
|
||||
* [Terrarum Sample Module](https://github.com/curioustorvald/terrarum-sample-module-project) — Template for creating modules
|
||||
|
||||
## Getting Help
|
||||
|
||||
* Check this documentation portal
|
||||
* Review sample module code
|
||||
* Search existing modules for examples
|
||||
* Open an issue on GitHub for bugs
|
||||
* Join the community for questions
|
||||
|
||||
---
|
||||
|
||||
**Happy modding!**
|
||||
|
||||
629
Fixtures.md
Normal file
629
Fixtures.md
Normal file
@@ -0,0 +1,629 @@
|
||||
# Fixtures
|
||||
|
||||
**Audience:** Engine maintainers and module developers working with placeable objects and electrical systems.
|
||||
|
||||
Fixtures are stationary, placeable objects in the world such as furniture, machines, and decorative items. They occupy tiles, interact with the player, and can have complex behaviours including electrical connectivity and music playback.
|
||||
|
||||
## Overview
|
||||
|
||||
Fixtures provide:
|
||||
- **Tile occupation** — Reserve space in the world with BlockBox
|
||||
- **Interaction** — Player can click to use or open UI
|
||||
- **Inventory** — Store items internally
|
||||
- **Electrical connectivity** — Wire-based power and signals
|
||||
- **Music playback** — Play audio files through fixtures
|
||||
- **Spawn validation** — Check for floor/wall/ceiling requirements
|
||||
|
||||
## FixtureBase
|
||||
|
||||
The base class for all fixtures.
|
||||
|
||||
### Class Hierarchy
|
||||
|
||||
```
|
||||
Actor
|
||||
└─ ActorWithBody
|
||||
└─ FixtureBase
|
||||
├─ Electric (fixtures with wire connectivity)
|
||||
│ ├─ FixtureJukebox
|
||||
│ ├─ FixtureMusicalTurntable
|
||||
│ ├─ FixtureLogicSignalEmitter
|
||||
│ └─ ...
|
||||
├─ FixtureTapestry
|
||||
├─ FixtureWorkbench
|
||||
└─ ...
|
||||
```
|
||||
|
||||
### Core Properties
|
||||
|
||||
```kotlin
|
||||
class FixtureBase : ActorWithBody, CuedByTerrainChange {
|
||||
// Spawn requirements
|
||||
@Transient open val spawnNeedsWall: Boolean = false
|
||||
@Transient open val spawnNeedsFloor: Boolean = true
|
||||
@Transient open val spawnNeedsStableFloor: Boolean = false
|
||||
@Transient open val spawnNeedsCeiling: Boolean = false
|
||||
|
||||
// Physical presence
|
||||
lateinit var blockBox: BlockBox
|
||||
@Transient var blockBoxProps: BlockBoxProps
|
||||
var worldBlockPos: Point2i? // Tile position in world, null if not placed
|
||||
|
||||
// Interaction
|
||||
@Transient var nameFun: () -> String
|
||||
@Transient var mainUI: UICanvas?
|
||||
var inventory: FixtureInventory?
|
||||
|
||||
// State
|
||||
@Transient var inOperation = false
|
||||
@Transient var spawnRequestedTime: Long
|
||||
}
|
||||
```
|
||||
|
||||
### BlockBox
|
||||
|
||||
Defines the physical space a fixture occupies:
|
||||
|
||||
```kotlin
|
||||
data class BlockBox(
|
||||
val collisionType: ItemID = NO_COLLISION,
|
||||
val width: Int = 0, // Tile-wise width
|
||||
val height: Int = 0 // Tile-wise height
|
||||
)
|
||||
```
|
||||
|
||||
**Collision Types:**
|
||||
|
||||
```kotlin
|
||||
companion object {
|
||||
const val NO_COLLISION = Block.ACTORBLOCK_NO_COLLISION
|
||||
const val FULL_COLLISION = Block.ACTORBLOCK_FULL_COLLISION
|
||||
const val ALLOW_MOVE_DOWN = Block.ACTORBLOCK_ALLOW_MOVE_DOWN // Platform
|
||||
const val NO_PASS_RIGHT = Block.ACTORBLOCK_NO_PASS_RIGHT
|
||||
const val NO_PASS_LEFT = Block.ACTORBLOCK_NO_PASS_LEFT
|
||||
|
||||
val NULL = BlockBox() // No tile occupation
|
||||
}
|
||||
```
|
||||
|
||||
**Example BlockBoxes:**
|
||||
|
||||
```kotlin
|
||||
// 2×3 fixture with no collision (decorative painting)
|
||||
BlockBox(BlockBox.NO_COLLISION, 2, 3)
|
||||
|
||||
// 1×2 fixture with full collision (statue)
|
||||
BlockBox(BlockBox.FULL_COLLISION, 1, 2)
|
||||
|
||||
// 2×1 fixture that acts as platform (table)
|
||||
BlockBox(BlockBox.ALLOW_MOVE_DOWN, 2, 1)
|
||||
```
|
||||
|
||||
### Constructor
|
||||
|
||||
```kotlin
|
||||
constructor(
|
||||
blockBox0: BlockBox,
|
||||
blockBoxProps: BlockBoxProps = BlockBoxProps(0),
|
||||
renderOrder: RenderOrder = RenderOrder.MIDDLE,
|
||||
nameFun: () -> String,
|
||||
mainUI: UICanvas? = null,
|
||||
inventory: FixtureInventory? = null,
|
||||
id: ActorID? = null
|
||||
)
|
||||
```
|
||||
|
||||
**Example Fixture:**
|
||||
|
||||
```kotlin
|
||||
class FixtureWorkbench : FixtureBase(
|
||||
BlockBox(BlockBox.FULL_COLLISION, 3, 2), // 3 tiles wide, 2 tiles tall
|
||||
nameFun = { Lang["ITEM_WORKBENCH"] },
|
||||
mainUI = UIWorkbench()
|
||||
) {
|
||||
init {
|
||||
makeNewSprite(getSpritesheet("basegame", "sprites/fixtures/workbench.tga", 48, 32))
|
||||
density = 600.0
|
||||
actorValue[AVKey.BASEMASS] = 150.0
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Spawning
|
||||
|
||||
#### Spawn Requirements
|
||||
|
||||
Fixtures specify their spawning constraints:
|
||||
|
||||
```kotlin
|
||||
// Requires solid floor below (default)
|
||||
spawnNeedsFloor = true
|
||||
spawnNeedsStableFloor = false
|
||||
|
||||
// Requires stable floor (no platforms)
|
||||
spawnNeedsFloor = false
|
||||
spawnNeedsStableFloor = true
|
||||
|
||||
// Requires solid wall behind
|
||||
spawnNeedsWall = true
|
||||
|
||||
// Requires solid ceiling above (hanging fixtures)
|
||||
spawnNeedsCeiling = true
|
||||
|
||||
// Requires wall OR floor (either satisfies requirement)
|
||||
spawnNeedsWall = true
|
||||
spawnNeedsFloor = true
|
||||
```
|
||||
|
||||
#### Spawning Process
|
||||
|
||||
```kotlin
|
||||
fun spawn(posX0: Int, posY0: Int, installersUUID: UUID?): Boolean {
|
||||
// posX0, posY0: tile-wise bottom-centre position
|
||||
|
||||
// 1. Calculate top-left position
|
||||
val posXtl = (posX0 - blockBox.width.minus(1).div(2)) fmod world.width
|
||||
val posYtl = posY0 - blockBox.height + 1
|
||||
|
||||
// 2. Check spawn validity
|
||||
if (!canSpawnHere(posX0, posY0)) {
|
||||
return false // Blocked by tiles or lacks required floor/wall
|
||||
}
|
||||
|
||||
// 3. Place filler blocks in terrain layer
|
||||
placeActorBlocks()
|
||||
|
||||
// 4. Set hitbox and position
|
||||
worldBlockPos = Point2i(posXtl, posYtl)
|
||||
hitbox.setFromWidthHeight(
|
||||
posXtl * TILE_SIZED,
|
||||
posYtl * TILE_SIZED,
|
||||
blockBox.width * TILE_SIZED,
|
||||
blockBox.height * TILE_SIZED
|
||||
)
|
||||
|
||||
// 5. Add to world
|
||||
INGAME.queueActorAddition(this)
|
||||
|
||||
// 6. Make placement sound and dust particles
|
||||
makeNoiseAndDust(posXtl, posYtl)
|
||||
|
||||
// 7. Call custom spawn logic
|
||||
onSpawn(posX0, posY0)
|
||||
|
||||
return true
|
||||
}
|
||||
```
|
||||
|
||||
### Interaction
|
||||
|
||||
#### Click Interaction
|
||||
|
||||
```kotlin
|
||||
open fun onInteract(mx: Double, my: Double) {
|
||||
// Fired on mouse click
|
||||
// Do NOT override if fixture has mainUI (handled automatically)
|
||||
}
|
||||
```
|
||||
|
||||
**Example:**
|
||||
|
||||
```kotlin
|
||||
override fun onInteract(mx: Double, my: Double) {
|
||||
// Toggle torch on/off
|
||||
isLit = !isLit
|
||||
playSound("torch_toggle")
|
||||
}
|
||||
```
|
||||
|
||||
#### UI Integration
|
||||
|
||||
If `mainUI` is set, clicking the fixture opens the UI automatically:
|
||||
|
||||
```kotlin
|
||||
class FixtureWorkbench : FixtureBase(
|
||||
blockBox = BlockBox(BlockBox.FULL_COLLISION, 3, 2),
|
||||
nameFun = { Lang["ITEM_WORKBENCH"] },
|
||||
mainUI = UIWorkbench() // Automatically opens on click
|
||||
)
|
||||
```
|
||||
|
||||
### Quick Lookup Parameters
|
||||
|
||||
Add tooltip information visible on hover:
|
||||
|
||||
```kotlin
|
||||
init {
|
||||
// With label
|
||||
addQuickLookupParam("STAT_TEMPERATURE") {
|
||||
"${temperature}°C"
|
||||
}
|
||||
|
||||
// Without label (dynamic text only)
|
||||
addQuickLookupParam {
|
||||
if (isPowered) "§o§Powered§.§" else ""
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Despawning
|
||||
|
||||
```kotlin
|
||||
override fun despawn() {
|
||||
if (canBeDespawned) {
|
||||
// Remove filler blocks
|
||||
forEachBlockbox { x, y, _, _ ->
|
||||
world.setTileTerrain(x, y, Block.AIR, true)
|
||||
}
|
||||
|
||||
worldBlockPos = null
|
||||
mainUI?.dispose()
|
||||
|
||||
super.despawn()
|
||||
}
|
||||
}
|
||||
|
||||
override val canBeDespawned: Boolean
|
||||
get() = inventory?.isEmpty() ?: true
|
||||
```
|
||||
|
||||
Fixtures with non-empty inventory **cannot be despawned** until emptied.
|
||||
|
||||
### BlockBox Iteration
|
||||
|
||||
```kotlin
|
||||
// Iterate over all tiles occupied by fixture
|
||||
forEachBlockbox { x, y, offsetX, offsetY ->
|
||||
// x, y: world tile coordinates
|
||||
// offsetX, offsetY: offset from fixture's top-left corner
|
||||
|
||||
world.setTileTerrain(x, y, Block.AIR, true)
|
||||
}
|
||||
|
||||
// Get all positions as list
|
||||
val positions: List<Pair<Int, Int>>? = everyBlockboxPos
|
||||
```
|
||||
|
||||
### Serialisation
|
||||
|
||||
**Transient fields** (not serialised):
|
||||
- `@Transient var nameFun`
|
||||
- `@Transient var mainUI`
|
||||
- `@Transient var blockBoxProps`
|
||||
- Sprite and rendering data
|
||||
|
||||
**Serialised fields:**
|
||||
- `blockBox`
|
||||
- `inventory`
|
||||
- `worldBlockPos`
|
||||
- Custom state variables
|
||||
|
||||
**Reconstruction:**
|
||||
|
||||
```kotlin
|
||||
override fun reload() {
|
||||
super.reload()
|
||||
// Reconstruct transient state
|
||||
nameFun = { Lang["ITEM_WORKBENCH"] }
|
||||
mainUI = UIWorkbench()
|
||||
sprite = loadSprite()
|
||||
}
|
||||
```
|
||||
|
||||
## Electric Fixtures
|
||||
|
||||
Electric fixtures support wire-based connectivity for power and signals.
|
||||
|
||||
### Class Definition
|
||||
|
||||
```kotlin
|
||||
open class Electric : FixtureBase {
|
||||
// Wire emitters (outputs)
|
||||
@Transient val wireEmitterTypes: HashMap<BlockBoxIndex, WireEmissionType>
|
||||
val wireEmission: HashMap<BlockBoxIndex, Vector2>
|
||||
|
||||
// Wire sinks (inputs)
|
||||
@Transient val wireSinkTypes: HashMap<BlockBoxIndex, WireEmissionType>
|
||||
val wireConsumption: HashMap<BlockBoxIndex, Vector2>
|
||||
|
||||
// Energy storage
|
||||
val chargeStored: HashMap<String, Double>
|
||||
}
|
||||
```
|
||||
|
||||
### Wire Types
|
||||
|
||||
Common wire emission types:
|
||||
|
||||
- **`"appliance_power"`** — AC power (230V/120V)
|
||||
- **`"digital_bit"`** — Digital signal (0.0 = LOW, 1.0 = HIGH)
|
||||
- **`"heating_power"`** — Thermal energy
|
||||
- **`"analog_audio"`** — Audio signal
|
||||
|
||||
### Configuring Wires
|
||||
|
||||
```kotlin
|
||||
class FixtureJukebox : Electric {
|
||||
init {
|
||||
// Set sink at (0, 2) in BlockBox for appliance power
|
||||
setWireSinkAt(0, 2, "appliance_power")
|
||||
|
||||
// Power consumption: 350W real, 0VAR reactive
|
||||
setWireConsumptionAt(0, 2, Vector2(350.0, 0.0))
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```kotlin
|
||||
class FixtureSolarPanel : Electric {
|
||||
init {
|
||||
// Set emitter at (1, 0) for appliance power
|
||||
setWireEmitterAt(1, 0, "appliance_power")
|
||||
|
||||
// Power generation updated per frame
|
||||
setWireEmissionAt(1, 0, Vector2(powerGenerated, 0.0))
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Digital Signals
|
||||
|
||||
Electric fixtures can respond to digital signals:
|
||||
|
||||
```kotlin
|
||||
class FixtureLogicSignalEmitter : Electric {
|
||||
init {
|
||||
setWireSinkAt(0, 0, "digital_bit")
|
||||
setWireEmitterAt(1, 0, "digital_bit")
|
||||
}
|
||||
|
||||
// Called when signal rises from LOW to HIGH
|
||||
override fun onRisingEdge(readFrom: BlockBoxIndex) {
|
||||
println("Signal went HIGH at index $readFrom")
|
||||
// Set output HIGH
|
||||
setWireEmissionAt(1, 0, Vector2(1.0, 0.0))
|
||||
}
|
||||
|
||||
// Called when signal falls from HIGH to LOW
|
||||
override fun onFallingEdge(readFrom: BlockBoxIndex) {
|
||||
println("Signal went LOW at index $readFrom")
|
||||
// Set output LOW
|
||||
setWireEmissionAt(1, 0, Vector2(0.0, 0.0))
|
||||
}
|
||||
|
||||
// Called after edge detection
|
||||
override fun updateSignal() {
|
||||
// Update internal logic
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Signal Thresholds
|
||||
|
||||
```kotlin
|
||||
companion object {
|
||||
const val ELECTRIC_THRESHOLD_HIGH = 0.6666666666666666 // 66.7%
|
||||
const val ELECTRIC_THRESHOLD_LOW = 0.3333333333333333 // 33.3%
|
||||
const val ELECTRIC_THRESHOLD_EDGE_DELTA = 0.33333333333333337
|
||||
}
|
||||
```
|
||||
|
||||
**Signal Detection:**
|
||||
|
||||
- **Rising edge** — Signal crosses from below `THRESHOLD_LOW` to above `THRESHOLD_HIGH`
|
||||
- **Falling edge** — Signal crosses from above `THRESHOLD_HIGH` to below `THRESHOLD_LOW`
|
||||
|
||||
### Reading Wire State
|
||||
|
||||
```kotlin
|
||||
// Get wire state at offset (x, y) for given type
|
||||
val state: Vector2 = getWireStateAt(offsetX, offsetY, "digital_bit")
|
||||
|
||||
// Check if digital signal is HIGH
|
||||
if (isSignalHigh(0, 0)) {
|
||||
// Input is HIGH
|
||||
}
|
||||
|
||||
// Check if digital signal is LOW
|
||||
if (isSignalLow(0, 0)) {
|
||||
// Input is LOW
|
||||
}
|
||||
```
|
||||
|
||||
### Wire Emission
|
||||
|
||||
```kotlin
|
||||
override fun updateImpl(delta: Float) {
|
||||
super.updateImpl(delta)
|
||||
|
||||
// Update power generation based on sunlight
|
||||
val sunlight = world.globalLight
|
||||
val powerGenerated = sunlight * 500.0 // Up to 500W
|
||||
|
||||
setWireEmissionAt(0, 0, Vector2(powerGenerated, 0.0))
|
||||
}
|
||||
```
|
||||
|
||||
### Complex Example: Logic Gate
|
||||
|
||||
```kotlin
|
||||
class FixtureLogicAND : Electric {
|
||||
init {
|
||||
// Two inputs
|
||||
setWireSinkAt(0, 0, "digital_bit")
|
||||
setWireSinkAt(1, 0, "digital_bit")
|
||||
|
||||
// One output
|
||||
setWireEmitterAt(2, 0, "digital_bit")
|
||||
}
|
||||
|
||||
override fun updateSignal() {
|
||||
// AND gate logic
|
||||
val inputA = isSignalHigh(0, 0)
|
||||
val inputB = isSignalHigh(1, 0)
|
||||
val output = if (inputA && inputB) 1.0 else 0.0
|
||||
|
||||
setWireEmissionAt(2, 0, Vector2(output, 0.0))
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## PlaysMusic Interface
|
||||
|
||||
A marker interface for fixtures that can play music.
|
||||
|
||||
```kotlin
|
||||
interface PlaysMusic {
|
||||
// Marker interface (no methods)
|
||||
}
|
||||
```
|
||||
|
||||
**Purpose:** Identifies fixtures that play music for game systems:
|
||||
- Music service coordination (prevent multiple music sources)
|
||||
- Audio priority management
|
||||
- Jukebox/radio functionality
|
||||
|
||||
**Usage:**
|
||||
|
||||
```kotlin
|
||||
class FixtureJukebox : Electric, PlaysMusic {
|
||||
@Transient var musicNowPlaying: MusicContainer? = null
|
||||
|
||||
val musicIsPlaying: Boolean
|
||||
get() = musicNowPlaying != null
|
||||
|
||||
fun playDisc(index: Int) {
|
||||
val disc = discInventory[index]
|
||||
val musicFile = (ItemCodex[disc] as? ItemFileRef)?.getAsGdxFile()
|
||||
|
||||
musicNowPlaying = MusicContainer(title, musicFile.file()) {
|
||||
// Callback when music ends
|
||||
stopPlayback()
|
||||
}
|
||||
|
||||
MusicService.playMusicalFixture(
|
||||
action = { startAudio(musicNowPlaying!!) },
|
||||
musicFinished = { !musicIsPlaying },
|
||||
onSuccess = { /* ... */ },
|
||||
onFailure = { /* ... */ }
|
||||
)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Built-in PlaysMusic Fixtures:**
|
||||
- `FixtureJukebox` — Plays music discs
|
||||
- `FixtureMusicalTurntable` — Vinyl record player
|
||||
- `FixtureRadio` — Streams music
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Creating a Simple Fixture
|
||||
|
||||
```kotlin
|
||||
class FixtureTikiTorch : FixtureBase(
|
||||
BlockBox(BlockBox.NO_COLLISION, 1, 2),
|
||||
nameFun = { Lang["ITEM_TIKI_TORCH"] }
|
||||
) {
|
||||
@Transient var isLit = true
|
||||
|
||||
init {
|
||||
makeNewSprite(getSpritesheet("basegame", "sprites/fixtures/tiki_torch.tga", 16, 32))
|
||||
density = 500.0
|
||||
actorValue[AVKey.BASEMASS] = 5.0
|
||||
}
|
||||
|
||||
override fun onInteract(mx: Double, my: Double) {
|
||||
isLit = !isLit
|
||||
}
|
||||
|
||||
override fun updateImpl(delta: Float) {
|
||||
super.updateImpl(delta)
|
||||
sprite?.currentRow = if (isLit) 0 else 1
|
||||
}
|
||||
|
||||
override fun reload() {
|
||||
super.reload()
|
||||
// Reconstruct sprite
|
||||
makeNewSprite(getSpritesheet("basegame", "sprites/fixtures/tiki_torch.tga", 16, 32))
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Creating an Electric Fixture
|
||||
|
||||
```kotlin
|
||||
class FixtureLamp : Electric {
|
||||
constructor() : super(
|
||||
BlockBox(BlockBox.NO_COLLISION, 1, 1),
|
||||
nameFun = { Lang["ITEM_LAMP"] }
|
||||
)
|
||||
|
||||
@Transient var isPowered = false
|
||||
|
||||
init {
|
||||
makeNewSprite(getSpritesheet("basegame", "sprites/fixtures/lamp.tga", 16, 16))
|
||||
|
||||
// Require power at bottom tile
|
||||
setWireSinkAt(0, 0, "appliance_power")
|
||||
setWireConsumptionAt(0, 0, Vector2(60.0, 0.0)) // 60W lamp
|
||||
}
|
||||
|
||||
override fun updateImpl(delta: Float) {
|
||||
super.updateImpl(delta)
|
||||
|
||||
// Check if receiving power
|
||||
val powerReceived = getWireStateAt(0, 0, "appliance_power")
|
||||
isPowered = powerReceived.x > 10.0 // At least 10W
|
||||
|
||||
// Update sprite
|
||||
sprite?.currentRow = if (isPowered) 0 else 1
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Fixture with Inventory
|
||||
|
||||
```kotlin
|
||||
class FixtureChest : FixtureBase(
|
||||
BlockBox(BlockBox.FULL_COLLISION, 2, 2),
|
||||
nameFun = { Lang["ITEM_CHEST"] },
|
||||
mainUI = UIChest(),
|
||||
inventory = FixtureInventory(20) // 20 slots
|
||||
) {
|
||||
init {
|
||||
makeNewSprite(getSpritesheet("basegame", "sprites/fixtures/chest.tga", 32, 32))
|
||||
(mainUI as UIChest).setInventory(inventory!!)
|
||||
}
|
||||
|
||||
override val canBeDespawned: Boolean
|
||||
get() = inventory?.isEmpty() ?: true
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practises
|
||||
|
||||
1. **Always call `super.updateImpl(delta)`** — Required for fixture systems to work
|
||||
2. **Mark UI/sprites as `@Transient`** — They cannot be serialised
|
||||
3. **Implement `reload()`** — Reconstruct all transient fields
|
||||
4. **Use `forEachBlockbox` for tile operations** — Handles BlockBox correctly
|
||||
5. **Check `canBeDespawned` before despawn** — Prevent loss of inventory contents
|
||||
6. **Set `inOperation` when active** — Enables chunk anchoring for machines
|
||||
7. **Use `makeNoiseAndDust()` on spawn** — Provides visual/audio feedback
|
||||
8. **Don't override `onInteract()` if using `mainUI`** — Handled automatically
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- **Forgetting to call `placeActorBlocks()`** — Fixture doesn't occupy tiles
|
||||
- **Not implementing `reload()`** — Crashes when loading save
|
||||
- **Setting wire emissions in constructor** — Use `updateImpl()` instead
|
||||
- **Mixing up BlockBox coordinates** — Use `forEachBlockbox` for clarity
|
||||
- **Despawning fixtures with inventory** — Check `canBeDespawned` first
|
||||
- **Directly modifying `worldBlockPos`** — Use `spawn()` instead
|
||||
|
||||
## See Also
|
||||
|
||||
- [[Actors]] — Base actor system
|
||||
- [[Animation-Description-Language]] — ADL for humanoid sprites
|
||||
- [[Inventory]] — Inventory system
|
||||
- [[World]] — World tile management
|
||||
260
Glossary.md
Normal file
260
Glossary.md
Normal file
@@ -0,0 +1,260 @@
|
||||
# Glossary
|
||||
|
||||
This page defines the key terminology used throughout the Terrarum game engine.
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### Actor
|
||||
An entity in the game world that can be updated and rendered. Actors are the fundamental interactive objects in the game, including players, NPCs, particles, projectiles, and fixtures. All actors extend the `Actor` base class and have a unique **Reference ID**.
|
||||
|
||||
See also: [[Actors]]
|
||||
|
||||
### Reference ID (Actor ID)
|
||||
A unique integer identifier assigned to each Actor in the game. Valid Reference IDs range from 16,777,216 to 0x7FFF_FFFF. The Reference ID is used to track and retrieve actors during the game's lifecycle.
|
||||
|
||||
### Render Order
|
||||
Determines the drawing layer for actors. From back to front:
|
||||
- **FAR_BEHIND** — Wires and conduits
|
||||
- **BEHIND** — Tapestries, some particles (obstructed by terrain)
|
||||
- **MIDDLE** — Standard actors (players, NPCs, creatures)
|
||||
- **MIDTOP** — Projectiles, thrown items
|
||||
- **FRONT** — Front walls (blocks that obstruct actors)
|
||||
- **OVERLAY** — Screen overlays, not affected by lightmap
|
||||
|
||||
### ActorWithBody
|
||||
An Actor that has physics properties, including position, velocity, hitbox, and collision. Most interactive game entities inherit from ActorWithBody rather than the base Actor class.
|
||||
|
||||
### Hitbox
|
||||
An axis-aligned bounding box (AABB) that defines an actor's collision area. The engine uses an AABB-based physics system optimised for handling millions of tile bodies.
|
||||
|
||||
### Block
|
||||
A tile in the game world. Blocks can be terrain, walls, platforms, or other tile-based objects. Each block has properties such as strength, density, material, luminosity, and collision behaviour.
|
||||
|
||||
### Wall
|
||||
A block placed in the wall layer, typically behind terrain. Walls do not provide collision by default but can affect lighting and provide visual background.
|
||||
|
||||
### Terrain
|
||||
A block placed in the terrain layer. Terrain blocks typically provide collision and are what players interact with directly.
|
||||
|
||||
### Tile
|
||||
A single grid position in the game world. The default tile size is 16×16 pixels (though this can be configured via `TILE_SIZE`). Used interchangeably with "block" in some contexts.
|
||||
|
||||
### Item
|
||||
A game object that can exist in inventories, be dropped in the world, or be used by actors. Items extend the `GameItem` class and have unique **Item IDs**.
|
||||
|
||||
### Item ID
|
||||
A string identifier for items following the format `item@<module>:<id>` (e.g., `item@basegame:1`). For blocks used as items, the format is `<module>:<id>` (e.g., `basegame:32`).
|
||||
|
||||
### Fixture
|
||||
A special type of actor representing interactable world objects such as doors, chests, crafting stations, and other furniture. Fixtures often have associated UI interactions.
|
||||
|
||||
## World & Generation
|
||||
|
||||
### GameWorld
|
||||
The container for all world data, including terrain layers, fluid simulation, lighting, wirings, and environmental properties. A GameWorld represents a single playable world/dimension.
|
||||
|
||||
### Chunk
|
||||
A subdivision of the world used for efficient storage and generation. Chunks are `CHUNK_W × CHUNK_H` tiles in size. The world generates chunks on-demand rather than all at once, enabling fast world creation for vast world sizes.
|
||||
|
||||
### Block Layer
|
||||
A 2D array storing block data for the world. The engine uses multiple specialised layers:
|
||||
- **layerTerrain** — Foreground blocks with collision
|
||||
- **layerWall** — Background wall blocks
|
||||
- **layerOres** — Ore deposits overlaid on terrain
|
||||
- **layerFluids** — Fluid simulation data
|
||||
|
||||
### Spawn Point
|
||||
The tilewise coordinate where players initially appear in a world. Stored as `spawnX` and `spawnY` in the GameWorld.
|
||||
|
||||
### World Time
|
||||
The in-game time system. Measured in seconds since the world's epoch. The engine simulates day/night cycles, seasons, and celestial events based on world time.
|
||||
|
||||
### Gravitation
|
||||
The gravitational acceleration vector applied to physics bodies. Currently, only downward gravity is supported. Default is approximately 9.8 m/s² in game units.
|
||||
|
||||
## Modules & Loading
|
||||
|
||||
### Module
|
||||
A package of game content (blocks, items, actors, code) that can be loaded by the engine. Modules are the modding/extension system for Terrarum. See also: [[Modules]]
|
||||
|
||||
### Internal Module
|
||||
A module shipped with the game, stored in `<game exec dir>/assets/mods/`. These modules form the "base game" and become read-only when packaged.
|
||||
|
||||
### User Module
|
||||
A user-created mod stored in `<appdata>/Modules/`. These can be freely modified by end users.
|
||||
|
||||
### Entry Point
|
||||
A class extending `ModuleEntryPoint` that initialises and registers a module's content with the game. The entry point is specified in the module's `metadata.properties`.
|
||||
|
||||
### ModMgr
|
||||
The Module Manager (ModMgr) singleton that handles module loading, dependency resolution, and content registration. It provides loaders for blocks, items, materials, languages, and other game content.
|
||||
|
||||
### Codex
|
||||
A registry that stores game content by ID. The engine maintains several codices:
|
||||
- **BlockCodex** — All blocks and terrain types
|
||||
- **ItemCodex** — All items
|
||||
- **WireCodex** — All wire/conduit types
|
||||
- **MaterialCodex** — All material definitions
|
||||
- **FactionCodex** — All faction definitions
|
||||
- **CraftingCodex** — All crafting recipes
|
||||
- **AudioCodex** — All audio assets
|
||||
- **WeatherCodex** — All weather types
|
||||
- **FluidCodex** — All fluid types
|
||||
- **OreCodex** — All ore types
|
||||
|
||||
## UI System
|
||||
|
||||
### UICanvas
|
||||
The base class for all UI screens and windows. A UICanvas can contain UIItems and sub-UIs, and manages its own show/hide animations and event handling.
|
||||
|
||||
### UIItem
|
||||
An individual UI element (button, slider, text field, etc.) that can be added to a UICanvas. UIItems have event listeners for clicks, drags, keyboard input, and updates.
|
||||
|
||||
### UIHandler
|
||||
Manages the lifecycle and state of a UICanvas, including opening/closing animations, positioning, and visibility state.
|
||||
|
||||
### IngameInstance
|
||||
A game screen that represents an active game session. Extends LibGDX's `Screen` interface and manages the game world, actors, UI, and rendering pipeline. The title screen and ingame view are both IngameInstances.
|
||||
|
||||
## Graphics & Rendering
|
||||
|
||||
### Autotiling
|
||||
A system that automatically selects the correct sprite variant for blocks based on neighbouring blocks. Terrarum uses a 7×7 tile atlas (112×112 pixels) for full autotiling support with connection rules.
|
||||
|
||||
### Tile Atlas
|
||||
A texture atlas containing all variants of a block's sprite for autotiling. The engine supports several atlas formats: 16×16 (single tile), 64×16 (wall stickers), 128×16 (platforms), 112×112 (full autotiling), and 224×224 (seasonal autotiling).
|
||||
|
||||
### Lightmap
|
||||
A texture representing the light levels (RGB+UV) for each tile in the visible area. The engine simulates realistic light propagation with transmittance and supports both static and dynamic light sources.
|
||||
|
||||
### Dynamic Light Function (dlfn)
|
||||
Defines how a luminous block's light output changes over time:
|
||||
- **0** — Static (constant brightness)
|
||||
- **1** — Torch flicker
|
||||
- **2** — Current global light (sun, star, moon)
|
||||
- **3** — Daylight at noon
|
||||
- **4** — Slow breath (gentle pulsing)
|
||||
- **5** — Pulsating (rhythmic)
|
||||
|
||||
### FlippingSpriteBatch
|
||||
A custom SpriteBatch implementation used throughout the engine for rendering sprites and textures.
|
||||
|
||||
### WorldCamera
|
||||
The orthographic camera that determines what portion of the world is currently visible on screen.
|
||||
|
||||
### Shader
|
||||
GLSL programs running on the GPU to render special effects. Terrarum uses OpenGL 3.2 Core Profile with GLSL version 1.50. Shaders are stored in `src/shaders/`.
|
||||
|
||||
## Physics & Collision
|
||||
|
||||
### AABB
|
||||
Axis-Aligned Bounding Box. The collision detection system used by the engine. All hitboxes are rectangular and aligned to the world axes (not rotated).
|
||||
|
||||
### Phys Properties
|
||||
Physical properties of an ActorWithBody, including:
|
||||
- **Mass** — Affects momentum and collision response
|
||||
- **Friction** — How much the actor resists movement on surfaces
|
||||
- **Immobile** — Whether the actor can move or is fixed in place
|
||||
- **Density** — Mass per unit volume (water = 1000 in game units)
|
||||
|
||||
### METER
|
||||
A fundamental constant defining the relationship between pixels and physics simulation. As of the current version, 1 metre equals 25 pixels. This constant is used throughout physics calculations.
|
||||
|
||||
### PHYS_TIME_FRAME
|
||||
The time step used by the physics simulator. Currently set to the value of METER for convenient 1:1 correspondence between game units and SI units.
|
||||
|
||||
## Audio
|
||||
|
||||
### AudioMixer
|
||||
The central audio system managing all sound output. Supports spatial audio, effects processing, and mixer buses.
|
||||
|
||||
### Track
|
||||
An audio channel in the mixer. The engine provides multiple dynamic tracks for music and sound effects, each with independent volume control, filters, and effects.
|
||||
|
||||
### MusicContainer
|
||||
A wrapper for music assets in the AudioBank. Manages music playback with crossfading and looping.
|
||||
|
||||
### Spatial Audio
|
||||
3D sound positioning system that adjusts volume and panning based on the listener's position relative to the sound source.
|
||||
|
||||
## Serialisation & Save/Load
|
||||
|
||||
### VirtualDisk
|
||||
The file archival format used for save games. Based on the TerranVirtualDisk library. Each savegame consists of a world disk and a player disk.
|
||||
|
||||
### Disk Skimmer
|
||||
A utility for reading savegame metadata without loading the entire save file. Used for displaying save information in load screens.
|
||||
|
||||
### Transient
|
||||
A Kotlin annotation (`@Transient`) marking fields that should not be serialised when saving the game state. Transient fields must be reconstructed when loading.
|
||||
|
||||
### Reload
|
||||
A method called on actors after deserialisation to reconstruct transient fields and re-establish references.
|
||||
|
||||
## Internationalisation
|
||||
|
||||
### Lang
|
||||
The language system managing text translations. Supports 20+ languages with dynamic font switching.
|
||||
|
||||
### IME
|
||||
Input Method Editor. Allows entry of complex scripts (e.g., Chinese, Japanese, Korean) using keyboard layouts. See also: [[Keyboard Layout and IME]]
|
||||
|
||||
### Low Layer
|
||||
The base keyboard layout (typically Latin characters like QWERTY) used by the IME.
|
||||
|
||||
### High Layer
|
||||
The advanced keyboard layout for complex scripts managed by the IME. Can be phonetic (reading from Low Layer) or independent.
|
||||
|
||||
### Terrarum Sans Bitmap
|
||||
The custom bitmap font system supporting multilingual text rendering. Maintained in a separate repository.
|
||||
|
||||
## Configuration
|
||||
|
||||
### App Config
|
||||
Global engine configuration stored in `config.json` in the appdata directory. Includes graphics settings, controls, audio settings, and debug options.
|
||||
|
||||
### Module Config
|
||||
Configuration specific to a module. Default values are in `<module>/default.json`, with user modifications stored in the global config file with the module name prefix (e.g., `basegame:config_key`).
|
||||
|
||||
### TILE_SIZE
|
||||
A configurable constant defining the pixel dimensions of one tile. Default is 16 pixels. Changing this requires engine recompilation.
|
||||
|
||||
## Debugging
|
||||
|
||||
### Debug Mode
|
||||
Enabled when `IS_DEVELOPMENT_BUILD` is true. Activates additional logging, assertions, and debug visualisations.
|
||||
|
||||
### Console
|
||||
An in-game developer console accessible during gameplay (typically via F12). Allows executing commands, spawning items/actors, and manipulating game state.
|
||||
|
||||
### hs_err_pid
|
||||
Log files generated by the JVM when it crashes. Useful for debugging native crashes during development.
|
||||
|
||||
## Miscellaneous
|
||||
|
||||
### Faction
|
||||
A group affiliation system for actors. Factions can have relationships (friendly, neutral, hostile) that affect AI behaviour and combat.
|
||||
|
||||
### Weather
|
||||
Environmental effects like rain, snow, or fog. Managed by the WeatherMixer and can affect visibility, lighting, and gameplay.
|
||||
|
||||
### Wire / Conduit
|
||||
A special type of block placed in the FAR_BEHIND render layer that connects devices and transmits signals or resources. Multiple wire types can occupy the same tile position.
|
||||
|
||||
### Wirings
|
||||
A hash map storing wire connection data for the world. Each block position can have multiple conduit types with different connection states.
|
||||
|
||||
### Canister
|
||||
A container type for storing fluids, gases, or other substances. Defined in the CanistersCodex.
|
||||
|
||||
### RAW
|
||||
Raws And Wiki-syntax. A data definition format inspired by Dwarf Fortress, used for defining creature properties and other complex game data. See: [[Creature RAW|Development:RAW]]
|
||||
|
||||
### Tapestry
|
||||
Decorative images that can be placed in the world as background art. See: [[Art Forgery|Modules:Tapestries]]
|
||||
|
||||
### RGBA8888
|
||||
A type alias for Int, representing a 32-bit colour value in RGBA format (8 bits per channel).
|
||||
|
||||
### Cvec
|
||||
Colour vector. A colour representation used for lighting calculations, supporting RGB plus UV (ultraviolet) channels.
|
||||
524
Inventory.md
Normal file
524
Inventory.md
Normal file
@@ -0,0 +1,524 @@
|
||||
# Inventory
|
||||
|
||||
The inventory system manages storage and organisation of items for actors, fixtures, and the world itself. It handles item stacking, equipment, encumbrance, and item persistence.
|
||||
|
||||
## Overview
|
||||
|
||||
Terrarum uses a flexible inventory system with several specialisations:
|
||||
|
||||
1. **FixtureInventory** — Base inventory for storage containers
|
||||
2. **ActorInventory** — Extended inventory for actors with equipment and quickslots
|
||||
3. **ItemTable** — Dynamic item storage for worlds
|
||||
|
||||
## GameItem
|
||||
|
||||
All items in the game extend the `GameItem` base class.
|
||||
|
||||
### Item Identification
|
||||
|
||||
Items use string-based IDs:
|
||||
|
||||
```kotlin
|
||||
typealias ItemID = String
|
||||
```
|
||||
|
||||
**Item ID formats:**
|
||||
- **Static items:** `item@<module>:<id>` (e.g., `item@basegame:1`)
|
||||
- **Blocks as items:** `<module>:<id>` (e.g., `basegame:32`)
|
||||
- **Dynamic items:** `item@<module>:D<number>` (e.g., `item@basegame:D4096`)
|
||||
- **Actor items:** Special prefix for actors in pockets
|
||||
|
||||
### Original ID vs. Dynamic ID
|
||||
|
||||
Items have two ID fields:
|
||||
|
||||
```kotlin
|
||||
val originalID: ItemID // The base item type (e.g., "item@basegame:1")
|
||||
var dynamicID: ItemID // The specific instance (may be same as originalID)
|
||||
```
|
||||
|
||||
**Static items** have `originalID == dynamicID` and can stack.
|
||||
|
||||
**Dynamic items** get unique `dynamicID` values and represent individual instances (e.g., a worn pickaxe).
|
||||
|
||||
### Key Properties
|
||||
|
||||
```kotlin
|
||||
// Basic Properties
|
||||
var baseMass: Double // Mass in kilograms
|
||||
var baseToolSize: Double? // Physical size for tools
|
||||
var inventoryCategory: String // "weapon", "tool", "armour", etc.
|
||||
|
||||
// Display
|
||||
var originalName: String // Translation key
|
||||
var name: String // Displayed name (with custom names)
|
||||
var nameColour: Color // Item name colour (rarity, etc.)
|
||||
var nameSecondary: String // Additional description
|
||||
|
||||
// Behaviour
|
||||
val canBeDynamic: Boolean // Can create unique instances
|
||||
val isCurrentlyDynamic: Boolean // Is this a dynamic instance
|
||||
var stackable: Boolean // Can stack in inventory
|
||||
val isConsumable: Boolean // Destroyed on use
|
||||
var isUnique: Boolean // Only one can exist
|
||||
var equipPosition: Int // Where to equip (EquipPosition enum)
|
||||
```
|
||||
|
||||
### Item Categories
|
||||
|
||||
The `inventoryCategory` classifies items:
|
||||
- **`weapon`** — Melee and ranged weapons
|
||||
- **`tool`** — Pickaxes, axes, hammers
|
||||
- **`armour`** — Protective equipment
|
||||
- **`block`** — Placeable blocks
|
||||
- **`wire`** — Wires and conduits
|
||||
- **`fixture`** — Furniture and fixtures
|
||||
- **`generic`** — Miscellaneous items
|
||||
- **`quest`** — Quest-related items
|
||||
|
||||
### Equipment Positions
|
||||
|
||||
Items can be equipped in specific slots defined by `EquipPosition`:
|
||||
|
||||
```kotlin
|
||||
EquipPosition.HAND_PRIMARY // Main hand
|
||||
EquipPosition.HAND_SECONDARY // Off hand
|
||||
EquipPosition.HEADGEAR // Helmet
|
||||
EquipPosition.ARMOUR // Body armour
|
||||
EquipPosition.LEGGINGS // Leg armour
|
||||
EquipPosition.BOOTS // Footwear
|
||||
EquipPosition.ACCESSORY_1 // Ring, amulet, etc.
|
||||
EquipPosition.ACCESSORY_2
|
||||
// ... more accessory slots
|
||||
EquipPosition.NULL // Not equippable
|
||||
```
|
||||
|
||||
### Dynamic vs. Static Items
|
||||
|
||||
**Static items** are identical instances that stack:
|
||||
- Blocks
|
||||
- Consumables
|
||||
- Resources
|
||||
- Stackable materials
|
||||
|
||||
**Dynamic items** have individual state:
|
||||
- Tools with durability
|
||||
- Weapons with enchantments
|
||||
- Armour with damage
|
||||
- Custom-named items
|
||||
|
||||
Creating a dynamic instance:
|
||||
|
||||
```kotlin
|
||||
val dynamicItem = staticItem.makeDynamic(inventory)
|
||||
```
|
||||
|
||||
Dynamic items get assigned IDs in the range 32768..1048575.
|
||||
|
||||
### Durability
|
||||
|
||||
Dynamic items can have durability:
|
||||
|
||||
```kotlin
|
||||
var durability: Float // Current durability (0.0 = broken)
|
||||
val maxDurability: Float // Maximum durability when new
|
||||
```
|
||||
|
||||
When durability reaches 0, the item is destroyed.
|
||||
|
||||
### Material System
|
||||
|
||||
Items are made from materials:
|
||||
|
||||
```kotlin
|
||||
val materialId: String // Material identifier
|
||||
val material: Material // Full material object
|
||||
```
|
||||
|
||||
Materials affect item properties like mass, durability, and value. See `MaterialCodex` for available materials.
|
||||
|
||||
### Combustibility
|
||||
|
||||
Items with the `COMBUSTIBLE` tag can be used as fuel:
|
||||
|
||||
```kotlin
|
||||
var calories: Double // Energy content (game-calories)
|
||||
var smokiness: Float // Smoke emission rate
|
||||
```
|
||||
|
||||
1 game-calorie ≈ 5 watt-hours. An item burning for 80 seconds at 60 FPS has 4800 calories.
|
||||
|
||||
### Tags
|
||||
|
||||
Items use tags to mark special properties:
|
||||
|
||||
```kotlin
|
||||
interface TaggedProp {
|
||||
fun hasTag(tag: String): Boolean
|
||||
fun addTag(tag: String)
|
||||
}
|
||||
```
|
||||
|
||||
Common tags:
|
||||
- **`TOOL`** — Can be used as a tool
|
||||
- **`WEAPON`** — Can be used as a weapon
|
||||
- **`COMBUSTIBLE`** — Can be burned as fuel
|
||||
- **`MAGIC`** — Has magical properties
|
||||
- **`TREASURE`** — Valuable loot
|
||||
|
||||
## FixtureInventory
|
||||
|
||||
The base inventory class for storage containers.
|
||||
|
||||
### Creating an Inventory
|
||||
|
||||
```kotlin
|
||||
val inventory = FixtureInventory(
|
||||
maxCapacity = 1000L, // Maximum capacity
|
||||
capacityMode = CAPACITY_MODE_COUNT
|
||||
)
|
||||
```
|
||||
|
||||
### Capacity Modes
|
||||
|
||||
```kotlin
|
||||
CAPACITY_MODE_COUNT // Limit by item count
|
||||
CAPACITY_MODE_WEIGHT // Limit by total weight
|
||||
CAPACITY_MODE_NO_ENCUMBER // Unlimited capacity
|
||||
```
|
||||
|
||||
### Adding Items
|
||||
|
||||
```kotlin
|
||||
// Add an item
|
||||
inventory.add(item: GameItem, count: Long = 1L)
|
||||
|
||||
// Add by ID
|
||||
inventory.add(itemID: ItemID, count: Long)
|
||||
```
|
||||
|
||||
Items automatically stack if they're stackable and have matching IDs.
|
||||
|
||||
### Removing Items
|
||||
|
||||
```kotlin
|
||||
// Remove by ID
|
||||
val removedCount = inventory.remove(itemID: ItemID, count: Long): Long
|
||||
|
||||
// Remove by item
|
||||
val removedCount = inventory.remove(item: GameItem, count: Long): Long
|
||||
```
|
||||
|
||||
Returns the actual number removed (may be less than requested).
|
||||
|
||||
### Querying Items
|
||||
|
||||
```kotlin
|
||||
// Check if item exists
|
||||
val has: Boolean = inventory.has(itemID: ItemID, minCount: Long = 1L)
|
||||
|
||||
// Search by ID
|
||||
val pair: InventoryPair? = inventory.searchByID(itemID: ItemID)
|
||||
|
||||
// Get item count
|
||||
val count: Long = inventory.count(itemID: ItemID)
|
||||
```
|
||||
|
||||
### InventoryPair
|
||||
|
||||
Inventory items are stored as pairs:
|
||||
|
||||
```kotlin
|
||||
data class InventoryPair(
|
||||
val itm: ItemID, // Item ID
|
||||
var qty: Long // Quantity
|
||||
)
|
||||
```
|
||||
|
||||
### Item List
|
||||
|
||||
Access all items in the inventory:
|
||||
|
||||
```kotlin
|
||||
val itemList: List<InventoryPair> = inventory.itemList
|
||||
```
|
||||
|
||||
**Important:** Don't modify the item list directly. Use `add()` and `remove()` methods.
|
||||
|
||||
### Capacity Tracking
|
||||
|
||||
```kotlin
|
||||
val capacity: Long // Current used capacity
|
||||
val maxCapacity: Long // Maximum capacity
|
||||
val encumberment: Double // 0.0-1.0+ (over 1.0 = overencumbered)
|
||||
```
|
||||
|
||||
### Clearing Inventory
|
||||
|
||||
```kotlin
|
||||
val removedItems: List<InventoryPair> = inventory.clear()
|
||||
```
|
||||
|
||||
Returns all items that were removed.
|
||||
|
||||
## ActorInventory
|
||||
|
||||
Extended inventory for actors (players, NPCs) with equipment and quickslots.
|
||||
|
||||
### Creating an Actor Inventory
|
||||
|
||||
```kotlin
|
||||
val inventory = ActorInventory(
|
||||
actor = playerActor,
|
||||
maxCapacity = 10000L,
|
||||
capacityMode = CAPACITY_MODE_WEIGHT
|
||||
)
|
||||
```
|
||||
|
||||
### Equipment System
|
||||
|
||||
Actors can equip items in specific slots:
|
||||
|
||||
```kotlin
|
||||
// Equipment array (indexed by EquipPosition)
|
||||
val itemEquipped: Array<ItemID?> = inventory.itemEquipped
|
||||
|
||||
// Equip an item
|
||||
actor.equipItem(itemID: ItemID)
|
||||
|
||||
// Unequip an item
|
||||
actor.unequipItem(itemID: ItemID)
|
||||
```
|
||||
|
||||
**Important:** Equipped items must also exist in the main `itemList`. The equipment array stores references (`dynamicID`), not separate copies.
|
||||
|
||||
### Quickslots
|
||||
|
||||
Actors have a quickslot bar for fast access:
|
||||
|
||||
```kotlin
|
||||
val quickSlot: Array<ItemID?> = inventory.quickSlot // 10 slots by default
|
||||
|
||||
// Set quickslot
|
||||
inventory.setQuickslotItem(slot: Int, dynamicID: ItemID?)
|
||||
|
||||
// Get quickslot item
|
||||
val pair: InventoryPair? = inventory.getQuickslotItem(slot: Int?)
|
||||
```
|
||||
|
||||
Quickslots reference items in the main inventory by their `dynamicID`.
|
||||
|
||||
### Dynamic Capacity
|
||||
|
||||
For actors, capacity can scale with actor size:
|
||||
|
||||
```kotlin
|
||||
val maxCapacityByActor: Double // Scaled by actor's scale squared
|
||||
```
|
||||
|
||||
This means larger actors can carry more.
|
||||
|
||||
### Consuming Items
|
||||
|
||||
```kotlin
|
||||
inventory.consumeItem(item: GameItem, amount: Long = 1L)
|
||||
```
|
||||
|
||||
This method:
|
||||
1. Removes consumable items
|
||||
2. Applies durability damage to tools/weapons
|
||||
3. Auto-equips replacements when tools break
|
||||
4. Handles dynamic item unpacking
|
||||
|
||||
### Item Durability Management
|
||||
|
||||
When using tools/weapons, durability decreases:
|
||||
|
||||
```kotlin
|
||||
// Damage calculation
|
||||
val baseDamage = actor.avStrength / 1000.0
|
||||
item.durability -= damageAmount
|
||||
|
||||
// When durability reaches 0
|
||||
if (item.durability <= 0) {
|
||||
// Item is removed and auto-replaced if available
|
||||
}
|
||||
```
|
||||
|
||||
## ItemCodex
|
||||
|
||||
The global registry of all items:
|
||||
|
||||
```kotlin
|
||||
object ItemCodex {
|
||||
// Look up item by ID
|
||||
operator fun get(itemID: ItemID): GameItem?
|
||||
|
||||
// Register a new item
|
||||
fun registerItem(item: GameItem)
|
||||
}
|
||||
```
|
||||
|
||||
Access items via the global codex:
|
||||
|
||||
```kotlin
|
||||
val item = ItemCodex[itemID]
|
||||
```
|
||||
|
||||
## Item Images
|
||||
|
||||
Items have associated sprites:
|
||||
|
||||
```kotlin
|
||||
// Get item image (use this, not item.itemImage directly)
|
||||
val texture: TextureRegion = ItemCodex.getItemImage(item)
|
||||
```
|
||||
|
||||
**Never read `item.itemImage` directly** due to initialization order issues. Always use `ItemCodex.getItemImage()`.
|
||||
|
||||
## Inventory UI
|
||||
|
||||
ActorInventory integrates with several UI components:
|
||||
|
||||
### UIQuickslotBar
|
||||
Displays and manages quickslot items.
|
||||
|
||||
### UIInventoryFull
|
||||
Full inventory screen with equipment, storage, and crafting.
|
||||
|
||||
### InventoryTransactionNegotiator
|
||||
Handles item transfers between inventories (drag-and-drop, shift-click, etc.).
|
||||
|
||||
## Best Practises
|
||||
|
||||
1. **Use dynamic IDs for equipment** — Store `dynamicID` in equipment/quickslot arrays, not `originalID`
|
||||
2. **Check canBeDynamic before creating instances** — Only dynamic items can have individual state
|
||||
3. **Clean up equipment on item removal** — ActorInventory does this automatically
|
||||
4. **Validate item existence** — Always check `ItemCodex[id]` returns non-null
|
||||
5. **Use ItemCodex.getItemImage()** — Never access `item.itemImage` directly
|
||||
6. **Scale capacity by actor size** — Use `maxCapacityByActor` for realistic encumbrance
|
||||
7. **Handle item consumption properly** — Use `consumeItem()` for tools to manage durability
|
||||
8. **Respect capacity modes** — Check `encumberment` to limit player movement when overencumbered
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Adding a Block to Inventory
|
||||
|
||||
```kotlin
|
||||
// Blocks use module:id format, not item@module:id
|
||||
val blockID = "basegame:32"
|
||||
inventory.add(blockID, 64L) // Add 64 blocks
|
||||
```
|
||||
|
||||
### Equipping a Tool
|
||||
|
||||
```kotlin
|
||||
val pickaxe = ItemCodex["item@basegame:pickaxe_iron"]
|
||||
if (pickaxe != null && pickaxe.canBeDynamic) {
|
||||
// Create dynamic instance
|
||||
val dynamicPickaxe = pickaxe.makeDynamic(actor.inventory)
|
||||
actor.inventory.add(dynamicPickaxe)
|
||||
|
||||
// Equip it
|
||||
actor.equipItem(dynamicPickaxe.dynamicID)
|
||||
|
||||
// Add to quickslot
|
||||
actor.inventory.setQuickslotItem(0, dynamicPickaxe.dynamicID)
|
||||
}
|
||||
```
|
||||
|
||||
### Checking Crafting Requirements
|
||||
|
||||
```kotlin
|
||||
fun hasRequiredItems(inventory: FixtureInventory, requirements: Map<ItemID, Long>): Boolean {
|
||||
return requirements.all { (itemID, count) ->
|
||||
inventory.has(itemID, count)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Transferring Items Between Inventories
|
||||
|
||||
```kotlin
|
||||
fun transferItem(from: FixtureInventory, to: FixtureInventory, itemID: ItemID, count: Long): Long {
|
||||
val removed = from.remove(itemID, count)
|
||||
if (removed > 0) {
|
||||
to.add(itemID, removed)
|
||||
}
|
||||
return removed
|
||||
}
|
||||
```
|
||||
|
||||
### Finding All Tools
|
||||
|
||||
```kotlin
|
||||
val tools = actor.inventory.itemList
|
||||
.mapNotNull { ItemCodex[it.itm] }
|
||||
.filter { it.hasTag("TOOL") }
|
||||
```
|
||||
|
||||
## Serialisation
|
||||
|
||||
Inventories are serialisable for save games. However:
|
||||
|
||||
- **`actor` field is `@Transient`** — Reconstructed on reload
|
||||
- **Equipment and quickslots** are saved
|
||||
- **Dynamic items** preserve their unique state
|
||||
|
||||
On reload, call:
|
||||
|
||||
```kotlin
|
||||
inventory.actor = restoredActor
|
||||
```
|
||||
|
||||
## Advanced Topics
|
||||
|
||||
### Custom Item Classes
|
||||
|
||||
Create custom items by extending GameItem:
|
||||
|
||||
```kotlin
|
||||
class MyCustomItem : GameItem("item@mymod:custom_item") {
|
||||
override var baseMass = 1.0
|
||||
override var baseToolSize: Double? = null
|
||||
override val canBeDynamic = true
|
||||
override val materialId = "iron"
|
||||
override var inventoryCategory = "generic"
|
||||
|
||||
init {
|
||||
originalName = "ITEM_CUSTOM_NAME"
|
||||
equipPosition = EquipPosition.HAND_PRIMARY
|
||||
}
|
||||
|
||||
override fun startPrimaryUse(actor: ActorWithBody, delta: Float) {
|
||||
// Custom usage logic
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Inventory Filtering
|
||||
|
||||
Filter items by category or tag:
|
||||
|
||||
```kotlin
|
||||
val weapons = inventory.itemList
|
||||
.mapNotNull { ItemCodex[it.itm] }
|
||||
.filter { it.inventoryCategory == "weapon" }
|
||||
```
|
||||
|
||||
### Weight Calculation
|
||||
|
||||
Calculate total inventory weight:
|
||||
|
||||
```kotlin
|
||||
val totalWeight = inventory.itemList.sumOf { (itemID, quantity) ->
|
||||
(ItemCodex[itemID]?.baseMass ?: 0.0) * quantity
|
||||
}
|
||||
```
|
||||
|
||||
## See Also
|
||||
|
||||
- [[Glossary]] — Item and inventory terminology
|
||||
- [[Modules:Items]] — Creating items in modules
|
||||
- [[Development:Items]] — In-depth item implementation
|
||||
- [[Actors]] — Actor system that uses inventories
|
||||
839
Items.md
Normal file
839
Items.md
Normal file
@@ -0,0 +1,839 @@
|
||||
# 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:
|
||||
|
||||
```kotlin
|
||||
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`:
|
||||
|
||||
```kotlin
|
||||
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:**
|
||||
|
||||
```kotlin
|
||||
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:
|
||||
|
||||
```csv
|
||||
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:
|
||||
|
||||
```kotlin
|
||||
// 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:
|
||||
|
||||
```kotlin
|
||||
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`:
|
||||
|
||||
```kotlin
|
||||
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`:
|
||||
|
||||
```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`:
|
||||
|
||||
```kotlin
|
||||
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:
|
||||
|
||||
```kotlin
|
||||
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:
|
||||
|
||||
```kotlin
|
||||
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:
|
||||
|
||||
```kotlin
|
||||
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:**
|
||||
|
||||
```kotlin
|
||||
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:
|
||||
|
||||
```kotlin
|
||||
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:
|
||||
|
||||
```kotlin
|
||||
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:**
|
||||
|
||||
```kotlin
|
||||
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:
|
||||
|
||||
```kotlin
|
||||
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:
|
||||
|
||||
```kotlin
|
||||
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:
|
||||
|
||||
```kotlin
|
||||
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:
|
||||
|
||||
```kotlin
|
||||
if (actor.actorValue.getAsBoolean("mymod:item.buffGiven") != true) {
|
||||
// Apply buff
|
||||
actor.actorValue["mymod:item.buffGiven"] = true
|
||||
}
|
||||
```
|
||||
|
||||
#### effectOnUnequip
|
||||
|
||||
Called once when item is unequipped:
|
||||
|
||||
```kotlin
|
||||
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:
|
||||
|
||||
```kotlin
|
||||
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:
|
||||
|
||||
```kotlin
|
||||
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:
|
||||
|
||||
```kotlin
|
||||
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:
|
||||
|
||||
```kotlin
|
||||
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:
|
||||
|
||||
```kotlin
|
||||
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:**
|
||||
|
||||
```kotlin
|
||||
// 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:
|
||||
|
||||
```kotlin
|
||||
// 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:**
|
||||
|
||||
```kotlin
|
||||
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:
|
||||
|
||||
```kotlin
|
||||
object ItemCodex {
|
||||
val itemCodex = ItemTable() // Static items
|
||||
val dynamicItemInventory = ItemTable() // Dynamic items
|
||||
val dynamicToStaticTable = HashMap<ItemID, ItemID>() // Dynamic → Static mapping
|
||||
}
|
||||
```
|
||||
|
||||
**Lookup:**
|
||||
|
||||
```kotlin
|
||||
val item = ItemCodex["dyn:123456789"] // Checks dynamic inventory first
|
||||
val originalID = ItemCodex.dynamicToStaticTable["dyn:123456789"] // "basegame:sword_iron"
|
||||
```
|
||||
|
||||
## Example Items
|
||||
|
||||
### Simple Consumable
|
||||
|
||||
```kotlin
|
||||
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
|
||||
|
||||
```kotlin
|
||||
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
|
||||
|
||||
```kotlin
|
||||
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
|
||||
|
||||
- [[Modules-Codex-Systems#ItemCodex]] — ItemCodex reference
|
||||
- [[Modules-Setup]] — Creating modules with items
|
||||
- [[Blocks]] — Block items and placement
|
||||
- [[Actors]] — Actor inventory system
|
||||
- [[Fixtures]] — Fixture items
|
||||
505
Languages.md
Normal file
505
Languages.md
Normal file
@@ -0,0 +1,505 @@
|
||||
# Languages
|
||||
|
||||
Terrarum features comprehensive internationalisation (i18n) support with over 20 languages, a custom multilingual font system, and sophisticated text rendering including support for complex scripts.
|
||||
|
||||
## Overview
|
||||
|
||||
The language system provides:
|
||||
- **20+ supported languages** — From English to Japanese to Hindi
|
||||
- **Terrarum Sans Bitmap** — Custom multilingual bitmap font
|
||||
- **String templates** — Dynamic text formatting
|
||||
- **Pluralisation** — Language-specific plural rules
|
||||
- **Postpositions** — Korean/Japanese grammatical particles
|
||||
- **Fallback system** — Graceful handling of missing translations
|
||||
- **Polyglot support** — Integration with Polyglot format
|
||||
|
||||
## Language Codes
|
||||
|
||||
Languages are identified by ISO locale codes:
|
||||
|
||||
### Supported Languages
|
||||
|
||||
| Code | Language |
|
||||
|------|----------|
|
||||
| `en` | English |
|
||||
| `koKR` | Korean |
|
||||
| `jaJP` | Japanese |
|
||||
| `zhCN` | Chinese (Simplified) |
|
||||
| `de` | German |
|
||||
| `frFR` | French |
|
||||
| `es` | Spanish |
|
||||
| `it` | Italian |
|
||||
| `ptBR` | Portuguese (Brazilian) |
|
||||
| `ruRU` | Russian |
|
||||
| `plPL` | Polish |
|
||||
| `nlNL` | Dutch |
|
||||
| `daDK` | Danish |
|
||||
| `noNB` | Norwegian (Bokmål) |
|
||||
| `fiFI` | Finnish |
|
||||
| `isIC` | Icelandic |
|
||||
| `csCZ` | Czech |
|
||||
| `huHU` | Hungarian |
|
||||
| `bgBG` | Bulgarian |
|
||||
| `elGR` | Greek |
|
||||
| `hiIN` | Hindi |
|
||||
|
||||
## Lang Object
|
||||
|
||||
The `Lang` singleton manages all translations:
|
||||
|
||||
### Getting Translations
|
||||
|
||||
```kotlin
|
||||
// Basic usage
|
||||
val text = Lang["STRING_ID"]
|
||||
|
||||
// With capitalisation
|
||||
val capitalised = Lang["string_id", capitalise = true]
|
||||
|
||||
// Safe access (returns null if missing)
|
||||
val text = Lang.getOrNull("STRING_ID")
|
||||
```
|
||||
|
||||
### String ID Format
|
||||
|
||||
Translation keys follow a naming convention:
|
||||
|
||||
```
|
||||
CONTEXT_SPECIFIC_DESCRIPTION
|
||||
|
||||
Examples:
|
||||
MENU_LANGUAGE_THIS // Menu item
|
||||
BLOCK_STONE_NAME // Block name
|
||||
ITEM_PICKAXE_DESC // Item description
|
||||
UI_INVENTORY_TITLE // UI text
|
||||
```
|
||||
|
||||
## Translation Files
|
||||
|
||||
### File Structure
|
||||
|
||||
Translations are organised by language:
|
||||
|
||||
```
|
||||
assets/locales/
|
||||
├── en/
|
||||
│ ├── main.json
|
||||
│ ├── blocks.json
|
||||
│ └── items.json
|
||||
├── koKR/
|
||||
│ ├── main.json
|
||||
│ └── ...
|
||||
└── jaJP/
|
||||
└── ...
|
||||
```
|
||||
|
||||
### Regular Format
|
||||
|
||||
Standard translation files use simple JSON:
|
||||
|
||||
```json
|
||||
{
|
||||
"MENU_NEW_GAME": "New Game",
|
||||
"MENU_LOAD_GAME": "Load Game",
|
||||
"MENU_OPTIONS": "Options",
|
||||
"BLOCK_STONE_NAME": "Stone",
|
||||
"ITEM_PICKAXE_NAME": "Pickaxe"
|
||||
}
|
||||
```
|
||||
|
||||
### Polyglot Format
|
||||
|
||||
For Polyglot-compatible files (prefix: `Polyglot-100_*.json`):
|
||||
|
||||
```json
|
||||
{
|
||||
"resources": {
|
||||
"polyglot": { /* metadata */ },
|
||||
"data": [
|
||||
{
|
||||
"n": "STRING_ID",
|
||||
"s": "Translated Text"
|
||||
},
|
||||
{
|
||||
"n": "ANOTHER_STRING_ID",
|
||||
"s": "Another Translation"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## String Templates
|
||||
|
||||
Templates allow dynamic text insertion:
|
||||
|
||||
### Basic Template Syntax
|
||||
|
||||
```kotlin
|
||||
// Translation file:
|
||||
"WELCOME_MESSAGE": "Welcome, %s!"
|
||||
|
||||
// Usage:
|
||||
val text = String.format(Lang["WELCOME_MESSAGE"], playerName)
|
||||
// Result: "Welcome, Steve!"
|
||||
```
|
||||
|
||||
### Bind Operator
|
||||
|
||||
The `>>=` operator binds strings to templates:
|
||||
|
||||
```kotlin
|
||||
// Define template
|
||||
"WALL_NAME_TEMPLATE": "%s Wall"
|
||||
|
||||
// Use bind operator in item names
|
||||
item.originalName = "BLOCK_STONE>>=WALL_NAME_TEMPLATE"
|
||||
|
||||
// Result: "Stone Wall"
|
||||
```
|
||||
|
||||
This is particularly useful for generating variants:
|
||||
|
||||
```kotlin
|
||||
"BLOCK_WOOD_OAK" // "Oak Wood"
|
||||
"BLOCK_WOOD_OAK>>=WALL_NAME_TEMPLATE" // "Oak Wood Wall"
|
||||
```
|
||||
|
||||
## Advanced Features
|
||||
|
||||
### Korean Postpositions
|
||||
|
||||
Korean requires dynamic postposition selection based on the final character:
|
||||
|
||||
```kotlin
|
||||
// Automatic postposition selection
|
||||
val name = "사과" // Apple (ends with vowel)
|
||||
Lang.formatKoreanPostposition(name, "은는") // Returns "는"
|
||||
|
||||
val name2 = "돌" // Stone (ends with consonant)
|
||||
Lang.formatKoreanPostposition(name2, "은는") // Returns "은"
|
||||
```
|
||||
|
||||
The system supports:
|
||||
- **은/는** — Topic marker
|
||||
- **이/가** — Subject marker
|
||||
- **을/를** — Object marker
|
||||
- **로/으로** — Direction marker
|
||||
|
||||
### Pluralisation
|
||||
|
||||
Different languages have different plural rules:
|
||||
|
||||
#### English
|
||||
|
||||
```kotlin
|
||||
// Regular plurals: add 's'
|
||||
"apple" -> "apples"
|
||||
|
||||
// Irregular handled specially
|
||||
"photo", "demo" -> normal 's' plural
|
||||
```
|
||||
|
||||
#### French
|
||||
|
||||
```kotlin
|
||||
// Special words with normal 's' plural
|
||||
"bal", "banal", "fatal", "final"
|
||||
|
||||
// Others follow French rules
|
||||
```
|
||||
|
||||
The pluralisation system is extensible for each language.
|
||||
|
||||
### Capitalisation
|
||||
|
||||
Automatic capitalisation respects language rules:
|
||||
|
||||
```kotlin
|
||||
val text = Lang["menu_item", capitalise = true]
|
||||
// English: "Menu Item"
|
||||
// German: "Menü-Element" (noun capitalisation rules)
|
||||
```
|
||||
|
||||
## Terrarum Sans Bitmap
|
||||
|
||||
The game uses a custom bitmap font supporting all languages:
|
||||
|
||||
### Font Features
|
||||
|
||||
- **Multilingual** — Latin, Cyrillic, Greek, CJK, Hangul, Devanagari
|
||||
- **Bitmap-based** — Pixel-perfect rendering at any size
|
||||
- **Fallback chains** — Graceful degradation for missing glyphs
|
||||
- **SDF rendering** — Smooth scaling with signed distance fields
|
||||
|
||||
### Font Sizes
|
||||
|
||||
The font system provides multiple sizes:
|
||||
|
||||
- **24pt** — UI text
|
||||
- **32pt** — Large text, headings
|
||||
- **48pt** — Very large text
|
||||
- **BigAlphNum** — Numbers and basic letters
|
||||
- **TinyAlphNum** — Tiny fallback font
|
||||
|
||||
See: [Terrarum Sans Bitmap Repository](https://github.com/minjaesong/Terrarum-sans-bitmap)
|
||||
|
||||
## Adding Translations
|
||||
|
||||
### For Modules
|
||||
|
||||
Add language files in your module:
|
||||
|
||||
```
|
||||
<module>/locales/
|
||||
├── en/
|
||||
│ └── mymod.json
|
||||
└── koKR/
|
||||
└── mymod.json
|
||||
```
|
||||
|
||||
Register them with the language loader:
|
||||
|
||||
```kotlin
|
||||
ModMgr.GameLanguageLoader.loadAll(
|
||||
moduleInfo,
|
||||
File("<module>/locales/")
|
||||
)
|
||||
```
|
||||
|
||||
### Translation File Example
|
||||
|
||||
```json
|
||||
{
|
||||
"MYMOD_ITEM_SPECIAL_SWORD": "Special Sword",
|
||||
"MYMOD_ITEM_SPECIAL_SWORD_DESC": "A sword with special properties",
|
||||
"MYMOD_BLOCK_MAGIC_STONE": "Magic Stone",
|
||||
"MYMOD_UI_CRAFTING_TITLE": "Magical Crafting"
|
||||
}
|
||||
```
|
||||
|
||||
### Best Practises
|
||||
|
||||
1. **Use prefixes** — Namespace your string IDs with module name
|
||||
2. **Group by context** — Separate files for items, blocks, UI
|
||||
3. **Provide fallbacks** — Always have English (`en`) translations
|
||||
4. **Keep keys stable** — Don't change IDs once released
|
||||
5. **Use templates** — Avoid duplicating similar strings
|
||||
6. **Test all languages** — Ensure text fits in UI elements
|
||||
|
||||
## Missing Translations
|
||||
|
||||
The system handles missing translations gracefully:
|
||||
|
||||
```kotlin
|
||||
val text = Lang["NONEXISTENT_KEY"]
|
||||
// Returns: "$NONEXISTENT_KEY" (with $ prefix)
|
||||
```
|
||||
|
||||
The `$` prefix indicates a missing translation. Check logs for warnings.
|
||||
|
||||
### Fallback Chain
|
||||
|
||||
1. Try requested language (e.g., `koKR`)
|
||||
2. Fall back to English (`en`)
|
||||
3. Return `$KEY` if not found
|
||||
|
||||
## Language Selection
|
||||
|
||||
### Setting Language
|
||||
|
||||
```kotlin
|
||||
App.GAME_LOCALE = "koKR" // Set to Korean
|
||||
Lang.load(File("./assets/locales/")) // Reload translations
|
||||
```
|
||||
|
||||
### Current Language
|
||||
|
||||
```kotlin
|
||||
val currentLang = App.GAME_LOCALE
|
||||
```
|
||||
|
||||
### Available Languages
|
||||
|
||||
```kotlin
|
||||
val languages: Set<String> = Lang.languageList
|
||||
```
|
||||
|
||||
## Caching System
|
||||
|
||||
The Lang object caches decoded strings for performance:
|
||||
|
||||
```kotlin
|
||||
// Cached per locale
|
||||
private val decodeCache = HashMap<String, HashMap<String, String>>()
|
||||
```
|
||||
|
||||
Cache is cleared when language changes.
|
||||
|
||||
## Name Generation
|
||||
|
||||
The system includes name generators for various cultures:
|
||||
|
||||
### Namesets
|
||||
|
||||
Namesets are CSV files defining name components:
|
||||
|
||||
```
|
||||
assets/locales/nameset_scandinavian_m.csv
|
||||
assets/locales/nameset_scandinavian_f.csv
|
||||
assets/locales/nameset_russian_m.csv
|
||||
assets/locales/nameset_russian_f.csv
|
||||
assets/locales/nameset_dwarven.csv
|
||||
assets/locales/nameset_exotic_deities.csv
|
||||
```
|
||||
|
||||
### Using Namesets
|
||||
|
||||
```kotlin
|
||||
val name = RandomWordsName.generate("scandinavian_m")
|
||||
// Returns: "Ragnar", "Bjorn", "Olaf", etc.
|
||||
```
|
||||
|
||||
## Language Properties
|
||||
|
||||
Language-specific properties are defined in `langprop.csv`:
|
||||
|
||||
```csv
|
||||
lang,direction,font,features
|
||||
en,ltr,latin,plural_s
|
||||
koKR,ltr,hangul,postposition
|
||||
jaJP,ltr,japanese,
|
||||
ar,rtl,arabic,rtl_text
|
||||
```
|
||||
|
||||
Properties include:
|
||||
- **direction** — Text direction (ltr/rtl)
|
||||
- **font** — Primary font family
|
||||
- **features** — Language-specific features
|
||||
|
||||
## Advanced Usage
|
||||
|
||||
### Dynamic Text Generation
|
||||
|
||||
Generate text with multiple variables:
|
||||
|
||||
```kotlin
|
||||
val template = Lang["CRAFTING_RECIPE_YIELDS"]
|
||||
// "Crafting %s with %s yields %d %s"
|
||||
|
||||
val text = String.format(
|
||||
template,
|
||||
item1Name,
|
||||
item2Name,
|
||||
quantity,
|
||||
resultName
|
||||
)
|
||||
```
|
||||
|
||||
### Formatting Numbers
|
||||
|
||||
Use locale-aware number formatting:
|
||||
|
||||
```kotlin
|
||||
val formatter = NumberFormat.getInstance(Locale(App.GAME_LOCALE))
|
||||
val formatted = formatter.format(12345.67)
|
||||
// English: "12,345.67"
|
||||
// German: "12.345,67"
|
||||
// French: "12 345,67"
|
||||
```
|
||||
|
||||
### Handling Long Text
|
||||
|
||||
For UI elements, check text width:
|
||||
|
||||
```kotlin
|
||||
val font = App.fontGame
|
||||
val width = font.getWidth(translatedText)
|
||||
|
||||
if (width > maxWidth) {
|
||||
// Truncate or wrap text
|
||||
}
|
||||
```
|
||||
|
||||
## Keyboard Input
|
||||
|
||||
See also: [[Keyboard Layout and IME]]
|
||||
|
||||
The IME system allows input in any language:
|
||||
|
||||
- **Low Layer** — Base keyboard (e.g., QWERTY)
|
||||
- **High Layer** — Language-specific input (e.g., Korean Hangul)
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Item Names with Templates
|
||||
|
||||
```kotlin
|
||||
class MyBlock : BlockBase() {
|
||||
init {
|
||||
originalName = "BLOCK_MYBLOCK>>=BLOCK_WALL_NAME_TEMPLATE"
|
||||
// Automatically generates "My Block Wall"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### UI Text with Variables
|
||||
|
||||
```kotlin
|
||||
fun showInventoryCount(count: Int) {
|
||||
val text = String.format(
|
||||
Lang["UI_INVENTORY_ITEMS_COUNT"],
|
||||
count
|
||||
)
|
||||
// "Items: 42"
|
||||
}
|
||||
```
|
||||
|
||||
### Safe Translation Access
|
||||
|
||||
```kotlin
|
||||
val text = Lang.getOrNull("OPTIONAL_STRING_ID") ?: "Default Text"
|
||||
```
|
||||
|
||||
## Debugging
|
||||
|
||||
### Finding Missing Translations
|
||||
|
||||
Search for `$` in rendered text:
|
||||
|
||||
```kotlin
|
||||
if (text.startsWith("$")) {
|
||||
println("Missing translation: ${text.substring(1)}")
|
||||
}
|
||||
```
|
||||
|
||||
### Logging Untranslated Strings
|
||||
|
||||
Enable debug logging:
|
||||
|
||||
```kotlin
|
||||
App.IS_DEVELOPMENT_BUILD = true
|
||||
```
|
||||
|
||||
Missing translations will be logged to console.
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
1. **Cache translations** — Don't call `Lang[]` every frame
|
||||
2. **Preload common strings** — Load frequently-used text at init
|
||||
3. **Avoid string concatenation** — Use templates instead
|
||||
4. **Batch font measurements** — Measure multiple strings together
|
||||
|
||||
## Limitations
|
||||
|
||||
- **No RTL support yet** — Right-to-left languages not fully implemented
|
||||
- **Limited plural forms** — Only basic plural rules
|
||||
- **Static at runtime** — Language changes require restart
|
||||
- **No gender agreement** — Some languages need grammatical gender
|
||||
|
||||
## See Also
|
||||
|
||||
- [[Glossary]] — Language-related terminology
|
||||
- [[Keyboard Layout and IME]] — Input methods for complex scripts
|
||||
- [Terrarum Sans Bitmap](https://github.com/minjaesong/Terrarum-sans-bitmap) — Font repository
|
||||
- [[Modules]] — Adding translations to modules
|
||||
@@ -37,3 +37,4 @@ The user config will have a key with `<module name>:` prepended: for example, if
|
||||
## Recondite References
|
||||
* [[Creature RAW|Development:RAW]]
|
||||
* [[Faction|Development:Faction]]
|
||||
* [[Codex System|Modules:Codex Systems]]
|
||||
|
||||
606
Modules:Codex-Systems.md
Normal file
606
Modules:Codex-Systems.md
Normal file
@@ -0,0 +1,606 @@
|
||||
# Codex Systems
|
||||
|
||||
**Audience:** Module developers and engine maintainers working with game content registration.
|
||||
|
||||
Terrarum's content is registered through **Codex** systems — centralised registries for game subsystems. Each Codex stores definitions loaded from CSV or JSON files in modules, accessible via unique identifiers.
|
||||
|
||||
## Overview
|
||||
|
||||
Codex systems provide:
|
||||
- **Centralised registration** — Single source of truth for all content
|
||||
- **Module isolation** — Each module's content is prefixed with module name
|
||||
- **Hot-reloading support** — Later modules override earlier definitions
|
||||
- **Type-safe access** — Compile-time checking via ItemID type aliases
|
||||
- **Tag-based queries** — Search by tags for similar content
|
||||
|
||||
## Codex Architecture
|
||||
|
||||
### Common Pattern
|
||||
|
||||
All Codices follow this structure:
|
||||
|
||||
```kotlin
|
||||
class SomeCodex {
|
||||
@Transient val registry = HashMap<ItemID, SomeProperty>()
|
||||
@Transient private val nullProp = SomeProperty()
|
||||
|
||||
// Access by ID
|
||||
operator fun get(id: ItemID?): SomeProperty {
|
||||
return registry[id] ?: nullProp
|
||||
}
|
||||
|
||||
// Load from module
|
||||
fun fromModule(module: String, path: String) {
|
||||
register(module, CSVFetcher.readFromModule(module, path))
|
||||
}
|
||||
|
||||
private fun register(module: String, records: List<CSVRecord>) {
|
||||
records.forEach { setProp(module, it) }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### ID Naming Convention
|
||||
|
||||
All IDs follow the pattern: `[prefix]@<modulename>:<id>`
|
||||
|
||||
**Examples:**
|
||||
- Block: `basegame:1` (stone)
|
||||
- Fluid: `fluid@basegame:1` (water)
|
||||
- Ore: `ores@basegame:3` (iron ore)
|
||||
- Item: `basegame:pickaxe_copper`
|
||||
|
||||
### Global Singletons
|
||||
|
||||
Codices are stored as global singletons:
|
||||
|
||||
```kotlin
|
||||
object Terrarum {
|
||||
lateinit var blockCodex: BlockCodex
|
||||
lateinit var fluidCodex: FluidCodex
|
||||
}
|
||||
|
||||
object ItemCodex {
|
||||
val itemCodex = ItemTable()
|
||||
}
|
||||
|
||||
object WeatherCodex {
|
||||
internal val weatherById = HashMap<String, BaseModularWeather>()
|
||||
}
|
||||
```
|
||||
|
||||
## BlockCodex
|
||||
|
||||
Defines properties for all terrain and wall tiles.
|
||||
|
||||
### CSV Format
|
||||
|
||||
**File:** `blocks/blocks.csv`
|
||||
|
||||
**Note:** CSVs use **semicolons** (`;`) as delimiters, not commas.
|
||||
|
||||
```csv
|
||||
"id";"drop";"spawn";"name";"shdr";"shdg";"shdb";"shduv";"str";"dsty";"mate";"solid";"wall";"grav";"dlfn";"fv";"fr";"lumr";"lumg";"lumb";"lumuv";"refl";"tags"
|
||||
"0";"N/A";"N/A";"BLOCK_AIR";"0.0312";"0.0312";"0.0312";"0.0312";"1";"1";"AIIR";"0";"1";"N/A";"0";"0";"4";"0.0000";"0.0000";"0.0000";"0.0000";"0.0";"INCONSEQUENTIAL,AIR,NORANDTILE"
|
||||
"2";"basegame:2";"basegame:2";"BLOCK_STONE";"0.6290";"0.6290";"0.6290";"0.6290";"120";"2600";"ROCK";"1";"0";"N/A";"0";"0";"16";"0.0000";"0.0000";"0.0000";"0.0000";"0.0";"STONE,NATURAL,MINERAL"
|
||||
```
|
||||
|
||||
**Key Fields:**
|
||||
- `id` — Numeric tile ID (unique within module)
|
||||
- `name` — Translation key
|
||||
- `shdr/shdg/shdb/shduv` — Shade colour (RGB + UV channels, 0.0-1.0)
|
||||
- `str` — Strength/HP (mining difficulty)
|
||||
- `dsty` — Density (kg/m³)
|
||||
- `mate` — Material ID (4-letter code)
|
||||
- `solid` — Is solid (1) or passable (0)
|
||||
- `wall` — Is wall tile (1) or terrain (0)
|
||||
- `fr` — Horizontal friction coefficient (16 = normal)
|
||||
- `lumr/lumg/lumb/lumuv` — Luminosity (RGB + UV channels, 0.0-1.0)
|
||||
- `tags` — Comma-separated tags (STONE, SOIL, etc.)
|
||||
- `drop` — Item ID dropped when mined
|
||||
- `spawn` — Item ID used to place this block
|
||||
|
||||
### Usage
|
||||
|
||||
```kotlin
|
||||
// Access block properties
|
||||
val stone = BlockCodex["basegame:1"]
|
||||
println("HP: ${stone.strength}")
|
||||
println("Solid: ${stone.isSolid}")
|
||||
println("Friction: ${stone.frictionCoeff}")
|
||||
|
||||
// Check tags
|
||||
if (stone.hasTag("STONE")) {
|
||||
println("This is stone!")
|
||||
}
|
||||
|
||||
// Get all blocks with a tag
|
||||
val soilBlocks = BlockCodex.blockProps.values.filter { it.hasTag("SOIL") }
|
||||
```
|
||||
|
||||
### Loading
|
||||
|
||||
```kotlin
|
||||
object GameBlockLoader {
|
||||
init {
|
||||
Terrarum.blockCodex = BlockCodex()
|
||||
}
|
||||
|
||||
operator fun invoke(module: String) {
|
||||
Terrarum.blockCodex.fromModule(module, "blocks/blocks.csv") { tile ->
|
||||
// Register block as item
|
||||
ItemCodex[tile.id] = makeNewItemObj(tile, isWall = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## ItemCodex
|
||||
|
||||
Defines all items, including blocks, tools, and fixtures.
|
||||
|
||||
### Item Types
|
||||
|
||||
Items come from multiple sources:
|
||||
1. **Blocks** — Automatically registered from BlockCodex
|
||||
2. **Static items** — Defined in `items/items.csv`
|
||||
3. **Dynamic items** — Runtime-created (customised tools, signed books)
|
||||
4. **Actor items** — Items created from actors (drops)
|
||||
5. **Fixture items** — Items that spawn fixtures
|
||||
|
||||
### CSV Format
|
||||
|
||||
**File:** `items/itemid.csv`
|
||||
|
||||
**Note:** Items are registered by **fully-qualified class names**, not inline properties.
|
||||
|
||||
```csv
|
||||
id;classname;tags
|
||||
1;net.torvald.terrarum.modulebasegame.gameitems.PickaxeCopper;TOOL,PICK
|
||||
2;net.torvald.terrarum.modulebasegame.gameitems.PickaxeIron;TOOL,PICK
|
||||
14;net.torvald.terrarum.modulebasegame.gameitems.PickaxeWood;TOOL,PICK
|
||||
```
|
||||
|
||||
Each item is a Kotlin class extending `GameItem`:
|
||||
|
||||
```kotlin
|
||||
class PickaxeCopper(originalID: ItemID) : GameItem(originalID) {
|
||||
override var baseMass = 2.5
|
||||
override var baseToolSize: Double? = 1.0
|
||||
override val canBeDynamic = true
|
||||
override var inventoryCategory = "tool"
|
||||
override val materialId = "COPR"
|
||||
|
||||
init {
|
||||
originalName = "ITEM_PICKAXE_COPPER"
|
||||
tags.add("TOOL")
|
||||
tags.add("PICK")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Usage
|
||||
|
||||
```kotlin
|
||||
// Get item
|
||||
val pickaxe = ItemCodex["basegame:pickaxe_copper"]
|
||||
println("Mass: ${pickaxe.mass}")
|
||||
println("Stackable: ${pickaxe.maxStackSize}")
|
||||
|
||||
// Check if item exists
|
||||
if (ItemCodex.itemCodex.containsKey("basegame:sword_iron")) {
|
||||
// Item exists
|
||||
}
|
||||
|
||||
// Create dynamic item
|
||||
val customSword = ItemCodex["basegame:sword_iron"]?.copy()
|
||||
customSword.tags.add("LEGENDARY")
|
||||
val dynamicID = "dyn:${UUID.randomUUID()}"
|
||||
ItemCodex.registerNewDynamicItem(dynamicID, customSword)
|
||||
```
|
||||
|
||||
### Dynamic Items
|
||||
|
||||
**Dynamic items** are runtime-created items that can be modified:
|
||||
|
||||
```kotlin
|
||||
// Original static item
|
||||
val baseSword = ItemCodex["basegame:sword_iron"]
|
||||
|
||||
// Create customised version
|
||||
val legendarySwor =d baseSword.copy()
|
||||
legendarySword.nameStr = "§o§Excalibur§.§"
|
||||
legendarySword.tags.add("LEGENDARY")
|
||||
legendarySword.actorValue.set(AVKey.DAMAGE, 999.0)
|
||||
|
||||
// Register with dynamic ID
|
||||
val dynamicID = "dyn:${System.nanoTime()}"
|
||||
ItemCodex.registerNewDynamicItem(dynamicID, legendarySword)
|
||||
```
|
||||
|
||||
**Dynamic item serialisation:**
|
||||
- `dynamicItemInventory` — Stores all dynamic items
|
||||
- `dynamicToStaticTable` — Maps dynamic ID → original static ID
|
||||
- Both saved to disk with player/world data
|
||||
|
||||
## MaterialCodex
|
||||
|
||||
Defines physical materials and their properties.
|
||||
|
||||
### CSV Format
|
||||
|
||||
**File:** `materials/materials.csv`
|
||||
|
||||
**Note:** Uses semicolons (`;`) as delimiters.
|
||||
|
||||
```csv
|
||||
idst;tens;impf;dsty;fmod;endurance;tcond;reach;rcs;sondrefl;comments
|
||||
WOOD;10;10;800;0.3;0.23;0.17;5;18;0.5;just a generic wood
|
||||
ROCK;15;210;3000;0.55;0.64;2.9;5;48;1.0;data is that of marble
|
||||
COPR;30;120;8900;0.35;0.35;400;5;82;0.8;copper
|
||||
IRON;50;210;7800;0.3;0.48;80;5;115;0.9;wrought iron
|
||||
```
|
||||
|
||||
**Key Fields:**
|
||||
- `idst` — 4-letter material ID code
|
||||
- `tens` — Tensile strength
|
||||
- `impf` — Impact force resistance
|
||||
- `dsty` — Density (kg/m³)
|
||||
- `fmod` — Flexibility modulus
|
||||
- `endurance` — Durability/endurance
|
||||
- `tcond` — Thermal conductivity
|
||||
- `reach` — Reach/range factor
|
||||
- `rcs` — Radar cross-section (for detection)
|
||||
- `sondrefl` — Sound reflectance (0.0-1.0)
|
||||
- `comments` — Human-readable notes
|
||||
|
||||
### Usage
|
||||
|
||||
```kotlin
|
||||
val iron = MaterialCodex["IRON"]
|
||||
println("Density: ${iron.density} kg/m³")
|
||||
println("Melting point: ${iron.meltingPoint}°C")
|
||||
println("Conductive: ${iron.hasTag("CONDUCTIVE")}")
|
||||
```
|
||||
|
||||
## FluidCodex
|
||||
|
||||
Defines liquid properties for fluid simulation.
|
||||
|
||||
### CSV Format
|
||||
|
||||
**File:** `fluid/fluids.csv`
|
||||
|
||||
**Note:** Uses semicolons (`;`) as delimiters.
|
||||
|
||||
```csv
|
||||
"id";"name";"shdr";"shdg";"shdb";"shduv";"str";"dsty";"mate";"lumr";"lumg";"lumb";"lumuv";"colour";"vscs";"refl";"tags";"therm"
|
||||
"1";"BLOCK_WATER";"0.1016";"0.0744";"0.0508";"0.0826";"100";"1000";"WATR";"0.0000";"0.0000";"0.0000";"0.0000";"005599A6";"5";"0.0";"NATURAL";0
|
||||
"2";"BLOCK_LAVA";"0.1252";"0.1252";"0.1252";"0.1252";"100";"2600";"ROCK";"0.7664";"0.2032";"0.0000";"0.0000";"FF4600E6";"16";"0.0";"NATURAL,MOLTEN";2
|
||||
```
|
||||
|
||||
**Key Fields:**
|
||||
- `id` — Numeric fluid ID
|
||||
- `name` — Translation key
|
||||
- `shdr/shdg/shdb/shduv` — Shade colour (RGB + UV, 0.0-1.0)
|
||||
- `lumr/lumg/lumb/lumuv` — Luminosity (RGB + UV, 0.0-1.0)
|
||||
- `str` — Fluid strength (flow pressure)
|
||||
- `dsty` — Density (kg/m³)
|
||||
- `mate` — Material ID (4-letter code)
|
||||
- `vscs` — Viscosity (resistance to flow)
|
||||
- `colour` — Display colour (RRGGBBAA hex, no 0x prefix)
|
||||
- `refl` — Reflectance (0.0-1.0)
|
||||
- `therm` — Thermal state (-1=cryogenic, 0=normal, 1=hot, 2=molten)
|
||||
- `tags` — Comma-separated tags
|
||||
|
||||
### Usage
|
||||
|
||||
```kotlin
|
||||
val water = FluidCodex["fluid@basegame:1"]
|
||||
println("Density: ${water.density}")
|
||||
println("Viscosity: ${water.viscosity}")
|
||||
println("Colour: ${water.colour.toString(16)}")
|
||||
|
||||
// Flow simulation uses these properties
|
||||
val flowSpeed = fluid.strength / fluid.viscosity
|
||||
```
|
||||
|
||||
## OreCodex
|
||||
|
||||
Defines ore generation parameters for world generation.
|
||||
|
||||
### CSV Format
|
||||
|
||||
**File:** `ores/ores.csv`
|
||||
|
||||
**Note:** Uses semicolons (`;`) as delimiters.
|
||||
|
||||
```csv
|
||||
"id";"item";"tags";"versionsince"
|
||||
"1";"item@basegame:128";"COPPER,MALACHITE";0
|
||||
"2";"item@basegame:129";"IRON,HAEMATITE";0
|
||||
"3";"item@basegame:130";"COAL,CARBON";0
|
||||
"6";"item@basegame:133";"GOLD,NATURAL_GOLD";0
|
||||
```
|
||||
|
||||
**Key Fields:**
|
||||
- `id` — Numeric ore ID
|
||||
- `item` — Item ID that represents this ore
|
||||
- `tags` — Comma-separated tags (ore type, mineral name)
|
||||
- `versionsince` — Version when ore was introduced
|
||||
|
||||
### Usage
|
||||
|
||||
```kotlin
|
||||
val ironOre = OreCodex["ores@basegame:1"]
|
||||
println("Item drop: ${ironOre.item}")
|
||||
println("Rare: ${ironOre.hasTag("RARE")}")
|
||||
|
||||
// Used by world generator
|
||||
val allOres = OreCodex.getAll()
|
||||
allOres.filter { it.hasTag("METAL") }.forEach { ore ->
|
||||
worldgen.generateOreVein(ore)
|
||||
}
|
||||
```
|
||||
|
||||
## WeatherCodex
|
||||
|
||||
Defines weather systems with skybox, clouds, and lighting.
|
||||
|
||||
### JSON Format
|
||||
|
||||
**File:** `weathers/clear_day.json`
|
||||
|
||||
```json
|
||||
{
|
||||
"identifier": "clear_day",
|
||||
"tags": "CLEAR,DAY",
|
||||
"skyboxGradColourMap": "lut:sky_clear_day.tga",
|
||||
"daylightClut": "lut:daylight_clear.tga",
|
||||
"cloudChance": 0.3,
|
||||
"windSpeed": 2.5,
|
||||
"windSpeedVariance": 1.0,
|
||||
"windSpeedDamping": 0.95,
|
||||
"cloudGamma": [1.0, 1.0],
|
||||
"cloudGammaVariance": [0.1, 0.1],
|
||||
"shaderVibrancy": [1.0, 0.9, 0.8],
|
||||
"clouds": {
|
||||
"cumulus": {
|
||||
"filename": "cloud_cumulus.tga",
|
||||
"tw": 64,
|
||||
"th": 32,
|
||||
"probability": 0.7,
|
||||
"baseScale": 1.0,
|
||||
"scaleVariance": 0.3,
|
||||
"altLow": 0.6,
|
||||
"altHigh": 0.8
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Usage
|
||||
|
||||
```kotlin
|
||||
// Get weather by ID
|
||||
val clearDay = WeatherCodex.getById("clear_day")
|
||||
|
||||
// Query by tags
|
||||
val clearWeathers = WeatherCodex.getByTag("CLEAR")
|
||||
val stormyWeathers = WeatherCodex.getByAllTagsOf("STORM", "RAIN")
|
||||
|
||||
// Random weather
|
||||
val randomWeather = WeatherCodex.getRandom(tag = "DAY")
|
||||
|
||||
// Apply weather to world
|
||||
world.weather = clearDay
|
||||
```
|
||||
|
||||
## CraftingCodex
|
||||
|
||||
Defines crafting recipes for workbenches.
|
||||
|
||||
### JSON Format
|
||||
|
||||
**File:** `crafting/tools.json` (or other recipe files)
|
||||
|
||||
**Note:** JSON format supports C-style comments (`/* */`).
|
||||
|
||||
```json
|
||||
{
|
||||
"item@basegame:14": { /* wooden pick */
|
||||
"workbench": "basiccrafting",
|
||||
"ingredients": [[1, 5, "$WOOD", 2, "item@basegame:18"]] /* 5 woods, 2 sticks */
|
||||
},
|
||||
"item@basegame:1": { /* copper pick */
|
||||
"workbench": "basiccrafting,metalworking",
|
||||
"ingredients": [[1, 5, "item@basegame:112", 2, "item@basegame:18"]] /* 5 bars, 2 sticks */
|
||||
},
|
||||
"item@basegame:2": { /* iron pick */
|
||||
"workbench": "basiccrafting,metalworking",
|
||||
"ingredients": [[1, 5, "item@basegame:113", 2, "item@basegame:18"]] /* 5 bars, 2 sticks */
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Format:**
|
||||
- `workbench` — Required workbench ID (comma-separated for multiple workbenches)
|
||||
- `ingredients` — Array of recipe variations (multiple ways to craft same item)
|
||||
- First element: MOQ (minimum output quantity)
|
||||
- Remaining elements: alternating quantity and item ID
|
||||
- `$TAG` — Tag-based ingredient (any item with tag, e.g., `$WOOD`, `$ROCK`)
|
||||
|
||||
### Usage
|
||||
|
||||
```kotlin
|
||||
// Get recipes for item
|
||||
val recipes = CraftingCodex.props["basegame:pickaxe_iron"]
|
||||
recipes?.forEach { recipe ->
|
||||
println("Workbench: ${recipe.workbench}")
|
||||
println("Makes: ${recipe.moq} items")
|
||||
recipe.ingredients.forEach { ingredient ->
|
||||
println(" - ${ingredient.qty}× ${ingredient.item}")
|
||||
}
|
||||
}
|
||||
|
||||
// Check if craftable
|
||||
fun canCraft(itemID: ItemID, inventory: Inventory): Boolean {
|
||||
val recipes = CraftingCodex.props[itemID] ?: return false
|
||||
return recipes.any { recipe ->
|
||||
recipe.ingredients.all { ingredient ->
|
||||
inventory.has(ingredient.item, ingredient.qty)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## CanistersCodex
|
||||
|
||||
Defines canister properties for fluid containers.
|
||||
|
||||
### CSV Format
|
||||
|
||||
**File:** `canisters/canisters.csv`
|
||||
|
||||
**Note:** Uses semicolons (`;`) as delimiters.
|
||||
|
||||
```csv
|
||||
id;itemid;tags
|
||||
1;item@basegame:59;FLUIDSTORAGE,OPENSTORAGE,NOEXTREMETHERM
|
||||
2;item@basegame:60;FLUIDSTORAGE,OPENSTORAGE
|
||||
```
|
||||
|
||||
**Key Fields:**
|
||||
- `id` — Numeric canister ID
|
||||
- `itemid` — Item ID for the canister item
|
||||
- `tags` — Comma-separated tags (storage type, constraints)
|
||||
|
||||
### Usage
|
||||
|
||||
```kotlin
|
||||
val bucket = CanistersCodex["basegame_1"]
|
||||
println("Item: ${bucket.itemID}")
|
||||
println("Is bucket: ${bucket.hasTag("BUCKET")}")
|
||||
|
||||
// Get canister for item
|
||||
val canister = CanistersCodex.CanisterProps.values.find {
|
||||
it.itemID == "basegame:bucket_wooden"
|
||||
}
|
||||
```
|
||||
|
||||
## TerrarumWorldWatchdog
|
||||
|
||||
Defines periodic world update functions.
|
||||
|
||||
### Structure
|
||||
|
||||
```kotlin
|
||||
abstract class TerrarumWorldWatchdog(val runIntervalByTick: Int) {
|
||||
abstract operator fun invoke(world: GameWorld)
|
||||
}
|
||||
```
|
||||
|
||||
**`runIntervalByTick`:**
|
||||
- `1` — Every tick
|
||||
- `60` — Every second (at 60 TPS)
|
||||
- `1200` — Every 20 seconds
|
||||
|
||||
### Usage
|
||||
|
||||
```kotlin
|
||||
class MyModWatchdog : TerrarumWorldWatchdog(60) {
|
||||
override fun invoke(world: GameWorld) {
|
||||
// Runs every second
|
||||
world.actors.forEach { actor ->
|
||||
if (actor.hasTag("BURNING")) {
|
||||
actor.takeDamage(1.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Register in module entry point
|
||||
override fun invoke() {
|
||||
ModMgr.registerWatchdog(MyModWatchdog())
|
||||
}
|
||||
```
|
||||
|
||||
## ExtraGUI and Retexturing
|
||||
|
||||
These are specialised systems for UI extensions and texture replacements.
|
||||
|
||||
### ExtraGUI
|
||||
|
||||
Defines additional UI overlays:
|
||||
|
||||
```kotlin
|
||||
// Register custom GUI
|
||||
ModMgr.registerExtraGUI("mymod:custom_hud") {
|
||||
MyCustomHUDCanvas()
|
||||
}
|
||||
```
|
||||
|
||||
### Retexturing
|
||||
|
||||
Allows texture pack overrides:
|
||||
|
||||
```kotlin
|
||||
// Register texture replacement
|
||||
ModMgr.registerRetexture("basegame:stone") {
|
||||
TextureRegionPack(getFile("blocks/stone_alternate.tga"), 16, 16)
|
||||
}
|
||||
```
|
||||
|
||||
## Module Loading Order
|
||||
|
||||
Codices must be loaded in **dependency order**:
|
||||
|
||||
```kotlin
|
||||
override fun invoke() {
|
||||
// GROUP 0: Dependencies for everything
|
||||
ModMgr.GameMaterialLoader.invoke(moduleName)
|
||||
ModMgr.GameFluidLoader.invoke(moduleName)
|
||||
|
||||
// GROUP 1: Items depend on materials
|
||||
ModMgr.GameItemLoader.invoke(moduleName)
|
||||
|
||||
// GROUP 2: Blocks/ores depend on items/materials
|
||||
ModMgr.GameBlockLoader.invoke(moduleName)
|
||||
ModMgr.GameOreLoader.invoke(moduleName)
|
||||
|
||||
// GROUP 3: Everything else
|
||||
ModMgr.GameCraftingRecipeLoader.invoke(moduleName)
|
||||
ModMgr.GameLanguageLoader.invoke(moduleName)
|
||||
ModMgr.GameAudioLoader.invoke(moduleName)
|
||||
ModMgr.GameWeatherLoader.invoke(moduleName)
|
||||
ModMgr.GameCanistersLoader.invoke(moduleName)
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practises
|
||||
|
||||
1. **Use consistent ID naming** — `modulename:itemname` for all IDs
|
||||
2. **Tag extensively** — Tags enable flexible queries
|
||||
3. **Document CSV columns** — Comment headers with field descriptions
|
||||
4. **Version your content** — Use `versionsince` field for compatibility
|
||||
5. **Validate on load** — Check for missing dependencies
|
||||
6. **Use null objects** — Return sensible defaults for missing IDs
|
||||
7. **Prefix virtual tiles** — Use `virtualtile:` prefix
|
||||
8. **Don't hardcode IDs** — Use string constants or enums
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- **Wrong module name** — ID won't resolve (`baseagme:` vs `basegame:`)
|
||||
- **Missing prefix** — Fluids need `fluid@`, ores need `ores@`
|
||||
- **Load order violation** — Loading blocks before materials
|
||||
- **Circular dependencies** — Item A requires Item B which requires Item A
|
||||
- **Forgetting tags** — Can't query content without tags
|
||||
- **Not calling reload()** — Dynamic items need manual reconstruction
|
||||
- **Hardcoded numeric IDs** — Use string IDs for portability
|
||||
|
||||
## See Also
|
||||
|
||||
- [[Modules-Setup]] — Creating and loading modules
|
||||
- [[Blocks]] — Block system details
|
||||
- [[Items]] — Item system details
|
||||
- [[World]] — World generation using Codices
|
||||
702
Modules:Setup.md
702
Modules:Setup.md
@@ -1,21 +1,106 @@
|
||||
## Metadata
|
||||
Every module must be able to introduce themselves through the `metadata.properties` file.
|
||||
# Module Setup
|
||||
|
||||
The metadata file is a standard-issue Java properties file with some required keys.
|
||||
**Audience:** Module developers creating new game content or total conversion mods.
|
||||
|
||||
### metadata.properties
|
||||
A metadata must have the following keys:
|
||||
Modules (mods) extend Terrarum with new blocks, items, actors, and systems. This guide covers the complete module structure, loading process, and best practises for creating modules.
|
||||
|
||||
- **propername** — the displayed name of the module
|
||||
- **description** — a short text describing the module
|
||||
- **author** — the author of the module
|
||||
- **package** — the root package of the module
|
||||
- **entrypoint** — the fully-qualified class name of the Entry Point (see Contents § Entry Point)
|
||||
- **version** — the version of the module. The version format must strictly follow the [Semver 2.0.0](https://semver.org/) scheme
|
||||
- **releasedate** — the release date of the module of the version. The date format must be `YYYY-MM-DD`
|
||||
- **jar** — the name of the Jar file the module's codes are contained. If there is none, leave it as a blank
|
||||
- **jarhash** — the Sha256sum of the Jar file. If there is no Jar file, leave it as a blank
|
||||
- **dependency** — list the other modules that this module requires
|
||||
## Overview
|
||||
|
||||
Modules provide:
|
||||
- **Content registration** — Blocks, items, fluids, weather, crafting
|
||||
- **Code execution** — Custom actors, fixtures, game logic
|
||||
- **Asset bundling** — Textures, sounds, translations
|
||||
- **Dependency management** — Load order and version requirements
|
||||
- **Configuration** — User-customisable settings
|
||||
|
||||
## Module Structure
|
||||
|
||||
### Directory Layout
|
||||
|
||||
```
|
||||
mods/
|
||||
└── mymod/
|
||||
├── metadata.properties # Required: Module information
|
||||
├── default.json # Optional: Default configuration
|
||||
├── mymod.jar # Optional: Compiled code
|
||||
├── icon.png # Optional: Icon for the module
|
||||
├── blocks/
|
||||
│ └── blocks.csv # Block definitions
|
||||
├── items/
|
||||
│ ├── itemid.csv # Item class registrations
|
||||
│ └── items.tga # Item spritesheet
|
||||
├── materials/
|
||||
│ └── materials.csv # Material properties
|
||||
├── fluids/
|
||||
│ └── fluids.csv # Fluid definitions
|
||||
├── ores/
|
||||
│ └── ores.csv # Ore generation params
|
||||
├── crafting/
|
||||
│ ├── tools.json # Crafting recipes for tools
|
||||
│ └── smelting.json # Smelting recipes
|
||||
├── weathers/
|
||||
│ ├── clear_day.json # Weather definitions
|
||||
│ └── sky_clear_day.tga # Weather assets
|
||||
├── locales/
|
||||
│ ├── en/ # English translations
|
||||
│ └── koKR/ # Korean translations
|
||||
├── audio/
|
||||
│ ├── music/ # Music tracks
|
||||
│ └── sfx/ # Sound effects
|
||||
└── sprites/
|
||||
├── actors/ # Actor sprites
|
||||
└── fixtures/ # Fixture sprites
|
||||
```
|
||||
|
||||
### Required Files
|
||||
|
||||
1. **`metadata.properties`** — Module metadata
|
||||
2. **At least one Codex file** — Content to load (blocks, items, etc.)
|
||||
|
||||
## metadata.properties
|
||||
|
||||
Defines module information and loading behaviour.
|
||||
|
||||
### Format
|
||||
|
||||
```properties
|
||||
# Module identity
|
||||
propername=My Awesome Mod
|
||||
author=YourName
|
||||
version=1.0.0
|
||||
description=A description of what this mod does
|
||||
description_ko=Non-latin character must be encoded with unicode literals like: \uACDF
|
||||
|
||||
# Module loading
|
||||
order=10
|
||||
entrypoint=net.torvald.mymod.EntryPoint
|
||||
jar=mymod.jar
|
||||
jarhash=<sha256sum of mymod.jar>
|
||||
dependency=basegame
|
||||
|
||||
# Release info
|
||||
releasedate=2025-01-15
|
||||
package=net.torvald.mymod
|
||||
```
|
||||
|
||||
### Fields
|
||||
|
||||
**Identity:**
|
||||
- `propername` — Human-readable module name (required)
|
||||
- `author` — Module author (required)
|
||||
- `version` — Semantic version string (required)
|
||||
- `description` — English description (required)
|
||||
- `description_<lang>` — Translated descriptions (optional)
|
||||
- `package` — Java package name (required if using code)
|
||||
|
||||
**Loading:**
|
||||
- `entrypoint` — Fully-qualified class name of ModuleEntryPoint (required if using code)
|
||||
- `jar` — JAR filename containing code (required if using code)
|
||||
- `jarhash` — SHA256 hash of the JAR file
|
||||
- `dependency` — Comma-separated list of required modules
|
||||
|
||||
**Release:**
|
||||
- `releasedate` — ISO date (YYYY-MM-DD)
|
||||
|
||||
### Dependency Format
|
||||
|
||||
@@ -38,133 +123,546 @@ For example, if your module requires `basegame` 0.3 or higher, the dependency st
|
||||
- The change on the major version number denotes incompatible API changes, and therefore you're expected to investigate the changes, test if your module still works, and then manually update your module to ensure the end user that your module is still operational with the new version of the dependency.
|
||||
|
||||
### icon.png
|
||||
|
||||
A module can have an icon about themselves. The image must be sized 48x48 and in 32-bit colour PNG format.
|
||||
|
||||
### Jar File
|
||||
A module may have one Jar file. Two or more Jars are not allowed: if your module requires external libraries, package them as a Fatjar.
|
||||
## default.json
|
||||
|
||||
## Contents
|
||||
Except for the [[retextures|Modules:Retextures]], there are *technically* no strict directory structure required, certain ingame elements loaded by the engine expects certain paths.
|
||||
Provides default configuration values.
|
||||
|
||||
A [sample project](https://github.com/curioustorvald/terrarum-sample-module-project) is available!
|
||||
### Format
|
||||
|
||||
### Entry Point
|
||||
If your module has a Jar file, it must have a single class that extends `net.torvald.terrarum.ModuleEntryPoint`: such class is called an Entry Point. The Entry Point initialises and installs your module to the game. What the Entry Point does to load your module depends on you, but the engine provides the **Modmgr** for installing ingame elements to the game.
|
||||
```json
|
||||
{
|
||||
"enableFeatureX": true,
|
||||
"debugMode": false,
|
||||
"difficultyScale": 1.0,
|
||||
"customSettings": {
|
||||
"spawnRate": 0.5,
|
||||
"maxEnemies": 100
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### The Modmgr
|
||||
**Access in code:**
|
||||
|
||||
The Modmgr provides the functions to load the following ingame elements:
|
||||
- ModMgr.**GameBlockLoader** — loads the blocks and wires
|
||||
- ModMgr.**GameItemLoader** — loads the items
|
||||
- ModMgr.**GameMaterialLoader** — loads the materials that come with the blocks and items
|
||||
- ModMgr.**GameLanguageLoader** — loads the text translations
|
||||
- ModMgr.**GameRetextureLoader** — loads the retextures
|
||||
- ModMgr.**GameCraftingRecipeLoader** — loads the crafting recipes
|
||||
```kotlin
|
||||
val enabled = App.getConfigBoolean("mymod.enableFeatureX")
|
||||
val difficulty = App.getConfigDouble("mymod.difficultyScale")
|
||||
```
|
||||
|
||||
The Modmgr will load the blocks/items, assign them the Item IDs, and register them to their respective *Codex*.
|
||||
Configuration is automatically namespaced with module name.
|
||||
|
||||
#### GameBlockLoader
|
||||
## Module Entry Point
|
||||
|
||||
The GameBlockLoader looks for the `<module root>/blocks/blocks.csv` for the list of blocks and the `<module root>/wires/wires.csv` for the list of wires.
|
||||
The `ModuleEntryPoint` is invoked when the module loads.
|
||||
|
||||
Every block within a module gets a unique number as an ID. A texture of the block must be named as the ID, with the extension of `.tga`.
|
||||
### Structure
|
||||
|
||||
The Item ID for the blocks will be `<module name>:<id defined in the csv>` e.g. `basegame:32`
|
||||
```kotlin
|
||||
package net.torvald.mymod
|
||||
|
||||
##### blocks.csv
|
||||
import net.torvald.terrarum.*
|
||||
|
||||
Every block's property is defined on the csv file with following columns:
|
||||
class EntryPoint : ModuleEntryPoint() {
|
||||
|
||||
- id: ID of this block
|
||||
- drop: Which item the DroppedItem actually adds to your inventory
|
||||
- spawn: Which item the DroppedItem should impersonate when spawned
|
||||
- name: String identifier of the block
|
||||
- shdr: Shade Red (light absorption). Valid range 0.0–1.0+
|
||||
- shdg: Shade Green (light absorption). Valid range 0.0–1.0+
|
||||
- shdb: Shade Blue (light absorption). Valid range 0.0–1.0+
|
||||
- shduv: Shade UV (light absorbtion). Valid range 0.0–1.0+
|
||||
- lumr: Luminosity Red (light intensity). Valid range 0.0–1.0+
|
||||
- lumg: Luminosity Green (light intensity). Valid range 0.0–1.0+
|
||||
- lumb: Luminosity Blue (light intensity). Valid range 0.0–1.0+
|
||||
- lumuv: Luminosity UV (light intensity). Valid range 0.0–1.0+
|
||||
- str: Strength of the block
|
||||
- dsty: Density of the block. Water have 1000 in the in-game scale
|
||||
- mate: Material of the block (defined in the MaterialCodex; see GameMaterialLoader)
|
||||
- solid: Whether the file has full collision (0/1)
|
||||
- plat: Whether the block should behave like a platform (0/1)
|
||||
- wall: Whether the block can be used as a wall (0/1)
|
||||
- grav: Whether the block should fall through the empty space. N/A to not make it fall; 0 to fall immediately (e.g. Sand), non-zero to indicate that number of floating blocks can be supported (e.g. Scaffolding)
|
||||
- dlfn: Dynamic Light Function. 0=Static. Please see <strong>notes</strong>
|
||||
- fv: Vertical friction when player slide on the cliff. 0 means not slide-able
|
||||
- fr: Horizontal friction. <16:slippery; 16:regular; >16:sticky
|
||||
- colour: [Fluids] Colour of the block in hexadecimal RGBA.
|
||||
- vscs: [Fluids] Viscocity of the block. 16 for water.
|
||||
- refl: [NOT Fluids] Reflectance of the block, used by the light calculation. Valid range 0.0–1.0
|
||||
- tags: Tags used by the crafting system and the game's internals
|
||||
private val moduleName = "mymod"
|
||||
|
||||
Notes on `dlfn`:
|
||||
override fun getTitleScreen(batch: FlippingSpriteBatch): IngameInstance? {
|
||||
// Return custom title screen (optional)
|
||||
// Only the first module's title screen is used
|
||||
return MyTitleScreen(batch)
|
||||
}
|
||||
|
||||
- dlfn stands for the Dynamic Luminosity Function.
|
||||
- 0—static; 1—torch flicker; 2—current global light (sun, star, moon); 3—daylight at noon; 4—slow breath; 5—pulsating
|
||||
override fun invoke() {
|
||||
// Called when module loads
|
||||
// Register content here
|
||||
loadContent()
|
||||
registerActors()
|
||||
setupCustomSystems()
|
||||
}
|
||||
|
||||
##### Block Texture Format
|
||||
override fun dispose() {
|
||||
// Called when game shuts down
|
||||
// Clean up resources
|
||||
}
|
||||
|
||||
Block spritesheet must be one of following sizes (assuming global TILE_SIZE of 16): 16x16, 64x16, 128x16, 112x112, 224x224. Different sheet size denotes different format of the sheet.
|
||||
private fun loadContent() {
|
||||
// Load assets
|
||||
CommonResourcePool.addToLoadingList("$moduleName.items") {
|
||||
ItemSheet(ModMgr.getGdxFile(moduleName, "items/items.tga"))
|
||||
}
|
||||
CommonResourcePool.loadAll()
|
||||
|
||||
- 16x16: Just a single tile. No autotiling will be performed, and the other tiles won't connect to it.
|
||||
- 64x16: A "wall sticker". Used by torches. Indices: free-floating, planted-on-left, planted-on-right, planted-on-bottom
|
||||
- 128x16: A "platforms". Used by platforms. Indices: middle, right-end, left-end, planted-on-left-middle, planted-on-left-end, planted-on-right-middle, planted-on-right-end, single-piece
|
||||
- 112x112: The full autotiling.
|
||||
// Load Codices in dependency order
|
||||
ModMgr.GameMaterialLoader.invoke(moduleName)
|
||||
ModMgr.GameFluidLoader.invoke(moduleName)
|
||||
ModMgr.GameItemLoader.invoke(moduleName)
|
||||
ModMgr.GameBlockLoader.invoke(moduleName)
|
||||
ModMgr.GameOreLoader.invoke(moduleName)
|
||||
ModMgr.GameCraftingRecipeLoader.invoke(moduleName)
|
||||
ModMgr.GameLanguageLoader.invoke(moduleName)
|
||||
ModMgr.GameAudioLoader.invoke(moduleName)
|
||||
ModMgr.GameWeatherLoader.invoke(moduleName)
|
||||
ModMgr.GameCanistersLoader.invoke(moduleName)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||

|
||||
## Codex Loading
|
||||
|
||||
- The "empty" tile at the (6,5) work as a "barcode" to assign properties. Each row on the square encodes a number in binary: pixel plotted = 1
|
||||
- Topmost row: the connection type
|
||||
- …0000—connect mutually (every tile tagged as 0 will connect to each other); …0001—connect to self
|
||||
- Second row: the mask type
|
||||
- …0010—use the autotiling (this row is reserved for future expansion; for now just mark it accordingly)
|
||||
- 224x224: Same as 112x112, but has seasonal variations. On render, colours from the current and the next seasons will be blended according to the ingame calendar time.
|
||||
- Top left—summer; Top right—autumn; Bottom right—winter; Bottom left—spring
|
||||
Codices are loaded via `ModMgr.Game*Loader` objects.
|
||||
|
||||
#### GameItemLoader
|
||||
### Loading Order
|
||||
|
||||
The GameItemLoader looks for the `<module root>/items/itemid.csv` for the list of items.
|
||||
**Critical:** Load in dependency order!
|
||||
|
||||
The Item ID for the items will be `item@<module name>:<id defined in the csv>` e.g. `item@basegame:1`
|
||||
```kotlin
|
||||
// GROUP 0: No dependencies
|
||||
ModMgr.GameMaterialLoader.invoke(moduleName)
|
||||
ModMgr.GameFluidLoader.invoke(moduleName)
|
||||
|
||||
##### itemid.csv
|
||||
// GROUP 1: Depends on materials
|
||||
ModMgr.GameItemLoader.invoke(moduleName)
|
||||
|
||||
Every item's id-classname pair is defined on the csv file with following columns:
|
||||
// GROUP 2: Depends on items and materials
|
||||
ModMgr.GameBlockLoader.invoke(moduleName)
|
||||
ModMgr.GameOreLoader.invoke(moduleName)
|
||||
|
||||
- id: ID of this item
|
||||
- classname: the full classname of the item which extends `net.torvald.terrarum.gameitems.GameItem`
|
||||
// GROUP 3: Depends on items and blocks
|
||||
ModMgr.GameCraftingRecipeLoader.invoke(moduleName)
|
||||
ModMgr.GameLanguageLoader.invoke(moduleName)
|
||||
ModMgr.GameAudioLoader.invoke(moduleName)
|
||||
ModMgr.GameWeatherLoader.invoke(moduleName)
|
||||
ModMgr.GameCanistersLoader.invoke(moduleName)
|
||||
```
|
||||
|
||||
#### GameMaterialLoader
|
||||
### Loader Details
|
||||
|
||||
The GameMaterialLoader looks for the `<module root>/materials/materials.csv` for the list of materials.
|
||||
Each loader expects specific files:
|
||||
|
||||
TODO
|
||||
| Loader | File Path | Format |
|
||||
|--------|-----------|--------|
|
||||
| `GameMaterialLoader` | `materials/materials.csv` | CSV |
|
||||
| `GameFluidLoader` | `fluids/fluids.csv` | CSV |
|
||||
| `GameItemLoader` | `items/itemid.csv` | CSV |
|
||||
| `GameBlockLoader` | `blocks/blocks.csv` | CSV |
|
||||
| `GameOreLoader` | `ores/ores.csv` | CSV |
|
||||
| `GameCraftingRecipeLoader` | `crafting/*.json` | JSON |
|
||||
| `GameLanguageLoader` | `locales/*/*.txt` | Text |
|
||||
| `GameAudioLoader` | `audio/*` | Audio files |
|
||||
| `GameWeatherLoader` | `weathers/*.json` | JSON |
|
||||
| `GameCanistersLoader` | `canisters/canisters.csv` | CSV |
|
||||
|
||||
#### GameLanguageLoader
|
||||
## Custom Code
|
||||
|
||||
The GameMaterialLoader looks for the `<module root>/locales/` for the translations.
|
||||
Modules can include custom Kotlin/Java code.
|
||||
|
||||
TODP
|
||||
### JAR Structure
|
||||
|
||||
#### GameRetextureLoader
|
||||
```
|
||||
mymod.jar
|
||||
└── net/torvald/mymod/
|
||||
├── EntryPoint.class
|
||||
├── actors/
|
||||
│ └── MyCustomActor.class
|
||||
├── items/
|
||||
│ └── MyCustomItem.class
|
||||
└── fixtures/
|
||||
└── MyCustomFixture.class
|
||||
```
|
||||
|
||||
The GameRetextureLoader looks for the `<module root>/retextures/` for the retextures.
|
||||
### Registering Custom Actors
|
||||
|
||||
For the retexture file formats, please refer to the [[Modules:Retextures]].
|
||||
```kotlin
|
||||
class EntryPoint : ModuleEntryPoint() {
|
||||
override fun invoke() {
|
||||
// Register custom actor
|
||||
ActorRegistry.register("mymod:custom_npc") {
|
||||
MyCustomNPC()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#### GameCraftingRecipeLoader
|
||||
class MyCustomNPC : ActorWithBody(RenderOrder.MIDTOP, PhysProperties.HUMANOID_DEFAULT(), "mymod:custom_npc") {
|
||||
init {
|
||||
val sprite = ADProperties(ModMgr.getGdxFile("mymod", "sprites/custom_npc.properties"))
|
||||
this.sprite = AssembledSpriteAnimation(sprite, this, false, false)
|
||||
}
|
||||
|
||||
The GameCraftingRecipeLoader looks for the `<module root>/crafting/*.json` for the crafting recipes.
|
||||
override fun updateImpl(delta: Float) {
|
||||
super.updateImpl(delta)
|
||||
// Custom AI logic
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
TODQ
|
||||
### Registering Custom Items
|
||||
|
||||
### The Title Screen
|
||||
```kotlin
|
||||
class MyCustomSword(originalID: ItemID) : GameItem(originalID) {
|
||||
override var baseMass = 1.5
|
||||
|
||||
For your set of modules to be recognised as a fully-fledged game, the first module in the Load Order must have a Title Screen, which extends `net.torvald.terrarum.IngameInstance`. When a game is being loaded, the Title Screen will be presented first to the end users. Other than that, a title screen is simply a standard `com.badlogic.gdx.Screen`.
|
||||
init {
|
||||
itemImage = ItemCodex.getItemImage(this)
|
||||
tags.add("WEAPON")
|
||||
tags.add("LEGENDARY")
|
||||
originalName = "mymod:legendary_sword"
|
||||
}
|
||||
|
||||
Only the first module will be inspected for loading the Title Screen; other instances of it on the subsequent modules, if any, will be ignored.
|
||||
override fun effectWhileEquipped(actor: ActorWithBody, delta: Float) {
|
||||
if (actor.actorValue.getAsBoolean("mymod:legendary_sword.strengthGiven") != true) {
|
||||
// Give strength buff while equipped
|
||||
actor.actorValue[AVKey.STRENGTHBUFF] += 50.0
|
||||
// Only give the buff once
|
||||
actor.actorValue["mymod:legendary_sword.strengthGiven"] = true
|
||||
}
|
||||
}
|
||||
|
||||
override fun effectOnUnequip(actor: ActorWithBody) {
|
||||
// Remove strength buff while unequipped
|
||||
actor.actorValue[AVKey.STRENGTHBUFF] += 50.0
|
||||
actor.actorValue.remove("mymod:legendary_sword.strengthGiven")
|
||||
}
|
||||
}
|
||||
|
||||
// Register in EntryPoint
|
||||
ItemCodex.itemCodex["mymod:legendary_sword"] = MyCustomSword()
|
||||
```
|
||||
|
||||
## Asset Loading
|
||||
|
||||
### Textures
|
||||
|
||||
```kotlin
|
||||
// Load spritesheet
|
||||
CommonResourcePool.addToLoadingList("mymod.custom_tiles") {
|
||||
TextureRegionPack(ModMgr.getGdxFile("mymod", "sprites/custom_tiles.tga"), 16, 16)
|
||||
}
|
||||
|
||||
// Load single image
|
||||
val texture = ModMgr.getGdxFile("mymod", "sprites/logo.png")
|
||||
```
|
||||
|
||||
### Audio
|
||||
|
||||
```kotlin
|
||||
// Load music
|
||||
val music = MusicContainer("My Song", ModMgr.getFile("mymod", "audio/music/song.ogg"))
|
||||
|
||||
// Load sound effect
|
||||
AudioCodex.addToLoadingList("mymod:explosion") {
|
||||
SoundContainer(ModMgr.getGdxFile("mymod", "audio/sfx/explosion.ogg"))
|
||||
}
|
||||
```
|
||||
|
||||
### Translations
|
||||
|
||||
**File:** `locales/en/items.txt` (or other translation files)
|
||||
|
||||
```
|
||||
ITEM_CUSTOM_SWORD=Legendary Sword
|
||||
ITEM_CUSTOM_SWORD_DESC=A blade of immense power.
|
||||
ACTOR_CUSTOM_NPC=Mysterious Stranger
|
||||
```
|
||||
|
||||
**Usage:**
|
||||
|
||||
```kotlin
|
||||
val name = Lang["ITEM_CUSTOM_SWORD"]
|
||||
val description = Lang["ITEM_CUSTOM_SWORD_DESC"]
|
||||
```
|
||||
|
||||
## Module Dependencies
|
||||
|
||||
Specify required modules in `metadata.properties`:
|
||||
|
||||
```properties
|
||||
dependencies=basegame,anothermod
|
||||
```
|
||||
|
||||
**Dependency resolution:**
|
||||
1. Load order file specifies which modules to load
|
||||
2. Dependencies are checked before loading
|
||||
3. Missing dependencies cause module to fail loading
|
||||
4. Circular dependencies are not allowed
|
||||
|
||||
## Load Order File
|
||||
|
||||
**File:** `<appdata>/load_order.csv`
|
||||
|
||||
```csv
|
||||
# Module load order
|
||||
basegame
|
||||
mymod
|
||||
anothermod
|
||||
```
|
||||
|
||||
**Rules:**
|
||||
- One module per line
|
||||
- Comments start with `#`
|
||||
- First module must provide title screen
|
||||
- Modules are loaded in order
|
||||
- Later modules override earlier content
|
||||
|
||||
## Module Override
|
||||
|
||||
Later modules can **override** content from earlier modules:
|
||||
|
||||
```kotlin
|
||||
// In mymod's EntryPoint
|
||||
override fun invoke() {
|
||||
// Load content normally
|
||||
ModMgr.GameBlockLoader.invoke("mymod")
|
||||
|
||||
// Override basegame stone with custom properties
|
||||
val customStone = BlockCodex["basegame:1"].copy()
|
||||
customStone.strength = 200 // Make stone harder
|
||||
BlockCodex.blockProps["basegame:1"] = customStone
|
||||
}
|
||||
```
|
||||
|
||||
This allows:
|
||||
- **Texture packs** — Replace sprites
|
||||
- **Balance mods** — Tweak block/item properties
|
||||
- **Total conversions** — Replace all content
|
||||
|
||||
## Configuration System
|
||||
|
||||
### Defining Config
|
||||
|
||||
**`metadata.properties`:**
|
||||
|
||||
```properties
|
||||
configplan=enableNewFeature,spawnRateMultiplier,debugLogging
|
||||
```
|
||||
|
||||
**`default.json`:**
|
||||
|
||||
```json
|
||||
{
|
||||
"enableNewFeature": true,
|
||||
"spawnRateMultiplier": 1.0,
|
||||
"debugLogging": false
|
||||
}
|
||||
```
|
||||
|
||||
### Accessing Config
|
||||
|
||||
```kotlin
|
||||
// In module code
|
||||
val enabled = App.getConfigBoolean("mymod.enableNewFeature")
|
||||
val multiplier = App.getConfigDouble("mymod.spawnRateMultiplier")
|
||||
|
||||
if (App.getConfigBoolean("mymod.debugLogging")) {
|
||||
println("Debug: Spawning actor at $x, $y")
|
||||
}
|
||||
```
|
||||
|
||||
**Config is automatically namespaced** — `enableNewFeature` becomes `mymod.enableNewFeature`.
|
||||
|
||||
## Debugging Modules
|
||||
|
||||
### Development Build
|
||||
|
||||
Enable development features in `config.jse`:
|
||||
|
||||
```javascript
|
||||
config.enableScriptMods = true;
|
||||
config.developerMode = true;
|
||||
```
|
||||
|
||||
### Logging
|
||||
|
||||
```kotlin
|
||||
App.printdbg(this, "Module loaded successfully")
|
||||
App.printmsg(this, "Important message")
|
||||
|
||||
if (App.IS_DEVELOPMENT_BUILD) {
|
||||
println("[MyMod] Detailed debug info")
|
||||
}
|
||||
```
|
||||
|
||||
### Error Handling
|
||||
|
||||
```kotlin
|
||||
override fun invoke() {
|
||||
try {
|
||||
ModMgr.GameBlockLoader.invoke(moduleName)
|
||||
}
|
||||
catch (e: Exception) {
|
||||
App.printmsg(this, "Failed to load blocks: ${e.message}")
|
||||
e.printStackTrace()
|
||||
ModMgr.logError(ModMgr.LoadErrorType.MY_FAULT, moduleName, e)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Module Distribution
|
||||
|
||||
### Packaging
|
||||
|
||||
1. **Compile code** → `mymod.jar`
|
||||
2. **Organise assets** → directory structure
|
||||
3. **Write `metadata.properties`**
|
||||
4. **Create `default.json`** (if using config)
|
||||
5. **Test with load order**
|
||||
6. **Archive** → `mymod.zip`
|
||||
|
||||
### Distribution Formats
|
||||
|
||||
- **Directory** — For development (`mods/mymod/`)
|
||||
- **ZIP archive** — For end users (extract to `mods/`)
|
||||
|
||||
### Version Compatibility
|
||||
|
||||
Use semantic versioning: `MAJOR.MINOR.PATCH`
|
||||
|
||||
```properties
|
||||
version=1.2.3
|
||||
```
|
||||
|
||||
- **MAJOR** — Breaking changes
|
||||
- **MINOR** — New features (backwards-compatible)
|
||||
- **PATCH** — Bug fixes
|
||||
|
||||
## Example: Complete Module
|
||||
|
||||
### metadata.properties
|
||||
|
||||
```properties
|
||||
name=Better Tools
|
||||
author=Alice
|
||||
version=1.0.0
|
||||
description=Adds diamond tools to the game
|
||||
order=20
|
||||
entrypoint=net.alice.bettertools.EntryPoint
|
||||
jarfile=bettertools.jar
|
||||
dependencies=basegame
|
||||
releasedate=2025-01-15
|
||||
packagename=net.alice.bettertools
|
||||
```
|
||||
|
||||
### EntryPoint.kt
|
||||
|
||||
```kotlin
|
||||
package net.alice.bettertools
|
||||
|
||||
import net.torvald.terrarum.*
|
||||
|
||||
class EntryPoint : ModuleEntryPoint() {
|
||||
|
||||
private val moduleName = "bettertools"
|
||||
|
||||
override fun invoke() {
|
||||
// Load assets
|
||||
CommonResourcePool.addToLoadingList("$moduleName.items") {
|
||||
ItemSheet(ModMgr.getGdxFile(moduleName, "items/items.tga"))
|
||||
}
|
||||
CommonResourcePool.loadAll()
|
||||
|
||||
// Load content
|
||||
ModMgr.GameMaterialLoader.invoke(moduleName)
|
||||
ModMgr.GameItemLoader.invoke(moduleName)
|
||||
ModMgr.GameCraftingRecipeLoader.invoke(moduleName)
|
||||
ModMgr.GameLanguageLoader.invoke(moduleName)
|
||||
|
||||
println("[BetterTools] Loaded successfully!")
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
// Cleanup if needed
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### materials/materials.csv
|
||||
|
||||
```csv
|
||||
idst;tens;impf;dsty;fmod;endurance;tcond;reach;rcs;sondrefl;comments
|
||||
DIAM;100;500;3500;0.2;0.95;2000;5;95;1.0;diamond - hardest material
|
||||
```
|
||||
|
||||
### items/itemid.csv
|
||||
|
||||
**Note:** Items are registered by fully-qualified class names.
|
||||
|
||||
```csv
|
||||
id;classname;tags
|
||||
pickaxe_diamond;net.alice.bettertools.PickaxeDiamond;TOOL,PICK
|
||||
axe_diamond;net.alice.bettertools.AxeDiamond;TOOL,AXE
|
||||
```
|
||||
|
||||
Each item needs a corresponding Kotlin class:
|
||||
|
||||
```kotlin
|
||||
package net.alice.bettertools
|
||||
|
||||
class PickaxeDiamond(originalID: ItemID) : GameItem(originalID) {
|
||||
override var baseMass = 1.8
|
||||
override var baseToolSize: Double? = 2.5
|
||||
override val materialId = "DIAM"
|
||||
|
||||
init {
|
||||
originalName = "ITEM_PICKAXE_DIAMOND"
|
||||
tags.add("TOOL")
|
||||
tags.add("PICK")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### crafting/tools.json
|
||||
|
||||
```json
|
||||
{
|
||||
"item@bettertools:pickaxe_diamond": { /* diamond pick */
|
||||
"workbench": "basiccrafting,metalworking",
|
||||
"ingredients": [[1, 5, "item@bettertools:diamond_gem", 2, "item@basegame:18"]] /* 5 gems, 2 sticks */
|
||||
},
|
||||
"item@bettertools:axe_diamond": { /* diamond axe */
|
||||
"workbench": "basiccrafting,metalworking",
|
||||
"ingredients": [[1, 5, "item@bettertools:diamond_gem", 2, "item@basegame:18"]]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### locales/en/items.txt
|
||||
|
||||
```
|
||||
ITEM_PICKAXE_DIAMOND=Diamond Pickaxe
|
||||
ITEM_PICKAXE_DIAMOND_DESC=The ultimate mining tool.
|
||||
ITEM_AXE_DIAMOND=Diamond Axe
|
||||
ITEM_AXE_DIAMOND_DESC=Chops trees with ease.
|
||||
```
|
||||
|
||||
## Best Practises
|
||||
|
||||
1. **Use unique module names** — Avoid conflicts with other mods
|
||||
2. **Namespace all IDs** — `modulename:itemname`
|
||||
3. **Document your config** — Add comments to `default.json`
|
||||
4. **Test load order** — Test as first module, middle module, last module
|
||||
5. **Version your content** — Track compatibility
|
||||
6. **Provide translations** — At least English
|
||||
7. **Handle errors gracefully** — Log, don't crash
|
||||
8. **Clean up resources** — Implement `dispose()`
|
||||
9. **Follow conventions** — Match basegame patterns
|
||||
10. **Test with other mods** — Ensure compatibility
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- **Wrong entry point class name** — Module won't load
|
||||
- **Missing JAR file** — Code not found
|
||||
- **Load order violations** — Dependencies loaded after dependents
|
||||
- **Hardcoded paths** — Use `ModMgr.getGdxFile()`
|
||||
- **Forgotten asset loading** — Textures not in CommonResourcePool
|
||||
- **ID conflicts** — Two modules use same IDs
|
||||
- **Circular dependencies** — A depends on B depends on A
|
||||
- **Not testing clean install** — Missing files in distribution
|
||||
|
||||
## See Also
|
||||
|
||||
- [[Modules:Codex Systems]] — Detailed Codex documentation
|
||||
- [[Blocks]] — Creating custom blocks
|
||||
- [[Items]] — Creating custom items
|
||||
- [[Actors]] — Creating custom actors
|
||||
- [[Fixtures]] — Creating custom fixtures
|
||||
|
||||
555
Physics-Engine.md
Normal file
555
Physics-Engine.md
Normal file
@@ -0,0 +1,555 @@
|
||||
# Physics Engine
|
||||
|
||||
Terrarum features a custom AABB-based physics system optimised for handling millions of tile bodies alongside dynamic actors. The system simulates Newtonian motion, collision detection, and gravitational forces.
|
||||
|
||||
## Overview
|
||||
|
||||
The physics engine provides:
|
||||
- **AABB collision detection** — Axis-Aligned Bounding Box system
|
||||
- **Tile-based optimisation** — Efficient handling of millions of static blocks
|
||||
- **Newtonian simulation** — Velocity, acceleration, momentum
|
||||
- **Gravity system** — Configurable gravitational acceleration
|
||||
- **Friction and damping** — Surface and air resistance
|
||||
- **Multi-threading support** — Scalable actor updates (currently disabled for performance)
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### Units and Scale
|
||||
|
||||
**METER Constant:**
|
||||
```kotlin
|
||||
companion object {
|
||||
const val METER = 25.0 // 1 metre = 25 pixels
|
||||
}
|
||||
```
|
||||
|
||||
This fundamental constant defines the relationship between pixels and physics simulation:
|
||||
- **1 metre** = 25 pixels
|
||||
- **1 tile** = 16 pixels = 0.64 metres
|
||||
|
||||
### Time Step
|
||||
|
||||
**PHYS_TIME_FRAME** (from `Terrarum.kt`):
|
||||
```kotlin
|
||||
const val PHYS_TIME_FRAME: Double = METER // = 25.0
|
||||
```
|
||||
|
||||
The physics time step is set to match METER, creating a 1:1 correspondence between game units and SI units in calculations.
|
||||
|
||||
**Frame Rate:**
|
||||
|
||||
Velocity and acceleration scale with delta time to maintain consistent physics at any frame rate. The physics simulation assumes a reference of 60 FPS, with values scaling proportionally:
|
||||
|
||||
```kotlin
|
||||
// Velocity units: pixels per 1/60 seconds
|
||||
// When resolving velocity, account for actual framerate:
|
||||
// v_resolved = v * (60 * delta) where delta is frame time in seconds
|
||||
```
|
||||
|
||||
## PhysProperties
|
||||
|
||||
Physical objects have properties defining their physics behaviour:
|
||||
|
||||
```kotlin
|
||||
class PhysProperties(
|
||||
var immobile: Boolean = false,
|
||||
var usePhysics: Boolean = true,
|
||||
var movementType: Int = PhysicalTypes.NORMAL,
|
||||
// ... more properties
|
||||
)
|
||||
```
|
||||
|
||||
### Common Presets
|
||||
|
||||
```kotlin
|
||||
PhysProperties.HUMANOID_DEFAULT() // Standard character physics
|
||||
PhysProperties.FLYING() // Flying entities
|
||||
PhysProperties.PROJECTILE() // Bullets, arrows
|
||||
PhysProperties.STATIONARY() // Fixed objects
|
||||
```
|
||||
|
||||
### Movement Types
|
||||
|
||||
```kotlin
|
||||
object PhysicalTypes {
|
||||
const val NORMAL = 0 // Standard gravity physics
|
||||
const val FLYING = 1 // No gravity, free movement
|
||||
const val RIDING = 2 // Mounted on vehicle
|
||||
const val SWIMMING = 3 // Fluid dynamics
|
||||
}
|
||||
```
|
||||
|
||||
## ActorWithBody Physics
|
||||
|
||||
Actors inheriting from `ActorWithBody` have full physics simulation.
|
||||
|
||||
### Velocity System
|
||||
|
||||
ActorWithBody maintains two velocity vectors:
|
||||
|
||||
#### External Velocity
|
||||
Physics-driven movement (gravity, collisions, forces):
|
||||
|
||||
```kotlin
|
||||
internal val externalV: Vector2 // pixels per frame at 60 FPS
|
||||
```
|
||||
|
||||
#### Controller Velocity
|
||||
Player/AI-controlled movement:
|
||||
|
||||
```kotlin
|
||||
var controllerV: Vector2? // Only for Controllable actors
|
||||
```
|
||||
|
||||
### Applying Forces
|
||||
|
||||
Add acceleration to velocity:
|
||||
|
||||
```kotlin
|
||||
// Apply gravity (delta is frame time, 60.0 is reference FPS)
|
||||
externalV.y += world.gravitation.y * delta * 60.0
|
||||
|
||||
// Apply horizontal force
|
||||
externalV.x += pushForce
|
||||
|
||||
// Apply damping
|
||||
externalV.scl(dampingFactor)
|
||||
```
|
||||
|
||||
### Velocity Limits
|
||||
|
||||
```kotlin
|
||||
private val VELO_HARD_LIMIT = 100.0 // Maximum velocity magnitude
|
||||
|
||||
// Clamp velocity
|
||||
if (externalV.len() > VELO_HARD_LIMIT) {
|
||||
externalV.nor().scl(VELO_HARD_LIMIT)
|
||||
}
|
||||
```
|
||||
|
||||
## Collision Detection
|
||||
|
||||
### AABB System
|
||||
|
||||
All collision uses Axis-Aligned Bounding Boxes:
|
||||
|
||||
```kotlin
|
||||
class Hitbox(
|
||||
var startX: Double, // Left edge
|
||||
var startY: Double, // Top edge
|
||||
var width: Double, // Width
|
||||
var height: Double // Height
|
||||
) {
|
||||
val endX: Double get() = startX + width
|
||||
val endY: Double get() = startY + height
|
||||
val centerX: Double get() = startX + width / 2
|
||||
val centerY: Double get() = startY + height / 2
|
||||
}
|
||||
```
|
||||
|
||||
### Hitbox Queries
|
||||
|
||||
```kotlin
|
||||
fun containsPoint(x: Double, y: Double): Boolean
|
||||
fun intersects(other: Hitbox): Boolean
|
||||
fun overlaps(x: Double, y: Double, width: Double, height: Double): Boolean
|
||||
```
|
||||
|
||||
### Tile Collision
|
||||
|
||||
#### Half-Integer Tilewise Hitbox
|
||||
|
||||
For physics collision with tiles:
|
||||
|
||||
```kotlin
|
||||
val hIntTilewiseHitbox: Hitbox
|
||||
```
|
||||
|
||||
This hitbox represents the actor's position in "half-integer" tile coordinates, aligned to tile centres for physics calculations.
|
||||
|
||||
#### Integer Tilewise Hitbox
|
||||
|
||||
For block occupation checks:
|
||||
|
||||
```kotlin
|
||||
val intTilewiseHitbox: Hitbox
|
||||
```
|
||||
|
||||
This represents which tiles the actor fully or partially occupies.
|
||||
|
||||
### Collision Iteration
|
||||
|
||||
Check collision with surrounding tiles:
|
||||
|
||||
```kotlin
|
||||
for (tx in hIntTilewiseHitbox.startX.toInt()..hIntTilewiseHitbox.endX.toInt()) {
|
||||
for (ty in hIntTilewiseHitbox.startY.toInt()..hIntTilewiseHitbox.endY.toInt()) {
|
||||
val block = world.getTileFromTerrain(tx, ty)
|
||||
if (BlockCodex[block]?.isSolid == true) {
|
||||
// Handle collision
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Mass and Momentum
|
||||
|
||||
### Mass Calculation
|
||||
|
||||
Actor mass scales with volume (cubic scaling):
|
||||
|
||||
```kotlin
|
||||
val mass: Double = baseMass * scale³
|
||||
```
|
||||
|
||||
Where:
|
||||
- **baseMass** — Mass at scale = 1.0 (in kilograms)
|
||||
- **scale** — Actor's size multiplier
|
||||
|
||||
### Momentum
|
||||
|
||||
```kotlin
|
||||
val momentum: Vector2 = velocity * mass
|
||||
```
|
||||
|
||||
Larger, heavier actors have more momentum and are harder to stop.
|
||||
|
||||
## Gravity System
|
||||
|
||||
### World Gravity
|
||||
|
||||
Each world defines gravitational acceleration:
|
||||
|
||||
```kotlin
|
||||
var gravitation: Vector2 = DEFAULT_GRAVITATION
|
||||
```
|
||||
|
||||
Default gravity approximates Earth's 9.8 m/s²:
|
||||
```kotlin
|
||||
// Typical value (not a defined constant in codebase):
|
||||
// val DEFAULT_GRAVITATION = Vector2(0.0, 9.8 * METER / (60.0 * 60.0))
|
||||
// This converts 9.8 m/s² to game units (pixels per frame²)
|
||||
```
|
||||
|
||||
### Applying Gravity
|
||||
|
||||
Gravity is applied each physics update:
|
||||
|
||||
```kotlin
|
||||
if (physProp.usePhysics && physProp.movementType == PhysicalTypes.NORMAL) {
|
||||
externalV.add(world.gravitation * delta * 60.0) // delta = frame time in seconds
|
||||
}
|
||||
```
|
||||
|
||||
### Gravity Exceptions
|
||||
|
||||
- **Flying actors** (movementType = FLYING) — No gravity
|
||||
- **Immobile actors** (immobile = true) — No movement
|
||||
- **Riding actors** (movementType = RIDING) — Follow mount's motion
|
||||
|
||||
## Friction
|
||||
|
||||
### Surface Friction
|
||||
|
||||
Blocks define horizontal friction:
|
||||
|
||||
```kotlin
|
||||
val frictionCoeff: Float // <16: slippery, 16: normal, >16: sticky
|
||||
```
|
||||
|
||||
Friction is applied when actors touch ground:
|
||||
|
||||
```kotlin
|
||||
if (onGround) {
|
||||
val friction = groundBlock.frictionCoeff / 16.0
|
||||
controllerV.x *= friction
|
||||
}
|
||||
```
|
||||
|
||||
### Vertical Friction
|
||||
|
||||
For wall sliding:
|
||||
|
||||
```kotlin
|
||||
val verticalFriction: Float // 0 = not slideable
|
||||
```
|
||||
|
||||
Applied when actor is against a wall:
|
||||
|
||||
```kotlin
|
||||
if (againstWall && block.verticalFriction > 0) {
|
||||
externalV.y *= block.verticalFriction
|
||||
}
|
||||
```
|
||||
|
||||
### Air Resistance
|
||||
|
||||
Damping applied to all movement:
|
||||
|
||||
```kotlin
|
||||
val airDamping = 0.99 // Slight damping
|
||||
|
||||
externalV.scl(airDamping)
|
||||
controllerV?.scl(airDamping)
|
||||
```
|
||||
|
||||
## Advanced Physics
|
||||
|
||||
### Platform Collision
|
||||
|
||||
Platforms are special blocks:
|
||||
|
||||
```kotlin
|
||||
val isPlatform: Boolean // One-way collision
|
||||
```
|
||||
|
||||
Actors can jump through from below but land on top:
|
||||
|
||||
```kotlin
|
||||
if (block.isPlatform && actor.externalV.y <= 0 && actor.bottomY <= blockTopY) {
|
||||
// Land on platform
|
||||
} else if (actor.externalV.y > 0) {
|
||||
// Pass through from below
|
||||
}
|
||||
```
|
||||
|
||||
### Falling Blocks
|
||||
|
||||
Blocks can fall due to gravity:
|
||||
|
||||
```kotlin
|
||||
val gravitation: Int?
|
||||
// null: Don't fall
|
||||
// 0: Fall immediately (sand)
|
||||
// N: Support N floating blocks (scaffolding)
|
||||
```
|
||||
|
||||
Falling blocks become temporary actors and settle when they hit solid ground.
|
||||
|
||||
### Fluid Dynamics
|
||||
|
||||
Actors in fluids experience:
|
||||
|
||||
- **Buoyancy** — Upward force based on density difference
|
||||
- **Viscosity** — Movement resistance
|
||||
- **Drag** — Velocity-dependent resistance
|
||||
|
||||
```kotlin
|
||||
if (inFluid) {
|
||||
val fluid = FluidCodex[fluidID]
|
||||
val buoyancy = (fluid.density - actor.density) * volume * gravitation
|
||||
externalV.y -= buoyancy * delta
|
||||
|
||||
// Apply fluid drag
|
||||
val drag = fluid.viscosity / 16.0
|
||||
externalV.scl(drag)
|
||||
}
|
||||
```
|
||||
|
||||
## Collision Response
|
||||
|
||||
### Separation
|
||||
|
||||
When actors collide, separate them:
|
||||
|
||||
```kotlin
|
||||
fun separate(actor: ActorWithBody, block: Hitbox) {
|
||||
val overlapX = min(actor.hitbox.endX - block.startX, block.endX - actor.hitbox.startX)
|
||||
val overlapY = min(actor.hitbox.endY - block.startY, block.endY - actor.hitbox.startY)
|
||||
|
||||
if (overlapX < overlapY) {
|
||||
// Separate horizontally
|
||||
if (actor.hitbox.centerX < block.centerX) {
|
||||
actor.translatePosition(-overlapX, 0.0)
|
||||
} else {
|
||||
actor.translatePosition(overlapX, 0.0)
|
||||
}
|
||||
actor.externalV.x = 0.0
|
||||
} else {
|
||||
// Separate vertically
|
||||
if (actor.hitbox.centerY < block.centerY) {
|
||||
actor.translatePosition(0.0, -overlapY)
|
||||
} else {
|
||||
actor.translatePosition(0.0, overlapY)
|
||||
}
|
||||
actor.externalV.y = 0.0
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Bouncing
|
||||
|
||||
Apply elasticity on collision:
|
||||
|
||||
```kotlin
|
||||
val restitution = 0.5 // 0 = no bounce, 1 = perfect bounce
|
||||
|
||||
if (collisionWithGround) {
|
||||
externalV.y = -externalV.y * restitution
|
||||
if (abs(externalV.y) < minBounceVelocity) {
|
||||
externalV.y = 0.0 // Stop bouncing
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Performance Optimisations
|
||||
|
||||
### Spatial Partitioning
|
||||
|
||||
The engine uses PR-Tree for efficient actor queries:
|
||||
|
||||
```kotlin
|
||||
val actorTree: PRTree<ActorWithBody>
|
||||
|
||||
// Query actors in area
|
||||
val nearbyActors = actorTree.find(
|
||||
minX, minY,
|
||||
maxX, maxY
|
||||
)
|
||||
```
|
||||
|
||||
### Sleeping Actors
|
||||
|
||||
Stationary actors can be flagged as sleeping:
|
||||
|
||||
```kotlin
|
||||
var isStationary: Boolean = true
|
||||
```
|
||||
|
||||
Sleeping actors skip expensive physics calculations.
|
||||
|
||||
### PHYS_EPSILON_DIST
|
||||
|
||||
Small epsilon for floating-point precision:
|
||||
|
||||
```kotlin
|
||||
companion object {
|
||||
const val PHYS_EPSILON_DIST = 1.0 / 256.0
|
||||
}
|
||||
```
|
||||
|
||||
Used to avoid edge cases in collision detection.
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Jumping
|
||||
|
||||
```kotlin
|
||||
fun jump(jumpStrength: Double) {
|
||||
if (onGround) {
|
||||
externalV.y = -jumpStrength
|
||||
onGround = false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Walking
|
||||
|
||||
```kotlin
|
||||
fun moveHorizontal(direction: Double, speed: Double) {
|
||||
controllerV?.x = direction * speed
|
||||
}
|
||||
```
|
||||
|
||||
### Applying Knockback
|
||||
|
||||
```kotlin
|
||||
fun applyKnockback(angle: Double, force: Double) {
|
||||
externalV.x += cos(angle) * force
|
||||
externalV.y += sin(angle) * force
|
||||
}
|
||||
```
|
||||
|
||||
### Ground Check
|
||||
|
||||
```kotlin
|
||||
fun isOnGround(): Boolean {
|
||||
val feetY = hitbox.endY
|
||||
val checkY = (feetY + PHYS_EPSILON_DIST) / TILE_SIZE
|
||||
|
||||
for (tx in intTilewiseHitbox.startX.toInt()..intTilewiseHitbox.endX.toInt()) {
|
||||
val block = world.getTileFromTerrain(tx, checkY.toInt())
|
||||
if (BlockCodex[block]?.isSolid == true) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
```
|
||||
|
||||
## Multi-Threading
|
||||
|
||||
The engine supports multi-threaded actor updates:
|
||||
|
||||
```kotlin
|
||||
class ThreadActorUpdate(
|
||||
val actors: List<ActorWithBody>,
|
||||
val delta: Float
|
||||
) : Runnable {
|
||||
override fun run() {
|
||||
actors.forEach { it.update(delta) }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
However, multi-threading is currently disabled as it's slower for typical actor counts (< tens of thousands).
|
||||
|
||||
## Debug Visualisation
|
||||
|
||||
### Render Hitboxes
|
||||
|
||||
```kotlin
|
||||
if (App.IS_DEVELOPMENT_BUILD) {
|
||||
shapeRenderer.begin(ShapeRenderer.ShapeType.Line)
|
||||
shapeRenderer.setColor(Color.RED)
|
||||
shapeRenderer.rect(
|
||||
hitbox.startX.toFloat(),
|
||||
hitbox.startY.toFloat(),
|
||||
hitbox.width.toFloat(),
|
||||
hitbox.height.toFloat()
|
||||
)
|
||||
shapeRenderer.end()
|
||||
}
|
||||
```
|
||||
|
||||
### Velocity Vectors
|
||||
|
||||
```kotlin
|
||||
shapeRenderer.line(
|
||||
position.x, position.y,
|
||||
position.x + externalV.x * 10, position.y + externalV.y * 10
|
||||
)
|
||||
```
|
||||
|
||||
## Best Practises
|
||||
|
||||
1. **Use controller velocity for input** — Keep player control separate from physics
|
||||
2. **Apply forces gradually** — Large instantaneous forces cause clipping
|
||||
3. **Check TILE_SIZE boundaries** — Ensure coordinates are in correct space
|
||||
4. **Clamp velocities** — Prevent excessive speeds causing tunnelling
|
||||
5. **Mark immobile actors** — Optimise performance for static objects
|
||||
6. **Use epsilon for comparisons** — Account for floating-point precision
|
||||
7. **Profile physics calculations** — Identify expensive collision checks
|
||||
|
||||
## Limitations
|
||||
|
||||
- **No rotation** — All hitboxes are axis-aligned
|
||||
- **Gravity is downward only** — No reverse or lateral gravity
|
||||
- **Single-threaded by default** — Multi-threading disabled for performance
|
||||
- **Discrete collision** — Fast-moving objects can tunnel through thin obstacles
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
Possible improvements:
|
||||
- Continuous collision detection
|
||||
- Rotated hitboxes
|
||||
- Soft-body physics
|
||||
- Better fluid simulation
|
||||
- Multi-directional gravity
|
||||
|
||||
## See Also
|
||||
|
||||
- [[Glossary]] — Physics terminology
|
||||
- [[Actors]] — Actor system using physics
|
||||
- [[World]] — World structure and tile properties
|
||||
- [[Modules:Blocks]] — Defining block physics properties
|
||||
633
Rendering-Pipeline.md
Normal file
633
Rendering-Pipeline.md
Normal file
@@ -0,0 +1,633 @@
|
||||
# Rendering Pipeline
|
||||
|
||||
The Terrarum rendering pipeline is built on LibGDX with OpenGL 3.2 Core Profile, featuring tile-based rendering, dynamic lighting, and sophisticated visual effects.
|
||||
|
||||
## Overview
|
||||
|
||||
The rendering system provides:
|
||||
- **Tile-based world rendering** with autotiling
|
||||
- **RGB+UV lightmap** system with transmittance
|
||||
- **Multi-layer sprite rendering** (up to 64 layers)
|
||||
- **Custom shaders** (GLSL 1.50)
|
||||
- **Post-processing effects**
|
||||
- **Weather and particle systems**
|
||||
|
||||
## OpenGL Configuration
|
||||
|
||||
### Version Requirements
|
||||
|
||||
Terrarum targets **OpenGL 3.2 Core Profile** for maximum compatibility, especially with macOS:
|
||||
|
||||
```kotlin
|
||||
val appConfig = Lwjgl3ApplicationConfiguration()
|
||||
appConfig.setOpenGLEmulation(
|
||||
Lwjgl3ApplicationConfiguration.GLEmulation.GL30,
|
||||
3, 2 // OpenGL 3.2
|
||||
)
|
||||
```
|
||||
|
||||
### GLSL Version
|
||||
|
||||
All shaders must use **GLSL 1.50**:
|
||||
|
||||
```glsl
|
||||
#version 150
|
||||
```
|
||||
|
||||
See also: [[OpenGL Considerations]]
|
||||
|
||||
### Shader Syntax Changes
|
||||
|
||||
GLSL 1.50 requires modern syntax:
|
||||
|
||||
**Vertex Shaders:**
|
||||
- `attribute` → `in`
|
||||
- `varying` → `out`
|
||||
|
||||
**Fragment Shaders:**
|
||||
- `varying` → `in`
|
||||
- `gl_FragColor` → custom `out vec4 fragColor`
|
||||
|
||||
## Render Layers
|
||||
|
||||
Actors and world elements are rendered in ordered layers:
|
||||
|
||||
### Layer Order (Back to Front)
|
||||
|
||||
1. **FAR_BEHIND** — Wires and conduits
|
||||
2. **BEHIND** — Tapestries, background particles
|
||||
3. **MIDDLE** — Actors (players, NPCs, creatures)
|
||||
4. **MIDTOP** — Projectiles, thrown items
|
||||
5. **FRONT** — Front walls and barriers
|
||||
6. **OVERLAY** — Screen overlays (unaffected by lighting)
|
||||
|
||||
Each actor's `renderOrder` property determines its layer.
|
||||
|
||||
## World Rendering
|
||||
|
||||
### BlocksDrawer
|
||||
|
||||
The `BlocksDrawer` class handles tile rendering:
|
||||
|
||||
```kotlin
|
||||
class BlocksDrawer(
|
||||
val world: GameWorld,
|
||||
val camera: WorldCamera
|
||||
)
|
||||
```
|
||||
|
||||
#### Tile Rendering Process
|
||||
|
||||
1. **Calculate visible area** from camera
|
||||
2. **Iterate visible tiles** within camera bounds
|
||||
3. **Fetch block data** from world layers
|
||||
4. **Determine sprite variant** using autotiling
|
||||
5. **Apply lighting** from lightmap
|
||||
6. **Batch draw** tiles to screen
|
||||
|
||||
### Tile Atlas System
|
||||
|
||||
Blocks use texture atlases for autotiling variants:
|
||||
|
||||
#### Atlas Formats
|
||||
|
||||
- **16×16** — Single tile (no autotiling)
|
||||
- **64×16** — Wall stickers (4 variants: free, left, right, bottom)
|
||||
- **128×16** — Platforms (8 variants)
|
||||
- **112×112** — Full autotiling (7×7 grid = 49 variants)
|
||||
- **224×224** — Seasonal autotiling (4 seasons × 49 variants)
|
||||
|
||||
#### Autotiling Algorithm
|
||||
|
||||
```kotlin
|
||||
// Calculate tile connections (8 neighbours)
|
||||
val connections = calculateConnections(x, y, blockType)
|
||||
|
||||
// Map to atlas coordinates
|
||||
val atlasX = connectionPattern and 0x7 // 0-6
|
||||
val atlasY = (connectionPattern shr 3) and 0x7 // 0-6
|
||||
|
||||
// Select sprite region
|
||||
val sprite = atlas.get(atlasX * 16, atlasY * 16, 16, 16)
|
||||
```
|
||||
|
||||
#### Connection Rules
|
||||
|
||||
Autotiling tiles have a "barcode" pixel in the atlas (position 6,5) encoding:
|
||||
|
||||
**Top row (connection type):**
|
||||
- `0000` — Connect mutually (all tagged tiles connect)
|
||||
- `0001` — Connect to self only
|
||||
|
||||
**Second row (mask type):**
|
||||
- `0010` — Use autotiling
|
||||
|
||||
### Seasonal Variation
|
||||
|
||||
224×224 atlases support seasonal colour blending:
|
||||
|
||||
```
|
||||
┌─────────┬─────────┐
|
||||
│ Summer │ Autumn │
|
||||
├─────────┼─────────┤
|
||||
│ Spring │ Winter │
|
||||
└─────────┴─────────┘
|
||||
```
|
||||
|
||||
Current and next season textures blend based on in-game time.
|
||||
|
||||
## Lighting System
|
||||
|
||||
### RGB+UV Lightmap
|
||||
|
||||
Terrarum simulates four light channels:
|
||||
|
||||
- **R** — Red light
|
||||
- **G** — Green light
|
||||
- **B** — Blue light
|
||||
- **UV** — Ultraviolet light
|
||||
|
||||
Each channel propagates independently with transmittance.
|
||||
|
||||
### LightmapRenderer
|
||||
|
||||
```kotlin
|
||||
class LightmapRenderer(
|
||||
val world: GameWorld,
|
||||
val camera: WorldCamera
|
||||
)
|
||||
```
|
||||
|
||||
#### Light Calculation
|
||||
|
||||
1. **Initialize with global light** (sun/moon/ambient)
|
||||
2. **Add static luminous blocks** (torches, lava, etc.)
|
||||
3. **Add dynamic lights** (actors, particles)
|
||||
4. **Propagate light** through tiles
|
||||
5. **Apply transmittance** based on block properties
|
||||
6. **Render to lightmap texture**
|
||||
|
||||
### Block Lighting Properties
|
||||
|
||||
Blocks define lighting behaviour:
|
||||
|
||||
```kotlin
|
||||
// Light absorption (0.0 = transparent, 1.0 = opaque)
|
||||
val shadeR: Float
|
||||
val shadeG: Float
|
||||
val shadeB: Float
|
||||
val shadeUV: Float
|
||||
|
||||
// Light emission (0.0 = none, 1.0+ = bright)
|
||||
val lumR: Float
|
||||
val lumG: Float
|
||||
val lumB: Float
|
||||
val lumUV: Float
|
||||
|
||||
// Reflectance for non-fluids (0.0-1.0)
|
||||
val reflectance: Float
|
||||
```
|
||||
|
||||
### Dynamic Light Functions
|
||||
|
||||
Luminous blocks can have time-varying brightness:
|
||||
|
||||
```kotlin
|
||||
enum class DynamicLightFunction {
|
||||
STATIC, // 0: Constant brightness
|
||||
TORCH_FLICKER, // 1: Flickering torch
|
||||
GLOBAL_LIGHT, // 2: Sun/moon light
|
||||
DAYLIGHT_NOON, // 3: Fixed noon brightness
|
||||
SLOW_BREATH, // 4: Gentle pulsing
|
||||
PULSATING // 5: Rhythmic pulsing
|
||||
}
|
||||
```
|
||||
|
||||
### Corner Occlusion
|
||||
|
||||
The lighting system includes corner darkening as a visual effect using a simple shader, enhancing depth perception.
|
||||
|
||||
## Sprite Rendering
|
||||
|
||||
### FlippingSpriteBatch
|
||||
|
||||
Terrarum uses a custom `FlippingSpriteBatch` for all sprite rendering:
|
||||
|
||||
```kotlin
|
||||
val batch = FlippingSpriteBatch()
|
||||
|
||||
batch.begin()
|
||||
// Draw sprites
|
||||
batch.end()
|
||||
```
|
||||
|
||||
This custom implementation adds engine-specific optimisations.
|
||||
|
||||
### Actor Sprites
|
||||
|
||||
Actors use the `SpriteAnimation` system:
|
||||
|
||||
```kotlin
|
||||
actor.sprite = SheetSpriteAnimation(
|
||||
spriteSheet,
|
||||
frameWidth, frameHeight,
|
||||
frameDuration
|
||||
)
|
||||
|
||||
// Update animation
|
||||
actor.sprite.update(delta)
|
||||
|
||||
// Render
|
||||
batch.draw(actor.sprite.currentFrame, x, y)
|
||||
```
|
||||
|
||||
### Multi-Layer Sprites
|
||||
|
||||
Actors support multiple sprite layers:
|
||||
|
||||
```kotlin
|
||||
@Transient var sprite: SpriteAnimation? // Base sprite
|
||||
@Transient var spriteGlow: SpriteAnimation? // Glow layer
|
||||
@Transient var spriteEmissive: SpriteAnimation? // Emissive layer
|
||||
```
|
||||
|
||||
Layers are composited in order:
|
||||
1. Base sprite (affected by lighting)
|
||||
2. Glow layer (additive blend)
|
||||
3. Emissive layer (full brightness)
|
||||
|
||||
### Blend Modes
|
||||
|
||||
```kotlin
|
||||
enum class BlendMode {
|
||||
NORMAL, // Standard alpha blending
|
||||
ADDITIVE, // Additive blending (glow effects)
|
||||
MULTIPLY, // Multiplicative blending (shadows)
|
||||
}
|
||||
|
||||
actor.drawMode = BlendMode.ADDITIVE
|
||||
```
|
||||
|
||||
## Camera System
|
||||
|
||||
### WorldCamera
|
||||
|
||||
The camera determines the visible area:
|
||||
|
||||
```kotlin
|
||||
class WorldCamera(
|
||||
val width: Int, // Screen width
|
||||
val height: Int // Screen height
|
||||
) : OrthographicCamera()
|
||||
```
|
||||
|
||||
#### Camera Properties
|
||||
|
||||
```kotlin
|
||||
val xTileStart: Int // First visible tile X
|
||||
val yTileStart: Int // First visible tile Y
|
||||
val xTileEnd: Int // Last visible tile X
|
||||
val yTileEnd: Int // Last visible tile Y
|
||||
```
|
||||
|
||||
#### Camera Movement
|
||||
|
||||
```kotlin
|
||||
camera.position.set(targetX, targetY, 0f)
|
||||
camera.update()
|
||||
```
|
||||
|
||||
### Screen Zoom
|
||||
|
||||
IngameInstance supports camera zoom:
|
||||
|
||||
```kotlin
|
||||
var screenZoom: Float = 1.0f
|
||||
val ZOOM_MINIMUM = 1.0f
|
||||
val ZOOM_MAXIMUM = 4.0f
|
||||
|
||||
// Apply zoom
|
||||
camera.zoom = 1f / screenZoom
|
||||
```
|
||||
|
||||
## Shader System
|
||||
|
||||
### ShaderMgr
|
||||
|
||||
The shader manager loads and manages GLSL programs:
|
||||
|
||||
```kotlin
|
||||
object ShaderMgr {
|
||||
fun get(shaderName: String): ShaderProgram
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Shaders
|
||||
|
||||
Shaders are stored in `src/shaders/`:
|
||||
|
||||
```
|
||||
src/shaders/
|
||||
├── default.vert // Default vertex shader
|
||||
├── default.frag // Default fragment shader
|
||||
├── lightmap.frag // Lightmap shader
|
||||
└── postprocess.frag // Post-processing
|
||||
```
|
||||
|
||||
### Shader Example
|
||||
|
||||
**Vertex Shader (default.vert):**
|
||||
|
||||
```glsl
|
||||
#version 150
|
||||
|
||||
in vec2 a_position;
|
||||
in vec2 a_texCoord0;
|
||||
in vec4 a_color;
|
||||
|
||||
out vec2 v_texCoords;
|
||||
out vec4 v_color;
|
||||
|
||||
uniform mat4 u_projTrans;
|
||||
|
||||
void main() {
|
||||
v_texCoords = a_texCoord0;
|
||||
v_color = a_color;
|
||||
gl_Position = u_projTrans * vec4(a_position, 0.0, 1.0);
|
||||
}
|
||||
```
|
||||
|
||||
**Fragment Shader (default.frag):**
|
||||
|
||||
```glsl
|
||||
#version 150
|
||||
|
||||
in vec2 v_texCoords;
|
||||
in vec4 v_color;
|
||||
|
||||
out vec4 fragColor;
|
||||
|
||||
uniform sampler2D u_texture;
|
||||
|
||||
void main() {
|
||||
fragColor = texture(u_texture, v_texCoords) * v_color;
|
||||
}
|
||||
```
|
||||
|
||||
### Applying Shaders
|
||||
|
||||
```kotlin
|
||||
val shader = ShaderMgr["myshader"]
|
||||
batch.shader = shader
|
||||
shader.setUniformf("u_customParam", 1.0f)
|
||||
```
|
||||
|
||||
## Frame Buffer System
|
||||
|
||||
### FrameBufferManager
|
||||
|
||||
Manages off-screen rendering targets:
|
||||
|
||||
```kotlin
|
||||
object FrameBufferManager {
|
||||
fun createBuffer(width: Int, height: Int): FrameBuffer
|
||||
}
|
||||
```
|
||||
|
||||
### Render-to-Texture
|
||||
|
||||
```kotlin
|
||||
val fbo = FrameBuffer(Pixmap.Format.RGBA8888, width, height, false)
|
||||
|
||||
fbo.begin()
|
||||
// Render to framebuffer
|
||||
renderWorld()
|
||||
fbo.end()
|
||||
|
||||
// Use as texture
|
||||
val texture = fbo.colorBufferTexture
|
||||
batch.draw(texture, x, y)
|
||||
```
|
||||
|
||||
### Post-Processing
|
||||
|
||||
```kotlin
|
||||
// Render scene to FBO
|
||||
sceneFBO.begin()
|
||||
renderScene()
|
||||
sceneFBO.end()
|
||||
|
||||
// Apply post-processing
|
||||
batch.shader = postProcessShader
|
||||
batch.draw(sceneFBO.colorBufferTexture, 0f, 0f)
|
||||
```
|
||||
|
||||
## Particle System
|
||||
|
||||
### Creating Particles
|
||||
|
||||
```kotlin
|
||||
fun createParticle(
|
||||
x: Double,
|
||||
y: Double,
|
||||
velocityX: Double,
|
||||
velocityY: Double,
|
||||
lifetime: Float,
|
||||
sprite: TextureRegion
|
||||
): ActorWithBody
|
||||
```
|
||||
|
||||
### Block Break Particles
|
||||
|
||||
```kotlin
|
||||
createRandomBlockParticle(
|
||||
world,
|
||||
blockX,
|
||||
blockY,
|
||||
blockID,
|
||||
particleCount = 8
|
||||
)
|
||||
```
|
||||
|
||||
Particles are lightweight actors rendered in appropriate layers.
|
||||
|
||||
## Weather Effects
|
||||
|
||||
### WeatherMixer
|
||||
|
||||
Manages weather rendering:
|
||||
|
||||
```kotlin
|
||||
val weatherBox = world.weatherbox
|
||||
|
||||
weatherBox.currentWeather // Active weather type
|
||||
weatherBox.windSpeed // Wind speed
|
||||
weatherBox.windDir // Wind direction
|
||||
```
|
||||
|
||||
Weather effects (rain, snow, fog) are rendered as particles with physics simulation.
|
||||
|
||||
## UI Rendering
|
||||
|
||||
UI elements render on top of the game world:
|
||||
|
||||
```kotlin
|
||||
// Render game world
|
||||
renderWorld()
|
||||
|
||||
// Render UI (unaffected by lighting)
|
||||
batch.shader = null // Reset shader
|
||||
uiCanvas.render(batch)
|
||||
```
|
||||
|
||||
See also: [[UI Framework]]
|
||||
|
||||
## Performance Optimisations
|
||||
|
||||
### Batch Rendering
|
||||
|
||||
Minimise draw calls by batching:
|
||||
|
||||
```kotlin
|
||||
batch.begin()
|
||||
// Draw many sprites
|
||||
for (tile in visibleTiles) {
|
||||
batch.draw(tile.texture, x, y)
|
||||
}
|
||||
batch.end() // Single draw call
|
||||
```
|
||||
|
||||
### Culling
|
||||
|
||||
Only render visible elements:
|
||||
|
||||
```kotlin
|
||||
if (actor.hitbox.intersects(camera.bounds)) {
|
||||
actor.render(batch)
|
||||
}
|
||||
```
|
||||
|
||||
### Texture Atlas
|
||||
|
||||
Use texture atlases to reduce texture switches:
|
||||
|
||||
```kotlin
|
||||
val atlas = TextureAtlas("sprites/packed.atlas")
|
||||
```
|
||||
|
||||
### Level of Detail
|
||||
|
||||
Reduce detail for distant objects:
|
||||
|
||||
```kotlin
|
||||
val distance = actor.position.dst(camera.position)
|
||||
if (distance > LOD_THRESHOLD) {
|
||||
renderSimplified(actor)
|
||||
} else {
|
||||
renderDetailed(actor)
|
||||
}
|
||||
```
|
||||
|
||||
## Debug Rendering
|
||||
|
||||
### Debug Overlays
|
||||
|
||||
```kotlin
|
||||
if (App.IS_DEVELOPMENT_BUILD) {
|
||||
renderHitboxes()
|
||||
renderTileGrid()
|
||||
renderLightValues()
|
||||
}
|
||||
```
|
||||
|
||||
### Shape Renderer
|
||||
|
||||
Use `ShapeRenderer` for debug visuals:
|
||||
|
||||
```kotlin
|
||||
val shapes = ShapeRenderer()
|
||||
|
||||
shapes.begin(ShapeRenderer.ShapeType.Line)
|
||||
shapes.rect(hitbox.x, hitbox.y, hitbox.width, hitbox.height)
|
||||
shapes.end()
|
||||
```
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Rendering a Tile
|
||||
|
||||
```kotlin
|
||||
val blockID = world.getTileFromTerrain(x, y)
|
||||
val block = BlockCodex[blockID]
|
||||
val sprite = CreateTileAtlas.getSprite(blockID, x, y)
|
||||
val light = lightmap.getLight(x, y)
|
||||
|
||||
batch.setColor(light.r, light.g, light.b, 1f)
|
||||
batch.draw(sprite, x * TILE_SIZE, y * TILE_SIZE)
|
||||
batch.setColor(1f, 1f, 1f, 1f)
|
||||
```
|
||||
|
||||
### Rendering an Actor
|
||||
|
||||
```kotlin
|
||||
actor.sprite?.update(delta)
|
||||
val frame = actor.sprite?.currentFrame
|
||||
|
||||
val light = getLightAtPosition(actor.hitbox.centerX, actor.hitbox.centerY)
|
||||
batch.setColor(light.r, light.g, light.b, 1f)
|
||||
batch.draw(
|
||||
frame,
|
||||
actor.hitbox.startX,
|
||||
actor.hitbox.startY
|
||||
)
|
||||
batch.setColor(1f, 1f, 1f, 1f)
|
||||
```
|
||||
|
||||
### Custom Shader Effect
|
||||
|
||||
```kotlin
|
||||
val shader = ShaderMgr["wave"]
|
||||
batch.shader = shader
|
||||
shader.setUniformf("u_time", gameTime)
|
||||
shader.setUniformf("u_amplitude", 0.1f)
|
||||
|
||||
batch.draw(texture, x, y)
|
||||
|
||||
batch.shader = null // Reset to default
|
||||
```
|
||||
|
||||
## Best Practises
|
||||
|
||||
1. **Batch draw calls** — Group similar sprites together
|
||||
2. **Use texture atlases** — Reduce texture binding overhead
|
||||
3. **Cull off-screen objects** — Don't render invisible actors
|
||||
4. **Cache sprite references** — Don't recreate textures every frame
|
||||
5. **Reset batch colour** — Always reset to white after tinting
|
||||
6. **Profile rendering** — Identify bottlenecks with timing
|
||||
7. **Minimise shader switches** — Group by shader when possible
|
||||
8. **Use appropriate precision** — `lowp`/`mediump` in shaders when sufficient
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Black Screen
|
||||
- Check OpenGL version support
|
||||
- Verify shader compilation (check logs)
|
||||
- Ensure camera is positioned correctly
|
||||
|
||||
### Flickering
|
||||
- Z-fighting: Ensure proper layer ordering
|
||||
- Missing batch.begin()/end() calls
|
||||
- Texture binding issues
|
||||
|
||||
### Performance Issues
|
||||
- Too many draw calls (use atlases)
|
||||
- Large lightmap calculations
|
||||
- Excessive particle count
|
||||
- Missing culling
|
||||
|
||||
## See Also
|
||||
|
||||
- [[OpenGL Considerations]] — GL 3.2 requirements
|
||||
- [[Glossary]] — Rendering terminology
|
||||
- [[World]] — World data structure
|
||||
- [[Actors]] — Actor rendering
|
||||
524
Save-and-Load.md
Normal file
524
Save-and-Load.md
Normal file
@@ -0,0 +1,524 @@
|
||||
# Save and Load
|
||||
|
||||
Terrarum uses a custom save game format called **TerranVirtualDisk** (TEVD), which stores game data as virtual disk images. Each save consists of two separate disk files: one for the world and one for the player.
|
||||
|
||||
## Overview
|
||||
|
||||
The save system provides:
|
||||
- **Chunked world storage** — Efficient storage for massive worlds
|
||||
- **Version tracking** — Snapshot numbers for save compatibility
|
||||
- **Compression** — Multiple compression algorithms (Gzip, Zstd, Snappy)
|
||||
- **Integrity checking** — CRC-32 verification and SHA-256 hashing
|
||||
- **Metadata** — Save type, game mode, creation time
|
||||
|
||||
## Save File Structure
|
||||
|
||||
### Two-Disk System
|
||||
|
||||
Each save game consists of two virtual disks:
|
||||
|
||||
1. **World Disk** — Contains the world, terrain, actors, and environment
|
||||
2. **Player Disk** — Contains player data, inventory, and stats
|
||||
|
||||
This separation allows:
|
||||
- Sharing worlds between players
|
||||
- Moving players between worlds
|
||||
|
||||
### File Locations
|
||||
|
||||
Saves are stored in the appdata directory:
|
||||
|
||||
```
|
||||
<appdata>/Savegames/
|
||||
├── WorldName_w.tvd # World disk
|
||||
└── PlayerName_p.tvd # Player disk
|
||||
```
|
||||
|
||||
## VirtualDisk Format
|
||||
|
||||
### TEVD Specification
|
||||
|
||||
VirtualDisk implements the **TerranVirtualDisk** format (version 254), a custom archival format designed for game saves.
|
||||
|
||||
#### Header Structure (300 bytes)
|
||||
|
||||
```
|
||||
Offset Size Field
|
||||
0 4 Magic: "TEVd"
|
||||
4 6 Disk size (48-bit, max 256 TiB)
|
||||
10 32 Disk name (short)
|
||||
42 4 CRC-32 checksum
|
||||
46 1 Version (0xFE)
|
||||
47 1 Marker byte (0xFE)
|
||||
48 1 Disk properties flags
|
||||
49 1 Save type flags
|
||||
50 1 Kind (player/world)
|
||||
51 1 Origin flags
|
||||
52 2 Snapshot number
|
||||
54 1 Game mode
|
||||
55 9 Reserved extra info bytes
|
||||
64 236 Rest of disk name (long)
|
||||
```
|
||||
|
||||
### Entry Structure
|
||||
|
||||
Each file in the disk is stored as an entry:
|
||||
|
||||
```
|
||||
EntryID (8 bytes) // Unique file identifier
|
||||
Size (6 bytes) // File size in bytes
|
||||
Timestamp (6 bytes) // Creation timestamp
|
||||
Compression (1 byte) // Compression method
|
||||
Data (variable) // File contents
|
||||
```
|
||||
|
||||
### Entry IDs
|
||||
|
||||
Special entry IDs are reserved for specific purposes:
|
||||
|
||||
- **0x00** — Metadata entry
|
||||
- **0x01..0xFFFFFFFE** — User data entries
|
||||
- **0xFEFEFEFE** — Originally reserved for footer (now freed)
|
||||
- **0xFFFFFFFF** — Invalidated entry marker
|
||||
|
||||
## Save Types
|
||||
|
||||
### World Saves
|
||||
|
||||
World disks (`VDSaveKind.WORLD_DATA`) contain:
|
||||
|
||||
- **WorldInfo** — Dimensions, time, spawn points, seeds
|
||||
- **Terrain layers** — Terrain, wall, ore, fluid data (chunked)
|
||||
- **Actors** — All actors in the world (except the player)
|
||||
- **Weather** — Current weather state
|
||||
- **Wirings** — Wire/conduit networks
|
||||
- **Extra fields** — Module-specific data
|
||||
|
||||
### Player Saves
|
||||
|
||||
Player disks (`VDSaveKind.PLAYER_DATA`) contain:
|
||||
|
||||
- **PlayerInfo** — Position, stats, actor values
|
||||
- **Inventory** — Items, equipment, quickslots
|
||||
- **Player-specific data** — Quests, achievements, etc.
|
||||
|
||||
### Full Save vs. Quicksave
|
||||
|
||||
Saves can be marked as quicksaves:
|
||||
|
||||
```kotlin
|
||||
// Save type flags
|
||||
0b0000_00ab
|
||||
b: 0 = full save, 1 = quicksave
|
||||
a: 1 = autosave
|
||||
```
|
||||
|
||||
#### How Quicksaves Work
|
||||
|
||||
**Quicksave (append-only):**
|
||||
1. Modified entries are written to the **end** of the disk file
|
||||
2. This creates **duplicate entries** with the same Entry ID at different offsets
|
||||
3. When reading the disk, the entry at the **later offset** takes precedence
|
||||
4. Earlier duplicates are **ignored**
|
||||
5. The disk becomes "dirty" with obsolete entries still taking up space
|
||||
|
||||
**Example:**
|
||||
```
|
||||
Initial disk:
|
||||
Offset 0x1000: Entry ID 42 (original world data)
|
||||
Offset 0x2000: Entry ID 43 (original player data)
|
||||
|
||||
After quicksave:
|
||||
Offset 0x1000: Entry ID 42 (original - IGNORED on read)
|
||||
Offset 0x2000: Entry ID 43 (original)
|
||||
Offset 0x3000: Entry ID 42 (updated - USED on read) ← Appended by quicksave
|
||||
```
|
||||
|
||||
**Full Save (rebuild):**
|
||||
1. Disk is **completely rebuilt** from scratch
|
||||
2. Each entry appears **only once**
|
||||
3. All obsolete/deleted entries are **removed**
|
||||
4. Disk is compacted to minimum size
|
||||
5. No duplicate entries exist
|
||||
|
||||
This makes quicksaves faster (no rebuild) but results in larger file sizes over time. Periodic full saves compact the disk by removing duplicate and deleted entries.
|
||||
|
||||
## Snapshot System
|
||||
|
||||
### Snapshot Numbers
|
||||
|
||||
Saves are versioned using snapshot numbers in the format: `YYwWWX`
|
||||
|
||||
```kotlin
|
||||
// Format: 0b A_yyyyyyy wwwwww_aa
|
||||
// Example: 23w40f
|
||||
// 23 = year 2023
|
||||
// w40 = ISO week 40
|
||||
// f = revision 6 (a=1, b=2, ..., f=6)
|
||||
```
|
||||
|
||||
This allows tracking save compatibility and detecting version mismatches.
|
||||
|
||||
### Version Compatibility
|
||||
|
||||
When loading a save, the engine checks:
|
||||
|
||||
1. **Snapshot number** — Warn if from newer version
|
||||
2. **GENVER** — Game version that created the save
|
||||
3. **Module versions** — Check module compatibility
|
||||
|
||||
## Serialisation
|
||||
|
||||
### JSON-Based Serialisation
|
||||
|
||||
Game state is serialised to JSON using LibGDX's JSON library with custom serialisers.
|
||||
|
||||
#### Custom Serialisers
|
||||
|
||||
The engine provides serialisers for:
|
||||
|
||||
- **BigInteger** — Arbitrary precision integers
|
||||
- **BlockLayerGenericI16** — Terrain layers with SHA-256 hashing
|
||||
- **WorldTime** — Temporal data
|
||||
- **HashArray** — Sparse arrays
|
||||
- **HashedWirings** — Wire networks
|
||||
- **ZipCodedStr** — Compressed strings
|
||||
|
||||
#### Example Serialisation
|
||||
|
||||
```kotlin
|
||||
val json = Common.jsoner
|
||||
val jsonString = json.toJson(gameWorld)
|
||||
val loadedWorld = json.fromJson(GameWorld::class.java, jsonString)
|
||||
```
|
||||
|
||||
### Transient Fields
|
||||
|
||||
Fields marked `@Transient` are not serialised:
|
||||
|
||||
```kotlin
|
||||
@Transient var sprite: SpriteAnimation? = null
|
||||
@Transient lateinit var actor: Pocketed
|
||||
```
|
||||
|
||||
**These must be reconstructed in the `reload()` method after deserialisation.**
|
||||
|
||||
### Reload Method
|
||||
|
||||
After loading, actors and objects call `reload()` to reconstruct transient state:
|
||||
|
||||
```kotlin
|
||||
override fun reload() {
|
||||
super.reload()
|
||||
// Reconstruct sprites, UI, caches, etc.
|
||||
sprite = loadSprite()
|
||||
actor = retrieveActorReference()
|
||||
}
|
||||
```
|
||||
|
||||
## DiskSkimmer
|
||||
|
||||
DiskSkimmer provides efficient read/write access to virtual disks without loading the entire disk into memory.
|
||||
|
||||
### Creating a Skimmer
|
||||
|
||||
```kotlin
|
||||
val skimmer = DiskSkimmer(File("save.tvd"))
|
||||
```
|
||||
|
||||
### Reading Entries
|
||||
|
||||
```kotlin
|
||||
// Get entry by ID
|
||||
val data: ByteArray64 = skimmer.requestFile(entryID)
|
||||
|
||||
// Read specific entry without building DOM
|
||||
val inputStream = data.getAsInputStream()
|
||||
```
|
||||
|
||||
### Writing Entries
|
||||
|
||||
```kotlin
|
||||
// Write new entry
|
||||
skimmer.appendEntry(entryID, data, compression = Common.COMP_ZSTD)
|
||||
```
|
||||
|
||||
### Rebuilding (Compaction)
|
||||
|
||||
Dirty disks accumulate duplicate and deleted entries from quicksaves. **Full saves** perform compaction:
|
||||
|
||||
```kotlin
|
||||
skimmer.rebuild() // Rebuild entry table, removing duplicates and deleted entries
|
||||
skimmer.sync() // Write clean disk to file
|
||||
```
|
||||
|
||||
The `rebuild()` process:
|
||||
1. Scans all entries in the disk
|
||||
2. For duplicate Entry IDs, keeps only the **latest** (highest offset)
|
||||
3. Removes entries marked as deleted (ID = 0xFFFFFFFF)
|
||||
4. Writes a clean, compacted disk with no duplicates
|
||||
|
||||
This is what distinguishes a **full save** from a **quicksave**.
|
||||
|
||||
## Compression
|
||||
|
||||
### Supported Algorithms
|
||||
|
||||
```kotlin
|
||||
COMP_NONE = 0 // No compression
|
||||
COMP_GZIP = 1 // Gzip compression
|
||||
COMP_ZSTD = 3 // Zstandard (recommended)
|
||||
COMP_SNAPPY = 4 // Snappy (fast, lower ratio)
|
||||
```
|
||||
|
||||
### Using Compression
|
||||
|
||||
```kotlin
|
||||
// Compress data before writing
|
||||
val compressedStream = ZstdOutputStream(outputStream)
|
||||
compressedStream.write(data)
|
||||
compressedStream.close()
|
||||
|
||||
// Decompress on read
|
||||
val decompressedStream = ZstdInputStream(inputStream)
|
||||
val data = decompressedStream.readBytes()
|
||||
```
|
||||
|
||||
**Zstandard (COMP_ZSTD)** provides the best balance of speed and compression ratio for save games.
|
||||
|
||||
## SavegameCollection
|
||||
|
||||
The SavegameCollection manages all save files:
|
||||
|
||||
```kotlin
|
||||
object SavegameCollection {
|
||||
// List all world saves
|
||||
fun getWorlds(): List<SaveMetadata>
|
||||
|
||||
// List all player saves
|
||||
fun getPlayers(): List<SaveMetadata>
|
||||
|
||||
// Load world
|
||||
fun loadWorld(worldFile: File): GameWorld
|
||||
|
||||
// Load player
|
||||
fun loadPlayer(playerFile: File): IngamePlayer
|
||||
}
|
||||
```
|
||||
|
||||
## Save and Load Workflow
|
||||
|
||||
### Saving a Game
|
||||
|
||||
**Quicksave (append-only, faster):**
|
||||
```kotlin
|
||||
// 1. Prepare save data
|
||||
val world = INGAME.world
|
||||
val player = INGAME.actorNowPlaying as IngamePlayer
|
||||
|
||||
// 2. Open existing disk (don't rebuild)
|
||||
val worldDisk = DiskSkimmer(File("${worldName}_w.tvd"))
|
||||
|
||||
// 3. Serialise changed data to JSON
|
||||
val worldJson = Common.jsoner.toJson(world)
|
||||
val compressedWorld = compress(worldJson, Common.COMP_ZSTD)
|
||||
|
||||
// 4. Append to disk (creates duplicate entries)
|
||||
worldDisk.appendEntry(0x00, compressedWorld) // Entry 0x00 now exists twice!
|
||||
|
||||
// 5. Sync without rebuilding
|
||||
worldDisk.sync() // Disk is now "dirty" with duplicates
|
||||
```
|
||||
|
||||
**Full Save (rebuild, compacted):**
|
||||
```kotlin
|
||||
// Same as quicksave, but with rebuild before sync:
|
||||
|
||||
// 4. Append entries
|
||||
worldDisk.appendEntry(0x00, compressedWorld)
|
||||
|
||||
// 5. Rebuild and sync (removes duplicates)
|
||||
worldDisk.rebuild() // Compact: remove old duplicates and deleted entries
|
||||
worldDisk.sync() // Write clean disk
|
||||
```
|
||||
|
||||
### Loading a Game
|
||||
|
||||
```kotlin
|
||||
// 1. Open disks
|
||||
val worldDisk = DiskSkimmer(File("${worldName}_w.tvd"))
|
||||
val playerDisk = DiskSkimmer(File("${playerName}_p.tvd"))
|
||||
|
||||
// 2. Read and decompress (automatically uses latest entry if duplicates exist)
|
||||
val worldData = worldDisk.requestFile(0x00) // Gets entry at highest offset
|
||||
val worldJson = decompress(worldData)
|
||||
|
||||
// 3. Deserialise from JSON
|
||||
val world = Common.jsoner.fromJson(GameWorld::class.java, worldJson)
|
||||
|
||||
// 4. Reload transient fields
|
||||
world.layerTerrain.reload()
|
||||
world.actors.forEach { actor -> actor.reload() }
|
||||
|
||||
// 5. Repeat for player
|
||||
// ...
|
||||
|
||||
// 6. Initialise game
|
||||
INGAME.world = world
|
||||
INGAME.actorNowPlaying = player
|
||||
```
|
||||
|
||||
## Integrity Checking
|
||||
|
||||
### CRC-32 Verification
|
||||
|
||||
Virtual disks store a CRC-32 checksum in the header:
|
||||
|
||||
```kotlin
|
||||
// Calculate CRC
|
||||
val crc = CRC32()
|
||||
entries.map { it.crc }.sorted().forEach { crc.update(it) }
|
||||
val diskCRC = crc.value
|
||||
|
||||
// Verify on load
|
||||
if (diskCRC != storedCRC) {
|
||||
throw IOException("Disk CRC mismatch: corrupted save file")
|
||||
}
|
||||
```
|
||||
|
||||
### SHA-256 Hashing
|
||||
|
||||
Block layers are hashed to detect corruption:
|
||||
|
||||
```kotlin
|
||||
// During save
|
||||
val hash = SHA256(blockLayer.bytes)
|
||||
writeHash(hash)
|
||||
|
||||
// During load
|
||||
val loadedHash = SHA256(blockLayer.bytes)
|
||||
if (loadedHash != storedHash) {
|
||||
throw BlockLayerHashMismatchError(storedHash, loadedHash, blockLayer)
|
||||
}
|
||||
```
|
||||
|
||||
## Chunked World Storage
|
||||
|
||||
Worlds are stored in chunks to reduce save/load time:
|
||||
|
||||
### Chunk System
|
||||
|
||||
- Only **modified chunks** are saved
|
||||
- **Unmodified chunks** are regenerated on load
|
||||
- **Chunk flags** track modification state
|
||||
|
||||
### Saving Chunks
|
||||
|
||||
```kotlin
|
||||
for (chunk in world.chunks) {
|
||||
if (chunk.isModified) {
|
||||
saveChunk(chunk)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Loading Chunks
|
||||
|
||||
```kotlin
|
||||
for (chunk in world.chunks) {
|
||||
if (chunkExistsInSave(chunk)) {
|
||||
loadChunk(chunk)
|
||||
} else {
|
||||
regenerateChunk(chunk) // Use world generator
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practises
|
||||
|
||||
1. **Mark UI and sprites as @Transient** — They cannot be serialised
|
||||
2. **Implement reload() for all custom classes** — Reconstruct transient fields
|
||||
3. **Use Zstd compression** — Best performance for game saves
|
||||
4. **Use quicksaves for autosaves** — Fast append-only saves during gameplay
|
||||
5. **Use full saves when exiting** — Rebuild and compact the disk when player quits
|
||||
6. **Rebuild disks periodically** — Compact after multiple quicksaves to prevent file bloat
|
||||
7. **Validate snapshot versions** — Warn users of version mismatches
|
||||
8. **Hash critical data** — Detect corruption in block layers
|
||||
9. **Chunk world saves** — Only save modified chunks
|
||||
10. **Separate world and player** — Allow sharing and portability
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- **Forgetting to call reload()** — Leads to null sprite/UI crashes
|
||||
- **Not marking fields @Transient** — Causes serialisation errors
|
||||
- **Saving entire worlds** — Use chunking for large worlds
|
||||
- **Only using quicksaves** — Disk files bloat with duplicate entries; use full saves periodically
|
||||
- **Rebuilding on every save** — Too slow for autosaves; use quicksaves during gameplay
|
||||
- **Ignoring CRC errors** — Corrupted saves can crash the game
|
||||
- **Not compressing data** — Saves become unnecessarily large
|
||||
- **Hardcoding entry IDs** — Use constants or enums
|
||||
|
||||
## Advanced Topics
|
||||
|
||||
### Dynamic Item Persistence
|
||||
|
||||
Dynamic items (tools, weapons) require special handling:
|
||||
|
||||
```kotlin
|
||||
// World stores dynamic item table
|
||||
world.dynamicItemInventory: ItemTable
|
||||
|
||||
// Map dynamic IDs to static IDs
|
||||
world.dynamicToStaticTable: ItemRemapTable
|
||||
|
||||
// When loading, remap IDs
|
||||
item.dynamicID = world.dynamicToStaticTable[oldDynamicID]
|
||||
```
|
||||
|
||||
### Module Compatibility
|
||||
|
||||
Saves store module versions. On load:
|
||||
|
||||
```kotlin
|
||||
// Check each module
|
||||
for (module in save.modules) {
|
||||
if (!ModMgr.isCompatible(module.name, module.version)) {
|
||||
warn("Module ${module.name} version mismatch")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Save Data
|
||||
|
||||
Modules can add custom save data using extra fields:
|
||||
|
||||
```kotlin
|
||||
// Add extra field
|
||||
world.extraFields["mymod:custom_data"] = MySerializable()
|
||||
|
||||
// Retrieve on load
|
||||
val customData = world.extraFields["mymod:custom_data"] as? MySerializable
|
||||
```
|
||||
|
||||
### Autosave
|
||||
|
||||
Implement autosave by periodically calling save in a background thread:
|
||||
|
||||
```kotlin
|
||||
// Every N seconds
|
||||
if (timeSinceLastSave > autosaveInterval) {
|
||||
Thread {
|
||||
saveGame(isAutosave = true, isQuicksave = true)
|
||||
}.start()
|
||||
}
|
||||
```
|
||||
|
||||
Mark autosaves in the save type flags (`a` bit).
|
||||
|
||||
## See Also
|
||||
|
||||
- [[Glossary]] — Save/load terminology
|
||||
- [[World]] — World structure and management
|
||||
- [[Actors]] — Actor serialisation and reload
|
||||
- [[Inventory]] — Inventory persistence
|
||||
- [TerranVirtualDisk](https://github.com/minjaesong/TerranVirtualDisk) — Virtual disk format library
|
||||
634
Tile-Atlas-System.md
Normal file
634
Tile-Atlas-System.md
Normal file
@@ -0,0 +1,634 @@
|
||||
# Tile Atlas System (Engine Internals)
|
||||
|
||||
**Audience:** Engine maintainers and advanced developers working on core rendering systems.
|
||||
|
||||
This document describes the internal implementation of Terrarum's tile atlas system, including six-season blending, subtiling, and dynamic atlas generation.
|
||||
|
||||
## Overview
|
||||
|
||||
The tile atlas system (`CreateTileAtlas`) dynamically builds texture atlases from block textures across all loaded modules, supporting:
|
||||
- **Six seasonal variations** with temporal blending
|
||||
- **Subtiling system** for 8×8 pixel subdivisions
|
||||
- **Multiple atlas formats** (16×16, 64×16, 128×16, 112×112, 168×136, 224×224)
|
||||
- **Glow and emissive layers** for self-illumination
|
||||
- **Dynamic retexturing** via module overrides
|
||||
- **Item image generation** from world tiles
|
||||
|
||||
## Six-Season System
|
||||
|
||||
### Season Atlases
|
||||
|
||||
The engine maintains **six separate texture atlases** for seasonal variation:
|
||||
|
||||
```kotlin
|
||||
lateinit var atlasPrevernal: Pixmap // Pre-spring (late winter transitioning)
|
||||
lateinit var atlasVernal: Pixmap // Spring
|
||||
lateinit var atlasAestival: Pixmap // Summer
|
||||
lateinit var atlasSerotinal: Pixmap // Late summer
|
||||
lateinit var atlasAutumnal: Pixmap // Autumn
|
||||
lateinit var atlasHibernal: Pixmap // Winter
|
||||
```
|
||||
|
||||
### Season Names
|
||||
|
||||
| Atlas | Season | Calendar Period |
|
||||
|-------|--------|-----------------|
|
||||
| Prevernal | Pre-spring | Late Month 12 to Early Month 1 |
|
||||
| Vernal | Spring | Months 1-2 |
|
||||
| Aestival | Summer | Months 3-5 |
|
||||
| Serotinal | Late Summer | Months 6-7 |
|
||||
| Autumnal | Autumn | Months 8-10 |
|
||||
| Hibernal | Winter | Months 11-12 |
|
||||
|
||||
### Seasonal Blending
|
||||
|
||||
The rendering shader can blend between two seasonal atlases using the `tilesBlend` uniform:
|
||||
|
||||
```glsl
|
||||
// In tiling.frag shader
|
||||
uniform sampler2D tilesAtlas; // Primary season atlas
|
||||
uniform sampler2D tilesBlendAtlas; // Secondary season atlas for blending
|
||||
uniform float tilesBlend = 0.0; // Blend factor [0..1]
|
||||
|
||||
vec4 tileCol = texture(tilesAtlas, finalUVCoordForTile);
|
||||
vec4 tileAltCol = texture(tilesBlendAtlas, finalUVCoordForTile);
|
||||
vec4 finalColor = mix(tileCol, tileAltCol, tilesBlend);
|
||||
```
|
||||
|
||||
Game code selects which two seasonal atlases to use and sets the blend factor. By default, `atlasVernal` (spring) is used.
|
||||
|
||||
### Seasonal Texture Formats
|
||||
|
||||
#### Standard Blocks (112×112)
|
||||
Single-season blocks populate all six atlases with identical textures.
|
||||
|
||||
#### Seasonal Blocks (224×224 or 336×224)
|
||||
|
||||
**Four-season format** (224×224) provides four seasons in a quad layout:
|
||||
|
||||
```
|
||||
┌───────────┬───────────┐
|
||||
│ Aestival │ Autumnal │ 112×112 each
|
||||
│ (Summer) │ (Autumn) │
|
||||
├───────────┼───────────┤
|
||||
│ Vernal │ Hibernal │
|
||||
│ (Spring) │ (Winter) │
|
||||
└───────────┴───────────┘
|
||||
```
|
||||
|
||||
For blocks using this format, the same texture is reused for Prevernal and Serotinal seasons
|
||||
|
||||
#### Six-Season Layout
|
||||
|
||||
For textures with full six-season support (336×224 for standard blocks, or 3× width for subtiled blocks):
|
||||
|
||||
```
|
||||
Horizontal layout (336 × 224):
|
||||
┌─────────┬─────────┬─────────┐
|
||||
│Prevernal│ Vernal │Aestival │ 112×112 each (top row)
|
||||
├─────────┼─────────┼─────────┤
|
||||
│Hibernal │Autumnal │Serotinal│ 112×112 each (bottom row)
|
||||
└─────────┴─────────┴─────────┘
|
||||
```
|
||||
|
||||
The atlas creation code extracts each 112×112 region and places it into the corresponding seasonal atlas. No interpolation occurs—all six seasons are explicitly provided in the source texture.
|
||||
|
||||
## Subtiling System
|
||||
|
||||
### Subtile Size
|
||||
|
||||
Subtiles are subdivisions of standard tiles:
|
||||
|
||||
```kotlin
|
||||
const val TILE_SIZE = 16 // Standard tile
|
||||
const val SUBTILE_SIZE = 8 // Half-tile subdivision
|
||||
```
|
||||
|
||||
### Subtile Grid
|
||||
|
||||
Each tile divides into a 2×2 subtile grid:
|
||||
|
||||
```
|
||||
┌────┬────┐
|
||||
│ 0 │ 1 │ 8×8 each
|
||||
├────┼────┤
|
||||
│ 3 │ 2 │
|
||||
└────┴────┘
|
||||
```
|
||||
|
||||
Subtile indices: 0 (top-left), 1 (top-right), 2 (bottom-right), 3 (bottom-left).
|
||||
|
||||
### Subtile Atlases
|
||||
|
||||
Two special atlas formats use subtiling:
|
||||
|
||||
#### Generic Subtile Atlas (104×136)
|
||||
|
||||
```kotlin
|
||||
val W_SUBTILE_GENERIC = 104 // Width in subtiles
|
||||
val H_SUBTILE = 136 // Height in subtiles
|
||||
```
|
||||
|
||||
Total: 13 tiles wide × 17 tiles tall = 221 tile positions.
|
||||
|
||||
#### Grass Subtile Atlas (168×136)
|
||||
|
||||
```kotlin
|
||||
val W_SUBTILE_GRASS = 168 // Width in subtiles
|
||||
val H_SUBTILE = 136 // Height in subtiles
|
||||
```
|
||||
|
||||
Total: 21 tiles wide × 17 tiles tall = 357 tile positions.
|
||||
|
||||
### Subtile Layout
|
||||
|
||||
Subtile atlases store tile variants at subtile resolution for advanced tiling patterns.
|
||||
|
||||
**Example grass subtiling:**
|
||||
- Positions (0,0) to (3,0): Top-left subtiles for different connection patterns
|
||||
- Positions (0,1) to (3,1): Top-right subtiles
|
||||
- And so on...
|
||||
|
||||
### Subtile Offset Vectors
|
||||
|
||||
```kotlin
|
||||
val subtileOffsetVectors = arrayOf(
|
||||
Point2i(0, 0), // Top-left
|
||||
Point2i(SUBTILE_SIZE, 0), // Top-right
|
||||
Point2i(SUBTILE_SIZE, SUBTILE_SIZE), // Bottom-right
|
||||
Point2i(0, SUBTILE_SIZE) // Bottom-left
|
||||
)
|
||||
```
|
||||
|
||||
Used for extracting and compositing subtiles from atlases.
|
||||
|
||||
### Tiling Modes
|
||||
|
||||
```kotlin
|
||||
const val TILING_FULL = 0 // Standard autotiling with flip/rotation
|
||||
const val TILING_FULL_NOFLIP = 1 // Standard autotiling without flip/rotation
|
||||
const val TILING_BRICK_SMALL = 2 // Small brick pattern (4 rows per tile)
|
||||
const val TILING_BRICK_SMALL_NOFLIP = 3 // Small brick without flip/rotation
|
||||
const val TILING_BRICK_LARGE = 4 // Large brick pattern (2 rows per tile)
|
||||
const val TILING_BRICK_LARGE_NOFLIP = 5 // Large brick without flip/rotation
|
||||
```
|
||||
|
||||
Each mode defines different subtile selection patterns and whether flip/rotation variants are applied.
|
||||
|
||||
### Item Image Generation from Subtiles
|
||||
|
||||
When creating item icons from subtiled blocks:
|
||||
|
||||
```kotlin
|
||||
val tileOffsetsForItemImageFromSubtile = arrayOf(
|
||||
intArrayOf(4*2, 4*5, 4*8, 4*11), // TILING_FULL
|
||||
intArrayOf(4*2, 4*5+2, 4*8+2, 4*11), // TILING_BRICK_SMALL
|
||||
intArrayOf(4*2+2, 4*5, 4*8+2, 4*11) // TILING_BRICK_LARGE
|
||||
)
|
||||
```
|
||||
|
||||
These offsets select representative subtiles to composite into a 16×16 item icon.
|
||||
|
||||
## Atlas Generation Process
|
||||
|
||||
### Initialisation
|
||||
|
||||
```kotlin
|
||||
operator fun invoke(updateExisting: Boolean = false) {
|
||||
// 1. Create six season atlases
|
||||
atlasPrevernal = Pixmap(TILES_IN_X * TILE_SIZE, TILES_IN_X * TILE_SIZE, RGBA8888)
|
||||
// ... create all six + glow + emissive
|
||||
|
||||
// 2. Load init.tga (predefined tiles: air, breakage stages, etc.)
|
||||
drawInitPixmap()
|
||||
|
||||
// 3. Scan all modules for block textures
|
||||
val tgaList = collectBlockTextures()
|
||||
|
||||
// 4. Process each texture into atlases
|
||||
tgaList.forEach { (modname, file) ->
|
||||
fileToAtlantes(modname, file, glowFile, emissiveFile)
|
||||
}
|
||||
|
||||
// 5. Generate item images
|
||||
generateItemImages()
|
||||
|
||||
// 6. Create terrain colour map
|
||||
generateTerrainColourMap()
|
||||
}
|
||||
```
|
||||
|
||||
### Texture Collection
|
||||
|
||||
The engine scans three directories:
|
||||
- **blocks/** — Standard terrain blocks
|
||||
- **ores/** — Ore overlays
|
||||
- **fluid/** — Fluid textures
|
||||
|
||||
Only textures with corresponding codex entries are loaded:
|
||||
|
||||
```kotlin
|
||||
dir.list()
|
||||
.filter { it.extension() == "tga" &&
|
||||
BlockCodex["$modname:${it.nameWithoutExtension()}"] != null }
|
||||
.forEach { fileToAtlantes(modname, it) }
|
||||
```
|
||||
|
||||
### File to Atlas Processing
|
||||
|
||||
```kotlin
|
||||
private fun fileToAtlantes(
|
||||
modname: String,
|
||||
diffuse: FileHandle,
|
||||
glow: FileHandle?,
|
||||
emissive: FileHandle?,
|
||||
prefix: String? // e.g., "ores" or "fluid" for non-block textures
|
||||
) {
|
||||
val sourcePixmap = Pixmap(diffuse)
|
||||
|
||||
// Determine atlas format from dimensions
|
||||
when {
|
||||
sourcePixmap.width == 16 && sourcePixmap.height == 16 ->
|
||||
processSingleTile(sourcePixmap)
|
||||
|
||||
sourcePixmap.width == 112 && sourcePixmap.height == 112 ->
|
||||
processAutotilingBlock(sourcePixmap)
|
||||
|
||||
sourcePixmap.width == 224 && sourcePixmap.height == 224 ->
|
||||
processSeasonalBlock(sourcePixmap)
|
||||
|
||||
sourcePixmap.width == W_SUBTILE_GENERIC * SUBTILE_SIZE ->
|
||||
processSubtiledBlock(sourcePixmap, SUBTILE_GENERIC)
|
||||
|
||||
sourcePixmap.width == W_SUBTILE_GRASS * SUBTILE_SIZE ->
|
||||
processSubtiledBlock(sourcePixmap, SUBTILE_GRASS)
|
||||
|
||||
// ... other formats
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Barcode System
|
||||
|
||||
112×112 autotiling textures embed metadata in a "barcode" region at position (6,5):
|
||||
|
||||
```
|
||||
Grid position (6, 5) = pixel column 96-111, row 80-95
|
||||
```
|
||||
|
||||
#### Barcode Encoding
|
||||
|
||||
Two rows of 16 pixels encode properties as binary:
|
||||
|
||||
**Row 1 (top):** Connection type
|
||||
- Bits read left-to-right as binary number
|
||||
- `0000` = Connect mutually (CONNECT_MUTUAL)
|
||||
- `0001` = Connect to self only (CONNECT_SELF)
|
||||
|
||||
**Row 2:** Mask type
|
||||
- `0010` = Use autotiling (MASK_47)
|
||||
- Other values for special tiling modes
|
||||
|
||||
**Reading the barcode (right-to-left):**
|
||||
|
||||
```kotlin
|
||||
// In fileToAtlantes() for 112×112 textures
|
||||
var connectionType = 0
|
||||
var maskType = 0
|
||||
for (bit in 0 until TILE_SIZE) { // TILE_SIZE = 16
|
||||
val x = (7 * TILE_SIZE - 1) - bit // Read right-to-left: x = 111 down to 96
|
||||
val y1 = 5 * TILE_SIZE // y1 = 80 (connection type row)
|
||||
val y2 = y1 + 1 // y2 = 81 (mask type row)
|
||||
val pixel1 = (tilesPixmap.getPixel(x, y1).and(255) >= 128).toInt(bit)
|
||||
val pixel2 = (tilesPixmap.getPixel(x, y2).and(255) >= 128).toInt(bit)
|
||||
|
||||
connectionType += pixel1
|
||||
maskType += pixel2
|
||||
}
|
||||
// Helper extension: fun Boolean.toInt(shift: Int) = if (this) (1 shl shift) else 0
|
||||
```
|
||||
|
||||
The barcode is read **right-to-left** (from pixel x=111 to x=96), with each white pixel representing a binary 1.
|
||||
|
||||
### RenderTag Generation
|
||||
|
||||
Each block gets a RenderTag storing atlas metadata:
|
||||
|
||||
```kotlin
|
||||
data class RenderTag(
|
||||
val tileNumber: Int, // Position in atlas (0-based index)
|
||||
val connectionType: Int, // CONNECT_SELF, CONNECT_MUTUAL, etc.
|
||||
val maskType: Int, // MASK_47, MASK_PLATFORM, etc.
|
||||
val tilingMode: Int, // TILING_FULL, TILING_BRICK_SMALL, etc.
|
||||
val postProcessing: Int // POSTPROCESS_NONE, POSTPROCESS_DEBLOCKING, etc.
|
||||
)
|
||||
```
|
||||
|
||||
Stored in hash maps:
|
||||
|
||||
```kotlin
|
||||
lateinit var tags: HashMap<ItemID, RenderTag> // By block ID
|
||||
lateinit var tagsByTileNum: HashArray<RenderTag> // By tile number
|
||||
```
|
||||
|
||||
## Item Image Generation
|
||||
|
||||
### Terrain and Wall Images
|
||||
|
||||
Separate item textures for terrain and wall placement:
|
||||
|
||||
```kotlin
|
||||
lateinit var itemTerrainTexture: Texture
|
||||
lateinit var itemWallTexture: Texture
|
||||
```
|
||||
|
||||
### Generation Process
|
||||
|
||||
```kotlin
|
||||
tags.forEach { (id, tag) ->
|
||||
val itemSheetNum = tileIDtoItemSheetNumber(id)
|
||||
|
||||
if (tag.maskType >= 16) {
|
||||
// Subtiled block - composite from subtiles
|
||||
val subtilePositions = tileOffsetsForItemImageFromSubtile[tag.tilingMode / 2]
|
||||
compositeSub tiles(subtilePositions, itemSheetNum)
|
||||
} else {
|
||||
// Standard block - copy representative tile
|
||||
val atlasPos = tag.tileNumber + maskOffsetForItemImage(tag.maskType)
|
||||
copyTileToItemSheet(atlasPos, itemSheetNum)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Wall Darkening
|
||||
|
||||
Wall item images are darkened for visual distinction:
|
||||
|
||||
```kotlin
|
||||
val WALL_OVERLAY_COLOUR = Color(.72f, .72f, .72f, 1f)
|
||||
|
||||
for (y in 0 until itemWallPixmap.height) {
|
||||
for (x in 0 until itemWallPixmap.width) {
|
||||
val color = Color(itemWallPixmap.getPixel(x, y))
|
||||
color.mul(WALL_OVERLAY_COLOUR)
|
||||
itemWallPixmap.drawPixel(x, y, color.toRGBA())
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Glow and Emissive Layers
|
||||
|
||||
### Separate Atlases
|
||||
|
||||
```kotlin
|
||||
lateinit var atlasGlow: Pixmap // Additive glow layer
|
||||
lateinit var atlasEmissive: Pixmap // Full-brightness layer
|
||||
```
|
||||
|
||||
### File Naming Convention
|
||||
|
||||
For a block texture `32.tga`:
|
||||
- **Main texture:** `32.tga`
|
||||
- **Glow layer:** `32_glow.tga` (optional)
|
||||
- **Emissive layer:** `32_emsv.tga` (optional)
|
||||
|
||||
### Rendering
|
||||
|
||||
Glow and emissive layers render separately:
|
||||
|
||||
```kotlin
|
||||
// 1. Render base tile (affected by lighting)
|
||||
batch.draw(baseTexture, x, y)
|
||||
|
||||
// 2. Render glow (additive blend)
|
||||
batch.setBlendFunction(GL_SRC_ALPHA, GL_ONE)
|
||||
batch.draw(glowTexture, x, y)
|
||||
|
||||
// 3. Render emissive (full brightness)
|
||||
batch.setColor(1f, 1f, 1f, 1f) // Ignore lighting
|
||||
batch.draw(emissiveTexture, x, y)
|
||||
batch.setBlendFunction(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
|
||||
```
|
||||
|
||||
## Terrain Colour Map
|
||||
|
||||
Pre-calculated average colours for each block:
|
||||
|
||||
```kotlin
|
||||
lateinit var terrainTileColourMap: HashMap<ItemID, Cvec>
|
||||
```
|
||||
|
||||
### Generation
|
||||
|
||||
```kotlin
|
||||
for (id in itemSheetNumbers) {
|
||||
val tileNum = itemSheetNumbers[id]
|
||||
val tx = (tileNum % TILES_IN_X) * TILE_SIZE
|
||||
val ty = (tileNum / TILES_IN_X) * TILE_SIZE
|
||||
|
||||
var r = 0f; var g = 0f; var b = 0f; var a = 0f
|
||||
|
||||
// Average all pixels in the tile
|
||||
for (y in ty until ty + TILE_SIZE) {
|
||||
for (x in tx until tx + TILE_SIZE) {
|
||||
val pixel = itemTerrainPixmap.getPixel(x, y)
|
||||
r += extractRed(pixel)
|
||||
g += extractGreen(pixel)
|
||||
b += extractBlue(pixel)
|
||||
a += extractAlpha(pixel)
|
||||
}
|
||||
}
|
||||
|
||||
val pixelCount = TILE_SIZE * TILE_SIZE
|
||||
terrainTileColourMap[id] = Cvec(
|
||||
r / pixelCount,
|
||||
g / pixelCount,
|
||||
b / pixelCount,
|
||||
a / pixelCount
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Usage
|
||||
|
||||
Colour maps are used for:
|
||||
- Minimap rendering
|
||||
- Particle colours when blocks break
|
||||
- LOD (Level of Detail) rendering
|
||||
- Fast block type identification
|
||||
|
||||
## Atlas Size and Limits
|
||||
|
||||
### Configuration
|
||||
|
||||
```kotlin
|
||||
var MAX_TEX_SIZE = 2048 // Configurable atlas dimension
|
||||
var TILES_IN_X = MAX_TEX_SIZE / TILE_SIZE // = 128 tiles
|
||||
var SUBTILES_IN_X = MAX_TEX_SIZE / SUBTILE_SIZE // = 256 subtiles
|
||||
```
|
||||
|
||||
### Capacity
|
||||
|
||||
With 2048×2048 atlases:
|
||||
- **Standard tiles:** 128 × 128 = 16,384 tile positions
|
||||
- **Subtiles:** 256 × 256 = 65,536 subtile positions
|
||||
|
||||
### Dynamic Resizing
|
||||
|
||||
The init.tga can be wider than MAX_TEX_SIZE. The engine tiles it vertically:
|
||||
|
||||
```kotlin
|
||||
// If init.tga is 4096 pixels wide but MAX_TEX_SIZE = 2048:
|
||||
// Split into two 2048×16 strips stacked vertically
|
||||
|
||||
val stripsNeeded = ceil(initPixmap.width / MAX_TEX_SIZE)
|
||||
for (strip in 0 until stripsNeeded) {
|
||||
val srcX = strip * MAX_TEX_SIZE
|
||||
val destY = strip * TILE_SIZE
|
||||
atlas.drawPixmap(initPixmap, srcX, 0, MAX_TEX_SIZE, TILE_SIZE, 0, destY, MAX_TEX_SIZE, TILE_SIZE)
|
||||
}
|
||||
```
|
||||
|
||||
## Atlas Cursor and Allocation
|
||||
|
||||
### Atlas Cursor
|
||||
|
||||
Tracks next available tile position:
|
||||
|
||||
```kotlin
|
||||
private var atlasCursor = 66 // First 66 tiles reserved
|
||||
```
|
||||
|
||||
Reserved tiles (0-65):
|
||||
- Tile 0: Air (transparent)
|
||||
- Tiles 1-15: Reserved for engine use
|
||||
- Tiles 16-65: Breakage stages, update markers, etc.
|
||||
|
||||
### Allocation
|
||||
|
||||
```kotlin
|
||||
private fun allocateTileSpace(tilesNeeded: Int): Int {
|
||||
val allocated = atlasCursor
|
||||
atlasCursor += tilesNeeded
|
||||
|
||||
if (atlasCursor >= TOTAL_TILES) {
|
||||
throw Error("Atlas full! Needed $tilesNeeded more tiles but only ${TOTAL_TILES - allocated} remaining.")
|
||||
}
|
||||
|
||||
return allocated
|
||||
}
|
||||
```
|
||||
|
||||
### Item Sheet Cursor
|
||||
|
||||
Separate cursor for item sheet allocation:
|
||||
|
||||
```kotlin
|
||||
private var itemSheetCursor = 16 // Reserve first 16 item positions
|
||||
```
|
||||
|
||||
## Retexturing System
|
||||
|
||||
### Alt File Paths
|
||||
|
||||
Modules can override textures from other modules:
|
||||
|
||||
```kotlin
|
||||
val altFilePaths: HashMap<String, FileHandle> = ModMgr.GameRetextureLoader.altFilePaths
|
||||
```
|
||||
|
||||
### Override Resolution
|
||||
|
||||
```kotlin
|
||||
val originalFile = Gdx.files.internal("basegame/blocks/32.tga")
|
||||
val overrideFile = altFilePaths.getOrDefault(originalFile.path(), originalFile)
|
||||
// Use overrideFile for atlas generation
|
||||
```
|
||||
|
||||
Texture packs populate `altFilePaths` to replace textures without modifying original modules.
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
### Memory Usage
|
||||
|
||||
Six season atlases + glow + emissive = **8 atlases**
|
||||
|
||||
With 2048×2048 RGBA8888:
|
||||
- Per atlas: 2048 × 2048 × 4 bytes = 16 MB
|
||||
- Total: 8 × 16 MB = **128 MB** of atlas memory
|
||||
|
||||
Plus item sheets (~2048×2048 × 6 = 48 MB).
|
||||
|
||||
**Total texture memory: ~176 MB**
|
||||
|
||||
### Generation Time
|
||||
|
||||
Atlas generation is expensive:
|
||||
- Runs once at startup
|
||||
- Can be cached (planned feature)
|
||||
- Blocks game startup until complete
|
||||
|
||||
### Optimisations
|
||||
|
||||
1. **Lazy subtile generation** — Only process subtiled blocks that exist in BlockCodex
|
||||
2. **Parallel texture loading** — Could load multiple modules concurrently
|
||||
3. **Atlas caching** — Save generated atlases to disk (not yet implemented)
|
||||
4. **Compression** — Use texture compression (DXT/ETC) on GPUs that support it
|
||||
|
||||
## Debugging
|
||||
|
||||
### Export Atlases
|
||||
|
||||
Uncomment debug code to export atlases:
|
||||
|
||||
```kotlin
|
||||
PixmapIO2.writeTGA(Gdx.files.absolute("atlas_0_prevernal.tga"), atlasPrevernal, false)
|
||||
PixmapIO2.writeTGA(Gdx.files.absolute("atlas_1_vernal.tga"), atlasVernal, false)
|
||||
// ... export all six
|
||||
```
|
||||
|
||||
### Verify Barcodes
|
||||
|
||||
Check barcode region is transparent except for set bits:
|
||||
|
||||
```kotlin
|
||||
for (y in barcodeY until barcodeY + 2) {
|
||||
for (x in barcodeX until barcodeX + 16) {
|
||||
val pixel = pixmap.getPixel(x, y)
|
||||
if (!isTransparent(pixel) && !isWhite(pixel)) {
|
||||
println("WARNING: Barcode contains non-binary pixel at ($x, $y)")
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Atlas Visualisation
|
||||
|
||||
Render atlas to screen with grid overlay:
|
||||
|
||||
```kotlin
|
||||
batch.draw(atlasTexture, 0f, 0f)
|
||||
shapeRenderer.begin(ShapeRenderer.ShapeType.Line)
|
||||
for (i in 0 until TILES_IN_X) {
|
||||
shapeRenderer.line(i * TILE_SIZE, 0, i * TILE_SIZE, MAX_TEX_SIZE)
|
||||
shapeRenderer.line(0, i * TILE_SIZE, MAX_TEX_SIZE, i * TILE_SIZE)
|
||||
}
|
||||
shapeRenderer.end()
|
||||
```
|
||||
|
||||
## Potential Enhancements
|
||||
|
||||
1. **Atlas caching** — Save generated atlases to disk to avoid regeneration on startup
|
||||
2. **GPU texture compression** — Use DXT/ETC formats to reduce VRAM usage
|
||||
3. **Mipmap generation** — For better filtering at distance
|
||||
4. **Animated block textures** — Frame-by-frame animation support
|
||||
5. **Normal maps** — For advanced lighting effects
|
||||
6. **Parallax mapping** — Create depth illusion
|
||||
|
||||
Note: Atlas expansion is already implemented (see `expandAtlantes()` method).
|
||||
|
||||
## See Also
|
||||
|
||||
- [[Rendering Pipeline]] — How atlases are used during rendering
|
||||
- [[Autotiling In-Depth]] — Detailed autotiling algorithm
|
||||
- [[Modules:Blocks]] — Creating block textures for modules
|
||||
642
UI-Framework.md
Normal file
642
UI-Framework.md
Normal file
@@ -0,0 +1,642 @@
|
||||
# UI Framework
|
||||
|
||||
Terrarum features a comprehensive UI framework built on a canvas-based architecture with event handling, animations, and a rich set of pre-built UI components.
|
||||
|
||||
## Overview
|
||||
|
||||
The UI system provides:
|
||||
- **UICanvas** — Base class for all UI screens
|
||||
- **UIItem** — Individual UI elements (buttons, sliders, text fields)
|
||||
- **UIHandler** — Manages lifecycle and animations
|
||||
- **Event system** — Mouse, keyboard, and gamepad input
|
||||
- **Layout management** — Positioning and sizing
|
||||
- **Built-in components** — Buttons, lists, sliders, and more
|
||||
|
||||
## Architecture
|
||||
|
||||
### UICanvas
|
||||
|
||||
The base class for all UI screens and windows:
|
||||
|
||||
```kotlin
|
||||
abstract class UICanvas(
|
||||
toggleKeyLiteral: String? = null,
|
||||
toggleButtonLiteral: String? = null,
|
||||
customPositioning: Boolean = false
|
||||
) : Disposable {
|
||||
|
||||
abstract var width: Int
|
||||
abstract var height: Int
|
||||
|
||||
val handler: UIHandler
|
||||
val uiItems: List<UIItem>
|
||||
|
||||
abstract fun updateImpl(delta: Float)
|
||||
abstract fun renderImpl(batch: SpriteBatch, camera: OrthographicCamera)
|
||||
}
|
||||
```
|
||||
|
||||
### UIHandler
|
||||
|
||||
Manages the canvas lifecycle:
|
||||
|
||||
```kotlin
|
||||
class UIHandler(
|
||||
val toggleKey: String?,
|
||||
val toggleButton: String?,
|
||||
val customPositioning: Boolean
|
||||
) {
|
||||
var posX: Int
|
||||
var posY: Int
|
||||
var isOpened: Boolean
|
||||
var isOpening: Boolean
|
||||
var isClosing: Boolean
|
||||
}
|
||||
```
|
||||
|
||||
## Creating a UI
|
||||
|
||||
### Basic UICanvas Example
|
||||
|
||||
```kotlin
|
||||
class MyUI : UICanvas() {
|
||||
|
||||
override var width = 800
|
||||
override var height = 600
|
||||
|
||||
init {
|
||||
// Position the UI
|
||||
handler.initialX = (App.scr.width - width) / 2
|
||||
handler.initialY = (App.scr.height - height) / 2
|
||||
|
||||
// Add UI items
|
||||
addUIItem(
|
||||
UIItemTextButton(
|
||||
this,
|
||||
text = "Click Me",
|
||||
x = 50,
|
||||
y = 50,
|
||||
width = 200,
|
||||
clickOnceListener = { _, _ ->
|
||||
println("Button clicked!")
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun updateImpl(delta: Float) {
|
||||
// Update logic
|
||||
}
|
||||
|
||||
override fun renderImpl(batch: SpriteBatch, camera: OrthographicCamera) {
|
||||
// Custom rendering
|
||||
batch.color = Color.WHITE
|
||||
// Draw UI background, decorations, etc.
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
// Clean up resources
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## UI Lifecycle
|
||||
|
||||
### Show/Hide Timeline
|
||||
|
||||
The UI lifecycle follows this sequence:
|
||||
|
||||
```
|
||||
User triggers open
|
||||
↓
|
||||
show() -- Called once when opening starts
|
||||
↓
|
||||
doOpening() -- Called every frame while opening
|
||||
doOpening()
|
||||
↓
|
||||
endOpening() -- Called once when fully opened
|
||||
↓
|
||||
(UI is open)
|
||||
↓
|
||||
User triggers close
|
||||
↓
|
||||
doClosing() -- Called every frame while closing
|
||||
doClosing()
|
||||
↓
|
||||
endClosing() -- Called once when fully closed
|
||||
↓
|
||||
hide() -- Called once when hidden
|
||||
```
|
||||
|
||||
### Override Points
|
||||
|
||||
```kotlin
|
||||
override fun show() {
|
||||
super.show()
|
||||
// Initialize when opening
|
||||
}
|
||||
|
||||
override fun hide() {
|
||||
super.hide()
|
||||
// Clean up when closing
|
||||
}
|
||||
|
||||
override fun doOpening(delta: Float) {
|
||||
super.doOpening(delta)
|
||||
// Opening animation logic
|
||||
}
|
||||
|
||||
override fun doClosing(delta: Float) {
|
||||
super.doClosing(delta)
|
||||
// Closing animation logic
|
||||
}
|
||||
|
||||
override fun endOpening(delta: Float) {
|
||||
super.endOpening(delta)
|
||||
// Finalize opening
|
||||
}
|
||||
|
||||
override fun endClosing(delta: Float) {
|
||||
super.endClosing(delta)
|
||||
// Finalize closing
|
||||
}
|
||||
```
|
||||
|
||||
## UIItem Components
|
||||
|
||||
UIItems are individual UI elements added to a canvas.
|
||||
|
||||
### Base UIItem
|
||||
|
||||
```kotlin
|
||||
abstract class UIItem(
|
||||
val parentUI: UICanvas,
|
||||
var posX: Int,
|
||||
var posY: Int
|
||||
) {
|
||||
abstract var width: Int
|
||||
abstract var height: Int
|
||||
|
||||
var clickOnceListener: ((Int, Int) -> Unit)? = null
|
||||
var touchDraggedListener: ((Int, Int, Int, Int) -> Unit)? = null
|
||||
var keyDownListener: ((Int) -> Unit)? = null
|
||||
// ... more event listeners
|
||||
}
|
||||
```
|
||||
|
||||
### Built-in Components
|
||||
|
||||
#### UIItemTextButton
|
||||
|
||||
Clickable button with text:
|
||||
|
||||
```kotlin
|
||||
UIItemTextButton(
|
||||
parentUI = this,
|
||||
text = "OK",
|
||||
x = 100,
|
||||
y = 100,
|
||||
width = 150,
|
||||
clickOnceListener = { _, _ ->
|
||||
// Handle click
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
#### UIItemImageButton
|
||||
|
||||
Button with an image:
|
||||
|
||||
```kotlin
|
||||
UIItemImageButton(
|
||||
parentUI = this,
|
||||
image = texture,
|
||||
x = 50,
|
||||
y = 50,
|
||||
width = 64,
|
||||
height = 64,
|
||||
clickOnceListener = { _, _ ->
|
||||
// Handle click
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
#### UIItemHorzSlider
|
||||
|
||||
Horizontal slider:
|
||||
|
||||
```kotlin
|
||||
UIItemHorzSlider(
|
||||
parentUI = this,
|
||||
x = 50,
|
||||
y = 100,
|
||||
width = 200,
|
||||
min = 0.0,
|
||||
max = 100.0,
|
||||
initial = 50.0,
|
||||
changeListener = { newValue ->
|
||||
println("Value: $newValue")
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
#### UIItemTextLineInput
|
||||
|
||||
Single-line text input:
|
||||
|
||||
```kotlin
|
||||
UIItemTextLineInput(
|
||||
parentUI = this,
|
||||
x = 50,
|
||||
y = 50,
|
||||
width = 300,
|
||||
placeholder = "Enter text...",
|
||||
textCommitListener = { text ->
|
||||
println("Entered: $text")
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
#### UIItemTextArea
|
||||
|
||||
Multi-line text area:
|
||||
|
||||
```kotlin
|
||||
UIItemTextArea(
|
||||
parentUI = this,
|
||||
x = 50,
|
||||
y = 50,
|
||||
width = 400,
|
||||
height = 200,
|
||||
readOnly = false
|
||||
)
|
||||
```
|
||||
|
||||
#### UIItemList
|
||||
|
||||
Scrollable list:
|
||||
|
||||
```kotlin
|
||||
UIItemList<String>(
|
||||
parentUI = this,
|
||||
x = 50,
|
||||
y = 50,
|
||||
width = 300,
|
||||
height = 400,
|
||||
items = listOf("Item 1", "Item 2", "Item 3"),
|
||||
itemRenderer = { item, y ->
|
||||
// Render item
|
||||
},
|
||||
clickListener = { item ->
|
||||
println("Selected: $item")
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
#### UIItemToggleButton
|
||||
|
||||
Toggle button (on/off):
|
||||
|
||||
```kotlin
|
||||
UIItemToggleButton(
|
||||
parentUI = this,
|
||||
x = 50,
|
||||
y = 50,
|
||||
width = 150,
|
||||
labelText = "Enable Feature",
|
||||
initialState = false,
|
||||
changeListener = { isOn ->
|
||||
println("Toggle: $isOn")
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
## Event Handling
|
||||
|
||||
### Mouse Events
|
||||
|
||||
```kotlin
|
||||
val button = UIItemTextButton(...)
|
||||
|
||||
button.clickOnceListener = { mouseX, mouseY ->
|
||||
// Single click
|
||||
}
|
||||
|
||||
button.touchDownListener = { screenX, screenY, pointer, button ->
|
||||
// Mouse button pressed
|
||||
}
|
||||
|
||||
button.touchUpListener = { screenX, screenY, pointer, button ->
|
||||
// Mouse button released
|
||||
}
|
||||
|
||||
button.touchDraggedListener = { screenX, screenY, deltaX, deltaY ->
|
||||
// Mouse dragged while holding
|
||||
}
|
||||
```
|
||||
|
||||
### Keyboard Events
|
||||
|
||||
```kotlin
|
||||
item.keyDownListener = { keycode ->
|
||||
when (keycode) {
|
||||
Input.Keys.ENTER -> handleEnter()
|
||||
Input.Keys.ESCAPE -> handleEscape()
|
||||
}
|
||||
}
|
||||
|
||||
item.keyTypedListener = { character ->
|
||||
handleTypedChar(character)
|
||||
}
|
||||
```
|
||||
|
||||
### Scroll Events
|
||||
|
||||
```kotlin
|
||||
item.scrolledListener = { amountX, amountY ->
|
||||
// Mouse wheel scrolled
|
||||
scrollOffset += amountY * scrollSpeed
|
||||
}
|
||||
```
|
||||
|
||||
## Positioning
|
||||
|
||||
### Absolute Positioning
|
||||
|
||||
```kotlin
|
||||
// Position relative to canvas
|
||||
uiItem.posX = 100
|
||||
uiItem.posY = 50
|
||||
```
|
||||
|
||||
### Relative Positioning
|
||||
|
||||
```kotlin
|
||||
// Center horizontally
|
||||
uiItem.posX = (width - uiItem.width) / 2
|
||||
|
||||
// Align to right
|
||||
uiItem.posX = width - uiItem.width - margin
|
||||
|
||||
// Align to bottom
|
||||
uiItem.posY = height - uiItem.height - margin
|
||||
```
|
||||
|
||||
### Custom Positioning
|
||||
|
||||
```kotlin
|
||||
override fun updateUI(delta: Float) {
|
||||
// Dynamic positioning
|
||||
followButton.posX = targetX - followButton.width / 2
|
||||
followButton.posY = targetY - followButton.height / 2
|
||||
}
|
||||
```
|
||||
|
||||
## Sub-UIs
|
||||
|
||||
UICanvases can contain child UIs:
|
||||
|
||||
```kotlin
|
||||
class ParentUI : UICanvas() {
|
||||
|
||||
val childUI = ChildUI()
|
||||
|
||||
init {
|
||||
addSubUI(childUI)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Child UIs:
|
||||
- Have their own lifecycle
|
||||
- Can be opened/closed independently
|
||||
- Positioned relative to parent
|
||||
|
||||
## Animations
|
||||
|
||||
### Opening Animation
|
||||
|
||||
```kotlin
|
||||
override fun doOpening(delta: Float) {
|
||||
// Fade in
|
||||
opacity = min(1.0f, opacity + delta * fadeSpeed)
|
||||
|
||||
// Slide in from top
|
||||
handler.posY = lerp(startY, targetY, openProgress)
|
||||
|
||||
super.doOpening(delta)
|
||||
}
|
||||
```
|
||||
|
||||
### Closing Animation
|
||||
|
||||
```kotlin
|
||||
override fun doClosing(delta: Float) {
|
||||
// Fade out
|
||||
opacity = max(0.0f, opacity - delta * fadeSpeed)
|
||||
|
||||
// Slide out to bottom
|
||||
handler.posY = lerp(targetY, endY, closeProgress)
|
||||
|
||||
super.doClosing(delta)
|
||||
}
|
||||
```
|
||||
|
||||
## Mouse Control
|
||||
|
||||
UIs can be mouse-controlled:
|
||||
|
||||
```kotlin
|
||||
interface MouseControlled {
|
||||
fun touchDragged(screenX: Int, screenY: Int, pointer: Int): Boolean
|
||||
fun touchDown(screenX: Int, screenY: Int, pointer: Int, button: Int): Boolean
|
||||
fun touchUp(screenX: Int, screenY: Int, pointer: Int, button: Int): Boolean
|
||||
fun scrolled(amountX: Float, amountY: Float): Boolean
|
||||
}
|
||||
```
|
||||
|
||||
## Keyboard Control
|
||||
|
||||
UIs can be keyboard-navigable:
|
||||
|
||||
```kotlin
|
||||
interface KeyControlled {
|
||||
fun keyDown(keycode: Int): Boolean
|
||||
fun keyUp(keycode: Int): Boolean
|
||||
fun keyTyped(character: Char): Boolean
|
||||
}
|
||||
```
|
||||
|
||||
## Tooltips
|
||||
|
||||
UIItems can display tooltips on hover:
|
||||
|
||||
```kotlin
|
||||
override fun getTooltipText(mouseX: Int, mouseY: Int): String? {
|
||||
return if (mouseOver) {
|
||||
"This is a helpful tooltip"
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The tooltip system automatically displays text near the cursor.
|
||||
|
||||
## Toolkit Utilities
|
||||
|
||||
The `Toolkit` object provides UI utilities:
|
||||
|
||||
```kotlin
|
||||
object Toolkit {
|
||||
val drawWidth: Int // UI render area width
|
||||
val drawHeight: Int // UI render area height
|
||||
|
||||
fun drawBoxBorder(batch: SpriteBatch, x: Int, y: Int, w: Int, h: Int)
|
||||
fun drawBoxFilled(batch: SpriteBatch, x: Int, y: Int, w: Int, h: Int)
|
||||
}
|
||||
```
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Modal Dialog
|
||||
|
||||
```kotlin
|
||||
class ConfirmDialog(
|
||||
val message: String,
|
||||
val onConfirm: () -> Unit
|
||||
) : UICanvas() {
|
||||
|
||||
override var width = 400
|
||||
override var height = 200
|
||||
|
||||
init {
|
||||
// Center on screen
|
||||
handler.initialX = (App.scr.width - width) / 2
|
||||
handler.initialY = (App.scr.height - height) / 2
|
||||
|
||||
// Yes button
|
||||
addUIItem(UIItemTextButton(
|
||||
this, "Yes",
|
||||
x = 50, y = 150, width = 100,
|
||||
clickOnceListener = { _, _ ->
|
||||
onConfirm()
|
||||
handler.setAsClose()
|
||||
}
|
||||
))
|
||||
|
||||
// No button
|
||||
addUIItem(UIItemTextButton(
|
||||
this, "No",
|
||||
x = 250, y = 150, width = 100,
|
||||
clickOnceListener = { _, _ ->
|
||||
handler.setAsClose()
|
||||
}
|
||||
))
|
||||
}
|
||||
|
||||
override fun renderImpl(batch: SpriteBatch, camera: OrthographicCamera) {
|
||||
// Draw background
|
||||
Toolkit.drawBoxFilled(batch, posX, posY, width, height)
|
||||
|
||||
// Draw message
|
||||
App.fontGame.draw(batch, message, posX + 50f, posY + 50f)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Settings Screen
|
||||
|
||||
```kotlin
|
||||
class SettingsUI : UICanvas() {
|
||||
|
||||
val volumeSlider = UIItemHorzSlider(...)
|
||||
val fullscreenToggle = UIItemToggleButton(...)
|
||||
|
||||
init {
|
||||
addUIItem(volumeSlider)
|
||||
addUIItem(fullscreenToggle)
|
||||
|
||||
// Apply button
|
||||
addUIItem(UIItemTextButton(
|
||||
this, "Apply",
|
||||
clickOnceListener = { _, _ ->
|
||||
applySettings()
|
||||
}
|
||||
))
|
||||
}
|
||||
|
||||
private fun applySettings() {
|
||||
App.audioMixer.masterVolume = volumeSlider.value.toFloat()
|
||||
setFullscreen(fullscreenToggle.isOn)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Inventory Grid
|
||||
|
||||
```kotlin
|
||||
class InventoryUI : UICanvas() {
|
||||
|
||||
val inventoryGrid = UIItemInventoryItemGrid(
|
||||
parentUI = this,
|
||||
inventory = player.inventory,
|
||||
x = 50,
|
||||
y = 50,
|
||||
columns = 10,
|
||||
rows = 5,
|
||||
cellSize = 48
|
||||
)
|
||||
|
||||
init {
|
||||
addUIItem(inventoryGrid)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practises
|
||||
|
||||
1. **Dispose resources** — Always implement dispose() properly
|
||||
2. **Cache calculations** — Don't recalculate positions every frame
|
||||
3. **Use event listeners** — Don't poll mouse state manually
|
||||
4. **Batch rendering** — Group draw calls efficiently
|
||||
5. **Handle edge cases** — Check bounds, null items, empty lists
|
||||
6. **Provide keyboard navigation** — Accessibility and convenience
|
||||
7. **Test at different resolutions** — Ensure UI scales properly
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
1. **Minimize UIItem count** — Too many items slow updates
|
||||
2. **Cull off-screen items** — Skip rendering invisible elements
|
||||
3. **Pool objects** — Reuse UIItems when possible
|
||||
4. **Optimize render calls** — Batch similar draw operations
|
||||
5. **Lazy initialization** — Create heavy UIs only when needed
|
||||
|
||||
## Debugging
|
||||
|
||||
### Debug Overlays
|
||||
|
||||
```kotlin
|
||||
if (App.IS_DEVELOPMENT_BUILD) {
|
||||
// Draw UIItem bounds
|
||||
shapeRenderer.rect(posX, posY, width, height)
|
||||
|
||||
// Show mouse position
|
||||
println("Mouse: ($mouseX, $mouseY)")
|
||||
}
|
||||
```
|
||||
|
||||
### UI State Logging
|
||||
|
||||
```kotlin
|
||||
println("UI opened: ${handler.isOpened}")
|
||||
println("UI opening: ${handler.isOpening}")
|
||||
println("UI closing: ${handler.isClosing}")
|
||||
```
|
||||
|
||||
## See Also
|
||||
|
||||
- [[Glossary]] — UI terminology
|
||||
- [[Inventory]] — Inventory UI system
|
||||
- [[Modules]] — Adding UIs to modules
|
||||
- [[Languages]] — Localising UI text
|
||||
483
World-Time-and-Calendar.md
Normal file
483
World-Time-and-Calendar.md
Normal file
@@ -0,0 +1,483 @@
|
||||
# World Time and Calendar
|
||||
|
||||
**Audience:** Terrarum the game maintainers working with ingame time-based systems.
|
||||
|
||||
Terrarum the game uses a custom calendar system tailored specifically for the ingame lore. This guide covers the calendar structure, time representation, date formatting, and working with world time programmatically.
|
||||
|
||||
## Overview
|
||||
|
||||
The world time system provides:
|
||||
- **Custom calendar** — 4 seasons, 30 days each, no leap years
|
||||
- **8-day week** — Inspired by The World Calendar
|
||||
- **24-hour clock** — No AM/PM, forced 24-hour format
|
||||
- **Real-time mapping** — One game day = 22 real minutes
|
||||
- **ISO 8601 compliance** — Time intervals follow international standard
|
||||
- **Predictable equinoxes** — Always occur on the 15th of each month
|
||||
|
||||
## Calendar Structure
|
||||
|
||||
### The Yearly Calendar
|
||||
|
||||
A year consists of 4 seasons (months), each lasting exactly 30 days. There are no leap years.
|
||||
|
||||
```
|
||||
=========================
|
||||
|Mo|Ty|Mi|To|Fr|La|Su|Ve|
|
||||
|--|--|--|--|--|--|--|--|
|
||||
| 1| 2| 3| 4| 5| 6| 7| | <- Spring
|
||||
| 8| 9|10|11|12|13|14| |
|
||||
|15|16|17|18|19|20|21| |
|
||||
|22|23|24|25|26|27|28| |
|
||||
|29|30| 1| 2| 3| 4| 5| | <- Summer
|
||||
| 6| 7| 8| 9|10|11|12| |
|
||||
|13|14|15|16|17|18|19| |
|
||||
|20|21|22|23|24|25|26| |
|
||||
|27|28|29|30| 1| 2| 3| | <- Autumn
|
||||
| 4| 5| 6| 7| 8| 9|10| |
|
||||
|11|12|13|14|15|16|17| |
|
||||
|18|19|20|21|22|23|24| |
|
||||
|25|26|27|28|29|30| 1| | <- Winter
|
||||
| 2| 3| 4| 5| 6| 7| 8| |
|
||||
| 9|10|11|12|13|14|15| |
|
||||
|16|17|18|19|20|21|22| |
|
||||
|23|24|25|26|27|28|29|30|
|
||||
=========================
|
||||
```
|
||||
|
||||
### Key Calendar Facts
|
||||
|
||||
- **Year length:** 120 days
|
||||
- **Week length:** 7 or 8 days
|
||||
- **Seasons:** Spring, Summer, Autumn, Winter (30 days each)
|
||||
- **Week starts:** Monday (Mondag)
|
||||
- **8th day of the week:** Verddag (Winter 30th) — New Year's Eve holiday
|
||||
- **New Year:** Spring 1st (Mondag)
|
||||
- **Equinoxes/Solstices:** Always on the 15th of each season
|
||||
|
||||
### Day Names
|
||||
|
||||
| Day | Name | Abbreviation |
|
||||
|-----|------|--------------|
|
||||
| 1 | Mondag | Mo |
|
||||
| 2 | Tysdag | Ty |
|
||||
| 3 | Middag | Mi |
|
||||
| 4 | Torsdag | To |
|
||||
| 5 | Fredag | Fr |
|
||||
| 6 | Lagsdag | La |
|
||||
| 7 | Sundag | Su |
|
||||
| 8 | Verddag | Ve |
|
||||
|
||||
**Note:** Verddag only occurs on Winter 30th as the year's 8th-day week completion.
|
||||
|
||||
### Season Names
|
||||
|
||||
| Season | Number | Days | Notes |
|
||||
|--------|--------|------|-------|
|
||||
| Spring | 1 | 1-30 | Spring 1st is New Year |
|
||||
| Summer | 2 | 1-30 | |
|
||||
| Autumn | 3 | 1-30 | |
|
||||
| Winter | 4 | 1-30 | Winter 30th is New Year's Eve (Verddag) |
|
||||
|
||||
## Time Representation
|
||||
|
||||
### WorldTime Class
|
||||
|
||||
Time is managed by the `WorldTime` class:
|
||||
|
||||
```kotlin
|
||||
class WorldTime(initTime: Long = 0L) {
|
||||
var TIME_T = 0L // Time in seconds since epoch
|
||||
|
||||
// Time components
|
||||
val seconds: Int // 0-59
|
||||
val minutes: Int // 0-59
|
||||
val hours: Int // 0-23
|
||||
|
||||
// Date components
|
||||
val days: Int // 1-30
|
||||
val months: Int // 1-4 (Spring=1, Summer=2, Autumn=3, Winter=4)
|
||||
val years: Int // Year number
|
||||
|
||||
val dayOfWeek: Int // 0-7 (0=Mondag, 7=Verddag)
|
||||
val dayOfYear: Int // 0-119
|
||||
}
|
||||
```
|
||||
|
||||
### Time Constants
|
||||
|
||||
```kotlin
|
||||
const val MINUTE_SEC = 60 // 60 seconds per minute
|
||||
const val HOUR_MIN = 60 // 60 minutes per hour
|
||||
const val HOURS_PER_DAY = 22 // 22 hours per day
|
||||
const val HOUR_SEC = 3600 // 3600 seconds per hour
|
||||
const val DAY_LENGTH = 79200 // 22 * 3600 seconds per day
|
||||
const val YEAR_DAYS = 120 // 120 days per year
|
||||
```
|
||||
|
||||
### Epoch
|
||||
|
||||
The epoch (time zero) is:
|
||||
- **Year 1, Spring 1st, 00:00:00** (Mondag)
|
||||
- Represented as: `0001-01-01` or `00010101`
|
||||
|
||||
## Date Formatting
|
||||
|
||||
### Human-Readable Format
|
||||
|
||||
Format: `Year-MonthName-Date`
|
||||
|
||||
```kotlin
|
||||
// Examples:
|
||||
"0125-Spring-07" // Year 125, Spring 7th
|
||||
"0125-Summ-15" // Year 125, Summer 15th (solstice)
|
||||
"0125-Autu-22" // Year 125, Autumn 22nd
|
||||
"0125-Wint-30" // Year 125, Winter 30th (Verddag)
|
||||
```
|
||||
|
||||
**Usage:**
|
||||
|
||||
```kotlin
|
||||
val formattedTime = worldTime.getFormattedTime()
|
||||
// Returns: "0125-Spring-07"
|
||||
```
|
||||
|
||||
### Number-Only Format
|
||||
|
||||
Format: `Year-Month-Date` where months are numbered 1-4:
|
||||
|
||||
```kotlin
|
||||
// Examples:
|
||||
"0125-01-07" // Year 125, Spring 7th
|
||||
"0125-02-15" // Year 125, Summer 15th
|
||||
"0125-03-22" // Year 125, Autumn 22nd
|
||||
"0125-04-30" // Year 125, Winter 30th
|
||||
```
|
||||
|
||||
### Computerised Format
|
||||
|
||||
Format: `YearMonthDate` (no separators)
|
||||
|
||||
```kotlin
|
||||
// Examples:
|
||||
01250107 // Year 125, Spring 7th
|
||||
01250215 // Year 125, Summer 15th
|
||||
01250322 // Year 125, Autumn 22nd
|
||||
01250430 // Year 125, Winter 30th
|
||||
```
|
||||
|
||||
**Usage:**
|
||||
|
||||
```kotlin
|
||||
val filenameTime = worldTime.getFilenameTime()
|
||||
// Returns: 01250107
|
||||
```
|
||||
|
||||
### Short Time Format
|
||||
|
||||
Abbreviated for UI display:
|
||||
|
||||
```kotlin
|
||||
val shortTime = worldTime.getShortTime()
|
||||
// Returns: "Spr-07" or "Wint-30"
|
||||
```
|
||||
|
||||
## Working with Time
|
||||
|
||||
### Getting Current Time
|
||||
|
||||
```kotlin
|
||||
val worldTime = INGAME.world.worldTime
|
||||
val currentHour = worldTime.hours
|
||||
val currentDay = worldTime.days
|
||||
val currentMonth = worldTime.months
|
||||
val currentYear = worldTime.years
|
||||
```
|
||||
|
||||
### Advancing Time
|
||||
|
||||
```kotlin
|
||||
// Advance by 1 second
|
||||
worldTime.addTime(1)
|
||||
|
||||
// Advance by 1 minute
|
||||
worldTime.addTime(WorldTime.MINUTE_SEC)
|
||||
|
||||
// Advance by 1 hour
|
||||
worldTime.addTime(WorldTime.HOUR_SEC)
|
||||
|
||||
// Advance by 1 day
|
||||
worldTime.addTime(WorldTime.DAY_LENGTH)
|
||||
```
|
||||
|
||||
### Setting Specific Time
|
||||
|
||||
```kotlin
|
||||
// Set to a specific timestamp
|
||||
worldTime.TIME_T = targetTime
|
||||
|
||||
// Set to specific date/time
|
||||
worldTime.setTime(year = 125, month = 1, day = 15, hour = 12, minute = 30)
|
||||
```
|
||||
|
||||
### Time Calculations
|
||||
|
||||
#### Day of Week
|
||||
|
||||
```kotlin
|
||||
val dayOfWeek = worldTime.dayOfWeek
|
||||
when (dayOfWeek) {
|
||||
0 -> println("Mondag")
|
||||
1 -> println("Tysdag")
|
||||
2 -> println("Middag")
|
||||
3 -> println("Torsdag")
|
||||
4 -> println("Fredag")
|
||||
5 -> println("Lagsdag")
|
||||
6 -> println("Sundag")
|
||||
7 -> println("Verddag") // Only on Winter 30th
|
||||
}
|
||||
```
|
||||
|
||||
#### Day of Year
|
||||
|
||||
```kotlin
|
||||
val dayOfYear = worldTime.dayOfYear // 0-119
|
||||
val season = dayOfYear / 30 // 0=Spring, 1=Summer, 2=Autumn, 3=Winter
|
||||
val dayInSeason = (dayOfYear % 30) + 1 // 1-30
|
||||
```
|
||||
|
||||
#### Time Until Event
|
||||
|
||||
```kotlin
|
||||
// Calculate time until next sunrise (hour 6)
|
||||
val currentHour = worldTime.hours
|
||||
val hoursUntilSunrise = if (currentHour < 6) {
|
||||
6 - currentHour
|
||||
} else {
|
||||
(22 - currentHour) + 6 // Tomorrow
|
||||
}
|
||||
|
||||
val secondsUntilSunrise = hoursUntilSunrise * WorldTime.HOUR_SEC
|
||||
```
|
||||
|
||||
## Solar Cycle
|
||||
|
||||
### Day/Night Cycle
|
||||
|
||||
The day lasts 22 hours with the following structure:
|
||||
|
||||
```kotlin
|
||||
// Approximate solar positions
|
||||
0h - 5h → Night (dark)
|
||||
6h - 8h → Dawn (sunrise)
|
||||
9h - 13h → Day (bright)
|
||||
14h - 16h → Afternoon
|
||||
17h - 19h → Dusk (sunset)
|
||||
20h - 21h → Evening (twilight)
|
||||
```
|
||||
|
||||
### Solar Angle Calculation
|
||||
|
||||
```kotlin
|
||||
fun getSolarAngle(time: WorldTime): Double {
|
||||
val hourAngle = (time.hours + time.minutes / 60.0) / 22.0
|
||||
return hourAngle * 2 * Math.PI
|
||||
}
|
||||
|
||||
fun isDaytime(time: WorldTime): Boolean {
|
||||
return time.hours in 6..18
|
||||
}
|
||||
|
||||
fun isNighttime(time: WorldTime): Boolean {
|
||||
return time.hours < 6 || time.hours > 18
|
||||
}
|
||||
```
|
||||
|
||||
## Seasonal Effects
|
||||
|
||||
### Season Detection
|
||||
|
||||
```kotlin
|
||||
fun getCurrentSeason(time: WorldTime): Season {
|
||||
return when (time.months) {
|
||||
1 -> Season.SPRING
|
||||
2 -> Season.SUMMER
|
||||
3 -> Season.AUTUMN
|
||||
4 -> Season.WINTER
|
||||
else -> Season.SPRING
|
||||
}
|
||||
}
|
||||
|
||||
enum class Season {
|
||||
SPRING, SUMMER, AUTUMN, WINTER
|
||||
}
|
||||
```
|
||||
|
||||
### Equinoxes and Solstices
|
||||
|
||||
```kotlin
|
||||
fun isEquinoxOrSolstice(time: WorldTime): Boolean {
|
||||
return time.days == 15
|
||||
}
|
||||
|
||||
fun getCelestialEvent(time: WorldTime): String? {
|
||||
if (time.days != 15) return null
|
||||
|
||||
return when (time.months) {
|
||||
1 -> "Spring Equinox"
|
||||
2 -> "Summer Solstice"
|
||||
3 -> "Autumn Equinox"
|
||||
4 -> "Winter Solstice"
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Real-Time Mapping
|
||||
|
||||
### Game Time vs. Real Time
|
||||
|
||||
One game day = 22 real minutes
|
||||
|
||||
```kotlin
|
||||
const val REAL_SECONDS_PER_GAME_DAY = 22 * 60 // 1320 real seconds
|
||||
const val GAME_SECONDS_PER_REAL_SECOND = WorldTime.DAY_LENGTH / REAL_SECONDS_PER_GAME_DAY
|
||||
// = 86400 / 1320 ≈ 65.45
|
||||
|
||||
// One real second ≈ 65 game seconds = 1 game minute
|
||||
```
|
||||
|
||||
### Time Scale Calculations
|
||||
|
||||
```kotlin
|
||||
// Convert real seconds to game seconds
|
||||
fun realToGameTime(realSeconds: Float): Long {
|
||||
return (realSeconds * GAME_SECONDS_PER_REAL_SECOND).toLong()
|
||||
}
|
||||
|
||||
// Convert game seconds to real seconds
|
||||
fun gameToRealTime(gameSeconds: Long): Float {
|
||||
return gameSeconds / GAME_SECONDS_PER_REAL_SECOND.toFloat()
|
||||
}
|
||||
|
||||
// Example: 5 real minutes
|
||||
val fiveRealMinutes = 5 * 60 // 300 real seconds
|
||||
val gameTime = realToGameTime(300f) // 18000 game seconds = 5 game hours
|
||||
```
|
||||
|
||||
## Time-Based Events
|
||||
|
||||
### Scheduling Events
|
||||
|
||||
```kotlin
|
||||
class ScheduledEvent(
|
||||
val triggerTime: Long,
|
||||
val action: (WorldTime) -> Unit
|
||||
)
|
||||
|
||||
val eventQueue = PriorityQueue<ScheduledEvent> { a, b ->
|
||||
a.triggerTime.compareTo(b.triggerTime)
|
||||
}
|
||||
|
||||
// Schedule event
|
||||
fun scheduleEvent(worldTime: WorldTime, hoursFromNow: Int, action: (WorldTime) -> Unit) {
|
||||
val triggerTime = worldTime.TIME_T + (hoursFromNow * WorldTime.HOUR_SEC)
|
||||
eventQueue.add(ScheduledEvent(triggerTime, action))
|
||||
}
|
||||
|
||||
// Process events
|
||||
fun processEvents(worldTime: WorldTime) {
|
||||
while (eventQueue.isNotEmpty() && eventQueue.peek().triggerTime <= worldTime.TIME_T) {
|
||||
val event = eventQueue.poll()
|
||||
event.action(worldTime)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Daily Events
|
||||
|
||||
```kotlin
|
||||
// Check if it's a specific hour
|
||||
fun checkDailyEvent(worldTime: WorldTime, targetHour: Int, action: () -> Unit) {
|
||||
if (worldTime.hours == targetHour && worldTime.minutes == 0 && worldTime.seconds == 0) {
|
||||
action()
|
||||
}
|
||||
}
|
||||
|
||||
// Example: Daily shop restock at 6:00
|
||||
if (worldTime.hours == 6 && worldTime.minutes == 0) {
|
||||
restockShop()
|
||||
}
|
||||
```
|
||||
|
||||
### Seasonal Events
|
||||
|
||||
```kotlin
|
||||
// Check for season start
|
||||
fun checkSeasonChange(worldTime: WorldTime, previousMonth: Int) {
|
||||
if (worldTime.months != previousMonth && worldTime.days == 1) {
|
||||
onSeasonChange(getCurrentSeason(worldTime))
|
||||
}
|
||||
}
|
||||
|
||||
// Example: Seasonal weather change
|
||||
fun onSeasonChange(season: Season) {
|
||||
when (season) {
|
||||
Season.SPRING -> weatherSystem.setSpringWeather()
|
||||
Season.SUMMER -> weatherSystem.setSummerWeather()
|
||||
Season.AUTUMN -> weatherSystem.setAutumnWeather()
|
||||
Season.WINTER -> weatherSystem.setWinterWeather()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Serialisation
|
||||
|
||||
### Saving Time
|
||||
|
||||
```kotlin
|
||||
fun saveTime(worldTime: WorldTime): ByteArray {
|
||||
return worldTime.TIME_T.toByteArray()
|
||||
}
|
||||
```
|
||||
|
||||
### Loading Time
|
||||
|
||||
```kotlin
|
||||
fun loadTime(data: ByteArray): WorldTime {
|
||||
val timeT = data.toLong()
|
||||
return WorldTime(timeT)
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practises
|
||||
|
||||
1. **Use WorldTime instance** — Don't calculate time manually
|
||||
2. **Check equinoxes on day 15** — Predictable solar events
|
||||
3. **Handle Verddag correctly** — 8th day only on Winter 30th
|
||||
4. **Use constants** — Don't hardcode time values
|
||||
5. **Schedule via TIME_T** — Use absolute timestamps for events
|
||||
6. **Format consistently** — Use provided formatting functions
|
||||
7. **Test year boundaries** — Ensure proper rollover from Winter 30 to Spring 1
|
||||
8. **Consider time zones** — Game uses single global time
|
||||
9. **Handle pause** — Time stops when game is paused
|
||||
10. **Sync with world save** — Always serialise TIME_T
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- **Assuming 24-hour days** — Days are 22 hours
|
||||
- **Forgetting Verddag** — 8th day only occurs once per year
|
||||
- **Hardcoding month numbers** — Use season constants
|
||||
- **Not handling year rollover** — Winter 30 → Spring 1
|
||||
- **Assuming AM/PM** — 24-hour clock enforced
|
||||
- **Miscalculating real-time mapping** — 1 real second = 1 game minute
|
||||
- **Ignoring equinox timing** — Always on 15th, not varying
|
||||
- **Using wall-clock time** — Game time is independent
|
||||
- **Forgetting to update events** — Process event queue regularly
|
||||
- **Not serialising properly** — TIME_T must be saved
|
||||
|
||||
## See Also
|
||||
|
||||
- [[World]] — World generation and management
|
||||
- [[Actors]] — Time-based actor behaviour
|
||||
- [[Weather]] — Seasonal weather systems
|
||||
- [[Modules-Setup]] — Module time integration
|
||||
502
World.md
Normal file
502
World.md
Normal file
@@ -0,0 +1,502 @@
|
||||
# World
|
||||
|
||||
The **GameWorld** is the container for all environmental data in Terrarum, including terrain, fluids, lighting, weather, and time. Each GameWorld represents a single playable dimension or planet.
|
||||
|
||||
## Overview
|
||||
|
||||
A GameWorld consists of:
|
||||
- **Block Layers** — Terrain, walls, ores, and fluids
|
||||
- **World Time** — Day/night cycle and calendar
|
||||
- **Environmental Properties** — Gravity, temperature, lighting
|
||||
- **Metadata** — Dimensions, spawn points, creation time
|
||||
- **Weather System** — Wind, precipitation, and atmospheric effects
|
||||
- **Wirings** — Electrical/conduit connections between blocks
|
||||
|
||||
## Creating a World
|
||||
|
||||
### World Construction
|
||||
|
||||
To create a new world programmatically:
|
||||
|
||||
```kotlin
|
||||
val world = GameWorld(
|
||||
width = 8192, // Width in tiles
|
||||
height = 2048, // Height in tiles
|
||||
creationTIME_T = App.getTIME_T(),
|
||||
lastPlayTIME_T = App.getTIME_T()
|
||||
)
|
||||
```
|
||||
|
||||
### World Dimensions
|
||||
|
||||
World dimensions are measured in **tiles**. Common world sizes:
|
||||
|
||||
- **Small:** 4096 × 1024
|
||||
- **Medium:** 8192 × 2048
|
||||
- **Large:** 16384 × 4096
|
||||
- **Massive:** 32768 × 8192
|
||||
|
||||
The engine supports millions of tiles through its chunked loading system.
|
||||
|
||||
**Important:** Dimensions must be positive integers. Worlds cannot be resized after creation.
|
||||
|
||||
### World Indices
|
||||
|
||||
Each world has a unique UUID:
|
||||
|
||||
```kotlin
|
||||
val worldIndex: UUID // Unique identifier for this world
|
||||
```
|
||||
|
||||
This index distinguishes worlds in multi-world save games.
|
||||
|
||||
## Block Layers
|
||||
|
||||
GameWorld uses multiple specialised layers to store different types of blocks:
|
||||
|
||||
### Layer Types
|
||||
|
||||
#### layerTerrain
|
||||
Foreground blocks that provide collision and form the main playable surface:
|
||||
|
||||
```kotlin
|
||||
val layerTerrain: BlockLayerGenericI16
|
||||
```
|
||||
|
||||
- Stores 16-bit block IDs
|
||||
- Provides collision for actors
|
||||
- Rendered in front of walls
|
||||
|
||||
#### layerWall
|
||||
Background wall blocks:
|
||||
|
||||
```kotlin
|
||||
val layerWall: BlockLayerGenericI16
|
||||
```
|
||||
|
||||
- Stores 16-bit block IDs
|
||||
- Typically non-solid
|
||||
- Rendered behind terrain
|
||||
- Affects lighting propagation
|
||||
|
||||
#### layerOres
|
||||
Ore deposits overlaid on terrain blocks:
|
||||
|
||||
```kotlin
|
||||
val layerOres: BlockLayerOresI16I8
|
||||
```
|
||||
|
||||
- Stores 16-bit ore type + 8-bit ore coverage
|
||||
- Rendered as overlay on terrain blocks
|
||||
- Shares damage values with terrain
|
||||
|
||||
#### layerFluids
|
||||
Fluid simulation data:
|
||||
|
||||
```kotlin
|
||||
val layerFluids: BlockLayerFluidI16F16
|
||||
```
|
||||
|
||||
- Stores 16-bit fluid type + 16-bit fluid amount
|
||||
- Uses cellular automata for flow simulation
|
||||
- Fluids have viscosity, colour, and density
|
||||
|
||||
### Accessing Blocks
|
||||
|
||||
Block layers use tile coordinates (integers):
|
||||
|
||||
```kotlin
|
||||
// Get a terrain block
|
||||
val blockID: ItemID = world.getTileFromTerrain(x, y)
|
||||
|
||||
// Set a terrain block
|
||||
world.setTileTerrain(x, y, blockID, false)
|
||||
|
||||
// Get a wall block
|
||||
val wallID: ItemID = world.getTileFromWall(x, y)
|
||||
|
||||
// Set a wall block
|
||||
world.setTileWall(x, y, wallID, false)
|
||||
```
|
||||
|
||||
**Coordinates:**
|
||||
- **x** — Horizontal tile position (0 to width-1)
|
||||
- **y** — Vertical tile position (0 to height-1)
|
||||
- **Origin** — Top-left corner is (0, 0)
|
||||
|
||||
### Block Damage
|
||||
|
||||
Blocks can be partially damaged:
|
||||
|
||||
```kotlin
|
||||
val terrainDamages: HashArray<Float> // 0.0 = undamaged, 1.0 = destroyed
|
||||
val wallDamages: HashArray<Float>
|
||||
```
|
||||
|
||||
Damage is indexed by block address (computed from x, y coordinates).
|
||||
|
||||
## Chunk System
|
||||
|
||||
Worlds are divided into **chunks** for efficient storage and generation:
|
||||
|
||||
### Chunk Dimensions
|
||||
|
||||
```kotlin
|
||||
val CHUNK_W = 128 // Chunk width in tiles
|
||||
val CHUNK_H = 128 // Chunk height in tiles
|
||||
```
|
||||
|
||||
Chunks enable:
|
||||
1. **Lazy Generation** — Generate terrain on-demand as players explore
|
||||
2. **Memory Efficiency** — Only load visible/nearby chunks
|
||||
3. **Save Optimisation** — Only save modified chunks
|
||||
|
||||
### Chunk Flags
|
||||
|
||||
Each chunk has a byte of flags:
|
||||
|
||||
```kotlin
|
||||
val chunkFlags: Array<ByteArray>
|
||||
```
|
||||
|
||||
Chunk flags track:
|
||||
- Generation status
|
||||
- Modification state
|
||||
- Active/inactive state
|
||||
|
||||
The world generates partially during creation, then generates additional chunks as needed during gameplay, enabling fast world creation even for massive world sizes.
|
||||
|
||||
## Tile Number Mapping
|
||||
|
||||
GameWorld maintains bidirectional mappings between block names (ItemIDs) and internal numbers:
|
||||
|
||||
```kotlin
|
||||
// Name to number (for setting blocks)
|
||||
val tileNameToNumberMap: HashMap<ItemID, Int>
|
||||
|
||||
// Number to name (for getting blocks)
|
||||
val tileNumberToNameMap: HashArray<ItemID>
|
||||
```
|
||||
|
||||
### Special Tile Numbers
|
||||
|
||||
- **0** — `Block.AIR` (empty space)
|
||||
- **1** — `Block.UPDATE` (triggers block update)
|
||||
- **65535** — `Block.NOT_GENERATED` (chunk not yet generated)
|
||||
|
||||
This mapping system allows worlds to persist blocks even when mods are added or removed, using the `dynamicToStaticTable` for remapping.
|
||||
|
||||
## Spawn Points
|
||||
|
||||
### Player Spawn
|
||||
|
||||
The tilewise coordinates where players initially appear:
|
||||
|
||||
```kotlin
|
||||
var spawnX: Int
|
||||
var spawnY: Int
|
||||
|
||||
// Convenience property
|
||||
var spawnPoint: Point2i
|
||||
```
|
||||
|
||||
### Portal Point
|
||||
|
||||
Optional alternative spawn point (e.g., for teleportation):
|
||||
|
||||
```kotlin
|
||||
var portalPoint: Point2i?
|
||||
```
|
||||
|
||||
## World Time
|
||||
|
||||
GameWorld includes a sophisticated time system:
|
||||
|
||||
```kotlin
|
||||
val worldTime: WorldTime
|
||||
```
|
||||
|
||||
### Time Units
|
||||
|
||||
WorldTime uses **seconds** as the base unit:
|
||||
|
||||
```kotlin
|
||||
const val SECOND_SEC = 1L
|
||||
const val MINUTE_SEC = 60L
|
||||
const val HOUR_SEC = 3600L
|
||||
const val DAY_SEC = 86400L
|
||||
```
|
||||
|
||||
### Calendar System
|
||||
|
||||
The world uses a 360-day calendar:
|
||||
- **12 months** of 30 days each
|
||||
- **Days** are 24 hours
|
||||
- **Year 125 (EPOCH)** is the default starting year
|
||||
|
||||
### Time Properties
|
||||
|
||||
```kotlin
|
||||
worldTime.timeDelta // Seconds elapsed since last update
|
||||
worldTime.TIME_T // Total seconds since epoch
|
||||
```
|
||||
|
||||
### Celestial Calculations
|
||||
|
||||
WorldTime calculates sun/moon positions and phases:
|
||||
|
||||
```kotlin
|
||||
worldTime.solarNoonTime // Time of solar noon today
|
||||
worldTime.lunarPhase // Current moon phase (0.0-1.0)
|
||||
worldTime.eclipticLongitude // Sun's position on ecliptic
|
||||
```
|
||||
|
||||
The time system simulates realistic seasons, equinoxes, and solstices.
|
||||
|
||||
### Day/Night Cycle
|
||||
|
||||
The day/night cycle affects:
|
||||
1. **Global lighting** (`world.globalLight`)
|
||||
2. **Actor behaviour** (day/night AI changes)
|
||||
3. **Block properties** (dynamic light sources)
|
||||
|
||||
## Environmental Properties
|
||||
|
||||
### Gravity
|
||||
|
||||
Gravitational acceleration vector:
|
||||
|
||||
```kotlin
|
||||
var gravitation: Vector2 = DEFAULT_GRAVITATION
|
||||
```
|
||||
|
||||
Default gravity is approximately 9.8 m/s² downward. Currently, only downward gravity is supported.
|
||||
|
||||
### Global Light
|
||||
|
||||
The baseline lighting level for the world:
|
||||
|
||||
```kotlin
|
||||
var globalLight: Cvec // RGB+UV colour vector
|
||||
```
|
||||
|
||||
Global light represents:
|
||||
- Sunlight during day
|
||||
- Moonlight/starlight at night
|
||||
- Ambient light in caves (typically near-zero)
|
||||
|
||||
Global light is **additive** with block luminosity when calculating the final lightmap.
|
||||
|
||||
### Average Temperature
|
||||
|
||||
The world's baseline temperature in Kelvin:
|
||||
|
||||
```kotlin
|
||||
var averageTemperature: Float = 288f // 15°C
|
||||
```
|
||||
|
||||
This affects environmental generation and could be used for climate simulation.
|
||||
|
||||
## Weather System
|
||||
|
||||
GameWorld includes a weather simulation:
|
||||
|
||||
```kotlin
|
||||
var weatherbox: Weatherbox
|
||||
```
|
||||
|
||||
### Weatherbox
|
||||
|
||||
The weatherbox manages:
|
||||
- **Current weather type** (clear, rain, snow, fog, etc.)
|
||||
- **Wind direction and speed** (with temporal smoothing)
|
||||
- **Precipitation intensity**
|
||||
- **Atmospheric effects**
|
||||
|
||||
Weather types are defined in the **WeatherCodex**.
|
||||
|
||||
### Wind
|
||||
|
||||
Wind is simulated with temporal coherence using control points:
|
||||
|
||||
```kotlin
|
||||
weatherbox.windDir // Wind direction (0.0-1.0 representing angles)
|
||||
weatherbox.windSpeed // Wind speed (m/s)
|
||||
```
|
||||
|
||||
Both properties use a point-interpolation system (pM3, pM2, pM1, p0, p1, p2, p3) for smooth transitions.
|
||||
|
||||
## Wiring and Conduits
|
||||
|
||||
GameWorld supports wire/conduit networks for connecting devices:
|
||||
|
||||
```kotlin
|
||||
val wirings: HashedWirings
|
||||
```
|
||||
|
||||
### Wire Types
|
||||
|
||||
Multiple wire types can occupy the same tile position:
|
||||
- Electrical wires
|
||||
- Fluid pipes
|
||||
- Logic gates
|
||||
- Data cables
|
||||
|
||||
Each wire type is stored separately in the wirings hash map.
|
||||
|
||||
### Wire Connections
|
||||
|
||||
Wires connect in four directions (up, down, left, right) using bit flags:
|
||||
|
||||
```kotlin
|
||||
// Connection bit positions
|
||||
WIRE_POS_MAP = [1, 2, 4, 8] // Up, Right, Down, Left
|
||||
```
|
||||
|
||||
## Dynamic Items
|
||||
|
||||
Worlds can have dynamic item inventories that persist:
|
||||
|
||||
```kotlin
|
||||
internal val dynamicItemInventory: ItemTable
|
||||
```
|
||||
|
||||
This stores items that are unique to this world (e.g., procedurally generated equipment).
|
||||
|
||||
## Game Rules
|
||||
|
||||
Arbitrary world-specific configuration:
|
||||
|
||||
```kotlin
|
||||
val gameRules: KVHashMap
|
||||
```
|
||||
|
||||
Game rules can store custom world settings such as:
|
||||
- Difficulty modifiers
|
||||
- Gameplay options
|
||||
- World-specific flags
|
||||
|
||||
## World Lifecycle
|
||||
|
||||
### Creation Time
|
||||
|
||||
```kotlin
|
||||
internal var creationTime: Long // Unix timestamp
|
||||
```
|
||||
|
||||
Records when the world was first created.
|
||||
|
||||
### Play Time
|
||||
|
||||
```kotlin
|
||||
internal var lastPlayTime: Long // Last time the world was played
|
||||
internal var totalPlayTime: Long // Cumulative play time in seconds
|
||||
```
|
||||
|
||||
These track how long the world has been played.
|
||||
|
||||
### Disposal
|
||||
|
||||
When switching worlds or exiting, dispose of the world properly:
|
||||
|
||||
```kotlin
|
||||
world.dispose()
|
||||
```
|
||||
|
||||
This frees:
|
||||
- Block layer memory
|
||||
- Chunk data
|
||||
- Cached resources
|
||||
|
||||
**Warning:** Once disposed, a world cannot be used again. Set `world.disposed` flag to prevent accidental access.
|
||||
|
||||
## Advanced Topics
|
||||
|
||||
### SimpleGameWorld
|
||||
|
||||
For small, fixed-size worlds (e.g., mini-games), use `SimpleGameWorld`:
|
||||
|
||||
```kotlin
|
||||
class SimpleGameWorld(width: Int, height: Int) : GameWorld(width, height)
|
||||
```
|
||||
|
||||
SimpleGameWorld stores all data in a single file rather than using chunked storage.
|
||||
|
||||
### Random Seeds
|
||||
|
||||
Worlds store 128 random seeds for deterministic generation:
|
||||
|
||||
```kotlin
|
||||
val randSeeds: LongArray // 256 longs = 128 128-bit seeds
|
||||
```
|
||||
|
||||
Specific ranges are reserved:
|
||||
- Seeds 0-1: Roguelike randomiser
|
||||
- Seeds 2-3: Weather mixer
|
||||
- Seeds 4+: Available for custom use
|
||||
|
||||
### Generator Seed
|
||||
|
||||
The primary seed used for world generation:
|
||||
|
||||
```kotlin
|
||||
var generatorSeed: Long
|
||||
```
|
||||
|
||||
This seed determines the initial terrain, caves, ores, and structures.
|
||||
|
||||
## Best Practises
|
||||
|
||||
1. **Always check bounds** before accessing blocks: `x in 0 until world.width`
|
||||
2. **Use spawn points** rather than hardcoding coordinates
|
||||
3. **Dispose worlds** when switching or exiting to prevent memory leaks
|
||||
4. **Update world time** every frame using `worldTime.timeDelta`
|
||||
5. **Respect chunk boundaries** when implementing generation algorithms
|
||||
6. **Use tile number mapping** for mod compatibility
|
||||
7. **Set appropriate global light** for the world's environment
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Iterating Over Visible Tiles
|
||||
|
||||
```kotlin
|
||||
val camera: WorldCamera = ...
|
||||
for (y in camera.yTileStart until camera.yTileEnd) {
|
||||
for (x in camera.xTileStart until camera.xTileEnd) {
|
||||
val block = world.getTileFromTerrain(x, y)
|
||||
// Process block
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Finding Safe Spawn Point
|
||||
|
||||
```kotlin
|
||||
fun findSafeSpawnY(world: GameWorld, x: Int): Int {
|
||||
for (y in 0 until world.height) {
|
||||
val terrain = world.getTileFromTerrain(x, y)
|
||||
val above = world.getTileFromTerrain(x, y - 1)
|
||||
if (BlockCodex[terrain].isSolid && !BlockCodex[above].isSolid) {
|
||||
return y - 1 // One tile above solid ground
|
||||
}
|
||||
}
|
||||
return world.spawnY // Fallback
|
||||
}
|
||||
```
|
||||
|
||||
### Checking Block Properties
|
||||
|
||||
```kotlin
|
||||
fun isBlockSolid(world: GameWorld, x: Int, y: Int): Boolean {
|
||||
val blockID = world.getTileFromTerrain(x, y)
|
||||
return BlockCodex[blockID]?.isSolid ?: false
|
||||
}
|
||||
```
|
||||
|
||||
## See Also
|
||||
|
||||
- [[Glossary]] — World-related terminology
|
||||
- [[Save and Load]] — Persisting worlds to disk
|
||||
- [[Modules:Blocks]] — Defining custom blocks
|
||||
- [[Modules:Weather]] — Creating weather types
|
||||
- [[Development:RAW]] — Data-driven world generation
|
||||
Reference in New Issue
Block a user