Initialize Movement for Attack

󰃭 2025-03-30

This is a detailed look at how enemy movement is initialized. Including determining the target direction and distance. This specifically is tracing movement when the Enemy is trying to attack a target but is not close enough. The various “Wander” commands differ in same ways

The ApplyMovement Function moves enemies in a direction which is stored at $7E9786,X (Where X is the current enemy being processed). This will cover how those values are initialized in the case of an enemy attacking a target a close range.

If step by step analysis of SNES assembly isn’t what you’re interested in, skip ahead to the practical example at the end where we walk step by step through a battle.

Initialize movement variables

Ready to move timer

A few levels up in the call stack from the rest of the functions discussed here is the function that decides if an enemy is ready to move. Enemies have a movement timer stored at $7E98A0,X where X is the offset of the current enemy, this ticks down every frame and when it reaches 0 it resets and the function to intialize movement is called. This means that movement can only start on frames that are a multiple of the max movement timer.

For example, The Blue Imps in Truce Canyon have a max timer of 0x10, so they can only start movement every 16 frames. (This may be consistent across all enemies? To be determined).

At the beginning of a battle enemy 0’s timer is set to max, enemy 1’s timer is set to max + 1, etc. in order to stagger the movement timers for enemies. On subsequent resets each timer is set to the normal max.

CalculateDirection

CalculateDirection is a function that calculates an angle between two entities. The source X/Y is stored in $D3/$D4 and the target X/Y is stored in $D5/$D6 prior to calling the function.

The output is a direction in the A register and a copy in $DB. The direction is represented as between 0x00 and 0xC0

Note: The X/Y diff here is calculated as source_x - target_x and source_y - target_y. This results in a diff that can be applied, but its’ the diff that can be added to the target to get the source rather than the other way around.

As an example

source_x = 0x5
source_y = 0xA
target_x = 0x2
target_y = 0xC

source_x - target_x == 0x3
source_y - target_y == 0xFE (-2)

The final direction is still calculated from source->target, but this was something I found a bit confusing while working out what was happening here. (It’s been many years since I took Trig, this observation is probably obvious for someone who works with it regularly).

CalculateDirection:
; Calculate X vector
C10222    SEC
C10223    LDA $D3           ; Load source X coord
C10225    SBC $D5           ; Subtract target X coord
C10227    STA $D7           ; Store to temp
; This checks the sign of the previous subtraction by subtracting the carry from 0
C10229    LDA #$00  
C1022B    SBC #$00  
C1022D    STA $D8           ; Store sign bit to temp
; Calculate Y vector, same operations
C1022F    SEC
C10230    LDA $D4
C10232    SBC $D6
C10234    STA $D9
C10236    LDA #$00
C10238    SBC #$00
C1023A    STA $DA
; Calculate absolute X distance
; $DE = $distance ^ $sign
C1023C    LDA $D7
C1023E    EOR $D8 
C10240    SEC
C10241    SBC $D8 
C10243    STA $DE
C10245    STZ $DF
; Calculate absolute Y distance
; $E0 = $distance ^ $sign
C10247    LDA $D9
C10249    EOR $DA
C1024B    SEC
C1024C    SBC $DA
C1024E    STA $E0
C10250    STZ $E1
; Use the previous calculations to lookup a angle value
; The two values are combined together into E2, which is then used to index into the lookup table
C10252    REP #$20
C10254    LDA $DE
C10256    LSR
C10257    LSR
C10258    LSR
C10259    STA $DC
C1025B    LDA $E0
C1025D    AND #$FFF8
C10260    ASL
C10261    ASL
C10262    CLC
C10263    ADC $DC
C10265    STA $E2
C10267    ASL
C10268    TAX
C10269    TDC
C1026A    SEP #$20
C1026C    LDX $E2
C1026E    LDA $C0F300,X     ; Load from lookup table
; Determine which quadrant the direction is in.
; If X/Y are both positive add 0x80
; If X/Y are both negative return as is
; If +X/-Y subtract value from 0x80
; if -X/+Y negate
C10274    LDA $D8           ; Check X sign
C10276    BMI $C1028C       ; If X is negative, branch to $C1028C
C10278    LDA $DA           ; Check Y sign
C1027A    BMI $C10284       ; If Y is negative, branch to $C10284
; +X/+Y
C1027C    CLC
C1027D    LDA #$80          ; If both X and Y are positive
C1027F    ADC $DB               ; Add $80 to the lookup value
C10281    STA $DB               ; Store the result and return
C10283    RTS
; +X/-Y               
C10284    SEC
C10285    LDA #$80
C10287    SBC $DB
C10289    STA $DB
C1028B    RTS
C1028C    LDA $DA
C1028E    BMI $C10296
; -X/-Y
C10290    TDC
C10291    SEC
C10292    SBC $DB
C10294    STA $DB
; -X/+Y
C10296    LDA $DB
C10298    RTS

