Martian: Difference between revisions

From ZAMN Hacking
Content deleted Content added
No edit summary
mNo edit summary
 
(13 intermediate revisions by 2 users not shown)
Line 1: Line 1:
{{Infobox Monster|img=Martian.png|hp=1|points=200}}
{{Infobox Monster
|img=Martian.png
{{Infobox Entity|entity_pointer=$81:99DF (normal)<br/>$81:9A3E (top of screen)}}
|hp=1
The martian is a [[respawning monster]]. There are two different types of martians: normal and top of screen.
|points=200
|special=Bubbles players
|resists=-
|weak_to=-
}}

{{Infobox Entity
|entity_pointer=$81:99DF
|pointer1_type=normal
|entity_pointer2=$81:9A3E
|pointer2_type=top of screen
}}

The martian is a [[respawning monster]]. Martians mainly use their [[martian bubble gun|bubble gun]] to attack, but can also hurt players and [[victim]]s via contact damage. They aren't resistant or vulnerable to any weapon and are exclusively found in the [[grass]] tileset. They only have 1 hit point and award 200 points to the player when killed. There are two different types of martians: normal and top of screen.


== Behavior ==
== Behavior ==
Line 39: Line 53:
Note that both types of martians will target [[decoys]], but will not attempt to shoot at them.
Note that both types of martians will target [[decoys]], but will not attempt to shoot at them.


{{BPMN_embed|BPMN:Martian state diagram}}
{{BPMN_embed|BPMN:Martian state diagram|248}}

== Animation ==

WIP.


== Bugs ==
== Bugs ==


* [[Fire extinguisher|Freezing]] or [[Bubble gun|bubbling]] a martian, then letting it despawn will count as killing it. This is because the logic that determines if a martian was killed or not checks if it was ever hit by a weapon, not if it lost all its HP.
* [[Fire extinguisher|Freezing]] or [[Bubble gun|bubbling]] a martian, then letting it despawn will count as killing it. This will increase their kill count in the final tally and award the player the corresponding amount of points. This is because the logic that determines if a martian was killed or not checks if it was ever hit by a weapon, not if it lost all its HP.
* Top of screen martians check if their updated spawn location is solid, and immediately despawn if it is. However, this check only looks for tiles that are solid to players, not solid to monsters. So, if the martian starts on a tile that is solid to monsters, but not players, it will be allowed to spawn, but will be unable to move.
* Top of screen martians check if their updated spawn location is solid, and immediately despawn if it is. However, this check only looks for tiles that are solid to players, not solid to monsters. So, if the martian starts on a tile that is solid to monsters, but not players, it will be allowed to spawn, but will be unable to move.

== Trivia ==

* There are 55 normal martian spawners and 20 top of the screen martian spawners in the game.
* Shooting martians 10 times with the [[martian bubble gun]] will award the player with the [[martian bubbled bonus]]. Strangely, this [[bonus]] is only enabled in [[level 41]].
* The official manual mentions that martians hate [[tomatoes]], but this isn't reflected in-game in any way.
* The martian design is likely based on the aliens from the 1957 movie [https://en.wikipedia.org/wiki/Invasion_of_the_Saucer_Men Invasion of the Saucer Men].


== RAM map ==
== RAM map ==
Line 116: Line 141:
|}
|}


