/// Constants /// --------- // Can't declare these as `static const` variables, because that would break // compile-time Subpixel arithmetic #define PELLET_LEFT_MIN (PLAYFIELD_LEFT) #define PELLET_LEFT_MAX (PLAYFIELD_RIGHT - PELLET_W) #define PELLET_TOP_MIN (PLAYFIELD_TOP) #define PELLET_TOP_MAX (PLAYFIELD_BOTTOM) #define PELLET_BOUNCE_TOP_MIN (PLAYFIELD_TOP + ((PELLET_H / 4) * 3)) #define PELLET_VELOCITY_MAX to_sp(8.0f) static const unsigned int PELLET_DESTROY_SCORE = 10; static const int PELLET_DECAY_FRAMES = 20; static const int PELLET_DECAY_CELS = 2; /// --------- /// Globals /// ------- pellet_t near *p; bool pellet_interlace = false; unsigned int pellet_destroy_score_delta = 0; #include "th01/sprites/pellet.csp" /// ------- CPellets::CPellets(void) { int i; p = iteration_start(); for(i = 0; i < PELLET_COUNT; i++, p++) { p->moving = false; p->not_rendered = false; } alive_count = 0; for(i = 0; i < sizeof(unknown_zero) / sizeof(unknown_zero[0]); i++) { unknown_zero[i] = 0; } } void vector2_to_player_from( screen_x_t x, screen_y_t y, subpixel_t &ret_x, subpixel_t &ret_y, subpixel_t length, unsigned char plus_angle ) { plus_angle = iatan2( (player_center_y - y), ((player_left + (PLAYER_W / 2) - (PELLET_W / 2)) - x) ) + plus_angle; ret_x = polar_x(0, length, plus_angle); ret_y = polar_y(0, length, plus_angle); } // Sets the velocity for pellet #[i] in the given [pattern]. Returns true if // this was the last pellet for this pattern. bool16 pattern_velocity_set( Subpixel &ret_x, Subpixel &ret_y, pellet_pattern_t pattern, subpixel_t speed, int &i, screen_x_t pellet_left, screen_y_t pellet_top ) { // ZUN bug: Due to this default, add_pattern() ends up repeatedly calling // this function for [pattern] values not covered by the switch below, // until it iterated over the entire pellet array... bool16 done = false; unsigned char angle = 0x00; unsigned char spread_delta = 0x00; #define to_aim_or_not_to_aim() \ if(pattern >= PP_AIMED_SPREADS) { \ goto aim; \ } \ /* Static pattern; add a 90° angle, so that 0° points downwards */ \ angle += 0x40; \ goto no_aim; switch(pattern) { case PP_1: ret_y.v = speed; ret_x.set(0.0f); done = true; break; case PP_1_AIMED: vector2_between( pellet_left, pellet_top, player_left + 8, player_center_y, ret_x.v, ret_y.v, speed ); done = true; break; case PP_2_SPREAD_WIDE: case PP_2_SPREAD_WIDE_AIMED: spread_delta = 0x08; // fallthrough case PP_2_SPREAD_NARROW: case PP_2_SPREAD_NARROW_AIMED: /**/ if(i == 0) { angle = (+0x04 + spread_delta); } else if(i == 1) { angle = (-0x04 - spread_delta); done = true; } to_aim_or_not_to_aim(); case PP_3_SPREAD_WIDE: case PP_3_SPREAD_WIDE_AIMED: spread_delta = 0x05; // fallthrough case PP_3_SPREAD_NARROW: case PP_3_SPREAD_NARROW_AIMED: /**/ if(i == 0) { angle = 0x00; } else if(i == 1) { angle = (+0x04 + spread_delta); } else if(i == 2) { angle = (-0x04 - spread_delta); done = true; } to_aim_or_not_to_aim(); case PP_4_SPREAD_WIDE: case PP_4_SPREAD_WIDE_AIMED: spread_delta = 0x04; // fallthrough case PP_4_SPREAD_NARROW: case PP_4_SPREAD_NARROW_AIMED: /**/ if(i == 0) { angle = (+0x04 + spread_delta); } else if(i == 1) { angle = (-0x04 - spread_delta); } else if(i == 2) { angle = (+0x0C + (spread_delta * 3)); } else if(i == 3) { angle = (-0x0C - (spread_delta * 3)); done = true; } to_aim_or_not_to_aim(); case PP_5_SPREAD_WIDE: case PP_5_SPREAD_WIDE_AIMED: spread_delta = 0x04; // fallthrough case PP_5_SPREAD_NARROW: case PP_5_SPREAD_NARROW_AIMED: /**/ if(i == 0) { angle = 0x00; } else if(i == 1) { angle = (+0x04 + spread_delta); } else if(i == 2) { angle = (-0x04 - spread_delta); } else if(i == 3) { angle = (+0x08 + (spread_delta * 2)); } else if(i == 4) { angle = (-0x08 - (spread_delta * 2)); done = true; } to_aim_or_not_to_aim(); case PP_1_RANDOM_NARROW_AIMED: angle = ((rand() & 0x0F) - 0x07); done = true; goto aim; case PP_1_RANDOM_WIDE: angle = ((rand() & 0x3F) + 0x20); done = true; goto no_aim; aim: vector2_to_player_from( pellet_left, pellet_top, ret_x.v, ret_y.v, speed, angle ); break; no_aim: vector2(ret_x.v, ret_y.v, speed, angle); break; } i++; return done; } inline subpixel_t base_speed_for_rank(void) { return (static_cast(rank) == RANK_EASY) ? to_sp(0.0f) : (static_cast(rank) == RANK_NORMAL) ? to_sp(0.375f) : (static_cast(rank) == RANK_HARD) ? to_sp(0.75f) : to_sp(1.125f); } #define speed_set(speed) \ speed += base_speed_for_rank(); \ speed += ((resident->pellet_speed * speed) / to_sp(2.5f)); \ if(speed < to_sp(1.0f)) { \ speed = to_sp(1.0f); \ } #define pellet_init(pellet, left, top, pattern) \ pellet->decay_frame = 0; \ pellet->cur_left.v = TO_SP(left); \ pellet->cur_top.v = TO_SP(top); \ pellet->cloud_left = left; \ pellet->cloud_top = top; \ if(spawn_with_cloud) { \ pellet->cloud_frame = 1; \ } else { \ pellet->moving = true; \ } \ pellet->from_pattern = pattern; void CPellets::add_pattern( screen_x_t left, screen_y_t top, pellet_pattern_t pattern, subpixel_t speed ) { int i; int pattern_i = 0; int pattern_done; Subpixel vel_x; Subpixel vel_y; // Should be >=, but yeah, it's just an inconsequential oversight. if(alive_count > PELLET_COUNT) { return; } if( (left >= PLAYFIELD_RIGHT) || (top < (PLAYFIELD_TOP - PELLET_H)) || (left < (PLAYFIELD_LEFT - PELLET_W)) || (top > PLAYFIELD_BOTTOM) ) { return; } speed_set(speed); p = iteration_start(); for(i = 0; i < PELLET_COUNT; i++, p++) { if(p->moving == true) { continue; } if(p->cloud_frame) { continue; } pellet_init(p, left, top, pattern); p->prev_left.v = -1; p->age = 0; alive_count++; pattern_done = pattern_velocity_set( vel_x, vel_y, pattern, speed, pattern_i, left, top ); p->velocity.x.v = vel_x.v; p->velocity.y.v = vel_y.v; if(pattern_done == true) { return; } } } void CPellets::add_single( screen_x_t left, screen_y_t top, int angle, subpixel_t speed_base, pellet_motion_t motion_type, subpixel_t speed_for_motion_fixed, screen_x_t spin_center_x, screen_y_t spin_center_y ) { int i; Subpixel vel_x; Subpixel vel_y; // Should be >=, but yeah, it's just an inconsequential oversight. if(alive_count > PELLET_COUNT) { return; } speed_set(speed_base); p = iteration_start(); for(i = 0; i < PELLET_COUNT; i++, p++) { if(p->moving == true) { continue; } if(p->cloud_frame) { continue; } pellet_init(p, left, top, PP_NONE); p->motion_type = motion_type; p->prev_left.v = -1; p->age = 0; alive_count++; p->spin_center.x.v = TO_SP(spin_center_x); p->spin_center.y.v = TO_SP(spin_center_y); if(motion_type == PM_SPIN) { vector2(vel_x.v, vel_y.v, speed_for_motion_fixed, angle); p->spin_velocity.x.v = vel_x.v; p->spin_velocity.y.v = vel_y.v; p->angle = iatan2( (p->cur_top - p->spin_center.y), (p->cur_left - p->spin_center.x) ); } vector2(vel_x.v, vel_y.v, speed_base, angle); p->speed.v = speed_for_motion_fixed; p->velocity.x.v = vel_x.v; p->velocity.y.v = vel_y.v; return; } } void CPellets::motion_type_apply_for_cur(void) { Subpixel velocity_to_player_x; Subpixel velocity_to_player_y; switch(p->motion_type) { case PM_GRAVITY: p->velocity.y.v += p->speed.v; break; case PM_SLING_AIMED: if(p->sling_direction == PSD_NONE) { p->sling_direction = static_cast( (rand() & 1) + PSD_CLOCKWISE ); } if(p->sling_direction == PSD_CLOCKWISE) { vector2(p->velocity.x.v, p->velocity.y.v, p->speed.v, p->angle); } else /* PSD_COUNTERCLOCKWISE */ { vector2(p->velocity.y.v, p->velocity.x.v, p->speed.v, p->angle); } p->angle += PELLET_SLING_DELTA_ANGLE; if(p->angle > 0x100) { vector2_between( p->cur_left.to_pixel(), p->cur_top.to_pixel(), player_left + 8, player_center_y, p->velocity.x.v, p->velocity.y.v, p->speed.v ); p->from_pattern = PP_1_AIMED; p->angle = 0x00; } break; case PM_BOUNCE_FROM_TOP_THEN_GRAVITY: // Wow... Three ZUN bugs in one single if() expression. // 1) // 2) Pellets are clipped at both the left (1) and the right (2) // edge of the playfield at those exact same coordinates, // *before* this code gets to run. Thus, they won't ever // bounce from those two sides here. // 3) Comparing screen-space coordinates to subpixels. What's a // type system? :zunpet: if( (p->cur_left.v <= PELLET_LEFT_MIN) || (p->cur_left.to_pixel() >= to_sp(PELLET_LEFT_MAX)) || (p->cur_top.v <= to_sp(PELLET_BOUNCE_TOP_MIN)) ) { p->velocity.x.v = -p->velocity.x.v; p->velocity.y.set(0.0f); p->motion_type = PM_GRAVITY; // Nope, this doesn't help. if(p->cur_left.v <= to_sp(PELLET_LEFT_MIN)) { p->cur_left.set(PELLET_LEFT_MIN + 1.0f); } if(p->cur_left.v >= to_sp(PELLET_LEFT_MAX)) { p->cur_left.set(PELLET_LEFT_MAX - 1.0f); } if(p->cur_top.v <= to_sp(PELLET_TOP_MIN)) { p->cur_top.set(PELLET_TOP_MIN + 1.0f); } } break; case PM_FALL_STRAIGHT_FROM_TOP_THEN_NORMAL: if(p->cur_top.to_pixel() <= PELLET_BOUNCE_TOP_MIN) { p->velocity.x.set(0.0f); p->velocity.y.v = p->speed.v; p->motion_type = PM_NORMAL; if(p->cur_top.to_pixel() <= PLAYFIELD_TOP) { p->cur_top.set(PLAYFIELD_TOP + 1.0f); } } break; case PM_SPIN: p->cur_left.v = polar_x( p->spin_center.x.v, PELLET_SPIN_CIRCLE_RADIUS, p->angle ); p->cur_top.v = polar_y( p->spin_center.y.v, PELLET_SPIN_CIRCLE_RADIUS, p->angle ); p->spin_center.x.v += p->spin_velocity.x.v; p->spin_center.y.v += p->spin_velocity.y.v; p->velocity.set(0.0f, 0.0f); p->angle += PELLET_SPIN_DELTA_ANGLE; break; case PM_CHASE: vector2_between( p->cur_left.to_pixel(), p->cur_top.to_pixel(), player_left + 8, player_center_y, velocity_to_player_x.v, velocity_to_player_y.v, p->speed.v ); if(p->cur_top.v < PELLET_CHASE_TOP_MAX) { #define chase(cur, target) \ (cur > target) ? to_sp(-0.0625f) : \ (cur < target) ? to_sp( 0.0625f) : \ to_sp(0.0f); p->velocity.x.v += chase(p->velocity.x.v, velocity_to_player_x.v); p->velocity.y.v += chase(p->velocity.y.v, velocity_to_player_y.v); #undef chase } break; } } void pellet_put(screen_x_t left, vram_y_t top, int cel) { // Some `__asm` statements here look like they could be expressed using // register pseudovariables. However, TCC would then use a different // instruction than the one in ZUN's original binary. _ES = SEG_PLANE_B; _AX = (left >> 3); _DX = top; _DX <<= 6; __asm add ax, dx; _DX >>= 2; __asm add ax, dx; __asm mov di, ax; _AX = (left & 7) << 4; _BX = cel; _BX <<= 7; __asm add ax, bx; _AX += reinterpret_cast(sPELLET); __asm mov si, ax; _CX = PELLET_H; put_loop: { __asm movsw _DI += (ROW_SIZE - sizeof(dots16_t)); if(static_cast(_DI) >= PLANE_SIZE) { return; } } __asm loop put_loop; } void pellet_render(screen_x_t left, screen_y_t top, int cel) { grcg_setcolor_rmw(7); pellet_put(left, top, cel); grcg_off(); } inline bool16 overlaps_shot( screen_x_t pellet_left, screen_y_t pellet_top, int i ) { return overlap_lt_gt( pellet_left, pellet_top, PELLET_W, PELLET_H, Shots.left[i], Shots.top[i], SHOT_W, SHOT_H ); } inline bool16 overlaps_orb(screen_x_t pellet_left, screen_y_t pellet_top) { return overlap_lt_gt( pellet_left, pellet_top, PELLET_W, PELLET_H, orb_cur_left, orb_cur_top, ORB_W, ORB_H ); } bool16 CPellets::visible_after_hittests_for_cur( screen_x_t pellet_left, screen_y_t pellet_top ) { // Well, well. Since ZUN uses this super sloppy 16x8 rectangle to unblit // 8x8 pellets, there's now the (completely unnecessary) possibility of // accidentally unblitting parts of a sprite that was previously drawn // into the 8 pixels right of a pellet. // "Oh, I know! Let's test the entire 16 pixels, and in case we got an // entity there, we simply make the *pellet* invisible for this frame! // Then we don't even have to unblit it later! :tannedcirno:" // // Except that the only entity type this is done for are the player shots, // and also only for 3 basically random ones? Note that indices 0, 1, and // 2 in the shot array don't necessarily have to be the 3 ones that // spawned last... although given the sheer ~quality~ of all this code, it // might very well have been intended like that? Or maybe this code was // written when the intended SHOT_COUNT was still 3. Urgh. // // Also note that the code is still testing the left 8 pixels that make up // the actual pellet, despite already having hit-tested them with a call // to CShots::hittest_pellet() before. Turns out that the check there // (which actually does affect gameplay) is more precise and excludes the // transparent edges of the shot sprite, which the blitting-related check // here shouldn't do. (This entire paragraph would have been unnecessary // if this code had just tested all 16 pixels at once, rather than having // this weird, bloated, and slow split into two 8-pixel checks.) // // This is certainly a contender for the most stupid piece of code in this // game. What about the player sprite, the Orb (for which we *do* the same // test, but always return `true` regardless), or, heck, *other pellets*? // It's just so hilarious how these mitigations don't help fixing the // underlying problem, at all. for(int i = 0; i < 3; i++) { if( (Shots.is_moving(i) == true) && (overlaps_shot(pellet_left, pellet_top, i) == true) ) { return false; } else if(overlaps_shot((pellet_left + 8), pellet_top, i) == true) { return false; } } if(p->decay_frame) { return true; } if( (overlaps_orb(pellet_left, pellet_top) == true) || (overlaps_orb((pellet_left + 8), pellet_top) == true) ) { // Hey, let's also process a collision! Why not?! This is one magical // Orb, after all. Even collides with pellets it doesn't actually hit. p->velocity.y.v >>= 1; p->velocity.x.v >>= 1; p->decay_frame = 1; pellet_destroy_score_delta += PELLET_DESTROY_SCORE; return true; } if((player_deflecting == true) && (overlap_lt_gt( pellet_left, pellet_top, PELLET_W, PELLET_H, (player_left - PELLET_W), (player_top - (PELLET_H * 2)), (PLAYER_W + (PELLET_W * 2)), (PLAYER_H + (PELLET_H * 2)) ) == true)) { char deflect_angle; if(p->cur_left.to_pixel() <= (player_left + 12)) { deflect_angle = 0x80; } else { deflect_angle = 0x00; } vector2(p->velocity.x.v, p->velocity.y.v, to_sp(8.0f), deflect_angle); if(!p->from_pattern) { p->motion_type = PM_NORMAL; } // Yes, deflected pellets aren't rendered on the frames they're // deflected on! return false; } return true; } void CPellets::decay_tick_for_cur(void) { p->decay_frame++; if(p->decay_frame > (PELLET_DECAY_FRAMES + 1)) { p->decay_frame = 0; p->moving = false; alive_count--; } } void CPellets::unput_update_render(void) { int i; interlace_field = (interlace_field == false) ? true : false; clouds_unput_update_render(); p = iteration_start(); for(i = 0; i < PELLET_COUNT; i++, p++) { if(p->moving == false) { continue; } if(!pellet_interlace || (interlace_field == (i % 2))) { if(p->not_rendered == false && (p->prev_left.v != -1)) { p->sloppy_wide_unput(); } } if(p->from_pattern == PP_NONE && p->motion_type) { motion_type_apply_for_cur(); } if(p->velocity.y.v > PELLET_VELOCITY_MAX) { p->velocity.y.v = PELLET_VELOCITY_MAX; } if(p->velocity.x.v > PELLET_VELOCITY_MAX) { p->velocity.x.v = PELLET_VELOCITY_MAX; } p->cur_top.v += p->velocity.y.v; p->cur_left.v += p->velocity.x.v; // Shot<->Pellet hit testing if(p->decay_frame == 0) { if(Shots.hittest_pellet( p->cur_left.to_pixel(), p->cur_top.to_pixel() )) { p->decay_frame = 0; p->moving = false; alive_count--; p->sloppy_wide_unput(); } } // Clipping if( p->cur_top.v >= to_sp(PELLET_TOP_MAX) || p->cur_top.v < to_sp(PELLET_TOP_MIN) || p->cur_left.v >= to_sp(PELLET_LEFT_MAX) || p->cur_left.v <= to_sp(PELLET_LEFT_MIN) ) { p->moving = false; alive_count--; p->decay_frame = 0; p->sloppy_wide_unput(); } } Shots.unput_update_render(); p = iteration_start(); for(i = 0; i < PELLET_COUNT; i++, p++) { if(p->moving == false) { continue; } if(!pellet_interlace || ((interlace_field & 1) == (i % 2))) { if(visible_after_hittests_for_cur( p->cur_left.to_pixel(), p->cur_top.to_pixel() ) == true) { if(p->not_rendered == true) { p->not_rendered = false; } #define render pellet_render #define decay_frames_for_cel(cel) \ ((PELLET_DECAY_FRAMES / PELLET_DECAY_CELS) * cel) if(p->decay_frame == 0) { render(p->cur_left.to_pixel(), p->cur_top.to_pixel(), 0); } else if(p->decay_frame <= decay_frames_for_cel(1)) { render(p->cur_left.to_pixel(), p->cur_top.to_pixel(), 1); } else if(p->decay_frame < decay_frames_for_cel(2)) { render(p->cur_left.to_pixel(), p->cur_top.to_pixel(), 2); } #undef decay_frames_for_cel #undef render } else { p->not_rendered = true; } p->prev_left.v = p->cur_left.v; p->prev_top.v = p->cur_top.v; } p->age++; if(p->decay_frame) { decay_tick_for_cur(); } else if(hittest_player_for_cur()) { if(p->not_rendered == false) { p->sloppy_wide_unput(); } p->moving = false; alive_count--; p->decay_frame = 0; } } } void CPellets::unput_and_reset_all(void) { p = iteration_start(); for(int i = 0; i < PELLET_COUNT; i++, p++) { if(p->moving == false) { continue; } if(p->not_rendered == false) { p->sloppy_wide_unput_at_cur_pos(); } p->decay_frame = 0; p->moving = false; p->cloud_frame = 0; } alive_count = 0; } void CPellets::decay_all(void) { p = iteration_start(); for(int i = 0; i < PELLET_COUNT; i++, p++) { if(p->moving == false) { continue; } if(p->decay_frame) { continue; } p->velocity.y.v /= 1.5f; p->velocity.x.v /= 1.5f; p->decay_frame = 1; pellet_destroy_score_delta += PELLET_DESTROY_SCORE; } } void CPellets::reset_all(void) { p = iteration_start(); for(int i = 0; i < PELLET_COUNT; i++, p++) { if(p->moving == false) { continue; } p->moving = false; p->decay_frame = 0; p->cloud_frame = 0; } alive_count = 0; } bool16 CPellets::hittest_player_for_cur(void) { if(player_invincible == true || p->decay_frame) { return false; } if( (p->cur_left.to_pixel() >= (player_left + 4)) && (p->cur_left.to_pixel() <= (player_left + 20)) && (p->cur_top.to_pixel() >= (player_top + (player_sliding * 8))) && // Yup, <, not <= as in the overlap_point_le_ge() macro. (p->cur_top.to_pixel() < (player_top + PLAYER_H - PELLET_H)) ) { done = true; return true; } return false; } static const int CLOUD_COL = 7; void CPellets::clouds_unput_update_render(void) { p = iteration_start(); for(int i = 0; i < PELLET_COUNT; i++, p++) { if(p->cloud_frame == 0) { continue; } p->cloud_frame++; if(p->cloud_frame == 2) { pellet_cloud_put_8(p->cloud_left, p->cloud_top, CLOUD_COL, 0); } else if(p->cloud_frame == 5) { pellet_cloud_unput_8(p->cloud_left, p->cloud_top, 0); } else if(p->cloud_frame == 6) { pellet_cloud_put_8(p->cloud_left, p->cloud_top, CLOUD_COL, 1); } else if(p->cloud_frame == 9) { pellet_cloud_unput_8(p->cloud_left, p->cloud_top, 1); p->cloud_frame = 0; p->moving = true; } } }