Werewolf: Difference between revisions

From ZAMN Hacking
Content deleted Content added
No edit summary
No edit summary
 
(10 intermediate revisions by 2 users not shown)
Line 1:
{{Infobox Monster|img=Werewolf.png|hp=11|points=300}}
|img=Werewolf.png
{{Infobox Entity|entity_pointer=$81:ABF5}}
|hp=11
The werewolf is a [[respawning monster]]. Werewolves will only spawn if the level has [[palette fade]] that has completed, though this condition can be bypassed by using $81:ABFB as the entity pointer instead.
|points=300
|special=Jumps over obstacles
|resists=-
|weak_to=[[File:Silverware HUD.png|frameless|link=silverware]]
}}
 
{{Infobox Entity
|entity_pointer=$81:ABF5
}}
 
The werewolf is a [[respawning monster]]. Werewolves attack via contact damage and by clawing at players and [[victim]]s. They can jump long distances and over walls and obstacles. They are vulnerable to [[silverware]], which kills them in one hit, and can be found in the [[grass]], [[castle]] and [[office/cave]] tilesets. They have 11 hit points and award 300 points to the player when killed. Werewolves will only spawn if the level has a [[palette fade]] that has completed, though this condition can be bypassed by using $81:ABFB as the entity pointer instead. Additionally, [[tourists]] present in a level after a palette fade has completed will transform into werewolves, though this can be prevented by changing byte [X] to FF in the level palette.
 
== Behavior ==
Line 20 ⟶ 31:
* '''Attacking:''' Perform a 27 frame attack animation. During the last 12 frames of this, there is an additional sprite hitbox created 25 pixels to the left or right of the werewolf. Once complete, go back to the walking state.
 
{{BPMN_embed|BPMN:Werewolf state diagram|377}}
 
== Weapon damageAnimation ==
 
WIP.
Werewolves take normal damage from all weapons except [[silverware]], which kills them in one hit.
 
== Bugs ==
Line 35 ⟶ 46:
* Werewolves only despawn if they are exactly 360 pixels away from the nearest targetable sprite. Since players move at 2 px/frame orthogonally, the distance can go directly from 359 to 361, causing the werewolf not to despawn.
* It's not clear if this is a bug or intended behavior, but werewolves will never jump straight up or down. This is because their target position would be exactly on top of the sprite they are trying to jump at, and since all target sprites are solid to monsters, the jump is not allowed.
 
== Trivia ==
 
* There are a total of 56 werewolf spawners in the game.
 
== RAM map ==
Line 42 ⟶ 57:
{| class="wikitable"
|-
! Address !! Length !! [[Data types|Type]] !! Name !! Description
|-
| $00 || 2 || int16 || x || X position
|-
| $02 || 2 || int16 || y || Y position
|}
 