A python equivalent of this function

def calculate_direction(source_x, source_y, target_x, target_y):
    """
    Calculates the direction angle between two sets of coordinates.
    Simulates the assembly routine at C10222.
    
    Args:
        source_x: X coordinate of the source point
        source_y: Y coordinate of the source point
        target_x: X coordinate of the target point
        target_y: Y coordinate of the target point
        angle_table: The lookup table from C0F300 (should have 1024 entries)
        
    Returns:
        Direction value (0-255, representing 0-360 degrees)
    """
    # Calculate X difference with sign
    x_diff = source_x - target_x
    x_sign = 0 if x_diff >= 0 else 0xFF
    
    # Calculate Y difference with sign
    y_diff = source_y - target_y
    y_sign = 0 if y_diff >= 0 else 0xFF
    
    # Convert to absolute values (using EOR and subtraction like the original)
    # Mask with 0xFF to simulate 8-bit registers
    x_abs = ((x_diff ^ x_sign) - x_sign) 0xFF
    y_abs = ((y_diff ^ y_sign) - y_sign) 0xFF
    
    # Prepare the table index
    x_component = x_abs >> 3  # Divide by 8
    y_component = (y_abs & 0xFFF8) << 2  # Mask to multiple of 8, then multiply by 4
    
    # Combine to form the final index
    combined_index = (y_component + x_component) & 0x3FF  # Limit to 10 bits (0-1023)
    
    # Look up the value from the provided table
    direction = angle_table[combined_index]
    
    # Adjust based on quadrant
    if x_sign == 0:  
        if y_sign != 0: 
            direction = 0x80 - direction
        else:
            direction += 0x80
    else:
        if y_sign == 0:
            pass
        else:
            direction = 0x00 - direction
    
    return direction & 0xFF

CalculateDistance

Finally, a comparatively simple function that calculates the distance between two entities

CalculateDistance:
; The A register holds the direction when entering this function
; First translate that direction into a lookup index
C101F9    REP #$20
C101FB    ASL
C101FC    ASL
C101FD    AND #$03FF
C10200    TAX
C10201    LDA $C0F900,X     ; Sine table lookup
C10205    AND #$00FF        ; Only keep the low byte
C10208    CPX #$0200       
C1020B    BCC $C10211       ; If the index was >= 0x200
C1020D    EOR #$FFFF            ; Negate it
C10210    INC                   ; Add one for 2's complement
C10211:                     
C10211    STA $A7           ; Store the value to A7
C10213    LDA $AE           ; Loads number of movement steps
C10215    AND #$00FF        ; Only keep the low byte
C10218    STA $A5           ; Store in $A5
C1021A    SEP #$20          ; Set 8-bit accumulator mode
; This function multiplies $A7 and $A5 and adds the result to $AA in 16-bit mode
; It then switches to 8-bit accumulator mode and that is the mode we use to read the result.
; This only keeps the high byte of the result, effectively dividing it by 255
C1021C    JSR $00A7 
C1021F    LDA $AA
C10221    RTS

And a python equivalent:

def calc_distance(direction, movement_speed):
    index = (direction << 2) & 0x3FF  # ASL twice and mask to 10 bits
    value = sine_table[index]  # Get value from table
    
    # Apply inversion for values in second half of the table (>= 0x200)
    if index >= 0x200:
        value *= -1
    
    # Scale by movement speed and only keep high byte
    result = (value * movement_speed) // 255 
    return result

InitializeMovement

This gives us the building blocks needed to intialize movement.

$DirTemp is $DB, it is populated either by the CalculateDirection function or if the direction has already been calculated it’s read from $9786 and stored in $DB. $EnemyIndex is $9C

