/* Copyright (C) 2018-2019 Parallel Realities This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ #include "hub.h" static void unlockAllMissions(void); static void unlockMission(char *id); static int requiredMissionUnlocked(char *id); static void loadMissions(void); static HubMission *getMissionAt(int x, int y); static void drawMissions(void); static void drawInfoBar(void); static void drawMissionInfo(void); static void drawPlusSettings(void); static void logic(void); static void draw(void); static void startMission(void); static void cancel(void); static void options(void); static void stats(void); static void trophies(void); static void quit(void); static void returnFromTrophyStats(void); static void doCursor(void); static void doMissionSelect(void); static void doMissionInfo(void); static void drawHudWidgets(void); static void awardMissionTrophies(void); static void returnFromOptions(void); void destroyHub(void); static void startMissionPlus(void); static HubMission hubMissionHead; static HubMission *hubMissionTail; static HubMission *selectedMission; static Atlas *worldMap; static Atlas *alert; static Texture *clouds; static Sprite *cursorSpr; static Sprite *keySprites[MAX_KEY_TYPES]; static Texture *atlasTexture; static int unlockedMissions; static PointF cursor; static float blipSize; static float blipValue; static int showing; static int doPlusSettings; static PointF cloudPos; void initHub(void) { int unlockTeeka, i; HubMission *mission, *teeka; Tuple *t; startSectionTransition(); memset(&hubMissionHead, 0, sizeof(HubMission)); hubMissionTail = &hubMissionHead; memset(&keySprites, 0, sizeof(Sprite*) * MAX_KEY_TYPES); selectedMission = NULL; loadMusic("music/61321__mansardian__news-background.ogg"); atlasTexture = getTexture("gfx/atlas/atlas.png"); worldMap = getImageFromAtlas("gfx/hub/worldMap.jpg"); alert = getImageFromAtlas("gfx/hub/alert.png"); clouds = getTexture("gfx/hub/clouds.png"); cursorSpr = getSprite("Cursor"); for (i = 0 ; i < MAX_KEY_TYPES ; i++) { if (game.keys[i].value.i > 0) { keySprites[i] = getSprite(game.keys[i].key); } } cursor.x = app.config.winWidth / 2; cursor.y = app.config.winHeight / 2; getWidget("startMission", "mission")->action = startMission; getWidget("cancel", "mission")->action = cancel; getWidget("options", "hub")->action = options; getWidget("stats", "hub")->action = stats; getWidget("trophies", "hub")->action = trophies; getWidget("quit", "hub")->action = quit; getWidget("ok", "stats")->action = returnFromTrophyStats; getWidget("ok", "trophies")->action = returnFromTrophyStats; getWidget("startMission", "missionPlus")->action = startMissionPlus; getWidget("cancel", "missionPlus")->action = cancel; loadMissions(); if (dev.cheatLevels) { unlockAllMissions(); } game.totalMissions = 0; unlockedMissions = 0; game.stats[STAT_MISSIONS_COMPLETE] = 0; unlockTeeka = 1; blipValue = 0; doPlusSettings = 0; showing = SHOW_NONE; cursor.x = app.config.winWidth / 2; cursor.y = app.config.winHeight / 2; SDL_WarpMouseInWindow(app.window, cursor.x, cursor.y); game.isComplete = 0; for (t = game.missionStatusHead.next ; t != NULL ; t = t->next) { if (t->value.i != MS_INCOMPLETE) { unlockedMissions++; } if (t->value.i == MS_COMPLETE) { game.stats[STAT_MISSIONS_COMPLETE]++; if (strcmp(t->key, "teeka") == 0) { game.isComplete = 1; unlockTeeka = 0; } } } teeka = NULL; unlockedMissions = 0; for (mission = hubMissionHead.next ; mission != NULL ; mission = mission->next) { if (requiredMissionUnlocked(mission->requires) || dev.cheatLevels || game.isComplete) { unlockMission(mission->id); } mission->status = getMissionStatus(mission->id); if (!game.isComplete) { if (strcmp(mission->id, "teeka") == 0) { teeka = mission; } else if (mission->status != MS_COMPLETE) { unlockTeeka = 0; } if (mission->status == MS_MISSING_HEART_CELL) { STRNCPY(mission->description, app.strings[ST_HEART_CELL], MAX_DESCRIPTION_LENGTH); } } else { STRNCPY(mission->description, app.strings[ST_FREEPLAY], MAX_DESCRIPTION_LENGTH); } game.totalMissions++; if (mission->status != MS_LOCKED) { unlockedMissions++; } } if (teeka != NULL) { if (unlockTeeka) { unlockMission("teeka"); teeka->status = MS_INCOMPLETE; } else { teeka->status = MS_LOCKED; } } awardMissionTrophies(); cloudPos.x = randF() - randF(); cloudPos.y = randF() - randF(); app.delegate.logic = &logic; app.delegate.draw = &draw; app.restrictTrophyAlert = 0; playMusic(1); endSectionTransition(); } static void logic(void) { blipValue += 0.1; blipSize = 64 + (sin(blipValue) * 16); scrollBackground(cloudPos.x, cloudPos.y); animateSprites(); switch (showing) { case SHOW_NONE: doCursor(); if (selectedMission == NULL) { doMissionSelect(); } else { doMissionInfo(); } break; case SHOW_WIDGETS: doWidgets(); if (app.keyboard[SDL_SCANCODE_ESCAPE]) { playSound(SND_MENU_BACK, 0); showing = SHOW_NONE; app.keyboard[SDL_SCANCODE_ESCAPE] = 0; } break; case SHOW_STATS: doStats(); if (app.keyboard[SDL_SCANCODE_ESCAPE]) { playSound(SND_MENU_BACK, 0); returnFromTrophyStats(); } break; case SHOW_TROPHIES: doTrophies(); if (app.keyboard[SDL_SCANCODE_ESCAPE]) { playSound(SND_MENU_BACK, 0); returnFromTrophyStats(); } break; default: break; } } static void doCursor(void) { if (app.mouse.dx != 0 || app.mouse.dy != 0) { cursor.x = app.mouse.x; cursor.y = app.mouse.y; } if (isControl(CONTROL_UP) || app.keyboard[SDL_SCANCODE_UP]) { cursor.y -= CURSOR_SPEED; SDL_WarpMouseInWindow(app.window, cursor.x, cursor.y); } if (isControl(CONTROL_DOWN) || app.keyboard[SDL_SCANCODE_DOWN]) { cursor.y += CURSOR_SPEED; SDL_WarpMouseInWindow(app.window, cursor.x, cursor.y); } if (isControl(CONTROL_LEFT) || app.keyboard[SDL_SCANCODE_LEFT]) { cursor.x -= CURSOR_SPEED; SDL_WarpMouseInWindow(app.window, cursor.x, cursor.y); } if (isControl(CONTROL_RIGHT) || app.keyboard[SDL_SCANCODE_RIGHT]) { cursor.x += CURSOR_SPEED; SDL_WarpMouseInWindow(app.window, cursor.x, cursor.y); } } static void doMissionSelect(void) { HubMission *m; if (app.keyboard[SDL_SCANCODE_ESCAPE]) { playSound(SND_MENU_BACK, 0); showWidgetGroup("hub"); showing = SHOW_WIDGETS; app.keyboard[SDL_SCANCODE_ESCAPE] = 0; } else if (isControl(CONTROL_FIRE) || app.mouse.button[SDL_BUTTON_LEFT]) { m = getMissionAt(cursor.x, cursor.y); if (m != NULL) { selectedMission = m; app.mouse.button[SDL_BUTTON_LEFT] = 0; clearControl(CONTROL_FIRE); showWidgetGroup("mission"); } } } static void doMissionInfo(void) { Widget *w; w = selectWidgetAt(cursor.x - app.uiOffset.x, cursor.y - app.uiOffset.y); if ((w != NULL) && (isControl(CONTROL_FIRE) || app.mouse.button[SDL_BUTTON_LEFT])) { if (w->type == WT_BUTTON) { w->action(); } else if (w->type == WT_SPINNER) { /* assuming there are only two options */ w->value[0] = !w->value[0]; } app.mouse.button[SDL_BUTTON_LEFT] = 0; clearControl(CONTROL_FIRE); } if (app.keyboard[SDL_SCANCODE_ESCAPE]) { cancel(); } } static void draw(void) { blitRectScaled(atlasTexture->texture, 0, 0, app.config.winWidth, app.config.winHeight, &worldMap->rect, 0); drawBackground(clouds->texture); drawMissions(); drawInfoBar(); switch (showing) { case SHOW_NONE: if (selectedMission != NULL) { if (!doPlusSettings) { drawMissionInfo(); } else { drawPlusSettings(); } SDL_SetRenderTarget(app.renderer, app.uiBuffer); drawWidgets(); /* draw on both the UI buffer and main buffer, just to cheat */ blitRect(atlasTexture->texture, cursor.x - app.uiOffset.x, cursor.y - app.uiOffset.y, getCurrentFrame(cursorSpr), 1); SDL_SetRenderTarget(app.renderer, app.backBuffer); } blitRect(atlasTexture->texture, cursor.x, cursor.y, getCurrentFrame(cursorSpr), 1); break; case SHOW_WIDGETS: drawHudWidgets(); break; case SHOW_STATS: drawStats(); break; case SHOW_TROPHIES: drawTrophies(); break; } } static void drawMissions(void) { HubMission *mission; double ratioX, ratioY; /* the original Attrition is based on 800x600, so multiply up */ ratioX = app.config.winWidth / 800.0; ratioY = app.config.winHeight / 600.0; for (mission = hubMissionHead.next ; mission != NULL ; mission = mission->next) { switch (mission->status) { case MS_INCOMPLETE: SDL_SetTextureColorMod(atlasTexture->texture, 255, 0, 0); blitRectScaled(atlasTexture->texture, mission->x * ratioX, mission->y * ratioY, blipSize, blipSize, &alert->rect, 1); drawText(mission->x * ratioX, (mission->y * ratioY) - 32, 18, TA_CENTER, colors.white, mission->name); break; case MS_PARTIAL: case MS_MISSING_HEART_CELL: SDL_SetTextureColorMod(atlasTexture->texture, 255, 255, 0); blitRectScaled(atlasTexture->texture, mission->x * ratioX, mission->y * ratioY, blipSize, blipSize, &alert->rect, 1); drawText(mission->x * ratioX, (mission->y * ratioY) - 32, 18, TA_CENTER, colors.white, mission->name); break; default: break; } } SDL_SetTextureColorMod(atlasTexture->texture, 255, 255, 255); } static void drawInfoBar(void) { int x; x = (50 + (app.config.winWidth - 50)) / 5; drawRect(0, 0, app.config.winWidth, 32, 0, 0, 0, 192); drawText(10, 5, 18, TA_LEFT, colors.white, app.strings[ST_HUB_MISSIONS], game.stats[STAT_MISSIONS_COMPLETE], unlockedMissions); drawText(x, 5, 18, TA_CENTER, colors.white, app.strings[ST_HUB_MIAS], game.stats[STAT_MIAS_RESCUED], game.totalMIAs); drawText(x * 2, 5, 18, TA_CENTER, colors.white, app.strings[ST_HUB_TARGETS], game.stats[STAT_TARGETS_DEFEATED], game.totalTargets); drawText(x * 3, 5, 18, TA_CENTER, colors.white, app.strings[ST_HUB_KEYS], game.stats[STAT_KEYS_FOUND], game.totalKeys); drawText(x * 4, 5, 18, TA_CENTER, colors.white, app.strings[ST_HUB_HEARTS], game.stats[STAT_HEARTS_FOUND], game.totalHearts); drawText(app.config.winWidth - 10, 5, 18, TA_RIGHT, colors.white, app.strings[ST_HUB_CELLS], game.stats[STAT_CELLS_FOUND], game.totalCells); } static void drawHudWidgets(void) { drawRect(0, 0, app.config.winWidth, app.config.winHeight, 0, 0, 0, 128); SDL_SetRenderTarget(app.renderer, app.uiBuffer); drawWidgetFrame(); drawWidgets(); SDL_SetRenderTarget(app.renderer, app.backBuffer); } static void drawMissionInfo(void) { int w, h, x, y, size, mid, i; drawRect(0, 0, app.config.winWidth, app.config.winHeight, 0, 0, 0, 128); SDL_SetRenderTarget(app.renderer, app.uiBuffer); w = 800; h = 550; x = (UI_WIDTH - w) / 2; y = (UI_HEIGHT - h) / 2; drawRect(x, y, w, h, 0, 0, 0, 192); drawOutlineRect(x, y, w, h, 255, 255, 255, 255); drawText(UI_WIDTH / 2, y + 25, 32, TA_CENTER, colors.white, selectedMission->name); app.textWidth = (w - 25); drawText(x + 15, y + 100, 22, TA_LEFT, colors.white, selectedMission->description); app.textWidth = 0; size = 65; mid = size / 2; y = (((UI_HEIGHT - h) / 2) + h) - 225; drawText(UI_WIDTH / 2, y, 24, TA_CENTER, colors.white, "Keys"); y += 64; x = ((UI_WIDTH - w) / 2) + 30; for (i = 0 ; i < MAX_KEY_TYPES ; i++) { drawRect(x, y, size, size, 0, 0, 0, 128); drawOutlineRect(x, y, size, size, 255, 255, 255, 255); if (game.keys[i].value.i > 0) { blitRect(atlasTexture->texture, x + mid, y + mid + 7, getCurrentFrame(keySprites[i]), 1); drawText(x + size - 5, y, 18, TA_RIGHT, colors.white, "%d", game.keys[i].value.i); } x += (size + 30); } SDL_SetRenderTarget(app.renderer, app.backBuffer); } static void drawPlusSettings(void) { int w, h, x, y; drawRect(0, 0, app.config.winWidth, app.config.winHeight, 0, 0, 0, 128); SDL_SetRenderTarget(app.renderer, app.uiBuffer); w = 800; h = 550; x = (UI_WIDTH - w) / 2; y = (UI_HEIGHT - h) / 2; drawRect(x, y, w, h, 0, 0, 0, 192); drawOutlineRect(x, y, w, h, 255, 255, 255, 255); drawText(UI_WIDTH / 2, y + 25, 32, TA_CENTER, colors.white, selectedMission->name); drawText(UI_WIDTH / 2, y + 75, 24, TA_CENTER, colors.white, app.strings[ST_MISSION_CONFIG]); SDL_SetRenderTarget(app.renderer, app.backBuffer); } static void unlockMission(char *id) { Tuple *t; for (t = game.missionStatusHead.next ; t != NULL ; t = t->next) { if (strcmp(t->key, id) == 0) { if (t->value.i == MS_LOCKED || game.isComplete) { t->value.i = MS_INCOMPLETE; /* if the game is complete, don't reset these two */ if (game.isComplete && (strcmp(t->key, "teeka") == 0 || strcmp(t->key, "beachApproach") == 0)) { t->value.i = MS_COMPLETE; } SDL_LogMessage(SDL_LOG_CATEGORY_APPLICATION, SDL_LOG_PRIORITY_DEBUG, "Unlocked mission %s", id); } return; } } t = malloc(sizeof(Tuple)); memset(t, 0, sizeof(Tuple)); game.missionStatusTail->next = t; game.missionStatusTail = t; STRNCPY(t->key, id, MAX_NAME_LENGTH); t->value.i = MS_INCOMPLETE; SDL_LogMessage(SDL_LOG_CATEGORY_APPLICATION, SDL_LOG_PRIORITY_DEBUG, "Unlocked mission %s", id); } static int requiredMissionUnlocked(char *id) { Tuple *t; if (strlen(id) > 0) { for (t = game.missionStatusHead.next ; t != NULL ; t = t->next) { if (strcmp(t->key, id) == 0) { return (t->value.i == MS_PARTIAL || t->value.i == MS_COMPLETE); } } } else { return 1; } return 0; } static void unlockAllMissions(void) { HubMission *mission; for (mission = hubMissionHead.next ; mission != NULL ; mission = mission->next) { if (mission->status == MS_LOCKED || mission->status == MS_INCOMPLETE) { mission->status = MS_INCOMPLETE; unlockMission(mission->id); } } } HubMission *getMissionAt(int x, int y) { HubMission *rtn; HubMission *mission; float distance, dist; double ratioX, ratioY; /* the original Attrition is based on 800x600, so multiply up */ ratioX = app.config.winWidth / 800.0; ratioY = app.config.winHeight / 600.0; rtn = NULL; distance = 32; for (mission = hubMissionHead.next ; mission != NULL ; mission = mission->next) { if (mission->status == MS_INCOMPLETE || mission->status == MS_MISSING_HEART_CELL || mission->status == MS_PARTIAL) { dist = getDistance(x, y, mission->x * ratioX, mission->y * ratioY); if (dist < distance) { rtn = mission; distance = dist; } } } return rtn; } static void startMission(void) { if (!game.isComplete) { STRNCPY(game.worldId, selectedMission->id, MAX_NAME_LENGTH); saveGame(0); stopMusic(); destroyHub(); initWorld(); } else { hideAllWidgets(); showWidgetGroup("missionPlus"); doPlusSettings = 1; } } static void cancel(void) { hideAllWidgets(); showing = SHOW_NONE; selectedMission = NULL; doPlusSettings = 0; app.keyboard[SDL_SCANCODE_ESCAPE] = 0; } static void startMissionPlus(void) { game.plus = 0; if (getWidget("noDoors", "missionPlus")->value[0]) { game.plus |= PLUS_NO_DOORS; } if (getWidget("randomEnemies", "missionPlus")->value[0]) { game.plus |= PLUS_RANDOM; } if (getWidget("tougherEnemies", "missionPlus")->value[0]) { game.plus |= PLUS_STRONGER; } if (getWidget("defeatAllEnemies", "missionPlus")->value[0]) { game.plus |= PLUS_KILL_ALL; } if (getWidget("mirrorWorld", "missionPlus")->value[0]) { game.plus |= PLUS_MIRROR; } STRNCPY(game.worldId, selectedMission->id, MAX_NAME_LENGTH); saveGame(0); stopMusic(); destroyHub(); initWorld(); } static void options(void) { initOptions(returnFromOptions); } static void stats(void) { showing = SHOW_STATS; initStatsDisplay(); showWidgetGroup("stats"); } static void trophies(void) { showing = SHOW_TROPHIES; showWidgetGroup("trophies"); } static void quit(void) { stopMusic(); destroyHub(); initTitle(); } static void returnFromTrophyStats(void) { showWidgetGroup("hub"); showing = SHOW_WIDGETS; app.keyboard[SDL_SCANCODE_ESCAPE] = 0; } static void returnFromOptions(void) { app.delegate.logic = &logic; app.delegate.draw = &draw; returnFromTrophyStats(); } static void loadMissions(void) { cJSON *root, *node; char *text; HubMission *mission; text = readFile("data/hub/missions.json"); root = cJSON_Parse(text); for (node = cJSON_GetObjectItem(root, "missions")->child ; node != NULL ; node = node->next) { mission = malloc(sizeof(HubMission)); memset(mission, 0, sizeof(HubMission)); hubMissionTail->next = mission; hubMissionTail = mission; STRNCPY(mission->id, cJSON_GetObjectItem(node, "id")->valuestring, MAX_NAME_LENGTH); STRNCPY(mission->name, cJSON_GetObjectItem(node, "name")->valuestring, MAX_NAME_LENGTH); STRNCPY(mission->description, _(cJSON_GetObjectItem(node, "description")->valuestring), MAX_DESCRIPTION_LENGTH); STRNCPY(mission->requires, cJSON_GetObjectItem(node, "requires")->valuestring, MAX_NAME_LENGTH); mission->status = MS_LOCKED; mission->x = cJSON_GetObjectItem(node, "x")->valuedouble; mission->y = cJSON_GetObjectItem(node, "y")->valuedouble; } cJSON_Delete(root); free(text); } static void awardMissionTrophies(void) { int beach, greenlands, underground, outpost, save; HubMission *mission; beach = greenlands = underground = outpost = 1; save = 0; for (mission = hubMissionHead.next ; mission != NULL ; mission = mission->next) { if (mission->status != MS_COMPLETE) { if (strstr(mission->id, "beach")) { beach = 0; } else if (strstr(mission->id, "greenlands")) { greenlands = 0; } else if (strstr(mission->id, "underground")) { underground = 0; } else if (strstr(mission->id, "outpost")) { outpost = 0; } } } if (beach) { awardTrophy("BEACH"); save = 1; } if (greenlands) { awardTrophy("GREENLANDS"); save = 1; } if (underground) { awardTrophy("UNDERGROUND"); save = 1; } if (outpost) { awardTrophy("OUTPOST"); save = 1; } /* ignore training mission */ if (game.stats[STAT_MISSIONS_COMPLETE] == 2) { awardTrophy("CLEAN"); save = 1; } /* ignore Teeka's */ if (game.totalMissions - game.stats[STAT_MISSIONS_COMPLETE] == 1) { awardTrophy("FULLY_CLEAN"); save = 1; } if (save) { saveGame(0); } } void destroyHub(void) { HubMission *m; while (hubMissionHead.next) { m = hubMissionHead.next; hubMissionHead.next = m->next; free(m); } memset(&hubMissionHead, 0, sizeof(HubMission)); hubMissionTail = &hubMissionHead; }