Martian: Difference between revisions
Content deleted Content added
Piranhaplant (talk | contribs) No edit summary |
Piranhaplant (talk | contribs) No edit summary |
||
Line 115: | Line 115: | ||
| $7E || 2 || int16 0-based || freezeTimer || Amount of time to stay [[Fire extinguisher|frozen]] |
| $7E || 2 || int16 0-based || freezeTimer || Amount of time to stay [[Fire extinguisher|frozen]] |
||
|} |
|} |
||
== Pseudocode == |
|||
spriteTableDefault = { |
|||
0xD66D, 0xD69E, 0xD6C7, 0xD6F8, |
|||
0xD721, 0xD752, 0xD783, 0xD7B4, |
|||
0xD7E5, 0xD816, 0xD847, 0xD878 |
|||
} |
|||
shootHandlerDefault(direction) { |
|||
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 |
|||
this.setSprite(this.shootSpriteIndexes[direction]) |
|||
if (direction < DOWN) { |
|||
this.sprite.flipX = false |
|||
} else { |
|||
this.sprite.flipX = true |
|||
} |
|||
{{ROM name|$80:825E}}($81:F380) |
|||
{{ROM name|$80:8353}}(12) |
|||
} else { |
|||
this.shotThrottle -= 1 |
|||
} |
|||
} |
|||
shootSpriteIndexes = { 0, 8, 0, 0, 0, 4, 0, 0, 0 } |
|||
main_Normal() { |
|||
{{RAM name|$7E:00DE}} += 0x1E |
|||
this.init() |
|||
this.setStateToNormal() |
|||
while (!this.dead) { |
|||
{{ROM name|$80:8353}}(1) |
|||
this.state() |
|||
} |
|||
if (this.collidedSpriteType != 0) { |
|||
{{ROM name|$80:C7D9}}(this.collidedSpriteType & 0x8000, 200) |
|||
{{RAM name|$7E:1F72}} += 1 |
|||
{{ROM name|$81:832C}}(this.deathAnimation_Normal) |
|||
} |
|||
{{RAM name|$7E:00DE}} -= 0x1E |
|||
while ({{RAM name|$7E:00DE}} < 0) { } |
|||
{{ROM name|$80:BE41}}(this.sprite) |
|||
} |
|||
deathAnimation_Normal = { |
|||
{ 0xD998, 5}, { 0xD96F, 5 }, { 0xD946, 5 }, { 0xDA2B, 5 }, |
|||
{ 0, 0 } |
|||
} |
|||
main_TopOfScreen() { |
|||
if (this.tryToSpawnAtTopOfScreen()) { |
|||
return |
|||
} |
|||
{{RAM name|$7E:00DE}} += 0x1E |
|||
this.init() |
|||
this.setStateToTopOfScreen() |
|||
while (!this.dead) { |
|||
{{ROM name|$80:8353}}(1) |
|||
this.state() |
|||
} |
|||
if (this.collidedSpriteType != 0) { |
|||
{{ROM name|$80:C7D9}}(this.collidedSpriteType & 0x8000, 200) |
|||
{{RAM name|$7E:1F72}} += 1 |
|||
{{ROM name|$81:832C}}(this.deathAnimation_TopOfScreen) |
|||
} |
|||
{{RAM name|$7E:00DE}} -= 0x1E |
|||
while ({{RAM name|$7E:00DE}} < 0) { } |
|||
{{ROM name|$80:BE41}}(this.sprite) |
|||
} |
|||
deathAnimation_TopOfScreen = { |
|||
{ 0xD998, 5}, { 0xD96F, 5 }, { 0xD946, 5 }, { 0xDA2B, 5 }, |
|||
{ 0, 0 } |
|||
} |
|||
tryToSpawnAtTopOfScreen() { |
|||
args.Y = {{RAM name|$7E:1B6C}} + 8 |
|||
return {{ROM name|$80:AE14}}(args.x, args.y) || {{ROM name|$80:B422}}(args.x, args.y) |
|||
} |
|||
init() { |
|||
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.sprite.tileData = $90:D66D |
|||
this.sprite.entity = {{RAM name|$7E:0008}} |
|||
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(this.shootHandlerDefault) |
|||
this.setSpriteTable(this.spriteTableDefault) |
|||
{{ROM name|$81:832C}}(this.spawnAnimation) |
|||
{{ROM name|$80:8475}}(this.collisionHandler) |
|||
} |
|||
spawnAnimation = { |
|||
{ 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) { |
|||
if (spriteType < SQUIRT_GUN) { |
|||
return false |
|||
} |
|||
this.collidedSpriteType = spriteType |
|||
weaponType = spriteType & 0x7FFF |
|||
if (weaponType == MARTIAN_BUBBLE_GUN) { |
|||
{{RAM name|$7E:1FDC}}[{{ROM name|$80:9D6A}}((spriteType & 0x8000) >> 15)] += 1 |
|||
return {{ROM name|$81:83C6}}() |
|||
} else if (weaponType == FIRE_EXTINGUISHER) { |
|||
return {{ROM name|$81:847E}}() |
|||
} else { |
|||
newHealth = this.health - {{ROM name|$81:8561}}[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 {{ROM name|$81:8506}}() |
|||
} |
|||
return false |
|||
} |
|||
} |
|||
setSpriteTable(spriteTable) { |
|||
this.spriteTable = spriteTable |
|||
} |
|||
setShootHandler(shootHandler) { |
|||
this.shootHandler = shootHandler |
|||
} |
|||
getDirectionToLineUpWithTarget() { |
|||
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) { |
|||
if ({{RAM name|$7E:0020}} % 4 == 0) { |
|||
return |
|||
} |
|||
this.newX = this.x + this.moveAmount[direction][0] |
|||
this.newY = this.y + this.moveAmount[direction][1] |
|||
if (!{{ROM name|$80:AE97}}(this.newX, this.y) && |
|||
!{{ROM name|$80:B422}}(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:B422}}(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, -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 } |
|||
setSprite(index) { |
|||
this.sprite.tileData.lowBytes = this.spriteTable[index] |
|||
} |
|||
updateAnimation_Normal() { |
|||
index = this.walkingAnimations[this.direction][this.animationFrame] |
|||
if (index >= 12) { |
|||
index = 8 |
|||
} |
|||
this.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 = { |
|||
{ 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) { |
|||
if (this.shootHandler != null) { |
|||
this.shootHandler(direction) |
|||
} |
|||
} |
|||
setStateToNormal() { |
|||
this.state = this.normalState |
|||
} |
|||
normalState() { |
|||
direction = {{ROM name|$80:B379}}(this.x, this.y) |
|||
if (direction != NONE) { |
|||
this.shoot(direction) |
|||
} |
|||
this.directionChangeTimer -= 1 |
|||
if (this.directionChangeTimer < 0) { |
|||
this.directionChangeTimer = 60 |
|||
this.targetDistance, this.targetSprite, this.targetXOffset, this.targetYOffset = |
|||
{{ROM name|$80:B123}}(this.x, this.y) |
|||
if (this.targetDistance < 60) { |
|||
{{ROM name|$80:B3F1}}(this.sprite, this.targetSprite) |
|||
this.direction = |
|||
this.oppositeDirection[{{ROM name|$80:B22A}}(this.sprite, this.targetSprite)] |
|||
} else if (this.targetDistance < 80) { |
|||
this.direction = this.getDirectionToLineUpWithTarget() |
|||
} else if (this.targetDistance < 224) { |
|||
{{ROM name|$80:B3F1}}(this.sprite, this.targetSprite) |
|||
this.direction = {{ROM name|$80:B22A}}(this.sprite, this.targetSprite) |
|||
} else { |
|||
direction = {{ROM name|$80:B2A5}}(208, this.x, this.y) |
|||
if (direction == NONE) { |
|||
this.dead = true |
|||
} |
|||
return |
|||
} |
|||
} |
|||
this.move(this.direction) |
|||
this.updateAnimation_Normal() |
|||
} |
|||
setStateToTopOfScreen() { |
|||
this.state = this.topOfScreenState |
|||
this.framesUntilSideUpdate = 0 |
|||
this.targetSide = 0 |
|||
} |
|||
topOfScreenState() { |
|||
direction = {{ROM name|$80:B2A5}}(208, this.x, this.y) |
|||
if (direction == NONE) { |
|||
this.dead = true |
|||
} |
|||
if (this.anyPlayerAbove()) { |
|||
this.animationFrame = 0 |
|||
this.framesUntilAnimUpdate = 0 |
|||
this.setStateToNormal() |
|||
return |
|||
} |
|||
direction = {{ROM name|$80:B379}}(this.x, this.y) |
|||
if (direction != NONE) { |
|||
this.shoot(direction) |
|||
} else if ({{ROM name|$80:9D39}}() < 30) { |
|||
this.shoot(DOWN) |
|||
} |
|||
this.framesUntilSideUpdate -= 1 |
|||
if (this.framesUntilSideUpdate < 0) { |
|||
this.framesUntilSideUpdate = 32 |
|||
this.targetDistance, this.targetSprite = {{ROM name|$80:B123}}(this.x, this.y) |
|||
if (this.targetDistance < 32) { |
|||
{{ROM name|$80:B3F1}}(this.sprite, this.targetSprite) |
|||
direction = this.oppositeDirection[{{ROM name|$80:B22A}}(this.sprite, this.targetSprite)] |
|||
} else { |
|||
{{ROM name|$80:B3F1}}(this.sprite, this.targetSprite) |
|||
direction = {{ROM name|$80:B22A}}(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() { |
|||
move(this.direction) |
|||
this.updateAnimation_TopOfScreen() |
|||
} |
|||
updateAnimation_TopOfScreen() { |
|||
this.framesUntilAnimUpdate -= 1 |
|||
if (this.framesUntilAnimUpdate < 0) { |
|||
this.framesUntilAnimUpdate = 4 |
|||
this.animationFrame = (this.animationFrame + 1) % 4 |
|||
this.setSprite(this.topOfScreenAnimation[this.animationFrame]) |
|||
} |
|||
} |
|||
topOfScreenAnimation = { 4, 5, 6, 7 } |
|||
anyPlayerAbove() { |
|||
if ({{RAM name|$7E:00D2}}[0] != null && {{RAM name|$7E:00D2}}[0].y < this.y) { |
|||
return true |
|||
} |
|||
if ({{RAM name|$7E:00D2}}[1] != null && {{RAM name|$7E:00D2}}[1].y < this.y) { |
|||
return true |
|||
} |
|||
return false |
|||
} |
|||
[[Category:Respawning monster]] |
[[Category:Respawning monster]] |
Revision as of 21:36, 10 July 2024
Monster data | |
---|---|
![]() | |
HP | 1 |
Points | 200 |
Special | {{{special}}} |
Resists | {{{resists}}} |
Weak to | {{{weak_to}}} |
Entity data | |
---|---|
Entity pointer | $81:99DF (normal) $81:9A3E (top of screen) |
The martian is a respawning monster. There are two different types of martians: normal and top of screen.
Behavior
Normal martian
![](http://static.miraheze.org/zamnhackingwiki/thumb/6/68/MartianAreas.png/256px-MartianAreas.png)
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
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.
Bugs
- Freezing or 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.
- 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.
RAM map
Entity arguments
Address | Length | Type | Name | Description |
---|---|---|---|---|
$00 | 2 | int16 | x | X position |
$02 | 2 | int16 | y | Y position |
Entity memory
Address | Length | Type | Name | Description |
---|---|---|---|---|
$08 | 2 | pointer16 | sprite | Pointer to sprite |
$0A | 2 | 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 = { 0xD66D, 0xD69E, 0xD6C7, 0xD6F8, 0xD721, 0xD752, 0xD783, 0xD7B4, 0xD7E5, 0xD816, 0xD847, 0xD878 } shootHandlerDefault(direction) { 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 this.setSprite(this.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 } main_Normal() { $7E:00DE += 0x1E this.init() this.setStateToNormal() while (!this.dead) { waitFrames(1) this.state() } if (this.collidedSpriteType != 0) { givePoints(this.collidedSpriteType & 0x8000, 200) martiansKilled += 1 showAnimation(this.deathAnimation_Normal) } $7E:00DE -= 0x1E while ($7E:00DE < 0) { } deleteSprite(this.sprite) } deathAnimation_Normal = { { 0xD998, 5}, { 0xD96F, 5 }, { 0xD946, 5 }, { 0xDA2B, 5 }, { 0, 0 } } main_TopOfScreen() { if (this.tryToSpawnAtTopOfScreen()) { return } $7E:00DE += 0x1E this.init() this.setStateToTopOfScreen() while (!this.dead) { waitFrames(1) this.state() } if (this.collidedSpriteType != 0) { givePoints(this.collidedSpriteType & 0x8000, 200) martiansKilled += 1 showAnimation(this.deathAnimation_TopOfScreen) } $7E:00DE -= 0x1E while ($7E:00DE < 0) { } deleteSprite(this.sprite) } deathAnimation_TopOfScreen = { { 0xD998, 5}, { 0xD96F, 5 }, { 0xD946, 5 }, { 0xDA2B, 5 }, { 0, 0 } } tryToSpawnAtTopOfScreen() { args.Y = cameraY + 8 return bgSolidToPlayers(args.x, args.y) || outsideOfLevel(args.x, args.y) } init() { 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(this.shootHandlerDefault) this.setSpriteTable(this.spriteTableDefault) showAnimation(this.spawnAnimation) setCollisionHandler(this.collisionHandler) } spawnAnimation = { { 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) { 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) { this.spriteTable = spriteTable } setShootHandler(shootHandler) { this.shootHandler = shootHandler } getDirectionToLineUpWithTarget() { 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) { if (frameCounter % 4 == 0) { return } this.newX = this.x + this.moveAmount[direction][0] this.newY = this.y + this.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 = { { 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 } setSprite(index) { this.sprite.tileData.lowBytes = this.spriteTable[index] } updateAnimation_Normal() { index = this.walkingAnimations[this.direction][this.animationFrame] if (index >= 12) { index = 8 } this.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 = { { 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) { if (this.shootHandler != null) { this.shootHandler(direction) } } setStateToNormal() { this.state = this.normalState } normalState() { direction = findShootDirection(this.x, this.y) if (direction != NONE) { this.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 = this.oppositeDirection[getDirectionToTarget(this.sprite, this.targetSprite)] } else if (this.targetDistance < 80) { this.direction = this.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 } } this.move(this.direction) this.updateAnimation_Normal() } setStateToTopOfScreen() { this.state = this.topOfScreenState this.framesUntilSideUpdate = 0 this.targetSide = 0 } topOfScreenState() { direction = findPlayerInRangeDirection(208, this.x, this.y) if (direction == NONE) { this.dead = true } if (this.anyPlayerAbove()) { this.animationFrame = 0 this.framesUntilAnimUpdate = 0 this.setStateToNormal() return } direction = findShootDirection(this.x, this.y) if (direction != NONE) { this.shoot(direction) } else if (getRandomByte() < 30) { this.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 = this.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() { move(this.direction) this.updateAnimation_TopOfScreen() } updateAnimation_TopOfScreen() { this.framesUntilAnimUpdate -= 1 if (this.framesUntilAnimUpdate < 0) { this.framesUntilAnimUpdate = 4 this.animationFrame = (this.animationFrame + 1) % 4 this.setSprite(this.topOfScreenAnimation[this.animationFrame]) } } topOfScreenAnimation = { 4, 5, 6, 7 } anyPlayerAbove() { if (playerSprites[0] != null && playerSprites[0].y < this.y) { return true } if (playerSprites[1] != null && playerSprites[1].y < this.y) { return true } return false }