diff --git a/src/d_clisrv.c b/src/d_clisrv.c index 691fa1b55..c4a85bfad 100644 --- a/src/d_clisrv.c +++ b/src/d_clisrv.c @@ -918,7 +918,7 @@ static boolean CL_SendJoin(void) for (; i < MAXSPLITSCREENPLAYERS; i++) strncpy(netbuffer->u.clientcfg.names[i], va("Player %c", 'A' + i), MAXPLAYERNAME); - memcpy(&netbuffer->u.clientcfg.availabilities, R_GetSkinAvailabilities(false, false), MAXAVAILABILITY*sizeof(UINT8)); + memcpy(&netbuffer->u.clientcfg.availabilities, R_GetSkinAvailabilities(false, -1), MAXAVAILABILITY*sizeof(UINT8)); // Don't leak old signatures from prior sessions. memset(&netbuffer->u.clientcfg.challengeResponse, 0, sizeof(((clientconfig_pak *)0)->challengeResponse)); @@ -3968,7 +3968,7 @@ boolean SV_SpawnServer(void) // strictly speaking, i'm not convinced the following is necessary // but I'm not confident enough to remove it entirely in case it breaks something { - UINT8 *availabilitiesbuffer = R_GetSkinAvailabilities(false, false); + UINT8 *availabilitiesbuffer = R_GetSkinAvailabilities(false, -1); SINT8 node = 0; for (; node < MAXNETNODES; node++) result |= SV_AddWaitingPlayers(node, availabilitiesbuffer, @@ -5607,6 +5607,9 @@ static INT16 Consistancy(void) mo = (mobj_t *)th; + if (TypeIsNetSynced(mo->type) == false) + continue; + if (mo->flags & (MF_SPECIAL | MF_SOLID | MF_PUSHABLE | MF_BOSS | MF_MISSILE | MF_SPRING | MF_MONITOR | MF_FIRE | MF_ENEMY | MF_PAIN | MF_STICKY)) { ret -= mo->type; @@ -5620,7 +5623,7 @@ static INT16 Consistancy(void) ret -= mo->flags; ret += mo->flags2; ret -= mo->eflags; - if (mo->target) + if (mo->target && TypeIsNetSynced(mo->target->type)) { ret += mo->target->type; ret -= mo->target->x; @@ -5636,11 +5639,11 @@ static INT16 Consistancy(void) ret -= mo->target->state - states; ret += mo->target->tics; ret -= mo->target->sprite; - ret += mo->target->frame; + //ret += mo->target->frame; } else ret ^= 0x3333; - if (mo->tracer && mo->tracer->type != MT_OVERLAY) + if (mo->tracer && TypeIsNetSynced(mo->tracer->type)) { ret += mo->tracer->type; ret -= mo->tracer->x; @@ -5656,12 +5659,12 @@ static INT16 Consistancy(void) ret -= mo->tracer->state - states; ret += mo->tracer->tics; ret -= mo->tracer->sprite; - ret += mo->tracer->frame; + //ret += mo->tracer->frame; } else ret ^= 0xAAAA; // SRB2Kart: We use hnext & hprev very extensively - if (mo->hnext && mo->hnext->type != MT_OVERLAY) + if (mo->hnext && TypeIsNetSynced(mo->hnext->type)) { ret += mo->hnext->type; ret -= mo->hnext->x; @@ -5677,11 +5680,11 @@ static INT16 Consistancy(void) ret -= mo->hnext->state - states; ret += mo->hnext->tics; ret -= mo->hnext->sprite; - ret += mo->hnext->frame; + //ret += mo->hnext->frame; } else ret ^= 0x5555; - if (mo->hprev && mo->hprev->type != MT_OVERLAY) + if (mo->hprev && TypeIsNetSynced(mo->hprev->type)) { ret += mo->hprev->type; ret -= mo->hprev->x; @@ -5697,14 +5700,14 @@ static INT16 Consistancy(void) ret -= mo->hprev->state - states; ret += mo->hprev->tics; ret -= mo->hprev->sprite; - ret += mo->hprev->frame; + //ret += mo->hprev->frame; } else ret ^= 0xCCCC; ret -= mo->state - states; ret += mo->tics; ret -= mo->sprite; - ret += mo->frame; + //ret += mo->frame; } } } diff --git a/src/d_player.h b/src/d_player.h index 1b9744efb..4676f1076 100644 --- a/src/d_player.h +++ b/src/d_player.h @@ -39,6 +39,10 @@ extern "C" { #endif +// Maximum laps per map. +// (done here as p_local.h, the previous host, has this as a dependency - but we must use it here) +#define MAX_LAPS 99 + // Extra abilities/settings for skins (combinable stuff) typedef enum { @@ -383,6 +387,19 @@ struct botvars_t // player_t struct for round-specific condition tracking +typedef enum +{ + UFOD_GENERIC = 1, + UFOD_BOOST = 1<<1, + UFOD_WHIP = 1<<2, + UFOD_BANANA = 1<<3, + UFOD_ORBINAUT = 1<<4, + UFOD_JAWZ = 1<<5, + UFOD_SPB = 1<<6, + UFOD_GACHABOM = 1<<7, + // free up to and including 1<<31 +} targetdamaging_t; + struct roundconditions_t { // Reduce the number of checks by only updating when this is true @@ -393,10 +410,31 @@ struct roundconditions_t boolean touched_offroad; boolean touched_sneakerpanel; boolean debt_rings; + boolean faulted; + + // Basically the same, but it's a specific event where no is an easy default boolean tripwire_hyuu; + boolean whip_hyuu; boolean spb_neuter; boolean landmine_dunk; boolean hit_midair; + boolean hit_drafter_lookback; + boolean giant_foe_shrunken_orbi; + boolean returntosender_mark; + + UINT8 hittrackhazard[((MAX_LAPS+1)/8) + 1]; + + // Attack-based conditions + targetdamaging_t targetdamaging; + UINT8 gachabom_miser; + + fixed_t maxspeed; + + tic_t continuousdraft; + tic_t continuousdraft_best; + + UINT8 consecutive_grow_lasers; + UINT8 best_consecutive_grow_lasers; mobjeflag_t wet_player; diff --git a/src/deh_soc.c b/src/deh_soc.c index d3a48cb6f..394e2d12d 100644 --- a/src/deh_soc.c +++ b/src/deh_soc.c @@ -73,6 +73,8 @@ fixed_t get_number(const char *word) #define PARAMCHECK(n) do { if (!params[n]) { deh_warning("Too few parameters, need %d", n); return; }} while (0) +#define EXTENDEDPARAMCHECK(spos, n) do { if (!spos || !(*spos)) { deh_warning("Missing extended parameter, need at least %d", n); return; }} while (0) + /* ======================================================================== */ // Load a dehacked file format /* ======================================================================== */ @@ -1220,6 +1222,8 @@ void readlevelheader(MYFILE *f, char * name) break; deh_strlcpy(mapheaderinfo[num]->musname[j], tmp, sizeof(mapheaderinfo[num]->musname[j]), va("Level header %d: music", num)); + if (j) + mapheaderinfo[num]->cache_muslock[j - 1] = MAXUNLOCKABLES; j++; } while ((tmp = strtok(NULL,",")) != NULL); @@ -1379,6 +1383,28 @@ void readlevelheader(MYFILE *f, char * name) } else if (fastcmp(word, "GRAVITY")) mapheaderinfo[num]->gravity = FLOAT_TO_FIXED(atof(word2)); + else if (fastcmp(word, "DESTROYOBJECTSFORCHALLENGES")) + { + if (fastcmp(word2, "NONE")) + { + mapheaderinfo[num]->destroyforchallenge_size = 0; + } + else + { + UINT8 j = 0; // i was declared elsewhere + tmp = strtok(word2, ","); + do { + if (j >= MAXDESTRUCTIBLES) + break; + mapheaderinfo[num]->destroyforchallenge[j] = get_mobjtype(word2); + j++; + } while ((tmp = strtok(NULL,",")) != NULL); + + if (tmp != NULL) + deh_warning("Level header %d: additional destructibles past %d discarded", num, MAXDESTRUCTIBLES); + mapheaderinfo[num]->destroyforchallenge_size = j; + } + } else deh_warning("Level header %d: unknown word '%s'", num, word); } @@ -2295,6 +2321,55 @@ void reademblemdata(MYFILE *f, INT32 num) Z_Free(s); } +static INT16 parseunlockabletype(char *type) +{ + if (fastcmp(type, "EXTRAMEDAL")) + return SECRET_EXTRAMEDAL; + else if (fastcmp(type, "CUP")) + return SECRET_CUP; + else if (fastcmp(type, "MAP")) + return SECRET_MAP; + else if (fastcmp(type, "ALTMUSIC")) + return SECRET_ALTMUSIC; + else if (fastcmp(type, "SKIN")) + return SECRET_SKIN; + else if (fastcmp(type, "FOLLOWER")) + return SECRET_FOLLOWER; + else if (fastcmp(type, "COLOR")) + return SECRET_COLOR; + + else if (fastcmp(type, "HARDSPEED")) + return SECRET_HARDSPEED; + else if (fastcmp(type, "MASTERMODE")) + return SECRET_MASTERMODE; + else if (fastcmp(type, "ENCORE")) + return SECRET_ENCORE; + else if (fastcmp(type, "TIMEATTACK")) + return SECRET_TIMEATTACK; + else if (fastcmp(type, "PRISONBREAK")) + return SECRET_PRISONBREAK; + else if (fastcmp(type, "SPECIALATTACK")) + return SECRET_SPECIALATTACK; + else if (fastcmp(type, "SPBATTACK")) + return SECRET_SPBATTACK; + else if (fastcmp(type, "ONLINE")) + return SECRET_ONLINE; + else if (fastcmp(type, "ADDONS")) + return SECRET_ADDONS; + else if (fastcmp(type, "EGGTV")) + return SECRET_EGGTV; + else if (fastcmp(type, "SOUNDTEST")) + return SECRET_SOUNDTEST; + else if (fastcmp(type, "ALTTITLE")) + return SECRET_ALTTITLE; + else if (fastcmp(type, "MEMETAUNTS")) + return SECRET_MEMETAUNTS; + else if (fastcmp(type, "ITEMFINDER")) + return SECRET_ITEMFINDER; + + return SECRET_NONE; +} + void readunlockable(MYFILE *f, INT32 num) { char *s = Z_Malloc(MAXLINELEN, PU_STATIC, NULL); @@ -2349,50 +2424,7 @@ void readunlockable(MYFILE *f, INT32 num) unlockables[num].majorunlock = (UINT8)(i != 0 || word2[0] == 'T' || word2[0] == 'Y'); else if (fastcmp(word, "TYPE")) { - if (fastcmp(word2, "NONE")) - unlockables[num].type = SECRET_NONE; - else if (fastcmp(word2, "EXTRAMEDAL")) - unlockables[num].type = SECRET_EXTRAMEDAL; - else if (fastcmp(word2, "CUP")) - unlockables[num].type = SECRET_CUP; - else if (fastcmp(word2, "MAP")) - unlockables[num].type = SECRET_MAP; - else if (fastcmp(word2, "SKIN")) - unlockables[num].type = SECRET_SKIN; - else if (fastcmp(word2, "FOLLOWER")) - unlockables[num].type = SECRET_FOLLOWER; - else if (fastcmp(word2, "COLOR")) - unlockables[num].type = SECRET_COLOR; - else if (fastcmp(word2, "HARDSPEED")) - unlockables[num].type = SECRET_HARDSPEED; - else if (fastcmp(word2, "MASTERMODE")) - unlockables[num].type = SECRET_MASTERMODE; - else if (fastcmp(word2, "ENCORE")) - unlockables[num].type = SECRET_ENCORE; - else if (fastcmp(word2, "TIMEATTACK")) - unlockables[num].type = SECRET_TIMEATTACK; - else if (fastcmp(word2, "PRISONBREAK")) - unlockables[num].type = SECRET_PRISONBREAK; - else if (fastcmp(word2, "SPECIALATTACK")) - unlockables[num].type = SECRET_SPECIALATTACK; - else if (fastcmp(word2, "SPBATTACK")) - unlockables[num].type = SECRET_SPBATTACK; - else if (fastcmp(word2, "ONLINE")) - unlockables[num].type = SECRET_ONLINE; - else if (fastcmp(word2, "ADDONS")) - unlockables[num].type = SECRET_ADDONS; - else if (fastcmp(word2, "EGGTV")) - unlockables[num].type = SECRET_EGGTV; - else if (fastcmp(word2, "SOUNDTEST")) - unlockables[num].type = SECRET_SOUNDTEST; - else if (fastcmp(word2, "ALTTITLE")) - unlockables[num].type = SECRET_ALTTITLE; - else if (fastcmp(word2, "MEMETAUNTS")) - unlockables[num].type = SECRET_MEMETAUNTS; - else if (fastcmp(word2, "ITEMFINDER")) - unlockables[num].type = SECRET_ITEMFINDER; - else - unlockables[num].type = (INT16)i; + unlockables[num].type = parseunlockabletype(word2); unlockables[num].stringVarCache = -1; } else if (fastcmp(word, "VAR")) @@ -2420,47 +2452,133 @@ void readunlockable(MYFILE *f, INT32 num) Z_Free(s); } +// This is a home-grown strtok(" ") equivalent so we can isolate the first chunk without destroying the rest of the line. +static void conditiongetparam(char **params, UINT8 paramid, char **spos) +{ + if (*spos == NULL || *(*spos) == '\0') + { + params[paramid] = NULL; + return; + } + + params[paramid] = *spos; + while (*(*spos) != '\0' && *(*spos) != ' ') + { + *(*spos) = toupper(*(*spos)); + (*spos)++; + } + if (*(*spos) == ' ') + { + *(*spos) = '\0'; + (*spos)++; + + while (*(*spos) == ' ') + (*spos)++; + } +} + static void readcondition(UINT16 set, UINT32 id, char *word2) { INT32 i; - char *params[5]; // condition, requirement, extra info, extra info, stringvar - char *spos; + const UINT8 MAXCONDITIONPARAMS = 5; + char *params[MAXCONDITIONPARAMS]; // condition, requirement, extra info, extra info, stringvar + char *spos = NULL; char *stringvar = NULL; - conditiontype_t ty; + conditiontype_t ty = UC_NONE; INT32 re = 0; INT16 x1 = 0, x2 = 0; INT32 offset = 0; -#if 0 - char *endpos = word2 + strlen(word2); -#endif - - spos = strtok(word2, " "); - - for (i = 0; i < 5; ++i) + // Lop the leading spaces off + if (word2 && *word2) { - if (spos != NULL) - { - params[i] = spos; - spos = strtok(NULL, " "); - } - else - params[i] = NULL; + spos = word2; + while (*spos == ' ') + spos++; } + conditiongetparam(params, 0, &spos); + if (!params[0]) { deh_warning("condition line is empty for condition ID %d", id+1); return; } - if (fastcmp(params[0], "PLAYTIME")) + // We do free descriptions first. + + if (fastcmp(params[0], "DESCRIPTIONOVERRIDE")) + { + EXTENDEDPARAMCHECK(spos, 1); + ty = UC_DESCRIPTIONOVERRIDE; + + stringvar = Z_StrDup(spos); + } + else if (fastcmp(params[0], "PASSWORD")) + { + EXTENDEDPARAMCHECK(spos, 1); + ty = UC_PASSWORD; + + stringvar = Z_StrDup(spos); + } + + if (ty != UC_NONE) + goto setcondition; + + // Now conditions that take one standard param and one free description. + + conditiongetparam(params, 1, &spos); + + if (fastcmp(params[0], "WETPLAYER")) { PARAMCHECK(1); - ty = UC_PLAYTIME + offset; - re = atoi(params[1]); + //EXTENDEDPARAMCHECK(spos, 2); + ty = UCRP_WETPLAYER; + re = MFE_UNDERWATER; + x1 = 1; + + if (fastcmp(params[1], "STRICT")) + re |= MFE_TOUCHWATER; + else if (fastcmp(params[1], "STANDARD")) + ; + else + { + deh_warning("liquid strictness requirement \"%s\" invalid for condition ID %d", params[1], id+1); + return; + } + + if (spos && *spos) + stringvar = Z_StrDup(spos); + } + else if (fastcmp(params[0], "MAPDESTROYOBJECTS")) + { + PARAMCHECK(1); + EXTENDEDPARAMCHECK(spos, 2); + ty = UCRP_MAPDESTROYOBJECTS; + re = G_MapNumber(params[1]); + + stringvar = Z_StrDup(spos); + } + + if (ty != UC_NONE) + goto setcondition; + + // Now for all other conditions. + + for (i = 2; i < MAXCONDITIONPARAMS; i++) + { + conditiongetparam(params, i, &spos); + } + + if (ty != UC_NONE) + ; + else if (fastcmp(params[0], "PLAYTIME")) + { + PARAMCHECK(1); + ty = UC_PLAYTIME; + re = get_number(params[1]); } else if (fastcmp(params[0], "ROUNDSPLAYED")) { @@ -2506,6 +2624,12 @@ static void readcondition(UINT16 set, UINT32 id, char *word2) return; } } + else if (fastcmp(params[0], "TOTALTUMBLETIME")) + { + PARAMCHECK(1); + ty = UC_TOTALTUMBLETIME; + re = get_number(params[1]); + } else if (fastcmp(params[0], "GAMECLEAR")) { ty = UC_GAMECLEAR; @@ -2515,12 +2639,13 @@ static void readcondition(UINT16 set, UINT32 id, char *word2) { PARAMCHECK(1); ty = UC_OVERALLTIME; - re = atoi(params[1]); + re = get_number(params[1]); } else if ((offset=0) || fastcmp(params[0], "MAPVISITED") || (++offset && fastcmp(params[0], "MAPBEATEN")) || (++offset && fastcmp(params[0], "MAPENCORE")) - || (++offset && fastcmp(params[0], "MAPSPBATTACK"))) + || (++offset && fastcmp(params[0], "MAPSPBATTACK")) + || (++offset && fastcmp(params[0], "MAPMYSTICMELODY"))) { PARAMCHECK(1); ty = UC_MAPVISITED + offset; @@ -2536,7 +2661,7 @@ static void readcondition(UINT16 set, UINT32 id, char *word2) { PARAMCHECK(2); ty = UC_MAPTIME; - re = atoi(params[2]); + re = get_number(params[2]); x1 = G_MapNumber(params[1]); if (x1 >= nummapheaders) @@ -2617,6 +2742,40 @@ static void readcondition(UINT16 set, UINT32 id, char *word2) return; } } + else if (fastcmp(params[0], "UNLOCKPERCENT")) + { + PARAMCHECK(1); + ty = UC_UNLOCKPERCENT; + re = atoi(params[1]); + x1 = SECRET_NONE; + + // Valid percentages only! + if (re <= 0 || re > 100) + { + deh_warning("Condition percent %d out of range (1 - 100) for condition ID %d", re, id+1); + return; + } + + if (params[2] && params[2][0]) + { + x1 = parseunlockabletype(params[2]); + + if (x1 <= SECRET_NONE || x1 >= SECRET_ONEPERBOARD) + { + deh_warning("Condition challenge type \"%s\" invalid for condition ID %d", params[2], id+1); + return; + } + + x2 = 0; + if (params[3]) + { + // fudge value + x2 = atoi(params[3]); + } + } + else + x2 = 1; // guaranteed fudge for raw Unlockables count + } else if ((offset=0) || fastcmp(params[0], "ADDON") || (++offset && fastcmp(params[0], "CREDITS")) || (++offset && fastcmp(params[0], "REPLAY")) @@ -2625,13 +2784,6 @@ static void readcondition(UINT16 set, UINT32 id, char *word2) //PARAMCHECK(1); ty = UC_ADDON + offset; } - else if (fastcmp(params[0], "PASSWORD")) - { - PARAMCHECK(1); - ty = UC_PASSWORD; - stringvar = Z_StrDup(params[1]); - re = -1; - } else if (fastcmp(params[0], "SPRAYCAN")) { PARAMCHECK(1); @@ -2641,6 +2793,22 @@ static void readcondition(UINT16 set, UINT32 id, char *word2) // Force at head of the list? x1 = (params[2] && (params[2][0] == 'Y' || params[2][0] == 'T')) ? 1 : 0; } + else if (fastcmp(params[0], "PRISONEGGCD")) + { + ty = UC_PRISONEGGCD; + re = NEXTMAP_INVALID; + + if (params[1]) + { + re = G_MapNumber(params[1]); + + if (re >= nummapheaders) + { + deh_warning("Invalid level %s for condition ID %d", params[1], id+1); + return; + } + } + } else if ((offset=0) || fastcmp(params[0], "AND") || (++offset && fastcmp(params[0], "COMMA"))) { @@ -2691,6 +2859,13 @@ static void readcondition(UINT16 set, UINT32 id, char *word2) return; } } + else if (fastcmp(params[0], "HASFOLLOWER")) + { + PARAMCHECK(1); + ty = UCRP_HASFOLLOWER; + stringvar = Z_StrDup(params[1]); + re = -1; + } else if (fastcmp(params[0], "ISDIFFICULTY")) { //PARAMCHECK(1); @@ -2701,9 +2876,9 @@ static void readcondition(UINT16 set, UINT32 id, char *word2) if (fastcmp(params[1], "NORMAL")) ; else if (fastcmp(params[1], "HARD")) - x1 = KARTSPEED_HARD; + re = KARTSPEED_HARD; else if (fastcmp(params[1], "MASTER")) - x1 = KARTGP_MASTER; + re = KARTGP_MASTER; else { deh_warning("gamespeed requirement \"%s\" invalid for condition ID %d", params[1], id+1); @@ -2715,6 +2890,9 @@ static void readcondition(UINT16 set, UINT32 id, char *word2) { PARAMCHECK(1); ty = UCRP_PODIUMCUP; + + re = -1; + if (!fastcmp(params[1], "ANY")) { cupheader_t *cup = kartcupheaders; UINT32 hash = quickncasehash(params[1], MAXCUPNAME); @@ -2768,18 +2946,29 @@ static void readcondition(UINT16 set, UINT32 id, char *word2) } } else if ((offset=0) || fastcmp(params[0], "PODIUMEMERALD") - || (++offset && fastcmp(params[0], "PODIUMPRIZE"))) + || (++offset && fastcmp(params[0], "PODIUMPRIZE")) + || (++offset && fastcmp(params[0], "PODIUMNOCONTINUES"))) { //PARAMCHECK(1); ty = UCRP_PODIUMEMERALD + offset; } else if ((offset=0) || fastcmp(params[0], "FINISHCOOL") + || (++offset && fastcmp(params[0], "FINISHPERFECT")) || (++offset && fastcmp(params[0], "FINISHALLPRISONS")) - || (++offset && fastcmp(params[0], "NOCONTEST"))) + || (++offset && fastcmp(params[0], "NOCONTEST")) + || (++offset && fastcmp(params[0], "SMASHUFO")) + || (++offset && fastcmp(params[0], "CHASEDBYSPB"))) { //PARAMCHECK(1); ty = UCRP_FINISHCOOL + offset; } + else if (fastcmp(params[0], "MAKERETIRE")) + { + PARAMCHECK(1); + ty = UCRP_MAKERETIRE; + stringvar = Z_StrDup(params[1]); + re = -1; + } else if ((offset=0) || fastcmp(params[0], "FINISHPLACE") || (++offset && fastcmp(params[0], "FINISHPLACEEXACT"))) { @@ -2793,6 +2982,31 @@ static void readcondition(UINT16 set, UINT32 id, char *word2) return; } } + else if (fastcmp(params[0], "FINISHGRADE")) + { + PARAMCHECK(1); + ty = UCRP_FINISHGRADE; + + re = -1; + if (!params[1][1]) + { + switch (params[1][0]) + { + case 'E': { re = GRADE_E; break; } + case 'D': { re = GRADE_D; break; } + case 'C': { re = GRADE_C; break; } + case 'B': { re = GRADE_B; break; } + case 'A': { re = GRADE_A; break; } + default: { break; } + } + } + + if (re == -1) + { + deh_warning("Invalid grade %s for condition ID %d", params[1], id+1); + return; + } + } else if ((offset=0) || fastcmp(params[0], "FINISHTIME") || (++offset && fastcmp(params[0], "FINISHTIMEEXACT")) || (++offset && fastcmp(params[0], "FINISHTIMELEFT"))) @@ -2807,6 +3021,55 @@ static void readcondition(UINT16 set, UINT32 id, char *word2) return; } } + else if ((offset=0) || fastcmp(params[0], "RINGS") + || (++offset && fastcmp(params[0], "RINGSEXACT"))) + { + PARAMCHECK(1); + ty = UCRP_RINGS + offset; + re = get_number(params[1]); + + if (re < -20 || re > 20) + { + deh_warning("Invalid ring count %d for condition ID %d", re, id+1); + return; + } + } + else if (fastcmp(params[0], "SPEEDOMETER")) + { + PARAMCHECK(1); + ty = UCRP_SPEEDOMETER; + re = get_number(params[1]); + + if (re < 100 || re > 999) + { + deh_warning("Speed percent %d out of range (100 - 999) for condition ID %d", re, id+1); + return; + } + } + else if (fastcmp(params[0], "DRAFTDURATION")) + { + PARAMCHECK(1); + ty = UCRP_DRAFTDURATION; + re = get_number(params[1]); + + if (re < 5) + { + deh_warning("Duration %d seconds too low for condition ID %d", re, id+1); + return; + } + } + else if (fastcmp(params[0], "GROWCONSECUTIVEBEAMS")) + { + PARAMCHECK(1); + ty = UCRP_GROWCONSECUTIVEBEAMS; + re = get_number(params[1]); + + if (re < 2 || re > UINT8_MAX) + { + deh_warning("Touch count %d out of range (2 - %u) for condition ID %d", re, UINT8_MAX, id+1); + return; + } + } else if (fastcmp(params[0], "TRIGGER")) { PARAMCHECK(1); @@ -2819,32 +3082,12 @@ static void readcondition(UINT16 set, UINT32 id, char *word2) deh_warning("Trigger ID %d out of range (0 - 31) for condition ID %d", re, id+1); return; } - - // The following undid the effects of strtok. - // Unfortunately, there is no way it can reasonably undo the effects of strupr. - // If we want custom descriptions for map execution triggers, we're gonna need a different method. -#if 0 - // undo affect of strtok - i = 5; - // so spos will still be the strtok from earlier - while (i >= 2) - { - if (!spos) - continue; - while (*spos != '\0') - spos++; - if (spos < endpos) - *spos = ' '; - spos = params[--i]; - } - - stringvar = Z_StrDup(params[2]); -#endif } else if ((offset=0) || fastcmp(params[0], "FALLOFF") || (++offset && fastcmp(params[0], "TOUCHOFFROAD")) || (++offset && fastcmp(params[0], "TOUCHSNEAKERPANEL")) - || (++offset && fastcmp(params[0], "RINGDEBT"))) + || (++offset && fastcmp(params[0], "RINGDEBT")) + || (++offset && fastcmp(params[0], "FAULTED"))) { PARAMCHECK(1); ty = UCRP_FALLOFF + offset; @@ -2854,32 +3097,73 @@ static void readcondition(UINT16 set, UINT32 id, char *word2) re = 0; } else if ((offset=0) || fastcmp(params[0], "TRIPWIREHYUU") + || (++offset && fastcmp(params[0], "WHIPHYUU")) || (++offset && fastcmp(params[0], "SPBNEUTER")) || (++offset && fastcmp(params[0], "LANDMINEDUNK")) - || (++offset && fastcmp(params[0], "HITMIDAIR"))) + || (++offset && fastcmp(params[0], "HITMIDAIR")) + || (++offset && fastcmp(params[0], "HITDRAFTERLOOKBACK")) + || (++offset && fastcmp(params[0], "GIANTRACERSHRUNKENORBI")) + || (++offset && fastcmp(params[0], "RETURNMARKTOSENDER"))) { //PARAMCHECK(1); ty = UCRP_TRIPWIREHYUU + offset; } - else if (fastcmp(params[0], "WETPLAYER")) + else if (fastcmp(params[0], "TRACKHAZARD")) { PARAMCHECK(1); - ty = UCRP_WETPLAYER; - re = MFE_UNDERWATER; - x1 = 1; + ty = UCRP_TRACKHAZARD; + re = 1; + x1 = -1; + + if (params[1][0] == 'F' || params[1][0] == 'N' || params[1][0] == '0') + re = 0; if (params[2]) { - if (fastcmp(params[2], "STRICT")) - re |= MFE_TOUCHWATER; + if (fastcmp(params[2], "FINAL")) + x1 = -2; else { - deh_warning("liquid strictness requirement \"%s\" invalid for condition ID %d", params[2], id+1); - return; + x1 = atoi(params[2]); + + if (re < 0 || re > MAX_LAPS) + { + deh_warning("Lap number %d out of range (0 - %u) for condition ID %d", x1, MAX_LAPS, id+1); + return; + } } } + } + else if (fastcmp(params[0], "TARGETATTACKMETHOD")) + { + PARAMCHECK(1); + ty = UCRP_TARGETATTACKMETHOD; - stringvar = Z_StrDup(params[1]); + // See targetdamaging_t + if (fastcmp(params[1], "BOOST")) + re = UFOD_BOOST; + else if (fastcmp(params[1], "WHIP")) + re = UFOD_WHIP; + else if (fastcmp(params[1], "BANANA")) + re = UFOD_BANANA; + else if (fastcmp(params[1], "ORBINAUT")) + re = UFOD_ORBINAUT; + else if (fastcmp(params[1], "JAWZ")) + re = UFOD_JAWZ; + else if (fastcmp(params[1], "SPB")) + re = UFOD_SPB; + else if (fastcmp(params[1], "GACHABOM")) + re = UFOD_GACHABOM; + else + { + deh_warning("Unknown attack method %s for condition ID %d", params[1], id+1); + return; + } + } + else if (fastcmp(params[0], "GACHABOMMISER")) + { + //PARAMCHECK(1); + ty = UCRP_GACHABOMMISER; } else { @@ -2887,6 +3171,7 @@ static void readcondition(UINT16 set, UINT32 id, char *word2) return; } +setcondition: M_AddRawCondition(set, (UINT8)id, ty, re, x1, x2, stringvar); } @@ -2929,7 +3214,7 @@ void readconditionset(MYFILE *f, UINT16 setnum) // Now get the part after word2 = tmp += 2; - strupr(word2); + //strupr(word2); if (fastncmp(word, "CONDITION", 9)) { @@ -3913,6 +4198,11 @@ if (!followers[numfollowers].field) \ NOSTATE(hitconfirmstate, "HITCONFIRMSTATE"); #undef NOSTATE + if (!followers[numfollowers].hornsound) + { + followers[numfollowers].hornsound = sfx_horn00; + } + CONS_Printf("Added follower '%s'\n", testname); if (followers[numfollowers].category < numfollowercategories) followercategories[followers[numfollowers].category].numincategory++; diff --git a/src/deh_tables.c b/src/deh_tables.c index 9c0fc670c..458c3aed0 100644 --- a/src/deh_tables.c +++ b/src/deh_tables.c @@ -1195,6 +1195,25 @@ const char *const STATE_LIST[] = { // array length left dynamic for sanity testi // Spray Can "S_SPRAYCAN", + // Ancient Shrine + "S_ANCIENTSHRINE", + + "S_MORB1", + "S_MORB2", + "S_MORB3", + "S_MORB4", + "S_MORB5", + "S_MORB6", + "S_MORB7", + "S_MORB8", + "S_MORB9", + "S_MORB10", + "S_MORB11", + "S_MORB12", + "S_MORB13", + "S_MORB14", + "S_MORB15", + // Chaos Emeralds "S_CHAOSEMERALD1", "S_CHAOSEMERALD2", @@ -1215,10 +1234,8 @@ const char *const STATE_LIST[] = { // array length left dynamic for sanity testi "S_EMERALDFLARE1", - // Emerald hunt shards - "S_SHRD1", - "S_SHRD2", - "S_SHRD3", + // Prison Egg Drops + "S_PRISONEGGDROP_CD", // Bubble Source "S_BUBBLES1", @@ -4845,11 +4862,11 @@ const char *const MOBJTYPE_LIST[] = { // array length left dynamic for sanity t "MT_BLUEFLAG", // Blue CTF Flag "MT_EMBLEM", "MT_SPRAYCAN", + "MT_ANCIENTSHRINE", "MT_EMERALD", "MT_EMERALDSPARK", "MT_EMERALDFLARE", - "MT_EMERHUNT", // Emerald Hunt - "MT_EMERALDSPAWN", // Emerald spawner w/ delay + "MT_PRISONEGGDROP", // Springs and others "MT_FAN", diff --git a/src/dehacked.c b/src/dehacked.c index 66567fb05..bc3e3ae0a 100644 --- a/src/dehacked.c +++ b/src/dehacked.c @@ -491,6 +491,7 @@ static void DEH_LoadDehackedFile(MYFILE *f, boolean mainfile) cup = Z_Calloc(sizeof (cupheader_t), PU_STATIC, NULL); cup->id = numkartcupheaders; cup->monitor = 1; + cup->cache_cuplock = MAXUNLOCKABLES; deh_strlcpy(cup->name, word2, sizeof(cup->name), va("Cup header %s: name", word2)); cup->namehash = hash; diff --git a/src/doomstat.h b/src/doomstat.h index ce80df3e4..edf5fe334 100644 --- a/src/doomstat.h +++ b/src/doomstat.h @@ -142,7 +142,8 @@ struct skinreference_t #define MV_BEATEN (1<<1) #define MV_ENCORE (1<<2) #define MV_SPBATTACK (1<<3) -#define MV_MAX (MV_VISITED|MV_BEATEN|MV_ENCORE|MV_SPBATTACK) +#define MV_MYSTICMELODY (1<<4) +#define MV_MAX (MV_VISITED|MV_BEATEN|MV_ENCORE|MV_SPBATTACK|MV_MYSTICMELODY) #define MV_FINISHNEEDED (1<<7) #define MV_PERSISTUNLOADED (MV_SPBATTACK|MV_FINISHNEEDED) @@ -407,6 +408,8 @@ struct cupheader_t boolean playcredits; ///< Play the credits? + UINT16 cache_cuplock; ///< Cached Unlockable ID + cupwindata_t windata[4]; ///< Data for cup visitation cupheader_t *next; ///< Next cup in linked list }; @@ -437,6 +440,7 @@ struct staffbrief_t }; #define MAXMUSNAMES 3 // maximum definable music tracks per level +#define MAXDESTRUCTIBLES 3 #define MAXHEADERFOLLOWERS 32 struct mapheader_lighting_t @@ -492,13 +496,14 @@ struct mapheader_t char relevantskin[SKINNAMESIZE+1]; ///< Skin to use for tutorial (if not provided, uses Eggman.) // Music information - char musname[MAXMUSNAMES][7]; ///< Music tracks to play. First dimension is the track number, second is the music string. "" for no music. - char associatedmus[MAXMUSNAMES][7]; ///< Associated music tracks for sound test unlock. - char positionmus[7]; ///< Custom Position track. Doesn't play in Encore or other fun game-controlled contexts - UINT8 musname_size; ///< Number of music tracks defined - UINT8 associatedmus_size; ///< Number of associated music tracks defined - UINT16 mustrack; ///< Subsong to play. Only really relevant for music modules and specific formats supported by GME. 0 to ignore. - UINT32 muspos; ///< Music position to jump to. + char musname[MAXMUSNAMES][7]; ///< Music tracks to play. First dimension is the track number, second is the music string. "" for no music. + UINT16 cache_muslock[MAXMUSNAMES-1]; ///< Cached Alt Music IDs + char associatedmus[MAXMUSNAMES][7]; ///< Associated music tracks for sound test unlock. + char positionmus[7]; ///< Custom Position track. Doesn't play in Encore or other fun game-controlled contexts + UINT8 musname_size; ///< Number of music tracks defined + UINT8 associatedmus_size; ///< Number of associated music tracks defined + UINT16 mustrack; ///< Subsong to play. Only really relevant for music modules and specific formats supported by GME. 0 to ignore. + UINT32 muspos; ///< Music position to jump to. // Sky information UINT8 weather; ///< See preciptype_t @@ -533,8 +538,12 @@ struct mapheader_t UINT8 precutscenenum; ///< Cutscene number to play BEFORE a level starts. UINT8 cutscenenum; ///< Cutscene number to use, 0 for none. + mobjtype_t destroyforchallenge[MAXDESTRUCTIBLES]; ///< Assistive for UCRP_MAPDESTROYOBJECTS + UINT8 destroyforchallenge_size; ///< Number for above + UINT32 _saveid; ///< Purely assistive in gamedata save processes UINT16 cache_spraycan; ///< Cached Spraycan ID + UINT16 cache_maplock; ///< Cached Unlockable ID // Lua information UINT8 numCustomOptions; ///< Internal. For Lua custom value support. @@ -717,6 +726,7 @@ extern INT32 luabanks[NUM_LUABANKS]; extern INT32 nummaprings; //keep track of spawned rings/coins extern UINT8 nummapspraycans; +extern UINT16 numchallengedestructibles; extern UINT32 bluescore; ///< Blue Team Scores extern UINT32 redscore; ///< Red Team Scores @@ -765,8 +775,6 @@ extern UINT8 useSeal; extern UINT8 use1upSound; extern UINT8 maxXtraLife; // Max extra lives from rings -extern mobj_t *hunt1, *hunt2, *hunt3; // Emerald hunt locations - struct exitcondition_t { boolean losing; diff --git a/src/g_demo.c b/src/g_demo.c index 5c5c58fe0..bedbf1e41 100644 --- a/src/g_demo.c +++ b/src/g_demo.c @@ -2233,7 +2233,7 @@ static void G_SaveDemoSkins(UINT8 **pp) { char skin[16]; UINT8 i; - UINT8 *availabilitiesbuffer = R_GetSkinAvailabilities(true, false); + UINT8 *availabilitiesbuffer = R_GetSkinAvailabilities(true, -1); WRITEUINT8((*pp), numskins); for (i = 0; i < numskins; i++) diff --git a/src/g_game.c b/src/g_game.c index 72a580b47..f036e61a7 100644 --- a/src/g_game.c +++ b/src/g_game.c @@ -222,6 +222,7 @@ UINT32 bluescore, redscore; // CTF and Team Match team scores INT32 nummaprings = 0; UINT8 nummapspraycans = 0; +UINT16 numchallengedestructibles = 0; // Elminates unnecessary searching. boolean CheckForBustableBlocks; @@ -270,11 +271,6 @@ UINT8 introtoplay; UINT8 creditscutscene; UINT8 useSeal = 1; -// Emerald locations -mobj_t *hunt1; -mobj_t *hunt2; -mobj_t *hunt3; - tic_t racecountdown, exitcountdown, musiccountdown; // for racing exitcondition_t g_exit; @@ -534,20 +530,12 @@ void G_UpdateRecords(void) && (time < UINT32_MAX)) // DNF mapheaderinfo[gamemap-1]->records.time = time; } - else - { - mapheaderinfo[gamemap-1]->records.time = 0; - } if (modeattacking & ATTACKING_LAP) { if ((mapheaderinfo[gamemap-1]->records.lap == 0) || (bestlap < mapheaderinfo[gamemap-1]->records.lap)) mapheaderinfo[gamemap-1]->records.lap = bestlap; } - else - { - mapheaderinfo[gamemap-1]->records.lap = 0; - } // Check emblems when level data is updated if ((earnedEmblems = M_CheckLevelEmblems())) @@ -4038,48 +4026,21 @@ static void G_DoCompleted(void) { INT32 i; - if (modeattacking && pausedelay) - pausedelay = 0; - // We do this here so Challenges-related sounds aren't insta-killed S_StopSounds(); - if (legitimateexit && !demo.playback && !mapreset) // (yes you're allowed to unlock stuff this way when the game is modified) - { - if (gametype != GT_TUTORIAL) - { - UINT8 roundtype = GDGT_CUSTOM; - - if (gametype == GT_RACE) - roundtype = GDGT_RACE; - else if (gametype == GT_BATTLE) - roundtype = (battleprisons ? GDGT_PRISONS : GDGT_BATTLE); - else if (gametype == GT_SPECIAL || gametype == GT_VERSUS) - roundtype = GDGT_SPECIAL; - - gamedata->roundsplayed[roundtype]++; - } - gamedata->pendingkeyrounds++; - - // Done before forced addition of PF_NOCONTEST to make UCRP_NOCONTEST harder to achieve - M_UpdateUnlockablesAndExtraEmblems(true, true); - gamedata->deferredsave = true; - } - - if (gamedata->deferredsave) - G_SaveGameData(); - - legitimateexit = false; - - gameaction = ga_nothing; - - if (metalplayback) - G_StopMetalDemo(); - if (metalrecording) - G_StopMetalRecording(false); - - G_SetGamestate(GS_NULL); - wipegamestate = GS_NULL; + // First, loop over all players to: + // - fake bot results + // - set client power add + // - grand prix updates (for those who have finished) + // - for bots + // - set up difficulty increase (if applicable) + // - for humans + // - update Rings + // - award Lives + // - update over-all GP rank + // - wipe some level-only player struct data + // (The common thread is it needs to be done before Challenges updates.) for (i = 0; i < MAXPLAYERS; i++) { @@ -4090,8 +4051,7 @@ static void G_DoCompleted(void) player_t *const player = &players[i]; - // Exitlevel shouldn't get you the points - if (player->exiting == false && (player->pflags & PF_NOCONTEST) == 0) + if ((player->exiting == 0) && (player->pflags & PF_NOCONTEST) == 0) { clientPowerAdd[i] = 0; @@ -4099,13 +4059,9 @@ static void G_DoCompleted(void) { K_FakeBotResults(player); } - else - { - player->pflags |= PF_NOCONTEST; - } } - if (grandprixinfo.gp == true && grandprixinfo.wonround == true && player->exiting == true) + if (grandprixinfo.gp == true && grandprixinfo.wonround == true && player->exiting) { if (player->bot == true) { @@ -4141,11 +4097,77 @@ static void G_DoCompleted(void) G_PlayerFinishLevel(i); // take away cards and stuff } - if (automapactive) - AM_Stop(); + // Then, do gamedata-relevant material. + // This has to be done second because some Challenges + // are dependent on round standings. + if (legitimateexit && !demo.playback && !mapreset) + { + if (gametype != GT_TUTORIAL) + { + UINT8 roundtype = GDGT_CUSTOM; - prevmap = (INT16)(gamemap-1); + if (gametype == GT_RACE) + roundtype = GDGT_RACE; + else if (gametype == GT_BATTLE) + roundtype = (battleprisons ? GDGT_PRISONS : GDGT_BATTLE); + else if (gametype == GT_SPECIAL || gametype == GT_VERSUS) + roundtype = GDGT_SPECIAL; + gamedata->roundsplayed[roundtype]++; + } + gamedata->pendingkeyrounds++; + + M_UpdateUnlockablesAndExtraEmblems(true, true); + gamedata->deferredsave = true; + } + + // This isn't in the above block because other + // mechanisms can queue up a gamedata save. + if (gamedata->deferredsave) + G_SaveGameData(); + + // Then, update some important game state. + { + legitimateexit = false; + + if (modeattacking && pausedelay) + pausedelay = 0; + + gameaction = ga_nothing; + + if (metalplayback) + G_StopMetalDemo(); + if (metalrecording) + G_StopMetalRecording(false); + + if (automapactive) + AM_Stop(); + + G_SetGamestate(GS_NULL); + wipegamestate = GS_NULL; + + prevmap = (INT16)(gamemap-1); + } + + // Finally, if you're not exiting, guarantee NO CONTEST. + // We do this seperately from the loop above Challenges, + // so NOCONTEST-related Challenges don't fire on exitlevel. + for (i = 0; i < MAXPLAYERS; i++) + { + if (playeringame[i] == false) + { + continue; + } + + if (players[i].exiting || (players[i].pflags & PF_NOCONTEST)) + { + continue; + } + + players[i].pflags |= PF_NOCONTEST; + } + + // And lastly, everything in anticipation for Intermission/level change. if (!demo.playback) { // Set up power level gametype scrambles @@ -4404,14 +4426,17 @@ void G_LoadGameSettings(void) } #define GD_VERSIONCHECK 0xBA5ED123 // Change every major version, as usual -#define GD_VERSIONMINOR 6 // Change every format update +#define GD_VERSIONMINOR 9 // Change every format update +// You can't rearrange these without a special format update typedef enum { GDEVER_ADDON = 1, GDEVER_CREDITS = 1<<1, GDEVER_REPLAY = 1<<2, GDEVER_SPECIAL = 1<<3, + GDEVER_KEYTUTORIAL = 1<<4, + GDEVER_KEYMAJORSKIP = 1<<5, } gdeverdone_t; static const char *G_GameDataFolder(void) @@ -4497,7 +4522,7 @@ void G_LoadGameData(void) FIL_WriteFile(va("%s" PATHSEP "%s.bak", srb2home, gamedatafilename), save.buffer, save.size); } - if ((versionMinor == 0 || versionMinor == 1) + if ((versionMinor <= 6) #ifdef DEVELOP || M_CheckParm("-resetchallengegrid") #endif @@ -4517,6 +4542,11 @@ void G_LoadGameData(void) { gamedata->totalrings = READUINT32(save.p); + if (versionMinor >= 9) + { + gamedata->totaltumbletime = READUINT32(save.p); + } + for (i = 0; i < GDGT_MAX; i++) { gamedata->roundsplayed[i] = READUINT32(save.p); @@ -4544,6 +4574,8 @@ void G_LoadGameData(void) gamedata->everfinishedcredits = !!(everflags & GDEVER_CREDITS); gamedata->eversavedreplay = !!(everflags & GDEVER_REPLAY); gamedata->everseenspecial = !!(everflags & GDEVER_SPECIAL); + gamedata->chaokeytutorial = !!(everflags & GDEVER_KEYTUTORIAL); + gamedata->majorkeyskipattempted = !!(everflags & GDEVER_KEYMAJORSKIP); } else { @@ -4557,6 +4589,13 @@ void G_LoadGameData(void) save.p += 4; // no direct equivalent to matchesplayed } + // Prison Egg Pickups + if (versionMinor >= 8) + { + gamedata->thisprisoneggpickup = READUINT16(save.p); + gamedata->prisoneggstothispickup = READUINT16(save.p); + } + { // Quick & dirty hash for what mod this save file is for. UINT32 modID = READUINT32(save.p); @@ -4609,7 +4648,7 @@ void G_LoadGameData(void) if (gridunusable) { UINT16 burn = READUINT16(save.p); // Previous challengegridwidth - UINT8 height = (versionMinor > 0) ? CHALLENGEGRIDHEIGHT : 5; + UINT8 height = (versionMinor && versionMinor <= 6) ? 4 : CHALLENGEGRIDHEIGHT; save.p += (burn * height * unlockreadsize); // Step over previous grid data gamedata->challengegridwidth = 0; @@ -5060,10 +5099,11 @@ void G_SaveGameData(void) } length = (4+1+1+ - 4+4+ + 4+4+4+ (4*GDGT_MAX)+ 4+1+2+2+ 4+ + 2+2+ 4+ (MAXEMBLEMS+(MAXUNLOCKABLES*2)+MAXCONDITIONSETS)+ 4+2); @@ -5200,6 +5240,7 @@ void G_SaveGameData(void) WRITEUINT32(save.p, gamedata->totalplaytime); // 4 WRITEUINT32(save.p, gamedata->totalrings); // 4 + WRITEUINT32(save.p, gamedata->totaltumbletime); // 4 for (i = 0; i < GDGT_MAX; i++) // 4 * GDGT_MAX { @@ -5222,10 +5263,18 @@ void G_SaveGameData(void) everflags |= GDEVER_REPLAY; if (gamedata->everseenspecial) everflags |= GDEVER_SPECIAL; + if (gamedata->chaokeytutorial) + everflags |= GDEVER_KEYTUTORIAL; + if (gamedata->majorkeyskipattempted) + everflags |= GDEVER_KEYMAJORSKIP; WRITEUINT32(save.p, everflags); // 4 } + // Prison Egg Pickups + WRITEUINT16(save.p, gamedata->thisprisoneggpickup); // 2 + WRITEUINT16(save.p, gamedata->prisoneggstothispickup); // 2 + WRITEUINT32(save.p, quickncasehash(timeattackfolder, 64)); // 4 // To save space, use one bit per collected/achieved/unlocked flag diff --git a/src/g_input.c b/src/g_input.c index 6e1b3f8d9..86ceb1d61 100644 --- a/src/g_input.c +++ b/src/g_input.c @@ -21,6 +21,7 @@ #include "i_joy.h" // JOYAXISRANGE #include "r_draw.h" // GTC_ macros for assigning gamepad indicator colors #include "v_video.h" // V_GetColor for assigning gamepad indictaor colors +#include "r_skins.h" // skins[].prefcolor for assigning gamepad indicator colors #include "z_zone.h" // current state of the keys @@ -206,9 +207,7 @@ void G_SetDeviceForPlayer(INT32 player, INT32 device) void G_SetPlayerGamepadIndicatorToPlayerColor(INT32 player) { INT32 device; - INT32 skin; UINT16 skincolor; - UINT8 *colormap; byteColor_t byte_color; I_Assert(player >= 0 && player < MAXSPLITSCREENPLAYERS); @@ -220,16 +219,16 @@ void G_SetPlayerGamepadIndicatorToPlayerColor(INT32 player) return; } - skin = cv_skin[player].value; skincolor = cv_playercolor[player].value; - colormap = R_GetTranslationColormap(skin, skincolor, GTC_MENUCACHE); - - if (colormap == NULL) + if (skincolor == SKINCOLOR_NONE) { - return; + INT32 skin = cv_skin[player].value; + if (skin == -1) + skin = 0; + skincolor = skins[skin].prefcolor; } - byte_color = V_GetColor(colormap[104]).s; + byte_color = V_GetColor(skincolors[skincolor].ramp[8]).s; I_SetGamepadIndicatorColor(device, byte_color.red, byte_color.green, byte_color.blue); } diff --git a/src/info.c b/src/info.c index d68daacb3..8a69214c5 100644 --- a/src/info.c +++ b/src/info.c @@ -143,10 +143,15 @@ char sprnames[NUMSPRITES + 1][5] = "NCHP", // NiGHTS chip "NSTR", // NiGHTS star "EMBM", // Emblem + "SPCN", // Spray Can + "MMSH", // Ancient Shrine + "MORB", // One Morbillion "EMRC", // Chaos Emeralds "SEMR", // Super Emeralds "ESPK", - "SHRD", // Emerald Hunt + + // Prison Egg Drops + "ALTM", // Interactive Objects "BBLS", // water bubble source @@ -635,7 +640,6 @@ char sprnames[NUMSPRITES + 1][5] = "POKE", // Pokey "AUDI", // Audience members "DECO", // Old 1.0 Kart Decoratives + New misc ones - "SPCN", // Spray Can replaces all the old D00Dkart objects "SNES", // Sprites for SNES remake maps "GBAS", // Sprites for GBA remake maps "SPRS", // Sapphire Coast Spring Shell @@ -1907,6 +1911,25 @@ state_t states[NUMSTATES] = // Spray Can {SPR_SPCN, FF_ANIMATE|FF_SEMIBRIGHT, -1, {NULL}, 15, 2, S_NULL}, // S_SPRAYCAN + // Ancient Shrine + {SPR_MMSH, 0, -1, {NULL}, 0, 0, S_NULL}, // S_ANCIENTSHRINE + + {SPR_MORB, 0|FF_ADD, 1, {A_FireShrink}, 2*FRACUNIT/3, 12, S_MORB2}, // S_MORB1 + {SPR_MORB, 1|FF_ADD, 1, {NULL}, 0, 0, S_MORB3}, // S_MORB2 + {SPR_MORB, 2|FF_ADD, 1, {NULL}, 0, 0, S_MORB4}, // S_MORB3 + {SPR_MORB, 3|FF_ADD, 1, {NULL}, 0, 0, S_MORB5}, // S_MORB4 + {SPR_MORB, 4|FF_ADD, 1, {NULL}, 0, 0, S_MORB6}, // S_MORB5 + {SPR_MORB, 5|FF_ADD, 1, {NULL}, 0, 0, S_MORB7}, // S_MORB6 + {SPR_MORB, 6|FF_ADD, 1, {NULL}, 0, 0, S_MORB8}, // S_MORB7 + {SPR_MORB, 7|FF_ADD, 4, {NULL}, 0, 0, S_MORB9}, // S_MORB8 + {SPR_MORB, 6|FF_ADD, 1, {A_FireShrink}, 1, 12, S_MORB10}, // S_MORB9 + {SPR_MORB, 5|FF_ADD, 1, {NULL}, 0, 0, S_MORB11}, // S_MORB10 + {SPR_MORB, 4|FF_ADD, 1, {NULL}, 0, 0, S_MORB12}, // S_MORB11 + {SPR_MORB, 3|FF_ADD, 1, {NULL}, 0, 0, S_MORB13}, // S_MORB12 + {SPR_MORB, 2|FF_ADD, 1, {NULL}, 0, 0, S_MORB14}, // S_MORB13 + {SPR_MORB, 1|FF_ADD, 1, {NULL}, 0, 0, S_MORB15}, // S_MORB14 + {SPR_MORB, 0|FF_ADD, 1, {NULL}, 0, 0, S_NULL}, // S_MORB15 + // Chaos Emeralds {SPR_EMRC, FF_FULLBRIGHT, 1, {NULL}, 0, 0, S_CHAOSEMERALD2}, // S_CHAOSEMERALD1 {SPR_EMRC, FF_FULLBRIGHT|FF_ADD, 1, {NULL}, 0, 0, S_CHAOSEMERALD1}, // S_CHAOSEMERALD2 @@ -1927,10 +1950,8 @@ state_t states[NUMSTATES] = {SPR_LENS, FF_FULLBRIGHT|FF_ADD|FF_TRANS10|FF_ANIMATE|11, 8, {NULL}, 7, 1, S_GAINAX_MID2}, // S_EMERALDFLARE1 - // Emerald hunt shards - {SPR_SHRD, 0, -1, {NULL}, 0, 0, S_NULL}, // S_SHRD1 - {SPR_SHRD, 1, -1, {NULL}, 0, 0, S_NULL}, // S_SHRD2 - {SPR_SHRD, 2, -1, {NULL}, 0, 0, S_NULL}, // S_SHRD3 + // Prison Egg Drops + {SPR_ALTM, 0|FF_PAPERSPRITE|FF_SEMIBRIGHT, -1, {NULL}, 0, 0, S_NULL}, // S_PRISONEGGDROP_CD // Bubble Source {SPR_BBLS, 0, 8, {A_BubbleSpawn}, 2048, 0, S_BUBBLES2}, // S_BUBBLES1 @@ -8392,6 +8413,33 @@ mobjinfo_t mobjinfo[NUMMOBJTYPES] = S_NULL // raisestate }, + { // MT_ANCIENTSHRINE + 2256, // doomednum + S_ANCIENTSHRINE,// spawnstate + 1000, // spawnhealth + S_NULL, // seestate + sfx_None, // seesound + 8, // reactiontime + sfx_None, // attacksound + S_NULL, // painstate + 0, // painchance + sfx_None, // painsound + S_NULL, // meleestate + S_NULL, // missilestate + S_NULL, // deathstate + S_NULL, // xdeathstate + sfx_None, // deathsound + 0, // speed + 48*FRACUNIT, // radius + 80*FRACUNIT, // height + 0, // display offset + 0, // mass + 0, // damage + sfx_None, // activesound + MF_NOGRAVITY|MF_SOLID|MF_DONTENCOREMAP, // flags + S_NULL // raisestate + }, + { // MT_EMERALD -1, // doomednum S_CHAOSEMERALD1, // spawnstate @@ -8473,9 +8521,9 @@ mobjinfo_t mobjinfo[NUMMOBJTYPES] = S_NULL // raisestate }, - { // MT_EMERHUNT - 320, // doomednum - S_SHRD1, // spawnstate + { // MT_PRISONEGGDROP + -1, // doomednum + S_INVISIBLE, // spawnstate 1000, // spawnhealth S_NULL, // seestate sfx_None, // seesound @@ -8486,44 +8534,17 @@ mobjinfo_t mobjinfo[NUMMOBJTYPES] = sfx_None, // painsound S_NULL, // meleestate S_NULL, // missilestate - S_SPRK1, // deathstate - S_NULL, // xdeathstate - sfx_cgot, // deathsound - 8, // speed - 12*FRACUNIT, // radius - 42*FRACUNIT, // height - 0, // display offset - 4, // mass - 0, // damage - sfx_None, // activesound - MF_SPECIAL|MF_NOGRAVITY, // flags - S_NULL // raisestate - }, - - { // MT_EMERALDSPAWN - 321, // doomednum - S_INVISIBLE, // spawnstate - 1000, // spawnhealth - S_NULL, // seestate - sfx_None, // seesound - 0, // reactiontime - sfx_None, // attacksound - S_NULL, // painstate - 0, // painchance - sfx_None, // painsound - S_NULL, // meleestate - S_NULL, // missilestate S_NULL, // deathstate S_NULL, // xdeathstate - sfx_None, // deathsound + sfx_s3k9c, // deathsound 0, // speed - 8, // radius - 8, // height + 65*FRACUNIT, // radius + 130*FRACUNIT, // height 0, // display offset - 10, // mass + 16, // mass 0, // damage sfx_None, // activesound - MF_NOBLOCKMAP|MF_NOGRAVITY|MF_NOSECTOR, // flags + MF_SPECIAL|MF_PICKUPFROMBELOW|MF_DONTENCOREMAP, // flags S_NULL // raisestate }, diff --git a/src/info.h b/src/info.h index 9c0a2cb0a..d99201fb7 100644 --- a/src/info.h +++ b/src/info.h @@ -698,10 +698,15 @@ typedef enum sprite SPR_NCHP, // NiGHTS chip SPR_NSTR, // NiGHTS star SPR_EMBM, // Emblem + SPR_SPCN, // Spray Can + SPR_MMSH, // Ancient Shrine + SPR_MORB, // One Morbillion SPR_EMRC, // Chaos Emeralds SPR_SEMR, // Super Emeralds SPR_ESPK, - SPR_SHRD, // Emerald Hunt + + // Prison Egg Drops + SPR_ALTM, // Interactive Objects SPR_BBLS, // water bubble source @@ -1190,7 +1195,6 @@ typedef enum sprite SPR_POKE, // Pokey SPR_AUDI, // Audience members SPR_DECO, // Old 1.0 Kart Decoratives + New misc ones - SPR_SPCN, // Spray Can replaces all the old D00Dkart objects SPR_SNES, // Sprites for SNES remake maps SPR_GBAS, // Sprites for GBA remake maps SPR_SPRS, // Sapphire Coast Spring Shell @@ -2387,6 +2391,25 @@ typedef enum state // Spray Can S_SPRAYCAN, + // Ancient Shrine + S_ANCIENTSHRINE, + + S_MORB1, + S_MORB2, + S_MORB3, + S_MORB4, + S_MORB5, + S_MORB6, + S_MORB7, + S_MORB8, + S_MORB9, + S_MORB10, + S_MORB11, + S_MORB12, + S_MORB13, + S_MORB14, + S_MORB15, + // Chaos Emeralds S_CHAOSEMERALD1, S_CHAOSEMERALD2, @@ -2407,10 +2430,8 @@ typedef enum state S_EMERALDFLARE1, - // Emerald hunt shards - S_SHRD1, - S_SHRD2, - S_SHRD3, + // Prison Egg Drops + S_PRISONEGGDROP_CD, // Bubble Source S_BUBBLES1, @@ -6076,11 +6097,11 @@ typedef enum mobj_type MT_BLUEFLAG, // Blue CTF Flag MT_EMBLEM, MT_SPRAYCAN, + MT_ANCIENTSHRINE, MT_EMERALD, MT_EMERALDSPARK, MT_EMERALDFLARE, - MT_EMERHUNT, // Emerald Hunt - MT_EMERALDSPAWN, // Emerald spawner w/ delay + MT_PRISONEGGDROP, // Springs and others MT_FAN, diff --git a/src/k_bot.cpp b/src/k_bot.cpp index d6b070153..2ce727ce9 100644 --- a/src/k_bot.cpp +++ b/src/k_bot.cpp @@ -59,7 +59,7 @@ void K_SetBot(UINT8 newplayernum, UINT8 skinnum, UINT8 difficulty, botStyle_e st playernode[newplayernum] = servernode; // this will permit unlocks - memcpy(&players[newplayernum].availabilities, R_GetSkinAvailabilities(false, true), MAXAVAILABILITY*sizeof(UINT8)); + memcpy(&players[newplayernum].availabilities, R_GetSkinAvailabilities(false, skinnum), MAXAVAILABILITY*sizeof(UINT8)); players[newplayernum].splitscreenindex = 0; players[newplayernum].bot = true; diff --git a/src/k_collide.cpp b/src/k_collide.cpp index 5afd75d18..503481bb3 100644 --- a/src/k_collide.cpp +++ b/src/k_collide.cpp @@ -910,6 +910,14 @@ boolean K_InstaWhipCollide(mobj_t *shield, mobj_t *victim) K_AddHitLag(victim, victimHitlag, true); K_AddHitLag(attacker, attackerHitlag, false); shield->hitlag = attacker->hitlag; + + if (attackerPlayer->roundconditions.whip_hyuu == false + && attackerPlayer->hyudorotimer > 0) + { + attackerPlayer->roundconditions.whip_hyuu = true; + attackerPlayer->roundconditions.checkthisframe = true; + } + return true; } return false; diff --git a/src/k_follower.c b/src/k_follower.c index 4cbfb84b9..f288d63f1 100644 --- a/src/k_follower.c +++ b/src/k_follower.c @@ -710,25 +710,47 @@ void K_HandleFollower(player_t *player) } /*-------------------------------------------------- - void K_FollowerHornTaunt(player_t *taunter, player_t *victim) + void K_FollowerHornTaunt(player_t *taunter, player_t *victim, boolean mysticmelodyspecial) See header file for description. --------------------------------------------------*/ -void K_FollowerHornTaunt(player_t *taunter, player_t *victim) +void K_FollowerHornTaunt(player_t *taunter, player_t *victim, boolean mysticmelodyspecial) { + // Basic checks if ( - (cv_karthorns.value == 0) - || taunter == NULL + taunter == NULL || victim == NULL || taunter->followerskin < 0 || taunter->followerskin >= numfollowers + ) + { + return; + } + + const follower_t *fl = &followers[taunter->followerskin]; + + // Restrict mystic melody special status + if (mysticmelodyspecial == true) + { + mysticmelodyspecial = ( + (demo.playback == false) // No downloading somebody else's replay + && (fl->hornsound == sfx_melody) // Must be the Mystic Melody + && (taunter->bot == false) // No getting your puppies to do it for you + && P_IsLocalPlayer(taunter) // Must be in your party + && !(mapheaderinfo[gamemap-1]->records.mapvisited & MV_MYSTICMELODY) // Not already done + ); + } + + // More expensive checks + if ( + (cv_karthorns.value == 0 && mysticmelodyspecial == false) || (P_IsDisplayPlayer(victim) == false && cv_karthorns.value != 2) || P_MobjWasRemoved(taunter->mo) == true || P_MobjWasRemoved(taunter->follower) == true ) + { return; - - const follower_t *fl = &followers[taunter->followerskin]; + } const boolean tasteful = (taunter->karthud[khud_taunthorns] == 0); @@ -737,6 +759,40 @@ void K_FollowerHornTaunt(player_t *taunter, player_t *victim) mobj_t *honk = taunter->follower->hprev; const fixed_t desiredscale = (2*taunter->mo->scale)/3; + if (mysticmelodyspecial == true) + { + mobj_t *mobj = NULL, *next = NULL; + + for (mobj = trackercap; mobj; mobj = next) + { + next = mobj->itnext; + if (mobj->type != MT_ANCIENTSHRINE) + { + // Not relevant + continue; + } + + if (P_MobjWasRemoved(mobj->tracer) == false) + { + // Already initiated + continue; + } + + // Cleverly a mobj type where TypeIsNetSynced is false + P_SetTarget(&mobj->tracer, P_SpawnMobjFromMobj(mobj, 0, 0, 0, MT_HORNCODE)); + + if (P_MobjWasRemoved(mobj->tracer) == true) + { + // Unrecoverable?! + continue; + } + + // This is a helper non-netsynced countdown + mobj->tracer->renderflags |= RF_DONTDRAW; + mobj->tracer->fuse = 2*TICRATE; + } + } + if (P_MobjWasRemoved(honk) == true) { honk = P_SpawnMobj( @@ -773,12 +829,21 @@ void K_FollowerHornTaunt(player_t *taunter, player_t *victim) honk->fuse = TICRATE/2; honk->renderflags |= RF_DONTDRAW; - if (P_IsDisplayPlayer(victim) || P_IsDisplayPlayer(taunter)) - S_StartSound(NULL, fl->hornsound); - honk->flags2 |= MF2_AMBUSH; } - honk->renderflags &= ~K_GetPlayerDontDrawFlag(victim); + UINT32 dontdrawflag = K_GetPlayerDontDrawFlag(victim); + + // A display player is affected! + if (dontdrawflag != 0) + { + // Only play the sound for the first seen display player + if ((honk->renderflags & RF_DONTDRAW) == RF_DONTDRAW) + { + S_StartSound(NULL, fl->hornsound); + } + + honk->renderflags &= ~dontdrawflag; + } } } diff --git a/src/k_follower.h b/src/k_follower.h index 9da3d4e73..af66b8113 100644 --- a/src/k_follower.h +++ b/src/k_follower.h @@ -227,19 +227,20 @@ void K_HandleFollower(player_t *player); void K_RemoveFollower(player_t *player); /*-------------------------------------------------- - void K_FollowerHornTaunt(player_t *taunter, player_t *victim) + void K_FollowerHornTaunt(player_t *taunter, player_t *victim, boolean mysticmelodyspecial) Plays horn and spawns object (MOSTLY non-netsynced) Input Arguments:- taunter - Source player with a follower victim - Player that hears and sees the honk + mysticmelodyspecial - Special Mystic Melody behaviour Return:- None --------------------------------------------------*/ -void K_FollowerHornTaunt(player_t *taunter, player_t *victim); +void K_FollowerHornTaunt(player_t *taunter, player_t *victim, boolean mysticmelodyspecial); #ifdef __cplusplus } // extern "C" diff --git a/src/k_grandprix.c b/src/k_grandprix.c index 41522f3d8..9f2ed102f 100644 --- a/src/k_grandprix.c +++ b/src/k_grandprix.c @@ -217,7 +217,7 @@ void K_InitGrandPrixBots(void) for (j = 0; j < numplayers; j++) { player_t *p = &players[competitors[j]]; - char *rivalname = skins[p->skin].rivals[i]; + const char *rivalname = skins[p->skin].rivals[i]; INT32 rivalnum = R_SkinAvailable(rivalname); // Intentionally referenced before (currently dummied out) unlock check. Such a tease! @@ -714,11 +714,13 @@ void K_RetireBots(void) if (usableskins > 0) { - UINT8 index = P_RandomKey(PR_RULESCRAMBLE, usableskins); + UINT8 index = P_RandomKey(PR_BOTS, usableskins); skinnum = grabskins[index]; grabskins[index] = grabskins[--usableskins]; } + memcpy(&bot->availabilities, R_GetSkinAvailabilities(false, skinnum), MAXAVAILABILITY*sizeof(UINT8)); + bot->botvars.difficulty = newDifficulty; bot->botvars.diffincrease = 0; @@ -763,9 +765,15 @@ void K_FakeBotResults(player_t *bot) } } - if (besttime == UINT32_MAX // No one finished, so you don't finish either. - || bot->distancetofinish >= worstdist) // Last place, you aren't going to finish. + if (besttime == UINT32_MAX) // No one finished, so you don't finish either. { + // We don't apply PF_NOCONTEST in the exitlevel case - that's done for all players in G_DoCompleted. + return; + } + + if (bot->distancetofinish >= worstdist) // Last place, you aren't going to finish. + { + // This was a successful murder! bot->pflags |= PF_NOCONTEST; return; } diff --git a/src/k_hud.c b/src/k_hud.c index 82c8232d3..b56ed1b02 100644 --- a/src/k_hud.c +++ b/src/k_hud.c @@ -75,8 +75,8 @@ static patch_t *kp_racefinish[6]; static patch_t *kp_positionnum[10][2][2]; // number, overlay or underlay, splitscreen -static patch_t *kp_facenum[MAXPLAYERS+1]; -patch_t *kp_facehighlight[8]; +patch_t *kp_facenum[MAXPLAYERS+1]; +static patch_t *kp_facehighlight[8]; static patch_t *kp_nocontestminimap; static patch_t *kp_spbminimap; diff --git a/src/k_hud.h b/src/k_hud.h index c5efb621b..fb94b0734 100644 --- a/src/k_hud.h +++ b/src/k_hud.h @@ -82,6 +82,7 @@ extern patch_t *kp_button_right[2]; extern patch_t *kp_button_left[2]; extern patch_t *kp_eggnum[6]; +extern patch_t *kp_facenum[MAXPLAYERS+1]; #ifdef __cplusplus } // extern "C" diff --git a/src/k_kart.c b/src/k_kart.c index 1b3e02257..182e0a7cb 100644 --- a/src/k_kart.c +++ b/src/k_kart.c @@ -267,6 +267,22 @@ void K_TimerInit(void) { K_SpawnDuelOnlyItems(); } + + if ( + battleprisons == true + && grandprixinfo.gp == true + && netgame == false + && gamedata->thisprisoneggpickup_cached != NULL + && gamedata->prisoneggstothispickup == 0 + && maptargets > 1 + ) + { + // This calculation is like this so... + // - You can't get a Prison Egg Drop on the last broken target + // - If it were 0 at minimum there'd be a slight bias towards the start of the round + // - This is bad because it benefits CD farming like in Brawl :D + gamedata->prisoneggstothispickup = 1 + M_RandomKey(maptargets - 1); + } } UINT32 K_GetPlayerDontDrawFlag(player_t *player) @@ -1133,6 +1149,7 @@ static void K_UpdateOffroad(player_t *player) player->offroad = offroadstrength; if (player->roundconditions.touched_offroad == false + && !(player->exiting || (player->pflags & PF_NOCONTEST)) && player->offroad > (2*offroadstrength) / TICRATE) { player->roundconditions.touched_offroad = true; @@ -1449,7 +1466,8 @@ static void K_UpdateDraft(player_t *player) if (K_TryDraft(player, otherPlayer->mo, minDist, draftdistance, leniency) == true) { - return; // Finished doing our draft. + //return; + goto draftdurationhandling; // Finished doing our draft. } } } @@ -1484,7 +1502,14 @@ static void K_UpdateDraft(player_t *player) { player->draftpower = 0; player->lastdraft = -1; + player->roundconditions.continuousdraft = 0; + return; } + +draftdurationhandling: + player->roundconditions.continuousdraft++; + if (player->roundconditions.continuousdraft > player->roundconditions.continuousdraft_best) + player->roundconditions.continuousdraft_best = player->roundconditions.continuousdraft; } void K_KartPainEnergyFling(player_t *player) @@ -2035,10 +2060,10 @@ static SINT8 K_GlanceAtPlayers(player_t *glancePlayer, boolean horn) { const fixed_t maxdistance = FixedMul(1280 * mapobjectscale, K_GetKartGameSpeedScalar(gamespeed)); const angle_t blindSpotSize = ANG10; // ANG5 - UINT8 i; SINT8 glanceDir = 0; SINT8 lastValidGlance = 0; - boolean podiumspecial = (K_PodiumSequence() == true && glancePlayer->nextwaypoint == NULL && glancePlayer->speed == 0); + const boolean podiumspecial = (K_PodiumSequence() == true && glancePlayer->nextwaypoint == NULL && glancePlayer->speed == 0); + boolean mysticmelodyspecial = false; if (podiumspecial) { @@ -2057,43 +2082,47 @@ static SINT8 K_GlanceAtPlayers(player_t *glancePlayer, boolean horn) // See if there's any players coming up behind us. // If so, your character will glance at 'em. - for (i = 0; i < MAXPLAYERS; i++) + mobj_t *victim = NULL, *victimnext = NULL; + + for (victim = trackercap; victim; victim = victimnext) { - player_t *p; + player_t *p = victim->player; angle_t back; angle_t diff; fixed_t distance; SINT8 dir = -1; - if (!playeringame[i]) + victimnext = victim->itnext; + + if (p != NULL) { - // Invalid player - continue; + if (p == glancePlayer) + { + // FOOL! Don't glance at yerself! + continue; + } + + if (p->spectator || p->hyudorotimer > 0) + { + // Not playing / invisible + continue; + } + + if (podiumspecial && p->position >= glancePlayer->position) + { + // On the podium, only look with envy, not condesencion + continue; + } } - - p = &players[i]; - - if (p == glancePlayer) + else if (victim->type != MT_ANCIENTSHRINE) { - // FOOL! Don't glance at yerself! - continue; - } - - if (!p->mo || P_MobjWasRemoved(p->mo)) - { - // Invalid mobj - continue; - } - - if (p->spectator || p->hyudorotimer > 0) - { - // Not playing / invisible + // Ancient Shrines are a special exception to glance logic. continue; } if (!podiumspecial) { - distance = R_PointToDist2(glancePlayer->mo->x, glancePlayer->mo->y, p->mo->x, p->mo->y); + distance = R_PointToDist2(glancePlayer->mo->x, glancePlayer->mo->y, victim->x, victim->y); if (distance > maxdistance) { @@ -2101,13 +2130,9 @@ static SINT8 K_GlanceAtPlayers(player_t *glancePlayer, boolean horn) continue; } } - else if (p->position >= glancePlayer->position) - { - continue; - } back = glancePlayer->mo->angle + ANGLE_180; - diff = R_PointToAngle2(glancePlayer->mo->x, glancePlayer->mo->y, p->mo->x, p->mo->y) - back; + diff = R_PointToAngle2(glancePlayer->mo->x, glancePlayer->mo->y, victim->x, victim->y) - back; if (diff > ANGLE_180) { @@ -2127,7 +2152,7 @@ static SINT8 K_GlanceAtPlayers(player_t *glancePlayer, boolean horn) continue; } - if (!podiumspecial && P_CheckSight(glancePlayer->mo, p->mo) == false) + if (!podiumspecial && P_CheckSight(glancePlayer->mo, victim) == false) { // Blocked by a wall, we can't glance at 'em! continue; @@ -2142,7 +2167,14 @@ static SINT8 K_GlanceAtPlayers(player_t *glancePlayer, boolean horn) if (horn == true) { - K_FollowerHornTaunt(glancePlayer, p); + if (p != NULL) + { + K_FollowerHornTaunt(glancePlayer, p, false); + } + else if (victim->type == MT_ANCIENTSHRINE) + { + mysticmelodyspecial = true; + } } } @@ -2150,7 +2182,7 @@ static SINT8 K_GlanceAtPlayers(player_t *glancePlayer, boolean horn) { const boolean tasteful = (glancePlayer->karthud[khud_taunthorns] == 0); - K_FollowerHornTaunt(glancePlayer, glancePlayer); + K_FollowerHornTaunt(glancePlayer, glancePlayer, mysticmelodyspecial); if (tasteful && glancePlayer->karthud[khud_taunthorns] < 2*TICRATE) glancePlayer->karthud[khud_taunthorns] = 2*TICRATE; @@ -3810,6 +3842,7 @@ void K_RemoveGrowShrink(player_t *player) } player->growshrinktimer = 0; + player->roundconditions.consecutive_grow_lasers = 0; } boolean K_IsBigger(mobj_t *compare, mobj_t *other) @@ -6066,6 +6099,7 @@ void K_DoSneaker(player_t *player, INT32 type) const fixed_t intendedboost = FRACUNIT/2; if (player->roundconditions.touched_sneakerpanel == false + && !(player->exiting || (player->pflags & PF_NOCONTEST)) && player->floorboost != 0) { player->roundconditions.touched_sneakerpanel = true; @@ -7624,6 +7658,14 @@ void K_KartPlayerHUDUpdate(player_t *player) } else player->karthud[khud_finish] = 0; + + if (demo.playback == false && P_IsLocalPlayer(player) == true) + { + if (player->tumbleBounces != 0 && gamedata->totaltumbletime != UINT32_MAX) + { + gamedata->totaltumbletime++; + } + } } #undef RINGANIM_DELAYMAX @@ -8407,7 +8449,11 @@ void K_KartPlayerThink(player_t *player, ticcmd_t *cmd) //player->flashing = 0; eggsexplode = P_SpawnMobj(player->mo->x, player->mo->y, player->mo->z, MT_SPBEXPLOSION); eggsexplode->height = 2 * player->mo->height; - eggsexplode->color = player->mo->color; + K_FlipFromObject(eggsexplode, player->mo); + + eggsexplode->threshold = KITEM_EGGMAN; + + P_SetTarget(&eggsexplode->tracer, player->mo); if (player->eggmanblame >= 0 && player->eggmanblame < MAXPLAYERS @@ -11783,6 +11829,10 @@ void K_MoveKartPlayer(player_t *player, boolean onground) K_ThrowKartItem(player, true, MT_GACHABOM, 0, 0, 0); K_PlayAttackTaunt(player->mo); player->itemamount--; + player->roundconditions.gachabom_miser = ( + (player->roundconditions.gachabom_miser == 0) + ? 1 : 0xFF + ); K_UpdateHnextList(player, false); } break; @@ -12361,6 +12411,7 @@ void K_EggmanTransfer(player_t *source, player_t *victim) K_AddHitLag(victim->mo, 2, true); K_DropItems(victim); victim->eggmanexplode = 6*TICRATE; + victim->eggmanblame = (source - players); K_StopRoulette(&victim->itemRoulette); if (P_IsDisplayPlayer(victim)) @@ -12368,6 +12419,7 @@ void K_EggmanTransfer(player_t *source, player_t *victim) K_AddHitLag(source->mo, 2, true); source->eggmanexplode = 0; + source->eggmanblame = -1; K_StopRoulette(&source->itemRoulette); source->eggmanTransferDelay = 10; diff --git a/src/k_menu.h b/src/k_menu.h index 858749463..f2fd07acf 100644 --- a/src/k_menu.h +++ b/src/k_menu.h @@ -1142,6 +1142,9 @@ void M_HandleImageDef(INT32 choice); #define recommendedflags V_GREENMAP #define warningflags V_GRAYMAP +// For some menu highlights +UINT16 M_GetCvPlayerColor(UINT8 pnum); + void M_UpdateMenuBGImage(boolean forceReset); void M_DrawMenuBackground(void); void M_DrawMenuForeground(void); @@ -1201,19 +1204,34 @@ void M_DrawAddons(void); #define RIGHTUNLOCKSCROLL 3 #define LEFTUNLOCKSCROLL (RIGHTUNLOCKSCROLL-1) -#define CC_TOTAL 0 -#define CC_UNLOCKED 1 -#define CC_TALLY 2 -#define CC_ANIM 3 -#define CC_CHAOANIM 4 -#define CC_CHAONOPE 5 -#define CC_MAX 6 +typedef enum +{ + CMC_TOTAL = 0, + CMC_UNLOCKED, + + CMC_KEYED, + CMC_MAJORSKIPPED, + + CMC_PERCENT, + + CMC_MEDALID, + CMC_MEDALBLANK, + CMC_MEDALFILLED, + + CMC_ANIM, + CMC_CHAOANIM, + CMC_CHAONOPE, + + CMC_MAX, +} challengesmenucount_e; #define TILEFLIP_MAX 16 -#define CHAOHOLD_MAX (3*TICRATE/2) -#define CHAOHOLD_BEGIN 7 -#define CHAOHOLD_END 3 +#define CHAOHOLD_STANDARD (40) // (Close to 3*TICRATE/2 after padding, but adjusted to evenly divide by 10) +#define CHAOHOLD_MAJOR (60) //(3*CHAOHOLD_STANDARD/2) +#define CHAOHOLD_BEGIN (7) +#define CHAOHOLD_END (3) +#define CHAOHOLD_PADDING (CHAOHOLD_BEGIN + CHAOHOLD_END) extern struct timeattackmenu_s { @@ -1241,12 +1259,12 @@ extern struct challengesmenu_s { boolean pending; boolean requestnew; - boolean chaokeyadd; + boolean chaokeyadd, keywasadded; UINT8 chaokeyhold; boolean requestflip; - UINT16 unlockcount[CC_MAX]; + UINT16 unlockcount[CMC_MAX]; UINT8 fade; } challengesmenu; @@ -1256,6 +1274,7 @@ void M_Challenges(INT32 choice); void M_DrawChallenges(void); void M_ChallengesTick(void); boolean M_ChallengesInputs(INT32 ch); +boolean M_CanKeyHiliTile(void); typedef enum { diff --git a/src/k_menudraw.c b/src/k_menudraw.c index bf0712b14..365b190d7 100644 --- a/src/k_menudraw.c +++ b/src/k_menudraw.c @@ -231,6 +231,22 @@ void M_DrawMenuBackground(void) } } +UINT16 M_GetCvPlayerColor(UINT8 pnum) +{ + if (pnum >= MAXSPLITSCREENPLAYERS) + return SKINCOLOR_NONE; + + UINT16 color = cv_playercolor[pnum].value; + if (color != SKINCOLOR_NONE) + return color; + + INT32 skin = R_SkinAvailable(cv_skin[pnum].string); + if (skin == -1) + return SKINCOLOR_NONE; + + return skins[skin].prefcolor; +} + static void M_DrawMenuParty(void) { const INT32 PLATTER_WIDTH = 19; @@ -253,6 +269,18 @@ static void M_DrawMenuParty(void) x = 2; y = BASEVIDHEIGHT - small->height - 2; + // Despite the work put into it, can't use M_GetCvPlayerColor directly - we need to reference skin always. + #define grab_skin_and_colormap(pnum) \ + { \ + skin = R_SkinAvailable(cv_skin[pnum].string); \ + color = cv_playercolor[pnum].value; \ + if (skin == -1) \ + skin = 0; \ + if (color == SKINCOLOR_NONE) \ + color = skins[skin].prefcolor; \ + colormap = R_GetTranslationColormap(skin, color, GTC_MENUCACHE); \ + } + switch (setup_numplayers) { case 1: @@ -260,9 +288,7 @@ static void M_DrawMenuParty(void) x -= 8; V_DrawScaledPatch(x, y, 0, small); - skin = R_SkinAvailable(cv_skin[0].string); - color = cv_playercolor[0].value; - colormap = R_GetTranslationColormap(skin, color, GTC_MENUCACHE); + grab_skin_and_colormap(0); V_DrawMappedPatch(x + 22, y + 8, 0, faceprefix[skin][FACE_MINIMAP], colormap); break; @@ -273,15 +299,11 @@ static void M_DrawMenuParty(void) V_DrawScaledPatch(x, y, 0, small); V_DrawScaledPatch(x + PLATTER_OFFSET, y - PLATTER_STAGGER, 0, small); - skin = R_SkinAvailable(cv_skin[1].string); - color = cv_playercolor[1].value; - colormap = R_GetTranslationColormap(skin, color, GTC_MENUCACHE); + grab_skin_and_colormap(1); V_DrawMappedPatch(x + PLATTER_OFFSET + 22, y - PLATTER_STAGGER + 8, 0, faceprefix[skin][FACE_MINIMAP], colormap); - skin = R_SkinAvailable(cv_skin[0].string); - color = cv_playercolor[0].value; - colormap = R_GetTranslationColormap(skin, color, GTC_MENUCACHE); + grab_skin_and_colormap(0); V_DrawMappedPatch(x + 22, y + 8, 0, faceprefix[skin][FACE_MINIMAP], colormap); break; @@ -291,21 +313,15 @@ static void M_DrawMenuParty(void) V_DrawScaledPatch(x, y, 0, large); V_DrawScaledPatch(x + PLATTER_OFFSET, y - PLATTER_STAGGER, 0, small); - skin = R_SkinAvailable(cv_skin[1].string); - color = cv_playercolor[1].value; - colormap = R_GetTranslationColormap(skin, color, GTC_MENUCACHE); + grab_skin_and_colormap(1); V_DrawMappedPatch(x + PLATTER_OFFSET + 22, y - PLATTER_STAGGER + 8, 0, faceprefix[skin][FACE_MINIMAP], colormap); - skin = R_SkinAvailable(cv_skin[0].string); - color = cv_playercolor[0].value; - colormap = R_GetTranslationColormap(skin, color, GTC_MENUCACHE); + grab_skin_and_colormap(0); V_DrawMappedPatch(x + 12, y - 2, 0, faceprefix[skin][FACE_MINIMAP], colormap); - skin = R_SkinAvailable(cv_skin[2].string); - color = cv_playercolor[2].value; - colormap = R_GetTranslationColormap(skin, color, GTC_MENUCACHE); + grab_skin_and_colormap(2); V_DrawMappedPatch(x + 22, y + 8, 0, faceprefix[skin][FACE_MINIMAP], colormap); break; @@ -315,27 +331,19 @@ static void M_DrawMenuParty(void) V_DrawScaledPatch(x, y, 0, large); V_DrawScaledPatch(x + PLATTER_OFFSET, y - PLATTER_STAGGER, 0, large); - skin = R_SkinAvailable(cv_skin[1].string); - color = cv_playercolor[1].value; - colormap = R_GetTranslationColormap(skin, color, GTC_MENUCACHE); + grab_skin_and_colormap(1); V_DrawMappedPatch(x + PLATTER_OFFSET + 12, y - PLATTER_STAGGER - 2, 0, faceprefix[skin][FACE_MINIMAP], colormap); - skin = R_SkinAvailable(cv_skin[0].string); - color = cv_playercolor[0].value; - colormap = R_GetTranslationColormap(skin, color, GTC_MENUCACHE); + grab_skin_and_colormap(0); V_DrawMappedPatch(x + 12, y - 2, 0, faceprefix[skin][FACE_MINIMAP], colormap); - skin = R_SkinAvailable(cv_skin[3].string); - color = cv_playercolor[3].value; - colormap = R_GetTranslationColormap(skin, color, GTC_MENUCACHE); + grab_skin_and_colormap(3); V_DrawMappedPatch(x + PLATTER_OFFSET + 22, y - PLATTER_STAGGER + 8, 0, faceprefix[skin][FACE_MINIMAP], colormap); - skin = R_SkinAvailable(cv_skin[2].string); - color = cv_playercolor[2].value; - colormap = R_GetTranslationColormap(skin, color, GTC_MENUCACHE); + grab_skin_and_colormap(2); V_DrawMappedPatch(x + 22, y + 8, 0, faceprefix[skin][FACE_MINIMAP], colormap); break; @@ -346,6 +354,8 @@ static void M_DrawMenuParty(void) } } + #undef grab_skin_and_color + x += PLATTER_WIDTH; y += small->height; V_DrawScaledPatch(x + 16, y - 12, 0, W_CachePatchName(va("OPPRNK0%d", setup_numplayers % 10), PU_CACHE)); @@ -5261,7 +5271,7 @@ static void M_DrawChallengeTile(INT16 i, INT16 j, INT32 x, INT32 y, boolean hili if (categoryside) { - char categoryid = '8'; + char categoryid = '0'; colormap = bgmap; switch (ref->type) { @@ -5299,6 +5309,9 @@ static void M_DrawChallengeTile(INT16 i, INT16 j, INT32 x, INT32 y, boolean hili case SECRET_SPBATTACK: categoryid = '7'; break; + case SECRET_ALTMUSIC: + categoryid = '9'; + break; } pat = W_CachePatchName(va("UN_RR0%c%c", categoryid, @@ -5361,6 +5374,9 @@ static void M_DrawChallengeTile(INT16 i, INT16 j, INT32 x, INT32 y, boolean hili case SECRET_MAP: iconid = 14; break; + case SECRET_ALTMUSIC: + iconid = 16; + break; case SECRET_HARDSPEED: iconid = 3; @@ -5469,7 +5485,7 @@ drawborder: buffer[7] = (skullAnimCounter/5) ? '2' : '1'; pat = W_CachePatchName(buffer, PU_CACHE); - colormap = R_GetTranslationColormap(TC_DEFAULT, cv_playercolor[0].value, GTC_MENUCACHE); + colormap = R_GetTranslationColormap(TC_DEFAULT, M_GetCvPlayerColor(0), GTC_MENUCACHE); V_DrawFixedPatch( x*FRACUNIT, y*FRACUNIT, @@ -5555,7 +5571,7 @@ static void M_DrawChallengePreview(INT32 x, INT32 y) addflags ^= V_FLIP; // This sprite is left/right flipped! } - V_DrawFixedPatch(x*FRACUNIT, (y+6)*FRACUNIT, FRACUNIT, addflags, patch, NULL); + V_DrawFixedPatch(x*FRACUNIT, (y+2)*FRACUNIT, FRACUNIT, addflags, patch, NULL); return; } @@ -5583,7 +5599,7 @@ static void M_DrawChallengePreview(INT32 x, INT32 y) break; } - M_DrawCharacterIconAndEngine(4, BASEVIDHEIGHT-(4+16), i, colormap, (i == skin)); + M_DrawCharacterIconAndEngine(4, BASEVIDHEIGHT-(4+16), i, colormap, (i != skin)); } break; } @@ -5870,7 +5886,7 @@ static void M_DrawChallengePreview(INT32 x, INT32 y) } else { - colormap = R_GetTranslationColormap(TC_DEFAULT, cv_playercolor[0].value, GTC_MENUCACHE); + colormap = R_GetTranslationColormap(TC_DEFAULT, M_GetCvPlayerColor(0), GTC_MENUCACHE); V_DrawFixedPatch((x+40)<chaokeys < GDMAX_CHAOKEYS) + { + #if (GDCONVERT_ROUNDSTOKEY != 32) + offs = ((gamedata->pendingkeyroundoffset * keybarlen)/GDCONVERT_ROUNDSTOKEY); + #else + offs = gamedata->pendingkeyroundoffset; + #endif + } + + if (offs > 0) + V_DrawFill(1+2, keybary, offs, 1, 0); + if (offs < keybarlen) + V_DrawFadeFill(1+2+offs, keybary, keybarlen-offs, 1, 0, 31, challengetransparentstrength); + } + + // Counter + { + INT32 textx = 4, texty = 20-challengesmenu.unlockcount[CMC_CHAOANIM]; + UINT8 numbers[4]; + numbers[0] = ((gamedata->chaokeys / 100) % 10); + numbers[1] = ((gamedata->chaokeys / 10) % 10); + numbers[2] = (gamedata->chaokeys % 10); + + numbers[3] = ((gamedata->chaokeys / 1000) % 10); + if (numbers[3] != 0) + { + V_DrawScaledPatch(textx - 4, texty, 0, kp_facenum[numbers[3]]); + textx += 2; + } + + UINT8 i = 0; + while (i < 3) + { + V_DrawScaledPatch(textx, texty, 0, kp_facenum[numbers[i]]); + textx += 6; + i++; + } + } + + UINT8 keysbeingused = 0; + + // The Chao Key swooping animation + if (challengesmenu.currentunlock < MAXUNLOCKABLES && challengesmenu.chaokeyhold) + { + fixed_t baseradius = challengesgridstep; + + boolean major = false, ending = false; + if (unlockables[challengesmenu.currentunlock].majorunlock == true) + { + major = true; + tilex += challengesgridstep/2; + tiley += challengesgridstep/2; + baseradius = (7*baseradius)/4; + } + + const INT32 chaohold_duration = + CHAOHOLD_PADDING + + (major + ? CHAOHOLD_MAJOR + : CHAOHOLD_STANDARD + ); + + if (challengesmenu.chaokeyhold >= chaohold_duration - CHAOHOLD_END) + { + ending = true; + baseradius = ((chaohold_duration - challengesmenu.chaokeyhold)*baseradius)*(FRACUNIT/CHAOHOLD_END); + } + + INT16 specifickeyholdtime = challengesmenu.chaokeyhold; + + for (; keysbeingused < (major ? 10 : 1); keysbeingused++, specifickeyholdtime -= (CHAOHOLD_STANDARD/10)) + { + fixed_t radius = baseradius; + fixed_t thiskeyx, thiskeyy; + fixed_t keyholdrotation = 0; + + if (specifickeyholdtime < CHAOHOLD_BEGIN) + { + if (specifickeyholdtime <= 0) + { + // Nothing following will be relevant + break; + } + + radius = (specifickeyholdtime*radius)*(FRACUNIT/CHAOHOLD_BEGIN); + thiskeyx = keyx + specifickeyholdtime*((tilex*FRACUNIT) - keyx)/CHAOHOLD_BEGIN; + thiskeyy = keyy + specifickeyholdtime*((tiley*FRACUNIT) - keyy)/CHAOHOLD_BEGIN; + } + else + { + keyholdrotation = (-36 * keysbeingused) * FRACUNIT; // 360/10 + + if (ending == false) + { + radius <<= FRACBITS; + + keyholdrotation += 360 * ((challengesmenu.chaokeyhold - CHAOHOLD_BEGIN)) + * (FRACUNIT/(CHAOHOLD_STANDARD)); // intentionally not chaohold_duration + + if (keysbeingused == 0) + { + INT32 time = (major ? 5 : 3) - (keyholdrotation - 1) / (90 * FRACUNIT); + if (time <= 5 && time >= 0) + V_DrawScaledPatch(tilex + 2, tiley - 2, 0, kp_eggnum[time]); + } + } + + thiskeyx = tilex*FRACUNIT; + thiskeyy = tiley*FRACUNIT; + } + + if (radius != 0) + { + angle_t ang = (FixedAngle( + keyholdrotation + ) >> ANGLETOFINESHIFT) & FINEMASK; + + thiskeyx += FixedMul(radius, FINESINE(ang)); + thiskeyy -= FixedMul(radius, FINECOSINE(ang)); + } + + V_DrawFixedPatch(thiskeyx, thiskeyy, FRACUNIT, 0, key, NULL); + } + } + + // The final Chao Key on the stack + { + UINT8 *lastkeycolormap = NULL; + + if (gamedata->chaokeys <= keysbeingused) + { + // Greyed out if there's going to be none left + lastkeycolormap = R_GetTranslationColormap(TC_BLINK, SKINCOLOR_BLACK, GTC_MENUCACHE); + } + + V_DrawFixedPatch(keyx, keyy, FRACUNIT, 0, key, lastkeycolormap); + + // Extra glowverlay if you can use a Chao Key + if (keysbeingused == 0 && M_CanKeyHiliTile()) + { + INT32 trans = (((challengesmenu.ticker/5) % 6) - 3); + if (trans) + { + trans = ((trans < 0) + ? (10 + trans) + : (10 - trans) + ) << V_ALPHASHIFT; + + V_DrawFixedPatch(keyx, keyy, FRACUNIT, trans, key, + R_GetTranslationColormap(TC_ALLWHITE, 0, GTC_MENUCACHE) + ); + } + } + } +} + +void M_DrawChallenges(void) +{ INT32 x = currentMenu->x, explodex, selectx = 0, selecty = 0; INT32 y; INT16 i, j; @@ -5914,6 +6108,7 @@ void M_DrawChallenges(void) } // Do underlay for everything else early so the bottom of the reticule doesn't get shaded over. + if (challengesmenu.currentunlock < MAXUNLOCKABLES) { y = 120; @@ -5932,7 +6127,7 @@ void M_DrawChallenges(void) y = currentMenu->y; - V_DrawFadeFill(0, y-2, BASEVIDWIDTH, 90, 0, 31, challengetransparentstrength); + V_DrawFadeFill(0, y-2, BASEVIDWIDTH, (challengesgridstep * CHALLENGEGRIDHEIGHT) + 2, 0, 31, challengetransparentstrength); x -= (challengesgridstep-1); @@ -6017,108 +6212,85 @@ challengedesc: { str = "???"; //M_CreateSecretMenuOption(str); } - } - else - { - str = "---"; - } - offset = V_LSTitleLowStringWidth(str, 0) / 2; - V_DrawLSTitleLowString(BASEVIDWIDTH/2 - offset, y+6, 0, str); + offset = V_LSTitleLowStringWidth(str, 0) / 2; + V_DrawLSTitleLowString(BASEVIDWIDTH/2 - offset, y+6, 0, str); + } } - // Tally + // Wings { - str = va("%d/%d", - challengesmenu.unlockcount[CC_UNLOCKED] + challengesmenu.unlockcount[CC_TALLY], - challengesmenu.unlockcount[CC_TOTAL] - ); - V_DrawRightAlignedTimerString(BASEVIDWIDTH-7, 9-challengesmenu.unlockcount[CC_ANIM], 0, str); + const INT32 endy = 18, endlen = 38; + patch_t *endwing = W_CachePatchName("K_BOSB01", PU_CACHE); + + V_DrawFill(0, endy, endlen, 11, 24); + V_DrawFixedPatch(endlen*FRACUNIT, endy*FRACUNIT, FRACUNIT, V_FLIP, endwing, NULL); + + V_DrawFill(BASEVIDWIDTH - endlen, endy, endlen, 11, 24); + V_DrawFixedPatch((BASEVIDWIDTH - endlen)*FRACUNIT, endy*FRACUNIT, FRACUNIT, 0, endwing, NULL); } - // Chao Keys + // Percentage { - patch_t *key = W_CachePatchName("UN_CHA00", PU_CACHE); - INT32 offs = challengesmenu.unlockcount[CC_CHAONOPE]; - if (offs & 1) - offs = -offs; - offs /= 2; - - if (gamedata->chaokeys > 9) - { - offs -= 6; - if (gamedata->chaokeys > 99) - offs -= 2; // as far as we can go - } - - fixed_t keyx = (8+offs)*FRACUNIT, keyy = 5*FRACUNIT; - - const char *timerstr = va("%u", gamedata->chaokeys); - - V_DrawTimerString((27+offs), 9-challengesmenu.unlockcount[CC_CHAOANIM], 0, timerstr); - - K_drawButton( - (27 + offs + V_TimerStringWidth(timerstr, 0) + 2) << FRACBITS, - 11 << FRACBITS, - 0, kp_button_c[1], - M_MenuExtraHeld(pid) + patch_t *medal = W_CachePatchName( + va("UN_MDL%c", '0' + challengesmenu.unlockcount[CMC_MEDALID]), + PU_CACHE ); - offs = challengekeybarwidth; - if (gamedata->chaokeys < GDMAX_CHAOKEYS) - offs = ((gamedata->pendingkeyroundoffset * challengekeybarwidth)/GDCONVERT_ROUNDSTOKEY); + fixed_t medalchopy = 1; - if (offs > 0) - V_DrawFill(1, 25, offs, 2, 0); - if (offs < challengekeybarwidth) - V_DrawFadeFill(1+offs, 25, challengekeybarwidth-offs, 2, 0, 31, challengetransparentstrength); - - if (challengesmenu.chaokeyhold) + for (i = CMC_MEDALBLANK; i <= CMC_MEDALFILLED; i++) { - fixed_t keyholdrotation = 0, radius = challengesgridstep; + if (challengesmenu.unlockcount[i] == 0) + continue; - if (challengesmenu.chaokeyhold < CHAOHOLD_BEGIN) + V_SetClipRect( + 0, + medalchopy << FRACBITS, + BASEVIDWIDTH << FRACBITS, + (medalchopy + challengesmenu.unlockcount[i]) << FRACBITS, + 0 + ); + + UINT8 *medalcolormap = NULL; + if (i == CMC_MEDALBLANK) { - radius = (challengesmenu.chaokeyhold*radius)*(FRACUNIT/CHAOHOLD_BEGIN); - keyx += challengesmenu.chaokeyhold*((selectx*FRACUNIT) - keyx)/CHAOHOLD_BEGIN; - keyy += challengesmenu.chaokeyhold*((selecty*FRACUNIT) - keyy)/CHAOHOLD_BEGIN; + medalcolormap = R_GetTranslationColormap(TC_BLINK, SKINCOLOR_BLACK, GTC_MENUCACHE); } - else + else if (challengesmenu.unlockcount[CMC_MEDALID] == 0) { - if (challengesmenu.chaokeyhold < CHAOHOLD_MAX - CHAOHOLD_END) - { - radius <<= FRACBITS; - - keyholdrotation = 360 * ((challengesmenu.chaokeyhold - CHAOHOLD_BEGIN)) - * (FRACUNIT/(CHAOHOLD_MAX - (CHAOHOLD_BEGIN + CHAOHOLD_END))); - - INT32 time = 3 - (keyholdrotation - 1) / (90 * FRACUNIT); - if (time <= 5 && time >= 0) - V_DrawScaledPatch(selectx + 2, selecty - 2, 0, kp_eggnum[time]); - } - else - { - radius = ((CHAOHOLD_MAX - challengesmenu.chaokeyhold)*radius)*(FRACUNIT/CHAOHOLD_END); - } - - keyx = selectx*FRACUNIT; - keyy = selecty*FRACUNIT; + medalcolormap = R_GetTranslationColormap(TC_DEFAULT, M_GetCvPlayerColor(0), GTC_MENUCACHE); } - if (radius) - { - angle_t ang = (FixedAngle( - keyholdrotation - ) >> ANGLETOFINESHIFT) & FINEMASK; + V_DrawFixedPatch((BASEVIDWIDTH - 31)*FRACUNIT, 1*FRACUNIT, FRACUNIT, 0, medal, medalcolormap); - keyx += FixedMul(radius, FINESINE(ang)); - keyy -= FixedMul(radius, FINECOSINE(ang)); - } + V_ClearClipRect(); + + medalchopy += challengesmenu.unlockcount[i]; } - V_DrawFixedPatch(keyx, keyy, FRACUNIT, 0, key, NULL); + INT32 textx = BASEVIDWIDTH - 21, texty = 20-challengesmenu.unlockcount[CMC_ANIM]; + UINT8 numbers[3]; + numbers[0] = ((challengesmenu.unlockcount[CMC_PERCENT] / 100) % 10); + numbers[1] = ((challengesmenu.unlockcount[CMC_PERCENT] / 10) % 10); + numbers[2] = (challengesmenu.unlockcount[CMC_PERCENT] % 10); + + patch_t *percent = W_CachePatchName("K_SPDML1", PU_CACHE); + + V_DrawScaledPatch(textx + 2, texty, 0, percent); + + i = 3; + while (i) + { + i--; + textx -= 6; + V_DrawScaledPatch(textx, texty, 0, kp_facenum[numbers[i]]); + } } + // Chao Key information + M_DrawChallengeKeys(selectx, selecty); + // Derived from M_DrawCharSelectPreview x = 40; y = BASEVIDHEIGHT-16; @@ -6143,7 +6315,6 @@ challengedesc: #undef challengetransparentstrength #undef challengesgridstep -#undef challengekeybarwidth // Statistics menu @@ -6226,6 +6397,12 @@ static void M_DrawMapMedals(INT32 mapnum, INT32 x, INT32 y) } x -= 8; } + + if (mapheaderinfo[mapnum]->records.mapvisited & MV_MYSTICMELODY) + { + V_DrawScaledPatch(x, y, 0, W_CachePatchName("GOTMEL", PU_CACHE)); + x -= 8; + } } static void M_DrawStatsMaps(void) @@ -6681,7 +6858,7 @@ void M_DrawStatistics(void) case statisticspage_maps: { - pagename = "LEVELS & MEDALS"; + pagename = "COURSES & MEDALS"; M_DrawStatsMaps(); break; } diff --git a/src/k_respawn.c b/src/k_respawn.c index 3a83616a6..21af8253d 100644 --- a/src/k_respawn.c +++ b/src/k_respawn.c @@ -111,6 +111,12 @@ void K_DoFault(player_t *player) player->pflags |= PF_FAULT; player->mo->renderflags |= RF_DONTDRAW; player->mo->flags |= MF_NOCLIPTHING; + + if (player->roundconditions.faulted == false) + { + player->roundconditions.faulted = true; + player->roundconditions.checkthisframe = true; + } } } diff --git a/src/k_tally.cpp b/src/k_tally.cpp index eba1452c8..15e11a2c7 100644 --- a/src/k_tally.cpp +++ b/src/k_tally.cpp @@ -800,6 +800,9 @@ void level_tally_t::Tick(void) transition = 0; transitionTime = TICRATE/7; delay = TICRATE/2; + + // for UCRP_FINISHGRADE + owner->roundconditions.checkthisframe = true; } else { diff --git a/src/m_cheat.c b/src/m_cheat.c index e44735eb9..e91e445fd 100644 --- a/src/m_cheat.c +++ b/src/m_cheat.c @@ -167,7 +167,7 @@ static UINT8 cheatf_devmode(void) static cheatseq_t cheat_warp = { NULL, cheatf_warp, - (UINT8[]){ SCRAMBLE('p'), SCRAMBLE('l'), SCRAMBLE('a'), SCRAMBLE('c'), SCRAMBLE('e'), SCRAMBLE('h'), SCRAMBLE('o'), SCRAMBLE('l'), SCRAMBLE('d'), SCRAMBLE('e'), SCRAMBLE('r'), 0xff } + (UINT8[]){ SCRAMBLE('c'), SCRAMBLE('h'), SCRAMBLE('a'), SCRAMBLE('o'), SCRAMBLE('s'), SCRAMBLE(' '), SCRAMBLE('z'), SCRAMBLE('e'), SCRAMBLE('r'), SCRAMBLE('o'), SCRAMBLE(' '), SCRAMBLE('6'), SCRAMBLE('4'), 0xff } }; static cheatseq_t cheat_wrongwarp = { @@ -263,7 +263,7 @@ boolean cht_Interpret(const char *password) cheatseqid = 0; while (cheatseqlist[cheatseqid]) { - ret += cht_CheckCheat(cheatseqlist[cheatseqid], *password, (password == endofpassword)); + ret += cht_CheckCheat(cheatseqlist[cheatseqid], tolower(*password), (password == endofpassword)); cheatseqid++; } diff --git a/src/m_cond.c b/src/m_cond.c index eecc73ae6..39383aafc 100644 --- a/src/m_cond.c +++ b/src/m_cond.c @@ -417,7 +417,16 @@ void M_UpdateChallengeGridExtraData(challengegridextradata_t *extradata) for (j = 0; j < CHALLENGEGRIDHEIGHT; j++) { id = (i * CHALLENGEGRIDHEIGHT) + j; - extradata[id].flags = CHE_NONE; + num = gamedata->challengegrid[id]; + if (num >= MAXUNLOCKABLES || unlockables[num].majorunlock == false || gamedata->unlocked[num] == true) + { + extradata[id].flags = CHE_NONE; + continue; + } + + // We only do this for locked large tiles, to reduce the + // complexity of most standard tile challenge comparisons + extradata[id].flags = CHE_ALLCLEAR; } } @@ -468,12 +477,22 @@ void M_UpdateChallengeGridExtraData(challengegridextradata_t *extradata) if (extradata[id].flags == CHE_HINT) { + // CHE_ALLCLEAR has already been removed, + // and CHE_HINT has already been applied, + // so nothing more needs to be done here. continue; } } - else if (work < MAXUNLOCKABLES && gamedata->unlocked[work]) + else if (work < MAXUNLOCKABLES) { - extradata[id].flags = CHE_HINT; + if (gamedata->unlocked[work] == true) + { + extradata[id].flags |= CHE_HINT; + } + else + { + extradata[id].flags &= ~CHE_ALLCLEAR; + } } } @@ -495,9 +514,13 @@ void M_UpdateChallengeGridExtraData(challengegridextradata_t *extradata) { //CONS_Printf(" %d - %d to left of %d is valid\n", work, tempid, id); // If we haven't already updated our id, it's the one to our left. - if (extradata[id].flags == CHE_HINT) + if (extradata[id].flags & CHE_HINT) { - extradata[tempid].flags = CHE_HINT; + extradata[tempid].flags |= CHE_HINT; + } + if (!(extradata[id].flags & CHE_ALLCLEAR)) + { + extradata[tempid].flags &= ~CHE_ALLCLEAR; } extradata[id].flags = CHE_CONNECTEDLEFT; id = tempid; @@ -505,16 +528,25 @@ void M_UpdateChallengeGridExtraData(challengegridextradata_t *extradata) /*else CONS_Printf(" %d - %d to left of %d is invalid\n", work, tempid, id);*/ } - else if (work < MAXUNLOCKABLES && gamedata->unlocked[work]) + else if (work < MAXUNLOCKABLES) { - extradata[id].flags = CHE_HINT; - continue; + if (gamedata->unlocked[work] == true) + { + extradata[id].flags |= CHE_HINT; + } + else + { + extradata[id].flags &= ~CHE_ALLCLEAR; + } } } // Since we're not modifying id past this point, the conditions become much simpler. - if ((extradata[id].flags & (CHE_HINT|CHE_DONTDRAW)) == CHE_HINT) + if (extradata[id].flags == CHE_HINT) { + // CHE_ALLCLEAR has already been removed, + // and CHE_HINT has already been applied, + // so nothing more needs to be done here. continue; } @@ -528,10 +560,16 @@ void M_UpdateChallengeGridExtraData(challengegridextradata_t *extradata) { ; } - else if (work < MAXUNLOCKABLES && gamedata->unlocked[work]) + else if (work < MAXUNLOCKABLES) { - extradata[id].flags = CHE_HINT; - continue; + if (gamedata->unlocked[work] == true) + { + extradata[id].flags |= CHE_HINT; + } + else + { + extradata[id].flags &= ~CHE_ALLCLEAR; + } } } @@ -551,10 +589,16 @@ void M_UpdateChallengeGridExtraData(challengegridextradata_t *extradata) { ; } - else if (work < MAXUNLOCKABLES && gamedata->unlocked[work]) + else if (work < MAXUNLOCKABLES) { - extradata[id].flags = CHE_HINT; - continue; + if (gamedata->unlocked[work] == true) + { + extradata[id].flags |= CHE_HINT; + } + else + { + extradata[id].flags &= ~CHE_ALLCLEAR; + } } } } @@ -605,6 +649,7 @@ void M_ClearStats(void) UINT8 i; gamedata->totalplaytime = 0; gamedata->totalrings = 0; + gamedata->totaltumbletime = 0; for (i = 0; i < GDGT_MAX; ++i) gamedata->roundsplayed[i] = 0; gamedata->timesBeaten = 0; @@ -614,6 +659,8 @@ void M_ClearStats(void) gamedata->eversavedreplay = false; gamedata->everseenspecial = false; gamedata->evercrashed = false; + gamedata->chaokeytutorial = false; + gamedata->majorkeyskipattempted = false; gamedata->musicstate = GDMUSIC_NONE; gamedata->importprofilewins = false; @@ -633,12 +680,34 @@ void M_ClearSecrets(void) gamedata->numspraycans = 0; gamedata->gotspraycans = 0; - UINT16 i; + Z_Free(gamedata->prisoneggpickups); + gamedata->prisoneggpickups = NULL; + gamedata->numprisoneggpickups = 0; + gamedata->gettableprisoneggpickups = 0; + gamedata->thisprisoneggpickup = MAXCONDITIONSETS; + gamedata->thisprisoneggpickup_cached = NULL; + gamedata->thisprisoneggpickupgrabbed = false; + + UINT16 i, j; for (i = 0; i < nummapheaders; i++) { if (!mapheaderinfo[i]) continue; + mapheaderinfo[i]->cache_spraycan = UINT16_MAX; + + mapheaderinfo[i]->cache_maplock = MAXUNLOCKABLES; + + for (j = 1; j < mapheaderinfo[i]->musname_size; j++) + { + mapheaderinfo[i]->cache_muslock[j-1] = MAXUNLOCKABLES; + } + } + + cupheader_t *cup; + for (cup = kartcupheaders; cup; cup = cup->next) + { + cup->cache_cuplock = MAXUNLOCKABLES; } for (i = 0; i < numskincolors; i++) @@ -653,7 +722,9 @@ void M_ClearSecrets(void) gamedata->pendingkeyrounds = 0; gamedata->pendingkeyroundoffset = 0; gamedata->keyspending = 0; - gamedata->chaokeys = 3; // Start with 3 !! + + gamedata->chaokeys = GDINIT_CHAOKEYS; + gamedata->prisoneggstothispickup = GDINIT_PRISONSTOPRIZE; } // For lack of a better idea on where to put this @@ -746,14 +817,15 @@ static void M_AssignSpraycans(void) M_Shuffle_UINT16(tempcanlist + prependoffset - (prependlen - 1), prependlen); // Put at the front of the main list - // (technically reverses the prepend order, but it + // (technically messes with the main order, but it // was LITERALLY just shuffled so it doesn't matter) - while (prependlen) + i = 0; + while (i < prependlen) { - prependlen--; - tempcanlist[listlen] = tempcanlist[prependlen]; - tempcanlist[prependlen] = tempcanlist[prependoffset - prependlen]; + tempcanlist[listlen] = tempcanlist[i]; + tempcanlist[i] = tempcanlist[prependoffset - i]; listlen++; + i++; } } @@ -774,13 +846,311 @@ static void M_AssignSpraycans(void) } } +static void M_InitPrisonEggPickups(void) +{ + // Init ordered list of skincolors + UINT16 temppickups[MAXCONDITIONSETS]; + UINT16 listlen = 0; + + UINT32 i, j; + conditionset_t *c; + condition_t *cn; + + for (i = 0; i < MAXCONDITIONSETS; ++i) + { + // Optimisation - unlike Spray Cans, these are rebuilt every game launch/savedata wipe. + // Therefore, we don't need to re-store the ones that have been achieved. + if (gamedata->achieved[i]) + continue; + + c = &conditionSets[i]; + if (!c->numconditions) + continue; + + for (j = 0; j < c->numconditions; ++j) + { + cn = &c->condition[j]; + if (cn->type != UC_PRISONEGGCD) + continue; + + temppickups[listlen] = i; + listlen++; + break; + } + } + + if (!listlen) + { + return; + } + + // This list doesn't need to be shuffled because it's always being randomly grabbed. + // (Unlike Spray Cans, you don't know which CD you miss out on.) + + gamedata->prisoneggpickups = Z_Realloc( + gamedata->prisoneggpickups, + sizeof(UINT16) * listlen, + PU_STATIC, + NULL); + + while (gamedata->numprisoneggpickups < listlen) + { + gamedata->prisoneggpickups[gamedata->numprisoneggpickups] + = temppickups[gamedata->numprisoneggpickups]; + gamedata->numprisoneggpickups++; + } + + M_UpdateNextPrisonEggPickup(); +} + +void M_UpdateNextPrisonEggPickup(void) +{ + UINT16 i = gamedata->gettableprisoneggpickups, j, swap; + + conditionset_t *c; + condition_t *cn; + + boolean firstrun = true; + +cacheprisoneggpickup: + + // Check if the current roll is fine + gamedata->thisprisoneggpickup_cached = NULL; + if (gamedata->thisprisoneggpickup < MAXCONDITIONSETS) + { + //CONS_Printf("CACHE TEST: thisprisoneggpickup is set to %u\n", gamedata->thisprisoneggpickup); + if (gamedata->achieved[gamedata->thisprisoneggpickup] == false) + { + c = &conditionSets[gamedata->thisprisoneggpickup]; + if (c->numconditions) + { + for (j = 0; j < c->numconditions; ++j) + { + cn = &c->condition[j]; + if (cn->type != UC_PRISONEGGCD) + continue; + + if (cn->requirement < nummapheaders && M_MapLocked(cn->requirement+1)) + continue; + + // Good! Attach the cache. + gamedata->thisprisoneggpickup_cached = cn; + //CONS_Printf(" successfully set to cn!\n"); + break; + } + } + } + + if (gamedata->thisprisoneggpickup_cached == NULL) + { + gamedata->thisprisoneggpickup = MAXCONDITIONSETS; + gamedata->thisprisoneggpickupgrabbed = false; + } + } + + if (firstrun && gamedata->numprisoneggpickups && gamedata->thisprisoneggpickup == MAXCONDITIONSETS) + { + for (; i < gamedata->numprisoneggpickups; i++) + { + if (gamedata->achieved[gamedata->prisoneggpickups[i]] == false) + { + c = &conditionSets[gamedata->prisoneggpickups[i]]; + if (c->numconditions) + { + for (j = 0; j < c->numconditions; ++j) + { + cn = &c->condition[j]; + if (cn->type != UC_PRISONEGGCD) + continue; + + // Locked associated map? Keep in the rear end dimension! + if (cn->requirement < nummapheaders && M_MapLocked(cn->requirement+1)) + break; // not continue intentionally + + // Okay, this should be available. + // Bring to the front! + swap = gamedata->prisoneggpickups[gamedata->gettableprisoneggpickups]; + gamedata->prisoneggpickups[gamedata->gettableprisoneggpickups] = + gamedata->prisoneggpickups[i]; + gamedata->prisoneggpickups[i] = swap; + + gamedata->gettableprisoneggpickups++; + + break; + } + + if (j < c->numconditions) + continue; + } + } + + // Fell all the way through? + // Push this all the way to the back, and lop it off! + + swap = gamedata->prisoneggpickups[gamedata->numprisoneggpickups]; + gamedata->prisoneggpickups[gamedata->numprisoneggpickups] = + gamedata->prisoneggpickups[i]; + gamedata->prisoneggpickups[i] = swap; + + gamedata->numprisoneggpickups--; + i--; // We run the loop again for this entry + } + + if (gamedata->gettableprisoneggpickups) + { + gamedata->thisprisoneggpickup = + gamedata->prisoneggpickups[ + M_RandomKey(gamedata->gettableprisoneggpickups) + ]; + + firstrun = false; + goto cacheprisoneggpickup; + } + } + + //CONS_Printf("thisprisoneggpickup = %u (MAXCONDITIONSETS is %u)\n", gamedata->thisprisoneggpickup, MAXCONDITIONSETS); +} + +static void M_PrecacheLevelLocks(void) +{ + UINT16 i, j; + + for (i = 0; i < MAXUNLOCKABLES; ++i) + { + switch (unlockables[i].type) + { + // SECRET_SKIN, SECRET_COLOR, SECRET_FOLLOWER are instantiated too late to use + case SECRET_MAP: + { + UINT16 map = M_UnlockableMapNum(&unlockables[i]); + if (map < nummapheaders + && mapheaderinfo[map]) + { + if (mapheaderinfo[map]->cache_maplock != MAXUNLOCKABLES) + CONS_Alert(CONS_ERROR, "Unlockable %u: Too many SECRET_MAPs associated with Level %s\n", i, mapheaderinfo[map]->lumpname); + mapheaderinfo[map]->cache_maplock = i; + } + break; + } + + case SECRET_ALTMUSIC: + { + UINT16 map = M_UnlockableMapNum(&unlockables[i]); + const char *tempstr = NULL; + + if (map < nummapheaders + && mapheaderinfo[map]) + { + for (j = 1; j < mapheaderinfo[map]->musname_size; j++) + { + if (mapheaderinfo[map]->cache_muslock[j - 1] != MAXUNLOCKABLES) + { + continue; + } + + mapheaderinfo[map]->cache_muslock[j - 1] = i; + + UINT8 positionid = 0; + + if (mapheaderinfo[map]->cup) + { + for (positionid = 0; positionid < CUPCACHE_PODIUM; positionid++) + { + if (mapheaderinfo[map]->cup->cachedlevels[positionid] != map) + continue; + break; + } + + if (positionid < CUPCACHE_PODIUM) + { + char prefix = 'R'; + if (positionid >= CUPCACHE_BONUS) + { + positionid -= (CUPCACHE_BONUS); + prefix = 'B'; + } + + tempstr = va( + "Music: %s Cup %c%u %c", + mapheaderinfo[map]->cup->realname, + prefix, + positionid + 1, + 'A' + j // :D ? + ); + } + } + + if (tempstr == NULL) + { + UINT16 mapcheck; + for (mapcheck = 0; mapcheck < map; mapcheck++) + { + if (!mapheaderinfo[mapcheck] || mapheaderinfo[mapcheck]->cup != NULL) + continue; + if (mapheaderinfo[mapcheck]->menuflags & (LF2_HIDEINSTATS|LF2_HIDEINMENU)) + continue; + if (((mapheaderinfo[mapcheck]->typeoflevel & TOL_TUTORIAL) == TOL_TUTORIAL) + != ((mapheaderinfo[map]->typeoflevel & TOL_TUTORIAL) == TOL_TUTORIAL)) + continue; + + // We don't check for locked, because the levels exist + positionid++; + } + + tempstr = va( + "Music: %s #%u %c", + (mapheaderinfo[map]->typeoflevel & TOL_TUTORIAL) ? "Tutorial" : "Lost and Found", + positionid + 1, + 'A' + j // :D ? + ); + } + + break; + } + if (j == mapheaderinfo[map]->musname_size) + CONS_Alert(CONS_ERROR, "Unlockable %u: Too many SECRET_ALTMUSICs associated with Level %s\n", i, mapheaderinfo[map]->lumpname); + } + + if (tempstr == NULL) + tempstr = va("INVALID MUSIC UNLOCK %u", i); + + strlcpy(unlockables[i].name, tempstr, sizeof (unlockables[i].name)); + + break; + } + + case SECRET_CUP: + { + cupheader_t *cup = M_UnlockableCup(&unlockables[i]); + if (cup) + { + if (cup->cache_cuplock != MAXUNLOCKABLES) + CONS_Alert(CONS_ERROR, "Unlockable %u: Too many SECRET_CUPs associated with Cup %s\n", i, cup->name); + cup->cache_cuplock = i; + break; + } + break; + } + + default: + break; + } + } +} + void M_FinaliseGameData(void) { //M_PopulateChallengeGrid(); -- This can be done lazily when we actually need it + // Precache as many unlockables as is meaningfully feasible + M_PrecacheLevelLocks(); + // Place the spraycans, which CAN'T be done lazily. M_AssignSpraycans(); + // You could probably do the Prison Egg Pickups lazily, but it'd be a lagspike mid-combat. + M_InitPrisonEggPickups(); + // Don't consider loaded until it's a success! // It used to do this much earlier, but this would cause the gamedata // to save over itself when it I_Errors from corruption, which can @@ -798,7 +1168,7 @@ void M_FinaliseGameData(void) void M_UpdateConditionSetsPending(void) { - UINT32 i, j; + UINT32 i, j, k; conditionset_t *c; condition_t *cn; @@ -818,6 +1188,7 @@ void M_UpdateConditionSetsPending(void) { case UC_CHARACTERWINS: case UCRP_ISCHARACTER: + case UCRP_MAKERETIRE: { cn->requirement = R_SkinAvailable(cn->stringvar); @@ -833,6 +1204,29 @@ void M_UpdateConditionSetsPending(void) break; } + case UCRP_HASFOLLOWER: + { + // match deh_soc readfollower() + for (k = 0; cn->stringvar[k]; k++) + { + if (cn->stringvar[k] == '_') + cn->stringvar[k] = ' '; + } + + cn->requirement = K_FollowerAvailable(cn->stringvar); + + if (cn->requirement < 0) + { + CONS_Alert(CONS_WARNING, "UC TYPE %u: Invalid character %s for condition ID %d", cn->type, cn->stringvar, cn->id+1); + continue; + } + + Z_Free(cn->stringvar); + cn->stringvar = NULL; + + break; + } + case UCRP_WETPLAYER: { if (cn->extrainfo1) @@ -935,6 +1329,8 @@ boolean M_CheckCondition(condition_t *cn, player_t *player) { switch (cn->type) { + case UC_NONE: + return false; case UC_PLAYTIME: // Requires total playing time >= x return (gamedata->totalplaytime >= (unsigned)cn->requirement); case UC_ROUNDSPLAYED: // Requires any level completed >= x times @@ -955,6 +1351,8 @@ boolean M_CheckCondition(condition_t *cn, player_t *player) } case UC_TOTALRINGS: // Requires grabbing >= x rings return (gamedata->totalrings >= (unsigned)cn->requirement); + case UC_TOTALTUMBLETIME: // Requires total tumbling time >= x + return (gamedata->totaltumbletime >= (unsigned)cn->requirement); case UC_GAMECLEAR: // Requires game beaten >= x times return (gamedata->timesBeaten >= (unsigned)cn->requirement); case UC_OVERALLTIME: // Requires overall time <= x @@ -963,6 +1361,7 @@ boolean M_CheckCondition(condition_t *cn, player_t *player) case UC_MAPBEATEN: // Requires map x to be beaten case UC_MAPENCORE: // Requires map x to be beaten in encore case UC_MAPSPBATTACK: // Requires map x to be beaten in SPB Attack + case UC_MAPMYSTICMELODY: // Mystic Melody on map x's Ancient Shrine { UINT8 mvtype = MV_VISITED; if (cn->type == UC_MAPBEATEN) @@ -971,6 +1370,8 @@ boolean M_CheckCondition(condition_t *cn, player_t *player) mvtype = MV_ENCORE; else if (cn->type == UC_MAPSPBATTACK) mvtype = MV_SPBATTACK; + else if (cn->type == UC_MAPMYSTICMELODY) + mvtype = MV_MYSTICMELODY; return ((cn->requirement < nummapheaders) && (mapheaderinfo[cn->requirement]) @@ -1012,6 +1413,77 @@ boolean M_CheckCondition(condition_t *cn, player_t *player) case UC_CONDITIONSET: // requires condition set x to already be achieved return M_Achieved(cn->requirement-1); + case UC_UNLOCKPERCENT: + { + UINT16 i, unlocked = cn->extrainfo2, total = 0; + + // Special case for maps + if (cn->extrainfo1 == SECRET_MAP) + { + for (i = 0; i < basenummapheaders; i++) + { + if (!mapheaderinfo[i] || mapheaderinfo[i]->menuflags & (LF2_HIDEINSTATS|LF2_HIDEINMENU)) + continue; + + total++; + + // Check for completion + if ((mapheaderinfo[i]->menuflags & LF2_FINISHNEEDED) + && !(mapheaderinfo[i]->records.mapvisited & MV_BEATEN)) + continue; + + // Check for unlock + if (M_MapLocked(i+1)) + continue; + + unlocked++; + } + } + // Special case for SECRET_COLOR + else if (cn->extrainfo1 == SECRET_COLOR) + { + total = gamedata->numspraycans; + unlocked = gamedata->gotspraycans; + } + // Special case for raw Challenge count + else if (cn->extrainfo1 == SECRET_NONE) + { + for (i = 0; i < MAXUNLOCKABLES; i++) + { + if (unlockables[i].type == SECRET_NONE) + continue; + + total++; + + if (M_Achieved(unlockables[i].conditionset - 1) == false) + continue; + + unlocked++; + } + } + else + { + for (i = 0; i < MAXUNLOCKABLES; i++) + { + if (unlockables[i].type != cn->extrainfo1) + continue; + + total++; + + if (gamedata->unlocked[i] == false) + continue; + + unlocked++; + } + } + + if (!total) + return false; + + // No need to do a pesky divide + return ((100 * unlocked) >= (total * cn->requirement)); + } + case UC_ADDON: return ((gamedata->everloadedaddon == true) && M_SecretUnlocked(SECRET_ADDONS, true)); @@ -1029,6 +1501,7 @@ boolean M_CheckCondition(condition_t *cn, player_t *player) return false; case UC_PASSWORD: return (cn->stringvar == NULL); + case UC_SPRAYCAN: { if (cn->requirement <= 0 @@ -1043,9 +1516,13 @@ boolean M_CheckCondition(condition_t *cn, player_t *player) return (gamedata->spraycans[can_id].map < nummapheaders); } + case UC_PRISONEGGCD: + return ((gamedata->thisprisoneggpickupgrabbed == true) && (cn == gamedata->thisprisoneggpickup_cached)); + // Just for string building case UC_AND: case UC_COMMA: + case UC_DESCRIPTIONOVERRIDE: return true; case UCRP_PREFIX_GRANDPRIX: @@ -1071,6 +1548,8 @@ boolean M_CheckCondition(condition_t *cn, player_t *player) skins[player->skin].kartweight, skins[player->skin].flags ) == (unsigned)cn->requirement); + case UCRP_HASFOLLOWER: + return (cn->requirement != -1 && player->followerskin == cn->requirement); case UCRP_ISDIFFICULTY: if (grandprixinfo.gp == false) return (gamespeed >= cn->requirement); @@ -1079,12 +1558,16 @@ boolean M_CheckCondition(condition_t *cn, player_t *player) return (grandprixinfo.gamespeed >= cn->requirement); case UCRP_PODIUMCUP: - if (K_PodiumRanking() == false) + if (grandprixinfo.gp == false || K_PodiumRanking() == false) return false; if (grandprixinfo.cup == NULL - || grandprixinfo.cup->id != cn->requirement) + || ( + cn->requirement != -1 // Any + && grandprixinfo.cup->id != cn->requirement + ) + ) return false; - if (cn->extrainfo2) + if (cn->extrainfo2 != 0) return (K_PodiumGrade() >= (unsigned)cn->requirement); if (cn->extrainfo1 != 0) return (player->position != 0 @@ -1092,14 +1575,27 @@ boolean M_CheckCondition(condition_t *cn, player_t *player) return true; case UCRP_PODIUMEMERALD: case UCRP_PODIUMPRIZE: - return (K_PodiumRanking() == true + return (grandprixinfo.gp == true + && K_PodiumRanking() == true && grandprixinfo.rank.specialWon == true); + case UCRP_PODIUMNOCONTINUES: + return (grandprixinfo.gp == true + && K_PodiumRanking() == true + && grandprixinfo.rank.continuesUsed == 0); case UCRP_FINISHCOOL: return (player->exiting && !(player->pflags & PF_NOCONTEST) && M_NotFreePlay() && !K_IsPlayerLosing(player)); + case UCRP_FINISHPERFECT: + return (player->exiting + && !(player->pflags & PF_NOCONTEST) + && M_NotFreePlay() + && (gamespeed != KARTSPEED_EASY) + && (player->tally.active == true) + && (player->tally.totalLaps > 0) // Only true if not Time Attack + && (player->tally.laps >= player->tally.totalLaps)); case UCRP_FINISHALLPRISONS: return (battleprisons && !(player->pflags & PF_NOCONTEST) @@ -1107,6 +1603,77 @@ boolean M_CheckCondition(condition_t *cn, player_t *player) && numtargets >= maptargets); case UCRP_NOCONTEST: return (player->pflags & PF_NOCONTEST); + + case UCRP_SMASHUFO: + return ( + specialstageinfo.valid == true + && ( + P_MobjWasRemoved(specialstageinfo.ufo) + || specialstageinfo.ufo->health <= 1 + ) + ); + case UCRP_CHASEDBYSPB: + // The PERFECT implementation would check spbplace, iterate over trackercap, etc. + // But the game already has this handy-dandy SPB signal for us... + // It's only MAYBE invalid in modded context. And mods can already cheat... + return ((player->pflags & PF_RINGLOCK) == PF_RINGLOCK); + case UCRP_MAPDESTROYOBJECTS: + return ( + gamemap == cn->requirement+1 + && numchallengedestructibles == UINT16_MAX + ); + + case UCRP_MAKERETIRE: + { + // You can't "make" someone retire in coop. + if (K_Cooperative() == true) + { + return false; + } + + // The following is basically UCRP_FINISHCOOL, + // but without the M_NotFreePlay check since this + // condition is already dependent on other players. + if ((player->exiting + && !(player->pflags & PF_NOCONTEST) + && !K_IsPlayerLosing(player)) == false) + { + return false; + } + + UINT8 i; + for (i = 0; i < MAXPLAYERS; i++) + { + if (playeringame[i] == false) + { + continue; + } + + // This player is ME! + if (player == players+i) + { + continue; + } + + // This player didn't NO CONTEST. + if (!(players[i].pflags & PF_NOCONTEST)) + { + continue; + } + + // This player doesn't have the right skin. + if (players[i].skin != cn->requirement) + { + continue; + } + + // Okay, the right player is dead! + break; + } + + return (i != MAXPLAYERS); + } + case UCRP_FINISHPLACE: return (player->exiting && !(player->pflags & PF_NOCONTEST) @@ -1118,6 +1685,14 @@ boolean M_CheckCondition(condition_t *cn, player_t *player) && !(player->pflags & PF_NOCONTEST) && M_NotFreePlay() && player->position == cn->requirement); + case UCRP_FINISHGRADE: + return (player->exiting + && !(player->pflags & PF_NOCONTEST) + && M_NotFreePlay() + && (player->tally.active == true) + && (player->tally.state >= TALLY_ST_GRADE_APPEAR) + && (player->tally.state < TALLY_ST_DONE) + && (player->tally.rank >= cn->requirement)); case UCRP_FINISHTIME: return (player->exiting && !(player->pflags & PF_NOCONTEST) @@ -1136,26 +1711,135 @@ boolean M_CheckCondition(condition_t *cn, player_t *player) && player->realtime < timelimitintics && (timelimitintics + extratimeintics + secretextratime - player->realtime) >= (unsigned)cn->requirement); + case UCRP_RINGS: + return (player->hudrings >= cn->requirement); + case UCRP_RINGSEXACT: + return (player->hudrings == cn->requirement); + + case UCRP_SPEEDOMETER: + return (player->roundconditions.maxspeed >= cn->requirement); + case UCRP_DRAFTDURATION: + return (player->roundconditions.continuousdraft_best >= ((tic_t)cn->requirement)*TICRATE); + case UCRP_GROWCONSECUTIVEBEAMS: + return (player->roundconditions.best_consecutive_grow_lasers >= cn->requirement); + case UCRP_TRIGGER: // requires map trigger set return !!(player->roundconditions.unlocktriggers & (1 << cn->requirement)); case UCRP_FALLOFF: - return (player->roundconditions.fell_off == (cn->requirement == 1)); + return ((cn->requirement == 1 || player->exiting || (player->pflags & PF_NOCONTEST)) + && player->roundconditions.fell_off == (cn->requirement == 1)); case UCRP_TOUCHOFFROAD: - return (player->roundconditions.touched_offroad == (cn->requirement == 1)); + return ((cn->requirement == 1 || player->exiting || (player->pflags & PF_NOCONTEST)) + && player->roundconditions.touched_offroad == (cn->requirement == 1)); case UCRP_TOUCHSNEAKERPANEL: - return (player->roundconditions.touched_sneakerpanel == (cn->requirement == 1)); + return ((cn->requirement == 1 || player->exiting || (player->pflags & PF_NOCONTEST)) + && player->roundconditions.touched_sneakerpanel == (cn->requirement == 1)); case UCRP_RINGDEBT: - return (!(gametyperules & GTR_SPHERES) && (player->roundconditions.debt_rings == (cn->requirement == 1))); + return (!(gametyperules & GTR_SPHERES) + && (cn->requirement == 1 || player->exiting || (player->pflags & PF_NOCONTEST)) + && (player->roundconditions.debt_rings == (cn->requirement == 1))); + case UCRP_FAULTED: + return ((cn->requirement == 1 || player->latestlap >= 1) + && (player->roundconditions.faulted == (cn->requirement == 1))); case UCRP_TRIPWIREHYUU: return (player->roundconditions.tripwire_hyuu); + case UCRP_WHIPHYUU: + return (player->roundconditions.whip_hyuu); case UCRP_SPBNEUTER: return (player->roundconditions.spb_neuter); case UCRP_LANDMINEDUNK: return (player->roundconditions.landmine_dunk); case UCRP_HITMIDAIR: return (player->roundconditions.hit_midair); + case UCRP_HITDRAFTERLOOKBACK: + return (player->roundconditions.hit_drafter_lookback); + case UCRP_GIANTRACERSHRUNKENORBI: + return (player->roundconditions.giant_foe_shrunken_orbi); + case UCRP_RETURNMARKTOSENDER: + return (player->roundconditions.returntosender_mark); + + case UCRP_TRACKHAZARD: + { + INT16 requiredlap = cn->extrainfo1; + + if (requiredlap < 0) + { + // Prevents lowered numlaps from activating it + // (this also handles exiting, for all-laps situations) + requiredlap = max(mapheaderinfo[gamemap-1]->numlaps, numlaps); + } + + // cn->requirement is used as an offset here + // so if you need to get hit on lap x, the + // condition can fire while that lap is active + // but if you need to NOT get hit on lap X, + // it only fires once the lap is complete + if (player->latestlap <= (requiredlap - cn->requirement)) + return false; + + UINT8 requiredbit = 1<<(requiredlap & 7); + requiredlap /= 8; + + if (cn->extrainfo1 == -1) + { + if (cn->requirement == 0) + { + // The "don't get hit on any lap" check is trivial. + for (; requiredlap > 0; requiredlap--) + { + if (player->roundconditions.hittrackhazard[requiredlap] != 0) + return false; + } + + return (player->roundconditions.hittrackhazard[0] == 0); + } + + // The following is my attempt at a major optimisation. + // The naive version was MAX_LAP bools, which is ridiculous. + + // Check the highest relevant byte for all necessary bits. + // We only do this if an == 0xFF/0xFE check wouldn't satisfy. + if (requiredbit != (1<<7)) + { + // Last bit MAYBE not needed, POSITION doesn't count. + const UINT8 finalbit = (requiredlap == 0) ? 1 : 0; + while (requiredbit != finalbit) + { + if (!(player->roundconditions.hittrackhazard[requiredlap] & requiredbit)) + return false; + requiredbit /= 2; + } + + if (requiredlap == 0) + return true; + + requiredlap--; + } + + // All bytes between the top and the bottom need to be checked for saturation. + for (; requiredlap > 0; requiredlap--) + { + if (player->roundconditions.hittrackhazard[requiredlap] != 0xFF) + return false; + } + + // Last bit not needed, POSITION doesn't count. + return (player->roundconditions.hittrackhazard[0] == 0xFE); + } + + return (((player->roundconditions.hittrackhazard[requiredlap] & requiredbit) == requiredbit) == (cn->requirement == 1)); + } + + case UCRP_TARGETATTACKMETHOD: + return (player->roundconditions.targetdamaging == (targetdamaging_t)cn->requirement); + + case UCRP_GACHABOMMISER: + return ( + player->roundconditions.targetdamaging == UFOD_GACHABOM + && player->roundconditions.gachabom_miser != 0xFF + ); case UCRP_WETPLAYER: return (((player->roundconditions.wet_player & cn->requirement) == 0) @@ -1185,7 +1869,7 @@ static boolean M_CheckConditionSet(conditionset_t *c, player_t *player) continue; // Skip entries that are JUST for string building - if (cn->type == UC_AND || cn->type == UC_COMMA) + if (cn->type == UC_AND || cn->type == UC_COMMA || cn->type == UC_DESCRIPTIONOVERRIDE) continue; lastID = cn->id; @@ -1214,7 +1898,19 @@ static char *M_BuildConditionTitle(UINT16 map) || M_MapLocked(map+1)) return Z_StrDup("???"); - title = ref = G_BuildMapTitle(map+1); + if (mapheaderinfo[map]->menuttl[0]) + { + if (mapheaderinfo[map]->typeoflevel & TOL_TUTORIAL) + { + // Intentionally not forced uppercase + return Z_StrDup(va("the %s Tutorial", mapheaderinfo[map]->menuttl)); + } + title = ref = Z_StrDup(mapheaderinfo[map]->menuttl); + } + else + { + title = ref = G_BuildMapTitle(map+1); + } if (!title) I_Error("M_BuildConditionTitle: out of memory"); @@ -1228,6 +1924,54 @@ static char *M_BuildConditionTitle(UINT16 map) return title; } +static const char *M_GetConditionCharacter(INT32 skin, boolean directlyrequires) +{ + // First we check for direct unlock. + boolean permitname = R_SkinUsable(-1, skin, false); + + if (permitname == false && directlyrequires == false) + { + // If there's no direct unlock, we CAN check for if the + // character is the Rival of somebody we DO have unlocked... + + UINT8 i, j; + for (i = 0; i < numskins; i++) + { + if (i == skin) + continue; + + if (R_SkinUsable(-1, i, false) == false) + continue; + + for (j = 0; j < SKINRIVALS; j++) + { + const char *rivalname = skins[i].rivals[j]; + INT32 rivalnum = R_SkinAvailable(rivalname); + + if (rivalnum != skin) + continue; + + // We can see this character as a Rival! + break; + } + + if (j == SKINRIVALS) + continue; + + // "break" our way up the nesting... + break; + } + + // We stopped before the end, we can see it! + if (i != numskins) + permitname = true; + } + + return (permitname) + ? skins[skin].realname + : "???"; +} + static const char *M_GetNthType(UINT8 position) { if (position == 1) @@ -1248,19 +1992,15 @@ static const char *M_GetConditionString(condition_t *cn) // If this function returns NULL, it stops building the condition and just does ???'s. -#define BUILDCONDITIONTITLE(i) (M_BuildConditionTitle(i)) - switch (cn->type) { case UC_PLAYTIME: // Requires total playing time >= x - return va("play for %i:%02i:%02i", G_TicsToHours(cn->requirement), G_TicsToMinutes(cn->requirement, false), G_TicsToSeconds(cn->requirement)); case UC_ROUNDSPLAYED: // Requires any level completed >= x times - if (cn->extrainfo1 == GDGT_MAX) work = ""; else if (cn->extrainfo1 != GDGT_RACE && cn->extrainfo1 != GDGT_BATTLE // Base gametypes @@ -1298,6 +2038,12 @@ static const char *M_GetConditionString(condition_t *cn) return va("collect %u,%03u Rings", (cn->requirement/1000), (cn->requirement%1000)); return va("collect %u Rings", cn->requirement); + case UC_TOTALTUMBLETIME: + return va("tumble through the air for %i:%02i.%02i", + G_TicsToMinutes(cn->requirement, true), + G_TicsToSeconds(cn->requirement), + G_TicsToCentiseconds(cn->requirement)); + case UC_GAMECLEAR: // Requires game beaten >= x times if (cn->requirement > 1) return va("beat game %d times", cn->requirement); @@ -1314,13 +2060,14 @@ static const char *M_GetConditionString(condition_t *cn) case UC_MAPBEATEN: // Requires map x to be beaten case UC_MAPENCORE: // Requires map x to be beaten in encore case UC_MAPSPBATTACK: // Requires map x to be beaten in SPB Attack + case UC_MAPMYSTICMELODY: // Mystic Melody on map x's Ancient Shrine { const char *prefix = ""; if (cn->requirement >= nummapheaders || !mapheaderinfo[cn->requirement]) return va("INVALID MAP CONDITION \"%d:%d\"", cn->type, cn->requirement); - title = BUILDCONDITIONTITLE(cn->requirement); + title = M_BuildConditionTitle(cn->requirement); if (cn->type == UC_MAPSPBATTACK) prefix = (M_SecretUnlocked(SECRET_SPBATTACK, true) ? "SPB ATTACK: " : "???: "); @@ -1332,12 +2079,13 @@ static const char *M_GetConditionString(condition_t *cn) work = "visit"; else if (cn->type == UC_MAPSPBATTACK) work = "conquer"; + else if (cn->type == UC_MAPMYSTICMELODY) + work = "play a melody for the ancient shrine in"; - work = va("%s%s %s%s", + work = va("%s%s %s", prefix, work, - title, - (cn->type == UC_MAPENCORE) ? " in Encore Mode" : ""); + title); Z_Free(title); return work; } @@ -1347,7 +2095,7 @@ static const char *M_GetConditionString(condition_t *cn) if (cn->extrainfo1 >= nummapheaders || !mapheaderinfo[cn->extrainfo1]) return va("INVALID MAP CONDITION \"%d:%d:%d\"", cn->type, cn->extrainfo1, cn->requirement); - title = BUILDCONDITIONTITLE(cn->extrainfo1); + title = M_BuildConditionTitle(cn->extrainfo1); work = va("beat %s in %i:%02i.%02i", title, G_TicsToMinutes(cn->requirement, true), G_TicsToSeconds(cn->requirement), @@ -1361,9 +2109,7 @@ static const char *M_GetConditionString(condition_t *cn) { if (cn->requirement < 0 || !skins[cn->requirement].realname[0]) return va("INVALID CHAR CONDITION \"%d:%d:%d\"", cn->type, cn->requirement, cn->extrainfo1); - work = (R_SkinUsable(-1, cn->requirement, false)) - ? skins[cn->requirement].realname - : "???"; + work = M_GetConditionCharacter(cn->requirement, true); return va("win %d Round%s as %s", cn->extrainfo1, cn->extrainfo1 == 1 ? "" : "s", @@ -1419,7 +2165,7 @@ static const char *M_GetConditionString(condition_t *cn) if (checkLevel >= nummapheaders || !mapheaderinfo[checkLevel]) return va("INVALID MEDAL MAP \"%d:%d\"", cn->requirement, checkLevel); - title = BUILDCONDITIONTITLE(checkLevel); + title = M_BuildConditionTitle(checkLevel); switch (emblemlocations[i].type) { case ET_MAP: @@ -1493,6 +2239,62 @@ static const char *M_GetConditionString(condition_t *cn) ? unlockables[cn->requirement-1].name : "???"); + case UC_UNLOCKPERCENT: + { + boolean checkavailable = false; + + switch (cn->extrainfo1) + { + case SECRET_NONE: + work = "completion"; + break; + case SECRET_EXTRAMEDAL: + work = "of Challenge Medals"; + break; + case SECRET_CUP: + work = "of Cups"; + break; + case SECRET_MAP: + work = "of Courses"; + break; + case SECRET_ALTMUSIC: + work = "of alternate music"; + checkavailable = true; + break; + case SECRET_SKIN: + work = "of Characters"; + checkavailable = true; + break; + case SECRET_FOLLOWER: + work = "of Followers"; + checkavailable = true; + break; + case SECRET_COLOR: + work = (gamedata->gotspraycans == 0) ? "of ???" : "of Spray Cans"; + //checkavailable = true; + break; + default: + return va("INVALID CHALLENGE FOR PERCENT \"%d\"", cn->requirement); + } + + if (checkavailable == true) + { + for (i = 0; i < MAXUNLOCKABLES; ++i) + { + if (unlockables[i].type != cn->extrainfo1) + continue; + if (gamedata->unlocked[i] == false) + continue; + break; + } + + if (i == MAXUNLOCKABLES) + work = "of ???"; + } + + return va("CHALLENGES: get %u%% %s", cn->requirement, work); + } + case UC_ADDON: if (!M_SecretUnlocked(SECRET_ADDONS, true)) return NULL; @@ -1507,6 +2309,7 @@ static const char *M_GetConditionString(condition_t *cn) return NULL; case UC_PASSWORD: return "enter a secret password"; + case UC_SPRAYCAN: { if (cn->requirement <= 0 @@ -1530,10 +2333,16 @@ static const char *M_GetConditionString(condition_t *cn) return va("grab %d Spray Cans", can_id + 1); } + case UC_PRISONEGGCD: + // :butterfly: "alternatively you could say 'grab a hot toooon' or 'smooth beeat'" + return "BONUS ROUND: grab a prize from a Prison Egg"; + case UC_AND: return "&"; case UC_COMMA: return ","; + case UC_DESCRIPTIONOVERRIDE: + return cn->stringvar; case UCRP_PREFIX_GRANDPRIX: return "GRAND PRIX:"; @@ -1554,7 +2363,7 @@ static const char *M_GetConditionString(condition_t *cn) if (cn->requirement >= nummapheaders || !mapheaderinfo[cn->requirement]) return va("INVALID MAP CONDITION \"%d:%d\":", cn->type, cn->requirement); - title = BUILDCONDITIONTITLE(cn->requirement); + title = M_BuildConditionTitle(cn->requirement); work = va("%s:", title); Z_Free(title); return work; @@ -1562,19 +2371,24 @@ static const char *M_GetConditionString(condition_t *cn) if (cn->requirement >= nummapheaders || !mapheaderinfo[cn->requirement]) return va("INVALID MAP CONDITION \"%d:%d\"", cn->type, cn->requirement); - title = BUILDCONDITIONTITLE(cn->requirement); + title = M_BuildConditionTitle(cn->requirement); work = va("on %s", title); Z_Free(title); return work; case UCRP_ISCHARACTER: if (cn->requirement < 0 || !skins[cn->requirement].realname[0]) return va("INVALID CHAR CONDITION \"%d:%d\"", cn->type, cn->requirement); - work = (R_SkinUsable(-1, cn->requirement, false)) - ? skins[cn->requirement].realname - : "???"; + work = M_GetConditionCharacter(cn->requirement, true); return va("as %s", work); case UCRP_ISENGINECLASS: return va("with engine class %c", 'A' + cn->requirement); + case UCRP_HASFOLLOWER: + if (cn->requirement < 0 || !followers[cn->requirement].name[0]) + return va("INVALID FOLLOWER CONDITION \"%d:%d\"", cn->type, cn->requirement); + work = (K_FollowerUsable(cn->requirement)) + ? followers[cn->requirement].name + : "???"; + return va("with %s in tow", work); case UCRP_ISDIFFICULTY: { const char *speedtext = ""; @@ -1634,11 +2448,18 @@ static const char *M_GetConditionString(condition_t *cn) orbetter = " or better in"; } + if (cn->requirement == -1) + { + return va("%s%s any Cup", + completetype, orbetter + ); + } + for (cup = kartcupheaders; cup; cup = cup->next) { if (cup->id != cn->requirement) continue; - return va("%s%s %s CUP", + return va("%s%s %s Cup", completetype, orbetter, (M_CupLocked(cup) ? "???" : cup->realname) ); @@ -1653,18 +2474,71 @@ static const char *M_GetConditionString(condition_t *cn) if (!gamedata->everseenspecial) return "???"; return "collect the prize"; + case UCRP_PODIUMNOCONTINUES: + return "without using any continues"; case UCRP_FINISHCOOL: return "finish in good standing"; + case UCRP_FINISHPERFECT: + return "finish a perfect round"; case UCRP_FINISHALLPRISONS: - return "break every prison"; + return "break every Prison Egg"; case UCRP_NOCONTEST: return "NO CONTEST"; + + case UCRP_SMASHUFO: + if (!gamedata->everseenspecial) + return NULL; + return "smash the UFO Catcher"; + case UCRP_CHASEDBYSPB: + return "while chased by a Self-Propelled Bomb"; + case UCRP_MAPDESTROYOBJECTS: + { + if (cn->stringvar == NULL) + return va("INVALID DESTROY CONDITION \"%d\"", cn->type); + + title = M_BuildConditionTitle(cn->requirement); + work = va("%s: destroy all the %s", title, cn->stringvar); + Z_Free(title); + return work; + } + + case UCRP_MAKERETIRE: + { + if (cn->requirement < 0 || !skins[cn->requirement].realname[0]) + return va("INVALID CHAR CONDITION \"%d:%d\"", cn->type, cn->requirement); + + work = M_GetConditionCharacter(cn->requirement, false); + return va("make %s retire", work); + } + case UCRP_FINISHPLACE: case UCRP_FINISHPLACEEXACT: return va("finish in %d%s%s", cn->requirement, M_GetNthType(cn->requirement), ((cn->type == UCRP_FINISHPLACE && cn->requirement > 1) ? " or better" : "")); + case UCRP_FINISHGRADE: + { + char gradeletter = '?'; + const char *orbetter = ""; + + switch (cn->requirement) + { + case GRADE_E: { gradeletter = 'E'; break; } + case GRADE_D: { gradeletter = 'D'; break; } + case GRADE_C: { gradeletter = 'C'; break; } + case GRADE_B: { gradeletter = 'B'; break; } + case GRADE_A: { gradeletter = 'A'; break; } + default: { break; } + } + + if (cn->requirement < GRADE_A) + orbetter = " or better"; + + return va("get grade %c%s", + gradeletter, orbetter + ); + } case UCRP_FINISHTIME: return va("finish in %i:%02i.%02i", G_TicsToMinutes(cn->requirement, true), @@ -1680,39 +2554,118 @@ static const char *M_GetConditionString(condition_t *cn) G_TicsToSeconds(cn->requirement), G_TicsToCentiseconds(cn->requirement)); + case UCRP_RINGS: + if (cn->requirement != 20) + return va("with at least %d Rings", cn->requirement); + // FALLTHRU + case UCRP_RINGSEXACT: + return va("with exactly %d Rings", cn->requirement); + + case UCRP_SPEEDOMETER: + return va("reach %s%u%% on the speedometer", + (cn->requirement == 999) + ? "" : "at least ", + cn->requirement + ); + case UCRP_DRAFTDURATION: + return va("consistently draft off other racers for %u seconds", cn->requirement); + case UCRP_GROWCONSECUTIVEBEAMS: + return va("touch the blue beams from your own Shrink at least %u times before returning to normal size", cn->requirement); + case UCRP_TRIGGER: return "do something special"; case UCRP_FALLOFF: - return (cn->requirement == 1) ? "fall off the course" : "without falling off"; + return (cn->requirement == 1) ? "fall off the course" : "don't fall off the course"; case UCRP_TOUCHOFFROAD: - return (cn->requirement == 1) ? "touch offroad" : "without touching any offroad"; + return (cn->requirement == 1) ? "touch offroad" : "don't touch any offroad"; case UCRP_TOUCHSNEAKERPANEL: - return (cn->requirement == 1) ? "touch a Sneaker Panel" : "without touching any Sneaker Panels"; + return (cn->requirement == 1) ? "touch a Sneaker Panel" : "don't touch any Sneaker Panels"; case UCRP_RINGDEBT: - return (cn->requirement == 1) ? "go into Ring debt" : "without going into Ring debt"; + return (cn->requirement == 1) ? "go into Ring debt" : "don't go into Ring debt"; + case UCRP_FAULTED: + return (cn->requirement == 1) ? "FAULT during POSITION" : "don't FAULT during POSITION"; case UCRP_TRIPWIREHYUU: - return "go through Tripwire after getting snared by Hyudoro"; + return "go through Tripwire while afflicted by Hyudoro"; + case UCRP_WHIPHYUU: + return "Insta-Whip a racer while afflicted by Hyudoro"; case UCRP_SPBNEUTER: - return "shock a Self Propelled Bomb into submission"; + return "shock a Self-Propelled Bomb into submission"; case UCRP_LANDMINEDUNK: - return "dunk a Landmine on another racer's head"; + return "dunk a Land Mine on another racer's head"; case UCRP_HITMIDAIR: return "hit another racer with a projectile while you're both in the air"; + case UCRP_HITDRAFTERLOOKBACK: + return "hit a racer drafting off you while looking back at them"; + case UCRP_GIANTRACERSHRUNKENORBI: + return "hit a giant racer with a shrunken Orbinaut"; + case UCRP_RETURNMARKTOSENDER: + return "when cursed with Eggmark, blow up the racer responsible"; + + case UCRP_TRACKHAZARD: + { + work = (cn->requirement == 1) ? "touch a track hazard" : "don't touch any track hazards"; + if (cn->extrainfo1 == -1) + return va("%s%s", work, (cn->requirement == 1) ? " on every lap" : ""); + if (cn->extrainfo1 == -2) + return va("%s on the final lap", work); + if (cn->extrainfo1 == 0) + return va("%s during POSITION", work); + return va("%s on lap %u", work, cn->extrainfo1); + } + + case UCRP_TARGETATTACKMETHOD: + { + work = NULL; + + switch (cn->requirement) + { + // See targetdamaging_t + case UFOD_BOOST: + work = "boost power"; + break; + case UFOD_WHIP: + work = "Insta-Whip"; + break; + case UFOD_BANANA: + work = "Bananas"; + break; + case UFOD_ORBINAUT: + work = "Orbinauts"; + break; + case UFOD_JAWZ: + work = "Jawz"; + break; + case UFOD_SPB: + work = "Self-Propelled Bombs"; + break; + case UFOD_GACHABOM: + work = "Gachabom"; + break; + default: + break; + } + + if (work == NULL) + return va("INVALID ATTACK CONDITION \"%d:%d\"", cn->type, cn->requirement); + + return va("using only %s", work); + } + + case UCRP_GACHABOMMISER: + return "using exactly one Gachabom repeatedly"; case UCRP_WETPLAYER: return va("without %s %s", (cn->requirement & MFE_TOUCHWATER) ? "touching any" : "going into", - cn->stringvar); + (cn->stringvar) ? cn->stringvar : "water"); default: break; } // UC_MAPTRIGGER and UC_CONDITIONSET are explicitly very hard to support proper descriptions for return va("UNSUPPORTED CONDITION \"%d\"", cn->type); - -#undef BUILDCONDITIONTITLE } char *M_BuildConditionSetString(UINT16 unlockid) @@ -1774,6 +2727,10 @@ char *M_BuildConditionSetString(UINT16 unlockid) stopasap = true; work = "???"; } + else if (cn->type == UC_DESCRIPTIONOVERRIDE) + { + stopasap = true; + } worklen = strlen(work); strncat(message, work, len); @@ -1801,12 +2758,9 @@ char *M_BuildConditionSetString(UINT16 unlockid) break; } - // If we didn't find a prefix, just start from the first character again. - if (!message[i]) - i = 0; - - // Okay, now make the first non-whitespace character after the prefix a capital. + // Okay, now make the first non-whitespace character after this a capital. // Doesn't matter if !isalpha() - toupper is a no-op. + // (If the first loop hit the string's end, the message[i] check keeps us safe) for (; message[i]; i++) { if ((message[i] & 0x80) || isspace(message[i])) @@ -1814,6 +2768,16 @@ char *M_BuildConditionSetString(UINT16 unlockid) message[i] = toupper(message[i]); break; } + + // Also do this for the prefix. + // This might seem redundant, but "the Controls Tutorial:" is a possible prefix! + for (i = 0; message[i]; i++) + { + if ((message[i] & 0x80) || isspace(message[i])) + continue; + message[i] = toupper(message[i]); + break; + } } // Finally, do a clean wordwrap! @@ -1914,6 +2878,8 @@ boolean M_UpdateUnlockablesAndExtraEmblems(boolean loud, boolean doall) { response = M_CheckUnlockConditions(NULL); + M_UpdateNextPrisonEggPickup(); + if (gamedata->pendingkeyrounds == 0 || (gamedata->chaokeys >= GDMAX_CHAOKEYS)) { @@ -2187,8 +3153,6 @@ boolean M_SecretUnlocked(INT32 type, boolean local) boolean M_CupLocked(cupheader_t *cup) { - UINT16 i; - // Don't lock maps in dedicated servers. // That just makes hosts' lives hell. if (dedicated) @@ -2201,6 +3165,9 @@ boolean M_CupLocked(cupheader_t *cup) if (!cup) return false; +#if 0 // perfect uncached behaviour + UINT16 i; + for (i = 0; i < MAXUNLOCKABLES; ++i) { if (unlockables[i].type != SECRET_CUP) @@ -2209,14 +3176,16 @@ boolean M_CupLocked(cupheader_t *cup) continue; return !M_CheckNetUnlockByID(i); } +#else + if (cup->cache_cuplock < MAXUNLOCKABLES) + return !M_CheckNetUnlockByID(cup->cache_cuplock); +#endif return false; } boolean M_MapLocked(UINT16 mapnum) { - UINT16 i; - // Don't lock maps in dedicated servers. // That just makes hosts' lives hell. if (dedicated) @@ -2237,6 +3206,9 @@ boolean M_MapLocked(UINT16 mapnum) return M_CupLocked(mapheaderinfo[mapnum-1]->cup); } +#if 0 // perfect uncached behaviour + UINT16 i; + for (i = 0; i < MAXUNLOCKABLES; ++i) { if (unlockables[i].type != SECRET_MAP) @@ -2245,6 +3217,10 @@ boolean M_MapLocked(UINT16 mapnum) continue; return !M_CheckNetUnlockByID(i); } +#else + if (mapheaderinfo[mapnum-1]->cache_maplock < MAXUNLOCKABLES) + return !M_CheckNetUnlockByID(mapheaderinfo[mapnum-1]->cache_maplock); +#endif return false; } @@ -2495,7 +3471,7 @@ cupheader_t *M_UnlockableCup(unlockable_t *unlock) UINT16 M_UnlockableMapNum(unlockable_t *unlock) { - if (unlock->type != SECRET_MAP) + if (unlock->type != SECRET_MAP && unlock->type != SECRET_ALTMUSIC) { // This isn't a map unlockable... return NEXTMAP_INVALID; diff --git a/src/m_cond.h b/src/m_cond.h index b330b942e..832200d08 100644 --- a/src/m_cond.h +++ b/src/m_cond.h @@ -28,9 +28,12 @@ extern "C" { // [required] typedef enum { + UC_NONE, + UC_PLAYTIME, // PLAYTIME [tics] UC_ROUNDSPLAYED, // ROUNDSPLAYED [x played] UC_TOTALRINGS, // TOTALRINGS [x collected] + UC_TOTALTUMBLETIME, // TOTALTUMBLETIME [tics] UC_GAMECLEAR, // GAMECLEAR UC_OVERALLTIME, // OVERALLTIME [time to beat, tics] @@ -39,6 +42,7 @@ typedef enum UC_MAPBEATEN, // MAPBEATEN [map] UC_MAPENCORE, // MAPENCORE [map] UC_MAPSPBATTACK, // MAPSPBATTACK [map] + UC_MAPMYSTICMELODY, // MAPMYSTICMELODY [map] UC_MAPTIME, // MAPTIME [map] [time to beat, tics] UC_CHARACTERWINS, // CHARACTERWINS [character] [x rounds] @@ -53,6 +57,8 @@ typedef enum UC_UNLOCKABLE, // UNLOCKABLE [unlockable number] UC_CONDITIONSET, // CONDITIONSET [condition set number] + UC_UNLOCKPERCENT, // Unlock of [unlockable type] + UC_ADDON, // Ever loaded a custom file? UC_CREDITS, // Finish watching the credits UC_REPLAY, // Save a replay @@ -62,9 +68,12 @@ typedef enum UC_SPRAYCAN, // Grab a spraycan + UC_PRISONEGGCD, // Grab a CD from a Prison Egg + // Just for string building UC_AND, UC_COMMA, + UC_DESCRIPTIONOVERRIDE, UCRP_REQUIRESPLAYING, // All conditions below this can only be checked if (Playing() && gamestate == GS_LEVEL). @@ -79,36 +88,64 @@ typedef enum UCRP_ISCHARACTER, // character == [skin] UCRP_ISENGINECLASS, // engine class [class] + UCRP_HASFOLLOWER, // follower == [followerskin] UCRP_ISDIFFICULTY, // difficulty >= [difficulty] UCRP_PODIUMCUP, // cup == [cup] [optional: >= grade OR place] UCRP_PODIUMEMERALD, // Get to podium sequence with that cup's emerald UCRP_PODIUMPRIZE, // Get to podium sequence with that cup's bonus (alternate string version of UCRP_PODIUMEMERALD + UCRP_PODIUMNOCONTINUES, // Get to podium sequence without any continues UCRP_FINISHCOOL, // Finish in good standing + UCRP_FINISHPERFECT, // Finish a perfect race UCRP_FINISHALLPRISONS, // Break all prisons UCRP_NOCONTEST, // No Contest + UCRP_SMASHUFO, // Smash the UFO Catcher + UCRP_CHASEDBYSPB, // Chased by SPB + UCRP_MAPDESTROYOBJECTS, // LEVELNAME: Destroy all [object names] -- CAUTION: You have to add to the level's header too to get them successfully tracked! + + UCRP_MAKERETIRE, // Make another player of [skin] No Contest + UCRP_FINISHPLACE, // Finish at least [place] UCRP_FINISHPLACEEXACT, // Finish at [place] exactly + UCRP_FINISHGRADE, // Finish with at least grade [grade] + UCRP_FINISHTIME, // Finish <= [time, tics] UCRP_FINISHTIMEEXACT, // Finish == [time, tics] UCRP_FINISHTIMELEFT, // Finish with at least [time, tics] to spare + UCRP_RINGS, // >= [rings] + UCRP_RINGSEXACT, // == [rings] + + UCRP_SPEEDOMETER, // >= [percentage] + UCRP_DRAFTDURATION, // >= [time, seconds] + UCRP_GROWCONSECUTIVEBEAMS, // touch more than n times consecutively + UCRP_TRIGGER, // Map execution trigger [id] UCRP_FALLOFF, // Fall off (or don't) UCRP_TOUCHOFFROAD, // Touch offroad (or don't) UCRP_TOUCHSNEAKERPANEL, // Either touch sneaker panel (or don't) UCRP_RINGDEBT, // Go into debt (or don't) + UCRP_FAULTED, // FAULT UCRP_TRIPWIREHYUU, // Go through tripwire with Hyudoro + UCRP_WHIPHYUU, // Use Insta-Whip with Hyudoro UCRP_SPBNEUTER, // Kill an SPB with Lightning UCRP_LANDMINEDUNK, // huh? you died? that's weird. all i did was try to hug you... UCRP_HITMIDAIR, // Hit another player mid-air with a kartfielditem + UCRP_HITDRAFTERLOOKBACK, // Hit a player that's behind you, while looking back at them, and they're drafting off you + UCRP_GIANTRACERSHRUNKENORBI, // Hit a giant racer with a shrunken Orbinaut + UCRP_RETURNMARKTOSENDER, // Hit the player responsible for Eggman Marking you with that explosion - UCRP_WETPLAYER, // Don't touch [fluid] + UCRP_TRACKHAZARD, // (Don't) get hit by a track hazard (maybe specific lap) + + UCRP_TARGETATTACKMETHOD, // Break targets/UFO using only one method + UCRP_GACHABOMMISER, // Break targets/UFO using exactly one Gachabom repeatedly + + UCRP_WETPLAYER, // Don't touch [strictness] [fluid] } conditiontype_t; // Condition Set information @@ -184,14 +221,18 @@ typedef enum // Level restrictions SECRET_CUP, // Permit access to entire cup (overrides SECRET_MAP) SECRET_MAP, // Permit access to single map + SECRET_ALTMUSIC, // Permit access to single map music track // Player restrictions SECRET_SKIN, // Permit this character SECRET_FOLLOWER, // Permit this follower SECRET_COLOR, // Permit this color + // Everything below this line is supposed to be only one per Challenges list + SECRET_ONEPERBOARD, + // Difficulty restrictions - SECRET_HARDSPEED, // Permit Hard gamespeed + SECRET_HARDSPEED = SECRET_ONEPERBOARD, // Permit Hard gamespeed SECRET_MASTERMODE, // Permit Master Mode bots in GP SECRET_ENCORE, // Permit Encore option @@ -219,7 +260,7 @@ typedef enum #define MAXEMBLEMS (MAXCONDITIONSETS*2) #define MAXUNLOCKABLES MAXCONDITIONSETS -#define CHALLENGEGRIDHEIGHT 4 +#define CHALLENGEGRIDHEIGHT 5 #ifdef DEVELOP #define CHALLENGEGRIDLOOPWIDTH 3 #else @@ -240,11 +281,10 @@ typedef enum { #define GDMAX_RINGS 999999999 #define GDMAX_CHAOKEYS 9999 -#ifdef DEVELOP -#define GDCONVERT_ROUNDSTOKEY 20 -#else -#define GDCONVERT_ROUNDSTOKEY 50 -#endif +#define GDCONVERT_ROUNDSTOKEY 32 + +#define GDINIT_CHAOKEYS 3 // Start with 3 Chao Keys !! +#define GDINIT_PRISONSTOPRIZE 30 // 30 Prison Eggs to your [Wild Prize] !! typedef enum { GDGT_RACE, @@ -287,6 +327,15 @@ struct gamedata_t UINT16 gotspraycans; candata_t* spraycans; + // PRISON EGG PICKUPS + UINT16 numprisoneggpickups; + UINT16 gettableprisoneggpickups; + UINT16 thisprisoneggpickup; + condition_t *thisprisoneggpickup_cached; + boolean thisprisoneggpickupgrabbed; + UINT16 prisoneggstothispickup; + UINT16* prisoneggpickups; + // CHALLENGE GRID UINT16 challengegridwidth; UINT16 *challengegrid; @@ -298,6 +347,7 @@ struct gamedata_t UINT32 totalplaytime; UINT32 roundsplayed[GDGT_MAX]; UINT32 totalrings; + UINT32 totaltumbletime; // Chao Key condition bypass UINT32 pendingkeyrounds; @@ -311,6 +361,8 @@ struct gamedata_t boolean eversavedreplay; boolean everseenspecial; boolean evercrashed; + boolean chaokeytutorial; + boolean majorkeyskipattempted; gdmusic_t musicstate; // BACKWARDS COMPAT ASSIST @@ -347,6 +399,7 @@ void M_UpdateChallengeGridExtraData(challengegridextradata_t *extradata); #define CHE_CONNECTEDLEFT (1<<1) #define CHE_CONNECTEDUP (1<<2) #define CHE_DONTDRAW (CHE_CONNECTEDLEFT|CHE_CONNECTEDUP) +#define CHE_ALLCLEAR (1<<3) char *M_BuildConditionSetString(UINT16 unlockid); #define DESCRIPTIONWIDTH 170 @@ -372,6 +425,8 @@ boolean M_UpdateUnlockablesAndExtraEmblems(boolean loud, boolean doall); #define PENDING_CHAOKEYS (UINT16_MAX-1) UINT16 M_GetNextAchievedUnlock(boolean canskipchaokeys); +void M_UpdateNextPrisonEggPickup(void); + UINT16 M_CheckLevelEmblems(void); UINT16 M_CompletionEmblems(void); diff --git a/src/menus/extras-challenges.c b/src/menus/extras-challenges.c index 4ca2363e2..92b4d48ee 100644 --- a/src/menus/extras-challenges.c +++ b/src/menus/extras-challenges.c @@ -21,7 +21,7 @@ menu_t MISC_ChallengesDef = { &MainDef, 0, MISC_ChallengesStatsDummyMenu, - BASEVIDWIDTH/2, 30, + BASEVIDWIDTH/2, 11, 0, 0, 0, "UNLOCK", @@ -54,6 +54,83 @@ menu_t MISC_StatisticsDef = { struct challengesmenu_s challengesmenu; +static void M_UpdateChallengeGridVisuals(void) +{ + UINT16 i; + + challengesmenu.unlockcount[CMC_UNLOCKED] = 0; + challengesmenu.unlockcount[CMC_TOTAL] = 0; + + for (i = 0; i < MAXUNLOCKABLES; i++) + { + if (!unlockables[i].conditionset) + { + continue; + } + + challengesmenu.unlockcount[CMC_TOTAL]++; + + if (!gamedata->unlocked[i]) + { + continue; + } + + challengesmenu.unlockcount[CMC_UNLOCKED]++; + + if (M_Achieved(unlockables[i].conditionset - 1) == true) + { + continue; + } + + challengesmenu.unlockcount[CMC_KEYED]++; + + if (unlockables[i].majorunlock == false) + { + continue; + } + + challengesmenu.unlockcount[CMC_MAJORSKIPPED]++; + } + + challengesmenu.unlockcount[CMC_PERCENT] = + (100 * challengesmenu.unlockcount[CMC_UNLOCKED]) + /challengesmenu.unlockcount[CMC_TOTAL]; + + #define medalheight (19) + + challengesmenu.unlockcount[CMC_MEDALID] = 0; + + challengesmenu.unlockcount[CMC_MEDALFILLED] = + (medalheight * ( + challengesmenu.unlockcount[CMC_UNLOCKED] + - challengesmenu.unlockcount[CMC_MAJORSKIPPED] + )) / challengesmenu.unlockcount[CMC_TOTAL]; + + if (challengesmenu.unlockcount[CMC_PERCENT] == 100) + { + if (challengesmenu.unlockcount[CMC_KEYED] == 0) + { + challengesmenu.unlockcount[CMC_MEDALID] = 2; + challengesmenu.unlockcount[CMC_PERCENT]++; // 101% + } + else if (challengesmenu.unlockcount[CMC_MAJORSKIPPED] == 0) + { + challengesmenu.unlockcount[CMC_MEDALID] = 1; + } + } + else + { + if (challengesmenu.unlockcount[CMC_MEDALFILLED] == 0 && challengesmenu.unlockcount[CMC_UNLOCKED] != 0) + { + // Cheat to give you a sliver of pixel. + challengesmenu.unlockcount[CMC_MEDALFILLED] = 1; + } + } + + challengesmenu.unlockcount[CMC_MEDALBLANK] = + medalheight - challengesmenu.unlockcount[CMC_MEDALFILLED]; +} + static void M_ChallengesAutoFocus(UINT16 unlockid, boolean fresh) { UINT16 i; @@ -210,7 +287,7 @@ static void M_ChallengesAutoFocus(UINT16 unlockid, boolean fresh) menu_t *M_InterruptMenuWithChallenges(menu_t *desiredmenu) { - UINT16 i, newunlock; + UINT16 newunlock; if (Playing()) return desiredmenu; @@ -231,6 +308,7 @@ menu_t *M_InterruptMenuWithChallenges(menu_t *desiredmenu) challengesmenu.requestflip = false; challengesmenu.requestnew = false; challengesmenu.chaokeyadd = false; + challengesmenu.keywasadded = false; challengesmenu.chaokeyhold = 0; challengesmenu.currentunlock = MAXUNLOCKABLES; challengesmenu.unlockcondition = NULL; @@ -246,22 +324,8 @@ menu_t *M_InterruptMenuWithChallenges(menu_t *desiredmenu) memset(setup_explosions, 0, sizeof(setup_explosions)); memset(&challengesmenu.unlockcount, 0, sizeof(challengesmenu.unlockcount)); - for (i = 0; i < MAXUNLOCKABLES; i++) - { - if (!unlockables[i].conditionset) - { - continue; - } - challengesmenu.unlockcount[CC_TOTAL]++; - - if (!gamedata->unlocked[i]) - { - continue; - } - - challengesmenu.unlockcount[CC_UNLOCKED]++; - } + M_UpdateChallengeGridVisuals(); if (challengesmenu.pending) M_ChallengesAutoFocus(newunlock, true); @@ -290,10 +354,10 @@ void M_Challenges(INT32 choice) M_SetupNextMenu(&MISC_ChallengesDef, false); } -static boolean M_CanKeyHiliTile(boolean devskip) +boolean M_CanKeyHiliTile(void) { // No keys to do it with? - if (gamedata->chaokeys == 0 && !devskip) + if (gamedata->chaokeys == 0) return false; // No tile data? @@ -308,22 +372,73 @@ static boolean M_CanKeyHiliTile(boolean devskip) if (gamedata->unlocked[challengesmenu.currentunlock] == true) return false; - // Marked as unskippable? - if (unlockables[challengesmenu.currentunlock].majorunlock == true && !devskip) - return false; - UINT16 i = (challengesmenu.hilix * CHALLENGEGRIDHEIGHT) + challengesmenu.hiliy; // Not a hinted tile OR a fresh board. if (!(challengesmenu.extradata[i].flags & CHE_HINT) - && (challengesmenu.unlockcount[CC_UNLOCKED] + challengesmenu.unlockcount[CC_TALLY] > 0) - && !devskip) + && (challengesmenu.unlockcount[CMC_UNLOCKED] > 0)) return false; + // Marked as major? + if (unlockables[challengesmenu.currentunlock].majorunlock == true) + { + if (!(challengesmenu.extradata[i].flags & CHE_ALLCLEAR)) + return false; + + if (gamedata->chaokeys < 10) + return false; + } + // All good! return true; } +enum { + CCTUTORIAL_KEYGEN = 0, + CCTUTORIAL_MAJORSKIP, +} cctutorial_e; + +static void M_ChallengesTutorial(UINT8 option) +{ + switch (option) + { + case CCTUTORIAL_KEYGEN: + { + M_StartMessage("Challenges & Chao Keys", + va(M_GetText( + "You just generated a Chao Key!\n" + "These can clear tough Challenges.\n" + "\n" + "Use them wisely - it'll take\n" + "%u rounds to pick up another!\n" + ), GDCONVERT_ROUNDSTOKEY + ), NULL, MM_NOTHING, NULL, NULL); + gamedata->chaokeytutorial = true; + break; + } + case CCTUTORIAL_MAJORSKIP: + { + M_StartMessage("Big Challenges & Chao Keys", + M_GetText( + "Watch out! You need 10 Chao Keys.\n" + "to break open Big Challenge tiles.\n" + "\n" + "You'll also need to unlock\n" + "the surrounding tiles first.\n" + ), NULL, MM_NOTHING, NULL, NULL); + gamedata->majorkeyskipattempted = true; + break; + } + default: + { + M_StartMessage("M_ChallengesTutorial ERROR", + "Invalid argument!?\n", + NULL, MM_NOTHING, NULL, NULL); + break; + } + } +} + void M_ChallengesTick(void) { const UINT8 pid = 0; @@ -338,7 +453,7 @@ void M_ChallengesTick(void) if (setup_explosions[i].tics > 0) setup_explosions[i].tics--; } - for (i = CC_ANIM; i < CC_MAX; i++) + for (i = CMC_ANIM; i < CMC_MAX; i++) { if (challengesmenu.unlockcount[i] > 0) challengesmenu.unlockcount[i]--; @@ -370,25 +485,28 @@ void M_ChallengesTick(void) if (challengesmenu.chaokeyhold) { - boolean devskip = false; -#ifdef DEVELOP - devskip = M_MenuButtonHeld(pid, MBT_Z); -#endif - // A little messy, but don't freak out, this is just so devs don't crash the game on non-tiles - if ((devskip || M_MenuExtraHeld(pid)) && M_CanKeyHiliTile(devskip)) + if (M_MenuExtraHeld(pid) && M_CanKeyHiliTile()) { // Not pressed just this frame? if (!M_MenuExtraPressed(pid)) { challengesmenu.chaokeyhold++; - if (challengesmenu.chaokeyhold > CHAOHOLD_MAX) + const UINT32 chaohold_duration = + CHAOHOLD_PADDING + + ((unlockables[challengesmenu.currentunlock].majorunlock == true) + ? CHAOHOLD_MAJOR + : CHAOHOLD_STANDARD + ); + + if (challengesmenu.chaokeyhold > chaohold_duration) { #ifndef CHAOKEYDEBUG - gamedata->chaokeys--; + gamedata->chaokeys -= (unlockables[challengesmenu.currentunlock].majorunlock == true) + ? 10 : 1; #endif challengesmenu.chaokeyhold = 0; - challengesmenu.unlockcount[CC_CHAOANIM]++; + challengesmenu.unlockcount[CMC_CHAOANIM]++; S_StartSound(NULL, sfx_chchng); @@ -401,7 +519,7 @@ void M_ChallengesTick(void) else { challengesmenu.chaokeyhold = 0; - challengesmenu.unlockcount[CC_CHAONOPE] = 6; + challengesmenu.unlockcount[CMC_CHAONOPE] = 6; S_StartSound(NULL, sfx_s3k7b); //sfx_s3kb2 } } @@ -461,10 +579,12 @@ void M_ChallengesTick(void) S_StartSound(NULL, sfx_achiev); gamedata->keyspending--; gamedata->chaokeys++; - challengesmenu.unlockcount[CC_CHAOANIM]++; + challengesmenu.unlockcount[CMC_CHAOANIM]++; if (gamedata->musicstate < GDMUSIC_KEYG) gamedata->musicstate = GDMUSIC_KEYG; + + challengesmenu.keywasadded = true; } } } @@ -511,9 +631,9 @@ void M_ChallengesTick(void) if (challengesmenu.unlockcondition) Z_Free(challengesmenu.unlockcondition); challengesmenu.unlockcondition = M_BuildConditionSetString(challengesmenu.currentunlock); + M_UpdateChallengeGridVisuals(); - challengesmenu.unlockcount[CC_TALLY]++; - challengesmenu.unlockcount[CC_ANIM]++; + challengesmenu.unlockcount[CMC_ANIM]++; if (challengesmenu.extradata) { @@ -558,14 +678,7 @@ void M_ChallengesTick(void) if (bombcolor == SKINCOLOR_NONE) { - bombcolor = cv_playercolor[0].value; - if (bombcolor == SKINCOLOR_NONE) - { - INT32 psk = R_SkinAvailable(cv_skin[0].string); - if (psk == -1) - psk = 0; - bombcolor = skins[psk].prefcolor; - } + bombcolor = M_GetCvPlayerColor(0); } i = (ref->majorunlock && M_RandomChance(FRACUNIT/2)) ? 1 : 0; @@ -581,15 +694,6 @@ void M_ChallengesTick(void) } else if (!challengesmenu.chaokeyhold) { - - // Tick down the tally. (currently not visible) - /*if ((challengesmenu.ticker & 1) - && challengesmenu.unlockcount[CC_TALLY] > 0) - { - challengesmenu.unlockcount[CC_TALLY]--; - challengesmenu.unlockcount[CC_UNLOCKED]++; - }*/ - if (challengesmenu.fade > 0) { // Fade decrease. @@ -597,6 +701,12 @@ void M_ChallengesTick(void) { // Play music the moment control returns. M_PlayMenuJam(); + + if (gamedata->chaokeytutorial == false + && challengesmenu.keywasadded == true) + { + M_ChallengesTutorial(CCTUTORIAL_KEYGEN); + } } } } @@ -616,13 +726,21 @@ boolean M_ChallengesInputs(INT32 ch) } else if (M_MenuExtraPressed(pid)) { - if (M_CanKeyHiliTile(false)) + if (gamedata->chaokeytutorial == true + && gamedata->majorkeyskipattempted == false + && challengesmenu.currentunlock < MAXUNLOCKABLES + && gamedata->unlocked[challengesmenu.currentunlock] == false + && unlockables[challengesmenu.currentunlock].majorunlock == true) + { + M_ChallengesTutorial(CCTUTORIAL_MAJORSKIP); + } + else if (M_CanKeyHiliTile()) { challengesmenu.chaokeyhold = 1; } else { - challengesmenu.unlockcount[CC_CHAONOPE] = 6; + challengesmenu.unlockcount[CMC_CHAONOPE] = 6; S_StartSound(NULL, sfx_s3k7b); //sfx_s3kb2 #ifdef CHAOKEYDEBUG @@ -630,10 +748,7 @@ boolean M_ChallengesInputs(INT32 ch) { gamedata->unlocked[challengesmenu.currentunlock] = gamedata->unlockpending[challengesmenu.currentunlock] = false; - if (challengesmenu.unlockcount[CC_TALLY] > 0) - challengesmenu.unlockcount[CC_TALLY]--; - else - challengesmenu.unlockcount[CC_UNLOCKED]--; + M_UpdateChallengeGridVisuals(); } #endif } @@ -642,7 +757,15 @@ boolean M_ChallengesInputs(INT32 ch) #ifdef DEVELOP else if (M_MenuButtonPressed(pid, MBT_Z)) { - challengesmenu.chaokeyhold = 1; + gamedata->chaokeys++; + challengesmenu.unlockcount[CMC_CHAOANIM]++; + + if (gamedata->chaokeytutorial == false) + { + M_ChallengesTutorial(CCTUTORIAL_KEYGEN); + } + + S_StartSound(NULL, sfx_dbgsal); return true; } #endif diff --git a/src/objects/gachabom-rebound.cpp b/src/objects/gachabom-rebound.cpp index 07aec4868..7da4a3f81 100644 --- a/src/objects/gachabom-rebound.cpp +++ b/src/objects/gachabom-rebound.cpp @@ -73,6 +73,8 @@ bool award_target(mobj_t* mobj) { player->itemtype = KITEM_GACHABOM; player->itemamount++; + if (player->roundconditions.gachabom_miser == 1) + player->roundconditions.gachabom_miser = 0; return true; } diff --git a/src/objects/shrink.c b/src/objects/shrink.c index 23613ff52..04b703328 100644 --- a/src/objects/shrink.c +++ b/src/objects/shrink.c @@ -547,6 +547,16 @@ boolean Obj_ShrinkLaserCollide(mobj_t *gun, mobj_t *victim) victim->player->growshrinktimer += 6*TICRATE; S_StartSound(victim, sfx_kc5a); + if (victim->player->roundconditions.consecutive_grow_lasers < UINT8_MAX) + { + victim->player->roundconditions.consecutive_grow_lasers++; + if (victim->player->roundconditions.consecutive_grow_lasers > victim->player->roundconditions.best_consecutive_grow_lasers) + { + victim->player->roundconditions.best_consecutive_grow_lasers + = victim->player->roundconditions.consecutive_grow_lasers; + } + } + if (prevTimer <= 0) { victim->scalespeed = mapobjectscale/TICRATE; diff --git a/src/objects/spb.c b/src/objects/spb.c index 6877918d9..b90da6789 100644 --- a/src/objects/spb.c +++ b/src/objects/spb.c @@ -1025,6 +1025,8 @@ void Obj_SPBExplode(mobj_t *spb) P_SetTarget(&spbExplode->target, spb_owner(spb)); } + spbExplode->threshold = KITEM_SPB; + // Tell the explosion to use alternate knockback. spbExplode->movefactor = ((SPB_CHASETIMESCALE - spb_chasetime(spb)) * SPB_CHASETIMEMUL) / SPB_CHASETIMESCALE; diff --git a/src/objects/ufo.c b/src/objects/ufo.c index cb9351aed..a52b112ed 100644 --- a/src/objects/ufo.c +++ b/src/objects/ufo.c @@ -18,6 +18,7 @@ #include "../k_objects.h" #include "../m_random.h" #include "../p_local.h" +#include "../m_cond.h" #include "../r_main.h" #include "../s_sound.h" #include "../g_game.h" @@ -763,52 +764,86 @@ static void UFOKillPieces(mobj_t *ufo) static UINT8 GetUFODamage(mobj_t *inflictor, UINT8 damageType) { + UINT8 ret = 0; + targetdamaging_t targetdamaging = UFOD_GENERIC; + if (inflictor != NULL && P_MobjWasRemoved(inflictor) == false) { switch (inflictor->type) { + // Shields deal chip damage. case MT_JAWZ_SHIELD: + { + targetdamaging = UFOD_JAWZ; + ret = 10; + break; + } case MT_ORBINAUT_SHIELD: + { + targetdamaging = UFOD_ORBINAUT; + ret = 10; + break; + } case MT_INSTAWHIP: { - // Shields deal chip damage. - return 10; + targetdamaging = UFOD_WHIP; + ret = 10; + break; } case MT_JAWZ: { // Thrown Jawz deal a bit extra. - return 15; + targetdamaging = UFOD_JAWZ; + ret = 15; + break; } case MT_ORBINAUT: { // Thrown orbinauts deal double damage. - return 20; + targetdamaging = UFOD_ORBINAUT; + ret = 20; + break; + } + case MT_GACHABOM: + { + // Thrown gachabom need to be tracked, but have no special damage value as of yet. + targetdamaging = UFOD_GACHABOM; + break; } case MT_SPB: { // SPB deals triple damage. - return 30; + targetdamaging |= UFOD_SPB; + ret = 30; + break; } case MT_BANANA: { + targetdamaging = UFOD_BANANA; + // Banana snipes deal triple damage, // laid down bananas deal regular damage. if (inflictor->health > 1) { - return 30; + ret = 30; + break; } - return 10; + ret = 10; + break; } case MT_PLAYER: { // Players deal damage relative to how many sneakers they used. - return 15 * max(1, inflictor->player->numsneakers); + targetdamaging = UFOD_BOOST; + ret = 15 * max(1, inflictor->player->numsneakers); + break; } case MT_SPECIAL_UFO: { // UFODebugSetHealth - return 1; + ret = 1; + break; } default: { @@ -817,6 +852,11 @@ static UINT8 GetUFODamage(mobj_t *inflictor, UINT8 damageType) } } + P_TrackRoundConditionTargetDamage(targetdamaging); + + if (ret != 0) + return ret; + // Guess from damage type. switch (damageType & DMG_TYPEMASK) { @@ -884,6 +924,8 @@ boolean Obj_SpecialUFODamage(mobj_t *ufo, mobj_t *inflictor, mobj_t *source, UIN // Destroy the UFO parts, and make the emerald collectible! UFOKillPieces(ufo); + gamedata->deferredconditioncheck = true; // Check Challenges! + ufo->flags = (ufo->flags & ~MF_SHOOTABLE) | (MF_SPECIAL|MF_PICKUPFROMBELOW); ufo->shadowscale = FRACUNIT/3; diff --git a/src/p_enemy.c b/src/p_enemy.c index 084ccb169..81cd6c517 100644 --- a/src/p_enemy.c +++ b/src/p_enemy.c @@ -12652,11 +12652,11 @@ void A_FireShrink(mobj_t *actor) INT32 locvar1 = var1; INT32 locvar2 = var2; - if (LUA_CallAction(A_FIRESHRINK, actor)) + if (LUA_CallAction(A_FIRESHRINK, actor) || locvar2 == 0) return; actor->destscale = locvar1; - actor->scalespeed = FRACUNIT/locvar2; + actor->scalespeed = mapobjectscale/locvar2; } // Function: A_SpawnPterabytes diff --git a/src/p_floor.c b/src/p_floor.c index 2d25e8208..4b96192c8 100644 --- a/src/p_floor.c +++ b/src/p_floor.c @@ -980,7 +980,6 @@ static mobj_t *SearchMarioNode(msecnode_t *node) case MT_THOK: case MT_GHOST: case MT_OVERLAY: - case MT_EMERALDSPAWN: case MT_ELEMENTAL_ORB: case MT_ATTRACT_ORB: case MT_FORCE_ORB: diff --git a/src/p_inter.c b/src/p_inter.c index 78ec9864e..bf70ce28e 100644 --- a/src/p_inter.c +++ b/src/p_inter.c @@ -206,10 +206,19 @@ boolean P_CanPickupEmblem(player_t *player, INT32 emblemID) return false; } - if (player->bot) + if (player != NULL) { - // Your nefarious opponent puppy can't grab these for you. - return false; + if (player->bot) + { + // Your nefarious opponent puppy can't grab these for you. + return false; + } + + if (player->exiting) + { + // Yeah but YOU didn't actually do it now did you + return false; + } } return true; @@ -714,6 +723,12 @@ void P_TouchSpecialThing(mobj_t *special, mobj_t *toucher, boolean heightcheck) return; } + if (player->exiting) + { + // Yeah but YOU didn't actually do it now did you + return; + } + if (!P_IsLocalPlayer(player)) { // Must be party. @@ -799,6 +814,72 @@ void P_TouchSpecialThing(mobj_t *special, mobj_t *toucher, boolean heightcheck) return; } + case MT_PRISONEGGDROP: + { + if (demo.playback) + { + // Never collect emblems in replays. + return; + } + + if (player->bot) + { + // Your nefarious opponent puppy can't grab these for you. + return; + } + + if (!P_IsLocalPlayer(player)) + { + // Must be party. + return; + } + + if (special->hitlag || special->scale < mapobjectscale/2) + { + // Don't get during the initial activation + return; + } + + if (special->extravalue1) + { + // Don't get during destruction + return; + } + + if ( + grandprixinfo.gp == true // Bonus Round + && netgame == false // game design + makes it easier to implement + && gamedata->thisprisoneggpickup_cached != NULL + ) + { + gamedata->thisprisoneggpickupgrabbed = true; + if (gamedata->prisoneggstothispickup < GDINIT_PRISONSTOPRIZE) + { + // Just in case it's set absurdly low for testing. + gamedata->prisoneggstothispickup = GDINIT_PRISONSTOPRIZE; + } + + if (!M_UpdateUnlockablesAndExtraEmblems(true, true)) + S_StartSound(NULL, sfx_ncitem); + gamedata->deferredsave = true; + } + + statenum_t teststate = (special->state-states); + + if (teststate == S_PRISONEGGDROP_CD) + { + special->momz = P_MobjFlip(special) * 2 * mapobjectscale; + special->flags = (special->flags & ~MF_SPECIAL) | (MF_NOGRAVITY|MF_NOCLIPHEIGHT); + special->extravalue1 = 1; + + special->renderflags = (special->renderflags & ~RF_BRIGHTMASK) | (RF_ADD | RF_FULLBRIGHT); + + return; + } + + break; + } + case MT_LSZ_BUNGEE: Obj_BungeeSpecial(special, player); return; @@ -910,10 +991,25 @@ void P_TouchCheatcheck(mobj_t *post, player_t *player, boolean snaptopost) player->cheatchecknum = post->health; } -static void P_AddBrokenPrison(mobj_t *target, mobj_t *source) +void P_TrackRoundConditionTargetDamage(targetdamaging_t targetdamaging) { - (void)target; + UINT8 i; + for (i = 0; i <= splitscreen; i++) + { + if (!playeringame[g_localplayers[i]]) + continue; + if (players[g_localplayers[i]].spectator) + continue; + players[g_localplayers[i]].roundconditions.targetdamaging |= targetdamaging; + /* -- the following isn't needed because we can just check for targetdamaging == UFOD_GACHABOM + if (targetdamaging != UFOD_GACHABOM) + players[g_localplayers[i]].roundconditions.gachabom_miser = 0xFF; + */ + } +} +static void P_AddBrokenPrison(mobj_t *target, mobj_t *inflictor, mobj_t *source) +{ if (!battleprisons) return; @@ -935,6 +1031,48 @@ static void P_AddBrokenPrison(mobj_t *target, mobj_t *source) K_SpawnBattlePoints(source->player, NULL, 1); } + targetdamaging_t targetdamaging = UFOD_GENERIC; + if (P_MobjWasRemoved(inflictor) == true) + ; + else switch (inflictor->type) + { + case MT_GACHABOM: + targetdamaging = UFOD_GACHABOM; + break; + case MT_ORBINAUT: + case MT_ORBINAUT_SHIELD: + targetdamaging = UFOD_ORBINAUT; + break; + case MT_BANANA: + targetdamaging = UFOD_BANANA; + break; + case MT_INSTAWHIP: + targetdamaging = UFOD_WHIP; + break; + // This is only accessible for MT_CDUFO's touch! + case MT_PLAYER: + targetdamaging = UFOD_BOOST; + break; + // The following can't be accessed in standard play... + // but the cost of tracking them here is trivial :D + case MT_JAWZ: + case MT_JAWZ_SHIELD: + targetdamaging = UFOD_JAWZ; + break; + case MT_SPB: + targetdamaging = UFOD_SPB; + break; + default: + break; + } + + P_TrackRoundConditionTargetDamage(targetdamaging); + + if (gamedata->prisoneggstothispickup) + { + gamedata->prisoneggstothispickup--; + } + if (++numtargets >= maptargets) { P_DoAllPlayersExit(0, (grandprixinfo.gp == true)); @@ -947,6 +1085,55 @@ static void P_AddBrokenPrison(mobj_t *target, mobj_t *source) extratimeintics += 10*TICRATE; secretextratime = TICRATE/2; } + + if ( + grandprixinfo.gp == true // Bonus Round + && demo.playback == false // Not playback + && netgame == false // game design + makes it easier to implement + && gamedata->thisprisoneggpickup_cached != NULL + && gamedata->prisoneggstothispickup == 0 + && gamedata->thisprisoneggpickupgrabbed == false + ) + { + // Will be 0 for the next level + gamedata->prisoneggstothispickup = (maptargets - numtargets); + + mobj_t *secretpickup = P_SpawnMobj( + target->x, target->y, + target->z + target->height/2, + MT_PRISONEGGDROP + ); + + if (secretpickup) + { + secretpickup->hitlag = target->hitlag; + + secretpickup->z -= secretpickup->height/2; + + P_SetScale(secretpickup, mapobjectscale/TICRATE); + // secretpickup->destscale = mapobjectscale; -- safe assumption it's already set? + secretpickup->scalespeed = (2*mapobjectscale)/(3*TICRATE); + + // flags are NOT from the target - just in case it's just been placed on the ceiling as a gimmick + secretpickup->flags2 |= (source->flags2 & MF2_OBJECTFLIP); + secretpickup->eflags |= (source->eflags & MFE_VERTICALFLIP); + + // Okay these have to use M_Random because replays... + // The spawning of these won't be recorded back! + const angle_t launchangle = FixedAngle(M_RandomRange(60, 80) * FRACUNIT); + const fixed_t launchmomentum = 20 * mapobjectscale; + + secretpickup->momz = P_MobjFlip(target) // THIS one uses target! + * P_ReturnThrustY(secretpickup, launchangle, launchmomentum); + + secretpickup->angle = FixedAngle(M_RandomKey(360) * FRACUNIT); + + P_InstaThrust( + secretpickup, secretpickup->angle, + P_ReturnThrustX(secretpickup, launchangle, launchmomentum) + ); + } + } } } @@ -1887,7 +2074,7 @@ void P_KillMobj(mobj_t *target, mobj_t *inflictor, mobj_t *source, UINT8 damaget S_StartSound(target, sfx_mbs60); - P_AddBrokenPrison(target, source); + P_AddBrokenPrison(target, inflictor, source); } break; @@ -1897,7 +2084,7 @@ void P_KillMobj(mobj_t *target, mobj_t *inflictor, mobj_t *source, UINT8 damaget target->momz = -(3*mapobjectscale)/2; target->fuse = 2*TICRATE; - P_AddBrokenPrison(target, source); + P_AddBrokenPrison(target, inflictor, source); break; case MT_BATTLEBUMPER: @@ -2193,6 +2380,8 @@ static boolean P_PlayerHitsPlayer(mobj_t *target, mobj_t *inflictor, mobj_t *sou static boolean P_KillPlayer(player_t *player, mobj_t *inflictor, mobj_t *source, UINT8 type) { + const boolean beforeexit = !(player->exiting || (player->pflags & PF_NOCONTEST)); + if (type == DMG_SPECTATOR && (G_GametypeHasTeams() || G_GametypeHasSpectators())) { P_SetPlayerSpectator(player-players); @@ -2240,7 +2429,8 @@ static boolean P_KillPlayer(player_t *player, mobj_t *inflictor, mobj_t *source, { case DMG_DEATHPIT: // Fell off the stage - if (player->roundconditions.fell_off == false) + if (player->roundconditions.fell_off == false + && beforeexit == true) { player->roundconditions.fell_off = true; player->roundconditions.checkthisframe = true; @@ -2500,16 +2690,63 @@ boolean P_DamageMobj(mobj_t *target, mobj_t *inflictor, mobj_t *source, INT32 da } } - if (inflictor && source && source->player) + if (source && source->player) { if (source->player->roundconditions.hit_midair == false - && K_IsMissileOrKartItem(source) + && source != target + && inflictor + && K_IsMissileOrKartItem(inflictor) && target->player->airtime > TICRATE/2 && source->player->airtime > TICRATE/2) { source->player->roundconditions.hit_midair = true; source->player->roundconditions.checkthisframe = true; } + + if (source->player->roundconditions.hit_drafter_lookback == false + && source != target + && target->player->lastdraft == (source->player - players) + && (K_GetKartButtons(source->player) & BT_LOOKBACK) == BT_LOOKBACK + /*&& (AngleDelta(K_MomentumAngle(source), R_PointToAngle2(source->x, source->y, target->x, target->y)) > ANGLE_90)*/) + { + source->player->roundconditions.hit_drafter_lookback = true; + source->player->roundconditions.checkthisframe = true; + } + + if (source->player->roundconditions.giant_foe_shrunken_orbi == false + && source != target + && player->growshrinktimer > 0 + && !P_MobjWasRemoved(inflictor) + && inflictor->type == MT_ORBINAUT + && inflictor->scale < FixedMul((FRACUNIT + SHRINK_SCALE), mapobjectscale * 2)) // halfway between base scale and shrink scale, a little bit of leeway + { + source->player->roundconditions.giant_foe_shrunken_orbi = true; + source->player->roundconditions.checkthisframe = true; + } + + if (source == target + && !P_MobjWasRemoved(inflictor) + && inflictor->type == MT_SPBEXPLOSION + && inflictor->threshold == KITEM_EGGMAN + && !P_MobjWasRemoved(inflictor->tracer) + && inflictor->tracer != source + && inflictor->tracer->player + && inflictor->tracer->player->roundconditions.returntosender_mark == false) + { + inflictor->tracer->player->roundconditions.returntosender_mark = true; + inflictor->tracer->player->roundconditions.checkthisframe = true; + } + } + else if (!(inflictor && inflictor->player) + && player->laps <= numlaps + && damagetype != DMG_DEATHPIT) + { + const UINT8 requiredbit = 1<<(player->laps & 7); + if (!(player->roundconditions.hittrackhazard[player->laps/8] & requiredbit)) + { + player->roundconditions.hittrackhazard[player->laps/8] |= requiredbit; + player->roundconditions.checkthisframe = true; + } } // Instant-Death diff --git a/src/p_local.h b/src/p_local.h index 4cf3fbd7c..fccc0287f 100644 --- a/src/p_local.h +++ b/src/p_local.h @@ -32,9 +32,6 @@ extern "C" { //#define VIEWHEIGHTS "41" -// Maximum laps per map. -#define MAX_LAPS 99 - // Maximum player score. #define MAXSCORE 99999990 // 999999990 @@ -260,7 +257,6 @@ void P_RecalcPrecipInSector(sector_t *sector); void P_PrecipitationEffects(void); void P_RemoveMobj(mobj_t *th); -boolean P_MobjWasRemoved(const mobj_t *th); void P_RemoveSavegameMobj(mobj_t *th); boolean P_SetPlayerMobjState(mobj_t *mobj, statenum_t state); boolean P_SetMobjState(mobj_t *mobj, statenum_t state); @@ -547,6 +543,8 @@ void P_UpdateLastPickup(player_t *player, UINT8 type); boolean P_CanPickupEmblem(player_t *player, INT32 emblemID); boolean P_EmblemWasCollected(INT32 emblemID); +void P_TrackRoundConditionTargetDamage(targetdamaging_t targetdamaging); + // // P_SPEC // diff --git a/src/p_mobj.c b/src/p_mobj.c index 28e7790e5..e04cd2c5d 100644 --- a/src/p_mobj.c +++ b/src/p_mobj.c @@ -5339,6 +5339,10 @@ static boolean P_IsTrackerType(INT32 type) case MT_JAWZ: return true; + // Players need to be able to glance at the Ancient Shrines + case MT_ANCIENTSHRINE: + return true; + // Primarily for minimap data, handle with care case MT_SPB: case MT_BATTLECAPSULE: @@ -5349,6 +5353,7 @@ static boolean P_IsTrackerType(INT32 type) case MT_PLAYER: return true; + // HUD tracking case MT_OVERTIME_CENTER: case MT_MONITOR: case MT_EMERALD: @@ -7226,7 +7231,7 @@ static boolean P_MobjRegularThink(mobj_t *mobj) mobj->frame &= ~FF_TRANSMASK; mobj->renderflags &= ~RF_TRANSMASK; - if (P_EmblemWasCollected(mobj->health - 1) || !P_CanPickupEmblem(&players[consoleplayer], mobj->health - 1)) + if (P_EmblemWasCollected(mobj->health - 1) || !P_CanPickupEmblem(NULL, mobj->health - 1)) { trans = tr_trans50; } @@ -7251,6 +7256,64 @@ static boolean P_MobjRegularThink(mobj_t *mobj) break; } + case MT_ANCIENTSHRINE: + { + boolean docolorized = false; + + if (P_MobjWasRemoved(mobj->tracer) == false) + { + if (mobj->tracer->fuse == 1) + { + if (!(mapheaderinfo[gamemap-1]->records.mapvisited & MV_MYSTICMELODY)) + { + mapheaderinfo[gamemap-1]->records.mapvisited |= MV_MYSTICMELODY; + + if (!M_UpdateUnlockablesAndExtraEmblems(true, true)) + S_StartSound(NULL, sfx_ncitem); + gamedata->deferredsave = true; + } + } + + // Non-RNG-advancing equivalent of Obj_SpawnEmeraldSparks + if (leveltime % 3 == 0) + { + mobj_t *sparkle = P_SpawnMobjFromMobj( + mobj, + M_RandomRange(-48, 48) * FRACUNIT, + M_RandomRange(-48, 48) * FRACUNIT, + M_RandomRange(0, 64) * FRACUNIT, + MT_SPARK + ); + P_SetMobjState(sparkle, S_MORB1); + + sparkle->color = SKINCOLOR_PLAGUE; + sparkle->momz += 6 * mobj->scale * P_MobjFlip(mobj); + P_SetScale(sparkle, 2); + } + + docolorized = !!(leveltime & 1); + } + + if (mobj->colorized != docolorized) + { + if (docolorized) + { + mobj->colorized = true; + mobj->color = SKINCOLOR_PLAGUE; + mobj->spriteyoffset = 1; + } + else + { + mobj->colorized = false; + mobj->color = SKINCOLOR_NONE; + mobj->spriteyoffset = 0; + } + } + + mobj->frame = (mapheaderinfo[gamemap-1]->records.mapvisited & MV_MYSTICMELODY) ? 1 : 0; + + break; + } case MT_FLOATINGITEM: { P_ResetPitchRoll(mobj); @@ -7615,6 +7678,7 @@ static boolean P_MobjRegularThink(mobj_t *mobj) break; } case MT_EMERALD: + { Obj_EmeraldThink(mobj); if (P_MobjWasRemoved(mobj)) @@ -7622,6 +7686,94 @@ static boolean P_MobjRegularThink(mobj_t *mobj) return false; } break; + } + case MT_PRISONEGGDROP: + { + // If it gets any more complicated than this I'll make an objects/prisoneggdrop.c file, promise + // ~toast 121023 + + statenum_t teststate = S_NULL; + + if (mobj->flags2 & MF2_AMBUSH) + { + if (mobj->extravalue1 == 0 && P_IsObjectOnGround(mobj)) + { + if (P_CheckDeathPitCollide(mobj)) + { + P_RemoveMobj(mobj); + return false; + } + + mobj->momx = mobj->momy = 0; + mobj->flags2 |= MF2_STRONGBOX; + } + + teststate = (mobj->state-states); + } + else if (!netgame) + { + if (gamedata->thisprisoneggpickup_cached->type == UC_PRISONEGGCD) + { + teststate = S_PRISONEGGDROP_CD; + mobj->renderflags |= RF_SEMIBRIGHT; + } + + P_SetMobjStateNF(mobj, teststate); + + if (P_MobjWasRemoved(mobj)) + { + return false; + } + + S_StartSound(NULL, sfx_cdsprk); + + mobj->z += P_MobjFlip(mobj); + mobj->flags2 |= MF2_AMBUSH; + } + + if (teststate == S_PRISONEGGDROP_CD) + { + if (mobj->extravalue1) + { + ++mobj->extravalue1; + + INT32 trans = (mobj->extravalue1 * NUMTRANSMAPS) / (TICRATE); + if (trans >= NUMTRANSMAPS) + { + P_RemoveMobj(mobj); + return false; + } + + mobj->angle += ANGLE_MAX/(TICRATE/3); + mobj->renderflags = (mobj->renderflags & ~RF_TRANSMASK) | (trans << RF_TRANSSHIFT); + } + else + { + if (mobj->flags2 & MF2_STRONGBOX) + mobj->angle += ANGLE_MAX/TICRATE; + else + mobj->angle += ANGLE_MAX/(TICRATE/3); + + // Non-RNG-advancing equivalent of Obj_SpawnEmeraldSparks + if (leveltime % 3 == 0) + { + mobj_t *sparkle = P_SpawnMobjFromMobj( + mobj, + M_RandomRange(-48, 48) * FRACUNIT, + M_RandomRange(-48, 48) * FRACUNIT, + M_RandomRange(0, 64) * FRACUNIT, + MT_SPARK + ); + P_SetMobjStateNF(sparkle, mobjinfo[MT_EMERALDSPARK].spawnstate); + + sparkle->color = M_RandomChance(FRACUNIT/2) ? SKINCOLOR_ULTRAMARINE : SKINCOLOR_MAGENTA; + sparkle->momz += 8 * mobj->scale * P_MobjFlip(mobj); + } + } + } + + break; + } case MT_EMERALDFLARE: Obj_EmeraldFlareThink(mobj); @@ -11454,18 +11606,40 @@ void P_RemoveMobj(mobj_t *mobj) mobj->thinker.function.acp1 = (actionf_p1)P_MobjThinker; // needed for P_UnsetThingPosition, etc. to work. // Rings only, please! - if (mobj->spawnpoint && - (mobj->type == MT_RING - || mobj->type == MT_BLUESPHERE) - && !(mobj->flags2 & MF2_DONTRESPAWN)) + if (mobj->spawnpoint == NULL) + ; + else { - //CONS_Printf("added to queue at tic %d\n", leveltime); - itemrespawnque[iquehead] = mobj->spawnpoint; - itemrespawntime[iquehead] = leveltime; - iquehead = (iquehead+1)&(ITEMQUESIZE-1); - // lose one off the end? - if (iquehead == iquetail) - iquetail = (iquetail+1)&(ITEMQUESIZE-1); + if ((mobj->type == MT_RING + || mobj->type == MT_BLUESPHERE) + && !(mobj->flags2 & MF2_DONTRESPAWN)) + { + //CONS_Printf("added to queue at tic %d\n", leveltime); + itemrespawnque[iquehead] = mobj->spawnpoint; + itemrespawntime[iquehead] = leveltime; + iquehead = (iquehead+1)&(ITEMQUESIZE-1); + // lose one off the end? + if (iquehead == iquetail) + iquetail = (iquetail+1)&(ITEMQUESIZE-1); + } + + if (numchallengedestructibles && numchallengedestructibles != UINT16_MAX) + { + UINT8 i; + for (i = 0; i < mapheaderinfo[gamemap-1]->destroyforchallenge_size; i++) + { + if (mobj->type != mapheaderinfo[gamemap-1]->destroyforchallenge[i]) + continue; + + if ((--numchallengedestructibles) == 0) + { + numchallengedestructibles = UINT16_MAX; + gamedata->deferredconditioncheck = true; + } + + break; + } + } } if (P_IsTrackerType(mobj->type)) @@ -13209,6 +13383,22 @@ static boolean P_SetupSpawnedMapThing(mapthing_t *mthing, mobj_t *mobj) nummapspraycans++; break; } + case MT_ANCIENTSHRINE: + { + angle_t remainderangle = (mobj->angle % ANGLE_90); + + if (remainderangle) + { + // Always lock to 90 degree grid. + if (remainderangle > ANGLE_45) + mobj->angle += ANGLE_90; + mobj->angle -= remainderangle; + } + + P_SetScale(mobj, mobj->destscale = 2*mobj->scale); + + break; + } case MT_SKYBOX: { P_InitSkyboxPoint(mobj, mthing); @@ -13988,12 +14178,12 @@ static boolean P_SetupSpawnedMapThing(mapthing_t *mthing, mobj_t *mobj) return true; } -static mobj_t *P_SpawnMobjFromMapThing(mapthing_t *mthing, fixed_t x, fixed_t y, fixed_t z, mobjtype_t i) +static mobj_t *P_SpawnMobjFromMapThing(mapthing_t *mthing, fixed_t x, fixed_t y, fixed_t z, mobjtype_t type) { mobj_t *mobj = NULL; size_t arg = SIZE_MAX; - mobj = P_SpawnMobj(x, y, z, i); + mobj = P_SpawnMobj(x, y, z, type); mobj->spawnpoint = mthing; mobj->angle = FixedAngle(mthing->angle << FRACBITS); @@ -14077,6 +14267,19 @@ static mobj_t *P_SpawnMobjFromMapThing(mapthing_t *mthing, fixed_t x, fixed_t y, mobj->flags2 |= MF2_OBJECTFLIP; } + if (mapheaderinfo[gamemap-1]->destroyforchallenge_size && numchallengedestructibles != UINT16_MAX) + { + UINT8 i; + for (i = 0; i < mapheaderinfo[gamemap-1]->destroyforchallenge_size; i++) + { + if (type != mapheaderinfo[gamemap-1]->destroyforchallenge[i]) + continue; + + numchallengedestructibles++; + break; + } + } + return mobj; } diff --git a/src/p_mobj.h b/src/p_mobj.h index 3c20f5d8d..382bfcd9e 100644 --- a/src/p_mobj.h +++ b/src/p_mobj.h @@ -510,6 +510,9 @@ struct precipmobj_t tic_t lastThink; }; +// It's extremely important that all mobj_t*-reading code have access to this. +boolean P_MobjWasRemoved(const mobj_t *th); + struct actioncache_t { actioncache_t *next; diff --git a/src/p_saveg.c b/src/p_saveg.c index 4a8fd3027..86baa506e 100644 --- a/src/p_saveg.c +++ b/src/p_saveg.c @@ -2696,7 +2696,7 @@ static UINT32 SaveSlope(const pslope_t *slope) return 0xFFFFFFFF; } -static boolean TypeIsNetSynced(mobjtype_t type) +boolean TypeIsNetSynced(mobjtype_t type) { // Ignore stationary hoops - these will be respawned from mapthings. if (type == MT_HOOP) @@ -2718,6 +2718,10 @@ static boolean TypeIsNetSynced(mobjtype_t type) if (type == MT_HORNCODE) return false; + // MT_PRISONEGGDROP: Yeah these are completely local + if (type == MT_PRISONEGGDROP) + return false; + return true; } @@ -5738,7 +5742,7 @@ static inline void P_ArchiveMisc(savebuffer_t *save) WRITEUINT32(save->p, grandprixinfo.rank.laps); WRITEUINT32(save->p, grandprixinfo.rank.totalLaps); - WRITEUINT32(save->p, grandprixinfo.rank.continuesUsed); + WRITEUINT32(save->p, (grandprixinfo.rank.continuesUsed + 1)); WRITEUINT32(save->p, grandprixinfo.rank.prisons); WRITEUINT32(save->p, grandprixinfo.rank.totalPrisons); @@ -6064,6 +6068,8 @@ static void P_NetArchiveMisc(savebuffer_t *save, boolean resending) WRITEUINT32(save->p, darktimer); WRITEFIXED(save->p, darkness); + WRITEUINT16(save->p, numchallengedestructibles); + // Is it paused? if (paused) WRITEUINT8(save->p, 0x2f); @@ -6246,6 +6252,8 @@ static boolean P_NetUnArchiveMisc(savebuffer_t *save, boolean reloading) darktimer = READUINT32(save->p); darkness = READFIXED(save->p); + numchallengedestructibles = READUINT16(save->p); + // Is it paused? if (READUINT8(save->p) == 0x2f) paused = true; diff --git a/src/p_saveg.h b/src/p_saveg.h index 66345367d..8657ee9d2 100644 --- a/src/p_saveg.h +++ b/src/p_saveg.h @@ -92,6 +92,8 @@ boolean P_SaveBufferFromFile(savebuffer_t *save, char const *name); void P_SaveBufferFree(savebuffer_t *save); size_t P_SaveBufferRemaining(const savebuffer_t *save); +boolean TypeIsNetSynced(mobjtype_t type); + #ifdef __cplusplus } // extern "C" #endif diff --git a/src/p_setup.c b/src/p_setup.c index 307d6969a..e7759aa1b 100644 --- a/src/p_setup.c +++ b/src/p_setup.c @@ -463,8 +463,12 @@ static void P_ClearSingleMapHeaderInfo(INT16 num) mapheaderinfo[num]->justPlayed = 0; mapheaderinfo[num]->anger = 0; + mapheaderinfo[num]->destroyforchallenge_size = 0; + mapheaderinfo[num]->cache_spraycan = UINT16_MAX; + mapheaderinfo[num]->cache_maplock = MAXUNLOCKABLES; + mapheaderinfo[num]->customopts = NULL; mapheaderinfo[num]->numCustomOptions = 0; } @@ -7551,9 +7555,7 @@ static void P_InitLevelSettings(void) battleprisons = false; nummapspraycans = 0; - - // emerald hunt - hunt1 = hunt2 = hunt3 = NULL; + numchallengedestructibles = 0; // circuit, race and competition stuff numcheatchecks = 0; @@ -8057,10 +8059,32 @@ static void P_InitMinimapInfo(void) void P_ResetLevelMusic(void) { + mapmusrng = 0; + if (mapheaderinfo[gamemap-1]->musname_size > 1) - mapmusrng = P_RandomKey(PR_MUSICSELECT, mapheaderinfo[gamemap-1]->musname_size); - else - mapmusrng = 0; + { + UINT8 tempmapmus[MAXMUSNAMES], tempmapmus_size = 1, i; + + tempmapmus[0] = 0; + + for (i = 1; i < mapheaderinfo[gamemap-1]->musname_size; i++) + { + if (mapheaderinfo[gamemap-1]->cache_muslock[i-1] < MAXUNLOCKABLES + && !M_CheckNetUnlockByID(mapheaderinfo[gamemap-1]->cache_muslock[i-1])) + continue; + + //CONS_Printf("TEST - %u\n", i); + + tempmapmus[tempmapmus_size++] = i; + } + + if (tempmapmus_size > 1) + { + mapmusrng = P_RandomKey(PR_MUSICSELECT, tempmapmus_size); + //CONS_Printf("Rolled position %u, maps to %u\n", mapmusrng, tempmapmus[mapmusrng]); + mapmusrng = tempmapmus[mapmusrng]; + } + } } void P_LoadLevelMusic(void) diff --git a/src/p_user.c b/src/p_user.c index 17f04c76f..7126afb9a 100644 --- a/src/p_user.c +++ b/src/p_user.c @@ -519,7 +519,9 @@ INT32 P_GivePlayerRings(player_t *player, INT32 num_rings) player->rings += num_rings; - if (player->roundconditions.debt_rings == false && player->rings < 0) + if (player->roundconditions.debt_rings == false + && !(player->exiting || (player->pflags & PF_NOCONTEST)) + && player->rings < 0) { player->roundconditions.debt_rings = true; player->roundconditions.checkthisframe = true; @@ -1920,6 +1922,19 @@ static void P_3dMovement(player_t *player) // Calculates player's speed based on distance-of-a-line formula player->speed = R_PointToDist2(0, 0, player->rmomx, player->rmomy); + const fixed_t topspeedometer = K_GetKartSpeed(player, false, true); + + if (player->speed > topspeedometer) + { + const fixed_t convSpeed = (player->speed * 100) / topspeedometer; + + if (convSpeed > player->roundconditions.maxspeed) + { + player->roundconditions.maxspeed = convSpeed; + //player->roundconditions.checkthisframe = true; -- no, safe to leave until lapchange at worst + } + } + // Monster Iestyn - 04-11-13 // Quadrants are stupid, excessive and broken, let's do this a much simpler way! // Get delta angle from rmom angle and player angle first diff --git a/src/r_skins.c b/src/r_skins.c index 938f963f5..f67368daa 100644 --- a/src/r_skins.c +++ b/src/r_skins.c @@ -186,13 +186,13 @@ void R_InitSkins(void) M_UpdateConditionSetsPending(); } -UINT8 *R_GetSkinAvailabilities(boolean demolock, boolean forbots) +UINT8 *R_GetSkinAvailabilities(boolean demolock, INT32 botforcecharacter) { UINT16 i; UINT8 shif, byte; INT32 skinid; static UINT8 responsebuffer[MAXAVAILABILITY]; - UINT8 defaultbotskin = R_BotDefaultSkin(); + const boolean forbots = (botforcecharacter != -1); memset(&responsebuffer, 0, sizeof(responsebuffer)); @@ -207,7 +207,7 @@ UINT8 *R_GetSkinAvailabilities(boolean demolock, boolean forbots) continue; if ((forbots - ? (M_CheckNetUnlockByID(i) || skinid == defaultbotskin) // Assert the host's lock. + ? (M_CheckNetUnlockByID(i) || skinid == botforcecharacter) // Assert the host's lock. : gamedata->unlocked[i]) // Assert the local lock. != true && !demolock) continue; @@ -452,7 +452,7 @@ void SetPlayerSkinByNum(INT32 playernum, INT32 skinnum) if (P_IsLocalPlayer(player)) CONS_Alert(CONS_WARNING, M_GetText("Requested skin %d not found\n"), skinnum); - else if(server || IsPlayerAdmin(consoleplayer)) + else if (server || IsPlayerAdmin(consoleplayer)) CONS_Alert(CONS_WARNING, "Player %d (%s) skin %d not found\n", playernum, player_names[playernum], skinnum); SetSkin(player, GetPlayerDefaultSkin(playernum)); // not found put the eggman skin diff --git a/src/r_skins.h b/src/r_skins.h index 9a1c45ce6..89f1c2b91 100644 --- a/src/r_skins.h +++ b/src/r_skins.h @@ -113,7 +113,7 @@ void R_PatchSkins(UINT16 wadnum, boolean mainfile); // Access INT32 R_SkinAvailable(const char *name); boolean R_SkinUsable(INT32 playernum, INT32 skinnum, boolean demoskins); -UINT8 *R_GetSkinAvailabilities(boolean demolock, boolean forbots); +UINT8 *R_GetSkinAvailabilities(boolean demolock, INT32 botforcecharacter); // Setting void SetPlayerSkin(INT32 playernum,const char *skinname); diff --git a/src/s_sound.c b/src/s_sound.c index 759674f1a..c0bec85fc 100644 --- a/src/s_sound.c +++ b/src/s_sound.c @@ -1224,7 +1224,7 @@ musicdef_t *musicdefstart = NULL; struct cursongcredit cursongcredit; // Currently displayed song credit info struct soundtest soundtest = {.tune = ""}; // Sound Test (sound test) -static void S_InsertMusicAtSoundTestSequenceTail(const char *musname, UINT16 map, musicdef_t ***tail) +static void S_InsertMusicAtSoundTestSequenceTail(const char *musname, UINT16 map, UINT8 altref, musicdef_t ***tail) { UINT8 i = 0; musicdef_t *def = S_FindMusicDef(musname, &i); @@ -1237,6 +1237,7 @@ static void S_InsertMusicAtSoundTestSequenceTail(const char *musname, UINT16 map def->sequence.id = soundtest.sequence.id; def->sequence.map = map; + def->sequence.altref = altref; // So what we're doing here is to avoid iterating // for every insertion, we dereference the pointer @@ -1255,17 +1256,17 @@ static void S_InsertMapIntoSoundTestSequence(UINT16 map, musicdef_t ***tail) if (mapheaderinfo[map]->positionmus[0]) { - S_InsertMusicAtSoundTestSequenceTail(mapheaderinfo[map]->positionmus, map, tail); + S_InsertMusicAtSoundTestSequenceTail(mapheaderinfo[map]->positionmus, map, 0, tail); } for (i = 0; i < mapheaderinfo[map]->musname_size; i++) { - S_InsertMusicAtSoundTestSequenceTail(mapheaderinfo[map]->musname[i], map, tail); + S_InsertMusicAtSoundTestSequenceTail(mapheaderinfo[map]->musname[i], map, i, tail); } for (i = 0; i < mapheaderinfo[map]->associatedmus_size; i++) { - S_InsertMusicAtSoundTestSequenceTail(mapheaderinfo[map]->associatedmus[i], map, tail); + S_InsertMusicAtSoundTestSequenceTail(mapheaderinfo[map]->associatedmus[i], map, ALTREF_REQUIRESBEATEN, tail); } } @@ -1352,6 +1353,7 @@ void S_PopulateSoundTestSequence(void) def->sequence.id = soundtest.sequence.id; def->sequence.map = NEXTMAP_INVALID; + def->sequence.altref = 0; def->sequence.next = soundtest.sequence.next; soundtest.sequence.next = def; @@ -1369,6 +1371,7 @@ void S_PopulateSoundTestSequence(void) def->sequence.id = soundtest.sequence.id; def->sequence.map = NEXTMAP_INVALID; + def->sequence.altref = 0; def->sequence.next = *tail; *tail = def; @@ -1380,14 +1383,27 @@ static boolean S_SoundTestDefLocked(musicdef_t *def) { // temporary - i'd like to find a way to conditionally hide // specific musicdefs that don't have any map associated. - if (def->sequence.map >= nummapheaders) + if (def->sequence.map >= nummapheaders || !mapheaderinfo[def->sequence.map]) return false; + mapheader_t *header = mapheaderinfo[def->sequence.map]; + // Is the level tied to SP progression? - if ((mapheaderinfo[def->sequence.map]->menuflags & (LF2_FINISHNEEDED|LF2_HIDEINMENU)) - && !(mapheaderinfo[def->sequence.map]->records.mapvisited & MV_BEATEN)) + if (( + (header->menuflags & (LF2_FINISHNEEDED|LF2_HIDEINMENU)) + || (def->sequence.altref == ALTREF_REQUIRESBEATEN) // Associated music only when completed + ) + && !(header->records.mapvisited & MV_BEATEN)) return true; + if (def->sequence.altref != 0 && def->sequence.altref < header->musname_size) + { + // Alt music requires unlocking the alt + if ((header->cache_muslock[def->sequence.altref - 1] < MAXUNLOCKABLES) + && gamedata->unlocked[header->cache_muslock[def->sequence.altref - 1]] == false) + return true; + } + // Finally, do a full-fat map check. return M_MapLocked(def->sequence.map+1); } diff --git a/src/s_sound.h b/src/s_sound.h index 7840596ad..bb20f8994 100644 --- a/src/s_sound.h +++ b/src/s_sound.h @@ -144,11 +144,13 @@ boolean S_MusicNotInFocus(void); #define MAXDEFTRACKS 3 +#define ALTREF_REQUIRESBEATEN UINT8_MAX struct soundtestsequence_t { UINT8 id; UINT16 map; + UINT8 altref; musicdef_t *next; size_t shuffleinfo; diff --git a/src/sounds.c b/src/sounds.c index ebcda4cf9..54003ce27 100644 --- a/src/sounds.c +++ b/src/sounds.c @@ -1196,6 +1196,8 @@ sfxinfo_t S_sfx[NUMSFX] = {"wchrg2", false, 64, 64, -1, NULL, 0, -1, -1, LUMPERROR, ""}, // SF_X2AWAYSOUND {"horn00", false, 255, 0, -1, NULL, 0, -1, -1, LUMPERROR, "/"}, // HORNCODE + {"melody", false, 255, 0, -1, NULL, 0, -1, -1, LUMPERROR, "/"}, // Mystic Melody + {"cdsprk", false, 255, 0, -1, NULL, 0, -1, -1, LUMPERROR, "/"}, // Prison Egg CD sparkling {"monch", false, 255, 0, -1, NULL, 0, -1, -1, LUMPERROR, ""}, {"etexpl", false, 255, 0, -1, NULL, 0, -1, -1, LUMPERROR, "Game crash"}, diff --git a/src/sounds.h b/src/sounds.h index a3329cf90..76960c87b 100644 --- a/src/sounds.h +++ b/src/sounds.h @@ -1265,6 +1265,8 @@ typedef enum sfx_wchrg2, sfx_horn00, + sfx_melody, + sfx_cdsprk, sfx_monch, sfx_etexpl,