initialize_movement:
C1379A    LDX $EnemyIndex   ; Load the current enemy index
C1379C    STZ $A5FD,X       ; Zero out something enemy related
C1379F    LDA $9791,X       ; Load indicator of whether a direction has been set
C137A2    BEQ $C137AB       ; If a direction is set...
C137A4    LDA $9786,X       ; Load direction
C137A7    STA $DirTemp      ; Store it to temp
C137A9    BRA $C137CC       ; Unconditional jump
C137AB:                     ; Else
; Calculate a new direction
C137AB    LDA $1D0F,X       ; Load enemy X position
C137AE    STA $D3           ; Store it to a temp variable
C137B0    STA $98AF,X       ; Store it to a the target X
C137B3    LDA $1D26,X       ; Do the same thing with Y
C137B6    STA $D4
C137B8    STA $98B7,X
C137BB    LDA $9752,X       ; Load target PC
C137BE    TAY
C137BF    LDA $1D0C,Y       ; Load target PC X
C137C2    STA $D5           ; Store it to temp
C137C4    LDA $1D23,Y       ; Load target PC Y
C137C7    STA $D6           ; store it to temp
C137C9    JSR $0222         ; CalculateDirection and store in $DirTemp

C137CC:
C137CC    LDA #$08          ; This 8 matches the number of movement steps used in ApplyMovement
C137CE    STA $AE
C137D0    LDX $EnemyIndex   ; Load enemy index into X
C137D2    LDA $DirTemp      ; Load and store the direction to the enemies
C137D4    STA $9786,X       ; direction address
C137D7    JSR $01F9         ; CalculateDistance for X
C137DA    STA $8C           ; Store it to $8C
C137DC    CLC
C137DD    LDA $DirTemp      ; Reload direction again
C137DF    ADC #$40          ; Add 0x40
C137E1    JSR $01F9         ; CalculateDistance for Y
C137E4    STA $8E           ; Store to $8E

C137E6    LDA $DirTemp
C137E8    TAX
C137E9    LDA $C0F700,X     ; This table maps angles to sprite facing directions
C137ED    LDX $EnemyIndex           
C137EF    STA $96DE,X       ; Store sprite direction for this enemy
C137F2    LDA $9752,X       ; Load current target for this enemy
C137F5    TAY
; Make a copy of target's X and Y
C137F6    LDA $1D0C,Y      
C137F9    STA $A039,Y
C137FC    LDA $1D23,Y
C137FF    STA $A050,Y
; Make a copy of the enemie's X and Y
C13802    TXA
C13803    CLC
C13804    ADC #$03
C13806    TAX
C13807    LDA $1D0C,X
C1380A    STA $A039,X
C1380D    LDA $1D23,X
C13810    STA $A050,X
; Determine which distance to use when determining if we're close enough
C13813    LDA $A2B5         ; This is a parameter in the Enemy AI script
C13816    DEC               ; Decrement it
C13817    BNE $C1381E       ; If $A2B5 was 1 then
; This function checks to see if the distance between the enemy and
; the target is < 0x400. 
C13819    JSR $2AEF         
C1381C    BRA MoveTowardsTargetIfNotInRange
C1381E    DEC               ; Decrement AI value again
C1381F    BNE $C13826       ; If $A2B5 was 2 then
 ; This path hasn't been analyzed yet, but based on the branch
 ; to MoveTowardsTargetIfNotInRange it is probably setting a different
 ; distance threshold
C13821    JSR $2AFB        
C13824    BRA MoveTowardsTargetIfNotInRange
; If $A2B5 was 3 do something similar to if it was 2
C13826    DEC
C13827    BNE $C13831
C13829    JSR $2B07
; The implementation of MoveTowardsTargetIfNotInRange follows this line
; so the BRA instruction isn't needed for this scenario

At this point we’ve calculated a direction, how far to move, and whether we’re “in range”. Then all paths lead to MoveTowardsTargetIfNotInRange

Move towards the target for attack

CalcBoundaries

This function calculates the collision boundaries of a sprite and stores them for collision detection.

CalcBoundaries:
; Calculate the left boundary and store it to $975A
C1285A    SEC                ; Set carry flag (for subtraction)
C1285B    LDA $A039,X        ; Load X position
C1285E    BMI $C12868        ; Branch if negative (left side of screen)
C12860    SBC $971E,X        ; Subtract width value
C12863    BPL $C1286B        ; If result is positive, store it
C12865    TDC                ; Otherwise, set A to 0
C12866    BRA $C1286B        ; Branch to store
C12868:                      ; If X was negative:
C12868    SBC $971E,X        ; Still subtract width
C1286B:                      ; Store result
C1286B    STA $975A,X        ; Store left boundary

