ReC98/th02/end/end.cpp

1128 lines
30 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#include <stddef.h>
#include "planar.h"
#include "libs/master.lib/master.hpp"
#include "th01/hardware/egc.h"
// ZUN bloat: Needed for code generation reasons in the single graph_putsa_fx()
// call during the verdict screen that pushes a string pointer with a
// calculated offset.
#define const
#include "th01/hardware/grppsafx.h"
#undef const
#include "th02/score.h"
#include "th02/v_colors.hpp"
#include "th02/resident.hpp"
#include "th02/core/globals.hpp"
#include "th02/hardware/frmdelay.h"
#include "th02/hardware/input.hpp"
#include "th02/formats/end.hpp"
#include "th02/formats/pi.h"
#include "th02/gaiji/gaiji.h"
#include "th02/gaiji/score_p.hpp"
#include "th02/snd/snd.h"
#include "th02/end/staff.hpp"
#include "th02/shiftjis/end.hpp"
#include "th02/shiftjis/title.hpp"
#include "th02/sprites/verdict.hpp"
#include "th02/gaiji/ranks_c.c"
// Constants
// ---------
static const int CUTSCENE_PIC_SLOT = 0;
static const int ENDFT_CELS = 11;
enum end_text_colors_t {
V_YELLOW = 6,
V_GREEN = 9,
};
// ---------
// Coordinates
// -----------
static const screen_y_t CUTSCENE_PIC_TOP = ((RES_Y / 2) - (CUTSCENE_PIC_H / 2));
static const pixel_t END_LINE_W = (END_LINE_LENGTH * GLYPH_HALF_W);
static const screen_x_t END_LINE_LEFT = ((RES_X / 2) - (END_LINE_W / 2));
static const screen_y_t END_LINE_TOP = (
CUTSCENE_PIC_TOP + CUTSCENE_PIC_H + (GLYPH_H * 2)
);
static const screen_x_t END_LINE_RIGHT = (END_LINE_LEFT + END_LINE_W);
static const screen_y_t END_LINE_BOTTOM = (END_LINE_TOP + GLYPH_H);
static const pixel_t ENDFT_W = 80;
static const pixel_t ENDFT_H = 32;
static const pixel_t ENDFT_SEGMENT_W = 16;
static const pixel_t ENDFT_SEGMENT_COUNT = (ENDFT_W / ENDFT_SEGMENT_W);
static const screen_x_t STAFFROLL_TEXT_LEFT = (
STAFFROLL_PIC_RIGHT + (GLYPH_FULL_W * 4)
);
static const screen_y_t STAFFROLL_TEXT_TOP = ((RES_Y / 2) - (GLYPH_H / 2));
static const pixel_t VERDICT_LABEL_PADDED_W = (VERDICT_LABEL_W + GLYPH_FULL_W);
static const pixel_t VERDICT_VALUE_CELL_W = (SCORE_DIGITS * GAIJI_W);
static const pixel_t VERDICT_W = (
VERDICT_LABEL_PADDED_W + VERDICT_VALUE_CELL_W
);
static const screen_x_t VERDICT_LABEL_LEFT = 64;
static const screen_x_t VERDICT_VALUE_LEFT = (
VERDICT_LABEL_LEFT + VERDICT_LABEL_PADDED_W
);
static const screen_x_t VERDICT_THANKYOU_LEFT = (
VERDICT_LABEL_LEFT + (VERDICT_W / 2) - (shiftjis_w(VERDICT_THANKYOU) / 2)
);
// Not quite centered… but then again, nothing in this table really is.
static const screen_x_t VERDICT_VALUE_SINGLEDIGIT_LEFT = (
VERDICT_VALUE_LEFT + (VERDICT_VALUE_CELL_W / 2)
);
// This one's at least right-aligned correctly.
static const screen_x_t VERDICT_VALUE_RANK_LEFT = (
VERDICT_LABEL_LEFT + VERDICT_W - ((sizeof(gbcRANKS[0]) - 1) * GAIJI_W)
);
inline screen_y_t verdict_row_top(int row) { \
return (96 + (row * GLYPH_H * 2));
}
// -----------
// State
// -----
shiftjis_t end_text[100][END_LINE_SIZE];
int8_t line_col_and_fx;
inline void line_col_set(vc_t vc) {
line_col_and_fx = (vc | FX_WEIGHT_BOLD);
}
bool line_type_allow_fast_forward_and_automatically_clear_end_line;
// -----
// Function ordering fails
// -----------------------
void near end_line_clear(void);
void extra_unlock_animate(void);
// -----------------------
void pascal near end_load(const char *fn)
{
file_ropen(fn);
// ZUN landmine: No check to ensure that the size is ≤ sizeof(end_text).
// Dynamic allocation would have made more sense...
size_t size = file_size();
file_read(end_text, size);
file_close();
}
inline void verdict_label_put(int row, screen_x_t left, shiftjis_t* str) {
graph_putsa_fx(left, verdict_row_top(row), line_col_and_fx, str);
}
void pascal near verdict_value_score_put(
screen_x_t left, screen_y_t top, score_t score
)
{
#define on_digit(i, gaiji) { \
graph_gaiji_putc((left + (GAIJI_W * i)), top, gaiji, V_WHITE); \
}
gaiji_score_put(score, on_digit, false);
#undef on_digit
}
#define verdict_value_singledigit_put(row, gaiji) { \
graph_gaiji_putc( \
VERDICT_VALUE_SINGLEDIGIT_LEFT, verdict_row_top(row), gaiji, V_WHITE \
); \
}
#define verdict_value_gaiji_string_put(row, left, str) { \
graph_gaiji_puts(left, verdict_row_top(row), GAIJI_W, str, V_WHITE); \
}
void pascal near line_type(
screen_x_t left,
screen_y_t top,
shiftjis_ank_amount_t len,
shiftjis_t* str,
int frames_per_kanji
) {
// ZUN landmine: Susceptible to buffer overflows if [len] is larger.
shiftjis_t buf[RES_X / GLYPH_HALF_W] = { '\0' };
int i = 0;
for(int loop = 0; loop < len; loop += int(sizeof(shiftjis_kanji_t))) {
// ZUN bug: Does not address the PC-98 keyboard quirk documented in the
// `Research/HOLDKEY` example. As a result, the eventual key release
// scancode won't be filtered and will get through to [key_det], ...
input_reset_sense();
buf[i] = str[i];
i++;
buf[i] = str[i];
i++;
buf[i] = '\0';
graph_putsa_fx(left, top, line_col_and_fx, buf);
// …which leads to the code taking the branch with the longer delay
// every once in a while, even if the player still holds a key. While
// the condition checks for *any* key being held, the probability of
// taking the wrong branch remains identical regardless of how many
// keys are held down, as the key release scancodes are only sent for
// the last key that was pressed.
if(
line_type_allow_fast_forward_and_automatically_clear_end_line &&
(key_det != INPUT_NONE)
) {
// As [loop] increases by 2 on each iteration, the loop will
// alternate between this delay and no delay at all, resulting in
// two kanji every ([frames_per_kanji] / 3) frames.
if(loop & 3) {
frame_delay(frames_per_kanji / 3);
}
} else {
frame_delay(frames_per_kanji);
}
}
for(i = 0; i < 20; i++) {
input_reset_sense();
if(
line_type_allow_fast_forward_and_automatically_clear_end_line &&
(key_det != INPUT_NONE)
) {
// ZUN bloat: Technically, a 0-frame delay is not a no-op. Because
// it still resets [vsync_Count1], it forms a frame boundary in
// case the current frame didn't finish rendering within a single
// VSync interrupt.
// In this context though, this function call might as well be a
// `break`. Without the 614.4 µs delay to address the PC-98
// keyboard quirk documented in the `Research/HOLDKEY` example, a
// single call to input_reset_sense() encompasses
//
// • 8 INT 18h, AH=04h calls,
// • 18 comparisons, and
// • 14 branches with OR assignments.
//
// The exact implementation of INT 18h varies between PC-98 models,
// so there's no point in precisely counting CPU cycles here. But
// given that this blocking loop does nothing else *and* we're sure
// to get here at the very start of a frame, it makes sense to
// assume that 20 of those calls can easily fit within the ≈600,000
// cycles we have for every frame on the game's target 33 MHz CPUs.
frame_delay(0);
} else {
frame_delay(2);
}
}
if(line_type_allow_fast_forward_and_automatically_clear_end_line) {
end_line_clear();
}
}
void verdict_kanji_1_to_0_masked(
screen_x_t left, screen_y_t top, const dots16_t mask[VERDICT_MASK_H]
)
{
static_assert(VERDICT_MASK_H == GLYPH_H);
Planar<dots_t(VERDICT_MASK_W)> dots;
vram_offset_t vo = vram_offset_shift(left, top);
for(pixel_t row = 0; row < VERDICT_MASK_H; row++) {
// ZUN bloat: Thanks to the blit functions being macros, `mask[row]` is
// evaluated a total of 5 times. Once would be enough.
graph_accesspage(1);
VRAM_SNAP_PLANAR(dots, vo, VERDICT_MASK_W);
graph_accesspage(0);
grcg_setcolor(GC_RMW, 0);
grcg_put(vo, mask[row], VERDICT_MASK_W);
grcg_off();
vram_or_planar_masked(vo, dots, VERDICT_MASK_W, mask[row]);
vo += ROW_SIZE;
}
}
void verdict_row_1_to_0_animate(
screen_x_t left, screen_y_t top, shiftjis_kanji_amount_t len
)
{
// ZUN bloat: This array is not `static`, and will be needlessly copied
// into a local variable at every call to the function.
#include "th02/sprites/verdict.csp"
shiftjis_kanji_amount_t i;
for(int mask = 0; mask < VERDICT_MASK_COUNT; mask++) {
for(i = 0; i < len; i++) {
verdict_kanji_1_to_0_masked(
(left + (i * GLYPH_FULL_W)),
top,
&sVERDICT_MASKS[mask][0]
);
}
frame_delay(10);
}
}
// Full row
inline void verdict_row_1_to_0_animate(
int row, shiftjis_kanji_amount_t len = (VERDICT_W / GLYPH_FULL_W)
) {
verdict_row_1_to_0_animate(VERDICT_LABEL_LEFT, verdict_row_top(row), len);
}
void pascal near gaiji_boldfont_str_from_positive_3_digit_value(
int value, // ZUN bloat: Not meant to support unsigned values.
gaiji_th02_t str[4]
)
{
enum {
DIGITS = 3,
};
int divisor = 100; // Must match DIGITS!
int8_t digit;
uint8_t past_leading_zeroes = 0;
int i = 0;
while(i < DIGITS) {
digit = (value / divisor);
if(past_leading_zeroes == 0) {
past_leading_zeroes = digit;
}
if(past_leading_zeroes || (i == (DIGITS - 1))) {
str[i] = gaiji_th02_t(gb_0_ + digit);
} else {
str[i] = gb_SP;
}
value -= (digit * divisor);
i++;
divisor /= 10;
}
str[i] = gs_NULL;
}
// ZUN bloat: Same algorithm as in TH01, same problems. Also could be a
// single proper function.
#define pic_put(left, top, rows, quarter, quarter_offset_y) { \
uvram_offset_t vram_offset_src = ( \
(quarter == 0) ? vram_offset_shift(0, 0) : \
(quarter == 1) ? vram_offset_shift(CUTSCENE_PIC_W, 0) : \
(quarter == 2) ? vram_offset_shift(0, CUTSCENE_PIC_H) : \
/*quarter == 3*/ vram_offset_shift(CUTSCENE_PIC_W, CUTSCENE_PIC_H) \
); \
uvram_offset_t vram_offset_dst = vram_offset_shift(left, top); \
vram_offset_src += (quarter_offset_y * ROW_SIZE); \
\
egc_start_copy(); \
\
pixel_t y = quarter_offset_y; \
vram_byte_amount_t vram_x; \
while(y < (rows + quarter_offset_y)) { \
vram_x = 0; \
while(vram_x < CUTSCENE_PIC_VRAM_W) { \
egc_temp_t d; \
\
graph_accesspage(1); d = egc_chunk(vram_offset_src); \
graph_accesspage(0); egc_chunk(vram_offset_dst) = d; \
\
vram_x += EGC_REGISTER_SIZE; \
vram_offset_src += EGC_REGISTER_SIZE; \
vram_offset_dst += EGC_REGISTER_SIZE; \
} \
y++; \
vram_offset_dst += (ROW_SIZE - CUTSCENE_PIC_VRAM_W); \
vram_offset_src += (ROW_SIZE - CUTSCENE_PIC_VRAM_W); \
} \
egc_off(); \
}
void pascal near end_pic_show(int quarter)
{
pic_put(CUTSCENE_PIC_LEFT, CUTSCENE_PIC_TOP, CUTSCENE_PIC_H, quarter, 0);
}
void pascal near staffroll_pic_put(screen_x_t left, screen_y_t top, int quarter)
{
pic_put(left, top, CUTSCENE_PIC_H, quarter, 0);
}
void pascal near end_pic_put_rows(
int quarter, pixel_t quarter_offset_y, pixel_t rows
)
{
pic_put(
CUTSCENE_PIC_LEFT, CUTSCENE_PIC_TOP, rows, quarter, quarter_offset_y
);
}
void near end_line_clear(void)
{
screen_y_t y;
screen_y_t x;
grcg_setcolor(GC_RMW, 0);
for(y = END_LINE_TOP; y < END_LINE_BOTTOM; y++) {
for(x = END_LINE_LEFT; x < END_LINE_RIGHT; x += GLYPH_FULL_W) {
vram_offset_t vo = vram_offset_shift(x, y);
grcg_put(vo, 0xFFFF, GLYPH_FULL_W);
}
}
grcg_off();
}
void near end_to_staffroll_animate(void)
{
enum {
VELOCITY = 4,
// ZUN bloat: (CUTSCENE_PIC_H - 1) would have been enough.
SHIFT_H = (RES_Y - 1 - CUTSCENE_PIC_TOP),
};
end_load("end3.txt");
frame_delay(30);
palette_white_out(1);
snd_load("ending.m", SND_LOAD_SONG);
snd_kaja_func(KAJA_SONG_PLAY, 0);
end_line_clear();
palette_white_in(4);
snd_delay_until_measure(5);
screen_x_t left_prev = CUTSCENE_PIC_LEFT;
// ZUN quirk: Off by 4? Did you mean ">="?
while(left_prev > (STAFFROLL_PIC_LEFT + VELOCITY)) {
// ZUN bloat: Could have made use of the fact that this image is
// surrounded by black pixels. Shifting [CUTSCENE_PIC_W + VELOCITY]
// pixels would have avoided the need to use the GRCG below, as well
// as the subsequent...
egc_shift_left(
left_prev,
CUTSCENE_PIC_TOP,
(left_prev + CUTSCENE_PIC_W - 1),
SHIFT_H,
VELOCITY
);
grcg_setcolor(GC_RMW, 0);
grcg_boxfill(
// ZUN bug: …which cuts off [VELOCITY] pixels at the right side.
(left_prev + CUTSCENE_PIC_W - (VELOCITY * 2)),
CUTSCENE_PIC_TOP,
(left_prev + CUTSCENE_PIC_W - 1),
SHIFT_H
);
grcg_off();
frame_delay(1);
left_prev -= VELOCITY;
}
}
inline void end_pics_load_palette_show(const char *fn) {
graph_accesspage(1);
pi_fullres_load_palette_apply_put_free(CUTSCENE_PIC_SLOT, fn);
}
// Calling line_type(int, int) directly from the loop would add a useless load
// and store for [line].
#define end_line_type_raw(line, frames) { \
line_type( \
END_LINE_LEFT, END_LINE_TOP, END_LINE_LENGTH, end_text[line], frames \
); \
}
inline void end_line_type(int line, int frames_per_kanji = 6) {
end_line_type_raw(line, frames_per_kanji);
}
// ZUN bloat: Spending 3,344 bytes of the code segment on script code is way
// too much. The most effective first compression step would be to turn the
// text color changes into a line-indexed array (similar to the generic face
// arrays used for in-game dialog), and this into a proper function.
#define end_lines_type_from_to(first, last) { \
for(i = first; i <= last; i++) { \
end_line_type_raw(i, 6); \
} \
}
inline void end_load_and_start_animate(const char* text_fn) {
end_load(text_fn);
snd_load("end1.m", SND_LOAD_SONG);
snd_kaja_func(KAJA_SONG_PLAY, 0);
palette_black();
end_pics_load_palette_show("ed01.pi");
palette_black_in(2);
frame_delay(40);
end_pic_show(0);
line_col_set(V_WHITE);
line_type_allow_fast_forward_and_automatically_clear_end_line = true;
end_line_type(0);
frame_delay(20);
palette_black_out(1);
}
void near end_bad_animate(void)
{
int i;
end_load_and_start_animate("end1.txt");
end_pic_show(1);
palette_black_in(1);
end_lines_type_from_to(1, 3);
end_pic_show(2);
end_lines_type_from_to(4, 5);
line_col_set(V_YELLOW);
end_line_type(6);
line_col_set(V_WHITE);
end_lines_type_from_to(7, 9);
line_col_set(V_YELLOW);
end_line_type(10);
line_col_set(V_WHITE);
end_line_type(11);
frame_delay(20);
end_line_type(12);
// Scroll up to Marisa
enum {
VELOCITY = 2,
};
#define frame i
for(frame = 0; frame < (CUTSCENE_PIC_H / VELOCITY); frame++) {
// ZUN bloat: Redundant; end_pic_put_rows() returns with VRAM page 0
// accessed.
graph_accesspage(0);
egc_shift_down(
CUTSCENE_PIC_LEFT,
CUTSCENE_PIC_TOP,
(CUTSCENE_PIC_LEFT + CUTSCENE_PIC_W - 1),
(CUTSCENE_PIC_TOP + CUTSCENE_PIC_H - 1 - VELOCITY),
VELOCITY
);
end_pic_put_rows(
3, ((CUTSCENE_PIC_H - VELOCITY) - (frame * VELOCITY)), VELOCITY
);
frame_delay(1);
}
#undef i
end_line_type(13);
line_col_set(V_YELLOW);
end_lines_type_from_to(14, 15);
line_col_set(V_WHITE);
end_lines_type_from_to(16, 17);
line_col_set(V_YELLOW);
end_lines_type_from_to(18, 20);
line_col_set(V_WHITE);
end_line_type(21);
line_col_set(V_YELLOW);
end_line_type(22);
line_col_set(V_WHITE);
end_line_type(23);
// ZUN landmine: Blacking out *after* loading runs the black-out animation
// with the new palette while the shown VRAM page still contains a pic from
// the previous .PI file. This only works in the original game because the
// palettes of the original ED01.PI and ED02.PI are identical.
end_pics_load_palette_show("ed02.pi");
palette_black_out(2);
if(resident->shottype == 0) {
end_pic_show(0);
} else if(resident->shottype == 1) {
end_pic_show(2);
} else {
end_pic_show(3);
}
palette_black_in(2);
line_col_set(V_YELLOW);
end_line_type(24);
line_col_set(V_WHITE);
end_lines_type_from_to(25, 26);
line_col_set(V_YELLOW);
if(resident->shottype == 0) {
end_lines_type_from_to(27, 28);
line_col_set(V_WHITE);
end_lines_type_from_to(29, 30);
end_pic_show(1);
line_col_set(V_YELLOW);
end_line_type(31);
line_col_set(V_WHITE);
end_line_type(32, 12);
line_type_allow_fast_forward_and_automatically_clear_end_line = false;
end_line_type(33, 9);
} else if(resident->shottype == 1) {
end_line_type(34);
line_col_set(V_WHITE);
end_line_type(35);
line_col_set(V_YELLOW);
end_line_type(36);
line_col_set(V_WHITE);
end_lines_type_from_to(37, 38);
line_col_set(V_YELLOW);
end_line_type(39);
line_col_set(V_WHITE);
end_line_type(40, 12);
line_type_allow_fast_forward_and_automatically_clear_end_line = false;
end_line_type(41, 9);
} else if(resident->shottype == 2) {
end_line_type(42);
line_col_set(V_WHITE);
end_line_type(43);
line_col_set(V_YELLOW);
end_lines_type_from_to(44, 45);
line_col_set(V_WHITE);
end_lines_type_from_to(46, 47);
line_col_set(V_YELLOW);
end_line_type(48);
line_col_set(V_WHITE);
end_line_type(49, 12);
line_type_allow_fast_forward_and_automatically_clear_end_line = false;
end_line_type(50, 9);
}
end_to_staffroll_animate();
}
void near end_good_animate(void)
{
int i;
end_load_and_start_animate("end2.txt");
end_pics_load_palette_show("ed03.pi");
end_pic_show(0);
palette_black_in(1);
end_lines_type_from_to(1, 3);
end_pic_show(1);
end_lines_type_from_to(4, 9);
end_pic_show(2);
end_lines_type_from_to(10, 11);
end_pic_show(3);
end_lines_type_from_to(12, 13);
palette_entry_rgb("ed03a.rgb"); // Change to a grayscale palette
palette_show();
line_col_set(V_GREEN);
end_lines_type_from_to(14, 15);
palette_black_out(2);
end_pics_load_palette_show("ed04.pi");
graph_accesspage(0); // ZUN bloat: Redundant, overridden by end_pic_show()
end_pic_show(0);
palette_black_in(2);
line_col_set(V_WHITE);
end_line_type(16);
line_col_set(V_GREEN);
end_line_type(17);
line_col_set(V_WHITE);
end_lines_type_from_to(18, 19);
end_pic_show(1);
line_col_set(V_GREEN);
end_lines_type_from_to(20, 21);
line_col_set(V_WHITE);
end_line_type(22);
line_col_set(V_GREEN);
end_lines_type_from_to(23, 24);
line_col_set(V_WHITE);
end_line_type(25);
line_col_set(V_GREEN);
end_line_type(26);
frame_delay(10);
if(resident->shottype == 0) {
end_line_type(27);
frame_delay(30);
end_pic_show(2);
line_col_set(V_WHITE);
end_line_type(28);
end_pic_show(3);
end_line_type(29);
line_col_set(V_GREEN);
end_lines_type_from_to(30, 31);
line_col_set(V_WHITE);
end_lines_type_from_to(32, 36);
line_col_set(V_GREEN);
end_lines_type_from_to(37, 38);
line_col_set(V_WHITE);
end_line_type(39);
palette_black_out(2);
end_pics_load_palette_show("ed05.pi");
end_pic_show(0);
palette_black_in(2);
end_lines_type_from_to(40, 46);
line_type_allow_fast_forward_and_automatically_clear_end_line = false;
end_line_type(47, 12);
} else if(resident->shottype == 1) {
end_line_type(48);
end_line_type(49);
line_col_set(V_WHITE);
end_line_type(50);
line_col_set(V_GREEN);
end_line_type(51);
frame_delay(30);
end_pic_show(2);
line_col_set(V_WHITE);
end_line_type(52);
end_pic_show(3);
end_line_type(53);
line_col_set(V_GREEN);
end_line_type(54);
line_col_set(V_WHITE);
end_line_type(55);
line_col_set(V_GREEN);
end_line_type(56);
line_col_set(V_WHITE);
end_line_type(57);
line_col_set(V_GREEN);
end_lines_type_from_to(58, 61);
line_col_set(V_WHITE);
end_lines_type_from_to(62, 66);
palette_black_out(2);
end_pics_load_palette_show("ed05.pi");
end_pic_show(1);
palette_black_in(2);
end_lines_type_from_to(67, 68);
end_line_type(69, 12);
line_type_allow_fast_forward_and_automatically_clear_end_line = false;
end_line_type(70, 12);
} else {
end_line_type(71);
line_col_set(V_WHITE);
end_line_type(72);
line_col_set(V_GREEN);
end_line_type(73);
frame_delay(30);
end_pic_show(2);
line_col_set(V_WHITE);
end_line_type(74);
end_pic_show(3);
end_line_type(75);
line_col_set(V_GREEN);
end_line_type(76);
line_col_set(V_WHITE);
end_line_type(77);
line_col_set(V_GREEN);
end_line_type(78);
line_col_set(V_WHITE);
end_line_type(79);
line_col_set(V_GREEN);
end_line_type(80);
line_col_set(V_WHITE);
end_line_type(81);
line_col_set(V_GREEN);
end_line_type(82);
line_col_set(V_WHITE);
end_line_type(83);
palette_black_out(2);
end_pics_load_palette_show("ed05.pi");
end_pic_show(2);
palette_black_in(2);
end_lines_type_from_to(84, 91);
// ZUN bloat: Could have been included in the loop. (As if it matters
// at this point...)
end_line_type(92);
line_type_allow_fast_forward_and_automatically_clear_end_line = false;
end_line_type(93, 12);
}
end_to_staffroll_animate();
}
void pascal near endft_put(screen_x_t left, screen_y_t top, int patnum_base)
{
int patnum = patnum_base;
int segment = 0;
while(segment < ENDFT_SEGMENT_COUNT) {
over_put_8(left, top, patnum);
segment++;
patnum++;
left += ENDFT_SEGMENT_W;
}
}
void near staffroll_text_clear(void)
{
grcg_setcolor(GC_RMW, 0);
// ZUN bloat: 5 lines of text are both too many and too little. There are
// only two staff sections that list more people than just ZUN:
// • The 「グラフィック」 section covers 4 lines, not 5
// • The 「TEST PLAYER」 section stretches 7.5 lines, and only
// doesn't need to be covered because it's the last section and the game
// transitions to the verdict screen afterwards.
grcg_boxfill(
STAFFROLL_TEXT_LEFT,
STAFFROLL_TEXT_TOP,
(RES_X - 1),
(STAFFROLL_TEXT_TOP + (5 * GLYPH_H) - 1)
);
grcg_off();
}
void pascal near staffroll_rotrect_and_put_pic_animate(
unsigned char angle_speed, int quarter, unsigned char angle_start
)
{
// ZUN landmine: This function always runs immediately after an expensive
// operation (640×400 .PI image loading and blitting to VRAM, 320×200 VRAM
// inter-page copy, or hardware palette loading from a packed file),
// without any frame_delay() before. As the Staff Roll is single-buffered,
// this all but ensures a tearing line on the first frame of the rotating
// rectangle animation.
staffroll_rotrect_animate(angle_speed, angle_start);
staffroll_pic_put(STAFFROLL_PIC_LEFT, STAFFROLL_PIC_TOP, quarter);
frame_delay(4);
palette_100();
}
inline void staffroll_text_put(
pixel_t additional_left, float row, shiftjis_t* str
) {
graph_putsa_fx(
(STAFFROLL_TEXT_LEFT + additional_left),
(STAFFROLL_TEXT_TOP + pixel_t((row * GLYPH_H))),
(V_WHITE | FX_WEIGHT_BOLD),
str
);
}
void near staffroll_and_verdict_animate(void)
{
/// Staff roll
/// ----------
enum {
ENDFT_LEFT = (STAFFROLL_TEXT_LEFT + GLYPH_FULL_W),
ENDFT_TOP = (STAFFROLL_TEXT_TOP + (GLYPH_H / 2) - (ENDFT_H / 2)),
VERSION_LEFT = (ENDFT_LEFT + ENDFT_W + GLYPH_FULL_W),
VELOCITY = 4,
};
snd_delay_until_measure(6);
int i = 0;
while(i < (ENDFT_CELS * ENDFT_SEGMENT_COUNT)) {
endft_put(ENDFT_LEFT, ENDFT_TOP, i);
frame_delay(4);
i += ENDFT_SEGMENT_COUNT;
}
graph_putsa_fx(
VERSION_LEFT,
STAFFROLL_TEXT_TOP,
(V_WHITE | FX_WEIGHT_BOLD),
GAME_VERSION
);
snd_delay_until_measure(8);
// Move game title and version down to make room for the staff roll text
// ---------------------------------------------------------------------
// ZUN bloat: Calculated in terms of the ENDFT.BFT top position, but
// would have saved a lot of [VELOCITY] additions if it was calculated in
// terms of the version string's top position instead. (Note that the first
// loop iteration is a no-op, so make sure to pull out the frame_delay(1)
// call when debloating.)
#define endft_top i
endft_top = (STAFFROLL_TEXT_TOP - (VELOCITY * 2));
while(endft_top < (RES_Y - ENDFT_H)) {
grcg_setcolor(GC_RMW, 0);
grcg_boxfill(
VERSION_LEFT,
(endft_top + VELOCITY),
(VERSION_LEFT + shiftjis_w(GAME_VERSION) - 1),
(endft_top + VELOCITY + GLYPH_H - 1)
);
grcg_off();
endft_put(
ENDFT_LEFT, endft_top, ((ENDFT_CELS - 1) * ENDFT_SEGMENT_COUNT)
);
graph_putsa_fx(
VERSION_LEFT,
(endft_top + (VELOCITY * 2)),
(V_WHITE | FX_WEIGHT_BOLD),
GAME_VERSION
);
frame_delay(1);
i += VELOCITY;
}
#undef endft_top
// ---------------------------------------------------------------------
snd_delay_until_measure(9);
line_col_set(V_WHITE);
line_type(
(STAFFROLL_TEXT_LEFT + GLYPH_FULL_W),
STAFFROLL_TEXT_TOP,
(sizeof(STAFFROLL_TITLE) - 1),
STAFFROLL_TITLE,
12
);
snd_delay_until_measure(13);
graph_accesspage(1);
pi_fullres_load_palette_apply_put_free(CUTSCENE_PIC_SLOT, "ed06.pi");
graph_accesspage(0);
staffroll_rotrect_and_put_pic_animate(0x04, 0, 0x29);
staffroll_text_clear();
staffroll_text_put(0, 0, STAFFROLL_PROGRAM);
snd_delay_until_measure(17);
// ZUN quirk: This palette has #FCC instead of #FFF at the 0-based index
// #15, discoloring every part of the screen.
palette_entry_rgb_show("ed06b.rgb");
staffroll_rotrect_and_put_pic_animate(-0x04, 2, 0x29);
snd_delay_until_measure(21);
// ZUN bloat: Will be immediately overwritten with the animation. (And
// we're still on the wrong palette.)
staffroll_pic_put(STAFFROLL_PIC_LEFT, STAFFROLL_PIC_TOP, 3);
palette_entry_rgb_show("ed06c.rgb");
staffroll_rotrect_and_put_pic_animate(0x04, 3, 0x29);
graph_accesspage(1);
pi_fullres_load_put_free(CUTSCENE_PIC_SLOT, "ed07.pi"); // Unchanged palette
graph_accesspage(0);
snd_delay_until_measure(25);
staffroll_text_clear();
staffroll_text_put(0, 0, STAFFROLL_GRAPHIC_1);
staffroll_text_put(0, 2, STAFFROLL_GRAPHIC_2);
staffroll_text_put(0, 3, STAFFROLL_GRAPHIC_3);
palette_entry_rgb_show("ed07a.rgb");
staffroll_rotrect_and_put_pic_animate(-0x04, 0, 0x29);
snd_delay_until_measure(29);
palette_entry_rgb_show("ed07b.rgb");
staffroll_rotrect_and_put_pic_animate(0x08, 1, -0x17);
snd_delay_until_measure(33);
// ZUN bloat: Will be immediately overwritten with the animation. It also
// wastes time on the frame, since this isn't double-buffered and
// staffroll_rotrect_animate() immediately begins drawing (see the landmine
// in that function).
staffroll_pic_put(STAFFROLL_PIC_LEFT, STAFFROLL_PIC_TOP, 2);
staffroll_rotrect_and_put_pic_animate(-0x08, 2, -0x17);
snd_delay_until_measure(37);
staffroll_text_clear();
staffroll_text_put(GLYPH_FULL_W, 0, STAFFROLL_MUSIC);
// ZUN bloat: We're still on this palette. Disk I/O isn't free!
palette_entry_rgb_show("ed07b.rgb");
staffroll_rotrect_and_put_pic_animate(0x08, 3, -0x17);
graph_accesspage(1);
pi_load(CUTSCENE_PIC_SLOT, "ed08.pi");
pi_put_8(0, 0, CUTSCENE_PIC_SLOT);
graph_accesspage(0);
snd_delay_until_measure(41);
pi_palette_apply(CUTSCENE_PIC_SLOT);
pi_free(CUTSCENE_PIC_SLOT);
staffroll_rotrect_and_put_pic_animate(-0x08, 0, -0x17);
snd_delay_until_measure(45);
palette_entry_rgb_show("ed08a.rgb");
staffroll_rotrect_and_put_pic_animate(0x0C, 1, 0x29);
snd_delay_until_measure(49);
staffroll_text_clear();
staffroll_text_put(0, 0.0, STAFFROLL_TESTER_1);
staffroll_text_put(0, 2.0, STAFFROLL_TESTER_2);
staffroll_text_put(0, 3.5, STAFFROLL_TESTER_3);
staffroll_text_put(0, 5.0, STAFFROLL_TESTER_4);
staffroll_text_put(0, 6.5, STAFFROLL_TESTER_5);
palette_entry_rgb_show("ed08b.rgb");
staffroll_rotrect_and_put_pic_animate(-0x0C, 2, 0x29);
snd_delay_until_measure(53);
palette_entry_rgb_show("ed08c.rgb");
staffroll_rotrect_and_put_pic_animate(0x0C, 3, 0x29);
snd_delay_until_measure(57);
palette_black_out(4);
/// ----------
/// Verdict
/// -------
/// VRAM page 1 receives the final text on each line that VRAM page 0
/// animates towards.
enum {
SKILL_DIGITS = 3,
ROW_FRAMES = 100,
};
graph_accesspage(1);
pi_fullres_load_palette_apply_put_free(CUTSCENE_PIC_SLOT, "ED09.pi");
graph_copy_page(0);
palette_black_in(4);
frame_delay(100);
// Thank you for playing
graph_accesspage(1);
verdict_label_put(0, VERDICT_THANKYOU_LEFT, VERDICT_THANKYOU);
verdict_row_1_to_0_animate(
VERDICT_THANKYOU_LEFT,
verdict_row_top(0),
((sizeof(VERDICT_THANKYOU) - 1) / sizeof(shiftjis_kanji_t))
);
frame_delay(ROW_FRAMES * 2);
// Score
graph_accesspage(1);
verdict_label_put(1, VERDICT_LABEL_LEFT, VERDICT_LABEL_SCORE);
verdict_value_score_put(VERDICT_VALUE_LEFT, verdict_row_top(1), score);
verdict_row_1_to_0_animate(1);
frame_delay(ROW_FRAMES);
// Continues
graph_accesspage(1);
verdict_label_put(2, VERDICT_LABEL_LEFT, VERDICT_LABEL_CONTINUES);
verdict_value_singledigit_put(2, (gb_0_ + resident->continues_used));
verdict_row_1_to_0_animate(2);
frame_delay(ROW_FRAMES);
// Rank
graph_accesspage(1);
verdict_label_put(3, VERDICT_LABEL_LEFT, VERDICT_LABEL_RANK);
verdict_value_gaiji_string_put(3, VERDICT_VALUE_RANK_LEFT, gbcRANKS[rank]);
verdict_row_1_to_0_animate(3);
frame_delay(ROW_FRAMES);
// Starting lives
graph_accesspage(1);
verdict_label_put(4, VERDICT_LABEL_LEFT, VERDICT_LABEL_START_LIVES);
verdict_value_singledigit_put(4, (gb_1_ + resident->start_lives));
verdict_row_1_to_0_animate(4);
frame_delay(ROW_FRAMES);
// Starting bombs
graph_accesspage(1);
verdict_label_put(5, VERDICT_LABEL_LEFT, VERDICT_LABEL_START_BOMBS);
verdict_value_singledigit_put(5, (gb_0_ + resident->start_bombs));
verdict_row_1_to_0_animate(5);
frame_delay(ROW_FRAMES + (ROW_FRAMES / 2));
// Skill
graph_accesspage(1);
int skill = resident->skill;
// ZUN bloat: Could have been worked into the mapping below.
if(skill > 100) {
skill = 100;
} else if(skill < 0) {
skill = 0;
}
gaiji_th02_t skill_gaiji[SKILL_DIGITS + 1];
verdict_label_put(6, VERDICT_LABEL_LEFT, VERDICT_LABEL_TITLE);
gaiji_boldfont_str_from_positive_3_digit_value(skill, skill_gaiji);
verdict_value_gaiji_string_put(
6, VERDICT_VALUE_LEFT, reinterpret_cast<const char *>(skill_gaiji)
);
/**/ if(skill == 100) { i = 0; }
else if(skill >= 90) { i = 1; }
else if(skill >= 80) { i = 2; }
else if(skill >= 70) {
if(skill == 77) {
i = 3;
} else {
i = 4;
}
}
else if(skill >= 60) { i = 5; }
else if(skill >= 50) { i = 6; }
else if(skill >= 40) { i = 7; }
else if(skill >= 30) { i = 8; }
else if(skill >= 20) { i = 9; }
else if(skill >= 10) { i = 10; }
else /* */ { i = 11; }
graph_putsa_fx(
(VERDICT_VALUE_LEFT + (SKILL_DIGITS * GAIJI_W)),
verdict_row_top(6),
line_col_and_fx,
end_text[i]
);
verdict_row_1_to_0_animate(
6, ((sizeof(VERDICT_LABEL_TITLE) - 1) / sizeof(shiftjis_kanji_t))
);
frame_delay(ROW_FRAMES + 20);
verdict_row_1_to_0_animate(
VERDICT_VALUE_LEFT,
verdict_row_top(6),
// ZUN landmine: Should not cut off the final possible kanji.
(SKILL_DIGITS + ((END_LINE_LENGTH - 1) / sizeof(shiftjis_kanji_t)))
);
frame_delay(200);
// Copyright
graph_accesspage(1);
verdict_label_put(8, VERDICT_LABEL_LEFT, VERDICT_COPYRIGHT "\0all.pi");
verdict_row_1_to_0_animate(
8, ((sizeof(VERDICT_COPYRIGHT) - 1) / sizeof(shiftjis_kanji_t))
);
key_delay();
palette_black_out(5);
/* TODO: Replace with the decompiled call
* extra_unlock_animate();
* once that function is part of this translation unit */
_asm { nop; push cs; call near ptr extra_unlock_animate; }
graph_clear();
/// -------
}