Line 53 ⟶ 68:
{| class="wikitable"
|-
! Address !! Length !! [[Data types|Type]] !! Name !! Description
|-
| $08 || 2 || pointer16 || sprite || Pointer to sprite
|-
| $0A || 2 || pointer16 || state || Current state subroutine pointer
|-
| $0C || 2 || uint16 || animationFrame || Current animation frame (x2 during attack animation)
|-
| $0E || 2 || int16 0-based || framesUntilAnimUpdate || Number of frames until next animation frame
|-
| $10 || 2 || boolean || dead || Dead
|-
| $12 || 2 || int16 || x || X position
|-
| $14 || 2 || int16 || y || Y position
|-
| $16 || 2 || int16 || newX || New X position
|-
| $18 || 2 || int16 || newY || New Y position
|-
| $1A || 2 || direction || direction || Current direction
|-
| $1C || 2 || unused || || Unused
|-
| $1E || 2 || optional pointer16 || jumpTargetSprite || Pointer to sprite that is being jumped at (0xFFFF if empty)
|-
| $20 || 2 || int16 || jumpTargetX || Jump target X position
|-
| $22 || 2 || int16 || jumpTargetY || Jump target Y position
|-
| $24 || 2 || int16 x4 || zVelocity || Z velocity during jump
|-
| $26 || 2 || uint16 || jumpDuration || Duration of jump in frames
|-
| $28 || 2 || uint16 || xFraction || Fractional portion of X position during jump (full X position = $12 + ($28/$26))
|-
| $2A || 2 || uint16 || yFraction || Fractional portion of Y position during jump (full Y position = $14 + ($2A/$26))
|-
| $2C || 2 || uint16 || jumpXMagnitude || Jump distance X magnitude
|-
| $2E || 2 || uint16 || jumpYMagnitude || Jump distance Y magnitude
|-
| $30 || 2 || int16 || jumpXSign || Jump distance X sign (1 if jump is right, -1 if left)
|-
| $32 || 2 || int16 || jumpYSign || Jump distance Y sign (1 if jump is down, -1 if up)
|-
| $34 || 2 || direction || jumpDirection || Jump direction (only left or right due to a [[#Bugs|bug]])
|-
| $36 || 2 || uint16 || temp || Temporary storage
|-
| $38 || 2 || optional pointer16 || attackSprite || Extra attack hitbox sprite (0xFFFF if empty)
|-
| $3A || 2 || int16 0-based || prevHealth || Previous health
|-
| $3C || 2 || int16 0-based || health || Health
|-
| $3E || 2 || sprite type || collidedSpriteType || Type of weapon shot sprite collided with
|-
| $40 || 2 || unused || || Unused
|-
| $42 || 2 || pointer16 || targetSprite || Pointer to target sprite
|-
| $44 || 2 || uint16 || targetDistance || Distance to target sprite
|- class="breakrow"
| $7E || 2 || unusedint16 0-based || ValuefreezeTimer is|| setAmount butof nottime usedto stay [[Fire extinguisher|frozen]]
|}
 
{{Pseudocode header}}
init() { // $81:A643
{{ROM name|$81:8000}}()
this.x = args.x
this.y = args.y
this.sprite.tileData = $90:E9D8
this.sprite.alternatePalette = 6
this.sprite.visible = true
this.sprite.type = MONSTER
this.animationFrame = 0
this.framesUntilAnimUpdate = 0
this.attackSprite = -1
this.jumpTargetSprite = -1
this.freezeTimer = 0
this.collidedSpriteType = NONE
this.dead = false
{{ROM name|$80:8353}}(20)
this.health = 10
this.prevHealth = 10
setStateToWalking()
}
updateWalkingAnimation() { // $81:A694
if (this.direction == 0) {
return
}
this.sprite.tileData.bank = 0x90
this.sprite.tileData.lowBytes = walkingAnimations[this.direction - 1][this.animationFrame][1]
this.sprite.flipX = walkingAnimations[this.direction - 1][this.animationFrame][0]
}
walkingAnimations = { // $81:A6C1
{ { false, 0xEA3A }, { false, 0xEA6B }, { false, 0xEA9C }, { false, 0xEACD } },
{ { false, 0xE8B2 }, { false, 0xE8E3 }, { false, 0xE914 }, { false, 0xE945 } },
{ { false, 0xE8B2 }, { false, 0xE8E3 }, { false, 0xE914 }, { false, 0xE945 } },
{ { false, 0xE8B2 }, { false, 0xE8E3 }, { false, 0xE914 }, { false, 0xE945 } },
{ { false, 0xE976 }, { false, 0xE9A7 }, { false, 0xE9D8 }, { false, 0xEA09 } },
{ { true , 0xE8B2 }, { true , 0xE8E3 }, { true , 0xE914 }, { true , 0xE945 } },
{ { true , 0xE8B2 }, { true , 0xE8E3 }, { true , 0xE914 }, { true , 0xE945 } },
{ { true , 0xE8B2 }, { true , 0xE8E3 }, { true , 0xE914 }, { true , 0xE945 } }
}
walk() { // $81:A741
if ({{ROM name|$80:9D39}}() & 2 == 0) {
doWalk()
}
doWalk()
}
doWalk() { // $81:A74D
if (this.targetDistance == 360) {
this.dead = true
this.collidedSpriteType = NONE
return
}
{{ROM name|$80:B3F1}}(this.sprite, this.targetSprite)
this.direction = {{ROM name|$80:B22A}}(this.sprite, this.targetSprite)
if (this.direction == NONE) {
return
}
this.newX = this.sprite.x + moveAmount[this.direction][0]
this.newY = this.sprite.y + moveAmount[this.direction][1]
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
}
this.sprite.x = this.x
this.sprite.y = this.y
}
moveAmount = { // $81:A7C2
{ 0, 0 },
{ 0, -1 },
{ 1, -1 },
{ 1, 0 },
{ 1, 1 },
{ 0, 1 },
{ -1, 1 },
{ -1, 0 },
{ -1, -1 }
}
canJumpToTarget() { // $81:A7E6
// Note: the following function is bugged to only return LEFT, RIGHT, or NONE
this.jumpDirection = {{ROM name|$80:B1EC}}(this.sprite, this.jumpTargetX, this.jumpTargetY)
offset = this.jumpTargetX - this.x
this.jumpXMagnitude = abs(offset)
this.jumpXSign = offset < 0 ? -1 : 1
if (this.jumpXMagnitude == 19) {
return false
}
offset = this.jumpTargetY - this.y
this.jumpYMagnitude = abs(offset)
this.jumpYSign = offset < 0 ? -1 : 1
greaterMagnitude = max(this.jumpXMagnitude, this.jumpYMagnitude)
this.jumpDuration = greaterMagnitude / 4
this.zVelocity = greaterMagnitude / 8
if (!{{ROM name|$80:AF66}}(this.jumpTargetX, this.jumpTargetY) &&
!{{ROM name|$80:BF67}}(this.sprite, this.jumpTargetX, this.jumpTargetY) &&
!{{ROM name|$80:B422}}(this.jumpTargetX, this.jumpTargetY))
this.xFraction = 0
this.yFraction = 0
return true, this.zVelocity, this.jumpDirection
}
return false
}
updateJumpZ() { // $81:A85A
this.sprite.z += this.zVelocity / 4
this.zVelocity -= 1
}
updateJumpPosition() { // $81:A874
f = this.xFraction + this.jumpXMagnitude
while (f >= this.jumpDuration) {
this.x += this.jumpXSign
f -= this.jumpDuration
}
this.xFraction = f
f = this.yFraction + this.jumpYMagnitude
while (f >= this.jumpDuration) {
this.y += this.jumpYSign
f -= this.jumpDuration
}
this.yFraction = f
}
checkIfShouldJump() { // $81:A8A7
if (health != prevHealth) {
this.temp = {{ROM name|$80:9D39}}()
this.jumpTargetX = ({{ROM name|$80:9D39}}() % 32) + 20 // Random value 20-51
this.jumpTargetY = ({{ROM name|$80:9D39}}() % 32) + 15 // Random value 15-46
if (this.temp & 1 != 0) {
this.jumpTargetX = -this.jumpTargetX
}
if (this.temp & 2 != 0) {
this.jumpTargetY = -this.jumpTargetY
}
this.jumpTargetX += this.x
this.jumpTargetY += this.y
this.prevHealth = this.health
this.jumpTargetSprite = -1
} else {
if ({{ROM name|$80:9D39}}() >= 15 || this.targetDistance >= 325) {
return
}
this.jumpTargetSprite = this.targetSprite
this.jumpTargetX = this.jumpTargetSprite.x
this.jumpTargetY = this.jumpTargetSprite.y
if (abs(this.jumpTargetX - this.x) + abs(this.jumpTargetY - this.y) < 70) {
return
}
this.jumpTargetX += jumpXSpacing[this.direction]
if (this.jumpTargetSprite.type != VICTIM) {
this.jumpTargetX += nonVictimJumpAdditionalXSpacing[this.direction]
}
}
canJump, zVelocity, jumpDirection = canJumpToTarget()
if (canJump) {
this.zVelocity = zVelocity
this.direction = jumpDirection
setStateToPreJump()
}
}
nonVictimJumpAdditionalXSpacing = { 0, 0, 96, 96, 96, 0, -96, -96, -96 } // $81:A96A
jumpXSpacing = { 0, 0, 28, 28, 28, 0, -28, -28, -28 } // $81:A97C
setStateToPreJump() { // $81:A98E
this.state = preJumpState
}
preJumpState() { // $81:A994
this.sprite.flipX = preJumpFlipPerDirection[this.direction]
{{ROM name|$81:832C}}(preJumpAnimation)
setStateToJumping()
}
preJumpAnimation = { { 0xEAFE, 7 }, { 0xEB27, 5 }, { 0xEB58, 5 }, { 0, 0 } } // $81:A9B3
preJumpFlipPerDirection = { false, false, false, false, false, false, true, true, true } // $81:A9C3
setStateToJumping() { // $81:A9D5
this.state = jumpingState
this.sprite.tileData.lowBytes = 0xEB89
this.sprite.visible = true
this.sprite.bgPriority = true
this.sprite.type = NONE
}
jumpingState() { // $81:A9F0
updateJumpZ()
updateJumpPosition()
this.sprite.x = this.x
this.sprite.y = this.y
if (this.sprite.z == 0) {
if (this.jumpTargetSprite >= 0 && abs(this.jumpTargetSprite.x - this.jumpTargetX) < 9) {
this.jumpTargetX += jumpLandingOffsets[this.direction * 2]
this.jumpTargetY += jumpLandingOffsets[this.direction * 2 + 1]
}
this.x = this.jumpTargetX
this.sprite.x = this.jumpTargetX
this.y = this.jumpTargetY
this.sprite.y = this.jumpTargetY
setStateToPostJump()
}
}
jumpLandingOffsets = { // $81:AA49
0,
15, -9,
15, -9,
15, 0,
15, 8,
15, 8,
-15, -9,
-15, 0,
-15, -9
}
setStateToPostJump() { // $81:AA6B
this.state = postJumpState
}
postJumpState() { // $81:AA71
this.sprite.bgPriority = false
this.sprite.type = MONSTER
{{ROM name|$81:832C}}(postJumpAnimation)
if (canAttack()) {
setStateToAttacking()
} else {
{{ROM name|$80:8353}}(({{ROM name|$80:9D39}} % 16) + 1)
setStateToWalking()
}
}
postJumpAnimation = { { 0xEBC2, 5 }, { 0xEBFB, 5 }, { 0xEC24, 5 }, { 0, 0 } } // $81:AA9E
setStateToAttacking() { // $81:AAAE
this.state = attackingState
this.attackSprite = createAttackSprite()
this.framesUntilAnimUpdate = 0
this.animationFrame = 0
this.sprite.flipX = attackFlipPerDirection[this.direction]
}
attackingState() { // $81:AACF
this.framesUntilAnimUpdate -= 1
if (this.framesUntilAnimUpdate < 0) {
this.framesUntilAnimUpdate = 2
this.animationFrame += 1
tileData = attackAnimation[this.animationFrame]
if (tileData != 0) {
this.sprite.tileData.lowBytes = tileData
if (this.animationFrame >= 6) {
this.attackSprite.visible = true
}
} else {
{{ROM name|$80:BE41}}(this.attackSprite)
this.attackSprite = -1
setStateToWalking()
}
}
}
attackAnimation = { // $81:AB0A
0, 0xEC55, 0xEC7E, 0xECAF, 0xECF8, 0xED29, 0xED52, 0xED8B
0xEDBC, 0xEDF5, 0
}
attackFlipPerDirection = { false, false, false, false, false, false, true, true, true } // $81:AB20
createAttackSprite() { // $81:AB32
sprite = {{ROM name|$80:BE0C}}()
sprite.x = this.sprite.x + attackSpriteXOffsetPerDirection[this.direction]
sprite.z = 0
sprite.y = this.sprite.y
sprite.type = MONSTER
sprite.entity = {{RAM name|$7E:0008}}
sprite.tileData = null
return sprite
}
attackSpriteXOffsetPerDirection = { 0, 0, -25, -25, -25, 0, 25, 25, 25 } // $81:AB6B
canAttack() { // $81:AB7D
if (this.targetDistance >= 325) {
return false
}
if (abs(this.targetSprite.y - this.y) >= 4) {
return false
}
xOffset = this.targetSprite.x - this.x
if (abs(xOffset) >= 29) {
return false
}
this.direction = xOffset >= 0 ? LEFT : RIGHT // Note: this is backwards
return true
}
setStateToWalking() { // $81:ABB6
{{ROM name|$80:8475}}(collisionHandler)
this.state = walkingState
}
walkingState() { // $81:ABC6
this.targetDistance, this.targetSprite = {{ROM name|$80:B123}}(this.x, this.y)
walk()
this.framesUntilAnimUpdate -= 1
if (framesUntilAnimUpdate < 0) {
this.framesUntilAnimUpdate = 3
this.animationFrame = (this.animationFrame + 1) % 4
updateWalkingAnimation()
}
checkIfShouldJump()
if (canAttack()) {
setStateToAttacking()
}
}
main() { // $81:ABF5
if (!{{RAM name|$7E:1F94}}) {
return
}
{{RAM name|$7E:00DE}} += 0x1C
if ({{RAM name|$7E:1F52}} == 1) {
{{ROM name|$80:CC3B}}(0x2B)
}
init()
while (!this.dead) {
{{ROM name|$80:8353}}(1)
this.state()
}
{{ROM name|$80:8475}}(null)
if (this.attackSprite != -1) {
{{ROM name|$80:BE41}}(this.attackSprite)
}
if (this.collidedSpriteType != NONE) {
{{ROM name|$80:C7D9}}(this.collidedSpriteType & 0x8000, 300)
{{RAM name|$7E:1F80}} += 1
this.sprite.type = NONE
this.sprite.tileData.bank = 0x8F
{{ROM name|$80:CC3B}}(0x2D)
{{ROM name|$81:832C}}(deathAnimation)
}
{{RAM name|$7E:00DE}} -= 0x1C
while ({{RAM name|$7E:00DE}} < 0) { }
{{ROM name|$80:BE41}}(this.sprite)
}
deathAnimation = { // $81:AC7A
{ 0xDA43, 12 }, { 0xDA7C, 12 }, { 0xDACD, 12 }, { 0xDB26, 12 },
{ 0xDB6F, 12 }, { 0, 0 }
}
collisionHandler(spriteType) { // $81:AC92
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 if (weaponType == SILVERWARE) {
return killed(weaponType)
} else {
newHealth = this.health - {{ROM name|$81:8561}}[weaponType - SQUIRT_GUN]
if (newHealth < 0) {
return killed(newHealth)
} else if (newHealth != this.health) {
this.health = newHealth
return {{ROM name|$81:8506}}()
}
return false
}
}
killed(newHealth) { // $81:ACC6 (this is in the middle of code that is part of the above subroutine)
this.dead = true
this.health = newHealth
this.freezeTimer = 0
return true
}
{{Pseudocode footer}}
 
[[Category:Respawning monster]]
[[Category:MonsterBug]]
[[Category:Entity]]

Latest revision as of 18:49, 17 August 2024

Monster data
HP 11
Points 300
Special Jumps over obstacles
Resists -
Weak to
Entity data
Entity pointer $81:ABF5

The werewolf is a respawning monster. Werewolves attack via contact damage and by clawing at players and victims. They can jump long distances and over walls and obstacles. They are vulnerable to silverware, which kills them in one hit, and can be found in the grass, castle and office/cave tilesets. They have 11 hit points and award 300 points to the player when killed. Werewolves will only spawn if the level has a palette fade that has completed, though this condition can be bypassed by using $81:ABFB as the entity pointer instead. Additionally, tourists present in a level after a palette fade has completed will transform into werewolves, though this can be prevented by changing byte [X] to FF in the level palette.

Behavior[edit]

Werewolves can be in one of three states:

  • Walking: Move towards the nearest targetable sprite at an average rate of 1.5 px/frame. Werewolves start in this state.
    • If the werewolf is exactly 360 pixels away from the target, then despawn.
    • If the werewolf's health has changed, jump in a random diagonal direction a distance of 20-51 pixels on the X axis and 15-46 pixels on the Y axis.
    • If the werewolf is between 70 and 344 pixels of the target, then there is a 15/255 (5.88%) chance to jump at the target.
      • If the target is a victim, jump 28 pixels past the target on the X axis.
      • Otherwise, jump 124 pixels past the target on the X axis.
    • If the werewolf is less than 4 pixels from the target on the Y axis and less than 29 pixels on the X axis, then attack.
  • Jumping: Jumping is implemented as three separate states:
    • Pre-jump: Do a 17 frame pre-jump animation.
    • Main jump: Move at a constant speed towards the destination point. The total duration of the jump (in frames) is the distance on the larger of the two axes divided by 4. The werewolf has no collision during this phase. After the jump is complete, the game attempts to adjust the werewolf's position if it lands too close to the target, but this is bugged.
    • Post-jump: Do a 15 frame landing animation. Then attack if the werewolf is less than 4 pixels from the target on the Y axis and less than 29 pixels on the X axis. If not attacking, wait a random amount of time from 1 to 16 frames, then go back to the walking state.
  • Attacking: Perform a 27 frame attack animation. During the last 12 frames of this, there is an additional sprite hitbox created 25 pixels to the left or right of the werewolf. Once complete, go back to the walking state.

Animation[edit]

WIP.

Bugs[edit]

  • Werewolves attempt to adjust their position upon landing if they are within 9 pixels of the target on the X axis. It is intended to move an additional 15 pixels on the X axis and 8 pixels on the Y axis, but there are several bugs in the implementation.
    • The position offset table has the wrong values for the down-left entry (uses values for up-left instead).
    • The table is indexed incorrectly and reads the wrong values for each direction.
    • The function that determines the direction of the jump is bugged to only return left, right, or none. This bug actually prevents reading past the end of the table due to the incorrect indexing.
    • The end result of these bugs is that the werewolf will move down 15 pixels if the jump was to the right, and up 15 pixels if it was to the left.
  • Werewolves only despawn if they are exactly 360 pixels away from the nearest targetable sprite. Since players move at 2 px/frame orthogonally, the distance can go directly from 359 to 361, causing the werewolf not to despawn.
  • It's not clear if this is a bug or intended behavior, but werewolves will never jump straight up or down. This is because their target position would be exactly on top of the sprite they are trying to jump at, and since all target sprites are solid to monsters, the jump is not allowed.

Trivia[edit]

  • There are a total of 56 werewolf spawners 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 pointer16 state Current state subroutine pointer
$0C 2 uint16 animationFrame Current animation frame (x2 during attack animation)
$0E 2 int16 0-based framesUntilAnimUpdate Number of frames until next animation frame
$10 2 boolean dead Dead
$12 2 int16 x X position
$14 2 int16 y Y position
$16 2 int16 newX New X position
$18 2 int16 newY New Y position
$1A 2 direction direction Current direction
$1C 2 unused Unused
$1E 2 optional pointer16 jumpTargetSprite Pointer to sprite that is being jumped at (0xFFFF if empty)
$20 2 int16 jumpTargetX Jump target X position
$22 2 int16 jumpTargetY Jump target Y position
$24 2 int16 x4 zVelocity Z velocity during jump
$26 2 uint16 jumpDuration Duration of jump in frames
$28 2 uint16 xFraction Fractional portion of X position during jump (full X position = $12 + ($28/$26))
$2A 2 uint16 yFraction Fractional portion of Y position during jump (full Y position = $14 + ($2A/$26))
$2C 2 uint16 jumpXMagnitude Jump distance X magnitude
$2E 2 uint16 jumpYMagnitude Jump distance Y magnitude
$30 2 int16 jumpXSign Jump distance X sign (1 if jump is right, -1 if left)
$32 2 int16 jumpYSign Jump distance Y sign (1 if jump is down, -1 if up)
$34 2 direction jumpDirection Jump direction (only left or right due to a bug)
$36 2 uint16 temp Temporary storage
$38 2 optional pointer16 attackSprite Extra attack hitbox sprite (0xFFFF if empty)
$3A 2 int16 0-based prevHealth Previous health
$3C 2 int16 0-based health Health
$3E 2 sprite type collidedSpriteType Type of weapon shot sprite collided with
$40 2 unused Unused
$42 2 pointer16 targetSprite Pointer to target sprite
$44 2 uint16 targetDistance Distance to target sprite
$7E 2 int16 0-based freezeTimer Amount of time to stay frozen

Pseudocode

init() { // $81:A643
	createMonsterSprite()
	this.x = args.x
	this.y = args.y
	this.sprite.tileData = $90:E9D8
	this.sprite.alternatePalette = 6
	this.sprite.visible = true
	this.sprite.type = MONSTER
	this.animationFrame = 0
	this.framesUntilAnimUpdate = 0
	this.attackSprite = -1
	this.jumpTargetSprite = -1
	this.freezeTimer = 0
	this.collidedSpriteType = NONE
	this.dead = false
	waitFrames(20)
	this.health = 10
	this.prevHealth = 10
	setStateToWalking()
}

updateWalkingAnimation() { // $81:A694
	if (this.direction == 0) {
		return
	}
	this.sprite.tileData.bank = 0x90
	this.sprite.tileData.lowBytes = walkingAnimations[this.direction - 1][this.animationFrame][1]
	this.sprite.flipX = walkingAnimations[this.direction - 1][this.animationFrame][0]
}

walkingAnimations = { // $81:A6C1
	{ { false, 0xEA3A }, { false, 0xEA6B }, { false, 0xEA9C }, { false, 0xEACD } },
	{ { false, 0xE8B2 }, { false, 0xE8E3 }, { false, 0xE914 }, { false, 0xE945 } },
	{ { false, 0xE8B2 }, { false, 0xE8E3 }, { false, 0xE914 }, { false, 0xE945 } },
	{ { false, 0xE8B2 }, { false, 0xE8E3 }, { false, 0xE914 }, { false, 0xE945 } },
	{ { false, 0xE976 }, { false, 0xE9A7 }, { false, 0xE9D8 }, { false, 0xEA09 } },
	{ { true , 0xE8B2 }, { true , 0xE8E3 }, { true , 0xE914 }, { true , 0xE945 } },
	{ { true , 0xE8B2 }, { true , 0xE8E3 }, { true , 0xE914 }, { true , 0xE945 } },
	{ { true , 0xE8B2 }, { true , 0xE8E3 }, { true , 0xE914 }, { true , 0xE945 } }
}

walk() { // $81:A741
	if (getRandomByte() & 2 == 0) {
		doWalk()
	}
	doWalk()
}

doWalk() { // $81:A74D
	if (this.targetDistance == 360) {
		this.dead = true
		this.collidedSpriteType = NONE
		return
	}

	snapToTarget(this.sprite, this.targetSprite)
	this.direction = getDirectionToTarget(this.sprite, this.targetSprite)
	if (this.direction == NONE) {
		return
	}

	this.newX = this.sprite.x + moveAmount[this.direction][0]
	this.newY = this.sprite.y + moveAmount[this.direction][1]
	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
	}
	this.sprite.x = this.x
	this.sprite.y = this.y
}