; Calculate the right boundary and store it to $9770
C1286E    CLC                ; Clear carry flag (for addition)
C1286F    LDA $A039,X        ; Load X position again
C12872    BPL $C1287D        ; Branch if positive (right side of screen)
C12874    ADC $971E,X        ; Add width
C12877    BMI $C12880        ; If result still negative, store it
C12879    LDA #$FF           ; Otherwise, cap at FF (255)
C1287B    BRA $C12880        ; Branch to store
C1287D:                      ; If X was positive:
C1287D    ADC $971E,X        ; Add width
C12880:                      ; Store result
C12880    STA $9770,X        ; Store right boundary

; Calculate upper boundary and store it to $9765
C12883    SEC                ; Set carry flag (for subtraction)
C12884    LDA $A050,X        ; Load Y position
C12887    BMI $C12891        ; Branch if negative (top of screen)
C12889    SBC $9729,X        ; Subtract height value
C1288C    BPL $C12894        ; If result is positive, store it
C1288E    TDC                ; Otherwise, set A to 0
C1288F    BRA $C12894        ; Branch to store
C12891:                      ; If Y was negative:
C12891    SBC $9729,X        ; Still subtract height
C12894:                      ; Store result
C12894    STA $9765,X        ; Store top boundary

; Calculate lower bounadry and store it to $977B
; This includes handling a special case where bit 2 is set in $2989
; When this is the case the Y position is used as the bottom boundary
; Otherwise 8 pixels are added to the Y position
C12897    LDA $2989          ; Load battle settings byte 1
C1289A    AND #$04           ; Check bit 2
C1289C    BEQ $C128A6        ; If not set, branch to normal calculation
C1289E    LDA $A050,X        ; Load Y position
C128A1    STA $977B,X        ; Store directly as bottom boundary
C128A4    BRA $C128AF        ; Branch to RTS
C128A6:                      ; Normal calculation:
C128A6    CLC                ; Clear carry flag (for addition)
C128A7    LDA $A050,X        ; Load Y position
C128AA    ADC #$08           ; Add 8 pixels
C128AC    STA $977B,X        ; Store as bottom boundary
C128AF    RTS                ; Return from subroutine

CheckBoundaryCollision

This function takes the previously calculated boundaries for the current entity and then compares it’s boundary with the boundaries of all other entities to check for collisions. If the entity collides with another enemy then 0x81 is returned, if it collides with a PC then 0x80 is returned. If no collision occurs then 0 is returned.

C128B0    TDC             
C128B1    TAY             
C128B2    LDX $80                       ; Load current entity index into X
$C128B4:                                ; Main loop start
C128B4    CPY $80                       ; Compare Y with current entity index
C128B6    BEQ no_collision              ; Skip if comparing entity with itself
C128B8    LDA $96F5,Y                   ; Check if entity Y is active
C128BB    BEQ no_collision              ; Skip if entity Y is inactive
C128BD    LDA $9FF7,Y                   ; Check another flag
C128C0    BMI no_collision              ; Skip if bit 7 is set
C128C2    LDA $A5CD,Y                   ; Check another flag 
C128C5    BMI no_collision              ; Skip if bit 7 is set
; Check left boundary
C128C7    LDA $975A,X                   ; Load left bound of entity X
C128CA    CMP $975A,Y                   ; Compare with left bound of entity Y
C128CD    BCC $C128F1                   ; If X's left < Y's left, branch to check other case
C128CF    CMP $9770,Y                   ; Compare X's left with Y's right
C128D2    BEQ $C128D6                   ; If equal, continue checks
C128D4    BCS no_collision              ; If X's left >= Y's right, no collision, continue loop
$C128D6:                                ; X's left bound is in Y's horizontal range
C128D6    LDA $9765,X                   ; Load top bound of entity X
C128D9    CMP $9765,Y                   ; Compare with top bound of entity Y
C128DC    BCC $C128E7                   ; If X's top < Y's top, branch to check other case
C128DE    CMP $977B,Y                   ; Compare X's top with Y's bottom
C128E1    BEQ collision_detected        ; If equal, collision detected
C128E3    BCC collision_detected        ; If X's top < Y's bottom, collision detected
C128E5    BCS no_collision              ; If X's top >= Y's bottom, no collision
C128E7:
C128E7    LDA $977B,X
C128EA    CMP $9765,Y
C128ED    BCC no_collision
C128EF    BCS collision_detected
C128F1:
C128F1    LDA $9770,X
C128F4    CMP $975A,Y
C128F7    BCC no_collision
C128F9    LDA $9765,X
C128FC    CMP $9765,Y
C128FF    BCC $C1290A
C12901    CMP $977B,Y
C12904    BEQ collision_detected
C12906    BCC collision_detected
C12908    BCS no_collision
C1290A:
C1290A    LDA $977B,X
C1290D    CMP $9765,Y
C12910    BCC no_collision
C12912    BCS collision_detected
no_collision:                           ; No collision with entity Y
C12914    INY                           ; Increment Y to check next entity
C12915    CPY #$000B                    ; Compare Y with max entities (11)
C12918    BNE $C128B4                   ; If not done, continue loop
C1291A    TDC                           ; If no collisions found, clear A (return 0)
C1291B    BRA $return                   ; Branch to return
collision_detected:                     ; Collision detected!
C1291D    LDA #$80                      ; Load A with $80 (bit 7 set)
C1291F    CPY #$0003                    ; Compare Y with 3
C12922    BCC $return                   ; If Y < 3 (player character), return $80
C12924    INC                           ; Otherwise (enemy), increment A to $81
return:
C12925    RTS             

