As for the Zodiac code.... I know about that. I've looked at that code. It gave me a headache. And since I haven't been able to get far enough in the demo to where those enemies appear, I don't know how they actually behave.
Shame on you. The code might be long (100k lines) but everything you want is in a single script, General_Sidescrolling_Enemy. There are two of them because the script got too long to compile and/or run correctly.
If you want to make custom enemies, I suggest you use logic like this. I think it's commented pretty well, but heres' the logic behind it.
There is a single script that runs all (or many) of the enemies. Hence the "general" in the name. Here's how it works.
(1) Initialize the Enemy
Basically, when the script starts, it checks to see what kind of enemy it is. I accomplish this by passing the FFC a variable using it's "Ax" field, but you could do it by setting the FFC, or even using the data variables if you can do it. The important thing is that you want the field that sets the enemy type to be something another script can set, not a D0 - D7 variable that the user has to set. This becomes important later.
Anyway, once the script determines what kind of enemy it is (and, if you're using Ax as the variable-passing field, set that field back to 0 so it doesn't muck up the enemy's movement) then the script sets up variables for that kind of enemy. It creates a fire-type enemy to act as the Ghost and sets that NPC's health and item drops accordingly. It sets some local variables to control the damage and whatever else you care about for this kind of enemy.
Since I spawn my enemies randomly, here's where the code calls another function that looks for a place to spawn this particular enemy. I have different spawn requirements for different enemies; some have to be on the ceiling, some need to be on the wall, etc. Since I can't pass back X and Y variables, I just have this little sub-function move the FFC to its spawn point directly.
Another thing I do when initializing an enemy is I grab a second FFC and set it to a blank damage combo dependent on the "damage" level of this enemy. It gets stashed off screen initially (not very far; if you push FFCs much beyond 1 tile off screen they will stop functioning, so beware of that). This becomes important later.
(2) Using Hit Points to Determine Loop While Enemy Is Alive
Once that's all done, the script enters a while loop that executes while the NPC is alive. Now, I do something strange with hit points to determine if the NPC is alive. See. all the enemies in Zodiac, including bosses, actually have 100 more hit points that you can see. Why would I do that? Here's the thing. I want to know when the enemy dies and do something afterwards. An explosion, clearing the FFC's variables, whatever. I ran into a problem back in 2008 with this where once an NPC dies, my script had a hard time detecting that. I suspect what was happening was the NPC pointer was dependent on the Screen's NPC index, so killing one enemy reset the index and sometimes resulted in the pointer staying valid but pointing at something else.
Whatever the reason, I found it better to always set hit points 100 HIGHER than the in-game hit points I wanted. Then, when the enemy's hit points drop below 100, I can tell the script to stash the enemy off screen while I do whatever I want to do when it dies, and then clean up the NPC while resetting the FFC. Anyway, that means that the While loop that determines each NPC's behavior after it is set up runs while the ghost's hit points are greater than 100. If that stops being true, the code moves on from the loop to the death behavior.
(3) Graphics and Collision Detection
What does the enemy have to do while it is alive? Well, a few things. It has to draw graphics showing itself and it has to detect collisions. Now, not every enemy in my game cares about the solidity of combos, but they do all care about contact with Link. Since I want enemies to be of arbitrary size, and I want the graphics to line up with the collision detection for the player, I handle both (1) drawing and (2) collision detection with the player using the same function.
This function accepts the ID of the enemy and some other information; check the code. If the enemy is frozen, this function draws it using a blue palette and does nothing else. If the enemy has recently been damaged, it randomly assigns a new CSet each frame. Whatever else it is doing, it will then draw the appropriate frame of animation (using Drawtile directly). It will also check to see if the player is within the enemy's damage area. This is usually just a square, since that's easy, but it doesn't have to be. You can define whatever shape area you want. Some of my enemies, like the hoppers, have kind of a U-shape area.
If the player is inside this zone, for one frame, the script takes that off-screen damage FFC we set up earlier and plops it on top of the player. That way, the ZClassic engine handles all the knockback and damage and that jazz. (It also means you shouldn't use the anti-spike boots in a quest that does things this way, although you could use an offsreen NPC for the same effect if you really wanted to.)
(4) Behavior
Okay, now we have to tell the enemy what to do. I tend to organize enemies with simple AI based on States. The enemy can be in one of several "states," each organized around a simple concept. One state might be walking back and forth. Or jumping. Or firing a shot. Anyway, within each state I increment "state_counter" that I use to determine time within that state. At some point, perhaps after enough time has passed or after the player is in a certain position or whatever, the enemy has to make a decision about a new state. And so it does. In a nutshell, that's how every enemy in Zodiac thinks.
I handle collision with solid combos here, state by state. That's because some enemies have different collision in different states. For enemies that are effected by gravity, I usually set up a constant gravity effect outside of any particular state.
(5) Death
Since ZClassic is keeping track of damage to the NPC, all you have to do is wait for the ghosts HP to drop below 100. When that happens, you kill the associated NPC (triggering its item drop) and set the Data field of this FFC and the FFC it was using for damage to 0. That stops them from doing anything and sets them up to be used again later. Easy peasy. If you want, you can have another loop here before you get that far that keeps drawing the enemy during an explosion. Spawning lweapon Bomb Explosions in a general area around the FFC gives you a nice explosion result.
(6) In-game Setup
Now, you don't want to set up each friggin' enemy on every FFC of every screen. That would be tedious as hell. So, instead, I have another function that sets them up for you. On each room, you load up the enemy setter function on an FFC. In it's variables, tell it the Index of the enemies to spawn, and how many. This script then goes and looks for FFCs using data 0 (meaning they are not in use), assigns them the general_enemy script, and passes them the ID of the enemy it should start acting like.
You COULD handle the initial placement of the enemy in this function, too, and that's probably the best place for it. Because I had already coded random placement directly into the enemy script, I had to pass a second variable to the newly born enemy (using Ay) that tells it if it needs to find a home or not.
When would you not want the NPC to find its own home? Well, if it was spawned, that's why. You can use the general_enemy script to make things like spawned worms, missiles, and whatever the hell else. Any other script in the game can go look for a Data == 0 FFC and then load the right variables into it to make a new enemy. But, if you don't turn off the spawn location part of the initalization, it will not spawn where your new script wants it.
And...
That's it, really. There are some small wrinkles for particular problems in some of the enemy code, but this is all that you're looking at. It's really easy to manipulate this code. For a simple enemy that doesn't make complicated decisions, you can use this kind of code to crank out new NPCs at like two or three an hour. It's neat.