moveAmount = { // $81:A7C2
	{  0,  0 },
	{  0, -1 },
	{  1, -1 },
	{  1,  0 },
	{  1,  1 },
	{  0,  1 },
	{ -1,  1 },
	{ -1,  0 },
	{ -1, -1 }
}

canJumpToTarget() { // $81:A7E6
	// Note: the following function is bugged to only return LEFT, RIGHT, or NONE
	this.jumpDirection = getDirectionToTargetPosition(this.sprite, this.jumpTargetX, this.jumpTargetY)

	offset = this.jumpTargetX - this.x
	this.jumpXMagnitude = abs(offset)
	this.jumpXSign = offset < 0 ? -1 : 1
	if (this.jumpXMagnitude == 19) {
		return false
	}

	offset = this.jumpTargetY - this.y
	this.jumpYMagnitude = abs(offset)
	this.jumpYSign = offset < 0 ? -1 : 1

	greaterMagnitude = max(this.jumpXMagnitude, this.jumpYMagnitude)
	this.jumpDuration = greaterMagnitude / 4
	this.zVelocity = greaterMagnitude / 8

	if (!bgSolidToSpecialMonsters(this.jumpTargetX, this.jumpTargetY) &&
	    !touchingSpriteSolidToMonsters(this.sprite, this.jumpTargetX, this.jumpTargetY) &&
	    !outsideOfLevel(this.jumpTargetX, this.jumpTargetY))
		this.xFraction = 0
		this.yFraction = 0
		return true, this.zVelocity, this.jumpDirection
	}
	return false
}