MoveTowardsTargetIfNotInRangec

MoveTowardsTargetIfNotInRange:
C1382C    LDA $9872
C1382F    BMI target_not_in_range
; If the target is already in range some values are updated and the target's
; X/Y coordinates are stored at $9831,X and $9839,X where X is the attacking
; entity ID.
; Right now the focus is on movement and this is the case where movement is
; finished, so this may be investigated further when researching attacks themselves.
C13831    LDX $EnemyIndex               ; Load current enemy index
C13833    INC $9829,X                   ; Increment some counter/state variable
C13836    LDA $5E0D,X                   ; Load a value (believed to be an AI script value)
C13839    STA $5E1D,X                   ; Store it elsewhere
C1383C    LDA $9752,X                   ; Load current enemy's target entity ID
C1383F    TAY                           ; Transfer to Y for indexing
C13840    LDA $1D0C,Y                   ; Load target's X coordinate
C13843    STA $9831,X                   ; Store as target X coordinate
C13846    LDA $1D23,Y                   ; Load target's Y coordinate
C13849    STA $9839,X                   ; Store as target Y coordinate
C1384C    STZ $98A7,X                   ; Clear movement flag, we're done moving
C1384F    BRA exit                      ; Branch to exit

target_not_in_range:
C13851    LDX $EnemyIndex               ; Load current enemy index
C13853    CLC                           ; Clear carry for addition
C13854    LDA $1D26,X                   ; Load Y coordinate
C13857    ADC $8C                       ; Add movement delta Y
C13859    STA $A053,X                   ; Store new Y position
C1385C    CLC                           ; Clear carry for addition
C1385D    LDA $1D0F,X                   ; Load X coordinate
C13860    ADC $8E                       ; Add movement delta X
C13862    STA $A03C,X                   ; Store new X position
; Here the entity ID is incremented 3 times. This is because the functions that
; are about to be called are working with the Entity index rather than the Enemy index.
; That is, PCs are Entity 0-2, Enemies are Entity 3-B, but we've been working with 0 indexed
; Enemy IDs (0-8).
C13865    INX
C13866    INX
C13867    INX
C13868    STX $80
C1386A    JSR $CalcBoundaries           ; Populate boundary positions for this entity

