// DR. ROBOTNIK'S RING RACERS //----------------------------------------------------------------------------- // Copyright (C) 2024 by Sally "TehRealSalt" Cochenour // Copyright (C) 2024 by Kart Krew // // This program is free software distributed under the // terms of the GNU General Public License, version 2. // See the 'LICENSE' file for more details. //----------------------------------------------------------------------------- /// \file k_vote.c /// \brief Voting screen #include "k_vote.h" #include "doomdef.h" #include "doomstat.h" #include "d_main.h" #include "f_finale.h" #include "g_game.h" #include "hu_stuff.h" #include "i_net.h" #include "i_video.h" #include "p_tick.h" #include "r_defs.h" #include "r_skins.h" #include "s_sound.h" #include "st_stuff.h" #include "v_video.h" #include "w_wad.h" #include "y_inter.h" #include "z_zone.h" #include "k_menu.h" #include "m_misc.h" #include "i_system.h" #include "p_setup.h" #include "r_local.h" #include "p_local.h" #include "m_cond.h" // condition sets #include "lua_hook.h" // IntermissionThinker hook #include "lua_hud.h" #include "lua_hudlib_drawlist.h" #include "m_random.h" // M_RandomKey #include "g_input.h" // G_PlayerInputDown #include "k_hud.h" // K_DrawMapThumbnail #include "k_battle.h" #include "k_boss.h" #include "k_pwrlv.h" #include "k_grandprix.h" #include "k_color.h" #include "music.h" #ifdef HWRENDER #include "hardware/hw_main.h" #endif // Wait for any player to vote before starting the timer. // Disabled because a player can join and be sent to // the waiting screen, and if there's only 1 player in // the vote screen they can idle for as long as they want, // effectively locking the server up. // This can be re-enabled if you want to send the vote // screen in gamestate. (I don't feel like it.) //#define VOTE_TIME_WAIT_FOR_VOTE #define UNLOAD(x) if (x) {Patch_Free(x);} x = NULL; #define CLEANUP(x) x = NULL; #define PLANET_FRAMES (9) #define TEXT_LEVEL_SCROLL (2*FRACUNIT) #define TEXT_DERR_SCROLL (2*FRACUNIT) #define ARM_FRAMES (4) #define BULB_FRAMES (4) #define SELECTOR_FRAMES (2) #define CATCHER_SPEED (8*FRACUNIT) #define CATCHER_Y_OFFSET (48*FRACUNIT) #define CATCHER_OFFSCREEN (-CATCHER_Y_OFFSET * 2) #define CATCHER_Y_OFFSET_SMALL (CATCHER_Y_OFFSET / 2) #define SELECTION_WIDTH (72*FRACUNIT) #define SELECTION_HEIGHT ((SELECTION_WIDTH * BASEVIDHEIGHT) / BASEVIDWIDTH) #define SELECTION_X (10*FRACUNIT + (SELECTION_WIDTH >> 1)) #define SELECTION_Y (144*FRACUNIT + (SELECTION_HEIGHT >> 1)) #define SELECTION_SPACE (4*FRACUNIT) #define SELECTION_SPACING_W (SELECTION_WIDTH + SELECTION_SPACE) #define SELECTION_SPACING_H (SELECTION_HEIGHT + SELECTION_SPACE) #define SELECTION_HOP (10*FRACUNIT) #define SELECTOR_SPACE (8*FRACUNIT) #define SELECTOR_Y ((SELECTION_HEIGHT / 2) + SELECTOR_SPACE) #define SELECTOR_HEIGHT ((30*FRACUNIT) + SELECTOR_SPACE) #define PILE_WIDTH (46*FRACUNIT) #define PILE_HEIGHT ((PILE_WIDTH * BASEVIDHEIGHT) / BASEVIDWIDTH) #define PILE_SPACE (4*FRACUNIT) #define PILE_SPACING_W (PILE_WIDTH + PILE_SPACE) #define PILE_SPACING_H (PILE_HEIGHT + PILE_SPACE) #define LOTS_OF_VOTES_X (120*FRACUNIT) #define LOTS_OF_VOTES_Y (80*FRACUNIT) // Give time for the animations to finish before finalizing the vote stages. #define SELECT_DELAY_TIME (TICRATE*4) #define PICK_DELAY_TIME (TICRATE/2) #define MAP_ANGER_MAX (2) // Catcher data enum { CATCHER_NA = 0, CATCHER_FG_LOWER, CATCHER_FG_GRAB, CATCHER_FG_STRUGGLE, CATCHER_FG_POPUP, CATCHER_FG_RISE, CATCHER_BG_LOWER, CATCHER_BG_RELEASE, CATCHER_BG_RISE, }; typedef struct { fixed_t x, y; fixed_t destX, destY; UINT8 spr; boolean small; UINT8 action; tic_t delay; SINT8 level; UINT8 player; fixed_t anim; // UI scope variable } y_vote_catcher; // Clientside & splitscreen player info. typedef struct { y_vote_catcher catcher; SINT8 selection; UINT8 delay; boolean sentTimeOutVote; fixed_t x, destX; } y_vote_player; // Vote "pile" data. Objects for each vote scattered about. typedef struct { fixed_t x, y; fixed_t destX, destY; y_vote_catcher catcher; } y_vote_pile; // Voting roulette variables. typedef struct { y_vote_pile pile[VOTE_TOTAL]; UINT8 anim; UINT8 tics; UINT32 offset; UINT32 endOffset; UINT8 syncTime; } y_vote_roulette; // General vote variables typedef struct { INT32 timer; INT32 tic, endtic; INT32 selectFinalize, pickFinalize; boolean notYetPicked; boolean loaded; SINT8 deferredLevel; y_vote_player players[MAXSPLITSCREENPLAYERS]; y_vote_roulette roulette; } y_vote_data; // Voting level drawing typedef struct { char str[128]; size_t str_len; boolean encore; fixed_t hop; } y_vote_draw_level; // Voting selector drawing typedef struct { fixed_t x; fixed_t destX; } y_vote_draw_selector; // General vote drawing typedef struct { patch_t *ruby_icon; fixed_t ruby_height; patch_t *bg_planet[PLANET_FRAMES]; patch_t *bg_checker; patch_t *bg_levelText; patch_t *bg_derrText; patch_t *catcher_ufo[2]; patch_t *catcher_arms[2][ARM_FRAMES]; patch_t *catcher_pole[2]; patch_t *catcher_bulb[2][BULB_FRAMES]; fixed_t selectTransition; y_vote_draw_level levels[VOTE_NUM_LEVELS]; patch_t *selector_arrow; patch_t *selector_letter[MAXSPLITSCREENPLAYERS][2]; y_vote_draw_selector selectors[MAXSPLITSCREENPLAYERS]; } y_vote_draw; static y_vote_data vote = {0}; static y_vote_draw vote_draw = {0}; boolean Y_PlayerIDCanVote(const UINT8 playerId) { player_t *player = NULL; if (playerId == VOTE_SPECIAL) { // Special vote spot, always allow return true; } if (playerId >= MAXPLAYERS || playeringame[playerId] == false) { return false; } player = &players[playerId]; if (player->spectator == true) { return false; } if (player->bot == true && cv_botscanvote.value == 0) { // Bots may only vote if the server allows it return false; } return true; } static boolean Y_PlayerCanSelect(const UINT8 localId) { const UINT8 p = g_localplayers[localId]; if (g_pickedVote != VOTE_NOT_PICKED) { return false; } if (g_votes[p] != VOTE_NOT_PICKED) { return false; } if (vote.players[localId].catcher.action != CATCHER_NA || vote.players[localId].catcher.delay > 0) { return false; } return Y_PlayerIDCanVote(p); } static void Y_SortPile(void) { UINT8 numVotes = 0; UINT8 votesLeft = 0; INT32 i; for (i = 0; i < VOTE_TOTAL; i++) { if (g_votes[i] == VOTE_NOT_PICKED) { continue; } numVotes++; } if (numVotes == 0) { return; } votesLeft = numVotes; for (i = 0; i < VOTE_TOTAL; i++) { y_vote_pile *const pile = &vote.roulette.pile[i]; if (g_votes[i] == VOTE_NOT_PICKED) { continue; } // Just center it for now. pile->destX = BASEVIDWIDTH << FRACBITS >> 1; pile->destY = BASEVIDHEIGHT << FRACBITS >> 1; if (numVotes <= 1) { ; // NOP } else if (numVotes == 2) { // Offset just a bit from the center. if (votesLeft <= 1) { pile->destX += PILE_SPACING_W >> 1; } else { pile->destX -= PILE_SPACING_W >> 1; } } else if (numVotes <= 12) { const boolean odd = ((numVotes % 2) != 0); UINT8 rowSize = (numVotes + 1) / 2; INT32 xOffset = 0; if (votesLeft > rowSize) { SINT8 topRowIndex = (rowSize - ((votesLeft - 1) % rowSize)) - 1; if (odd == true) { rowSize--; topRowIndex--; } xOffset = -(rowSize - 1) + (topRowIndex << 1); pile->destY -= PILE_SPACING_H >> 1; } else { const SINT8 botRowIndex = votesLeft - 1; xOffset = -(rowSize - 1) + (botRowIndex << 1); pile->destY += PILE_SPACING_H >> 1; } pile->destX += (PILE_SPACING_W >> 1) * xOffset; } else { angle_t a = ANGLE_90 + (ANGLE_MAX / numVotes) * (votesLeft - 1); pile->destX += FixedMul(LOTS_OF_VOTES_X, FINECOSINE(a >> ANGLETOFINESHIFT)); pile->destY += FixedMul(LOTS_OF_VOTES_Y, -FINESINE(a >> ANGLETOFINESHIFT)); } votesLeft--; if (votesLeft == 0) { break; } } } void Y_SetPlayersVote(const UINT8 playerId, SINT8 newVote) { y_vote_pile *const pile = &vote.roulette.pile[playerId]; y_vote_catcher *const catcher = &pile->catcher; if (gamestate != GS_VOTING) { return; } if (newVote < 0 || newVote >= VOTE_NUM_LEVELS) { newVote = VOTE_NOT_PICKED; } g_votes[playerId] = newVote; Y_SortPile(); pile->x = pile->destX; pile->y = pile->destY; catcher->action = CATCHER_BG_LOWER; catcher->x = catcher->destX = pile->x; catcher->y = CATCHER_OFFSCREEN; catcher->destY = pile->y; catcher->spr = ARM_FRAMES-1; catcher->level = g_votes[playerId]; catcher->player = playerId; #ifdef VOTE_TIME_WAIT_FOR_VOTE if (vote.timer == -1) { // Someone has voted, so start the timer now. vote.timer = cv_votetime.value * TICRATE; } #endif } static void Y_DrawVoteThumbnail(fixed_t center_x, fixed_t center_y, fixed_t width, INT32 flags, SINT8 v, boolean dim, SINT8 playerID) { const boolean encore = vote_draw.levels[v].encore; const fixed_t height = (width * BASEVIDHEIGHT) / BASEVIDWIDTH; const fixed_t x = center_x - (width >> 1); const fixed_t y = center_y - (height >> 1); INT32 fx, fy, fw, fh; INT32 dupx, dupy; if (v < 0 || v >= VOTE_NUM_LEVELS) { return; } dupx = vid.dupx; dupy = vid.dupy; if (flags & V_SCALEPATCHMASK) { switch ((flags & V_SCALEPATCHMASK) >> V_SCALEPATCHSHIFT) { case 1: // V_NOSCALEPATCH dupx = dupy = 1; break; case 2: // V_SMALLSCALEPATCH dupx = vid.smalldupx; dupy = vid.smalldupy; break; case 3: // V_MEDSCALEPATCH dupx = vid.meddupx; dupy = vid.meddupy; break; default: break; } } // only use one dup, to avoid stretching (har har) dupx = dupy = (dupx < dupy ? dupx : dupy); fx = FixedMul(x, dupx << FRACBITS) >> FRACBITS; fy = FixedMul(y, dupy << FRACBITS) >> FRACBITS; fw = FixedMul(width - 1, dupx << FRACBITS) >> FRACBITS; // Why does only this need -1 to match up? IDFK fh = FixedMul(height, dupy << FRACBITS) >> FRACBITS; V_AdjustXYWithSnap(&fx, &fy, flags, dupx, dupy); V_DrawFill( fx - dupx, fy - dupy, fw + (dupx << 1), fh + (dupy << 1), 0|flags|V_NOSCALESTART ); K_DrawMapThumbnail( x, y, width, flags | ((encore == true) ? V_FLIP : 0), g_voteLevels[v][0], (dim == true ? R_GetTranslationColormap(TC_RAINBOW, SKINCOLOR_GREY, GTC_MENUCACHE) : NULL) ); if (encore == true) { const fixed_t rubyScale = width / 72; V_DrawFixedPatch( center_x, center_y - FixedMul(vote_draw.ruby_height << 1, rubyScale), rubyScale, flags, vote_draw.ruby_icon, NULL ); } if (dim == true) { V_DrawFadeFill( fx, fy, fw, fh, flags|V_NOSCALESTART, 31, 5 ); } if (playerID >= 0) { const INT32 whiteSq = 16 * dupx; if (playerID < MAXPLAYERS) { UINT8 *playerMap = R_GetTranslationColormap(players[playerID].skin, players[playerID].skincolor, GTC_CACHE); patch_t *playerPatch = faceprefix[players[playerID].skin][FACE_RANK]; V_DrawFixedPatch( (fx + fw - whiteSq + dupx) * FRACUNIT, (fy + fh - whiteSq + dupy) * FRACUNIT, FRACUNIT, flags|V_NOSCALESTART, playerPatch, playerMap ); } else { const fixed_t iconHeight = (14 << FRACBITS); const fixed_t iconWidth = (iconHeight * 320) / 200; V_DrawFill( fx + fw - whiteSq + dupx, fy + fh - whiteSq + dupy, whiteSq, whiteSq, 0|flags|V_NOSCALESTART ); V_SetClipRect( fx + fw - whiteSq + (2 * dupx), fy + fh - whiteSq + (2 * dupy), whiteSq - (2 * dupx), whiteSq - (2 * dupy), flags|V_NOSCALESTART ); K_DrawMapThumbnail( ((fx + fw - whiteSq + (2 * dupx)) * FRACUNIT) - (iconWidth - iconHeight), (fy + fh - whiteSq + (2 * dupy)) * FRACUNIT, iconWidth, flags | V_NOSCALESTART | ((encore == true) ? V_FLIP : 0), g_voteLevels[v][0], NULL ); V_ClearClipRect(); } } } static void Y_DrawCatcher(y_vote_catcher *catcher) { #define NUM_UFO_COLORS (8) static const skincolornum_t ufoColors[NUM_UFO_COLORS] = { SKINCOLOR_EMERALD, SKINCOLOR_SWAMP, SKINCOLOR_TAFFY, SKINCOLOR_ROSE, SKINCOLOR_CYAN, SKINCOLOR_NAVY, SKINCOLOR_GOLD, SKINCOLOR_BRONZE, }; #define NUM_BULB_COLORS (2) static const skincolornum_t bulbColors[NUM_BULB_COLORS] = { SKINCOLOR_JAWZ, SKINCOLOR_LILAC, }; const UINT8 sizeOffset = (catcher->small == true) ? 1 : 0; tic_t colorTic = 0; fixed_t baseX = INT32_MAX; fixed_t x = INT32_MAX; fixed_t y = INT32_MAX; UINT8 *craneColor = NULL; UINT8 *bulbColor = NULL; if (catcher->action == CATCHER_NA) { // Don't display in the empty state return; } catcher->anim += renderdeltatics; colorTic = (catcher->anim / 3) / FRACUNIT; baseX = catcher->x; if (catcher->action == CATCHER_FG_STRUGGLE) { if ((catcher->anim / FRACUNIT) & 1) { baseX += FRACUNIT; } else { baseX -= FRACUNIT; } } x = baseX - (vote_draw.catcher_ufo[sizeOffset]->width * FRACUNIT / 2); y = catcher->y - (vote_draw.catcher_ufo[sizeOffset]->height * FRACUNIT) + ((catcher->small == true) ? CATCHER_Y_OFFSET_SMALL : CATCHER_Y_OFFSET); craneColor = R_GetTranslationColormap(TC_DEFAULT, ufoColors[colorTic % NUM_UFO_COLORS], GTC_MENUCACHE); bulbColor = R_GetTranslationColormap(TC_DEFAULT, bulbColors[(colorTic / BULB_FRAMES) % NUM_BULB_COLORS], GTC_MENUCACHE); if (catcher->level != VOTE_NOT_PICKED) { Y_DrawVoteThumbnail( baseX, catcher->y, ((catcher->small == true) ? PILE_WIDTH : SELECTION_WIDTH), 0, catcher->level, false, catcher->player ); } V_DrawFixedPatch( x, y, FRACUNIT, 0, vote_draw.catcher_arms[sizeOffset][catcher->spr % ARM_FRAMES], craneColor ); V_DrawFixedPatch( x, y, FRACUNIT, 0, vote_draw.catcher_bulb[sizeOffset][colorTic % BULB_FRAMES], bulbColor ); V_DrawFixedPatch( x, y, FRACUNIT, 0, vote_draw.catcher_ufo[sizeOffset], craneColor ); V_DrawFixedPatch( x, y, FRACUNIT, 0, vote_draw.catcher_pole[sizeOffset], NULL ); #undef NUM_UFO_COLORS #undef NUM_BULB_COLORS } // // Y_VoteDrawer // // Draws the voting screen! // static void Y_DrawVoteBackground(void) { static UINT32 bgTimer = 0; static fixed_t derrPos = 0; const fixed_t derrLoop = vote_draw.bg_derrText->width * FRACUNIT; static fixed_t levelPos = 0; const fixed_t levelLoop = vote_draw.bg_levelText->height * FRACUNIT; if (cv_reducevfx.value) { bgTimer = 0; } const UINT8 planetFrame = (bgTimer / FRACUNIT) % PLANET_FRAMES; V_DrawFixedPatch( 0, 0, FRACUNIT, 0, vote_draw.bg_planet[planetFrame], NULL ); V_DrawFixedPatch( (BASEVIDWIDTH - vote_draw.bg_checker->width) * FRACUNIT, 0, FRACUNIT, V_ADD|V_TRANSLUCENT, vote_draw.bg_checker, NULL ); V_DrawFixedPatch( (BASEVIDWIDTH - vote_draw.bg_checker->width) * FRACUNIT, 0, FRACUNIT, V_ADD|V_TRANSLUCENT, vote_draw.bg_checker, NULL ); levelPos += FixedMul(TEXT_DERR_SCROLL, renderdeltatics); while (levelPos > levelLoop) { levelPos -= levelLoop; } V_DrawFixedPatch( ((BASEVIDWIDTH - vote_draw.bg_levelText->width) * FRACUNIT) - levelPos, -levelPos, FRACUNIT, V_ADD, vote_draw.bg_levelText, NULL ); V_DrawFixedPatch( ((BASEVIDWIDTH - vote_draw.bg_levelText->width) * FRACUNIT) - levelPos + levelLoop, -levelPos + levelLoop, FRACUNIT, V_ADD, vote_draw.bg_levelText, NULL ); derrPos += FixedMul(TEXT_DERR_SCROLL, renderdeltatics); while (derrPos > derrLoop) { derrPos -= derrLoop; } V_DrawFixedPatch( -derrPos, (BASEVIDHEIGHT - vote_draw.bg_derrText->height) * FRACUNIT, FRACUNIT, V_SUBTRACT, vote_draw.bg_derrText, NULL ); V_DrawFixedPatch( -derrPos + derrLoop, (BASEVIDHEIGHT - vote_draw.bg_derrText->height) * FRACUNIT, FRACUNIT, V_SUBTRACT, vote_draw.bg_derrText, NULL ); if (!cv_reducevfx.value) { bgTimer += renderdeltatics; } } static void Y_DrawVoteSelector(const fixed_t y, const fixed_t time, const UINT8 localPlayer) { const fixed_t destX = SELECTION_X + (vote.players[localPlayer].selection * SELECTION_SPACING_W); vote_draw.selectors[localPlayer].x += FixedMul( (destX - vote_draw.selectors[localPlayer].x) * 3 / 4, renderdeltatics ); if (Y_PlayerCanSelect(localPlayer) == false) { return; } static const UINT8 freq = 7; UINT8 *colormap = NULL; if (splitscreen > 0) { const UINT8 blink = ((time / freq / FRACUNIT) & 1); colormap = R_GetTranslationColormap(TC_RAINBOW, players[ g_localplayers[localPlayer] ].skincolor, GTC_CACHE); V_DrawFixedPatch( vote_draw.selectors[localPlayer].x, y - SELECTOR_Y - (9*FRACUNIT), FRACUNIT, 0, vote_draw.selector_letter[localPlayer][blink], colormap ); } fixed_t bob = FixedMul((time / freq * 2) + (FRACUNIT / 2), ANGLE_90); if (localPlayer & 1) { bob = FCOS(bob); } else { bob = FSIN(bob); } V_DrawFixedPatch( vote_draw.selectors[localPlayer].x, y - SELECTOR_Y + bob, FRACUNIT, 0, vote_draw.selector_arrow, colormap ); } static void Y_DrawVoteSelection(fixed_t offset) { static fixed_t animTimer = 0; animTimer += renderdeltatics; const size_t charAnim = animTimer / FRACUNIT / 4; fixed_t x = SELECTION_X; fixed_t y = SELECTION_Y + FixedMul(offset, (SELECTION_HEIGHT + SELECTOR_HEIGHT) * 2); INT32 i; // // Draw map icons // for (i = 0; i < VOTE_NUM_LEVELS; i++) { boolean selected = false; fixed_t destHop = 0; INT32 j; for (j = 0; j <= splitscreen; j++) // another loop for drawing the selection backgrounds in the right order, grumble grumble.. { const UINT8 p = g_localplayers[j]; if (vote.players[j].selection != i) { continue; } if (g_votes[p] != VOTE_NOT_PICKED || Y_PlayerIDCanVote(p) == false) { continue; } selected = true; break; } if (selected == true) { destHop = SELECTION_HOP; } vote_draw.levels[i].hop += FixedMul( (destHop - vote_draw.levels[i].hop) / 2, renderdeltatics ); if (vote_draw.levels[i].hop > FRACUNIT >> 2) { const fixed_t height = (SELECTION_WIDTH * BASEVIDHEIGHT) / BASEVIDWIDTH; const fixed_t tx = x - (SELECTION_WIDTH >> 1); const fixed_t ty = y + (height >> 1); INT32 fx, fy, fw, fh; INT32 dupx, dupy; dupx = vid.dupx; dupy = vid.dupy; // only use one dup, to avoid stretching (har har) dupx = dupy = (dupx < dupy ? dupx : dupy); fx = FixedMul(tx, dupx << FRACBITS) >> FRACBITS; fy = FixedMul(ty, dupy << FRACBITS) >> FRACBITS; fw = FixedMul(SELECTION_WIDTH - 1, dupx << FRACBITS) >> FRACBITS; // Why does only this need -1 to match up? IDFK fh = FixedMul(SELECTION_HOP, dupy << FRACBITS) >> FRACBITS; V_AdjustXYWithSnap(&fx, &fy, 0, dupx, dupy); V_DrawFill( fx - dupx, fy - fh + dupy, fw + (dupx << 1), fh, 31|V_NOSCALESTART ); size_t ci; for (ci = 0; ci < 12; ci++) { const size_t c = (ci + charAnim) % vote_draw.levels[i].str_len; V_DrawCharacterScaled( (fx + (6 * dupx * ci)) << FRACBITS, (fy - fh + dupy) << FRACBITS, FRACUNIT, V_ORANGEMAP | V_FORCEUPPERCASE | V_NOSCALESTART, MED_FONT, vote_draw.levels[i].str[c], NULL ); } } Y_DrawVoteThumbnail( x, y - vote_draw.levels[i].hop, SELECTION_WIDTH, 0, i, (selected == false), -1 ); x += SELECTION_SPACING_W; } // // Draw our catchers // for (i = 0; i <= splitscreen; i++) { Y_DrawCatcher(&vote.players[i].catcher); } if (offset != FRACUNIT) { // // Draw splitscreen selectors // //if (splitscreen > 0) { const UINT8 priority = vote.tic % (splitscreen + 1); for (i = 0; i <= splitscreen; i++) { if (i == priority) { continue; } Y_DrawVoteSelector(y, animTimer, i); } Y_DrawVoteSelector(y, animTimer, priority); } } } static void Y_DrawVotePile(void) { INT32 i; for (i = 0; i < VOTE_TOTAL; i++) { y_vote_pile *const pile = &vote.roulette.pile[i]; y_vote_catcher *const catcher = &pile->catcher; if (catcher->level != VOTE_NOT_PICKED) { continue; } if (g_votes[i] == VOTE_NOT_PICKED) { continue; } Y_DrawVoteThumbnail( pile->x, pile->y, PILE_WIDTH, 0, g_votes[i], (i != vote.roulette.anim || g_pickedVote == VOTE_NOT_PICKED), i ); } for (i = 0; i < VOTE_TOTAL; i++) { Y_DrawCatcher(&vote.roulette.pile[i].catcher); } } void Y_VoteDrawer(void) { static angle_t rubyFloatTime = 0; if (rendermode == render_none) { return; } if (vote.tic >= vote.endtic && vote.endtic != -1) { return; } if (vote.loaded == false) { return; } vote_draw.ruby_height = FINESINE(rubyFloatTime >> ANGLETOFINESHIFT); rubyFloatTime += FixedMul(ANGLE_MAX / NEWTICRATE, renderdeltatics); if (vote.loaded == true) { boolean slideOut = true; INT32 i; for (i = 0; i <= splitscreen; i++) { if (g_votes[ g_localplayers[i] ] == VOTE_NOT_PICKED) { slideOut = false; break; } } vote_draw.selectTransition += FixedMul( ((slideOut ? FRACUNIT : 0) - vote_draw.selectTransition) / 2, renderdeltatics ); } Y_DrawVoteBackground(); Y_DrawVotePile(); Y_DrawVoteSelection(vote_draw.selectTransition); if (vote.timer > 0) { const INT32 tickDown = (vote.timer + 1) / TICRATE; // See also y_inter.c V__DrawOneScaleString( 2*FRACUNIT, (BASEVIDHEIGHT - (2+8))*FRACUNIT, FRACUNIT, 0, NULL, OPPRF_FONT, va("%d", tickDown) ); } M_DrawMenuForeground(); } // // Y_VoteStop // // Vote screen's selection stops moving // static void Y_FinalizeVote(const SINT8 level) { nextmap = g_voteLevels[level][0]; deferencoremode = ((g_voteLevels[level][1] & VOTE_MOD_ENCORE) == VOTE_MOD_ENCORE); } static void Y_VoteStops(SINT8 pick, SINT8 level) { Y_FinalizeVote(level); if (netgame && P_IsPartyPlayer(&players[pick])) { S_StartSound(NULL, sfx_yeeeah); // yeeeah! } else { S_StartSound(NULL, sfx_kc48); // just a cool sound } } static void Y_PlayerSendVote(const UINT8 localPlayer) { y_vote_player *const player = &vote.players[localPlayer]; y_vote_catcher *const catcher = &player->catcher; catcher->action = CATCHER_FG_LOWER; catcher->x = catcher->destX = SELECTION_X + (SELECTION_SPACING_W * player->selection); catcher->y = CATCHER_OFFSCREEN; catcher->destY = SELECTION_Y - SELECTION_HOP; catcher->spr = 0; catcher->level = VOTE_NOT_PICKED; S_StartSound(NULL, sfx_kc37); } static boolean Y_TickGenericCatcher(y_vote_catcher *const catcher) { fixed_t spd = CATCHER_SPEED; fixed_t xDelta = catcher->destX - catcher->x; if (xDelta != 0) { // Move X position first if (abs(xDelta) <= spd) { catcher->x = catcher->destX; } else { if (xDelta < 0) { catcher->x -= spd; } else { catcher->x += spd; } } } else { // Then start moving Y position fixed_t yDelta = catcher->destY - catcher->y; if (abs(yDelta) <= spd) { catcher->y = catcher->destY; } else { if (yDelta < 0) { catcher->y -= spd; } else { catcher->y += spd; } } } if (catcher->delay > 0) { catcher->delay--; return false; } return true; } static void Y_TickPlayerCatcher(const UINT8 localPlayer) { y_vote_player *const player = &vote.players[localPlayer]; y_vote_catcher *const catcher = &player->catcher; if (Y_TickGenericCatcher(catcher) == false) { return; } switch (catcher->action) { case CATCHER_FG_LOWER: { if (catcher->x == catcher->destX && catcher->y == catcher->destY) { catcher->action++; S_StopSoundByNum(sfx_kc37); S_StartSound(NULL, sfx_kc68); } break; } case CATCHER_FG_GRAB: { catcher->spr++; if (catcher->spr >= ARM_FRAMES-1) { catcher->action = CATCHER_FG_STRUGGLE; catcher->level = vote.players[localPlayer].selection; catcher->delay = 20; } break; } case CATCHER_FG_STRUGGLE: { catcher->action = CATCHER_FG_POPUP; catcher->destY -= SELECTION_HOP * 3; catcher->delay = 15; break; } case CATCHER_FG_POPUP: { catcher->action = CATCHER_FG_RISE; catcher->destY = CATCHER_OFFSCREEN; S_StartSound(NULL, sfx_kc37); break; } case CATCHER_FG_RISE: { if (catcher->x == catcher->destX && catcher->y == catcher->destY) { D_ModifyClientVote(g_localplayers[localPlayer], vote.players[localPlayer].selection); catcher->action = CATCHER_NA; catcher->delay = 5; S_StopSoundByNum(sfx_kc37); } break; } default: { catcher->action = CATCHER_NA; break; } } } static void Y_TickPileCatcher(const UINT8 playerId) { y_vote_pile *const pile = &vote.roulette.pile[playerId]; y_vote_catcher *const catcher = &pile->catcher; if (Y_TickGenericCatcher(catcher) == false) { return; } switch (catcher->action) { case CATCHER_BG_LOWER: { if (catcher->x == catcher->destX && catcher->y == catcher->destY) { catcher->level = VOTE_NOT_PICKED; catcher->action++; } break; } case CATCHER_BG_RELEASE: { catcher->spr--; if (catcher->spr == 0) { catcher->destY = CATCHER_OFFSCREEN; catcher->action = CATCHER_BG_RISE; } break; } case CATCHER_BG_RISE: { if (catcher->x == catcher->destX && catcher->y == catcher->destY) { catcher->action = CATCHER_NA; } break; } default: { catcher->action = CATCHER_NA; break; } } } static void Y_TickPlayerPile(const UINT8 playerId) { y_vote_pile *const pile = &vote.roulette.pile[playerId]; y_vote_catcher *const catcher = &pile->catcher; fixed_t movedX = 0; fixed_t movedY = 0; if (g_votes[playerId] == VOTE_NOT_PICKED) { catcher->action = CATCHER_NA; return; } movedX = (pile->destX - pile->x) / 2; movedY = (pile->destY - pile->y) / 2; if (movedX != 0 || movedY != 0) { pile->x += movedX; pile->y += movedY; catcher->x += movedX; catcher->y += movedY; catcher->destX += movedX; catcher->destY += movedY; } Y_TickPileCatcher(playerId); } static void Y_TickVoteRoulette(void) { INT32 i; vote.timer = 0; vote.roulette.syncTime++; if (vote.endtic == -1) { UINT8 tempvotes[VOTE_TOTAL]; UINT8 numvotes = 0; for (i = 0; i < VOTE_TOTAL; i++) { if (g_votes[i] == VOTE_NOT_PICKED) { continue; } tempvotes[numvotes] = i; numvotes++; } if (numvotes < 1) // Whoops! Get outta here. { Y_EndVote(); G_AfterIntermission(); return; } if (vote.roulette.tics > 0) { vote.roulette.tics--; } else { vote.roulette.offset++; vote.roulette.tics = min(5, 7 * vote.roulette.offset / 40); S_StartSound(NULL, sfx_kc39); } if (vote.roulette.endOffset == 0 || vote.roulette.offset < vote.roulette.endOffset) { vote.roulette.anim = tempvotes[((g_pickedVote + vote.roulette.offset) % numvotes)]; } if (vote.roulette.offset > 20) { if (vote.roulette.endOffset == 0) { if (vote.roulette.syncTime % 51 == 0) // Song is 1.45 seconds long (sorry @ whoever wants to replace it in a music wad :V) { for (i = 5; i >= 3; i--) // Find a suitable place to stop { if (tempvotes[((g_pickedVote + vote.roulette.offset + i) % numvotes)] == g_pickedVote) { vote.roulette.endOffset = vote.roulette.offset + i; if (M_RandomChance(FRACUNIT/4)) // Let it cheat occasionally~ { vote.roulette.endOffset++; } Music_Play("vote_end"); break; } } } } else if (vote.roulette.offset >= vote.roulette.endOffset) { vote.endtic = vote.tic + (3*TICRATE); Y_VoteStops(g_pickedVote, vote.deferredLevel); } } } else { vote.roulette.anim = g_pickedVote; } } static void Y_TryMapAngerVote(void) { SINT8 angryMaps[VOTE_NUM_LEVELS] = { -1 }; size_t angryMapsCount = 0; boolean mapVoted[VOTE_NUM_LEVELS] = { false }; INT32 pick = 0; INT32 numPlayers = 0; INT32 i = 0; for (i = 0; i < MAXPLAYERS; i++) { if (Y_PlayerIDCanVote(i) == false) { continue; } numPlayers++; if (g_votes[i] != VOTE_NOT_PICKED) { mapVoted[ g_votes[i] ] = true; } } if (numPlayers < 3) { // Don't handle map anger if there's not enough players. return; } for (i = 0; i < VOTE_NUM_LEVELS; i++) { const INT16 mapID = g_voteLevels[i][0]; if (mapVoted[i] == true) { // Someone voted for us, no need to be angry anymore :) mapheaderinfo[ mapID ]->anger = 0; } else { // Increment map anger for maps that weren't picked by a single soul. mapheaderinfo[ mapID ]->anger++; if (mapheaderinfo[ mapID ]->anger > MAP_ANGER_MAX) { // If they are angry enough, then it can vote for itself! angryMaps[ angryMapsCount ] = i; angryMapsCount++; } } } if (angryMapsCount == 0) { return; } // Set the special vote to a random angry map. pick = M_RandomKey(angryMapsCount); D_ModifyClientVote(UINT8_MAX, angryMaps[pick]); } static void Y_TickVoteSelection(void) { boolean everyone_voted = true;/* the default condition */ INT32 i; if (vote.tic < 3*(NEWTICRATE/7)) // give it some time before letting you control it :V { return; } /* The vote ended, but it will take at least a tic for that to reach us from the server. Don't let me change the vote now, it won't matter anyway! */ for (i = 0; i <= splitscreen; i++) { boolean moved = false; if (Y_PlayerCanSelect(i) == true) { if (vote.players[i].delay > 0) { vote.players[i].delay--; } else if (vote.timer == 0) { // Time's up, send our vote ASAP. if (vote.players[i].sentTimeOutVote == false) { Y_PlayerSendVote(i); vote.players[i].sentTimeOutVote = true; vote.players[i].delay = NEWTICRATE/7; } } else if (menuactive == false && chat_on == false) { if (G_PlayerInputDown(i, gc_left, 0)) { vote.players[i].selection--; moved = true; } if (G_PlayerInputDown(i, gc_right, 0)) { vote.players[i].selection++; moved = true; } if (vote.players[i].selection < 0) { vote.players[i].selection = VOTE_NUM_LEVELS - 1; } if (vote.players[i].selection >= VOTE_NUM_LEVELS) { vote.players[i].selection = 0; } if (G_PlayerInputDown(i, gc_a, 0) && moved == false) { Y_PlayerSendVote(i); moved = true; } } } if (moved == true) { S_StartSound(NULL, sfx_kc4a); vote.players[i].delay = NEWTICRATE/7; } } for (i = 0; i < MAXPLAYERS; i++) { if (Y_PlayerIDCanVote(i) == false) { continue; } if (players[i].bot == true && g_votes[i] == VOTE_NOT_PICKED) { if (( M_RandomFixed() % 100 ) == 0) { // bots vote randomly D_ModifyClientVote(i, M_RandomKey(VOTE_NUM_LEVELS)); } } if (g_votes[i] == VOTE_NOT_PICKED) { everyone_voted = false; } } if (everyone_voted == true) { vote.timer = 0; vote.selectFinalize = SELECT_DELAY_TIME; } if (vote.timer == 0) { if (vote.selectFinalize < SELECT_DELAY_TIME) { vote.selectFinalize++; } } else { vote.selectFinalize = 0; } if (vote.selectFinalize >= SELECT_DELAY_TIME) { if (vote.pickFinalize < PICK_DELAY_TIME) { vote.pickFinalize++; } else if (vote.endtic == -1) { vote.notYetPicked = false; /* don't pick vote twice */ if (server) { Y_TryMapAngerVote(); D_PickVote(); } } } else if (vote.timer > 0) { vote.timer--; vote.pickFinalize = 0; } } // // Y_VoteTicker // // Vote screen thinking :eggthinking: // void Y_VoteTicker(void) { INT32 i; if (paused || P_AutoPause() || vote.loaded == false) { return; } LUA_HOOK(VoteThinker); vote.tic++; if (vote.tic == vote.endtic) { Y_EndVote(); G_AfterIntermission(); return; } // Correct invalid votes as early as possible, // before they're processed by the rest of the ticker for (i = 0; i < MAXPLAYERS; i++) { if (Y_PlayerIDCanVote(i) == false) { // Spectators are the lower class, and have // effectively no voice in the government. Democracy sucks. g_votes[i] = VOTE_NOT_PICKED; } } if (server && g_pickedVote != VOTE_NOT_PICKED && g_votes[g_pickedVote] == VOTE_NOT_PICKED) // Uh oh! The person who got picked left! Recalculate, quick! { D_PickVote(); } if (vote.tic == 0) { Music_Play("vote"); } if (g_pickedVote != VOTE_NOT_PICKED) { Y_TickVoteRoulette(); } else if (vote.notYetPicked == true) { Y_TickVoteSelection(); } for (i = 0; i <= splitscreen; i++) { Y_TickPlayerCatcher(i); } Y_SortPile(); for (i = 0; i < VOTE_TOTAL; i++) { Y_TickPlayerPile(i); } } // // Y_StartVote // // MK online style voting screen, appears after intermission // static void Y_InitVoteDrawing(void) { INT32 i = 0, j = 0; vote_draw.ruby_icon = W_CachePatchName("RUBYICON", PU_STATIC); for (i = 0; i < PLANET_FRAMES; i++) { vote_draw.bg_planet[i] = W_CachePatchName(va("VT_BG_%d", i + 1), PU_STATIC); } vote_draw.bg_checker = W_CachePatchName("VT_RACE", PU_STATIC); vote_draw.bg_levelText = W_CachePatchName("VT_WELC", PU_STATIC); vote_draw.bg_derrText = W_CachePatchName("VT_DERR", PU_STATIC); vote_draw.catcher_ufo[0] = W_CachePatchName("VT_UFO1", PU_STATIC); vote_draw.catcher_ufo[1] = W_CachePatchName("VS_UFO1", PU_STATIC); for (i = 0; i < ARM_FRAMES; i++) { vote_draw.catcher_arms[0][i] = W_CachePatchName(va("VT_ARMS%d", i + 1), PU_STATIC); vote_draw.catcher_arms[1][i] = W_CachePatchName(va("VS_ARMS%d", i + 1), PU_STATIC); } vote_draw.catcher_pole[0] = W_CachePatchName("VT_POLE", PU_STATIC); vote_draw.catcher_pole[1] = W_CachePatchName("VS_POLE", PU_STATIC); for (i = 0; i < BULB_FRAMES; i++) { vote_draw.catcher_bulb[0][i] = W_CachePatchName(va("VT_BULB%d", i + 1), PU_STATIC); vote_draw.catcher_bulb[1][i] = W_CachePatchName(va("VS_BULB%d", i + 1), PU_STATIC); } for (i = 0; i < VOTE_NUM_LEVELS; i++) { const mapheader_t *header = mapheaderinfo[g_voteLevels[i][0]]; // set up the encore vote_draw.levels[i].encore = (g_voteLevels[i][1] & VOTE_MOD_ENCORE); // set up the level title string memset(vote_draw.levels[i].str, 0, sizeof(vote_draw.levels[i].str)); vote_draw.levels[i].str_len = 0; vote_draw.levels[i].str_len += snprintf( vote_draw.levels[i].str + vote_draw.levels[i].str_len, sizeof(vote_draw.levels[i].str) - vote_draw.levels[i].str_len, "%s", header->lvlttl ); if (header->zonttl[0]) { vote_draw.levels[i].str_len += snprintf( vote_draw.levels[i].str + vote_draw.levels[i].str_len, sizeof(vote_draw.levels[i].str) - vote_draw.levels[i].str_len, " %s", header->zonttl ); } if (header->actnum > 0) { vote_draw.levels[i].str_len += snprintf( vote_draw.levels[i].str + vote_draw.levels[i].str_len, sizeof(vote_draw.levels[i].str) - vote_draw.levels[i].str_len, " %d", header->actnum ); } vote_draw.levels[i].str_len += snprintf( vote_draw.levels[i].str + vote_draw.levels[i].str_len, sizeof(vote_draw.levels[i].str) - vote_draw.levels[i].str_len, " " ); } for (i = 0; i < MAXSPLITSCREENPLAYERS; i++) { y_vote_player *const player = &vote.players[i]; vote_draw.selectors[i].x = vote_draw.selectors[i].destX = SELECTION_X + (player->selection * SELECTION_SPACING_W); for (j = 0; j < SELECTOR_FRAMES; j++) { vote_draw.selector_letter[i][j] = W_CachePatchName(va("VSSPTR%c%d", 'A' + i, j + 1), PU_STATIC); } } vote_draw.selector_arrow = W_CachePatchName("VSSPTR1", PU_STATIC); vote_draw.selectTransition = FRACUNIT; } void Y_StartVote(void) { INT32 i = 0; memset(&vote, 0, sizeof(vote)); memset(&vote_draw, 0, sizeof(vote_draw)); // Restarting vote from the menu: stop any long sounds // that were playing (kc37). S_StopSounds(); vote.tic = vote.endtic = -1; #ifdef VOTE_TIME_WAIT_FOR_VOTE vote.timer = -1; // Timer is not set until the first vote is added #else vote.timer = cv_votetime.value * TICRATE; #endif g_pickedVote = VOTE_NOT_PICKED; vote.notYetPicked = true; for (i = 0; i < MAXSPLITSCREENPLAYERS; i++) { y_vote_player *const player = &vote.players[i]; y_vote_catcher *const catcher = &player->catcher; player->selection = (i % VOTE_NUM_LEVELS); catcher->action = CATCHER_NA; catcher->small = false; catcher->player = -1; } for (i = 0; i < VOTE_TOTAL; i++) { y_vote_pile *const pile = &vote.roulette.pile[i]; y_vote_catcher *const catcher = &pile->catcher; g_votes[i] = VOTE_NOT_PICKED; catcher->action = CATCHER_NA; catcher->small = true; catcher->player = i; } Y_InitVoteDrawing(); vote.loaded = true; Automate_Run(AEV_VOTESTART); } // // Y_UnloadVoteData // static void Y_UnloadVoteData(void) { INT32 i, j; vote.loaded = false; if (rendermode != render_soft) { return; } UNLOAD(vote_draw.ruby_icon); for (i = 0; i < PLANET_FRAMES; i++) { UNLOAD(vote_draw.bg_planet[i]); } UNLOAD(vote_draw.bg_checker); UNLOAD(vote_draw.bg_levelText); UNLOAD(vote_draw.bg_derrText); for (j = 0; j < 2; j++) { UNLOAD(vote_draw.catcher_ufo[j]); for (i = 0; i < ARM_FRAMES; i++) { UNLOAD(vote_draw.catcher_arms[j][i]); } UNLOAD(vote_draw.catcher_pole[j]); for (i = 0; i < BULB_FRAMES; i++) { UNLOAD(vote_draw.catcher_bulb[j][i]); } } for (j = 0; j < MAXSPLITSCREENPLAYERS; j++) { for (i = 0; i < SELECTOR_FRAMES; i++) { UNLOAD(vote_draw.selector_letter[j][i]); } } UNLOAD(vote_draw.selector_arrow); } // // Y_EndVote // void Y_EndVote(void) { if (nextmap >= NEXTMAP_SPECIAL) { // Don't leave nextmap unset if the vote is ended through // weird means! (such as a dedicated server becoming empty) // If nextmap was left at NEXTMAP_VOTING, we'd crash! Y_FinalizeVote(0); } Y_UnloadVoteData(); vote.endtic = -1; } // // Y_SetupVoteFinish // enum { VOTE_END_IMMEDIATE = 0, VOTE_END_QUICK, VOTE_END_NORMAL, }; void Y_SetupVoteFinish(SINT8 pick, SINT8 level) { if (vote.loaded == false) { return; } if (pick == VOTE_NOT_PICKED || level == VOTE_NOT_PICKED) // No other votes? We gotta get out of here, then! { Y_EndVote(); G_AfterIntermission(); return; } if (g_pickedVote == VOTE_NOT_PICKED) { INT32 i; SINT8 votecompare = VOTE_NOT_PICKED; INT32 endtype = VOTE_END_IMMEDIATE; vote.roulette.syncTime = 0; for (i = 0; i < VOTE_TOTAL; i++) { if (g_votes[i] == VOTE_NOT_PICKED) { continue; } if (votecompare == VOTE_NOT_PICKED) { votecompare = g_votes[i]; endtype = VOTE_END_QUICK; } else if (g_votes[i] != votecompare) { endtype = VOTE_END_NORMAL; break; } } switch (endtype) { case VOTE_END_IMMEDIATE: { // Might as well put it here, too, just in case. Y_EndVote(); G_AfterIntermission(); return; } case VOTE_END_QUICK: { // Only one unique vote, so just end it immediately. vote.endtic = vote.tic + (5*TICRATE); Music_Play("vote_end"); Y_VoteStops(pick, level); break; } default: { Music_Play("vote_suspense"); break; } } } vote.deferredLevel = level; g_pickedVote = pick; vote.timer = -1; vote.selectFinalize = SELECT_DELAY_TIME; vote.pickFinalize = PICK_DELAY_TIME; }