updateJumpZ() { // $81:A85A
	this.sprite.z += this.zVelocity / 4
	this.zVelocity -= 1
}

updateJumpPosition() { // $81:A874
	f = this.xFraction + this.jumpXMagnitude
	while (f >= this.jumpDuration) {
		this.x += this.jumpXSign
		f -= this.jumpDuration
	}
	this.xFraction = f

	f = this.yFraction + this.jumpYMagnitude
	while (f >= this.jumpDuration) {
		this.y += this.jumpYSign
		f -= this.jumpDuration
	}
	this.yFraction = f
}

checkIfShouldJump() { // $81:A8A7
	if (health != prevHealth) {
		this.temp = getRandomByte()
		this.jumpTargetX = (getRandomByte() % 32) + 20 // Random value 20-51
		this.jumpTargetY = (getRandomByte() % 32) + 15 // Random value 15-46
		if (this.temp & 1 != 0) {
			this.jumpTargetX = -this.jumpTargetX
		}
		if (this.temp & 2 != 0) {
			this.jumpTargetY = -this.jumpTargetY
		}
		this.jumpTargetX += this.x
		this.jumpTargetY += this.y
		this.prevHealth = this.health
		this.jumpTargetSprite = -1
	} else {
		if (getRandomByte() >= 15 || this.targetDistance >= 325) {
			return
		}

		this.jumpTargetSprite = this.targetSprite
		this.jumpTargetX = this.jumpTargetSprite.x
		this.jumpTargetY = this.jumpTargetSprite.y
		if (abs(this.jumpTargetX - this.x) + abs(this.jumpTargetY - this.y) < 70) {
			return
		}
		this.jumpTargetX += jumpXSpacing[this.direction]
		if (this.jumpTargetSprite.type != VICTIM) {
			this.jumpTargetX += nonVictimJumpAdditionalXSpacing[this.direction]
		}
	}

	canJump, zVelocity, jumpDirection = canJumpToTarget()
	if (canJump) {
		this.zVelocity = zVelocity
		this.direction = jumpDirection
		setStateToPreJump()
	}
}

