/* * HISTORICAL PARTIAL mechanics transcription for TDKPIN.EXE. * * Target SHA-256: * a9022f1894e3e6e21fc42e8f6c932f7c549ca77f63aaa0c488bb9d55d9d0174c * * This superseded intermediate is retained only as provenance for the earlier * live-probe work. It is not part of the completed readable-C deliverable. * The authoritative complete address-linked implementations are under * reconstructed/, especially tdkpin_flipper_collision.c, * tdkpin_flipper_bitmap_geometry.c, tdkpin_physics.c, and tdkpin_timer_tick.c. */ #include #include enum { SOUND_RESOURCE_BASE = 2000, SOUND_CLAW_CAPTURE = SOUND_RESOURCE_BASE + 15, SOUND_CLAW_RELEASE = SOUND_RESOURCE_BASE + 16, SOUND_FLIPPER_MOVE = SOUND_RESOURCE_BASE + 21, }; typedef struct { int16_t x; int16_t y; int16_t width; int16_t height; } RectI16; typedef struct { bool left_control_down; /* object +0xbda; scan code 0x1d */ bool keypad_enter_down; /* object +0xbdb; scan code 0x1c */ int16_t left_position; /* object +0xbd4; recovered range 0..1 */ int16_t right_position; /* object +0xbd6; recovered range 0..1 */ } FlipperState; typedef enum { CLAW_OPENING_OR_IDLE = 0, CLAW_CLOSING = 1, } ClawSpriteBank; typedef struct { bool active; /* DAT_1028_07ca */ uint16_t frame; /* DAT_1028_086d; initialized to 10 */ uint16_t target_frame; /* DAT_1028_086f; initialized to 10 */ ClawSpriteBank bank; /* DAT_1028_0873 */ bool animation_blocked; /* DAT_1028_0874 */ bool ball_suspended; /* game object +0xbdf */ } ClawState; typedef struct { uint16_t low; int16_t high; } MillipixelWords; typedef struct { MillipixelWords x; MillipixelWords y; } RawClawReleasePosition; typedef struct { float x; float y; } Vec2F; /* * FUN_1008_0002 at 1008:0002. * The called loader/player resolves argument N as Win16 WAVE resource 2000+N. */ static void play_sound_number(uint16_t number) { if (sound_is_not_muted() && sound_device_is_available()) { play_wave_resource(SOUND_RESOURCE_BASE + number); } } /* * FUN_1000_638e / FUN_1000_6bf8 (keyboard release/press branches). * The original controls are binary input flags. Motion happens later in * update_flippers(), once per changed edge. */ static void set_flipper_key(FlipperState *state, uint8_t scan_code, bool down) { if (scan_code == 0x1d) { state->left_control_down = down; } else if (scan_code == 0x1c) { state->keypad_enter_down = down; } } /* * FUN_1000_7ed9 at 1000:7ed9 and FUN_1000_8b0d at 1000:8b0d. * delta is -1 for key-down and +1 for key-up. The first routine updates * the live collision geometry and always starts sound 2021; the second moves * the matching rendered flipper geometry. */ static void move_flipper(int flipper_number, int delta) { play_sound_number(21); update_flipper_collision_geometry(flipper_number, delta); update_flipper_bitmap_geometry(flipper_number, delta); } /* FUN_1000_99f0 at 1000:99f0. */ static void update_flippers(FlipperState *state) { if (state->left_control_down && state->left_position == 0) { state->left_position++; move_flipper(1, -1); } else if (!state->left_control_down && state->left_position != 0) { state->left_position--; move_flipper(1, +1); } if (state->keypad_enter_down && state->right_position == 0) { state->right_position++; move_flipper(2, -1); } else if (!state->keypad_enter_down && state->right_position != 0) { state->right_position--; move_flipper(2, +1); } } /* * FUN_1008_0e08 at 1008:0e08. * Resource 900 is a 900x296 sheet: nine 100x74 frames in each of four rows. * Closing uses rows 0/74; opening and returning use rows 148/222. The * destination is the logical playfield rectangle x=238, y=47, w=100, h=74. */ static RectI16 claw_source_rect(uint16_t frame, ClawSpriteBank bank) { uint16_t column_frame = frame; int16_t source_y = bank == CLAW_CLOSING ? 0 : 148; if (column_frame > 9) { source_y += 74; column_frame -= 9; } return (RectI16){ .x = (int16_t)((column_frame - 1) * 100), .y = source_y, .width = 100, .height = 74, }; } static void render_claw(uint16_t frame, ClawSpriteBank bank) { const RectI16 destination = {238, 47, 100, 74}; if (frame == 0) { restore_playfield_background(destination); } else { blit_claw_sprite(claw_source_rect(frame, bank), destination); } } /* The only terminal frames selected by the collision branch at 1000:c79c. */ static const uint16_t CLAW_TERMINAL_FRAMES[4] = {1, 6, 7, 18}; /* * Raw signed 32-bit millipixel positions assigned by export ordinal 10 at * 1000:eab1. FUN_1000_9bca adds 500, divides by 1000, then subtracts the * eight-pixel ball radius only for bitmap placement. The physics position is * therefore the raw value divided by 1000, not a 16.16 fixed-point number. */ static RawClawReleasePosition raw_claw_release_position(uint16_t frame) { switch (frame) { case 1: case 3: return (RawClawReleasePosition){{0xefd0, 3}, {0x30b0, 1}}; case 2: return (RawClawReleasePosition){{0xf3b8, 3}, {0x3c68, 1}}; case 4: return (RawClawReleasePosition){{0xfb88, 3}, {0x4ff0, 1}}; case 5: return (RawClawReleasePosition){{0x0f10, 4}, {0x5f90, 1}}; case 6: return (RawClawReleasePosition){{0x1eb0, 4}, {0x6f30, 1}}; case 7: return (RawClawReleasePosition){{0x3238, 4}, {0x7ae8, 1}}; case 8: return (RawClawReleasePosition){{0x3df0, 4}, {0x7ed0, 1}}; case 9: return (RawClawReleasePosition){{0x4d90, 4}, {0x8a88, 1}}; case 18: return (RawClawReleasePosition){{0xf588, 4}, {0x6760, 1}}; default: unreachable_original_state(); return (RawClawReleasePosition){{0, 0}, {0, 0}}; } } static int32_t join_millipixel_words(MillipixelWords value) { return ((int32_t)value.high << 16) | value.low; } static Vec2F claw_release_position_pixels(uint16_t frame) { RawClawReleasePosition raw = raw_claw_release_position(frame); return (Vec2F){ join_millipixel_words(raw.x) / 1000.0f, join_millipixel_words(raw.y) / 1000.0f, }; } /* * Real48 constants loaded in 1000:eab1 after converting speed scalar 3800. * The result is millipixels per timer tick. Only 1, 6, 7, and 18 are selected * as random terminal frames, but all switch cases are retained here. */ static Vec2F claw_release_velocity_millipixels_per_tick(uint16_t frame) { switch (frame) { case 1: return (Vec2F){3800.0f * -0.60f, 3800.0f * 0.00f}; case 2: return (Vec2F){3800.0f * -0.45f, 3800.0f * 0.05f}; case 3: return (Vec2F){3800.0f * -0.70f, 3800.0f * 0.30f}; case 4: return (Vec2F){3800.0f * -0.30f, 3800.0f * 0.20f}; case 5: return (Vec2F){3800.0f * -0.32f, 3800.0f * 0.22f}; case 6: return (Vec2F){3800.0f * -0.40f, 3800.0f * 0.60f}; case 7: return (Vec2F){3800.0f * -0.31f, 3800.0f * 0.70f}; case 8: return (Vec2F){3800.0f * -0.20f, 3800.0f * 0.80f}; case 9: return (Vec2F){3800.0f * -0.10f, 3800.0f * 0.90f}; case 18: return (Vec2F){0.0f, 1000.0f}; default: unreachable_original_state(); return (Vec2F){0.0f, 0.0f}; } } /* Object-id 0x59 branch inside FUN_1000_c79c at 1000:c79c. */ static void begin_claw_capture(ClawState *claw, uint16_t random_0_to_3) { if (claw->animation_blocked) { return; } claw->active = true; claw->target_frame = CLAW_TERMINAL_FRAMES[random_0_to_3]; claw->bank = CLAW_CLOSING; claw->ball_suspended = true; zero_ball_motion(); /* These moving collision records are absent while the arm holds the ball. */ set_collision_objects_enabled(12, 20, false); play_sound_number(15); /* recovered call immediately before this branch */ } /* Claw branch of exported ordinal 10 at 1000:eab1. */ static void update_claw(ClawState *claw) { if (!claw->active || claw->animation_blocked) { return; } if (claw->frame != claw->target_frame) { claw->frame += claw->frame < claw->target_frame ? 1 : -1; render_claw(claw->frame, claw->bank); return; } if (claw->frame == 10) { render_claw(0, CLAW_OPENING_OR_IDLE); if (claw->bank == CLAW_OPENING_OR_IDLE) { claw->active = false; set_collision_objects_enabled(12, 20, true); } return; } /* Terminal closing frame: redraw from the opening bank, release, return. */ render_claw(claw->frame, CLAW_OPENING_OR_IDLE); claw->target_frame = 10; claw->bank = CLAW_OPENING_OR_IDLE; reset_ball_forces(); set_ball_position_pixels(claw_release_position_pixels(claw->frame)); set_ball_velocity_millipixels_per_tick( claw_release_velocity_millipixels_per_tick(claw->frame)); commit_ball_position(); play_sound_number(16); claw->ball_suspended = false; }