/* Copyright (C) 2018 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 "world.h" static void logic(void); static void draw(void); static void doWorldStart(void); static void doWorldInProgress(void); static void doWorldObserving(void); static void doWorldPaused(void); static void doWorldComplete(void); static void doGameComplete(void); static void doGameOver(void); static void doCommon(void); static void drawNormal(void); static void addHelperItems(void); static void spawnEnemies(void); static int canAdd(Unit *u, int mx, int my); static void startMission(void); static void drawInGameWidgets(void); static void handleWidgets(void); static void resume(void); static void options(void); static void stats(void); static void trophies(void); static void quit(void); static void returnFromTrophyStats(void); static void drawQuit(void); static void drawGameOver(void); void quitMission(void); static void returnFromOptions(void); void autoCompleteMission(void); static void retry(void); static void hub(void); static void title(void); static Texture *background; static Texture *atlasTexture; static Atlas *missionFailed; static int observationIndex; static int showing; void initWorld(void) { startSectionTransition(); loadWorld(game.worldId); world.currentStatus = getMissionStatus(game.worldId); SDL_LogMessage(SDL_LOG_CATEGORY_APPLICATION, SDL_LOG_PRIORITY_DEBUG, "world.currentStatus = %d", world.currentStatus); background = getTexture(world.background); atlasTexture = getTexture("gfx/atlas/atlas.png"); missionFailed = getImageFromAtlas("gfx/main/missionFailed.png"); loadMusic(world.music); initQuadtree(&world.quadtree); initObjectives(); initParticles(); initHud(); initWeapons(); initEffects(); initItems(); initMap(); initEntities(); addKeysFromStash(); world.enemySpawnTimer = (FPS * rrnd(world.minEnemySpawnTime, world.maxEnemySpawnTime)); world.state = WS_START; observationIndex = 0; getWidget("resume", "gamePaused")->action = resume; getWidget("options", "gamePaused")->action = options; getWidget("stats", "gamePaused")->action = stats; getWidget("trophies", "gamePaused")->action = trophies; getWidget("quit", "gamePaused")->action = quit; getWidget("ok", "stats")->action = returnFromTrophyStats; getWidget("ok", "trophies")->action = returnFromTrophyStats; getWidget("ok", "gameQuit")->action = quitMission; getWidget("cancel", "gameQuit")->action = returnFromTrophyStats; getWidget("retry", "gameOver")->action = retry; getWidget("hub", "gameOver")->action = hub; getWidget("title", "gameOver")->action = title; if (world.missionType == MT_BOSS) { startMission(); hideAllWidgets(); world.betweenTimer = 0; } else { world.bob->flags |= EF_GONE; playMusic(1); } game.stats[STAT_MISSIONS_PLAYED]++; app.delegate.logic = logic; app.delegate.draw = draw; app.restrictTrophyAlert = 1; endSectionTransition(); } static void logic(void) { if (--world.betweenTimer <= 0) { world.betweenTimer = 0; switch (world.state) { case WS_START: doWorldStart(); break; case WS_IN_PROGRESS: doWorldInProgress(); break; case WS_OBSERVING: doWorldObserving(); break; case WS_PAUSED: doWorldPaused(); break; case WS_COMPLETE: doWorldComplete(); break; case WS_GAME_OVER: doGameOver(); break; case WS_GAME_COMPLETE: doGameComplete(); break; default: break; } } if (--world.mapAnimTimer < 0) { world.mapAnimTimer = 4; } } static void draw(void) { clearScreen(); switch (world.state) { case WS_PAUSED: drawNormal(); drawMissionStatus(); break; case WS_START: drawNormal(); drawMissionStatus(); drawText(SCREEN_WIDTH / 2, SCREEN_HEIGHT - 80, 24, TA_CENTER, colors.white, _("Press Fire to Continue")); break; case WS_GAME_OVER: drawNormal(); drawGameOver(); break; default: if (world.betweenTimer == 0) { drawNormal(); drawHud(); } break; } switch (showing) { case SHOW_WIDGETS: drawInGameWidgets(); break; case SHOW_STATS: drawStats(); break; case SHOW_TROPHIES: drawTrophies(); break; case SHOW_QUIT: drawQuit(); break; } } static void drawInGameWidgets(void) { drawRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT, 0, 0, 0, 128); drawWidgetFrame(); drawWidgets(); } static void drawNormal(void) { blitScaled(background->texture, 0, 0, SCREEN_WIDTH, SCREEN_HEIGHT, 0); drawEntities(PLANE_BACKGROUND); drawParticles(PLANE_BACKGROUND); drawMap(); drawEntities(PLANE_FOREGROUND); drawParticles(PLANE_FOREGROUND); } static void startMission(void) { Entity *self; SDL_Rect *r; self = (Entity*)world.bob; world.state = WS_IN_PROGRESS; world.betweenTimer = FPS / 2; r = &self->sprite[self->facing]->frames[self->spriteFrame]->rect; self->w = r->w; self->h = r->h; resetAtCheckpoint(); world.entityToTrack = self; self->flags &= ~EF_GONE; } static void doWorldStart(void) { float dist; if (world.entityToTrack != NULL) { dist = cameraChase(world.entityToTrack, 5); if ((dist <= world.entityToTrack->w && dist <= world.entityToTrack->h) || world.entityChaseTimer <= 0) { world.entityToTrack = getRandomObjectiveEntity(); world.entityChaseTimer = FPS * 5; } } else { world.entityToTrack = getRandomObjectiveEntity(); cameraTrack(world.entityToTrack); } world.entityChaseTimer = MAX(world.entityChaseTimer - 1, 0); doCommon(); if (isAcceptControl()) { clearControls(); startMission(); } } static void doWorldInProgress(void) { cameraTrack(world.entityToTrack); doPlayer(); if (showing == SHOW_NONE) { doBob(); doCommon(); doLocationTriggers(); if (world.allObjectivesComplete && world.state != WS_COMPLETE) { world.bob->flags |= EF_IMMUNE; if (world.bob->stunTimer > 0) { world.bob->stunTimer = 0; } if (strcmp(world.id, "teeka") == 0) { world.state = WS_GAME_COMPLETE; } else { world.state = WS_COMPLETE; } world.missionCompleteTimer = FPS * 3; stopMusic(); } if (isControl(CONTROL_PAUSE)) { world.state = WS_PAUSED; pauseSound(1); clearControl(CONTROL_PAUSE); } if (isControl(CONTROL_MAP)) { pauseSound(1); initRadar(); clearControl(CONTROL_MAP); } if (app.keyboard[SDL_SCANCODE_ESCAPE]) { app.keyboard[SDL_SCANCODE_ESCAPE] = 0; showWidgetGroup("gamePaused"); playSound(SND_MENU_BACK, 0); showing = SHOW_WIDGETS; } if (world.observationTimer > 0) { if (--world.observationTimer == FPS) { world.entityToTrack = world.entitiesToObserve[0]; world.state = WS_OBSERVING; } } } else if (showing == SHOW_WIDGETS) { handleWidgets(); } else if (showing == SHOW_STATS) { doStats(); if (app.keyboard[SDL_SCANCODE_ESCAPE]) { playSound(SND_MENU_BACK, 0); returnFromTrophyStats(); } } else if (showing == SHOW_TROPHIES) { doTrophies(); if (app.keyboard[SDL_SCANCODE_ESCAPE]) { playSound(SND_MENU_BACK, 0); returnFromTrophyStats(); } } else if (showing == SHOW_QUIT) { handleWidgets(); if (app.keyboard[SDL_SCANCODE_ESCAPE]) { returnFromTrophyStats(); } } } static void handleWidgets(void) { doWidgets(); if (app.keyboard[SDL_SCANCODE_ESCAPE]) { playSound(SND_MENU_BACK, 0); resume(); } } static void doWorldObserving(void) { int tx, ty; float diffX, diffY; tx = world.entityToTrack->x - (SCREEN_WIDTH / 2); ty = world.entityToTrack->y - (SCREEN_HEIGHT / 2); doEntitiesStatic(); diffX = abs(camera.x - tx) / 20; diffY = abs(camera.y - ty) / 20; diffX = MAX(3, MIN(50, diffX)); diffY = MAX(3, MIN(50, diffY)); if (camera.x > tx) { camera.x -= diffX; } if (camera.x < tx) { camera.x += diffX; } if (camera.y > ty) { camera.y -= diffY; } if (camera.y < ty) { camera.y += diffY; } if (collision(camera.x, camera.y, MAP_TILE_SIZE, MAP_TILE_SIZE, tx, ty, MAP_TILE_SIZE, MAP_TILE_SIZE)) { if (--world.observationTimer <= 0) { if (++observationIndex < MAX_ENTS_TO_OBSERVE && world.entitiesToObserve[observationIndex] != NULL) { world.entityToTrack = world.entitiesToObserve[observationIndex]; world.observationTimer = FPS; } else { memset(world.entitiesToObserve, 0, sizeof(Entity*) * MAX_ENTS_TO_OBSERVE); world.entityToTrack = (Entity*)world.bob; world.state = WS_IN_PROGRESS; observationIndex = 0; } } } } static void doWorldPaused(void) { animateSprites(); if (isControl(CONTROL_PAUSE)) { pauseSound(0); world.state = WS_IN_PROGRESS; clearControl(CONTROL_PAUSE); } } static void doWorldComplete(void) { world.missionCompleteTimer--; if (world.missionCompleteTimer == 0) { initPostMission(); } else if (world.missionCompleteTimer == FPS * 1.5) { dropCarriedItems(); world.bob->flags |= EF_GONE; addTeleportStars((Entity*)world.bob); playSound(SND_TELEPORT, world.bob->uniqueId % MAX_SND_CHANNELS); } else { doBob(); } doCommon(); } static void doGameComplete(void) { world.missionCompleteTimer--; if (world.missionCompleteTimer <= 0) { initEnding(); } else { doBob(); } doCommon(); } static void doGameOver(void) { if (world.gameOverTimer == -FPS) { stopMusic(); } else if (world.gameOverTimer == -FPS * 2) { loadMusic("music/Sadness.ogg"); playMusic(0); } else if (world.gameOverTimer == -FPS * 3) { showWidgetGroup("gameOver"); } world.gameOverTimer = MAX(-FPS * 5, world.gameOverTimer - 1); doCommon(); if (world.gameOverTimer <= -FPS * 3) { doWidgets(); } } static void doCommon(void) { animateSprites(); world.frameCounter++; world.frameCounter %= (FPS * 10); doHud(); if (world.minEnemySpawnTime > 0 && !dev.cheatNoEnemies) { spawnEnemies(); } if (world.missionType == MT_BOSS && --world.helperItemTimer <= 0) { addHelperItems(); } doEntities(); doParticles(); } static void addHelperItems(void) { int x, y, w, h; w = world.map.bounds.w - world.map.bounds.x; h = world.map.bounds.h - world.map.bounds.y; x = world.map.bounds.x + (rand() % w); y = world.map.bounds.y + 1; if (world.map.data[x / MAP_TILE_SIZE][y / MAP_TILE_SIZE] == 0) { dropRandomCherry(x, y); } x = world.map.bounds.x + (rand() % w); y = world.map.bounds.y + (rand() % h); if (world.map.data[x / MAP_TILE_SIZE][y / MAP_TILE_SIZE] == 0) { addRandomWeapon(x, y); } world.helperItemTimer = FPS * rrnd(3, 5); } static void spawnEnemies(void) { char name[MAX_NAME_LENGTH]; int r, x, y; Unit *u; if (world.numToSpawn == 0) { if (--world.enemySpawnTimer <= 0) { world.numToSpawn = 3 + (rand() % 3); world.spawnInterval = 0; } return; } if (--world.spawnInterval <= 0) { r = (rand() % world.numEnemyTypes); x = world.bob->x; x += ((randF() - randF()) * 5) * MAP_TILE_SIZE; y = world.bob->y; y += ((randF() - randF()) * 5) * MAP_TILE_SIZE; if (x >= world.map.bounds.x && y >= world.map.bounds.y && x < world.map.bounds.w + SCREEN_WIDTH - 64 && y < world.map.bounds.h + SCREEN_HEIGHT - 64) { sprintf(name, "%s%s", world.enemyTypes[r], (rand() % 2 ? "Blob" : "EyeDroid")); u = (Unit*)createEntity(name); self = (Entity*)u; u->animate(); x /= MAP_TILE_SIZE; y /= MAP_TILE_SIZE; if (canAdd(u, x, y)) { u->x = x * MAP_TILE_SIZE; u->y = y * MAP_TILE_SIZE; u->spawnedIn = 1; u->canCarryItem = 0; addTeleportStars((Entity*)u); playBattleSound(SND_APPEAR, u->uniqueId % MAX_SND_CHANNELS, u->x, u->y); } } world.spawnInterval = rrnd(FPS / 4, FPS / 2); if (--world.numToSpawn <= 0) { world.enemySpawnTimer = (FPS * rrnd(world.minEnemySpawnTime, world.maxEnemySpawnTime)); } } } static int canAdd(Unit *u, int mx, int my) { int i; if (isSolid(mx, my)) { return 0; } if (isLiquid(mx, my) && (!(u->flags & EF_WATER_BREATHING))) { return 0; } if (!(u->flags & EF_WEIGHTLESS)) { for (i = 0 ; i < 10 ; i++) { if (isLiquid(mx, my + i)) { return 0; } if (isWalkable(mx, my + i)) { return 1; } } /* long drop */ return 0; } return 1; } void observeActivation(Entity *e) { int i; if (!isOnScreen(e) && (!(e->flags & EF_NO_OBSERVE))) { e->flags |= EF_NO_OBSERVE; for (i = 0 ; i < MAX_ENTS_TO_OBSERVE ; i++) { if (world.entitiesToObserve[i] == NULL) { world.entitiesToObserve[i] = e; world.observationTimer = FPS * 1.5; return; } else if (getDistance(e->x, e->y, world.entitiesToObserve[i]->x, world.entitiesToObserve[i]->y) < SCREEN_HEIGHT - 50) { return; } } SDL_LogMessage(SDL_LOG_CATEGORY_APPLICATION, SDL_LOG_PRIORITY_ERROR, "Can't observe entity - out of array space"); exit(1); } } void drawGameOver(void) { int fadeAmount; if (world.gameOverTimer <= -FPS) { fadeAmount = MIN((world.gameOverTimer + FPS) * -1, 128); } drawRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT, 0, 0, 0, fadeAmount); if (world.gameOverTimer <= -FPS * 2) { blitRect(atlasTexture->texture, SCREEN_WIDTH / 2, 280, &missionFailed->rect, 1); if (world.gameOverTimer <= -FPS * 3) { drawWidgetFrame(); drawWidgets(); } } } void drawQuit(void) { SDL_Rect r; drawRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT, 0, 0, 0, 128); r.w = 650; r.h = 325; r.x = (SCREEN_WIDTH / 2) - r.w / 2; r.y = (SCREEN_HEIGHT / 2) - r.h / 2; drawRect(r.x, r.y, r.w, r.h, 0, 0, 0, 192); drawOutlineRect(r.x, r.y, r.w, r.h, 200, 200, 200, 255); limitTextWidth(r.w - 100); drawText(SCREEN_WIDTH / 2, r.y + 10, 26, TA_CENTER, colors.white, "Quit and return to hub?"); if (world.missionType == MT_TRAINING) { drawText(SCREEN_WIDTH / 2, r.y + 65, 26, TA_CENTER, colors.white, "As this is a tutorial mission, you can skip it and move onto the main game."); } else if (world.isReturnVisit) { drawText(SCREEN_WIDTH / 2, r.y + 65, 26, TA_CENTER, colors.white, "Your progress on this mission will be saved."); } else { drawText(SCREEN_WIDTH / 2, r.y + 65, 26, TA_CENTER, colors.white, "Warning: if you quit now, you will lose all progress on this level."); } limitTextWidth(0); drawWidgets(); } void exitRadar(void) { startSectionTransition(); app.delegate.logic = logic; app.delegate.draw = draw; endSectionTransition(); } static void resume(void) { app.keyboard[SDL_SCANCODE_ESCAPE] = 0; hideAllWidgets(); showing = SHOW_NONE; } 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) { showing = SHOW_QUIT; showWidgetGroup("gameQuit"); } static void retry(void) { retryMission(); } static void hub(void) { returnToHub(); } static void title(void) { returnToTitle(); } static void returnFromTrophyStats(void) { showWidgetGroup("gamePaused"); showing = SHOW_WIDGETS; app.keyboard[SDL_SCANCODE_ESCAPE] = 0; } void quitMission(void) { resume(); stopMusic(); world.state = WS_COMPLETE; world.missionCompleteTimer = (FPS * 1.5) + 1; if (world.missionType == MT_TRAINING) { autoCompleteMission(); } } static void returnFromOptions(void) { app.delegate.logic = logic; app.delegate.draw = draw; returnFromTrophyStats(); } void autoCompleteMission(void) { Objective *o; Entity *e; for (o = world.objectiveHead.next ; o != NULL ; o = o->next) { o->currentValue = o->targetValue; } for (e = world.entityHead.next ; e != NULL ; e = e->next) { switch (e->type) { case ET_MIA: e->alive = ALIVE_DEAD; game.stats[STAT_MIAS_RESCUED]++; break; case ET_KEY: if (!(e->flags & EF_GONE)) { e->alive = ALIVE_DEAD; game.stats[STAT_KEYS_FOUND]++; } break; default: break; } } } void destroyWorld(void) { int i; for (i = 0 ; i < world.numEnemyTypes ; i++) { free(world.enemyTypes[i]); } free(world.enemyTypes); destroyTriggers(); destroyObjectives(); destroyEntities(); destroyParticles(); destroyQuadtree(); }