C1386D    STZ $9875
C13870    LDA $A2B5                     ; This is one of the AI Script parameters
C13873    CMP #$03                      ; If it's not 3 then keep the value as is
C13875    BNE $not_three                ; and keep $9875 as 0
C13877    LDA #$01                      ; Otherwise, load 1 into A and $9875
C13879    STA $9875
not_three:
C1387C    JSR $CheckEnvironmentCollision
C1387F    BMI $collision_true
; If the AI parameter is not 3 then we also perform entity collision detection.
; Otherwise we skip that check.
C13881    LDA $A2B5                     ; Load AI parameter again
C13884    CMP #$03                      ; Compare with 3
C13886    BEQ $movement_success         ; If 3, branch to movement success
C13888    JSR $28B0                     ; Call entity collision detection routine
C1388B    BPL $C138AF                   ; If no collision with entities, branch to movement success
collision_true:
; If a collision is detected the enemy's direction is modified, the movement flag is cleared
; and they will try to move in this new direction next time.
C1388D    LDX $EnemyIndex
C1388F    CLC
C13890    LDA $DirTemp
C13892    ADC #$40                      ; Add 90 degrees to the direction
; These shifts are taking the new direction and simplifying it down to a single
; cardinal direction. So for example, if the enemy is trying to move 45 degrees
; down and to the right 90 degrees would be added, moving it down and to the left
; but then that is essentially rounded down to just moving down.
; If collision detection fails again for the same entity it would then move left,
; then up, then right on subsequent failures.
C13894    LSR
C13895    LSR
C13896    LSR
C13897    LSR
C13898    LSR
C13899    LSR
C1389A    ASL
C1389B    ASL
C1389C    ASL
C1389D    ASL
C1389E    ASL
C1389F    ASL
C138A0    STA $DirTemp
C138A2    LDA $DirTemp
C138A4    STA $9786,X                   ; Store the new direction
C138A7    INC $9791,X                   ; Increment collision counter
C138AA    STZ $98A7,X                   ; Zero out movement flag so that ApplyMovement skips this entity
C138AD    BRA $exit
movement_success:
C138AF    LDX $EnemyIndex
C138B1    STZ $9791,X                   ; Clear collision counter
C138B4    LDA #$01
C138B6    STA $98A7,X                   ; Set enemy ready to move in ApplyMovement
exit:
; Increment X again to get the Entity ID rather than Enemy ID, store it to $80, and return
C138B9    INX
C138BA    INX
C138BB    INX
C138BC    STX $80
C138BE    RTS

At this point, the enemy is either ready to move towards the target PC or their movement would cause a collision and they are ready to be recalculated next time through. However, since the movement flag was cleared out that enemy will need to wait out it’s movement timer before it can try again, so in the meantime it will stand still.

A Practical Example

During the first battle in Truce Canyon against the three Blue Imps the first entry in their AI script is to do a close range attack on the nearest PC (there’s only one PC so it’s always Crono)

First frame

After a few more frames Enemy 0’s movement timer is up so it calculates a direction. Enemy 0 is the top right Blue Imp in this fight. When the direction was initially calculated it was calculated as 0x55. However, a collision was detected (either the flowers or the rock, I’m not 100% sure at this point). Because of the collision the direction is recalculated as direction + 0x40 rounded down to the nearest 90 degree angle which is 0x80.

Wrapping my head around the angles here is tough, keep in mind that X values increase going left to right while Y values increase going top to bottom (so the top left of the screen is 0,0 rather then the bottom left as you would expect in a normal graph). Because of this and angle of 0x40 is straight down rather than straight up and 0xC0 is straight up. However, because X behaves like a normal graph 0x00 is right and 0x80 is left.

With this in mind, an angle of 0x55 is down and slightly left, which matches what we see in the image. Since there is a collision the angle is moved to be straight left instead.

This will all need to wait though because the collision causes the movement timer to reset, so it will be another 16 frames before enemy 0 tries to move again.

First enemy direction

This frame is more straight forward, there are no obstacles for the next Blue Imp so it simply calculates the direction towards Crono and is marked ready to move because they did not have collisions.

While it was marked active, they do not move on this frame because of their move rate (discussed in ApplyMovement) only moves them every other frame.

Next enemy direction

On the next frame the final enemy sets it’s direction and Enemy 1’s (E1) movement timer decrements.

Final enemy direction

On the next frame E1’s movement timer has hit 0 and reset, so it moves. Meanwhile E2’s movement timer decrements.

E1 moves

E2 moves and E1’s movement timer decrements again.

E2 moves

Several frames later E1 is about to finish the movement.

E1 finishing

The next frame E1 has finished and E2 is about to finish.

E2 finishing

Both enemies have now finished moving. E0 is finally about to come out of the penalty box.

E2 finished

On the next frame E0 is finally marked as active.

E0 ready to move

Several frames later, E0 has finished moving.

E0 finished

From here the pattern continues, E0 hasn’t cleared the flowers/rock so it fails the collision check again and will be delayed another 16 frames. E1 and E2 will finish getting close enough to attack.

What’s next?

One thing that can be seen in the images from the example is that the movement timers are updating 2 times per ATB tick, but sometimes ApplyMovement is called 3 times. It’s not clear at this point what the timing is, hopefully it is something as simple as having a 3 update frame every X frames.

This analysis has only covered one movement scenario, the movement towards a PC prior to a close range attack. There are many other movement scenarios that also need to be analyzed.