/// Makai Stage 10 Boss - YuugenMagan /// --------------------------------- #include #include "platform.h" #include "decomp.hpp" #include "pc98.h" #include "planar.h" #include "master.hpp" #include "th01/rank.h" #include "th01/resident.hpp" #include "th01/v_colors.hpp" #include "th01/math/area.hpp" #include "th01/math/dir.hpp" #include "th01/math/overlap.hpp" #include "th01/math/polar.hpp" #include "th01/math/subpixel.hpp" #include "th01/math/vector.hpp" #include "th01/hardware/egc.h" #include "th01/hardware/graph.h" #include "th01/hardware/palette.h" #include "th01/snd/mdrv2.h" #include "th01/formats/grp.h" #include "th01/formats/ptn.hpp" #include "th01/sprites/pellet.h" #include "th01/main/entity.hpp" #include "th01/main/particle.hpp" #include "th01/main/playfld.hpp" #include "th01/main/player/orb.hpp" #include "th01/main/boss/defeat.hpp" #include "th01/main/boss/boss.hpp" #include "th01/main/boss/entity_a.hpp" #include "th01/main/boss/palette.hpp" #include "th01/main/bullet/laser_s.hpp" #include "th01/main/bullet/line.hpp" #include "th01/main/bullet/missile.hpp" #include "th01/main/bullet/pellet.hpp" #include "th01/main/hud/hp.hpp" #include "th01/main/player/player.hpp" #include "th01/main/stage/palette.hpp" #include "th01/main/stage/stages.hpp" // Coordinates // ----------- static const pixel_t EYE_W = 64; static const pixel_t EYE_H = 48; static const pixel_t PUPIL_H = 12; static const pixel_t IRIS_H = 7; static const pixel_t EYE_LATERAL_CENTER_DISTANCE_X = 224; static const pixel_t EYE_SOUTH_CENTER_DISTANCE_X = 96; static const pixel_t EYE_NORTH_LATERAL_DISTANCE_Y = 64; static const pixel_t EYE_LATERAL_SOUTH_DISTANCE_Y = 48; // Center of the kimono figure static const screen_y_t PENTAGRAM_REGULAR_CENTER_Y = 189; static const screen_x_t EYE_WEST_LEFT = ( PLAYFIELD_CENTER_X - EYE_LATERAL_CENTER_DISTANCE_X - (EYE_W / 2) ); static const screen_x_t EYE_EAST_LEFT = ( PLAYFIELD_CENTER_X + EYE_LATERAL_CENTER_DISTANCE_X - (EYE_W / 2) ); static const screen_x_t EYE_SOUTHWEST_LEFT = ( PLAYFIELD_CENTER_X - EYE_SOUTH_CENTER_DISTANCE_X - (EYE_W / 2) ); static const screen_x_t EYE_SOUTHEAST_LEFT = ( PLAYFIELD_CENTER_X + EYE_SOUTH_CENTER_DISTANCE_X - (EYE_W / 2) ); static const screen_x_t EYE_NORTH_LEFT = (PLAYFIELD_CENTER_X - (EYE_W / 2)); static const screen_y_t EYE_NORTH_TOP = PLAYFIELD_TOP; static const screen_y_t EYE_LATERAL_TOP = ( EYE_NORTH_TOP + EYE_NORTH_LATERAL_DISTANCE_Y ); static const screen_y_t EYE_SOUTH_TOP = ( EYE_LATERAL_TOP + EYE_LATERAL_SOUTH_DISTANCE_Y ); // ----------- enum yuugenmagan_colors_t { COL_YOKOSHIMA = 15, // The big 邪 in the background }; // Always denotes the last phase that ends with that amount of HP. enum yuugenmagan_hp_t { HP_TOTAL = 16, HP_PHASE_1_END = 15, HP_PHASE_3_END = 12, HP_PHASE_5_END = 10, HP_PHASE_7_END = 8, HP_PHASE_9_END = 2, HP_PHASE_13_END = 0, }; // Entities // -------- static const int EYE_COUNT = 5; enum eye_cel_t { C_HIDDEN = 0, C_CLOSED = 1, C_HALFOPEN = 2, C_DOWN = 3, C_LEFT = 4, C_RIGHT = 5, C_AHEAD = 6, }; // Eye flags typedef int8_t eye_flag_t; // Code generation wants 16-bit constants, unfortunately static const int EF_NONE = 0; static const int EF_WEST = (1 << 0); static const int EF_EAST = (1 << 1); static const int EF_SOUTHWEST = (1 << 2); static const int EF_SOUTHEAST = (1 << 3); static const int EF_NORTH = (1 << 4); static const int EF_RESET = (1 << 5); struct CEyeEntity : public CBossEntitySized { // Relative pupil and iris coordinates // ----------------------------------- // These only apply to the player-facing cels (C_DOWN, C_LEFT, and C_RIGHT). // ZUN quirk: Doesn't really correspond to any precise feature of any eye // cel. The best match is the center of the iris on C_DOWN, but even that // would be off by 1 pixel – not to mention very wrong for every other cel. screen_x_t offcenter_x(void) const { return (cur_center_x() - 4); } // Correct for C_LEFT and C_RIGHT, off by 1 pixel for C_DOWN. screen_y_t iris_top(void) const { return (cur_center_y() + 4); } // Correct for C_LEFT and C_RIGHT, off by 1 pixel for C_DOWN. screen_y_t iris_center_y(void) const { return (iris_top() + (IRIS_H / 2) + 1); } // Correct for C_LEFT and C_RIGHT, off by 1 pixel for C_DOWN. screen_y_t pupil_bottom(void) const { return (cur_center_y() + PUPIL_H); } // ----------------------------------- void track_player(void) { if((cur_center_x() - player_left) > (EYE_W / 2)) { set_image(C_LEFT); } else if((player_left - cur_center_x()) > (EYE_W / 2)) { set_image(C_RIGHT); } else { set_image(C_DOWN); } } void downwards_laser_put(void) const { graph_r_vline( offcenter_x(), iris_top(), (PLAYFIELD_BOTTOM - 2), V_WHITE ); } void downwards_laser_unput(void) const { graph_r_line_unput( offcenter_x(), iris_top(), offcenter_x(), PLAYFIELD_BOTTOM ); } void fire_from_iris_center(pellet_group_t group, float speed_base) const { Pellets.add_group( (cur_center_x() - (PELLET_W / 2)), (iris_center_y() - (PELLET_H / 2)), group, to_sp(speed_base) ); } void fire_from_bottom_center(pellet_group_t group, float speed_base) const { Pellets.add_group( (cur_center_x() - (PELLET_W / 2)), (pupil_bottom() - (PELLET_H / 2)), group, to_sp(speed_base) ); } void fire_from_bottom_center( bool mirror_angle_horizontally, const unsigned char &angle, float speed_base, pellet_motion_t motion_type = PM_REGULAR, float speed_for_motion_fixed = 0.0f ) const { Pellets.add_single( (cur_center_x() - (PELLET_W / 2)), (pupil_bottom() - (PELLET_H / 2)), (mirror_angle_horizontally ? (0x80 - angle) : angle), to_sp(speed_base), motion_type, to_sp(speed_for_motion_fixed) ); } }; #define eye_west reinterpret_cast(boss_entity_0) #define eye_east reinterpret_cast(boss_entity_1) #define eye_southwest reinterpret_cast(boss_entity_2) #define eye_southeast reinterpret_cast(boss_entity_3) #define eye_north reinterpret_cast(boss_entity_4) // For [eye_predicate], pass a macro that takes a single eye_flag_t parameter. #define eyes_foreach_if(eye_predicate, method) { \ if(eye_predicate( EF_WEST)) { eye_west.method(); } \ if(eye_predicate( EF_EAST)) { eye_east.method(); } \ if(eye_predicate(EF_SOUTHWEST)) { eye_southwest.method(); } \ if(eye_predicate(EF_SOUTHEAST)) { eye_southeast.method(); } \ if(eye_predicate( EF_NORTH)) { eye_north.method(); } \ } inline void eyes_locked_unput_and_put_then_track(eye_flag_t eye_flag) { #define eye_flag_is_set(bit) ( \ eye_flag & bit \ ) // Unblitting/rendering first, and *then* tracking? Depending on when the // eye's cel is updated within the entity's lock, this order introduces a // variable tracking latency from 1 to 4 frames. Which isn't all too bad, // and might even be considered somewhat realistic. Certainly not ZUN quirk // territory. eyes_foreach_if(eye_flag_is_set, locked_unput_and_put_8); eyes_foreach_if(eye_flag_is_set, track_player); #undef eye_flag_is_set } #define eyes_set_image(eye_flag, cel) { \ if(eye_flag & EF_WEST) { eye_west.set_image(cel); } \ if(eye_flag & EF_EAST) { eye_east.set_image(cel); } \ if(eye_flag & EF_SOUTHWEST) { eye_southwest.set_image(cel); } \ if(eye_flag & EF_SOUTHEAST) { eye_southeast.set_image(cel); } \ if(eye_flag & EF_NORTH) { eye_north.set_image(cel); } \ } inline void yuugenmagan_ent_load(void) { eye_west.load("boss2.bos", 0); eye_east.metadata_assign(eye_west); eye_southwest.metadata_assign(eye_west); eye_southeast.metadata_assign(eye_west); eye_north.metadata_assign(eye_west); } inline void yuugenmagan_ent_free(void) { bos_entity_free(0); } // -------- // .PTN // ---- static const main_ptn_slot_t PTN_SLOT_MISSILE = PTN_SLOT_BOSS_1; // ---- // Patterns // -------- // Yes! Finally a proper stateless version of this concept! inline int select_for_rank( int for_easy, int for_normal, int for_hard, int for_lunatic ) { return ( (rank == RANK_EASY) ? for_easy : (rank == RANK_NORMAL) ? for_normal : (rank == RANK_HARD) ? for_hard : (rank == RANK_LUNATIC) ? for_lunatic : for_normal ); } static int pattern_interval; // -------- void yuugenmagan_load(void) { yuugenmagan_ent_load(); void yuugenmagan_setup(void); yuugenmagan_setup(); Missiles.load(PTN_SLOT_MISSILE); Missiles.reset(); } void yuugenmagan_setup(void) { svc2 col; int comp; grp_palette_load_show("boss2.grp"); boss_palette_snap(); eye_west .set_image(C_HIDDEN); eye_east .set_image(C_HIDDEN); eye_southwest.set_image(C_HIDDEN); eye_southeast.set_image(C_HIDDEN); eye_north .set_image(C_HIDDEN); palette_copy(boss_post_defeat_palette, z_Palettes, col, comp); // These exactly correspond to the yellow boxes in BOSS2.GRP. eye_west .pos_set( EYE_WEST_LEFT, EYE_LATERAL_TOP); eye_east .pos_set( EYE_EAST_LEFT, EYE_LATERAL_TOP); eye_southwest.pos_set(EYE_SOUTHWEST_LEFT, EYE_SOUTH_TOP); eye_southeast.pos_set(EYE_SOUTHEAST_LEFT, EYE_SOUTH_TOP); eye_north .pos_set( EYE_NORTH_LEFT, EYE_NORTH_TOP); eye_west .hitbox_orb_set(-4, -4, (EYE_W + 4), EYE_H); eye_east .hitbox_orb_set(-4, -4, (EYE_W + 4), EYE_H); eye_southwest.hitbox_orb_set(-4, -4, (EYE_W + 4), EYE_H); eye_southeast.hitbox_orb_set(-4, -4, (EYE_W + 4), EYE_H); eye_north .hitbox_orb_set(-4, -4, (EYE_W + 4), EYE_H); eye_west .hitbox_orb_deactivate(); eye_east .hitbox_orb_deactivate(); eye_southwest.hitbox_orb_deactivate(); eye_southeast.hitbox_orb_deactivate(); eye_north .hitbox_orb_deactivate(); boss_phase_frame = 0; boss_phase = 0; boss_hp = HP_TOTAL; hud_hp_first_white = HP_PHASE_3_END; hud_hp_first_redwhite = HP_PHASE_7_END; // ZUN bloat: Redundant, no particles are shown in this fight. particles_unput_update_render(PO_INITIALIZE, V_WHITE); } void unused_formula(int a, int b, int& ret) { double delta = (b - a); ret = ((delta * isqrt(3)) / 2.0f); } void yuugenmagan_free(void) { yuugenmagan_ent_free(); ptn_free(PTN_SLOT_MISSILE); } // Phases // ------ static const int EYE_OPENING_FRAMES = 60; static const int EYE_TOGGLE_FRAMES = (EYE_OPENING_FRAMES + 10); static const int SUBPHASE_PREPARE_FRAMES = 100; static const int SUBPHASE_TIMEOUT_FRAMES = 200; enum phase_0_keyframe_t { KEYFRAME_LATERAL_OPENING = SUBPHASE_PREPARE_FRAMES, KEYFRAME_SOUTH_OPENING = 120, KEYFRAME_NORTH_OPENING = 140, KEYFRAME_LATERAL_OPEN = (KEYFRAME_LATERAL_OPENING + EYE_OPENING_FRAMES), KEYFRAME_SOUTH_OPEN = (KEYFRAME_SOUTH_OPENING + EYE_OPENING_FRAMES), KEYFRAME_NORTH_OPEN = (KEYFRAME_NORTH_OPENING + EYE_OPENING_FRAMES), KEYFRAME_CLOSING = 240, KEYFRAME_CLOSED = 260, KEYFRAME_HIDDEN = 280, KEYFRAME_LATERAL_LASER_DONE = 300, KEYFRAME_SOUTH_LASER_DONE = 320, KEYFRAME_NORTH_LASER_DONE = 330, }; // Matches the animation in eyes_toggle_and_yokoshima_recolor(). inline void phase_0_eye_open(CEyeEntity& eye, int frame, int frame_first) { eye.set_image( ((frame - frame_first) < ((EYE_OPENING_FRAMES / 3) * 1)) ? C_CLOSED : ((frame - frame_first) < ((EYE_OPENING_FRAMES / 3) * 2)) ? C_HALFOPEN : ((frame - frame_first) < ((EYE_OPENING_FRAMES / 3) * 3)) ? C_AHEAD : /* */ C_DOWN ); } inline void phase_0_eye_close(CEyeEntity& eye, int frame) { eye.set_image( (frame == KEYFRAME_CLOSING) ? C_HALFOPEN : (frame == KEYFRAME_CLOSED) ? C_CLOSED : C_HIDDEN ); } inline void phase_0_eyes_open(int frame) { if( (frame >= KEYFRAME_LATERAL_OPENING) && (frame <= KEYFRAME_LATERAL_OPEN) ) { phase_0_eye_open(eye_west, frame, KEYFRAME_LATERAL_OPENING); phase_0_eye_open(eye_east, frame, KEYFRAME_LATERAL_OPENING); } if((frame >= KEYFRAME_SOUTH_OPENING) && (frame <= KEYFRAME_SOUTH_OPEN)) { phase_0_eye_open(eye_southwest, frame, KEYFRAME_SOUTH_OPENING); phase_0_eye_open(eye_southeast, frame, KEYFRAME_SOUTH_OPENING); } if((frame >= KEYFRAME_NORTH_OPENING) && (frame <= KEYFRAME_NORTH_OPEN)) { phase_0_eye_open(eye_north, frame, KEYFRAME_NORTH_OPENING); } } inline void phase_0_eyes_close(int frame) { phase_0_eye_close(eye_west, frame); phase_0_eye_close(eye_east, frame); phase_0_eye_close(eye_southwest, frame); phase_0_eye_close(eye_southeast, frame); phase_0_eye_close(eye_north, frame); } void phase_0_downwards_lasers(void) { #define laser_hittest(eye, player_w) ( \ overlap_low_center_lt_gt( \ player_left, player_w, eye.offcenter_x(), (EYE_W - (EYE_W / 4)) \ ) \ ) if(boss_phase_frame > KEYFRAME_LATERAL_OPEN) { if(boss_phase_frame < KEYFRAME_LATERAL_LASER_DONE) { eye_west.downwards_laser_put(); eye_east.downwards_laser_put(); // ZUN quirk: The hitbox for the rightmost eye is much larger than // the other ones?! if( laser_hittest(eye_west, PLAYER_W) || laser_hittest(eye_east, (PLAYER_W / 4)) ) { player_is_hit = true; } } if(boss_phase_frame > KEYFRAME_SOUTH_OPEN) { if(boss_phase_frame < KEYFRAME_SOUTH_LASER_DONE) { eye_southwest.downwards_laser_put(); eye_southeast.downwards_laser_put(); if( laser_hittest(eye_southwest, PLAYER_W) || laser_hittest(eye_southeast, PLAYER_W) ) { player_is_hit = true; } } if(boss_phase_frame > KEYFRAME_NORTH_OPEN) { eye_north.downwards_laser_put(); if(laser_hittest(eye_north, PLAYER_W)) { player_is_hit = true; } } } } if(player_invincible) { player_is_hit = false; } #undef laser_hittest } inline void phase_0_fire(CEyeEntity& eye) { eye.fire_from_iris_center(PG_5_SPREAD_WIDE_AIMED, 3.375f); } inline void fire_from_lateral(pellet_group_t group, float speed_base) { eye_west.fire_from_bottom_center(group, speed_base); eye_east.fire_from_bottom_center(group, speed_base); } void phase_1_pellets_from_lateral() { if((boss_phase_frame % pattern_interval) == 10) { fire_from_lateral(PG_2_SPREAD_WIDE_AIMED, 1.5f); } else if((boss_phase_frame % pattern_interval) == 25) { fire_from_lateral(PG_2_SPREAD_WIDE_AIMED, 1.75f); } else if((boss_phase_frame % pattern_interval) == 40) { fire_from_lateral(PG_2_SPREAD_WIDE_AIMED, 2.0f); } else if((boss_phase_frame % pattern_interval) == 60) { fire_from_lateral(PG_2_SPREAD_WIDE_AIMED, 2.75f); } else if((boss_phase_frame % pattern_interval) == 80) { fire_from_lateral(PG_2_SPREAD_WIDE_AIMED, 3.125f); } else if((boss_phase_frame % pattern_interval) == 110) { fire_from_lateral(PG_1_AIMED, 4.25f); } } // ------ // Aimed missiles // -------------- static const int MISSILE_INTERVAL = 8; // The original code generation of missile_angle() combines all constant parts // into a single immediate argument, so these can't be defined in terms of // CEyeEntity's cur_center_x() and cur_center_y() helpers. // (cur_center_x() + 4) kind of corresponds to the right edge of C_DOWN. static const pixel_t MISSILE_OFFSET_LEFT = ((EYE_W / 2) + 4 - (MISSILE_W / 2)); // (pupil_bottom() - (MISSILE_H / 2)) static const pixel_t MISSILE_OFFSET_TOP = ( (EYE_H / 2) + PUPIL_H - (MISSILE_H / 2) ); inline unsigned char missile_angle( const CEyeEntity& eye, const screen_x_t& target_x ) { return iatan2( (player_bottom() - eye.cur_top + MISSILE_OFFSET_TOP), (target_x - eye.cur_left + MISSILE_OFFSET_LEFT) ); } void pascal near fire_missile_pair_from_south( unsigned char angle_for_southwest, unsigned char angle_for_southeast ) { point_t velocity; vector2(velocity.x, velocity.y, 8, angle_for_southwest); Missiles.add( (eye_southwest.cur_left + MISSILE_OFFSET_LEFT), (eye_southwest.cur_top + MISSILE_OFFSET_TOP), velocity.x, velocity.y ); vector2(velocity.x, velocity.y, 8, angle_for_southeast); Missiles.add( (eye_southeast.cur_left + MISSILE_OFFSET_LEFT), (eye_southeast.cur_top + MISSILE_OFFSET_TOP), velocity.x, velocity.y ); } enum missile_subphase_t { MSP_PREPARE = 0, MSP_AIM_AND_FIRE_AROUND = 1, MSP_SHIFT_ANGLE_1 = 2, MSP_SHIFT_ANGLE_2 = 3, MSP_COUNT = 4, }; #define missile_subphase_variation(variation, msp) ( \ (variation * MSP_COUNT) + msp \ ) // MODDERS: Simply replace with ((subphase / MSP_COUNT) == variation). #define missile_subphase_variation_is( \ subphase, variation, msp, variations_max \ ) ( \ (variations_max >= (variation + 1)) && \ (subphase == missile_subphase_variation(variation, msp)) \ ) // MODDERS: Simply replace with ((subphase % MSP_COUNT) == msp). #define missile_subphase_is(subphase, msp, variations_max) ( \ missile_subphase_variation_is(subphase, 0, msp, variations_max) || \ missile_subphase_variation_is(subphase, 1, msp, variations_max) || \ missile_subphase_variation_is(subphase, 2, msp, variations_max) \ ) inline void missile_pairs_shift_angle_1_away( const int& subphase, unsigned char& angle_southwest, unsigned char& angle_southeast ) { angle_southwest -= 0x02; angle_southeast += 0x02; } #define missile_pairs_shift_angle_2_towards( \ subphase, angle_southwest, angle_southeast \ ) { \ if(subphase == missile_subphase_variation(0, MSP_SHIFT_ANGLE_2)) { \ angle_southwest += 0x04; \ } else if(subphase == missile_subphase_variation(1, MSP_SHIFT_ANGLE_2)) { \ angle_southeast -= 0x04;\ } else if(subphase == missile_subphase_variation(2, MSP_SHIFT_ANGLE_2)) { \ angle_southwest += 0x02; \ angle_southeast -= 0x02; \ } \ } inline void missile_pairs_shift_angle_1_clock( const int& subphase, unsigned char& angle_southwest, unsigned char& angle_southeast ) { if(subphase == missile_subphase_variation(0, MSP_SHIFT_ANGLE_1)) { angle_southwest -= 0x02; angle_southeast -= 0x02; } else { angle_southwest += 0x02; angle_southeast += 0x02; } } #define missile_pairs_shift_angle_2_clock( \ subphase, angle_southwest, angle_southeast \ ) { \ if(subphase == missile_subphase_variation(0, MSP_SHIFT_ANGLE_2)) { \ angle_southwest += 0x03; \ angle_southeast += 0x03; \ } else if(subphase == missile_subphase_variation(1, MSP_SHIFT_ANGLE_2)) { \ angle_southwest -= 0x03; \ angle_southeast -= 0x03; \ } \ } // Processes a single frame of the missile pair pattern. [subphase] cycles // between the fields of missile_subphase_t, offset by randomly selected // variations. These can be differentiated in [shift_angle_1_func] and // [shift_angle_2_func]. #define pattern_missile_pairs_from_south( \ subphase, \ missile_pairs_fired_in_subphase, \ target_left, \ angle_southwest, \ angle_southeast, \ iterations_done, \ variations_max, \ shift_angle_1_func, \ shift_angle_2_func \ ) { \ eyes_locked_unput_and_put_then_track(EF_SOUTHWEST | EF_SOUTHEAST); \ \ if(boss_phase_frame >= (SUBPHASE_PREPARE_FRAMES - 10)) { \ /** \ * Hardcoding two maximum variations? Doesn't matter though: \ * If we're in MSP_PREPARE, we're always in variation 0 (see below). \ */ \ if(missile_subphase_is(subphase, MSP_PREPARE, 2)) { \ eye_southwest.set_image(C_CLOSED); \ eye_southeast.set_image(C_CLOSED); \ } \ } \ if( \ (boss_phase_frame == SUBPHASE_PREPARE_FRAMES) || \ missile_subphase_is(subphase, MSP_AIM_AND_FIRE_AROUND, variations_max) \ ) { \ if(missile_subphase_variation_is(subphase, 0, MSP_PREPARE, 1)) { \ subphase = missile_subphase_variation( \ (rand() % variations_max), MSP_AIM_AND_FIRE_AROUND \ ); \ target_left = (player_left - (MISSILE_W / 2)); \ angle_southwest = missile_angle(eye_southwest, target_left); \ angle_southeast = missile_angle(eye_southeast, target_left); \ angle_southwest -= 0x10; \ angle_southeast += 0x10; \ } \ if((boss_phase_frame % MISSILE_INTERVAL) == 0) { \ fire_missile_pair_from_south(angle_southwest, angle_southeast); \ } \ if(boss_phase_frame == SUBPHASE_TIMEOUT_FRAMES) { \ subphase++; \ missile_pairs_fired_in_subphase = 0; \ } \ } \ if(missile_subphase_is(subphase, MSP_SHIFT_ANGLE_1, variations_max)) { \ if((boss_phase_frame % MISSILE_INTERVAL) == 1) { \ missile_pairs_fired_in_subphase++; \ shift_angle_1_func(subphase, angle_southwest, angle_southeast); \ fire_missile_pair_from_south(angle_southwest, angle_southeast); \ if(missile_pairs_fired_in_subphase > 10) { \ subphase++; \ missile_pairs_fired_in_subphase = 0; \ } \ } \ } else if( \ missile_subphase_is(subphase, MSP_SHIFT_ANGLE_2, variations_max) \ ) { \ if((boss_phase_frame % MISSILE_INTERVAL) == 1) { \ missile_pairs_fired_in_subphase++; \ shift_angle_2_func(subphase, angle_southwest, angle_southeast); \ fire_missile_pair_from_south(angle_southwest, angle_southeast); \ if(missile_pairs_fired_in_subphase > 10) { \ /** \ * Yup, always resetting to variation 0, which will in turn \ * select a new random one. \ */ \ subphase = missile_subphase_variation(0, MSP_PREPARE); \ \ missile_pairs_fired_in_subphase = 0; \ boss_phase_frame = 0; \ iterations_done++; \ } \ } \ } \ } // -------------- // Lasers across the playfield // --------------------------- static const pixel_t LASER_W = 3; static const pixel_t LASER_VELOCITY = 4; static const int LASER_INTERVAL = 4; static const pixel_t LASER_VELOCITY_STEP = (LASER_VELOCITY * LASER_INTERVAL); static const dots8_t LASER_DOTS = ( ((1 << LASER_W) - 1) << (BYTE_DOTS - LASER_W) // (***_____) ); enum laser_subphase_t { LSP_PREPARE = 0, LSP_ACTIVE = 1, LSP_DONE = 2, }; inline int laser_subphase(int iteration, laser_subphase_t lsp) { return ((iteration * LSP_DONE) + lsp); } #define laser_pattern_should_run(subphase, iteration) ( \ ( \ (boss_phase_frame == 40) && \ (subphase == laser_subphase(iteration, LSP_PREPARE)) \ ) || (subphase == laser_subphase(iteration, LSP_ACTIVE)) \ ) #define laser_eye eye_north inline screen_x_t laser_left(void) { return laser_eye.offcenter_x(); } inline screen_y_t laser_top(void) { return laser_eye.iris_top(); } inline screen_x_t laser_right(const pixel_t& x_edge_offset, x_direction_t dir) { return ((dir == X_LEFT) ? (PLAYFIELD_RIGHT - x_edge_offset) : (PLAYFIELD_LEFT + x_edge_offset) ); } inline void laser_unput_render_hittest( const pixel_t& x_edge_offset, x_direction_t dir ) { screen_x_t cur_right = laser_right(x_edge_offset, dir); screen_y_t cur_top = laser_top(); screen_y_t cur_left = laser_left(); screen_x_t prev_right = (laser_right(x_edge_offset, dir) - ( dir ? -LASER_VELOCITY_STEP : LASER_VELOCITY_STEP )); screen_y_t prev_top = laser_top(); screen_y_t prev_left = laser_left(); linebullet_unput(prev_left, prev_top, prev_right, PLAYFIELD_BOTTOM); linebullet_put_patterned_and_hittest( cur_left, cur_top, cur_right, PLAYFIELD_BOTTOM, V_WHITE, LASER_DOTS ); } inline void laser_unput(const pixel_t& x_edge_offset, x_direction_t dir) { linebullet_unput( laser_left(), laser_top(), laser_right(x_edge_offset, dir), PLAYFIELD_BOTTOM ); } // Separate function to work around the `Condition is always true/false` and // `Unreachable code` warnings. Should really be done unconditionally, though. inline void conditionally_reset(pixel_t& x_edge_offset, bool cond) { if(cond) { x_edge_offset = 0; } } #define pattern_single_laser_across_playfield( \ subphase, redundant_lvalue, x_edge_offset, iteration, right_to_left \ ) \ if(subphase == laser_subphase(iteration, LSP_PREPARE)) { \ subphase = laser_subphase(iteration, LSP_ACTIVE); \ redundant_lvalue = 0; /* ZUN bloat */ \ conditionally_reset(x_edge_offset, right_to_left); \ } \ if((boss_phase_frame % LASER_INTERVAL) == 1) { \ /* Track the laser target */ \ if(x_edge_offset <= (PLAYFIELD_CENTER_X - EYE_W)) { \ laser_eye.set_image(right_to_left ? C_RIGHT : C_LEFT); \ } else if(x_edge_offset >= (PLAYFIELD_CENTER_X + EYE_W)) { \ laser_eye.set_image(right_to_left ? C_LEFT : C_RIGHT); \ } else { \ laser_eye.set_image(C_DOWN); \ } \ \ laser_unput_render_hittest(x_edge_offset, right_to_left); \ if(x_edge_offset >= (PLAYFIELD_W - (PLAYER_W + (PLAYER_W / 2)))) { \ laser_unput(x_edge_offset, right_to_left); \ subphase = laser_subphase((iteration + 1), LSP_PREPARE); \ boss_phase_frame = 0; \ \ /** * ZUN bloat: Done after LSP_PREPARE, where it's much more \ * appropriate. \ */ \ x_edge_offset = 0; \ \ if(rank > RANK_NORMAL) { \ laser_eye.fire_from_iris_center( \ PG_3_SPREAD_NARROW_AIMED, 0.1875f \ ); \ } \ } \ x_edge_offset += LASER_VELOCITY_STEP; \ } #define pattern_dual_lasers_across_playfield( \ subphase, redundant_lvalue, x_edge_offset, iteration \ ) \ if(subphase == laser_subphase(iteration, LSP_PREPARE)) { \ subphase = laser_subphase(iteration, LSP_ACTIVE); \ redundant_lvalue = 0; /* ZUN bloat */ \ x_edge_offset = 0; \ } \ if((boss_phase_frame % LASER_INTERVAL) == 1) { \ laser_eye.set_image(C_AHEAD); \ \ /** \ * ZUN bug: The really should be both unblitted and *then* both \ * rendered. With graph_r_line_unput() unblitting 32 horizontal \ * pixels for every row, the top part of the X_RIGHT-moving laser \ * will never be fully visible. \ */ \ laser_unput_render_hittest(x_edge_offset, X_RIGHT); \ laser_unput_render_hittest(x_edge_offset, X_LEFT); \ if(x_edge_offset >= (PLAYFIELD_CENTER_X - PLAYER_W)) { \ laser_unput(x_edge_offset, X_RIGHT); \ laser_unput(x_edge_offset, X_LEFT); \ subphase = laser_subphase(iteration, LSP_DONE); \ boss_phase_frame = 0; \ x_edge_offset = 0; \ if(rank == RANK_HARD) { \ laser_eye.fire_from_iris_center( \ PG_3_SPREAD_NARROW_AIMED, 0.1875f \ ); \ } \ if(rank == RANK_LUNATIC) { \ laser_eye.fire_from_iris_center( \ PG_5_SPREAD_NARROW_AIMED, 0.25f \ ); \ } \ } \ x_edge_offset += LASER_VELOCITY_STEP; \ } // --------------------------- /// Pentagram /// --------- static const int PENTAGRAM_INTERVAL = 4; static const int PENTAGRAM_POINTS = 5; static struct { // Corners, arranged in counterclockwise order. screen_x_t x[PENTAGRAM_POINTS]; screen_y_t y[PENTAGRAM_POINTS]; pixel_t radius; screen_point_t center; // This reversed order ruins a potential PUSH optimization... pixel_t velocity_y; pixel_t velocity_x; void unput(void) { linebullet_unput(x[0], y[0], x[2], y[2]); linebullet_unput(x[2], y[2], x[4], y[4]); linebullet_unput(x[4], y[4], x[1], y[1]); linebullet_unput(x[1], y[1], x[3], y[3]); linebullet_unput(x[3], y[3], x[0], y[0]); } void put_and_hittest(void) { linebullet_put_and_hittest(x[0], y[0], x[2], y[2], V_WHITE); linebullet_put_and_hittest(x[2], y[2], x[4], y[4], V_WHITE); linebullet_put_and_hittest(x[4], y[4], x[1], y[1], V_WHITE); linebullet_put_and_hittest(x[1], y[1], x[3], y[3], V_WHITE); linebullet_put_and_hittest(x[3], y[3], x[0], y[0], V_WHITE); } void aim(const screen_x_t& target_x, screen_y_t target_y) { vector2_between( center.x, inhibit_Z3(center.y), target_x, target_y, velocity_x, velocity_y, 4 ); } void move(bool dumb_workaround_for_different_call_positions = true) { if(dumb_workaround_for_different_call_positions) { center.y += velocity_y; center.x += velocity_x; } } } pentagram; // Initial position of the pentagram, with corners in eyes // ------------------------------------------------------- inline void pentagram_between_eyes_put( const CEyeEntity& eye_1, const CEyeEntity& eye_2 ) { linebullet_put_and_hittest( eye_1.offcenter_x(), eye_1.iris_top(), eye_2.offcenter_x(), eye_2.iris_top(), V_WHITE ); } inline void pentagram_between_eyes_put(void) { pentagram_between_eyes_put(eye_north, eye_southwest); pentagram_between_eyes_put(eye_southwest, eye_east); pentagram_between_eyes_put(eye_east, eye_west); pentagram_between_eyes_put(eye_west, eye_southeast); pentagram_between_eyes_put(eye_southeast, eye_north); } // ------------------------------------------------------- // Shrink animation // ---------------- // No weird fraction for once! static const screen_y_t PENTAGRAM_SHRINK_TARGET_CENTER_Y = PLAYFIELD_CENTER_Y; static const pixel_t PENTAGRAM_RADIUS_FINAL = 64; static const unsigned char PENTAGRAM_ANGLE_INITIAL = 0xC0; // (-0x40) // As the largest distance, we can define all other shrink distances in terms // of this one. Technically, the radius factor should be // // cos(PENTAGRAM_ANGLE_INITIAL + (1 × (360° / PENTAGRAM_POINTS))) // // for the 1st clockwise point on the pentagram. But since that comes out to // roughly ≈ 0.95 and thus *almost* 1, it's acceptable to just round up. static const pixel_t PENTAGRAM_SHRINK_DISTANCE = ( EYE_LATERAL_CENTER_DISTANCE_X - (PENTAGRAM_RADIUS_FINAL * 1) ); inline screen_x_t pentagram_shrink_x(eye_flag_t eye, const pixel_t& distance) { enum { DISTANCE_SOUTH = static_cast( // cos(PENTAGRAM_ANGLE_INITIAL + (2 × (360° / PENTAGRAM_POINTS))) EYE_SOUTH_CENTER_DISTANCE_X - (PENTAGRAM_RADIUS_FINAL * 0.587f) ), // Integer arithmetic can never be precise for the distances required // here. All these calculations are at least an attempt of deriving // those magic shrink factors, even if we have to cheat by "rounding // up" in the end. DIVISOR_SOUTH = ((PENTAGRAM_SHRINK_DISTANCE / DISTANCE_SOUTH) + 1), }; if(eye == EF_WEST) { return ( eye_west.offcenter_x() + distance); } else if(eye == EF_EAST) { return ( eye_east.offcenter_x() - distance); } else if(eye == EF_SOUTHWEST) { return (eye_southwest.offcenter_x() + (distance / DIVISOR_SOUTH)); } else if(eye == EF_SOUTHEAST) { return (eye_southeast.offcenter_x() - (distance / DIVISOR_SOUTH)); } return eye_north.offcenter_x(); } inline screen_y_t pentagram_shrink_y(eye_flag_t eye, const pixel_t& distance) { enum { TARGET_CENTER_Y = PENTAGRAM_SHRINK_TARGET_CENTER_Y, TARGET_NORTH = static_cast( // sin(PENTAGRAM_ANGLE_INITIAL + (1 × (360° / PENTAGRAM_POINTS))) TARGET_CENTER_Y - (PENTAGRAM_RADIUS_FINAL * 1) ), TARGET_LATERAL = static_cast( // sin(PENTAGRAM_ANGLE_INITIAL + (2 × (360° / PENTAGRAM_POINTS))) TARGET_CENTER_Y + (PENTAGRAM_RADIUS_FINAL * -0.309f) ), TARGET_SOUTH = static_cast( // sin(PENTAGRAM_ANGLE_INITIAL + (3 × (360° / PENTAGRAM_POINTS))) TARGET_CENTER_Y + (PENTAGRAM_RADIUS_FINAL * +0.809f) ), DISTANCE_NORTH = (TARGET_NORTH - (EYE_NORTH_TOP + (EYE_H / 2))), DISTANCE_LATERAL = (TARGET_LATERAL - (EYE_LATERAL_TOP + (EYE_H / 2))), DISTANCE_SOUTH = (TARGET_SOUTH - (EYE_LATERAL_TOP + (EYE_H / 2))), // Integer arithmetic can never be precise for the distances required // here. All these calculations are at least an attempt of deriving // those magic shrink factors, even if we have to cheat by "rounding // up" in the end. DIVISOR_NORTH = (PENTAGRAM_SHRINK_DISTANCE / DISTANCE_NORTH), DIVISOR_LATERAL = ((PENTAGRAM_SHRINK_DISTANCE / DISTANCE_LATERAL) + 1), DIVISOR_SOUTH = ((PENTAGRAM_SHRINK_DISTANCE / DISTANCE_SOUTH) + 1), }; if(eye == EF_WEST) { return (eye_west.iris_top() + (distance / DIVISOR_LATERAL)); } else if(eye == EF_EAST) { return (eye_east.iris_top() + (distance / DIVISOR_LATERAL)); } else if(eye == EF_SOUTHWEST) { return (eye_southwest.iris_top() + (distance / DIVISOR_SOUTH)); } else if(eye == EF_SOUTHEAST) { return (eye_southeast.iris_top() + (distance / DIVISOR_SOUTH)); } return (eye_north.iris_top() + (distance / DIVISOR_NORTH)); } inline void pentagram_shrink_unput( eye_flag_t eye_1, eye_flag_t eye_2, const pixel_t& distance ) { linebullet_unput( pentagram_shrink_x(eye_1, distance), pentagram_shrink_y(eye_1, distance), pentagram_shrink_x(eye_2, distance), pentagram_shrink_y(eye_2, distance) ); } inline void pentagram_shrink_put( eye_flag_t eye_1, eye_flag_t eye_2, const pixel_t& distance ) { linebullet_put_and_hittest( pentagram_shrink_x(eye_1, distance), pentagram_shrink_y(eye_1, distance), pentagram_shrink_x(eye_2, distance), pentagram_shrink_y(eye_2, distance), V_WHITE ); } #define pentagram_shrink(func, distance) { \ func(EF_NORTH, EF_SOUTHWEST, distance); \ func(EF_SOUTHWEST, EF_EAST, distance); \ func(EF_EAST, EF_WEST, distance); \ func(EF_WEST, EF_SOUTHEAST, distance); \ func(EF_SOUTHEAST, EF_NORTH, distance); \ } // ---------------- // Regular pentagram, with spin and slam phases // -------------------------------------------- #define pentagram_corners_set_regular(i, angle_offset) { \ for(i = 0; i < PENTAGRAM_POINTS; i++) { \ pentagram.x[i] = polar_x( \ pentagram.center.x, \ pentagram.radius, \ (angle_offset + (i * (0x100 / PENTAGRAM_POINTS))) \ ); \ pentagram.y[i] = polar_y( \ pentagram.center.y, \ pentagram.radius, \ (angle_offset + (i * (0x100 / PENTAGRAM_POINTS))) \ ); \ } \ } // Not "subphases", as these run independent of [boss_phase] – that one does // in fact loop back at the end of the *_SLAM_INTO_PLAYER_* phases. enum pentagram_attack_phase_t { PAP_PREPARE_1 = 0, PAP_SPIN_CLOCKWISE_1 = 1, PAP_SPIN_COUNTERCLOCKWISE_1 = 2, PAP_SPIN_CLOCKWISE_2 = 3, PAP_SPIN_COUNTERCLOCKWISE_2 = 4, PAP_SPIN_CLOCKWISE_3 = 5, PAP_SLAM_INTO_PLAYER_1 = 6, PAP_PREPARE_2 = 7, PAP_GROW = 8, PAP_SLAM_INTO_PLAYER_2 = 9, _pentagram_attack_phase_t_FORCE_INT16 = 0x7FFF }; #define pentagram_in_slam_phase(phase) ( \ (phase == PAP_SLAM_INTO_PLAYER_1) || (phase == PAP_SLAM_INTO_PLAYER_2) \ ) #define pentagram_in_clockwise_spin_phase(phase) ( \ (phase == PAP_SPIN_CLOCKWISE_1) || \ (phase == PAP_SPIN_CLOCKWISE_2) || \ (phase == PAP_SPIN_CLOCKWISE_3) \ ) #define pentagram_in_counterclockwise_spin_phase(phase) ( \ (phase == PAP_SPIN_COUNTERCLOCKWISE_1) || \ (phase == PAP_SPIN_COUNTERCLOCKWISE_2) \ ) // Also performs collision detection. void pascal near pentagram_regular_unput_update_render( int angle_offset // ACTUAL TYPE: unsigned char ) { int i; pentagram.unput(); pentagram_corners_set_regular(i, angle_offset); pentagram.put_and_hittest(); } // MODDERS: Merge into the other pentagram structure. struct Pentagram { pentagram_attack_phase_t phase; int angle; // ACTUAL TYPE: unsigned char void spin(clock_direction_t dir) { if(dir == CLOCKWISE) { angle += ((boss_phase_frame / 64) + 0x02); } else { angle -= ((boss_phase_frame / 64) + 0x02); } } void unput_update_render_regular(void) const { pentagram_regular_unput_update_render(angle); } }; // ZUN bug: The pentagram is not rendered in the preparation phases, and can // thus easily be unblitted by overlapping sprites, most notably player shots // or HARRY UP pellets. #define pentagram_prepare_update(pentagram_, phase_id) { \ if( \ (boss_phase_frame == SUBPHASE_PREPARE_FRAMES) && \ (pentagram_.phase == phase_id) \ ) { \ pentagram_.phase = static_cast( \ phase_id + 1 \ ); \ boss_phase_frame = 0; \ } \ } // The next two are forced to be used inside an `if` statement, returning a // non-zero value if the phase is done. #define pentagram_spin_unput_update_render(pentagram_, dir) \ (boss_phase_frame % PENTAGRAM_INTERVAL) == 0) { \ pentagram_.spin(dir); \ pentagram_.unput_update_render_regular(); \ } \ if(boss_phase_frame >= SUBPHASE_TIMEOUT_FRAMES #define pentagram_slam_unput_update_render( \ pentagram_, homing_threshold_from_bottom, move_first \ ) \ (boss_phase_frame % PENTAGRAM_INTERVAL) == 0) { \ if(pentagram.center.y < ( \ PLAYFIELD_BOTTOM - (homing_threshold_from_bottom) \ )) { \ pentagram.aim(player_left, player_bottom()); \ } \ pentagram.move(move_first); \ pentagram_.spin(CLOCKWISE); \ pentagram.move(!move_first); \ pentagram_.unput_update_render_regular(); \ } \ if(pentagram.center.y >= (PLAYFIELD_BOTTOM + ((PLAYFIELD_H / 84) * 5)) // -------------------------------------------- /// --------- // Function ordering fails // ----------------------- // Opens and closes the given eyes, and changes the 邪 color in [stage_palette] // (and, via transferring it to [z_Palettes], also the hardware color) by // gradually incrementing and decrementing the given components on 9 out of 10 // frames. Takes ownership of [frame]. To keep the 邪 color unchanged, set // [yokoshima_comp_inc] and [yokoshima_comp_dec] to a value ≥ COMPONENT_COUNT. // // ZUN quirk: While the final [z_Palettes] values are clamped to 0x0 and 0xF, // the [yokoshima_comp_inc] component in the [stage_palette] is *not* clamped // to 0xF after incrementing. This overflow can (and does) affect the final // color on successive recoloring operations. // // MODDERS: Make the component parameters unsigned. void eyes_toggle_and_yokoshima_recolor( eye_flag_t eyes_to_close, eye_flag_t eyes_to_open, int yokoshima_comp_dec, int yokoshima_comp_inc, int& frame ); // ----------------------- // Separate function to work around the `Condition is always true/false` and // `Unreachable code` warnings. (It's unnecessary anyway, though.) inline void conditionally_reset_missiles(bool cond) { if(cond) { Missiles.reset(); } } #define yuugenmagan_defeat_if(cond, reset_missiles, tmp_i) { \ if(cond) { \ mdrv2_bgm_fade_out_nonblock(); \ Pellets.unput_and_reset_nonclouds(); \ conditionally_reset_missiles(reset_missiles); \ \ /* 5? Triply broken, since this fight doesn't even use lasers... */ \ shootout_lasers_unput_and_reset_broken(tmp_i, 5); \ boss_defeat_animate(); \ \ /** \ * ZUN bloat: Already done at the start of REIIDEN.EXE's main(). \ * The REIIDEN.EXE process restarts after the end of a scene \ * anyway, making this load doubly pointless. \ */ \ scene_init_and_load(3); \ } \ } void yuugenmagan_main(void) { const vc_t flash_colors[2] = { 1, 11 }; int i; static int invincibility_frame; static union { int missile_pairs_fired_in_subphase; int subphase_frame; // Always increasing, even when going right-to-left. pixel_t x_edge_offset; pixel_t distance; int unused; } u1; static union { int subphase; int yokoshima_comp_dec; int unused; } u2; static screen_x_t target_left; static pixel_t unused_distance; // Compared to just reusing [invincibility_frame], this "copy" has the // advantage of not being reset every 40 frames, and thus lasting the full // EYE_TOGGLE_FRAMES. static int after_hit_frames; static Pentagram pentagram_; static union { int8_t iterations_done; int8_t yokoshima_comp_inc; eye_flag_t eyes_open; int8_t unused; } u3; static struct { bool initial_hp_rendered; void frame_common(void) { boss_phase_frame++; invincibility_frame++; } void next(int phase_new) { boss_phase = phase_new; boss_phase_frame = 0; invincibility_frame = 0; } void next(int phase_new, int& u2_element_to_reset) { boss_phase = phase_new; u2_element_to_reset = 0; boss_phase_frame = 0; invincibility_frame = 0; } void next( int phase_new, int& u2_element_to_reset, int8_t& u3_element_to_reset, int8_t new_value_for_u3 = 0 ) { boss_phase = phase_new; u2_element_to_reset = 0; u3_element_to_reset = new_value_for_u3; boss_phase_frame = 0; invincibility_frame = 0; } // Respawns the pentagram. void back_from_13_to_10(pentagram_attack_phase_t pentagram_phase_new) { if(pentagram_phase_new == PAP_PREPARE_1) { pentagram_.phase = pentagram_phase_new; } boss_phase_frame = 0; boss_phase = 10; u2.yokoshima_comp_dec = COMPONENT_COUNT; u3.yokoshima_comp_inc = COMPONENT_COUNT; if(pentagram_phase_new == PAP_PREPARE_2) { pentagram_.phase = pentagram_phase_new; } invincibility_frame = 0; u1.unused = 0; pentagram.unput(); } } phase; static union { unsigned char missile_southwest; unsigned char pellet_east; unsigned char tmp; // MODDERS: Turn into a scope-local variable } angle; static unsigned char angle_missile_southeast; static struct { bool16 invincible; void update_and_render(const vc_t (&flash_colors)[2]) { #define hittest(eye) ( \ (eye.hittest_orb() == true) && (eye.image() != C_HIDDEN) \ ) boss_hit_update_and_render( invincibility_frame, invincible, boss_hp, flash_colors, (sizeof(flash_colors) / sizeof(flash_colors[0])), 5000, boss_nop, ( hittest(eye_west) || hittest(eye_east) || hittest(eye_southwest) || hittest(eye_southeast) || hittest(eye_north) ) ); #undef hittest } } hit = { false }; Missiles.unput_update_render(); if(boss_phase == 0) { // Downwards lasers from every eye, in a symmetric sequence from the // left and right edges of the playfield towards the center hud_hp_increment_render( phase.initial_hp_rendered, boss_hp, boss_phase_frame ); boss_phase_frame++; eye_west.locked_unput_and_put_8(); eye_east.locked_unput_and_put_8(); eye_southwest.locked_unput_and_put_8(); eye_southeast.locked_unput_and_put_8(); eye_north.locked_unput_and_put_8(); phase_0_downwards_lasers(); if((boss_phase_frame % 40) == 0) { // Target color: (13, 13, 5) ⇒ #DD5 z_Palettes[COL_YOKOSHIMA].c.r++; // ZUN bloat z_Palettes[COL_YOKOSHIMA].c.g++; // ZUN bloat stage_palette[COL_YOKOSHIMA].c.r++; stage_palette[COL_YOKOSHIMA].c.g++; z_palette_set_all_show(stage_palette); } if(boss_phase_frame == KEYFRAME_LATERAL_OPENING) { phase_0_eyes_open(KEYFRAME_LATERAL_OPENING); // ZUN bloat: Without a call to hit.update_and_render(), any hitbox // manipulation in this phase is doubly redundant... eye_west.hitbox_orb_activate(); eye_east.hitbox_orb_activate(); } else if(boss_phase_frame == KEYFRAME_SOUTH_OPENING) { phase_0_eyes_open(KEYFRAME_SOUTH_OPENING); eye_southwest.hitbox_orb_activate(); eye_southeast.hitbox_orb_activate(); } else if(boss_phase_frame == KEYFRAME_NORTH_OPENING) { phase_0_eyes_open(KEYFRAME_NORTH_OPENING); eye_north.hitbox_orb_activate(); } else if(boss_phase_frame == KEYFRAME_LATERAL_OPEN) { phase_0_eyes_open(KEYFRAME_LATERAL_OPEN); } else if(boss_phase_frame == KEYFRAME_SOUTH_OPEN) { phase_0_eyes_open(KEYFRAME_SOUTH_OPEN); } else if(boss_phase_frame == KEYFRAME_NORTH_OPEN) { phase_0_eyes_open(KEYFRAME_NORTH_OPEN); } else if(boss_phase_frame == KEYFRAME_CLOSING) { phase_0_eyes_close(KEYFRAME_CLOSING); eye_west.hitbox_orb_deactivate(); eye_east.hitbox_orb_deactivate(); eye_southwest.hitbox_orb_deactivate(); eye_southeast.hitbox_orb_deactivate(); eye_north.hitbox_orb_deactivate(); // ZUN quirk: Did you mean ">= RANK_HARD"? Because... if(rank == RANK_HARD) { phase_0_fire(eye_north); phase_0_fire(eye_southeast); phase_0_fire(eye_southwest); // ... this condition can now never be true, making the Lunatic // version of this subpattern effectively unused. if(rank == RANK_LUNATIC) { phase_0_fire(eye_west); phase_0_fire(eye_east); } } } else if(boss_phase_frame == KEYFRAME_CLOSED) { phase_0_eyes_close(KEYFRAME_CLOSED); if(rank == RANK_LUNATIC) { phase_0_fire(eye_north); phase_0_fire(eye_southeast); phase_0_fire(eye_southwest); phase_0_fire(eye_west); phase_0_fire(eye_east); } } else if(boss_phase_frame == KEYFRAME_HIDDEN) { phase_0_eyes_close(KEYFRAME_HIDDEN); } else if(boss_phase_frame == KEYFRAME_LATERAL_LASER_DONE) { eye_west.set_image(C_CLOSED); eye_east.set_image(C_CLOSED); eye_west.downwards_laser_unput(); eye_east.downwards_laser_unput(); } else if(boss_phase_frame == KEYFRAME_SOUTH_LASER_DONE) { eye_west.set_image(C_HALFOPEN); eye_east.set_image(C_HALFOPEN); eye_west.hitbox_orb_activate(); eye_east.hitbox_orb_activate(); eye_southwest.downwards_laser_unput(); eye_southeast.downwards_laser_unput(); } else if(boss_phase_frame > KEYFRAME_NORTH_LASER_DONE) { boss_phase = 1; hit.invincible = false; boss_phase_frame = 0; z_palette_set_all_show(stage_palette); boss_palette_snap(); phase.initial_hp_rendered = false; // ZUN bloat // ZUN bloat: These won't be visible at all, since we get here on // the second frame with the entity lock active. On the next frame, // these two eyes will immediately adjust themselves to the // player's tracked position. eye_west.set_image(C_LEFT); eye_east.set_image(C_DOWN); pattern_interval = select_for_rank(350, 300, 200, 130); eye_north.downwards_laser_unput(); } } else if(boss_phase == 1) { // Slow pellets from the lateral eyes phase.frame_common(); eyes_locked_unput_and_put_then_track(EF_WEST | EF_EAST); phase_1_pellets_from_lateral(); hit.update_and_render(flash_colors); if((boss_hp <= HP_PHASE_1_END) || (boss_phase_frame > 1100)) { phase.next(2); } } else if(boss_phase == 2) { // (13, 13, 5) → (0, 13, 68) ⇒ #0DF (cyan) eyes_toggle_and_yokoshima_recolor( (EF_WEST | EF_EAST), (EF_SOUTHWEST | EF_SOUTHEAST), COMPONENT_R, COMPONENT_B, boss_phase_frame ); if(boss_phase_frame >= EYE_TOGGLE_FRAMES) { phase.next(3, u2.subphase, u3.iterations_done); u1.missile_pairs_fired_in_subphase = 0; z_palette_set_all_show(stage_palette); boss_palette_snap(); pattern_interval = select_for_rank(8, 12, 16, 20); // (unused) } } else if(boss_phase == 3) { // Missiles from the southern eyes, whose angles first shift away from // Reimu's tracked position and then towards it phase.frame_common(); pattern_missile_pairs_from_south( u2.subphase, u1.missile_pairs_fired_in_subphase, target_left, angle.missile_southwest, angle_missile_southeast, u3.iterations_done, 3, missile_pairs_shift_angle_1_away, missile_pairs_shift_angle_2_towards ); hit.update_and_render(flash_colors); if((boss_hp <= HP_PHASE_3_END) || (u3.iterations_done >= 5)) { phase.next(4); } } else if(boss_phase == 4) { // (0, 13, 68) → (63, 0, 68) ⇒ #F0F (magenta) eyes_toggle_and_yokoshima_recolor( (EF_SOUTHWEST | EF_SOUTHEAST), (EF_WEST | EF_EAST), COMPONENT_G, COMPONENT_R, boss_phase_frame ); if(boss_phase_frame >= EYE_TOGGLE_FRAMES) { phase.next(5, u2.subphase, u3.iterations_done); u1.subphase_frame = 0; z_palette_set_all_show(stage_palette); boss_palette_snap(); unused_distance = (player_bottom() - eye_west.cur_top); pattern_interval = select_for_rank(12, 8, 4, 2); angle.pellet_east = 0x00; } } else if(boss_phase == 5) { // Circular pellets sprayed from the lateral eyes enum phase_5_subphase_t { P5_PREPARE_1 = 0, P5_SPRAY_1 = 1, P5_PREPARE_2 = 2, P5_SPRAY_2 = 3, }; phase.frame_common(); // ZUN bloat: Redundant and confusing. Just use the regular // [boss_phase_frame] exclusively? u1.subphase_frame++; angle.pellet_east--; eyes_locked_unput_and_put_then_track(EF_WEST | EF_EAST); if( (boss_phase_frame == SUBPHASE_PREPARE_FRAMES) && (u2.subphase == P5_PREPARE_1) ) { u1.subphase_frame = 0; u2.subphase = P5_SPRAY_1; } if(u2.subphase == P5_SPRAY_1) { if((u1.subphase_frame % pattern_interval) == 0) { eye_west.fire_from_bottom_center( true, angle.pellet_east, 4.0f ); eye_east.fire_from_bottom_center( false, angle.pellet_east, 4.0f ); } if((u1.subphase_frame / 5) >= 36) { u2.subphase = P5_PREPARE_2; boss_phase_frame = 0; } } if( (u2.subphase == P5_PREPARE_2) && (boss_phase_frame > (SUBPHASE_PREPARE_FRAMES / 2)) ) { u1.subphase_frame = -15; u2.subphase = P5_SPRAY_2; angle.pellet_east = 0x80; } if(u2.subphase == P5_SPRAY_2) { if((u1.subphase_frame % pattern_interval) == 0) { // Since these pellets don't reach the top of the playfield // before this subphase ends, they don't actually move // differently compared to the first subphase. eye_west.fire_from_bottom_center( true, angle.pellet_east, 4.0f, PM_FALL_STRAIGHT_FROM_TOP_THEN_REGULAR, 4.0f ); eye_east.fire_from_bottom_center( false, angle.pellet_east, 4.0f, PM_FALL_STRAIGHT_FROM_TOP_THEN_REGULAR, 4.0f ); } if((u1.subphase_frame / 5) >= 20) { u2.subphase = P5_PREPARE_1; boss_phase_frame = 0; u3.iterations_done++; angle.pellet_east = 0x00; } } hit.update_and_render(flash_colors); if((boss_hp <= HP_PHASE_5_END) || (u3.iterations_done > 4)) { phase.next(6); } } else if(boss_phase == 6) { // (63, 0, 68) → (0, 0, 68) ⇒ #00F (blue) eyes_toggle_and_yokoshima_recolor( (EF_WEST | EF_EAST), (EF_SOUTHWEST | EF_SOUTHEAST), COMPONENT_R, COMPONENT_COUNT, boss_phase_frame ); if(boss_phase_frame >= EYE_TOGGLE_FRAMES) { phase.next(7, u2.subphase, u3.iterations_done); u1.missile_pairs_fired_in_subphase = 0; pentagram_.angle = 0x00; // ZUN bloat z_palette_set_all_show(stage_palette); boss_palette_snap(); pattern_interval = select_for_rank(10, 16, 20, 24); // (unused) } } else if(boss_phase == 7) { // Missiles from the southern eyes, with (counter)clockwise angle shifts phase.frame_common(); pattern_missile_pairs_from_south( u2.subphase, u1.missile_pairs_fired_in_subphase, target_left, angle.missile_southwest, angle_missile_southeast, u3.iterations_done, 2, missile_pairs_shift_angle_1_clock, missile_pairs_shift_angle_2_clock ); hit.update_and_render(flash_colors); if((boss_hp <= HP_PHASE_7_END) || (u3.iterations_done > 4)) { phase.next(8); } } else if(boss_phase == 8) { // (0, 0, 68) → (0, 63, 5) ⇒ #0F5 (green, via cyan) eyes_toggle_and_yokoshima_recolor( (EF_SOUTHWEST | EF_SOUTHEAST), EF_NORTH, COMPONENT_B, COMPONENT_G, boss_phase_frame ); if(boss_phase_frame >= EYE_TOGGLE_FRAMES) { phase.next(9, u2.subphase); u1.x_edge_offset = 0; pentagram_.angle = 0x00; z_palette_set_all_show(stage_palette); boss_palette_snap(); } } else if(boss_phase == 9) { // 3-pixel 3-laser sequence from the northern eye phase.frame_common(); laser_eye.locked_unput_and_put_8(); if((boss_phase_frame >= 30) && ( (u2.subphase == laser_subphase(0, LSP_PREPARE)) || (u2.subphase == laser_subphase(1, LSP_PREPARE)) || (u2.subphase == laser_subphase(2, LSP_PREPARE)) )) { laser_eye.set_image(C_CLOSED); } if(laser_pattern_should_run(u2.subphase, 0)) { pattern_single_laser_across_playfield( u2.subphase, pentagram_.angle, u1.x_edge_offset, 0, X_RIGHT ); } else if(laser_pattern_should_run(u2.subphase, 1)) { pattern_single_laser_across_playfield( u2.subphase, pentagram_.angle, u1.x_edge_offset, 1, X_LEFT ); } else if(laser_pattern_should_run(u2.subphase, 2)) { pattern_dual_lasers_across_playfield( u2.subphase, pentagram_.angle, u1.x_edge_offset, 2 ); } hit.update_and_render(flash_colors); if( (boss_hp <= HP_PHASE_9_END) || (u2.subphase == laser_subphase(2, LSP_DONE)) ) { // ZUN bug: We can get here with a laser still on screen, // particularly when holding ↵ Return in debug mode. There should // at least be an unblitting call here. boss_phase = 10; boss_phase_frame = 0; invincibility_frame = 0; u2.yokoshima_comp_dec = COMPONENT_G; u3.yokoshima_comp_inc = COMPONENT_R; // Must be done here, as phase 13 loops back into phase 10 with // this phase variable set to PAP_PREPARE_2. pentagram_.phase = PAP_PREPARE_1; } } else if(boss_phase == 10) { // Prepare pentagram spawning by opening all eyes // First iteration: (0, 63, 5) → (63, 0, 5) ⇒ #F05 (red, via yellow) // Later iterations don't change the color; see back_from_13_to_10(). eyes_toggle_and_yokoshima_recolor( EF_NONE, (EF_WEST | EF_EAST | EF_SOUTHWEST | EF_SOUTHEAST | EF_NORTH), u2.yokoshima_comp_dec, u3.yokoshima_comp_inc, boss_phase_frame ); if(boss_phase_frame >= EYE_TOGGLE_FRAMES) { phase.next(11, u2.subphase, u3.unused, 31 /* ZUN bloat */); u1.distance = 0; pentagram_.angle = 0x00; // Finally not redundant. z_palette_set_all_show(stage_palette); boss_palette_snap(); } } else if(boss_phase == 11) { // Spawns the pentagram with one corner out of every eye, then // gradually shrinks and moves it towards the center of the playfield enum phase_11_subphase_t { P11_PREPARE = 0, P11_CORNERS_IN_EYES = 1, P11_MOVE = 2, P11_DONE = 3, }; phase.frame_common(); eyes_locked_unput_and_put_then_track( EF_WEST | EF_EAST | EF_SOUTHWEST | EF_SOUTHEAST | EF_NORTH ); if( (boss_phase_frame == SUBPHASE_PREPARE_FRAMES) || (u2.subphase == P11_CORNERS_IN_EYES) ) { if(u2.subphase == P11_PREPARE) { u2.subphase = P11_CORNERS_IN_EYES; target_left = player_left; // unused } // Every frame, for a change? No unblitting glitches here, then. :P pentagram_between_eyes_put(); if(boss_phase_frame == 140) { u2.subphase = P11_MOVE; u1.distance = 0; } } if( (u2.subphase == P11_MOVE) && ((boss_phase_frame % PENTAGRAM_INTERVAL) == 1) ) { // ZUN bug: Doing this after blitting the eyes is guaranteed to // rip holes into them for at least one frame. pentagram_shrink(pentagram_shrink_unput, u1.distance); u1.distance += (PENTAGRAM_SHRINK_DISTANCE / 20); pentagram_shrink(pentagram_shrink_put, u1.distance); // ZUN quirk: Should be >=, and ideally even be clamped to exactly // that value. With >, the shrink animation lasts for one step // longer than it should, leaving the pentagram both smaller and // moved slightly off-center. if(u1.distance > PENTAGRAM_SHRINK_DISTANCE) { // ZUN quirk: This uses offcenter_x(), which makes sure that // the regular pentagram will stay off-center even *if* the // above condition were corrected. pentagram.x[0] = pentagram_shrink_x(EF_NORTH, u1.distance); pentagram.y[0] = pentagram_shrink_y(EF_NORTH, u1.distance); // ZUN bloat: All further coordinate assignments are unused and // can be deleted. The values are never read in this phase, and // phase 12 directly assigns regular polygon coordinates before // rendering. // Correct ones, too: This seems to be a relic from a time when // ZUN stored pentagram corner coordinates in interleaved line // order (north → southwest → east → west → southeast) rather // than in (counter-)clockwise angle order. Therefore, these // wouldn't even result in a pentagram with the rendering // function that ended up in the final game. pentagram.x[1] = pentagram_shrink_x(EF_SOUTHWEST, u1.distance); pentagram.y[1] = pentagram_shrink_y(EF_SOUTHWEST, u1.distance); pentagram.x[2] = pentagram_shrink_x(EF_EAST, u1.distance); pentagram.y[2] = pentagram_shrink_y(EF_EAST, u1.distance); pentagram.x[3] = pentagram_shrink_x(EF_WEST, u1.distance); pentagram.y[3] = pentagram_shrink_y(EF_WEST, u1.distance); pentagram.x[4] = pentagram_shrink_x(EF_SOUTHEAST, u1.distance); pentagram.y[4] = pentagram_shrink_y(EF_SOUTHEAST, u1.distance); u2.subphase = P11_DONE; } } hit.update_and_render(flash_colors); yuugenmagan_defeat_if((boss_hp <= HP_PHASE_13_END), false, i); if( ((boss_hp <= HP_PHASE_13_END) || (u2.subphase == P11_DONE)) && !hit.invincible ) { phase.next(12, u2.unused, u3.eyes_open, EF_WEST); u1.unused = 0; pentagram.center.y = (pentagram.y[0] + PENTAGRAM_RADIUS_FINAL); pentagram.center.x = pentagram.x[0]; pentagram.radius = PENTAGRAM_RADIUS_FINAL; // ZUN bug: This might look redundant, but it sets the coordinates // for the first unblitting call in phase 12. Which are wrong, // because PENTAGRAM_ANGLE_INITIAL is different. pentagram_corners_set_regular(i, 0x00); // Work around the inaccuracies of 8-bit angles and make sure that // at least the lateral corners perfectly line up vertically. pentagram.y[1] = pentagram.y[4]; // Used in phase 13. pattern_interval = select_for_rank(24, 14, 10, 8); } } else if(boss_phase == 12) { // Moves the pentagram on top of the kimono figure enum { // Fixing the 8 pixels we've overshot above. In fact, the whole // reason for overshooting in the first place might have been to // ensure that *this* distance cleanly divides by 3?! DISTANCE_Y = (( PENTAGRAM_SHRINK_TARGET_CENTER_Y - PENTAGRAM_REGULAR_CENTER_Y ) + 8), STEPS = (EYE_TOGGLE_FRAMES / PENTAGRAM_INTERVAL), Y_STEP = (DISTANCE_Y / STEPS), }; // ZUN bloat: [boss_phase_frame] would have worked as well, and saved // the addition. invincibility_frame++; // ZUN quirk: Stays on red for the first iteration of the phase, due to // merely going // // (63, 0, 5) → (126, 0, 5) ⇒ #F05 // // On the second iteration though, the same addition causes the red // component to overflow into negative numbers, which are clamped to // zero: // // (126, 0, 5) → (-67, 0, 5) ⇒ #005 (dark blue) // // The apparent pattern then repeats, leading to the color alternating // between red and dark blue on every 2.03 iterations if the player // manages to stall the fight long enough. eyes_toggle_and_yokoshima_recolor( (EF_EAST | EF_SOUTHWEST | EF_SOUTHEAST | EF_NORTH), EF_WEST, COMPONENT_G, COMPONENT_R, boss_phase_frame ); if((invincibility_frame % PENTAGRAM_INTERVAL) == 0) { // ZUN bug: Missing an unblitting call for the final appearance of // the shrunk pentagram from the previous phase. This would // require: // • storing the shrink animation coordinates inside the structure // rather than recalculating them for both unblitting and // blitting operations, // • and not overwriting them with wrong ones at the end of the // previous phase. pentagram.center.y -= Y_STEP; pentagram_regular_unput_update_render(PENTAGRAM_ANGLE_INITIAL); } if(boss_phase_frame >= EYE_TOGGLE_FRAMES) { phase.next(13, u2.unused, u3.eyes_open, EF_WEST); u1.unused = 0; // ZUN quirk: Should *maybe* have been PENTAGRAM_ANGLE_INITIAL to // achieve a seamless shift between moving up and spinning. Would // change the final pattern, though. pentagram_.angle = 0x00; after_hit_frames = 0; z_palette_set_all_show(stage_palette); boss_palette_snap(); } } else if(boss_phase == 13) { enum { GROW_RADIUS = (SUBPHASE_TIMEOUT_FRAMES / PENTAGRAM_INTERVAL), GROWN_RADIUS = (PENTAGRAM_RADIUS_FINAL + GROW_RADIUS), }; #define eye_is_open(bit) ( \ u3.eyes_open & bit \ ) // ZUN bloat: Just a more pedantic version of the one above. #define eye_is_open_pedantic(bit) ( \ eye_is_open(bit) == bit \ ) phase.frame_common(); u1.unused++; eyes_foreach_if(eye_is_open_pedantic, locked_unput_and_put_8); if(hit.invincible && (after_hit_frames == 0)) { u3.eyes_open <<= 1; after_hit_frames = 1; if(u3.eyes_open == EF_RESET) { u3.eyes_open = EF_WEST; } } else if(after_hit_frames > 0) { eyes_toggle_and_yokoshima_recolor( (u3.eyes_open == EF_WEST) ? EF_NORTH : (u3.eyes_open >> 1), u3.eyes_open, COMPONENT_COUNT, COMPONENT_COUNT, after_hit_frames ); if(after_hit_frames >= EYE_TOGGLE_FRAMES) { after_hit_frames = 0; u1.unused = 0; } } else { eyes_foreach_if(eye_is_open, track_player); } if( (u2.unused == 0) && ((boss_phase_frame % pattern_interval) == 0) && !pentagram_in_slam_phase(pentagram_.phase) ) { for(i = 0; i < PENTAGRAM_POINTS; i++) { angle.tmp = iatan2( (pentagram.y[i] - pentagram.center.y), (pentagram.x[i] - pentagram.center.x) ); Pellets.add_single( pentagram.x[i], pentagram.y[i], angle.tmp, to_sp(3.0f) ); } } pentagram_prepare_update(pentagram_, PAP_PREPARE_1); pentagram_prepare_update(pentagram_, PAP_PREPARE_2); if(pentagram_in_clockwise_spin_phase(pentagram_.phase)) { if(pentagram_spin_unput_update_render(pentagram_, CLOCKWISE)) { reinterpret_cast(pentagram_.phase)++; boss_phase_frame = 0; } } if(pentagram_in_counterclockwise_spin_phase(pentagram_.phase)) { if(pentagram_spin_unput_update_render( pentagram_, COUNTERCLOCKWISE )) { reinterpret_cast(pentagram_.phase)++; boss_phase_frame = 0; pentagram.aim(player_left, player_bottom()); } } else if(pentagram_.phase == PAP_SLAM_INTO_PLAYER_1) { if(pentagram_slam_unput_update_render( pentagram_, (PLAYER_H + 4 + PENTAGRAM_RADIUS_FINAL), false )) { phase.back_from_13_to_10(PAP_PREPARE_2); } } else if(pentagram_.phase == PAP_GROW) { if((boss_phase_frame % PENTAGRAM_INTERVAL) == 0) { pentagram.radius++; pentagram_.spin(CLOCKWISE); pentagram_.unput_update_render_regular(); } if(boss_phase_frame >= SUBPHASE_TIMEOUT_FRAMES) { pentagram_.phase = PAP_SLAM_INTO_PLAYER_2; boss_phase_frame = 0; pentagram.aim(player_left, player_bottom()); } } if(pentagram_.phase == PAP_SLAM_INTO_PLAYER_2) { if(pentagram_slam_unput_update_render( pentagram_, (GROWN_RADIUS + (PLAYER_H - 6)), true )) { phase.back_from_13_to_10(PAP_PREPARE_1); } } hit.update_and_render(flash_colors); yuugenmagan_defeat_if((boss_hp <= HP_PHASE_13_END), true, i); #undef eye_is_open_pedantic #undef eye_is_open } } void eyes_toggle_and_yokoshima_recolor( eye_flag_t eyes_to_close, eye_flag_t eyes_to_open, int yokoshima_comp_dec, int yokoshima_comp_inc, int& frame ) { #define eye_should_be_closed(bit) ( \ eyes_to_close & bit \ ) #define eye_should_be_opened(bit) ( \ eyes_to_open & bit \ ) #define eye_is_toggled(bit) ( \ ((eyes_to_close & bit) == bit) || ((eyes_to_open & bit) == bit) \ ) eyes_foreach_if(eye_is_toggled, locked_unput_and_put_8); frame++; // ZUN quirk: That's a color change on every frame *except* the 10th one... if((frame % 10) != 0) { if(yokoshima_comp_dec < COMPONENT_COUNT) { // This clamping condition is actually rather important to keep // [stage_palette] from running into negative numbers. if(z_Palettes[COL_YOKOSHIMA].v[yokoshima_comp_dec] > 0) { z_Palettes[COL_YOKOSHIMA].v[yokoshima_comp_dec]--; // ZUN bloat stage_palette[COL_YOKOSHIMA].v[yokoshima_comp_dec]--; } } if(yokoshima_comp_inc < COMPONENT_COUNT) { // ZUN quirk: No equivalent clamping condition here. The final // color in [z_Palettes] is clamped to 0xF, but there's nothing to // prevent the [stage_palette] version from running past 0xF. // If a component that was previously incremented like this is then // decremented in subsequent recoloring operations, this overflow // manifests itself as a noticeable delay: The component will spend // most of the recoloring time above 0xF, and only fall back into // the valid color range at the *end* of the recoloring phase. If // the previously component additionally started out with a non-0x0 // value (which is the case for the blue component in the original // game), decrementing it for a constant amount of frames can only // ever reach that brighter initial value again. z_Palettes[COL_YOKOSHIMA].v[yokoshima_comp_inc]++; // ZUN bloat stage_palette[COL_YOKOSHIMA].v[yokoshima_comp_inc]++; } // ZUN bloat: z_palette_set_show(COL_YOKOSHIMA) is enough. z_palette_set_all_show(stage_palette); } // Matches the animation in phase_0_eye_open(). if(frame == ((EYE_OPENING_FRAMES / 3) * 1)) { eyes_set_image(eyes_to_close, C_HALFOPEN); eyes_set_image(eyes_to_open, C_CLOSED); eyes_foreach_if(eye_should_be_opened, hitbox_orb_activate); } else if(frame == ((EYE_OPENING_FRAMES / 3) * 2)) { eyes_set_image(eyes_to_close, C_CLOSED); eyes_set_image(eyes_to_open, C_HALFOPEN); eyes_foreach_if(eye_should_be_closed, hitbox_orb_deactivate); } else if(frame == ((EYE_OPENING_FRAMES / 3) * 3)) { eyes_set_image(eyes_to_close, C_HIDDEN); eyes_set_image(eyes_to_open, C_AHEAD); } #undef eye_should_be_closed #undef eye_should_be_opened #undef eye_is_toggled }