// Jetpack Revision 48 (short version-no multi_prop.h support) /* Created by OlivierFlying747-8 with lots of help from Fredrik Hubinette aka profezzorn, http://fredrik.hubbe.net/lightsaber/proffieos.html Copyright (c) 2016-2019 Fredrik Hubinette Copyright (c) 2025 OlivierFlying747-8 with contributions by: Fredrik Hubinette aka profezzorn, Ryan Ogurek aka ryryog25, Bryan Connor aka NoSloppy, In case of problem, you can find us at: https://crucible.hubbe.net somebody will be there to help. Distributed under the terms of the GNU General Public License v3. http://www.gnu.org/licenses/ Includes 1, 2 and 3 button controls. However, I would recommend a 2 or 3 buttons setup because I think a 1 button setup is "cumbersome" to use! Buttons handeling was initially based on saber_sa22c_buttons.h Explanation: First Power Button Press: Plays the jetpack idle starting sound. Loops the idle sound until 1 minute passes, the jetpack is started, or the aux button is pressed (plays a false start sound and restarts the 1-minute idle loop). If no "Second Power Button Press" within 1 minute, the jetpack will switch off. Second Power Button Press: Plays the jetpack starting sound. Loops the jetpack flying sound until the power button is pressed again. Third Power Button Press: Plays the jetpack shutting down to idle sound. Loops the idle sound until 1 minute passes, the jetpack is restarted, or the Aux button is pressed (restarts the 1-minute idle loop). Aux Button Presses: (for a 2 or 3 buttons setup) When flying: Plays a stuttering sound. When idle: Plays a false start sound and restarts the 1-minute idle loop. When off: Plays a self-destruct, melt-down or dud sound randomly! (surprise!) The jetpack turns off automatically if idle for more than 1 minute. This can be changed with "#define JETPACK_IDLE_TIME 1000 * 60 * 1" (millisec * sec * minutes) in your config, you can change the time to what you want (but don't make it zero or you will not be able to make the jetpack go into "flight mode". I have it by default to 1 min for testing/debugging purposes. I guess a good number, once I know everything is working as it should be, would be around 15 to 20sec. Enough for various sound effects to complete, enough to give you time to restart or enough to play with "false start" function. Loop Function: Monitors the 1-minute timer (or user defined) during idle mode to turn off the jetpack completely if the time expires. Plays idle or flight mode on loop for the required time. To do: create animations for OLED for Missile Launch (based on Mando S02E06 - if you know, you know!) & make a fun jetpack sound font. Add buttons "click" (pressed) "clack" released sound(s) EFFECT(s) ??? Create DIM "mode" for blades in idle mode. */ #ifndef PROPS_JETPACK_PROP_H #define PROPS_JETPACK_PROP_H #if NUM_BUTTONS < 1 #error Your prop NEEDS 1 or more buttons to use jetpack_prop (2 is recomended) #endif #include "prop_base.h" #define PROP_TYPE Jetpack #ifndef JETPACK_IDLE_TIME #define JETPACK_IDLE_TIME 1000 * 60 * 1 // Jetpack max idle mode time in millisec (default 1 min) before auto-shutdown #endif // == sounds for system == EFFECT(battery); // for EFFECT_BATTERY_LEVEL //EFFECT(dim); // for EFFECT_POWERSAVE (dim blades while in idle mode) (not codded yet) EFFECT(mute); // Notification before muted ignition to avoid confusion. EFFECT(vmbegin); // for Begin Volume Menu EFFECT(vmend); // for End Volume Menu EFFECT(voldown); // for decrease volume EFFECT(volup); // for increase volume EFFECT(volmax); // for maximum volume reached EFFECT(volmin); // for minimum volume reached // == sounds for jetpack == EFFECT(startidlemode); // jetpack starting to idle (0 to idle) EFFECT(idlemode); // jetpack idle sound (looping idle sound) EFFECT(startflightmode); // jetpack starting sound (idle to flight) EFFECT(flightmode); // jetpack flying sound (looping flight sound) EFFECT(stopflightmode); // jetpack shutdown sound (flight to idle) EFFECT(stopidlemode); // jetpack stopping sound (idle to 0) // == sounds for mishaps == EFFECT(falsestart); // jetpack false start sound (pre-flight) EFFECT(stuttering); // jetpack stuttering sound (in flight) EFFECT(selfdestruct); // jetpack exploding sound (pre-idle) EFFECT(meltdown); // jetpack melt-down sound (pre-idle) EFFECT(dud); // jetpack not exploding or melting down sound (what could be a good dud sound?) "Oh Frack, it's a dud!" // == sounds for missile == EFFECT(aiming); // viewfinder coming down "click" sound EFFECT(targetting); // viewfinder search/finds target sound EFFECT(missilelaunch); // missile launch sound EFFECT(missilegoesboom); // double explosion in the distance sound EFFECT(mandotalk); // "Nice shot! I was aiming for the other one! EFFECT(disarm); // viewfinder going back up (reverse "click") sound "shlack" class Jetpack : public PROP_INHERIT_PREFIX PropBase { public: Jetpack() : PropBase(), flight_(false), idle_(false), timer_(0) {} const char* name() override { return "Jetpack"; } // Based on SA22C Volume Menu void VolumeUp() { PVLOG_DEBUG << "Volume Up\n"; if (dynamic_mixer.get_volume() < VOLUME) { dynamic_mixer.set_volume(std::min(VOLUME + VOLUME * 0.1, dynamic_mixer.get_volume() + VOLUME * 0.1)); if (!hybrid_font.PlayPolyphonic(&SFX_volup)) beeper.Beep(0.5, 2000); PVLOG_DEBUG << "Volume Up - Current Volume: " << dynamic_mixer.get_volume() << "\n"; } else { // Cycle through ends of Volume Menu option #ifdef VOLUME_MENU_CYCLE if (!max_vol_reached_) { if (!hybrid_font.PlayPolyphonic(&SFX_volmax)) beeper.Beep(0.5, 3000); PVLOG_DEBUG << "Maximum Volume: \n"; max_vol_reached_ = true; } else { dynamic_mixer.set_volume(std::max(VOLUME * 0.1, dynamic_mixer.get_volume() - VOLUME * 0.9)); if (!hybrid_font.PlayPolyphonic(&SFX_volmin)) beeper.Beep(0.5, 1000); PVLOG_DEBUG << "Minimum Volume: \n"; max_vol_reached_ = false; } #else if (!hybrid_font.PlayPolyphonic(&SFX_volmax)) beeper.Beep(0.5, 3000); PVLOG_DEBUG << "Maximum Volume: \n"; #endif } } // Based on SA22C Volume Menu void VolumeDown() { PVLOG_DEBUG << "Volume Down\n"; if (dynamic_mixer.get_volume() > (0.1 * VOLUME)) { dynamic_mixer.set_volume(std::max(VOLUME * 0.1, dynamic_mixer.get_volume() - VOLUME * 0.1)); if (!hybrid_font.PlayPolyphonic(&SFX_voldown)) beeper.Beep(0.5, 2000); PVLOG_DEBUG << "Volume Down - Current Volume: " << dynamic_mixer.get_volume() << "\n"; } else { // Cycle through ends of Volume Menu option #ifdef VOLUME_MENU_CYCLE if (!min_vol_reached_) { if (!hybrid_font.PlayPolyphonic(&SFX_volmin)) beeper.Beep(0.5, 1000); PVLOG_DEBUG << "Minimum Volume: \n"; min_vol_reached_ = true; } else { dynamic_mixer.set_volume(VOLUME); if (!hybrid_font.PlayPolyphonic(&SFX_volmax)) beeper.Beep(0.5, 3000); PVLOG_DEBUG << "Maximum Volume: \n"; min_vol_reached_ = false; } #else if (!hybrid_font.PlayPolyphonic(&SFX_volmin)) beeper.Beep(0.5, 1000); PVLOG_DEBUG << "Minimum Volume: \n"; #endif } } // I used SA22C Event2 as a base, and I added my "twist" on it, since a jetpack prop/backpack with a // Proffie board in it can't realisticly have twist or stab motion events. You do not want to have to // do front/back/side flips or run into a wall to activate someting! bool Event2(enum BUTTON button, EVENT event, uint32_t modifiers) override { switch (EVENTID(button, event, modifiers)) { #ifdef BLADE_DETECT_PIN case EVENTID(BUTTON_BLADE_DETECT, EVENT_LATCH_ON, MODE_ANY_BUTTON | MODE_ON): case EVENTID(BUTTON_BLADE_DETECT, EVENT_LATCH_ON, MODE_ANY_BUTTON | MODE_OFF): blade_detected_ = true; FindBladeAgain(); SaberBase::DoBladeDetect(true); return true; case EVENTID(BUTTON_BLADE_DETECT, EVENT_LATCH_OFF, MODE_ANY_BUTTON | MODE_ON): case EVENTID(BUTTON_BLADE_DETECT, EVENT_LATCH_OFF, MODE_ANY_BUTTON | MODE_OFF): blade_detected_ = false; FindBladeAgain(); SaberBase::DoBladeDetect(false); return true; #endif // Jetpack ON AND Volume Up: case EVENTID(BUTTON_POWER, EVENT_FIRST_SAVED_CLICK_SHORT, MODE_OFF): case EVENTID(BUTTON_POWER, EVENT_FIRST_SAVED_CLICK_SHORT, MODE_ON): if (!mode_volume_) { if (idle_) StartFlightMode(); // Transition from idle to flight (rev more up) else if (flight_) StopFlightMode(); // Transition from flight to idle (rev down) else StartIdleMode(); // Jetpack initial start from 0 to idle (rev up) // if idle, it goes up / if up, it goes down to idle / if not idle or not up, it needs to go "up" to idle. } else VolumeUp(); return true; // Jetpack mishaps: case EVENTID(BUTTON_POWER, EVENT_FIRST_HELD_MEDIUM, MODE_OFF): case EVENTID(BUTTON_POWER, EVENT_FIRST_HELD_MEDIUM, MODE_ON): JetpackMishaps(); return true; case EVENTID(BUTTON_POWER, EVENT_SECOND_SAVED_CLICK_SHORT, MODE_OFF): case EVENTID(BUTTON_POWER, EVENT_SECOND_SAVED_CLICK_SHORT, MODE_ON): PerformMissileLaunchSequence(); return true; case EVENTID(BUTTON_POWER, EVENT_FIRST_CLICK_LONG, MODE_OFF): // Next Preset (1 button): #if NUM_BUTTONS == 1 if (!mode_volume_) next_preset(); return true; #else // Start or Stop Track (2 and 3 buttons): StartOrStopTrack(); return true; #endif case EVENTID(BUTTON_POWER, EVENT_FIRST_CLICK_LONG, MODE_ON): // Volume Down (1 button): if (mode_volume_) VolumeDown(); return true; // Next Preset and Volume Down (2 and 3 buttons): case EVENTID(BUTTON_AUX, EVENT_FIRST_CLICK_SHORT, MODE_OFF): if (!mode_volume_) next_preset(); else VolumeDown(); return true; case EVENTID(BUTTON_AUX, EVENT_FIRST_CLICK_SHORT, MODE_ON): if (mode_volume_) VolumeDown(); return true; #if NUM_BUTTONS >= 3 // Previous Preset (3 buttons): case EVENTID(BUTTON_AUX2, EVENT_FIRST_CLICK_SHORT, MODE_OFF): #else // Previous Preset (1 and 2 buttons): case EVENTID(BUTTON_POWER, EVENT_FIRST_HELD_LONG, MODE_OFF): #endif if (!mode_volume_) previous_preset(); return true; // Activate Muted (Turn's Saber ON Muted): case EVENTID(BUTTON_POWER, EVENT_SECOND_HELD, MODE_OFF): if (SetMute(true)) { unmute_on_deactivation_ = true; if (!hybrid_font.PlayPolyphonic(&SFX_mute)) { // was announcemode(&SFX_mute); // Use beeper for fallback sounds beeper.Beep(0.05, 2000); beeper.Silence(0.05); beeper.Beep(0.05, 2000); } FastOn(); } return true; // Color Change mode (1 button): #if NUM_BUTTONS == 1 case EVENTID(BUTTON_POWER, EVENT_THIRD_SAVED_CLICK_SHORT, MODE_ON): #ifndef DISABLE_COLOR_CHANGE ToggleColorChangeMode(); #endif return true; #else // Color Change mode (2 buttons): #if NUM_BUTTONS == 2 #ifndef DISABLE_COLOR_CHANGE case EVENTID(BUTTON_AUX, EVENT_FIRST_CLICK_SHORT, MODE_ON): ToggleColorChangeMode(); return true; #endif #else // Color Change mode (3 buttons): #ifndef DISABLE_COLOR_CHANGE case EVENTID(BUTTON_AUX2, EVENT_FIRST_CLICK_SHORT, MODE_ON): ToggleColorChangeMode(); return true; #endif #endif #endif #if NUM_BUTTONS == 1 // Start or Stop Track (1 button): case EVENTID(BUTTON_POWER, EVENT_FIRST_HELD_LONG, MODE_ON): StartOrStopTrack(); return true; #endif #if NUM_BUTTONS == 1 // Enter/Exit Volume MENU (1 button): case EVENTID(BUTTON_NONE, EVENT_SECOND_HELD, MODE_ON): #else // Enter/Exit Volume MENU (2 and 3 buttons): case EVENTID(BUTTON_AUX, EVENT_SECOND_HELD, MODE_ON): case EVENTID(BUTTON_AUX, EVENT_SECOND_HELD, MODE_OFF): #endif if (!mode_volume_) { mode_volume_ = true; if (!hybrid_font.PlayPolyphonic(&SFX_vmbegin)); beeper.Beep(0.5, 3000); PVLOG_NORMAL << "Enter Volume Menu\n"; } else { mode_volume_ = false; if (!hybrid_font.PlayPolyphonic(&SFX_vmend)); beeper.Beep(0.5, 3000); PVLOG_NORMAL << "Exit Volume Menu\n"; } return true; // Battery level: #if NUM_BUTTONS == 1 // 1 button case EVENTID(BUTTON_POWER, EVENT_THIRD_SAVED_CLICK_SHORT, MODE_OFF): #else // 2 and 3 buttons case EVENTID(BUTTON_AUX, EVENT_FIRST_HELD_LONG, MODE_OFF): #endif talkie.SayDigit((int)floorf(battery_monitor.battery())); talkie.Say(spPOINT); talkie.SayDigit(((int)floorf(battery_monitor.battery() * 10)) % 10); talkie.SayDigit(((int)floorf(battery_monitor.battery() * 100)) % 10); talkie.Say(spVOLTS); return true; // Events that needs to be handled regardless of what other buttons are pressed. case EVENTID(BUTTON_POWER, EVENT_RELEASED, MODE_ANY_BUTTON | MODE_ON): case EVENTID(BUTTON_AUX, EVENT_RELEASED, MODE_ANY_BUTTON | MODE_ON): case EVENTID(BUTTON_AUX2, EVENT_RELEASED, MODE_ANY_BUTTON | MODE_ON): if (SaberBase::Lockup()) { // Does jetpack need this ??? No Lockup in jetpack SaberBase::DoEndLockup(); // Does jetpack need this ??? No Lockup in jetpack SaberBase::SetLockup(SaberBase::LOCKUP_NONE); // Does jetpack need this ??? No Lockup in jetpack return true; } } // switch (EVENTID) return false; // No action } // Event2 /*******************************************************************************\ * If you could check my code below, and let me know how I can improve the sound * * playing capability of my jetpack prop, I would highly appreciate. Thank you. * \*******************************************************************************/ void MissileHelper(Effect* effect1,EffectType effect2) { PVLOG_DEBUG << "Queuing sound: \n"; missile_sound_queue_.Play(SoundToPlay(effect1)); SaberBase::DoEffect(effect2,0); } // I want the effects to play one after the other without overlapping or interrupting one another and without gaps. // Sequential sound queue for missile launch sequence void PerformMissileLaunchSequence() { PVLOG_NORMAL << "Starting Missile Launch Sequence\n"; // create animation for OLED of viewfinder coming down MissileHelper(&SFX_aiming, EFFECT_AIMING); // create animation for OLED of targetting MissileHelper(&SFX_targetting, EFFECT_TARGETTING); // create animation for OLED of jetpack launching missile MissileHelper(&SFX_missilelaunch, EFFECT_MISSILELAUNCH); // create animation for OLED of explosion MissileHelper(&SFX_missilegoesboom,EFFECT_MISSILEGOESBOOM); if (!flight_) { // Perform "Mando Talk" if not in flight mode! // create animation for OLED of mando & boba talking MissileHelper(&SFX_mandotalk, EFFECT_MANDOTALK); PVLOG_NORMAL << "Mando: Nice shot!\nBoba: I was aiming for the other one!\n"; } // create animation for OLED of viewfinder going back up MissileHelper(&SFX_disarm, EFFECT_DISARM); } // Handles Jetpack mishaps events void JetpackMishaps() { if (jetpack_wav_player_.isPlaying()) { return; // Wait for the current sound to finish } if (flight_) { // Jetpack flying, play stuttering sound jetpack_wav_player_.StopFromReader(); // Stop flight sound JetpackHelper(SFX_stuttering,EFFECT_STUTTERING); // Play stuttering sound } else if (idle_) { // Jetpack idle, play false start and reset idle timer jetpack_wav_player_.StopFromReader(); // Stop idle sound JetpackHelper(SFX_falsestart,EFFECT_FALSESTART); // Play false start sound timer_ = millis(); // Reset idle timer } else ChooseRandomEffect(); // Play random mishap effect } // Jetpack off, play self destruct, meltdown or dud (randomly between the three) void ChooseRandomEffect() { int choice = random(3); // Generate a random number: 0, 1, or 2 switch (choice) { case 0: JetpackHelper(SFX_selfdestruct,EFFECT_SELFDESTRUCT); break; case 1: JetpackHelper(SFX_meltdown, EFFECT_MELTDOWN); break; case 2: JetpackHelper(SFX_dud, EFFECT_DUD); break; default: JetpackHelper(SFX_selfdestruct,EFFECT_SELFDESTRUCT); break; } } void JetpackHelper(Effect& effect3, EffectType effect4) { // Create a FileID from the Effect object and play it Effect::FileID file_id(&effect3, 0, 0); // Adjust parameters (file, sub, alt) as needed jetpack_wav_player_.PlayOnce(file_id); // Use the FileID to play the sound SaberBase::DoEffect(effect4, 0); // Execute the visual effect } // Initial start, transition to Idle Mode (from 0 to idle) void StartIdleMode() { if (jetpack_wav_player_.isPlaying()) { return; // Wait for the current sound to finish } JetpackHelper(SFX_startidlemode,EFFECT_STARTIDLEMODE); // start transition to idle mode flight_ = false; idle_ = true; timer_ = millis(); // Reset idle timer FastOn(); PVLOG_STATUS << "Jetpack Idling\n"; } // Transition to Flying Mode (from idle to flying) void StartFlightMode() { if (jetpack_wav_player_.isPlaying()) { return; // Wait for the current sound to finish } JetpackHelper(SFX_startflightmode,EFFECT_STARTFLIGHTMODE); // start transition to flight mode flight_ = true; idle_ = false; PVLOG_STATUS << "Jetpack Flying\n"; } // Transition from Flying Mode (from flying to idle) void StopFlightMode() { if (jetpack_wav_player_.isPlaying()) { return; // Wait for the current sound to finish } JetpackHelper(SFX_stopflightmode,EFFECT_STOPFLIGHTMODE); // start transition from flight mode flight_ = false; idle_ = true; } // Stop Idle Mode (Jetpack completely off - idle to off) void StopIdleMode() { if (jetpack_wav_player_.isPlaying()) { return; // Wait for the current sound to finish } JetpackHelper(SFX_stopidlemode,EFFECT_STOPIDLEMODE); // start transition from idle mode flight_ = false; idle_ = false; Off(OFF_FAST); // Fully stop the jetpack } void Loop() override { unsigned long now = millis(); // Handle missile sound playback and log completion missile_sound_queue_.PollSoundQueue(missile_wav_player_); // Handle missile sound playback if (!missile_sound_queue_.busy()) { PVLOG_NORMAL << "Missile Launch Sequence Completed!\n"; } // Stop idle mode after timeout if (idle_ && (now - timer_ > JETPACK_IDLE_TIME)) { StopIdleMode(); // Stop jetpack if idle for more than defined time return; // Exit early to avoid further processing } // Start idle loop after transition sound finishes if (idle_) IdlePlaying(); // Start flight loop after transition sound finishes if (flight_) FlightPlaying(); } void IdlePlaying() { if (idle_ && !jetpack_wav_player_.isPlaying()) { jetpack_wav_player_.PlayLoop(&SFX_idlemode); // Start idle loop SaberBase::DoEffect(EFFECT_IDLEMODE,0); } } void FlightPlaying() { if (flight_ && !jetpack_wav_player_.isPlaying()) { jetpack_wav_player_.PlayLoop(&SFX_flightmode); // Start flight loop SaberBase::DoEffect(EFFECT_FLIGHTMODE,0); } } void DoMotion(const Vec3&, bool) override { } // jetpack doesn't have "movement effects", // but if it did, just imagine for an instant someone trying to replicate stab, aka running face first // into the nearest wall with their backpack/jetpack prop. void SB_Effect(EffectType effect, EffectLocation location) override { switch (effect) { default: return; // Missile effects: case EFFECT_AIMING: PVLOG_STATUS << "Aiming\n"; return;//Must not be interrupted and must play in sequence with one another! case EFFECT_TARGETTING: PVLOG_STATUS << "Targetting\n"; return;//Must not be interrupted and must play in sequence with one another! case EFFECT_MISSILELAUNCH: PVLOG_STATUS << "Missile Launch\n"; return;//Must not be interrupted and must play in sequence with one another! case EFFECT_MISSILEGOESBOOM: PVLOG_STATUS << "Missile Explodes\n"; return;//Must not be interrupted and must play in sequence with one another! case EFFECT_MANDOTALK: PVLOG_STATUS << "Mando & Boba Talking\n"; return;//Must not be interrupted and must play in sequence with one another! case EFFECT_DISARM: PVLOG_STATUS << "Disarm Targetting\n"; return;//Must not be interrupted and must play in sequence with one another! // Engine mishap effects: case EFFECT_FALSESTART: PVLOG_STATUS << "Jetpack False Start\n"; return; case EFFECT_STUTTERING: PVLOG_STATUS << "Jetpack Stuttering\n"; return; case EFFECT_SELFDESTRUCT: PVLOG_STATUS << "Jetpack Self Destruct\n"; return; case EFFECT_MELTDOWN: PVLOG_STATUS << "Jetpack Meltdown\n"; return; case EFFECT_DUD: PVLOG_STATUS << "Oh Frack!\nIt's a Dud!\n"; return; // Engine normal effects: case EFFECT_STARTIDLEMODE: PVLOG_STATUS << "Jetpack Starting to Idle\n"; return;//after startidlemode, play idle mode on loop case EFFECT_IDLEMODE: PVLOG_STATUS << "Jetpack in idle mode.\n"; return;//plays on loop case EFFECT_STARTFLIGHTMODE: PVLOG_STATUS << "Jetpack Starting to Flight\n"; return;//after startflightmode, play flight mode on loop case EFFECT_FLIGHTMODE: PVLOG_STATUS << "Jetpack in flight mode.\n"; return;//plays on loop case EFFECT_STOPFLIGHTMODE: PVLOG_STATUS << "Jetpack Slowing Down to Idle\n"; return;//after stopflightmode, play idle on loop case EFFECT_STOPIDLEMODE: PVLOG_STATUS << "Jetpack Completely Off\n"; return; // System effects: // On-Demand Battery Level case EFFECT_BATTERY_LEVEL: if (!hybrid_font.PlayPolyphonic(&SFX_battery)) { beeper.Beep(1.0, 475); beeper.Beep(0.5, 693); beeper.Beep(0.16, 625); beeper.Beep(0.16, 595); beeper.Beep(0.16, 525); beeper.Beep(1.1, 950); beeper.Beep(0.5, 693); PVLOG_NORMAL << "May the Force be with you...always.\n";} return; } // switch (effect) } // SB_Effect private: // State variables bool idle_; // Whether the jetpack is in idle mode bool flight_; // Whether the jetpack is in flight mode unsigned long timer_; // Timer for idle mode timeout // Sound effects handling RefPtr missile_wav_player_; // For missile sound playback SoundQueue<6> missile_sound_queue_; // Handles missile launch sequence sounds PlayWav jetpack_wav_player_; // Handles idle, flight, and mishap sounds // Volume handling bool mode_volume_ = false; // from sa22c for volume menu bool max_vol_reached_ = false; // from sa22c for volume menu bool min_vol_reached_ = false; // from sa22c for volume menu }; #endif // PROPS_JETPACK_PROP_H