- 📝 Posted:
- 🚚 Summary of:
- P0212, P0213
- ⌨ Commits:
d398a94...363fd54
,363fd54...158a91e
- 💰 Funded by:
- LeyDud, Lmocinemod, GhostRiderCog, Ember2528
- 🏷 Tags:
Wow, it's been 3 days and I'm already back with an unexpectedly long post about TH01's bonus point screens? 3 days used to take much longer in my previous projects…
Before I talk about graphics for the rest of this post, let's start with the exact calculations for both bonuses. Touhou Wiki already got these right, but it still makes sense to provide them here, in a format that allows you to cross-reference them with the source code more easily. For the card-flipping stage bonus:
Time | min(( Stage timer * 3), 6553) |
---|---|
Continuous | min(( Highest card combo * 100), 6553) |
Bomb&Player | min((( Lives * 200) + ( Bombs * 100)), 6553) |
STAGE | min(( (Stage number - 1) * 200), 6553) |
BONUS Point | Sum of all above values * 10 |
The boss stage bonus is calculated from the exact same metrics, despite half of them being labeled differently. The only actual differences are in the higher multipliers and in the cap for the stage number bonus. Why remove it if raising it high enough also effectively disables it?
Time | min(( Stage timer * 5), 6553) |
---|---|
Continuous | min(( Highest card combo * 200), 6553) |
MIKOsan | min((( Lives * 500) + ( Bombs * 200)), 6553) |
Clear | min(( Stage number * 1000), 65530) |
TOTLE | Sum of all above values * 10 |
The transition between the gameplay and TOTLE screens is one of the more impressive effects showcased in this game, especially due to how wavy it often tends to look. Aside from the palette interpolation (which is, by the way, the first time ZUN wrote a correct interpolation algorithm between two 4-bit palettes), the core of the effect is quite simple. With the TOTLE image blitted to VRAM page 1:
- Shift the contents of a line on VRAM page 0 by 32 pixels, alternating the shift direction between right edge → left edge (even Y values) and the other way round (odd Y values)
- Keep a cursor for the destination pixels on VRAM page 1 for every line, starting at the respective opposite edge
- Blit the 32 pixels at the VRAM page 1 cursor to the newly freed 32 pixels on VRAM page 0, and advance the cursor towards the other edge
- Successive line shifts will then include these newly blitted 32 pixels as well
- Repeat
(640 / 32) = 20
times, after which all new pixels will be in their intended place
So it's really more like two interlaced shift effects with opposite directions, starting on different scanlines. No trigonometry involved at all.
Horizontally scrolling pixels on a single VRAM page remains one of the few
📝 appropriate uses of the EGC in a fullscreen 640×400 PC-98 game,
regardless of the copied block size. The few inter-page copies in this
effect are also reasonable: With 8 new lines starting on each effect frame,
up to (8 × 20) =
160 lines are transferred at any given time, resulting
in a maximum of (160 × 2 × 2) =
640 VRAM page switches per frame for the newly
transferred pixels. Not that frame rate matters in this situation to begin
with though, as the game is doing nothing else while playing this effect.
What does sort of matter: Why 32 pixels every 2 frames, instead of 16
pixels on every frame? There's no performance difference between doing one
half of the work in one frame, or two halves of the work in two frames. It's
not like the overhead of another loop has a serious impact here,
especially with the PC-98 VRAM being said to have rather high
latencies. 32 pixels over 2 frames is also harder to code, so ZUN
must have done it on purpose. Guess he really wanted to go for that 📽
cinematic 30 FPS look 📽 here…
Once all the metrics have been calculated, ZUN animates each value with a
rather fancy left-to-right typing effect. As 16×16 images that use a single
bright-red color, these numbers would be
perfect candidates for gaiji… except that ZUN wanted to render them at the
more natural Y positions of the labels inside CLEAR3.GRP
that
are far from aligned to the 8×16 text RAM grid. Not having been in the mood
for hardcoding another set of monochrome sprites as C arrays that day, ZUN
made the still reasonable choice of storing the image data for these numbers
in the single-color .GRC form– yeah, no, of course he once again
chose the .PTN hammer, and its
📝 16×16 "quarter" wrapper functions around nominal 32×32 sprites.
Why do I bring up such a detail? What's actually going on there is that ZUN
loops through and blits each digit from 0 to 9, and then continues the loop
with "digit" numbers from 10 to 19, stopping before the number whose ones
digit equals the one that should stay on screen. No problem with that in
theory, and the .PTN sprite selection is correct… but the .PTN
quarter selection isn't, as ZUN wrote (digit % 4)
instead of the correct ((digit % 10) % 4)
.
Since .PTN quarters are indexed in a row-major
way, the 10-19 part of the loop thus ends up blitting
2 →
3 →
0 →
1 →
6 →
7 →
4 →
5 →
(nothing):
Seriously though? If the deadline is looming and you've got to rush some part of your game, a standalone screen that doesn't affect anything is the best place to pick. At 4 milliseconds per digit, the animation goes by so fast that this quirk might even add to its perceived fanciness. It's exactly the reason why I've always been rather careful with labeling such quirks as "bugs". And in the end, the code does perform one more blitting call after the loop to make sure that the correct digit remains on screen.
The remaining ¾ of the second push went towards transferring the final data definitions from ASM to C land. Most of the details there paint a rather depressing picture about ZUN's original code layout and the bloat that came with it, but it did end on a real highlight. There was some unused data between ZUN's non-master.lib VSync and text RAM code that I just moved away in September 2015 without taking a closer look at it. Those bytes kind of look like another hardcoded 1bpp image though… wait, what?!
Lovely! With no mouse-related code left in the game otherwise, this cursor sprite provides some great fuel for wild fan theories about TH01's development history:
- Could ZUN have 📝 stolen the basic PC-98 VSync or text RAM function code from a source that also implemented mouse support?
- Did
he have a mouse-controlled level editor during development? It's highly
likely that he had something, given all the
📝 bit twiddling seen in the
STAGE?.DAT
format. - Or was this game actually meant to have mouse-controllable portions at some point during development? Even if it would have just been the menus.
… Actually, you know what, with all shared data moved to C land, I might as
well finish FUUIN.EXE
right now. The last secret hidden in its
main()
function: Just like GAME.BAT
supports
launching the game in various debug modes from the DOS command line,
FUUIN.EXE
can directly launch one of the game's endings. As
long as the MDRV2 driver is installed, you can enter
fuuin t1
for the 魔界/Makai Good Ending, or
fuuin t
for 地獄/Jigoku Good Ending.
Unfortunately, the command-line parameter can only control the route.
Choosing between a Good or Bad Ending is still done exclusively through
TH01's resident structure, and the continues_per_scene
array in
particular. But if you pre-allocate that structure somehow and set one of
the members to a nonzero value, it would work. Trainers, anyone?
Alright, gotta get back to the code if I want to have any chance of
finishing this game before the 15th… Next up: The final 17
functions in REIIDEN.EXE
that tie everything together and add
some more debug features on top.