Zombie: Difference between revisions
Piranhaplant (talk | contribs) No edit summary |
Piranhaplant (talk | contribs) No edit summary |
||
(17 intermediate revisions by 2 users not shown) | |||
Line 1: | Line 1: | ||
{{Infobox Monster |
{{Infobox Monster |
||
|img=Zombie.png |
|||
{{Infobox Entity|entity_pointer=$81:87F8 (normal)<br/>$81:88CA (hard)}} |
|||
|hp=1 |
|||
The zombie is a [[respawning monster]]. There are two different types of zombies: normal and hard. They behave mostly the same, but there are slight differences that make the hard version more aggressive. |
|||
|points=100 |
|||
|special=- |
|||
|resists=- |
|||
|weak_to=- |
|||
}} |
|||
{{Infobox Entity |
|||
|entity_pointer=$81:87F8 |
|||
|pointer1_type=normal |
|||
|entity_pointer2=$81:88CA |
|||
|pointer2_type=hard |
|||
}} |
|||
The zombie is a [[respawning monster]]. Zombies attack via contact damage, have no special powers and aren't resistant or vulnerable to any [[weapon]]. They can be found in all [[tileset]]s, only have 1 hit point and award 100 points to the player when killed. There are two different types of zombies: normal and hard. They behave mostly the same, but there are slight differences that make the hard version more aggressive. |
|||
== Behavior == |
== Behavior == |
||
Line 10: | Line 24: | ||
* '''Targeting:''' Move towards the nearest targetable sprite. A zombie enters this state when it comes within a certain distance of a targetable sprite. The zombie will leave this state and go back to the wandering state if there is no longer a targetable sprite within another (slightly larger) distance. |
* '''Targeting:''' Move towards the nearest targetable sprite. A zombie enters this state when it comes within a certain distance of a targetable sprite. The zombie will leave this state and go back to the wandering state if there is no longer a targetable sprite within another (slightly larger) distance. |
||
{{BPMN_embed|BPMN:Zombie state diagram}} |
{{BPMN_embed|BPMN:Zombie state diagram|288}} |
||
Zombies will despawn if they are a distance of 208 pixels or more on a single axis from the closest player, and are not within target range of any targetable sprites. The SNES screen has a width of 256 pixels, so this means it is possible for a zombie to despawn while still on screen if all players are on the very left or right of the screen and the zombie is on the opposite side. |
Zombies will despawn if they are a distance of 208 pixels or more on a single axis from the closest player, and are not within target range of any targetable sprites. The SNES screen has a width of 256 pixels, so this means it is possible for a zombie to despawn while still on screen if all players are on the very left or right of the screen and the zombie is on the opposite side. |
||
== Differences in hard version == |
=== Differences in hard version === |
||
==== Major differences ==== |
|||
[[File:Zombie follow wall comparison.gif|frame|Normal (left) vs. hard (right) targeting through a wall]] |
|||
* Start targeting at distance of 159 pixels (instead of 64 pixels for normal). They continue targeting while there is a target within 179 pixels (instead of 69 pixels for normal). |
* Start targeting at distance of 159 pixels (instead of 64 pixels for normal). They continue targeting while there is a target within 179 pixels (instead of 69 pixels for normal). |
||
* Moves at a speed of 1 px/frame when wandering and following wall (instead of 0.5 px/frame for normal). The speed when targeting is 1 px/frame for both versions. |
* Moves at a speed of 1 px/frame when wandering and following wall (instead of 0.5 px/frame for normal). The speed when targeting is 1 px/frame for both versions. |
||
==== Minor differences ==== |
|||
[[File:Zombie follow wall comparison.gif|class=notpageimage|frame|Normal (left) vs. hard (right) targeting through a wall]] |
|||
* Position update logic is different when targeting. When targeting through a wall, normal zombies will get stuck, but the hard version will still follow along the wall. |
* Position update logic is different when targeting. When targeting through a wall, normal zombies will get stuck, but the hard version will still follow along the wall. |
||
* The angle to rotate when following a wall is stored in a variable instead of being hardcoded. However, this variable is never initialized, so it will retain whatever value happened to be in RAM previously. This generally results in the zombie sticking to the wall instead of following it. |
* The angle to rotate when following a wall is stored in a variable instead of being hardcoded. However, this variable is never initialized, so it will retain whatever value happened to be in RAM previously. This generally results in the zombie sticking to the wall instead of following it. |
||
Line 27: | Line 46: | ||
Zombies have a four frame animation cycle, with 8 frames in between each animation frame. There is a separate set of animation frames for each of the 4 orthogonal directions, chosen based on which direction the zombie is moving. If the zombie is moving diagonal, it uses the left or right animation. |
Zombies have a four frame animation cycle, with 8 frames in between each animation frame. There is a separate set of animation frames for each of the 4 orthogonal directions, chosen based on which direction the zombie is moving. If the zombie is moving diagonal, it uses the left or right animation. |
||
== Bugs == |
|||
* Hard zombies do not generally follow walls like they should. See [[#Differences in hard version|Differences in hard version]]. |
|||
* The hard zombies have a partially implemented feature that appears to be something that would make them stop targeting for a certain amount of time. In its incomplete state, it generally does not affect their behavior, but if a zombie stays following a wall for around 9 minutes, the timer will underflow. This will cause the zombie to stop targeting for around another 9 minutes until the timer reaches zero again. |
|||
== Trivia == |
|||
*With 152 spawners for normal zombies and 127 spawners for hard zombies, this is the most common monster that players will face in the game. |
|||
== RAM map == |
== RAM map == |
||
Line 34: | Line 62: | ||
{| class="wikitable" |
{| class="wikitable" |
||
|- |
|- |
||
! Address !! Length !! [[Data types|Type]] !! Description |
! Address !! Length !! [[Data types|Type]] !! Name !! Description |
||
|- |
|- |
||
| $00 || 2 || int16 || X position |
| $00 || 2 || int16 || x || X position |
||
|- |
|- |
||
| $02 || 2 || int16 || Y position |
| $02 || 2 || int16 || y || Y position |
||
|} |
|} |
||
Line 45: | Line 73: | ||
{| class="wikitable" |
{| class="wikitable" |
||
|- |
|- |
||
! Address !! Length !! [[Data types|Type]] !! Description |
! Address !! Length !! [[Data types|Type]] !! Name !! Description |
||
|- |
|- |
||
| $08 || 2 || pointer16 || Pointer to sprite |
| $08 || 2 || pointer16 || sprite || Pointer to sprite |
||
|- |
|- |
||
| $0A || 2 || uint16 || Number of 2-frame cycles until next animation frame |
| $0A || 2 || uint16 || cyclesUntilAnimUpdate || Number of 2-frame cycles until next animation frame |
||
|- |
|- |
||
| $0C || 2 || uint16 x2 || Current animation frame |
| $0C || 2 || uint16 x2 || animationFrame || Current animation frame |
||
|- |
|- |
||
| $0E || 2 || direction x2 || Current direction |
| $0E || 2 || direction x2 || direction || Current direction |
||
|- |
|- |
||
| $10 || 2 || direction x2 || New direction |
| $10 || 2 || direction x2 || newDirection || New direction |
||
|- |
|- |
||
| $12 || 2 || uint16 || Death status. 0 = alive, 0xF5F5 = killed, other = despawned |
| $12 || 2 || uint16 || deathStatus || Death status. 0 = alive, 0xF5F5 = killed, other = despawned |
||
|- |
|- |
||
| $14 || 2 || pointer16 || Current state subroutine pointer |
| $14 || 2 || pointer16 || state || Current state subroutine pointer |
||
|- |
|- |
||
| $16 || 2 || int16 || X position |
| $16 || 2 || int16 || x || X position |
||
|- |
|- |
||
| $18 || 2 || int16 || Y position |
| $18 || 2 || int16 || y || Y position |
||
|- |
|- |
||
| $1A || 2 || int16 || New X position |
| $1A || 2 || int16 || newX || New X position |
||
|- |
|- |
||
| $1C || 2 || int16 || New Y position |
| $1C || 2 || int16 || newY || New Y position |
||
|- |
|- |
||
| $1E || 2 || int16 0-based || Health |
| $1E || 2 || int16 0-based || health || Health |
||
|- |
|- |
||
| $20 || 2 || direction x4 || Direction to target |
| $20 || 2 || direction x4 || targetDirection || Direction to target |
||
|- |
|- |
||
| $22 || 2 || sprite type || Type of sprite collided with |
| $22 || 2 || sprite type || collidedSpriteType || Type of sprite collided with |
||
|- |
|- |
||
| $24 || 2 || |
| $24 || 2 || int16 || dontTargetTimer || Number of frames to ignore targeting for (basically unused) (hard only) |
||
|- |
|- |
||
| $26 || 2 || int16 || Temp X position (hard only) |
| $26 || 2 || int16 || tempX || Temp X position (hard only) |
||
|- |
|- |
||
| $28 || 2 || int16 || Temp Y position (hard only) |
| $28 || 2 || int16 || tempY || Temp Y position (hard only) |
||
|- |
|- |
||
| $2A || 2 || uint16 || Number of times left to move when targeting (hard only) |
| $2A || 2 || uint16 || moveCounter || Number of times left to move when targeting (hard only) |
||
|- |
|- |
||
| $2C || 2 || int16 || Amount to rotate when following wall (hard only) |
| $2C || 2 || int16 || wallRotateAngle || Amount to rotate when following wall (hard only) |
||
|- |
|- |
||
| $2E || 2 || pointer16 || Pointer to target sprite (not used) (normal only) |
| $2E || 2 || pointer16 || targetSprite || Pointer to target sprite (not used) (normal only) |
||
|- class="breakrow" |
|- class="breakrow" |
||
| $7E || 2 || |
| $7E || 2 || int16 0-based || freezeTimer || Amount of time to stay [[Fire extinguisher|frozen]] |
||
|} |
|} |
||
{{Pseudocode header}} |
|||
moveAmount_Normal = { // $81:858B |
|||
{ 0, 0 }, |
|||
{ 0, -1 }, |
|||
{ 1, -1 }, |
|||
{ 1, 0 }, |
|||
{ 1, 1 }, |
|||
{ 0, 1 }, |
|||
{ -1, 1 }, |
|||
{ -1, 0 }, |
|||
{ -1, -1 } |
|||
} |
|||
moveAmount_Hard = { // $81:85AF |
|||
{ 0, 0 }, |
|||
{ 0, -2 }, |
|||
{ 2, -2 }, |
|||
{ 2, 0 }, |
|||
{ 2, 2 }, |
|||
{ 0, 2 }, |
|||
{ -2, 2 }, |
|||
{ -2, 0 }, |
|||
{ -2, -2 } |
|||
} |
|||
newPositionIsBlocked() { // $81:85D3 |
|||
return {{ROM name|$80:AE97}}(this.newX, this.newY) || |
|||
{{ROM name|$80:BF67}}(this.sprite, this.newX, this.newY) |
|||
} |
|||
wanderInRandomDirection_Normal() { // $81:85EB |
|||
this.direction = ({{ROM name|$80:9D39}}() % 4) * 2 + 1 |
|||
setStateToWandering_Normal() |
|||
} |
|||
setStateToWandering_Normal() { // $81:85FB |
|||
this.state = wanderingState_Normal |
|||
wanderingState_Normal() |
|||
} |
|||
wanderingState_Normal() { // $81:8600 |
|||
this.newX = this.x + moveAmount_Normal[this.direction][0] |
|||
this.newY = this.y + moveAmount_Normal[this.direction][1] |
|||
if (!{{ROM name|$80:AE97}}(this.newX, this.newY)) { |
|||
if (!{{ROM name|$80:BF67}}(this.sprite, this.newX, this.newY)) { |
|||
this.x = this.newX |
|||
this.sprite.x = this.newX |
|||
this.y = this.newY |
|||
this.sprite.y = this.newY |
|||
} |
|||
} else { |
|||
followWall_Normal() |
|||
} |
|||
} |
|||
followWall_Normal() { // $81:863E |
|||
this.direction = ((this.direction - 1) + 2) % 8 + 1 // Rotate 90 degrees clockwise |
|||
setStateToFollowingWall_Normal() |
|||
} |
|||
setStateToFollowingWall_Normal() { // $81:8650 |
|||
this.state = followingWallState_Normal |
|||
} |
|||
followingWallState_Normal() { // $81:8656 |
|||
this.newDirection = ((this.direction - 1) - 2) % 8 + 1 // Rotate 90 degrees counterclockwise |
|||
this.newX = this.x + moveAmount_Normal[this.newDirection][0] |
|||
this.newY = this.y + moveAmount_Normal[this.newDirection][1] |
|||
if (!newPositionIsBlocked()) { |
|||
this.direction = this.newDirection |
|||
} |
|||
this.newX = this.x + moveAmount_Normal[this.direction][0] |
|||
this.newY = this.y + moveAmount_Normal[this.direction][1] |
|||
if (!newPositionIsBlocked()) { |
|||
this.x = this.newX |
|||
this.sprite.x = this.newX |
|||
this.y = this.newY |
|||
this.sprite.y = this.newY |
|||
} else { |
|||
followWall_Normal() |
|||
} |
|||
} |
|||
setStateToTargeting_Normal() { // $81:86AD |
|||
this.state = targetingState_Normal |
|||
} |
|||
targetingState_Normal() { // $81:86B3 |
|||
distance, this.targetSprite = {{ROM name|$80:B123}}(this.x, this.y) |
|||
if (distance >= 70) { |
|||
wanderInRandomDirection_Normal() |
|||
return |
|||
} |
|||
{{ROM name|$80:B3F1}}(this.sprite, this.targetSprite) |
|||
this.direction = {{ROM name|$80:B22A}}(this.sprite, this.targetSprite) |
|||
if (this.direction == 0) { |
|||
wanderInRandomDirection_Normal() |
|||
return |
|||
} |
|||
this.targetDirection = this.direction |
|||
for (i = 0; i < 2; i++) { |
|||
this.newX = this.x + moveAmount_Normal[this.targetDirection][0] |
|||
this.newY = this.y + moveAmount_Normal[this.targetDirection][1] |
|||
if (!newPositionIsBlocked()) { |
|||
this.x = this.newX |
|||
this.sprite.x = this.newX |
|||
this.y = this.newY |
|||
this.sprite.y = this.newY |
|||
} |
|||
} |
|||
} |
|||
checkForTarget_Normal() { // $81:8706 |
|||
distance = {{ROM name|$80:B123}}(this.x, this.y) |
|||
if (distance < 65) { |
|||
setStateToTargeting_Normal() |
|||
return |
|||
} |
|||
direction = {{ROM name|$80:B2A5}}(208, this.x, this.y) |
|||
if (direction == NONE) { |
|||
this.deathStatus -= 1 |
|||
} |
|||
} |
|||
killed() { // $81:8727 |
|||
{{ROM name|$80:C7D9}}(this.collidedSpriteType & 0x8000, 100) |
|||
this.deathStatus = 0xF5F5 |
|||
} |
|||
updateAnimation() { // $81:8736 |
|||
this.cyclesUntilAnimUpdate -= 1 |
|||
if (this.cyclesUntilAnimUpdate == 0) { |
|||
this.cyclesUntilAnimUpdate = 4 |
|||
this.animationFrame = (this.animationFrame + 1) % 4 |
|||
this.sprite.tileData.lowBytes = walkingAnimations[this.direction][this.animationFrame] |
|||
this.sprite.tileData.bank = 0x90 |
|||
if (this.direction < DOWN_LEFT) { |
|||
this.sprite.flipX = false |
|||
} else { |
|||
this.sprite.flipX = true |
|||
} |
|||
} |
|||
} |
|||
walkingAnimations = { // $81:8776 |
|||
{ 0xCABC, 0xCADD, 0xCAFE, 0xCB1F }, |
|||
{ 0xCB40, 0xCB61, 0xCB82, 0xCBA3 }, |
|||
{ 0xCA28, 0xCA51, 0xCA72, 0xCA9B }, |
|||
{ 0xCA28, 0xCA51, 0xCA72, 0xCA9B }, |
|||
{ 0xCA28, 0xCA51, 0xCA72, 0xCA9B }, |
|||
{ 0xCABC, 0xCADD, 0xCAFE, 0xCB1F }, |
|||
{ 0xCA28, 0xCA51, 0xCA72, 0xCA9B }, |
|||
{ 0xCA28, 0xCA51, 0xCA72, 0xCA9B }, |
|||
{ 0xCA28, 0xCA51, 0xCA72, 0xCA9B } |
|||
} |
|||
init() { // $81:87BE |
|||
{{ROM name|$81:8000}}() |
|||
this.animationFrame = 0 |
|||
this.x = args.x |
|||
this.sprite.x = args.x |
|||
this.sprite.z = 0 |
|||
this.y = args.y |
|||
this.sprite.y = args.y |
|||
this.sprite.tileData = $90:C98A |
|||
this.sprite.visible = true |
|||
this.sprite.alternatePalette = 6 |
|||
this.collidedSpriteType = 0 |
|||
this.freezeTimer = 0 |
|||
} |
|||
main_Normal() { // $81:87F8 |
|||
{{RAM name|$7E:00DE}} += 0x14 |
|||
this.cyclesUntilAnimUpdate = 7 |
|||
init() |
|||
{{ROM name|$81:832C}}(spawnAnimation_Normal) |
|||
this.sprite.type = MONSTER |
|||
wanderInRandomDirection_Normal() |
|||
this.health = 0 |
|||
{{ROM name|$80:8475}}(collisionHandler) |
|||
this.deathStatus = 0 |
|||
while (this.deathStatus == 0) { |
|||
{{ROM name|$80:8353}}(2) |
|||
checkForTarget_Normal() |
|||
this.state() |
|||
updateAnimation() |
|||
} |
|||
if (this.deathStatus == 0xF5F5) { |
|||
{{RAM name|$7E:1F64}} += 1 |
|||
{{ROM name|$81:83A3}}(mummy.deathAnimation, this.deathStatus, 0x90) |
|||
} |
|||
{{RAM name|$7E:00DE}} -= 0x14 |
|||
while ({{RAM name|$7E:00DE}} < 0) { } |
|||
{{ROM name|$80:BE41}}(this.sprite) |
|||
} |
|||
spawnAnimation_Normal = { // $81:886C |
|||
{ 0xC98A, 10 }, { 0xC993, 10 }, { 0xC9A4, 10 }, { 0xC9B5, 10 }, |
|||
{ 0xC9D6, 10 }, { 0xC9FF, 10 }, { 0, 0 } |
|||
} |
|||
collisionHandler(spriteType) { // $81:8888 |
|||
if (spriteType < SQUIRT_GUN) { |
|||
return false |
|||
} |
|||
this.collidedSpriteType = spriteType |
|||
weaponType = spriteType & 0x7FFF |
|||
if (weaponType == MARTIAN_BUBBLE_GUN) { |
|||
return {{ROM name|$81:83C6}}() |
|||
} else if (weaponType == FIRE_EXTINGUISHER) { |
|||
return {{ROM name|$81:847E}}() |
|||
} else { |
|||
newHealth = this.health - {{ROM name|$81:8561}}[weaponType - SQUIRT_GUN] |
|||
if (newHealth < 0) { |
|||
this.health = newHealth |
|||
this.freezeTimer = 0 |
|||
killed() |
|||
return true |
|||
} else if (newHealth != this.health) { |
|||
this.health = newHealth |
|||
return {{ROM name|$81:8506}}() |
|||
} |
|||
return false |
|||
} |
|||
} |
|||
main_Hard() { // $81:88CA |
|||
{{RAM name|$7E:00DE}} += 0x14 |
|||
this.cyclesUntilAnimUpdate = 7 |
|||
init() |
|||
{{ROM name|$81:832C}}(spawnAnimation_Hard) |
|||
this.sprite.type = MONSTER |
|||
wanderInRandomDirection_Hard() |
|||
this.health = 0 |
|||
this.dontTargetTimer = -1 |
|||
{{ROM name|$80:8475}}(collisionHandler) |
|||
this.deathStatus = 0 |
|||
while (this.deathStatus == 0) { |
|||
{{ROM name|$80:8353}}(2) |
|||
if (this.collidedSpriteType != 0) { |
|||
this.sprite.useAlternatePalette = false |
|||
this.collidedSpriteType = 0 |
|||
} else { |
|||
checkForTarget_Hard() |
|||
} |
|||
this.state() |
|||
updateAnimation() |
|||
} |
|||
if (this.deathStatus == 0xF5F5) { |
|||
{{RAM name|$7E:1F64}} += 1 |
|||
{{ROM name|$81:83A3}}(mummy.deathAnimation, this.deathStatus, 0x90) |
|||
} |
|||
{{RAM name|$7E:00DE}} -= 0x14 |
|||
while ({{RAM name|$7E:00DE}} < 0) { } |
|||
{{ROM name|$80:BE41}}(this.sprite) |
|||
} |
|||
spawnAnimation_Hard = { // $81:8956 |
|||
{ 0xC98A, 10 }, { 0xC993, 10 }, { 0xC9A4, 10 }, { 0xC9B5, 10 }, |
|||
{ 0xC9D6, 10 }, { 0xC9FF, 10 }, { 0, 0 } |
|||
} |
|||
// Unused. Exactly equivalent to the other animations table |
|||
walkingAnimations_Hard = { // $81:8972 |
|||
{ 0xCABC, 0xCADD, 0xCAFE, 0xCB1F }, |
|||
{ 0xCB40, 0xCB61, 0xCB82, 0xCBA3 }, |
|||
{ 0xCA28, 0xCA51, 0xCA72, 0xCA9B }, |
|||
{ 0xCA28, 0xCA51, 0xCA72, 0xCA9B }, |
|||
{ 0xCA28, 0xCA51, 0xCA72, 0xCA9B }, |
|||
{ 0xCABC, 0xCADD, 0xCAFE, 0xCB1F }, |
|||
{ 0xCA28, 0xCA51, 0xCA72, 0xCA9B }, |
|||
{ 0xCA28, 0xCA51, 0xCA72, 0xCA9B }, |
|||
{ 0xCA28, 0xCA51, 0xCA72, 0xCA9B } |
|||
} |
|||
setStateToWandering_Hard() { // $81:89BA |
|||
this.state = wanderingState_Hard |
|||
wanderingState_Hard() |
|||
} |
|||
wanderingState_Hard() { // $81:89BF |
|||
this.newX = this.x + moveAmount_Hard[this.direction][0] |
|||
this.newY = this.y + moveAmount_Hard[this.direction][1] |
|||
if (!{{ROM name|$80:AE97}}(this.newX, this.newY)) { |
|||
if (!{{ROM name|$80:BF67}}(this.sprite, this.newX, this.newY)) { |
|||
this.x = this.newX |
|||
this.sprite.x = this.newX |
|||
this.y = this.newY |
|||
this.sprite.y = this.newY |
|||
} |
|||
} else { |
|||
followWall_Hard() |
|||
} |
|||
} |
|||
tryToMoveWhileTargeting() { // $81:89FD |
|||
this.tempX = this.x |
|||
this.tempY = this.y |
|||
if (!{{ROM name|$80:AE97}}(this.newX, this.y) && |
|||
!{{ROM name|$80:BF67}}(this.sprite, this.newX, this.y)) { |
|||
this.x = this.newX |
|||
} |
|||
if (!{{ROM name|$80:AE97}}(this.x, this.newY) && |
|||
!{{ROM name|$80:BF67}}(this.sprite, this.x, this.newY)) { |
|||
this.y = this.newY |
|||
} |
|||
if (this.x != this.tempX || this.y != this.tempY) { |
|||
this.dontTargetTimer = 0 |
|||
return false |
|||
} else { |
|||
return true |
|||
} |
|||
} |
|||
followWall_Hard() { // $81:8A4B |
|||
this.direction = ((this.direction - 1) + this.wallRotateAngle) % 8 + 1 |
|||
setStateToFollowingWall_Hard() |
|||
} |
|||
wanderInRandomDirection_Hard() { // $81:8A5C |
|||
this.direction = ({{ROM name|$80:9D39}}() % 4) * 2 + 1 |
|||
setStateToWandering_Hard() |
|||
} |
|||
setStateToFollowingWall_Hard() { // $81:8A6C |
|||
this.state = followingWallState_Hard |
|||
} |
|||
followingWallState_Hard() { // $81:8A72 |
|||
this.dontTargetTimer -= 1 |
|||
this.newDirection = ((this.direction - 1) - this.wallRotateAngle) % 8 + 1 |
|||
this.newX = this.x + moveAmount_Hard[this.newDirection][0] |
|||
this.newY = this.y + moveAmount_Hard[this.newDirection][1] |
|||
if (!newPositionIsBlocked()) { |
|||
this.direction = this.newDirection |
|||
} |
|||
this.newX = this.x + moveAmount_Hard[this.direction][0] |
|||
this.newY = this.y + moveAmount_Hard[this.direction][1] |
|||
if (!newPositionIsBlocked()) { |
|||
this.x = this.newX |
|||
this.sprite.x = this.newX |
|||
this.y = this.newY |
|||
this.sprite.y = this.newY |
|||
} else { |
|||
followWall_Hard() |
|||
} |
|||
} |
|||
unused818ACA() { // $81:8ACA |
|||
this.dontTargetTimer = 0 |
|||
wanderInRandomDirection_Hard() |
|||
} |
|||
setStateToTargeting_Hard() { // $81:8AD2 |
|||
this.state = targetingState_Hard |
|||
} |
|||
targetingState_Hard() { // $81:8AD8 |
|||
distance, this.targetSprite = {{ROM name|$80:B123}}(this.x, this.y) |
|||
if (distance >= 180) { |
|||
wanderInRandomDirection_Hard() |
|||
return |
|||
} |
|||
{{ROM name|$80:B3F1}}(this.sprite, this.targetSprite) |
|||
this.direction = {{ROM name|$80:B22A}}(this.sprite, this.targetSprite) |
|||
if (this.direction == 0) { |
|||
wanderInRandomDirection_Hard() |
|||
return |
|||
} |
|||
this.targetDirection = this.direction |
|||
this.moveCounter = 1 |
|||
while (this.moveCounter > 0) { |
|||
this.newX = this.x + moveAmount_Hard[this.targetDirection][0] |
|||
this.newY = this.y + moveAmount_Hard[this.targetDirection][1] |
|||
if (tryToMoveWhileTargeting()) { |
|||
this.dontTargetTimer = 0 |
|||
wanderInRandomDirection_Hard() |
|||
return |
|||
} |
|||
this.sprite.x = this.x |
|||
this.sprite.y = this.y |
|||
this.moveCounter -= 1 |
|||
} |
|||
} |
|||
checkForTarget_Hard() { // $81:8B30 |
|||
this.dontTargetTimer -= 1 |
|||
if (this.dontTargetTimer >= 0) { |
|||
return |
|||
} |
|||
distance = {{ROM name|$80:B123}}(this.x, this.y) |
|||
if (distance < 160) { |
|||
setStateToTargeting_Hard() |
|||
return |
|||
} |
|||
direction = {{ROM name|$80:B2A5}}(208, this.x, this.y) |
|||
if (direction == NONE) { |
|||
this.deathStatus -= 1 |
|||
} |
|||
} |
|||
{{Pseudocode footer}} |
|||
[[Category:Respawning monster]] |
[[Category:Respawning monster]] |
||
[[Category:Bug]] |
Latest revision as of 18:43, 17 August 2024
Monster data | |
---|---|
HP | 1 |
Points | 100 |
Special | - |
Resists | - |
Weak to | - |
Entity data | |
---|---|
Entity pointer | $81:87F8 (normal) $81:88CA (hard) |
The zombie is a respawning monster. Zombies attack via contact damage, have no special powers and aren't resistant or vulnerable to any weapon. They can be found in all tilesets, only have 1 hit point and award 100 points to the player when killed. There are two different types of zombies: normal and hard. They behave mostly the same, but there are slight differences that make the hard version more aggressive.
Behavior[edit]
Zombies can be in one of three states:
- Wandering: Walk in a straight line in a random one of the four orthogonal directions. Zombies start in this state.
- Following wall: Go counterclockwise around the perimeter of a wall. A zombie will enter this state if it touches a wall (i.e. a tile that is solid to monsters) while in the wandering state.
- Targeting: Move towards the nearest targetable sprite. A zombie enters this state when it comes within a certain distance of a targetable sprite. The zombie will leave this state and go back to the wandering state if there is no longer a targetable sprite within another (slightly larger) distance.
Zombies will despawn if they are a distance of 208 pixels or more on a single axis from the closest player, and are not within target range of any targetable sprites. The SNES screen has a width of 256 pixels, so this means it is possible for a zombie to despawn while still on screen if all players are on the very left or right of the screen and the zombie is on the opposite side.
Differences in hard version[edit]
Major differences[edit]
- Start targeting at distance of 159 pixels (instead of 64 pixels for normal). They continue targeting while there is a target within 179 pixels (instead of 69 pixels for normal).
- Moves at a speed of 1 px/frame when wandering and following wall (instead of 0.5 px/frame for normal). The speed when targeting is 1 px/frame for both versions.
Minor differences[edit]
- Position update logic is different when targeting. When targeting through a wall, normal zombies will get stuck, but the hard version will still follow along the wall.
- The angle to rotate when following a wall is stored in a variable instead of being hardcoded. However, this variable is never initialized, so it will retain whatever value happened to be in RAM previously. This generally results in the zombie sticking to the wall instead of following it.
- Will switch to the wandering state if they are unable to move towards the target when targeting (because there is a wall in the way). The zombie will still likely be within target range, so it will usually immediately switch back to targeting, resulting in the zombie turning in place spastically.
Animation[edit]
Zombies have a four frame animation cycle, with 8 frames in between each animation frame. There is a separate set of animation frames for each of the 4 orthogonal directions, chosen based on which direction the zombie is moving. If the zombie is moving diagonal, it uses the left or right animation.
Bugs[edit]
- Hard zombies do not generally follow walls like they should. See Differences in hard version.
- The hard zombies have a partially implemented feature that appears to be something that would make them stop targeting for a certain amount of time. In its incomplete state, it generally does not affect their behavior, but if a zombie stays following a wall for around 9 minutes, the timer will underflow. This will cause the zombie to stop targeting for around another 9 minutes until the timer reaches zero again.
Trivia[edit]
- With 152 spawners for normal zombies and 127 spawners for hard zombies, this is the most common monster that players will face in the game.
RAM map[edit]
Entity arguments[edit]
Address | Length | Type | Name | Description |
---|---|---|---|---|
$00 | 2 | int16 | x | X position |
$02 | 2 | int16 | y | Y position |
Entity memory[edit]
Address | Length | Type | Name | Description |
---|---|---|---|---|
$08 | 2 | pointer16 | sprite | Pointer to sprite |
$0A | 2 | uint16 | cyclesUntilAnimUpdate | Number of 2-frame cycles until next animation frame |
$0C | 2 | uint16 x2 | animationFrame | Current animation frame |
$0E | 2 | direction x2 | direction | Current direction |
$10 | 2 | direction x2 | newDirection | New direction |
$12 | 2 | uint16 | deathStatus | Death status. 0 = alive, 0xF5F5 = killed, other = despawned |
$14 | 2 | pointer16 | state | Current state subroutine pointer |
$16 | 2 | int16 | x | X position |
$18 | 2 | int16 | y | Y position |
$1A | 2 | int16 | newX | New X position |
$1C | 2 | int16 | newY | New Y position |
$1E | 2 | int16 0-based | health | Health |
$20 | 2 | direction x4 | targetDirection | Direction to target |
$22 | 2 | sprite type | collidedSpriteType | Type of sprite collided with |
$24 | 2 | int16 | dontTargetTimer | Number of frames to ignore targeting for (basically unused) (hard only) |
$26 | 2 | int16 | tempX | Temp X position (hard only) |
$28 | 2 | int16 | tempY | Temp Y position (hard only) |
$2A | 2 | uint16 | moveCounter | Number of times left to move when targeting (hard only) |
$2C | 2 | int16 | wallRotateAngle | Amount to rotate when following wall (hard only) |
$2E | 2 | pointer16 | targetSprite | Pointer to target sprite (not used) (normal only) |
$7E | 2 | int16 0-based | freezeTimer | Amount of time to stay frozen |
Pseudocode
moveAmount_Normal = { // $81:858B { 0, 0 }, { 0, -1 }, { 1, -1 }, { 1, 0 }, { 1, 1 }, { 0, 1 }, { -1, 1 }, { -1, 0 }, { -1, -1 } } moveAmount_Hard = { // $81:85AF { 0, 0 }, { 0, -2 }, { 2, -2 }, { 2, 0 }, { 2, 2 }, { 0, 2 }, { -2, 2 }, { -2, 0 }, { -2, -2 } } newPositionIsBlocked() { // $81:85D3 return bgSolidToMonsters(this.newX, this.newY) || touchingSpriteSolidToMonsters(this.sprite, this.newX, this.newY) } wanderInRandomDirection_Normal() { // $81:85EB this.direction = (getRandomByte() % 4) * 2 + 1 setStateToWandering_Normal() } setStateToWandering_Normal() { // $81:85FB this.state = wanderingState_Normal wanderingState_Normal() } wanderingState_Normal() { // $81:8600 this.newX = this.x + moveAmount_Normal[this.direction][0] this.newY = this.y + moveAmount_Normal[this.direction][1] if (!bgSolidToMonsters(this.newX, this.newY)) { if (!touchingSpriteSolidToMonsters(this.sprite, this.newX, this.newY)) { this.x = this.newX this.sprite.x = this.newX this.y = this.newY this.sprite.y = this.newY } } else { followWall_Normal() } } followWall_Normal() { // $81:863E this.direction = ((this.direction - 1) + 2) % 8 + 1 // Rotate 90 degrees clockwise setStateToFollowingWall_Normal() } setStateToFollowingWall_Normal() { // $81:8650 this.state = followingWallState_Normal } followingWallState_Normal() { // $81:8656 this.newDirection = ((this.direction - 1) - 2) % 8 + 1 // Rotate 90 degrees counterclockwise this.newX = this.x + moveAmount_Normal[this.newDirection][0] this.newY = this.y + moveAmount_Normal[this.newDirection][1] if (!newPositionIsBlocked()) { this.direction = this.newDirection } this.newX = this.x + moveAmount_Normal[this.direction][0] this.newY = this.y + moveAmount_Normal[this.direction][1] if (!newPositionIsBlocked()) { this.x = this.newX this.sprite.x = this.newX this.y = this.newY this.sprite.y = this.newY } else { followWall_Normal() } } setStateToTargeting_Normal() { // $81:86AD this.state = targetingState_Normal } targetingState_Normal() { // $81:86B3 distance, this.targetSprite = findTarget(this.x, this.y) if (distance >= 70) { wanderInRandomDirection_Normal() return } snapToTarget(this.sprite, this.targetSprite) this.direction = getDirectionToTarget(this.sprite, this.targetSprite) if (this.direction == 0) { wanderInRandomDirection_Normal() return } this.targetDirection = this.direction for (i = 0; i < 2; i++) { this.newX = this.x + moveAmount_Normal[this.targetDirection][0] this.newY = this.y + moveAmount_Normal[this.targetDirection][1] if (!newPositionIsBlocked()) { this.x = this.newX this.sprite.x = this.newX this.y = this.newY this.sprite.y = this.newY } } } checkForTarget_Normal() { // $81:8706 distance = findTarget(this.x, this.y) if (distance < 65) { setStateToTargeting_Normal() return } direction = findPlayerInRangeDirection(208, this.x, this.y) if (direction == NONE) { this.deathStatus -= 1 } } killed() { // $81:8727 givePoints(this.collidedSpriteType & 0x8000, 100) this.deathStatus = 0xF5F5 } updateAnimation() { // $81:8736 this.cyclesUntilAnimUpdate -= 1 if (this.cyclesUntilAnimUpdate == 0) { this.cyclesUntilAnimUpdate = 4 this.animationFrame = (this.animationFrame + 1) % 4 this.sprite.tileData.lowBytes = walkingAnimations[this.direction][this.animationFrame] this.sprite.tileData.bank = 0x90 if (this.direction < DOWN_LEFT) { this.sprite.flipX = false } else { this.sprite.flipX = true } } } walkingAnimations = { // $81:8776 { 0xCABC, 0xCADD, 0xCAFE, 0xCB1F }, { 0xCB40, 0xCB61, 0xCB82, 0xCBA3 }, { 0xCA28, 0xCA51, 0xCA72, 0xCA9B }, { 0xCA28, 0xCA51, 0xCA72, 0xCA9B }, { 0xCA28, 0xCA51, 0xCA72, 0xCA9B }, { 0xCABC, 0xCADD, 0xCAFE, 0xCB1F }, { 0xCA28, 0xCA51, 0xCA72, 0xCA9B }, { 0xCA28, 0xCA51, 0xCA72, 0xCA9B }, { 0xCA28, 0xCA51, 0xCA72, 0xCA9B } } init() { // $81:87BE createMonsterSprite() this.animationFrame = 0 this.x = args.x this.sprite.x = args.x this.sprite.z = 0 this.y = args.y this.sprite.y = args.y this.sprite.tileData = $90:C98A this.sprite.visible = true this.sprite.alternatePalette = 6 this.collidedSpriteType = 0 this.freezeTimer = 0 } main_Normal() { // $81:87F8 $7E:00DE += 0x14 this.cyclesUntilAnimUpdate = 7 init() showAnimation(spawnAnimation_Normal) this.sprite.type = MONSTER wanderInRandomDirection_Normal() this.health = 0 setCollisionHandler(collisionHandler) this.deathStatus = 0 while (this.deathStatus == 0) { waitFrames(2) checkForTarget_Normal() this.state() updateAnimation() } if (this.deathStatus == 0xF5F5) { zombiesKilled += 1 showMonsterDeath(mummy.deathAnimation, this.deathStatus, 0x90) } $7E:00DE -= 0x14 while ($7E:00DE < 0) { } deleteSprite(this.sprite) } spawnAnimation_Normal = { // $81:886C { 0xC98A, 10 }, { 0xC993, 10 }, { 0xC9A4, 10 }, { 0xC9B5, 10 }, { 0xC9D6, 10 }, { 0xC9FF, 10 }, { 0, 0 } } collisionHandler(spriteType) { // $81:8888 if (spriteType < SQUIRT_GUN) { return false } this.collidedSpriteType = spriteType weaponType = spriteType & 0x7FFF if (weaponType == MARTIAN_BUBBLE_GUN) { return bubbleMonster() } else if (weaponType == FIRE_EXTINGUISHER) { return freezeMonster() } else { newHealth = this.health - weaponDamage[weaponType - SQUIRT_GUN] if (newHealth < 0) { this.health = newHealth this.freezeTimer = 0 killed() return true } else if (newHealth != this.health) { this.health = newHealth return showDamageAnimation() } return false } } main_Hard() { // $81:88CA $7E:00DE += 0x14 this.cyclesUntilAnimUpdate = 7 init() showAnimation(spawnAnimation_Hard) this.sprite.type = MONSTER wanderInRandomDirection_Hard() this.health = 0 this.dontTargetTimer = -1 setCollisionHandler(collisionHandler) this.deathStatus = 0 while (this.deathStatus == 0) { waitFrames(2) if (this.collidedSpriteType != 0) { this.sprite.useAlternatePalette = false this.collidedSpriteType = 0 } else { checkForTarget_Hard() } this.state() updateAnimation() } if (this.deathStatus == 0xF5F5) { zombiesKilled += 1 showMonsterDeath(mummy.deathAnimation, this.deathStatus, 0x90) } $7E:00DE -= 0x14 while ($7E:00DE < 0) { } deleteSprite(this.sprite) } spawnAnimation_Hard = { // $81:8956 { 0xC98A, 10 }, { 0xC993, 10 }, { 0xC9A4, 10 }, { 0xC9B5, 10 }, { 0xC9D6, 10 }, { 0xC9FF, 10 }, { 0, 0 } } // Unused. Exactly equivalent to the other animations table walkingAnimations_Hard = { // $81:8972 { 0xCABC, 0xCADD, 0xCAFE, 0xCB1F }, { 0xCB40, 0xCB61, 0xCB82, 0xCBA3 }, { 0xCA28, 0xCA51, 0xCA72, 0xCA9B }, { 0xCA28, 0xCA51, 0xCA72, 0xCA9B }, { 0xCA28, 0xCA51, 0xCA72, 0xCA9B }, { 0xCABC, 0xCADD, 0xCAFE, 0xCB1F }, { 0xCA28, 0xCA51, 0xCA72, 0xCA9B }, { 0xCA28, 0xCA51, 0xCA72, 0xCA9B }, { 0xCA28, 0xCA51, 0xCA72, 0xCA9B } } setStateToWandering_Hard() { // $81:89BA this.state = wanderingState_Hard wanderingState_Hard() } wanderingState_Hard() { // $81:89BF this.newX = this.x + moveAmount_Hard[this.direction][0] this.newY = this.y + moveAmount_Hard[this.direction][1] if (!bgSolidToMonsters(this.newX, this.newY)) { if (!touchingSpriteSolidToMonsters(this.sprite, this.newX, this.newY)) { this.x = this.newX this.sprite.x = this.newX this.y = this.newY this.sprite.y = this.newY } } else { followWall_Hard() } } tryToMoveWhileTargeting() { // $81:89FD this.tempX = this.x this.tempY = this.y if (!bgSolidToMonsters(this.newX, this.y) && !touchingSpriteSolidToMonsters(this.sprite, this.newX, this.y)) { this.x = this.newX } if (!bgSolidToMonsters(this.x, this.newY) && !touchingSpriteSolidToMonsters(this.sprite, this.x, this.newY)) { this.y = this.newY } if (this.x != this.tempX || this.y != this.tempY) { this.dontTargetTimer = 0 return false } else { return true } } followWall_Hard() { // $81:8A4B this.direction = ((this.direction - 1) + this.wallRotateAngle) % 8 + 1 setStateToFollowingWall_Hard() } wanderInRandomDirection_Hard() { // $81:8A5C this.direction = (getRandomByte() % 4) * 2 + 1 setStateToWandering_Hard() } setStateToFollowingWall_Hard() { // $81:8A6C this.state = followingWallState_Hard } followingWallState_Hard() { // $81:8A72 this.dontTargetTimer -= 1 this.newDirection = ((this.direction - 1) - this.wallRotateAngle) % 8 + 1 this.newX = this.x + moveAmount_Hard[this.newDirection][0] this.newY = this.y + moveAmount_Hard[this.newDirection][1] if (!newPositionIsBlocked()) { this.direction = this.newDirection } this.newX = this.x + moveAmount_Hard[this.direction][0] this.newY = this.y + moveAmount_Hard[this.direction][1] if (!newPositionIsBlocked()) { this.x = this.newX this.sprite.x = this.newX this.y = this.newY this.sprite.y = this.newY } else { followWall_Hard() } } unused818ACA() { // $81:8ACA this.dontTargetTimer = 0 wanderInRandomDirection_Hard() } setStateToTargeting_Hard() { // $81:8AD2 this.state = targetingState_Hard } targetingState_Hard() { // $81:8AD8 distance, this.targetSprite = findTarget(this.x, this.y) if (distance >= 180) { wanderInRandomDirection_Hard() return } snapToTarget(this.sprite, this.targetSprite) this.direction = getDirectionToTarget(this.sprite, this.targetSprite) if (this.direction == 0) { wanderInRandomDirection_Hard() return } this.targetDirection = this.direction this.moveCounter = 1 while (this.moveCounter > 0) { this.newX = this.x + moveAmount_Hard[this.targetDirection][0] this.newY = this.y + moveAmount_Hard[this.targetDirection][1] if (tryToMoveWhileTargeting()) { this.dontTargetTimer = 0 wanderInRandomDirection_Hard() return } this.sprite.x = this.x this.sprite.y = this.y this.moveCounter -= 1 } } checkForTarget_Hard() { // $81:8B30 this.dontTargetTimer -= 1 if (this.dontTargetTimer >= 0) { return } distance = findTarget(this.x, this.y) if (distance < 160) { setStateToTargeting_Hard() return } direction = findPlayerInRangeDirection(208, this.x, this.y) if (direction == NONE) { this.deathStatus -= 1 } }