Until we have menus implemented to change protocols, the Dubuque Protocol will be the default. Fixes Management/KTag_Project_Management#5 Co-authored-by: Joe Kearney <joe@clubk.club> Reviewed-on: #3
435 lines
20 KiB
C
435 lines
20 KiB
C
/*
|
|
* This program source code file is part of SystemK, a library in the KTag project.
|
|
*
|
|
* 🛡️ <https://ktag.clubk.club> 🃞
|
|
*
|
|
* 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 <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
/* Include Files */
|
|
#include "SystemK.h"
|
|
#include <stdlib.h>
|
|
|
|
#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);
|
|
}
|
|
}
|