nonVictimJumpAdditionalXSpacing = { 0, 0, 96, 96, 96, 0, -96, -96, -96 } // $81:A96A
jumpXSpacing = { 0, 0, 28, 28, 28, 0, -28, -28, -28 } // $81:A97C

setStateToPreJump() { // $81:A98E
	this.state = preJumpState
}

preJumpState() { // $81:A994
	this.sprite.flipX = preJumpFlipPerDirection[this.direction]
	showAnimation(preJumpAnimation)
	setStateToJumping()
}

preJumpAnimation = { { 0xEAFE, 7 }, { 0xEB27, 5 }, { 0xEB58, 5 }, { 0, 0 } } // $81:A9B3

preJumpFlipPerDirection = { false, false, false, false, false, false, true, true, true } // $81:A9C3

setStateToJumping() { // $81:A9D5
	this.state = jumpingState
	this.sprite.tileData.lowBytes = 0xEB89
	this.sprite.visible = true
	this.sprite.bgPriority = true
	this.sprite.type = NONE
}

jumpingState() { // $81:A9F0
	updateJumpZ()
	updateJumpPosition()
	this.sprite.x = this.x
	this.sprite.y = this.y

	if (this.sprite.z == 0) {
		if (this.jumpTargetSprite >= 0 && abs(this.jumpTargetSprite.x - this.jumpTargetX) < 9) {
			this.jumpTargetX += jumpLandingOffsets[this.direction * 2]
			this.jumpTargetY += jumpLandingOffsets[this.direction * 2 + 1]
		}
		this.x = this.jumpTargetX
		this.sprite.x = this.jumpTargetX
		this.y = this.jumpTargetY
		this.sprite.y = this.jumpTargetY
		setStateToPostJump()
	}
}

