// Jetpack Revision 49 (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() {} 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): missilelaunch_ = true; 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 // Completely turn jetpack off from any state ******** only for 2 & 3 buttons (while on): ******** case EVENTID(BUTTON_AUX2, EVENT_FIRST_HELD_LONG, MODE_ON): case EVENTID(BUTTON_AUX, EVENT_FIRST_HELD_LONG, MODE_ON): StopIdleMode(); // This will stop the jetpack but not the missile. return true; #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; } // 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. * \*******************************************************************************/ // I want the effects to play one after the other without overlapping or interrupting one another. void PerformMissileLaunchSequence() { STATE_MACHINE_BEGIN(); PVLOG_NORMAL << "Starting Missile Launch Sequence\n"; // create animation for OLED of viewfinder coming down SaberBase::DoEffect(EFFECT_AIMING,0); SLEEP(SaberBase::sound_length * 1000 + 10); // create animation for OLED of targetting SaberBase::DoEffect(EFFECT_TARGETTING,0); SLEEP(SaberBase::sound_length * 1000); // create animation for OLED of jetpack launching missile SaberBase::DoEffect(EFFECT_MISSILELAUNCH,0); SLEEP(SaberBase::sound_length * 1000); // create animation for OLED of explosion SaberBase::DoEffect(EFFECT_MISSILEGOESBOOM,0); SLEEP(SaberBase::sound_length * 1000 + 10); if (!flight_) { // Perform "Mando Talk" if not in flight mode! // create animation for OLED of mando & boba talking SaberBase::DoEffect(EFFECT_MANDOTALK,0); SLEEP(SaberBase::sound_length * 1000 + 10); PVLOG_NORMAL << "Mando: Nice shot!\nBoba: I was aiming for the other one!\n"; } // create animation for OLED of viewfinder going back up SaberBase::DoEffect(EFFECT_DISARM,0); SLEEP(SaberBase::sound_length * 1000); PVLOG_NORMAL << "Missile Launch Sequence Completed!\n"; missilelaunch_ = false; STATE_MACHINE_END(); } // 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 SaberBase::DoEffect(EFFECT_STUTTERING,0); // Play stuttering sound } else if (idle_) { // Jetpack idle, play false start and reset idle timer jetpack_wav_player_.StopFromReader(); // Stop idle sound SaberBase::DoEffect(EFFECT_FALSESTART,0); // 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: SaberBase::DoEffect(EFFECT_SELFDESTRUCT,0); break; case 1: SaberBase::DoEffect(EFFECT_MELTDOWN,0); break; case 2: SaberBase::DoEffect(EFFECT_DUD,0); break; default: SaberBase::DoEffect(EFFECT_SELFDESTRUCT,0); break; } } // 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 } SaberBase::DoEffect(EFFECT_STARTIDLEMODE, 0); // 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 } SaberBase::DoEffect(EFFECT_STARTFLIGHTMODE, 0); // 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 } SaberBase::DoEffect(EFFECT_STOPFLIGHTMODE, 0); // 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 } SaberBase::DoEffect(EFFECT_STOPIDLEMODE, 0); // start transition from idle mode flight_ = false; // Should already be false but StopIdleMode can be used to turn the jetpack completely off from any state. idle_ = false; Off(OFF_FAST); // Fully stop the jetpack } void Loop() override { unsigned long now = millis(); // Perform the missile launch sequence if (missilelaunch_) PerformMissileLaunchSequence(); // 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()) { SaberBase::DoEffect(EFFECT_IDLEMODE,0); // Start idle loop } } void FlightPlaying() { if (flight_ && !jetpack_wav_player_.isPlaying()) { SaberBase::DoEffect(EFFECT_FLIGHTMODE,0); // Start flight loop } } 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. // Helper1 (to avoid repetition) void EffectHelper (Effect* effect1,const char* msg1) { hybrid_font.PlayPolyphonic(effect1); PVLOG_STATUS << msg1 << "\n"; return; } // Helper2 (to avoid repetition) void JetpackHelper(Effect& effect2,const char* msg2) { // Create a FileID from the Effect object and play it Effect::FileID file_id(&effect2, 0, 0); // Adjust parameters (file, sub, alt) as needed jetpack_wav_player_.PlayOnce(file_id); // Use the FileID to play the sound PVLOG_STATUS << msg2 << "\n"; return; } void SB_Effect(EffectType effect, EffectLocation location) override { switch (effect) { default: return; // Missile effects: case EFFECT_AIMING: EffectHelper(&SFX_aiming, "Aiming");//Must not be interrupted and must play in sequence with one another! case EFFECT_TARGETTING: EffectHelper(&SFX_targetting, "Targetting");//Must not be interrupted and must play in sequence with one another! case EFFECT_MISSILELAUNCH: EffectHelper(&SFX_missilelaunch, "Missile Launch");//Must not be interrupted and must play in sequence with one another! case EFFECT_MISSILEGOESBOOM: EffectHelper(&SFX_missilegoesboom,"Missile Explodes");//Must not be interrupted and must play in sequence with one another! case EFFECT_MANDOTALK: EffectHelper(&SFX_mandotalk, "Mando & Boba Talking");//Must not be interrupted and must play in sequence with one another! case EFFECT_DISARM: EffectHelper(&SFX_disarm, "Disarm Targetting");//Must not be interrupted and must play in sequence with one another! // Engine mishap effects: case EFFECT_FALSESTART: EffectHelper(&SFX_falsestart, "False Start"); case EFFECT_STUTTERING: EffectHelper(&SFX_stuttering, "Stuttering"); case EFFECT_SELFDESTRUCT: EffectHelper(&SFX_selfdestruct, "Self Destruct"); case EFFECT_MELTDOWN: EffectHelper(&SFX_meltdown, "Meltdown"); case EFFECT_DUD: EffectHelper(&SFX_dud, "Dank Farrik!\nIt's a Dud!"); // Engine normal effects: case EFFECT_STARTIDLEMODE: JetpackHelper(SFX_startidlemode, "Jetpack Starting to Idle.") ;//after startidlemode, play idle mode on loop case EFFECT_IDLEMODE: jetpack_wav_player_.PlayLoop(&SFX_idlemode); PVLOG_STATUS << "Jetpack in Idle Loop Mode.\n";//plays on loop return; case EFFECT_STARTFLIGHTMODE: JetpackHelper(SFX_startflightmode, "Jetpack Starting to Flight.") ;//after startflightmode, play flight mode on loop case EFFECT_FLIGHTMODE: jetpack_wav_player_.PlayLoop(&SFX_flightmode); PVLOG_STATUS << "Jetpack in Flight Loop Mode.\n";//plays on loop return; case EFFECT_STOPFLIGHTMODE: JetpackHelper(SFX_stopflightmode,"Jetpack Slowing Down to Idle.") ;//after stopflightmode, play idle on loop case EFFECT_STOPIDLEMODE: JetpackHelper(SFX_stopidlemode, "Jetpack Completely Off."); // System effects: // On-Demand Battery Level case EFFECT_BATTERY_LEVEL: if (!hybrid_font.PlayPolyphonic(&SFX_battery)) { // Shows battery level visually on blade. 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 unsigned long timer_ = 0; // Timer for idle mode timeout bool idle_ = false; // Whether the jetpack is in idle mode bool flight_ = false; // Whether the jetpack is in flight mode bool missilelaunch_ = false; // Whether the missile is launching StateMachineState state_machine_; // Required for the SLEEP macro // Sound effects handling 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