Zombie: Difference between revisions

From ZAMN Hacking
Content added Content deleted
No edit summary
No edit summary
Line 16: Line 16:
== 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|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 32:


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 4.5 minutes, the timer will underflow. This will cause the zombie to stop targeting for around another 4.5 minutes until the timer reaches zero again.


== RAM map ==
== RAM map ==
Line 75: Line 85:
| $22 || 2 || sprite type || collidedSpriteType || Type of sprite collided with
| $22 || 2 || sprite type || collidedSpriteType || Type of sprite collided with
|-
|-
| $24 || 2 || unused || unused24 || Value is set and used, but appears to have no effect (hard only)
| $24 || 2 || int16 || dontTargetTimer || Number of frames to ignore targeting for (basically unused) (hard only)
|-
|-
| $26 || 2 || int16 || tempX || Temp X position (hard only)
| $26 || 2 || int16 || tempX || Temp X position (hard only)
Line 92: Line 102:
== Pseudocode ==
== Pseudocode ==


moveAmount_Normal = {
moveAmount = { { 0, 0 },
{ 0, -1 },
{ 0, 0 },
{ 1, -1 },
{ 0, -1 },
{ 1, 0 },
{ 1, -1 },
{ 1, 1 },
{ 1, 0 },
{ 0, 1 },
{ 1, 1 },
{ -1, 1 },
{ 0, 1 },
{ -1, 0 },
{ -1, 1 },
{ -1, -1 } }
{ -1, 0 },
{ -1, -1 }
}
moveAmount_Hard = {
{ 0, 0 },
{ 0, -2 },
{ 2, -2 },
{ 2, 0 },
{ 2, 2 },
{ 0, 2 },
{ -2, 2 },
{ -2, 0 },
{ -2, -2 }
}
newPositionIsBlocked() {
newPositionIsBlocked() {
Line 107: Line 131:
}
}
wanderInRandomDirection_Normal() {
wanderInRandomDirection() {
this.direction = ({{ROM name|$80:9D39}}() % 4) * 2 + 1
this.direction = ({{ROM name|$80:9D39}}() % 4) * 2 + 1
this.setStateToWandering()
this.setStateToWandering_Normal()
}
}
setStateToWandering_Normal() {
setStateToWandering() {
this.state = this.wanderingState
this.state = this.wanderingState_Normal
this.wanderingState()
this.wanderingState_Normal()
}
}
wanderingState_Normal() {
wanderingState() {
this.newX = this.x + this.moveAmount[this.direction][0]
this.newX = this.x + this.moveAmount_Normal[this.direction][0]
this.newY = this.y + this.moveAmount[this.direction][1]
this.newY = this.y + this.moveAmount_Normal[this.direction][1]
if (!{{ROM name|$80:AE97}}(this.newX, this.newY)) {
if (!{{ROM name|$80:AE97}}(this.newX, this.newY)) {
if (!{{ROM name|$80:BF67}}(this.sprite, this.newX, this.newY)) {
if (!{{ROM name|$80:BF67}}(this.sprite, this.newX, this.newY)) {
Line 128: Line 152:
}
}
} else {
} else {
this.followWall()
this.followWall_Normal()
}
}
}
}
followWall() {
followWall_Normal() {
this.direction = ((this.direction - 1) + 2) % 8 + 1 // Rotate 90 degrees clockwise
this.direction = ((this.direction - 1) + 2) % 8 + 1 // Rotate 90 degrees clockwise
this.setStateToFollowingWall()
this.setStateToFollowingWall_Normal()
}
}
setStateToFollowingWall_Normal() {
setStateToFollowingWall() {
this.state = this.followingWallState
this.state = this.followingWallState_Normal
}
}
followingWallState_Normal() {
followingWallState() {
this.newDirection = ((this.direction - 1) - 2) % 8 + 1 // Rotate 90 degrees counterclockwise
this.newDirection = ((this.direction - 1) - 2) % 8 + 1 // Rotate 90 degrees counterclockwise
this.newX = this.x + this.moveAmount[this.newDirection][0]
this.newX = this.x + this.moveAmount_Normal[this.newDirection][0]
this.newY = this.y + this.moveAmount[this.newDirection][1]
this.newY = this.y + this.moveAmount_Normal[this.newDirection][1]
if (!this.newPositionIsBlocked()) {
if (!this.newPositionIsBlocked()) {
this.direction = this.newDirection
this.direction = this.newDirection
}
}
this.newX = this.x + this.moveAmount[this.direction][0]
this.newX = this.x + this.moveAmount_Normal[this.direction][0]
this.newY = this.y + this.moveAmount[this.direction][1]
this.newY = this.y + this.moveAmount_Normal[this.direction][1]
if (!this.newPositionIsBlocked()) {
if (!this.newPositionIsBlocked()) {
this.x = this.newX
this.x = this.newX
Line 157: Line 181:
this.sprite.y = this.newY
this.sprite.y = this.newY
} else {
} else {
this.followWall()
this.followWall_Normal()
}
}
}
}
setStateToTargeting_Normal() {
setStateToTargeting() {
this.state = this.targetingState
this.state = this.targetingState_Normal
}
}
targetingState_Normal() {
targetingState() {
distance, this.targetSprite = {{ROM name|$80:B123}}(this.x, this.y)
distance, this.targetSprite = {{ROM name|$80:B123}}(this.x, this.y)
if (distance >= 70) {
if (distance >= 70) {
this.wanderInRandomDirection()
this.wanderInRandomDirection_Normal()
return
return
}
}
Line 175: Line 199:
this.direction = {{ROM name|$80:B22A}}(this.sprite, this.targetSprite)
this.direction = {{ROM name|$80:B22A}}(this.sprite, this.targetSprite)
if (this.direction == 0) {
if (this.direction == 0) {
this.wanderInRandomDirection()
this.wanderInRandomDirection_Normal()
return
return
}
}
Line 181: Line 205:
for (i = 0; i < 2; i++) {
for (i = 0; i < 2; i++) {
this.newX = this.x + this.moveAmount[this.targetDirection][0]
this.newX = this.x + this.moveAmount_Normal[this.targetDirection][0]
this.newY = this.y + this.moveAmount[this.targetDirection][1]
this.newY = this.y + this.moveAmount_Normal[this.targetDirection][1]
if (!this.newPositionIsBlocked()) {
if (!this.newPositionIsBlocked()) {
this.x = this.newX
this.x = this.newX
Line 192: Line 216:
}
}
checkForTarget_Normal() {
checkForTarget() {
distance = {{ROM name|$80:B123}}(this.x, this.y)
distance = {{ROM name|$80:B123}}(this.x, this.y)
if (distance < 65) {
if (distance < 65) {
this.setStateToTargeting()
this.setStateToTargeting_Normal()
return
return
}
}
Line 206: Line 230:
killed() {
killed() {
{{ROM name|$80:B2A5}}(this.collidedSpriteType & 0x8000, 100)
{{ROM name|$80:C7D9}}(this.collidedSpriteType & 0x8000, 100)
this.deathStatus = 0xF5F5
this.deathStatus = 0xF5F5
}
}
Line 226: Line 250:
}
}
animations = { { 0xCABC, 0xCADD, 0xCAFE, 0xCB1F },
animations = {
{ 0xCB40, 0xCB61, 0xCB82, 0xCBA3 },
{ 0xCABC, 0xCADD, 0xCAFE, 0xCB1F },
{ 0xCA28, 0xCA51, 0xCA72, 0xCA9B },
{ 0xCB40, 0xCB61, 0xCB82, 0xCBA3 },
{ 0xCA28, 0xCA51, 0xCA72, 0xCA9B },
{ 0xCA28, 0xCA51, 0xCA72, 0xCA9B },
{ 0xCA28, 0xCA51, 0xCA72, 0xCA9B },
{ 0xCA28, 0xCA51, 0xCA72, 0xCA9B },
{ 0xCABC, 0xCADD, 0xCAFE, 0xCB1F },
{ 0xCA28, 0xCA51, 0xCA72, 0xCA9B },
{ 0xCA28, 0xCA51, 0xCA72, 0xCA9B },
{ 0xCABC, 0xCADD, 0xCAFE, 0xCB1F },
{ 0xCA28, 0xCA51, 0xCA72, 0xCA9B },
{ 0xCA28, 0xCA51, 0xCA72, 0xCA9B },
{ 0xCA28, 0xCA51, 0xCA72, 0xCA9B } }
{ 0xCA28, 0xCA51, 0xCA72, 0xCA9B },
{ 0xCA28, 0xCA51, 0xCA72, 0xCA9B }
}
init() {
init() {
Line 251: Line 277:
}
}
mainNormal() {
main_Normal() {
{{RAM name|$7E:00DE}} += 0x14
{{RAM name|$7E:00DE}} += 0x14
this.cyclesUntilAnimUpdate = 7
this.cyclesUntilAnimUpdate = 7
this.init()
this.init()
{{ROM name|$81:832C}}(this.spawnAnimation)
{{ROM name|$81:832C}}(this.spawnAnimation_Normal)
this.sprite.type = MONSTER
this.sprite.type = MONSTER
this.wanderInRandomDirection()
this.wanderInRandomDirection_Normal()
this.health = 0
this.health = 0
{{ROM name|$80:8475}}(this.collisionHandler)
{{ROM name|$80:8475}}(this.collisionHandler)
Line 264: Line 290:
while (this.deathStatus == 0) {
while (this.deathStatus == 0) {
{{ROM name|$80:8353}}(2)
{{ROM name|$80:8353}}(2)
this.checkForTarget()
this.checkForTarget_Normal()
this.state()
this.state()
this.updateAnimation()
this.updateAnimation()
Line 279: Line 305:
}
}
spawnAnimation_Normal = {
spawnAnimation = { { 0xC98A, 10 }, { 0xC993, 10 }, { 0xC9A4, 10 }, { 0xC9B5, 10 },
{ 0xC9D6, 10 }, { 0xC9FF, 10 }, { 0, 0 } }
{ 0xC98A, 10 }, { 0xC993, 10 }, { 0xC9A4, 10 }, { 0xC9B5, 10 },
{ 0xC9D6, 10 }, { 0xC9FF, 10 }, { 0, 0 }
}
collisionHandler(spriteType) {
collisionHandler(spriteType) {
Line 305: Line 333:
}
}
return false
return false
}
}
main_Hard() {
{{RAM name|$7E:00DE}} += 0x14
this.cyclesUntilAnimUpdate = 7
this.init()
{{ROM name|$81:832C}}(this.spawnAnimation_Hard)
this.sprite.type = MONSTER
this.wanderInRandomDirection_Hard()
this.health = 0
this.dontTargetTimer = -1
{{ROM name|$80:8475}}(this.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 {
this.checkForTarget_Hard()
}
this.state()
this.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 = {
{ 0xC98A, 10 }, { 0xC993, 10 }, { 0xC9A4, 10 }, { 0xC9B5, 10 },
{ 0xC9D6, 10 }, { 0xC9FF, 10 }, { 0, 0 }
}
// Unused. Exactly equivalent to the other animations table
animations_Hard = {
{ 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() {
this.state = this.wanderingState_Hard
this.wanderingState_Hard()
}
wanderingState_Hard() {
this.newX = this.x + this.moveAmount_Hard[this.direction][0]
this.newY = this.y + this.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 {
this.followWall_Hard()
}
}
tryToMoveWhileTargeting() {
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() {
this.direction = ((this.direction - 1) + this.wallRotateAngle) % 8 + 1
this.setStateToFollowingWall_Hard()
}
wanderInRandomDirection_Hard() {
this.direction = ({{ROM name|$80:9D39}}() % 4) * 2 + 1
this.setStateToWandering_Hard()
}
setStateToFollowingWall_Hard() {
this.state = this.followingWallState_Hard
}
followingWallState_Hard() {
this.dontTargetTimer -= 1
this.newDirection = ((this.direction - 1) - this.wallRotateAngle) % 8 + 1
this.newX = this.x + this.moveAmount_Hard[this.newDirection][0]
this.newY = this.y + this.moveAmount_Hard[this.newDirection][1]
if (!this.newPositionIsBlocked()) {
this.direction = this.newDirection
}
this.newX = this.x + this.moveAmount_Hard[this.direction][0]
this.newY = this.y + this.moveAmount_Hard[this.direction][1]
if (!this.newPositionIsBlocked()) {
this.x = this.newX
this.sprite.x = this.newX
this.y = this.newY
this.sprite.y = this.newY
} else {
this.followWall_Hard()
}
}
unused818ACA() {
this.dontTargetTimer = 0
this.wanderInRandomDirection_Hard()
}
setStateToTargeting_Hard() {
this.state = this.targetingState_Hard
}
targetingState_Hard() {
distance, this.targetSprite = {{ROM name|$80:B123}}(this.x, this.y)
if (distance >= 180) {
this.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) {
this.wanderInRandomDirection_Hard()
return
}
this.targetDirection = this.direction
this.moveCounter = 1
while (this.moveCounter > 0) {
this.newX = this.x + this.moveAmount_Hard[this.targetDirection][0]
this.newY = this.y + this.moveAmount_Hard[this.targetDirection][1]
if (this.tryToMoveWhileTargeting()) {
this.endTargeting()
return
}
this.sprite.x = this.x
this.sprite.y = this.y
this.moveCounter -= 1
}
}
endTargeting() {
this.dontTargetTimer = 0
this.wanderInRandomDirection_Hard()
}
checkForTarget_Hard() {
this.dontTargetTimer -= 1
if (this.dontTargetTimer >= 0) {
return
}
distance = {{ROM name|$80:B123}}(this.x, this.y)
if (distance < 160) {
this.setStateToTargeting_Hard()
return
}
direction = {{ROM name|$80:B2A5}}(208, this.x, this.y)
if (direction == 0) {
this.deathStatus -= 1
}
}
}
}

Revision as of 02:58, 28 June 2024

Monster data
HP 1
Points 100
Entity data
Entity pointer $81:87F8 (normal)
$81:88CA (hard)

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.

Behavior

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

Major differences

  • 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

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.
  • 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

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.
  • 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 4.5 minutes, the timer will underflow. This will cause the zombie to stop targeting for around another 4.5 minutes until the timer reaches zero again.

RAM map

Entity arguments

Address Length Type Name Description
$00 2 int16 x X position
$02 2 int16 y Y position

Entity memory

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 = {
	{  0,  0 },
	{  0, -1 },
	{  1, -1 },
	{  1,  0 },
	{  1,  1 },
	{  0,  1 },
	{ -1,  1 },
	{ -1,  0 },
	{ -1, -1 }
}

moveAmount_Hard = {
	{  0,  0 },
	{  0, -2 },
	{  2, -2 },
	{  2,  0 },
	{  2,  2 },
	{  0,  2 },
	{ -2,  2 },
	{ -2,  0 },
	{ -2, -2 }
}

newPositionIsBlocked() {
	return bgSolidToMonsters(this.newX, this.newY) ||
	       touchingSpriteSolidToMonsters(this.sprite, this.newX, this.newY)
}

wanderInRandomDirection_Normal() {
	this.direction = (getRandomByte() % 4) * 2 + 1
	this.setStateToWandering_Normal()
}

setStateToWandering_Normal() {
	this.state = this.wanderingState_Normal
	this.wanderingState_Normal()
}

wanderingState_Normal() {
	this.newX = this.x + this.moveAmount_Normal[this.direction][0]
	this.newY = this.y + this.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 {
		this.followWall_Normal()
	}
}

followWall_Normal() {
	this.direction = ((this.direction - 1) + 2) % 8 + 1 // Rotate 90 degrees clockwise
	this.setStateToFollowingWall_Normal()
}

setStateToFollowingWall_Normal() {
	this.state = this.followingWallState_Normal
}

followingWallState_Normal() {
	this.newDirection = ((this.direction - 1) - 2) % 8 + 1 // Rotate 90 degrees counterclockwise
	this.newX = this.x + this.moveAmount_Normal[this.newDirection][0]
	this.newY = this.y + this.moveAmount_Normal[this.newDirection][1]
	if (!this.newPositionIsBlocked()) {
		this.direction = this.newDirection
	}

	this.newX = this.x + this.moveAmount_Normal[this.direction][0]
	this.newY = this.y + this.moveAmount_Normal[this.direction][1]
	if (!this.newPositionIsBlocked()) {
		this.x = this.newX
		this.sprite.x = this.newX
		this.y = this.newY
		this.sprite.y = this.newY
	} else {
		this.followWall_Normal()
	}
}

setStateToTargeting_Normal() {
	this.state = this.targetingState_Normal
}

targetingState_Normal() {
	distance, this.targetSprite = findTarget(this.x, this.y)
	if (distance >= 70) {
		this.wanderInRandomDirection_Normal()
		return
	}

	snapToTarget(this.sprite, this.targetSprite)
	this.direction = getDirectionToTarget(this.sprite, this.targetSprite)
	if (this.direction == 0) {
		this.wanderInRandomDirection_Normal()
		return
	}
	this.targetDirection = this.direction

	for (i = 0; i < 2; i++) {
		this.newX = this.x + this.moveAmount_Normal[this.targetDirection][0]
		this.newY = this.y + this.moveAmount_Normal[this.targetDirection][1]
		if (!this.newPositionIsBlocked()) {
			this.x = this.newX
			this.sprite.x = this.newX
			this.y = this.newY
			this.sprite.y = this.newY
		}
	}
}

checkForTarget_Normal() {
	distance = findTarget(this.x, this.y)
	if (distance < 65) {
		this.setStateToTargeting_Normal()
		return
	}

	direction = findPlayerInRange(208, this.x, this.y)
	if (direction == 0) {
		this.deathStatus -= 1
	}
}

killed() {
	givePoints(this.collidedSpriteType & 0x8000, 100)
	this.deathStatus = 0xF5F5
}

updateAnimation() {
	this.cyclesUntilAnimUpdate -= 1
	if (this.cyclesUntilAnimUpdate == 0) {
		this.cyclesUntilAnimUpdate = 4
		this.animationFrame = (this.animationFrame + 1) % 4

		this.sprite.tileData.lowBytes = this.animations[this.direction][this.animationFrame]
		this.sprite.tileData.bank = 0x90
		if (this.direction < DOWN_LEFT) {
			this.sprite.flipX = false
		} else {
			this.sprite.flipY = true
		}
	}
}

animations = {
	{ 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() {
	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() {
	$7E:00DE += 0x14
	this.cyclesUntilAnimUpdate = 7
	this.init()
	showAnimation(this.spawnAnimation_Normal)
	this.sprite.type = MONSTER
	this.wanderInRandomDirection_Normal()
	this.health = 0
	setCollisionHandler(this.collisionHandler)
	this.deathStatus = 0

	while (this.deathStatus == 0) {
		waitFrames(2)
		this.checkForTarget_Normal()
		this.state()
		this.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 = {
	{ 0xC98A, 10 }, { 0xC993, 10 }, { 0xC9A4, 10 }, { 0xC9B5, 10 },
	{ 0xC9D6, 10 }, { 0xC9FF, 10 }, { 0, 0 }
}

collisionHandler(spriteType) {
	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
			this.killed()
			return true
		} else if (newHealth != this.health) {
			this.health = newHealth
			return showDamageAnimation()
		}
		return false
	}
}

main_Hard() {
	$7E:00DE += 0x14
	this.cyclesUntilAnimUpdate = 7
	this.init()
	showAnimation(this.spawnAnimation_Hard)
	this.sprite.type = MONSTER
	this.wanderInRandomDirection_Hard()
	this.health = 0
	this.dontTargetTimer = -1
	setCollisionHandler(this.collisionHandler)
	this.deathStatus = 0

	while (this.deathStatus == 0) {
		waitFrames(2)
		if (this.collidedSpriteType != 0) {
			this.sprite.useAlternatePalette = false
			this.collidedSpriteType = 0
		} else {
			this.checkForTarget_Hard()
		}
		this.state()
		this.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 = {
	{ 0xC98A, 10 }, { 0xC993, 10 }, { 0xC9A4, 10 }, { 0xC9B5, 10 },
	{ 0xC9D6, 10 }, { 0xC9FF, 10 }, { 0, 0 }
}

// Unused. Exactly equivalent to the other animations table
animations_Hard = {
	{ 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() {
	this.state = this.wanderingState_Hard
	this.wanderingState_Hard()
}

wanderingState_Hard() {
	this.newX = this.x + this.moveAmount_Hard[this.direction][0]
	this.newY = this.y + this.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 {
		this.followWall_Hard()
	}
}

tryToMoveWhileTargeting() {
	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() {
	this.direction = ((this.direction - 1) + this.wallRotateAngle) % 8 + 1
	this.setStateToFollowingWall_Hard()
}

wanderInRandomDirection_Hard() {
	this.direction = (getRandomByte() % 4) * 2 + 1
	this.setStateToWandering_Hard()
}

setStateToFollowingWall_Hard() {
	this.state = this.followingWallState_Hard
}

followingWallState_Hard() {
	this.dontTargetTimer -= 1

	this.newDirection = ((this.direction - 1) - this.wallRotateAngle) % 8 + 1
	this.newX = this.x + this.moveAmount_Hard[this.newDirection][0]
	this.newY = this.y + this.moveAmount_Hard[this.newDirection][1]
	if (!this.newPositionIsBlocked()) {
		this.direction = this.newDirection
	}

	this.newX = this.x + this.moveAmount_Hard[this.direction][0]
	this.newY = this.y + this.moveAmount_Hard[this.direction][1]
	if (!this.newPositionIsBlocked()) {
		this.x = this.newX
		this.sprite.x = this.newX
		this.y = this.newY
		this.sprite.y = this.newY
	} else {
		this.followWall_Hard()
	}
}

unused818ACA() {
	this.dontTargetTimer = 0
	this.wanderInRandomDirection_Hard()
}

setStateToTargeting_Hard() {
	this.state = this.targetingState_Hard
}

targetingState_Hard() {
	distance, this.targetSprite = findTarget(this.x, this.y)
	if (distance >= 180) {
		this.wanderInRandomDirection_Hard()
		return
	}

	snapToTarget(this.sprite, this.targetSprite)
	this.direction = getDirectionToTarget(this.sprite, this.targetSprite)
	if (this.direction == 0) {
		this.wanderInRandomDirection_Hard()
		return
	}
	this.targetDirection = this.direction

	this.moveCounter = 1
	while (this.moveCounter > 0) {
		this.newX = this.x + this.moveAmount_Hard[this.targetDirection][0]
		this.newY = this.y + this.moveAmount_Hard[this.targetDirection][1]
		
		if (this.tryToMoveWhileTargeting()) {
			this.endTargeting()
			return
		}
		this.sprite.x = this.x
		this.sprite.y = this.y
		
		this.moveCounter -= 1
	}
}

endTargeting() {
	this.dontTargetTimer = 0
	this.wanderInRandomDirection_Hard()
}

checkForTarget_Hard() {
	this.dontTargetTimer -= 1
	if (this.dontTargetTimer >= 0) {
		return
	}

	distance = findTarget(this.x, this.y)
	if (distance < 160) {
		this.setStateToTargeting_Hard()
		return
	}

	direction = findPlayerInRange(208, this.x, this.y)
	if (direction == 0) {
		this.deathStatus -= 1
	}
}