|
Objects and Behaviors in ZZT Ultra
ZZT allows a designer to customize game behavior to a large extent. But
despite the ability to edit worlds and object code, there were always many
details in the ZZT and Super ZZT engines over which the designer could never
exercise control. There is much nuance to how certain syntax conventions are
handled in ZZT-OOP code, as well as very small issues with timing and
prefabricated object behaviors.
This page attempts to "read between the lines" and identify the subtleties
about ZZT that make the behaviors what they are. ZZT Ultra attempts to
reproduce and control as many subleties as possible. As they become known,
this page will expand to cover these extra details. With greater knowledge
comes a better, more robust copy of zzt_objs.txt.
Of course, if the behavior of everything in ZZT were totally limited to what
the original editor could produce, the implementation requirements for ZZT
Ultra would be relatively simple. But other editors were made and hidden
features and exploits were discovered, which means ZZT Ultra needs to account
for all the idiosyncrasies. So, hats off to folks like Kevin Vance for finding
out critical details, but curse the same people for upping the number of known
requirements that must be accounted for. If that makes any sense.
Object-Attribute Table
|
N U M B E R |
C H A R |
C O L O R |
N O S T A T |
A L W A Y S L I T |
D O M I N A N T C O L O R |
F U L L C O L O R |
T E X T D R A W |
C U S T O M D R A W |
B L O C K O B J E C T |
B L O C K P L A Y E R |
P U S H A B L E |
S Q U A S H A B L E |
C Y C L E |
S T E P X |
S T E P Y |
H A S O W N C H A R |
H A S O W N C O D E |
C U S T O M S T A R T |
EMPTY | 0 | 0 | 0 | 1 |
0 | 0 | 0 | 0 | 1 | 0 | 0 | 1 | 0 |
| | | | | |
FAKE | 27 | 178 | 14 | 1 |
0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 |
| | | | | |
FLOOR | 47 | 176 | 14 | 1 |
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| | | | | |
WATERN | 48 | 30 | 25 | 1 |
0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| | | | | |
WATERS | 49 | 31 | 25 | 1 |
0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| | | | | |
WATERW | 50 | 17 | 25 | 1 |
0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| | | | | |
WATERE | 51 | 16 | 25 | 1 |
0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| | | | | |
SOLID | 21 | 219 | 14 | 1 |
0 | 0 | 0 | 0 | 0 | 1 | 1 | 0 | 0 |
| | | | | |
NORMAL | 22 | 178 | 14 | 1 |
0 | 0 | 0 | 0 | 0 | 1 | 1 | 0 | 0 |
| | | | | |
BREAKABLE | 23 | 177 | 14 | 1 |
0 | 0 | 0 | 0 | 0 | 1 | 1 | 0 | 0 |
| | | | | |
INVISIBLE | 28 | 0 | 14 | 1 |
0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 |
| | | | | |
FOREST | 20 | 176 | 32 | 1 |
0 | 1 | 0 | 0 | 0 | 1 | 0 | 0 | 0 |
| | | | | |
WATER | 19 | 176 | 159 | 1 |
0 | 0 | 0 | 0 | 0 | 1 | 1 | 0 | 0 |
| | | | | |
LAVA | 19 | 111 | 78 | 1 |
0 | 0 | 0 | 0 | 0 | 1 | 1 | 0 | 0 |
| | | | | |
RICOCHET | 32 | 42 | 10 | 1 |
0 | 1 | 0 | 0 | 0 | 1 | 1 | 0 | 0 |
| | | | | |
BOARDEDGE | 1 | 219 | 14 | 1 |
0 | 0 | 0 | 0 | 0 | 1 | 1 | 0 | 0 |
| | | | | |
_TEXTBLUE | 73 | | 31 | 1 |
0 | 1 | 0 | 1 | 0 | 1 | 1 | 0 | 0 |
| | | | | |
_TEXTGREEN | 74 | | 47 | 1 |
0 | 1 | 0 | 1 | 0 | 1 | 1 | 0 | 0 |
| | | | | |
_TEXTCYAN | 75 | | 63 | 1 |
0 | 1 | 0 | 1 | 0 | 1 | 1 | 0 | 0 |
| | | | | |
_TEXTRED | 76 | | 79 | 1 |
0 | 1 | 0 | 1 | 0 | 1 | 1 | 0 | 0 |
| | | | | |
_TEXTPURPLE | 77 | | 95 | 1 |
0 | 1 | 0 | 1 | 0 | 1 | 1 | 0 | 0 |
| | | | | |
_TEXTBROWN | 78 | | 111 | 1 |
0 | 1 | 0 | 1 | 0 | 1 | 1 | 0 | 0 |
| | | | | |
_TEXTWHITE | 79 | | 15 | 1 |
0 | 1 | 0 | 1 | 0 | 1 | 1 | 0 | 0 |
| | | | | |
_BEAMHORIZ | 33 | 205 | 14 | 1 |
0 | 0 | 0 | 0 | 0 | 1 | 1 | 0 | 0 |
| | | | | |
_BEAMVERT | 43 | 186 | 14 | 1 |
0 | 0 | 0 | 0 | 0 | 1 | 1 | 0 | 0 |
| | | | | |
AMMO | 5 | 132 | 3 | 1 |
0 | 1 | 0 | 0 | 0 | 1 | 0 | 1 | 0 |
| | | | | |
TORCH | 6 | 157 | 6 | 1 |
1 | 1 | 0 | 0 | 0 | 1 | 0 | 0 | 0 |
| | | | | |
GEM | 7 | 4 | 14 | 1 |
0 | 0 | 0 | 0 | 0 | 1 | 0 | 1 | 1 |
| | | | | |
KEY | 8 | 12 | 15 | 1 |
0 | 0 | 0 | 0 | 0 | 1 | 0 | 1 | 0 |
| | | | | |
ENERGIZER | 14 | 127 | 5 | 1 |
0 | 1 | 0 | 0 | 0 | 1 | 0 | 0 | 0 |
| | | | | |
BOULDER | 24 | 254 | 14 | 1 |
0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 0 |
| | | | | |
SLIDERNS | 25 | 18 | 14 | 1 |
0 | 0 | 0 | 0 | 0 | 1 | 1 | 2 | 0 |
| | | | | |
SLIDEREW | 26 | 29 | 14 | 1 |
0 | 0 | 0 | 0 | 0 | 1 | 1 | 2 | 0 |
| | | | | |
DOOR | 9 | 10 | 15 | 1 |
0 | 0 | 0 | 0 | 1 | 1 | 1 | 0 | 0 |
| | | | | |
LINE | 31 | 249 | 14 | 1 |
0 | 0 | 0 | 0 | 1 | 1 | 1 | 0 | 0 |
| | | | | |
WEB | 63 | 249 | 14 | 1 |
0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 |
| | | | | |
PASSAGE | 11 | 240 | 127 | 0 |
1 | 0 | 1 | 0 | 1 | 1 | 1 | 0 | 0 |
3 | 0 | 0 | 0 | 0 | 0 |
CLOCKWISE | 16 | 179 | 14 | 0 |
0 | 0 | 0 | 0 | 0 | 1 | 1 | 0 | 0 |
3 | 0 | 0 | 1 | 0 | 0 |
COUNTER | 17 | 92 | 14 | 0 |
0 | 0 | 0 | 0 | 0 | 1 | 1 | 0 | 0 |
2 | 0 | 0 | 1 | 0 | 0 |
STAR | 15 | 47 | 12 | 0 |
0 | 1 | 0 | 0 | 0 | 1 | 0 | 0 | 0 |
1 | 0 | 0 | 1 | 0 | 0 |
BULLET | 18 | 248 | 15 | 0 |
0 | 1 | 0 | 0 | 0 | 1 | 0 | 0 | 1 |
1 | 0 | 0 | 0 | 0 | 0 |
STONE | 64 | 65 | 15 | 0 |
0 | 1 | 0 | 0 | 0 | 1 | 0 | 0 | 0 |
1 | 0 | 0 | 1 | 0 | 0 |
SCROLL | 10 | 232 | 15 | 0 |
0 | 1 | 0 | 0 | 0 | 1 | 0 | 1 | 0 |
1 | 0 | 0 | 0 | 1 | 1 |
OBJECT | 36 | 1 | 14 | 0 |
0 | 0 | 0 | 0 | 0 | 1 | 1 | 0 | 0 |
1 | 0 | 0 | 1 | 1 | 1 |
BOMB | 13 | 11 | 14 | 0 |
0 | 0 | 0 | 0 | 0 | 1 | 1 | 2 | 0 |
6 | 0 | 0 | 1 | 0 | 0 |
DUPLICATOR | 12 | 250 | 15 | 0 |
0 | 1 | 0 | 0 | 0 | 1 | 1 | 0 | 0 |
3 | 0 | 0 | 1 | 0 | 0 |
BLINKWALL | 29 | 206 | 14 | 0 |
0 | 0 | 0 | 0 | 0 | 1 | 1 | 0 | 0 |
1 | 0 | 0 | 0 | 0 | 0 |
BEAR | 34 | 153 | 6 | 0 |
0 | 1 | 0 | 0 | 0 | 1 | 0 | 1 | 1 |
3 | 0 | 0 | 0 | 0 | 0 |
RUFFIAN | 35 | 5 | 13 | 0 |
0 | 1 | 0 | 0 | 0 | 1 | 0 | 1 | 1 |
1 | 0 | 0 | 0 | 0 | 0 |
SLIME | 37 | 42 | 14 | 0 |
0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 |
3 | 0 | 0 | 0 | 0 | 0 |
SHARK | 38 | 94 | 15 | 0 |
0 | 1 | 0 | 0 | 0 | 1 | 1 | 0 | 0 |
3 | 0 | 0 | 0 | 0 | 0 |
SPINNINGGUN | 39 | 24 | 14 | 0 |
0 | 0 | 0 | 0 | 0 | 1 | 1 | 0 | 0 |
2 | 0 | 0 | 1 | 0 | 0 |
PUSHER | 40 | 16 | 14 | 0 |
0 | 0 | 0 | 0 | 0 | 1 | 1 | 0 | 0 |
4 | 0 | 0 | 1 | 0 | 0 |
LION | 41 | 234 | 12 | 0 |
0 | 1 | 0 | 0 | 0 | 1 | 0 | 1 | 1 |
2 | 0 | 0 | 0 | 0 | 0 |
TIGER | 42 | 227 | 11 | 0 |
0 | 1 | 0 | 0 | 0 | 1 | 0 | 1 | 1 |
2 | 0 | 0 | 0 | 0 | 0 |
ROTON | 59 | 148 | 13 | 0 |
0 | 1 | 0 | 0 | 0 | 1 | 0 | 1 | 1 |
1 | 0 | 0 | 0 | 0 | 0 |
SPIDER | 62 | 15 | 14 | 0 |
0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 |
1 | 0 | 0 | 0 | 0 | 0 |
DRAGONPUP | 60 | 148 | 4 | 0 |
0 | 1 | 0 | 0 | 0 | 1 | 0 | 1 | 1 |
1 | 0 | 0 | 1 | 0 | 0 |
PAIRER | 61 | 229 | 1 | 0 |
0 | 1 | 0 | 0 | 0 | 1 | 0 | 1 | 1 |
2 | 0 | 0 | 0 | 0 | 0 |
HEAD | 44 | 233 | 14 | 0 |
0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 1 |
2 | 0 | 0 | 0 | 0 | 0 |
SEGMENT | 45 | 79 | 14 | 0 |
0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 1 |
2 | 0 | 0 | 0 | 0 | 0 |
TRANSPORTER | 30 | 179 | 15 | 0 |
0 | 0 | 0 | 0 | 0 | 1 | 1 | 2 | 0 |
2 | 0 | 0 | 1 | 0 | 0 |
PLAYER | 4 | 2 | 31 | 0 |
1 | 1 | 1 | 0 | 0 | 1 | 0 | 1 | 0 |
1 | 0 | 0 | 1 | 0 | 0 |
There are some special combinations of these attributes that should be taken
into consideration:
- BLOCKOBJECT + PUSHABLE: Typical push behavior results in an
object that is reasonably "solid" but movable.
- BLOCKOBJECT + PUSHABLE + SQUASHABLE: Often used for enemies.
This type of object is pushable, but can be killed from push operations
that lack movement clearance.
- BLOCKOBJECT + (not PUSHABLE) + SQUASHABLE: Some enemies, such
as HEAD and SEGMENT, can be squashed by a push operation, but cannot
be pushed. They are always killed by a push operation, even if
there is movement clearance. However, these types will rigidly block
attempts to move items through a TRANSPORTER.
- (not BLOCKOBJECT) + (PUSHABLE): Nonblocking types that serve
as a valid push destination.
- (not BLOCKOBJECT) + (not PUSHABLE): Some nonblocking types,
such as FLOOR, can be moved to normally but cannot serve as a valid push
destination. These types therefore "fence in" pushables.
- CUSTOMDRAW + (not HASOWNCHAR): The $CUSTOMDRAW routine sets
the initial CHAR member to the default for the type.
- CUSTOMDRAW + HASOWNCHAR: The $CUSTOMDRAW routine sets the
initial CHAR member to the CHAR member of the object itself.
"No-stat" Types
Most of the types in ZZT and Super ZZT that lack status element definitions
are "items" and "terrains" per the original game editors. These tend to have
little or no ZZT-OOP code within zzt_objs.txt.
EMPTY
The least remarkable type of all. However, it is remarkable in one very
important respect: color still matters. Background is forced to be drawn black
even if the color attribute has non-black background, and the foreground color
is still relevant to placement and change events (e.g. RED EMPTY versus
GREEN EMPTY).
ZZT Ultra forces the background color to be black when it loads a legacy
world file. The foreground color (lower 4 bits) is left alone.
ZZT Ultra treats the EMPTY type as the main type code, which means
its code acts as a general recipient for dispatched messages that do not have
specific types as a target. A great deal of ZZT Ultra's business logic is
therefore handled in the ZZT-OOP code for EMPTY. None of this code actually
deals with the characteristics of the EMPTY square itself, because other types
usually conduct simple tests for EMPTY instead of using dispatched messages.
FAKE
Very similar to EMPTY; this is a non-blocking square that shows a message
when the PLAYER crosses it for the first time.
The type yields to pushables in such a way that it is overwritten. Once
the square is uncovered following such an overwrite, it becomes EMPTY; the
FAKE type is lost.
FLOOR
Very similar to EMPTY; this is a non-blocking square.
This type does not yield to pushables except in special cases.
WATERN, WATERS, WATERW, WATERE
Very similar to FLOOR; these are non-blocking squares.
These types do not yield to pushables except in special cases.
The PLAYER executes a water-current-following turn as part of its
$WALKBEHAVIOR handler. This movement occurs in addition to any ordinary
PLAYER movement that might have also happened during the same clock cycle.
With the exception of sound played on movement, this automatic movement is
nearly synonymous with ordinary PLAYER movement.
SOLID, NORMAL
These "impassible" blocking types constitute stock wall-building material
in ZZT boards. Functionally, they are identical, but they are identified
differently in ZZT-OOP and have different appearance.
BREAKABLE
This blocking type is similar to SOLID in functionality. Other types
will often destroy BREAKALBE types. The most common contexts are bullet
collision, star collision, and bomb explosion. Bears also remove BREAKABLE
types on contact.
INVISIBLE
This blocking type is similar to SOLID in functionality. The only major
difference is that the PLAYER changes a touched INVISIBLE type to NORMAL.
FOREST
This blocking type is similar to SOLID in functionality. The only major
difference is that the PLAYER can pass through FOREST. A message is shown
once when the PLAYER passes through FOREST for the first time.
When passing through FOREST, the PLAYER leaves behind EMPTY in ZZT and
FLOOR in Super ZZT.
WATER, LAVA
This "mostly" blocking type is similar to SOLID in functionality. The
PLAYER cannot move over it.
BULLET and STAR can pass through this type as if it were non-blocking.
This is accomplished in ZZT Ultra via #FORCEGO.
When ZZT Ultra loads a world, it swaps in the appropriate type based on
whether a ZZT or Super ZZT world is loaded: WATER for ZZT, LAVA for Super
ZZT. This also determines which message is shown when the PLAYER contacts
the type.
RICOCHET
This blocking type is similar to SOLID in functionality. Bullets behave
in interesting ways when they strike a blocking type near a RICOCHET.
BOARDEDGE
This blocking type is similar to SOLID in functionality. Normally, the
type is not designed to be seen, due to its presence as a "virtual perimeter"
for the board.
When the PLAYER contacts a BOARDEDGE, it attempts to transition to a
linked board if one is defined in the movement direction.
_TEXTBLUE, _TEXTGREEN, _TEXTCYAN, _TEXTRED, _TEXTPURPLE,
_TEXTBROWN, _TEXTWHITE
These blocking types are similar to SOLID in functionality. They have the
TEXTDRAW attribute, which flips the meaning of character and color attribute.
These types were not exposed in the original ZZT-OOP namespace, hence the
underscore prefixes.
_BEAMHORIZ, _BEAMVERT
These blocking types are similar to SOLID in functionality. Generated and
removed by BLINKWALL types, they are not inherently harmful, but rather just
a momentary solid rendition of a beam after it has been traced from the turret.
These types were not exposed in the original ZZT-OOP namespace, hence the
underscore prefixes.
AMMO
This is a blocking, pushable type. The PLAYER will collect this type
upon contact, adding to the inventory.
TORCH
This is a blocking type. The PLAYER will collect this type upon
contact, adding to the inventory.
GEM
This is a blocking, pushable, squashable type. The PLAYER will collect
this type upon contact, adding to the inventory. A BULLET destroys a GEM if
fired by the PLAYER; all other BULLETs simply stop without destroying the GEM.
While ZZT does not create status element objects for GEM, Super ZZT does.
In ZZT Ultra, the status element information for GEM, if it exists, is discarded
when a board is loaded initially. To deal with the issue of replacing the
contents underneath the type upon destruction or collection, ZZT Ultra uses the
same algorithm for GEM as it uses for AMMO and other collectibles near FLOORs.
KEY
This is a blocking, pushable type. The PLAYER will collect this type
upon contact, adding to the inventory, if the inventory slot for the color
is not already exhausted. If the inventory is exhausted, the PLAYER will not
move, showing a message instead.
ENERGIZER
This is a blocking type. The PLAYER will collect this type upon contact,
setting the property ENERGIZERCYCLES to 80 and the ENERGIZED global variable
to 1. Collection of an ENERGIZER also sends #ALL:ENERGIZE.
BOULDER
This is a blocking, pushable type. There is little else to say about the
type.
SLIDERNS
This is a blocking, pushable type. The $PUSHBEHAVIOR dispatch routine
accepts only push directions heading north or south; east and west directions
are rejected.
SLIDEREW
This is a blocking, pushable type. The $PUSHBEHAVIOR dispatch routine
accepts only push directions heading east or west; north and south directions
are rejected.
DOOR
This is a blocking type. The PLAYER will cross this type if a key of the
same color is in inventory. When opening the door, the space is replaced by
EMPTY or FLOOR depending on which type was under the player earlier.
The $CUSTOMDRAW dispatch routine ensures that the DOOR is drawn with a
white foreground (15) and background matching the supposed color of the DOOR.
It is necessary to define $CUSTOMDRAW because the foreground color must match
for the purpose of #CHANGE, etc.
LINE
This is a blocking type. It resembles SOLID in functionality.
The $CUSTOMDRAW dispatch routine is complicated. If no surrounding
linking types are detected, the type is drawn as an ordinary dot. If there
are adjacent linking types, double-line drawing characters are drawn such
that anywhere from one to four directions are effectively "networked."
Linking types include LINE and BOARDEDGE.
WEB
This is a non-blocking type. It resembles FLOOR in functionality.
The $CUSTOMDRAW dispatch routine is complicated. If no surrounding
linking types are detected, the type is drawn as an ordinary dot. If there
are adjacent linking types, single-line drawing characters are drawn such
that anywhere from one to four directions are effectively "networked."
Linking types include WEB, BOARDEDGE, and either of these types if they
happen to exist under a status element. Note that the Super ZZT behavior
for linking WEB to PLAYER was somewhat inconsistent due to the
fact that updates to linked types did not always occur promptly as a
result of scrolling the screen. ZZT Ultra does not try to reproduce
"partial" web-tearing.
"Stat" Types
The following types are represented by status elements.
PASSAGE
This is a blocking, full-color, always-lit type. It resembles SOLID
in functionality. The PLAYER navigates to the board number identified
by the P3 member upon contact.
During board transition, the PLAYER will not change from the last
established position in the target board if no passage of the same color
is located there. If there is a passage of the same color, the PLAYER
starts on top of this passage.
If there are multiple passages of the same color in the target board,
the last passage in the "top-down-then-right" scan order is picked
as the destination for the player.
Super ZZT tweaks the passage destination if there is same-board passage
navigation with multiple passages of the same color within the board.
The passage picked will never be the source passage unless it is the only
one of that color in the board. Thus Super ZZT allows "two-way" passages
to proliferate within any given board.
The $CUSTOMDRAW dispatch routine ensures that a normal PASSAGE is drawn
with a white foreground (15) and background matching the supposed color of
the PASSAGE. It is necessary to define $CUSTOMDRAW because the foreground
color must match for the purpose of #CHANGE, etc.
However, drawing will be slightly different if a non-standard foreground
color is used (not 15). The original foreground color as set in the editor
is displayed in such a circumstances, UNLESS an action occurs to modify the
passage color later, in which case, the foreground color reverts to 15.
CLOCKWISE
This is a blocking type with its own defined character. It resembles
SOLID in functionality. With each iteration, this conveyor type rotates
the surrounding 8 types in a clockwise direction if possible.
Rotation only occurs for a surrounding type if two conditions are met:
- Type must be pushable.
- Destination location must be EMPTY.
The original ZZT behavior for conveyors was rather buggy. Pushables
without status element representation could be moved easily, but those with
status elements sometimes had their internal values corrupted. ZZT Ultra
does not try to reproduce these bugs (e.g. spontaneously activated bombs).
COUNTER
This is a blocking type with its own defined character. It resembles
SOLID in functionality. With each iteration, this conveyor type rotates
the surrounding 8 types in a counterclockwise direction if possible.
Rotation only occurs for a surrounding type if two conditions are met:
- Type must be pushable.
- Destination location must be EMPTY.
It is notable that COUNTER has CYCLE=2 while CLOCKWISE has CYCLE=3. This
is rarely important for board design purposes, but it is very important for
the purpose of reproducing the original behavior.
The original ZZT behavior for conveyors was rather buggy. Pushables
without status element representation could be moved easily, but those with
status elements sometimes had their internal values corrupted. ZZT Ultra
does not try to reproduce these bugs (e.g. spontaneously activated bombs).
STAR
This is a blocking projectile object with its own defined character.
With each iteration, the STAR modifies its character and color. With every
other iteration, the STAR attempts to move towards the player (#TRY SEEK).
It makes as many as 50 move attempts before it dies.
The STAR will move in a pushing fashion as it tracks the player. There
are some special collision behaviors:
- PLAYER: Damage player and die.
- BREAKABLE: Destroy breakable and die.
- WATER/LAVA: #FORCEGO to destination.
The default timeout value (P2) for STAR is actually zero, which means it remains
for a full 255 move attempts if created using #PUT or #BECOME. The #THROWSTAR
command sets P2 to 50, which is a more reasonable timeout.
BULLET
This is a blocking projectile object. With each iteration, the BULLET travels
on a straight trajectory until it hits something.
The BULLET has a wide variety of special collision behaviors:
- Non-blocking type: Move normally.
- PLAYER: Damage player and die.
- BREAKABLE: Destroy breakable and die.
- GEM: Die, destroying GEM if BULLET originated from PLAYER (P1=0).
- WATER/LAVA: #FORCEGO to destination.
- BULLET: The "BULLET hitting a BULLET" behavior is strange and
inconsistent. If the BULLET originated from the PLAYER (P1=0), the
BULLET absorbs the other BULLET and continues forward. Otherwise,
the BULLET kills both the other BULLET and itself. The reasoning
here probably has something to do with giving the user a slight
advantage in a firefight, even though it is still possible for an
enemy BULLET to cancel the PLAYER's BULLET if the iteration order
of the objects happens to allow this.
- RICOCHET: Invert direction and repeat tests in opposite direction.
- Side-aligned RICOCHETs: Rotate direction opposite RICOCHET and
repeat tests in rotated direction.
- OBJECT: Send the OBJECT the SHOT message and die.
- Killable monsters: If BULLET did not originate from the PLAYER
(P1=1), the following tests are skipped and the BULLET dies.
- HEAD: Give 1 point, kill creature, and die.
- BEAR, LION, DRAGONPUP, PAIRER, SPIDER: Give 2 points, kill
creature, and die.
- RUFFIAN, ROTON, TIGER, SEGMENT: Give 3 points, kill
creature, and die.
- All other blocking types: Die.
The evaluation of RICOCHETs cannot cycle infinitely in the highly
unusual circumstance of a bullet surrounded on every side by some
combination of RICOCHETs and other blocking types. If RICOCHET evaluation
would potentially cycle infinitely, the BULLET simply dies, instead.
For BULLETs fired by the player (P1=0), the world property CURPLAYERSHOTS
is decremented when the BULLET dies. This is compared against the board
property MAXPLAYERSHOTS when evaluating whether or not the PLAYER can shoot
for boards with an upper shot limit.
STONE
This is a blocking, pushable object. The PLAYER will collect the STONE
instead of pushing it, which adds to the "Z" inventory.
A STONE's code only displays flashing colors and uppercase letter
characters.
SCROLL
This is a blocking, pushable object. The PLAYER will collect the SCROLL
instead of pushing it.
A SCROLL has a minimal amount of default code, which is reserved for
the display of flashing colors. The SCROLL is dispatched the $DISPSCROLL
message by the PLAYER as a way to activate the custom portion of the code.
After the dispatched message is over, the PLAYER destroys the scroll.
ZZT Ultra sets $SCROLLMSG global variable to 1 if there are enough lines
within the resulting text to show a large scroll interface. The PLAYER will
not step over the SCROLL in such a circumstance, although it will still kill
the SCROLL afterwards. Conversely, if there were only enough lines to display
a toast message, the PLAYER will destroy the SCROLL immediately, replacing
the SCROLL's square.
OBJECT
This is a blocking object. The PLAYER sends it the TOUCH message upon
contact.
An OBJECT has a minimal amount of default code. The initial behavior
is to move immediately to :$REALSTART, the label signifying the start of
the custom portion of the code. This is the same location where the
#RESTART command will go to.
The $WALKBEHAVIOR dispatched message handler attempts to move in the
FLOW direction. This will always work if a non-blocking type exists in
that direction. For ZZT worlds, a blocking type in the FLOW direction
will invoke #DONEDISPATCH and jump to the THUD label if it exists.
For Super ZZT worlds, #TRY FLOW is used, which can execute push operations;
the THUD label is only jumped to if no pushing is possible.
Obviously, an OBJECT is a fairly useless type without its custom
code portion.
BOMB
This is a blocking, pushable object. The PLAYER activates a dormant
BOMB upon contact. All subsequent contact from the PLAYER pushes the
BOMB. Non-PLAYER movement pushes the BOMB without activating it.
A BOMB, once activated, ticks from 9 down to 2 at CYCLE=6. Once the
BOMB reaches 1 (not zero for some reason), it explodes. A BOMB uses the
P1 member to store the countdown status. A value of P1=0 signifies a
dormant BOMB.
When the explosion occurs, the BOMB is no longer pushable.
An explosion is characterized by the deployment of the BOMB mask
in a #FORMASK loop. Killable types are replaced with breakables of
random coloration. After a cycle of waiting, the BOMB then removes all
breakables at the same BOMB mask, replacing them with EMPTY, and it
then dies.
If the PLAYER is at a BOMB mask square at the time of the explosion,
the PLAYER takes damage.
If an OBJECT is at a BOMB mask square at the time of the explosion,
the OBJECT is sent the BOMBED message.
The following types are killed by a bomb explosion.
- EMPTY: No points.
- BREAKABLE: No points.
- GEM: No points.
- BULLET: No points.
- STAR: No points.
- HEAD: Gives 1 point.
- BEAR: Gives 1 points.
- LION: Gives 1 points.
- DRAGONPUP: Gives 2 points.
- PAIRER: Gives 2 points.
- SPIDER: Gives 2 points.
- RUFFIAN: Gives 2 points.
- TIGER: Gives 2 points.
- ROTON: Gives 3 points.
- SEGMENT: Gives 3 points.
DUPLICATOR
This is a blocking object. A DUPLICATOR imposes a cycle of (9 - P2 * 3)
as it modifies its character to a larger "circle." After the last
"circle" expansion, the DUPLICATOR captures a CLONE in the source
direction, then attempts to #PUT the CLONE in the opposite direction.
Placement is successful if either the existing object(s) can be pushed
forth to make room for the CLONE, or the destination square is
nonblocking. If unable to place the CLONE at the destination, an
alternate sound is played and nothing else happens.
There are a total of 6 idle turns between duplication attempts.
BULLET, STAR, and various enemies invoke their "collision" behaviors
when the DUPLICATOR duplicates its clone and the PLAYER is standing
directly in front of the DUPLICATOR, which both damages the PLAYER and
kills the source.
When a PASSAGE is duplicated and any PLAYER instance (real or PLAYER
clone) is standing directly in front of the DUPLICATOR, the "hacked
passage navigation" behavior is invoked. This is a quirk exploited by
some ZZT adventures, which has the effect of changing the board without
the user's direct navigation action. ZZT Ultra acknowledges this type
of hack by setting the $TELHACK global variable to an object pointer to
the PASSAGE.
BLINKWALL
This is a blocking object. A BLINKWALL periodically generates a beam
in the step direction, killing or harming certain types in its path. The
pattern is repeated in a series of on-off events.
The first part of a BLINKWALL routine is the wait until the P1 period
is finished. P1 is set to the "period" which acts as a cyclical phase
before the first beam-generation event.
Next, the beam is generated. The beam kills (and keeps going) when it
encounters the following types: GEM, BEAR, LION, TIGER, RUFFIAN, HEAD,
SEGMENT, ROTON, DRAGONPUP, PAIRER, SLIME, and BULLET. The EMPTY type is
simply overwritten by the beam.
All other types stop the beam. The PLAYER will stop the beam only if
the strike would not bump the PLAYER out of the way.
If the PLAYER is in the path of the beam, the PLAYER takes damage and
is bumped either clockwise or counterclockwise to the beam direction, based
on which square is EMPTY. If both these perpendicular directions are
blocked, the behavior will depend on the BLINKWALLBUMP property. If
BLINKWALLBUMP=0, the PLAYER takes damage but is not bumped (the beam stops
at the PLAYER). If BLINKWALLBUMP=1, the PLAYER's health is reduced to zero
instead of normal damage being taken. Note that the RESTARTONZAP board
property prevents the PLAYER from having health drained all the way,
whether or not bumping to adjacent squares would be possible.
After waiting (P2 * 2 + 1) iterations, the BLINKWALL erases its own
beam, replacing it with EMPTY. It then waits another (P2 * 2 + 1)
iterations before returning to the beam-generation event.
BEAR
This standard enemy is blocking, pushable, and squashable. BEAR moves
at cycle 3 and tracks towards the PLAYER even when the PLAYER is energized.
The "sensitivity" or P1 determines the maximum number of "minor vector"
squares away from the PLAYER the BEAR must be before a tracking movement
occurs. At P1=8, the BEAR can only track the player at a direct-aligned
condition. At P1=0, the BEAR can track the player for a "minor vector"
difference as far as 8 squares away.
When a BEAR tracks the PLAYER, it always tries to move horizontally
before trying to move vertically, if sensitivity would place the BEAR in
range of both dimensions.
If a BEAR contacts the PLAYER, the PLAYER takes damage and the BEAR
dies. If a BEAR contacts a BREAKABLE, the BEAR dies, taking the BREAKABLE
with it.
RUFFIAN
This standard enemy is blocking, pushable, and squashable. If a RUFFIAN
moves into the PLAYER, the RUFFIAN dies and damages the PLAYER.
A RUFFIAN moves at cycle 1 in fits and starts. The RUFFIAN will move in
the step direction for a loosely-regulated walking period, then switch to no
movement for another loosely-regulated resting period. This movement/resting
behavior switches based on a probability, controlled by P2, or resting time:
the probability of a change is (9 - P2) / 17.
The intelligence (P1) of a RUFFIAN determines the probability that a
change to movement would choose SEEK as opposed to a random direction:
(1 + P1) / 9.
SLIME
This standard enemy is blocking, although the PLAYER destroys a SLIME upon
contact, turning it into a BREAKABLE of the same color.
A SLIME waits (P2 * 3 + 3) turns before spreading to the four surrounding
directions, with a random drop of turns by 1 imposed as a way of staggering
fast spawn rates. Such a spread event places a new SLIME at all surrounding,
non-blocking squares, which match the rate of the current SLIME. It should be
noted that the first successfully found non-blocking square is actually moved
towards, leaving behind a BREAKABLE of the same color. In this manner, the
SLIME will actually move instead of spawning a new SLIME. For all subsequent
non-blocking squares found, a new SLIME is placed.
If a SLIME has no non-blocking squares surrounding it, it becomes a
BREAKABLE of the same color.
One quirk of the staggered iteration order for objects is that SLIMEs tend
to multiply far more quickly than usual at the highest speed when expansion
is open-ended (all directions, or moving around a corner) as opposed to
steady-state (only one direction generally available in a narrow tunnel).
The reason this occurs is that newly-placed SLIMEs may or may not be iterated
near or on the same clock cycle as the original SLIME, but this only becomes
observable as a "speed-up" when enough space exists to have a cascading
effect from so many new SLIMEs.
SHARK
This standard enemy is blocking. Unlike most other enemies, there is no
way to destroy a SHARK under most circumstances. A SHARK remains in WATER
or LAVA for all of its movements, unless it is attacking the PLAYER. If
the player is at the destination, the SHARK disappears, damaging the PLAYER.
A SHARK moves at cycle 3. The intelligence (P1) of a SHARK determines
the probability that movement would choose SEEK as opposed to a random
direction: (1 + P1) / 10.
SPINNINGGUN
This standard enemy is blocking. A SPINNINGGUN decides at cycle 2 to
fire at the PLAYER based on its P1 and P2 members. P1 controls intelligence,
which determines probability that a shot will be fired towards SEEK versus
in a random direction. P2 contains firing information, which determines
both the likelihood of a bullet being fired and the projectile type to be
fired.
The probability that the SPINNINGGUN will pick a SEEK direction is
(1 + P1) / 9. However, if a SEEK direction is picked, the gun will not
actually fire unless the PLAYER is within a minor vector of 2 squares
difference. If SEEK is not picked, the gun is free to fire in any direction.
The value of P2 is split into two different sections. The first section
(the lower 7 bits) determines the probability of firing:
(1 + (P2&127)) / 9. The second section (the highest bit) determines
the projectile: BULLET=0; STAR=1.
PUSHER
This object is blocking, and exists only to push or squash pushables in
the step direction. A PUSHER moves at cycle 4, and will push if it can,
pausing indefinitely if it cannot.
LION
This standard enemy is blocking, pushable, and squashable. A LION
moves at cycle 2. If the LION touches the PLAYER, it dies and the PLAYER
takes damage.
The intelligence (P1) of a LION determines the probability that movement
would choose SEEK as opposed to a random direction: (1 + P1) / 10.
TIGER
This standard enemy is blocking, pushable, and squashable. A TIGER
moves at cycle 2. If the TIGER touches the PLAYER, it dies and the PLAYER
takes damage.
The intelligence (P1) of a TIGER determines the probability that movement
would choose SEEK as opposed to a random direction: (1 + P1) / 10.
Unlike LIONs, TIGERs might choose to shoot instead of move based on
specific conditions. If a TIGER is within a minor vector of 2 squares
difference, the TIGER will try to shoot a projectile towards the PLAYER.
Note that this shoot direction is never wild or ENERGIZED-affected; it is
always towards the PLAYER, even for a TIGER at lowest intelligence.
If a shot can be taken, P2 is used. The value of P2 is split into
two different sections. The first section (the lower 7 bits) determines
the probability of firing versus moving: (1 + (P2&127)) / 28. The
second section (the highest bit) determines the projectile: BULLET=0;
STAR=1.
ROTON
This standard enemy is blocking, pushable, and squashable. If a ROTON
moves into the PLAYER, the ROTON dies and damages the PLAYER.
A ROTON moves at cycle 1 in an extremely agressive and jittery fashion.
The intelligence (P1) of a ROTON determines the probability that movement
would choose SEEK as opposed to a random direction: (1 + P1) / 10.
Additionally, the ROTON's direction, after being picked above, is subject
to a RNDP rotation based on the switch rate (P2). The probability of this
"rotated" movement is the following: 1 / (10 - P2).
SPIDER
This standard enemy is blocking. If a SPIDER moves into the PLAYER,
the SPIDER dies and damages the PLAYER.
A SPIDER moves at cycle 1 in an extremely agressive and jittery fashion.
The intelligence (P1) of a SPIDER determines the probability that movement
would choose SEEK as opposed to a random direction: (1 + P1) / 10.
Unlike ROTONs, SPIDERs can only move along webs or into the PLAYER. A
SPIDER will always try additional adjacent webs if it cannot move in a
previously desired direction, so it never stops moving unless it is totally
isolated (webless).
DRAGONPUP
This standard enemy is blocking, pushable, and squashable. A DRAGONPUP
only hurts the PLAYER if the PLAYER collides with it; the enemy never moves
on its own. Instead, it simply animates its character.
The editor parameters don't actually mean anything.
PAIRER
This standard enemy is blocking, pushable, and squashable. A PAIRER
only hurts the PLAYER if the PLAYER collides with it; the enemy never moves
on its own. PAIRERs appear to be an unused enemy template in Super ZZT;
they were never used in any official world.
The editor parameters don't actually mean anything.
HEAD
This standard enemy is blocking and squashable. A centipede HEAD dies
if it touches the PLAYER, damaging the PLAYER. During board initialization,
a HEAD connects to adjacent SEGMENTs to form full centipedes. ZZT Ultra
does not honor previously set linkages; it always re-links centipedes in
an automated fashion when a board is loaded for the first time.
The intelligence (P1) of a HEAD determines the probability that movement
would turn towards the PLAYER if aligned vertically or horizontally. This
turn decision is independent of the turn decision made by deviance (P2).
When aligned with the PLAYER, the probability of seeking PLAYER is
(1 + P1) / 10. Intelligence is never evaluated when the PLAYER is not
aligned with the HEAD.
Deviance determines the probability that the HEAD will spontaneously
turn in a random direction if it can. The chances of such a turn are
P2 / 25. Note that most of the time, a centipede cannot turn unless
it is in an open area.
If a HEAD becomes trapped on all sides, it performs an inversion
operation. This is characterized by each linked member flipping their
LEADER and FOLLOWER members, the last linked SEGMENT becoming a HEAD, and
the HEAD becoming a SEGMENT. The last transmuted SEGMENT selects the
opposite of the last direction it had moved as its initial direction as
a new HEAD.
When a HEAD moves, it drags all connected SEGMENTs with it, such that
each SEGMENT replaces the position of the one in front of it.
When a HEAD hits the PLAYER when it has connected SEGMENTs, the SEGMENT
after it becomes a new HEAD immediately.
SEGMENT
This standard enemy is blocking and squashable. A centipede SEGMENT
does not actually move on its own; it is moved indirectly by a connected
HEAD.
A SEGMENT performs a periodic "keep alive" check for its LEADER. If it
finds that the LEADER member is no longer valid, it becomes a new HEAD,
with an initial direction of the last direction it had moved. Additionally,
the SEGMENT will attempt to link to other unattached SEGMENTs if its own
FOLLOWER is not valid.
TRANSPORTER
This type is blocking and "pushable" in a special way. A TRANSPORTER's
$PUSHBEHAVIOR routine performs a complex evaluation of pushability as a way
of determining the next available push evaluation location. The only action
directly executed by the TRANSPORTER is a character animation cycle.
There are two dispatched message handlers that TRANSPORTER supports:
the $PUSHBEHAVIOR handler, which is well-known, and the FINDDEST handler,
which is used only by the PLAYER when locating a specific warp destination.
Both of these routines perform roughly the same battery of tests, but
FINDDEST stops when it finds a destination as opposed to the complex, nested
evaluation required by $PUSHBEHAVIOR.
When FINDDEST is dispatched, the global variables $X and $Y are set to
the valid destination coordinates if navigation through the TRANSPORTER is
possible. If it is not possible, these variables are instead set to -1, -1.
The $PUSHBEHAVIOR handler leaves $PUSH at zero if pushing through the
TRANSPORTER is deemed impossible. If pushing is deemed possible, $PUSH is set
to 3, with $PUSHDESTX and $PUSHDESTY set to the immediate destination
coordinates for objects pushed through the TRANSPORTER. $PUSHDIR is not
modified as part of the evaluation.
When testing for navigation, the first step is to identify if the push
direction matches the TRANSPORTER direction. If the condition is not met,
the routine returns immediately with failure.
The next step is a test for blocked condition in the square
just beyond the TRANSPORTER. If not blocked, return with success.
The next step is a test for pushability in the square just beyond the
TRANSPORTER. Note that it is safe to invoke the conditional SAFEPUSH1 from
inside this handler because ZZT Ultra preserves and restores values of $PUSH,
$PUSHDIR, $PUSHDESTX, and $PUSHDESTY. If the square is pushable, return
with success.
The next step locates the nearest opposite-facing TRANSPORTER. If no
such TRANSPORTER can be found, return with failure.
If an opposite-facing TRANSPORTER is found, go back to the "test for
blocked condition" step and repeat the process as many times as necessary
until either a valid destination is reached or no open opposite-facing
TRANSPORTER can be found.
PLAYER
We have saved the best for last. The PLAYER is blocking and pushable,
and it has extremely complicated code.
ZZT Ultra locates and remembers the active PLAYER object whenever
switching to a new board. Thus, the $PLAYER global variable will always
point to a valid PLAYER in the board.
A single iteration of the PLAYER's object has it checking for
a wide variety of conditions, flags, modes, etc. This is because the
PLAYER may or may not be free to perform many different types of tasks,
movements, and damage-taking reactions based on whether a title screen
is shown, whether the game is paused, whether the PLAYER is dead, and
other conditions.
The PLAYER's behavior, as implemented in ZZT Ultra, is summarized in the
following "pseudocode" lines.
- If $MUSTRESTART set, the PLAYER is warped back to the entry square
saved in board properties PLAYERENTERX and PLAYERENTERY, whereupon the
game is paused. $MUSTRESTART is then cleared.
- $PMOVESOUND is set to 1.
- If $PLAYERMODE is set to 3 ("ZZT title-screen mode"), done with iteration.
- If the property ENERGIZERCYCLES is zero, the ENERGIZED flag is cleared, and
the PLAYER is vulnerable again.
- If the property ENERGIZERCYCLES is above zero, the counter is decreased by
one, and the PLAYER's character is animated.
- If the property ENERGIZERCYCLES reaches 10, the "ending energizer" sound is
played, interrupting the standard energizer music.
- If $PLAYERMODE is set to 2 ("Dead mode"), go to the DEADLOOP.
- If $PLAYERMODE is 3 or 4 (one of the "title screen" modes), done with iteration.
- If $TORCHCYCLES is 1, darken the area around the player with the TORCH
mask, play the torch-out sound, and hide the torch progress bar.
- If $TORCHCYCLES is above zero, the counter is decreased by one, and the
torch progress bar is updated.
- If $INITDIR is set, the movement direction (.DIR1) is posted from $INITDIR,
and the movement in this direction is attempted (go to DOMOVE).
- Read the input using #PLAYERINPUT. If a shot was fired (.DIR2 posted),
go to DOSHOOT. If a move was taken (.DIR1 posted), go to DOMOVE.
- If the board is dark, display the "torch needed" status message if it
had not been displayed yet.
- Done with iteration.
- $WALKBEHAVIOR dispatch handler: This handler does not actually use
the preexisting step direction; it instead sets .DIR1 if a WATERE,
WATERS, WATERW, or WATERN type exists under the PLAYER. If no
such type exists under the PLAYER, the handler just returns. If
there is such a type, #DISPATCHDONE is called, $PMOVESOUND is set to 0,
and control jumps to DOMOVE.
- DOMOVE label: This routine checks the move-to type to decide what
to do.
- GEM: Go to GETGEM.
- AMMO: Go to GETAMMO.
- TORCH: Go to GETTORCH.
- ENERGIZER: Go to GETENERGIZER.
- STONE: Go to GETSTONE.
- KEY: Go to GETKEY.
- FAKE: Go to MOVEFAKE.
- Blocking square: Go to MOVEBLOCKED.
- If none of the above types or conditions are satisfied, the PLAYER
takes an ordinary move to the next square. The camera chases the PLAYER
(for Super ZZT) and the TORCH mask is also moved to chase the PLAYER.
- LEAVEPASSAGE label: This code handles the generic passage-leaving
update to the old PLAYER square after movement is finished. If the PLAYER
had just left a PASSAGE ($PASSAGEEMERGE is set to 1), the old square is
replaced by a passage of the correct color and P3 destination. Nothing
special happens in terms of replacement if $PASSAGEEMERGE is set to zero.
$PASSAGEEMERGE is always set to zero as part of this code, and then the
iteration is done.
- MOVEBLOCKED label: This routine checks the move-to type, which is
definitely blocking, to decide what to do.
- BULLET, STAR: Go to MAYBEMOVEHURT.
- BEAR, RUFFIAN, LION, TIGER, HEAD, SEGMENT, ROTON, DRAGONPUP,
PAIRER, SPIDER: Go to MOVEHURT.
- SLIME: Go to KILLSLIME.
- INVISIBLE: Go to INVISBLOCKED.
- LAVA: Go to LAVABLOCKED.
- FOREST: Go to MOVEFOREST.
- BOARDEDGE: Go to EDGENAV.
- PASSAGE: Go to PASSAGENAV.
- DOOR: Go to MOVEDOOR.
- SCROLL: Go to MOVESCROLL.
- OBJECT: Go to OBJECTTOUCH.
- TRANSPORTER: Go to MOVETRANSPORTER.
- BOMB: Go to MOVEBOMB.
- If none of the above types or conditions are satisfied, test if the
square is pushable. If nonblocking, move normally and end the iteration.
If not pushable, play the push sound anyway for boulders and sliders and
end the iteration. If so, proceed with the push attempt (go to DOPUSH
label).
- DOPUSH label: Play push sound, move PLAYER, adjust camera and TORCH
mask aura, and go to LEAVEPASSAGE.
- COLLECTMOVE label: This is a general "collection-replacement" routine
for establishing what to put under the square after the PLAYER leaves the
square previously occupied by the item. The idea is that EMPTY is
always left (with the same color as the picked-up item) if the current
.UNDERID for the PLAYER is EMPTY. However, the current .UNDERID for
the PLAYER, if not EMPTY, is instead placed with the current .UNDERCOLOR.
This effectively "paints" the nearest floor tile for AMMO, GEM, etc. After
the placement is done, adjust camera and TORCH mask aura, and go to
LEAVEPASSAGE.
- GETGEM label: Display gem-collection message if never seen before,
increase GEMS property by 1, add to HEALTH property by GEMHEALTH config
property, play collection sound, and go to COLLECTMOVE.
- GETAMMO label: Display ammo-collection message if never seen before,
increase AMMO property by 1, play collection sound, and go to COLLECTMOVE.
- GETTORCH label: Display torch-collection message if never seen before,
increase TORCHES property by 1, play collection sound, and go to COLLECTMOVE.
- GETENERGIZER label: Display energizer-collection message if never seen
before, set ENERGIZERCYCLES to 80, set ENERGIZED flag, play energizer sound,
and go to COLLECTMOVE.
- GETSTONE label: Display stone-collection message, increase Z property
by 1, play collection sound, and go to COLLECTMOVE.
- GETKEY label: Establish foreground color of key, then test inventory
to see if the $KEYNLIMIT config property will allow inventory to hold one
more key of this color. The inventory property that stores the key is KEY$$COL,
where $COL is set to the foreground color of the key (a number from 0 to 15).
For example, KEY12 represents a RED KEY.
- If the key inventory is full for that color (default config setting only
allows a maximum of 1), no move will occur. A message is displayed, a sound
indicating max reached is played, and the iteration ends.
- If the key inventory would allow collection, inventory is updated, a
message is displayed, a collection sound is played,
allows a maximum of 1), no move will occur. A message is displayed, a sound
indicating max reached is played, and control goes to COLLECTMOVE.
- OBJECTTOUCH label: This is very straightforward. The OBJECT at the
destination is sent the TOUCH message. The iteration ends.
- KILLSLIME label: This is very straightforward. A "die" sound effect
is played, and the SLIME at the destination is changed to a BREAKABLE.
The iteration ends.
- MAYBEMOVEHURT label: For BULLETs and STARs, the PLAYER will overrun
and take damage only if the projectile is not on top of LAVA/WATER. If
the projectile is on top of LAVA/WATER, no movement occurs (the iteration
ends). Otherwise, go to MOVEHURT.
- MOVEHURT label: This is a generalized routine for a potentially
damage-inducing move into a harmful projectile or enemy.
- If $PLAYERMODE is not 1, movement occurs to the destination, killing
the object at the destination, without any damage to the PLAYER. Go to
LEAVEPASSAGE.
- If ENERGIZED, movement into enemies and projectiles kills them without
damage being taken, and points are awarded based on the type. Move PLAYER,
adjust camera and TORCH mask aura, and go to LEAVEPASSAGE.
- At this point, the PLAYER takes damage according to the PLAYERDAMAGE
config property. If this would drain the HEALTH property to zero or lower,
go to LASTMOVEDIE. Otherwise, play hurt sound, reset TIME board property,
set $MUSTRESTART to 1 if RESTARTONZAP board property is 1, move PLAYER,
adjust camera and TORCH mask aura, and go to LEAVEPASSAGE.
- At this point, the PLAYER takes damage according to the PLAYERDAMAGE
config property. If this would drain the HEALTH property to zero or lower,
go to LASTMOVEDIE. Otherwise, play hurt sound, reset TIME board property,
set $MUSTRESTART to 1 if RESTARTONZAP board property is 1, move PLAYER,
adjust camera and TORCH mask aura, and go to LEAVEPASSAGE.
- $RECVHURT dispatch handler: This message is dispatched when some
remote action hurts the PLAYER, such as running out of time, BULLET hit, or
BLINKWALL zap.
- If $PLAYERMODE is not 1, return.
- If $MUSTRESTART is 1, return.
- If ENERGIZED, return.
- At this point, the PLAYER takes damage according to the PLAYERDAMAGE
config property. If this would drain the HEALTH property to zero or lower,
go to LASTMOVEDIE. Otherwise, play hurt sound, reset TIME board property,
set $MUSTRESTART to 1 if RESTARTONZAP board property is 1, and return.
- LASTMOVEDIE label: Play "game over" sound, get out of dispatch mode,
set GAMESPEED property to maximum (zero), set $PLAYERMODE to 2 (dead mode).
- DEADLOOP label: Show "game over" message and end iteration.
- INVISBLOCKED label: Show message indicating blockage, change the
touched INVISIBLE to NORMAL, play sound, and end iteration.
- LAVABLOCKED label: Show message indicating blockage, play sound,
and end iteration.
- MOVEFAKE label: Show "fake wall" message if never shown before,
move PLAYER, adjust camera and TORCH mask aura, and go to LEAVEPASSAGE.
- MOVEFOREST label: Show "forest" message if never shown before, move
PLAYER, play sound, adjust camera and TORCH mask aura, and go to LEAVEPASSAGE.
The type dropped under the FOREST, as well as the sound played, will vary
depending on whether or not the game is ZZT or Super ZZT. ZZT drops EMPTY
and plays a single sound, while Super ZZT drops FLOOR and plays one of eight
tones cycled in a round-robin fashion.
- MOVEDOOR label: Test if inventory for a matching key color exists. If
there is a match, decrease the appropriate property by 1, display message,
and go to COLLECTMOVE. If no match, show a different "locked" message, play
a different sound, and end iteration.
- EDGENAV label: Establish source and destination boards. Nothing happens
(ending iteration) if there is no board linkage in the direction of
navigation.
- Update the source board's PLAYERENTERX and PLAYERENTERY properties; these
form the new starting position for zap-reentry.
- Remove the TORCH mask aura if it exists.
- Change the board to the destination. Dispatch to the main type code the
EVALMOVEXY message, which returns the following codes:
- $RESULT=0: Can definitely move to destination square.
- $RESULT=1: Might be able to move to destination square.
- $RESULT=2: Definitely cannot move to destination square.
- If unable to move to destination square, change board back to source,
re-instate TORCH mask aura if present, and end iteration.
- If able to move to destination square, reset TIME to zero, set PLAYER
location in destination to destination square, update PLAYERENTERX and
PLAYERENTERY board properties to destination square, impose TORCH mask
aura if applicable, and invoke board-to-board transition effect. Update
time-oriented GUI label and end iteration.
- PASSAGENAV label: Establish source and destination boards. The
passage P3 member contains the destination board number. Play passage-nav
sound. Remember passage color.
- Update the source board's PLAYERENTERX and PLAYERENTERY properties; these
form the new starting position for zap-reentry.
- Remove the TORCH mask aura if it exists.
- Change the board to the destination. Scan through all status
elements in the destination until a same-color passage is found. If
one is found, set $PASSAGEEMERGE to 1, and start the PLAYER on that spot.
If one is not found, do not move the PLAYER at the destination.
- Reset TIME to zero, update PLAYERENTERX and PLAYERENTERY board
properties to destination square, impose TORCH mask aura if applicable,
and invoke board-to-board transition effect. Update time-oriented GUI
label, pause the game, and end iteration.
- MOVESCROLL label: Play scroll sound, then dispatch the $DISPSCROLL
message to the SCROLL's code.
- If the variable $SCROLLMSG is set to 1, end iteration. The
immediately following action by the PLAYER will be a replacement of the
destination square with EMPTY (but not a movement to this destination).
This only happens after the user closes the scroll interface.
- If the variable $SCROLLMSG is set to 0, immediately replace the
destination with EMPTY, then move to it (the message from the SCROLL
will be a simple toast message). Adjust camera and TORCH mask aura,
and go to LEAVEPASSAGE.
- MOVEBOMB label: Check if bomb is activated. If not, dispatch the
ACTIVATE message to the bomb, then end iteration. If activated already,
go to DOPUSH.
- DOSHOOT label: This is the main handler for the player's shots.
- If no shots can be fired on the board, show a message if never shown
before, then end iteration.
- If a shot fired would exceed the number of shots allowable for the
board, end iteration.
- If no AMMO is left, show a message if never shown before, then end
iteration.
- If not blocked in firing direction, create a new "player-owned" BULLET
that flies in the firing direction. Deduct AMMO property by 1, play sound,
and end iteration.
- If blocked in firing direction, evaluate the type. This "point-blank
behavior" needs to take into account a variety of different firing contexts,
as "shot-hit" behavior needs to happen despite the fact that a BULLET is
not often created at this range.
- LAVA/WATER: Forcibly generate the BULLET anyway; go to DOSHOOT.
- GEM, BREAKABLE: Go to ELIMNEXT.
- OBJECT: Go to SENDSHOT.
- BULLET: Go to ELIMBULLET.
- HEAD, SEGMENT, BEAR, LION, TIGER, DRAGONPUP, PAIRER, SPIDER, ROTON, RUFFIAN:
Go to ELIMENEMY.
- All else: End iteration; no shot is fired.
- SENDSHOT label: If the config property POINTBLANKFIRING
is set to 1, the OBJECT is sent the SHOT message and AMMO is reduced by 1.
Note the original ZZT didn't allow for this. End iteration.
- ELIMNEXT label: Reduce AMMO by 1. Remove type, play breakable-hit sound,
and end iteration.
- ELIMBULLET label: Reduce AMMO by 1. Update overall BULLET count for board,
remove type, play breakable-hit sound, and end iteration.
- ELIMENEMY label: Reduce AMMO by 1. Dispatch remote-death message to enemy,
remove type, play enemy-death sound, and end iteration.
- MOVETRANSPORTER label: Dispatch FINDDEST to the TRANSPORTER. If the
values of $X and $Y come back -1, no navigation is possible (end iteration).
If navigation is possible, play transporter sound, update new PLAYER position,
adjust camera and TORCH mask aura, and end iteration.
GUI Behavior
All the original GUI templates are stored within zzt_guis.txt,
which ZZT Ultra loads automatically. The GUIs have the following names:
- ZZTTITLE: The "ZZT" title screen right-hand GUI.
- ZZTGAME: The "ZZT" main game right-hand GUI.
- PROVING: The "Super ZZT" full-screen title GUI for Proving Grounds.
- FOREST: The "Super ZZT" full-screen title GUI for Lost Forest.
- MONSTER: The "Super ZZT" full-screen title GUI for Monster Zoo.
- SZTINTRO: The "Super ZZT" intro screen GUI frame.
- SZTGAME: The "Super ZZT" main game GUI frame.
The GUI labeling system and key input system are fundamentally attached
to the active GUI itself, which supports all the key presses and momentary
updates in the original user interfaces for ZZT for Super ZZT.
ZZT Ultra also uses seamless changes when a new GUI is displayed. Moving
between 40-column and 80-column mode is instantaneous.
When a GUI is drawn, the viewport (the portion allocated to showing gridded
data for the board) is not updated automatically. An update or transition
effect will need to occur to make this shown, such as #UPDATEVIEWPORT,
#DISSOLVEVIEWPORT, or #SCROLLTOVISUALS.
Most of the momentary GUI updates are implemented in the form of GUI
labels, to which one of three things can be drawn: text, pens, and bars.
See #SETGUILABEL, #DRAWPEN, and #DRAWBAR for more information.
ZZT Ultra does not support the original editor GUIs. The rationale for
this decision is the basic difference in architecture with ZZT Ultra--the
editor GUIs that are supported reflect new capabilities as opposed to old
capabilities.
Timing Issues
The timing for the original ZZT games relied upon the slowest setting of
the internal clock to time the gameplay iterations. This value was 65536,
which, when figured into the internal clock's frequency-calculation formula,
gives us 1193180 / 65536 = 18.2 Hz.
There were only four speeds implemented in practice in ZZT: 6.067 Hz (the
"slow" throttled speed), 9.1 Hz (the "normal" throttled speed), 18.2 (the "fast"
throttled speed), and an unthrottled "fastest" speed. The default setting for
ZZT was 9.1 Hz for gameplay iterations (what #CYCLE 1 would yield). For Super
ZZT, there was no speed control setting available in the GUIs, which made 9.1 Hz
the only speed for that engine.
The "nine-speed pen selection" for ZZT is highly misleading. The reason only
four speeds were implemented in practice? Likely, the delay function used in the
original program did not entertain the idea that the timer had limited resolution.
If a timer is only resolute to 18.2 Hz, interstitial speeds are not possible and
end up being rounded as multiples of the resolution's interval (in this case,
1 / 18.2 = 0.05494505 seconds). If one needs an interval at, say, 0.025 seconds,
the engine would not have been able to support it.
Something that has caused a lot of confusion in attempts to re-create ZZT on
non-DOS platforms: the order in which the status elements are iterated. All
objects are stored in a buffer, which increases in size when new objects are
created (e.g. bullets fired), and decreases in size when objects are destroyed
(e.g. enemies killed). Of primary importance is what happens to the profile of
the buffer when an object is removed (dies): is everything "moved down" once
to fill the gap, or is the last status element relocated to the gap?
The behavior of ZZT and Super ZZT appears to be such that status elements
created earlier in the timeline have lower iteration order than those created
later. This implies that the buffer used did not leave "empty holes" after
object destruction, but in fact shrunk the buffer using end-move or
tail-relocation.
In ZZT Ultra, a compromise solution is picked. Removed status elements are
kept in the buffer until the entire iteration is over for all status elements
on that turn, with only a few exceptions. But there is also another ID-based
tracking system, which assigns a constantly-increasing unique identifier to
all status elements as they are created. This ends up having the same effect
when scanning through existing objects and creating new ones.
The "exceptions" only apply to various visual effects that must be handled
immediately: scroll interfaces and screen transition effects. The reason for
the exceptions is that some actions change the overall game mode away from the
iteration of real-time objects, so iteration cannot continue for the remainder
of the turn.
There is a property, LEGACYTICK, which queues the delays between objects
somewhat differently than the counter internally kept by each object. If the
property is 1, the original ZZT modulo of 420 is used to queue when an object
iterates using a special formula:
(tick % CYCLE == indexInSEBuffer % CYCLE)
The tick increases from 1 to 420, and it ends up iterating objects in a very
specific order that some ZZT worlds rely upon. Those objects with a CYCLE of
zero are never iterated.
ZZT-OOP Script Processing
ZZT-OOP syntax and command behavior seem simple on the surface. In reality,
though, the ZZT and Super ZZT behavior had many fine details that require
great precision during interpretation. Various bugs and features of ZZT-OOP's
original operation are reproduced in ZZT Ultra out of necessity, because so
many ZZT and Super ZZT games have been made that rely upon these quirks.
$WALKBEHAVIOR iteration frequency
When an OBJECT is subject to movement on its #WALK iteration, the movement
will only occur as frequently as the cycle. The $WALKBEHAVIOR message is
dispatched after successful execution of the object's main iteration. Thusly,
an OBJECT that walks east at cycle 3 would spend 2 cycles waiting for every
1 cycle moving east.
Turn limits based on object command count
The original ZZT-OOP had a mechanism for ending turns early even if movement
commands were not present. ZZT Ultra honors this mechanism only for types with
the HASOWNCODE attribute set. For all other types, no such mechanism
applies (although the property DETECTSCRIPTDEADLOCK can still catch
infinite looping behavior).
The "magic number" for ending turns early is 32. The following commands
count towards this number. If 32 is reached, the turn ends. The OBJMAGICNUMBER
property can modify this number.
- WALK
- ENDGAME
- END
- RESTART
- LOCK
- UNLOCK
- BIND
- SEND
- PUT
- SHOOT
- THROWSTAR
- CHANGE
- CHAR
- CYCLE
- SET
- CLEAR
- GIVE
- TAKE
- ZAP
- RESTORE
- IF
- PLAY
Some movement commands unequivocally end the turn immediately:
- GO
- FORCEGO
- TRY (if move successful)
- /
- ?
Text display immediately preceding #DIE or #BECOME
ZZT Ultra does not conclusively remove status elements from the system
until the turns are over for all objects for the frame. Of course, the
presence of a large scroll interface displayed with potential links means
that it is not appropriate to "kill" an object if its links would save it
from dying.
If #DIE or #BECOME appears in code immediately after a large text
interface must be shown, turns are ended just before the #DIE or #BECOME.
If no links are followed, the #DIE or #BECOME command is handled later,
during a future frame.
#BIND action subtlety
When a #BIND statement is encountered, ONAME member is deleted if it had
existed, instruction pointer is moved to the starting point for the new code,
and turns continue into the #RESTART position for the new code (there is no
inherent end of turn).
Label optimization
ZZT Ultra optimizes some label navigation operations at run-time. This
includes #SEND messages to self, #DISPATCH messages to the main type code,
and #SWITCHTYPE and #SWITCHVALUE labels. Optimization will not occur for
sent or dispatched messages to other non-specific types, and will not occur
for any type with the HASOWNCODE attribute.
#SEND messages to object names
When sending messages to all objects matching an object name (or ALL, or
OTHERS), the lock status is respected for the target, unless the
sender and receiver objects are identical. Thus an object could call
#SEND ALL:stuff and still jump to the stuff message, even if locked.
Validity of coordinate pairs when applied to board edge
The following contexts do not allow the board edge (X=0, X=SIZEX, Y=0, or
Y=SIZEY) to be referenced:
- PUSHATPOS: Always returns false for border
- SPAWN: Reports invalid coordinates
- SHOOT: No shot taken
- THROWSTAR: No shot taken
- OBJAT: Returns -1
- LIGHTEN: Has no effect
- DARKEN: Has no effect
- CLONE: Does not change CLONE type
- SETREGION: Does not change region
The following contexts do allow the board edge (X=0, X=SIZEX, Y=0, or
Y=SIZEY) to be referenced:
- SETPOS: It is generally a bad idea to place an object on the
border, but it can be useful. Objects can move from the board edge to
an adjacent on-board square as a way of quickly "unhiding" them.
- TYPEAT: Returns BOARDEDGE
- COLORAT: Returns color; generally not useful
- LITAT: Returns lit status; generally not useful
- ANYTO: Evaluates BOARDEDGE type
- TYPEIS: Evaluates BOARDEDGE type
- BLOCKEDAT: Evaluates BOARDEDGE type
Special inherent push behavior
When movement commands execute push operations, there are many "gray
areas" where pushing needs to be reconciled against types, stat/non-stat
presence, squashing, etc.
The $ALLPUSH global variable governs point-blank squashing
behavior for the commands /, ?, #GO, and #TRY. Setting the global variable
to 1 allows squashing for all squares, including the type at point-blank
range, while setting this to 0 allows squashing for all squares except for
the type at point-blank range. Note that this condition is only checked if
squashing needs to happen; these commands only squash if they have no
movement clearance for pushing without squashing.
No provision is made to use $ALLPUSH for the command #PUSHATPOS.
Status elements inherently violate the pushing rules when pushed over
types such as FLOOR. Because a status element can exist "above"
another type as it moves over it, the combination of (not BLOCKOBJECT) +
(not PUSHABLE) is treated as if it were (not BLOCKOBJECT) + (PUSHABLE).
Primarily, this is used to ensure that PLAYER and other objects can be
pushed over FLOOR and WATERn.
ZZT Ultra's push test loop and push operation loop have a failsafe
maximum of 1000 iterations. Typical board extents are far smaller than
this length. The purpose of the failsafe is to prevent infinite cycling
in case a $PUSHBEHAVIOR handler fails to advance properly.
The FLAGS member
It is rare that the FLAGS member of an object will need to be read, since
flags are normally relevant only to the engine itself. If one needs to read
the flags, they are interpreted as follows:
- FL_IDLE=1: Set if object is in an idle state.
- FL_LOCKED=2: Set if object is in a locked state.
- FL_PENDINGDEAD=4: Set if an object is about to be
destroyed, but cannot be destroyed immediately due to scroll interface
text that immediately precedes a #DIE, etc.
- FL_DEAD=8: Set if object was destroyed, but is still
within the status element buffer in preparation for removal at the end
of turns for the game tick iteration.
- FL_GHOST=16: Set if object has "ghost" status, remaining in
the board without being linked to the grid itself.
- FL_NOSTAT=32: Set if object has "statless" status, a special
state used to maintain compatibility with ZZT types that are supposed to
have status elements, but have stat information removed in the world file.
An object with FL_NOSTAT is not counted as part of the status element
total for the board, and it is generally used in conjunction with FL_LOCKED
and FL_IDLE.
- FL_DISPATCH=64: Set if the object is currently in a
dispatched message handler. Note that this flag is cleared if
#DONEDISPATCH is called.
- FL_UNDERLAYER=128: Set if the object has "under-layer" status.
This is used in conjunction with FL_GHOST when an object is forced under
another object via the UNDER or OVER pseudo-directions. When there is
no other object at the square, FL_UNDERLAYER and FL_GHOST are automatically
cleared and the object resumes normal execution.
#ZAP detailed label-finding behavior
The general behavior for #ZAP is to find the first matching label and
change it to a comment.
In reality, the label-finding behavior was extremely buggy in the original
ZZT engines, and only partially deterministic. Reproducing the original ZZT
behavior for #ZAP of labels is unfortunately a very difficult task, because
the bugs must be reproduced exactly. Failure to have the bugs present will
make worlds unplayable.
When searching for labels from #ZAP, an exact string match (case
insensitive) will always count. However, it is also possible for an inexact
match to count, as well. If the tested label is longer than the checked text,
it will still count as a match if:
- testedLabel.substr(0, len(checkedText)) matches checkedText
- The character located at testedLabel[len(checkedText)] is not alpha
or underscore
Thus, #ZAP abc would match not just the label :abc, but also
:abc1, :abc123, :abc9876, etc.
#RESTORE detailed comment-finding behavior
The general behavior for #RESTORE is to find all comments that match a
similar pattern and change them to labels, with specific early-out conditions
for the restoration loop.
In reality, the comment-finding behavior was extremely buggy in the original
ZZT engines, and only partially deterministic. Reproducing the original ZZT
behavior for #RESTORE of labels is unfortunately a very difficult task, because
the bugs must be reproduced exactly. Failure to have the bugs present will
make worlds unplayable.
When searching for comments from #RESTORE, the results will depend heavily on
the characteristics of the loop iteration. For example, the first iteration has
relatively simple logic, but subsequent iterations change the logic significantly.
The reason for this was ZZT's failure to secure separate string copying buffers
for tested source and destination strings, resulting in implicit overwrites that
the author likely never intended.
For iteration #1 of the restoration loop, restoration works more or less as
the inverse of #ZAP. A case-insensitive exact match results in restoration, while
a "starts with text" match is governed by the following rules:
- testedComment.substr(0, len(checkedText)) matches checkedText
- The character located at testedComment[len(checkedText)] is not alpha
or underscore
For iterations #2 and up, is the same as iteration #1 with one notable difference.
If the ZZT-OOP line immediately following the comment line is alphanumeric text,
the label is not restored, and the restoration loop early-outs.
Thus, the apparent lack of consistency is actually consistent, after all, but in
ways that most people would never be able to guess on their own. For this reason,
it is strongly advised that future ZZT-OOP programming not rely upon too-similar
zapped labels or labels that contain numbers.
|
|