mirror of https://github.com/nmlgc/ReC98.git
440 lines
12 KiB
C++
440 lines
12 KiB
C++
/// Jigoku Stage 15 Boss - Kikuri
|
|
/// -----------------------------
|
|
|
|
#include <stddef.h>
|
|
#include "th01/main/boss/palette.cpp"
|
|
|
|
#include "planar.h"
|
|
#include "th01/v_colors.hpp"
|
|
extern "C" {
|
|
#include "th01/hardware/egc.h"
|
|
#include "th01/hardware/input.hpp"
|
|
#include "th01/snd/mdrv2.h"
|
|
#include "th01/formats/ptn.hpp"
|
|
}
|
|
#include "th01/formats/pf.hpp"
|
|
#include "th01/math/area.hpp"
|
|
#include "th01/math/overlap.hpp"
|
|
#include "th01/math/subpixel.hpp"
|
|
#include "th01/sprites/pellet.h"
|
|
#include "th01/main/particle.hpp"
|
|
extern "C" {
|
|
#include "th01/main/playfld.hpp"
|
|
}
|
|
#include "th01/main/vars.hpp"
|
|
#include "th01/main/hud/hp.hpp"
|
|
#include "th01/main/bullet/pellet.hpp"
|
|
#include "th01/main/boss/boss.hpp"
|
|
#include "th01/main/boss/entity_a.hpp"
|
|
#include "th01/main/player/orb.hpp"
|
|
#include "th01/main/player/player.hpp"
|
|
|
|
// Coordinates
|
|
// -----------
|
|
|
|
static const screen_x_t DISC_CENTER_X = 320;
|
|
static const screen_y_t DISC_CENTER_Y = 180;
|
|
|
|
static const pixel_t HITBOX_W = 96;
|
|
static const pixel_t HITBOX_H = 48;
|
|
|
|
static const screen_x_t HITBOX_LEFT = (
|
|
DISC_CENTER_X - (HITBOX_W / 2) - (ORB_W / 2)
|
|
);
|
|
static const screen_x_t HITBOX_RIGHT = (
|
|
DISC_CENTER_X + (HITBOX_W / 2) - (ORB_W / 2)
|
|
);
|
|
|
|
// Not the actual Y coordinates of the original hitbox, due to a sign confusion
|
|
// bug in kikuri_hittest_orb()!
|
|
static const screen_y_t HITBOX_TOP = (DISC_CENTER_Y - HITBOX_H);
|
|
static const screen_y_t HITBOX_BOTTOM = DISC_CENTER_Y;
|
|
|
|
static const pixel_t SOUL_W = 32;
|
|
static const pixel_t SOUL_H = 32;
|
|
static const pixel_t TEAR_W = 16;
|
|
static const pixel_t TEAR_H = 16;
|
|
static const pixel_t RIPPLE_W = 16;
|
|
static const pixel_t RIPPLE_H = 16;
|
|
|
|
static const screen_x_t SOUL_AREA_LEFT = (PLAYFIELD_LEFT + (PLAYFIELD_W / 20));
|
|
static const screen_y_t SOUL_AREA_TOP = PLAYFIELD_TOP;
|
|
static const screen_x_t SOUL_AREA_RIGHT = (
|
|
PLAYFIELD_RIGHT - (PLAYFIELD_W / 20) - SOUL_W
|
|
);
|
|
static const screen_y_t SOUL_AREA_BOTTOM = PLAYFIELD_BOTTOM;
|
|
|
|
static const screen_y_t TEAR_TOP_MAX = (PLAYFIELD_BOTTOM - TEAR_H);
|
|
// -----------
|
|
|
|
// Always denotes the last phase that ends with that amount of HP.
|
|
enum kikuri_hp_t {
|
|
HP_TOTAL = 14,
|
|
PHASE_2_END_HP = 10,
|
|
PHASE_5_END_HP = 6,
|
|
PHASE_6_END_HP = 0,
|
|
};
|
|
|
|
// State that's suddenly no longer shared with other bosses
|
|
// --------------------------------------------------------
|
|
|
|
#define boss_phase kikuri_phase
|
|
extern int8_t boss_phase;
|
|
// --------------------------------------------------------
|
|
|
|
// Entities
|
|
// --------
|
|
|
|
static const int SOUL_COUNT = 2;
|
|
static const int SOUL_CELS = 3;
|
|
|
|
static const int TEAR_COUNT = 10;
|
|
|
|
#define tear_anim_frame kikuri_tear_anim_frame
|
|
#define souls kikuri_souls
|
|
#define tears kikuri_tears
|
|
// Nonzero values are also used to indicate whether a given tear is alive.
|
|
extern int8_t tear_anim_frame[TEAR_COUNT];
|
|
|
|
extern CBossEntitySized<SOUL_W, SOUL_H> souls[SOUL_COUNT + 3];
|
|
extern CBossEntity tears[10];
|
|
|
|
#define kikuri_ent_load(tmp_i) { \
|
|
souls[0].load("tamasii.bos", 0); \
|
|
souls[1].metadata_assign(souls[0]); \
|
|
\
|
|
tears[0].load("tamasii2.bos", 1); \
|
|
kikuri_ptn_load(); /* MODDERS: Don't nest these load calls. */ \
|
|
\
|
|
for(i = 1; i < TEAR_COUNT; i++) { \
|
|
tears[i].metadata_assign(tears[0]); \
|
|
} \
|
|
}
|
|
|
|
inline void kikuri_ent_free() {
|
|
bos_entity_free(0);
|
|
bos_entity_free(1);
|
|
}
|
|
// --------
|
|
|
|
#define flash_colors kikuri_flash_colors
|
|
#define invincible kikuri_invincible
|
|
#define invincibility_frame kikuri_invincibility_frame
|
|
#define initial_hp_rendered kikuri_initial_hp_rendered
|
|
extern bool16 invincible;
|
|
extern int invincibility_frame;
|
|
extern bool initial_hp_rendered;
|
|
|
|
// .PTN
|
|
// ----
|
|
|
|
static const main_ptn_slot_t PTN_SLOT_RIPPLE = PTN_SLOT_BOSS_1;
|
|
static const int PTN_RIPPLE = PTN_ID(PTN_SLOT_RIPPLE, 0);
|
|
|
|
enum kikuri_ripple_cel_t {
|
|
C_RIPPLE_LOW = 0,
|
|
C_RIPPLE_HIGH = 1,
|
|
C_RIPPLE_FILLED = 2,
|
|
C_RIPPLE_EMPTY = 3,
|
|
};
|
|
|
|
inline void kikuri_ptn_load(void) {
|
|
ptn_load(PTN_SLOT_RIPPLE, "tamayen.ptn");
|
|
}
|
|
|
|
inline void kikuri_ptn_free(void) {
|
|
ptn_free(PTN_SLOT_RIPPLE);
|
|
}
|
|
// ----
|
|
|
|
// Patterns
|
|
// --------
|
|
|
|
#define pattern_state kikuri_pattern_state
|
|
extern union {
|
|
int interval;
|
|
int speed_multiplied_by_8;
|
|
} pattern_state;
|
|
// --------
|
|
|
|
#define select_for_rank kikuri_select_for_rank
|
|
#include "th01/main/select_r.cpp"
|
|
|
|
void kikuri_load(void)
|
|
{
|
|
int i;
|
|
int j;
|
|
|
|
pellet_interlace = true;
|
|
Pellets.unknown_seven = 7;
|
|
palette_copy(boss_palette, z_Palettes, i, j);
|
|
|
|
for(i = 0; i < TEAR_COUNT; i++) {
|
|
tear_anim_frame[i] = 0;
|
|
}
|
|
|
|
kikuri_ent_load(i);
|
|
void kikuri_setup(void);
|
|
kikuri_setup();
|
|
particles_unput_update_render(PO_INITIALIZE, V_WHITE);
|
|
}
|
|
|
|
void kikuri_setup(void)
|
|
{
|
|
int col;
|
|
int comp;
|
|
|
|
boss_phase = 0;
|
|
boss_phase_frame = 0;
|
|
|
|
// Same HP and phase settings as Elis.
|
|
boss_hp = HP_TOTAL;
|
|
hud_hp_first_white = PHASE_2_END_HP;
|
|
hud_hp_first_redwhite = PHASE_5_END_HP;
|
|
|
|
souls[0].pos_set(0, 0, 50,
|
|
SOUL_AREA_LEFT, SOUL_AREA_RIGHT, SOUL_AREA_TOP, SOUL_AREA_BOTTOM
|
|
);
|
|
souls[1].pos_set(0, 0, 50,
|
|
SOUL_AREA_LEFT, SOUL_AREA_RIGHT, SOUL_AREA_TOP, SOUL_AREA_BOTTOM
|
|
);
|
|
|
|
palette_set_grayscale(boss_post_defeat_palette, 0x0, col, comp);
|
|
}
|
|
|
|
void kikuri_free(void)
|
|
{
|
|
kikuri_ent_free();
|
|
kikuri_ptn_free();
|
|
}
|
|
|
|
bool16 near kikuri_hittest_orb(void)
|
|
{
|
|
// Did you mean: > HITBOX_TOP? Using < describes a hitbox from the top of
|
|
// the playfield *until* that point instead...
|
|
if(
|
|
(orb_cur_left > HITBOX_LEFT) && (orb_cur_left < HITBOX_RIGHT) &&
|
|
(orb_cur_top < HITBOX_TOP) && (orb_cur_top < HITBOX_BOTTOM)
|
|
) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void pascal near soul_move_and_render(int i, pixel_t delta_x, pixel_t delta_y)
|
|
{
|
|
souls[i].move_lock_unput_and_put_8(0, delta_x, delta_y, 1);
|
|
if((boss_phase_frame % 12) == 0) {
|
|
if(souls[i].bos_image >= (SOUL_CELS - 1)) {
|
|
souls[i].bos_image = 0;
|
|
} else {
|
|
souls[i].set_image(souls[i].bos_image + 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
void pascal near tears_add(screen_x_t left, screen_y_t top)
|
|
{
|
|
for(int i = 0; i < TEAR_COUNT; i++) {
|
|
if(tear_anim_frame[i] == 0) {
|
|
tears[i].pos_set(
|
|
left, top, 50,
|
|
SOUL_AREA_LEFT, SOUL_AREA_RIGHT, SOUL_AREA_TOP, SOUL_AREA_BOTTOM
|
|
);
|
|
tears[i].bos_image = 0;
|
|
tear_anim_frame[i] = 1;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
bool16 pascal near tear_ripple_hittest(screen_x_t left, pixel_t extra_w)
|
|
{
|
|
if(player_invincible != true) {
|
|
// Translation: 8 pixels in Reimu's center vs. 10 pixels in the ripple
|
|
// sprite's center.
|
|
//
|
|
// ZUN bug: … except that ripples are rendered at byte-aligned X
|
|
// positions. This collision detection uses the internal X coordinate
|
|
// though, which can be (and due to the deterministic nature of this
|
|
// part of the fight, regularly is) anywhere within that byte. This
|
|
// ends up shifting this otherwise logical hitbox up to 7 pixels to the
|
|
// right, compared to where you would expect it based on how the
|
|
// sprites appear on screen.
|
|
//
|
|
// (The ideal fix would be to introduce unaligned rendering for these
|
|
// sprites, rather than byte-aligning the coordinates here. The latter
|
|
// would fork the game, obviously.)
|
|
if(
|
|
(player_left >= (left - ((PLAYER_W / 4) + (RIPPLE_W / 2)))) &&
|
|
(player_left <= (left + extra_w))
|
|
) {
|
|
done = true;
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void near tears_update_and_render(void)
|
|
{
|
|
for(int i = 0; i < TEAR_COUNT; i++) {
|
|
if(tear_anim_frame[i] != 0) {
|
|
if(tears[i].cur_top <= TEAR_TOP_MAX) {
|
|
tears[i].move_lock_unput_and_put_8(0, 0, +8, 1);
|
|
} else {
|
|
void pascal near ripple_update_and_render(
|
|
screen_x_t tear_left, screen_y_t tear_top_max, int8_t &frame
|
|
);
|
|
ripple_update_and_render(
|
|
tears[i].cur_left, TEAR_TOP_MAX, tear_anim_frame[i]
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Oh hey, ZUN did write the obvious wrapper function! Definitely nicer than
|
|
// the [ptn_sloppy_unput_before_alpha_put] hack, but it still suffers from the
|
|
// same word alignment bug.
|
|
void pascal near ripple_unput_and_put(
|
|
screen_x_t left, vram_y_t top, int ptn_id, int quarter
|
|
)
|
|
{
|
|
ptn_sloppy_unput_quarter_16(left, top);
|
|
ptn_put_quarter_8(left, top, ptn_id, quarter);
|
|
}
|
|
|
|
void pascal near ripple_update_and_render(
|
|
screen_x_t tear_left, screen_y_t tear_top_max, int8_t &frame
|
|
)
|
|
{
|
|
// The ripple effect relies entirely on previous cels not being unblitted,
|
|
// and the bottom 8 pixels of the C_RIPPLE_HIGH sprite being flood-filled.
|
|
// Therefore, placing each successive C_RIPPLE_HIGH sprite 8 pixels above
|
|
// the last one entirely avoids the need for C_RIPPLE_FILLED. (Using that
|
|
// sprite would have ensured the stability of the animation against an
|
|
// overlapping player Orb, though!)
|
|
|
|
// Expects [ptn_sloppy_unput_before_alpha_put] to be `true`.
|
|
#define unput_and_put(left, top, frame) { \
|
|
ptn_put_quarter_8( \
|
|
left, \
|
|
(top - ( \
|
|
(frame == 0) ? 0 : \
|
|
(frame <= 5) ? (8 * (frame - 1)) : \
|
|
(frame <= 9) ? (8 * (9 - frame)) : \
|
|
0 \
|
|
)), \
|
|
PTN_RIPPLE, \
|
|
( \
|
|
(frame == 0) ? C_RIPPLE_LOW : \
|
|
(frame <= 4) ? C_RIPPLE_HIGH : \
|
|
(frame <= 9) ? C_RIPPLE_LOW : \
|
|
C_RIPPLE_EMPTY /* effectively just unblitting the sprite */ \
|
|
) \
|
|
); \
|
|
}
|
|
|
|
#define unput_and_put_center(frame) { \
|
|
unput_and_put((tear_left + ( 0 * RIPPLE_W)), tear_top_max, frame); \
|
|
}
|
|
#define unput_and_put_inner(frame) { \
|
|
unput_and_put((tear_left + (-1 * RIPPLE_W)), tear_top_max, frame); \
|
|
unput_and_put((tear_left + (+1 * RIPPLE_W)), tear_top_max, frame); \
|
|
}
|
|
#define unput_and_put_outer(frame) { \
|
|
unput_and_put((tear_left + (-2 * RIPPLE_W)), tear_top_max, frame); \
|
|
unput_and_put((tear_left + (+2 * RIPPLE_W)), tear_top_max, frame); \
|
|
}
|
|
|
|
frame++;
|
|
|
|
// ZUN bug: Ripple sprites can be blitted to non-word-aligned X positions,
|
|
// which this hack doesn't support, as explained in its comment. Using such
|
|
// a hack in the first place only makes sense if you unblit and blit each
|
|
// ripple column individually before moving on to the next one. Which the
|
|
// code below does, by symmetrically moving out from the center to the
|
|
// left and right edges. But then, unblitting every successive ripple
|
|
// column by rounding its X coordinate down and up to the nearest word will
|
|
// also cause half of the previously drawn column to be unblitted. This is
|
|
// exactly why most ripple animations show up with weird empty 8-pixel-wide
|
|
// stripes on the inside (→ non-word-aligned X positions), while some do
|
|
// show up fine (→ word-aligned X positions).
|
|
ptn_sloppy_unput_before_alpha_put = true;
|
|
|
|
// Ultimate hardcoding! Using entities to track this state just wastes
|
|
// space, right? (Hint: It doesn't, this does.)
|
|
if(frame < 5) {
|
|
unput_and_put_center(0);
|
|
tear_ripple_hittest(tear_left, 0);
|
|
mdrv2_se_play(7);
|
|
} else if(frame < 10) {
|
|
unput_and_put_center(1);
|
|
tear_ripple_hittest(tear_left, 0);
|
|
} else if(frame < 15) {
|
|
tear_ripple_hittest((tear_left - RIPPLE_W), (RIPPLE_W * 2));
|
|
unput_and_put_center(2);
|
|
unput_and_put_inner(0);
|
|
} else if(frame < 20) {
|
|
tear_ripple_hittest((tear_left - RIPPLE_W), (RIPPLE_W * 2));
|
|
unput_and_put_center(3);
|
|
unput_and_put_inner(1);
|
|
} else if(frame < 25) {
|
|
tear_ripple_hittest((tear_left - (RIPPLE_W * 2)), (RIPPLE_W * 4));
|
|
unput_and_put_center(4);
|
|
unput_and_put_inner(2);
|
|
unput_and_put_outer(0);
|
|
} else if(frame < 30) {
|
|
tear_ripple_hittest((tear_left - (RIPPLE_W * 2)), (RIPPLE_W * 4));
|
|
unput_and_put_center(5);
|
|
unput_and_put_inner(3);
|
|
unput_and_put_outer(1);
|
|
} else if(frame < 35) {
|
|
tear_ripple_hittest((tear_left - (RIPPLE_W * 2)), (RIPPLE_W * 4));
|
|
unput_and_put_center(6);
|
|
unput_and_put_inner(4);
|
|
unput_and_put_outer(2);
|
|
} else if(frame < 40) {
|
|
tear_ripple_hittest((tear_left - (RIPPLE_W * 2)), (RIPPLE_W * 4));
|
|
unput_and_put_center(7);
|
|
unput_and_put_inner(5);
|
|
unput_and_put_outer(3);
|
|
} else if(frame < 45) {
|
|
tear_ripple_hittest((tear_left - (RIPPLE_W * 2)), (RIPPLE_W * 4));
|
|
unput_and_put_center(8);
|
|
unput_and_put_inner(6);
|
|
unput_and_put_outer(4);
|
|
} else if(frame < 50) {
|
|
tear_ripple_hittest((tear_left - (RIPPLE_W * 2)), (RIPPLE_W * 4));
|
|
unput_and_put_center(9);
|
|
unput_and_put_inner(7);
|
|
unput_and_put_outer(5);
|
|
} else if(frame < 55) {
|
|
tear_ripple_hittest((tear_left - (RIPPLE_W * 2)), (RIPPLE_W * 4));
|
|
unput_and_put_center(10);
|
|
unput_and_put_inner(8);
|
|
unput_and_put_outer(6);
|
|
} else if(frame < 60) {
|
|
tear_ripple_hittest((tear_left - (RIPPLE_W * 2)), (RIPPLE_W * 4));
|
|
unput_and_put_inner(9);
|
|
unput_and_put_outer(7);
|
|
} else if(frame < 65) {
|
|
unput_and_put_inner(10);
|
|
unput_and_put_outer(8);
|
|
} else if(frame < 70) {
|
|
unput_and_put_outer(9);
|
|
} else if(frame < 75) {
|
|
unput_and_put_outer(10);
|
|
} else {
|
|
frame = 0;
|
|
}
|
|
|
|
ptn_sloppy_unput_before_alpha_put = false;
|
|
|
|
#undef unput_and_put_outer
|
|
#undef unput_and_put_inner
|
|
#undef unput_and_put_center
|
|
#undef unput_and_put
|
|
}
|