jumpLandingOffsets = { // $81:AA49
	 0,
	 15, -9,
	 15, -9,
	 15,  0,
	 15,  8,
	 15,  8,
	-15, -9,
	-15,  0,
	-15, -9
}

setStateToPostJump() { // $81:AA6B
	this.state = postJumpState
}

postJumpState() { // $81:AA71
	this.sprite.bgPriority = false
	this.sprite.type = MONSTER
	showAnimation(postJumpAnimation)

	if (canAttack()) {
		setStateToAttacking()
	} else {
		waitFrames((getRandomByte % 16) + 1)
		setStateToWalking()
	}
}

postJumpAnimation = { { 0xEBC2, 5 }, { 0xEBFB, 5 }, { 0xEC24, 5 }, { 0, 0 } } // $81:AA9E

setStateToAttacking() { // $81:AAAE
	this.state = attackingState
	this.attackSprite = createAttackSprite()
	this.framesUntilAnimUpdate = 0
	this.animationFrame = 0
	this.sprite.flipX = attackFlipPerDirection[this.direction]
}

attackingState() { // $81:AACF
	this.framesUntilAnimUpdate -= 1
	if (this.framesUntilAnimUpdate < 0) {
		this.framesUntilAnimUpdate = 2
		this.animationFrame += 1

		tileData = attackAnimation[this.animationFrame]
		if (tileData != 0) {
			this.sprite.tileData.lowBytes = tileData
			if (this.animationFrame >= 6) {
				this.attackSprite.visible = true
			}
		} else {
			deleteSprite(this.attackSprite)
			this.attackSprite = -1
			setStateToWalking()
		}
	}
}

