High-level overview
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.
Full pseudocode
Disassembly Reference: items/player.asm:5
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()
}
}
func AnimateEnergized(ParamRecord* Params) {
// Toggle the player character between 01 and 02
if TileTypes[TTPlayer].Character == 2 {
TileTypes[TTPlayer].Character = 1
} else {
TileTypes[TTPlayer].Character = 2
}
// Cycle through background colors
if TickNumber % 2 == 0 {
// Set the player's tile color to white on the next BG color
let Color = ((TickNumber % 7) << 4) + 0x0f
BoardTiles[Params.X][Params.Y].Color = Color
} else {
// Set the player's tile color to white on black
BoardTiles[Params.X][Params.Y].Color = 0x0f
}
// Redraw the energized player
DrawTile(Params.X, Params.Y)
}
func RestorePlayerAppearance(ParamRecord* Params) {
// If the color or character is incorrect, reset them.
if (BoardTiles[Params.X][Params.Y] != 0x1f) || (TileTypes[TTPlayer].Character != 2) {
BoardTiles[Params.X][Params.Y] = 0x1f
TileTypes[TTPlayer].Character = 2
DrawTile(Params.X, Params.Y)
}
}
func EndGame() {
// Stop the player moving and shooting.
PlayerXStep = 0
PlayerYStep = 0
ShiftArrowPressed = 0
// If there isn't already a messenger at (0, 0), say the game over message.
if ParamIdxForXY(0, 0) != -1 {
SayMessage(32000, " Game over - Press ESCAPE")
}
// Fast-forward the game by setting the time between cycles to 0
TimePerCycle = 0
GameIsOver = 1
}
func TryShooting(ParamRecord* Params) {
// If the player is pressing shift-arrow, update the shoot direction
if (ShiftArrowPressed != 0) && ((PlayerXStep != 0) || (PlayerYStep != 0)) {
PlayerShootXStep = PlayerXStep
PlayerShootYStep = PlayerShootYStep
}
// Must have a direction to shoot
if (PlayerShootXStep == 0) && (PlayerShootYStep == 0) {
return
}
// Check if shooting is allowed on this board
if MaxShots == 0 {
if ShouldSayCantShootHere != 0 {
SayMessage(StdMessageDuration, "Can't shoot in this place!")
}
ShouldSayCantShootHere = 0
return
}
// Check if the player has any ammo
if CurrentAmmo == 0 {
if ShouldSayOutOfAmmo != 0 {
SayMessage(StdMessageDuration, "You don't have any ammo!")
}
ShouldSayOutOfAmmo = 0
return
}
// Count the player bullets on the board to see if you're under the board's Max Shots
var NumPlayerBullets = 0
let ParamCount = BoardParamCount
if ParamCount > 0 {
for CountIdx in range(ParamCount) {
let CountParams = BoardParams[CountIdx]
if BoardTiles[CountParams.X][CountParams.Y] == TTBullet {
// Check it's a player bullet
if CountParams.Param1 == SOPlayer {
NumPlayerBullets++
}
}
}
}
if MaxShots <= NumPlayerBullets {
return
}
// Player bullets are under the max count. Fire!
let DidShoot = Shoot(TTBullet, Params.X, Params.Y, PlayerShootXStep, PlayerShootYStep,
SOPlayer)
if DidShoot {
CurrentAmmo--
UpdateSideBar()
PlaySoundPriority(2, sndShoot)
// Cancel any player movement
PlayerXStep = 0
PlayerYStep = 0
}
}
func TryMoving(ParamRecord* Params) {
// Copy the current direction for shooting later
PlayerShootXStep = PlayerXStep
PlayerShootYStep = PlayerYStep
// Calculate the position of the destination
let X = Params.X + PlayerXStep
let Y = Params.Y + PlayerYStep
// Look up the touch function for the destination's tile type
let Touch = TileTypes[BoardTiles[X][Y].Type].TouchFunction
// Call the destination's touch function
let ParamIdx = 0
Touch(X, Y, ParamIdx, &PlayerXStep, &PlayerYStep)
// Check if the player is still moving after calling the touch function
if (PlayerXStep == 0) && (PlayerYStep == 0) {
return
}
// Play 110Hz footstep sound
if SoundBusy == 0 {
Sound(110)
}
// Check if the destination tile is passable
if TileTypes[BoardTiles[X][Y].Type].Passable == 0 {
// Stop the footstep click
if (SoundEnabled != 0) && (SoundBusy == 0) {
SoundOff()
}
return
}
// Stop the footstep click
if (SoundEnabled != 0) && (SoundBusy == 0) {
SoundOff()
}
// Move the player tile to its destination
MoveTileWithIdx(0, X, Y)
}
func HandleOtherKeyboardInput(ParamRecord* Params) {
if LastKeyCode == 'T' {
// T - Light a torch
TryLightTorch()
} else if (LastKeyCode == '\x1b' || LastKeyCode == 'Q') {
// ESC or Q - Prompt to quit
PromptQuit()
} else if LastKeyCode == 'S' {
// S - Save game
PromptSaveWorld("Save game:", SaveFilename, ".SAV")
} else if LastKeyCode == 'P' {
// P - Pause
if CurrentHealth > 0 { // can't pause if the player's dead
IsPaused = 1
}
} else if LastKeyCode == 'B' {
// B - Toggle sound
SoundEnabled = (SoundEnabled == 0) ? 1 : 0
StopPlayingSound()
UpdateSideBar()
LastKeyCode = ' '
} else if LastKeyCode == 'H' {
// H - Show help
ShowHelpFile("GAME.HLP", "Playing ZZT")
} else if LastKeyCode == 'F' {
// F - Show order form
ShowHelpFile("ORDER.HLP", "Order form")
} else if LastKeyCode == '?' {
// ? - Debug prompt
ShowDebugPrompt()
LastKeyCode = 0
}
}
func TryLightTorch(ParamRecord* Params) {
if TorchCyclesLeft > 0 {
return // A torch is already lit, nothing to do
}
// If there are no torches left, say a warning if we haven't already
if CurrentTorches <= 0 {
if ShouldSayOutOfTorches != 0 {
SayMessage(StdMessageDuration, "You don't have any torches!")
ShouldSayOutOfTorches = 0
}
return
}
// If the board is dark, say a warning if we haven't already
if BoardIsDark == 0 {
if ShouldSayDontNeedTorch != 0 {
SayMessage(StdMessageDuration, "Don't need torch - room is not dark!")
ShouldSayDontNeedTorch = 0
}
return
}
// Light a torch
CurrentTorches--
TorchCyclesLeft = 200
// Redraw all the tiles in a circle around the player (torch/bomb share a radius)
Explode(Params.X, Params.Y, EMRedrawOnly)
UpdateStatusBar()
}
func UpdateTorch(ParamRecord* Params) {
TorchCyclesLeft--
// If the torch just died, redraw the tiles around the player and play a sound
if TorchCyclesLeft == 0 {
Explode(Params.X, Params.Y, EMRedrawOnly)
PlaySoundPriority(3, sndTorchDied)
}
// Update the sidebar every 40 torch cycles
if TorchCyclesLeft % 40 == 0 {
UpdateSideBar()
}
}
func UpdateEnergizer(ParamRecord* Params) {
EnergizerCycles--
// When there are 10 cycles left, play the energizer done sound
if EnergizerCycles == 10 {
PlaySoundPriority(9, sndEnergizerDone)
return
}
// If the energizer just finished, fix the player color
if EnergizerCycles <= 0 {
BoardTiles[Params.X][Params.Y].Color = TileTypes[TTPlayer].Color
DrawTile(Params.X, Params.Y)
}
}
func UpdateBoardTime() {
if CurrentHealth <= 0 {
return
}
// Every 100 centiseconds (1 second) increment the time elapsed
if !CheckTimeElapsed(&BoardTimerCentis, 100) {
return
}
TimeElapsed++
if TimeLimit - 10 == TimeElapsed {
// When there's 10 seconds left, show the running out of time message and sound
SayMessage(StdMessageDuration, "Running out of time!")
PlaySoundPriority(3, sndRunningOutOfTime)
} else if TimeElapsed > TimeLimit {
// When time is up, attack the player
Attack(0) // param index 0 is the player
}
UpdateStatusBar()
}