Werewolf: Difference between revisions
Content added Content deleted
Piranhaplant (talk | contribs) No edit summary |
Piranhaplant (talk | contribs) No edit summary |
||
(4 intermediate revisions by the same user not shown) | |||
Line 42: | Line 42: | ||
{| class="wikitable" |
{| class="wikitable" |
||
|- |
|- |
||
! Address !! Length !! [[Data types|Type]] !! Description |
! Address !! Length !! [[Data types|Type]] !! Name !! Description |
||
|- |
|- |
||
| $00 || 2 || int16 || X position |
| $00 || 2 || int16 || x || X position |
||
|- |
|- |
||
| $02 || 2 || int16 || Y position |
| $02 || 2 || int16 || y || Y position |
||
|} |
|} |
||
Line 53: | Line 53: | ||
{| class="wikitable" |
{| class="wikitable" |
||
|- |
|- |
||
! Address !! Length !! [[Data types|Type]] !! Description |
! Address !! Length !! [[Data types|Type]] !! Name !! Description |
||
|- |
|- |
||
| $08 || 2 || pointer16 || Pointer to sprite |
| $08 || 2 || pointer16 || sprite || Pointer to sprite |
||
|- |
|- |
||
| $0A || 2 || pointer16 || Current state subroutine pointer |
| $0A || 2 || pointer16 || state || Current state subroutine pointer |
||
|- |
|- |
||
| $0C || 2 || uint16 || Current animation frame (x2 during attack animation) |
| $0C || 2 || uint16 || animationFrame || Current animation frame (x2 during attack animation) |
||
|- |
|- |
||
| $0E || 2 || int16 0-based || Number of frames until next animation frame |
| $0E || 2 || int16 0-based || framesUntilAnimUpdate || Number of frames until next animation frame |
||
|- |
|- |
||
| $10 || 2 || boolean || Dead |
| $10 || 2 || boolean || dead || Dead |
||
|- |
|- |
||
| $12 || 2 || int16 || X position |
| $12 || 2 || int16 || x || X position |
||
|- |
|- |
||
| $14 || 2 || int16 || Y position |
| $14 || 2 || int16 || y || Y position |
||
|- |
|- |
||
| $16 || 2 || int16 || New X position |
| $16 || 2 || int16 || newX || New X position |
||
|- |
|- |
||
| $18 || 2 || int16 || New Y position |
| $18 || 2 || int16 || newY || New Y position |
||
|- |
|- |
||
| $1A || 2 || direction || Current direction |
| $1A || 2 || direction || direction || Current direction |
||
|- |
|- |
||
| $1C || 2 || unused || Unused |
| $1C || 2 || unused || || Unused |
||
|- |
|- |
||
| $1E || 2 || optional pointer16 || Pointer to sprite that is being jumped at (0xFFFF if empty) |
| $1E || 2 || optional pointer16 || jumpTargetSprite || Pointer to sprite that is being jumped at (0xFFFF if empty) |
||
|- |
|- |
||
| $20 || 2 || int16 || Jump target X position |
| $20 || 2 || int16 || jumpTargetX || Jump target X position |
||
|- |
|- |
||
| $22 || 2 || int16 || Jump target Y position |
| $22 || 2 || int16 || jumpTargetY || Jump target Y position |
||
|- |
|- |
||
| $24 || 2 || int16 x4 || Z velocity during jump |
| $24 || 2 || int16 x4 || zVelocity || Z velocity during jump |
||
|- |
|- |
||
| $26 || 2 || uint16 || Duration of jump in frames |
| $26 || 2 || uint16 || jumpDuration || Duration of jump in frames |
||
|- |
|- |
||
| $28 || 2 || uint16 || Fractional portion of X position during jump (full X position = $12 + ($28/$26)) |
| $28 || 2 || uint16 || xFraction || Fractional portion of X position during jump (full X position = $12 + ($28/$26)) |
||
|- |
|- |
||
| $2A || 2 || uint16 || Fractional portion of Y position during jump (full Y position = $14 + ($2A/$26)) |
| $2A || 2 || uint16 || yFraction || Fractional portion of Y position during jump (full Y position = $14 + ($2A/$26)) |
||
|- |
|- |
||
| $2C || 2 || uint16 || Jump distance X magnitude |
| $2C || 2 || uint16 || jumpXMagnitude || Jump distance X magnitude |
||
|- |
|- |
||
| $2E || 2 || uint16 || Jump distance Y magnitude |
| $2E || 2 || uint16 || jumpYMagnitude || Jump distance Y magnitude |
||
|- |
|- |
||
| $30 || 2 || int16 || Jump distance X sign (1 if jump is right, -1 if left) |
| $30 || 2 || int16 || jumpXSign || Jump distance X sign (1 if jump is right, -1 if left) |
||
|- |
|- |
||
| $32 || 2 || int16 || Jump distance Y sign (1 if jump is down, -1 if up) |
| $32 || 2 || int16 || jumpYSign || Jump distance Y sign (1 if jump is down, -1 if up) |
||
|- |
|- |
||
| $34 || 2 || direction || Jump direction (only left or right due to a [[#Bugs|bug]] |
| $34 || 2 || direction || jumpDirection || Jump direction (only left or right due to a [[#Bugs|bug]]) |
||
|- |
|- |
||
| $36 || 2 || uint16 || Temporary storage |
| $36 || 2 || uint16 || temp || Temporary storage |
||
|- |
|- |
||
| $38 || 2 || optional pointer16 || Extra attack hitbox sprite (0xFFFF if empty) |
| $38 || 2 || optional pointer16 || attackSprite || Extra attack hitbox sprite (0xFFFF if empty) |
||
|- |
|- |
||
| $3A || 2 || int16 0-based || Previous health |
| $3A || 2 || int16 0-based || prevHealth || Previous health |
||
|- |
|- |
||
| $3C || 2 || int16 0-based || Health |
| $3C || 2 || int16 0-based || health || Health |
||
|- |
|- |
||
| $3E || 2 || sprite type || Type of weapon shot sprite collided with |
| $3E || 2 || sprite type || collidedSpriteType || Type of weapon shot sprite collided with |
||
|- |
|- |
||
| $40 || 2 || unused || Unused |
| $40 || 2 || unused || || Unused |
||
|- |
|- |
||
| $42 || 2 || pointer16 || Pointer to target sprite |
| $42 || 2 || pointer16 || targetSprite || Pointer to target sprite |
||
|- |
|- |
||
| $44 || 2 || uint16 || Distance to target sprite |
| $44 || 2 || uint16 || targetDistance || Distance to target sprite |
||
|- class="breakrow" |
|- class="breakrow" |
||
| $7E || 2 || |
| $7E || 2 || int16 0-based || freezeTimer || Amount of time to stay [[Fire extinguisher|frozen]] |
||
|} |
|} |
||
== Pseudocode == |
|||
init() { |
|||
{{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() { |
|||
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 = { |
|||
{ 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() { |
|||
if ({{ROM name|$80:9D39}}() & 2 == 0) { |
|||
doWalk() |
|||
} |
|||
doWalk() |
|||
} |
|||
doWalk() { |
|||
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 = { |
|||
{ 0, 0 }, |
|||
{ 0, -1 }, |
|||
{ 1, -1 }, |
|||
{ 1, 0 }, |
|||
{ 1, 1 }, |
|||
{ 0, 1 }, |
|||
{ -1, 1 }, |
|||
{ -1, 0 }, |
|||
{ -1, -1 } |
|||
} |
|||
canJumpToTarget() { |
|||
// 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() { |
|||
this.sprite.z += this.zVelocity / 4 |
|||
this.zVelocity -= 1 |
|||
} |
|||
updateJumpPosition() { |
|||
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() { |
|||
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 } |
|||
jumpXSpacing = { 0, 0, 28, 28, 28, 0, -28, -28, -28 } |
|||
setStateToPreJump() { |
|||
this.state = preJumpState |
|||
} |
|||
preJumpState() { |
|||
this.sprite.flipX = preJumpFlipPerDirection[this.direction] |
|||
{{ROM name|$81:832C}}(preJumpAnimation) |
|||
setStateToJumping() |
|||
} |
|||
preJumpAnimation = { { 0xEAFE, 7 }, { 0xEB27, 5 }, { 0xEB58, 5 }, { 0, 0 } } |
|||
preJumpFlipPerDirection = { false, false, false, false, false, false, true, true, true } |
|||
setStateToJumping() { |
|||
this.state = jumpingState |
|||
this.sprite.tileData.lowBytes = 0xEB89 |
|||
this.sprite.visible = true |
|||
this.sprite.bgPriority = true |
|||
this.sprite.type = NONE |
|||
} |
|||
jumpingState() { |
|||
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 = { |
|||
0, |
|||
15, -9, |
|||
15, -9, |
|||
15, 0, |
|||
15, 8, |
|||
15, 8, |
|||
-15, -9, |
|||
-15, 0, |
|||
-15, -9 |
|||
} |
|||
setStateToPostJump() { |
|||
this.state = postJumpState |
|||
} |
|||
postJumpState() { |
|||
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 } } |
|||
setStateToAttacking() { |
|||
this.state = attackingState |
|||
this.attackSprite = createAttackSprite() |
|||
this.framesUntilAnimUpdate = 0 |
|||
this.animationFrame = 0 |
|||
this.sprite.flipX = attackFlipPerDirection[this.direction] |
|||
} |
|||
attackingState() { |
|||
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 = { |
|||
0, 0xEC55, 0xEC7E, 0xECAF, 0xECF8, 0xED29, 0xED52, 0xED8B |
|||
0xEDBC, 0xEDF5, 0 |
|||
} |
|||
attackFlipPerDirection = { false, false, false, false, false, false, true, true, true } |
|||
createAttackSprite() { |
|||
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 } |
|||
canAttack() { |
|||
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() { |
|||
{{ROM name|$80:8475}}(collisionHandler) |
|||
this.state = walkingState |
|||
} |
|||
walkingState() { |
|||
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() { |
|||
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 = { |
|||
{ 0xDA43, 12 }, { 0xDA7C, 12 }, { 0xDACD, 12 }, { 0xDB26, 12 }, |
|||
{ 0xDB6F, 12 }, { 0, 0 } |
|||
} |
|||
collisionHandler(spriteType) { |
|||
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) { |
|||
this.dead = true |
|||
this.health = newHealth |
|||
this.freezeTimer = 0 |
|||
return true |
|||
} |
|||
[[Category:Respawning monster]] |
[[Category:Respawning monster]] |
||
[[Category: |
[[Category:Bug]] |
||
[[Category:Entity]] |
Latest revision as of 20:17, 14 July 2024
Monster data | |
---|---|
![]() | |
HP | 11 |
Points | 300 |
Entity data | |
---|---|
Entity pointer | $81:ABF5 |
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.
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.
Weapon damage[edit]
Werewolves take normal damage from all weapons except silverware, which kills them in one hit.
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.
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[edit]
init() { 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() { 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 = { { 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() { if (getRandomByte() & 2 == 0) { doWalk() } doWalk() } doWalk() { 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 = { { 0, 0 }, { 0, -1 }, { 1, -1 }, { 1, 0 }, { 1, 1 }, { 0, 1 }, { -1, 1 }, { -1, 0 }, { -1, -1 } } canJumpToTarget() { // 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() { this.sprite.z += this.zVelocity / 4 this.zVelocity -= 1 } updateJumpPosition() { 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() { 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 } jumpXSpacing = { 0, 0, 28, 28, 28, 0, -28, -28, -28 } setStateToPreJump() { this.state = preJumpState } preJumpState() { this.sprite.flipX = preJumpFlipPerDirection[this.direction] showAnimation(preJumpAnimation) setStateToJumping() } preJumpAnimation = { { 0xEAFE, 7 }, { 0xEB27, 5 }, { 0xEB58, 5 }, { 0, 0 } } preJumpFlipPerDirection = { false, false, false, false, false, false, true, true, true } setStateToJumping() { this.state = jumpingState this.sprite.tileData.lowBytes = 0xEB89 this.sprite.visible = true this.sprite.bgPriority = true this.sprite.type = NONE } jumpingState() { 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 = { 0, 15, -9, 15, -9, 15, 0, 15, 8, 15, 8, -15, -9, -15, 0, -15, -9 } setStateToPostJump() { this.state = postJumpState } postJumpState() { 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 } } setStateToAttacking() { this.state = attackingState this.attackSprite = createAttackSprite() this.framesUntilAnimUpdate = 0 this.animationFrame = 0 this.sprite.flipX = attackFlipPerDirection[this.direction] } attackingState() { 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 = { 0, 0xEC55, 0xEC7E, 0xECAF, 0xECF8, 0xED29, 0xED52, 0xED8B 0xEDBC, 0xEDF5, 0 } attackFlipPerDirection = { false, false, false, false, false, false, true, true, true } createAttackSprite() { 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 } canAttack() { 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() { setCollisionHandler(collisionHandler) this.state = walkingState } walkingState() { 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() { 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 = { { 0xDA43, 12 }, { 0xDA7C, 12 }, { 0xDACD, 12 }, { 0xDB26, 12 }, { 0xDB6F, 12 }, { 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 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) { this.dead = true this.health = newHealth this.freezeTimer = 0 return true }