After the random Faxanadu factoids of the last updates it's about time to make some more structured updates again: It's time to begin a new series of updates about a certain topic. And it's going to be quite a large series, so large in fact that I can't yet estimate how many parts this new series will contain. Anyway, the topic of this new series of updates is the collision detection code of Faxanadu. What exactly is going to be part of the next updates will become more clear after you've finished reading this update as the topic of this update is the main collision detection function from where all other functions dealing with collision detection are called.
Nearly all collision detection code is stored in ROM bank 14. The only exceptions are some calls to functions located in the Kernel bank and some meta-data and graphical data that must be loaded from other banks. The already mentioned main function of the collision detection code starts right at offset $8000, ends at offset $806D and is basically mainly a loop that calls the functions that perform the actual collision detection. For this reason it's a very good way to get an idea about what's going on under the hood. Let's get to the code.
At the very beginning of the main function two conditions for freezing all sprites on the screen are checked. Offset $426 contains a flag that indicates whether or not the user just used his elixir (used to restore the player's full health). People who know Faxanadu probably know that using the elixir freezes all sprites (including the player himself) until all health points are restored. The other check tests whether the hour glass (an item to freeze all enemies temporarily) is active. If it's not active then offset $42A will contain the value $FF, otherwise the remaining time of the hour glass is stored there.
If neither of these two items are active the value at offset $383 is increased. This value is used as some kind of timer when moving sprites.
Afterwards the main loop of the main function begins. All sprites on the screen are moved, updated and tested for all kinds of collisions with the player, magic, blocks and other things. There's a fixed maximum of 8 sprites per screen. This becomes obvious when looking at how information about active sprites is stored in memory. All data about the active sprites is stored in lots of arrays of size 8. The first of these arrays is located between offset $2CC and offset $2D3, it contains the sprite ID of each of the sprites on the screen. If less than 8 sprites are active the sprite ID of these inactive sprites is set to $FF and they are skipped in the main loop. The value at offset $378 is used to keep track on which sprite is in the process of being updated.
<br />$800F-> A2 7: LDX #$7 ; Max sprite number
<br />$8011-> 8E 78 3: STX $378
<br />$8014-> BD CC 2: LDA $2CC,X ; Sprite IDs
<br />$8017-> 30 2A: BMI $8043
Afterwards it's once again checked if the elixir is active and if this is in fact the case nearly all of the loop is skipped over. Then the second of the sprite data arrays ($34C - $353) is used the first time. This array contains timers that are activated when the user hits an enemy. In that case normal sprite movement isn't used to update the sprite anymore, succesfully hitting an enemy is shown to the user by making the enemy sprite flicker on the screen for a very brief period of time. If an enemy is still supposed to flicker (e.g. his flicker timer is still not zero) the jump is taken.
On with the next sprite data array ($334 - 34B). If the player hits an enemy with magic the ID of the magic used is stored in this array. This is necessary because certain kinds of magic force the enemy some pixels away from the player. This is also exactly what happens in the function at $8151 which is called afterwards.
Now comes the main part of loop. In the following lines of code all the major functions responsible for sprite movement are called. In the first function the sprite behaves according to it's behaviour specification data. The second function determines whether the player hit an enemy with his weapon, the third loop checks if the player hit an enemy with magic. The fourth loop calculates some sprite size information necessary for the next two functions which determine whether the player was hit by enemy magic or if the player touched another sprite. Like in the first function the last function is also about making the sprite behave in a certain way. More on all of that will come in later updates.
<br />$802E-> 20 6B A6: JSR $A66B ; Move sprite
<br />$8031-> 20 4 88: JSR $8804 ; Hit by weapon
<br />$8034-> 20 DC 8A: JSR $8ADC ; Hit by magic
<br />$8037-> 20 8 8A: JSR $8A08 ; Get sprite size
<br />$803A-> 20 7C 87: JSR $877C ; Player was hit by magic
<br />$803D-> 20 A 89: JSR $890A ; Player touched a sprite
<br />$8040-> 20 D2 8B: JSR $8BD2 ; Update sprites
The last part of the loop decreases the number of the sprite to update and restarts the loop if necessary.
After the main loop there's some code left. This code is responsible for updating a sprite after it was hit (the flickering effect). The flicker counter is decreased and the sprite is horizontally moved by two 2 pixels (that's what the function at the 2nd to last line does; it takes parameters in $374 and $375). Unless the sprite was one of three specifically specified sprites which never move anywhere because they're locked at the position where they are placed. If you wonder where exactly the flickering effect comes from, it's calculated from the value of the flicker counter when the sprite is redrawn.
Here's something neat that I had discovered, does it look familiar?
[pre]ROM:8877 AND #2 ; Check if player has pendant.
ROM:8879 BNE loc_8884 ; Continue reading the following lines, if the player doesn't have the pendant. Otherwise, skip to ROM:8884.
ROM:8880 ADC byte_0 ; Increase attack by 25%
ROM:8884 ; CODE XREF: TryToHitWithWeapon+75j[/pre]
The game originally gave you an attack bonus of 25%, if you don't have the pendant. Everyone else had just assumed that the pendant reduced attack power, which isn't exactly correct.
Thanks to your disassembly, I managed to solve this mystery.