2024-12-07

Sample Lufia 2 Fancy Character Movement Analysis

As I've noted previously, I've been working on decoding the scripting language Lufia 2 uses and trying to understand the less obvious details of what's going on behind the scenes in the various, well, scenes. (Lufia 1's scripting seems to be largely similar, incidentally, so feel free to use this as reference if looking into that.) Primarily, the point of this has been to extract the text cleanly and figure out what lines are used when, as well as what the progression of event flags looks like, but it's also been interesting to see how some of the more elaborate and expressive animations the game does so well work.

Let's take the scene in Tanbel when Maxim meets Guy and they decide to have a friendly duel as an example. It seems simple enough from the player's point of view. They face each other in an open plaza, Guy leaps at Maxim, and then they're interrupted before anything else happens. A surprising amount of effort goes into making the scene look good, though.

We'll start after everyone has moved into place and the initial banter has concluded.

Maxim and Guy face each other with an empty tile between them, while Tia watches from out of the way.

(Green text in blockquotes like the below is more or less what comes out of my script parsing program, minus the address offset data that isn't relevant here.)

{$34 1C 23: Change Actor $1C (Guy NPC)'s sprite to $23.}
{$37 0A: Brief delay, lasting 10 frames.}

Guy first switches to a different sprite to show him getting in a stance as preparation to make his leap.

Note that because he's not currently a party member, the Guy in this scene is a dedicated NPC.

{$4C 0C: Play sound effect #12}
{$34 1C 25: Change Actor $1C (Guy NPC)'s sprite to $25.}
{$34 00 1D: Change Actor $00 (Maxim)'s sprite to $1D.}
{$6C 1C FE FC: Draw Actor $1C (Guy NPC) at -2px X, -4px Y.}
{$30 5F 07: Put Actor $5F at map location #7.}
{$73 26 07: Play animation #38 at map location #7.}
{$37 01: Brief delay, lasting 1 frames.}

We hear Guy spring up, Maxim changes sprites to show that he's bracing himself, Guy changes into a leaping sprite and his displayed position is shifted two pixels to the left and four upward as his leap begins, and a dust cloud animation plays at Guy's starting location as Actor $5F is moved there. But what's Actor $5F?

Note that Guy has not logically moved at all; only the position where his sprite is drawn has changed. As far as the game is concerned, he's still actually on the ground where he started. If you could pause the scene and walk around, that's where you would find his collision and that's where you'd go to interact with him.

Of course, you can't pause the scene and walk around, so where he appears to be is all that really matters to the player at the moment.

{$6C 1C FC F8: Draw Actor $1C (Guy NPC) at -4px X, -8px Y.}
{$6C 5F FC 00: Draw Actor $5F at -4px X, 0px Y.}
{$37 02: Brief delay, lasting 2 frames.}

As Guy continues upward and leftward, Actor $5F follows him to the left, but stays at his initial height. It's his shadow!

Most sprites have shadows built in at the character's feet. Sprite $25 doesn't, so a separate shadow-only sprite is used in conjunction with it to give the impression that Guy is actually leaping upward and not merely circling around Maxim to the north side.

{$6C 1C FA F4: Draw Actor $1C (Guy NPC) at -6px X, -12px Y.}
{$6C 5F FA 00: Draw Actor $5F at -6px X, 0px Y.}
{$37 03: Brief delay, lasting 3 frames.}
{$6C 1C F7 F0: Draw Actor $1C (Guy NPC) at -9px X, -16px Y.}
{$6C 5F F7 00: Draw Actor $5F at -9px X, 0px Y.}
{$37 04: Brief delay, lasting 4 frames.}
{$6C 1C F4 EE: Draw Actor $1C (Guy NPC) at -12px X, -18px Y.}
{$6C 5F F4 00: Draw Actor $5F at -12px X, 0px Y.}
{$37 05: Brief delay, lasting 5 frames.}
{$6C 1C F0 EC: Draw Actor $1C (Guy NPC) at -16px X, -20px Y.}
{$6C 5F F0 00: Draw Actor $5F at -16px X, 0px Y.}
{$37 06: Brief delay, lasting 6 frames.}

The shadow continues leftward with Guy as he continues leftward and upward.

Notice that the delays are gradually increasing, while the vertical movements are gradually shortening, giving the impression of gravity affecting the jump.

Meanwhile, the horizonal increments increase at more or less the same rate as the delays, keeping the leftward speed more or less steady.

{$9E 14: Floating textbox; arg affects Y position:}
 Guy!!<$01=NEXT>

But now someone shouts, interrupting the action.

{$4C 14: Play sound effect #20.}
{$34 1C 23: Change Actor $1C (Guy NPC)'s sprite to $23.}
{$34 00 00: Change Actor $00 (Maxim)'s sprite to $00.}
{$6C 1C F0 EE: Draw Actor $1C (Guy NPC) at -16px X, -18px Y.}
{$37 04: Brief delay, lasting 4 frames.}

With the action interrupted, Maxim resets to his default sprite. Guy changes to a sprite with his arms in the air and starts falling with a corresponding sound effect.

Note that the detached shadow is still there on the ground below Guy, but the sprite he's switched to has its own built-in shadow, so he now has two shadows, one of which is hovering off the ground along with him. You should be able to spot this even at full speed if you're watching for it.

{$6C 1C F0 F0: Draw Actor $1C (Guy NPC) at -16px X, -16px Y.}
{$37 04: Brief delay, lasting 4 frames.}
{$6C 1C F0 F4: Draw Actor $1C (Guy NPC) at -16px X, -12px Y.}
{$37 04: Brief delay, lasting 4 frames.}
{$6C 1C F0 F8: Draw Actor $1C (Guy NPC) at -16px X, -8px Y.}
{$37 04: Brief delay, lasting 4 frames.}

Guy accelerates after beginning to fall, now topping out at 4 pixels per 4 frames, after starting with two steps of 2 pixels per 4 frames. His fall could be further smoothed out by using a series of 1-pixel shifts with 1-frame delays, but that seems like overkill. At full speed, it looks just fine as it is.

{$2E 5F: Deactivate Actor $5F.}
{$6C 1C F0 FC: Draw Actor $1C (Guy NPC) at -16px X, -4px Y.}
{$37 04: Brief delay, lasting 4 frames.}

Once Guy is a quarter of a tile from the ground, the detached shadow is removed by deactivating the associated actor.

{$6C 1C F0 00: Draw Actor $1C (Guy NPC) at -16px X, 0px Y.}
{$37 04: Brief delay, lasting 4 frames.}

And, at last, Guy has landed. The game is currently drawing him one full tile directly to the left of where it logically considers him to be.

{$34 1C 02: Change Actor $1C (Guy NPC)'s sprite to $02.}
{$30 1C 06: Put Actor $1C (Guy NPC) at map location #6.}
{$6C 1C 00 00: Draw Actor $1C (Guy NPC) 0 X and 0 Y pixels.}

Finally, Guy resets to his default sprite. The game also both moves his logical position onto the tile where he already appears to be and resets his rendering settings to draw him at his logical location, so although internally he's shifted one tile to the left, there's no visible movement.

All that just for Guy to leap at Maxim and fall to the ground! But the results are undeniable.

2024-12-01

The media has failed us

The local newspaper recently expressed some frustration with divisiveness and people talking past each other, and requested that readers write in, particularly regarding concrete reasons why they voted for one candidate or another. And I wouldn't normally bother, but something about the tone of it just got to me, particularly with how so much of mainstream media has, for years, consistently been bothsidesing everything and refusing to take a stand on anything, facts and consequences be damned.

I ended up toning down some of what I wanted to say and cutting out some relevant but perhaps overly verbose side rants, like noting how WaPo's billionaire owner decided to kiss up to fascism instead of allowing the actual writers to publish an endorsement of Kamala Harris, or how I wish she had been even half as much for they/them—which is a good thing to be!—as the opposing campaign claimed instead of remaining silent, or that most Democrats aren't even reliably progressive let alone leftist, or how the incoming Cabinet is composed of people who aside from general incompetence are openly hostile to the entire purpose of the departments they'll be in charge of, or...

...yeah, I think some editing was for the best. It's enough of a rant already without all that. Anyway, here's what I ended up sending them.