diff --git a/data/.wolf3d/N3Ddata.cdogscpn/pickups.json b/data/.wolf3d/N3Ddata.cdogscpn/pickups.json index 09d2d6b4f..4cf96717b 100644 --- a/data/.wolf3d/N3Ddata.cdogscpn/pickups.json +++ b/data/.wolf3d/N3Ddata.cdogscpn/pickups.json @@ -101,9 +101,9 @@ "Effects": [{ "Type": "Menu", "Menu": { - "Text": "What is 1+1?", + "Text": "This pickup is a template only; the first Item is the correct effects, the second the wrong effects.", "Items": [{ - "Text": "2", + "Text": "correct", "Effects": [{ "Type": "Score", "Score": 1000 @@ -119,7 +119,7 @@ "Sound": "bonus" }] }, { - "Text": "11", + "Text": "wrong", "Effects": [{ "Type": "Health", "Health": 4 diff --git a/src/cdogs/actors.c b/src/cdogs/actors.c index 2cd29cd74..e306929cf 100644 --- a/src/cdogs/actors.c +++ b/src/cdogs/actors.c @@ -1064,22 +1064,27 @@ int CommandActor(TActor *actor, int cmd, int ticks) if (Button1(cmd) && !Button1(actor->lastCmd)) { // Apply effects of pickup menu item and reset - const PickupMenuItem *m = CArrayGet( - &actor->pickupMenu.effect->u.Menu.Items, - actor->pickupMenu.index); - bool canPickup = false; - const char *sound = "menu_enter"; - CA_FOREACH(const PickupEffect, pe, m->Effects) - CASSERT(pe->Type != PICKUP_MENU, "can't have nested menu effects"); - canPickup = PickupApplyEffect( - actor, actor->pickupMenu.pickup, pe, true, &sound); - CA_FOREACH_END() - if (canPickup && sound != NULL) + if (actor->pickupMenu.index < + actor->pickupMenu.effect->u.Menu.Items.size) { - e = GameEventNew(GAME_EVENT_SOUND_AT); - strcpy(e.u.SoundAt.Sound, sound); - e.u.SoundAt.Pos = Vec2ToNet(actor->thing.Pos); - GameEventsEnqueue(&gGameEvents, e); + const PickupMenuItem *m = CArrayGet( + &actor->pickupMenu.effect->u.Menu.Items, + actor->pickupMenu.index); + bool canPickup = false; + const char *sound = "menu_enter"; + CA_FOREACH(const PickupEffect, pe, m->Effects) + CASSERT( + pe->Type != PICKUP_MENU, "can't have nested menu effects"); + canPickup = PickupApplyEffect( + actor, actor->pickupMenu.pickup, pe, true, &sound); + CA_FOREACH_END() + if (canPickup && sound != NULL) + { + e = GameEventNew(GAME_EVENT_SOUND_AT); + strcpy(e.u.SoundAt.Sound, sound); + e.u.SoundAt.Pos = Vec2ToNet(actor->thing.Pos); + GameEventsEnqueue(&gGameEvents, e); + } } actor->pickupMenu.pickup = NULL; actor->pickupMenu.effect = NULL; diff --git a/src/cdogs/cwolfmap/common.h b/src/cdogs/cwolfmap/common.h index 7b24c955e..999519a61 100644 --- a/src/cdogs/cwolfmap/common.h +++ b/src/cdogs/cwolfmap/common.h @@ -1,7 +1,7 @@ #pragma once +#include "wad/wad.h" #include #include -#include "wad/wad.h" typedef enum { @@ -30,3 +30,12 @@ typedef struct char *data; wad_t *wad; } CWAudio; + +typedef struct +{ + char *question; + char **answers; + int nAnswers; + // Assumes one correct answer + int correctIdx; +} CWN3DQuiz; diff --git a/src/cdogs/cwolfmap/cwolfmap.c b/src/cdogs/cwolfmap/cwolfmap.c index 83d06e453..f9cd0c4e7 100644 --- a/src/cdogs/cwolfmap/cwolfmap.c +++ b/src/cdogs/cwolfmap/cwolfmap.c @@ -166,6 +166,39 @@ int CWLoad(CWolfMap *map, const char *path, const int spearMission) map->levels[i].description = CWLevelN3DLoadDescription(languageBuf, i); } + + for (int q = 1;; q++) + { + char *question = CWLevelN3DLoadQuizQuestion(languageBuf, q); + if (question == NULL) + { + break; + } + map->nQuizzes++; + map->quizzes = + realloc(map->quizzes, map->nQuizzes * sizeof(CWN3DQuiz)); + CWN3DQuiz *quiz = &map->quizzes[map->nQuizzes - 1]; + memset(quiz, 0, sizeof *quiz); + quiz->question = question; + for (char a = 'A';; a++) + { + bool correct = false; + char *answer = + CWLevelN3DLoadQuizAnswer(languageBuf, q, a, &correct); + if (answer == NULL) + { + break; + } + quiz->nAnswers++; + quiz->answers = + realloc(quiz->answers, quiz->nAnswers * sizeof(char *)); + quiz->answers[quiz->nAnswers - 1] = answer; + if (correct) + { + quiz->correctIdx = quiz->nAnswers - 1; + } + } + } free(languageBuf); } @@ -370,6 +403,7 @@ void CWCopy(CWolfMap *dst, const CWolfMap *src) dst->vswap.sounds = malloc(vswapSoundsLen); memcpy(dst->vswap.sounds, src->vswap.sounds, vswapSoundsLen); // TODO: copy wad + // TODO: copy quizzes } void CWFree(CWolfMap *map) @@ -381,6 +415,10 @@ void CWFree(CWolfMap *map) LevelsFree(map); CWAudioFree(&map->audio); CWVSwapFree(&map->vswap); + for (int i = 0; i < map->nQuizzes; i++) + { + CWN3DQuizFree(&map->quizzes[i]); + } memset(map, 0, sizeof *map); } static void LevelFree(CWLevel *level); diff --git a/src/cdogs/cwolfmap/cwolfmap.h b/src/cdogs/cwolfmap/cwolfmap.h index 3030d9ca3..4d149b1ab 100644 --- a/src/cdogs/cwolfmap/cwolfmap.h +++ b/src/cdogs/cwolfmap/cwolfmap.h @@ -49,6 +49,8 @@ typedef struct CWAudio audio; CWVSwap vswap; CWMapType type; + CWN3DQuiz *quizzes; + int nQuizzes; } CWolfMap; CWMapType CWGetType( diff --git a/src/cdogs/cwolfmap/n3d.c b/src/cdogs/cwolfmap/n3d.c index 5d1af5024..a084c121b 100644 --- a/src/cdogs/cwolfmap/n3d.c +++ b/src/cdogs/cwolfmap/n3d.c @@ -27,7 +27,8 @@ char *CWN3DLoadLanguageEnu(const char *path) return buf; } -static char *LoadLanguageEnuString(const char *buf, const char *key) +static char *LoadLanguageEnuString( + const char *buf, const char *key, char **comment) { const char *start = buf; char linebuf[1024]; @@ -51,7 +52,15 @@ static char *LoadLanguageEnuString(const char *buf, const char *key) if (strcmp(linebuf, key) == 0) { char *value = equals + strlen(" = \""); - char *endQuote = linebuf + len - strlen("\";"); + char *endQuote = strstr(value, "\";"); + if (comment) + { + const char *commentStart = strstr(endQuote, "// "); + if (commentStart) + { + *comment = strdup(commentStart + strlen("// ")); + } + } *endQuote = '\0'; // Unescape newlines in the string char *dst = value; @@ -87,17 +96,41 @@ char *CWLevelN3DLoadDescription(const char *buf, const int level) switch (level) { case 0: - return LoadLanguageEnuString(buf, "NOAH_BRIEF_01"); + return LoadLanguageEnuString(buf, "NOAH_BRIEF_01", NULL); case 3: - return LoadLanguageEnuString(buf, "NOAH_BRIEF_02"); + return LoadLanguageEnuString(buf, "NOAH_BRIEF_02", NULL); case 7: - return LoadLanguageEnuString(buf, "NOAH_BRIEF_03"); + return LoadLanguageEnuString(buf, "NOAH_BRIEF_03", NULL); case 12: - return LoadLanguageEnuString(buf, "NOAH_BRIEF_04"); + return LoadLanguageEnuString(buf, "NOAH_BRIEF_04", NULL); case 17: - return LoadLanguageEnuString(buf, "NOAH_BRIEF_05"); + return LoadLanguageEnuString(buf, "NOAH_BRIEF_05", NULL); case 23: - return LoadLanguageEnuString(buf, "NOAH_BRIEF_06"); + return LoadLanguageEnuString(buf, "NOAH_BRIEF_06", NULL); } return NULL; } + +char *CWLevelN3DLoadQuizQuestion(const char *buf, const int quiz) +{ + char key[256]; + sprintf(key, "NOAH_QUIZ_Q%02d", quiz); + return LoadLanguageEnuString(buf, key, NULL); +} +char *CWLevelN3DLoadQuizAnswer( + const char *buf, const int quiz, const char answer, bool *correct) +{ + char key[256]; + sprintf(key, "NOAH_QUIZ_A%02d%c", quiz, answer); + char *comment = NULL; + char *result = LoadLanguageEnuString(buf, key, &comment); + *correct = comment && strcmp(comment, "Correct") == 0; + free(comment); + return result; +} + +void CWN3DQuizFree(CWN3DQuiz *quiz) +{ + free(quiz->question); + free(quiz->answers); +} diff --git a/src/cdogs/cwolfmap/n3d.h b/src/cdogs/cwolfmap/n3d.h index a64624813..06b09a95b 100644 --- a/src/cdogs/cwolfmap/n3d.h +++ b/src/cdogs/cwolfmap/n3d.h @@ -1,7 +1,19 @@ #pragma once +#include + +#include "common.h" + // Load the noah3d.pak/language.enu file to char buffer, containing briefs and // quizzes char *CWN3DLoadLanguageEnu(const char *path); char *CWLevelN3DLoadDescription(const char *buf, const int level); + +// Load a quiz question (quiz starts from #1) +char *CWLevelN3DLoadQuizQuestion(const char *buf, const int quiz); +// Load a quiz answer (quiz starts from #1, answer starts from 'A') +char *CWLevelN3DLoadQuizAnswer( + const char *buf, const int quiz, const char answer, bool *correct); + +void CWN3DQuizFree(CWN3DQuiz *quiz); \ No newline at end of file diff --git a/src/cdogs/map_wolf.c b/src/cdogs/map_wolf.c index 2453d2772..48a0afc2b 100644 --- a/src/cdogs/map_wolf.c +++ b/src/cdogs/map_wolf.c @@ -654,6 +654,7 @@ int MapWolfScan( } static void LoadSounds(const SoundDevice *s, const CWolfMap *map); +static void LoadN3DScrolls(const CWolfMap *map); static void LoadMission( CampaignSetting *c, const map_t tileClasses, CWolfMap *map, const int spearMission, const int missionIndex, const int numMissions); @@ -783,6 +784,12 @@ int MapWolfLoad( CharacterStoreCopy(&c->characters, &cs, &gPlayerTemplates.CustomClasses); + // Special case for N3D: generate scrolls with unique questions/answers + if (map->type == CWMAPTYPE_N3D && map->nQuizzes > 0) + { + LoadN3DScrolls(map); + } + for (int i = 0; i < map->nLevels; i++) { LoadMission(c, tileClasses, map, spearMission, i, numMissions); @@ -937,6 +944,54 @@ static void AddRandomSound( } } +static void LoadN3DScrolls(const CWolfMap *map) +{ + // Copy the effects from the "scroll" pickup + const PickupClass *scroll = StrPickupClass("scroll"); + const PickupEffect *menuEffect = CArrayGet(&scroll->Effects, 0); + const CArray *correctEffects = + &((const PickupMenuItem *)CArrayGet(&menuEffect->u.Menu.Items, 0)) + ->Effects; + const CArray *wrongEffects = + &((const PickupMenuItem *)CArrayGet(&menuEffect->u.Menu.Items, 1)) + ->Effects; + for (int i = 0; i < map->nQuizzes; i++) + { + PickupClass c; + PickupClassInit(&c); + char buf[256]; + sprintf(buf, "scroll%d", i); + CSTRDUP(c.Name, buf); + CPicCopyPic(&c.Pic, &scroll->Pic); + CSTRDUP(c.Sound, scroll->Sound); + + PickupEffect e; + memset(&e, 0, sizeof e); + e.Type = PICKUP_MENU; + const CWN3DQuiz *quiz = &map->quizzes[i]; + CSTRDUP(e.u.Menu.Text, quiz->question); + + CArrayInit(&e.u.Menu.Items, sizeof(PickupMenuItem)); + for (int j = 0; j < quiz->nAnswers; j++) + { + PickupMenuItem m; + PickupMenuItemInit(&m); + CSTRDUP(m.Text, quiz->answers[j]); + const CArray *effects = + j == quiz->correctIdx ? correctEffects : wrongEffects; + CA_FOREACH(const PickupEffect, pe, *effects) + PickupEffect ec = PickupEffectCopy(pe); + CArrayPushBack(&m.Effects, &ec); + CA_FOREACH_END() + CArrayPushBack(&e.u.Menu.Items, &m); + } + + CArrayPushBack(&c.Effects, &e); + + CArrayPushBack(&gPickupClasses.CustomClasses, &c); + } +} + static void LoadTile( MissionStatic *m, const uint16_t ch, const CWolfMap *map, const struct vec2i v, const int missionIndex); @@ -2285,8 +2340,9 @@ static void LoadEntity( switch (map->type) { case CWMAPTYPE_N3D: + // TODO: Place a random scroll pickup, use shuffling MissionStaticTryAddPickup( - &m->u.Static, StrPickupClass("scroll"), v); + &m->u.Static, StrPickupClass("scroll0"), v); break; default: MissionStaticTryAddItem( diff --git a/src/cdogs/pickup_class.c b/src/cdogs/pickup_class.c index 8fb619ff9..6a48cce46 100644 --- a/src/cdogs/pickup_class.c +++ b/src/cdogs/pickup_class.c @@ -172,11 +172,34 @@ int StrPickupClassId(const char *s) #define VERSION 3 -static void PickupClassInit(PickupClass *c) +void PickupClassInit(PickupClass *c) { memset(c, 0, sizeof *c); CArrayInit(&c->Effects, sizeof(PickupEffect)); } +void PickupMenuItemInit(PickupMenuItem *m) +{ + memset(m, 0, sizeof *m); + CArrayInit(&m->Effects, sizeof(PickupEffect)); +} +PickupEffect PickupEffectCopy(const PickupEffect *e) +{ + PickupEffect out; + memcpy(&out, e, sizeof out); + switch (e->Type) + { + case PICKUP_SOUND: + CSTRDUP(out.u.Sound, e->u.Sound); + break; + case PICKUP_MENU: + CASSERT(false, "copying pickup menu not implemented"); + break; + default: + // Do nothing + break; + } + return out; +} static void PickupEffectTerminate(PickupEffect *e); static void PickupMenuItemTerminate(PickupMenuItem *m) { @@ -388,8 +411,7 @@ static PickupEffect LoadPickupEffect(json_t *node, const int version) for (json_t *item = itemsNode->child; item; item = item->next) { PickupMenuItem m; - memset(&m, 0, sizeof m); - CArrayInit(&m.Effects, sizeof(PickupEffect)); + PickupMenuItemInit(&m); LoadStr(&m.Text, item, "Text"); json_t *effectsNode = json_find_first_label(item, "Effects")->child; diff --git a/src/cdogs/pickup_class.h b/src/cdogs/pickup_class.h index dc38fc59b..38bbc690a 100644 --- a/src/cdogs/pickup_class.h +++ b/src/cdogs/pickup_class.h @@ -107,6 +107,10 @@ PickupClass *KeyPickupClass(const char *style, const int i); PickupClass *PickupClassGetById(PickupClasses *classes, const int id); int StrPickupClassId(const char *s); +void PickupMenuItemInit(PickupMenuItem *m); +PickupEffect PickupEffectCopy(const PickupEffect *e); +void PickupClassInit(PickupClass *c); + void PickupClassesInit( PickupClasses *classes, const char *filename, const AmmoClasses *ammo, const WeaponClasses *guns);