== Pseudocode ==
{{Pseudocode header}}
spriteTableDefault = { // $81:9969

spriteTableDefault = {
0xD66D, 0xD69E, 0xD6C7, 0xD6F8,
0xD66D, 0xD69E, 0xD6C7, 0xD6F8,
0xD721, 0xD752, 0xD783, 0xD7B4,
0xD721, 0xD752, 0xD783, 0xD7B4,
Line 124: Line 148:
}
}
shootHandlerDefault(direction) {
shootHandlerDefault(direction) { // $81:9981
bubbleGunShot.args.direction = direction
bubbleGunShot.args.direction = direction
bubbleGunShot.args.x = this.x
bubbleGunShot.args.x = this.x
Line 132: Line 156:
if (this.shotThrottle == 0) {
if (this.shotThrottle == 0) {
this.shotThrottle = 60
this.shotThrottle = 60
this.setSprite(this.shootSpriteIndexes[direction])
setSprite(shootSpriteIndexes[direction])
if (direction < DOWN) {
if (direction < DOWN) {
this.sprite.flipX = false
this.sprite.flipX = false
Line 146: Line 170:
}
}
shootSpriteIndexes = { 0, 8, 0, 0, 0, 4, 0, 0, 0 }
shootSpriteIndexes = { 0, 8, 0, 0, 0, 4, 0, 0, 0 } // $81:99CD
main_Normal() {
main_Normal() { // $81:99DF
{{RAM name|$7E:00DE}} += 0x1E
{{RAM name|$7E:00DE}} += 0x1E
this.init()
init()
this.setStateToNormal()
setStateToNormal()
while (!this.dead) {
while (!this.dead) {
Line 161: Line 185:
{{ROM name|$80:C7D9}}(this.collidedSpriteType & 0x8000, 200)
{{ROM name|$80:C7D9}}(this.collidedSpriteType & 0x8000, 200)
{{RAM name|$7E:1F72}} += 1
{{RAM name|$7E:1F72}} += 1
{{ROM name|$81:832C}}(this.deathAnimation_Normal)
{{ROM name|$81:832C}}(deathAnimation_Normal)
}
}
Line 169: Line 193:
}
}
deathAnimation_Normal = {
deathAnimation_Normal = { // $81:9A2A
{ 0xD998, 5}, { 0xD96F, 5 }, { 0xD946, 5 }, { 0xDA2B, 5 },
{ 0xD998, 5}, { 0xD96F, 5 }, { 0xD946, 5 }, { 0xDA2B, 5 },
{ 0, 0 }
{ 0, 0 }
}
}
main_TopOfScreen() {
main_TopOfScreen() { // $81:9A3E
if (this.tryToSpawnAtTopOfScreen()) {
if (tryToSpawnAtTopOfScreen()) {
return
return
}
}
{{RAM name|$7E:00DE}} += 0x1E
{{RAM name|$7E:00DE}} += 0x1E
this.init()
init()
this.setStateToTopOfScreen()
setStateToTopOfScreen()
while (!this.dead) {
while (!this.dead) {
Line 191: Line 215:
{{ROM name|$80:C7D9}}(this.collidedSpriteType & 0x8000, 200)
{{ROM name|$80:C7D9}}(this.collidedSpriteType & 0x8000, 200)
{{RAM name|$7E:1F72}} += 1
{{RAM name|$7E:1F72}} += 1
{{ROM name|$81:832C}}(this.deathAnimation_TopOfScreen)
{{ROM name|$81:832C}}(deathAnimation_TopOfScreen)
}
}
Line 199: Line 223:
}
}
deathAnimation_TopOfScreen = {
deathAnimation_TopOfScreen = { // $81:9A8E
{ 0xD998, 5}, { 0xD96F, 5 }, { 0xD946, 5 }, { 0xDA2B, 5 },
{ 0xD998, 5}, { 0xD96F, 5 }, { 0xD946, 5 }, { 0xDA2B, 5 },
{ 0, 0 }
{ 0, 0 }
}
}
tryToSpawnAtTopOfScreen() {
tryToSpawnAtTopOfScreen() { // $81:9AA2
args.Y = {{RAM name|$7E:1B6C}} + 8
args.Y = {{RAM name|$7E:1B6C}} + 8
return {{ROM name|$80:AE14}}(args.x, args.y) || {{ROM name|$80:B422}}(args.x, args.y)
return {{ROM name|$80:AE14}}(args.x, args.y) || {{ROM name|$80:B422}}(args.x, args.y)
}
}
init() {
init() { // $81:9AC0
this.sprite = {{ROM name|$80:BE0C}}()
this.sprite = {{ROM name|$80:BE0C}}()
this.x = args.x
this.x = args.x
Line 233: Line 257:
this.health = 0
this.health = 0
this.setShootHandler(this.shootHandlerDefault)
this.setShootHandler(shootHandlerDefault)
this.setSpriteTable(this.spriteTableDefault)
this.setSpriteTable(spriteTableDefault)
{{ROM name|$81:832C}}(this.spawnAnimation)
{{ROM name|$81:832C}}(spawnAnimation)
{{ROM name|$80:8475}}(this.collisionHandler)
{{ROM name|$80:8475}}(collisionHandler)
}
}
spawnAnimation = {
spawnAnimation = { // $81:9B3B
{ 0xD8BA, 6 }, { 0xD8D3, 6 }, { 0xD8F4, 6 }, { 0xD91D, 6 }
{ 0xD8BA, 6 }, { 0xD8D3, 6 }, { 0xD8F4, 6 }, { 0xD91D, 6 }
{ 0xD946, 6 }, { 0xD96F, 6 }, { 0xD998, 6 }, { 0xD9C9, 4 }
{ 0xD946, 6 }, { 0xD96F, 6 }, { 0xD998, 6 }, { 0xD9C9, 4 }
Line 245: Line 269:
}
}
collisionHandler(spriteType) {
collisionHandler(spriteType) { // $81:9B6B
if (spriteType < SQUIRT_GUN) {
if (spriteType < SQUIRT_GUN) {
return false
return false
Line 272: Line 296:
}
}
setSpriteTable(spriteTable) {
setSpriteTable(spriteTable) { // $81:9B6B
this.spriteTable = spriteTable
this.spriteTable = spriteTable
}
}
setShootHandler(shootHandler) {
setShootHandler(shootHandler) { // $81:9BBD
this.shootHandler = shootHandler
this.shootHandler = shootHandler
}
}
getDirectionToLineUpWithTarget() {
getDirectionToLineUpWithTarget() { // $81:9BC0
this.targetXDistance = abs(this.targetXOffset)
this.targetXDistance = abs(this.targetXOffset)
if (this.targetXDistance <= abs(this.targetYOffset)) {
if (this.targetXDistance <= abs(this.targetYOffset)) {
Line 301: Line 325:
}
}
move(direction) {
move(direction) { // $81:9BF3
if ({{RAM name|$7E:0020}} % 4 == 0) {
if ({{RAM name|$7E:0020}} % 4 == 0) {
return
return
}
}
this.newX = this.x + this.moveAmount[direction][0]
this.newX = this.x + moveAmount[direction][0]
this.newY = this.y + this.moveAmount[direction][1]
this.newY = this.y + moveAmount[direction][1]
if (!{{ROM name|$80:AE97}}(this.newX, this.y) &&
if (!{{ROM name|$80:AE97}}(this.newX, this.y) &&
Line 323: Line 347:
}
}
moveAmount = {
moveAmount = { // $81:9C62
{ 0, 0 },
{ 0, 0 },
{ 0, -2 },
{ 0, -2 },
Line 335: Line 359:
}
}
oppositeDirection = { NONE, DOWN, DOWN_LEFT, LEFT, UP_LEFT, UP, UP_RIGHT, RIGHT, DOWN_RIGHT }
oppositeDirection = { NONE, DOWN, DOWN_LEFT, LEFT, UP_LEFT, UP, UP_RIGHT, RIGHT, DOWN_RIGHT } // $81:9C86
setSprite(index) {
setSprite(index) { // $81:9C8F
this.sprite.tileData.lowBytes = this.spriteTable[index]
this.sprite.tileData.lowBytes = this.spriteTable[index]
}
}
updateAnimation_Normal() {
updateAnimation_Normal() { // $81:9C99
index = this.walkingAnimations[this.direction][this.animationFrame]
index = walkingAnimations[this.direction][this.animationFrame]
if (index >= 12) {
if (index >= 12) {
index = 8
index = 8
}
}
this.setSprite(index)
setSprite(index)
if (this.direction < DOWN) {
if (this.direction < DOWN) {
this.sprite.flipX = false
this.sprite.flipX = false
Line 361: Line 385:
}
}
walkingAnimations = {
walkingAnimations = { // $81:9CD7
{ 4, 5, 6, 7 },
{ 4, 5, 6, 7 },
{ 8, 9, 10, 11 },
{ 8, 9, 10, 11 },
Line 373: Line 397:
}
}
shoot(direction) {
shoot(direction) { // $81:9D1F
if (this.shootHandler != null) {
if (this.shootHandler != null) {
this.shootHandler(direction)
this.shootHandler(direction)
Line 379: Line 403:
}
}
setStateToNormal() {
setStateToNormal() { // $81:9D2A
this.state = this.normalState
this.state = normalState
}
}
normalState() {
normalState() { // $81:9D30
direction = {{ROM name|$80:B379}}(this.x, this.y)
direction = {{ROM name|$80:B379}}(this.x, this.y)
if (direction != NONE) {
if (direction != NONE) {
this.shoot(direction)
shoot(direction)
}
}
Line 397: Line 421:
if (this.targetDistance < 60) {
if (this.targetDistance < 60) {
{{ROM name|$80:B3F1}}(this.sprite, this.targetSprite)
{{ROM name|$80:B3F1}}(this.sprite, this.targetSprite)
this.direction =
this.direction = oppositeDirection&#91;{{ROM name|$80:B22A}}(this.sprite, this.targetSprite)]
this.oppositeDirection&#91;{{ROM name|$80:B22A}}(this.sprite, this.targetSprite)]
} else if (this.targetDistance < 80) {
} else if (this.targetDistance < 80) {
this.direction = this.getDirectionToLineUpWithTarget()
this.direction = getDirectionToLineUpWithTarget()
} else if (this.targetDistance < 224) {
} else if (this.targetDistance < 224) {
{{ROM name|$80:B3F1}}(this.sprite, this.targetSprite)
{{ROM name|$80:B3F1}}(this.sprite, this.targetSprite)
Line 413: Line 436:
}
}
this.move(this.direction)
move(this.direction)
this.updateAnimation_Normal()
updateAnimation_Normal()
}
}
setStateToTopOfScreen() {
setStateToTopOfScreen() { // $81:9DB1
this.state = this.topOfScreenState
this.state = topOfScreenState
this.framesUntilSideUpdate = 0
this.framesUntilSideUpdate = 0
this.targetSide = 0
this.targetSide = 0
}
}
topOfScreenState() {
topOfScreenState() { // $81:9DBB
direction = {{ROM name|$80:B2A5}}(208, this.x, this.y)
direction = {{ROM name|$80:B2A5}}(208, this.x, this.y)
if (direction == NONE) {
if (direction == NONE) {
Line 429: Line 452:
}
}
if (this.anyPlayerAbove()) {
if (anyPlayerAbove()) {
this.animationFrame = 0
this.animationFrame = 0
this.framesUntilAnimUpdate = 0
this.framesUntilAnimUpdate = 0
this.setStateToNormal()
setStateToNormal()
return
return
}
}
Line 438: Line 461:
direction = {{ROM name|$80:B379}}(this.x, this.y)
direction = {{ROM name|$80:B379}}(this.x, this.y)
if (direction != NONE) {
if (direction != NONE) {
this.shoot(direction)
shoot(direction)
} else if ({{ROM name|$80:9D39}}() < 30) {
} else if ({{ROM name|$80:9D39}}() < 30) {
this.shoot(DOWN)
shoot(DOWN)
}
}
Line 450: Line 473:
if (this.targetDistance < 32) {
if (this.targetDistance < 32) {
{{ROM name|$80:B3F1}}(this.sprite, this.targetSprite)
{{ROM name|$80:B3F1}}(this.sprite, this.targetSprite)
direction = this.oppositeDirection&#91;{{ROM name|$80:B22A}}(this.sprite, this.targetSprite)]
direction = oppositeDirection&#91;{{ROM name|$80:B22A}}(this.sprite, this.targetSprite)]
} else {
} else {
{{ROM name|$80:B3F1}}(this.sprite, this.targetSprite)
{{ROM name|$80:B3F1}}(this.sprite, this.targetSprite)
Line 470: Line 493:
}
}
moveTopOfScreen() { // $81:9E40 (this is in the middle of code that is part of the above subroutine)
moveTopOfScreen() {
move(this.direction)
move(this.direction)
this.updateAnimation_TopOfScreen()
updateAnimation_TopOfScreen()
}
}
updateAnimation_TopOfScreen() {
updateAnimation_TopOfScreen() { // $81:9E8D
this.framesUntilAnimUpdate -= 1
this.framesUntilAnimUpdate -= 1
if (this.framesUntilAnimUpdate < 0) {
if (this.framesUntilAnimUpdate < 0) {
Line 481: Line 504:
this.animationFrame = (this.animationFrame + 1) % 4
this.animationFrame = (this.animationFrame + 1) % 4
this.setSprite(this.topOfScreenAnimation[this.animationFrame])
setSprite(topOfScreenAnimation[this.animationFrame])
}
}
}
}
topOfScreenAnimation = { 4, 5, 6, 7 }
topOfScreenAnimation = { 4, 5, 6, 7 } // $81:9EA7
anyPlayerAbove() {
anyPlayerAbove() { // $81:9EAF
if ({{RAM name|$7E:00D2}}[0] != null && {{RAM name|$7E:00D2}}[0].y < this.y) {
if ({{RAM name|$7E:00D2}}[0] != null && {{RAM name|$7E:00D2}}[0].y < this.y) {
return true
return true
Line 496: Line 519:
return false
return false
}
}
{{Pseudocode footer}}


[[Category:Respawning monster]]
[[Category:Respawning monster]]
[[Category:Bug]]

Latest revision as of 18:46, 17 August 2024

Monster data
HP 1
Points 200
Special Bubbles players
Resists -
Weak to -
Entity data
Entity pointer $81:99DF (normal)
$81:9A3E (top of screen)

The martian is a respawning monster. Martians mainly use their bubble gun to attack, but can also hurt players and victims via contact damage. They aren't resistant or vulnerable to any weapon and are exclusively found in the grass tileset. They only have 1 hit point and award 200 points to the player when killed. There are two different types of martians: normal and top of screen.

Behavior[edit]

Normal martian[edit]

Normal martian behavior areas.
Red: Move away
Yellow: Line up
Pink: Move towards
Blue: Keep previous direction
Outside all colors: Despawn
Light area in center is the size of the screen

Normal martians will try to stay about 70 pixels away from a target, while lining themselves up for a shot on the target. However, the relatively large amount of time between direction updates means that they generally just dart around diagonally in the vicinity of the target.

  • Every frame, attempt to shoot at a player or victim if there is one within 8 pixels of the martian on a single axis. Every 61 attempts, a shot will actually be fired.
    • When a shot is fired, stand in place for 12 frames.
  • Every 61 frames (about once per second) update the movement direction:
    • If the nearest target is less than 60 pixels away, move away from the target.
    • If the nearest target is between 60 and 79 pixels away, move in the direction of the shortest distance to line up with the target on a single axis.
    • If the nearest target is between 80 and 223 pixels away, move towards the target.
    • If none of the above is true and there is no player within 207 pixels on a single axis, then despawn.
  • Move in the current direction at a speed of 1.5 px/frame.

Top of screen martian[edit]

Top of screen martians will move side to side above a target, trying to maintain about 108 pixels of space vertically. Upon spawning, they will set their Y position to 8 pixels below the top of the screen.

  • If there is no player within 207 pixels on a single axis, despawn.
  • If there is a player above the martian, convert into a normal martian.
  • Every frame, attempt to shoot at a player or victim if there is one within 8 pixels of the martian on a single axis. Every 61 attempts, a shot will actually be fired.
    • If a shot was not attempted in this way, randomly attempt to shoot down with probability 30/256 (about 11.72%).
    • When a shot is fired, stand in place for 12 frames.
  • Every 33 frames, update horizontal movement direction:
    • If the nearest target is less than 32 pixels away, move away from the target.
    • If the nearest target is 32 pixels or more away, move towards the target.
  • Move based on the current horizontal movement direction:
    • If the nearest target is less than 96 pixels below the martian, move up-left or up-right at a speed of 3 px/frame.
    • If the nearest target is between 96 and 119 pixels below the martian, move left or right at a speed of 1.5 px/frame.
    • If the nearest target is 120 pixels or more below the martian, move down-left or down-right at a speed of 3 px/frame.

Note that both types of martians will target decoys, but will not attempt to shoot at them.

Animation[edit]

WIP.

Bugs[edit]

  • Freezing or bubbling a martian, then letting it despawn will count as killing it. This will increase their kill count in the final tally and award the player the corresponding amount of points. This is because the logic that determines if a martian was killed or not checks if it was ever hit by a weapon, not if it lost all its HP.
  • Top of screen martians check if their updated spawn location is solid, and immediately despawn if it is. However, this check only looks for tiles that are solid to players, not solid to monsters. So, if the martian starts on a tile that is solid to monsters, but not players, it will be allowed to spawn, but will be unable to move.

Trivia[edit]

  • There are 55 normal martian spawners and 20 top of the screen martian spawners in the game.
  • Shooting martians 10 times with the martian bubble gun will award the player with the martian bubbled bonus. Strangely, this bonus is only enabled in level 41.
  • The official manual mentions that martians hate tomatoes, but this isn't reflected in-game in any way.
  • The martian design is likely based on the aliens from the 1957 movie Invasion of the Saucer Men.

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 int16 x X position
$0E 2 int16 y Y position
$10 2 int16 newX New X position
$12 2 int16 newY New Y position
$14 2 pointer16 spriteTable Pointer to sprite table
$16 2 pointer16 targetSprite Pointer to target sprite
$18 2 uint16 targetDistance Distance to target
$1A 2 unused Unused
$1C 2 int16 targetXOffset Target sprite X offset from martian
$1E 2 int16 targetYOffset Target sprite Y offset from martian
$20 2 uint16 targetXDistance Target sprite X distance from martian (absolute value of $1C)
$22 2 optional pointer16 shootHandler 'Shoot' subroutine pointer
$24 2 uint16 0-based shotThrottle Number of shot attempts until shot will be fired
$26 2 boolean dead Dead
$28 2 direction x2 direction Current direction
$2A 2 int16 0-based directionChangeTimer Number of frames until direction update (normal state only)
$2C 2 uint16 animationFrame Current animation frame
$2E 2 int16 0-based framesUntilAnimUpdate Number of frames until next animation frame
$30 2 sprite type collidedSpriteType Type of weapon shot sprite collided with
$32 2 int16 0-based health Health
$34 2 int16 0-based framesUntilSideUpdate Number of frames until target side update (top of screen only)
$36 2 direction x2 targetSide Target side (left or right) (top of screen only)
$7E 2 int16 0-based freezeTimer Amount of time to stay frozen

Pseudocode

spriteTableDefault = { // $81:9969
	0xD66D, 0xD69E, 0xD6C7, 0xD6F8,
	0xD721, 0xD752, 0xD783, 0xD7B4,
	0xD7E5, 0xD816, 0xD847, 0xD878
}

shootHandlerDefault(direction) { // $81:9981
	bubbleGunShot.args.direction = direction
	bubbleGunShot.args.x = this.x
	bubbleGunShot.args.y = this.y
	bubbleGunShot.args.character = -1

	if (this.shotThrottle == 0) {
		this.shotThrottle = 60
		setSprite(shootSpriteIndexes[direction])
		if (direction < DOWN) {
			this.sprite.flipX = false
		} else {
			this.sprite.flipX = true
		}
		
		createEntity($81:F380)
		waitFrames(12)
	} else {
		this.shotThrottle -= 1
	}
}

shootSpriteIndexes = { 0, 8, 0, 0, 0, 4, 0, 0, 0 } // $81:99CD

main_Normal() { // $81:99DF
	$7E:00DE += 0x1E
	init()
	setStateToNormal()

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

	if (this.collidedSpriteType != 0) {
		givePoints(this.collidedSpriteType & 0x8000, 200)
		martiansKilled += 1
		showAnimation(deathAnimation_Normal)
	}

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

deathAnimation_Normal = { // $81:9A2A
	{ 0xD998, 5}, { 0xD96F, 5 }, { 0xD946, 5 }, { 0xDA2B, 5 },
	{ 0, 0 }
}

main_TopOfScreen() { // $81:9A3E
	if (tryToSpawnAtTopOfScreen()) {
		return
	}

	$7E:00DE += 0x1E
	init()
	setStateToTopOfScreen()

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

	if (this.collidedSpriteType != 0) {
		givePoints(this.collidedSpriteType & 0x8000, 200)
		martiansKilled += 1
		showAnimation(deathAnimation_TopOfScreen)
	}

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

deathAnimation_TopOfScreen = { // $81:9A8E
	{ 0xD998, 5}, { 0xD96F, 5 }, { 0xD946, 5 }, { 0xDA2B, 5 },
	{ 0, 0 }
}

tryToSpawnAtTopOfScreen() { // $81:9AA2
	args.Y = cameraY + 8
	return bgSolidToPlayers(args.x, args.y) || outsideOfLevel(args.x, args.y)
}

init() { // $81:9AC0
	this.sprite = createSprite()
	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:D66D
	this.sprite.entity = currentEntity
	this.sprite.type = MONSTER
	this.sprite.visible = true
	this.sprite.alternatePalette = 6
	this.sprite.type = MONSTER

	this.shotThrottle = 0
	this.animationFrame = 0
	this.framesUntilAnimUpdate = 0
	this.shootHandler = null
	this.dead = false
	this.directionChangeTimer = 0
	this.freezeTimer = 0
	this.collidedSpriteType = 0
	this.health = 0

	this.setShootHandler(shootHandlerDefault)
	this.setSpriteTable(spriteTableDefault)
	showAnimation(spawnAnimation)
	setCollisionHandler(collisionHandler)
}

spawnAnimation = { // $81:9B3B
	{ 0xD8BA, 6 }, { 0xD8D3, 6 }, { 0xD8F4, 6 }, { 0xD91D, 6 }
	{ 0xD946, 6 }, { 0xD96F, 6 }, { 0xD998, 6 }, { 0xD9C9, 4 }
	{ 0xD9FA, 4 }, { 0xDA2B, 4 }, { 0xD66D, 1 }, { 0, 0 }
}

collisionHandler(spriteType) { // $81:9B6B
	if (spriteType < SQUIRT_GUN) {
		return false
	}
	this.collidedSpriteType = spriteType

	weaponType = spriteType & 0x7FFF
	if (weaponType == MARTIAN_BUBBLE_GUN) {
		martiansBubbled[playerForCharacter((spriteType & 0x8000) >> 15)] += 1
		return bubbleMonster()
	} else if (weaponType == FIRE_EXTINGUISHER) {
		return freezeMonster()
	} else {
		newHealth = this.health - weaponDamage[weaponType - SQUIRT_GUN]
		if (newHealth < 0) {
			this.dead = true
			this.health = newHealth
			this.freezeTimer = 0
			return true
		} else if (newHealth != this.health) {
			this.health = newHealth
			return showDamageAnimation()
		}
		return false
	}
}

setSpriteTable(spriteTable) { // $81:9B6B
	this.spriteTable = spriteTable
}

setShootHandler(shootHandler) { // $81:9BBD
	this.shootHandler = shootHandler
}

getDirectionToLineUpWithTarget() { // $81:9BC0
	this.targetXDistance = abs(this.targetXOffset)
	if (this.targetXDistance <= abs(this.targetYOffset)) {
		if (this.targetXOffset == 0) {
			return NONE
		} else if (this.targetXOffset < 0) {
			return LEFT
		} else {
			return RIGHT
		}
	} else {
		if (this.targetYOffset == 0) {
			return NONE
		} else if (this.targetYOffset < 0) {
			return UP
		} else {
			return DOWN
		}
	}
}

move(direction) { // $81:9BF3
	if (frameCounter % 4 == 0) {
		return
	}

	this.newX = this.x + moveAmount[direction][0]
	this.newY = this.y + moveAmount[direction][1]

	if (!bgSolidToMonsters(this.newX, this.y) &&
	    !outsideOfLevel(this.newX, this.y) &&
	    !touchingSpriteSolidToMonsters(this.sprite, this.newX, this.y)) {
		this.x = this.newX
	}
	if (!bgSolidToMonsters(this.x, this.newY) &&
	    !outsideOfLevel(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:9C62
	{  0,  0 },
	{  0, -2 },
	{  2, -2 },
	{  2,  0 },
	{  2,  2 },
	{  0,  2 },
	{ -2,  2 },
	{ -2,  0 },
	{ -2, -2 }
}

oppositeDirection = { NONE, DOWN, DOWN_LEFT, LEFT, UP_LEFT, UP, UP_RIGHT, RIGHT, DOWN_RIGHT } // $81:9C86

setSprite(index) { // $81:9C8F
	this.sprite.tileData.lowBytes = this.spriteTable[index]
}

updateAnimation_Normal() { // $81:9C99
	index = walkingAnimations[this.direction][this.animationFrame]
	if (index >= 12) {
		index = 8
	}

	setSprite(index)
	if (this.direction < DOWN) {
		this.sprite.flipX = false
	} else {
		this.sprite.flipX = true
	}

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

walkingAnimations = { // $81:9CD7
	{ 4,  5,  6,  7  },
	{ 8,  9,  10, 11 },
	{ 0,  1,  2,  3  },
	{ 0,  1,  2,  3  },
	{ 0,  1,  2,  3  },
	{ 4,  5,  6,  7  },
	{ 0,  1,  2,  3  },
	{ 0,  1,  2,  3  },
	{ 0,  1,  2,  3  }
}

shoot(direction) { // $81:9D1F
	if (this.shootHandler != null) {
		this.shootHandler(direction)
	}
}

setStateToNormal() { // $81:9D2A
	this.state = normalState
}

normalState() { // $81:9D30
	direction = findShootDirection(this.x, this.y)
	if (direction != NONE) {
		shoot(direction)
	}

	this.directionChangeTimer -= 1
	if (this.directionChangeTimer < 0) {
		this.directionChangeTimer = 60

		this.targetDistance, this.targetSprite, this.targetXOffset, this.targetYOffset =
		    findTarget(this.x, this.y)
		if (this.targetDistance < 60) {
			snapToTarget(this.sprite, this.targetSprite)
			this.direction = oppositeDirection[getDirectionToTarget(this.sprite, this.targetSprite)]
		} else if (this.targetDistance < 80) {
			this.direction = getDirectionToLineUpWithTarget()
		} else if (this.targetDistance < 224) {
			snapToTarget(this.sprite, this.targetSprite)
			this.direction = getDirectionToTarget(this.sprite, this.targetSprite)
		} else {
			direction = findPlayerInRangeDirection(208, this.x, this.y)
			if (direction == NONE) {
				this.dead = true
			}
			return
		}
	}

	move(this.direction)
	updateAnimation_Normal()
}

setStateToTopOfScreen() { // $81:9DB1
	this.state = topOfScreenState
	this.framesUntilSideUpdate = 0
	this.targetSide = 0
}

topOfScreenState() { // $81:9DBB
	direction = findPlayerInRangeDirection(208, this.x, this.y)
	if (direction == NONE) {
		this.dead = true
	}

	if (anyPlayerAbove()) {
		this.animationFrame = 0
		this.framesUntilAnimUpdate = 0
		setStateToNormal()
		return
	}

	direction = findShootDirection(this.x, this.y)
	if (direction != NONE) {
		shoot(direction)
	} else if (getRandomByte() < 30) {
		shoot(DOWN)
	}

	this.framesUntilSideUpdate -= 1
	if (this.framesUntilSideUpdate < 0) {
		this.framesUntilSideUpdate = 32

		this.targetDistance, this.targetSprite = findTarget(this.x, this.y)
		if (this.targetDistance < 32) {
			snapToTarget(this.sprite, this.targetSprite)
			direction = oppositeDirection[getDirectionToTarget(this.sprite, this.targetSprite)]
		} else {
			snapToTarget(this.sprite, this.targetSprite)
			direction = getDirectionToTarget(this.sprite, this.targetSprite)
		}
		this.targetSide = direction < DOWN ? RIGHT : LEFT
	}

	this.direction = this.targetSide
	targetYDistance = abs(this.targetSprite.y - this.y)
	if (targetYDistance < 96) {
		this.direction = this.targetSide < DOWN ? UP_RIGHT : UP_LEFT
		moveTopOfScreen()
	} else if (targetYDistance >= 120) {
		this.direction = this.targetSide < DOWN ? DOWN_RIGHT : DOWN_LEFT
		moveTopOfScreen()
	}
	moveTopOfScreen()
}

moveTopOfScreen() { // $81:9E40 (this is in the middle of code that is part of the above subroutine)
	move(this.direction)
	updateAnimation_TopOfScreen()
}

updateAnimation_TopOfScreen() { // $81:9E8D
	this.framesUntilAnimUpdate -= 1
	if (this.framesUntilAnimUpdate < 0) {
		this.framesUntilAnimUpdate = 4

		this.animationFrame = (this.animationFrame + 1) % 4
		setSprite(topOfScreenAnimation[this.animationFrame])
	}
}

topOfScreenAnimation = { 4, 5, 6, 7 } // $81:9EA7

anyPlayerAbove() { // $81:9EAF
	if (playerSprites[0] != null && playerSprites[0].y < this.y) {
		return true
	}
	if (playerSprites[1] != null && playerSprites[1].y < this.y) {
		return true
	}
	return false
}