attackAnimation = { // $81:AB0A
	0, 0xEC55, 0xEC7E, 0xECAF, 0xECF8, 0xED29, 0xED52, 0xED8B
	0xEDBC, 0xEDF5, 0
}

attackFlipPerDirection = { false, false, false, false, false, false, true, true, true } // $81:AB20

createAttackSprite() { // $81:AB32
	sprite = createSprite()
	sprite.x = this.sprite.x + attackSpriteXOffsetPerDirection[this.direction]
	sprite.z = 0
	sprite.y = this.sprite.y
	sprite.type = MONSTER
	sprite.entity = currentEntity
	sprite.tileData = null
	return sprite
}

attackSpriteXOffsetPerDirection = { 0, 0, -25, -25, -25, 0, 25, 25, 25 } // $81:AB6B

canAttack() { // $81:AB7D
	if (this.targetDistance >= 325) {
		return false
	}

	if (abs(this.targetSprite.y - this.y) >= 4) {
		return false
	}
	xOffset = this.targetSprite.x - this.x
	if (abs(xOffset) >= 29) {
		return false
	}
	this.direction = xOffset >= 0 ? LEFT : RIGHT // Note: this is backwards
	return true
}

setStateToWalking() { // $81:ABB6
	setCollisionHandler(collisionHandler)
	this.state = walkingState
}

