//------------------------------------------------------------------------- /* Copyright (C) 2010-2019 EDuke32 developers and contributors Copyright (C) 2019 sirlemonhead, Nuke.YKT This file is part of PCExhumed. PCExhumed is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License version 2 as published by the Free Software Foundation. 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ //------------------------------------------------------------------------- #include "ns.h" #include "compat.h" #include "build.h" #include "exhumed.h" #include "names.h" #include "engine.h" #include "c_bind.h" #include "status.h" #include "sound.h" #include "names.h" #include "ps_input.h" #include "view.h" #include "raze_sound.h" #include "v_2ddrawer.h" #include "v_font.h" #include "texturemanager.h" #include "gamestate.h" #include "multipatchtexture.h" #include "screenjob.h" #include "sequence.h" #include "v_draw.h" #include "m_random.h" #include "gstrings.h" #include #include BEGIN_PS_NS int selectedlevelnew; //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- void InitFonts() { GlyphSet fontdata; fontdata.Insert(127, TexMan.FindGameTexture("TINYBLAK")); // this is only here to widen the color range of the font to produce a better translation. for (int i = 0; i < 26; i++) { fontdata.Insert('A' + i, tileGetTexture(3522 + i)); } for (int i = 0; i < 10; i++) { fontdata.Insert('0' + i, tileGetTexture(3555 + i)); } fontdata.Insert('.', tileGetTexture(3548)); fontdata.Insert(':', tileGetTexture(3548)); // we have no colon but need something here. fontdata.Insert('/', tileGetTexture(3552)); // same here fontdata.Insert('!', tileGetTexture(3549)); fontdata.Insert('?', tileGetTexture(3550)); fontdata.Insert(',', tileGetTexture(3551)); fontdata.Insert('`', tileGetTexture(3552)); fontdata.Insert('\'', tileGetTexture(3552)); fontdata.Insert('"', tileGetTexture(3553)); fontdata.Insert('-', tileGetTexture(3554)); fontdata.Insert('_', tileGetTexture(3554)); fontdata.Insert(127, TexMan.FindGameTexture("TINYBLAK")); // this is only here to widen the color range of the font to produce a better translation. GlyphSet::Iterator it(fontdata); GlyphSet::Pair* pair; while (it.NextPair(pair)) pair->Value->SetOffsetsNotForFont(); SmallFont = new ::FFont("SmallFont", nullptr, "defsmallfont", 0, 0, 0, -1, 4, false, false, false, &fontdata); SmallFont->SetKerning(1); fontdata.Clear(); fontdata.Insert(127, TexMan.FindGameTexture("TINYBLAK")); // this is only here to widen the color range of the font to produce a better translation. for (int i = 0; i < 26; i++) { fontdata.Insert('A' + i, tileGetTexture(3624 + i)); } for (int i = 0; i < 10; i++) { fontdata.Insert('0' + i, tileGetTexture(3657 + i)); } fontdata.Insert('!', tileGetTexture(3651)); fontdata.Insert('"', tileGetTexture(3655)); fontdata.Insert('\'', tileGetTexture(3654)); fontdata.Insert('`', tileGetTexture(3654)); fontdata.Insert('.', tileGetTexture(3650)); fontdata.Insert(',', tileGetTexture(3653)); fontdata.Insert('-', tileGetTexture(3656)); fontdata.Insert('?', tileGetTexture(3652)); fontdata.Insert(127, TexMan.FindGameTexture("TINYBLAK")); // this is only here to widen the color range of the font to produce a better translation. GlyphSet::Iterator it2(fontdata); while (it2.NextPair(pair)) pair->Value->SetOffsetsNotForFont(); SmallFont2 = new ::FFont("SmallFont2", nullptr, "defsmallfont2", 0, 0, 0, -1, 4, false, false, false, &fontdata); SmallFont2->SetKerning(1); } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- void DrawAbs(int tile, double x, double y, int shade = 0) { DrawTexture(twod, tileGetTexture(tile), x, y, DTA_FullscreenScale, FSMode_ScaleToFit43, DTA_VirtualWidth, 320, DTA_VirtualHeight, 200, DTA_TopLeft, true, DTA_Color, shadeToLight(shade), TAG_DONE); } void DrawRel(int tile, double x, double y, int shade) { // This is slightly different than what the backend does here, but critical for some graphics. int offx = (tileWidth(tile) >> 1) + tileLeftOffset(tile); int offy = (tileHeight(tile) >> 1) + tileTopOffset(tile); DrawAbs(tile, x - offx, y - offy, shade); } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- // this might be static within the DoPlasma function? static uint8_t* PlasmaBuffer; static int nPlasmaTile = kTile4092; static int nLogoTile; static unsigned int nSmokeBottom; static unsigned int nSmokeRight; static unsigned int nSmokeTop; static unsigned int nSmokeLeft; static int nextPlasmaTic; static int plasma_A[5] = { 0 }; static int plasma_B[5] = { 0 }; static int plasma_C[5] = { 0 }; static FRandom rnd_plasma; enum { kPlasmaWidth = 320, kPlasmaHeight = 80, }; //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- void menu_DoPlasma() { int ptile = nPlasmaTile; if (totalclock >= nextPlasmaTic || !PlasmaBuffer) { nextPlasmaTic = (int)totalclock + 4; if (!nLogoTile) nLogoTile = EXHUMED ? kExhumedLogo : kPowerslaveLogo; if (!PlasmaBuffer) { auto pixels = TileFiles.tileCreate(kTile4092, kPlasmaWidth, kPlasmaHeight); memset(pixels, 96, kPlasmaWidth * kPlasmaHeight); PlasmaBuffer = TileFiles.tileCreate(kTile4093, kPlasmaWidth, kPlasmaHeight); memset(PlasmaBuffer, 96, kPlasmaWidth * kPlasmaHeight); nSmokeLeft = 160 - tilesiz[nLogoTile].x / 2; nSmokeRight = nSmokeLeft + tilesiz[nLogoTile].x; nSmokeTop = 40 - tilesiz[nLogoTile].y / 2; nSmokeBottom = nSmokeTop + tilesiz[nLogoTile].y - 1; for (int i = 0; i < 5; i++) { int logoWidth = tilesiz[nLogoTile].x; plasma_C[i] = (nSmokeLeft + rand() % logoWidth) << 16; plasma_B[i] = (rnd_plasma.GenRand32() % 327680) + 0x10000; if (rnd_plasma.GenRand32()&1) { plasma_B[i] = -plasma_B[i]; } plasma_A[i] = rnd_plasma.GenRand32() & 1; } } uint8_t* plasmapix = tileData(nPlasmaTile); uint8_t* r_ebx = plasmapix + 81; const uint8_t* r_edx = tileData(nPlasmaTile ^ 1) + 81; // flip between value of 4092 and 4093 with xor for (int x = 0; x < kPlasmaWidth - 2; x++) { for (int y = 0; y < kPlasmaHeight - 2; y++) { uint8_t al = *r_edx; if (al != 96) { if (al > 158) { *r_ebx = al - 1; } else { *r_ebx = 96; } } else { if (rnd_plasma.GenRand32() & 1) { *r_ebx = *r_edx; } else { uint8_t al = *(r_edx + 1); uint8_t cl = *(r_edx - 1); if (al <= cl) { al = cl; } cl = al; al = *(r_edx - 80); if (cl <= al) { cl = al; } al = *(r_edx + 80); if (cl <= al) { cl = al; } al = *(r_edx + 80); if (cl <= al) { cl = al; } al = *(r_edx + 80); if (cl <= al) { cl = al; } al = *(r_edx - 79); if (cl > al) { al = cl; } cl = *(r_edx - 81); if (al <= cl) { al = cl; } cl = al; if (al <= 159) { *r_ebx = 96; } else { if (!(rnd_plasma.GenRand32() & 1)) { cl--; } *r_ebx = cl; } } } // before restarting inner loop r_edx++; r_ebx++; } // before restarting outer loop r_edx += 2; r_ebx += 2; } auto logopix = tilePtr(nLogoTile); for (int j = 0; j < 5; j++) { int pB = plasma_B[j]; int pC = plasma_C[j]; int badOffset = (pC >> 16) < nSmokeLeft || (pC >> 16) >= nSmokeRight; const uint8_t* ptr3 = (logopix + ((pC >> 16) - nSmokeLeft) * tilesiz[nLogoTile].y); plasma_C[j] += plasma_B[j]; if ((pB > 0 && (plasma_C[j] >> 16) >= nSmokeRight) || (pB < 0 && (plasma_C[j] >> 16) <= nSmokeLeft)) { int esi = plasma_A[j]; plasma_B[j] = -plasma_B[j]; plasma_A[j] = esi == 0; } if (badOffset) continue; unsigned int nSmokeOffset = 0; if (plasma_A[j]) { nSmokeOffset = nSmokeTop; while (nSmokeOffset < nSmokeBottom) { uint8_t al = *ptr3; if (al != TRANSPARENT_INDEX && al != 96) { break; } nSmokeOffset++; ptr3++; } } else { nSmokeOffset = nSmokeBottom; ptr3 += tilesiz[nLogoTile].y - 1; while (nSmokeOffset > nSmokeTop) { uint8_t al = *ptr3; if (al != TRANSPARENT_INDEX && al != 96) { break; } nSmokeOffset--; ptr3--; } } uint8_t* v28 = plasmapix + (80 * (plasma_C[j] >> 16)); v28[nSmokeOffset] = 175; } TileFiles.InvalidateTile(nPlasmaTile); // flip between tile 4092 and 4093 if (nPlasmaTile == kTile4092) { nPlasmaTile = kTile4093; } else if (nPlasmaTile == kTile4093) { nPlasmaTile = kTile4092; } } DrawAbs(ptile, 0, 0); DrawRel(nLogoTile, 160, 40); // draw the fire urn/lamp thingies int dword_9AB5F = ((int)totalclock / 16) & 3; DrawRel(kTile3512 + dword_9AB5F, 50, 150); DrawRel(kTile3512 + ((dword_9AB5F + 2) & 3), 270, 150); } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- class DLobotomyScreen : public DImageScreen { public: DLobotomyScreen(FGameTexture *tex, int fade) : DImageScreen(tex, fade) {} int Frame(uint64_t clock, bool skiprequest) override { if (clock == 0) PlayLocalSound(StaticSound[kSoundJonLaugh2], 7000, false, CHANF_UI); return DImageScreen::Frame(clock, skiprequest); } }; //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- static const short skullDurations[] = { 6, 25, 43, 50, 68, 78, 101, 111, 134, 158, 173, 230, 600 }; class DMainTitle : public DScreenJob { const char* a; const char* b; int state = 0; int var_18; int var_4 = 0; int esi = 130; int nCount = 0; int start; public: DMainTitle() : DScreenJob(fadein) { a = GStrings("TXT_EX_COPYRIGHT1"); b = GStrings("TXT_EX_COPYRIGHT2"); var_18 = skullDurations[0]; } int Frame(uint64_t clock, bool skiprequest) override { int ticker = clock * 120 / 1'000'000'000; if (clock == 0) { PlayLocalSound(StaticSound[59], 0, true, CHANF_UI); playCDtrack(19, true); } if (clock > 1'000'000 && state == 0 && !soundEngine->IsSourcePlayingSomething(SOURCE_None, nullptr,CHAN_AUTO, -1)) { if (time(0) & 0xF) PlayGameOverSound(); else PlayLocalSound(StaticSound[61], 0, false, CHANF_UI); state = 1; start = ticker; } menu_DoPlasma(); DrawRel(kSkullHead, 160, 100); switch (state) { case 0: DrawRel(kSkullJaw, 161, 130); break; case 1: { int nStringWidth = SmallFont->StringWidth(a); DrawText(twod, SmallFont, CR_UNTRANSLATED, 160 - nStringWidth / 2, 200 - 24, a, DTA_FullscreenScale, FSMode_ScaleToFit43, DTA_VirtualWidth, 320, DTA_VirtualHeight, 200, TAG_DONE); nStringWidth = SmallFont->StringWidth(b); DrawText(twod, SmallFont, CR_UNTRANSLATED, 160 - nStringWidth / 2, 200 - 16, b, DTA_FullscreenScale, FSMode_ScaleToFit43, DTA_VirtualWidth, 320, DTA_VirtualHeight, 200, TAG_DONE); if (ticker > var_18) { nCount++; if (nCount > 12) return 0; var_18 = start + skullDurations[nCount]; var_4 = var_4 == 0; } short nTile = kSkullJaw; if (var_4) { if (esi >= 135) nTile = kTile3583; else esi += 5; } else if (esi <= 130) esi = 130; else esi -= 2; int y; if (nTile == kTile3583) { y = 131; } else { y = esi; if (y > 135) y = 135; } DrawRel(nTile, 161, y); break; } } return skiprequest? -1 : 1; } }; //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- DScreenJob *PlayMovie(const char* fileName); void DoTitle(CompletionFunc completion) { JobDesc jobs[5]; int job = 0; jobs[job++] = { Create(tileGetTexture(EXHUMED ? kTileBMGLogo : kTilePIELogo), DScreenJob::fadein | DScreenJob::fadeout) }; jobs[job++] = { Create(tileGetTexture(seq_GetSeqPicnum(kSeqScreens, 0, 0)), DScreenJob::fadein | DScreenJob::fadeout) }; jobs[job++] = { PlayMovie("book.mov") }; jobs[job++] = { Create() }; RunScreenJob(jobs, job, completion, true, true); } //--------------------------------------------------------------------------- // // pre-level map display // //--------------------------------------------------------------------------- static const int8_t MapLevelOffsets[] = { 0, 50, 10, 20, 0, 45, -20, 20, 5, 0, -10, 10, 30, -20, 0, 20, 0, 0, 0, 0 }; struct TILEFRAMEDEF { short nTile; short xOffs; short yOffs; }; // 22 bytes struct MapNamePlaque { short xPos; short yPos; TILEFRAMEDEF tiles[2]; TILEFRAMEDEF text; }; static const MapNamePlaque mapNamePlaques[] = { { 100, 170, kTile3376, 0, 0, kTile3377, 0, 0, kTile3411, 18, 6 }, { 230, 10, kTile3378, 0, 0, kTile3379, 0, 0, kTile3414, 18, 6 }, // DENDUR (level 2) { 180, 125, kTile3380, 0, 0, kTile3381, 0, 0, kTile3417, 18, 6 }, // Kalabash { 10, 95, kTile3382, 0, 0, kTile3383, 0, 0, kTile3420, 18, 6 }, { 210, 160, kTile3384, 0, 0, kTile3385, 0, 0, kTile3423, 18, 6 }, { 10, 110, kTile3371, 0, 0, kTile3386, 0, 0, kTile3426, 18, 6 }, { 10, 50, kTile3387, 0, 0, kTile3388, 0, 0, kTile3429, 18, 6 }, { 140, 0, kTile3389, 0, 0, kTile3390, 0, 0, kTile3432, 18, 6 }, { 30, 20, kTile3391, 0, 0, kTile3392, 0, 0, kTile3435, 18, 6 }, { 200, 150, kTile3409, 0, 0, kTile3410, 0, 0, kTile3418, 20, 4 }, { 145, 170, kTile3393, 0, 0, kTile3394, 0, 0, kTile3438, 18, 6 }, { 80, 80, kTile3395, 0, 0, kTile3396, 0, 0, kTile3441, 18, 6 }, { 15, 0, kTile3397, 0, 0, kTile3398, 0, 0, kTile3444, 18, 5 }, { 220, 35, kTile3399, 0, 0, kTile3400, 0, 0, kTile3447, 18, 6 }, { 190, 40, kTile3401, 0, 0, kTile3402, 0, 0, kTile3450, 18, 6 }, { 20, 130, kTile3403, 0, 0, kTile3404, 0, 0, kTile3453, 19, 6 }, { 220, 160, kTile3405, 0, 0, kTile3406, 0, 0, kTile3456, 18, 6 }, { 20, 10, kTile3407, 0, 0, kTile3408, 0, 0, kTile3459, 18, 6 }, { 200, 10, kTile3412, 0, 0, kTile3413, 0, 0, kTile3419, 18, 5 }, { 20, 10, kTile3415, 0, 0, kTile3416, 0, 0, kTile3421, 19, 4 } }; // 3 different types of fire, each with 4 frames static const TILEFRAMEDEF FireTiles[3][4] = { {{ kTile3484,0,3 },{ kTile3485,0,0 },{ kTile3486,0,3 },{ kTile3487,0,0 }}, {{ kTile3488,1,0 },{ kTile3489,1,0 },{ kTile3490,0,1 },{ kTile3491,1,1 }}, {{ kTile3492,1,2 },{ kTile3493,1,0 },{ kTile3494,1,2 },{ kTile3495,1,0 }} }; struct Fire { short nFireType; short xPos; short yPos; }; // 20 bytes struct MapFire { short nFires; Fire fires[3]; }; /* level 1 - 3 fires level 2 - 3 fires level 3 - 1 fire */ static const MapFire MapLevelFires[] = { 3, {{0, 107, 95}, {1, 58, 140}, {2, 28, 38}}, 3, {{2, 240, 0}, {0, 237, 32}, {1, 200, 30}}, 2, {{2, 250, 57}, {0, 250, 43}, {2, 200, 70}}, 2, {{1, 82, 59}, {2, 84, 16}, {0, 10, 95}}, 2, {{2, 237, 50}, {1, 215, 42}, {1, 210, 50}}, 3, {{0, 40, 7}, {1, 75, 6}, {2, 100, 10}}, 3, {{0, 58, 61}, {1, 85, 80}, {2, 111, 63}}, 3, {{0, 260, 65}, {1, 228, 0}, {2, 259, 15}}, 2, {{0, 81, 38}, {2, 58, 38}, {2, 30, 20}}, 3, {{0, 259, 49}, {1, 248, 76}, {2, 290, 65}}, 3, {{2, 227, 66}, {0, 224, 98}, {1, 277, 30}}, 2, {{0, 100, 10}, {2, 48, 76}, {2, 80, 80}}, 3, {{0, 17, 2}, {1, 29, 49}, {2, 53, 28}}, 3, {{0, 266, 42}, {1, 283, 99}, {2, 243, 108}}, 2, {{0, 238, 19}, {2, 240, 92}, {2, 190, 40}}, 2, {{0, 27, 0}, {1, 70, 40}, {0, 20, 130}}, 3, {{0, 275, 65}, {1, 235, 8}, {2, 274, 6}}, 3, {{0, 75, 45}, {1, 152, 105}, {2, 24, 68}}, 3, {{0, 290, 25}, {1, 225, 63}, {2, 260, 110}}, 0, {{1, 20, 10}, {1, 20, 10}, {1, 20, 10}} }; class DMapScreen : public DScreenJob { int i; int x = 0; int var_2C = 0; int nIdleSeconds = 0; int startTime = 0; int runtimer = 0; int curYPos, destYPos; int nLevel, nLevelNew, nLevelBest; public: DMapScreen(int nLevel_, int nLevelNew_, int nLevelBest_) : DScreenJob(fadein|fadeout), nLevel(nLevel_), nLevelNew(nLevelNew_), nLevelBest(nLevelBest_) { curYPos = MapLevelOffsets[nLevel] + (200 * (nLevel / 2)); destYPos = MapLevelOffsets[nLevelNew] + (200 * (nLevelNew / 2)); if (curYPos < destYPos) { var_2C = 2; } if (curYPos > destYPos) { var_2C = -2; } // Trim smoke in widescreen #if 0 vec2_t mapwinxy1 = windowxy1, mapwinxy2 = windowxy2; int32_t width = mapwinxy2.x - mapwinxy1.x + 1, height = mapwinxy2.y - mapwinxy1.y + 1; if (3 * width > 4 * height) { mapwinxy1.x += (width - 4 * height / 3) / 2; mapwinxy2.x -= (width - 4 * height / 3) / 2; } #endif } int Frame(uint64_t clock, bool skiprequest) override { int totalclock = int(clock * 120 / 1'000'000'000); twod->ClearScreen(); if ((totalclock - startTime) / kTimerTicks) { nIdleSeconds++; startTime = totalclock; } int tileY = curYPos; // Draw the background screens for (i = 0; i < 10; i++) { DrawAbs(kTile3353 + i, x, tileY); tileY -= 200; } // for each level - drawing the 'level completed' on-fire smoke markers for (i = 0; i < kMap20; i++) { int screenY = (i >> 1) * -200; if (nLevelBest >= i) // check if the player has finished this level { for (int j = 0; j < MapLevelFires[i].nFires; j++) { int nFireFrame = (((int)totalclock >> 4) & 3); assert(nFireFrame >= 0 && nFireFrame < 4); int nFireType = MapLevelFires[i].fires[j].nFireType; assert(nFireType >= 0 && nFireType < 3); int nTile = FireTiles[nFireType][nFireFrame].nTile; int smokeX = MapLevelFires[i].fires[j].xPos + FireTiles[nFireType][nFireFrame].xOffs; int smokeY = MapLevelFires[i].fires[j].yPos + FireTiles[nFireType][nFireFrame].yOffs + curYPos + screenY; // Use rotatesprite to trim smoke in widescreen DrawAbs(nTile, smokeX, smokeY); // Todo: mask out the sides of the screen if the background is not widescreen. } } int t = ((((int)totalclock & 16) >> 4)); int nTile = mapNamePlaques[i].tiles[t].nTile; int nameX = mapNamePlaques[i].xPos + mapNamePlaques[i].tiles[t].xOffs; int nameY = mapNamePlaques[i].yPos + mapNamePlaques[i].tiles[t].yOffs + curYPos + screenY; // Draw level name plaque DrawAbs(nTile, nameX, nameY); int8_t shade = 96; if (nLevelNew == i) { shade = (Sin(16 * (int)totalclock) + 31) >> 8; } else if (nLevelBest >= i) { shade = 31; } int textY = mapNamePlaques[i].yPos + mapNamePlaques[i].text.yOffs + curYPos + screenY; int textX = mapNamePlaques[i].xPos + mapNamePlaques[i].text.xOffs; nTile = mapNamePlaques[i].text.nTile; // draw the text, alternating between red and black DrawAbs(nTile, textX, textY, shade); } if (curYPos != destYPos) { // scroll the map every couple of ms if (totalclock - runtimer >= (kTimerTicks / 32)) { curYPos += var_2C; runtimer = (int)totalclock; } if (inputState.CheckAllInput()) { if (var_2C < 8) { var_2C *= 2; } } if (curYPos > destYPos&& var_2C > 0) { curYPos = destYPos; } if (curYPos < destYPos && var_2C < 0) { curYPos = destYPos; } nIdleSeconds = 0; } selectedlevelnew = nLevelNew + 1; return skiprequest? -1 : nIdleSeconds < 12? 1 : 0; } bool ProcessInput() override { if (inputState.GetKeyStatus(sc_UpArrow)) { inputState.ClearKeyStatus(sc_UpArrow); if (curYPos == destYPos && nLevelNew <= nLevelBest) { nLevelNew++; assert(nLevelNew < 20); destYPos = MapLevelOffsets[nLevelNew] + (200 * (nLevelNew / 2)); if (curYPos <= destYPos) { var_2C = 2; } else { var_2C = -2; } nIdleSeconds = 0; } return true; } if (inputState.GetKeyStatus(sc_DownArrow)) { inputState.ClearKeyStatus(sc_DownArrow); if (curYPos == destYPos && nLevelNew > 0) { nLevelNew--; assert(nLevelNew >= 0); destYPos = MapLevelOffsets[nLevelNew] + (200 * (nLevelNew / 2)); if (curYPos <= destYPos) { var_2C = 2; } else { var_2C = -2; } nIdleSeconds = 0; } return true; } return false; } }; void menu_DrawTheMap(int nLevel, int nLevelNew, int nLevelBest, TArray &jobs) { if (nLevel > kMap20 || nLevelNew > kMap20) // max single player levels { return; } #ifdef _DEBUG nLevelBest = kMap20; #endif if (nLevel < 1) nLevel = 1; if (nLevelNew < 1) nLevelNew = nLevel; // 0-offset the level numbers jobs.Push( { Create(nLevel-1, nLevelNew-1, nLevelBest-1) }); } //--------------------------------------------------------------------------- // // text overlay // //--------------------------------------------------------------------------- void TextOverlay::Start(int starttime) { lastclock = starttime; } void TextOverlay::ComputeCinemaText() { int i = 0; for (auto &line : screentext) { int nWidth = SmallFont->StringWidth(line); nLeft[i++] = 160 - nWidth / 2; } nCrawlY = 199; nHeight = screentext.Size() * 10; } void TextOverlay::ReadyCinemaText(uint16_t nVal) { FStringf label("TXT_EX_CINEMA%d", nVal + 1); label = GStrings(label); screentext = label.Split("\n"); ComputeCinemaText(); } void TextOverlay::DisplayText() { if (nHeight + nCrawlY > 0) { double y = nCrawlY; unsigned int i = 0; while (i < screentext.Size() && y <= 199) { if (y >= -10) { DrawText(twod, SmallFont, CR_UNDEFINED, nLeft[i], y, screentext[i], DTA_FullscreenScale, FSMode_ScaleToFit43, DTA_VirtualWidth, 320, DTA_VirtualHeight, 200, TAG_DONE); } i++; y += 10; } } } bool TextOverlay::AdvanceCinemaText(int totalclock) { if (nHeight + nCrawlY > 0 || CDplaying()) { nCrawlY-= (totalclock - lastclock) / 15.; // do proper interpolation. lastclock = totalclock; return true; } return false; } //--------------------------------------------------------------------------- // // cinema // //--------------------------------------------------------------------------- static const char * const cinpalfname[] = { "3454.pal", "3452.pal", "3449.pal", "3445.pal", "set.pal", "3448.pal", "3446.pal", "hsc1.pal", "2972.pal", "2973.pal", "2974.pal", "2975.pal", "2976.pal", "heli.pal", "2978.pal", "terror.pal" }; void uploadCinemaPalettes() { for (int i = 0; i < countof(cinpalfname); i++) { uint8_t palette[768] = {}; auto hFile = fileSystem.OpenFileReader(cinpalfname[i]); if (hFile.isOpen()) hFile.Read(palette, 768); for (auto& c : palette) c <<= 2; paletteSetColorTable(ANIMPAL+i, palette, false, true); } } //--------------------------------------------------------------------------- // // cinema // //--------------------------------------------------------------------------- class DCinema : public DScreenJob { TextOverlay text; short cinematile; int currentCinemaPalette; int edx; int check; public: DCinema(int nVal, int checklevel = -1) : DScreenJob(fadein|fadeout) { static const short cinematiles[] = { 3454, 3452, 3449, 3445, 3451, 3448, 3446}; static const int8_t bxvals[] = { 4, 0, 2, 7, 3, 8, 6 }; static const int8_t dxvals[] = { 4, -1, 2, -1, 3, 8, 6 }; if (nVal < 1 || nVal >7) return; cinematile = cinematiles[nVal-1]; currentCinemaPalette = nVal; edx = dxvals[nVal - 1]; text.Start(0); text.ReadyCinemaText(bxvals[nVal - 1]); check = checklevel; } int Frame(uint64_t clock, bool skiprequest) override { if (clock == 0) { if (check > 0 && check != selectedlevelnew) return 0; // immediately abort if the player selected a different level on the map StopAllSounds(); if (edx != -1) { playCDtrack(edx + 2, false); } } twod->ClearScreen(); DrawTexture(twod, tileGetTexture(cinematile), 0, 0, DTA_FullscreenEx, FSMode_ScaleToFit43, DTA_TranslationIndex, TRANSLATION(Translation_BasePalettes, currentCinemaPalette), TAG_DONE); text.DisplayText(); auto cont = text.AdvanceCinemaText(clock * 129 / 1'000'000'000); int ret = skiprequest ? -1 : cont ? 1 : 0; // quit the game if we've finished level 4 and displayed the advert text if (ISDEMOVER && currentCinemaPalette == 3 && ret != 1) { ExitGame(); } return ret; } }; //--------------------------------------------------------------------------- // // last level cinema // //--------------------------------------------------------------------------- class DLastLevelCinema : public DScreenJob { int var_24 = 16; int var_28 = 12; int ebp; int nEndTime = 240; int phase = 0; int nextclock = 4; unsigned int nStringTypeOn, nCharTypeOn; int screencnt = 0; TArray screentext; public: DLastLevelCinema() : DScreenJob(fadein | fadeout) {} private: void DoStatic(int a, int b) { auto pixels = TileFiles.tileMakeWritable(kTileLoboLaptop); int v2 = 160 - a / 2; int v4 = 81 - b / 2; int var_18 = v2 + a; int v5 = v4 + b; auto pTile = (pixels + (200 * v2)) + v4; TileFiles.InvalidateTile(kTileLoboLaptop); while (v2 < var_18) { uint8_t* pStart = pTile; pTile += 200; int v7 = v4; while (v7 < v5) { *pStart = RandomBit() * 16; v7++; pStart++; } v2++; } } void Phase1() { if (var_24 >= 116) { if (var_28 < 192) var_28 += 20; } else { var_24 += 20; } DoStatic(var_28, var_24); } bool InitPhase2() { FStringf label("TXT_EX_LASTLEVEL%d", screencnt + 1); label = GStrings(label); screentext = label.Split("\n"); if (screentext.Size() == 0) return false; nStringTypeOn = 0; nCharTypeOn = 0; ebp = screentext.Size() * 4; // half height of the entire text ebp = 81 - ebp; // offset from the screen's center. auto tex = dynamic_cast(tileGetTexture(kTileLoboLaptop)->GetTexture()->GetImage()); if (tex) tex->Reload(); return true; } bool Phase3() { DoStatic(var_28, var_24); if (var_28 > 20) { var_28 -= 20; return true; } if (var_24 > 20) { var_24 -= 20; return true; } return false; } void DisplayPhase2() { DrawTexture(twod, tileGetTexture(kTileLoboLaptop), 0, 0, DTA_FullscreenEx, FSMode_ScaleToFit43, TAG_DONE); int yy = ebp; for (int i = 0; i < nStringTypeOn; i++, yy += 8) { DrawText(twod, SmallFont2, CR_UNTRANSLATED, 70, yy, screentext[i], DTA_FullscreenScale, FSMode_ScaleToFit43, DTA_VirtualWidth, 320, DTA_VirtualHeight, 200, TAG_DONE); } DrawText(twod, SmallFont2, CR_UNTRANSLATED, 70, yy, screentext[nStringTypeOn], DTA_FullscreenScale, FSMode_ScaleToFit43, DTA_VirtualWidth, 320, DTA_VirtualHeight, 200, DTA_TextLen, nCharTypeOn, TAG_DONE); } int Frame(uint64_t clock, bool skiprequest) override { if (clock == 0) { PlayLocalSound(StaticSound[kSound75], 0, false, CHANF_UI); phase = 1; } int totalclock = clock * 120 / 1'000'000'000; switch (phase) { case 1: if (totalclock >= nextclock) { Phase1(); nextclock += 4; } DrawTexture(twod, tileGetTexture(kTileLoboLaptop), 0, 0, DTA_FullscreenEx, FSMode_ScaleToFit43, TAG_DONE); if (skiprequest || totalclock >= 240) { InitPhase2(); phase = 2; skiprequest = 0; } break; case 2: if (totalclock >= nextclock) { if (screentext[nStringTypeOn][nCharTypeOn] != ' ') PlayLocalSound(StaticSound[kSound71], 0, false, CHANF_UI); nCharTypeOn++; nextclock += 4; if (screentext[nStringTypeOn][nCharTypeOn] == 0) { nCharTypeOn = 0; nStringTypeOn++; if (nStringTypeOn >= screentext.Size()) { nextclock = (kTimerTicks * (screentext.Size() + 2)) + (int)totalclock; phase = 3; } } } DisplayPhase2(); if (skiprequest) { nextclock = (kTimerTicks * (screentext.Size() + 2)) + (int)totalclock; phase = 3; } break; case 3: DisplayPhase2(); if (totalclock >= nextclock || skiprequest) { PlayLocalSound(StaticSound[kSound75], 0, false, CHANF_UI); phase = 4; nextclock = totalclock + 240; skiprequest = 0; } break; case 4: if (totalclock >= nextclock) { skiprequest |= !Phase3(); nextclock += 4; } if (skiprequest || totalclock >= 240) { // Go to the next text page. if (screencnt != 2) { screencnt++; nextclock = totalclock + 240; skiprequest = 0; phase = 1; } else return skiprequest ? -1 : 0; } } return 1; } }; //--------------------------------------------------------------------------- // // Credits roll // //--------------------------------------------------------------------------- class DExCredits : public DScreenJob { TArray credits; TArray pagelines; uint64_t page; uint64_t pagetime; public: DExCredits() { auto textdata = fileSystem.LoadFile("credits.txt", 1); FString text = (char*)textdata.Data(); text.Substitute("\r", ""); credits = text.Split("\n\n"); } private: int Frame(uint64_t clock, bool skiprequest) override { if (clock == 0) { if (credits.Size() == 0) return 0; playCDtrack(19, false); pagetime = 0; page = -1; } if (clock >= pagetime || skiprequest) { page++; if (page < credits.Size()) pagelines = credits[page].Split("\n"); else { if (skiprequest || !CDplaying()) return 0; pagelines.Clear(); } pagetime = clock + 2'000'000'000; // } twod->ClearScreen(); int y = 100 - ((10 * (pagelines.Size() - 1)) / 2); for (unsigned i = 0; i < pagelines.Size(); i++) { uint64_t ptime = (pagetime-clock) / 1'000'000; int light; if (ptime < 255) light = ptime; else if (ptime > 2000 - 255) light = 2000 - ptime; else light = 255; auto color = PalEntry(255, light, light, light); int nStringWidth = SmallFont->StringWidth(pagelines[i]); DrawText(twod, SmallFont, CR_UNTRANSLATED, 160 - nStringWidth / 2, y, pagelines[i], DTA_FullscreenScale, FSMode_ScaleToFit43, DTA_VirtualWidth, 320, DTA_VirtualHeight, 200, DTA_Color, color, TAG_DONE); y += 10; } return 1; } }; //--------------------------------------------------------------------------- // // player died // //--------------------------------------------------------------------------- void DoGameOverScene(bool finallevel) { JobDesc job; if (finallevel) { playCDtrack(9, false); //FadeToWhite(); job = { Create(4) }; } else { StopCD(); PlayGameOverSound(); job = { Create(tileGetTexture(kTile3591), DScreenJob::fadein | DScreenJob::fadeout, 0x7fffffff) }; } RunScreenJob(&job, 1, [](bool) { gamestate = GS_STARTUP; }); } void DoAfterCinemaScene(int nLevel, TArray& jobs) { static const uint8_t nAfterScene[] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 7, 0, 0, 0, 0, 6 }; if (nAfterScene[nLevel]) jobs.Push({ Create(nAfterScene[nLevel]) }); if (nLevel == 20) jobs.Push({ Create() }); } void DoBeforeCinemaScene(int nLevel, TArray& jobs) { if (nLevel == 5) jobs.Push({ Create(3) }); else if (nLevel == 11) jobs.Push({ Create(1, 11) }); } END_PS_NS