Table of Contents
- Fixtures
- Overview
- FixtureBase
- Class Hierarchy
- Core Properties
- BlockBox
- Constructor
- Spawning
- Interaction
- Quick Lookup Parameters
- Despawning
- BlockBox Iteration
- Serialisation
- Electric Fixtures
- Class Definition
- Wire Types
- Configuring Wires
- Digital Signals
- Signal Thresholds
- Reading Wire State
- Wire Emission
- Complex Example: Logic Gate
- PlaysMusic Interface
- Common Patterns
- Best Practises
- Common Pitfalls
- See Also
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
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:
data class BlockBox(
val collisionType: ItemID = NO_COLLISION,
val width: Int = 0, // Tile-wise width
val height: Int = 0 // Tile-wise height
)
Collision Types:
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:
// 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
constructor(
blockBox0: BlockBox,
blockBoxProps: BlockBoxProps = BlockBoxProps(0),
renderOrder: RenderOrder = RenderOrder.MIDDLE,
nameFun: () -> String,
mainUI: UICanvas? = null,
inventory: FixtureInventory? = null,
id: ActorID? = null
)
Example Fixture:
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:
// 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
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
open fun onInteract(mx: Double, my: Double) {
// Fired on mouse click
// Do NOT override if fixture has mainUI (handled automatically)
}
Example:
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:
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:
init {
// With label
addQuickLookupParam("STAT_TEMPERATURE") {
"${temperature}°C"
}
// Without label (dynamic text only)
addQuickLookupParam {
if (isPowered) "§o§Powered§.§" else ""
}
}
Despawning
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
// 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:
blockBoxinventoryworldBlockPos- Custom state variables
Reconstruction:
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
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
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))
}
}
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:
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
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_LOWto aboveTHRESHOLD_HIGH - Falling edge — Signal crosses from above
THRESHOLD_HIGHto belowTHRESHOLD_LOW
Reading Wire State
// 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
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
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.
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:
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 discsFixtureMusicalTurntable— Vinyl record playerFixtureRadio— Streams music
Common Patterns
Creating a Simple Fixture
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
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
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
- Always call
super.updateImpl(delta)— Required for fixture systems to work - Mark UI/sprites as
@Transient— They cannot be serialised - Implement
reload()— Reconstruct all transient fields - Use
forEachBlockboxfor tile operations — Handles BlockBox correctly - Check
canBeDespawnedbefore despawn — Prevent loss of inventory contents - Set
inOperationwhen active — Enables chunk anchoring for machines - Use
makeNoiseAndDust()on spawn — Provides visual/audio feedback - Don't override
onInteract()if usingmainUI— 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
forEachBlockboxfor clarity - Despawning fixtures with inventory — Check
canBeDespawnedfirst - Directly modifying
worldBlockPos— Usespawn()instead
See Also
- Actors — Base actor system
- Animation-Description-Language — ADL for humanoid sprites
- Inventory — Inventory system
- World — World tile management