Conveyor (clockwise)

Clockwise conveyors rotate pushable tiles around themselves. The main work of this tile is done by Convey in support functions.

Tick function

func TickConveyorCW(int16 ParamIdx) {
    let Params = BoardParams[ParamIdx]
    // Force a redraw every tick
    DrawTile(Params.X, Params.Y)
    Convey(Params.X, Params.Y, 1)
}

Conveyor (counterclockwise)

Counterclockwise conveyors rotate pushable tiles around themselves. The main work of this tile is done by Convey in support functions.

Tick function

func TickConveyorCCW(int16 ParamIdx) {
    let Params = BoardParams[ParamIdx]
    // Force a redraw every tick
    DrawTile(Params.X, Params.Y)
    Convey(Params.X, Params.Y, -1)
}

Duplicator

Duplicators periodically copy the tile from the source side to the opposite side. If blocked, they will try to push the blocking tile out of the way.

If the player stands in the destination tile of a duplicator when it’s active, the duplicator will call the source tile’s touch function.

Although this is implemented as a single procedure in ZZT, I have broken it into several smaller functions for clarity.

Tick function

func TickDuplicator(int16 ParamIdx) {
    let Params = BoardParams[ParamIdx]

    // Cycle through 5 frames of animation before duplicating
    if Params.Param1 <= 4 {  // animation frame
        Params.Param1 += 1
        DrawTile(Params.X, Params.Y)
    } else {
        Params.Param1 = 0  // reset frame count
        TryDuplication(Params)
    }

    // Update the cycle based on the duplication rate, ranging from 3 to 24 cycles.
    let Rate = Params.Param2
    Params.Cycle = (9 - Rate) * 3
}


func TryDuplication(ParamRecord* Params) {
    if CanDuplicate(Params) {
        let SourceTile = BoardTiles[Params.X + Params.StepX][Params.Y + Params.StepY]
        let SourceParamIdx = ParamIdxForXY(Params.X + Params.StepX, Params.Y + Params.StepY)
        if SourceParamIdx > 0 {
            // If the source tile has a parameter index, we have to spawn a copy.
            // We also check if the param count is less than 176 even though the max is 150.
            if BoardParamCount < 174) {
                let SourceParams = BoardParams[SourceParamIdx]
                Spawn(Params.X - Params.StepX, Params.Y - Params.StepY, SourceTile.Type,
                      SourceTile.Color, SourceParams.Cycle, SourceParams)
                DrawTile(Params.X - Params.StepX, Params.Y - Params.StepY)
            }
        } else if SourceParamIdx != 0 {  // never duplicate the player
            // Otherwise we can just copy the tile.
            BoardTiles[Params.X - Params.StepX][Params.Y - Params.StepY] = SourceTile
            DrawTile(Params.X - Params.StepX, Params.Y - Params.StepY)
        }
        PlaySoundPriority(3, sndDup)
    }

    Params.Param1 = 0  // reset frame count (redundant)
    DrawTile(Params.X, Params.Y)
}


//
// Check if the destination is blocked, pushing or touching as a side effect.
//
func CanDuplicate(ParamRecord* Params) -> Bool {
    var DestTile = BoardTiles[Params.X - Params.StepX][Params.Y - Params.StepY]

    // If the player is at the destination tile, call the source tiles' touch function.
    if DestTile.Type == TTPlayer {
        let SourceTile = BoardTiles[Params.X + Params.StepX][Params.Y + Params.StepY]
        let Touch = TileTypes[SourceTile.Type].TouchFunction
        // TODO: What are these unknown variables?
        Touch(Params.X + Params.StepX, Params.Y + Params.StepY, 0, UNKNOWN1, UNKNOWN2)
        return false
    }

    // If the destination isn't empty, try pushing it out of the way.
    if DestTile.Type != TTEmpty {
        TryPush(Params.X - Params.StepX, Params.Y - Params.StepY, -Params.StepX, -Params.StepY)
        DestTile = BoardTiles[Params.X - Params.StepX][Params.Y - Params.StepY]
    }

    // If the destination is still blocked, play the blocked sound.
    if DestTile.Type != TTEmpty {
        PlaySoundPriority(3, sndBlocked)
        return false
    }
    
    return true
}

Player

The player object handles energizer animation, shooting, moving, some keyboard input, and some other board related functions.

The player tick function is implemented as a single procedure, but its behavior is so complex that I have broken it into multiple functions for this analysis. All functions are listed in the Player section.

Tick function

func TickPlayer(int16 ParamIdx) {
    let Params = BoardParams[ParamIdx]

    // Animate the color/character changes when the player is energized.
    if EnergizerCycles > 0 {
        AnimateEnergized(Params)
    } else {
        RestorePlayerAppearance(Params)
    }

    // If the player has no health, end the game.
    if CurrentHealth <= 0 {
        EndGame()
    }

    // Shooting always overrides moving.
    if (ShiftArrowPressed != 0) || (LastKeyCode == ' ') {
        // Handle shooting if shift-arrow or space is pressed.
        TryShooting(Params)
    } else if (PlayerXStep != 0) || (PlayerYStep != 0) {
        // Handle moving if the player has an X- or Y-step.
        TryMoving(Params)
    }

    // Handle other keyboard input: quit, save, torch, etc.
    HandleOtherKeyboardInput(Params)

    // If a torch is lit, update the torch
    if TorchCyclesLeft > 0 {
        UpdateTorch(Params)
    }

    // If the player is energized, update their state
    if EnergizerCycles > 0 {
        UpdateEnergizer(Params)
    }

    // If the board has a time limit, update the time elapsed
    if TimeLimit > 0 {
        UpdateBoardTime()
    }
}

Scroll

Scrolls are objects that die when touched. Every tick the scroll cycles through the intense foreground colors.

Tick function

func TickScroll(int16 ParamIdx) {
    // Increment the scroll's color
    let Params = BoardParams[ParamIdx]
    BoardTiles[Params.X][Params.Y].Color += 1

    // Wrap back around to blue after white
    if BoardTiles[Params.X][Params.Y].Color > 15 {
        BoardTiles[Params.X][Params.Y].Color = 9
    }

    DrawTile(Params.X, Params.Y)
}