Clone: Difference between revisions
Piranhaplant (talk | contribs) No edit summary |
Piranhaplant (talk | contribs) No edit summary |
||
Line 112: | Line 112: | ||
this.animationFrame = 0 |
this.animationFrame = 0 |
||
while (this.animationFrame < 11) { |
while (this.animationFrame < 11) { |
||
this.sprite.tileData.lowBytes = |
this.sprite.tileData.lowBytes = spawnAnimation[this.animationFrame] |
||
{{ROM name|$80:8353}}(10) |
{{ROM name|$80:8353}}(10) |
||
this.animationFrame += 1 |
this.animationFrame += 1 |
||
Line 120: | Line 120: | ||
this.sprite.type = MONSTER |
this.sprite.type = MONSTER |
||
this.sprite.alternatePalette = 6 |
this.sprite.alternatePalette = 6 |
||
{{ROM name|$80:8475}}( |
{{ROM name|$80:8475}}(collisionHandler) |
||
this.mimicking = false |
this.mimicking = false |
||
this.collidedSpriteType = 0 |
this.collidedSpriteType = 0 |
||
Line 131: | Line 131: | ||
clonedCharacter = null |
clonedCharacter = null |
||
player = 1 |
for (player = 1; player >= 0; player -= 1) { |
||
while (player >= 0) { |
|||
if (!{{RAM name|$7E:1E88}}[player]) { |
if (!{{RAM name|$7E:1E88}}[player]) { |
||
clonedCharacter = {{RAM name|$7E:1E84}}[(player + 1) % 2] |
clonedCharacter = {{RAM name|$7E:1E84}}[(player + 1) % 2] |
||
break |
break |
||
} |
} |
||
player -= 1 |
|||
} |
} |
||
if (clonedCharacter = null) { |
if (clonedCharacter == null) { |
||
clonedCharacter = {{ROM name|$80:9D39}}() % 2 |
clonedCharacter = {{ROM name|$80:9D39}}() % 2 |
||
} |
} |
||
this.walkingAnimations = |
this.walkingAnimations = walkingAnimationsPerCharacter[clonedCharacter] |
||
this.spawnAnimation = |
this.spawnAnimation = spawnAnimationPerCharacter[clonedCharacter] |
||
if ({{RAM name|$7E:1E88}}[0] && {{RAM name|$7E:1E84}}[0] == clonedCharacter) { |
if ({{RAM name|$7E:1E88}}[0] && {{RAM name|$7E:1E84}}[0] == clonedCharacter) { |
||
this.clonedPlayer = 0 |
this.clonedPlayer = 0 |
||
Line 195: | Line 193: | ||
this.framesUntilAnimUpdate = 7 |
this.framesUntilAnimUpdate = 7 |
||
this.animationFrame = (this.animationFrame + 1) % 4 |
this.animationFrame = (this.animationFrame + 1) % 4 |
||
this.sprite.tileData.lowBytes = |
this.sprite.tileData.lowBytes = walkingAnimations[this.direction][this.animationFrame] |
||
} |
} |
||
} |
} |
||
Line 203: | Line 201: | ||
this.yPixels = this.y / 4 |
this.yPixels = this.y / 4 |
||
this.direction = {{ROM name|$80:B2A5}}(240, this.xPixels, this.yPixels) |
this.direction = {{ROM name|$80:B2A5}}(240, this.xPixels, this.yPixels) |
||
move(this.direction) |
|||
} |
} |
||
mimick() { |
mimick() { |
||
this.direction = {{RAM name|$7E:0072}}[this.clonedPlayer] |
this.direction = {{RAM name|$7E:0072}}[this.clonedPlayer] |
||
move(this.direction) |
|||
} |
} |
||
Line 216: | Line 214: | ||
} |
} |
||
{{RAM name|$7E:00DE}} += 0x18 |
{{RAM name|$7E:00DE}} += 0x18 |
||
determineClonedPlayer() |
|||
init() |
|||
while (true) { |
while (true) { |
||
Line 225: | Line 223: | ||
} |
} |
||
updateAnimation() |
|||
if (this.mimicking) { |
if (this.mimicking) { |
||
mimick() |
|||
} else { |
} else { |
||
target() |
|||
if (this.direction == NONE) { |
if (this.direction == NONE) { |
||
break |
break |
||
Line 245: | Line 243: | ||
{{ROM name|$80:C7D9}}(this.collidedSpriteType & 0x8000, 20) |
{{ROM name|$80:C7D9}}(this.collidedSpriteType & 0x8000, 20) |
||
{{RAM name|$7E:1F76}} += 1 |
{{RAM name|$7E:1F76}} += 1 |
||
{{ROM name|$81:83A3}}( |
{{ROM name|$81:83A3}}(deathAnimation_Normal, 0xF5F5, 0x90) |
||
} |
} |
||
Line 263: | Line 261: | ||
} |
} |
||
{{RAM name|$7E:00DE}} += 0x18 |
{{RAM name|$7E:00DE}} += 0x18 |
||
determineClonedPlayer() |
|||
init() |
|||
while (true) { |
while (true) { |
||
Line 272: | Line 270: | ||
} |
} |
||
updateAnimation() |
|||
target() |
|||
if (this.direction == NONE) { |
if (this.direction == NONE) { |
||
break |
break |
||
} |
} |
||
target() |
|||
if (this.direction == NONE) { |
if (this.direction == NONE) { |
||
break |
break |
||
Line 286: | Line 284: | ||
{{ROM name|$80:C7D9}}(this.collidedSpriteType & 0x8000, 20) |
{{ROM name|$80:C7D9}}(this.collidedSpriteType & 0x8000, 20) |
||
{{RAM name|$7E:1F76}} += 1 |
{{RAM name|$7E:1F76}} += 1 |
||
{{ROM name|$81:83A3}}( |
{{ROM name|$81:83A3}}(deathAnimation_Hard, 0xF5F5, 0x90) |
||
} |
} |
||
Line 299: | Line 297: | ||
} |
} |
||
walkingAnimationsPerCharacter = { |
walkingAnimationsPerCharacter = { walkingAnimationsZeke, walkingAnimationsJulie } |
||
walkingAnimationsZeke = { |
walkingAnimationsZeke = { |
||
{ 0x90ED, 0x90ED, 0x90ED, 0x90ED }, |
{ 0x90ED, 0x90ED, 0x90ED, 0x90ED }, |
||
Line 323: | Line 321: | ||
} |
} |
||
spawnAnimationPerCharacter = { |
spawnAnimationPerCharacter = { spawnAnimationZeke, spawnAnimationJulie } |
||
spawnAnimationZeke = { |
spawnAnimationZeke = { |
||
0x9F0D, 0x9F16, 0x9F27, 0x9F48, 0x9F69, 0x9F92, 0x9FC3, 0x9FF4, |
0x9F0D, 0x9F16, 0x9F27, 0x9F48, 0x9F69, 0x9F92, 0x9FC3, 0x9FF4, |
Revision as of 20:34, 13 July 2024
Monster data | |
---|---|
![]() | |
HP | 1 |
Points | 20 |
Special | {{{special}}} |
Resists | {{{resists}}} |
Weak to | {{{weak_to}}} |
Entity data | |
---|---|
Entity pointer | $81:8E89 (normal) $81:8F1F (hard) |
The clone is a respawning monster. There are two different types of clones: normal and hard.
Behavior
Cloning
Both types of clones start by picking a player to clone. If there is only one player in the game, that player is selected. If there are two players in the game, one of them is selected at random. If there are no players left in the game, the clone will not spawn.
Normal clone
Normal clones can be in one of two states:
- Targeting: Move towards the nearest player at a speed of 0.75 px/frame. This only targets players, not victims or decoys. If there is no player within 239 pixels on a single axis while in this state, then the clone will despawn.
- Mimicking: Move in the direction of the cloned player's d-pad input at a speed of 0.75 px/frame. Stand still if the player is not pressing any direction on the d-pad.
Clones start in the targeting state, but will switch to the mimicking state after 1 frame. Afterwards, the clone will repeatedly switch to the opposite state after a random amount of time between 1 and 255 frames (about 4.25 seconds).
Hard clone
Hard clones always target the nearest player. They move at a speed of 1.5 px/frame, twice as fast as the normal clone. They will also despawn if there is no player within 239 pixels on a single axis.
Animation
Clones have a 4 frame animation cycle, with 7 frames between each animation frame. There is a separate set of animation frames for each of the 4 orthogonal directions, chosen based on which direction the clone is moving. There is also a separate set of animations for Zeke and Julie, chosen based on the character of the player who is being cloned. If the clone is moving diagonal, it uses the left or right animation. If the clone is not moving, it uses a single right-facing sprite for all 4 animation frames.
Bugs
- Freezing or bubbling a clone, then letting it despawn will count as killing it. This is because the logic that determines if a clone was killed or not checks if it was ever hit by a weapon, not if it lost all its HP.
- The normal clone can randomly get a value of 0 for the amount of time in between state changes. If this happens, the value will underflow and it will take a full 65536 frames (about 18.2 minutes) for it to change states again.
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 | uint16 | framesUntilAnimUpdate | Number of frames until next animation frame |
$0C | 2 | uint16 | animationFrame | Current animation frame (x2 during spawn animation) |
$0E | 2 | direction | direction | Current direction |
$10 | 2 | unused | Unused | |
$12 | 2 | int16 | xPixels | X position in pixels (derived from $16) |
$14 | 2 | int16 | yPixels | Y position in pixels (derived from $18) |
$16 | 2 | int16 x4 | x | X position |
$18 | 2 | int16 x4 | y | Y position |
$1A | 2 | int16 | newXPixels | New X position in pixels |
$1C | 2 | int16 | newYPixels | New Y position in pixels |
$1E | 2 | int16 x4 | newX | New X position |
$20 | 2 | int16 x4 | newY | New Y position |
$22 | 2 | int16 0-based | health | Health |
$24 | 2 | uint16 | mimicking | Current state (0 = targeting, 1 = mimicking) |
$26 | 2 | uint16 | framesUntilStateSwitch | Number of frames until state change |
$28 | 2 | pointer16 | spawnAnimation | Pointer to spawn animation |
$2A | 2 | pointer16 | walkingAnimations | Pointer to walking animations table |
$2C | 2 | uint16 x2 | clonedPlayer | Which player is being cloned |
$2E | 2 | boolean | dead | Dead |
$30 | 2 | sprite type | collidedSpriteType | Type of weapon shot sprite collided with |
$7E | 2 | int16 0-based | freezeTimer | Amount of time to stay frozen |
Pseudocode
init() { createMonsterSprite() this.x = args.x * 4 this.y = args.y * 4 this.sprite.tileData = $90:9F0D this.sprite.visible = true this.framesUntilAnimUpdate = 7 this.animationFrame = 0 this.sprite.tileData.bank = 0x90 this.framesUntilStateSwitch = 1 playSound(0x1A) this.animationFrame = 0 while (this.animationFrame < 11) { this.sprite.tileData.lowBytes = spawnAnimation[this.animationFrame] waitFrames(10) this.animationFrame += 1 } this.health = 0 this.sprite.type = MONSTER this.sprite.alternatePalette = 6 setCollisionHandler(collisionHandler) this.mimicking = false this.collidedSpriteType = 0 this.animationFrame = 0 this.dead = false this.freezeTimer = 0 } determineClonedPlayer() { clonedCharacter = null for (player = 1; player >= 0; player -= 1) { if (!playerInGame[player]) { clonedCharacter = characterForPlayer[(player + 1) % 2] break } } if (clonedCharacter == null) { clonedCharacter = getRandomByte() % 2 } this.walkingAnimations = walkingAnimationsPerCharacter[clonedCharacter] this.spawnAnimation = spawnAnimationPerCharacter[clonedCharacter] if (playerInGame[0] && characterForPlayer[0] == clonedCharacter) { this.clonedPlayer = 0 } else { this.clonedPlayer = 1 } } move(direction) { this.newX = this.x + moveAmount[direction][0] this.newXPixels = this.newX / 4 this.newY = this.y + moveAmount[direction][1] this.newYPixels = this.newY / 4 if (!bgSolidToMonsters(this.newXPixels, this.yPixels) && !touchingSpriteSolidToMonsters(this.sprite, this.newXPixels, this.yPixels)) { this.x = this.newX this.xPixels = this.newXPixels } if (!bgSolidToMonsters(this.xPixels, this.newYPixels) && !touchingSpriteSolidToMonsters(this.sprite, this.xPixels, this.newYPixels)) { this.y = this.newY this.yPixels = this.newYPixels } this.sprite.x = this.xPixels this.sprite.y = this.yPixels if (this.direction < DOWN_LEFT) { this.sprite.flipX = false } else { this.sprite.flipX = true } } moveAmount = { { 0, 0 }, { 0, -3 }, { 3, -3 }, { 3, 0 }, { 3, 3 }, { 0, 3 }, { -3, 3 }, { -3, 0 }, { -3, -3 } } updateAnimation() { this.framesUntilAnimUpdate -= 1 if (this.framesUntilAnimUpdate == 0) { this.framesUntilAnimUpdate = 7 this.animationFrame = (this.animationFrame + 1) % 4 this.sprite.tileData.lowBytes = walkingAnimations[this.direction][this.animationFrame] } } target() { this.xPixels = this.x / 4 this.yPixels = this.y / 4 this.direction = findPlayerInRangeDirection(240, this.xPixels, this.yPixels) move(this.direction) } mimick() { this.direction = playerDPadDirections[this.clonedPlayer] move(this.direction) } main_Normal() { if (!playerInGame[0] && !playerInGame[1]) { return } $7E:00DE += 0x18 determineClonedPlayer() init() while (true) { waitFrames(1) if (this.dead) { break } updateAnimation() if (this.mimicking) { mimick() } else { target() if (this.direction == NONE) { break } } this.framesUntilStateSwitch -= 1 if (this.framesUntilStateSwitch == 0) { this.mimicking = !this.mimicking this.framesUntilStateSwitch = getRandomByte() } } if (this.collidedSpriteType != 0) { givePoints(this.collidedSpriteType & 0x8000, 20) clonesKilled += 1 showMonsterDeath(deathAnimation_Normal, 0xF5F5, 0x90) } deleteSprite(this.sprite) $7E:00DE -= 0x18 while ($7E:00DE < 0) { } } deathAnimation_Normal = { { 0xA01D, 5 }, { 0xA06E, 5 }, { 0xA0A7, 5 }, { 0xA0D8, 5 }, { 0xA0F9, 5 }, { 0xA11A, 5 }, { 0, 0 } } main_Hard() { if (!playerInGame[0] && !playerInGame[1]) { return } $7E:00DE += 0x18 determineClonedPlayer() init() while (true) { waitFrames(1) if (this.health < 0) { break } updateAnimation() target() if (this.direction == NONE) { break } target() if (this.direction == NONE) { break } } if (this.collidedSpriteType != 0) { givePoints(this.collidedSpriteType & 0x8000, 20) clonesKilled += 1 showMonsterDeath(deathAnimation_Hard, 0xF5F5, 0x90) } deleteSprite(this.sprite) $7E:00DE -= 0x18 while ($7E:00DE < 0) { } } deathAnimation_Hard = { { 0xA01D, 5 }, { 0xA06E, 5 }, { 0xA0A7, 5 }, { 0xA0D8, 5 }, { 0xA0F9, 5 }, { 0xA11A, 5 }, { 0, 0 } } walkingAnimationsPerCharacter = { walkingAnimationsZeke, walkingAnimationsJulie } walkingAnimationsZeke = { { 0x90ED, 0x90ED, 0x90ED, 0x90ED }, { 0x9210, 0x9229, 0x9242, 0x925B }, { 0x9138, 0x9159, 0x9172, 0x9193 }, { 0x9138, 0x9159, 0x9172, 0x9193 }, { 0x9138, 0x9159, 0x9172, 0x9193 }, { 0x91AC, 0x91C5, 0x91DE, 0x91F7 }, { 0x9138, 0x9159, 0x9172, 0x9193 }, { 0x9138, 0x9159, 0x9172, 0x9193 }, { 0x9138, 0x9159, 0x9172, 0x9193 } } walkingAnimationsJulie = { { 0x8285, 0x8285, 0x8285, 0x8285 }, { 0x83B8, 0x83D1, 0x83EA, 0x8403 }, { 0x82E0, 0x8301, 0x831A, 0x833B }, { 0x82E0, 0x8301, 0x831A, 0x833B }, { 0x82E0, 0x8301, 0x831A, 0x833B }, { 0x8354, 0x836D, 0x8386, 0x839F }, { 0x82E0, 0x8301, 0x831A, 0x833B }, { 0x82E0, 0x8301, 0x831A, 0x833B }, { 0x82E0, 0x8301, 0x831A, 0x833B } } spawnAnimationPerCharacter = { spawnAnimationZeke, spawnAnimationJulie } spawnAnimationZeke = { 0x9F0D, 0x9F16, 0x9F27, 0x9F48, 0x9F69, 0x9F92, 0x9FC3, 0x9FF4, 0x9627, 0x9640, 0x9659 } spawnAnimationJulie = { 0x9F0D, 0x9F16, 0x9F27, 0x9F48, 0x9F69, 0x9F92, 0x9FC3, 0x9FF4, 0x87C7, 0x87E0, 0x87F9 } 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 { 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 } }