Evil doll axe: Difference between revisions

From ZAMN Hacking
Content deleted Content added
No edit summary
No edit summary
 
(6 intermediate revisions by the same user not shown)
Line 1: Line 1:
{{Infobox Entity|entity_pointer=$81:B592}}
{{Infobox Entity|img=Evil doll axe.png|entity_pointer=$81:B4EA}}


An evil doll axe is an entity that is created by an [[evil doll]] whenever it throws an axe.
An evil doll axe is an entity that is created by an [[evil doll]] whenever it throws an axe.
Line 15: Line 15:
# The "down" direction is incorrectly mapped to "right"
# The "down" direction is incorrectly mapped to "right"
The result of these two bugs is that all orthogonal directions are mapped to the animation for "right", and all diagonal directions are mapped to the animation for "down-right". For orthogonal directions, this doesn't matter since they all share the same animation anyway. However, the down-left and up-left animations supposed to show the axe rotating counter-clockwise, but don't.
The result of these two bugs is that all orthogonal directions are mapped to the animation for "right", and all diagonal directions are mapped to the animation for "down-right". For orthogonal directions, this doesn't matter since they all share the same animation anyway. However, the down-left and up-left animations supposed to show the axe rotating counter-clockwise, but don't.

== Bugs ==

* Axes only despawn when going off screen in multiplayer due to using a subroutine that is intended for player entities to keep themselves from going too far from the other player.
* The animation does not display correctly in some directions (see [[#Animation|Animation]])
* The speed is 1 px/frame slower in the negative directions (up/left). This is because the velocity is applied to the positions using two separate add instructions, and the carry is not cleared between them.
* Death from sprites increments the "dead" variable to make it non-zero, but death from solid background tiles decrements it. It may be possible for these to cancel out and keep the axe alive.


== RAM map ==
== RAM map ==
Line 22: Line 29:
{| class="wikitable"
{| class="wikitable"
|-
|-
! Address !! Length !! 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
|-
|-
| $04 || 2 || int16 || X velocity
| $04 || 2 || int16 || velocityX || X velocity
|-
|-
| $06 || 2 || int16 || Y velocity
| $06 || 2 || int16 || velocityY || Y velocity
|}
|}


Line 37: Line 44:
{| class="wikitable"
{| class="wikitable"
|-
|-
! Address !! Length !! Type !! Description
! Address !! Length !! [[Data types|Type]] !! Name !! Description
|-
|-
| $08 || 2 || pointer16 || Pointer to sprite
| $08 || 2 || pointer16 || sprite || Pointer to sprite
|-
|-
| $0A || 2 || boolean || Axe has been destroyed
| $0A || 2 || boolean || dead || Axe has been destroyed
|-
|-
| $0C || 2 || int16 0-based || Health
| $0C || 2 || int16 0-based || health || Health
|-
|-
| $0E || 2 || pointer16 || Pointer to current state subroutine (always $B5B0)
| $0E || 2 || pointer16 || state || Current state subroutine pointer
|-
|-
| $10 || 2 || int16 || Number of frames until next animation frame
| $10 || 2 || int16 || framesUntilAnimUpdate || Number of frames until next animation frame
|-
|-
| $12 || 2 || int16 0-based || Number of frames per animation frame (always 5)
| $12 || 2 || int16 0-based || framesPerAnimFrame || Number of frames per animation frame
|-
|-
| $14 || 2 || uint16 || Current animation frame
| $14 || 2 || uint16 || animationFrame || Current animation frame
|-
|-
| $16 || 2 || enum || [[#Animation Directions|Animation direction]]
| $16 || 2 || enum x4 || direction || [[#Animation Directions|Animation direction]]
|-
|-
| $18 || 2 || int16 || X position
| $18 || 2 || int16 || x || X position
|-
|-
| $1A || 2 || unused || Unused
| $1A || 2 || unused || || Unused
|-
|-
| $1C || 2 || int16 || Y position
| $1C || 2 || int16 || y || Y position
|-
|-
| $1E || 2 || sprite type || Type of sprite collided with
| $1E || 2 || sprite type || collidedSpriteType || Type of sprite collided with
|-
|-
| $20 || 2 || int16 /2 || X velocity
| $20 || 2 || int16 /2 || velocityX || X velocity
|-
|-
| $22 || 2 || int16 /2 || Y velocity
| $22 || 2 || int16 /2 || velocityY || Y velocity
|-
|-
| $24 || 2 || pointer16 || Pointer to animation table (always $ADDE)
| $24 || 2 || pointer16 || animations || Pointer to animation table
|}
|}


Line 74: Line 81:
{| class="wikitable"
{| class="wikitable"
|-
|-
! Value !! Direction !! Bugged direction
! Value !! Value x4 !! Direction !! Bugged direction
|-
|-
| 0x00 || Up || Down
| 0x00 || 0x00 || Up || Down
|-
|-
| 0x04 || Up-right || Down-right
| 0x01 || 0x04 || Up-right || Down-right
|-
|-
| 0x08 || Right || Down
| 0x02 || 0x08 || Right || Down
|-
|-
| 0x0C || Down-right ||
| 0x03 || 0x0C || Down-right ||
|-
|-
| 0x10 || Down ||
| 0x04 || 0x10 || Down ||
|-
|-
| 0x14 || Down-left || Down-right
| 0x05 || 0x14 || Down-left || Down-right
|-
|-
| 0x18 || Left || Down
| 0x06 || 0x18 || Left || Down
|-
|-
| 0x1C || Up-left || Down-right
| 0x07 || 0x1C || Up-left || Down-right
|-
|-
| 0x20 || None ||
| 0x08 || 0x20 || None ||
|}
|}


{{Pseudocode header}}
[[Category:Entity]]
main() { // $81:B4EA
{{RAM name|$7E:00DE}} += 2
init()
while (!this.dead) {
this.collidedSpriteType = NONE
{{ROM name|$80:8353}}(1)
this.state()
updateSprite()
}
{{RAM name|$7E:00DE}} -= 2
while ({{RAM name|$7E:00DE}} < 0) { }
{{ROM name|$80:BE41}}(this.sprite)
}
init() { // $81:B523
this.sprite = {{ROM name|$80:BE0C}}()
this.x = args.x
this.sprite.x = args.x
this.sprite.z = 0
this.y = args.y
this.sprite.y = args.y
this.velocityX = args.velocityX * 2
this.velocityY = args.velocityY * 2
this.sprite.tileData = $90:CCEC
this.sprite.entity = {{RAM name|$7E:0008}}
this.sprite.type = MONSTER
this.sprite.visible = true
this.health = 1
this.framesPerAnimFrame = 5
this.framesUntilAnimUpdate = 0
this.dead = false
this.animationFrame = 0
this.animations = evilDoll.axeAnimations
determineDirection()
{{ROM name|$80:8475}}(collisionHandler)
setStateToNormal()
}
collisionHandler(spriteType) { // $81:B592
if (spriteType == 7 || spriteType == 8) {
this.collidedSpriteType = spriteType
this.health -= 1
if (this.health < 0) {
this.dead = true
}
}
return false
}
setStateToNormal() { // $81:B5AA
this.state = normalState
}
normalState() { // $81:B5B0
if (this.framesUntilAnimUpdate == 0) {
this.animationFrame += 1
if (this.animationFrame == 4) {
this.animationFrame = 0
}
}
this.x += this.velocityX * 2
this.x += this.velocityX < 0 ? 1 : 0 // From carry
this.y += this.velocityY * 2
this.y += this.velocityY < 0 ? 1 : 0 // From carry
if ({{ROM name|$80:AF2C}}(this.x, this.y) || {{ROM name|$80:A8B3}}(this.x, this.y)) {
this.dead = true
}
}
updateSprite() { // $81:B5EA
this.framesUntilAnimUpdate -= 1
if (this.framesUntilAnimUpdate < 0) {
this.framesUntilAnimUpdate = this.framesPerAnimFrame
}
this.sprite.x = this.x
this.sprite.y = this.y
this.sprite.flipX = this.animations[this.direction][this.animationFrame][0]
this.sprite.tileData.lowBytes =
evilDoll.spriteTable[this.animations[this.direction][this.animationFrame][1]]
this.sprite.tileData.bank = 0x90
}
determineDirection() { // $81:B630
index = 0
if (this.velocityX == -1) {
index += 8
} else if (this.velocityX != 0) {
index += 4
}
if (this.yVelocity == -1) {
index += 3
} else if (this.velocityY != 0) {
index += 2
}
this.direction = directions[index]
}
directions = { // $81:B658
8, 8, 4, 0 // None, None, Down, Up
4, 8, 3, 1 // Down, None, Down-right, Up-right ("Down" here should be "Right")
6, 8, 5, 7 // Left, None, Down-left, Up-left
}
{{Pseudocode footer}}

[[Category:Bug]]

Latest revision as of 19:10, 17 August 2024

Entity data
Entity pointer $81:B4EA

An evil doll axe is an entity that is created by an evil doll whenever it throws an axe.

Behavior[edit]

The axe moves at a velocity of 4 times the given input velocity per frame. In the base game, the input velocities are either 0, 1, or -1 on each axis, so it will move at a rate of 4 pixels per frame on each axis.

Axes start with 2 HP and will be instantly destroyed when they run out of health. However, axes only take damage from sprite types 7 and 8, which are a wall breaking from player punching it as a monster, and an unused type respectively. Axes will also be destroyed if they touch a background tile that is solid to weapons or if they go off screen in multiplayer. The off-screen condition does not apply in single player; they will continue to travel until they hit a solid tile or reach the edge of the level.

Animation[edit]

The axe animation has 4 different animation frames and there are 6 frames of time between each. The animation is supposed to differ slightly based on which direction the axe is moving, but there are two bugs in the implementation for this:

  1. Negative directions (up and left) are incorrectly mapped to the corresponding positive direction because the code only checks for the velocities being exactly equal to -1, but they have already been multiplied by 2 when this is checked.
  2. The "down" direction is incorrectly mapped to "right"

The result of these two bugs is that all orthogonal directions are mapped to the animation for "right", and all diagonal directions are mapped to the animation for "down-right". For orthogonal directions, this doesn't matter since they all share the same animation anyway. However, the down-left and up-left animations supposed to show the axe rotating counter-clockwise, but don't.

Bugs[edit]

  • Axes only despawn when going off screen in multiplayer due to using a subroutine that is intended for player entities to keep themselves from going too far from the other player.
  • The animation does not display correctly in some directions (see Animation)
  • The speed is 1 px/frame slower in the negative directions (up/left). This is because the velocity is applied to the positions using two separate add instructions, and the carry is not cleared between them.
  • Death from sprites increments the "dead" variable to make it non-zero, but death from solid background tiles decrements it. It may be possible for these to cancel out and keep the axe alive.

RAM map[edit]

Entity arguments[edit]

Address Length Type Name Description
$00 2 int16 x X position
$02 2 int16 y Y position
$04 2 int16 velocityX X velocity
$06 2 int16 velocityY Y velocity

Entity memory[edit]

Address Length Type Name Description
$08 2 pointer16 sprite Pointer to sprite
$0A 2 boolean dead Axe has been destroyed
$0C 2 int16 0-based health Health
$0E 2 pointer16 state Current state subroutine pointer
$10 2 int16 framesUntilAnimUpdate Number of frames until next animation frame
$12 2 int16 0-based framesPerAnimFrame Number of frames per animation frame
$14 2 uint16 animationFrame Current animation frame
$16 2 enum x4 direction Animation direction
$18 2 int16 x X position
$1A 2 unused Unused
$1C 2 int16 y Y position
$1E 2 sprite type collidedSpriteType Type of sprite collided with
$20 2 int16 /2 velocityX X velocity
$22 2 int16 /2 velocityY Y velocity
$24 2 pointer16 animations Pointer to animation table

Animation Directions[edit]

Value Value x4 Direction Bugged direction
0x00 0x00 Up Down
0x01 0x04 Up-right Down-right
0x02 0x08 Right Down
0x03 0x0C Down-right
0x04 0x10 Down
0x05 0x14 Down-left Down-right
0x06 0x18 Left Down
0x07 0x1C Up-left Down-right
0x08 0x20 None

Pseudocode

main() { // $81:B4EA
	$7E:00DE += 2
	init()

	while (!this.dead) {
		this.collidedSpriteType = NONE
		waitFrames(1)
		this.state()
		updateSprite()
	}

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

init() { // $81:B523
	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.velocityX = args.velocityX * 2
	this.velocityY = args.velocityY * 2
	this.sprite.tileData = $90:CCEC
	this.sprite.entity = currentEntity
	this.sprite.type = MONSTER
	this.sprite.visible = true
	this.health = 1
	this.framesPerAnimFrame = 5
	this.framesUntilAnimUpdate = 0
	this.dead = false
	this.animationFrame = 0
	this.animations = evilDoll.axeAnimations
	determineDirection()
	setCollisionHandler(collisionHandler)
	setStateToNormal()
}

collisionHandler(spriteType) { // $81:B592
	if (spriteType == 7 || spriteType == 8) {
		this.collidedSpriteType = spriteType
		this.health -= 1
		if (this.health < 0) {
			this.dead = true
		}
	}
	return false
}

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

normalState() { // $81:B5B0
	if (this.framesUntilAnimUpdate == 0) {
		this.animationFrame += 1
		if (this.animationFrame == 4) {
			this.animationFrame = 0
		}
	}

	this.x += this.velocityX * 2
	this.x += this.velocityX < 0 ? 1 : 0 // From carry
	this.y += this.velocityY * 2
	this.y += this.velocityY < 0 ? 1 : 0 // From carry

	if (bgSolidToWeapons(this.x, this.y) || positionTooFarFromOtherPlayer(this.x, this.y)) {
		this.dead = true
	}
}

updateSprite() { // $81:B5EA
	this.framesUntilAnimUpdate -= 1
	if (this.framesUntilAnimUpdate < 0) {
		this.framesUntilAnimUpdate = this.framesPerAnimFrame
	}

	this.sprite.x = this.x
	this.sprite.y = this.y
	this.sprite.flipX = this.animations[this.direction][this.animationFrame][0]
	this.sprite.tileData.lowBytes =
	    evilDoll.spriteTable[this.animations[this.direction][this.animationFrame][1]]
	this.sprite.tileData.bank = 0x90
}

determineDirection() { // $81:B630
	index = 0

	if (this.velocityX == -1) {
		index += 8
	} else if (this.velocityX != 0) {
		index += 4
	}

	if (this.yVelocity == -1) {
		index += 3
	} else if (this.velocityY != 0) {
		index += 2
	}

	this.direction = directions[index]
}

directions = { // $81:B658
	8, 8, 4, 0 // None, None, Down, Up
	4, 8, 3, 1 // Down, None, Down-right, Up-right ("Down" here should be "Right")
	6, 8, 5, 7 // Left, None, Down-left, Up-left
}