walkingState() { // $81:ABC6
	this.targetDistance, this.targetSprite = findTarget(this.x, this.y)
	walk()

	this.framesUntilAnimUpdate -= 1
	if (framesUntilAnimUpdate < 0) {
		this.framesUntilAnimUpdate = 3
		this.animationFrame = (this.animationFrame + 1) % 4
		updateWalkingAnimation()
	}

	checkIfShouldJump()
	if (canAttack()) {
		setStateToAttacking()
	}
}

main() { // $81:ABF5
	if (!paletteFadeComplete) {
		return
	}

	$7E:00DE += 0x1C
	if (currentExtraSounds == 1) {
		playSound(0x2B)
	}
	init()

	while (!this.dead) {
		waitFrames(1)
		this.state()
	}

	setCollisionHandler(null)
	if (this.attackSprite != -1) {
		deleteSprite(this.attackSprite)
	}
	if (this.collidedSpriteType != NONE) {
		givePoints(this.collidedSpriteType & 0x8000, 300)
		werewolvesKilled += 1
		this.sprite.type = NONE
		this.sprite.tileData.bank = 0x8F
		playSound(0x2D)
		showAnimation(deathAnimation)
	}

	$7E:00DE -= 0x1C
	while ($7E:00DE < 0) { }
	deleteSprite(this.sprite)
}

deathAnimation = { // $81:AC7A
	{ 0xDA43, 12 }, { 0xDA7C, 12 }, { 0xDACD, 12 }, { 0xDB26, 12 },
	{ 0xDB6F, 12 }, { 0, 0 }
}

collisionHandler(spriteType) { // $81:AC92
	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 if (weaponType == SILVERWARE) {
		return killed(weaponType)
	} else {
		newHealth = this.health - weaponDamage[weaponType - SQUIRT_GUN]
		if (newHealth < 0) {
			return killed(newHealth)
		} else if (newHealth != this.health) {
			this.health = newHealth
			return showDamageAnimation()
		}
		return false
	}
}

killed(newHealth) { // $81:ACC6 (this is in the middle of code that is part of the above subroutine)
	this.dead = true
	this.health = newHealth
	this.freezeTimer = 0
	return true
}