/* * This program source code file is part of SystemK, a library in the KTag project. * * 🛡️ 🃞 * * Copyright © 2016-2025 Joseph P. Kearney and the KTag developers. * * This program is free software: you can redistribute it and/or modify it under * the terms of the GNU Affero General Public License as published by the Free * Software Foundation, either version 3 of the License, or (at your option) any * later version. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more * details. * * There should be a copy of the GNU Affero General Public License in the LICENSE * file in the root of this repository. If not, see . */ /* Include Files */ #include "SystemK.h" #include #define LOG_INTERACTING_SUBSTATE static void Playing__Interacting_Entry(StateMachineContext_T * context); static void Playing__Interacting_Do(StateMachineContext_T * context); static void Playing__Interacting_Exit(StateMachineContext_T * context); static uint8_t Health_in_percent; //! Activities for the **Interacting** substate of the **Playing** state. const StateActivity_T STATE_PLAYING__INTERACTING_Activities = { .Entry = Playing__Interacting_Entry, .Do = Playing__Interacting_Do, .Exit = Playing__Interacting_Exit }; static TickType_t Time_Of_Last_Shot = 0; static const uint32_t FIXED_SHOT_HOLDOFF_in_ms = 3000; static const uint32_t RANDOM_SHOT_HOLDOFF_in_ms = 1000; static TickType_t Shot_Holdoff_Time; static color_t ReceivedTagColor = 0x00000000; static const uint32_t PURPLE_TAG_INVINCIBILITY_WINDOW_in_ms = 1000; #ifdef LOG_INTERACTING_SUBSTATE static const char *KLOG_TAG = "STATE_PLAYING__INTERACTING"; #endif // LOG_INTERACTING_SUBSTATE //! Sets up the Interacting substate. /*! * \param context Context in which this substate is being run. */ static void Playing__Interacting_Entry(StateMachineContext_T * context) { if (STATE_IsPlayingSubstate(context->States.Previous_State) == false) { Playing_Entry(context); srand(xTaskGetTickCount()); } BLE_UpdateStatusPacket(STATE_PLAYING__INTERACTING); LOG("Entering the Interacting substate of the Playing state."); } //! Executes the Interacting substate. /*! * \param context Context in which this substate is being run. */ static void Playing__Interacting_Do(StateMachineContext_T * context) { portBASE_TYPE xStatus; static KEvent_T Event; xStatus = Receive_KEvent(&Event); if (xStatus == pdPASS) { switch (Event.ID) { case KEVENT_TRIGGER_SWITCH_PRESSED: #ifdef LOG_INTERACTING_SUBSTATE KLOG_INFO(KLOG_TAG, "Trigger pressed."); #endif // LOG_INTERACTING_SUBSTATE if ((xTaskGetTickCount() - Time_Of_Last_Shot) > Shot_Holdoff_Time) { if (Send_Tag() != SYSTEMK_RESULT_SUCCESS) { KEvent_T misfire_event = { .ID = KEVENT_MISFIRE, .Data = (void *)0x00 }; Post_KEvent(&misfire_event); } } else { KEvent_T misfire_event = { .ID = KEVENT_MISFIRE, .Data = (void *)0x00 }; Post_KEvent(&misfire_event); } break; case KEVENT_TRIGGER_SWITCH_RELEASED: #ifdef LOG_INTERACTING_SUBSTATE KLOG_INFO(KLOG_TAG, "Trigger released."); #endif // LOG_INTERACTING_SUBSTATE { uint32_t duration_of_press_in_ms = (uint32_t)Event.Data; if (duration_of_press_in_ms > 10000) { AudioAction_T audio_action = {.ID = AUDIO_PLAY_ELECTRONIC_DANCE_MUSIC, .Data = (void *)0x00}; Perform_Audio_Action(&audio_action); NeoPixelsAction_T neopixels_action = {.ID = NEOPIXELS_TEST_PATTERN, .Prominence = NEOPIXELS_FOREGROUND, .Data = (void *)0x00}; xQueueSend(xQueueNeoPixels, &neopixels_action, 0); } // Was it a "long" press? else if (duration_of_press_in_ms > 3000) { Health_in_percent = (uint8_t)Get_Health(); AudioAction_T audio_action = {.ID = AUDIO_PRONOUNCE_NUMBER_0_TO_100, .Play_To_Completion = true, .Data = (void *)&Health_in_percent}; Perform_Audio_Action(&audio_action); AudioAction_T audio_action_two = {.ID = AUDIO_PLAY_HEALTH_REMAINING, .Data = (void *)0x00}; Perform_Audio_Action(&audio_action_two); NeoPixelsAction_T neopixels_action = {.ID = NEOPIXELS_HEALTH_REPORT, .Prominence = NEOPIXELS_FOREGROUND, .Data = (void *)&Health_in_percent}; xQueueSend(xQueueNeoPixels, &neopixels_action, 0); } } break; case KEVENT_UP_SWITCH_LONG_PRESSED: { AudioAction_T audio_action = {.ID = AUDIO_PLAY_ELECTRONIC_DANCE_MUSIC, .Data = (void *)0x00}; Perform_Audio_Action(&audio_action); NeoPixelsAction_T neopixels_action = {.ID = NEOPIXELS_TEST_PATTERN, .Prominence = NEOPIXELS_FOREGROUND, .Data = (void *)0x00}; xQueueSend(xQueueNeoPixels, &neopixels_action, 0); } break; case KEVENT_UP_SWITCH_RELEASED: { //AudioAction_T audio_action = {.ID = AUDIO_SILENCE, .Data = (void *)0x00}; //xQueueSend(xQueueAudio, &audio_action, 0); //NeoPixelsAction_T neopixels_action = {.ID = NEOPIXELS_ALL_OFF, .Prominence = NEOPIXELS_FOREGROUND, .Data = (void *)0x00}; //xQueueSend(xQueueNeoPixels, &neopixels_action, 0); } break; case KEVENT_DOWN_SWITCH_LONG_PRESSED: { NeoPixelsAction_T neopixels_action = {.ID = NEOPIXELS_FLASHLIGHT_ON, .Prominence = NEOPIXELS_FOREGROUND, .Data = (void *)0x00}; xQueueSend(xQueueNeoPixels, &neopixels_action, 0); } break; case KEVENT_DOWN_SWITCH_RELEASED: { NeoPixelsAction_T neopixels_action = {.ID = NEOPIXELS_ALL_OFF, .Prominence = NEOPIXELS_FOREGROUND, .Data = (void *)0x00}; xQueueSend(xQueueNeoPixels, &neopixels_action, 0); } break; case KEVENT_FORWARD_SWITCH_PRESSED: { NeoPixelsAction_T neopixels_action = {.ID = NEOPIXELS_FLAMETHROWER, .Prominence = NEOPIXELS_FOREGROUND, .Data = (void *)0x00}; xQueueSend(xQueueNeoPixels, &neopixels_action, 0); } break; case KEVENT_FORWARD_SWITCH_RELEASED: { NeoPixelsAction_T neopixels_action = {.ID = NEOPIXELS_ALL_OFF, .Prominence = NEOPIXELS_FOREGROUND, .Data = (void *)0x00}; xQueueSend(xQueueNeoPixels, &neopixels_action, 0); } break; case KEVENT_BACKWARD_SWITCH_PRESSED: { Health_in_percent = (uint8_t)Get_Health(); AudioAction_T audio_action = {.ID = AUDIO_PRONOUNCE_NUMBER_0_TO_100, .Play_To_Completion = true, .Data = (void *)&Health_in_percent}; Perform_Audio_Action(&audio_action); AudioAction_T audio_action_two = {.ID = AUDIO_PLAY_HEALTH_REMAINING, .Data = (void *)0x00}; Perform_Audio_Action(&audio_action_two); NeoPixelsAction_T neopixels_action = {.ID = NEOPIXELS_HEALTH_REPORT, .Prominence = NEOPIXELS_FOREGROUND, .Data = (void *)&Health_in_percent}; xQueueSend(xQueueNeoPixels, &neopixels_action, 0); } break; case KEVENT_BACKWARD_SWITCH_RELEASED: { NeoPixelsAction_T neopixels_action = {.ID = NEOPIXELS_ALL_OFF, .Prominence = NEOPIXELS_FOREGROUND, .Data = (void *)0x00}; xQueueSend(xQueueNeoPixels, &neopixels_action, 0); } break; case KEVENT_TAG_SENT: { // "Flip ya'? Double or nuthin'!" uint32_t holdoff_in_ms = (rand() % 2) * RANDOM_SHOT_HOLDOFF_in_ms; holdoff_in_ms += FIXED_SHOT_HOLDOFF_in_ms; Shot_Holdoff_Time = (holdoff_in_ms / portTICK_PERIOD_MS); #ifdef LOG_INTERACTING_SUBSTATE KLOG_INFO(KLOG_TAG, "Tag sent. Holdoff: %lu ms", holdoff_in_ms); #endif // LOG_INTERACTING_SUBSTATE Time_Of_Last_Shot = xTaskGetTickCount(); AudioAction_T audio_action = {.ID = AUDIO_PLAY_SHOT_FIRED, .Data = (void *)0x00}; Perform_Audio_Action(&audio_action); NeoPixelsAction_T neopixels_action = {.ID = NEOPIXELS_PLAY_SHOT_FIRED, .Prominence = NEOPIXELS_FOREGROUND, .Data = (void *)0x00}; xQueueSend(xQueueNeoPixels, &neopixels_action, 0); Increment_Shots_Fired(); } break; case KEVENT_TAG_RECEIVED: { uint8_t protocol = ((DecodedPacket_T*)Event.Data)->Tag.protocol; uint16_t team_ID = ((DecodedPacket_T*)Event.Data)->Tag.team_ID; uint16_t player_ID = ((DecodedPacket_T*)Event.Data)->Tag.player_ID; uint16_t damage = ((DecodedPacket_T*)Event.Data)->Tag.damage; ReceivedTagColor = ((DecodedPacket_T*)Event.Data)->Tag.color; TagSensorLocation_T receiver = ((DecodedPacket_T*)Event.Data)->Tag.receiver; TeamID_t rxd_common_team_ID = Resolve_Common_Team_ID(team_ID); #ifdef LOG_INTERACTING_SUBSTATE switch (receiver) { default: KLOG_INFO(KLOG_TAG, "Tag from unknown sensor: Team: %u Player: %u Damage: %u", team_ID, player_ID, damage); break; case TAG_SENSOR_FORWARD: KLOG_INFO(KLOG_TAG, "Tag from forward sensor: Team: %u Player: %u Damage: %u", team_ID, player_ID, damage); break; case TAG_SENSOR_LEFT: KLOG_INFO(KLOG_TAG, "Tag from left sensor: Team: %u Player: %u Damage: %u", team_ID, player_ID, damage); break; case TAG_SENSOR_RIGHT: KLOG_INFO(KLOG_TAG, "Tag from right sensor: Team: %u Player: %u Damage: %u", team_ID, player_ID, damage); break; case TAG_SENSOR_REMOTE: KLOG_INFO(KLOG_TAG, "Tag from remote sensor: Team: %u Player: %u Damage: %u", team_ID, player_ID, damage); break; } #endif // LOG_INTERACTING_SUBSTATE if (Still_Playing() == true) { uint8_t my_team_ID; uint8_t my_player_ID; (void) SETTINGS_get_uint8_t(SYSTEMK_SETTING_TEAMID, &my_team_ID); (void) SETTINGS_get_uint8_t(SYSTEMK_SETTING_PLAYERID, &my_player_ID); if (Team_Can_Tag_Me(rxd_common_team_ID) == true) { // Checks to make sure the tagger is not seeing its own tag. bool tag_might_be_mine = false; if (Resolve_Common_Team_ID(team_ID) == TEAM_PURPLE) { if ((xTaskGetTickCount() - Time_Of_Last_Shot) < (PURPLE_TAG_INVINCIBILITY_WINDOW_in_ms / portTICK_PERIOD_MS)) { // Special handling for receiving a Purple Tag while sending a Purple Tag using the DBQ Protocol. if (protocol == DUBUQUE_PROTOCOL) { if (damage <= 20) { tag_might_be_mine = true; } else { // If the accumulated damage is more than 20, it means I've received my own tag AND another tag. // Set the damage to the value of a single tag. damage = 10; } } // For protocols other than DBQ, we cannot distinguish my tag from yours. The solution is to // allow a period of invincibility from Purple tags after sending ours. // Note that this means for most protocols, simultaneous Purple vs. Purple results in no tags. else { tag_might_be_mine = true; } } } // If the tag is clearly not mine, process it. if (tag_might_be_mine == false) { Reduce_Health(damage); AudioAction_T audio_action = {.ID = AUDIO_PLAY_TAG_RECEIVED, .Data = (void *)0x00}; Perform_Audio_Action(&audio_action); NeoPixelsAction_T neopixels_action = {.ID = NEOPIXELS_TAG_RECEIVED, .Prominence = NEOPIXELS_FOREGROUND, .Data = (void *)&ReceivedTagColor}; xQueueSend(xQueueNeoPixels, &neopixels_action, 0); Increment_Tags_Received(); #ifdef LOG_INTERACTING_SUBSTATE KLOG_INFO(KLOG_TAG, "%u of %u health remaining", KTAG_Game_Data.My_Health, KTAG_Game_Data.Max_Health); #endif // LOG_INTERACTING_SUBSTATE } else { #ifdef LOG_INTERACTING_SUBSTATE KLOG_INFO(KLOG_TAG, "Tag was my own--ignoring."); #endif // LOG_INTERACTING_SUBSTATE } } else if (player_ID != my_player_ID) { // Friendly fire! AudioAction_T audio_action = {.ID = AUDIO_PLAY_FRIENDLY_FIRE, .Data = (void *)0x00}; Perform_Audio_Action(&audio_action); } } } FreeDecodedPacketBuffer(Event.Data); break; case KEVENT_TAGGED_OUT: { context->States.Next_State = STATE_PLAYING__TAGGED_OUT; } break; case KEVENT_MISFIRE: { AudioAction_T audio_action = {.ID = AUDIO_PLAY_MISFIRE, .Data = (void *)0x00}; Perform_Audio_Action(&audio_action); } break; case KEVENT_NEAR_MISS: { AudioAction_T audio_action = {.ID = AUDIO_PLAY_NEAR_MISS, .Data = (void *)0x00}; Perform_Audio_Action(&audio_action); } break; case KEVENT_COMMAND_RECEIVED: FreeDecodedPacketBuffer(Event.Data); break; case KEVENT_BLE_PACKET_RECEIVED: #ifdef LOG_INTERACTING_SUBSTATE //KLOG_INFO(KLOG_TAG, "KEVENT_BLE_PACKET_RECEIVED from %s", BLE_ADDR_To_Str(((BLE_Packet_T *)Event.Data)->Generic.BD_ADDR)); #endif // LOG_INTERACTING_SUBSTATE if (((BLE_Packet_T *)Event.Data)->Generic.type == BLE_PACKET_TYPE_STATUS) { HandleBLEStatusPacket((BLE_StatusPacket_T *)Event.Data); } else if (((BLE_Packet_T *)Event.Data)->Generic.type == BLE_PACKET_TYPE_TAG) { HandleBLETagPacket((BLE_TagPacket_T *)Event.Data); } else if (((BLE_Packet_T *)Event.Data)->Generic.type == BLE_PACKET_TYPE_EVENT) { HandleBLEEventPacket((BLE_EventPacket_T *)Event.Data, context); } else { BLE_FreePacketBuffer(Event.Data); } break; case KEVENT_ACCESSORY_SWITCH_PRESSED: { #ifdef LOG_INTERACTING_SUBSTATE uint32_t time_since_last_press_in_ms = (uint32_t)Event.Data; KLOG_INFO(KLOG_TAG, "Accessory pressed after %lu ms.", time_since_last_press_in_ms); #endif // LOG_INTERACTING_SUBSTATE if (Use_Bomb_If_Available() == true) { // BOOM! BLE_UpdateTagPacket(100, COLOR_YELLOW, (uint8_t[BD_ADDR_SIZE]){0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}); AudioAction_T audio_action = {.ID = AUDIO_PLAY_BOMB, .Data = (void *)0x00}; Perform_Audio_Action(&audio_action); } else { AudioAction_T audio_action = {.ID = AUDIO_PLAY_BONK, .Data = (void *)0x00}; Perform_Audio_Action(&audio_action); } } break; case KEVENT_ACCESSORY_SWITCH_RELEASED: { #ifdef LOG_INTERACTING_SUBSTATE uint32_t duration_of_press_in_ms = (uint32_t)Event.Data; KLOG_INFO(KLOG_TAG, "Accessory released after %lu ms.", duration_of_press_in_ms); #endif // LOG_INTERACTING_SUBSTATE } break; case KEVENT_GAME_OVER: { AudioAction_T audio_action = {.ID = AUDIO_PLAY_GAME_OVER, .Play_To_Completion = true}; Perform_Audio_Action(&audio_action); Transition_For_Event(context, STATE_WRAPPING_UP, &Event); } break; default: // All other events are ignored in this state. ProcessUnhandledEvent(&Event); break; } } } //! Cleans up the Interacting substate. /*! * \param context Context in which this substate is being run. */ static void Playing__Interacting_Exit(StateMachineContext_T * context) { if (STATE_IsPlayingSubstate(context->States.Next_State) == false) { Playing_Exit(context); } }