1
0
Fork 0
mirror of https://github.com/TerryCavanagh/VVVVVV.git synced 2024-11-19 01:19:41 +01:00
VVVVVV/desktop_version/src/Game.cpp

7539 lines
209 KiB
C++
Raw Normal View History

#define GAME_DEFINITION
2020-01-01 21:29:24 +01:00
#include "Game.h"
#include <sstream>
2020-01-01 21:29:24 +01:00
#include <stdlib.h>
#include <string.h>
#include <tinyxml2.h>
2020-01-01 21:29:24 +01:00
#include "ButtonGlyphs.h"
#include "Constants.h"
#include "CustomLevels.h"
#include "DeferCallbacks.h"
#include "Editor.h"
#include "Entity.h"
#include "Enums.h"
#include "FileSystemUtils.h"
Start rewrite of font system This is still a work in progress, but the existing font system has been removed and replaced by a new one, in Font.cpp. Design goals of the new font system include supporting colored button glyphs, different fonts for different languages, and larger fonts than 8x8 for Chinese, Japanese and Korean, while being able to support their 30000+ characters without hiccups, slowdowns or high memory usage. And to have more flexibility with fonts in general. Plus, Graphics.cpp was long enough as-is, so it's good to have a dedicated file for font storage. The old font system worked with a std::vector<SDL_Surface*> to store 8x8 surfaces for each character, and a std::map<int,int> to store mappings between codepoints and vector indexes. The new system has a per-font collection of pages for every block of 0x1000 (4096) codepoints, that may be allocated as needed. A glyph on a page contains the index of the glyph in the image (giving its coordinates), the advance (how much the cursor should advance, so the width of that glyph) and some flags which would be at least whether the glyph exists and whether it is colored. Most of the *new* features aren't implemented yet; it's currently hardcoded to the regular 8x8 font.png, but it should be functionally equivalent to the previous behavior. The only thing that doesn't really work yet is level-specific font.png, but that'll be supported again soon enough. This commit also adds fontmeta (xml) support. Since the fonts folder is mounted at graphics/, there are two main options for recognizing non-font.png fonts: the font files have to be prefixed with font (or font_) or some special file extension is involved to signal what files are fonts. I always had a font.xml in mind (so font_cn.xml, font_ja.xml, etc) but if there's ever gonna be a need for further xml files inside the graphics folder, we have a problem. So I named them .fontmeta instead. A .fontmeta file looks somewhat like this: <?xml version="1.0" encoding="UTF-8"?> <font_metadata> <width>12</width> <height>12</height> <white_teeth>1</white_teeth> <chars> <range start="0x20" end="0x7E"/> <range start="0x80" end="0x80"/> <range start="0xA0" end="0xDF"/> <range start="0x250" end="0x2A8"/> <range start="0x2AD" end="0x2AD"/> <range start="0x2C7" end="0x2C7"/> <range start="0x2C9" end="0x2CB"/> ... </chars> <special> <range start="0x00" end="0x1F" advance="6"/> <range start="0x61" end="0x66" color="1"/> <range start="0x63" end="0x63" color="0"/> </special> </font_metadata> The <chars> tag can be used to specify characters instead of in a .txt. The original idea was to just always use the existing .txt system for specifying the font charset, and only use the XML for the other stuff that the .txt doesn't cover. However, it's probably better to keep it simple if possible - having to only have a .png and a .fontmeta seems simpler than having the data spread out over three files. And a major advantage: Chinese fonts can have about 30000 characters! It's more efficient to be able to have a tag saying "now there's 20902 characters starting at U+4E00" than to include them all in a text file and having to UTF-8 decode every single one of them. If a font.txt exists, it takes priority over the <chars> tag, and in that case, there's no reason to include the <chars> tag in the XML. But font.txt has to be in the same directory as font.png, otherwise it is rejected. Same for font.fontmeta. If neither font.txt nor <chars> exist, then the font is seen as a 2.2-and-below-style ASCII font. In <special>: advance is the number of pixels the cursor advances after drawing the character (so the width of the character, without affecting the grid in the source image), color is whether the character should have its original colors retained when printed (for button glyphs). As for <white_teeth>: The renderer PR has replaced draw-time whitening of sprites/etc (using BlitSurfaceColoured) by load-time whitening of entire images (using LoadImage with TEX_WHITE as an argument). This means we have a problem: fonts have always had their glyphs whitened at printing time, and since I'm adding support for colored button glyphs, I changed it so glyphs would sometimes not be whitened. But if we can't whiten at print time, then we'd need to whiten at load time, and if we whiten the entire font, any colored glyphs will get destroyed too. If you whiten the image selectively, well, we need more code to target specific squares in the image, and it's kind of a waste when you need to whiten 30000 12x12 Chinese characters when you're only going to need a handful, but you don't know which ones. The solution: Whitening fonts is useless if all the non-colored glyphs are already white, so we don't need to do it anyway! However, any existing fonts that have non-white glyphs (and I know of at least one level like that) will still need to be whitened. So there is now a font property <white_teeth> that can be specified in the fontmeta, which indicates that the font is already pre-whitened. If not specified, traditional whitening behavior will be used, and the font cannot use colored glyphs.
2023-01-02 05:14:53 +01:00
#include "Font.h"
#include "GlitchrunnerMode.h"
#include "Graphics.h"
#include "LevelDebugger.h"
#include "Localization.h"
#include "LocalizationStorage.h"
#include "KeyPoll.h"
2020-01-01 21:29:24 +01:00
#include "MakeAndPlay.h"
#include "Map.h"
#include "Music.h"
#include "Network.h"
#include "RoomnameTranslator.h"
#include "Screen.h"
#include "Script.h"
#include "Unused.h"
#include "UtilityClass.h"
#include "VFormat.h"
#include "Vlogging.h"
#include "XMLUtils.h"
2020-01-01 21:29:24 +01:00
static bool GetButtonFromString(const char *pText, SDL_GameControllerButton *button)
2020-01-01 21:29:24 +01:00
{
if (*pText == '0' ||
*pText == 'a' ||
*pText == 'A')
{
*button = SDL_CONTROLLER_BUTTON_A;
return true;
}
Reduce dependency on libc functions During 2.3 development, there's been a gradual shift to using SDL stdlib functions instead of libc functions, but there are still some libc functions (or the same libc function but from the STL) in the code. Well, this patch replaces all the rest of them in one fell swoop. SDL's stdlib can replace most of these, but its SDL_min() and SDL_max() are inadequate - they aren't really functions, they're more like macros with a nasty penchant for double-evaluation. So I just made my own VVV_min() and VVV_max() functions and placed them in Maths.h instead, then replaced all the previous usages of min(), max(), std::min(), std::max(), SDL_min(), and SDL_max() with VVV_min() and VVV_max(). Additionally, there's no SDL_isxdigit(), so I just implemented my own VVV_isxdigit(). SDL has SDL_malloc() and SDL_free(), but they have some refcounting built in to them, so in order to use them with LodePNG, I have to replace the malloc() and free() that LodePNG uses. Which isn't too hard, I did it in a new file called ThirdPartyDeps.c, and LodePNG is now compiled with the LODEPNG_NO_COMPILE_ALLOCATORS definition. Lastly, I also refactored the awful strcpy() and strcat() usages in PLATFORM_migrateSaveData() to use SDL_snprintf() instead. I know save migration is getting axed in 2.4, but it still bothers me to have something like that in the codebase otherwise. Without further ado, here is the full list of functions that the codebase now uses: - SDL_strlcpy() instead of strcpy() - SDL_strlcat() instead of strcat() - SDL_snprintf() instead of sprintf(), strcpy(), or strcat() (see above) - VVV_min() instead of min(), std::min(), or SDL_min() - VVV_max() instead of max(), std::max(), or SDL_max() - VVV_isxdigit() instead of isxdigit() - SDL_strcmp() instead of strcmp() - SDL_strcasecmp() instead of strcasecmp() or Win32 strcmpi() - SDL_strstr() instead of strstr() - SDL_strlen() instead of strlen() - SDL_sscanf() instead of sscanf() - SDL_getenv() instead of getenv() - SDL_malloc() instead of malloc() (replacing in LodePNG as well) - SDL_free() instead of free() (replacing in LodePNG as well)
2021-01-12 01:17:45 +01:00
if (SDL_strcmp(pText, "1") == 0 ||
*pText == 'b' ||
*pText == 'B')
{
*button = SDL_CONTROLLER_BUTTON_B;
return true;
}
if (*pText == '2' ||
*pText == 'x' ||
*pText == 'X')
{
*button = SDL_CONTROLLER_BUTTON_X;
return true;
}
if (*pText == '3' ||
*pText == 'y' ||
*pText == 'Y')
{
*button = SDL_CONTROLLER_BUTTON_Y;
return true;
}
if (*pText == '4' ||
Reduce dependency on libc functions During 2.3 development, there's been a gradual shift to using SDL stdlib functions instead of libc functions, but there are still some libc functions (or the same libc function but from the STL) in the code. Well, this patch replaces all the rest of them in one fell swoop. SDL's stdlib can replace most of these, but its SDL_min() and SDL_max() are inadequate - they aren't really functions, they're more like macros with a nasty penchant for double-evaluation. So I just made my own VVV_min() and VVV_max() functions and placed them in Maths.h instead, then replaced all the previous usages of min(), max(), std::min(), std::max(), SDL_min(), and SDL_max() with VVV_min() and VVV_max(). Additionally, there's no SDL_isxdigit(), so I just implemented my own VVV_isxdigit(). SDL has SDL_malloc() and SDL_free(), but they have some refcounting built in to them, so in order to use them with LodePNG, I have to replace the malloc() and free() that LodePNG uses. Which isn't too hard, I did it in a new file called ThirdPartyDeps.c, and LodePNG is now compiled with the LODEPNG_NO_COMPILE_ALLOCATORS definition. Lastly, I also refactored the awful strcpy() and strcat() usages in PLATFORM_migrateSaveData() to use SDL_snprintf() instead. I know save migration is getting axed in 2.4, but it still bothers me to have something like that in the codebase otherwise. Without further ado, here is the full list of functions that the codebase now uses: - SDL_strlcpy() instead of strcpy() - SDL_strlcat() instead of strcat() - SDL_snprintf() instead of sprintf(), strcpy(), or strcat() (see above) - VVV_min() instead of min(), std::min(), or SDL_min() - VVV_max() instead of max(), std::max(), or SDL_max() - VVV_isxdigit() instead of isxdigit() - SDL_strcmp() instead of strcmp() - SDL_strcasecmp() instead of strcasecmp() or Win32 strcmpi() - SDL_strstr() instead of strstr() - SDL_strlen() instead of strlen() - SDL_sscanf() instead of sscanf() - SDL_getenv() instead of getenv() - SDL_malloc() instead of malloc() (replacing in LodePNG as well) - SDL_free() instead of free() (replacing in LodePNG as well)
2021-01-12 01:17:45 +01:00
SDL_strcasecmp(pText, "BACK") == 0)
{
*button = SDL_CONTROLLER_BUTTON_BACK;
return true;
}
if (*pText == '5' ||
Reduce dependency on libc functions During 2.3 development, there's been a gradual shift to using SDL stdlib functions instead of libc functions, but there are still some libc functions (or the same libc function but from the STL) in the code. Well, this patch replaces all the rest of them in one fell swoop. SDL's stdlib can replace most of these, but its SDL_min() and SDL_max() are inadequate - they aren't really functions, they're more like macros with a nasty penchant for double-evaluation. So I just made my own VVV_min() and VVV_max() functions and placed them in Maths.h instead, then replaced all the previous usages of min(), max(), std::min(), std::max(), SDL_min(), and SDL_max() with VVV_min() and VVV_max(). Additionally, there's no SDL_isxdigit(), so I just implemented my own VVV_isxdigit(). SDL has SDL_malloc() and SDL_free(), but they have some refcounting built in to them, so in order to use them with LodePNG, I have to replace the malloc() and free() that LodePNG uses. Which isn't too hard, I did it in a new file called ThirdPartyDeps.c, and LodePNG is now compiled with the LODEPNG_NO_COMPILE_ALLOCATORS definition. Lastly, I also refactored the awful strcpy() and strcat() usages in PLATFORM_migrateSaveData() to use SDL_snprintf() instead. I know save migration is getting axed in 2.4, but it still bothers me to have something like that in the codebase otherwise. Without further ado, here is the full list of functions that the codebase now uses: - SDL_strlcpy() instead of strcpy() - SDL_strlcat() instead of strcat() - SDL_snprintf() instead of sprintf(), strcpy(), or strcat() (see above) - VVV_min() instead of min(), std::min(), or SDL_min() - VVV_max() instead of max(), std::max(), or SDL_max() - VVV_isxdigit() instead of isxdigit() - SDL_strcmp() instead of strcmp() - SDL_strcasecmp() instead of strcasecmp() or Win32 strcmpi() - SDL_strstr() instead of strstr() - SDL_strlen() instead of strlen() - SDL_sscanf() instead of sscanf() - SDL_getenv() instead of getenv() - SDL_malloc() instead of malloc() (replacing in LodePNG as well) - SDL_free() instead of free() (replacing in LodePNG as well)
2021-01-12 01:17:45 +01:00
SDL_strcasecmp(pText, "GUIDE") == 0)
{
*button = SDL_CONTROLLER_BUTTON_GUIDE;
return true;
}
if (*pText == '6' ||
Reduce dependency on libc functions During 2.3 development, there's been a gradual shift to using SDL stdlib functions instead of libc functions, but there are still some libc functions (or the same libc function but from the STL) in the code. Well, this patch replaces all the rest of them in one fell swoop. SDL's stdlib can replace most of these, but its SDL_min() and SDL_max() are inadequate - they aren't really functions, they're more like macros with a nasty penchant for double-evaluation. So I just made my own VVV_min() and VVV_max() functions and placed them in Maths.h instead, then replaced all the previous usages of min(), max(), std::min(), std::max(), SDL_min(), and SDL_max() with VVV_min() and VVV_max(). Additionally, there's no SDL_isxdigit(), so I just implemented my own VVV_isxdigit(). SDL has SDL_malloc() and SDL_free(), but they have some refcounting built in to them, so in order to use them with LodePNG, I have to replace the malloc() and free() that LodePNG uses. Which isn't too hard, I did it in a new file called ThirdPartyDeps.c, and LodePNG is now compiled with the LODEPNG_NO_COMPILE_ALLOCATORS definition. Lastly, I also refactored the awful strcpy() and strcat() usages in PLATFORM_migrateSaveData() to use SDL_snprintf() instead. I know save migration is getting axed in 2.4, but it still bothers me to have something like that in the codebase otherwise. Without further ado, here is the full list of functions that the codebase now uses: - SDL_strlcpy() instead of strcpy() - SDL_strlcat() instead of strcat() - SDL_snprintf() instead of sprintf(), strcpy(), or strcat() (see above) - VVV_min() instead of min(), std::min(), or SDL_min() - VVV_max() instead of max(), std::max(), or SDL_max() - VVV_isxdigit() instead of isxdigit() - SDL_strcmp() instead of strcmp() - SDL_strcasecmp() instead of strcasecmp() or Win32 strcmpi() - SDL_strstr() instead of strstr() - SDL_strlen() instead of strlen() - SDL_sscanf() instead of sscanf() - SDL_getenv() instead of getenv() - SDL_malloc() instead of malloc() (replacing in LodePNG as well) - SDL_free() instead of free() (replacing in LodePNG as well)
2021-01-12 01:17:45 +01:00
SDL_strcasecmp(pText, "START") == 0)
{
*button = SDL_CONTROLLER_BUTTON_START;
return true;
}
if (*pText == '7' ||
Reduce dependency on libc functions During 2.3 development, there's been a gradual shift to using SDL stdlib functions instead of libc functions, but there are still some libc functions (or the same libc function but from the STL) in the code. Well, this patch replaces all the rest of them in one fell swoop. SDL's stdlib can replace most of these, but its SDL_min() and SDL_max() are inadequate - they aren't really functions, they're more like macros with a nasty penchant for double-evaluation. So I just made my own VVV_min() and VVV_max() functions and placed them in Maths.h instead, then replaced all the previous usages of min(), max(), std::min(), std::max(), SDL_min(), and SDL_max() with VVV_min() and VVV_max(). Additionally, there's no SDL_isxdigit(), so I just implemented my own VVV_isxdigit(). SDL has SDL_malloc() and SDL_free(), but they have some refcounting built in to them, so in order to use them with LodePNG, I have to replace the malloc() and free() that LodePNG uses. Which isn't too hard, I did it in a new file called ThirdPartyDeps.c, and LodePNG is now compiled with the LODEPNG_NO_COMPILE_ALLOCATORS definition. Lastly, I also refactored the awful strcpy() and strcat() usages in PLATFORM_migrateSaveData() to use SDL_snprintf() instead. I know save migration is getting axed in 2.4, but it still bothers me to have something like that in the codebase otherwise. Without further ado, here is the full list of functions that the codebase now uses: - SDL_strlcpy() instead of strcpy() - SDL_strlcat() instead of strcat() - SDL_snprintf() instead of sprintf(), strcpy(), or strcat() (see above) - VVV_min() instead of min(), std::min(), or SDL_min() - VVV_max() instead of max(), std::max(), or SDL_max() - VVV_isxdigit() instead of isxdigit() - SDL_strcmp() instead of strcmp() - SDL_strcasecmp() instead of strcasecmp() or Win32 strcmpi() - SDL_strstr() instead of strstr() - SDL_strlen() instead of strlen() - SDL_sscanf() instead of sscanf() - SDL_getenv() instead of getenv() - SDL_malloc() instead of malloc() (replacing in LodePNG as well) - SDL_free() instead of free() (replacing in LodePNG as well)
2021-01-12 01:17:45 +01:00
SDL_strcasecmp(pText, "LS") == 0)
{
*button = SDL_CONTROLLER_BUTTON_LEFTSTICK;
return true;
}
if (*pText == '8' ||
Reduce dependency on libc functions During 2.3 development, there's been a gradual shift to using SDL stdlib functions instead of libc functions, but there are still some libc functions (or the same libc function but from the STL) in the code. Well, this patch replaces all the rest of them in one fell swoop. SDL's stdlib can replace most of these, but its SDL_min() and SDL_max() are inadequate - they aren't really functions, they're more like macros with a nasty penchant for double-evaluation. So I just made my own VVV_min() and VVV_max() functions and placed them in Maths.h instead, then replaced all the previous usages of min(), max(), std::min(), std::max(), SDL_min(), and SDL_max() with VVV_min() and VVV_max(). Additionally, there's no SDL_isxdigit(), so I just implemented my own VVV_isxdigit(). SDL has SDL_malloc() and SDL_free(), but they have some refcounting built in to them, so in order to use them with LodePNG, I have to replace the malloc() and free() that LodePNG uses. Which isn't too hard, I did it in a new file called ThirdPartyDeps.c, and LodePNG is now compiled with the LODEPNG_NO_COMPILE_ALLOCATORS definition. Lastly, I also refactored the awful strcpy() and strcat() usages in PLATFORM_migrateSaveData() to use SDL_snprintf() instead. I know save migration is getting axed in 2.4, but it still bothers me to have something like that in the codebase otherwise. Without further ado, here is the full list of functions that the codebase now uses: - SDL_strlcpy() instead of strcpy() - SDL_strlcat() instead of strcat() - SDL_snprintf() instead of sprintf(), strcpy(), or strcat() (see above) - VVV_min() instead of min(), std::min(), or SDL_min() - VVV_max() instead of max(), std::max(), or SDL_max() - VVV_isxdigit() instead of isxdigit() - SDL_strcmp() instead of strcmp() - SDL_strcasecmp() instead of strcasecmp() or Win32 strcmpi() - SDL_strstr() instead of strstr() - SDL_strlen() instead of strlen() - SDL_sscanf() instead of sscanf() - SDL_getenv() instead of getenv() - SDL_malloc() instead of malloc() (replacing in LodePNG as well) - SDL_free() instead of free() (replacing in LodePNG as well)
2021-01-12 01:17:45 +01:00
SDL_strcasecmp(pText, "RS") == 0)
{
*button = SDL_CONTROLLER_BUTTON_RIGHTSTICK;
return true;
}
if (*pText == '9' ||
Reduce dependency on libc functions During 2.3 development, there's been a gradual shift to using SDL stdlib functions instead of libc functions, but there are still some libc functions (or the same libc function but from the STL) in the code. Well, this patch replaces all the rest of them in one fell swoop. SDL's stdlib can replace most of these, but its SDL_min() and SDL_max() are inadequate - they aren't really functions, they're more like macros with a nasty penchant for double-evaluation. So I just made my own VVV_min() and VVV_max() functions and placed them in Maths.h instead, then replaced all the previous usages of min(), max(), std::min(), std::max(), SDL_min(), and SDL_max() with VVV_min() and VVV_max(). Additionally, there's no SDL_isxdigit(), so I just implemented my own VVV_isxdigit(). SDL has SDL_malloc() and SDL_free(), but they have some refcounting built in to them, so in order to use them with LodePNG, I have to replace the malloc() and free() that LodePNG uses. Which isn't too hard, I did it in a new file called ThirdPartyDeps.c, and LodePNG is now compiled with the LODEPNG_NO_COMPILE_ALLOCATORS definition. Lastly, I also refactored the awful strcpy() and strcat() usages in PLATFORM_migrateSaveData() to use SDL_snprintf() instead. I know save migration is getting axed in 2.4, but it still bothers me to have something like that in the codebase otherwise. Without further ado, here is the full list of functions that the codebase now uses: - SDL_strlcpy() instead of strcpy() - SDL_strlcat() instead of strcat() - SDL_snprintf() instead of sprintf(), strcpy(), or strcat() (see above) - VVV_min() instead of min(), std::min(), or SDL_min() - VVV_max() instead of max(), std::max(), or SDL_max() - VVV_isxdigit() instead of isxdigit() - SDL_strcmp() instead of strcmp() - SDL_strcasecmp() instead of strcasecmp() or Win32 strcmpi() - SDL_strstr() instead of strstr() - SDL_strlen() instead of strlen() - SDL_sscanf() instead of sscanf() - SDL_getenv() instead of getenv() - SDL_malloc() instead of malloc() (replacing in LodePNG as well) - SDL_free() instead of free() (replacing in LodePNG as well)
2021-01-12 01:17:45 +01:00
SDL_strcasecmp(pText, "LB") == 0)
{
*button = SDL_CONTROLLER_BUTTON_LEFTSHOULDER;
return true;
}
Reduce dependency on libc functions During 2.3 development, there's been a gradual shift to using SDL stdlib functions instead of libc functions, but there are still some libc functions (or the same libc function but from the STL) in the code. Well, this patch replaces all the rest of them in one fell swoop. SDL's stdlib can replace most of these, but its SDL_min() and SDL_max() are inadequate - they aren't really functions, they're more like macros with a nasty penchant for double-evaluation. So I just made my own VVV_min() and VVV_max() functions and placed them in Maths.h instead, then replaced all the previous usages of min(), max(), std::min(), std::max(), SDL_min(), and SDL_max() with VVV_min() and VVV_max(). Additionally, there's no SDL_isxdigit(), so I just implemented my own VVV_isxdigit(). SDL has SDL_malloc() and SDL_free(), but they have some refcounting built in to them, so in order to use them with LodePNG, I have to replace the malloc() and free() that LodePNG uses. Which isn't too hard, I did it in a new file called ThirdPartyDeps.c, and LodePNG is now compiled with the LODEPNG_NO_COMPILE_ALLOCATORS definition. Lastly, I also refactored the awful strcpy() and strcat() usages in PLATFORM_migrateSaveData() to use SDL_snprintf() instead. I know save migration is getting axed in 2.4, but it still bothers me to have something like that in the codebase otherwise. Without further ado, here is the full list of functions that the codebase now uses: - SDL_strlcpy() instead of strcpy() - SDL_strlcat() instead of strcat() - SDL_snprintf() instead of sprintf(), strcpy(), or strcat() (see above) - VVV_min() instead of min(), std::min(), or SDL_min() - VVV_max() instead of max(), std::max(), or SDL_max() - VVV_isxdigit() instead of isxdigit() - SDL_strcmp() instead of strcmp() - SDL_strcasecmp() instead of strcasecmp() or Win32 strcmpi() - SDL_strstr() instead of strstr() - SDL_strlen() instead of strlen() - SDL_sscanf() instead of sscanf() - SDL_getenv() instead of getenv() - SDL_malloc() instead of malloc() (replacing in LodePNG as well) - SDL_free() instead of free() (replacing in LodePNG as well)
2021-01-12 01:17:45 +01:00
if (SDL_strcmp(pText, "10") == 0 ||
SDL_strcasecmp(pText, "RB") == 0)
{
*button = SDL_CONTROLLER_BUTTON_RIGHTSHOULDER;
return true;
}
return false;
2020-01-01 21:29:24 +01:00
}
// Unfortunate forward-declare... My hands are pretty tied
static void loadthissummary(
const char* filename,
struct Game::Summary* summary,
tinyxml2::XMLDocument& doc
);
static struct Game::Summary get_summary(
const char* filename,
const char* savename,
tinyxml2::XMLDocument& doc
) {
tinyxml2::XMLHandle hDoc(&doc);
struct Game::Summary summary;
SDL_zero(summary);
if (!FILESYSTEM_loadTiXml2Document(filename, doc))
{
vlog_info("%s not found", savename);
return summary;
}
loadthissummary(savename, &summary, doc);
return summary;
}
2020-01-01 21:29:24 +01:00
Allow using help/graphics/music/game/key/map/obj everywhere This commit makes `help`, `graphics`, `music`, `game`, `key`, `map`, and `obj` essentially static global objects that can be used everywhere. This is useful in case we ever need to add a new function in the future, so we don't have to bother with passing a new argument in which means we have to pass a new argument in to the function that calls that function which means having to pass a new argument into the function that calls THAT function, etc. which is a real headache when working on fan mods of the source code. Note that this changes NONE of the existing function signatures, it merely just makes those variables accessible everywhere in the same way `script` and `ed` are. Also note that some classes had to be initialized after the filesystem was initialized, but C++ would keep initializing them before the filesystem got initialized, because I *had* to put them at the top of `main.cpp`, or else they wouldn't be global variables. The only way to work around this was to use entityclass's initialization style (which I'm pretty sure entityclass of all things doesn't need to be initialized this way), where you actually initialize the class in an `init()` function, and so then you do `graphics.init()` after the filesystem initialization, AFTER doing `Graphics graphics` up at the top. I've had to do this for `graphics` (but only because its child GraphicsResources `grphx` needs to be initialized this way), `music`, and `game`. I don't think this will affect anything. Other than that, `help`, `key`, and `map` are still using the C++-intended method of having ClassName::ClassName() functions.
2020-01-29 08:35:03 +01:00
void Game::init(void)
2020-01-01 21:29:24 +01:00
{
SDL_strlcpy(magic, "[vVvVvV]game", sizeof(magic));
roomx = 0;
roomy = 0;
prevroomx = 0;
prevroomy = 0;
saverx = 0;
savery = 0;
savecolour = 0;
Allow using help/graphics/music/game/key/map/obj everywhere This commit makes `help`, `graphics`, `music`, `game`, `key`, `map`, and `obj` essentially static global objects that can be used everywhere. This is useful in case we ever need to add a new function in the future, so we don't have to bother with passing a new argument in which means we have to pass a new argument in to the function that calls that function which means having to pass a new argument into the function that calls THAT function, etc. which is a real headache when working on fan mods of the source code. Note that this changes NONE of the existing function signatures, it merely just makes those variables accessible everywhere in the same way `script` and `ed` are. Also note that some classes had to be initialized after the filesystem was initialized, but C++ would keep initializing them before the filesystem got initialized, because I *had* to put them at the top of `main.cpp`, or else they wouldn't be global variables. The only way to work around this was to use entityclass's initialization style (which I'm pretty sure entityclass of all things doesn't need to be initialized this way), where you actually initialize the class in an `init()` function, and so then you do `graphics.init()` after the filesystem initialization, AFTER doing `Graphics graphics` up at the top. I've had to do this for `graphics` (but only because its child GraphicsResources `grphx` needs to be initialized this way), `music`, and `game`. I don't think this will affect anything. Other than that, `help`, `key`, and `map` are still using the C++-intended method of having ClassName::ClassName() functions.
2020-01-29 08:35:03 +01:00
mutebutton = 0;
2020-01-01 21:29:24 +01:00
muted = false;
musicmuted = false;
musicmutebutton = 0;
2020-01-01 21:29:24 +01:00
glitchrunkludge = false;
gamestate = TITLEMODE;
prevgamestate = TITLEMODE;
2020-01-01 21:29:24 +01:00
hascontrol = true;
jumpheld = false;
advancetext = false;
jumppressed = 0;
gravitycontrol = 0;
teleport = false;
edteleportent = 0; //Added in the port!
companion = 0;
quickrestartkludge = false;
tapleft = 0;
tapright = 0;
press_right = false;
press_left = false;
press_action = false;
press_map = false;
press_interact = false;
interactheld = false;
separate_interact = false;
mapheld = false;
2020-01-01 21:29:24 +01:00
pausescript = false;
completestop = false;
activeactivity = -1;
act_fade = 0;
prev_act_fade = 0;
2020-01-01 21:29:24 +01:00
backgroundtext = false;
startscript = false;
inintermission = false;
alarmon = false;
alarmdelay = 0;
blackout = false;
creditposx = 0;
creditposy = 0;
creditposdelay = 0;
oldcreditposx = 0;
2020-01-01 21:29:24 +01:00
useteleporter = false;
teleport_to_teleporter = 0;
activetele = false;
readytotele = 0;
oldreadytotele = 0;
2020-01-01 21:29:24 +01:00
activity_r = 0;
activity_g = 0;
activity_b = 0;
activity_y = 0;
2020-01-01 21:29:24 +01:00
creditposition = 0;
oldcreditposition = 0;
2020-01-01 21:29:24 +01:00
bestgamedeaths = -1;
//Accessibility Options
colourblindmode = false;
noflashingmode = false;
slowdown = 30;
nodeathmode = false;
nocutscenes = false;
ndmresultcrewrescued = 0;
ndmresulttrinkets = 0;
2020-01-01 21:29:24 +01:00
customcol=0;
SDL_memset(crewstats, false, sizeof(crewstats));
SDL_memset(ndmresultcrewstats, false, sizeof(ndmresultcrewstats));
SDL_memset(besttimes, -1, sizeof(besttimes));
2020-07-01 05:02:18 +02:00
SDL_memset(bestframes, -1, sizeof(bestframes));
SDL_memset(besttrinkets, -1, sizeof(besttrinkets));
SDL_memset(bestlives, -1, sizeof(bestlives));
SDL_memset(bestrank, -1, sizeof(bestrank));
2020-01-01 21:29:24 +01:00
crewstats[0] = true;
lastsaved = 0;
//Menu stuff initiliased here:
SDL_memset(unlock, false, sizeof(unlock));
SDL_memset(unlocknotify, false, sizeof(unlock));
2020-01-01 21:29:24 +01:00
currentmenuoption = 0;
menutestmode = false;
current_credits_list_index = 0;
translator_credits_pagenum = 0;
2020-01-01 21:29:24 +01:00
menuxoff = 0;
menuyoff = 0;
menucountdown = 0;
levelpage=0;
playcustomlevel=0;
silence_settings_error = false;
2020-01-01 21:29:24 +01:00
deathcounts = 0;
gameoverdelay = 0;
framecounter = 0;
seed_use_sdl_getticks = false;
editor_disabled = false;
resetgameclock();
2020-01-01 21:29:24 +01:00
gamesaved = false;
2020-11-04 03:45:33 +01:00
gamesavefailed = false;
2020-01-01 21:29:24 +01:00
savetime = "00:00";
savetrinkets = 0;
intimetrial = false;
timetrialcountdown = 0;
timetrialshinytarget = 0;
timetrialparlost = false;
timetrialpar = 0;
timetrialcheater = false;
2020-01-01 21:29:24 +01:00
timetrialresulttime = 0;
timetrialresultframes = 0;
timetrialresultshinytarget = 0;
timetrialresulttrinkets = 0;
timetrialresultpar = 0;
timetrialresultdeaths = 0;
Add level exploring menu for translators I would, of course, recommend translators to translate the roomnames while playing the full game (optionally in invincibility) so they can immediately get all the context and maybe the most inspiration. And if you want to go back into a specific level, then there's always the time trials and intermission replays which will give you full coverage of all the room names. However, the time trials weren't really made for room name translation. They have some annoying features like the instant restart when you press ENTER at the wrong time, they remove context clues like teleporters and companions, but the worst problem is that the last room in a level is often completely untranslatable inside the time trials because you immediately get sent to the results screen... So, I added a new menu in the translator options, "explore game", which gives you access to all the time trials and the two intermissions, from the same menu. All these time trials (which they're still based off of, under the hood) are stripped of the annoying features that come with time trials. These are the changes I made to time trial behavior in translator exploring mode: - No 3-2-1-Go! countdown - No on-screen time/death/shiny/par - ENTER doesn't restart, and the map menu works. The entire map is also revealed. - Prize for the Reckless is in its normal form - The teleporters in Entanglement Generator, Wheeler's Wormhole and Level Complete are restored as context for room names (actually, we should probably restore them in time trials anyway? Their "press to teleport" prompt is already blocked out in time trials and they do nothing other than being a checkpoint. I guess the reason they were removed was to stop people from opening the teleporter menu when that was not specifically blocked out in time trials yet.) - The companions are there at the end of levels, and behave like in no death mode (become happy and follow you to the teleporter). Also for context. - At the end of each level, you're not suddenly sent to the menu, but you can use the teleporter at your leisure just like in the intermission replays. In the Final Level, you do get sent to the menu automatically, but after a longer delay. I made another mark on VVVVVV: don't be startled, I added gamestates. I wanted all teleporters at the end of levels to behave like the ones at the end of the intermission replays, and all handling for teleporting with specific companions is already done in gamestates, so rather than adding conditional blocks across 5 or so different gamestates, it made more sense to make a single gamestate for "teleporting in translator exploring mode" (3090). I also added an alternative to having to use gamestate 3500 or 82 for the end of the final level: 3091-3092. One other thing I want to add to the "explore game" menu: a per-level count of how many room names are left to translate. That shouldn't be too difficult, and I'm planning that for the next commit.
2022-11-26 03:33:17 +01:00
start_translator_exploring = false;
translator_exploring = false;
translator_exploring_allowtele = false;
translator_cutscene_test = false;
2020-01-01 21:29:24 +01:00
totalflips = 0;
hardestroom = "Welcome Aboard";
hardestroomdeaths = 0;
hardestroom_x = 13;
hardestroom_y = 5;
hardestroom_specialname = false;
hardestroom_finalstretch = false;
2020-01-01 21:29:24 +01:00
currentroomdeaths=0;
inertia = 1.1f;
swnmode = false;
swntimer = 0;
swngame = SWN_NONE; // Not playing sine wave ninja!
2020-01-01 21:29:24 +01:00
swnstate = 0;
swnstate2 = 0;
swnstate3 = 0;
swnstate4 = 0;
swndelay = 0;
swndeaths = 0;
supercrewmate = false;
scmhurt = false;
scmprogress = 0;
swncolstate = 0;
swncoldelay = 0;
swnrecord = 0;
swnbestrank = 0;
swnrank = 0;
swnmessage = 0;
clearcustomlevelstats();
saveFilePath = FILESYSTEM_getUserSaveDirectory();
2020-06-04 04:29:44 +02:00
tinyxml2::XMLDocument doc;
last_quicksave = get_summary("saves/qsave.vvv", "qsave.vvv", doc);
2020-01-01 21:29:24 +01:00
2020-06-04 04:29:44 +02:00
tinyxml2::XMLDocument docTele;
last_telesave = get_summary("saves/tsave.vvv", "tsave.vvv", doc);
2020-01-01 21:29:24 +01:00
screenshake = flashlight = 0 ;
stat_trinkets = 0;
state = 1;
statedelay = 0;
2022-12-07 00:20:48 +01:00
statelocked = false;
//updatestate();
skipfakeload = false;
ghostsenabled = false;
cliplaytest = false;
playx = 0;
playy = 0;
playrx = 0;
playry = 0;
playgc = 0;
fadetomenu = false;
fadetomenudelay = 0;
fadetolab = false;
fadetolabdelay = 0;
2021-04-09 12:10:22 +02:00
over30mode = true;
2021-08-05 23:31:20 +02:00
showingametimer = false;
ingame_titlemode = false;
ingame_editormode = false;
kludge_ingametemp = Menu::mainmenu;
slidermode = SLIDER_NONE;
disablepause = false;
disableaudiopause = false;
disabletemporaryaudiopause = true;
inputdelay = false;
old_skip_message_timer = 0;
skip_message_timer = 0;
setdefaultcontrollerbuttons();
}
void Game::setdefaultcontrollerbuttons(void)
{
if (controllerButton_flip.size() < 1)
{
controllerButton_flip.push_back(SDL_CONTROLLER_BUTTON_A);
}
if (controllerButton_map.size() < 1)
{
controllerButton_map.push_back(SDL_CONTROLLER_BUTTON_Y);
}
if (controllerButton_esc.size() < 1)
{
controllerButton_esc.push_back(SDL_CONTROLLER_BUTTON_B);
}
if (controllerButton_restart.size() < 1)
{
controllerButton_restart.push_back(SDL_CONTROLLER_BUTTON_RIGHTSHOULDER);
}
if (controllerButton_interact.size() < 1)
{
controllerButton_interact.push_back(SDL_CONTROLLER_BUTTON_X);
}
2020-01-01 21:29:24 +01:00
}
void Game::lifesequence(void)
2020-01-01 21:29:24 +01:00
{
if (lifeseq > 0)
{
int i = obj.getplayer();
if (INBOUNDS_VEC(i, obj.entities))
{
obj.entities[i].invis = false;
if (lifeseq == 2) obj.entities[i].invis = true;
if (lifeseq == 6) obj.entities[i].invis = true;
if (lifeseq >= 8) obj.entities[i].invis = true;
}
2020-01-01 21:29:24 +01:00
if (lifeseq > 5) gravitycontrol = savegc;
lifeseq--;
if (INBOUNDS_VEC(i, obj.entities) && (lifeseq <= 0 || noflashingmode))
2020-01-01 21:29:24 +01:00
{
obj.entities[i].invis = false;
}
}
}
void Game::clearcustomlevelstats(void)
2020-01-01 21:29:24 +01:00
{
//just clearing the array
customlevelstats.clear();
2020-01-01 21:29:24 +01:00
}
void Game::updatecustomlevelstats(std::string clevel, int cscore)
{
if (!map.custommodeforreal)
{
/* We are playtesting, don't update level stats */
return;
}
2020-01-01 21:29:24 +01:00
if (clevel.find("levels/") != std::string::npos)
{
clevel = clevel.substr(7);
}
if (customlevelstats.count(clevel) == 0 || cscore > customlevelstats[clevel])
2020-01-01 21:29:24 +01:00
{
customlevelstats[clevel] = cscore;
2020-01-01 21:29:24 +01:00
}
savecustomlevelstats();
}
void Game::deletecustomlevelstats(void)
{
customlevelstats.clear();
if (!FILESYSTEM_delete("saves/levelstats.vvv"))
{
vlog_error("Error deleting levelstats.vvv");
}
}
#define LOAD_ARRAY_RENAME(ARRAY_NAME, DEST) \
if (SDL_strcmp(pKey, #ARRAY_NAME) == 0 && pText[0] != '\0') \
{ \
Refactor loading arrays from XML to not use the STL The current way "arrays" from XML files are loaded (before this commit is applied) goes something like this: 1. Read the buffer of the contents of the tag using TinyXML-2. 2. Allocate a buffer on the heap of the same size, and copy the existing buffer to it. (This is what the statement `std::string TextString = pText;` does.) 3. For each delimiter in the heap-allocated buffer... a. Allocate another buffer on the heap, and copy the characters from the previous delimiter to the delimiter you just hit. b. Then allocate the buffer AGAIN, to copy it into an std::vector. 4. Then re-allocate every single buffer YET AGAIN, because you need to make a copy of the std::vector in split() to return it to the caller. As you can see, the existing way uses a lot of memory allocations and data marshalling, just to split some text. The problem here is mostly making a temporary std::vector of split text, before doing any actual useful work (most likely, putting it into an array or ANOTHER std::vector - if the latter, then that's yet another memory allocation on top of the memory allocation you already did; this memory allocation is unavoidable, unlike the ones mentioned earlier, which should be removed). So I noticed that since we're iterating over the entire string once (just to shove its contents into a temporary std::vector), and then basically iterating over it again - why can't the whole thing just be more immediate, and just be iterated over once? So that's what I've done here. I've axed the split() function (both of them, actually), and made next_split() and next_split_s(). next_split() will take an existing string and a starting index, and it will find the next occurrence of the given delimiter in the string. Once it does so, it will return the length from the previous starting index, and modify your starting index as well. The price for immediateness is that you're supposed to handle keeping the index of the previous starting index around in order to be able to use the function; updating it after each iteration is also your responsibility. (By the way, next_split() doesn't use SDL_strchr(), because we can't get the length of the substring for the last substring. We could handle this special case specifically, but it'd be uglier; it also introduces iterating over the last substring twice, when we only need to do it once.) next_split_s() does the same thing as next_split(), except it will copy the resulting substring into a buffer that you provide (along with its size). Useful if you don't particularly care about the length of the substring. All callers have been updated accordingly. This new system does not make ANY heap allocations at all; at worst, it allocates a temporary buffer on the stack, but that's only if you use next_split_s(); plus, it'd be a fixed-size buffer, and stack allocations are negligible anyway. This improves performance when loading any sort of XML file, especially loading custom levels - which, on my system at least, I can noticeably tell (there's less of a freeze when I load in to a custom level with lots of scripts). It also decreases memory usage, because the heap isn't being used just to iterate over some delimiters when XML files are loaded.
2021-02-13 01:37:29 +01:00
/* We're loading in 32-bit integers. If we need more than 16 chars,
* something is seriously wrong */ \
char buffer[16]; \
size_t start = 0; \
size_t i = 0; \
\
while (next_split_s(buffer, sizeof(buffer), &start, pText, ',')) \
{ \
Refactor loading arrays from XML to not use the STL The current way "arrays" from XML files are loaded (before this commit is applied) goes something like this: 1. Read the buffer of the contents of the tag using TinyXML-2. 2. Allocate a buffer on the heap of the same size, and copy the existing buffer to it. (This is what the statement `std::string TextString = pText;` does.) 3. For each delimiter in the heap-allocated buffer... a. Allocate another buffer on the heap, and copy the characters from the previous delimiter to the delimiter you just hit. b. Then allocate the buffer AGAIN, to copy it into an std::vector. 4. Then re-allocate every single buffer YET AGAIN, because you need to make a copy of the std::vector in split() to return it to the caller. As you can see, the existing way uses a lot of memory allocations and data marshalling, just to split some text. The problem here is mostly making a temporary std::vector of split text, before doing any actual useful work (most likely, putting it into an array or ANOTHER std::vector - if the latter, then that's yet another memory allocation on top of the memory allocation you already did; this memory allocation is unavoidable, unlike the ones mentioned earlier, which should be removed). So I noticed that since we're iterating over the entire string once (just to shove its contents into a temporary std::vector), and then basically iterating over it again - why can't the whole thing just be more immediate, and just be iterated over once? So that's what I've done here. I've axed the split() function (both of them, actually), and made next_split() and next_split_s(). next_split() will take an existing string and a starting index, and it will find the next occurrence of the given delimiter in the string. Once it does so, it will return the length from the previous starting index, and modify your starting index as well. The price for immediateness is that you're supposed to handle keeping the index of the previous starting index around in order to be able to use the function; updating it after each iteration is also your responsibility. (By the way, next_split() doesn't use SDL_strchr(), because we can't get the length of the substring for the last substring. We could handle this special case specifically, but it'd be uglier; it also introduces iterating over the last substring twice, when we only need to do it once.) next_split_s() does the same thing as next_split(), except it will copy the resulting substring into a buffer that you provide (along with its size). Useful if you don't particularly care about the length of the substring. All callers have been updated accordingly. This new system does not make ANY heap allocations at all; at worst, it allocates a temporary buffer on the stack, but that's only if you use next_split_s(); plus, it'd be a fixed-size buffer, and stack allocations are negligible anyway. This improves performance when loading any sort of XML file, especially loading custom levels - which, on my system at least, I can noticeably tell (there's less of a freeze when I load in to a custom level with lots of scripts). It also decreases memory usage, because the heap isn't being used just to iterate over some delimiters when XML files are loaded.
2021-02-13 01:37:29 +01:00
if (i >= SDL_arraysize(DEST)) \
{ \
break; \
} \
\
DEST[i] = help.Int(buffer); \
++i; \
} \
}
#define LOAD_ARRAY(ARRAY_NAME) LOAD_ARRAY_RENAME(ARRAY_NAME, ARRAY_NAME)
void Game::loadcustomlevelstats(void)
2020-01-01 21:29:24 +01:00
{
tinyxml2::XMLDocument doc;
tinyxml2::XMLHandle hDoc(&doc);
if (!FILESYSTEM_loadTiXml2Document("saves/levelstats.vvv", doc))
{
//No levelstats file exists; start new
customlevelstats.clear();
savecustomlevelstats();
return;
}
if (doc.Error())
{
vlog_error("Error parsing levelstats.vvv: %s", doc.ErrorStr());
return;
}
customlevelstats.clear();
tinyxml2::XMLElement* pElem;
tinyxml2::XMLElement* firstElement;
firstElement = hDoc
.FirstChildElement()
.FirstChildElement("Data")
.FirstChildElement()
.ToElement();
// First pass, look for the new system of storing stats
// If they don't exist, then fall back to the old system
for (pElem = firstElement; pElem != NULL; pElem = pElem->NextSiblingElement())
{
const char* pKey = pElem->Value();
const char* pText = pElem->GetText();
if (pText == NULL)
2020-01-01 21:29:24 +01:00
{
pText = "";
}
2020-01-01 21:29:24 +01:00
if (SDL_strcmp(pKey, "stats") == 0)
{
bool file_has_duplicates = false;
for (tinyxml2::XMLElement* stat_el = pElem->FirstChildElement(); stat_el; stat_el = stat_el->NextSiblingElement())
2020-01-01 21:29:24 +01:00
{
int score = 0;
std::string name;
2020-01-01 21:29:24 +01:00
if (stat_el->GetText() != NULL)
{
score = help.Int(stat_el->GetText());
}
2020-01-01 21:29:24 +01:00
if (stat_el->Attribute("name"))
2020-01-01 21:29:24 +01:00
{
name = stat_el->Attribute("name");
}
int existing = customlevelstats.count(name);
if (existing > 0)
{
file_has_duplicates = true;
}
if (existing == 0 || score > customlevelstats[name])
{
customlevelstats[name] = score;
2020-01-01 21:29:24 +01:00
}
}
2020-01-01 21:29:24 +01:00
if (file_has_duplicates)
{
/* This might be really inflated, so simply save the map we have now,
* so we don't have to keep loading a 90 MB file. */
savecustomlevelstats();
2020-01-01 21:29:24 +01:00
}
return;
}
}
2020-01-01 21:29:24 +01:00
// Since we're still here, we must be on the old system
std::vector<std::string> customlevelnames;
std::vector<int> customlevelscores;
for (pElem = firstElement; pElem; pElem=pElem->NextSiblingElement())
{
const char* pKey = pElem->Value();
const char* pText = pElem->GetText() ;
if(pText == NULL)
{
pText = "";
}
2020-01-01 21:29:24 +01:00
if (SDL_strcmp(pKey, "customlevelscore") == 0 && pText[0] != '\0')
{
char buffer[16];
size_t start = 0;
while (next_split_s(buffer, sizeof(buffer), &start, pText, ','))
{
customlevelscores.push_back(help.Int(buffer));
}
}
2020-01-01 21:29:24 +01:00
if (SDL_strcmp(pKey, "customlevelstats") == 0 && pText[0] != '\0')
{
Refactor loading arrays from XML to not use the STL The current way "arrays" from XML files are loaded (before this commit is applied) goes something like this: 1. Read the buffer of the contents of the tag using TinyXML-2. 2. Allocate a buffer on the heap of the same size, and copy the existing buffer to it. (This is what the statement `std::string TextString = pText;` does.) 3. For each delimiter in the heap-allocated buffer... a. Allocate another buffer on the heap, and copy the characters from the previous delimiter to the delimiter you just hit. b. Then allocate the buffer AGAIN, to copy it into an std::vector. 4. Then re-allocate every single buffer YET AGAIN, because you need to make a copy of the std::vector in split() to return it to the caller. As you can see, the existing way uses a lot of memory allocations and data marshalling, just to split some text. The problem here is mostly making a temporary std::vector of split text, before doing any actual useful work (most likely, putting it into an array or ANOTHER std::vector - if the latter, then that's yet another memory allocation on top of the memory allocation you already did; this memory allocation is unavoidable, unlike the ones mentioned earlier, which should be removed). So I noticed that since we're iterating over the entire string once (just to shove its contents into a temporary std::vector), and then basically iterating over it again - why can't the whole thing just be more immediate, and just be iterated over once? So that's what I've done here. I've axed the split() function (both of them, actually), and made next_split() and next_split_s(). next_split() will take an existing string and a starting index, and it will find the next occurrence of the given delimiter in the string. Once it does so, it will return the length from the previous starting index, and modify your starting index as well. The price for immediateness is that you're supposed to handle keeping the index of the previous starting index around in order to be able to use the function; updating it after each iteration is also your responsibility. (By the way, next_split() doesn't use SDL_strchr(), because we can't get the length of the substring for the last substring. We could handle this special case specifically, but it'd be uglier; it also introduces iterating over the last substring twice, when we only need to do it once.) next_split_s() does the same thing as next_split(), except it will copy the resulting substring into a buffer that you provide (along with its size). Useful if you don't particularly care about the length of the substring. All callers have been updated accordingly. This new system does not make ANY heap allocations at all; at worst, it allocates a temporary buffer on the stack, but that's only if you use next_split_s(); plus, it'd be a fixed-size buffer, and stack allocations are negligible anyway. This improves performance when loading any sort of XML file, especially loading custom levels - which, on my system at least, I can noticeably tell (there's less of a freeze when I load in to a custom level with lots of scripts). It also decreases memory usage, because the heap isn't being used just to iterate over some delimiters when XML files are loaded.
2021-02-13 01:37:29 +01:00
size_t start = 0;
size_t len = 0;
size_t prev_start = 0;
while (next_split(&start, &len, &pText[start], '|'))
{
Refactor loading arrays from XML to not use the STL The current way "arrays" from XML files are loaded (before this commit is applied) goes something like this: 1. Read the buffer of the contents of the tag using TinyXML-2. 2. Allocate a buffer on the heap of the same size, and copy the existing buffer to it. (This is what the statement `std::string TextString = pText;` does.) 3. For each delimiter in the heap-allocated buffer... a. Allocate another buffer on the heap, and copy the characters from the previous delimiter to the delimiter you just hit. b. Then allocate the buffer AGAIN, to copy it into an std::vector. 4. Then re-allocate every single buffer YET AGAIN, because you need to make a copy of the std::vector in split() to return it to the caller. As you can see, the existing way uses a lot of memory allocations and data marshalling, just to split some text. The problem here is mostly making a temporary std::vector of split text, before doing any actual useful work (most likely, putting it into an array or ANOTHER std::vector - if the latter, then that's yet another memory allocation on top of the memory allocation you already did; this memory allocation is unavoidable, unlike the ones mentioned earlier, which should be removed). So I noticed that since we're iterating over the entire string once (just to shove its contents into a temporary std::vector), and then basically iterating over it again - why can't the whole thing just be more immediate, and just be iterated over once? So that's what I've done here. I've axed the split() function (both of them, actually), and made next_split() and next_split_s(). next_split() will take an existing string and a starting index, and it will find the next occurrence of the given delimiter in the string. Once it does so, it will return the length from the previous starting index, and modify your starting index as well. The price for immediateness is that you're supposed to handle keeping the index of the previous starting index around in order to be able to use the function; updating it after each iteration is also your responsibility. (By the way, next_split() doesn't use SDL_strchr(), because we can't get the length of the substring for the last substring. We could handle this special case specifically, but it'd be uglier; it also introduces iterating over the last substring twice, when we only need to do it once.) next_split_s() does the same thing as next_split(), except it will copy the resulting substring into a buffer that you provide (along with its size). Useful if you don't particularly care about the length of the substring. All callers have been updated accordingly. This new system does not make ANY heap allocations at all; at worst, it allocates a temporary buffer on the stack, but that's only if you use next_split_s(); plus, it'd be a fixed-size buffer, and stack allocations are negligible anyway. This improves performance when loading any sort of XML file, especially loading custom levels - which, on my system at least, I can noticeably tell (there's less of a freeze when I load in to a custom level with lots of scripts). It also decreases memory usage, because the heap isn't being used just to iterate over some delimiters when XML files are loaded.
2021-02-13 01:37:29 +01:00
customlevelnames.push_back(std::string(&pText[prev_start], len));
prev_start = start;
}
2020-01-01 21:29:24 +01:00
}
}
// If the two arrays happen to differ in length, just go with the smallest one
for (size_t i = 0; i < SDL_min(customlevelnames.size(), customlevelscores.size()); i++)
{
const std::string& name = customlevelnames[i];
const int score = customlevelscores[i];
if (customlevelstats.count(name) == 0 || score > customlevelstats[name])
{
customlevelstats[name] = score;
}
}
2020-01-01 21:29:24 +01:00
}
void Game::savecustomlevelstats(void)
2020-01-01 21:29:24 +01:00
{
tinyxml2::XMLDocument doc;
bool already_exists = FILESYSTEM_loadTiXml2Document("saves/levelstats.vvv", doc);
if (!already_exists)
{
vlog_info("No levelstats.vvv found. Creating new file");
}
else if (doc.Error())
{
vlog_error("Error parsing existing levelstats.vvv: %s", doc.ErrorStr());
vlog_info("Creating new levelstats.vvv");
}
xml::update_declaration(doc);
2020-01-01 21:29:24 +01:00
tinyxml2::XMLElement * root = xml::update_element(doc, "Levelstats");
2020-01-01 21:29:24 +01:00
xml::update_comment(root, " Levelstats Save file ");
2020-01-01 21:29:24 +01:00
tinyxml2::XMLElement * msgs = xml::update_element(root, "Data");
2020-01-01 21:29:24 +01:00
int numcustomlevelstats = customlevelstats.size();
2020-01-01 21:29:24 +01:00
if(numcustomlevelstats>=200)numcustomlevelstats=199;
xml::update_tag(msgs, "numcustomlevelstats", numcustomlevelstats);
2020-01-01 21:29:24 +01:00
std::string customlevelscorestr;
std::string customlevelstatsstr;
std::map<std::string, int>::iterator iter;
for (iter = customlevelstats.begin(); iter != customlevelstats.end(); iter++)
2020-01-01 21:29:24 +01:00
{
customlevelscorestr += help.String(iter->second) + ",";
customlevelstatsstr += iter->first + "|";
2020-01-01 21:29:24 +01:00
}
xml::update_tag(msgs, "customlevelscore", customlevelscorestr.c_str());
xml::update_tag(msgs, "customlevelstats", customlevelstatsstr.c_str());
2020-01-01 21:29:24 +01:00
// New system
tinyxml2::XMLElement* msg = xml::update_element_delete_contents(msgs, "stats");
tinyxml2::XMLElement* stat_el;
for (iter = customlevelstats.begin(); iter != customlevelstats.end(); iter++)
{
stat_el = doc.NewElement("stat");
stat_el->SetAttribute("name", iter->first.c_str());
stat_el->LinkEndChild(doc.NewText(help.String(iter->second).c_str()));
msg->LinkEndChild(stat_el);
}
if(FILESYSTEM_saveTiXml2Document("saves/levelstats.vvv", doc))
2020-01-01 21:29:24 +01:00
{
vlog_info("Level stats saved");
2020-01-01 21:29:24 +01:00
}
else
{
vlog_error("Could Not Save level stats!");
vlog_error("Failed: %s%s", saveFilePath, "levelstats.vvv");
2020-01-01 21:29:24 +01:00
}
}
void Game::levelcomplete_textbox(void)
{
graphics.createtextboxflipme("", -1, 12, TEXT_COLOUR("cyan"));
graphics.addline(" ");
graphics.addline("");
graphics.addline("");
graphics.textboxprintflags(PR_FONT_8X8);
graphics.textboxcenterx();
graphics.setimage(TEXTIMAGE_LEVELCOMPLETE);
}
void Game::crewmate_textbox(const int color)
{
const int extra_cjk_height = (font::height(PR_FONT_INTERFACE) * 4) - 32;
graphics.createtextboxflipme("", -1, 64 + 8 + 16 - extra_cjk_height/2, TEXT_COLOUR("gray"));
/* This is a special case for wrapping, we MUST have two lines.
* So just make sure it can't fit in one line. */
const char* text = loc::gettext("You have rescued a crew member!");
std::string wrapped = font::string_wordwrap_balanced(PR_FONT_INTERFACE, text, font::len(PR_FONT_INTERFACE, text)-1);
size_t startline = 0;
size_t newline;
do {
size_t pos_n = wrapped.find('\n', startline);
size_t pos_p = wrapped.find('|', startline);
newline = SDL_min(pos_n, pos_p);
graphics.addline(wrapped.substr(startline, newline-startline));
startline = newline+1;
} while (newline != std::string::npos);
graphics.addline("");
graphics.textboxprintflags(PR_FONT_INTERFACE);
graphics.textboxcentertext();
float spaces_per_8 = font::len(PR_FONT_INTERFACE, " ")/8.0f;
graphics.textboxpad(SDL_ceilf(5/spaces_per_8), SDL_ceilf(2/spaces_per_8));
graphics.textboxcenterx();
graphics.addsprite(14, 12 + extra_cjk_height/2, 0, color);
}
void Game::remaining_textbox(void)
{
const int remaining = 6 - crewrescued();
char buffer[SCREEN_WIDTH_CHARS + 1];
if (remaining > 0)
{
loc::gettext_plural_fill(buffer, sizeof(buffer), "{n_crew|wordy} remain", "{n_crew|wordy} remains", "n_crew:int", remaining);
}
else
{
SDL_strlcpy(buffer, loc::gettext("All Crew Members Rescued!"), sizeof(buffer));
}
// In CJK, the "You have rescued" box becomes so big we should lower this one a bit...
const int cjk_lowering = font::height(PR_FONT_INTERFACE) - 8;
graphics.createtextboxflipme(buffer, -1, 128 + 16 + cjk_lowering, TEXT_COLOUR("gray"));
graphics.textboxprintflags(PR_FONT_INTERFACE);
graphics.textboxpad(2, 2);
graphics.textboxcenterx();
}
void Game::actionprompt_textbox(void)
{
char buffer[SCREEN_WIDTH_CHARS + 1];
vformat_buf(
buffer, sizeof(buffer),
loc::gettext("Press {button} to continue"),
"button:but",
vformat_button(ActionSet_InGame, Action_InGame_ACTION)
);
graphics.createtextboxflipme(buffer, -1, 196, TEXT_COLOUR("cyan"));
graphics.textboxprintflags(PR_FONT_INTERFACE);
graphics.textboxpad(1, 1);
graphics.textboxcenterx();
}
void Game::savetele_textbox(void)
{
if (inspecial() || map.custommode)
{
return;
}
if (savetele())
{
graphics.createtextboxflipme(loc::gettext("Game Saved"), -1, 12, TEXT_COLOUR("gray"));
graphics.textboxprintflags(PR_FONT_INTERFACE);
graphics.textboxpad(3, 3);
graphics.textboxcenterx();
graphics.textboxtimer(25);
}
else
{
graphics.createtextboxflipme(loc::gettext("ERROR: Could not save game!"), -1, 12, TEXT_COLOUR("red"));
graphics.textboxprintflags(PR_FONT_INTERFACE);
graphics.textboxwrap(2);
graphics.textboxpad(1, 1);
graphics.textboxcenterx();
graphics.textboxtimer(50);
}
}
void Game::setstate(const int gamestate)
2022-12-07 00:20:48 +01:00
{
if (!statelocked)
{
state = gamestate;
}
}
void Game::incstate(void)
2022-12-07 00:20:48 +01:00
{
if (!statelocked)
{
state++;
}
}
void Game::setstatedelay(const int delay)
2022-12-07 00:35:06 +01:00
{
if (!statelocked)
{
statedelay = delay;
}
}
void Game::lockstate(void)
2022-12-07 00:20:48 +01:00
{
statelocked = true;
}
void Game::unlockstate(void)
2022-12-07 00:20:48 +01:00
{
statelocked = false;
}
void Game::updatestate(void)
2020-01-01 21:29:24 +01:00
{
statedelay--;
if (statedelay <= 0)
{
2022-12-07 00:35:06 +01:00
statedelay = 0;
glitchrunkludge=false;
}
2020-01-01 21:29:24 +01:00
if (statedelay <= 0)
{
switch(state)
{
case 0:
//Do nothing here! Standard game state
Fix softlocks from turning off advancetext at wrong time When a text box in the script system (not the gamestate system) is displayed onscreen and "- Press ACTION to advance text -" is up, the game sets pausescript to true, so the script system won't blare past the text box and keep executing. Then it also sets advancetext to true. Crucially, these two variables are different, so if you have pausescript true but advancetext false, then what happens? Well, you get softlocked. There's no way to continue the script. How is this possible? Well, you can teleport to the (0,0) teleporter (the teleporter in the very top-left of the map) and regain control during the teleporter animation. To do that, in 2.2 and below, you have to press R at the same time you press Enter on the teleporter, or in 2.3 you can simply press R during the cutscene. Then once you teleport to the room, it's really precise and a bit difficult (especially if Viridian is invisible), but you can quickly walk over to the terminal in that room and press Enter on it. Then what will happen is the terminal script will run, but the teleporter gamestate sequence will finish and turn advancetext off in the middle of it. And then you're softlocked. To fix this, just add a check so if we're in gamestate 0 and there's a script running, but we have pausescript on and advancetext off, just turn pausescript off so the game automatically advances the script. This softlock was reported by Tzann on the VVVVVV speedrunning Discord.
2021-04-11 03:15:43 +02:00
if (script.running)
{
Fix softlocks from turning off advancetext at wrong time When a text box in the script system (not the gamestate system) is displayed onscreen and "- Press ACTION to advance text -" is up, the game sets pausescript to true, so the script system won't blare past the text box and keep executing. Then it also sets advancetext to true. Crucially, these two variables are different, so if you have pausescript true but advancetext false, then what happens? Well, you get softlocked. There's no way to continue the script. How is this possible? Well, you can teleport to the (0,0) teleporter (the teleporter in the very top-left of the map) and regain control during the teleporter animation. To do that, in 2.2 and below, you have to press R at the same time you press Enter on the teleporter, or in 2.3 you can simply press R during the cutscene. Then once you teleport to the room, it's really precise and a bit difficult (especially if Viridian is invisible), but you can quickly walk over to the terminal in that room and press Enter on it. Then what will happen is the terminal script will run, but the teleporter gamestate sequence will finish and turn advancetext off in the middle of it. And then you're softlocked. To fix this, just add a check so if we're in gamestate 0 and there's a script running, but we have pausescript on and advancetext off, just turn pausescript off so the game automatically advances the script. This softlock was reported by Tzann on the VVVVVV speedrunning Discord.
2021-04-11 03:15:43 +02:00
if (pausescript && !advancetext)
{
/* Prevent softlocks if we somehow don't have advancetext */
pausescript = false;
}
}
else
{
if (completestop)
{
/* Close potential collection dialogue if warping to ship */
graphics.textboxremove();
graphics.showcutscenebars = false;
}
Fix softlocks from turning off advancetext at wrong time When a text box in the script system (not the gamestate system) is displayed onscreen and "- Press ACTION to advance text -" is up, the game sets pausescript to true, so the script system won't blare past the text box and keep executing. Then it also sets advancetext to true. Crucially, these two variables are different, so if you have pausescript true but advancetext false, then what happens? Well, you get softlocked. There's no way to continue the script. How is this possible? Well, you can teleport to the (0,0) teleporter (the teleporter in the very top-left of the map) and regain control during the teleporter animation. To do that, in 2.2 and below, you have to press R at the same time you press Enter on the teleporter, or in 2.3 you can simply press R during the cutscene. Then once you teleport to the room, it's really precise and a bit difficult (especially if Viridian is invisible), but you can quickly walk over to the terminal in that room and press Enter on it. Then what will happen is the terminal script will run, but the teleporter gamestate sequence will finish and turn advancetext off in the middle of it. And then you're softlocked. To fix this, just add a check so if we're in gamestate 0 and there's a script running, but we have pausescript on and advancetext off, just turn pausescript off so the game automatically advances the script. This softlock was reported by Tzann on the VVVVVV speedrunning Discord.
2021-04-11 03:15:43 +02:00
/* Prevent softlocks if there's no cutscene running right now */
hascontrol = true;
Fix softlocks from mistimed trinket text skip You can skip the "You have found a shiny trinket!" cutscene. The conditions are that this can only be done in the main game, in the main dimension (no Polar Dimension), the checkpoint that you last touched must not be in the same room as the trinket, and you have to have skipped the Comms Relay cutscene. To do the skip, you press R on the exact frame (or previous frame, if input delay is enabled) that Viridian touches the trinket. Then, the gamestate will be immediately set to 0 (because of the gotoroom) and the cutscene will be skipped. Speedrunners of the main game, well, run the main game already, the only trinket in the Polar Dimension is not one you want to do a death warp at, and they have a habit of automatically skipping over the Comms Relay cutscene because they press R at the beginning of the run when Viridian teleports to Welcome Aboard, to warp back to the Ship and so they can leave rescuing Violet for later. So someone reported softlocking themselves by doing the trinket text skip in 2.3. The softlock is because they're stuck in a state where completestop is true but can't advance to a state that turns it off. How does this happen? It's because they pressed R too late and interrupted the gamestate sequence. In 2.2 and previous, if you're in the gamestate sequence then you can't press R at all, but 2.3 removes this restriction (on account of aiming to prevent softlocks). So only on the very first frame can you death warp and interrupt the gamestate sequence before it happens at all. Anyways to fix this, just turn completestop off automatically if we're in gamestate 0 and there's no script running. This softlock was reported by Euni on the VVVVVV speedrunning Discord.
2021-04-11 00:38:46 +02:00
completestop = false;
}
2020-01-01 21:29:24 +01:00
break;
case 1:
//Game initilisation
2022-12-07 00:20:48 +01:00
setstate(0);
2020-01-01 21:29:24 +01:00
break;
case 2:
//Opening cutscene
advancetext = true;
hascontrol = false;
2022-12-07 00:20:48 +01:00
setstate(3);
graphics.createtextbox("To do: write quick", 50, 80, TEXT_COLOUR("cyan"));
graphics.addline("intro to story!");
graphics.textboxprintflags(PR_FONT_8X8);
2020-01-01 21:29:24 +01:00
//Oh no! what happen to rest of crew etc crash into dimension
break;
case 4:
//End of opening cutscene for now
graphics.createtextbox(BUTTONGLYPHS_get_wasd_text(), -1, 195, TEXT_COLOUR("gray"));
graphics.textboxprintflags(PR_FONT_INTERFACE);
graphics.textboxwrap(4);
graphics.textboxcentertext();
graphics.textboxpad(2, 2);
graphics.textboxcenterx();
graphics.textboxtimer(60);
2022-12-07 00:20:48 +01:00
setstate(0);
2020-01-01 21:29:24 +01:00
break;
case 5:
//Demo over
advancetext = true;
hascontrol = false;
startscript = true;
newscript="returntohub";
obj.removetrigger(5);
2022-12-07 00:20:48 +01:00
setstate(6);
2020-01-01 21:29:24 +01:00
break;
case 7:
//End of opening cutscene for now
graphics.textboxremove();
2020-01-01 21:29:24 +01:00
hascontrol = true;
advancetext = false;
2022-12-07 00:20:48 +01:00
setstate(0);
2020-01-01 21:29:24 +01:00
break;
case 8:
//Enter dialogue
obj.removetrigger(8);
if (!obj.flags[13])
2020-01-01 21:29:24 +01:00
{
obj.flags[13] = true;
char buffer[SCREEN_WIDTH_CHARS*3 + 1];
vformat_buf(
buffer, sizeof(buffer),
loc::gettext("Press {button} to view map and quicksave"),
"button:but",
vformat_button(ActionSet_InGame, Action_InGame_Map)
);
graphics.createtextbox(buffer, -1, 155, TEXT_COLOUR("gray"));
graphics.textboxprintflags(PR_FONT_INTERFACE);
graphics.textboxwrap(4);
graphics.textboxcentertext();
graphics.textboxpad(2, 2);
graphics.textboxcenterx();
graphics.textboxtimer(60);
2020-01-01 21:29:24 +01:00
}
2022-12-07 00:20:48 +01:00
setstate(0);
2020-01-01 21:29:24 +01:00
break;
case 9:
Move Secret Lab nocompetitive check to Super Gravitron It turns out, despite the game attempting to prevent you from using invincibility or slowdown in the Super Gravitron by simply preventing you from entering the Secret Lab from the menu, it's still possible to enter the Super Gravitron with it anyways. Just have invincibility or slowdown (or both!) enabled, enter the game normally, and talk to Victoria when you have 20 trinkets, to start the epilogue cutscene. Yeah, that's a pretty big gaping hole right there... It's also possible to do a trick that speedrunners use called telejumping to the Secret Lab to bypass the invincibility/slowdown check, too. So rather than single-case patch both of these, I'm going to fix it as generally as possible, by moving the invincibility/slowdown check to the gamestate that starts the Super Gravitron, gamestate 9. If you have invincibility/slowdown enabled, you immediately get sent back to the Secret Lab. However, this check is ignored in custom levels, because custom levels may want to use the Super Gravitron and let players have invincibility/slowdown while doing so (and there are in fact custom levels out in the wild that use the Super Gravitron; it was like one of the first things done when people discovered internal scripting). No message pops up when the game sends you back to the Secret Lab, but no message popped up when the Secret Lab menu option was disabled previously in the first place, so I haven't made anything WORSE, per se. A nice effect of this is that you can have invincibility/slowdown enabled and still be able to go to the Secret Lab from the menu. This is useful if you just want to check your trophies and leave, without having to go out of your way to disable invincibility/slowdown just to go inside.
2021-05-04 03:57:13 +02:00
if (!map.custommode && nocompetitive())
{
returntolab();
startscript = true;
newscript = "disableaccessibility";
2022-12-07 00:20:48 +01:00
setstate(0);
Move Secret Lab nocompetitive check to Super Gravitron It turns out, despite the game attempting to prevent you from using invincibility or slowdown in the Super Gravitron by simply preventing you from entering the Secret Lab from the menu, it's still possible to enter the Super Gravitron with it anyways. Just have invincibility or slowdown (or both!) enabled, enter the game normally, and talk to Victoria when you have 20 trinkets, to start the epilogue cutscene. Yeah, that's a pretty big gaping hole right there... It's also possible to do a trick that speedrunners use called telejumping to the Secret Lab to bypass the invincibility/slowdown check, too. So rather than single-case patch both of these, I'm going to fix it as generally as possible, by moving the invincibility/slowdown check to the gamestate that starts the Super Gravitron, gamestate 9. If you have invincibility/slowdown enabled, you immediately get sent back to the Secret Lab. However, this check is ignored in custom levels, because custom levels may want to use the Super Gravitron and let players have invincibility/slowdown while doing so (and there are in fact custom levels out in the wild that use the Super Gravitron; it was like one of the first things done when people discovered internal scripting). No message pops up when the game sends you back to the Secret Lab, but no message popped up when the Secret Lab menu option was disabled previously in the first place, so I haven't made anything WORSE, per se. A nice effect of this is that you can have invincibility/slowdown enabled and still be able to go to the Secret Lab from the menu. This is useful if you just want to check your trophies and leave, without having to go out of your way to disable invincibility/slowdown just to go inside.
2021-05-04 03:57:13 +02:00
break;
}
2020-01-01 21:29:24 +01:00
//Start SWN Minigame Mode B
obj.removetrigger(9);
swnmode = true;
swngame = SWN_START_SUPERGRAVITRON_STEP_1;
2020-01-01 21:29:24 +01:00
swndelay = 150;
swntimer = 60 * 30;
//set the checkpoint in the middle of the screen
savepoint = 0;
savex = 148;
savey = 100;
savegc = 0;
saverx = roomx;
savery = roomy;
savedir = 0;
2022-12-07 00:20:48 +01:00
setstate(0);
2020-01-01 21:29:24 +01:00
break;
case 10:
//Start SWN Minigame Mode A
obj.removetrigger(10);
swnmode = true;
swngame = SWN_START_GRAVITRON_STEP_1;
2020-01-01 21:29:24 +01:00
swndelay = 150;
swntimer = 60 * 30;
//set the checkpoint in the middle of the screen
savepoint = 0;
savex = 148;
savey = 100;
savegc = 0;
saverx = roomx;
savery = roomy;
savedir = 0;
2022-12-07 00:20:48 +01:00
setstate(0);
2020-01-01 21:29:24 +01:00
break;
case 11:
{
2020-01-01 21:29:24 +01:00
//Intermission 1 instructional textbox, depends on last saved
graphics.textboxremovefast();
const char* floorceiling = graphics.flipmode ? "ceiling" : "floor";
const char* crewmate;
switch (lastsaved)
2020-01-01 21:29:24 +01:00
{
case 2:
crewmate = "Vitellary";
break;
case 3:
crewmate = "Vermilion";
break;
case 4:
crewmate = "Verdigris";
break;
case 5:
crewmate = "Victoria";
break;
default:
crewmate = "your companion";
}
char english[SCREEN_WIDTH_TILES*3 + 1]; /* ASCII only */
vformat_buf(english, sizeof(english),
"When you're NOT standing on the {floorceiling}, {crewmate} will stop and wait for you.",
"floorceiling:str, crewmate:str",
floorceiling, crewmate
);
graphics.createtextbox(loc::gettext(english), -1, 3, TEXT_COLOUR("gray"));
graphics.textboxprintflags(PR_FONT_INTERFACE);
graphics.textboxwrap(2);
graphics.textboxpadtowidth(36*8);
graphics.textboxcenterx();
graphics.textboxtimer(180);
2022-12-07 00:20:48 +01:00
setstate(0);
2020-01-01 21:29:24 +01:00
break;
}
2020-01-01 21:29:24 +01:00
case 12:
//Intermission 1 instructional textbox, depends on last saved
obj.removetrigger(12);
if (!obj.flags[61])
2020-01-01 21:29:24 +01:00
{
obj.flags[61] = true;
graphics.textboxremovefast();
const char* english;
switch (lastsaved)
2020-01-01 21:29:24 +01:00
{
case 2:
case 3:
case 4:
english = "You can't continue to the next room until he is safely across.";
break;
case 5:
english = "You can't continue to the next room until she is safely across.";
break;
default:
english = "You can't continue to the next room until they are safely across.";
2020-01-01 21:29:24 +01:00
}
graphics.createtextbox(loc::gettext(english), -1, 3, TEXT_COLOUR("gray"));
graphics.textboxprintflags(PR_FONT_INTERFACE);
graphics.textboxwrap(2);
graphics.textboxpadtowidth(36*8);
graphics.textboxcenterx();
graphics.textboxtimer(120);
2020-01-01 21:29:24 +01:00
}
2022-12-07 00:20:48 +01:00
setstate(0);
2020-01-01 21:29:24 +01:00
break;
case 13:
//textbox removal
obj.removetrigger(13);
graphics.textboxremovefast();
2022-12-07 00:20:48 +01:00
setstate(0);
2020-01-01 21:29:24 +01:00
break;
case 14:
{
2020-01-01 21:29:24 +01:00
//Intermission 1 instructional textbox, depends on last saved
const char* floorceiling = graphics.flipmode ? "ceiling" : "floor";
const char* crewmate;
switch (lastsaved)
2020-01-01 21:29:24 +01:00
{
case 2:
crewmate = "Vitellary";
break;
case 3:
crewmate = "Vermilion";
break;
case 4:
crewmate = "Verdigris";
break;
case 5:
crewmate = "Victoria";
break;
default:
crewmate = "your companion";
}
char english[SCREEN_WIDTH_TILES*3 + 1]; /* ASCII only */
vformat_buf(english, sizeof(english),
"When you're standing on the {floorceiling}, {crewmate} will try to walk to you.",
"floorceiling:str, crewmate:str",
floorceiling, crewmate
);
graphics.createtextbox(loc::gettext(english), -1, 3, TEXT_COLOUR("gray"));
graphics.textboxprintflags(PR_FONT_INTERFACE);
graphics.textboxwrap(2);
graphics.textboxpadtowidth(36*8);
graphics.textboxcenterx();
graphics.textboxtimer(280);
2020-01-01 21:29:24 +01:00
2022-12-07 00:20:48 +01:00
setstate(0);
2020-01-01 21:29:24 +01:00
break;
}
2020-01-01 21:29:24 +01:00
case 15:
{
2020-01-01 21:29:24 +01:00
//leaving the naughty corner
int i = obj.getplayer();
if (INBOUNDS_VEC(i, obj.entities))
{
obj.entities[obj.getplayer()].tile = 0;
}
2022-12-07 00:20:48 +01:00
setstate(0);
2020-01-01 21:29:24 +01:00
break;
}
2020-01-01 21:29:24 +01:00
case 16:
{
2020-01-01 21:29:24 +01:00
//entering the naughty corner
int i = obj.getplayer();
if(INBOUNDS_VEC(i, obj.entities) && obj.entities[i].tile == 0)
2020-01-01 21:29:24 +01:00
{
obj.entities[i].tile = 144;
music.playef(Sound_CRY);
2020-01-01 21:29:24 +01:00
}
2022-12-07 00:20:48 +01:00
setstate(0);
2020-01-01 21:29:24 +01:00
break;
}
2020-01-01 21:29:24 +01:00
case 17:
//Arrow key tutorial
obj.removetrigger(17);
if (BUTTONGLYPHS_keyboard_is_active())
{
graphics.createtextbox(loc::gettext("If you prefer, you can press UP or DOWN instead of ACTION to flip."), -1, 187, TEXT_COLOUR("gray"));
graphics.textboxprintflags(PR_FONT_INTERFACE);
graphics.textboxwrap(2);
graphics.textboxcentertext();
graphics.textboxpad(1, 1);
graphics.textboxcenterx();
graphics.textboxtimer(100);
}
2022-12-07 00:20:48 +01:00
setstate(0);
2020-01-01 21:29:24 +01:00
break;
case 20:
if (!obj.flags[1])
2020-01-01 21:29:24 +01:00
{
obj.flags[1] = true;
2022-12-07 00:20:48 +01:00
setstate(0);
graphics.textboxremove();
2020-01-01 21:29:24 +01:00
}
obj.removetrigger(20);
break;
case 21:
if (!obj.flags[2])
2020-01-01 21:29:24 +01:00
{
obj.flags[2] = true;
2022-12-07 00:20:48 +01:00
setstate(0);
graphics.textboxremove();
2020-01-01 21:29:24 +01:00
}
obj.removetrigger(21);
break;
case 22:
if (!obj.flags[3])
2020-01-01 21:29:24 +01:00
{
graphics.textboxremovefast();
obj.flags[3] = true;
2022-12-07 00:20:48 +01:00
setstate(0);
char buffer[SCREEN_WIDTH_CHARS*3 + 1];
vformat_buf(
buffer, sizeof(buffer),
loc::gettext("Press {button} to flip"),
"button:but",
vformat_button(ActionSet_InGame, Action_InGame_ACTION)
);
graphics.createtextbox(buffer, -1, 25, TEXT_COLOUR("gray"));
graphics.textboxprintflags(PR_FONT_INTERFACE);
graphics.textboxwrap(4);
graphics.textboxcentertext();
graphics.textboxpad(2, 2);
graphics.textboxcenterx();
graphics.textboxtimer(60);
2020-01-01 21:29:24 +01:00
}
obj.removetrigger(22);
break;
case 30:
//Generic "run script"
if (!obj.flags[4])
2020-01-01 21:29:24 +01:00
{
obj.flags[4] = true;
2020-01-01 21:29:24 +01:00
startscript = true;
newscript="firststeps";
2022-12-07 00:20:48 +01:00
setstate(0);
2020-01-01 21:29:24 +01:00
}
obj.removetrigger(30);
2022-12-07 00:20:48 +01:00
setstate(0);
2020-01-01 21:29:24 +01:00
break;
case 31:
2022-12-07 00:35:06 +01:00
//state = 55; setstatedelay(50);
2022-12-08 02:10:12 +01:00
setstate(0);
setstatedelay(0);
if (!obj.flags[6])
2020-01-01 21:29:24 +01:00
{
obj.flags[6] = true;
2020-01-01 21:29:24 +01:00
obj.flags[5] = true;
2020-01-01 21:29:24 +01:00
startscript = true;
newscript="communicationstation";
2022-12-08 02:10:12 +01:00
setstate(0);
setstatedelay(0);
2020-01-01 21:29:24 +01:00
}
obj.removetrigger(31);
break;
case 32:
//Generic "run script"
if (!obj.flags[7])
2020-01-01 21:29:24 +01:00
{
obj.flags[7] = true;
2020-01-01 21:29:24 +01:00
startscript = true;
newscript="teleporterback";
2022-12-07 00:20:48 +01:00
setstate(0);
2020-01-01 21:29:24 +01:00
}
obj.removetrigger(32);
2022-12-07 00:20:48 +01:00
setstate(0);
2020-01-01 21:29:24 +01:00
break;
case 33:
//Generic "run script"
if (!obj.flags[9])
2020-01-01 21:29:24 +01:00
{
obj.flags[9] = true;
2020-01-01 21:29:24 +01:00
startscript = true;
newscript="rescueblue";
2022-12-07 00:20:48 +01:00
setstate(0);
2020-01-01 21:29:24 +01:00
}
obj.removetrigger(33);
2022-12-07 00:20:48 +01:00
setstate(0);
2020-01-01 21:29:24 +01:00
break;
case 34:
//Generic "run script"
if (!obj.flags[10])
2020-01-01 21:29:24 +01:00
{
obj.flags[10] = true;
2020-01-01 21:29:24 +01:00
startscript = true;
newscript="rescueyellow";
2022-12-07 00:20:48 +01:00
setstate(0);
2020-01-01 21:29:24 +01:00
}
obj.removetrigger(34);
2022-12-07 00:20:48 +01:00
setstate(0);
2020-01-01 21:29:24 +01:00
break;
case 35:
//Generic "run script"
if (!obj.flags[11])
2020-01-01 21:29:24 +01:00
{
obj.flags[11] = true;
2020-01-01 21:29:24 +01:00
startscript = true;
newscript="rescuegreen";
2022-12-07 00:20:48 +01:00
setstate(0);
2020-01-01 21:29:24 +01:00
}
obj.removetrigger(35);
2022-12-07 00:20:48 +01:00
setstate(0);
2020-01-01 21:29:24 +01:00
break;
case 36:
//Generic "run script"
if (!obj.flags[8])
2020-01-01 21:29:24 +01:00
{
obj.flags[8] = true;
2020-01-01 21:29:24 +01:00
startscript = true;
newscript="rescuered";
2022-12-07 00:20:48 +01:00
setstate(0);
2020-01-01 21:29:24 +01:00
}
obj.removetrigger(36);
2022-12-07 00:20:48 +01:00
setstate(0);
2020-01-01 21:29:24 +01:00
break;
case 37:
//Generic "run script"
if (companion == 0)
{
startscript = true;
newscript="int2_yellow";
2022-12-07 00:20:48 +01:00
setstate(0);
2020-01-01 21:29:24 +01:00
}
obj.removetrigger(37);
2022-12-07 00:20:48 +01:00
setstate(0);
2020-01-01 21:29:24 +01:00
break;
case 38:
//Generic "run script"
if (companion == 0)
{
startscript = true;
newscript="int2_red";
2022-12-07 00:20:48 +01:00
setstate(0);
2020-01-01 21:29:24 +01:00
}
obj.removetrigger(38);
2022-12-07 00:20:48 +01:00
setstate(0);
2020-01-01 21:29:24 +01:00
break;
case 39:
//Generic "run script"
if (companion == 0)
{
startscript = true;
newscript="int2_green";
2022-12-07 00:20:48 +01:00
setstate(0);
2020-01-01 21:29:24 +01:00
}
obj.removetrigger(39);
2022-12-07 00:20:48 +01:00
setstate(0);
2020-01-01 21:29:24 +01:00
break;
case 40:
//Generic "run script"
if (companion == 0)
{
startscript = true;
newscript="int2_blue";
2022-12-07 00:20:48 +01:00
setstate(0);
2020-01-01 21:29:24 +01:00
}
obj.removetrigger(40);
2022-12-07 00:20:48 +01:00
setstate(0);
2020-01-01 21:29:24 +01:00
break;
case 41:
//Generic "run script"
if (!obj.flags[60])
2020-01-01 21:29:24 +01:00
{
obj.flags[60] = true;
2020-01-01 21:29:24 +01:00
startscript = true;
if (lastsaved == 2)
{
newscript = "int1yellow_2";
}
else if (lastsaved == 3)
{
newscript = "int1red_2";
}
else if (lastsaved == 4)
{
newscript = "int1green_2";
}
else if (lastsaved == 5)
{
newscript = "int1blue_2";
}
2022-12-07 00:20:48 +01:00
setstate(0);
2020-01-01 21:29:24 +01:00
}
obj.removetrigger(41);
2022-12-07 00:20:48 +01:00
setstate(0);
2020-01-01 21:29:24 +01:00
break;
case 42:
//Generic "run script"
if (!obj.flags[62])
2020-01-01 21:29:24 +01:00
{
obj.flags[62] = true;
2020-01-01 21:29:24 +01:00
startscript = true;
if (lastsaved == 2)
{
newscript = "int1yellow_3";
}
else if (lastsaved == 3)
{
newscript = "int1red_3";
}
else if (lastsaved == 4)
{
newscript = "int1green_3";
}
else if (lastsaved == 5)
{
newscript = "int1blue_3";
}
2022-12-07 00:20:48 +01:00
setstate(0);
2020-01-01 21:29:24 +01:00
}
obj.removetrigger(42);
2022-12-07 00:20:48 +01:00
setstate(0);
2020-01-01 21:29:24 +01:00
break;
case 43:
//Generic "run script"
if (!obj.flags[63])
2020-01-01 21:29:24 +01:00
{
obj.flags[63] = true;
2020-01-01 21:29:24 +01:00
startscript = true;
if (lastsaved == 2)
{
newscript = "int1yellow_4";
}
else if (lastsaved == 3)
{
newscript = "int1red_4";
}
else if (lastsaved == 4)
{
newscript = "int1green_4";
}
else if (lastsaved == 5)
{
newscript = "int1blue_4";
}
2022-12-07 00:20:48 +01:00
setstate(0);
2020-01-01 21:29:24 +01:00
}
obj.removetrigger(43);
2022-12-07 00:20:48 +01:00
setstate(0);
2020-01-01 21:29:24 +01:00
break;
case 44:
//Generic "run script"
if (!obj.flags[64])
2020-01-01 21:29:24 +01:00
{
obj.flags[64] = true;
2020-01-01 21:29:24 +01:00
startscript = true;
if (lastsaved == 2)
{
newscript = "int1yellow_5";
}
else if (lastsaved == 3)
{
newscript = "int1red_5";
}
else if (lastsaved == 4)
{
newscript = "int1green_5";
}
else if (lastsaved == 5)
{
newscript = "int1blue_5";
}
2022-12-07 00:20:48 +01:00
setstate(0);
2020-01-01 21:29:24 +01:00
}
obj.removetrigger(44);
2022-12-07 00:20:48 +01:00
setstate(0);
2020-01-01 21:29:24 +01:00
break;
case 45:
//Generic "run script"
if (!obj.flags[65])
2020-01-01 21:29:24 +01:00
{
obj.flags[65] = true;
2020-01-01 21:29:24 +01:00
startscript = true;
if (lastsaved == 2)
{
newscript = "int1yellow_6";
}
else if (lastsaved == 3)
{
newscript = "int1red_6";
}
else if (lastsaved == 4)
{
newscript = "int1green_6";
}
else if (lastsaved == 5)
{
newscript = "int1blue_6";
}
2022-12-07 00:20:48 +01:00
setstate(0);
2020-01-01 21:29:24 +01:00
}
obj.removetrigger(45);
2022-12-07 00:20:48 +01:00
setstate(0);
2020-01-01 21:29:24 +01:00
break;
case 46:
//Generic "run script"
if (!obj.flags[66])
2020-01-01 21:29:24 +01:00
{
obj.flags[66] = true;
2020-01-01 21:29:24 +01:00
startscript = true;
if (lastsaved == 2)
{
newscript = "int1yellow_7";
}
else if (lastsaved == 3)
{
newscript = "int1red_7";
}
else if (lastsaved == 4)
{
newscript = "int1green_7";
}
else if (lastsaved == 5)
{
newscript = "int1blue_7";
}
2022-12-07 00:20:48 +01:00
setstate(0);
2020-01-01 21:29:24 +01:00
}
obj.removetrigger(46);
2022-12-07 00:20:48 +01:00
setstate(0);
2020-01-01 21:29:24 +01:00
break;
case 47:
//Generic "run script"
if (!obj.flags[69])
2020-01-01 21:29:24 +01:00
{
obj.flags[69] = true;
2020-01-01 21:29:24 +01:00
startscript = true;
newscript="trenchwarfare";
2022-12-07 00:20:48 +01:00
setstate(0);
2020-01-01 21:29:24 +01:00
}
obj.removetrigger(47);
2022-12-07 00:20:48 +01:00
setstate(0);
2020-01-01 21:29:24 +01:00
break;
case 48:
//Generic "run script"
if (!obj.flags[70])
2020-01-01 21:29:24 +01:00
{
obj.flags[70] = true;
2020-01-01 21:29:24 +01:00
startscript = true;
newscript="trinketcollector";
2022-12-07 00:20:48 +01:00
setstate(0);
2020-01-01 21:29:24 +01:00
}
obj.removetrigger(48);
2022-12-07 00:20:48 +01:00
setstate(0);
2020-01-01 21:29:24 +01:00
break;
case 49:
//Start final level music
if (!obj.flags[71])
2020-01-01 21:29:24 +01:00
{
obj.flags[71] = true;
music.niceplay(Music_PREDESTINEDFATEREMIX);
2022-12-07 00:20:48 +01:00
setstate(0);
2020-01-01 21:29:24 +01:00
}
obj.removetrigger(49);
2022-12-07 00:20:48 +01:00
setstate(0);
2020-01-01 21:29:24 +01:00
break;
case 50:
music.playef(Sound_VIOLET);
graphics.createtextbox(loc::gettext("Help! Can anyone hear this message?"), 5, 8, TEXT_COLOUR("purple"));
graphics.textboxcommsrelay();
graphics.textboxtimer(60);
2022-12-07 00:20:48 +01:00
incstate();
2022-12-07 00:35:06 +01:00
setstatedelay(100);
2020-01-01 21:29:24 +01:00
break;
case 51:
music.playef(Sound_VIOLET);
graphics.createtextbox(loc::gettext("Verdigris? Are you out there? Are you ok?"), 5, 8, TEXT_COLOUR("purple"));
graphics.textboxcommsrelay();
graphics.textboxtimer(60);
2022-12-07 00:20:48 +01:00
incstate();
2022-12-07 00:35:06 +01:00
setstatedelay(100);
2020-01-01 21:29:24 +01:00
break;
case 52:
music.playef(Sound_VIOLET);
graphics.createtextbox(loc::gettext("Please help us! We've crashed and need assistance!"), 5, 8, TEXT_COLOUR("purple"));
graphics.textboxcommsrelay();
graphics.textboxtimer(60);
2022-12-07 00:20:48 +01:00
incstate();
2022-12-07 00:35:06 +01:00
setstatedelay(100);
2020-01-01 21:29:24 +01:00
break;
case 53:
music.playef(Sound_VIOLET);
graphics.createtextbox(loc::gettext("Hello? Anyone out there?"), 5, 8, TEXT_COLOUR("purple"));
graphics.textboxcommsrelay();
graphics.textboxtimer(60);
2022-12-07 00:20:48 +01:00
incstate();
2022-12-07 00:35:06 +01:00
setstatedelay(100);
2020-01-01 21:29:24 +01:00
break;
case 54:
music.playef(Sound_VIOLET);
graphics.createtextbox(loc::gettext("This is Doctor Violet from the D.S.S. Souleye! Please respond!"), 5, 8, TEXT_COLOUR("purple"));
graphics.textboxcommsrelay();
graphics.textboxtimer(60);
2022-12-07 00:20:48 +01:00
incstate();
2022-12-07 00:35:06 +01:00
setstatedelay(100);
2020-01-01 21:29:24 +01:00
break;
case 55:
music.playef(Sound_VIOLET);
graphics.createtextbox(loc::gettext("Please... Anyone..."), 5, 8, TEXT_COLOUR("purple"));
graphics.textboxcommsrelay();
graphics.textboxtimer(60);
2022-12-07 00:20:48 +01:00
incstate();
2022-12-07 00:35:06 +01:00
setstatedelay(100);
2020-01-01 21:29:24 +01:00
break;
case 56:
music.playef(Sound_VIOLET);
graphics.createtextbox(loc::gettext("Please be alright, everyone..."), 5, 8, TEXT_COLOUR("purple"));
graphics.textboxcommsrelay();
graphics.textboxtimer(60);
2022-12-07 00:20:48 +01:00
setstate(50);
2022-12-07 00:35:06 +01:00
setstatedelay(100);
2020-01-01 21:29:24 +01:00
break;
case 80:
//Used to return to menu from the game
Enumify all fade modes This removes the magic numbers previously used for controlling the fade mode, which are really not readable at all unless you already know what they mean. 0: FADE_NONE 1: FADE_FULLY_BLACK 2: FADE_START_FADEOUT 3: FADE_FADING_OUT 4: FADE_START_FADEIN 5: FADE_FADING_IN There is also the macro FADEMODE_IS_FADING, which indicates when the intention is to only check if the game is fading right now, which wasn't clearly conveyed previously. I also took the opportunity to clean up the style of any lines I touched. This included rewriting if-else chains into case-switches, turning one-liner if-then statements into proper blocks, fixing up comments, and even commenting the `fademode == FADE_NONE` on the tower spike checks (which, it was previously undocumented why that check was there, but I think I know why it's there). As for type safety, we already get some by transforming the variable types into the enum. Assignment is prohibited without a cast. But, apparently, comparison is perfectly legal and won't even give so much as a warning. To work around this and make absolutely sure I made all existing comparisons now use the enum, I temporarily changed it to be an `enum class`, which is a C++11 feature that makes it so all comparisons are illegal. Unfortunately, it scopes them in a namespace with the same name as a class, so I had to temporarily define macros to make sure my existing code worked. I also had to temporarily up the standard in CMakeLists.txt to get it to compile. But after all that was done, I found the rest of the places where a comparison to an integer was used, and fixed them.
2022-04-25 09:57:47 +02:00
if (graphics.fademode == FADE_FULLY_BLACK)
{
2022-12-07 00:20:48 +01:00
incstate();
Enumify all fade modes This removes the magic numbers previously used for controlling the fade mode, which are really not readable at all unless you already know what they mean. 0: FADE_NONE 1: FADE_FULLY_BLACK 2: FADE_START_FADEOUT 3: FADE_FADING_OUT 4: FADE_START_FADEIN 5: FADE_FADING_IN There is also the macro FADEMODE_IS_FADING, which indicates when the intention is to only check if the game is fading right now, which wasn't clearly conveyed previously. I also took the opportunity to clean up the style of any lines I touched. This included rewriting if-else chains into case-switches, turning one-liner if-then statements into proper blocks, fixing up comments, and even commenting the `fademode == FADE_NONE` on the tower spike checks (which, it was previously undocumented why that check was there, but I think I know why it's there). As for type safety, we already get some by transforming the variable types into the enum. Assignment is prohibited without a cast. But, apparently, comparison is perfectly legal and won't even give so much as a warning. To work around this and make absolutely sure I made all existing comparisons now use the enum, I temporarily changed it to be an `enum class`, which is a C++11 feature that makes it so all comparisons are illegal. Unfortunately, it scopes them in a namespace with the same name as a class, so I had to temporarily define macros to make sure my existing code worked. I also had to temporarily up the standard in CMakeLists.txt to get it to compile. But after all that was done, I found the rest of the places where a comparison to an integer was used, and fixed them.
2022-04-25 09:57:47 +02:00
}
2020-01-01 21:29:24 +01:00
break;
case 81:
quittomenu();
music.play(Music_PRESENTINGVVVVVV); //should be after quittomenu()
2022-12-07 00:20:48 +01:00
setstate(0);
2020-01-01 21:29:24 +01:00
break;
case 82:
//Time Trial Complete!
obj.removetrigger(82);
if (map.custommode && !map.custommodeforreal)
{
returntoeditor();
ed.show_note(loc::gettext("Time trial completed"));
break;
}
Add level exploring menu for translators I would, of course, recommend translators to translate the roomnames while playing the full game (optionally in invincibility) so they can immediately get all the context and maybe the most inspiration. And if you want to go back into a specific level, then there's always the time trials and intermission replays which will give you full coverage of all the room names. However, the time trials weren't really made for room name translation. They have some annoying features like the instant restart when you press ENTER at the wrong time, they remove context clues like teleporters and companions, but the worst problem is that the last room in a level is often completely untranslatable inside the time trials because you immediately get sent to the results screen... So, I added a new menu in the translator options, "explore game", which gives you access to all the time trials and the two intermissions, from the same menu. All these time trials (which they're still based off of, under the hood) are stripped of the annoying features that come with time trials. These are the changes I made to time trial behavior in translator exploring mode: - No 3-2-1-Go! countdown - No on-screen time/death/shiny/par - ENTER doesn't restart, and the map menu works. The entire map is also revealed. - Prize for the Reckless is in its normal form - The teleporters in Entanglement Generator, Wheeler's Wormhole and Level Complete are restored as context for room names (actually, we should probably restore them in time trials anyway? Their "press to teleport" prompt is already blocked out in time trials and they do nothing other than being a checkpoint. I guess the reason they were removed was to stop people from opening the teleporter menu when that was not specifically blocked out in time trials yet.) - The companions are there at the end of levels, and behave like in no death mode (become happy and follow you to the teleporter). Also for context. - At the end of each level, you're not suddenly sent to the menu, but you can use the teleporter at your leisure just like in the intermission replays. In the Final Level, you do get sent to the menu automatically, but after a longer delay. I made another mark on VVVVVV: don't be startled, I added gamestates. I wanted all teleporters at the end of levels to behave like the ones at the end of the intermission replays, and all handling for teleporting with specific companions is already done in gamestates, so rather than adding conditional blocks across 5 or so different gamestates, it made more sense to make a single gamestate for "teleporting in translator exploring mode" (3090). I also added an alternative to having to use gamestate 3500 or 82 for the end of the final level: 3091-3092. One other thing I want to add to the "explore game" menu: a per-level count of how many room names are left to translate. That shouldn't be too difficult, and I'm planning that for the next commit.
2022-11-26 03:33:17 +01:00
if (translator_exploring)
{
translator_exploring_allowtele = true;
setstate(0);
Add level exploring menu for translators I would, of course, recommend translators to translate the roomnames while playing the full game (optionally in invincibility) so they can immediately get all the context and maybe the most inspiration. And if you want to go back into a specific level, then there's always the time trials and intermission replays which will give you full coverage of all the room names. However, the time trials weren't really made for room name translation. They have some annoying features like the instant restart when you press ENTER at the wrong time, they remove context clues like teleporters and companions, but the worst problem is that the last room in a level is often completely untranslatable inside the time trials because you immediately get sent to the results screen... So, I added a new menu in the translator options, "explore game", which gives you access to all the time trials and the two intermissions, from the same menu. All these time trials (which they're still based off of, under the hood) are stripped of the annoying features that come with time trials. These are the changes I made to time trial behavior in translator exploring mode: - No 3-2-1-Go! countdown - No on-screen time/death/shiny/par - ENTER doesn't restart, and the map menu works. The entire map is also revealed. - Prize for the Reckless is in its normal form - The teleporters in Entanglement Generator, Wheeler's Wormhole and Level Complete are restored as context for room names (actually, we should probably restore them in time trials anyway? Their "press to teleport" prompt is already blocked out in time trials and they do nothing other than being a checkpoint. I guess the reason they were removed was to stop people from opening the teleporter menu when that was not specifically blocked out in time trials yet.) - The companions are there at the end of levels, and behave like in no death mode (become happy and follow you to the teleporter). Also for context. - At the end of each level, you're not suddenly sent to the menu, but you can use the teleporter at your leisure just like in the intermission replays. In the Final Level, you do get sent to the menu automatically, but after a longer delay. I made another mark on VVVVVV: don't be startled, I added gamestates. I wanted all teleporters at the end of levels to behave like the ones at the end of the intermission replays, and all handling for teleporting with specific companions is already done in gamestates, so rather than adding conditional blocks across 5 or so different gamestates, it made more sense to make a single gamestate for "teleporting in translator exploring mode" (3090). I also added an alternative to having to use gamestate 3500 or 82 for the end of the final level: 3091-3092. One other thing I want to add to the "explore game" menu: a per-level count of how many room names are left to translate. That shouldn't be too difficult, and I'm planning that for the next commit.
2022-11-26 03:33:17 +01:00
break;
}
2020-01-01 21:29:24 +01:00
hascontrol = false;
if (timetrialcheater)
{
SDL_zeroa(obj.collect);
}
Simplify time formatting functions Here's my notes on all the existing functions and what kind of time formats they output: - Game::giventimestring(int hrs, int min, int sec) H:MM:SS MM:SS - Game::timestring() // uses game.hours/minutes/seconds H:MM:SS MM:SS - Game::partimestring() // uses game.timetrialpar (seconds) MM:SS - Game::resulttimestring() // uses game.timetrialresulttime (sec) + timetrialresultframes (1/30s) MM:SS.CC - Game::timetstring(int t) // t = seconds MM:SS - Game::timestringcenti(char* buffer, const size_t buffer_size) // uses game.hours/minutes/seconds/frames H:MM:SS.CC MM:SS.CC - UtilityClass::timestring(int t) // t = frames, 30 frames = 1 second S:CC M:SS:CC This is kind of a mess, and there's a lot of functions that do the same thing except using different variables. For localization, I also want translators to be able to localize all these time formats - many languages use the decimal comma instead of the decimal point (12:34,56) maybe some languages really prefer something like 1時02分11秒44瞬... Which I don't know to be correct, but it's good to be prepared for it and not restrict translators arbitrarily to only changing ":" and "." when we can start making the system better in the first place. I added a new function, UtilityClass::format_time. This is the place where all time formats come together, given the number of seconds and optionally frames. I have simplified the above-mentioned functions somewhat, but I haven't given them a complete refactor or renaming - I mainly made sure that they all use the same backend so I can make the formats consistent and properly localizable. (And before we start shoving more temporary char buffers everywhere just to get rid of the std::string's, maybe we need to think of a globally used working buffer of size SCREEN_WIDTH_CHARS+1, as a register of sorts, for when any line of text needs to be made or processed, then printed, and then goes unused. Maybe help.textrow, or something like that.) As for this commit, the available time formats are now more consistent and changed a little in some places. Leading zeroes for the first unit are now no longer included, time trial results and the Super Gravitron can now display hours when they went to 60 minutes before, and we now always use .CC instead of :CC. These are the formats: - H:MM:SS - H:MM:SS.CC - M:SS - M:SS.CC - S.CC (only used when always_minutes=false, for the Gravitrons) Here's what changes to the current functions: - Game::partimestring() is removed - it was used in two places, and could be replaced by game.timetstring(game.timetrialpar) - Game::giventimestring(h,m,s) and Game::timestring() are now wrappers for the other functions - The four remaining functions (Game::resulttimestring(), Game::timetstring(t), Game::timestringcenti(buffer, buffer_size) and UtilityClass::timestring(t)) are now wrappers for the "central function", UtilityClass::format_time. - UtilityClass::twodigits(int t) is now unused so it's also removed. - I also added int UtilityClass::hms_to_seconds(int h, int m, int s)
2021-12-25 17:13:46 +01:00
timetrialresulttime = help.hms_to_seconds(hours, minutes, seconds);
timetrialresultframes = frames;
timetrialresulttrinkets = trinkets();
timetrialresultshinytarget = timetrialshinytarget;
timetrialresultpar = timetrialpar;
timetrialresultdeaths = deathcounts;
2020-01-01 21:29:24 +01:00
timetrialrank = 0;
if (timetrialresulttime <= timetrialpar) timetrialrank++;
if (trinkets() >= timetrialshinytarget) timetrialrank++;
2020-01-01 21:29:24 +01:00
if (deathcounts == 0) timetrialrank++;
if (timetrialresulttime < besttimes[timetriallevel]
|| (timetrialresulttime == besttimes[timetriallevel] && timetrialresultframes < bestframes[timetriallevel])
|| besttimes[timetriallevel]==-1)
2020-01-01 21:29:24 +01:00
{
besttimes[timetriallevel] = timetrialresulttime;
bestframes[timetriallevel] = timetrialresultframes;
2020-01-01 21:29:24 +01:00
}
if (timetrialresulttrinkets > besttrinkets[timetriallevel] || besttrinkets[timetriallevel]==-1)
2020-01-01 21:29:24 +01:00
{
besttrinkets[timetriallevel] = trinkets();
2020-01-01 21:29:24 +01:00
}
if (deathcounts < bestlives[timetriallevel] || bestlives[timetriallevel]==-1)
{
bestlives[timetriallevel] = deathcounts;
}
if (timetrialrank > bestrank[timetriallevel] || bestrank[timetriallevel]==-1)
{
bestrank[timetriallevel] = timetrialrank;
if (timetrialrank >= 3)
{
switch (timetriallevel)
{
case TimeTrial_SPACESTATION1:
unlockAchievement("vvvvvvtimetrial_station1_fixed");
break;
case TimeTrial_LABORATORY:
unlockAchievement("vvvvvvtimetrial_lab_fixed");
break;
case TimeTrial_TOWER:
unlockAchievement("vvvvvvtimetrial_tower_fixed");
break;
case TimeTrial_SPACESTATION2:
unlockAchievement("vvvvvvtimetrial_station2_fixed");
break;
case TimeTrial_WARPZONE:
unlockAchievement("vvvvvvtimetrial_warp_fixed");
break;
case TimeTrial_FINALLEVEL:
unlockAchievement("vvvvvvtimetrial_final_fixed");
}
}
2020-01-01 21:29:24 +01:00
}
savestatsandsettings();
2020-01-01 21:29:24 +01:00
Enumify all fade modes This removes the magic numbers previously used for controlling the fade mode, which are really not readable at all unless you already know what they mean. 0: FADE_NONE 1: FADE_FULLY_BLACK 2: FADE_START_FADEOUT 3: FADE_FADING_OUT 4: FADE_START_FADEIN 5: FADE_FADING_IN There is also the macro FADEMODE_IS_FADING, which indicates when the intention is to only check if the game is fading right now, which wasn't clearly conveyed previously. I also took the opportunity to clean up the style of any lines I touched. This included rewriting if-else chains into case-switches, turning one-liner if-then statements into proper blocks, fixing up comments, and even commenting the `fademode == FADE_NONE` on the tower spike checks (which, it was previously undocumented why that check was there, but I think I know why it's there). As for type safety, we already get some by transforming the variable types into the enum. Assignment is prohibited without a cast. But, apparently, comparison is perfectly legal and won't even give so much as a warning. To work around this and make absolutely sure I made all existing comparisons now use the enum, I temporarily changed it to be an `enum class`, which is a C++11 feature that makes it so all comparisons are illegal. Unfortunately, it scopes them in a namespace with the same name as a class, so I had to temporarily define macros to make sure my existing code worked. I also had to temporarily up the standard in CMakeLists.txt to get it to compile. But after all that was done, I found the rest of the places where a comparison to an integer was used, and fixed them.
2022-04-25 09:57:47 +02:00
graphics.fademode = FADE_START_FADEOUT;
2020-01-01 21:29:24 +01:00
music.fadeout();
2022-12-07 00:20:48 +01:00
incstate();
2020-01-01 21:29:24 +01:00
break;
case 83:
frames--;
Enumify all fade modes This removes the magic numbers previously used for controlling the fade mode, which are really not readable at all unless you already know what they mean. 0: FADE_NONE 1: FADE_FULLY_BLACK 2: FADE_START_FADEOUT 3: FADE_FADING_OUT 4: FADE_START_FADEIN 5: FADE_FADING_IN There is also the macro FADEMODE_IS_FADING, which indicates when the intention is to only check if the game is fading right now, which wasn't clearly conveyed previously. I also took the opportunity to clean up the style of any lines I touched. This included rewriting if-else chains into case-switches, turning one-liner if-then statements into proper blocks, fixing up comments, and even commenting the `fademode == FADE_NONE` on the tower spike checks (which, it was previously undocumented why that check was there, but I think I know why it's there). As for type safety, we already get some by transforming the variable types into the enum. Assignment is prohibited without a cast. But, apparently, comparison is perfectly legal and won't even give so much as a warning. To work around this and make absolutely sure I made all existing comparisons now use the enum, I temporarily changed it to be an `enum class`, which is a C++11 feature that makes it so all comparisons are illegal. Unfortunately, it scopes them in a namespace with the same name as a class, so I had to temporarily define macros to make sure my existing code worked. I also had to temporarily up the standard in CMakeLists.txt to get it to compile. But after all that was done, I found the rest of the places where a comparison to an integer was used, and fixed them.
2022-04-25 09:57:47 +02:00
if (graphics.fademode == FADE_FULLY_BLACK)
{
2022-12-07 00:20:48 +01:00
incstate();
Enumify all fade modes This removes the magic numbers previously used for controlling the fade mode, which are really not readable at all unless you already know what they mean. 0: FADE_NONE 1: FADE_FULLY_BLACK 2: FADE_START_FADEOUT 3: FADE_FADING_OUT 4: FADE_START_FADEIN 5: FADE_FADING_IN There is also the macro FADEMODE_IS_FADING, which indicates when the intention is to only check if the game is fading right now, which wasn't clearly conveyed previously. I also took the opportunity to clean up the style of any lines I touched. This included rewriting if-else chains into case-switches, turning one-liner if-then statements into proper blocks, fixing up comments, and even commenting the `fademode == FADE_NONE` on the tower spike checks (which, it was previously undocumented why that check was there, but I think I know why it's there). As for type safety, we already get some by transforming the variable types into the enum. Assignment is prohibited without a cast. But, apparently, comparison is perfectly legal and won't even give so much as a warning. To work around this and make absolutely sure I made all existing comparisons now use the enum, I temporarily changed it to be an `enum class`, which is a C++11 feature that makes it so all comparisons are illegal. Unfortunately, it scopes them in a namespace with the same name as a class, so I had to temporarily define macros to make sure my existing code worked. I also had to temporarily up the standard in CMakeLists.txt to get it to compile. But after all that was done, I found the rest of the places where a comparison to an integer was used, and fixed them.
2022-04-25 09:57:47 +02:00
}
2020-01-01 21:29:24 +01:00
break;
case 84:
Clean up all exit paths to the menu to use common code There are multiple different exit paths to the main menu. In 2.2, they all had a bunch of copy-pasted code. In 2.3 currently, most of them use game.quittomenu(), but there are some stragglers that still use hand-copied code. This is a bit of a problem, because all exit paths should consistently have FILESYSTEM_unmountassets(), as part of the 2.3 feature of per-level custom assets. Furthermore, most (but not all) of the paths call script.hardreset() too, and some of the stragglers don't. So there could be something persisting through to the title screen (like a really long flash/shake timer) that could only persist if exiting to the title screen through those paths. But, actually, it seems like there's a good reason for some of those to not call script.hardreset() - namely, dying or completing No Death Mode and completing a Time Trial presents some information onscreen that would get reset by script.hardreset(), so I'll fix that in a later commit. So what I've done for this commit is found every exit path that didn't already use game.quittomenu(), and made them use game.quittomenu(). As well, some of them had special handling that existed on top of them already having a corresponding entry in game.quittomenu() (but the path would take the special handling because it never did game.quittomenu()), so I removed that special handling as well (e.g. exiting from a custom level used returntomenu(Menu::levellist) when quittomenu() already had that same returntomenu()). The menu that exiting from the level editor returns to is now handled in game.quittomenu() as well, where the map.custommode branch now also checks for map.custommodeforreal. Unfortunately, it seems like entering the level editor doesn't properly initialize map.custommode, so entering the level editor now initializes map.custommode, too. I've also taken the music.play(6) out of game.quittomenu(), because not all exit paths immediately play Presenting VVVVVV, so all exit paths that DO immediately play Presenting VVVVVV now have music.play(6) special-cased for them, which is fine enough for me. Here is the list of all exit paths to the menu: - Exiting through the pause menu (without glitchrunner mode) - Exiting through the pause menu (with glitchrunner mode) - Completing a custom level - Completing a Time Trial - Dying in No Death Mode - Completing No Death Mode - Completing an Intermission replay - Exiting from the level editor - Completing the main game
2021-01-07 23:20:37 +01:00
quittomenu();
createmenu(Menu::timetrialcomplete);
2022-12-07 00:20:48 +01:00
setstate(0);
2020-01-01 21:29:24 +01:00
break;
case 85:
//Cutscene skip version of final level change
obj.removetrigger(85);
//Init final stretch
2022-12-07 00:20:48 +01:00
incstate();
music.playef(Sound_FLASH);
music.play(Music_POSITIVEFORCE);
obj.flags[72] = true;
2020-01-01 21:29:24 +01:00
screenshake = 10;
flashlight = 5;
map.finalstretch = true;
map.warpx = false;
map.warpy = false;
map.background = 6;
map.final_colormode = true;
map.final_colorframe = 1;
2022-12-07 00:20:48 +01:00
setstate(0);
2020-01-01 21:29:24 +01:00
break;
//From 90-100 are run scripts for the eurogamer expo only, remove later
case 90:
//Generic "run script"
startscript = true;
newscript="startexpolevel_station1";
obj.removetrigger(90);
2022-12-07 00:20:48 +01:00
setstate(0);
2020-01-01 21:29:24 +01:00
break;
case 91:
//Generic "run script"
startscript = true;
newscript="startexpolevel_lab";
obj.removetrigger(91);
2022-12-07 00:20:48 +01:00
setstate(0);
2020-01-01 21:29:24 +01:00
break;
case 92:
//Generic "run script"
startscript = true;
newscript="startexpolevel_warp";
obj.removetrigger(92);
2022-12-07 00:20:48 +01:00
setstate(0);
2020-01-01 21:29:24 +01:00
break;
case 93:
//Generic "run script"
startscript = true;
newscript="startexpolevel_tower";
obj.removetrigger(93);
2022-12-07 00:20:48 +01:00
setstate(0);
2020-01-01 21:29:24 +01:00
break;
case 94:
//Generic "run script"
startscript = true;
newscript="startexpolevel_station2";
obj.removetrigger(94);
2022-12-07 00:20:48 +01:00
setstate(0);
2020-01-01 21:29:24 +01:00
break;
case 95:
//Generic "run script"
startscript = true;
newscript="startexpolevel_final";
obj.removetrigger(95);
2022-12-07 00:20:48 +01:00
setstate(0);
2020-01-01 21:29:24 +01:00
break;
case 96:
//Used to return to gravitron to game
Enumify all fade modes This removes the magic numbers previously used for controlling the fade mode, which are really not readable at all unless you already know what they mean. 0: FADE_NONE 1: FADE_FULLY_BLACK 2: FADE_START_FADEOUT 3: FADE_FADING_OUT 4: FADE_START_FADEIN 5: FADE_FADING_IN There is also the macro FADEMODE_IS_FADING, which indicates when the intention is to only check if the game is fading right now, which wasn't clearly conveyed previously. I also took the opportunity to clean up the style of any lines I touched. This included rewriting if-else chains into case-switches, turning one-liner if-then statements into proper blocks, fixing up comments, and even commenting the `fademode == FADE_NONE` on the tower spike checks (which, it was previously undocumented why that check was there, but I think I know why it's there). As for type safety, we already get some by transforming the variable types into the enum. Assignment is prohibited without a cast. But, apparently, comparison is perfectly legal and won't even give so much as a warning. To work around this and make absolutely sure I made all existing comparisons now use the enum, I temporarily changed it to be an `enum class`, which is a C++11 feature that makes it so all comparisons are illegal. Unfortunately, it scopes them in a namespace with the same name as a class, so I had to temporarily define macros to make sure my existing code worked. I also had to temporarily up the standard in CMakeLists.txt to get it to compile. But after all that was done, I found the rest of the places where a comparison to an integer was used, and fixed them.
2022-04-25 09:57:47 +02:00
if (graphics.fademode == FADE_FULLY_BLACK)
{
2022-12-07 00:20:48 +01:00
incstate();
Enumify all fade modes This removes the magic numbers previously used for controlling the fade mode, which are really not readable at all unless you already know what they mean. 0: FADE_NONE 1: FADE_FULLY_BLACK 2: FADE_START_FADEOUT 3: FADE_FADING_OUT 4: FADE_START_FADEIN 5: FADE_FADING_IN There is also the macro FADEMODE_IS_FADING, which indicates when the intention is to only check if the game is fading right now, which wasn't clearly conveyed previously. I also took the opportunity to clean up the style of any lines I touched. This included rewriting if-else chains into case-switches, turning one-liner if-then statements into proper blocks, fixing up comments, and even commenting the `fademode == FADE_NONE` on the tower spike checks (which, it was previously undocumented why that check was there, but I think I know why it's there). As for type safety, we already get some by transforming the variable types into the enum. Assignment is prohibited without a cast. But, apparently, comparison is perfectly legal and won't even give so much as a warning. To work around this and make absolutely sure I made all existing comparisons now use the enum, I temporarily changed it to be an `enum class`, which is a C++11 feature that makes it so all comparisons are illegal. Unfortunately, it scopes them in a namespace with the same name as a class, so I had to temporarily define macros to make sure my existing code worked. I also had to temporarily up the standard in CMakeLists.txt to get it to compile. But after all that was done, I found the rest of the places where a comparison to an integer was used, and fixed them.
2022-04-25 09:57:47 +02:00
}
2020-01-01 21:29:24 +01:00
break;
case 97:
returntolab();
2022-12-07 00:20:48 +01:00
setstate(0);
2020-01-01 21:29:24 +01:00
break;
case 100:
//
// Meeting crewmate in the warpzone
//
obj.removetrigger(100);
if (!obj.flags[4])
2020-01-01 21:29:24 +01:00
{
obj.flags[4] = true;
2022-12-07 00:20:48 +01:00
incstate();
2020-01-01 21:29:24 +01:00
}
break;
case 101:
{
int i = obj.getplayer();
2020-01-01 21:29:24 +01:00
hascontrol = false;
if (INBOUNDS_VEC(i, obj.entities) && obj.entities[i].onroof > 0 && gravitycontrol == 1)
2020-01-01 21:29:24 +01:00
{
gravitycontrol = 0;
music.playef(Sound_UNFLIP);
2020-01-01 21:29:24 +01:00
}
if (INBOUNDS_VEC(i, obj.entities) && obj.entities[i].onground > 0)
2020-01-01 21:29:24 +01:00
{
2022-12-07 00:20:48 +01:00
incstate();
2020-01-01 21:29:24 +01:00
}
}
break;
case 102:
{
companion = 6;
int i = obj.getcompanion();
if (INBOUNDS_VEC(i, obj.entities))
{
obj.entities[i].tile = 0;
obj.entities[i].state = 1;
}
2020-01-01 21:29:24 +01:00
advancetext = true;
hascontrol = false;
graphics.createtextbox("Captain! I've been so worried!", 60, 90, 164, 255, 164);
graphics.textboxprintflags(PR_FONT_8X8);
2022-12-07 00:20:48 +01:00
incstate();
music.playef(Sound_VERDIGRIS);
2020-01-01 21:29:24 +01:00
}
break;
case 104:
graphics.createtextbox("I'm glad you're ok!", 135, 152, TEXT_COLOUR("cyan"));
graphics.textboxprintflags(PR_FONT_8X8);
2022-12-07 00:20:48 +01:00
incstate();
music.playef(Sound_VIRIDIAN);
graphics.textboxactive();
2020-01-01 21:29:24 +01:00
break;
case 106:
{
graphics.createtextbox("I've been trying to find a", 74, 70, 164, 255, 164);
graphics.addline("way out, but I keep going");
graphics.addline("around in circles...");
graphics.textboxprintflags(PR_FONT_8X8);
2022-12-07 00:20:48 +01:00
incstate();
music.playef(Sound_CRY);
graphics.textboxactive();
int i = obj.getcompanion();
if (INBOUNDS_VEC(i, obj.entities))
{
obj.entities[i].tile = 54;
obj.entities[i].state = 0;
}
2020-01-01 21:29:24 +01:00
}
break;
case 108:
graphics.createtextbox("Don't worry! I have a", 125, 152, TEXT_COLOUR("cyan"));
graphics.addline("teleporter key!");
graphics.textboxprintflags(PR_FONT_8X8);
2022-12-07 00:20:48 +01:00
incstate();
music.playef(Sound_VIRIDIAN);
graphics.textboxactive();
2020-01-01 21:29:24 +01:00
break;
case 110:
{
int i = obj.getcompanion();
if (INBOUNDS_VEC(i, obj.entities))
{
obj.entities[i].tile = 0;
obj.entities[i].state = 1;
}
graphics.createtextbox("Follow me!", 185, 154, TEXT_COLOUR("cyan"));
graphics.textboxprintflags(PR_FONT_8X8);
2022-12-07 00:20:48 +01:00
incstate();
music.playef(Sound_VIRIDIAN);
graphics.textboxactive();
2020-01-01 21:29:24 +01:00
}
break;
case 112:
graphics.textboxremove();
2020-01-01 21:29:24 +01:00
hascontrol = true;
advancetext = false;
2022-12-07 00:20:48 +01:00
setstate(0);
2020-01-01 21:29:24 +01:00
break;
case 115:
//
// Test script for space station, totally delete me!
//
hascontrol = false;
2022-12-07 00:20:48 +01:00
incstate();
2020-01-01 21:29:24 +01:00
break;
case 116:
advancetext = true;
hascontrol = false;
graphics.createtextbox("Sorry Eurogamers! Teleporting around", 60 - 20, 200, 255, 64, 64);
graphics.addline("the map doesn't work in this version!");
graphics.textboxprintflags(PR_FONT_8X8);
graphics.textboxcenterx();
2022-12-07 00:20:48 +01:00
incstate();
2020-01-01 21:29:24 +01:00
break;
case 118:
graphics.textboxremove();
2020-01-01 21:29:24 +01:00
hascontrol = true;
advancetext = false;
2022-12-07 00:20:48 +01:00
setstate(0);
2020-01-01 21:29:24 +01:00
break;
case 120:
//
// Meeting crewmate in the space station
//
obj.removetrigger(120);
if (!obj.flags[5])
2020-01-01 21:29:24 +01:00
{
obj.flags[5] = true;
2022-12-07 00:20:48 +01:00
incstate();
2020-01-01 21:29:24 +01:00
}
break;
case 121:
{
int i = obj.getplayer();
2020-01-01 21:29:24 +01:00
hascontrol = false;
if (INBOUNDS_VEC(i, obj.entities) && obj.entities[i].onground > 0 && gravitycontrol == 0)
2020-01-01 21:29:24 +01:00
{
gravitycontrol = 1;
music.playef(Sound_UNFLIP);
2020-01-01 21:29:24 +01:00
}
if (INBOUNDS_VEC(i, obj.entities) && obj.entities[i].onroof > 0)
2020-01-01 21:29:24 +01:00
{
2022-12-07 00:20:48 +01:00
incstate();
2020-01-01 21:29:24 +01:00
}
}
break;
case 122:
{
2020-01-01 21:29:24 +01:00
companion = 7;
int i = obj.getcompanion();
if (INBOUNDS_VEC(i, obj.entities))
{
obj.entities[i].tile = 6;
obj.entities[i].state = 1;
}
2020-01-01 21:29:24 +01:00
advancetext = true;
hascontrol = false;
graphics.createtextbox("Captain! You're ok!", 60-10, 90-40, TEXT_COLOUR("yellow"));
graphics.textboxprintflags(PR_FONT_8X8);
2022-12-07 00:20:48 +01:00
incstate();
music.playef(Sound_VITELLARY);
2020-01-01 21:29:24 +01:00
break;
}
2020-01-01 21:29:24 +01:00
case 124:
{
graphics.createtextbox("I've found a teleporter, but", 60-20, 90 - 40, TEXT_COLOUR("yellow"));
graphics.addline("I can't get it to go anywhere...");
graphics.textboxprintflags(PR_FONT_8X8);
2022-12-07 00:20:48 +01:00
incstate();
music.playef(Sound_CRY);
graphics.textboxactive();
2020-01-01 21:29:24 +01:00
break;
}
2020-01-01 21:29:24 +01:00
case 126:
graphics.createtextbox("I can help with that!", 125, 152-40, TEXT_COLOUR("cyan"));
graphics.textboxprintflags(PR_FONT_8X8);
2022-12-07 00:20:48 +01:00
incstate();
music.playef(Sound_VIRIDIAN);
graphics.textboxactive();
2020-01-01 21:29:24 +01:00
break;
case 128:
graphics.createtextbox("I have the teleporter", 130, 152-35, TEXT_COLOUR("cyan"));
graphics.addline("codex for our ship!");
graphics.textboxprintflags(PR_FONT_8X8);
2022-12-07 00:20:48 +01:00
incstate();
music.playef(Sound_VIRIDIAN);
graphics.textboxactive();
2020-01-01 21:29:24 +01:00
break;
case 130:
{
graphics.createtextbox("Yey! Let's go home!", 60-30, 90-35, TEXT_COLOUR("yellow"));
graphics.textboxprintflags(PR_FONT_8X8);
2022-12-07 00:20:48 +01:00
incstate();
music.playef(Sound_VITELLARY);
graphics.textboxactive();
int i = obj.getcompanion();
if (INBOUNDS_VEC(i, obj.entities))
{
obj.entities[i].tile = 6;
obj.entities[i].state = 1;
}
2020-01-01 21:29:24 +01:00
break;
}
2020-01-01 21:29:24 +01:00
case 132:
graphics.textboxremove();
2020-01-01 21:29:24 +01:00
hascontrol = true;
advancetext = false;
2022-12-07 00:20:48 +01:00
setstate(0);
2020-01-01 21:29:24 +01:00
break;
case 200:
//Init final stretch
2022-12-07 00:20:48 +01:00
incstate();
music.playef(Sound_FLASH);
obj.flags[72] = true;
2020-01-01 21:29:24 +01:00
screenshake = 10;
flashlight = 5;
map.finalstretch = true;
map.warpx = false;
map.warpy = false;
map.background = 6;
map.final_colormode = true;
map.final_colorframe = 1;
startscript = true;
newscript="finalterminal_finish";
2022-12-07 00:20:48 +01:00
setstate(0);
2020-01-01 21:29:24 +01:00
break;
// WARNING: If updating this code, make sure to update Map.cpp mapclass::twoframedelayfix()
2020-01-01 21:29:24 +01:00
case 300:
case 301:
case 302:
case 303:
case 304:
case 305:
case 306:
case 307:
case 308:
case 309:
case 310:
case 311:
case 312:
case 313:
case 314:
case 315:
case 316:
case 317:
case 318:
case 319:
case 320:
case 321:
case 322:
case 323:
case 324:
case 325:
case 326:
case 327:
case 328:
case 329:
case 330:
case 331:
case 332:
case 333:
case 334:
case 335:
case 336:
startscript = true;
newscript="custom_"+customscript[state - 300];
obj.removetrigger(state);
2022-12-07 00:20:48 +01:00
setstate(0);
2020-01-01 21:29:24 +01:00
break;
case 1000:
graphics.showcutscenebars = true;
2020-01-01 21:29:24 +01:00
hascontrol = false;
completestop = true;
2022-12-07 00:20:48 +01:00
incstate();
2022-12-07 00:35:06 +01:00
setstatedelay(15);
2020-01-01 21:29:24 +01:00
break;
case 1001:
{
2020-01-01 21:29:24 +01:00
//Found a trinket!
advancetext = true;
2022-12-07 00:20:48 +01:00
incstate();
graphics.createtextboxflipme(loc::gettext("Congratulations!\n\nYou have found a shiny trinket!"), 50, 85, TEXT_COLOUR("gray"));
graphics.textboxprintflags(PR_FONT_INTERFACE);
int h = graphics.textboxwrap(2);
graphics.textboxcentertext();
graphics.textboxpad(1, 1);
graphics.textboxcenterx();
2020-01-01 21:29:24 +01:00
int max_trinkets;
if(map.custommode)
{
max_trinkets = cl.numtrinkets();
2020-01-01 21:29:24 +01:00
}
else
{
max_trinkets = 20;
2020-01-01 21:29:24 +01:00
}
char buffer[SCREEN_WIDTH_CHARS + 1];
vformat_buf(
buffer, sizeof(buffer),
loc::gettext("{n_trinkets|wordy} out of {max_trinkets|wordy}"),
"n_trinkets:int, max_trinkets:int",
trinkets(), max_trinkets
);
graphics.createtextboxflipme(buffer, 50, 95+h, TEXT_COLOUR("gray"));
graphics.textboxprintflags(PR_FONT_INTERFACE);
graphics.textboxwrap(2);
graphics.textboxcentertext();
graphics.textboxpad(1, 1);
graphics.textboxcenterx();
2020-01-01 21:29:24 +01:00
break;
}
case 1002:
if (!advancetext)
{
// Prevent softlocks if we somehow don't have advancetext
2022-12-07 00:20:48 +01:00
incstate();
}
break;
2020-01-01 21:29:24 +01:00
case 1003:
graphics.textboxremove();
2020-01-01 21:29:24 +01:00
hascontrol = true;
advancetext = false;
completestop = false;
2022-12-07 00:20:48 +01:00
setstate(0);
if (music.currentsong > -1)
{
music.fadeMusicVolumeIn(3000);
}
graphics.showcutscenebars = false;
2020-01-01 21:29:24 +01:00
break;
case 1010:
graphics.showcutscenebars = true;
2020-01-01 21:29:24 +01:00
hascontrol = false;
completestop = true;
2022-12-07 00:20:48 +01:00
incstate();
2022-12-07 00:35:06 +01:00
setstatedelay(15);
2020-01-01 21:29:24 +01:00
break;
case 1011:
{
Fix crewmate-found text boxes overlapping in flip mode The problem was that the code seemed to be wrongly copy-pasted from the code for generating the trinket-found text boxes (to the point where even the comment for the crewmate-found text boxes didn't get changed from "//Found a trinket!"). For the trinket-found text boxes, they use y-positions 85 and 135 if not in flip mode, and y-positions 105 and 65 if the game IS in flip mode. These text boxes are positioned correctly in flip mode. However, for the crewmate-found text boxes, they use y-positions 85 and 135 if not in flip mode, as usual, but they use y-positions 105 and 135 if the game IS in flip mode. Looks like someone forgot to change the second y-position when copy-pasting code around. Which is actually a bit funny, because I can conclude from this that it seems like the code to position these text boxes in flip mode was bolted-on AFTER the initial code of these text boxes was written. I can also conclude (hot take incoming) that basically no one actually ever tested this game in flip mode (but that was already evident, given TerryCavanagh/VVVVVV#140, less strongly TerryCavanagh/VVVVVV#141, and TerryCavanagh/VVVVVV#142 is another flip-mode-related bug which I guess sorta kinda doesn't really count since text outline wasn't enabled until 2.3?). So I fixed the second y-position to be 65, just like the y-position the trinket text boxes use. I even took the opportunity to fix the comment to say "//Found a crewmate!" instead of "//Found a trinket!".
2020-02-16 07:58:42 +01:00
//Found a crewmate!
2020-01-01 21:29:24 +01:00
advancetext = true;
2022-12-07 00:20:48 +01:00
incstate();
graphics.createtextboxflipme(loc::gettext("Congratulations!\n\nYou have found a lost crewmate!"), 50, 85, TEXT_COLOUR("gray"));
graphics.textboxprintflags(PR_FONT_INTERFACE);
int h = graphics.textboxwrap(2);
graphics.textboxcentertext();
graphics.textboxpad(1, 1);
graphics.textboxcenterx();
2020-01-01 21:29:24 +01:00
if(cl.numcrewmates()-crewmates()==0)
{
graphics.createtextboxflipme(loc::gettext("All crewmates rescued!"), 50, 95+h, TEXT_COLOUR("gray"));
2020-01-01 21:29:24 +01:00
}
else
{
char buffer[SCREEN_WIDTH_CHARS + 1];
loc::gettext_plural_fill(
buffer, sizeof(buffer),
"{n_crew|wordy} remain", "{n_crew|wordy} remains",
"n_crew:int",
cl.numcrewmates()-crewmates()
);
graphics.createtextboxflipme(buffer, 50, 95+h, TEXT_COLOUR("gray"));
}
graphics.textboxprintflags(PR_FONT_INTERFACE);
graphics.textboxwrap(4);
graphics.textboxcentertext();
graphics.textboxpad(2, 2);
graphics.textboxcenterx();
2020-01-01 21:29:24 +01:00
break;
}
case 1012:
if (!advancetext)
{
// Prevent softlocks if we somehow don't have advancetext
2022-12-07 00:20:48 +01:00
incstate();
}
break;
2020-01-01 21:29:24 +01:00
case 1013:
graphics.textboxremove();
2020-01-01 21:29:24 +01:00
hascontrol = true;
advancetext = false;
completestop = false;
2022-12-07 00:20:48 +01:00
setstate(0);
2020-01-01 21:29:24 +01:00
if(cl.numcrewmates()-crewmates()==0)
2020-01-01 21:29:24 +01:00
{
if(map.custommodeforreal)
{
Enumify all fade modes This removes the magic numbers previously used for controlling the fade mode, which are really not readable at all unless you already know what they mean. 0: FADE_NONE 1: FADE_FULLY_BLACK 2: FADE_START_FADEOUT 3: FADE_FADING_OUT 4: FADE_START_FADEIN 5: FADE_FADING_IN There is also the macro FADEMODE_IS_FADING, which indicates when the intention is to only check if the game is fading right now, which wasn't clearly conveyed previously. I also took the opportunity to clean up the style of any lines I touched. This included rewriting if-else chains into case-switches, turning one-liner if-then statements into proper blocks, fixing up comments, and even commenting the `fademode == FADE_NONE` on the tower spike checks (which, it was previously undocumented why that check was there, but I think I know why it's there). As for type safety, we already get some by transforming the variable types into the enum. Assignment is prohibited without a cast. But, apparently, comparison is perfectly legal and won't even give so much as a warning. To work around this and make absolutely sure I made all existing comparisons now use the enum, I temporarily changed it to be an `enum class`, which is a C++11 feature that makes it so all comparisons are illegal. Unfortunately, it scopes them in a namespace with the same name as a class, so I had to temporarily define macros to make sure my existing code worked. I also had to temporarily up the standard in CMakeLists.txt to get it to compile. But after all that was done, I found the rest of the places where a comparison to an integer was used, and fixed them.
2022-04-25 09:57:47 +02:00
graphics.fademode = FADE_START_FADEOUT;
2022-12-07 00:20:48 +01:00
setstate(1014);
2020-01-01 21:29:24 +01:00
}
else
{
returntoeditor();
ed.show_note(loc::gettext("Level completed"));
2020-01-01 21:29:24 +01:00
}
}
else
{
if (cl.levmusic > 0)
{
music.fadeMusicVolumeIn(3000);
}
2020-01-01 21:29:24 +01:00
}
graphics.showcutscenebars = false;
2020-01-01 21:29:24 +01:00
break;
case 1014:
frames--;
Enumify all fade modes This removes the magic numbers previously used for controlling the fade mode, which are really not readable at all unless you already know what they mean. 0: FADE_NONE 1: FADE_FULLY_BLACK 2: FADE_START_FADEOUT 3: FADE_FADING_OUT 4: FADE_START_FADEIN 5: FADE_FADING_IN There is also the macro FADEMODE_IS_FADING, which indicates when the intention is to only check if the game is fading right now, which wasn't clearly conveyed previously. I also took the opportunity to clean up the style of any lines I touched. This included rewriting if-else chains into case-switches, turning one-liner if-then statements into proper blocks, fixing up comments, and even commenting the `fademode == FADE_NONE` on the tower spike checks (which, it was previously undocumented why that check was there, but I think I know why it's there). As for type safety, we already get some by transforming the variable types into the enum. Assignment is prohibited without a cast. But, apparently, comparison is perfectly legal and won't even give so much as a warning. To work around this and make absolutely sure I made all existing comparisons now use the enum, I temporarily changed it to be an `enum class`, which is a C++11 feature that makes it so all comparisons are illegal. Unfortunately, it scopes them in a namespace with the same name as a class, so I had to temporarily define macros to make sure my existing code worked. I also had to temporarily up the standard in CMakeLists.txt to get it to compile. But after all that was done, I found the rest of the places where a comparison to an integer was used, and fixed them.
2022-04-25 09:57:47 +02:00
if (graphics.fademode == FADE_FULLY_BLACK)
{
2022-12-07 00:20:48 +01:00
incstate();
Enumify all fade modes This removes the magic numbers previously used for controlling the fade mode, which are really not readable at all unless you already know what they mean. 0: FADE_NONE 1: FADE_FULLY_BLACK 2: FADE_START_FADEOUT 3: FADE_FADING_OUT 4: FADE_START_FADEIN 5: FADE_FADING_IN There is also the macro FADEMODE_IS_FADING, which indicates when the intention is to only check if the game is fading right now, which wasn't clearly conveyed previously. I also took the opportunity to clean up the style of any lines I touched. This included rewriting if-else chains into case-switches, turning one-liner if-then statements into proper blocks, fixing up comments, and even commenting the `fademode == FADE_NONE` on the tower spike checks (which, it was previously undocumented why that check was there, but I think I know why it's there). As for type safety, we already get some by transforming the variable types into the enum. Assignment is prohibited without a cast. But, apparently, comparison is perfectly legal and won't even give so much as a warning. To work around this and make absolutely sure I made all existing comparisons now use the enum, I temporarily changed it to be an `enum class`, which is a C++11 feature that makes it so all comparisons are illegal. Unfortunately, it scopes them in a namespace with the same name as a class, so I had to temporarily define macros to make sure my existing code worked. I also had to temporarily up the standard in CMakeLists.txt to get it to compile. But after all that was done, I found the rest of the places where a comparison to an integer was used, and fixed them.
2022-04-25 09:57:47 +02:00
}
2020-01-01 21:29:24 +01:00
break;
case 1015:
//Update level stats
/* FIXME: Have to add check to not save stats for the dumb hack
* `special/stdin.vvvvvv` filename... see elsewhere, grep for
* `special/stdin`! */
if(cl.numcrewmates()-crewmates()==0 && customlevelfilename != "levels/special/stdin.vvvvvv")
2020-01-01 21:29:24 +01:00
{
//Finished level
if (trinkets() >= cl.numtrinkets())
2020-01-01 21:29:24 +01:00
{
//and got all the trinkets!
updatecustomlevelstats(customlevelfilename, 3);
}
else
{
updatecustomlevelstats(customlevelfilename, 1);
}
}
quittomenu();
music.play(Music_PRESENTINGVVVVVV); //should be after quittomenu()
2022-12-07 00:20:48 +01:00
setstate(0);
2020-01-01 21:29:24 +01:00
break;
case 2000:
//Game Saved!
savetele_textbox();
2022-12-07 00:20:48 +01:00
setstate(0);
2020-01-01 21:29:24 +01:00
break;
case 2500:
music.play(Music_PAUSE);
2020-01-01 21:29:24 +01:00
//Activating a teleporter (appear)
2022-12-07 00:20:48 +01:00
incstate();
2022-12-07 00:35:06 +01:00
setstatedelay(15);
2020-01-01 21:29:24 +01:00
flashlight = 5;
screenshake = 90;
music.playef(Sound_FLASH);
2020-01-01 21:29:24 +01:00
break;
case 2501:
//Activating a teleporter 2
2022-12-07 00:20:48 +01:00
incstate();
2022-12-07 00:35:06 +01:00
setstatedelay(0);
2020-01-01 21:29:24 +01:00
flashlight = 5;
screenshake = 0;
//we're done here!
music.playef(Sound_TELEPORT);
2020-01-01 21:29:24 +01:00
break;
case 2502:
{
2020-01-01 21:29:24 +01:00
//Activating a teleporter 2
2022-12-07 00:20:48 +01:00
incstate();
2022-12-07 00:35:06 +01:00
setstatedelay(5);
2020-01-01 21:29:24 +01:00
int i = obj.getplayer();
if (INBOUNDS_VEC(i, obj.entities))
{
obj.entities[i].colour = 0;
obj.entities[i].invis = false;
int j = obj.getteleporter();
if (INBOUNDS_VEC(j, obj.entities))
{
obj.entities[i].xp = obj.entities[j].xp+44;
obj.entities[i].yp = obj.entities[j].yp+44;
Restore previous oldxp/oldyp variables in favor of lerpoldxp/lerpoldyp I was investigating a desync in my Nova TAS, and it turns out that the gravity line collision functions check for the `oldxp` and `oldyp` of the player, i.e. their position on the previous frame, along with their position on the current frame. So, if the player either collided with the gravity line last frame or this frame, then the player collided with the gravity line this frame. Except, that's not actually true. It turns out that `oldxp` and `oldyp` don't necessarily always correspond to the `xp` and `yp` of the player on the previous frame. It turns out that your `oldyp` will be updated if you stand on a vertically moving platform, before the gravity line collision function gets ran. So, if you were colliding with a gravity line on the previous frame, but you got moved out of there by a vertically moving platform, then you just don't collide with the gravity line at all. However, this behavior changed in 2.3 after my over-30-FPS patch got merged (#220). That patch took advantage of the existing `oldxp` and `oldyp` entity attributes, and uses them to interpolate their positions during rendering to make everything look real smooth. Previously, `oldxp` and `oldyp` would both be updated in `entityclass::updateentitylogic()`. However, I moved it in that patch to update right before `gameinput()` in `main.cpp`. As a result, `oldyp` no longer gets updated whenever the player stands on a vertically moving platform. This ends up desyncing my TAS. As expected, updating `oldyp` in `entityclass::movingplatformfix()` (the function responsible for moving the player whenever they stand on a vertically moving platform) makes it so that my TAS syncs, but the visuals are glitchy when standing on a vertically moving platform. And as much as I'd like to get rid of gravity lines checking for whether you've collided with them on the previous frame, doing that desyncs my TAS, too. In the end, it seems like I should just leave `oldxp` and `oldyp` alone, and switch to using dedicated variables that are never used in the physics of the game. So I'm introducing `lerpoldxp` and `lerpoldyp`, and replacing all instances of using `oldxp` and `oldyp` that my over-30-FPS patch added, with `lerpoldxp` and `lerpoldyp` instead. After doing this, and applying #503 as well, my Nova TAS syncs after some minor but acceptable fixes with Viridian's walkingframe.
2020-10-10 05:58:58 +02:00
obj.entities[i].lerpoldxp = obj.entities[i].xp;
obj.entities[i].lerpoldyp = obj.entities[i].yp;
}
obj.entities[i].ay = -6;
obj.entities[i].ax = 6;
obj.entities[i].vy = -6;
obj.entities[i].vx = 6;
}
2020-01-01 21:29:24 +01:00
i = obj.getteleporter();
if (INBOUNDS_VEC(i, obj.entities))
{
obj.entities[i].tile = 1;
obj.entities[i].colour = 101;
}
2020-01-01 21:29:24 +01:00
break;
}
2020-01-01 21:29:24 +01:00
case 2503:
{
2022-12-07 00:20:48 +01:00
incstate();
int i = obj.getplayer();
if (INBOUNDS_VEC(i, obj.entities))
{
obj.entities[i].xp += 10;
}
2020-01-01 21:29:24 +01:00
break;
}
2020-01-01 21:29:24 +01:00
case 2504:
{
2022-12-07 00:20:48 +01:00
incstate();
int i = obj.getplayer();
if (INBOUNDS_VEC(i, obj.entities))
{
//obj.entities[i].xp += 10;
}
2020-01-01 21:29:24 +01:00
break;
}
2020-01-01 21:29:24 +01:00
case 2505:
{
2022-12-07 00:20:48 +01:00
incstate();
int i = obj.getplayer();
if (INBOUNDS_VEC(i, obj.entities))
{
obj.entities[i].xp += 8;
}
2020-01-01 21:29:24 +01:00
break;
}
2020-01-01 21:29:24 +01:00
case 2506:
{
2022-12-07 00:20:48 +01:00
incstate();
int i = obj.getplayer();
if (INBOUNDS_VEC(i, obj.entities))
{
obj.entities[i].xp += 6;
}
2020-01-01 21:29:24 +01:00
break;
}
2020-01-01 21:29:24 +01:00
case 2507:
{
2022-12-07 00:20:48 +01:00
incstate();
2020-01-01 21:29:24 +01:00
break;
}
2020-01-01 21:29:24 +01:00
case 2508:
{
2022-12-07 00:20:48 +01:00
incstate();
int i = obj.getplayer();
if (INBOUNDS_VEC(i, obj.entities))
{
obj.entities[i].xp += 2;
}
2020-01-01 21:29:24 +01:00
break;
}
2020-01-01 21:29:24 +01:00
case 2509:
{
2022-12-07 00:20:48 +01:00
incstate();
2022-12-07 00:35:06 +01:00
setstatedelay(15);
int i = obj.getplayer();
if (INBOUNDS_VEC(i, obj.entities))
{
obj.entities[i].xp += 1;
}
2020-01-01 21:29:24 +01:00
break;
}
2020-01-01 21:29:24 +01:00
case 2510:
advancetext = true;
hascontrol = false;
graphics.createtextbox("Hello?", 125+24, 152-20, TEXT_COLOUR("cyan"));
graphics.textboxprintflags(PR_FONT_8X8);
2022-12-07 00:20:48 +01:00
incstate();
music.playef(Sound_VIRIDIAN);
graphics.textboxactive();
2020-01-01 21:29:24 +01:00
break;
case 2512:
advancetext = true;
hascontrol = false;
graphics.createtextbox("Is anyone there?", 125+8, 152-24, TEXT_COLOUR("cyan"));
graphics.textboxprintflags(PR_FONT_8X8);
2022-12-07 00:20:48 +01:00
incstate();
music.playef(Sound_VIRIDIAN);
graphics.textboxactive();
2020-01-01 21:29:24 +01:00
break;
case 2514:
graphics.textboxremove();
2020-01-01 21:29:24 +01:00
hascontrol = true;
advancetext = false;
2022-12-07 00:20:48 +01:00
setstate(0);
music.play(Music_POTENTIALFORANYTHING);
2020-01-01 21:29:24 +01:00
break;
case 3000:
//Activating a teleporter (long version for level complete)
2022-12-07 00:20:48 +01:00
incstate();
2022-12-07 00:35:06 +01:00
setstatedelay(30);
2020-01-01 21:29:24 +01:00
flashlight = 5;
screenshake = 90;
music.playef(Sound_FLASH);
2020-01-01 21:29:24 +01:00
break;
case 3001:
//Activating a teleporter 2
2022-12-07 00:20:48 +01:00
incstate();
2022-12-07 00:35:06 +01:00
setstatedelay(15);
2020-01-01 21:29:24 +01:00
flashlight = 5;
music.playef(Sound_FLASH);
2020-01-01 21:29:24 +01:00
break;
case 3002:
//Activating a teleporter 2
2022-12-07 00:20:48 +01:00
incstate();
2022-12-07 00:35:06 +01:00
setstatedelay(15);
2020-01-01 21:29:24 +01:00
flashlight = 5;
music.playef(Sound_FLASH);
2020-01-01 21:29:24 +01:00
break;
case 3003:
//Activating a teleporter 2
2022-12-07 00:20:48 +01:00
incstate();
2022-12-07 00:35:06 +01:00
setstatedelay(15);
2020-01-01 21:29:24 +01:00
flashlight = 5;
music.playef(Sound_FLASH);
2020-01-01 21:29:24 +01:00
break;
case 3004:
//Activating a teleporter 2
2022-12-07 00:20:48 +01:00
incstate();
2022-12-07 00:35:06 +01:00
setstatedelay(0);
2020-01-01 21:29:24 +01:00
flashlight = 5;
screenshake = 0;
//we're done here!
music.playef(Sound_TELEPORT);
2020-01-01 21:29:24 +01:00
break;
case 3005:
{
2020-01-01 21:29:24 +01:00
//Activating a teleporter 2
2022-12-07 00:20:48 +01:00
incstate();
2022-12-07 00:35:06 +01:00
setstatedelay(50);
2020-01-01 21:29:24 +01:00
switch(companion)
{
case 6:
2022-12-07 00:20:48 +01:00
setstate(3006);
2020-01-01 21:29:24 +01:00
break; //Warp Zone
case 7:
2022-12-07 00:20:48 +01:00
setstate(3020);
2020-01-01 21:29:24 +01:00
break; //Space Station
case 8:
2022-12-07 00:20:48 +01:00
setstate(3040);
2020-01-01 21:29:24 +01:00
break; //Lab
case 9:
2022-12-07 00:20:48 +01:00
setstate(3060);
2020-01-01 21:29:24 +01:00
break; //Tower
case 10:
2022-12-07 00:20:48 +01:00
setstate(3080);
2020-01-01 21:29:24 +01:00
break; //Intermission 2
case 11:
2022-12-07 00:20:48 +01:00
setstate(3085);
2020-01-01 21:29:24 +01:00
break; //Intermission 1
}
Add level exploring menu for translators I would, of course, recommend translators to translate the roomnames while playing the full game (optionally in invincibility) so they can immediately get all the context and maybe the most inspiration. And if you want to go back into a specific level, then there's always the time trials and intermission replays which will give you full coverage of all the room names. However, the time trials weren't really made for room name translation. They have some annoying features like the instant restart when you press ENTER at the wrong time, they remove context clues like teleporters and companions, but the worst problem is that the last room in a level is often completely untranslatable inside the time trials because you immediately get sent to the results screen... So, I added a new menu in the translator options, "explore game", which gives you access to all the time trials and the two intermissions, from the same menu. All these time trials (which they're still based off of, under the hood) are stripped of the annoying features that come with time trials. These are the changes I made to time trial behavior in translator exploring mode: - No 3-2-1-Go! countdown - No on-screen time/death/shiny/par - ENTER doesn't restart, and the map menu works. The entire map is also revealed. - Prize for the Reckless is in its normal form - The teleporters in Entanglement Generator, Wheeler's Wormhole and Level Complete are restored as context for room names (actually, we should probably restore them in time trials anyway? Their "press to teleport" prompt is already blocked out in time trials and they do nothing other than being a checkpoint. I guess the reason they were removed was to stop people from opening the teleporter menu when that was not specifically blocked out in time trials yet.) - The companions are there at the end of levels, and behave like in no death mode (become happy and follow you to the teleporter). Also for context. - At the end of each level, you're not suddenly sent to the menu, but you can use the teleporter at your leisure just like in the intermission replays. In the Final Level, you do get sent to the menu automatically, but after a longer delay. I made another mark on VVVVVV: don't be startled, I added gamestates. I wanted all teleporters at the end of levels to behave like the ones at the end of the intermission replays, and all handling for teleporting with specific companions is already done in gamestates, so rather than adding conditional blocks across 5 or so different gamestates, it made more sense to make a single gamestate for "teleporting in translator exploring mode" (3090). I also added an alternative to having to use gamestate 3500 or 82 for the end of the final level: 3091-3092. One other thing I want to add to the "explore game" menu: a per-level count of how many room names are left to translate. That shouldn't be too difficult, and I'm planning that for the next commit.
2022-11-26 03:33:17 +01:00
if (translator_exploring_allowtele)
{
setstate(3090);
Add level exploring menu for translators I would, of course, recommend translators to translate the roomnames while playing the full game (optionally in invincibility) so they can immediately get all the context and maybe the most inspiration. And if you want to go back into a specific level, then there's always the time trials and intermission replays which will give you full coverage of all the room names. However, the time trials weren't really made for room name translation. They have some annoying features like the instant restart when you press ENTER at the wrong time, they remove context clues like teleporters and companions, but the worst problem is that the last room in a level is often completely untranslatable inside the time trials because you immediately get sent to the results screen... So, I added a new menu in the translator options, "explore game", which gives you access to all the time trials and the two intermissions, from the same menu. All these time trials (which they're still based off of, under the hood) are stripped of the annoying features that come with time trials. These are the changes I made to time trial behavior in translator exploring mode: - No 3-2-1-Go! countdown - No on-screen time/death/shiny/par - ENTER doesn't restart, and the map menu works. The entire map is also revealed. - Prize for the Reckless is in its normal form - The teleporters in Entanglement Generator, Wheeler's Wormhole and Level Complete are restored as context for room names (actually, we should probably restore them in time trials anyway? Their "press to teleport" prompt is already blocked out in time trials and they do nothing other than being a checkpoint. I guess the reason they were removed was to stop people from opening the teleporter menu when that was not specifically blocked out in time trials yet.) - The companions are there at the end of levels, and behave like in no death mode (become happy and follow you to the teleporter). Also for context. - At the end of each level, you're not suddenly sent to the menu, but you can use the teleporter at your leisure just like in the intermission replays. In the Final Level, you do get sent to the menu automatically, but after a longer delay. I made another mark on VVVVVV: don't be startled, I added gamestates. I wanted all teleporters at the end of levels to behave like the ones at the end of the intermission replays, and all handling for teleporting with specific companions is already done in gamestates, so rather than adding conditional blocks across 5 or so different gamestates, it made more sense to make a single gamestate for "teleporting in translator exploring mode" (3090). I also added an alternative to having to use gamestate 3500 or 82 for the end of the final level: 3091-3092. One other thing I want to add to the "explore game" menu: a per-level count of how many room names are left to translate. That shouldn't be too difficult, and I'm planning that for the next commit.
2022-11-26 03:33:17 +01:00
}
int i = obj.getplayer();
if (INBOUNDS_VEC(i, obj.entities))
{
obj.entities[i].colour = 0;
obj.entities[i].invis = true;
}
2020-01-01 21:29:24 +01:00
i = obj.getcompanion();
if(INBOUNDS_VEC(i, obj.entities))
2020-01-01 21:29:24 +01:00
{
Fix entity and block indices after destroying them This patch restores some 2.2 behavior, fixing a regression caused by the refactor of properly using std::vectors. In 2.2, the game allocated 200 items in obj.entities, but used a system where each entity had an `active` attribute to signify if the entity actually existed or not. When dealing with entities, you would have to check this `active` flag, or else you'd be dealing with an entity that didn't actually exist. (By the way, what I'm saying applies to blocks and obj.blocks as well, except for some small differing details like the game allocating 500 block slots versus obj.entities's 200.) As a consequence, the game had to use a separate tracking variable, obj.nentity, because obj.entities.size() would just report 200, instead of the actual amount of entities. Needless to say, having to check for `active` and use `obj.nentity` is a bit error-prone, and it's messier than simply using the std::vector the way it was intended. Also, this resulted in a hard limit of 200 entities, which custom level makers ran into surprisingly quite often. 2.3 comes along, and removes the whole system. Now, std::vectors are properly being used, and obj.entities.size() reports the actual number of entities in the vector; you no longer have to check for `active` when dealing with entities of any sort. But there was one previous behavior of 2.2 that this system kind of forgets about - namely, the ability to have holes in between entities. You see, when an entity got disabled in 2.2 (which just meant turning its `active` off), the indices of all other entities stayed the same; the indice of the entity that got disabled stays there as a hole in the array. But when an entity gets removed in 2.3 (previous to this patch), the indices of every entity afterwards in the array get shifted down by one. std::vector isn't really meant to be able to contain holes. Do the indices of entities and blocks matter? Yes; they determine the order in which entities and blocks get evaluated (the highest indice gets evaluated first), and I had to fix some block evaluation order stuff in previous PRs. And in the case of entities, they matter hugely when using the recently-discovered Arbitrary Entity Manipulation glitch (where crewmate script commands are used on arbitrary entities by setting the `i` attribute of `scriptclass` and passing invalid crewmate identifiers to the commands). If you use Arbitrary Entity Manipulation after destroying some entities, there is a chance that your script won't work between 2.2 and 2.3. The indices also still determine the rendering order of entities (highest indice gets drawn first, which means lowest indice gets drawn in front of other entities). As an example: let's say we have the player at 0, a gravity line at 1, and a checkpoint at 2; then we destroy the gravity line and create a crewmate (let's do Violet). If we're able to have holes, then after removing the gravity line, none of the other indices shift. Then Violet will be created at indice 1, and will be drawn in front of the checkpoint. But if we can't have holes, then removing the gravity line results in the indice of the checkpoint shifting down to indice 1. Then Violet is created at indice 2, and gets drawn behind the checkpoint! This is a clear illustration of changing the behavior that existed in 2.2. However, I also don't want to go back to the `active` system of having to check an attribute before operating on an entity. So... what do we do to restore the holes? Well, we don't need to have an `active` attribute, or modify any existing code that operates on entities. Instead, we can just set the attributes of the entities so that they naturally get ignored by everything that comes into contact with it. For entities, we set their invis to true, and their size, type, and rule to -1 (the game never uses a size, type, or rule of -1 anywhere); for blocks, we set their type to -1, and their width and height to 0. obj.entities.size() will no longer necessarily equal the amount of entities in the room; rather, it will be the amount of entity SLOTS that have been allocated. But nothing that uses obj.entities.size() needs to actually know the amount of entities; it's mostly used for iterating over every entity in the vector. Excess entity slots get cleaned up upon every call of mapclass::gotoroom(), which will now deallocate entity slots starting from the end until it hits a player, at which point it will switch to disabling entity slots instead of removing them entirely. The entclass::clear() and blockclass::clear() functions have been restored because we need to call their initialization functions when reusing a block/entity slot; it's possible to create an entity with an invalid type number (it creates a glitchy Viridian), and without calling the initialization function again, it would simply not create anything. After this patch is applied, entity and block indices will be restored to how they behaved in 2.2.
2020-12-27 07:11:34 +01:00
obj.disableentity(i);
2020-01-01 21:29:24 +01:00
}
i = obj.getteleporter();
if (INBOUNDS_VEC(i, obj.entities))
{
obj.entities[i].tile = 1;
obj.entities[i].colour = 100;
}
2020-01-01 21:29:24 +01:00
break;
}
2020-01-01 21:29:24 +01:00
case 3006:
//Level complete! (warp zone)
unlocknum(Unlock_WARPZONE_COMPLETE);
2020-01-01 21:29:24 +01:00
lastsaved = 4;
music.play(Music_PATHCOMPLETE);
2022-12-07 00:20:48 +01:00
incstate();
2022-12-07 00:35:06 +01:00
setstatedelay(75);
2020-01-01 21:29:24 +01:00
levelcomplete_textbox();
2020-01-01 21:29:24 +01:00
break;
case 3007:
2022-12-07 00:20:48 +01:00
incstate();
2022-12-07 00:35:06 +01:00
setstatedelay(45);
2020-01-01 21:29:24 +01:00
crewmate_textbox(13);
2020-01-01 21:29:24 +01:00
break;
case 3008:
2022-12-07 00:20:48 +01:00
incstate();
2022-12-07 00:35:06 +01:00
setstatedelay(45);
2020-01-01 21:29:24 +01:00
remaining_textbox();
2020-01-01 21:29:24 +01:00
break;
case 3009:
2022-12-07 00:20:48 +01:00
incstate();
2022-12-07 00:35:06 +01:00
setstatedelay(0);
2020-01-01 21:29:24 +01:00
actionprompt_textbox();
2020-01-01 21:29:24 +01:00
break;
case 3010:
if (jumppressed)
{
2022-12-07 00:20:48 +01:00
incstate();
2022-12-07 00:35:06 +01:00
setstatedelay(30);
graphics.textboxremove();
2020-01-01 21:29:24 +01:00
}
break;
case 3011:
2022-12-08 02:10:12 +01:00
setstate(3070);
setstatedelay(0);
2020-01-01 21:29:24 +01:00
break;
case 3020:
//Level complete! (Space Station 2)
unlocknum(Unlock_SPACESTATION2_COMPLETE);
2020-01-01 21:29:24 +01:00
lastsaved = 2;
music.play(Music_PATHCOMPLETE);
2022-12-07 00:20:48 +01:00
incstate();
2022-12-07 00:35:06 +01:00
setstatedelay(75);
2020-01-01 21:29:24 +01:00
levelcomplete_textbox();
2020-01-01 21:29:24 +01:00
break;
case 3021:
2022-12-07 00:20:48 +01:00
incstate();
2022-12-07 00:35:06 +01:00
setstatedelay(45);
2020-01-01 21:29:24 +01:00
crewmate_textbox(14);
2020-01-01 21:29:24 +01:00
break;
case 3022:
2022-12-07 00:20:48 +01:00
incstate();
2022-12-07 00:35:06 +01:00
setstatedelay(45);
2020-01-01 21:29:24 +01:00
remaining_textbox();
2020-01-01 21:29:24 +01:00
break;
case 3023:
2022-12-07 00:20:48 +01:00
incstate();
2022-12-07 00:35:06 +01:00
setstatedelay(0);
2020-01-01 21:29:24 +01:00
actionprompt_textbox();
2020-01-01 21:29:24 +01:00
break;
case 3024:
if (jumppressed)
{
2022-12-07 00:20:48 +01:00
incstate();
2022-12-07 00:35:06 +01:00
setstatedelay(30);
graphics.textboxremove();
2020-01-01 21:29:24 +01:00
}
break;
case 3025:
2022-12-08 02:10:12 +01:00
setstate(3070);
setstatedelay(0);
2020-01-01 21:29:24 +01:00
break;
case 3040:
//Level complete! (Lab)
unlocknum(Unlock_LABORATORY_COMPLETE);
2020-01-01 21:29:24 +01:00
lastsaved = 5;
music.play(Music_PATHCOMPLETE);
2022-12-07 00:20:48 +01:00
incstate();
2022-12-07 00:35:06 +01:00
setstatedelay(75);
2020-01-01 21:29:24 +01:00
levelcomplete_textbox();
2020-01-01 21:29:24 +01:00
break;
case 3041:
2022-12-07 00:20:48 +01:00
incstate();
2022-12-07 00:35:06 +01:00
setstatedelay(45);
2020-01-01 21:29:24 +01:00
crewmate_textbox(16);
2020-01-01 21:29:24 +01:00
break;
case 3042:
2022-12-07 00:20:48 +01:00
incstate();
2022-12-07 00:35:06 +01:00
setstatedelay(45);
2020-01-01 21:29:24 +01:00
remaining_textbox();
2020-01-01 21:29:24 +01:00
break;
case 3043:
2022-12-07 00:20:48 +01:00
incstate();
2022-12-07 00:35:06 +01:00
setstatedelay(0);
2020-01-01 21:29:24 +01:00
actionprompt_textbox();
2020-01-01 21:29:24 +01:00
break;
case 3044:
if (jumppressed)
{
2022-12-07 00:20:48 +01:00
incstate();
2022-12-07 00:35:06 +01:00
setstatedelay(30);
graphics.textboxremove();
2020-01-01 21:29:24 +01:00
}
break;
case 3045:
2022-12-08 02:10:12 +01:00
setstate(3070);
setstatedelay(0);
2020-01-01 21:29:24 +01:00
break;
case 3050:
//Level complete! (Space Station 1)
unlocknum(Unlock_SPACESTATION1_COMPLETE);
2020-01-01 21:29:24 +01:00
lastsaved = 1;
music.play(Music_PATHCOMPLETE);
2022-12-07 00:20:48 +01:00
incstate();
2022-12-07 00:35:06 +01:00
setstatedelay(75);
2020-01-01 21:29:24 +01:00
levelcomplete_textbox();
2020-01-01 21:29:24 +01:00
break;
case 3051:
2022-12-07 00:20:48 +01:00
incstate();
2022-12-07 00:35:06 +01:00
setstatedelay(45);
2020-01-01 21:29:24 +01:00
crewmate_textbox(20);
2020-01-01 21:29:24 +01:00
break;
case 3052:
2022-12-07 00:20:48 +01:00
incstate();
2022-12-07 00:35:06 +01:00
setstatedelay(45);
2020-01-01 21:29:24 +01:00
remaining_textbox();
2020-01-01 21:29:24 +01:00
break;
case 3053:
2022-12-07 00:20:48 +01:00
incstate();
2022-12-07 00:35:06 +01:00
setstatedelay(0);
2020-01-01 21:29:24 +01:00
actionprompt_textbox();
2020-01-01 21:29:24 +01:00
break;
case 3054:
if (jumppressed)
{
2022-12-07 00:20:48 +01:00
incstate();
2022-12-07 00:35:06 +01:00
setstatedelay(30);
graphics.textboxremove();
2020-01-01 21:29:24 +01:00
teleportscript = "";
}
break;
case 3055:
Enumify all fade modes This removes the magic numbers previously used for controlling the fade mode, which are really not readable at all unless you already know what they mean. 0: FADE_NONE 1: FADE_FULLY_BLACK 2: FADE_START_FADEOUT 3: FADE_FADING_OUT 4: FADE_START_FADEIN 5: FADE_FADING_IN There is also the macro FADEMODE_IS_FADING, which indicates when the intention is to only check if the game is fading right now, which wasn't clearly conveyed previously. I also took the opportunity to clean up the style of any lines I touched. This included rewriting if-else chains into case-switches, turning one-liner if-then statements into proper blocks, fixing up comments, and even commenting the `fademode == FADE_NONE` on the tower spike checks (which, it was previously undocumented why that check was there, but I think I know why it's there). As for type safety, we already get some by transforming the variable types into the enum. Assignment is prohibited without a cast. But, apparently, comparison is perfectly legal and won't even give so much as a warning. To work around this and make absolutely sure I made all existing comparisons now use the enum, I temporarily changed it to be an `enum class`, which is a C++11 feature that makes it so all comparisons are illegal. Unfortunately, it scopes them in a namespace with the same name as a class, so I had to temporarily define macros to make sure my existing code worked. I also had to temporarily up the standard in CMakeLists.txt to get it to compile. But after all that was done, I found the rest of the places where a comparison to an integer was used, and fixed them.
2022-04-25 09:57:47 +02:00
graphics.fademode = FADE_START_FADEOUT;
2022-12-07 00:20:48 +01:00
incstate();
2022-12-07 00:35:06 +01:00
setstatedelay(10);
2020-01-01 21:29:24 +01:00
break;
case 3056:
Enumify all fade modes This removes the magic numbers previously used for controlling the fade mode, which are really not readable at all unless you already know what they mean. 0: FADE_NONE 1: FADE_FULLY_BLACK 2: FADE_START_FADEOUT 3: FADE_FADING_OUT 4: FADE_START_FADEIN 5: FADE_FADING_IN There is also the macro FADEMODE_IS_FADING, which indicates when the intention is to only check if the game is fading right now, which wasn't clearly conveyed previously. I also took the opportunity to clean up the style of any lines I touched. This included rewriting if-else chains into case-switches, turning one-liner if-then statements into proper blocks, fixing up comments, and even commenting the `fademode == FADE_NONE` on the tower spike checks (which, it was previously undocumented why that check was there, but I think I know why it's there). As for type safety, we already get some by transforming the variable types into the enum. Assignment is prohibited without a cast. But, apparently, comparison is perfectly legal and won't even give so much as a warning. To work around this and make absolutely sure I made all existing comparisons now use the enum, I temporarily changed it to be an `enum class`, which is a C++11 feature that makes it so all comparisons are illegal. Unfortunately, it scopes them in a namespace with the same name as a class, so I had to temporarily define macros to make sure my existing code worked. I also had to temporarily up the standard in CMakeLists.txt to get it to compile. But after all that was done, I found the rest of the places where a comparison to an integer was used, and fixed them.
2022-04-25 09:57:47 +02:00
if (graphics.fademode == FADE_FULLY_BLACK)
2020-01-01 21:29:24 +01:00
{
startscript = true;
Fix unwinnable save from rescuing Violet out of order You're intended to rescue Violet first, and not second, third, or fourth, and especially not last. If you rescue her second, third, or fourth, your crewmate progress will be reset, but you won't be able to re-rescue them again. This is because Vitellary, Verdigris, Victoria, and Vermilion will be temporarily marked as rescued during the `bigopenworld` cutscene, so duplicate versions of them don't spawn during the cutscene, and then will be marked as missing later to undo it. This first issue can be trivially fixed by simply toggling flags to prevent duplicates of them from spawning during the cutscene instead of fiddling with their rescue statuses. However, there is still another issue. If you rescue Violet last, then you won't be warped to the Final Level, meaning you can't properly complete the game. This can be fixed by adding a `crewrescued() == 6` check to the Space Station 1 Level Complete cutscene. There is additionally a temporary unrescuing of Violet so she doesn't get duplicated during the `bigopenworld` cutscene, and I've had to move that to the start of the `bigopenworld` and `bigopenworldskip` scripts, otherwise the `crewrescued() == 6` check won't work properly. I haven't added hooks for Intermission 1 or 2 because you're not really meant to play the intermissions with Violet (but you probably could anyway, there'd just be no dialogue). Oh, and the pre-Final Level cutscene expects the player to already be hidden before it starts playing, but if you rescue Violet last the player is still visible, so I've fixed that. But there still ends up being two Violets, so I'll probably replace it with a special cutscene later that's not so nonsensical.
2020-08-09 01:09:55 +02:00
if (crewrescued() == 6)
2020-01-01 21:29:24 +01:00
{
Fix unwinnable save from rescuing Violet out of order You're intended to rescue Violet first, and not second, third, or fourth, and especially not last. If you rescue her second, third, or fourth, your crewmate progress will be reset, but you won't be able to re-rescue them again. This is because Vitellary, Verdigris, Victoria, and Vermilion will be temporarily marked as rescued during the `bigopenworld` cutscene, so duplicate versions of them don't spawn during the cutscene, and then will be marked as missing later to undo it. This first issue can be trivially fixed by simply toggling flags to prevent duplicates of them from spawning during the cutscene instead of fiddling with their rescue statuses. However, there is still another issue. If you rescue Violet last, then you won't be warped to the Final Level, meaning you can't properly complete the game. This can be fixed by adding a `crewrescued() == 6` check to the Space Station 1 Level Complete cutscene. There is additionally a temporary unrescuing of Violet so she doesn't get duplicated during the `bigopenworld` cutscene, and I've had to move that to the start of the `bigopenworld` and `bigopenworldskip` scripts, otherwise the `crewrescued() == 6` check won't work properly. I haven't added hooks for Intermission 1 or 2 because you're not really meant to play the intermissions with Violet (but you probably could anyway, there'd just be no dialogue). Oh, and the pre-Final Level cutscene expects the player to already be hidden before it starts playing, but if you rescue Violet last the player is still visible, so I've fixed that. But there still ends up being two Violets, so I'll probably replace it with a special cutscene later that's not so nonsensical.
2020-08-09 01:09:55 +02:00
newscript = "startlevel_final";
2020-01-01 21:29:24 +01:00
}
else
{
Fix unwinnable save from rescuing Violet out of order You're intended to rescue Violet first, and not second, third, or fourth, and especially not last. If you rescue her second, third, or fourth, your crewmate progress will be reset, but you won't be able to re-rescue them again. This is because Vitellary, Verdigris, Victoria, and Vermilion will be temporarily marked as rescued during the `bigopenworld` cutscene, so duplicate versions of them don't spawn during the cutscene, and then will be marked as missing later to undo it. This first issue can be trivially fixed by simply toggling flags to prevent duplicates of them from spawning during the cutscene instead of fiddling with their rescue statuses. However, there is still another issue. If you rescue Violet last, then you won't be warped to the Final Level, meaning you can't properly complete the game. This can be fixed by adding a `crewrescued() == 6` check to the Space Station 1 Level Complete cutscene. There is additionally a temporary unrescuing of Violet so she doesn't get duplicated during the `bigopenworld` cutscene, and I've had to move that to the start of the `bigopenworld` and `bigopenworldskip` scripts, otherwise the `crewrescued() == 6` check won't work properly. I haven't added hooks for Intermission 1 or 2 because you're not really meant to play the intermissions with Violet (but you probably could anyway, there'd just be no dialogue). Oh, and the pre-Final Level cutscene expects the player to already be hidden before it starts playing, but if you rescue Violet last the player is still visible, so I've fixed that. But there still ends up being two Violets, so I'll probably replace it with a special cutscene later that's not so nonsensical.
2020-08-09 01:09:55 +02:00
if (nocutscenes)
{
newscript="bigopenworldskip";
}
else
{
newscript = "bigopenworld";
}
2020-01-01 21:29:24 +01:00
}
2022-12-07 00:20:48 +01:00
setstate(0);
2020-01-01 21:29:24 +01:00
}
break;
case 3060:
//Level complete! (Tower)
unlocknum(Unlock_TOWER_COMPLETE);
2020-01-01 21:29:24 +01:00
lastsaved = 3;
music.play(Music_PATHCOMPLETE);
2022-12-07 00:20:48 +01:00
incstate();
2022-12-07 00:35:06 +01:00
setstatedelay(75);
2020-01-01 21:29:24 +01:00
levelcomplete_textbox();
2020-01-01 21:29:24 +01:00
break;
case 3061:
2022-12-07 00:20:48 +01:00
incstate();
2022-12-07 00:35:06 +01:00
setstatedelay(45);
2020-01-01 21:29:24 +01:00
crewmate_textbox(15);
2020-01-01 21:29:24 +01:00
break;
case 3062:
2022-12-07 00:20:48 +01:00
incstate();
2022-12-07 00:35:06 +01:00
setstatedelay(45);
2020-01-01 21:29:24 +01:00
remaining_textbox();
2020-01-01 21:29:24 +01:00
break;
case 3063:
2022-12-07 00:20:48 +01:00
incstate();
2022-12-07 00:35:06 +01:00
setstatedelay(0);
2020-01-01 21:29:24 +01:00
actionprompt_textbox();
2020-01-01 21:29:24 +01:00
break;
case 3064:
if (jumppressed)
{
2022-12-07 00:20:48 +01:00
incstate();
2022-12-07 00:35:06 +01:00
setstatedelay(30);
graphics.textboxremove();
2020-01-01 21:29:24 +01:00
}
break;
case 3065:
2022-12-08 02:10:12 +01:00
setstate(3070);
setstatedelay(0);
2020-01-01 21:29:24 +01:00
break;
case 3070:
Enumify all fade modes This removes the magic numbers previously used for controlling the fade mode, which are really not readable at all unless you already know what they mean. 0: FADE_NONE 1: FADE_FULLY_BLACK 2: FADE_START_FADEOUT 3: FADE_FADING_OUT 4: FADE_START_FADEIN 5: FADE_FADING_IN There is also the macro FADEMODE_IS_FADING, which indicates when the intention is to only check if the game is fading right now, which wasn't clearly conveyed previously. I also took the opportunity to clean up the style of any lines I touched. This included rewriting if-else chains into case-switches, turning one-liner if-then statements into proper blocks, fixing up comments, and even commenting the `fademode == FADE_NONE` on the tower spike checks (which, it was previously undocumented why that check was there, but I think I know why it's there). As for type safety, we already get some by transforming the variable types into the enum. Assignment is prohibited without a cast. But, apparently, comparison is perfectly legal and won't even give so much as a warning. To work around this and make absolutely sure I made all existing comparisons now use the enum, I temporarily changed it to be an `enum class`, which is a C++11 feature that makes it so all comparisons are illegal. Unfortunately, it scopes them in a namespace with the same name as a class, so I had to temporarily define macros to make sure my existing code worked. I also had to temporarily up the standard in CMakeLists.txt to get it to compile. But after all that was done, I found the rest of the places where a comparison to an integer was used, and fixed them.
2022-04-25 09:57:47 +02:00
graphics.fademode = FADE_START_FADEOUT;
2022-12-07 00:20:48 +01:00
incstate();
2020-01-01 21:29:24 +01:00
break;
case 3071:
Enumify all fade modes This removes the magic numbers previously used for controlling the fade mode, which are really not readable at all unless you already know what they mean. 0: FADE_NONE 1: FADE_FULLY_BLACK 2: FADE_START_FADEOUT 3: FADE_FADING_OUT 4: FADE_START_FADEIN 5: FADE_FADING_IN There is also the macro FADEMODE_IS_FADING, which indicates when the intention is to only check if the game is fading right now, which wasn't clearly conveyed previously. I also took the opportunity to clean up the style of any lines I touched. This included rewriting if-else chains into case-switches, turning one-liner if-then statements into proper blocks, fixing up comments, and even commenting the `fademode == FADE_NONE` on the tower spike checks (which, it was previously undocumented why that check was there, but I think I know why it's there). As for type safety, we already get some by transforming the variable types into the enum. Assignment is prohibited without a cast. But, apparently, comparison is perfectly legal and won't even give so much as a warning. To work around this and make absolutely sure I made all existing comparisons now use the enum, I temporarily changed it to be an `enum class`, which is a C++11 feature that makes it so all comparisons are illegal. Unfortunately, it scopes them in a namespace with the same name as a class, so I had to temporarily define macros to make sure my existing code worked. I also had to temporarily up the standard in CMakeLists.txt to get it to compile. But after all that was done, I found the rest of the places where a comparison to an integer was used, and fixed them.
2022-04-25 09:57:47 +02:00
if (graphics.fademode == FADE_FULLY_BLACK)
{
2022-12-07 00:20:48 +01:00
incstate();
Enumify all fade modes This removes the magic numbers previously used for controlling the fade mode, which are really not readable at all unless you already know what they mean. 0: FADE_NONE 1: FADE_FULLY_BLACK 2: FADE_START_FADEOUT 3: FADE_FADING_OUT 4: FADE_START_FADEIN 5: FADE_FADING_IN There is also the macro FADEMODE_IS_FADING, which indicates when the intention is to only check if the game is fading right now, which wasn't clearly conveyed previously. I also took the opportunity to clean up the style of any lines I touched. This included rewriting if-else chains into case-switches, turning one-liner if-then statements into proper blocks, fixing up comments, and even commenting the `fademode == FADE_NONE` on the tower spike checks (which, it was previously undocumented why that check was there, but I think I know why it's there). As for type safety, we already get some by transforming the variable types into the enum. Assignment is prohibited without a cast. But, apparently, comparison is perfectly legal and won't even give so much as a warning. To work around this and make absolutely sure I made all existing comparisons now use the enum, I temporarily changed it to be an `enum class`, which is a C++11 feature that makes it so all comparisons are illegal. Unfortunately, it scopes them in a namespace with the same name as a class, so I had to temporarily define macros to make sure my existing code worked. I also had to temporarily up the standard in CMakeLists.txt to get it to compile. But after all that was done, I found the rest of the places where a comparison to an integer was used, and fixed them.
2022-04-25 09:57:47 +02:00
}
2020-01-01 21:29:24 +01:00
break;
case 3072:
//Ok, we need to adjust some flags based on who've we've rescued. Some of there conversation options
//change depending on when they get back to the ship.
if (lastsaved == 2)
{
if (crewstats[3]) obj.flags[25] = true;
if (crewstats[4]) obj.flags[26] = true;
if (crewstats[5]) obj.flags[24] = true;
2020-01-01 21:29:24 +01:00
}
else if (lastsaved == 3)
{
if (crewstats[2]) obj.flags[50] = true;
if (crewstats[4]) obj.flags[49] = true;
if (crewstats[5]) obj.flags[48] = true;
2020-01-01 21:29:24 +01:00
}
else if (lastsaved == 4)
{
if (crewstats[2]) obj.flags[54] = true;
if (crewstats[3]) obj.flags[55] = true;
if (crewstats[5]) obj.flags[56] = true;
2020-01-01 21:29:24 +01:00
}
else if (lastsaved == 5)
{
if (crewstats[2]) obj.flags[37] = true;
if (crewstats[3]) obj.flags[38] = true;
if (crewstats[4]) obj.flags[39] = true;
2020-01-01 21:29:24 +01:00
}
//We're pitch black now, make a decision
companion = 0;
if (crewrescued() == 6)
{
startscript = true;
newscript="startlevel_final";
2022-12-07 00:20:48 +01:00
setstate(0);
2020-01-01 21:29:24 +01:00
}
else if (crewrescued() == 4)
{
companion = 11;
supercrewmate = true;
scmprogress = 0;
startscript = true;
newscript = "intermission_1";
obj.flags[19] = true;
if (lastsaved == 2) obj.flags[32] = true;
if (lastsaved == 3) obj.flags[35] = true;
if (lastsaved == 4) obj.flags[34] = true;
if (lastsaved == 5) obj.flags[33] = true;
2022-12-07 00:20:48 +01:00
setstate(0);
2020-01-01 21:29:24 +01:00
}
else if (crewrescued() == 5)
{
startscript = true;
newscript = "intermission_2";
obj.flags[20] = true;
if (lastsaved == 2) obj.flags[32] = true;
if (lastsaved == 3) obj.flags[35] = true;
if (lastsaved == 4) obj.flags[34] = true;
if (lastsaved == 5) obj.flags[33] = true;
2022-12-07 00:20:48 +01:00
setstate(0);
2020-01-01 21:29:24 +01:00
}
else
{
startscript = true;
newscript="regularreturn";
2022-12-07 00:20:48 +01:00
setstate(0);
2020-01-01 21:29:24 +01:00
}
break;
case 3080:
//returning from an intermission, very like 3070
if (inintermission)
{
Enumify all fade modes This removes the magic numbers previously used for controlling the fade mode, which are really not readable at all unless you already know what they mean. 0: FADE_NONE 1: FADE_FULLY_BLACK 2: FADE_START_FADEOUT 3: FADE_FADING_OUT 4: FADE_START_FADEIN 5: FADE_FADING_IN There is also the macro FADEMODE_IS_FADING, which indicates when the intention is to only check if the game is fading right now, which wasn't clearly conveyed previously. I also took the opportunity to clean up the style of any lines I touched. This included rewriting if-else chains into case-switches, turning one-liner if-then statements into proper blocks, fixing up comments, and even commenting the `fademode == FADE_NONE` on the tower spike checks (which, it was previously undocumented why that check was there, but I think I know why it's there). As for type safety, we already get some by transforming the variable types into the enum. Assignment is prohibited without a cast. But, apparently, comparison is perfectly legal and won't even give so much as a warning. To work around this and make absolutely sure I made all existing comparisons now use the enum, I temporarily changed it to be an `enum class`, which is a C++11 feature that makes it so all comparisons are illegal. Unfortunately, it scopes them in a namespace with the same name as a class, so I had to temporarily define macros to make sure my existing code worked. I also had to temporarily up the standard in CMakeLists.txt to get it to compile. But after all that was done, I found the rest of the places where a comparison to an integer was used, and fixed them.
2022-04-25 09:57:47 +02:00
graphics.fademode = FADE_START_FADEOUT;
2020-01-01 21:29:24 +01:00
companion = 0;
2022-12-07 00:20:48 +01:00
setstate(3100);
2020-01-01 21:29:24 +01:00
}
else
{
unlocknum(Unlock_INTERMISSION2_COMPLETE);
Enumify all fade modes This removes the magic numbers previously used for controlling the fade mode, which are really not readable at all unless you already know what they mean. 0: FADE_NONE 1: FADE_FULLY_BLACK 2: FADE_START_FADEOUT 3: FADE_FADING_OUT 4: FADE_START_FADEIN 5: FADE_FADING_IN There is also the macro FADEMODE_IS_FADING, which indicates when the intention is to only check if the game is fading right now, which wasn't clearly conveyed previously. I also took the opportunity to clean up the style of any lines I touched. This included rewriting if-else chains into case-switches, turning one-liner if-then statements into proper blocks, fixing up comments, and even commenting the `fademode == FADE_NONE` on the tower spike checks (which, it was previously undocumented why that check was there, but I think I know why it's there). As for type safety, we already get some by transforming the variable types into the enum. Assignment is prohibited without a cast. But, apparently, comparison is perfectly legal and won't even give so much as a warning. To work around this and make absolutely sure I made all existing comparisons now use the enum, I temporarily changed it to be an `enum class`, which is a C++11 feature that makes it so all comparisons are illegal. Unfortunately, it scopes them in a namespace with the same name as a class, so I had to temporarily define macros to make sure my existing code worked. I also had to temporarily up the standard in CMakeLists.txt to get it to compile. But after all that was done, I found the rest of the places where a comparison to an integer was used, and fixed them.
2022-04-25 09:57:47 +02:00
graphics.fademode = FADE_START_FADEOUT;
2020-01-01 21:29:24 +01:00
companion = 0;
2022-12-07 00:20:48 +01:00
incstate();
2020-01-01 21:29:24 +01:00
}
break;
case 3081:
Enumify all fade modes This removes the magic numbers previously used for controlling the fade mode, which are really not readable at all unless you already know what they mean. 0: FADE_NONE 1: FADE_FULLY_BLACK 2: FADE_START_FADEOUT 3: FADE_FADING_OUT 4: FADE_START_FADEIN 5: FADE_FADING_IN There is also the macro FADEMODE_IS_FADING, which indicates when the intention is to only check if the game is fading right now, which wasn't clearly conveyed previously. I also took the opportunity to clean up the style of any lines I touched. This included rewriting if-else chains into case-switches, turning one-liner if-then statements into proper blocks, fixing up comments, and even commenting the `fademode == FADE_NONE` on the tower spike checks (which, it was previously undocumented why that check was there, but I think I know why it's there). As for type safety, we already get some by transforming the variable types into the enum. Assignment is prohibited without a cast. But, apparently, comparison is perfectly legal and won't even give so much as a warning. To work around this and make absolutely sure I made all existing comparisons now use the enum, I temporarily changed it to be an `enum class`, which is a C++11 feature that makes it so all comparisons are illegal. Unfortunately, it scopes them in a namespace with the same name as a class, so I had to temporarily define macros to make sure my existing code worked. I also had to temporarily up the standard in CMakeLists.txt to get it to compile. But after all that was done, I found the rest of the places where a comparison to an integer was used, and fixed them.
2022-04-25 09:57:47 +02:00
if (graphics.fademode == FADE_FULLY_BLACK)
{
2022-12-07 00:20:48 +01:00
incstate();
Enumify all fade modes This removes the magic numbers previously used for controlling the fade mode, which are really not readable at all unless you already know what they mean. 0: FADE_NONE 1: FADE_FULLY_BLACK 2: FADE_START_FADEOUT 3: FADE_FADING_OUT 4: FADE_START_FADEIN 5: FADE_FADING_IN There is also the macro FADEMODE_IS_FADING, which indicates when the intention is to only check if the game is fading right now, which wasn't clearly conveyed previously. I also took the opportunity to clean up the style of any lines I touched. This included rewriting if-else chains into case-switches, turning one-liner if-then statements into proper blocks, fixing up comments, and even commenting the `fademode == FADE_NONE` on the tower spike checks (which, it was previously undocumented why that check was there, but I think I know why it's there). As for type safety, we already get some by transforming the variable types into the enum. Assignment is prohibited without a cast. But, apparently, comparison is perfectly legal and won't even give so much as a warning. To work around this and make absolutely sure I made all existing comparisons now use the enum, I temporarily changed it to be an `enum class`, which is a C++11 feature that makes it so all comparisons are illegal. Unfortunately, it scopes them in a namespace with the same name as a class, so I had to temporarily define macros to make sure my existing code worked. I also had to temporarily up the standard in CMakeLists.txt to get it to compile. But after all that was done, I found the rest of the places where a comparison to an integer was used, and fixed them.
2022-04-25 09:57:47 +02:00
}
2020-01-01 21:29:24 +01:00
break;
case 3082:
map.finalmode = false;
startscript = true;
newscript="regularreturn";
2022-12-07 00:20:48 +01:00
setstate(0);
2020-01-01 21:29:24 +01:00
break;
case 3085:
//returning from an intermission, very like 3070
//return to menu from here
if (inintermission)
{
companion = 0;
supercrewmate = false;
2022-12-07 00:20:48 +01:00
incstate();
Enumify all fade modes This removes the magic numbers previously used for controlling the fade mode, which are really not readable at all unless you already know what they mean. 0: FADE_NONE 1: FADE_FULLY_BLACK 2: FADE_START_FADEOUT 3: FADE_FADING_OUT 4: FADE_START_FADEIN 5: FADE_FADING_IN There is also the macro FADEMODE_IS_FADING, which indicates when the intention is to only check if the game is fading right now, which wasn't clearly conveyed previously. I also took the opportunity to clean up the style of any lines I touched. This included rewriting if-else chains into case-switches, turning one-liner if-then statements into proper blocks, fixing up comments, and even commenting the `fademode == FADE_NONE` on the tower spike checks (which, it was previously undocumented why that check was there, but I think I know why it's there). As for type safety, we already get some by transforming the variable types into the enum. Assignment is prohibited without a cast. But, apparently, comparison is perfectly legal and won't even give so much as a warning. To work around this and make absolutely sure I made all existing comparisons now use the enum, I temporarily changed it to be an `enum class`, which is a C++11 feature that makes it so all comparisons are illegal. Unfortunately, it scopes them in a namespace with the same name as a class, so I had to temporarily define macros to make sure my existing code worked. I also had to temporarily up the standard in CMakeLists.txt to get it to compile. But after all that was done, I found the rest of the places where a comparison to an integer was used, and fixed them.
2022-04-25 09:57:47 +02:00
graphics.fademode = FADE_START_FADEOUT;
2020-01-01 21:29:24 +01:00
music.fadeout();
2022-12-07 00:20:48 +01:00
setstate(3100);
2020-01-01 21:29:24 +01:00
}
else
{
unlocknum(Unlock_INTERMISSION1_COMPLETE);
Enumify all fade modes This removes the magic numbers previously used for controlling the fade mode, which are really not readable at all unless you already know what they mean. 0: FADE_NONE 1: FADE_FULLY_BLACK 2: FADE_START_FADEOUT 3: FADE_FADING_OUT 4: FADE_START_FADEIN 5: FADE_FADING_IN There is also the macro FADEMODE_IS_FADING, which indicates when the intention is to only check if the game is fading right now, which wasn't clearly conveyed previously. I also took the opportunity to clean up the style of any lines I touched. This included rewriting if-else chains into case-switches, turning one-liner if-then statements into proper blocks, fixing up comments, and even commenting the `fademode == FADE_NONE` on the tower spike checks (which, it was previously undocumented why that check was there, but I think I know why it's there). As for type safety, we already get some by transforming the variable types into the enum. Assignment is prohibited without a cast. But, apparently, comparison is perfectly legal and won't even give so much as a warning. To work around this and make absolutely sure I made all existing comparisons now use the enum, I temporarily changed it to be an `enum class`, which is a C++11 feature that makes it so all comparisons are illegal. Unfortunately, it scopes them in a namespace with the same name as a class, so I had to temporarily define macros to make sure my existing code worked. I also had to temporarily up the standard in CMakeLists.txt to get it to compile. But after all that was done, I found the rest of the places where a comparison to an integer was used, and fixed them.
2022-04-25 09:57:47 +02:00
graphics.fademode = FADE_START_FADEOUT;
2020-01-01 21:29:24 +01:00
companion = 0;
supercrewmate = false;
2022-12-07 00:20:48 +01:00
incstate();
2020-01-01 21:29:24 +01:00
}
break;
case 3086:
Enumify all fade modes This removes the magic numbers previously used for controlling the fade mode, which are really not readable at all unless you already know what they mean. 0: FADE_NONE 1: FADE_FULLY_BLACK 2: FADE_START_FADEOUT 3: FADE_FADING_OUT 4: FADE_START_FADEIN 5: FADE_FADING_IN There is also the macro FADEMODE_IS_FADING, which indicates when the intention is to only check if the game is fading right now, which wasn't clearly conveyed previously. I also took the opportunity to clean up the style of any lines I touched. This included rewriting if-else chains into case-switches, turning one-liner if-then statements into proper blocks, fixing up comments, and even commenting the `fademode == FADE_NONE` on the tower spike checks (which, it was previously undocumented why that check was there, but I think I know why it's there). As for type safety, we already get some by transforming the variable types into the enum. Assignment is prohibited without a cast. But, apparently, comparison is perfectly legal and won't even give so much as a warning. To work around this and make absolutely sure I made all existing comparisons now use the enum, I temporarily changed it to be an `enum class`, which is a C++11 feature that makes it so all comparisons are illegal. Unfortunately, it scopes them in a namespace with the same name as a class, so I had to temporarily define macros to make sure my existing code worked. I also had to temporarily up the standard in CMakeLists.txt to get it to compile. But after all that was done, I found the rest of the places where a comparison to an integer was used, and fixed them.
2022-04-25 09:57:47 +02:00
if (graphics.fademode == FADE_FULLY_BLACK)
{
2022-12-07 00:20:48 +01:00
incstate();
Enumify all fade modes This removes the magic numbers previously used for controlling the fade mode, which are really not readable at all unless you already know what they mean. 0: FADE_NONE 1: FADE_FULLY_BLACK 2: FADE_START_FADEOUT 3: FADE_FADING_OUT 4: FADE_START_FADEIN 5: FADE_FADING_IN There is also the macro FADEMODE_IS_FADING, which indicates when the intention is to only check if the game is fading right now, which wasn't clearly conveyed previously. I also took the opportunity to clean up the style of any lines I touched. This included rewriting if-else chains into case-switches, turning one-liner if-then statements into proper blocks, fixing up comments, and even commenting the `fademode == FADE_NONE` on the tower spike checks (which, it was previously undocumented why that check was there, but I think I know why it's there). As for type safety, we already get some by transforming the variable types into the enum. Assignment is prohibited without a cast. But, apparently, comparison is perfectly legal and won't even give so much as a warning. To work around this and make absolutely sure I made all existing comparisons now use the enum, I temporarily changed it to be an `enum class`, which is a C++11 feature that makes it so all comparisons are illegal. Unfortunately, it scopes them in a namespace with the same name as a class, so I had to temporarily define macros to make sure my existing code worked. I also had to temporarily up the standard in CMakeLists.txt to get it to compile. But after all that was done, I found the rest of the places where a comparison to an integer was used, and fixed them.
2022-04-25 09:57:47 +02:00
}
2020-01-01 21:29:24 +01:00
break;
case 3087:
map.finalmode = false;
startscript = true;
newscript="regularreturn";
2022-12-07 00:20:48 +01:00
setstate(0);
2020-01-01 21:29:24 +01:00
break;
Add level exploring menu for translators I would, of course, recommend translators to translate the roomnames while playing the full game (optionally in invincibility) so they can immediately get all the context and maybe the most inspiration. And if you want to go back into a specific level, then there's always the time trials and intermission replays which will give you full coverage of all the room names. However, the time trials weren't really made for room name translation. They have some annoying features like the instant restart when you press ENTER at the wrong time, they remove context clues like teleporters and companions, but the worst problem is that the last room in a level is often completely untranslatable inside the time trials because you immediately get sent to the results screen... So, I added a new menu in the translator options, "explore game", which gives you access to all the time trials and the two intermissions, from the same menu. All these time trials (which they're still based off of, under the hood) are stripped of the annoying features that come with time trials. These are the changes I made to time trial behavior in translator exploring mode: - No 3-2-1-Go! countdown - No on-screen time/death/shiny/par - ENTER doesn't restart, and the map menu works. The entire map is also revealed. - Prize for the Reckless is in its normal form - The teleporters in Entanglement Generator, Wheeler's Wormhole and Level Complete are restored as context for room names (actually, we should probably restore them in time trials anyway? Their "press to teleport" prompt is already blocked out in time trials and they do nothing other than being a checkpoint. I guess the reason they were removed was to stop people from opening the teleporter menu when that was not specifically blocked out in time trials yet.) - The companions are there at the end of levels, and behave like in no death mode (become happy and follow you to the teleporter). Also for context. - At the end of each level, you're not suddenly sent to the menu, but you can use the teleporter at your leisure just like in the intermission replays. In the Final Level, you do get sent to the menu automatically, but after a longer delay. I made another mark on VVVVVV: don't be startled, I added gamestates. I wanted all teleporters at the end of levels to behave like the ones at the end of the intermission replays, and all handling for teleporting with specific companions is already done in gamestates, so rather than adding conditional blocks across 5 or so different gamestates, it made more sense to make a single gamestate for "teleporting in translator exploring mode" (3090). I also added an alternative to having to use gamestate 3500 or 82 for the end of the final level: 3091-3092. One other thing I want to add to the "explore game" menu: a per-level count of how many room names are left to translate. That shouldn't be too difficult, and I'm planning that for the next commit.
2022-11-26 03:33:17 +01:00
case 3090:
/* Teleporting in translator_exploring should be just like
* the intermission replays: simply return to the menu */
companion = 0;
supercrewmate = false;
graphics.fademode = FADE_START_FADEOUT;
music.fadeout();
setstate(3100);
Add level exploring menu for translators I would, of course, recommend translators to translate the roomnames while playing the full game (optionally in invincibility) so they can immediately get all the context and maybe the most inspiration. And if you want to go back into a specific level, then there's always the time trials and intermission replays which will give you full coverage of all the room names. However, the time trials weren't really made for room name translation. They have some annoying features like the instant restart when you press ENTER at the wrong time, they remove context clues like teleporters and companions, but the worst problem is that the last room in a level is often completely untranslatable inside the time trials because you immediately get sent to the results screen... So, I added a new menu in the translator options, "explore game", which gives you access to all the time trials and the two intermissions, from the same menu. All these time trials (which they're still based off of, under the hood) are stripped of the annoying features that come with time trials. These are the changes I made to time trial behavior in translator exploring mode: - No 3-2-1-Go! countdown - No on-screen time/death/shiny/par - ENTER doesn't restart, and the map menu works. The entire map is also revealed. - Prize for the Reckless is in its normal form - The teleporters in Entanglement Generator, Wheeler's Wormhole and Level Complete are restored as context for room names (actually, we should probably restore them in time trials anyway? Their "press to teleport" prompt is already blocked out in time trials and they do nothing other than being a checkpoint. I guess the reason they were removed was to stop people from opening the teleporter menu when that was not specifically blocked out in time trials yet.) - The companions are there at the end of levels, and behave like in no death mode (become happy and follow you to the teleporter). Also for context. - At the end of each level, you're not suddenly sent to the menu, but you can use the teleporter at your leisure just like in the intermission replays. In the Final Level, you do get sent to the menu automatically, but after a longer delay. I made another mark on VVVVVV: don't be startled, I added gamestates. I wanted all teleporters at the end of levels to behave like the ones at the end of the intermission replays, and all handling for teleporting with specific companions is already done in gamestates, so rather than adding conditional blocks across 5 or so different gamestates, it made more sense to make a single gamestate for "teleporting in translator exploring mode" (3090). I also added an alternative to having to use gamestate 3500 or 82 for the end of the final level: 3091-3092. One other thing I want to add to the "explore game" menu: a per-level count of how many room names are left to translate. That shouldn't be too difficult, and I'm planning that for the next commit.
2022-11-26 03:33:17 +01:00
break;
case 3091:
/* Different Final Level ending for translator_exploring */
music.fadeout();
incstate();
setstatedelay(60);
Add level exploring menu for translators I would, of course, recommend translators to translate the roomnames while playing the full game (optionally in invincibility) so they can immediately get all the context and maybe the most inspiration. And if you want to go back into a specific level, then there's always the time trials and intermission replays which will give you full coverage of all the room names. However, the time trials weren't really made for room name translation. They have some annoying features like the instant restart when you press ENTER at the wrong time, they remove context clues like teleporters and companions, but the worst problem is that the last room in a level is often completely untranslatable inside the time trials because you immediately get sent to the results screen... So, I added a new menu in the translator options, "explore game", which gives you access to all the time trials and the two intermissions, from the same menu. All these time trials (which they're still based off of, under the hood) are stripped of the annoying features that come with time trials. These are the changes I made to time trial behavior in translator exploring mode: - No 3-2-1-Go! countdown - No on-screen time/death/shiny/par - ENTER doesn't restart, and the map menu works. The entire map is also revealed. - Prize for the Reckless is in its normal form - The teleporters in Entanglement Generator, Wheeler's Wormhole and Level Complete are restored as context for room names (actually, we should probably restore them in time trials anyway? Their "press to teleport" prompt is already blocked out in time trials and they do nothing other than being a checkpoint. I guess the reason they were removed was to stop people from opening the teleporter menu when that was not specifically blocked out in time trials yet.) - The companions are there at the end of levels, and behave like in no death mode (become happy and follow you to the teleporter). Also for context. - At the end of each level, you're not suddenly sent to the menu, but you can use the teleporter at your leisure just like in the intermission replays. In the Final Level, you do get sent to the menu automatically, but after a longer delay. I made another mark on VVVVVV: don't be startled, I added gamestates. I wanted all teleporters at the end of levels to behave like the ones at the end of the intermission replays, and all handling for teleporting with specific companions is already done in gamestates, so rather than adding conditional blocks across 5 or so different gamestates, it made more sense to make a single gamestate for "teleporting in translator exploring mode" (3090). I also added an alternative to having to use gamestate 3500 or 82 for the end of the final level: 3091-3092. One other thing I want to add to the "explore game" menu: a per-level count of how many room names are left to translate. That shouldn't be too difficult, and I'm planning that for the next commit.
2022-11-26 03:33:17 +01:00
break;
case 3092:
graphics.fademode = FADE_START_FADEOUT;
setstate(3100);
Add level exploring menu for translators I would, of course, recommend translators to translate the roomnames while playing the full game (optionally in invincibility) so they can immediately get all the context and maybe the most inspiration. And if you want to go back into a specific level, then there's always the time trials and intermission replays which will give you full coverage of all the room names. However, the time trials weren't really made for room name translation. They have some annoying features like the instant restart when you press ENTER at the wrong time, they remove context clues like teleporters and companions, but the worst problem is that the last room in a level is often completely untranslatable inside the time trials because you immediately get sent to the results screen... So, I added a new menu in the translator options, "explore game", which gives you access to all the time trials and the two intermissions, from the same menu. All these time trials (which they're still based off of, under the hood) are stripped of the annoying features that come with time trials. These are the changes I made to time trial behavior in translator exploring mode: - No 3-2-1-Go! countdown - No on-screen time/death/shiny/par - ENTER doesn't restart, and the map menu works. The entire map is also revealed. - Prize for the Reckless is in its normal form - The teleporters in Entanglement Generator, Wheeler's Wormhole and Level Complete are restored as context for room names (actually, we should probably restore them in time trials anyway? Their "press to teleport" prompt is already blocked out in time trials and they do nothing other than being a checkpoint. I guess the reason they were removed was to stop people from opening the teleporter menu when that was not specifically blocked out in time trials yet.) - The companions are there at the end of levels, and behave like in no death mode (become happy and follow you to the teleporter). Also for context. - At the end of each level, you're not suddenly sent to the menu, but you can use the teleporter at your leisure just like in the intermission replays. In the Final Level, you do get sent to the menu automatically, but after a longer delay. I made another mark on VVVVVV: don't be startled, I added gamestates. I wanted all teleporters at the end of levels to behave like the ones at the end of the intermission replays, and all handling for teleporting with specific companions is already done in gamestates, so rather than adding conditional blocks across 5 or so different gamestates, it made more sense to make a single gamestate for "teleporting in translator exploring mode" (3090). I also added an alternative to having to use gamestate 3500 or 82 for the end of the final level: 3091-3092. One other thing I want to add to the "explore game" menu: a per-level count of how many room names are left to translate. That shouldn't be too difficult, and I'm planning that for the next commit.
2022-11-26 03:33:17 +01:00
break;
2020-01-01 21:29:24 +01:00
case 3100:
Enumify all fade modes This removes the magic numbers previously used for controlling the fade mode, which are really not readable at all unless you already know what they mean. 0: FADE_NONE 1: FADE_FULLY_BLACK 2: FADE_START_FADEOUT 3: FADE_FADING_OUT 4: FADE_START_FADEIN 5: FADE_FADING_IN There is also the macro FADEMODE_IS_FADING, which indicates when the intention is to only check if the game is fading right now, which wasn't clearly conveyed previously. I also took the opportunity to clean up the style of any lines I touched. This included rewriting if-else chains into case-switches, turning one-liner if-then statements into proper blocks, fixing up comments, and even commenting the `fademode == FADE_NONE` on the tower spike checks (which, it was previously undocumented why that check was there, but I think I know why it's there). As for type safety, we already get some by transforming the variable types into the enum. Assignment is prohibited without a cast. But, apparently, comparison is perfectly legal and won't even give so much as a warning. To work around this and make absolutely sure I made all existing comparisons now use the enum, I temporarily changed it to be an `enum class`, which is a C++11 feature that makes it so all comparisons are illegal. Unfortunately, it scopes them in a namespace with the same name as a class, so I had to temporarily define macros to make sure my existing code worked. I also had to temporarily up the standard in CMakeLists.txt to get it to compile. But after all that was done, I found the rest of the places where a comparison to an integer was used, and fixed them.
2022-04-25 09:57:47 +02:00
if (graphics.fademode == FADE_FULLY_BLACK)
{
2022-12-07 00:20:48 +01:00
incstate();
Enumify all fade modes This removes the magic numbers previously used for controlling the fade mode, which are really not readable at all unless you already know what they mean. 0: FADE_NONE 1: FADE_FULLY_BLACK 2: FADE_START_FADEOUT 3: FADE_FADING_OUT 4: FADE_START_FADEIN 5: FADE_FADING_IN There is also the macro FADEMODE_IS_FADING, which indicates when the intention is to only check if the game is fading right now, which wasn't clearly conveyed previously. I also took the opportunity to clean up the style of any lines I touched. This included rewriting if-else chains into case-switches, turning one-liner if-then statements into proper blocks, fixing up comments, and even commenting the `fademode == FADE_NONE` on the tower spike checks (which, it was previously undocumented why that check was there, but I think I know why it's there). As for type safety, we already get some by transforming the variable types into the enum. Assignment is prohibited without a cast. But, apparently, comparison is perfectly legal and won't even give so much as a warning. To work around this and make absolutely sure I made all existing comparisons now use the enum, I temporarily changed it to be an `enum class`, which is a C++11 feature that makes it so all comparisons are illegal. Unfortunately, it scopes them in a namespace with the same name as a class, so I had to temporarily define macros to make sure my existing code worked. I also had to temporarily up the standard in CMakeLists.txt to get it to compile. But after all that was done, I found the rest of the places where a comparison to an integer was used, and fixed them.
2022-04-25 09:57:47 +02:00
}
2020-01-01 21:29:24 +01:00
break;
case 3101:
Clean up all exit paths to the menu to use common code There are multiple different exit paths to the main menu. In 2.2, they all had a bunch of copy-pasted code. In 2.3 currently, most of them use game.quittomenu(), but there are some stragglers that still use hand-copied code. This is a bit of a problem, because all exit paths should consistently have FILESYSTEM_unmountassets(), as part of the 2.3 feature of per-level custom assets. Furthermore, most (but not all) of the paths call script.hardreset() too, and some of the stragglers don't. So there could be something persisting through to the title screen (like a really long flash/shake timer) that could only persist if exiting to the title screen through those paths. But, actually, it seems like there's a good reason for some of those to not call script.hardreset() - namely, dying or completing No Death Mode and completing a Time Trial presents some information onscreen that would get reset by script.hardreset(), so I'll fix that in a later commit. So what I've done for this commit is found every exit path that didn't already use game.quittomenu(), and made them use game.quittomenu(). As well, some of them had special handling that existed on top of them already having a corresponding entry in game.quittomenu() (but the path would take the special handling because it never did game.quittomenu()), so I removed that special handling as well (e.g. exiting from a custom level used returntomenu(Menu::levellist) when quittomenu() already had that same returntomenu()). The menu that exiting from the level editor returns to is now handled in game.quittomenu() as well, where the map.custommode branch now also checks for map.custommodeforreal. Unfortunately, it seems like entering the level editor doesn't properly initialize map.custommode, so entering the level editor now initializes map.custommode, too. I've also taken the music.play(6) out of game.quittomenu(), because not all exit paths immediately play Presenting VVVVVV, so all exit paths that DO immediately play Presenting VVVVVV now have music.play(6) special-cased for them, which is fine enough for me. Here is the list of all exit paths to the menu: - Exiting through the pause menu (without glitchrunner mode) - Exiting through the pause menu (with glitchrunner mode) - Completing a custom level - Completing a Time Trial - Dying in No Death Mode - Completing No Death Mode - Completing an Intermission replay - Exiting from the level editor - Completing the main game
2021-01-07 23:20:37 +01:00
quittomenu();
music.play(Music_PRESENTINGVVVVVV); //should be after quittomenu();
2022-12-07 00:20:48 +01:00
setstate(0);
2020-01-01 21:29:24 +01:00
break;
case 3500:
music.fadeout();
2022-12-07 00:20:48 +01:00
incstate();
2022-12-07 00:35:06 +01:00
setstatedelay(120);
2020-01-01 21:29:24 +01:00
break;
case 3501:
//Game complete!
unlockAchievement("vvvvvvgamecomplete");
unlocknum(UnlockTrophy_GAME_COMPLETE);
2020-01-01 21:29:24 +01:00
crewstats[0] = true;
2022-12-07 00:20:48 +01:00
incstate();
2022-12-07 00:35:06 +01:00
setstatedelay(75);
music.play(Music_PLENARY);
2020-01-01 21:29:24 +01:00
graphics.createtextboxflipme("", -1, 12, TEXT_COLOUR("cyan"));
graphics.addline(" ");
graphics.addline("");
graphics.addline("");
graphics.textboxprintflags(PR_FONT_8X8);
graphics.textboxcenterx();
graphics.setimage(TEXTIMAGE_GAMECOMPLETE);
2020-01-01 21:29:24 +01:00
break;
case 3502:
Simplify time formatting functions Here's my notes on all the existing functions and what kind of time formats they output: - Game::giventimestring(int hrs, int min, int sec) H:MM:SS MM:SS - Game::timestring() // uses game.hours/minutes/seconds H:MM:SS MM:SS - Game::partimestring() // uses game.timetrialpar (seconds) MM:SS - Game::resulttimestring() // uses game.timetrialresulttime (sec) + timetrialresultframes (1/30s) MM:SS.CC - Game::timetstring(int t) // t = seconds MM:SS - Game::timestringcenti(char* buffer, const size_t buffer_size) // uses game.hours/minutes/seconds/frames H:MM:SS.CC MM:SS.CC - UtilityClass::timestring(int t) // t = frames, 30 frames = 1 second S:CC M:SS:CC This is kind of a mess, and there's a lot of functions that do the same thing except using different variables. For localization, I also want translators to be able to localize all these time formats - many languages use the decimal comma instead of the decimal point (12:34,56) maybe some languages really prefer something like 1時02分11秒44瞬... Which I don't know to be correct, but it's good to be prepared for it and not restrict translators arbitrarily to only changing ":" and "." when we can start making the system better in the first place. I added a new function, UtilityClass::format_time. This is the place where all time formats come together, given the number of seconds and optionally frames. I have simplified the above-mentioned functions somewhat, but I haven't given them a complete refactor or renaming - I mainly made sure that they all use the same backend so I can make the formats consistent and properly localizable. (And before we start shoving more temporary char buffers everywhere just to get rid of the std::string's, maybe we need to think of a globally used working buffer of size SCREEN_WIDTH_CHARS+1, as a register of sorts, for when any line of text needs to be made or processed, then printed, and then goes unused. Maybe help.textrow, or something like that.) As for this commit, the available time formats are now more consistent and changed a little in some places. Leading zeroes for the first unit are now no longer included, time trial results and the Super Gravitron can now display hours when they went to 60 minutes before, and we now always use .CC instead of :CC. These are the formats: - H:MM:SS - H:MM:SS.CC - M:SS - M:SS.CC - S.CC (only used when always_minutes=false, for the Gravitrons) Here's what changes to the current functions: - Game::partimestring() is removed - it was used in two places, and could be replaced by game.timetstring(game.timetrialpar) - Game::giventimestring(h,m,s) and Game::timestring() are now wrappers for the other functions - The four remaining functions (Game::resulttimestring(), Game::timetstring(t), Game::timestringcenti(buffer, buffer_size) and UtilityClass::timestring(t)) are now wrappers for the "central function", UtilityClass::format_time. - UtilityClass::twodigits(int t) is now unused so it's also removed. - I also added int UtilityClass::hms_to_seconds(int h, int m, int s)
2021-12-25 17:13:46 +01:00
{
2022-12-07 00:20:48 +01:00
incstate();
setstatedelay(45+15);
2020-01-01 21:29:24 +01:00
graphics.createtextboxflipme(loc::gettext("All Crew Members Rescued!"), -1, 64, TEXT_COLOUR("transparent"));
graphics.textboxprintflags(PR_FONT_INTERFACE);
graphics.textboxcenterx();
Simplify time formatting functions Here's my notes on all the existing functions and what kind of time formats they output: - Game::giventimestring(int hrs, int min, int sec) H:MM:SS MM:SS - Game::timestring() // uses game.hours/minutes/seconds H:MM:SS MM:SS - Game::partimestring() // uses game.timetrialpar (seconds) MM:SS - Game::resulttimestring() // uses game.timetrialresulttime (sec) + timetrialresultframes (1/30s) MM:SS.CC - Game::timetstring(int t) // t = seconds MM:SS - Game::timestringcenti(char* buffer, const size_t buffer_size) // uses game.hours/minutes/seconds/frames H:MM:SS.CC MM:SS.CC - UtilityClass::timestring(int t) // t = frames, 30 frames = 1 second S:CC M:SS:CC This is kind of a mess, and there's a lot of functions that do the same thing except using different variables. For localization, I also want translators to be able to localize all these time formats - many languages use the decimal comma instead of the decimal point (12:34,56) maybe some languages really prefer something like 1時02分11秒44瞬... Which I don't know to be correct, but it's good to be prepared for it and not restrict translators arbitrarily to only changing ":" and "." when we can start making the system better in the first place. I added a new function, UtilityClass::format_time. This is the place where all time formats come together, given the number of seconds and optionally frames. I have simplified the above-mentioned functions somewhat, but I haven't given them a complete refactor or renaming - I mainly made sure that they all use the same backend so I can make the formats consistent and properly localizable. (And before we start shoving more temporary char buffers everywhere just to get rid of the std::string's, maybe we need to think of a globally used working buffer of size SCREEN_WIDTH_CHARS+1, as a register of sorts, for when any line of text needs to be made or processed, then printed, and then goes unused. Maybe help.textrow, or something like that.) As for this commit, the available time formats are now more consistent and changed a little in some places. Leading zeroes for the first unit are now no longer included, time trial results and the Super Gravitron can now display hours when they went to 60 minutes before, and we now always use .CC instead of :CC. These are the formats: - H:MM:SS - H:MM:SS.CC - M:SS - M:SS.CC - S.CC (only used when always_minutes=false, for the Gravitrons) Here's what changes to the current functions: - Game::partimestring() is removed - it was used in two places, and could be replaced by game.timetstring(game.timetrialpar) - Game::giventimestring(h,m,s) and Game::timestring() are now wrappers for the other functions - The four remaining functions (Game::resulttimestring(), Game::timetstring(t), Game::timestringcenti(buffer, buffer_size) and UtilityClass::timestring(t)) are now wrappers for the "central function", UtilityClass::format_time. - UtilityClass::twodigits(int t) is now unused so it's also removed. - I also added int UtilityClass::hms_to_seconds(int h, int m, int s)
2021-12-25 17:13:46 +01:00
char buffer[SCREEN_WIDTH_CHARS + 1];
timestringcenti(buffer, sizeof(buffer));
savetime = buffer;
2020-01-01 21:29:24 +01:00
break;
Simplify time formatting functions Here's my notes on all the existing functions and what kind of time formats they output: - Game::giventimestring(int hrs, int min, int sec) H:MM:SS MM:SS - Game::timestring() // uses game.hours/minutes/seconds H:MM:SS MM:SS - Game::partimestring() // uses game.timetrialpar (seconds) MM:SS - Game::resulttimestring() // uses game.timetrialresulttime (sec) + timetrialresultframes (1/30s) MM:SS.CC - Game::timetstring(int t) // t = seconds MM:SS - Game::timestringcenti(char* buffer, const size_t buffer_size) // uses game.hours/minutes/seconds/frames H:MM:SS.CC MM:SS.CC - UtilityClass::timestring(int t) // t = frames, 30 frames = 1 second S:CC M:SS:CC This is kind of a mess, and there's a lot of functions that do the same thing except using different variables. For localization, I also want translators to be able to localize all these time formats - many languages use the decimal comma instead of the decimal point (12:34,56) maybe some languages really prefer something like 1時02分11秒44瞬... Which I don't know to be correct, but it's good to be prepared for it and not restrict translators arbitrarily to only changing ":" and "." when we can start making the system better in the first place. I added a new function, UtilityClass::format_time. This is the place where all time formats come together, given the number of seconds and optionally frames. I have simplified the above-mentioned functions somewhat, but I haven't given them a complete refactor or renaming - I mainly made sure that they all use the same backend so I can make the formats consistent and properly localizable. (And before we start shoving more temporary char buffers everywhere just to get rid of the std::string's, maybe we need to think of a globally used working buffer of size SCREEN_WIDTH_CHARS+1, as a register of sorts, for when any line of text needs to be made or processed, then printed, and then goes unused. Maybe help.textrow, or something like that.) As for this commit, the available time formats are now more consistent and changed a little in some places. Leading zeroes for the first unit are now no longer included, time trial results and the Super Gravitron can now display hours when they went to 60 minutes before, and we now always use .CC instead of :CC. These are the formats: - H:MM:SS - H:MM:SS.CC - M:SS - M:SS.CC - S.CC (only used when always_minutes=false, for the Gravitrons) Here's what changes to the current functions: - Game::partimestring() is removed - it was used in two places, and could be replaced by game.timetstring(game.timetrialpar) - Game::giventimestring(h,m,s) and Game::timestring() are now wrappers for the other functions - The four remaining functions (Game::resulttimestring(), Game::timetstring(t), Game::timestringcenti(buffer, buffer_size) and UtilityClass::timestring(t)) are now wrappers for the "central function", UtilityClass::format_time. - UtilityClass::twodigits(int t) is now unused so it's also removed. - I also added int UtilityClass::hms_to_seconds(int h, int m, int s)
2021-12-25 17:13:46 +01:00
}
2020-01-01 21:29:24 +01:00
case 3503:
{
2022-12-07 00:20:48 +01:00
incstate();
2022-12-07 00:35:06 +01:00
setstatedelay(45);
2020-01-01 21:29:24 +01:00
const char* label = loc::gettext("Trinkets Found:");
char buffer[SCREEN_WIDTH_CHARS + 1];
vformat_buf(buffer, sizeof(buffer),
loc::gettext("{gamecomplete_n_trinkets|wordy}"),
"gamecomplete_n_trinkets:int",
trinkets()
);
graphics.createtextboxflipme(label, 170-font::len(PR_FONT_INTERFACE, label), 84, TEXT_COLOUR("transparent"));
graphics.textboxprintflags(PR_FONT_INTERFACE);
graphics.createtextboxflipme(buffer, 180, 84, TEXT_COLOUR("transparent"));
graphics.textboxprintflags(PR_FONT_INTERFACE);
2020-01-01 21:29:24 +01:00
break;
}
2020-01-01 21:29:24 +01:00
case 3504:
{
2022-12-07 00:20:48 +01:00
incstate();
setstatedelay(45+15);
2020-01-01 21:29:24 +01:00
const char* label = loc::gettext("Game Time:");
std::string tempstring = savetime;
graphics.createtextboxflipme(label, 170-font::len(PR_FONT_INTERFACE, label), 96, TEXT_COLOUR("transparent"));
graphics.textboxprintflags(PR_FONT_INTERFACE);
graphics.createtextboxflipme(tempstring, 180, 96, TEXT_COLOUR("transparent"));
graphics.textboxprintflags(PR_FONT_INTERFACE);
2020-01-01 21:29:24 +01:00
break;
}
2020-01-01 21:29:24 +01:00
case 3505:
{
2022-12-07 00:20:48 +01:00
incstate();
2022-12-07 00:35:06 +01:00
setstatedelay(45);
2020-01-01 21:29:24 +01:00
const char* label = loc::gettext("Total Flips:");
graphics.createtextboxflipme(label, 170-font::len(PR_FONT_INTERFACE, label), 123, TEXT_COLOUR("transparent"));
graphics.textboxprintflags(PR_FONT_INTERFACE);
graphics.createtextboxflipme(help.String(totalflips), 180, 123, TEXT_COLOUR("transparent"));
graphics.textboxprintflags(PR_FONT_INTERFACE);
2020-01-01 21:29:24 +01:00
break;
}
2020-01-01 21:29:24 +01:00
case 3506:
{
2022-12-07 00:20:48 +01:00
incstate();
setstatedelay(45+15);
2020-01-01 21:29:24 +01:00
const char* label = loc::gettext("Total Deaths:");
graphics.createtextboxflipme(label, 170-font::len(PR_FONT_INTERFACE, label), 135, TEXT_COLOUR("transparent"));
graphics.textboxprintflags(PR_FONT_INTERFACE);
graphics.createtextboxflipme(help.String(deathcounts), 180, 135, TEXT_COLOUR("transparent"));
graphics.textboxprintflags(PR_FONT_INTERFACE);
2020-01-01 21:29:24 +01:00
break;
}
2020-01-01 21:29:24 +01:00
case 3507:
{
2022-12-07 00:20:48 +01:00
incstate();
setstatedelay(45+15);
2020-01-01 21:29:24 +01:00
char buffer[SCREEN_WIDTH_CHARS + 1];
loc::gettext_plural_fill(
buffer, sizeof(buffer),
"Hardest Room (with {n_deaths} deaths)",
"Hardest Room (with {n_deaths} death)",
"n_deaths:int",
hardestroomdeaths
);
graphics.createtextboxflipme(buffer, -1, 158, TEXT_COLOUR("transparent"));
graphics.textboxprintflags(PR_FONT_INTERFACE);
graphics.textboxcenterx();
graphics.createtextboxflipme(
loc::gettext_roomname(map.custommode, hardestroom_x, hardestroom_y, hardestroom.c_str(), hardestroom_specialname),
-1, 170, TEXT_COLOUR("transparent")
);
graphics.textboxprintflags(PR_FONT_INTERFACE);
graphics.textboxcenterx();
2020-01-01 21:29:24 +01:00
break;
}
2020-01-01 21:29:24 +01:00
case 3508:
2022-12-07 00:20:48 +01:00
incstate();
2022-12-07 00:35:06 +01:00
setstatedelay(0);
2020-01-01 21:29:24 +01:00
actionprompt_textbox();
2020-01-01 21:29:24 +01:00
break;
case 3509:
if (jumppressed)
{
2022-12-07 00:20:48 +01:00
incstate();
2022-12-07 00:35:06 +01:00
setstatedelay(30);
graphics.textboxremove();
2020-01-01 21:29:24 +01:00
}
break;
case 3510:
//Save stats and stuff here
if (!obj.flags[73])
2020-01-01 21:29:24 +01:00
{
//flip mode complete
unlockAchievement("vvvvvvgamecompleteflip");
unlocknum(UnlockTrophy_FLIPMODE_COMPLETE);
2020-01-01 21:29:24 +01:00
}
#ifndef MAKEANDPLAY
if (!map.custommode)
2020-01-01 21:29:24 +01:00
{
if (bestgamedeaths == -1)
2020-01-01 21:29:24 +01:00
{
bestgamedeaths = deathcounts;
}
else
{
if (deathcounts < bestgamedeaths)
{
bestgamedeaths = deathcounts;
}
}
2020-01-01 21:29:24 +01:00
}
#endif
2020-01-01 21:29:24 +01:00
if (bestgamedeaths > -1) {
if (bestgamedeaths <= 500) {
unlockAchievement("vvvvvvcomplete500");
}
if (bestgamedeaths <= 250) {
unlockAchievement("vvvvvvcomplete250");
}
if (bestgamedeaths <= 100) {
unlockAchievement("vvvvvvcomplete100");
}
if (bestgamedeaths <= 50) {
unlockAchievement("vvvvvvcomplete50");
}
}
2020-01-01 21:29:24 +01:00
if (nodeathmode)
{
unlockAchievement("vvvvvvmaster"); //bloody hell
unlocknum(UnlockTrophy_NODEATHMODE_COMPLETE);
2022-12-08 02:10:12 +01:00
setstate(3520);
setstatedelay(0);
2020-01-01 21:29:24 +01:00
}
else
{
2022-12-07 00:35:06 +01:00
setstatedelay(120);
2022-12-07 00:20:48 +01:00
incstate();
2020-01-01 21:29:24 +01:00
}
savestatsandsettings();
2020-01-01 21:29:24 +01:00
break;
case 3511:
{
2020-01-01 21:29:24 +01:00
//Activating a teleporter (long version for level complete)
int i = obj.getplayer();
if (INBOUNDS_VEC(i, obj.entities))
{
obj.entities[i].colour = 102;
}
2020-01-01 21:29:24 +01:00
2022-12-07 00:20:48 +01:00
incstate();
2022-12-07 00:35:06 +01:00
setstatedelay(30);
2020-01-01 21:29:24 +01:00
flashlight = 5;
screenshake = 90;
music.playef(Sound_FLASH);
2020-01-01 21:29:24 +01:00
break;
}
2020-01-01 21:29:24 +01:00
case 3512:
//Activating a teleporter 2
2022-12-07 00:20:48 +01:00
incstate();
2022-12-07 00:35:06 +01:00
setstatedelay(15);
2020-01-01 21:29:24 +01:00
flashlight = 5;
music.playef(Sound_FLASH);
2020-01-01 21:29:24 +01:00
break;
case 3513:
//Activating a teleporter 2
2022-12-07 00:20:48 +01:00
incstate();
2022-12-07 00:35:06 +01:00
setstatedelay(15);
2020-01-01 21:29:24 +01:00
flashlight = 5;
music.playef(Sound_FLASH);
2020-01-01 21:29:24 +01:00
break;
case 3514:
//Activating a teleporter 2
2022-12-07 00:20:48 +01:00
incstate();
2022-12-07 00:35:06 +01:00
setstatedelay(15);
2020-01-01 21:29:24 +01:00
flashlight = 5;
music.playef(Sound_FLASH);
2020-01-01 21:29:24 +01:00
break;
case 3515:
{
2020-01-01 21:29:24 +01:00
//Activating a teleporter 2
2022-12-07 00:20:48 +01:00
incstate();
2022-12-07 00:35:06 +01:00
setstatedelay(0);
2020-01-01 21:29:24 +01:00
flashlight = 5;
screenshake = 0;
int i = obj.getplayer();
if (INBOUNDS_VEC(i, obj.entities))
{
obj.entities[i].colour = 0;
obj.entities[i].invis = true;
}
2020-01-01 21:29:24 +01:00
//we're done here!
music.playef(Sound_TELEPORT);
2022-12-07 00:35:06 +01:00
setstatedelay(60);
2020-01-01 21:29:24 +01:00
break;
}
2020-01-01 21:29:24 +01:00
case 3516:
Enumify all fade modes This removes the magic numbers previously used for controlling the fade mode, which are really not readable at all unless you already know what they mean. 0: FADE_NONE 1: FADE_FULLY_BLACK 2: FADE_START_FADEOUT 3: FADE_FADING_OUT 4: FADE_START_FADEIN 5: FADE_FADING_IN There is also the macro FADEMODE_IS_FADING, which indicates when the intention is to only check if the game is fading right now, which wasn't clearly conveyed previously. I also took the opportunity to clean up the style of any lines I touched. This included rewriting if-else chains into case-switches, turning one-liner if-then statements into proper blocks, fixing up comments, and even commenting the `fademode == FADE_NONE` on the tower spike checks (which, it was previously undocumented why that check was there, but I think I know why it's there). As for type safety, we already get some by transforming the variable types into the enum. Assignment is prohibited without a cast. But, apparently, comparison is perfectly legal and won't even give so much as a warning. To work around this and make absolutely sure I made all existing comparisons now use the enum, I temporarily changed it to be an `enum class`, which is a C++11 feature that makes it so all comparisons are illegal. Unfortunately, it scopes them in a namespace with the same name as a class, so I had to temporarily define macros to make sure my existing code worked. I also had to temporarily up the standard in CMakeLists.txt to get it to compile. But after all that was done, I found the rest of the places where a comparison to an integer was used, and fixed them.
2022-04-25 09:57:47 +02:00
graphics.fademode = FADE_START_FADEOUT;
2022-12-07 00:20:48 +01:00
incstate();
2020-01-01 21:29:24 +01:00
break;
case 3517:
Enumify all fade modes This removes the magic numbers previously used for controlling the fade mode, which are really not readable at all unless you already know what they mean. 0: FADE_NONE 1: FADE_FULLY_BLACK 2: FADE_START_FADEOUT 3: FADE_FADING_OUT 4: FADE_START_FADEIN 5: FADE_FADING_IN There is also the macro FADEMODE_IS_FADING, which indicates when the intention is to only check if the game is fading right now, which wasn't clearly conveyed previously. I also took the opportunity to clean up the style of any lines I touched. This included rewriting if-else chains into case-switches, turning one-liner if-then statements into proper blocks, fixing up comments, and even commenting the `fademode == FADE_NONE` on the tower spike checks (which, it was previously undocumented why that check was there, but I think I know why it's there). As for type safety, we already get some by transforming the variable types into the enum. Assignment is prohibited without a cast. But, apparently, comparison is perfectly legal and won't even give so much as a warning. To work around this and make absolutely sure I made all existing comparisons now use the enum, I temporarily changed it to be an `enum class`, which is a C++11 feature that makes it so all comparisons are illegal. Unfortunately, it scopes them in a namespace with the same name as a class, so I had to temporarily define macros to make sure my existing code worked. I also had to temporarily up the standard in CMakeLists.txt to get it to compile. But after all that was done, I found the rest of the places where a comparison to an integer was used, and fixed them.
2022-04-25 09:57:47 +02:00
if (graphics.fademode == FADE_FULLY_BLACK)
2020-01-01 21:29:24 +01:00
{
2022-12-07 00:20:48 +01:00
incstate();
2022-12-07 00:35:06 +01:00
setstatedelay(30);
2020-01-01 21:29:24 +01:00
}
break;
case 3518:
Enumify all fade modes This removes the magic numbers previously used for controlling the fade mode, which are really not readable at all unless you already know what they mean. 0: FADE_NONE 1: FADE_FULLY_BLACK 2: FADE_START_FADEOUT 3: FADE_FADING_OUT 4: FADE_START_FADEIN 5: FADE_FADING_IN There is also the macro FADEMODE_IS_FADING, which indicates when the intention is to only check if the game is fading right now, which wasn't clearly conveyed previously. I also took the opportunity to clean up the style of any lines I touched. This included rewriting if-else chains into case-switches, turning one-liner if-then statements into proper blocks, fixing up comments, and even commenting the `fademode == FADE_NONE` on the tower spike checks (which, it was previously undocumented why that check was there, but I think I know why it's there). As for type safety, we already get some by transforming the variable types into the enum. Assignment is prohibited without a cast. But, apparently, comparison is perfectly legal and won't even give so much as a warning. To work around this and make absolutely sure I made all existing comparisons now use the enum, I temporarily changed it to be an `enum class`, which is a C++11 feature that makes it so all comparisons are illegal. Unfortunately, it scopes them in a namespace with the same name as a class, so I had to temporarily define macros to make sure my existing code worked. I also had to temporarily up the standard in CMakeLists.txt to get it to compile. But after all that was done, I found the rest of the places where a comparison to an integer was used, and fixed them.
2022-04-25 09:57:47 +02:00
graphics.fademode = FADE_START_FADEIN;
2022-12-07 00:20:48 +01:00
setstate(0);
2022-12-07 00:35:06 +01:00
setstatedelay(30);
2020-01-01 21:29:24 +01:00
map.finalmode = false;
map.final_colormode = false;
map.final_mapcol = 0;
map.final_colorframe = 0;
map.finalstretch = false;
obj.flags[72] = false;
2020-01-01 21:29:24 +01:00
graphics.setbars(320);
2020-01-01 21:29:24 +01:00
teleport_to_new_area = true;
teleportscript = "gamecomplete";
break;
case 3520:
//NO DEATH MODE COMPLETE JESUS
hascontrol = false;
crewstats[0] = true;
Enumify all fade modes This removes the magic numbers previously used for controlling the fade mode, which are really not readable at all unless you already know what they mean. 0: FADE_NONE 1: FADE_FULLY_BLACK 2: FADE_START_FADEOUT 3: FADE_FADING_OUT 4: FADE_START_FADEIN 5: FADE_FADING_IN There is also the macro FADEMODE_IS_FADING, which indicates when the intention is to only check if the game is fading right now, which wasn't clearly conveyed previously. I also took the opportunity to clean up the style of any lines I touched. This included rewriting if-else chains into case-switches, turning one-liner if-then statements into proper blocks, fixing up comments, and even commenting the `fademode == FADE_NONE` on the tower spike checks (which, it was previously undocumented why that check was there, but I think I know why it's there). As for type safety, we already get some by transforming the variable types into the enum. Assignment is prohibited without a cast. But, apparently, comparison is perfectly legal and won't even give so much as a warning. To work around this and make absolutely sure I made all existing comparisons now use the enum, I temporarily changed it to be an `enum class`, which is a C++11 feature that makes it so all comparisons are illegal. Unfortunately, it scopes them in a namespace with the same name as a class, so I had to temporarily define macros to make sure my existing code worked. I also had to temporarily up the standard in CMakeLists.txt to get it to compile. But after all that was done, I found the rest of the places where a comparison to an integer was used, and fixed them.
2022-04-25 09:57:47 +02:00
graphics.fademode = FADE_START_FADEOUT;
2022-12-07 00:20:48 +01:00
incstate();
2020-01-01 21:29:24 +01:00
break;
case 3521:
Enumify all fade modes This removes the magic numbers previously used for controlling the fade mode, which are really not readable at all unless you already know what they mean. 0: FADE_NONE 1: FADE_FULLY_BLACK 2: FADE_START_FADEOUT 3: FADE_FADING_OUT 4: FADE_START_FADEIN 5: FADE_FADING_IN There is also the macro FADEMODE_IS_FADING, which indicates when the intention is to only check if the game is fading right now, which wasn't clearly conveyed previously. I also took the opportunity to clean up the style of any lines I touched. This included rewriting if-else chains into case-switches, turning one-liner if-then statements into proper blocks, fixing up comments, and even commenting the `fademode == FADE_NONE` on the tower spike checks (which, it was previously undocumented why that check was there, but I think I know why it's there). As for type safety, we already get some by transforming the variable types into the enum. Assignment is prohibited without a cast. But, apparently, comparison is perfectly legal and won't even give so much as a warning. To work around this and make absolutely sure I made all existing comparisons now use the enum, I temporarily changed it to be an `enum class`, which is a C++11 feature that makes it so all comparisons are illegal. Unfortunately, it scopes them in a namespace with the same name as a class, so I had to temporarily define macros to make sure my existing code worked. I also had to temporarily up the standard in CMakeLists.txt to get it to compile. But after all that was done, I found the rest of the places where a comparison to an integer was used, and fixed them.
2022-04-25 09:57:47 +02:00
if (graphics.fademode == FADE_FULLY_BLACK)
{
2022-12-07 00:20:48 +01:00
incstate();
Enumify all fade modes This removes the magic numbers previously used for controlling the fade mode, which are really not readable at all unless you already know what they mean. 0: FADE_NONE 1: FADE_FULLY_BLACK 2: FADE_START_FADEOUT 3: FADE_FADING_OUT 4: FADE_START_FADEIN 5: FADE_FADING_IN There is also the macro FADEMODE_IS_FADING, which indicates when the intention is to only check if the game is fading right now, which wasn't clearly conveyed previously. I also took the opportunity to clean up the style of any lines I touched. This included rewriting if-else chains into case-switches, turning one-liner if-then statements into proper blocks, fixing up comments, and even commenting the `fademode == FADE_NONE` on the tower spike checks (which, it was previously undocumented why that check was there, but I think I know why it's there). As for type safety, we already get some by transforming the variable types into the enum. Assignment is prohibited without a cast. But, apparently, comparison is perfectly legal and won't even give so much as a warning. To work around this and make absolutely sure I made all existing comparisons now use the enum, I temporarily changed it to be an `enum class`, which is a C++11 feature that makes it so all comparisons are illegal. Unfortunately, it scopes them in a namespace with the same name as a class, so I had to temporarily define macros to make sure my existing code worked. I also had to temporarily up the standard in CMakeLists.txt to get it to compile. But after all that was done, I found the rest of the places where a comparison to an integer was used, and fixed them.
2022-04-25 09:57:47 +02:00
}
2020-01-01 21:29:24 +01:00
break;
case 3522:
copyndmresults();
Clean up all exit paths to the menu to use common code There are multiple different exit paths to the main menu. In 2.2, they all had a bunch of copy-pasted code. In 2.3 currently, most of them use game.quittomenu(), but there are some stragglers that still use hand-copied code. This is a bit of a problem, because all exit paths should consistently have FILESYSTEM_unmountassets(), as part of the 2.3 feature of per-level custom assets. Furthermore, most (but not all) of the paths call script.hardreset() too, and some of the stragglers don't. So there could be something persisting through to the title screen (like a really long flash/shake timer) that could only persist if exiting to the title screen through those paths. But, actually, it seems like there's a good reason for some of those to not call script.hardreset() - namely, dying or completing No Death Mode and completing a Time Trial presents some information onscreen that would get reset by script.hardreset(), so I'll fix that in a later commit. So what I've done for this commit is found every exit path that didn't already use game.quittomenu(), and made them use game.quittomenu(). As well, some of them had special handling that existed on top of them already having a corresponding entry in game.quittomenu() (but the path would take the special handling because it never did game.quittomenu()), so I removed that special handling as well (e.g. exiting from a custom level used returntomenu(Menu::levellist) when quittomenu() already had that same returntomenu()). The menu that exiting from the level editor returns to is now handled in game.quittomenu() as well, where the map.custommode branch now also checks for map.custommodeforreal. Unfortunately, it seems like entering the level editor doesn't properly initialize map.custommode, so entering the level editor now initializes map.custommode, too. I've also taken the music.play(6) out of game.quittomenu(), because not all exit paths immediately play Presenting VVVVVV, so all exit paths that DO immediately play Presenting VVVVVV now have music.play(6) special-cased for them, which is fine enough for me. Here is the list of all exit paths to the menu: - Exiting through the pause menu (without glitchrunner mode) - Exiting through the pause menu (with glitchrunner mode) - Completing a custom level - Completing a Time Trial - Dying in No Death Mode - Completing No Death Mode - Completing an Intermission replay - Exiting from the level editor - Completing the main game
2021-01-07 23:20:37 +01:00
quittomenu();
createmenu(Menu::nodeathmodecomplete);
2022-12-07 00:20:48 +01:00
setstate(0);
2020-01-01 21:29:24 +01:00
break;
case 4000:
//Activating a teleporter (short version)
2022-12-07 00:20:48 +01:00
state++; // Increment manually -- gamestate modification might be locked at this point
2020-01-01 21:29:24 +01:00
statedelay = 10;
flashlight = 5;
screenshake = 10;
music.playef(Sound_FLASH);
2020-01-01 21:29:24 +01:00
break;
case 4001:
//Activating a teleporter 2
state++;
statedelay = 0;
flashlight = 5;
screenshake = 0;
//we're done here!
music.playef(Sound_TELEPORT);
2020-01-01 21:29:24 +01:00
break;
case 4002:
{
2020-01-01 21:29:24 +01:00
//Activating a teleporter 2
state++;
statedelay = 10;
int i = obj.getplayer();
if (INBOUNDS_VEC(i, obj.entities))
{
obj.entities[i].colour = 0;
obj.entities[i].invis = true;
}
2020-01-01 21:29:24 +01:00
i = obj.getteleporter();
if(INBOUNDS_VEC(i, obj.entities))
2020-01-01 21:29:24 +01:00
{
obj.entities[i].tile = 1;
obj.entities[i].colour = 100;
}
break;
}
2020-01-01 21:29:24 +01:00
case 4003:
2022-12-08 02:10:12 +01:00
state = 0;
2020-01-01 21:29:24 +01:00
statedelay = 0;
teleport_to_new_area = true;
2022-12-07 00:20:48 +01:00
unlockstate();
2020-01-01 21:29:24 +01:00
break;
case 4010:
//Activating a teleporter (default appear)
2022-12-07 00:20:48 +01:00
incstate();
2022-12-07 00:35:06 +01:00
setstatedelay(15);
2020-01-01 21:29:24 +01:00
flashlight = 5;
screenshake = 90;
music.playef(Sound_FLASH);
2020-01-01 21:29:24 +01:00
break;
case 4011:
//Activating a teleporter 2
2022-12-07 00:20:48 +01:00
incstate();
2022-12-07 00:35:06 +01:00
setstatedelay(0);
2020-01-01 21:29:24 +01:00
flashlight = 5;
screenshake = 0;
music.playef(Sound_TELEPORT);
2020-01-01 21:29:24 +01:00
break;
case 4012:
{
2020-01-01 21:29:24 +01:00
//Activating a teleporter 2
2022-12-07 00:20:48 +01:00
incstate();
2022-12-07 00:35:06 +01:00
setstatedelay(5);
2020-01-01 21:29:24 +01:00
int i = obj.getplayer();
int j = obj.getteleporter();
if (INBOUNDS_VEC(i, obj.entities))
2020-01-01 21:29:24 +01:00
{
if (INBOUNDS_VEC(j, obj.entities))
{
obj.entities[i].xp = obj.entities[j].xp+44;
obj.entities[i].yp = obj.entities[j].yp+44;
Restore previous oldxp/oldyp variables in favor of lerpoldxp/lerpoldyp I was investigating a desync in my Nova TAS, and it turns out that the gravity line collision functions check for the `oldxp` and `oldyp` of the player, i.e. their position on the previous frame, along with their position on the current frame. So, if the player either collided with the gravity line last frame or this frame, then the player collided with the gravity line this frame. Except, that's not actually true. It turns out that `oldxp` and `oldyp` don't necessarily always correspond to the `xp` and `yp` of the player on the previous frame. It turns out that your `oldyp` will be updated if you stand on a vertically moving platform, before the gravity line collision function gets ran. So, if you were colliding with a gravity line on the previous frame, but you got moved out of there by a vertically moving platform, then you just don't collide with the gravity line at all. However, this behavior changed in 2.3 after my over-30-FPS patch got merged (#220). That patch took advantage of the existing `oldxp` and `oldyp` entity attributes, and uses them to interpolate their positions during rendering to make everything look real smooth. Previously, `oldxp` and `oldyp` would both be updated in `entityclass::updateentitylogic()`. However, I moved it in that patch to update right before `gameinput()` in `main.cpp`. As a result, `oldyp` no longer gets updated whenever the player stands on a vertically moving platform. This ends up desyncing my TAS. As expected, updating `oldyp` in `entityclass::movingplatformfix()` (the function responsible for moving the player whenever they stand on a vertically moving platform) makes it so that my TAS syncs, but the visuals are glitchy when standing on a vertically moving platform. And as much as I'd like to get rid of gravity lines checking for whether you've collided with them on the previous frame, doing that desyncs my TAS, too. In the end, it seems like I should just leave `oldxp` and `oldyp` alone, and switch to using dedicated variables that are never used in the physics of the game. So I'm introducing `lerpoldxp` and `lerpoldyp`, and replacing all instances of using `oldxp` and `oldyp` that my over-30-FPS patch added, with `lerpoldxp` and `lerpoldyp` instead. After doing this, and applying #503 as well, my Nova TAS syncs after some minor but acceptable fixes with Viridian's walkingframe.
2020-10-10 05:58:58 +02:00
obj.entities[i].lerpoldxp = obj.entities[i].xp;
obj.entities[i].lerpoldyp = obj.entities[i].yp;
obj.entities[j].tile = 2;
obj.entities[j].colour = 101;
}
obj.entities[i].colour = 0;
obj.entities[i].invis = false;
obj.entities[i].dir = 1;
2020-01-01 21:29:24 +01:00
obj.entities[i].ay = -6;
obj.entities[i].ax = 6;
obj.entities[i].vy = -6;
obj.entities[i].vx = 6;
}
2020-01-01 21:29:24 +01:00
break;
}
2020-01-01 21:29:24 +01:00
case 4013:
{
2022-12-07 00:20:48 +01:00
incstate();
int i = obj.getplayer();
if (INBOUNDS_VEC(i, obj.entities))
{
obj.entities[i].xp += 10;
}
2020-01-01 21:29:24 +01:00
break;
}
2020-01-01 21:29:24 +01:00
case 4014:
{
2022-12-07 00:20:48 +01:00
incstate();
int i = obj.getplayer();
if (INBOUNDS_VEC(i, obj.entities))
{
obj.entities[i].xp += 10;
}
2020-01-01 21:29:24 +01:00
break;
}
2020-01-01 21:29:24 +01:00
case 4015:
{
2022-12-07 00:20:48 +01:00
incstate();
int i = obj.getplayer();
if (INBOUNDS_VEC(i, obj.entities))
{
obj.entities[i].xp += 8;
}
2020-01-01 21:29:24 +01:00
break;
}
2020-01-01 21:29:24 +01:00
case 4016:
{
2022-12-07 00:20:48 +01:00
incstate();
int i = obj.getplayer();
if (INBOUNDS_VEC(i, obj.entities))
{
obj.entities[i].xp += 6;
}
2020-01-01 21:29:24 +01:00
break;
}
2020-01-01 21:29:24 +01:00
case 4017:
{
2022-12-07 00:20:48 +01:00
incstate();
int i = obj.getplayer();
if (INBOUNDS_VEC(i, obj.entities))
{
obj.entities[i].xp += 3;
}
2020-01-01 21:29:24 +01:00
break;
}
2020-01-01 21:29:24 +01:00
case 4018:
{
2022-12-07 00:20:48 +01:00
incstate();
2022-12-07 00:35:06 +01:00
setstatedelay(15);
int i = obj.getplayer();
if (INBOUNDS_VEC(i, obj.entities))
{
obj.entities[i].xp += 1;
}
2020-01-01 21:29:24 +01:00
break;
}
2020-01-01 21:29:24 +01:00
case 4019:
{
2020-01-01 21:29:24 +01:00
if (intimetrial || nodeathmode || inintermission)
{
}
else
{
savetele();
2020-01-01 21:29:24 +01:00
}
int i = obj.getteleporter();
2020-01-01 21:29:24 +01:00
activetele = true;
if (INBOUNDS_VEC(i, obj.entities))
{
teleblock.x = obj.entities[i].xp - 32;
teleblock.y = obj.entities[i].yp - 32;
}
2020-01-01 21:29:24 +01:00
teleblock.w = 160;
teleblock.h = 160;
hascontrol = true;
advancetext = false;
2022-12-07 00:20:48 +01:00
setstate(0);
2020-01-01 21:29:24 +01:00
break;
}
2020-01-01 21:29:24 +01:00
case 4020:
//Activating a teleporter (default appear)
2022-12-07 00:20:48 +01:00
incstate();
2022-12-07 00:35:06 +01:00
setstatedelay(15);
2020-01-01 21:29:24 +01:00
flashlight = 5;
screenshake = 90;
music.playef(Sound_FLASH);
2020-01-01 21:29:24 +01:00
break;
case 4021:
//Activating a teleporter 2
2022-12-07 00:20:48 +01:00
incstate();
2022-12-07 00:35:06 +01:00
setstatedelay(0);
2020-01-01 21:29:24 +01:00
flashlight = 5;
screenshake = 0;
music.playef(Sound_TELEPORT);
2020-01-01 21:29:24 +01:00
break;
case 4022:
{
2020-01-01 21:29:24 +01:00
//Activating a teleporter 2
2022-12-07 00:20:48 +01:00
incstate();
2022-12-07 00:35:06 +01:00
setstatedelay(5);
2020-01-01 21:29:24 +01:00
int i = obj.getplayer();
int j = obj.getteleporter();
if (INBOUNDS_VEC(i, obj.entities))
2020-01-01 21:29:24 +01:00
{
if (INBOUNDS_VEC(j, obj.entities))
{
obj.entities[i].xp = obj.entities[j].xp+44;
obj.entities[i].yp = obj.entities[j].yp+44;
Restore previous oldxp/oldyp variables in favor of lerpoldxp/lerpoldyp I was investigating a desync in my Nova TAS, and it turns out that the gravity line collision functions check for the `oldxp` and `oldyp` of the player, i.e. their position on the previous frame, along with their position on the current frame. So, if the player either collided with the gravity line last frame or this frame, then the player collided with the gravity line this frame. Except, that's not actually true. It turns out that `oldxp` and `oldyp` don't necessarily always correspond to the `xp` and `yp` of the player on the previous frame. It turns out that your `oldyp` will be updated if you stand on a vertically moving platform, before the gravity line collision function gets ran. So, if you were colliding with a gravity line on the previous frame, but you got moved out of there by a vertically moving platform, then you just don't collide with the gravity line at all. However, this behavior changed in 2.3 after my over-30-FPS patch got merged (#220). That patch took advantage of the existing `oldxp` and `oldyp` entity attributes, and uses them to interpolate their positions during rendering to make everything look real smooth. Previously, `oldxp` and `oldyp` would both be updated in `entityclass::updateentitylogic()`. However, I moved it in that patch to update right before `gameinput()` in `main.cpp`. As a result, `oldyp` no longer gets updated whenever the player stands on a vertically moving platform. This ends up desyncing my TAS. As expected, updating `oldyp` in `entityclass::movingplatformfix()` (the function responsible for moving the player whenever they stand on a vertically moving platform) makes it so that my TAS syncs, but the visuals are glitchy when standing on a vertically moving platform. And as much as I'd like to get rid of gravity lines checking for whether you've collided with them on the previous frame, doing that desyncs my TAS, too. In the end, it seems like I should just leave `oldxp` and `oldyp` alone, and switch to using dedicated variables that are never used in the physics of the game. So I'm introducing `lerpoldxp` and `lerpoldyp`, and replacing all instances of using `oldxp` and `oldyp` that my over-30-FPS patch added, with `lerpoldxp` and `lerpoldyp` instead. After doing this, and applying #503 as well, my Nova TAS syncs after some minor but acceptable fixes with Viridian's walkingframe.
2020-10-10 05:58:58 +02:00
obj.entities[i].lerpoldxp = obj.entities[i].xp;
obj.entities[i].lerpoldyp = obj.entities[i].yp;
obj.entities[j].tile = 2;
obj.entities[j].colour = 101;
}
obj.entities[i].colour = 0;
obj.entities[i].invis = false;
obj.entities[i].dir = 1;
2020-01-01 21:29:24 +01:00
obj.entities[i].ay = -6;
obj.entities[i].ax = 6;
obj.entities[i].vy = -6;
obj.entities[i].vx = 6;
}
2020-01-01 21:29:24 +01:00
break;
}
2020-01-01 21:29:24 +01:00
case 4023:
{
2022-12-07 00:20:48 +01:00
incstate();
int i = obj.getplayer();
if (INBOUNDS_VEC(i, obj.entities))
{
obj.entities[i].xp += 12;
}
2020-01-01 21:29:24 +01:00
break;
}
2020-01-01 21:29:24 +01:00
case 4024:
{
2022-12-07 00:20:48 +01:00
incstate();
int i = obj.getplayer();
if (INBOUNDS_VEC(i, obj.entities))
{
obj.entities[i].xp += 12;
}
2020-01-01 21:29:24 +01:00
break;
}
2020-01-01 21:29:24 +01:00
case 4025:
{
2022-12-07 00:20:48 +01:00
incstate();
int i = obj.getplayer();
if (INBOUNDS_VEC(i, obj.entities))
{
obj.entities[i].xp += 10;
}
2020-01-01 21:29:24 +01:00
break;
}
2020-01-01 21:29:24 +01:00
case 4026:
{
2022-12-07 00:20:48 +01:00
incstate();
int i = obj.getplayer();
if (INBOUNDS_VEC(i, obj.entities))
{
obj.entities[i].xp += 8;
}
2020-01-01 21:29:24 +01:00
break;
}
2020-01-01 21:29:24 +01:00
case 4027:
{
2022-12-07 00:20:48 +01:00
incstate();
int i = obj.getplayer();
if (INBOUNDS_VEC(i, obj.entities))
{
obj.entities[i].xp += 5;
}
2020-01-01 21:29:24 +01:00
break;
}
2020-01-01 21:29:24 +01:00
case 4028:
{
2022-12-07 00:20:48 +01:00
incstate();
2022-12-07 00:35:06 +01:00
setstatedelay(15);
int i = obj.getplayer();
if (INBOUNDS_VEC(i, obj.entities))
{
obj.entities[i].xp += 2;
}
2020-01-01 21:29:24 +01:00
break;
}
2020-01-01 21:29:24 +01:00
case 4029:
hascontrol = true;
advancetext = false;
2022-12-07 00:20:48 +01:00
setstate(0);
2020-01-01 21:29:24 +01:00
break;
case 4030:
//Activating a teleporter (default appear)
2022-12-07 00:20:48 +01:00
incstate();
2022-12-07 00:35:06 +01:00
setstatedelay(15);
2020-01-01 21:29:24 +01:00
flashlight = 5;
screenshake = 90;
music.playef(Sound_FLASH);
2020-01-01 21:29:24 +01:00
break;
case 4031:
//Activating a teleporter 2
2022-12-07 00:20:48 +01:00
incstate();
2022-12-07 00:35:06 +01:00
setstatedelay(0);
2020-01-01 21:29:24 +01:00
flashlight = 5;
screenshake = 0;
music.playef(Sound_TELEPORT);
2020-01-01 21:29:24 +01:00
break;
case 4032:
{
2020-01-01 21:29:24 +01:00
//Activating a teleporter 2
2022-12-07 00:20:48 +01:00
incstate();
2022-12-07 00:35:06 +01:00
setstatedelay(5);
2020-01-01 21:29:24 +01:00
int i = obj.getplayer();
int j = obj.getteleporter();
if (INBOUNDS_VEC(i, obj.entities))
2020-01-01 21:29:24 +01:00
{
if (INBOUNDS_VEC(j, obj.entities))
{
obj.entities[i].xp = obj.entities[j].xp+44;
obj.entities[i].yp = obj.entities[j].yp+44;
Restore previous oldxp/oldyp variables in favor of lerpoldxp/lerpoldyp I was investigating a desync in my Nova TAS, and it turns out that the gravity line collision functions check for the `oldxp` and `oldyp` of the player, i.e. their position on the previous frame, along with their position on the current frame. So, if the player either collided with the gravity line last frame or this frame, then the player collided with the gravity line this frame. Except, that's not actually true. It turns out that `oldxp` and `oldyp` don't necessarily always correspond to the `xp` and `yp` of the player on the previous frame. It turns out that your `oldyp` will be updated if you stand on a vertically moving platform, before the gravity line collision function gets ran. So, if you were colliding with a gravity line on the previous frame, but you got moved out of there by a vertically moving platform, then you just don't collide with the gravity line at all. However, this behavior changed in 2.3 after my over-30-FPS patch got merged (#220). That patch took advantage of the existing `oldxp` and `oldyp` entity attributes, and uses them to interpolate their positions during rendering to make everything look real smooth. Previously, `oldxp` and `oldyp` would both be updated in `entityclass::updateentitylogic()`. However, I moved it in that patch to update right before `gameinput()` in `main.cpp`. As a result, `oldyp` no longer gets updated whenever the player stands on a vertically moving platform. This ends up desyncing my TAS. As expected, updating `oldyp` in `entityclass::movingplatformfix()` (the function responsible for moving the player whenever they stand on a vertically moving platform) makes it so that my TAS syncs, but the visuals are glitchy when standing on a vertically moving platform. And as much as I'd like to get rid of gravity lines checking for whether you've collided with them on the previous frame, doing that desyncs my TAS, too. In the end, it seems like I should just leave `oldxp` and `oldyp` alone, and switch to using dedicated variables that are never used in the physics of the game. So I'm introducing `lerpoldxp` and `lerpoldyp`, and replacing all instances of using `oldxp` and `oldyp` that my over-30-FPS patch added, with `lerpoldxp` and `lerpoldyp` instead. After doing this, and applying #503 as well, my Nova TAS syncs after some minor but acceptable fixes with Viridian's walkingframe.
2020-10-10 05:58:58 +02:00
obj.entities[i].lerpoldxp = obj.entities[i].xp;
obj.entities[i].lerpoldyp = obj.entities[i].yp;
obj.entities[j].tile = 2;
obj.entities[j].colour = 101;
}
obj.entities[i].colour = 0;
obj.entities[i].invis = false;
obj.entities[i].dir = 0;
2020-01-01 21:29:24 +01:00
obj.entities[i].ay = -6;
obj.entities[i].ax = -6;
obj.entities[i].vy = -6;
obj.entities[i].vx = -6;
}
2020-01-01 21:29:24 +01:00
break;
}
2020-01-01 21:29:24 +01:00
case 4033:
{
2022-12-07 00:20:48 +01:00
incstate();
int i = obj.getplayer();
if (INBOUNDS_VEC(i, obj.entities))
{
obj.entities[i].xp -= 12;
}
2020-01-01 21:29:24 +01:00
break;
}
2020-01-01 21:29:24 +01:00
case 4034:
{
2022-12-07 00:20:48 +01:00
incstate();
int i = obj.getplayer();
if (INBOUNDS_VEC(i, obj.entities))
{
obj.entities[i].xp -= 12;
}
2020-01-01 21:29:24 +01:00
break;
}
2020-01-01 21:29:24 +01:00
case 4035:
{
2022-12-07 00:20:48 +01:00
incstate();
int i = obj.getplayer();
if (INBOUNDS_VEC(i, obj.entities))
{
obj.entities[i].xp -= 10;
}
2020-01-01 21:29:24 +01:00
break;
}
2020-01-01 21:29:24 +01:00
case 4036:
{
2022-12-07 00:20:48 +01:00
incstate();
int i = obj.getplayer();
if (INBOUNDS_VEC(i, obj.entities))
{
obj.entities[i].xp -= 8;
}
2020-01-01 21:29:24 +01:00
break;
}
2020-01-01 21:29:24 +01:00
case 4037:
{
2022-12-07 00:20:48 +01:00
incstate();
int i = obj.getplayer();
if (INBOUNDS_VEC(i, obj.entities))
{
obj.entities[i].xp -= 5;
}
2020-01-01 21:29:24 +01:00
break;
}
2020-01-01 21:29:24 +01:00
case 4038:
{
2022-12-07 00:20:48 +01:00
incstate();
2022-12-07 00:35:06 +01:00
setstatedelay(15);
int i = obj.getplayer();
if (INBOUNDS_VEC(i, obj.entities))
{
obj.entities[i].xp -= 2;
}
2020-01-01 21:29:24 +01:00
break;
}
2020-01-01 21:29:24 +01:00
case 4039:
hascontrol = true;
advancetext = false;
2022-12-07 00:20:48 +01:00
setstate(0);
2020-01-01 21:29:24 +01:00
break;
case 4040:
//Activating a teleporter (default appear)
2022-12-07 00:20:48 +01:00
incstate();
2022-12-07 00:35:06 +01:00
setstatedelay(15);
2020-01-01 21:29:24 +01:00
flashlight = 5;
screenshake = 90;
music.playef(Sound_FLASH);
2020-01-01 21:29:24 +01:00
break;
case 4041:
//Activating a teleporter 2
2022-12-07 00:20:48 +01:00
incstate();
2022-12-07 00:35:06 +01:00
setstatedelay(0);
2020-01-01 21:29:24 +01:00
flashlight = 5;
screenshake = 0;
music.playef(Sound_TELEPORT);
2020-01-01 21:29:24 +01:00
break;
case 4042:
{
2020-01-01 21:29:24 +01:00
//Activating a teleporter 2
2022-12-07 00:20:48 +01:00
incstate();
2022-12-07 00:35:06 +01:00
setstatedelay(5);
2020-01-01 21:29:24 +01:00
int i = obj.getplayer();
int j = obj.getteleporter();
if (INBOUNDS_VEC(i, obj.entities))
2020-01-01 21:29:24 +01:00
{
if (INBOUNDS_VEC(j, obj.entities))
{
obj.entities[i].xp = obj.entities[j].xp+44;
obj.entities[i].yp = obj.entities[j].yp+44;
Restore previous oldxp/oldyp variables in favor of lerpoldxp/lerpoldyp I was investigating a desync in my Nova TAS, and it turns out that the gravity line collision functions check for the `oldxp` and `oldyp` of the player, i.e. their position on the previous frame, along with their position on the current frame. So, if the player either collided with the gravity line last frame or this frame, then the player collided with the gravity line this frame. Except, that's not actually true. It turns out that `oldxp` and `oldyp` don't necessarily always correspond to the `xp` and `yp` of the player on the previous frame. It turns out that your `oldyp` will be updated if you stand on a vertically moving platform, before the gravity line collision function gets ran. So, if you were colliding with a gravity line on the previous frame, but you got moved out of there by a vertically moving platform, then you just don't collide with the gravity line at all. However, this behavior changed in 2.3 after my over-30-FPS patch got merged (#220). That patch took advantage of the existing `oldxp` and `oldyp` entity attributes, and uses them to interpolate their positions during rendering to make everything look real smooth. Previously, `oldxp` and `oldyp` would both be updated in `entityclass::updateentitylogic()`. However, I moved it in that patch to update right before `gameinput()` in `main.cpp`. As a result, `oldyp` no longer gets updated whenever the player stands on a vertically moving platform. This ends up desyncing my TAS. As expected, updating `oldyp` in `entityclass::movingplatformfix()` (the function responsible for moving the player whenever they stand on a vertically moving platform) makes it so that my TAS syncs, but the visuals are glitchy when standing on a vertically moving platform. And as much as I'd like to get rid of gravity lines checking for whether you've collided with them on the previous frame, doing that desyncs my TAS, too. In the end, it seems like I should just leave `oldxp` and `oldyp` alone, and switch to using dedicated variables that are never used in the physics of the game. So I'm introducing `lerpoldxp` and `lerpoldyp`, and replacing all instances of using `oldxp` and `oldyp` that my over-30-FPS patch added, with `lerpoldxp` and `lerpoldyp` instead. After doing this, and applying #503 as well, my Nova TAS syncs after some minor but acceptable fixes with Viridian's walkingframe.
2020-10-10 05:58:58 +02:00
obj.entities[i].lerpoldxp = obj.entities[i].xp;
obj.entities[i].lerpoldyp = obj.entities[i].yp;
obj.entities[j].tile = 2;
obj.entities[j].colour = 101;
}
obj.entities[i].colour = 0;
obj.entities[i].invis = false;
obj.entities[i].dir = 1;
2020-01-01 21:29:24 +01:00
obj.entities[i].ay = -6;
obj.entities[i].ax = 6;
obj.entities[i].vy = -6;
obj.entities[i].vx = 6;
}
2020-01-01 21:29:24 +01:00
break;
}
2020-01-01 21:29:24 +01:00
case 4043:
{
2022-12-07 00:20:48 +01:00
incstate();
int i = obj.getplayer();
if (INBOUNDS_VEC(i, obj.entities))
{
obj.entities[i].xp += 12;
obj.entities[i].yp -= 15;
}
2020-01-01 21:29:24 +01:00
break;
}
2020-01-01 21:29:24 +01:00
case 4044:
{
2022-12-07 00:20:48 +01:00
incstate();
int i = obj.getplayer();
if (INBOUNDS_VEC(i, obj.entities))
{
obj.entities[i].xp += 12;
obj.entities[i].yp -= 10;
}
2020-01-01 21:29:24 +01:00
break;
}
2020-01-01 21:29:24 +01:00
case 4045:
{
2022-12-07 00:20:48 +01:00
incstate();
int i = obj.getplayer();
if (INBOUNDS_VEC(i, obj.entities))
{
obj.entities[i].xp += 12;
obj.entities[i].yp -= 10;
}
2020-01-01 21:29:24 +01:00
break;
}
2020-01-01 21:29:24 +01:00
case 4046:
{
2022-12-07 00:20:48 +01:00
incstate();
int i = obj.getplayer();
if (INBOUNDS_VEC(i, obj.entities))
{
obj.entities[i].xp += 8;
obj.entities[i].yp -= 8;
}
2020-01-01 21:29:24 +01:00
break;
}
2020-01-01 21:29:24 +01:00
case 4047:
{
2022-12-07 00:20:48 +01:00
incstate();
int i = obj.getplayer();
if (INBOUNDS_VEC(i, obj.entities))
{
obj.entities[i].xp += 6;
obj.entities[i].yp -= 8;
}
2020-01-01 21:29:24 +01:00
break;
}
2020-01-01 21:29:24 +01:00
case 4048:
{
2022-12-07 00:20:48 +01:00
incstate();
2022-12-07 00:35:06 +01:00
setstatedelay(15);
int i = obj.getplayer();
if (INBOUNDS_VEC(i, obj.entities))
{
obj.entities[i].xp += 3;
}
2020-01-01 21:29:24 +01:00
break;
}
2020-01-01 21:29:24 +01:00
case 4049:
hascontrol = true;
advancetext = false;
2022-12-07 00:20:48 +01:00
setstate(0);
2020-01-01 21:29:24 +01:00
break;
case 4050:
//Activating a teleporter (default appear)
2022-12-07 00:20:48 +01:00
incstate();
2022-12-07 00:35:06 +01:00
setstatedelay(15);
2020-01-01 21:29:24 +01:00
flashlight = 5;
screenshake = 90;
music.playef(Sound_FLASH);
2020-01-01 21:29:24 +01:00
break;
case 4051:
//Activating a teleporter 2
2022-12-07 00:20:48 +01:00
incstate();
2022-12-07 00:35:06 +01:00
setstatedelay(0);
2020-01-01 21:29:24 +01:00
flashlight = 5;
screenshake = 0;
music.playef(Sound_TELEPORT);
2020-01-01 21:29:24 +01:00
break;
case 4052:
{
2020-01-01 21:29:24 +01:00
//Activating a teleporter 2
2022-12-07 00:20:48 +01:00
incstate();
2022-12-07 00:35:06 +01:00
setstatedelay(5);
2020-01-01 21:29:24 +01:00
int i = obj.getplayer();
int j = obj.getteleporter();
if (INBOUNDS_VEC(i, obj.entities))
2020-01-01 21:29:24 +01:00
{
if (INBOUNDS_VEC(j, obj.entities))
{
obj.entities[i].xp = obj.entities[j].xp+44;
obj.entities[i].yp = obj.entities[j].yp+44;
Restore previous oldxp/oldyp variables in favor of lerpoldxp/lerpoldyp I was investigating a desync in my Nova TAS, and it turns out that the gravity line collision functions check for the `oldxp` and `oldyp` of the player, i.e. their position on the previous frame, along with their position on the current frame. So, if the player either collided with the gravity line last frame or this frame, then the player collided with the gravity line this frame. Except, that's not actually true. It turns out that `oldxp` and `oldyp` don't necessarily always correspond to the `xp` and `yp` of the player on the previous frame. It turns out that your `oldyp` will be updated if you stand on a vertically moving platform, before the gravity line collision function gets ran. So, if you were colliding with a gravity line on the previous frame, but you got moved out of there by a vertically moving platform, then you just don't collide with the gravity line at all. However, this behavior changed in 2.3 after my over-30-FPS patch got merged (#220). That patch took advantage of the existing `oldxp` and `oldyp` entity attributes, and uses them to interpolate their positions during rendering to make everything look real smooth. Previously, `oldxp` and `oldyp` would both be updated in `entityclass::updateentitylogic()`. However, I moved it in that patch to update right before `gameinput()` in `main.cpp`. As a result, `oldyp` no longer gets updated whenever the player stands on a vertically moving platform. This ends up desyncing my TAS. As expected, updating `oldyp` in `entityclass::movingplatformfix()` (the function responsible for moving the player whenever they stand on a vertically moving platform) makes it so that my TAS syncs, but the visuals are glitchy when standing on a vertically moving platform. And as much as I'd like to get rid of gravity lines checking for whether you've collided with them on the previous frame, doing that desyncs my TAS, too. In the end, it seems like I should just leave `oldxp` and `oldyp` alone, and switch to using dedicated variables that are never used in the physics of the game. So I'm introducing `lerpoldxp` and `lerpoldyp`, and replacing all instances of using `oldxp` and `oldyp` that my over-30-FPS patch added, with `lerpoldxp` and `lerpoldyp` instead. After doing this, and applying #503 as well, my Nova TAS syncs after some minor but acceptable fixes with Viridian's walkingframe.
2020-10-10 05:58:58 +02:00
obj.entities[i].lerpoldxp = obj.entities[i].xp;
obj.entities[i].lerpoldyp = obj.entities[i].yp;
obj.entities[j].tile = 2;
obj.entities[j].colour = 101;
}
obj.entities[i].colour = 0;
obj.entities[i].invis = false;
obj.entities[i].dir = 1;
2020-01-01 21:29:24 +01:00
obj.entities[i].ay = -6;
obj.entities[i].ax = 6;
obj.entities[i].vy = -6;
obj.entities[i].vx = 6;
}
2020-01-01 21:29:24 +01:00
break;
}
2020-01-01 21:29:24 +01:00
case 4053:
{
2022-12-07 00:20:48 +01:00
incstate();
int i = obj.getplayer();
if (INBOUNDS_VEC(i, obj.entities))
{
obj.entities[i].xp += 4;
obj.entities[i].yp -= 15;
}
2020-01-01 21:29:24 +01:00
break;
}
2020-01-01 21:29:24 +01:00
case 4054:
{
2022-12-07 00:20:48 +01:00
incstate();
int i = obj.getplayer();
if (INBOUNDS_VEC(i, obj.entities))
{
obj.entities[i].xp += 4;
obj.entities[i].yp -= 10;
}
2020-01-01 21:29:24 +01:00
break;
}
2020-01-01 21:29:24 +01:00
case 4055:
{
2022-12-07 00:20:48 +01:00
incstate();
int i = obj.getplayer();
if (INBOUNDS_VEC(i, obj.entities))
{
obj.entities[i].xp += 4;
obj.entities[i].yp -= 10;
}
2020-01-01 21:29:24 +01:00
break;
}
2020-01-01 21:29:24 +01:00
case 4056:
{
2022-12-07 00:20:48 +01:00
incstate();
int i = obj.getplayer();
if (INBOUNDS_VEC(i, obj.entities))
{
obj.entities[i].xp += 4;
obj.entities[i].yp -= 8;
}
2020-01-01 21:29:24 +01:00
break;
}
2020-01-01 21:29:24 +01:00
case 4057:
{
2022-12-07 00:20:48 +01:00
incstate();
int i = obj.getplayer();
if (INBOUNDS_VEC(i, obj.entities))
{
obj.entities[i].xp += 2;
obj.entities[i].yp -= 8;
}
2020-01-01 21:29:24 +01:00
break;
}
2020-01-01 21:29:24 +01:00
case 4058:
{
2022-12-07 00:20:48 +01:00
incstate();
2022-12-07 00:35:06 +01:00
setstatedelay(15);
int i = obj.getplayer();
if (INBOUNDS_VEC(i, obj.entities))
{
obj.entities[i].xp += 1;
}
2020-01-01 21:29:24 +01:00
break;
}
2020-01-01 21:29:24 +01:00
case 4059:
hascontrol = true;
advancetext = false;
2022-12-07 00:20:48 +01:00
setstate(0);
2020-01-01 21:29:24 +01:00
break;
case 4060:
//Activating a teleporter (default appear)
2022-12-07 00:20:48 +01:00
incstate();
2022-12-07 00:35:06 +01:00
setstatedelay(15);
2020-01-01 21:29:24 +01:00
flashlight = 5;
screenshake = 90;
music.playef(Sound_FLASH);
2020-01-01 21:29:24 +01:00
break;
case 4061:
//Activating a teleporter 2
2022-12-07 00:20:48 +01:00
incstate();
2022-12-07 00:35:06 +01:00
setstatedelay(0);
2020-01-01 21:29:24 +01:00
flashlight = 5;
screenshake = 0;
music.playef(Sound_TELEPORT);
2020-01-01 21:29:24 +01:00
break;
case 4062:
{
2020-01-01 21:29:24 +01:00
//Activating a teleporter 2
2022-12-07 00:20:48 +01:00
incstate();
2022-12-07 00:35:06 +01:00
setstatedelay(5);
2020-01-01 21:29:24 +01:00
int i = obj.getplayer();
int j = obj.getteleporter();
if (INBOUNDS_VEC(i, obj.entities))
2020-01-01 21:29:24 +01:00
{
if (INBOUNDS_VEC(j, obj.entities))
{
obj.entities[i].xp = obj.entities[j].xp+44;
obj.entities[i].yp = obj.entities[j].yp+44;
Restore previous oldxp/oldyp variables in favor of lerpoldxp/lerpoldyp I was investigating a desync in my Nova TAS, and it turns out that the gravity line collision functions check for the `oldxp` and `oldyp` of the player, i.e. their position on the previous frame, along with their position on the current frame. So, if the player either collided with the gravity line last frame or this frame, then the player collided with the gravity line this frame. Except, that's not actually true. It turns out that `oldxp` and `oldyp` don't necessarily always correspond to the `xp` and `yp` of the player on the previous frame. It turns out that your `oldyp` will be updated if you stand on a vertically moving platform, before the gravity line collision function gets ran. So, if you were colliding with a gravity line on the previous frame, but you got moved out of there by a vertically moving platform, then you just don't collide with the gravity line at all. However, this behavior changed in 2.3 after my over-30-FPS patch got merged (#220). That patch took advantage of the existing `oldxp` and `oldyp` entity attributes, and uses them to interpolate their positions during rendering to make everything look real smooth. Previously, `oldxp` and `oldyp` would both be updated in `entityclass::updateentitylogic()`. However, I moved it in that patch to update right before `gameinput()` in `main.cpp`. As a result, `oldyp` no longer gets updated whenever the player stands on a vertically moving platform. This ends up desyncing my TAS. As expected, updating `oldyp` in `entityclass::movingplatformfix()` (the function responsible for moving the player whenever they stand on a vertically moving platform) makes it so that my TAS syncs, but the visuals are glitchy when standing on a vertically moving platform. And as much as I'd like to get rid of gravity lines checking for whether you've collided with them on the previous frame, doing that desyncs my TAS, too. In the end, it seems like I should just leave `oldxp` and `oldyp` alone, and switch to using dedicated variables that are never used in the physics of the game. So I'm introducing `lerpoldxp` and `lerpoldyp`, and replacing all instances of using `oldxp` and `oldyp` that my over-30-FPS patch added, with `lerpoldxp` and `lerpoldyp` instead. After doing this, and applying #503 as well, my Nova TAS syncs after some minor but acceptable fixes with Viridian's walkingframe.
2020-10-10 05:58:58 +02:00
obj.entities[i].lerpoldxp = obj.entities[i].xp;
obj.entities[i].lerpoldyp = obj.entities[i].yp;
obj.entities[j].tile = 2;
obj.entities[j].colour = 101;
}
obj.entities[i].colour = 0;
obj.entities[i].invis = false;
obj.entities[i].dir = 0;
2020-01-01 21:29:24 +01:00
obj.entities[i].ay = -6;
obj.entities[i].ax = -6;
obj.entities[i].vy = -6;
obj.entities[i].vx = -6;
}
2020-01-01 21:29:24 +01:00
break;
}
2020-01-01 21:29:24 +01:00
case 4063:
{
2022-12-07 00:20:48 +01:00
incstate();
int i = obj.getplayer();
if (INBOUNDS_VEC(i, obj.entities))
{
obj.entities[i].xp -= 28;
obj.entities[i].yp -= 8;
}
2020-01-01 21:29:24 +01:00
break;
}
2020-01-01 21:29:24 +01:00
case 4064:
{
2022-12-07 00:20:48 +01:00
incstate();
int i = obj.getplayer();
if (INBOUNDS_VEC(i, obj.entities))
{
obj.entities[i].xp -= 28;
obj.entities[i].yp -= 8;
}
2020-01-01 21:29:24 +01:00
break;
}
2020-01-01 21:29:24 +01:00
case 4065:
{
2022-12-07 00:20:48 +01:00
incstate();
int i = obj.getplayer();
if (INBOUNDS_VEC(i, obj.entities))
{
obj.entities[i].xp -= 25;
}
2020-01-01 21:29:24 +01:00
break;
}
2020-01-01 21:29:24 +01:00
case 4066:
{
2022-12-07 00:20:48 +01:00
incstate();
int i = obj.getplayer();
if (INBOUNDS_VEC(i, obj.entities))
{
obj.entities[i].xp -= 25;
}
2020-01-01 21:29:24 +01:00
break;
}
2020-01-01 21:29:24 +01:00
case 4067:
{
2022-12-07 00:20:48 +01:00
incstate();
int i = obj.getplayer();
if (INBOUNDS_VEC(i, obj.entities))
{
obj.entities[i].xp -= 20;
}
2020-01-01 21:29:24 +01:00
break;
}
2020-01-01 21:29:24 +01:00
case 4068:
{
2022-12-07 00:20:48 +01:00
incstate();
2022-12-07 00:35:06 +01:00
setstatedelay(15);
int i = obj.getplayer();
if (INBOUNDS_VEC(i, obj.entities))
{
obj.entities[i].xp -= 16;
}
2020-01-01 21:29:24 +01:00
break;
}
2020-01-01 21:29:24 +01:00
case 4069:
hascontrol = true;
advancetext = false;
2022-12-07 00:20:48 +01:00
setstate(0);
2020-01-01 21:29:24 +01:00
break;
case 4070:
//Activating a teleporter (special for final script, player has colour changed to match rescued crewmate)
2022-12-07 00:20:48 +01:00
incstate();
2022-12-07 00:35:06 +01:00
setstatedelay(15);
2020-01-01 21:29:24 +01:00
flashlight = 5;
screenshake = 90;
music.playef(Sound_FLASH);
2020-01-01 21:29:24 +01:00
break;
case 4071:
//Activating a teleporter 2
2022-12-07 00:20:48 +01:00
incstate();
2022-12-07 00:35:06 +01:00
setstatedelay(0);
2020-01-01 21:29:24 +01:00
flashlight = 5;
screenshake = 0;
music.playef(Sound_TELEPORT);
2020-01-01 21:29:24 +01:00
break;
case 4072:
{
2020-01-01 21:29:24 +01:00
//Activating a teleporter 2
2022-12-07 00:20:48 +01:00
incstate();
2022-12-07 00:35:06 +01:00
setstatedelay(5);
2020-01-01 21:29:24 +01:00
int i = obj.getplayer();
int j = obj.getteleporter();
if (INBOUNDS_VEC(i, obj.entities))
2020-01-01 21:29:24 +01:00
{
if (INBOUNDS_VEC(j, obj.entities))
{
obj.entities[i].xp = obj.entities[j].xp+44;
obj.entities[i].yp = obj.entities[j].yp+44;
Restore previous oldxp/oldyp variables in favor of lerpoldxp/lerpoldyp I was investigating a desync in my Nova TAS, and it turns out that the gravity line collision functions check for the `oldxp` and `oldyp` of the player, i.e. their position on the previous frame, along with their position on the current frame. So, if the player either collided with the gravity line last frame or this frame, then the player collided with the gravity line this frame. Except, that's not actually true. It turns out that `oldxp` and `oldyp` don't necessarily always correspond to the `xp` and `yp` of the player on the previous frame. It turns out that your `oldyp` will be updated if you stand on a vertically moving platform, before the gravity line collision function gets ran. So, if you were colliding with a gravity line on the previous frame, but you got moved out of there by a vertically moving platform, then you just don't collide with the gravity line at all. However, this behavior changed in 2.3 after my over-30-FPS patch got merged (#220). That patch took advantage of the existing `oldxp` and `oldyp` entity attributes, and uses them to interpolate their positions during rendering to make everything look real smooth. Previously, `oldxp` and `oldyp` would both be updated in `entityclass::updateentitylogic()`. However, I moved it in that patch to update right before `gameinput()` in `main.cpp`. As a result, `oldyp` no longer gets updated whenever the player stands on a vertically moving platform. This ends up desyncing my TAS. As expected, updating `oldyp` in `entityclass::movingplatformfix()` (the function responsible for moving the player whenever they stand on a vertically moving platform) makes it so that my TAS syncs, but the visuals are glitchy when standing on a vertically moving platform. And as much as I'd like to get rid of gravity lines checking for whether you've collided with them on the previous frame, doing that desyncs my TAS, too. In the end, it seems like I should just leave `oldxp` and `oldyp` alone, and switch to using dedicated variables that are never used in the physics of the game. So I'm introducing `lerpoldxp` and `lerpoldyp`, and replacing all instances of using `oldxp` and `oldyp` that my over-30-FPS patch added, with `lerpoldxp` and `lerpoldyp` instead. After doing this, and applying #503 as well, my Nova TAS syncs after some minor but acceptable fixes with Viridian's walkingframe.
2020-10-10 05:58:58 +02:00
obj.entities[i].lerpoldxp = obj.entities[i].xp;
obj.entities[i].lerpoldyp = obj.entities[i].yp;
obj.entities[j].tile = 2;
obj.entities[j].colour = 101;
}
obj.entities[i].invis = false;
obj.entities[i].dir = 1;
obj.entities[i].colour = graphics.crewcolour(lastsaved);
2020-01-01 21:29:24 +01:00
obj.entities[i].ay = -6;
obj.entities[i].ax = 6;
obj.entities[i].vy = -6;
obj.entities[i].vx = 6;
}
2020-01-01 21:29:24 +01:00
break;
}
2020-01-01 21:29:24 +01:00
case 4073:
{
2022-12-07 00:20:48 +01:00
incstate();
int i = obj.getplayer();
if (INBOUNDS_VEC(i, obj.entities))
{
obj.entities[i].xp += 10;
}
2020-01-01 21:29:24 +01:00
break;
}
2020-01-01 21:29:24 +01:00
case 4074:
{
2022-12-07 00:20:48 +01:00
incstate();
int i = obj.getplayer();
if (INBOUNDS_VEC(i, obj.entities))
{
obj.entities[i].xp += 10;
}
2020-01-01 21:29:24 +01:00
break;
}
2020-01-01 21:29:24 +01:00
case 4075:
{
2022-12-07 00:20:48 +01:00
incstate();
int i = obj.getplayer();
if (INBOUNDS_VEC(i, obj.entities))
{
obj.entities[i].xp += 8;
}
2020-01-01 21:29:24 +01:00
break;
}
2020-01-01 21:29:24 +01:00
case 4076:
{
2022-12-07 00:20:48 +01:00
incstate();
int i = obj.getplayer();
if (INBOUNDS_VEC(i, obj.entities))
{
obj.entities[i].xp += 6;
}
2020-01-01 21:29:24 +01:00
break;
}
2020-01-01 21:29:24 +01:00
case 4077:
{
2022-12-07 00:20:48 +01:00
incstate();
int i = obj.getplayer();
if (INBOUNDS_VEC(i, obj.entities))
{
obj.entities[i].xp += 3;
}
2020-01-01 21:29:24 +01:00
break;
}
2020-01-01 21:29:24 +01:00
case 4078:
{
2022-12-07 00:20:48 +01:00
incstate();
2022-12-07 00:35:06 +01:00
setstatedelay(15);
int i = obj.getplayer();
if (INBOUNDS_VEC(i, obj.entities))
{
obj.entities[i].xp += 1;
}
2020-01-01 21:29:24 +01:00
break;
}
2020-01-01 21:29:24 +01:00
case 4079:
2022-12-07 00:20:48 +01:00
setstate(0);
2020-01-01 21:29:24 +01:00
startscript = true;
newscript = "finallevel_teleporter";
break;
case 4080:
//Activating a teleporter (default appear)
2022-12-07 00:20:48 +01:00
incstate();
2022-12-07 00:35:06 +01:00
setstatedelay(15);
2020-01-01 21:29:24 +01:00
flashlight = 5;
screenshake = 90;
music.playef(Sound_FLASH);
2020-01-01 21:29:24 +01:00
break;
case 4081:
//Activating a teleporter 2
2022-12-07 00:20:48 +01:00
incstate();
2022-12-07 00:35:06 +01:00
setstatedelay(0);
2020-01-01 21:29:24 +01:00
flashlight = 5;
screenshake = 0;
music.playef(Sound_TELEPORT);
2020-01-01 21:29:24 +01:00
break;
case 4082:
{
2020-01-01 21:29:24 +01:00
//Activating a teleporter 2
2022-12-07 00:20:48 +01:00
incstate();
2022-12-07 00:35:06 +01:00
setstatedelay(5);
2020-01-01 21:29:24 +01:00
int i = obj.getplayer();
int j = obj.getteleporter();
if (INBOUNDS_VEC(i, obj.entities))
2020-01-01 21:29:24 +01:00
{
if (INBOUNDS_VEC(j, obj.entities))
{
obj.entities[i].xp = obj.entities[j].xp+44;
obj.entities[i].yp = obj.entities[j].yp+44;
Restore previous oldxp/oldyp variables in favor of lerpoldxp/lerpoldyp I was investigating a desync in my Nova TAS, and it turns out that the gravity line collision functions check for the `oldxp` and `oldyp` of the player, i.e. their position on the previous frame, along with their position on the current frame. So, if the player either collided with the gravity line last frame or this frame, then the player collided with the gravity line this frame. Except, that's not actually true. It turns out that `oldxp` and `oldyp` don't necessarily always correspond to the `xp` and `yp` of the player on the previous frame. It turns out that your `oldyp` will be updated if you stand on a vertically moving platform, before the gravity line collision function gets ran. So, if you were colliding with a gravity line on the previous frame, but you got moved out of there by a vertically moving platform, then you just don't collide with the gravity line at all. However, this behavior changed in 2.3 after my over-30-FPS patch got merged (#220). That patch took advantage of the existing `oldxp` and `oldyp` entity attributes, and uses them to interpolate their positions during rendering to make everything look real smooth. Previously, `oldxp` and `oldyp` would both be updated in `entityclass::updateentitylogic()`. However, I moved it in that patch to update right before `gameinput()` in `main.cpp`. As a result, `oldyp` no longer gets updated whenever the player stands on a vertically moving platform. This ends up desyncing my TAS. As expected, updating `oldyp` in `entityclass::movingplatformfix()` (the function responsible for moving the player whenever they stand on a vertically moving platform) makes it so that my TAS syncs, but the visuals are glitchy when standing on a vertically moving platform. And as much as I'd like to get rid of gravity lines checking for whether you've collided with them on the previous frame, doing that desyncs my TAS, too. In the end, it seems like I should just leave `oldxp` and `oldyp` alone, and switch to using dedicated variables that are never used in the physics of the game. So I'm introducing `lerpoldxp` and `lerpoldyp`, and replacing all instances of using `oldxp` and `oldyp` that my over-30-FPS patch added, with `lerpoldxp` and `lerpoldyp` instead. After doing this, and applying #503 as well, my Nova TAS syncs after some minor but acceptable fixes with Viridian's walkingframe.
2020-10-10 05:58:58 +02:00
obj.entities[i].lerpoldxp = obj.entities[i].xp;
obj.entities[i].lerpoldyp = obj.entities[i].yp;
obj.entities[j].tile = 2;
obj.entities[j].colour = 101;
}
obj.entities[i].colour = 0;
obj.entities[i].invis = false;
obj.entities[i].dir = 1;
2020-01-01 21:29:24 +01:00
obj.entities[i].ay = -6;
obj.entities[i].ax = 6;
obj.entities[i].vy = -6;
obj.entities[i].vx = 6;
}
2020-01-01 21:29:24 +01:00
break;
}
2020-01-01 21:29:24 +01:00
case 4083:
{
2022-12-07 00:20:48 +01:00
incstate();
int i = obj.getplayer();
if (INBOUNDS_VEC(i, obj.entities))
{
obj.entities[i].xp += 10;
}
2020-01-01 21:29:24 +01:00
break;
}
2020-01-01 21:29:24 +01:00
case 4084:
{
2022-12-07 00:20:48 +01:00
incstate();
int i = obj.getplayer();
if (INBOUNDS_VEC(i, obj.entities))
{
obj.entities[i].xp += 10;
}
2020-01-01 21:29:24 +01:00
break;
}
2020-01-01 21:29:24 +01:00
case 4085:
{
2022-12-07 00:20:48 +01:00
incstate();
int i = obj.getplayer();
if (INBOUNDS_VEC(i, obj.entities))
{
obj.entities[i].xp += 8;
}
2020-01-01 21:29:24 +01:00
break;
}
2020-01-01 21:29:24 +01:00
case 4086:
{
2022-12-07 00:20:48 +01:00
incstate();
int i = obj.getplayer();
if (INBOUNDS_VEC(i, obj.entities))
{
obj.entities[i].xp += 6;
}
2020-01-01 21:29:24 +01:00
break;
}
2020-01-01 21:29:24 +01:00
case 4087:
{
2022-12-07 00:20:48 +01:00
incstate();
int i = obj.getplayer();
if (INBOUNDS_VEC(i, obj.entities))
{
obj.entities[i].xp += 3;
}
2020-01-01 21:29:24 +01:00
break;
}
2020-01-01 21:29:24 +01:00
case 4088:
{
2022-12-07 00:20:48 +01:00
incstate();
2022-12-07 00:35:06 +01:00
setstatedelay(15);
int i = obj.getplayer();
if (INBOUNDS_VEC(i, obj.entities))
{
obj.entities[i].xp += 1;
}
2020-01-01 21:29:24 +01:00
break;
}
2020-01-01 21:29:24 +01:00
case 4089:
startscript = true;
newscript = "gamecomplete_ending";
2022-12-07 00:20:48 +01:00
setstate(0);
2020-01-01 21:29:24 +01:00
break;
case 4090:
//Activating a teleporter (default appear)
2022-12-07 00:20:48 +01:00
incstate();
2022-12-07 00:35:06 +01:00
setstatedelay(15);
2020-01-01 21:29:24 +01:00
flashlight = 5;
screenshake = 90;
music.playef(Sound_FLASH);
2020-01-01 21:29:24 +01:00
break;
case 4091:
//Activating a teleporter 2
2022-12-07 00:20:48 +01:00
incstate();
2022-12-07 00:35:06 +01:00
setstatedelay(0);
2020-01-01 21:29:24 +01:00
flashlight = 5;
screenshake = 0;
music.playef(Sound_TELEPORT);
2020-01-01 21:29:24 +01:00
break;
case 4092:
{
2020-01-01 21:29:24 +01:00
//Activating a teleporter 2
2022-12-07 00:20:48 +01:00
incstate();
2022-12-07 00:35:06 +01:00
setstatedelay(5);
2020-01-01 21:29:24 +01:00
int i = obj.getplayer();
int j = obj.getteleporter();
if (INBOUNDS_VEC(i, obj.entities))
2020-01-01 21:29:24 +01:00
{
if (INBOUNDS_VEC(j, obj.entities))
{
obj.entities[i].xp = obj.entities[j].xp+44;
obj.entities[i].yp = obj.entities[j].yp+44;
Restore previous oldxp/oldyp variables in favor of lerpoldxp/lerpoldyp I was investigating a desync in my Nova TAS, and it turns out that the gravity line collision functions check for the `oldxp` and `oldyp` of the player, i.e. their position on the previous frame, along with their position on the current frame. So, if the player either collided with the gravity line last frame or this frame, then the player collided with the gravity line this frame. Except, that's not actually true. It turns out that `oldxp` and `oldyp` don't necessarily always correspond to the `xp` and `yp` of the player on the previous frame. It turns out that your `oldyp` will be updated if you stand on a vertically moving platform, before the gravity line collision function gets ran. So, if you were colliding with a gravity line on the previous frame, but you got moved out of there by a vertically moving platform, then you just don't collide with the gravity line at all. However, this behavior changed in 2.3 after my over-30-FPS patch got merged (#220). That patch took advantage of the existing `oldxp` and `oldyp` entity attributes, and uses them to interpolate their positions during rendering to make everything look real smooth. Previously, `oldxp` and `oldyp` would both be updated in `entityclass::updateentitylogic()`. However, I moved it in that patch to update right before `gameinput()` in `main.cpp`. As a result, `oldyp` no longer gets updated whenever the player stands on a vertically moving platform. This ends up desyncing my TAS. As expected, updating `oldyp` in `entityclass::movingplatformfix()` (the function responsible for moving the player whenever they stand on a vertically moving platform) makes it so that my TAS syncs, but the visuals are glitchy when standing on a vertically moving platform. And as much as I'd like to get rid of gravity lines checking for whether you've collided with them on the previous frame, doing that desyncs my TAS, too. In the end, it seems like I should just leave `oldxp` and `oldyp` alone, and switch to using dedicated variables that are never used in the physics of the game. So I'm introducing `lerpoldxp` and `lerpoldyp`, and replacing all instances of using `oldxp` and `oldyp` that my over-30-FPS patch added, with `lerpoldxp` and `lerpoldyp` instead. After doing this, and applying #503 as well, my Nova TAS syncs after some minor but acceptable fixes with Viridian's walkingframe.
2020-10-10 05:58:58 +02:00
obj.entities[i].lerpoldxp = obj.entities[i].xp;
obj.entities[i].lerpoldyp = obj.entities[i].yp;
obj.entities[j].tile = 2;
obj.entities[j].colour = 101;
}
obj.entities[i].colour = 0;
obj.entities[i].invis = false;
obj.entities[i].dir = 1;
2020-01-01 21:29:24 +01:00
obj.entities[i].ay = -6;
obj.entities[i].ax = 6;
obj.entities[i].vy = -6;
obj.entities[i].vx = 6;
}
2020-01-01 21:29:24 +01:00
break;
}
2020-01-01 21:29:24 +01:00
case 4093:
{
2022-12-07 00:20:48 +01:00
incstate();
int i = obj.getplayer();
if (INBOUNDS_VEC(i, obj.entities))
{
obj.entities[i].xp += 10;
}
2020-01-01 21:29:24 +01:00
break;
}
2020-01-01 21:29:24 +01:00
case 4094:
{
2022-12-07 00:20:48 +01:00
incstate();
int i = obj.getplayer();
if (INBOUNDS_VEC(i, obj.entities))
{
obj.entities[i].xp += 10;
}
2020-01-01 21:29:24 +01:00
break;
}
2020-01-01 21:29:24 +01:00
case 4095:
{
2022-12-07 00:20:48 +01:00
incstate();
int i = obj.getplayer();
if (INBOUNDS_VEC(i, obj.entities))
{
obj.entities[i].xp += 8;
}
2020-01-01 21:29:24 +01:00
break;
}
2020-01-01 21:29:24 +01:00
case 4096:
{
2022-12-07 00:20:48 +01:00
incstate();
int i = obj.getplayer();
if (INBOUNDS_VEC(i, obj.entities))
{
obj.entities[i].xp += 6;
}
2020-01-01 21:29:24 +01:00
break;
}
2020-01-01 21:29:24 +01:00
case 4097:
{
2022-12-07 00:20:48 +01:00
incstate();
int i = obj.getplayer();
if (INBOUNDS_VEC(i, obj.entities))
{
obj.entities[i].xp += 3;
}
2020-01-01 21:29:24 +01:00
break;
}
2020-01-01 21:29:24 +01:00
case 4098:
{
2022-12-07 00:20:48 +01:00
incstate();
2022-12-07 00:35:06 +01:00
setstatedelay(15);
int i = obj.getplayer();
if (INBOUNDS_VEC(i, obj.entities))
{
obj.entities[i].xp += 1;
}
2020-01-01 21:29:24 +01:00
break;
}
2020-01-01 21:29:24 +01:00
case 4099:
if (nocutscenes)
{
startscript = true;
newscript = "levelonecompleteskip";
}
else
{
startscript = true;
newscript = "levelonecomplete_ending";
}
2022-12-07 00:20:48 +01:00
setstate(0);
2020-01-01 21:29:24 +01:00
break;
}
}
}
void Game::gethardestroom(void)
2020-01-01 21:29:24 +01:00
{
if (currentroomdeaths > hardestroomdeaths)
{
hardestroomdeaths = currentroomdeaths;
hardestroom_x = roomx;
hardestroom_y = roomy;
hardestroom_finalstretch = map.finalstretch;
if (map.roomname[0] == '\0')
2020-01-01 21:29:24 +01:00
{
hardestroom = map.hiddenname;
hardestroom_specialname = true;
2020-01-01 21:29:24 +01:00
}
else
{
hardestroom = map.roomname;
hardestroom_specialname = map.roomname_special;
}
2020-01-01 21:29:24 +01:00
}
}
void Game::deletestats(void)
2020-01-01 21:29:24 +01:00
{
if (!FILESYSTEM_delete("saves/unlock.vvv"))
2020-01-01 21:29:24 +01:00
{
vlog_error("Error deleting saves/unlock.vvv");
2020-01-01 21:29:24 +01:00
}
else
2020-01-01 21:29:24 +01:00
{
for (int i = 0; i < numunlock; i++)
{
unlock[i] = false;
unlocknotify[i] = false;
}
for (int i = 0; i < numtrials; i++)
{
besttimes[i] = -1;
bestframes[i] = -1;
besttrinkets[i] = -1;
bestlives[i] = -1;
bestrank[i] = -1;
}
swnrecord = 0;
swnbestrank = 0;
bestgamedeaths = -1;
#ifndef MAKEANDPLAY
graphics.setflipmode = false;
#endif
stat_trinkets = 0;
2020-01-01 21:29:24 +01:00
}
}
void Game::deletesettings(void)
{
if (!FILESYSTEM_delete("saves/settings.vvv"))
{
vlog_error("Error deleting saves/settings.vvv");
}
}
void Game::unlocknum( int t )
2020-01-01 21:29:24 +01:00
{
#ifdef MAKEANDPLAY
UNUSED(t);
#else
if (map.custommode)
{
//Don't let custom levels unlock things!
return;
}
2020-01-01 21:29:24 +01:00
unlock[t] = true;
savestatsandsettings();
#endif
2020-01-01 21:29:24 +01:00
}
static bool stats_loaded = false;
void Game::loadstats(struct ScreenSettings* screen_settings)
2020-01-01 21:29:24 +01:00
{
tinyxml2::XMLDocument doc;
tinyxml2::XMLHandle hDoc(&doc);
tinyxml2::XMLElement* pElem;
tinyxml2::XMLElement* dataNode;
stats_loaded = true;
if (!FILESYSTEM_loadTiXml2Document("saves/unlock.vvv", doc))
2020-01-01 21:29:24 +01:00
{
// Save unlock.vvv only. Maybe we have a settings.vvv laying around too,
// and we don't want to overwrite that!
savestats(screen_settings);
return;
2020-01-01 21:29:24 +01:00
}
if (doc.Error())
2020-01-01 21:29:24 +01:00
{
vlog_error("Error parsing unlock.vvv: %s", doc.ErrorStr());
return;
2020-01-01 21:29:24 +01:00
}
dataNode = hDoc
.FirstChildElement()
.FirstChildElement("Data")
.FirstChildElement()
.ToElement();
for (pElem = dataNode; pElem != NULL; pElem=pElem->NextSiblingElement())
2020-01-01 21:29:24 +01:00
{
const char* pKey = pElem->Value();
2020-01-01 21:29:24 +01:00
const char* pText = pElem->GetText() ;
if (pText == NULL)
{
pText = "";
}
LOAD_ARRAY(unlock)
2020-01-01 21:29:24 +01:00
LOAD_ARRAY(unlocknotify)
2020-01-01 21:29:24 +01:00
LOAD_ARRAY(besttimes)
2020-01-01 21:29:24 +01:00
LOAD_ARRAY(bestframes)
LOAD_ARRAY(besttrinkets)
2020-01-01 21:29:24 +01:00
LOAD_ARRAY(bestlives)
2020-01-01 21:29:24 +01:00
LOAD_ARRAY(bestrank)
2020-01-01 21:29:24 +01:00
if (SDL_strcmp(pKey, "bestgamedeaths") == 0)
2020-01-01 21:29:24 +01:00
{
bestgamedeaths = help.Int(pText);
2020-01-01 21:29:24 +01:00
}
if (SDL_strcmp(pKey, "stat_trinkets") == 0)
2020-01-01 21:29:24 +01:00
{
stat_trinkets = help.Int(pText);
2020-01-01 21:29:24 +01:00
}
if (SDL_strcmp(pKey, "swnbestrank") == 0)
{
swnbestrank = help.Int(pText);
}
if (SDL_strcmp(pKey, "swnrecord") == 0)
{
swnrecord = help.Int(pText);
}
}
deserializesettings(dataNode, screen_settings);
}
void Game::deserializesettings(tinyxml2::XMLElement* dataNode, struct ScreenSettings* screen_settings)
{
// Don't duplicate controller buttons!
controllerButton_flip.clear();
controllerButton_map.clear();
controllerButton_esc.clear();
controllerButton_restart.clear();
controllerButton_interact.clear();
for (tinyxml2::XMLElement* pElem = dataNode;
pElem != NULL;
pElem = pElem->NextSiblingElement())
{
const char* pKey = pElem->Value();
const char* pText = pElem->GetText();
if (pText == NULL)
{
pText = "";
}
if (SDL_strcmp(pKey, "fullscreen") == 0)
2020-01-01 21:29:24 +01:00
{
screen_settings->fullscreen = help.Int(pText);
2020-01-01 21:29:24 +01:00
}
if (SDL_strcmp(pKey, "stretch") == 0)
{
screen_settings->scalingMode = help.Int(pText);
}
2020-01-01 21:29:24 +01:00
if (SDL_strcmp(pKey, "useLinearFilter") == 0)
{
screen_settings->linearFilter = help.Int(pText);
}
2020-01-01 21:29:24 +01:00
if (SDL_strcmp(pKey, "window_display") == 0)
{
screen_settings->windowDisplay = help.Int(pText);
}
if (SDL_strcmp(pKey, "window_width") == 0)
{
screen_settings->windowWidth = help.Int(pText);
}
if (SDL_strcmp(pKey, "window_height") == 0)
{
screen_settings->windowHeight = help.Int(pText);
}
2020-01-01 21:29:24 +01:00
if (SDL_strcmp(pKey, "noflashingmode") == 0)
2020-01-01 21:29:24 +01:00
{
noflashingmode = help.Int(pText);
2020-01-01 21:29:24 +01:00
}
if (SDL_strcmp(pKey, "colourblindmode") == 0)
2020-01-01 21:29:24 +01:00
{
colourblindmode = help.Int(pText);
2020-01-01 21:29:24 +01:00
}
if (SDL_strcmp(pKey, "setflipmode") == 0)
2020-01-01 21:29:24 +01:00
{
graphics.setflipmode = help.Int(pText);
2020-01-01 21:29:24 +01:00
}
if (SDL_strcmp(pKey, "invincibility") == 0)
2020-01-01 21:29:24 +01:00
{
map.invincibility = help.Int(pText);
2020-01-01 21:29:24 +01:00
}
if (SDL_strcmp(pKey, "slowdown") == 0)
2020-01-01 21:29:24 +01:00
{
slowdown = help.Int(pText);
2020-01-01 21:29:24 +01:00
}
if (SDL_strcmp(pKey, "advanced_smoothing") == 0)
2020-01-01 21:29:24 +01:00
{
screen_settings->badSignal = help.Int(pText);
2020-01-01 21:29:24 +01:00
}
if (SDL_strcmp(pKey, "usingmmmmmm") == 0)
2020-01-01 21:29:24 +01:00
{
music.usingmmmmmm = (bool) help.Int(pText);
2020-01-01 21:29:24 +01:00
}
if (SDL_strcmp(pKey, "ghostsenabled") == 0)
{
ghostsenabled = help.Int(pText);
}
if (SDL_strcmp(pKey, "skipfakeload") == 0)
{
skipfakeload = help.Int(pText);
}
if (SDL_strcmp(pKey, "disablepause") == 0)
{
disablepause = help.Int(pText);
}
if (SDL_strcmp(pKey, "disableaudiopause") == 0)
{
disableaudiopause = help.Int(pText);
}
if (SDL_strcmp(pKey, "over30mode") == 0)
{
over30mode = help.Int(pText);
}
if (SDL_strcmp(pKey, "inputdelay") == 0)
{
inputdelay = help.Int(pText);
}
if (SDL_strcmp(pKey, "glitchrunnermode") == 0)
{
GlitchrunnerMode_set(GlitchrunnerMode_string_to_enum(pText));
}
2021-08-05 23:31:20 +02:00
if (SDL_strcmp(pKey, "showingametimer") == 0)
{
showingametimer = help.Int(pText);
}
if (SDL_strcmp(pKey, "vsync") == 0)
{
screen_settings->useVsync = help.Int(pText);
}
if (SDL_strcmp(pKey, "notextoutline") == 0)
{
graphics.notextoutline = help.Int(pText);
}
if (SDL_strcmp(pKey, "translucentroomname") == 0)
{
graphics.translucentroomname = help.Int(pText);
}
if (SDL_strcmp(pKey, "musicvolume") == 0)
{
music.user_music_volume = help.Int(pText);
}
if (SDL_strcmp(pKey, "soundvolume") == 0)
{
music.user_sound_volume = help.Int(pText);
}
if (SDL_strcmp(pKey, "separate_interact") == 0)
{
separate_interact = help.Int(pText);
}
if (SDL_strcmp(pKey, "flipButton") == 0)
{
SDL_GameControllerButton newButton;
if (GetButtonFromString(pText, &newButton))
{
controllerButton_flip.push_back(newButton);
}
}
if (SDL_strcmp(pKey, "enterButton") == 0)
{
SDL_GameControllerButton newButton;
if (GetButtonFromString(pText, &newButton))
{
controllerButton_map.push_back(newButton);
}
}
if (SDL_strcmp(pKey, "escButton") == 0)
{
SDL_GameControllerButton newButton;
if (GetButtonFromString(pText, &newButton))
{
controllerButton_esc.push_back(newButton);
}
}
if (SDL_strcmp(pKey, "restartButton") == 0)
{
SDL_GameControllerButton newButton;
if (GetButtonFromString(pText, &newButton))
{
controllerButton_restart.push_back(newButton);
}
}
if (SDL_strcmp(pKey, "interactButton") == 0)
{
SDL_GameControllerButton newButton;
if (GetButtonFromString(pText, &newButton))
{
controllerButton_interact.push_back(newButton);
}
}
if (SDL_strcmp(pKey, "controllerSensitivity") == 0)
{
key.sensitivity = help.Int(pText);
}
2020-01-01 21:29:24 +01:00
if (SDL_strcmp(pKey, "lang") == 0)
{
loc::lang = std::string(pText);
}
if (SDL_strcmp(pKey, "lang_set") == 0)
{
loc::lang_set = help.Int(pText);
}
if (SDL_strcmp(pKey, "english_sprites") == 0)
{
loc::english_sprites = help.Int(pText);
}
if (SDL_strcmp(pKey, "new_level_font") == 0)
{
loc::new_level_font = std::string(pText);
}
if (SDL_strcmp(pKey, "roomname_translator") == 0 && loc::show_translator_menu)
{
roomname_translator::set_enabled(help.Int(pText));
}
2020-01-01 21:29:24 +01:00
}
setdefaultcontrollerbuttons();
2020-01-01 21:29:24 +01:00
}
bool Game::savestats(bool sync /*= true*/)
{
struct ScreenSettings screen_settings;
SDL_zero(screen_settings);
gameScreen.GetSettings(&screen_settings);
return savestats(&screen_settings, sync);
}
bool Game::savestats(const struct ScreenSettings* screen_settings, bool sync /*= true*/)
2020-01-01 21:29:24 +01:00
{
tinyxml2::XMLDocument doc;
bool already_exists;
if (!stats_loaded)
{
vlog_warn("Stats not loaded! Not writing unlock.vvv.");
return false;
}
already_exists = FILESYSTEM_loadTiXml2Document("saves/unlock.vvv", doc);
if (!already_exists)
{
vlog_info("No unlock.vvv found. Creating new file");
}
else if (doc.Error())
{
vlog_error("Error parsing existing unlock.vvv: %s", doc.ErrorStr());
vlog_info("Creating new unlock.vvv");
}
xml::update_declaration(doc);
2020-01-01 21:29:24 +01:00
tinyxml2::XMLElement * root = xml::update_element(doc, "Save");
2020-01-01 21:29:24 +01:00
xml::update_comment(root, " Save file " );
2020-01-01 21:29:24 +01:00
tinyxml2::XMLElement * dataNode = xml::update_element(root, "Data");
2020-01-01 21:29:24 +01:00
std::string s_unlock;
for(size_t i = 0; i < SDL_arraysize(unlock); i++ )
2020-01-01 21:29:24 +01:00
{
s_unlock += help.String(unlock[i]) + ",";
2020-01-01 21:29:24 +01:00
}
xml::update_tag(dataNode, "unlock", s_unlock.c_str());
2020-01-01 21:29:24 +01:00
std::string s_unlocknotify;
for(size_t i = 0; i < SDL_arraysize(unlocknotify); i++ )
2020-01-01 21:29:24 +01:00
{
s_unlocknotify += help.String(unlocknotify[i]) + ",";
2020-01-01 21:29:24 +01:00
}
xml::update_tag(dataNode, "unlocknotify", s_unlocknotify.c_str());
2020-01-01 21:29:24 +01:00
std::string s_besttimes;
for(size_t i = 0; i < SDL_arraysize(besttimes); i++ )
2020-01-01 21:29:24 +01:00
{
s_besttimes += help.String(besttimes[i]) + ",";
2020-01-01 21:29:24 +01:00
}
xml::update_tag(dataNode, "besttimes", s_besttimes.c_str());
2020-01-01 21:29:24 +01:00
std::string s_bestframes;
for (size_t i = 0; i < SDL_arraysize(bestframes); i++)
{
s_bestframes += help.String(bestframes[i]) + ",";
}
xml::update_tag(dataNode, "bestframes", s_bestframes.c_str());
2020-01-01 21:29:24 +01:00
std::string s_besttrinkets;
for(size_t i = 0; i < SDL_arraysize(besttrinkets); i++ )
2020-01-01 21:29:24 +01:00
{
s_besttrinkets += help.String(besttrinkets[i]) + ",";
2020-01-01 21:29:24 +01:00
}
xml::update_tag(dataNode, "besttrinkets", s_besttrinkets.c_str());
2020-01-01 21:29:24 +01:00
std::string s_bestlives;
for(size_t i = 0; i < SDL_arraysize(bestlives); i++ )
2020-01-01 21:29:24 +01:00
{
s_bestlives += help.String(bestlives[i]) + ",";
2020-01-01 21:29:24 +01:00
}
xml::update_tag(dataNode, "bestlives", s_bestlives.c_str());
2020-01-01 21:29:24 +01:00
std::string s_bestrank;
for(size_t i = 0; i < SDL_arraysize(bestrank); i++ )
2020-01-01 21:29:24 +01:00
{
s_bestrank += help.String(bestrank[i]) + ",";
2020-01-01 21:29:24 +01:00
}
xml::update_tag(dataNode, "bestrank", s_bestrank.c_str());
xml::update_tag(dataNode, "bestgamedeaths", bestgamedeaths);
2020-01-01 21:29:24 +01:00
xml::update_tag(dataNode, "stat_trinkets", stat_trinkets);
2020-01-01 21:29:24 +01:00
xml::update_tag(dataNode, "swnbestrank", swnbestrank);
xml::update_tag(dataNode, "swnrecord", swnrecord);
serializesettings(dataNode, screen_settings);
return FILESYSTEM_saveTiXml2Document("saves/unlock.vvv", doc, sync);
}
bool Game::savestatsandsettings(void)
{
const bool stats_saved = savestats(false);
const bool settings_saved = savesettings();
return stats_saved && settings_saved; // Not the same as `savestats() && savesettings()`!
}
void Game::savestatsandsettings_menu(void)
{
// Call Game::savestatsandsettings(), but upon failure, go to the save error screen
if (!savestatsandsettings() && !silence_settings_error)
{
createmenu(Menu::errorsavingsettings);
map.nexttowercolour();
}
}
void Game::serializesettings(tinyxml2::XMLElement* dataNode, const struct ScreenSettings* screen_settings)
{
tinyxml2::XMLDocument& doc = xml::get_document(dataNode);
xml::update_tag(dataNode, "fullscreen", (int) screen_settings->fullscreen);
2020-01-01 21:29:24 +01:00
xml::update_tag(dataNode, "stretch", screen_settings->scalingMode);
2020-01-01 21:29:24 +01:00
xml::update_tag(dataNode, "useLinearFilter", (int) screen_settings->linearFilter);
2020-01-01 21:29:24 +01:00
xml::update_tag(dataNode, "window_display", screen_settings->windowDisplay);
xml::update_tag(dataNode, "window_width", screen_settings->windowWidth);
2020-01-01 21:29:24 +01:00
xml::update_tag(dataNode, "window_height", screen_settings->windowHeight);
2020-01-01 21:29:24 +01:00
xml::update_tag(dataNode, "noflashingmode", noflashingmode);
2020-01-01 21:29:24 +01:00
xml::update_tag(dataNode, "colourblindmode", colourblindmode);
2020-01-01 21:29:24 +01:00
xml::update_tag(dataNode, "setflipmode", graphics.setflipmode);
2020-01-01 21:29:24 +01:00
xml::update_tag(dataNode, "invincibility", map.invincibility);
2020-01-01 21:29:24 +01:00
xml::update_tag(dataNode, "slowdown", slowdown);
2020-01-01 21:29:24 +01:00
xml::update_tag(dataNode, "advanced_smoothing", (int) screen_settings->badSignal);
2020-01-01 21:29:24 +01:00
xml::update_tag(dataNode, "usingmmmmmm", music.usingmmmmmm);
xml::update_tag(dataNode, "ghostsenabled", (int) ghostsenabled);
xml::update_tag(dataNode, "skipfakeload", (int) skipfakeload);
xml::update_tag(dataNode, "disablepause", (int) disablepause);
xml::update_tag(dataNode, "disableaudiopause", (int) disableaudiopause);
xml::update_tag(dataNode, "notextoutline", (int) graphics.notextoutline);
xml::update_tag(dataNode, "translucentroomname", (int) graphics.translucentroomname);
xml::update_tag(dataNode, "over30mode", (int) over30mode);
xml::update_tag(dataNode, "inputdelay", (int) inputdelay);
xml::update_tag(
dataNode,
"glitchrunnermode",
GlitchrunnerMode_enum_to_string(GlitchrunnerMode_get())
);
2021-08-05 23:31:20 +02:00
xml::update_tag(dataNode, "showingametimer", (int) showingametimer);
xml::update_tag(dataNode, "vsync", (int) screen_settings->useVsync);
xml::update_tag(dataNode, "musicvolume", music.user_music_volume);
xml::update_tag(dataNode, "soundvolume", music.user_sound_volume);
xml::update_tag(dataNode, "separate_interact", (int) separate_interact);
// Delete all controller buttons we had previously.
// dataNode->FirstChildElement() shouldn't be NULL at this point...
// we've already added a bunch of elements
for (tinyxml2::XMLElement* element = dataNode->FirstChildElement();
element != NULL;
/* Increment code handled separately */)
{
const char* name = element->Name();
if (SDL_strcmp(name, "flipButton") == 0
|| SDL_strcmp(name, "enterButton") == 0
|| SDL_strcmp(name, "escButton") == 0
|| SDL_strcmp(name, "restartButton") == 0
|| SDL_strcmp(name, "interactButton") == 0)
{
// Can't just doc.DeleteNode(element) and then go to next,
// element->NextSiblingElement() will be NULL.
// Instead, store pointer of element we want to delete. Then
// increment `element`. And THEN delete the element.
tinyxml2::XMLElement* delete_this = element;
element = element->NextSiblingElement();
doc.DeleteNode(delete_this);
continue;
}
element = element->NextSiblingElement();
}
// Now add them
2020-01-01 21:29:24 +01:00
for (size_t i = 0; i < controllerButton_flip.size(); i += 1)
{
tinyxml2::XMLElement* msg = doc.NewElement("flipButton");
msg->LinkEndChild(doc.NewText(help.String((int) controllerButton_flip[i]).c_str()));
2020-01-01 21:29:24 +01:00
dataNode->LinkEndChild(msg);
}
for (size_t i = 0; i < controllerButton_map.size(); i += 1)
{
tinyxml2::XMLElement* msg = doc.NewElement("enterButton");
msg->LinkEndChild(doc.NewText(help.String((int) controllerButton_map[i]).c_str()));
2020-01-01 21:29:24 +01:00
dataNode->LinkEndChild(msg);
}
for (size_t i = 0; i < controllerButton_esc.size(); i += 1)
{
tinyxml2::XMLElement* msg = doc.NewElement("escButton");
msg->LinkEndChild(doc.NewText(help.String((int) controllerButton_esc[i]).c_str()));
2020-01-01 21:29:24 +01:00
dataNode->LinkEndChild(msg);
}
for (size_t i = 0; i < controllerButton_restart.size(); i += 1)
{
tinyxml2::XMLElement* msg = doc.NewElement("restartButton");
msg->LinkEndChild(doc.NewText(help.String((int) controllerButton_restart[i]).c_str()));
dataNode->LinkEndChild(msg);
}
for (size_t i = 0; i < controllerButton_interact.size(); i += 1)
{
tinyxml2::XMLElement* msg = doc.NewElement("interactButton");
msg->LinkEndChild(doc.NewText(help.String((int) controllerButton_interact[i]).c_str()));
dataNode->LinkEndChild(msg);
}
2020-01-01 21:29:24 +01:00
xml::update_tag(dataNode, "controllerSensitivity", key.sensitivity);
xml::update_tag(dataNode, "lang", loc::lang.c_str());
xml::update_tag(dataNode, "lang_set", (int) loc::lang_set);
xml::update_tag(dataNode, "english_sprites", (int) loc::english_sprites);
xml::update_tag(dataNode, "new_level_font", loc::new_level_font.c_str());
xml::update_tag(dataNode, "roomname_translator", (int) roomname_translator::enabled);
}
2020-01-01 21:29:24 +01:00
static bool settings_loaded = false;
void Game::loadsettings(struct ScreenSettings* screen_settings)
{
tinyxml2::XMLDocument doc;
tinyxml2::XMLHandle hDoc(&doc);
tinyxml2::XMLElement* dataNode;
settings_loaded = true;
if (!FILESYSTEM_loadTiXml2Document("saves/settings.vvv", doc))
{
savesettings(screen_settings);
return;
}
if (doc.Error())
{
vlog_error("Error parsing settings.vvv: %s", doc.ErrorStr());
return;
}
dataNode = hDoc
.FirstChildElement()
.FirstChildElement("Data")
.FirstChildElement()
.ToElement();
deserializesettings(dataNode, screen_settings);
}
bool Game::savesettings(void)
{
struct ScreenSettings screen_settings;
SDL_zero(screen_settings);
gameScreen.GetSettings(&screen_settings);
return savesettings(&screen_settings);
}
bool Game::savesettings(const struct ScreenSettings* screen_settings)
{
tinyxml2::XMLDocument doc;
bool already_exists;
if (!settings_loaded)
{
vlog_warn("Settings not loaded! Not writing settings.vvv.");
return false;
}
already_exists = FILESYSTEM_loadTiXml2Document("saves/settings.vvv", doc);
if (!already_exists)
{
vlog_info("No settings.vvv found. Creating new file");
}
else if (doc.Error())
{
vlog_error("Error parsing existing settings.vvv: %s", doc.ErrorStr());
vlog_info("Creating new settings.vvv");
}
xml::update_declaration(doc);
tinyxml2::XMLElement* root = xml::update_element(doc, "Settings");
xml::update_comment(root, " Settings (duplicated from unlock.vvv) ");
tinyxml2::XMLElement* dataNode = xml::update_element(root, "Data");
serializesettings(dataNode, screen_settings);
return FILESYSTEM_saveTiXml2Document("saves/settings.vvv", doc);
2020-01-01 21:29:24 +01:00
}
void Game::customstart(void)
2020-01-01 21:29:24 +01:00
{
jumpheld = true;
savex = edsavex;
savey = edsavey;
saverx = edsaverx;
savery = edsavery;
savegc = edsavegc;
savedir = edsavedir; //Worldmap Start
savepoint = 0;
gravitycontrol = savegc;
2022-12-07 00:20:48 +01:00
setstate(0);
2020-01-01 21:29:24 +01:00
deathseq = -1;
lifeseq = 0;
}
void Game::start(void)
2020-01-01 21:29:24 +01:00
{
jumpheld = true;
savex = 232;
savey = 113;
saverx = 104;
savery = 110;
savegc = 0;
savedir = 1; //Worldmap Start
savepoint = 0;
gravitycontrol = savegc;
2022-12-07 00:20:48 +01:00
setstate(0);
2020-01-01 21:29:24 +01:00
deathseq = -1;
lifeseq = 0;
if (!nocutscenes)
{
music.play(Music_PAUSE);
}
2020-01-01 21:29:24 +01:00
}
void Game::deathsequence(void)
2020-01-01 21:29:24 +01:00
{
int i;
if (supercrewmate && scmhurt)
{
i = obj.getscm();
}
else
{
i = obj.getplayer();
}
if (INBOUNDS_VEC(i, obj.entities))
{
obj.entities[i].colour = 1;
2020-01-01 21:29:24 +01:00
obj.entities[i].invis = false;
}
2020-01-01 21:29:24 +01:00
if (deathseq == 30)
{
if (nodeathmode)
{
music.fadeout();
gameoverdelay = 60;
/* Fix a bug being able to play music on the Game Over screen */
music.nicefade = false;
2020-01-01 21:29:24 +01:00
}
deathcounts++;
music.playef(Sound_CRY);
if (INBOUNDS_VEC(i, obj.entities) && !noflashingmode)
{
obj.entities[i].invis = true;
}
2020-01-01 21:29:24 +01:00
if (map.finalmode)
{
if (roomx - 41 >= 0 && roomx - 41 < 20 && roomy - 48 >= 0 && roomy - 48 < 20)
{
map.roomdeathsfinal[roomx - 41 + (20 * (roomy - 48))]++;
currentroomdeaths = map.roomdeathsfinal[roomx - 41 + (20 * (roomy - 48))];
}
2020-01-01 21:29:24 +01:00
}
else
{
if (roomx - 100 >= 0 && roomx - 100 < 20 && roomy - 100 >= 0 && roomy - 100 < 20)
{
map.roomdeaths[roomx - 100 + (20*(roomy - 100))]++;
currentroomdeaths = map.roomdeaths[roomx - 100 + (20 * (roomy - 100))];
}
2020-01-01 21:29:24 +01:00
}
}
if (INBOUNDS_VEC(i, obj.entities) && !noflashingmode)
{
if (deathseq == 25) obj.entities[i].invis = true;
if (deathseq == 20) obj.entities[i].invis = true;
if (deathseq == 16) obj.entities[i].invis = true;
if (deathseq == 14) obj.entities[i].invis = true;
if (deathseq == 12) obj.entities[i].invis = true;
if (deathseq < 10) obj.entities[i].invis = true;
}
2020-01-01 21:29:24 +01:00
if (!nodeathmode)
{
if (INBOUNDS_VEC(i, obj.entities) && deathseq <= 1) obj.entities[i].invis = false;
2020-01-01 21:29:24 +01:00
}
else
{
gameoverdelay--;
}
}
void Game::startspecial( int t )
2020-01-01 21:29:24 +01:00
{
jumpheld = true;
switch(t)
{
case 0: //Secret Lab
savex = 104;
savey = 169;
saverx = 118;
savery = 106;
savegc = 0;
savedir = 1;
break;
case 1: //Intermission 1 (any)
savex = 80;
savey = 57;
saverx = 41;
savery = 56;
savegc = 0;
savedir = 0;
break;
default:
savex = 232;
savey = 113;
saverx = 104;
savery = 110;
savegc = 0;
savedir = 1; //Worldmap Start
break;
}
savepoint = 0;
gravitycontrol = savegc;
2022-12-07 00:20:48 +01:00
setstate(0);
2020-01-01 21:29:24 +01:00
deathseq = -1;
lifeseq = 0;
}
void Game::starttrial( int t )
2020-01-01 21:29:24 +01:00
{
jumpheld = true;
switch(t)
{
case 0: //Space Station 1
savex = 200;
savey = 161;
saverx = 113;
savery = 105;
savegc = 0;
savedir = 1;
break;
case 1: //Lab
savex = 191;
savey = 33;
saverx = 102;
savery = 116;
savegc = 0;
savedir = 1;
break;
case 2: //Tower
savex = 84;
savey = 193, saverx = 108;
savery = 109;
savegc = 0;
savedir = 1;
break;
case 3: //Space Station 2
savex = 148;
savey = 38;
saverx = 112;
savery = 114;
savegc = 1;
savedir = 0;
break;
case 4: //Warp
savex = 52;
savey = 73;
saverx = 114;
savery = 101;
savegc = 0;
savedir = 1;
break;
case 5: //Final
savex = 101;
savey = 113;
saverx = 46;
savery = 54;
savegc = 0;
savedir = 1;
break;
default:
savex = 232;
savey = 113;
saverx = 104;
savery = 110;
savegc = 0;
savedir = 1; //Worldmap Start
break;
}
savepoint = 0;
gravitycontrol = savegc;
2022-12-07 00:20:48 +01:00
setstate(0);
2020-01-01 21:29:24 +01:00
deathseq = -1;
lifeseq = 0;
}
void Game::loadquick(void)
2020-01-01 21:29:24 +01:00
{
tinyxml2::XMLDocument doc;
if (!FILESYSTEM_loadTiXml2Document("saves/qsave.vvv", doc)) return;
2020-01-01 21:29:24 +01:00
readmaingamesave("qsave.vvv", doc);
}
void Game::readmaingamesave(const char* savename, tinyxml2::XMLDocument& doc)
{
tinyxml2::XMLHandle hDoc(&doc);
tinyxml2::XMLElement* pElem;
2020-01-01 21:29:24 +01:00
if (doc.Error())
2020-01-01 21:29:24 +01:00
{
vlog_error("Error parsing %s: %s", savename, doc.ErrorStr());
return;
2020-01-01 21:29:24 +01:00
}
/* Even if we want the default hardest room to be Welcome Aboard, there are pre-2.4
* saves with JUST <hardestroom> which should take priority over the coords */
hardestroom_x = -1;
hardestroom_y = -1;
hardestroom_specialname = false;
hardestroom_finalstretch = false;
for (pElem = hDoc
.FirstChildElement()
.FirstChildElement("Data")
.FirstChildElement()
.ToElement();
pElem != NULL;
pElem = pElem->NextSiblingElement())
2020-01-01 21:29:24 +01:00
{
const char* pKey = pElem->Value();
const char* pText = pElem->GetText();
2020-01-01 21:29:24 +01:00
if(pText == NULL)
{
pText = "";
}
LOAD_ARRAY_RENAME(worldmap, map.explored)
2020-01-01 21:29:24 +01:00
LOAD_ARRAY_RENAME(flags, obj.flags)
2020-01-01 21:29:24 +01:00
LOAD_ARRAY(crewstats)
2020-01-01 21:29:24 +01:00
LOAD_ARRAY_RENAME(collect, obj.collect)
2020-01-01 21:29:24 +01:00
if (SDL_strcmp(pKey, "finalmode") == 0)
2020-01-01 21:29:24 +01:00
{
map.finalmode = help.Int(pText);
2020-01-01 21:29:24 +01:00
}
if (SDL_strcmp(pKey, "finalstretch") == 0)
2020-01-01 21:29:24 +01:00
{
map.finalstretch = help.Int(pText);
2020-01-01 21:29:24 +01:00
}
if (SDL_strcmp(pKey, "savex") == 0)
2020-01-01 21:29:24 +01:00
{
savex = help.Int(pText);
2020-01-01 21:29:24 +01:00
}
else if (SDL_strcmp(pKey, "savey") == 0)
2020-01-01 21:29:24 +01:00
{
savey = help.Int(pText);
2020-01-01 21:29:24 +01:00
}
else if (SDL_strcmp(pKey, "saverx") == 0)
2020-01-01 21:29:24 +01:00
{
saverx = help.Int(pText);
2020-01-01 21:29:24 +01:00
}
else if (SDL_strcmp(pKey, "savery") == 0)
2020-01-01 21:29:24 +01:00
{
savery = help.Int(pText);
2020-01-01 21:29:24 +01:00
}
else if (SDL_strcmp(pKey, "savegc") == 0)
2020-01-01 21:29:24 +01:00
{
savegc = help.Int(pText);
2020-01-01 21:29:24 +01:00
}
else if (SDL_strcmp(pKey, "savedir") == 0)
2020-01-01 21:29:24 +01:00
{
savedir= help.Int(pText);
2020-01-01 21:29:24 +01:00
}
else if (SDL_strcmp(pKey, "savepoint") == 0)
2020-01-01 21:29:24 +01:00
{
savepoint = help.Int(pText);
2020-01-01 21:29:24 +01:00
}
else if (SDL_strcmp(pKey, "companion") == 0)
2020-01-01 21:29:24 +01:00
{
companion = help.Int(pText);
2020-01-01 21:29:24 +01:00
}
else if (SDL_strcmp(pKey, "lastsaved") == 0)
2020-01-01 21:29:24 +01:00
{
lastsaved = help.Int(pText);
2020-01-01 21:29:24 +01:00
}
else if (SDL_strcmp(pKey, "teleportscript") == 0)
2020-01-01 21:29:24 +01:00
{
teleportscript = pText;
}
else if (SDL_strcmp(pKey, "supercrewmate") == 0)
2020-01-01 21:29:24 +01:00
{
supercrewmate = help.Int(pText);
2020-01-01 21:29:24 +01:00
}
else if (SDL_strcmp(pKey, "scmprogress") == 0)
2020-01-01 21:29:24 +01:00
{
scmprogress = help.Int(pText);
2020-01-01 21:29:24 +01:00
}
else if (SDL_strcmp(pKey, "frames") == 0)
2020-01-01 21:29:24 +01:00
{
frames = help.Int(pText);
2020-01-01 21:29:24 +01:00
}
else if (SDL_strcmp(pKey, "seconds") == 0)
2020-01-01 21:29:24 +01:00
{
seconds = help.Int(pText);
2020-01-01 21:29:24 +01:00
}
else if (SDL_strcmp(pKey, "minutes") == 0)
2020-01-01 21:29:24 +01:00
{
minutes = help.Int(pText);
2020-01-01 21:29:24 +01:00
}
else if (SDL_strcmp(pKey, "hours") == 0)
2020-01-01 21:29:24 +01:00
{
hours = help.Int(pText);
2020-01-01 21:29:24 +01:00
}
else if (SDL_strcmp(pKey, "deathcounts") == 0)
2020-01-01 21:29:24 +01:00
{
deathcounts = help.Int(pText);
2020-01-01 21:29:24 +01:00
}
else if (SDL_strcmp(pKey, "totalflips") == 0)
2020-01-01 21:29:24 +01:00
{
totalflips = help.Int(pText);
2020-01-01 21:29:24 +01:00
}
else if (SDL_strcmp(pKey, "hardestroom") == 0)
2020-01-01 21:29:24 +01:00
{
hardestroom = pText;
2020-01-01 21:29:24 +01:00
}
else if (SDL_strcmp(pKey, "hardestroomdeaths") == 0)
2020-01-01 21:29:24 +01:00
{
hardestroomdeaths = help.Int(pText);
2020-01-01 21:29:24 +01:00
}
else if (SDL_strcmp(pKey, "hardestroom_x") == 0)
{
hardestroom_x = help.Int(pText);
}
else if (SDL_strcmp(pKey, "hardestroom_y") == 0)
{
hardestroom_y = help.Int(pText);
}
else if (SDL_strcmp(pKey, "hardestroom_specialname") == 0)
{
hardestroom_specialname = help.Int(pText);
}
else if (SDL_strcmp(pKey, "hardestroom_finalstretch") == 0)
{
hardestroom_finalstretch = help.Int(pText);
}
else if (SDL_strcmp(pKey, "currentsong") == 0)
2020-01-01 21:29:24 +01:00
{
int song = help.Int(pText);
if (song != -1)
{
music.play(song);
}
2020-01-01 21:29:24 +01:00
}
else if (SDL_strcmp(pKey, "showtargets") == 0)
{
map.showtargets = help.Int(pText);
}
2020-01-01 21:29:24 +01:00
}
if (map.finalmode)
{
map.final_colormode = false;
map.final_mapcol = 0;
map.final_colorframe = 0;
}
if (map.finalstretch)
{
map.finalstretch = true;
map.final_colormode = true;
map.final_mapcol = 0;
map.final_colorframe = 1;
}
map.showteleporters = true;
if(obj.flags[12]) map.showtargets = true;
if (obj.flags[42]) map.showtrinkets = true;
2020-01-01 21:29:24 +01:00
}
void Game::customloadquick(const std::string& savfile)
2020-01-01 21:29:24 +01:00
{
tinyxml2::XMLDocument doc;
tinyxml2::XMLHandle hDoc(&doc);
tinyxml2::XMLElement* pElem;
std::string levelfile;
if (cliplaytest)
{
savex = playx;
savey = playy;
saverx = playrx;
savery = playry;
savegc = playgc;
if (playmusic > -1)
{
music.play(playmusic);
}
return;
}
levelfile = savfile.substr(7);
if (!FILESYSTEM_loadTiXml2Document(("saves/"+levelfile+".vvv").c_str(), doc))
2020-01-01 21:29:24 +01:00
{
vlog_error("%s.vvv not found", levelfile.c_str());
return;
}
2020-01-01 21:29:24 +01:00
if (doc.Error())
{
vlog_error("Error parsing %s.vvv: %s", levelfile.c_str(), doc.ErrorStr());
return;
2020-01-01 21:29:24 +01:00
}
// Like readmaingamesave(...), old saves have just <hardestroom>
hardestroom_x = -1;
hardestroom_y = -1;
hardestroom_specialname = false;
hardestroom_finalstretch = false;
for (pElem = hDoc
.FirstChildElement()
.FirstChildElement("Data")
.FirstChildElement()
.ToElement();
pElem != NULL;
pElem = pElem->NextSiblingElement())
2020-01-01 21:29:24 +01:00
{
const char* pKey = pElem->Value();
2020-01-01 21:29:24 +01:00
const char* pText = pElem->GetText() ;
if(pText == NULL)
{
pText = "";
}
LOAD_ARRAY_RENAME(worldmap, map.explored)
2020-01-01 21:29:24 +01:00
LOAD_ARRAY_RENAME(flags, obj.flags)
2020-01-01 21:29:24 +01:00
LOAD_ARRAY_RENAME(moods, obj.customcrewmoods)
2020-01-01 21:29:24 +01:00
LOAD_ARRAY(crewstats)
2020-01-01 21:29:24 +01:00
LOAD_ARRAY_RENAME(collect, obj.collect)
2020-01-01 21:29:24 +01:00
LOAD_ARRAY_RENAME(customcollect, obj.customcollect)
2020-01-01 21:29:24 +01:00
if (SDL_strcmp(pKey, "finalmode") == 0)
2020-01-01 21:29:24 +01:00
{
map.finalmode = help.Int(pText);
2020-01-01 21:29:24 +01:00
}
if (SDL_strcmp(pKey, "finalstretch") == 0)
2020-01-01 21:29:24 +01:00
{
map.finalstretch = help.Int(pText);
2020-01-01 21:29:24 +01:00
}
if (map.finalmode)
{
map.final_colormode = false;
map.final_mapcol = 0;
map.final_colorframe = 0;
}
if (map.finalstretch)
{
map.finalstretch = true;
map.final_colormode = true;
map.final_mapcol = 0;
map.final_colorframe = 1;
}
if (SDL_strcmp(pKey, "savex") == 0)
2020-01-01 21:29:24 +01:00
{
savex = help.Int(pText);
2020-01-01 21:29:24 +01:00
}
else if (SDL_strcmp(pKey, "savey") == 0)
2020-01-01 21:29:24 +01:00
{
savey = help.Int(pText);
2020-01-01 21:29:24 +01:00
}
else if (SDL_strcmp(pKey, "saverx") == 0)
2020-01-01 21:29:24 +01:00
{
saverx = help.Int(pText);
2020-01-01 21:29:24 +01:00
}
else if (SDL_strcmp(pKey, "savery") == 0)
2020-01-01 21:29:24 +01:00
{
savery = help.Int(pText);
2020-01-01 21:29:24 +01:00
}
else if (SDL_strcmp(pKey, "savegc") == 0)
2020-01-01 21:29:24 +01:00
{
savegc = help.Int(pText);
2020-01-01 21:29:24 +01:00
}
else if (SDL_strcmp(pKey, "savedir") == 0)
2020-01-01 21:29:24 +01:00
{
savedir= help.Int(pText);
2020-01-01 21:29:24 +01:00
}
else if (SDL_strcmp(pKey, "savepoint") == 0)
2020-01-01 21:29:24 +01:00
{
savepoint = help.Int(pText);
2020-01-01 21:29:24 +01:00
}
else if (SDL_strcmp(pKey, "savecolour") == 0)
{
savecolour = help.Int(pText);
}
else if (SDL_strcmp(pKey, "companion") == 0)
2020-01-01 21:29:24 +01:00
{
companion = help.Int(pText);
2020-01-01 21:29:24 +01:00
}
else if (SDL_strcmp(pKey, "lastsaved") == 0)
2020-01-01 21:29:24 +01:00
{
lastsaved = help.Int(pText);
2020-01-01 21:29:24 +01:00
}
else if (SDL_strcmp(pKey, "teleportscript") == 0)
2020-01-01 21:29:24 +01:00
{
teleportscript = pText;
}
else if (SDL_strcmp(pKey, "supercrewmate") == 0)
2020-01-01 21:29:24 +01:00
{
supercrewmate = help.Int(pText);
2020-01-01 21:29:24 +01:00
}
else if (SDL_strcmp(pKey, "scmprogress") == 0)
2020-01-01 21:29:24 +01:00
{
scmprogress = help.Int(pText);
2020-01-01 21:29:24 +01:00
}
else if (SDL_strcmp(pKey, "frames") == 0)
2020-01-01 21:29:24 +01:00
{
frames = help.Int(pText);
2020-01-01 21:29:24 +01:00
}
else if (SDL_strcmp(pKey, "seconds") == 0)
2020-01-01 21:29:24 +01:00
{
seconds = help.Int(pText);
2020-01-01 21:29:24 +01:00
}
else if (SDL_strcmp(pKey, "minutes") == 0)
2020-01-01 21:29:24 +01:00
{
minutes = help.Int(pText);
2020-01-01 21:29:24 +01:00
}
else if (SDL_strcmp(pKey, "hours") == 0)
2020-01-01 21:29:24 +01:00
{
hours = help.Int(pText);
2020-01-01 21:29:24 +01:00
}
else if (SDL_strcmp(pKey, "deathcounts") == 0)
2020-01-01 21:29:24 +01:00
{
deathcounts = help.Int(pText);
2020-01-01 21:29:24 +01:00
}
else if (SDL_strcmp(pKey, "totalflips") == 0)
2020-01-01 21:29:24 +01:00
{
totalflips = help.Int(pText);
2020-01-01 21:29:24 +01:00
}
else if (SDL_strcmp(pKey, "hardestroom") == 0)
2020-01-01 21:29:24 +01:00
{
hardestroom = pText;
2020-01-01 21:29:24 +01:00
}
else if (SDL_strcmp(pKey, "hardestroomdeaths") == 0)
2020-01-01 21:29:24 +01:00
{
hardestroomdeaths = help.Int(pText);
2020-01-01 21:29:24 +01:00
}
else if (SDL_strcmp(pKey, "hardestroom_x") == 0)
{
hardestroom_x = help.Int(pText);
}
else if (SDL_strcmp(pKey, "hardestroom_y") == 0)
{
hardestroom_y = help.Int(pText);
}
else if (SDL_strcmp(pKey, "hardestroom_specialname") == 0)
{
hardestroom_specialname = help.Int(pText);
}
else if (SDL_strcmp(pKey, "hardestroom_finalstretch") == 0)
{
hardestroom_finalstretch = help.Int(pText);
}
else if (SDL_strcmp(pKey, "currentsong") == 0)
2020-01-01 21:29:24 +01:00
{
int song = help.Int(pText);
if (song != -1)
{
music.play(song);
}
2020-01-01 21:29:24 +01:00
}
else if (SDL_strcmp(pKey, "lang_custom") == 0)
{
loc::lang_custom = pText;
if (pText[0] != '\0')
{
loc::loadtext_custom(NULL);
}
}
else if (SDL_strcmp(pKey, "showminimap") == 0)
2020-01-01 21:29:24 +01:00
{
map.customshowmm = help.Int(pText);
2020-01-01 21:29:24 +01:00
}
else if (SDL_strcmp(pKey, "disabletemporaryaudiopause") == 0)
{
disabletemporaryaudiopause = help.Int(pText);
}
else if (SDL_strcmp(pKey, "showtrinkets") == 0)
{
map.showtrinkets = help.Int(pText);
}
else if (SDL_strcmp(pKey, "roomname") == 0)
{
map.setroomname(pText);
map.roomnameset = true;
map.roomname_special = true;
}
2020-01-01 21:29:24 +01:00
}
}
static void loadthissummary(
const char* filename,
struct Game::Summary* summary,
tinyxml2::XMLDocument& doc
) {
tinyxml2::XMLHandle hDoc(&doc);
tinyxml2::XMLElement* pElem;
if (doc.Error())
2020-01-01 21:29:24 +01:00
{
vlog_error("Error parsing %s: %s", filename, doc.ErrorStr());
return;
2020-01-01 21:29:24 +01:00
}
summary->exists = true;
for (pElem = hDoc
.FirstChildElement()
.FirstChildElement("Data")
.FirstChildElement()
.ToElement();
pElem != NULL;
pElem = pElem->NextSiblingElement())
2020-01-01 21:29:24 +01:00
{
const char* pKey = pElem->Value();
const char* pText = pElem->GetText();
if (pText == NULL)
{
pText = "";
}
2020-01-01 21:29:24 +01:00
if (SDL_strcmp(pKey, "seconds") == 0)
{
summary->seconds = help.Int(pText);
}
else if (SDL_strcmp(pKey, "minutes") == 0)
{
summary->minutes = help.Int(pText);
}
else if (SDL_strcmp(pKey, "hours") == 0)
{
summary->hours = help.Int(pText);
}
else if (SDL_strcmp(pKey, "saverx") == 0)
{
summary->saverx = help.Int(pText);
}
else if (SDL_strcmp(pKey, "savery") == 0)
{
summary->savery = help.Int(pText);
}
else if (SDL_strcmp(pKey, "trinkets") == 0)
{
summary->trinkets = help.Int(pText);
2020-01-01 21:29:24 +01:00
}
LOAD_ARRAY_RENAME(crewstats, summary->crewstats)
}
}
void Game::loadsummary(void)
{
tinyxml2::XMLDocument doc;
2020-01-01 21:29:24 +01:00
SDL_zero(last_telesave);
SDL_zero(last_quicksave);
2020-01-01 21:29:24 +01:00
if (FILESYSTEM_loadTiXml2Document("saves/tsave.vvv", doc))
{
loadthissummary("tsave.vvv", &last_telesave, doc);
2020-01-01 21:29:24 +01:00
}
if (FILESYSTEM_loadTiXml2Document("saves/qsave.vvv", doc))
2020-01-01 21:29:24 +01:00
{
loadthissummary("qsave.vvv", &last_quicksave, doc);
2020-01-01 21:29:24 +01:00
}
}
void Game::initteleportermode(void)
2020-01-01 21:29:24 +01:00
{
//Set the teleporter variable to the right position!
teleport_to_teleporter = 0;
for (size_t i = 0; i < map.teleporters.size(); i++)
2020-01-01 21:29:24 +01:00
{
if (roomx == map.teleporters[i].x + 100 && roomy == map.teleporters[i].y + 100)
{
teleport_to_teleporter = i;
}
}
}
bool Game::savetele(void)
2020-01-01 21:29:24 +01:00
{
if (map.custommode || inspecial())
{
//Don't trash save data!
return false;
}
tinyxml2::XMLDocument doc;
bool already_exists = FILESYSTEM_loadTiXml2Document("saves/tsave.vvv", doc);
if (!already_exists)
{
vlog_info("No tsave.vvv found. Creating new file");
}
else if (doc.Error())
{
vlog_error("Error parsing existing tsave.vvv: %s", doc.ErrorStr());
vlog_info("Creating new tsave.vvv");
}
last_telesave = writemaingamesave(doc);
2020-01-01 21:29:24 +01:00
if(!FILESYSTEM_saveTiXml2Document("saves/tsave.vvv", doc))
2020-01-01 21:29:24 +01:00
{
vlog_error("Could Not Save game!");
vlog_error("Failed: %s%s", saveFilePath, "tsave.vvv");
return false;
2020-01-01 21:29:24 +01:00
}
vlog_info("Game saved");
return true;
}
2020-01-01 21:29:24 +01:00
bool Game::savequick(void)
{
if (map.custommode || inspecial())
2020-01-01 21:29:24 +01:00
{
//Don't trash save data!
2020-11-04 03:45:33 +01:00
return false;
2020-01-01 21:29:24 +01:00
}
tinyxml2::XMLDocument doc;
bool already_exists = FILESYSTEM_loadTiXml2Document("saves/qsave.vvv", doc);
if (!already_exists)
{
vlog_info("No qsave.vvv found. Creating new file");
}
else if (doc.Error())
{
vlog_error("Error parsing existing qsave.vvv: %s", doc.ErrorStr());
vlog_info("Creating new qsave.vvv");
}
last_quicksave = writemaingamesave(doc);
2020-01-01 21:29:24 +01:00
2020-11-04 03:45:33 +01:00
if(!FILESYSTEM_saveTiXml2Document("saves/qsave.vvv", doc))
2020-01-01 21:29:24 +01:00
{
vlog_error("Could Not Save game!");
vlog_error("Failed: %s%s", saveFilePath, "qsave.vvv");
2020-11-04 03:45:33 +01:00
return false;
2020-01-01 21:29:24 +01:00
}
vlog_info("Game saved");
2020-11-04 03:45:33 +01:00
return true;
}
2020-01-01 21:29:24 +01:00
// Returns summary of save
struct Game::Summary Game::writemaingamesave(tinyxml2::XMLDocument& doc)
2020-01-01 21:29:24 +01:00
{
//TODO make this code a bit cleaner.
struct Game::Summary summary;
SDL_zero(summary);
if (map.custommode || inspecial())
{
//Don't trash save data!
return summary;
}
xml::update_declaration(doc);
2020-01-01 21:29:24 +01:00
tinyxml2::XMLElement * root = xml::update_element(doc, "Save");
2020-01-01 21:29:24 +01:00
xml::update_comment(root, " Save file " );
2020-01-01 21:29:24 +01:00
tinyxml2::XMLElement * msgs = xml::update_element(root, "Data");
2020-01-01 21:29:24 +01:00
//Flags, map and stats
std::string mapExplored;
for(size_t i = 0; i < SDL_arraysize(map.explored); i++ )
2020-01-01 21:29:24 +01:00
{
mapExplored += help.String(map.explored[i]) + ",";
2020-01-01 21:29:24 +01:00
}
xml::update_tag(msgs, "worldmap", mapExplored.c_str());
2020-01-01 21:29:24 +01:00
std::string flags;
for(size_t i = 0; i < SDL_arraysize(obj.flags); i++ )
2020-01-01 21:29:24 +01:00
{
flags += help.String((int) obj.flags[i]) + ",";
2020-01-01 21:29:24 +01:00
}
xml::update_tag(msgs, "flags", flags.c_str());
2020-01-01 21:29:24 +01:00
std::string crewstatsString;
for(size_t i = 0; i < SDL_arraysize(crewstats); i++ )
2020-01-01 21:29:24 +01:00
{
crewstatsString += help.String(crewstats[i]) + ",";
2020-01-01 21:29:24 +01:00
}
xml::update_tag(msgs, "crewstats", crewstatsString.c_str());
2020-01-01 21:29:24 +01:00
std::string collect;
for(size_t i = 0; i < SDL_arraysize(obj.collect); i++ )
2020-01-01 21:29:24 +01:00
{
collect += help.String((int) obj.collect[i]) + ",";
2020-01-01 21:29:24 +01:00
}
xml::update_tag(msgs, "collect", collect.c_str());
2020-01-01 21:29:24 +01:00
//Position
xml::update_tag(msgs, "savex", savex);
2020-01-01 21:29:24 +01:00
xml::update_tag(msgs, "savey", savey);
2020-01-01 21:29:24 +01:00
xml::update_tag(msgs, "saverx", saverx);
2020-01-01 21:29:24 +01:00
xml::update_tag(msgs, "savery", savery);
2020-01-01 21:29:24 +01:00
xml::update_tag(msgs, "savegc", savegc);
2020-01-01 21:29:24 +01:00
xml::update_tag(msgs, "savedir", savedir);
2020-01-01 21:29:24 +01:00
xml::update_tag(msgs, "savepoint", savepoint);
2020-01-01 21:29:24 +01:00
int n_trinkets = trinkets();
xml::update_tag(msgs, "trinkets", n_trinkets);
2020-01-01 21:29:24 +01:00
//Special stats
if (music.nicefade)
2020-01-01 21:29:24 +01:00
{
xml::update_tag(msgs, "currentsong", music.nicechange);
2020-01-01 21:29:24 +01:00
}
else
{
xml::update_tag(msgs, "currentsong", music.currentsong);
2020-01-01 21:29:24 +01:00
}
xml::update_tag(msgs, "showtargets", (int) map.showtargets);
xml::update_tag(msgs, "teleportscript", teleportscript.c_str());
xml::update_tag(msgs, "companion", companion);
2020-01-01 21:29:24 +01:00
xml::update_tag(msgs, "lastsaved", lastsaved);
xml::update_tag(msgs, "supercrewmate", (int) supercrewmate);
2020-01-01 21:29:24 +01:00
xml::update_tag(msgs, "scmprogress", scmprogress);
2020-01-01 21:29:24 +01:00
xml::update_tag(msgs, "frames", frames);
xml::update_tag(msgs, "seconds", seconds);
2020-01-01 21:29:24 +01:00
xml::update_tag(msgs, "minutes", minutes);
xml::update_tag(msgs, "hours", hours);
2020-01-01 21:29:24 +01:00
xml::update_tag(msgs, "deathcounts", deathcounts);
xml::update_tag(msgs, "totalflips", totalflips);
2020-01-01 21:29:24 +01:00
xml::update_tag(msgs, "hardestroom", hardestroom.c_str());
xml::update_tag(msgs, "hardestroomdeaths", hardestroomdeaths);
xml::update_tag(msgs, "hardestroom_x", hardestroom_x);
xml::update_tag(msgs, "hardestroom_y", hardestroom_y);
xml::update_tag(msgs, "hardestroom_specialname", (int) hardestroom_specialname);
xml::update_tag(msgs, "hardestroom_finalstretch", (int) hardestroom_finalstretch);
2020-01-01 21:29:24 +01:00
xml::update_tag(msgs, "finalmode", (int) map.finalmode);
xml::update_tag(msgs, "finalstretch", (int) map.finalstretch);
std::string legacy_summary = std::string(map.currentarea(saverx, savery)) + ", " + timestring();
xml::update_tag(msgs, "summary", legacy_summary.c_str());
summary.exists = true;
summary.seconds = seconds;
summary.minutes = minutes;
summary.hours = hours;
summary.saverx = saverx;
summary.savery = savery;
summary.trinkets = n_trinkets;
SDL_memcpy(summary.crewstats, crewstats, sizeof(summary.crewstats));
2020-01-01 21:29:24 +01:00
return summary;
2020-01-01 21:29:24 +01:00
}
bool Game::customsavequick(const std::string& savfile)
2020-01-01 21:29:24 +01:00
{
const std::string levelfile = savfile.substr(7);
tinyxml2::XMLDocument doc;
bool already_exists = FILESYSTEM_loadTiXml2Document(("saves/" + levelfile + ".vvv").c_str(), doc);
if (!already_exists)
{
vlog_info("No %s.vvv found. Creating new file", levelfile.c_str());
}
else if (doc.Error())
{
vlog_error("Error parsing existing %s.vvv: %s", levelfile.c_str(), doc.ErrorStr());
vlog_info("Creating new %s.vvv", levelfile.c_str());
}
xml::update_declaration(doc);
2020-01-01 21:29:24 +01:00
tinyxml2::XMLElement * root = xml::update_element(doc, "Save");
2020-01-01 21:29:24 +01:00
xml::update_comment(root, " Save file ");
2020-01-01 21:29:24 +01:00
tinyxml2::XMLElement * msgs = xml::update_element(root, "Data");
2020-01-01 21:29:24 +01:00
//Flags, map and stats
std::string mapExplored;
for(size_t i = 0; i < SDL_arraysize(map.explored); i++ )
2020-01-01 21:29:24 +01:00
{
mapExplored += help.String(map.explored[i]) + ",";
2020-01-01 21:29:24 +01:00
}
xml::update_tag(msgs, "worldmap", mapExplored.c_str());
2020-01-01 21:29:24 +01:00
std::string flags;
for(size_t i = 0; i < SDL_arraysize(obj.flags); i++ )
2020-01-01 21:29:24 +01:00
{
flags += help.String((int) obj.flags[i]) + ",";
2020-01-01 21:29:24 +01:00
}
xml::update_tag(msgs, "flags", flags.c_str());
2020-01-01 21:29:24 +01:00
std::string moods;
for(size_t i = 0; i < SDL_arraysize(obj.customcrewmoods); i++ )
2020-01-01 21:29:24 +01:00
{
moods += help.String(obj.customcrewmoods[i]) + ",";
2020-01-01 21:29:24 +01:00
}
xml::update_tag(msgs, "moods", moods.c_str());
2020-01-01 21:29:24 +01:00
std::string crewstatsString;
for(size_t i = 0; i < SDL_arraysize(crewstats); i++ )
2020-01-01 21:29:24 +01:00
{
crewstatsString += help.String(crewstats[i]) + ",";
2020-01-01 21:29:24 +01:00
}
xml::update_tag(msgs, "crewstats", crewstatsString.c_str());
2020-01-01 21:29:24 +01:00
std::string collect;
for(size_t i = 0; i < SDL_arraysize(obj.collect); i++ )
2020-01-01 21:29:24 +01:00
{
collect += help.String((int) obj.collect[i]) + ",";
2020-01-01 21:29:24 +01:00
}
xml::update_tag(msgs, "collect", collect.c_str());
2020-01-01 21:29:24 +01:00
std::string customcollect;
for(size_t i = 0; i < SDL_arraysize(obj.customcollect); i++ )
2020-01-01 21:29:24 +01:00
{
customcollect += help.String((int) obj.customcollect[i]) + ",";
2020-01-01 21:29:24 +01:00
}
xml::update_tag(msgs, "customcollect", customcollect.c_str());
2020-01-01 21:29:24 +01:00
//Position
xml::update_tag(msgs, "savex", savex);
2020-01-01 21:29:24 +01:00
xml::update_tag(msgs, "savey", savey);
2020-01-01 21:29:24 +01:00
xml::update_tag(msgs, "saverx", saverx);
2020-01-01 21:29:24 +01:00
xml::update_tag(msgs, "savery", savery);
2020-01-01 21:29:24 +01:00
xml::update_tag(msgs, "savegc", savegc);
2020-01-01 21:29:24 +01:00
xml::update_tag(msgs, "savedir", savedir);
2020-01-01 21:29:24 +01:00
xml::update_tag(msgs, "savepoint", savepoint);
2020-01-01 21:29:24 +01:00
xml::update_tag(msgs, "savecolour", savecolour);
xml::update_tag(msgs, "trinkets", trinkets());
2020-01-01 21:29:24 +01:00
xml::update_tag(msgs, "crewmates", crewmates());
2020-01-01 21:29:24 +01:00
//Special stats
if (music.nicefade)
2020-01-01 21:29:24 +01:00
{
xml::update_tag(msgs, "currentsong", music.nicechange );
2020-01-01 21:29:24 +01:00
}
else
{
xml::update_tag(msgs, "currentsong", music.currentsong);
2020-01-01 21:29:24 +01:00
}
xml::update_tag(msgs, "lang_custom", loc::lang_custom.c_str());
xml::update_tag(msgs, "teleportscript", teleportscript.c_str());
xml::update_tag(msgs, "companion", companion);
xml::update_tag(msgs, "lastsaved", lastsaved);
xml::update_tag(msgs, "supercrewmate", (int) supercrewmate);
xml::update_tag(msgs, "scmprogress", scmprogress);
xml::update_tag(msgs, "frames", frames);
xml::update_tag(msgs, "seconds", seconds);
xml::update_tag(msgs, "minutes", minutes);
xml::update_tag(msgs, "hours", hours);
xml::update_tag(msgs, "deathcounts", deathcounts);
xml::update_tag(msgs, "totalflips", totalflips);
xml::update_tag(msgs, "hardestroom", hardestroom.c_str());
xml::update_tag(msgs, "hardestroomdeaths", hardestroomdeaths);
xml::update_tag(msgs, "hardestroom_x", hardestroom_x);
xml::update_tag(msgs, "hardestroom_y", hardestroom_y);
xml::update_tag(msgs, "hardestroom_specialname", (int) hardestroom_specialname);
xml::update_tag(msgs, "hardestroom_finalstretch", (int) hardestroom_finalstretch);
xml::update_tag(msgs, "showminimap", (int) map.customshowmm);
xml::update_tag(msgs, "disabletemporaryaudiopause", (int) disabletemporaryaudiopause);
xml::update_tag(msgs, "showtrinkets", (int) map.showtrinkets);
if (map.roomnameset)
{
xml::update_tag(msgs, "roomname", map.roomname);
}
else
{
// If there's roomname tags, remove them. There will probably only always be one, but just in case...
tinyxml2::XMLElement* element;
while ((element = msgs->FirstChildElement("roomname")) != NULL)
{
doc.DeleteNode(element);
}
}
std::string legacy_summary = customleveltitle + ", " + timestring();
xml::update_tag(msgs, "summary", legacy_summary.c_str());
2020-01-01 21:29:24 +01:00
2020-11-04 03:45:33 +01:00
if(!FILESYSTEM_saveTiXml2Document(("saves/"+levelfile+".vvv").c_str(), doc))
2020-01-01 21:29:24 +01:00
{
vlog_error("Could Not Save game!");
vlog_error("Failed: %s%s%s", saveFilePath, levelfile.c_str(), ".vvv");
2020-11-04 03:45:33 +01:00
return false;
2020-01-01 21:29:24 +01:00
}
vlog_info("Game saved");
2020-11-04 03:45:33 +01:00
return true;
2020-01-01 21:29:24 +01:00
}
void Game::loadtele(void)
2020-01-01 21:29:24 +01:00
{
tinyxml2::XMLDocument doc;
if (!FILESYSTEM_loadTiXml2Document("saves/tsave.vvv", doc)) return;
2020-01-01 21:29:24 +01:00
readmaingamesave("tsave.vvv", doc);
2020-01-01 21:29:24 +01:00
}
std::string Game::unrescued(void)
2020-01-01 21:29:24 +01:00
{
//Randomly return the name of an unrescued crewmate
//Localization is handled with regular cutscene dialogue
2020-01-01 21:29:24 +01:00
if (fRandom() * 100 > 50)
{
if (!crewstats[5]) return "Victoria";
if (!crewstats[2]) return "Vitellary";
if (!crewstats[4]) return "Verdigris";
if (!crewstats[3]) return "Vermilion";
}
else
{
if (fRandom() * 100 > 50)
{
if (!crewstats[2]) return "Vitellary";
if (!crewstats[4]) return "Verdigris";
if (!crewstats[3]) return "Vermilion";
if (!crewstats[5]) return "Victoria";
}
else
{
if (!crewstats[4]) return "Verdigris";
if (!crewstats[3]) return "Vermilion";
if (!crewstats[5]) return "Victoria";
if (!crewstats[2]) return "Vitellary";
}
}
return "you";
}
void Game::gameclock(void)
2020-01-01 21:29:24 +01:00
{
if (timetrialcountdown > 0)
{
return;
}
frames++;
if (frames >= 30)
{
frames -= 30;
seconds++;
if (seconds >= 60)
{
seconds -= 60;
minutes++;
if (minutes >= 60)
{
minutes -= 60;
hours++;
}
}
}
2020-01-01 21:29:24 +01:00
}
std::string Game::giventimestring( int hrs, int min, int sec )
2020-01-01 21:29:24 +01:00
{
Simplify time formatting functions Here's my notes on all the existing functions and what kind of time formats they output: - Game::giventimestring(int hrs, int min, int sec) H:MM:SS MM:SS - Game::timestring() // uses game.hours/minutes/seconds H:MM:SS MM:SS - Game::partimestring() // uses game.timetrialpar (seconds) MM:SS - Game::resulttimestring() // uses game.timetrialresulttime (sec) + timetrialresultframes (1/30s) MM:SS.CC - Game::timetstring(int t) // t = seconds MM:SS - Game::timestringcenti(char* buffer, const size_t buffer_size) // uses game.hours/minutes/seconds/frames H:MM:SS.CC MM:SS.CC - UtilityClass::timestring(int t) // t = frames, 30 frames = 1 second S:CC M:SS:CC This is kind of a mess, and there's a lot of functions that do the same thing except using different variables. For localization, I also want translators to be able to localize all these time formats - many languages use the decimal comma instead of the decimal point (12:34,56) maybe some languages really prefer something like 1時02分11秒44瞬... Which I don't know to be correct, but it's good to be prepared for it and not restrict translators arbitrarily to only changing ":" and "." when we can start making the system better in the first place. I added a new function, UtilityClass::format_time. This is the place where all time formats come together, given the number of seconds and optionally frames. I have simplified the above-mentioned functions somewhat, but I haven't given them a complete refactor or renaming - I mainly made sure that they all use the same backend so I can make the formats consistent and properly localizable. (And before we start shoving more temporary char buffers everywhere just to get rid of the std::string's, maybe we need to think of a globally used working buffer of size SCREEN_WIDTH_CHARS+1, as a register of sorts, for when any line of text needs to be made or processed, then printed, and then goes unused. Maybe help.textrow, or something like that.) As for this commit, the available time formats are now more consistent and changed a little in some places. Leading zeroes for the first unit are now no longer included, time trial results and the Super Gravitron can now display hours when they went to 60 minutes before, and we now always use .CC instead of :CC. These are the formats: - H:MM:SS - H:MM:SS.CC - M:SS - M:SS.CC - S.CC (only used when always_minutes=false, for the Gravitrons) Here's what changes to the current functions: - Game::partimestring() is removed - it was used in two places, and could be replaced by game.timetstring(game.timetrialpar) - Game::giventimestring(h,m,s) and Game::timestring() are now wrappers for the other functions - The four remaining functions (Game::resulttimestring(), Game::timetstring(t), Game::timestringcenti(buffer, buffer_size) and UtilityClass::timestring(t)) are now wrappers for the "central function", UtilityClass::format_time. - UtilityClass::twodigits(int t) is now unused so it's also removed. - I also added int UtilityClass::hms_to_seconds(int h, int m, int s)
2021-12-25 17:13:46 +01:00
return timetstring(help.hms_to_seconds(hrs, min, sec));
2020-01-01 21:29:24 +01:00
}
std::string Game::timestring(void)
2020-01-01 21:29:24 +01:00
{
Simplify time formatting functions Here's my notes on all the existing functions and what kind of time formats they output: - Game::giventimestring(int hrs, int min, int sec) H:MM:SS MM:SS - Game::timestring() // uses game.hours/minutes/seconds H:MM:SS MM:SS - Game::partimestring() // uses game.timetrialpar (seconds) MM:SS - Game::resulttimestring() // uses game.timetrialresulttime (sec) + timetrialresultframes (1/30s) MM:SS.CC - Game::timetstring(int t) // t = seconds MM:SS - Game::timestringcenti(char* buffer, const size_t buffer_size) // uses game.hours/minutes/seconds/frames H:MM:SS.CC MM:SS.CC - UtilityClass::timestring(int t) // t = frames, 30 frames = 1 second S:CC M:SS:CC This is kind of a mess, and there's a lot of functions that do the same thing except using different variables. For localization, I also want translators to be able to localize all these time formats - many languages use the decimal comma instead of the decimal point (12:34,56) maybe some languages really prefer something like 1時02分11秒44瞬... Which I don't know to be correct, but it's good to be prepared for it and not restrict translators arbitrarily to only changing ":" and "." when we can start making the system better in the first place. I added a new function, UtilityClass::format_time. This is the place where all time formats come together, given the number of seconds and optionally frames. I have simplified the above-mentioned functions somewhat, but I haven't given them a complete refactor or renaming - I mainly made sure that they all use the same backend so I can make the formats consistent and properly localizable. (And before we start shoving more temporary char buffers everywhere just to get rid of the std::string's, maybe we need to think of a globally used working buffer of size SCREEN_WIDTH_CHARS+1, as a register of sorts, for when any line of text needs to be made or processed, then printed, and then goes unused. Maybe help.textrow, or something like that.) As for this commit, the available time formats are now more consistent and changed a little in some places. Leading zeroes for the first unit are now no longer included, time trial results and the Super Gravitron can now display hours when they went to 60 minutes before, and we now always use .CC instead of :CC. These are the formats: - H:MM:SS - H:MM:SS.CC - M:SS - M:SS.CC - S.CC (only used when always_minutes=false, for the Gravitrons) Here's what changes to the current functions: - Game::partimestring() is removed - it was used in two places, and could be replaced by game.timetstring(game.timetrialpar) - Game::giventimestring(h,m,s) and Game::timestring() are now wrappers for the other functions - The four remaining functions (Game::resulttimestring(), Game::timetstring(t), Game::timestringcenti(buffer, buffer_size) and UtilityClass::timestring(t)) are now wrappers for the "central function", UtilityClass::format_time. - UtilityClass::twodigits(int t) is now unused so it's also removed. - I also added int UtilityClass::hms_to_seconds(int h, int m, int s)
2021-12-25 17:13:46 +01:00
return giventimestring(hours, minutes, seconds);
2020-01-01 21:29:24 +01:00
}
std::string Game::resulttimestring(void)
2020-01-01 21:29:24 +01:00
{
//given result time in seconds:
Simplify time formatting functions Here's my notes on all the existing functions and what kind of time formats they output: - Game::giventimestring(int hrs, int min, int sec) H:MM:SS MM:SS - Game::timestring() // uses game.hours/minutes/seconds H:MM:SS MM:SS - Game::partimestring() // uses game.timetrialpar (seconds) MM:SS - Game::resulttimestring() // uses game.timetrialresulttime (sec) + timetrialresultframes (1/30s) MM:SS.CC - Game::timetstring(int t) // t = seconds MM:SS - Game::timestringcenti(char* buffer, const size_t buffer_size) // uses game.hours/minutes/seconds/frames H:MM:SS.CC MM:SS.CC - UtilityClass::timestring(int t) // t = frames, 30 frames = 1 second S:CC M:SS:CC This is kind of a mess, and there's a lot of functions that do the same thing except using different variables. For localization, I also want translators to be able to localize all these time formats - many languages use the decimal comma instead of the decimal point (12:34,56) maybe some languages really prefer something like 1時02分11秒44瞬... Which I don't know to be correct, but it's good to be prepared for it and not restrict translators arbitrarily to only changing ":" and "." when we can start making the system better in the first place. I added a new function, UtilityClass::format_time. This is the place where all time formats come together, given the number of seconds and optionally frames. I have simplified the above-mentioned functions somewhat, but I haven't given them a complete refactor or renaming - I mainly made sure that they all use the same backend so I can make the formats consistent and properly localizable. (And before we start shoving more temporary char buffers everywhere just to get rid of the std::string's, maybe we need to think of a globally used working buffer of size SCREEN_WIDTH_CHARS+1, as a register of sorts, for when any line of text needs to be made or processed, then printed, and then goes unused. Maybe help.textrow, or something like that.) As for this commit, the available time formats are now more consistent and changed a little in some places. Leading zeroes for the first unit are now no longer included, time trial results and the Super Gravitron can now display hours when they went to 60 minutes before, and we now always use .CC instead of :CC. These are the formats: - H:MM:SS - H:MM:SS.CC - M:SS - M:SS.CC - S.CC (only used when always_minutes=false, for the Gravitrons) Here's what changes to the current functions: - Game::partimestring() is removed - it was used in two places, and could be replaced by game.timetstring(game.timetrialpar) - Game::giventimestring(h,m,s) and Game::timestring() are now wrappers for the other functions - The four remaining functions (Game::resulttimestring(), Game::timetstring(t), Game::timestringcenti(buffer, buffer_size) and UtilityClass::timestring(t)) are now wrappers for the "central function", UtilityClass::format_time. - UtilityClass::twodigits(int t) is now unused so it's also removed. - I also added int UtilityClass::hms_to_seconds(int h, int m, int s)
2021-12-25 17:13:46 +01:00
char output[SCREEN_WIDTH_CHARS + 1];
help.format_time(output, sizeof(output), timetrialresulttime, timetrialresultframes, true);
return output;
2020-01-01 21:29:24 +01:00
}
std::string Game::timetstring( int t )
2020-01-01 21:29:24 +01:00
{
//given par time in seconds:
Simplify time formatting functions Here's my notes on all the existing functions and what kind of time formats they output: - Game::giventimestring(int hrs, int min, int sec) H:MM:SS MM:SS - Game::timestring() // uses game.hours/minutes/seconds H:MM:SS MM:SS - Game::partimestring() // uses game.timetrialpar (seconds) MM:SS - Game::resulttimestring() // uses game.timetrialresulttime (sec) + timetrialresultframes (1/30s) MM:SS.CC - Game::timetstring(int t) // t = seconds MM:SS - Game::timestringcenti(char* buffer, const size_t buffer_size) // uses game.hours/minutes/seconds/frames H:MM:SS.CC MM:SS.CC - UtilityClass::timestring(int t) // t = frames, 30 frames = 1 second S:CC M:SS:CC This is kind of a mess, and there's a lot of functions that do the same thing except using different variables. For localization, I also want translators to be able to localize all these time formats - many languages use the decimal comma instead of the decimal point (12:34,56) maybe some languages really prefer something like 1時02分11秒44瞬... Which I don't know to be correct, but it's good to be prepared for it and not restrict translators arbitrarily to only changing ":" and "." when we can start making the system better in the first place. I added a new function, UtilityClass::format_time. This is the place where all time formats come together, given the number of seconds and optionally frames. I have simplified the above-mentioned functions somewhat, but I haven't given them a complete refactor or renaming - I mainly made sure that they all use the same backend so I can make the formats consistent and properly localizable. (And before we start shoving more temporary char buffers everywhere just to get rid of the std::string's, maybe we need to think of a globally used working buffer of size SCREEN_WIDTH_CHARS+1, as a register of sorts, for when any line of text needs to be made or processed, then printed, and then goes unused. Maybe help.textrow, or something like that.) As for this commit, the available time formats are now more consistent and changed a little in some places. Leading zeroes for the first unit are now no longer included, time trial results and the Super Gravitron can now display hours when they went to 60 minutes before, and we now always use .CC instead of :CC. These are the formats: - H:MM:SS - H:MM:SS.CC - M:SS - M:SS.CC - S.CC (only used when always_minutes=false, for the Gravitrons) Here's what changes to the current functions: - Game::partimestring() is removed - it was used in two places, and could be replaced by game.timetstring(game.timetrialpar) - Game::giventimestring(h,m,s) and Game::timestring() are now wrappers for the other functions - The four remaining functions (Game::resulttimestring(), Game::timetstring(t), Game::timestringcenti(buffer, buffer_size) and UtilityClass::timestring(t)) are now wrappers for the "central function", UtilityClass::format_time. - UtilityClass::twodigits(int t) is now unused so it's also removed. - I also added int UtilityClass::hms_to_seconds(int h, int m, int s)
2021-12-25 17:13:46 +01:00
char output[SCREEN_WIDTH_CHARS + 1];
help.format_time(output, sizeof(output), t, -1, true);
return output;
2020-01-01 21:29:24 +01:00
}
void Game::timestringcenti(char* buffer, const size_t buffer_size)
{
Simplify time formatting functions Here's my notes on all the existing functions and what kind of time formats they output: - Game::giventimestring(int hrs, int min, int sec) H:MM:SS MM:SS - Game::timestring() // uses game.hours/minutes/seconds H:MM:SS MM:SS - Game::partimestring() // uses game.timetrialpar (seconds) MM:SS - Game::resulttimestring() // uses game.timetrialresulttime (sec) + timetrialresultframes (1/30s) MM:SS.CC - Game::timetstring(int t) // t = seconds MM:SS - Game::timestringcenti(char* buffer, const size_t buffer_size) // uses game.hours/minutes/seconds/frames H:MM:SS.CC MM:SS.CC - UtilityClass::timestring(int t) // t = frames, 30 frames = 1 second S:CC M:SS:CC This is kind of a mess, and there's a lot of functions that do the same thing except using different variables. For localization, I also want translators to be able to localize all these time formats - many languages use the decimal comma instead of the decimal point (12:34,56) maybe some languages really prefer something like 1時02分11秒44瞬... Which I don't know to be correct, but it's good to be prepared for it and not restrict translators arbitrarily to only changing ":" and "." when we can start making the system better in the first place. I added a new function, UtilityClass::format_time. This is the place where all time formats come together, given the number of seconds and optionally frames. I have simplified the above-mentioned functions somewhat, but I haven't given them a complete refactor or renaming - I mainly made sure that they all use the same backend so I can make the formats consistent and properly localizable. (And before we start shoving more temporary char buffers everywhere just to get rid of the std::string's, maybe we need to think of a globally used working buffer of size SCREEN_WIDTH_CHARS+1, as a register of sorts, for when any line of text needs to be made or processed, then printed, and then goes unused. Maybe help.textrow, or something like that.) As for this commit, the available time formats are now more consistent and changed a little in some places. Leading zeroes for the first unit are now no longer included, time trial results and the Super Gravitron can now display hours when they went to 60 minutes before, and we now always use .CC instead of :CC. These are the formats: - H:MM:SS - H:MM:SS.CC - M:SS - M:SS.CC - S.CC (only used when always_minutes=false, for the Gravitrons) Here's what changes to the current functions: - Game::partimestring() is removed - it was used in two places, and could be replaced by game.timetstring(game.timetrialpar) - Game::giventimestring(h,m,s) and Game::timestring() are now wrappers for the other functions - The four remaining functions (Game::resulttimestring(), Game::timetstring(t), Game::timestringcenti(buffer, buffer_size) and UtilityClass::timestring(t)) are now wrappers for the "central function", UtilityClass::format_time. - UtilityClass::twodigits(int t) is now unused so it's also removed. - I also added int UtilityClass::hms_to_seconds(int h, int m, int s)
2021-12-25 17:13:46 +01:00
help.format_time(buffer, buffer_size, help.hms_to_seconds(hours, minutes, seconds), frames, true);
}
void Game::returnmenu(void)
{
if (menustack.empty())
{
vlog_error("Error: returning to previous menu frame on empty stack!");
return;
}
/* FIXME: Super bad kludge, don't hardcode this! */
if (currentmenuname == Menu::ed_music)
{
music.fadeout();
}
MenuStackFrame& frame = menustack[menustack.size()-1];
//Store this in case createmenu() removes the stack frame
int previousoption = frame.option;
createmenu(frame.name, true);
currentmenuoption = previousoption;
//Remove the stackframe now, but createmenu() might have already gotten to it
//if we were returning to the main menu
if (!menustack.empty())
{
menustack.pop_back();
}
}
void Game::returntomenu(enum Menu::MenuName t)
{
if (currentmenuname == t)
{
createmenu(t, true);
return;
}
//Unwind the menu stack until we reach our desired menu
int i = menustack.size() - 1;
while (i >= 0)
{
//If we pop it off we can't reference it anymore, so check for it now
bool is_the_menu_we_want = menustack[i].name == t;
returnmenu();
if (is_the_menu_we_want)
{
break;
}
i--;
}
}
void Game::createmenu( enum Menu::MenuName t, bool samemenu/*= false*/ )
2020-01-01 21:29:24 +01:00
{
if (t == Menu::mainmenu && !menutestmode)
{
//Either we've just booted up the game or returned from gamemode
//Whichever it is, we shouldn't have a stack,
//and most likely don't have a current stackframe
menustack.clear();
}
else if (!samemenu)
{
MenuStackFrame frame;
frame.option = currentmenuoption;
frame.name = currentmenuname;
menustack.push_back(frame);
currentmenuoption = 0;
}
2020-01-01 21:29:24 +01:00
currentmenuname = t;
menuyoff = 0;
Make menus automatically centered and narrowed All menus had a hardcoded X position (offset to an arbitrary starting point of 110) and a hardcoded horizontal spacing for the "staircasing" (mostly 30 pixels, but for some specific menus hardcoded to 15, 20 or something else). Not all menus were centered, and seem to have been manually made narrower (with lower horizontal spacing) whenever text ran offscreen during development. This system may already be hard to work with in an English-only menu system, since you may need to adjust horizontal spacing or positioning when adding an option. The main reason I made this change is that it's even less optimal when menu options have to be translated, since maximum string lengths are hard to determine, and it's easy to have menu options running offscreen, especially when not all menus are checked for all languages and when options could be added in the middle of a menu after translations of that menu are already checked. Now, menus are automatically centered based on their options, and they are automatically made narrower if they won't fit with the default horizontal spacing of 30 pixels (with some padding). The game.menuxoff variable for the menu X position is now also offset to 0 instead of 110 The _default_ horizontal spacing can be changed on a per-menu basis, and most menus (not all) which already had a narrower spacing set, retain that as a maximum spacing, simply because they looked odd with 30 pixels of spacing (especially the main menu). They will be made even narrower automatically if needed. In the most extreme case, the spacing can go down to 0 and options will be displayed right below each other. This isn't in the usual style of the game, but at least we did the best we could to prevent options running offscreen. The only exception to automatic menu centering and narrowing is the list of player levels, because it's a special case and existing behavior would be better than automatic centering there.
2020-06-29 02:09:52 +02:00
int maxspacing = 30; // maximum value for menuspacing, can only become lower.
2020-01-01 21:29:24 +01:00
menucountdown = 0;
menuoptions.clear();
2020-01-01 21:29:24 +01:00
switch (t)
2020-01-01 21:29:24 +01:00
{
case Menu::mainmenu:
if (ingame_titlemode)
{
/* We shouldn't be here! */
SDL_assert(0 && "Entering main menu from in-game options!");
break;
}
#if !defined(MAKEANDPLAY)
option(loc::gettext("play"));
#endif
option(loc::gettext("levels"));
option(loc::gettext("options"));
if (loc::show_translator_menu)
{
option(loc::gettext("translator"));
}
option(loc::gettext("credits"));
option(loc::gettext("quit"));
menuyoff = -10;
Make menus automatically centered and narrowed All menus had a hardcoded X position (offset to an arbitrary starting point of 110) and a hardcoded horizontal spacing for the "staircasing" (mostly 30 pixels, but for some specific menus hardcoded to 15, 20 or something else). Not all menus were centered, and seem to have been manually made narrower (with lower horizontal spacing) whenever text ran offscreen during development. This system may already be hard to work with in an English-only menu system, since you may need to adjust horizontal spacing or positioning when adding an option. The main reason I made this change is that it's even less optimal when menu options have to be translated, since maximum string lengths are hard to determine, and it's easy to have menu options running offscreen, especially when not all menus are checked for all languages and when options could be added in the middle of a menu after translations of that menu are already checked. Now, menus are automatically centered based on their options, and they are automatically made narrower if they won't fit with the default horizontal spacing of 30 pixels (with some padding). The game.menuxoff variable for the menu X position is now also offset to 0 instead of 110 The _default_ horizontal spacing can be changed on a per-menu basis, and most menus (not all) which already had a narrower spacing set, retain that as a maximum spacing, simply because they looked odd with 30 pixels of spacing (especially the main menu). They will be made even narrower automatically if needed. In the most extreme case, the spacing can go down to 0 and options will be displayed right below each other. This isn't in the usual style of the game, but at least we did the best we could to prevent options running offscreen. The only exception to automatic menu centering and narrowing is the list of player levels, because it's a special case and existing behavior would be better than automatic centering there.
2020-06-29 02:09:52 +02:00
maxspacing = 15;
break;
case Menu::playerworlds:
option(loc::gettext("play a level"));
option(loc::gettext("level editor"), !editor_disabled);
if (!editor_disabled)
{
option(loc::gettext("open level folder"), FILESYSTEM_openDirectoryEnabled());
option(loc::gettext("show level folder path"));
}
option(loc::gettext("return"));
menuyoff = -40;
Make menus automatically centered and narrowed All menus had a hardcoded X position (offset to an arbitrary starting point of 110) and a hardcoded horizontal spacing for the "staircasing" (mostly 30 pixels, but for some specific menus hardcoded to 15, 20 or something else). Not all menus were centered, and seem to have been manually made narrower (with lower horizontal spacing) whenever text ran offscreen during development. This system may already be hard to work with in an English-only menu system, since you may need to adjust horizontal spacing or positioning when adding an option. The main reason I made this change is that it's even less optimal when menu options have to be translated, since maximum string lengths are hard to determine, and it's easy to have menu options running offscreen, especially when not all menus are checked for all languages and when options could be added in the middle of a menu after translations of that menu are already checked. Now, menus are automatically centered based on their options, and they are automatically made narrower if they won't fit with the default horizontal spacing of 30 pixels (with some padding). The game.menuxoff variable for the menu X position is now also offset to 0 instead of 110 The _default_ horizontal spacing can be changed on a per-menu basis, and most menus (not all) which already had a narrower spacing set, retain that as a maximum spacing, simply because they looked odd with 30 pixels of spacing (especially the main menu). They will be made even narrower automatically if needed. In the most extreme case, the spacing can go down to 0 and options will be displayed right below each other. This isn't in the usual style of the game, but at least we did the best we could to prevent options running offscreen. The only exception to automatic menu centering and narrowing is the list of player levels, because it's a special case and existing behavior would be better than automatic centering there.
2020-06-29 02:09:52 +02:00
maxspacing = 15;
break;
case Menu::confirmshowlevelspath:
option(loc::gettext("no, don't show me"));
option(loc::gettext("yes, reveal the path"));
menuyoff = -10;
break;
case Menu::showlevelspath:
option(loc::gettext("return to levels"));
menuyoff = 60;
break;
case Menu::levellist:
if(cl.ListOfMetaData.size()==0)
2020-01-01 21:29:24 +01:00
{
option(loc::gettext("ok"));
2020-01-01 21:29:24 +01:00
menuyoff = -20;
}
else
{
for(int i=0; i<(int) cl.ListOfMetaData.size(); i++) // FIXME: int/size_t! -flibit
2020-01-01 21:29:24 +01:00
{
if(i>=levelpage*8 && i< (levelpage*8)+8)
{
const std::string filename = cl.ListOfMetaData[i].filename.substr(7);
int score = 0;
if (customlevelstats.count(filename) > 0)
2020-01-01 21:29:24 +01:00
{
score = customlevelstats[filename];
2020-01-01 21:29:24 +01:00
}
const char* prefix;
switch (score)
2020-01-01 21:29:24 +01:00
{
case 0:
2020-01-01 21:29:24 +01:00
{
static const char tmp[] = " ";
prefix = tmp;
break;
}
case 1:
{
static const char tmp[] = " * ";
prefix = tmp;
break;
}
case 3:
{
static const char tmp[] = "** ";
prefix = tmp;
break;
}
default:
SDL_assert(0 && "Unhandled menu text prefix!");
prefix = "";
break;
2020-01-01 21:29:24 +01:00
}
char text[MENU_TEXT_BYTES];
SDL_snprintf(text, sizeof(text), "%s%s", prefix, cl.ListOfMetaData[i].title.c_str());
for (size_t ii = 0; text[ii] != '\0'; ++ii)
{
text[ii] = SDL_tolower(text[ii]);
}
option(
text,
true,
cl.ListOfMetaData[i].title_is_gettext ? PR_FONT_INTERFACE : PR_FONT_IDX(
cl.ListOfMetaData[i].level_main_font_idx
)
);
2020-01-01 21:29:24 +01:00
}
}
if (cl.ListOfMetaData.size() > 8)
2020-01-01 21:29:24 +01:00
{
if((size_t) ((levelpage*8)+8) <cl.ListOfMetaData.size())
{
option(loc::gettext("next page"));
}
else
{
option(loc::gettext("first page"));
}
if (levelpage == 0)
{
option(loc::gettext("last page"));
}
else
{
option(loc::gettext("previous page"));
}
}
option(loc::gettext("return"));
2020-01-01 21:29:24 +01:00
Make menus automatically centered and narrowed All menus had a hardcoded X position (offset to an arbitrary starting point of 110) and a hardcoded horizontal spacing for the "staircasing" (mostly 30 pixels, but for some specific menus hardcoded to 15, 20 or something else). Not all menus were centered, and seem to have been manually made narrower (with lower horizontal spacing) whenever text ran offscreen during development. This system may already be hard to work with in an English-only menu system, since you may need to adjust horizontal spacing or positioning when adding an option. The main reason I made this change is that it's even less optimal when menu options have to be translated, since maximum string lengths are hard to determine, and it's easy to have menu options running offscreen, especially when not all menus are checked for all languages and when options could be added in the middle of a menu after translations of that menu are already checked. Now, menus are automatically centered based on their options, and they are automatically made narrower if they won't fit with the default horizontal spacing of 30 pixels (with some padding). The game.menuxoff variable for the menu X position is now also offset to 0 instead of 110 The _default_ horizontal spacing can be changed on a per-menu basis, and most menus (not all) which already had a narrower spacing set, retain that as a maximum spacing, simply because they looked odd with 30 pixels of spacing (especially the main menu). They will be made even narrower automatically if needed. In the most extreme case, the spacing can go down to 0 and options will be displayed right below each other. This isn't in the usual style of the game, but at least we did the best we could to prevent options running offscreen. The only exception to automatic menu centering and narrowing is the list of player levels, because it's a special case and existing behavior would be better than automatic centering there.
2020-06-29 02:09:52 +02:00
menuxoff = 20;
menuyoff = 70-(menuoptions.size()*10);
Make menus automatically centered and narrowed All menus had a hardcoded X position (offset to an arbitrary starting point of 110) and a hardcoded horizontal spacing for the "staircasing" (mostly 30 pixels, but for some specific menus hardcoded to 15, 20 or something else). Not all menus were centered, and seem to have been manually made narrower (with lower horizontal spacing) whenever text ran offscreen during development. This system may already be hard to work with in an English-only menu system, since you may need to adjust horizontal spacing or positioning when adding an option. The main reason I made this change is that it's even less optimal when menu options have to be translated, since maximum string lengths are hard to determine, and it's easy to have menu options running offscreen, especially when not all menus are checked for all languages and when options could be added in the middle of a menu after translations of that menu are already checked. Now, menus are automatically centered based on their options, and they are automatically made narrower if they won't fit with the default horizontal spacing of 30 pixels (with some padding). The game.menuxoff variable for the menu X position is now also offset to 0 instead of 110 The _default_ horizontal spacing can be changed on a per-menu basis, and most menus (not all) which already had a narrower spacing set, retain that as a maximum spacing, simply because they looked odd with 30 pixels of spacing (especially the main menu). They will be made even narrower automatically if needed. In the most extreme case, the spacing can go down to 0 and options will be displayed right below each other. This isn't in the usual style of the game, but at least we did the best we could to prevent options running offscreen. The only exception to automatic menu centering and narrowing is the list of player levels, because it's a special case and existing behavior would be better than automatic centering there.
2020-06-29 02:09:52 +02:00
menuspacing = 5;
return; // skip automatic centering, will turn out bad with levels list
2020-01-01 21:29:24 +01:00
}
break;
case Menu::quickloadlevel:
option(loc::gettext("continue from save"));
option(loc::gettext("start from beginning"));
option(loc::gettext("delete save"));
option(loc::gettext("back to levels"));
menuyoff = -30;
break;
case Menu::deletequicklevel:
option(loc::gettext("no! don't delete"));
option(loc::gettext("yes, delete save"));
menuyoff = 64;
break;
case Menu::youwannaquit:
option(loc::gettext("yes, quit"));
option(loc::gettext("no, return"));
menuyoff = -20;
break;
case Menu::errornostart:
option(loc::gettext("ok"));
menuyoff = -20;
break;
case Menu::gameplayoptions:
#if !defined(MAKEANDPLAY)
if (ingame_titlemode && unlock[Unlock_FLIPMODE])
#endif
{
option(loc::gettext("flip mode"));
}
option(loc::gettext("toggle fps"));
option(loc::gettext("speedrun options"));
option(loc::gettext("advanced options"));
option(loc::gettext("clear main game data"));
option(loc::gettext("clear custom level data"));
option(loc::gettext("return"));
menuyoff = -10;
maxspacing = 15;
break;
case Menu::graphicoptions:
if (!gameScreen.isForcedFullscreen())
{
option(loc::gettext("toggle fullscreen"));
}
option(loc::gettext("scaling mode"));
if (!gameScreen.isForcedFullscreen())
{
option(loc::gettext("resize to nearest"), gameScreen.isWindowed);
}
option(loc::gettext("toggle filter"));
option(loc::gettext("toggle analogue"));
option(loc::gettext("toggle vsync"));
option(loc::gettext("return"));
menuyoff = -10;
maxspacing = 15;
break;
case Menu::ed_settings:
option(loc::gettext("change description"));
option(loc::gettext("edit scripts"));
option(loc::gettext("change music"));
option(loc::gettext("editor ghosts"));
option(loc::gettext("load level"));
option(loc::gettext("save level"));
option(loc::gettext("options"));
option(loc::gettext("quit to main menu"));
2020-01-01 21:29:24 +01:00
menuyoff = -20;
Make menus automatically centered and narrowed All menus had a hardcoded X position (offset to an arbitrary starting point of 110) and a hardcoded horizontal spacing for the "staircasing" (mostly 30 pixels, but for some specific menus hardcoded to 15, 20 or something else). Not all menus were centered, and seem to have been manually made narrower (with lower horizontal spacing) whenever text ran offscreen during development. This system may already be hard to work with in an English-only menu system, since you may need to adjust horizontal spacing or positioning when adding an option. The main reason I made this change is that it's even less optimal when menu options have to be translated, since maximum string lengths are hard to determine, and it's easy to have menu options running offscreen, especially when not all menus are checked for all languages and when options could be added in the middle of a menu after translations of that menu are already checked. Now, menus are automatically centered based on their options, and they are automatically made narrower if they won't fit with the default horizontal spacing of 30 pixels (with some padding). The game.menuxoff variable for the menu X position is now also offset to 0 instead of 110 The _default_ horizontal spacing can be changed on a per-menu basis, and most menus (not all) which already had a narrower spacing set, retain that as a maximum spacing, simply because they looked odd with 30 pixels of spacing (especially the main menu). They will be made even narrower automatically if needed. In the most extreme case, the spacing can go down to 0 and options will be displayed right below each other. This isn't in the usual style of the game, but at least we did the best we could to prevent options running offscreen. The only exception to automatic menu centering and narrowing is the list of player levels, because it's a special case and existing behavior would be better than automatic centering there.
2020-06-29 02:09:52 +02:00
maxspacing = 15;
break;
case Menu::ed_desc:
option(loc::gettext("change name"));
option(loc::gettext("change author"));
option(loc::gettext("change description"));
option(loc::gettext("change website"));
option(loc::gettext("change font"));
option(loc::gettext("return"));
2020-01-01 21:29:24 +01:00
menuyoff = 6;
Make menus automatically centered and narrowed All menus had a hardcoded X position (offset to an arbitrary starting point of 110) and a hardcoded horizontal spacing for the "staircasing" (mostly 30 pixels, but for some specific menus hardcoded to 15, 20 or something else). Not all menus were centered, and seem to have been manually made narrower (with lower horizontal spacing) whenever text ran offscreen during development. This system may already be hard to work with in an English-only menu system, since you may need to adjust horizontal spacing or positioning when adding an option. The main reason I made this change is that it's even less optimal when menu options have to be translated, since maximum string lengths are hard to determine, and it's easy to have menu options running offscreen, especially when not all menus are checked for all languages and when options could be added in the middle of a menu after translations of that menu are already checked. Now, menus are automatically centered based on their options, and they are automatically made narrower if they won't fit with the default horizontal spacing of 30 pixels (with some padding). The game.menuxoff variable for the menu X position is now also offset to 0 instead of 110 The _default_ horizontal spacing can be changed on a per-menu basis, and most menus (not all) which already had a narrower spacing set, retain that as a maximum spacing, simply because they looked odd with 30 pixels of spacing (especially the main menu). They will be made even narrower automatically if needed. In the most extreme case, the spacing can go down to 0 and options will be displayed right below each other. This isn't in the usual style of the game, but at least we did the best we could to prevent options running offscreen. The only exception to automatic menu centering and narrowing is the list of player levels, because it's a special case and existing behavior would be better than automatic centering there.
2020-06-29 02:09:52 +02:00
maxspacing = 15;
break;
case Menu::ed_music:
option(loc::gettext("next song"));
option(loc::gettext("previous song"));
option(loc::gettext("return"));
2020-01-01 21:29:24 +01:00
menuyoff = 16;
Make menus automatically centered and narrowed All menus had a hardcoded X position (offset to an arbitrary starting point of 110) and a hardcoded horizontal spacing for the "staircasing" (mostly 30 pixels, but for some specific menus hardcoded to 15, 20 or something else). Not all menus were centered, and seem to have been manually made narrower (with lower horizontal spacing) whenever text ran offscreen during development. This system may already be hard to work with in an English-only menu system, since you may need to adjust horizontal spacing or positioning when adding an option. The main reason I made this change is that it's even less optimal when menu options have to be translated, since maximum string lengths are hard to determine, and it's easy to have menu options running offscreen, especially when not all menus are checked for all languages and when options could be added in the middle of a menu after translations of that menu are already checked. Now, menus are automatically centered based on their options, and they are automatically made narrower if they won't fit with the default horizontal spacing of 30 pixels (with some padding). The game.menuxoff variable for the menu X position is now also offset to 0 instead of 110 The _default_ horizontal spacing can be changed on a per-menu basis, and most menus (not all) which already had a narrower spacing set, retain that as a maximum spacing, simply because they looked odd with 30 pixels of spacing (especially the main menu). They will be made even narrower automatically if needed. In the most extreme case, the spacing can go down to 0 and options will be displayed right below each other. This isn't in the usual style of the game, but at least we did the best we could to prevent options running offscreen. The only exception to automatic menu centering and narrowing is the list of player levels, because it's a special case and existing behavior would be better than automatic centering there.
2020-06-29 02:09:52 +02:00
maxspacing = 15;
break;
case Menu::ed_quit:
option(loc::gettext("yes, save and quit"));
option(loc::gettext("no, quit without saving"));
option(loc::gettext("return to editor"));
2020-01-01 21:29:24 +01:00
menuyoff = 8;
Make menus automatically centered and narrowed All menus had a hardcoded X position (offset to an arbitrary starting point of 110) and a hardcoded horizontal spacing for the "staircasing" (mostly 30 pixels, but for some specific menus hardcoded to 15, 20 or something else). Not all menus were centered, and seem to have been manually made narrower (with lower horizontal spacing) whenever text ran offscreen during development. This system may already be hard to work with in an English-only menu system, since you may need to adjust horizontal spacing or positioning when adding an option. The main reason I made this change is that it's even less optimal when menu options have to be translated, since maximum string lengths are hard to determine, and it's easy to have menu options running offscreen, especially when not all menus are checked for all languages and when options could be added in the middle of a menu after translations of that menu are already checked. Now, menus are automatically centered based on their options, and they are automatically made narrower if they won't fit with the default horizontal spacing of 30 pixels (with some padding). The game.menuxoff variable for the menu X position is now also offset to 0 instead of 110 The _default_ horizontal spacing can be changed on a per-menu basis, and most menus (not all) which already had a narrower spacing set, retain that as a maximum spacing, simply because they looked odd with 30 pixels of spacing (especially the main menu). They will be made even narrower automatically if needed. In the most extreme case, the spacing can go down to 0 and options will be displayed right below each other. This isn't in the usual style of the game, but at least we did the best we could to prevent options running offscreen. The only exception to automatic menu centering and narrowing is the list of player levels, because it's a special case and existing behavior would be better than automatic centering there.
2020-06-29 02:09:52 +02:00
maxspacing = 15;
break;
case Menu::ed_font:
{
int option_match = -1;
for (uint8_t i = 0; i < font::font_idx_options_n; i++)
{
uint8_t idx = font::font_idx_options[i];
option(font::get_main_font_display_name(idx), true, PR_FONT_IDX(idx));
if (font::level_font_is_main_idx(idx))
{
option_match = i;
}
}
currentmenuoption = option_match != -1 ? option_match : 0;
maxspacing = 15;
break;
}
case Menu::options:
option(loc::gettext("gameplay"));
option(loc::gettext("graphics"));
option(loc::gettext("audio"));
option(loc::gettext("game pad"));
option(loc::gettext("accessibility"));
option(loc::gettext("language"), graphics.textboxes.empty());
option(loc::gettext("return"));
menuyoff = 0;
maxspacing = 15;
break;
case Menu::speedrunneroptions:
option(loc::gettext("glitchrunner mode"));
option(loc::gettext("input delay"));
option(loc::gettext("interact button"));
option(loc::gettext("fake load screen"));
option(loc::gettext("toggle in-game timer"));
option(loc::gettext("english sprites"));
option(loc::gettext("return"));
menuyoff = 0;
maxspacing = 15;
break;
case Menu::setglitchrunner:
{
int i;
option(loc::gettext("none"));
for (i = 1; i < GlitchrunnerNumVersions; ++i)
{
option(loc::gettext(GlitchrunnerMode_enum_to_string((enum GlitchrunnerMode) i)));
}
break;
}
case Menu::advancedoptions:
option(loc::gettext("unfocus pause"));
option(loc::gettext("unfocus audio pause"));
option(loc::gettext("room name background"));
option(loc::gettext("return"));
menuyoff = 0;
maxspacing = 15;
break;
case Menu::audiooptions:
option(loc::gettext("music volume"));
option(loc::gettext("sound volume"));
if (music.mmmmmm)
{
option(loc::gettext("soundtrack"));
}
option(loc::gettext("return"));
menuyoff = 0;
maxspacing = 15;
break;
case Menu::accessibility:
#if !defined(MAKEANDPLAY)
option(loc::gettext("unlock play modes"));
#endif
option(loc::gettext("invincibility"), !ingame_titlemode || !incompetitive());
option(loc::gettext("slowdown"), !ingame_titlemode || !incompetitive());
option(loc::gettext("animated backgrounds"));
option(loc::gettext("screen effects"));
option(loc::gettext("text outline"));
option(loc::gettext("return"));
menuyoff = 0;
maxspacing = 15;
break;
case Menu::controller:
option(loc::gettext("analog stick sensitivity"));
option(loc::gettext("bind flip"));
option(loc::gettext("bind enter"));
option(loc::gettext("bind menu"));
option(loc::gettext("bind restart"));
option(loc::gettext("bind interact"), separate_interact);
option(loc::gettext("return"));
menuyoff = 0;
maxspacing = 10;
break;
case Menu::language:
if (loc::languagelist.empty())
{
option(loc::gettext("ok"));
menuyoff = -20;
}
else
{
for (size_t i = 0; i < loc::languagelist.size(); i++)
{
if (loc::languagelist[i].nativename.empty())
option(loc::languagelist[i].code.c_str());
else
option(loc::languagelist[i].nativename.c_str(), true, PR_FONT_IDX(loc::languagelist[i].font_idx));
}
menuyoff = 70-(menuoptions.size()*10);
maxspacing = 5;
}
break;
case Menu::translator_main:
option(loc::gettext("translator options"));
option(loc::gettext("maintenance"));
option(loc::gettext("open lang folder"), FILESYSTEM_openDirectoryEnabled());
option(loc::gettext("return"));
menuyoff = 0;
break;
case Menu::translator_options:
option(loc::gettext("language statistics"));
option(loc::gettext("translate room names"));
Add level exploring menu for translators I would, of course, recommend translators to translate the roomnames while playing the full game (optionally in invincibility) so they can immediately get all the context and maybe the most inspiration. And if you want to go back into a specific level, then there's always the time trials and intermission replays which will give you full coverage of all the room names. However, the time trials weren't really made for room name translation. They have some annoying features like the instant restart when you press ENTER at the wrong time, they remove context clues like teleporters and companions, but the worst problem is that the last room in a level is often completely untranslatable inside the time trials because you immediately get sent to the results screen... So, I added a new menu in the translator options, "explore game", which gives you access to all the time trials and the two intermissions, from the same menu. All these time trials (which they're still based off of, under the hood) are stripped of the annoying features that come with time trials. These are the changes I made to time trial behavior in translator exploring mode: - No 3-2-1-Go! countdown - No on-screen time/death/shiny/par - ENTER doesn't restart, and the map menu works. The entire map is also revealed. - Prize for the Reckless is in its normal form - The teleporters in Entanglement Generator, Wheeler's Wormhole and Level Complete are restored as context for room names (actually, we should probably restore them in time trials anyway? Their "press to teleport" prompt is already blocked out in time trials and they do nothing other than being a checkpoint. I guess the reason they were removed was to stop people from opening the teleporter menu when that was not specifically blocked out in time trials yet.) - The companions are there at the end of levels, and behave like in no death mode (become happy and follow you to the teleporter). Also for context. - At the end of each level, you're not suddenly sent to the menu, but you can use the teleporter at your leisure just like in the intermission replays. In the Final Level, you do get sent to the menu automatically, but after a longer delay. I made another mark on VVVVVV: don't be startled, I added gamestates. I wanted all teleporters at the end of levels to behave like the ones at the end of the intermission replays, and all handling for teleporting with specific companions is already done in gamestates, so rather than adding conditional blocks across 5 or so different gamestates, it made more sense to make a single gamestate for "teleporting in translator exploring mode" (3090). I also added an alternative to having to use gamestate 3500 or 82 for the end of the final level: 3091-3092. One other thing I want to add to the "explore game" menu: a per-level count of how many room names are left to translate. That shouldn't be too difficult, and I'm planning that for the next commit.
2022-11-26 03:33:17 +01:00
option(loc::gettext("explore game"));
option(loc::gettext("menu test"));
option(loc::gettext("cutscene test"), loc::lang != "en");
option(loc::gettext("limits check"));
option(loc::gettext("return"));
menuyoff = 0;
break;
case Menu::translator_options_limitscheck:
option(loc::gettext("next page"));
option(loc::gettext("return"));
menuyoff = 64;
break;
case Menu::translator_options_stats:
option(loc::gettext("return"));
menuyoff = 64;
break;
Add level exploring menu for translators I would, of course, recommend translators to translate the roomnames while playing the full game (optionally in invincibility) so they can immediately get all the context and maybe the most inspiration. And if you want to go back into a specific level, then there's always the time trials and intermission replays which will give you full coverage of all the room names. However, the time trials weren't really made for room name translation. They have some annoying features like the instant restart when you press ENTER at the wrong time, they remove context clues like teleporters and companions, but the worst problem is that the last room in a level is often completely untranslatable inside the time trials because you immediately get sent to the results screen... So, I added a new menu in the translator options, "explore game", which gives you access to all the time trials and the two intermissions, from the same menu. All these time trials (which they're still based off of, under the hood) are stripped of the annoying features that come with time trials. These are the changes I made to time trial behavior in translator exploring mode: - No 3-2-1-Go! countdown - No on-screen time/death/shiny/par - ENTER doesn't restart, and the map menu works. The entire map is also revealed. - Prize for the Reckless is in its normal form - The teleporters in Entanglement Generator, Wheeler's Wormhole and Level Complete are restored as context for room names (actually, we should probably restore them in time trials anyway? Their "press to teleport" prompt is already blocked out in time trials and they do nothing other than being a checkpoint. I guess the reason they were removed was to stop people from opening the teleporter menu when that was not specifically blocked out in time trials yet.) - The companions are there at the end of levels, and behave like in no death mode (become happy and follow you to the teleporter). Also for context. - At the end of each level, you're not suddenly sent to the menu, but you can use the teleporter at your leisure just like in the intermission replays. In the Final Level, you do get sent to the menu automatically, but after a longer delay. I made another mark on VVVVVV: don't be startled, I added gamestates. I wanted all teleporters at the end of levels to behave like the ones at the end of the intermission replays, and all handling for teleporting with specific companions is already done in gamestates, so rather than adding conditional blocks across 5 or so different gamestates, it made more sense to make a single gamestate for "teleporting in translator exploring mode" (3090). I also added an alternative to having to use gamestate 3500 or 82 for the end of the final level: 3091-3092. One other thing I want to add to the "explore game" menu: a per-level count of how many room names are left to translate. That shouldn't be too difficult, and I'm planning that for the next commit.
2022-11-26 03:33:17 +01:00
case Menu::translator_options_exploregame:
option(loc::gettext("space station 1"));
option(loc::gettext("the laboratory"));
option(loc::gettext("the tower"));
option(loc::gettext("space station 2"));
option(loc::gettext("the warp zone"));
option(loc::gettext("intermission 1"));
option(loc::gettext("intermission 2"));
option(loc::gettext("the final level"));
option(loc::gettext("return"));
menuyoff = -20;
break;
case Menu::translator_options_cutscenetest:
for (
size_t i = (cutscenetest_menu_page*14);
i < (cutscenetest_menu_page*14)+14 && i < loc::testable_script_ids.size();
i++
)
{
option(loc::testable_script_ids[i].c_str());
}
if((cutscenetest_menu_page*14)+14 < loc::testable_script_ids.size())
{
option(loc::gettext("next page"));
}
else
{
option(loc::gettext("first page"));
}
if (cutscenetest_menu_page == 0)
{
option(loc::gettext("last page"));
}
else
{
option(loc::gettext("previous page"));
}
option(loc::gettext("from clipboard"));
option(loc::gettext("return"));
menuxoff = 20;
menuyoff = 55-(menuoptions.size()*10);
menuspacing = 5;
return; // skip automatic centering, will turn out bad with scripts list
case Menu::translator_maintenance:
option(loc::gettext("sync language files"));
option(loc::gettext("global statistics"), false);
option(loc::gettext("global limits check"));
option(loc::gettext("return"));
menuyoff = 0;
break;
case Menu::translator_maintenance_sync:
option(loc::gettext("sync"));
option(loc::gettext("return"));
menuyoff = 64;
break;
case Menu::translator_error_setlangwritedir:
option(loc::gettext("ok"));
menuyoff = 10;
break;
case Menu::cleardatamenu:
case Menu::clearcustomdatamenu:
option(loc::gettext("no! don't delete"));
option(loc::gettext("yes, delete everything"));
2020-01-01 21:29:24 +01:00
menuyoff = 64;
break;
case Menu::setinvincibility:
option(loc::gettext("no, return to options"));
option(loc::gettext("yes, enable"));
2020-01-01 21:29:24 +01:00
menuyoff = 64;
break;
case Menu::setslowdown:
option(loc::gettext("normal speed"));
option(loc::gettext("80% speed"));
option(loc::gettext("60% speed"));
option(loc::gettext("40% speed"));
2020-01-01 21:29:24 +01:00
menuyoff = 16;
break;
case Menu::unlockmenu:
option(loc::gettext("unlock time trials"));
option(loc::gettext("unlock intermissions"), !unlock[Unlock_INTERMISSION_REPLAYS]);
option(loc::gettext("unlock no death mode"), !unlock[Unlock_NODEATHMODE]);
option(loc::gettext("unlock flip mode"), !unlock[Unlock_FLIPMODE]);
option(loc::gettext("unlock ship jukebox"), (stat_trinkets<20));
option(loc::gettext("unlock secret lab"), !unlock[Unlock_SECRETLAB]);
option(loc::gettext("return"));
2020-01-01 21:29:24 +01:00
menuyoff = -20;
break;
case Menu::credits:
option(loc::gettext("next page"));
option(loc::gettext("last page"));
option(loc::gettext("return"));
2020-01-01 21:29:24 +01:00
menuyoff = 64;
break;
case Menu::credits2:
case Menu::credits25:
case Menu::credits3:
case Menu::credits4:
case Menu::credits5:
case Menu::credits_localisations_implementation:
case Menu::credits_localisations_translations:
option(loc::gettext("next page"));
option(loc::gettext("previous page"));
option(loc::gettext("return"));
2020-01-01 21:29:24 +01:00
menuyoff = 64;
break;
case Menu::credits6:
option(loc::gettext("first page"));
option(loc::gettext("previous page"));
option(loc::gettext("return"));
2020-01-01 21:29:24 +01:00
menuyoff = 64;
break;
case Menu::play:
{
2020-01-01 21:29:24 +01:00
//Ok, here's where the unlock stuff comes into it:
//First up, time trials:
int temp = 0;
if (unlock[Unlock_SPACESTATION1_COMPLETE]
&& stat_trinkets >= 3
&& !unlocknotify[Unlock_TIMETRIAL_SPACESTATION1])
{
temp++;
}
if (unlock[Unlock_LABORATORY_COMPLETE]
&& stat_trinkets >= 6
&& !unlocknotify[Unlock_TIMETRIAL_LABORATORY])
{
temp++;
}
if (unlock[Unlock_TOWER_COMPLETE]
&& stat_trinkets >= 9
&& !unlocknotify[Unlock_TIMETRIAL_TOWER])
{
temp++;
}
if (unlock[Unlock_SPACESTATION2_COMPLETE]
&& stat_trinkets >= 12
&& !unlocknotify[Unlock_TIMETRIAL_SPACESTATION2])
{
temp++;
}
if (unlock[Unlock_WARPZONE_COMPLETE]
&& stat_trinkets >= 15
&& !unlocknotify[Unlock_TIMETRIAL_WARPZONE])
{
temp++;
}
if (unlock[UnlockTrophy_GAME_COMPLETE]
&& stat_trinkets >= 18
&& !unlocknotify[Unlock_TIMETRIAL_FINALLEVEL])
{
temp++;
}
2020-01-01 21:29:24 +01:00
if (temp > 0)
{
//you've unlocked a time trial!
if (unlock[Unlock_SPACESTATION1_COMPLETE] && stat_trinkets >= 3)
2020-01-01 21:29:24 +01:00
{
unlocknotify[Unlock_TIMETRIAL_SPACESTATION1] = true;
unlock[Unlock_TIMETRIAL_SPACESTATION1] = true;
2020-01-01 21:29:24 +01:00
}
if (unlock[Unlock_LABORATORY_COMPLETE] && stat_trinkets >= 6)
2020-01-01 21:29:24 +01:00
{
unlocknotify[Unlock_TIMETRIAL_LABORATORY] = true;
unlock[Unlock_TIMETRIAL_LABORATORY] = true;
2020-01-01 21:29:24 +01:00
}
if (unlock[Unlock_TOWER_COMPLETE] && stat_trinkets >= 9)
2020-01-01 21:29:24 +01:00
{
unlocknotify[Unlock_TIMETRIAL_TOWER] = true;
unlock[Unlock_TIMETRIAL_TOWER] = true;
2020-01-01 21:29:24 +01:00
}
if (unlock[Unlock_SPACESTATION2_COMPLETE] && stat_trinkets >= 12)
2020-01-01 21:29:24 +01:00
{
unlocknotify[Unlock_TIMETRIAL_SPACESTATION2] = true;
unlock[Unlock_TIMETRIAL_SPACESTATION2] = true;
2020-01-01 21:29:24 +01:00
}
if (unlock[Unlock_WARPZONE_COMPLETE] && stat_trinkets >= 15)
2020-01-01 21:29:24 +01:00
{
unlocknotify[Unlock_TIMETRIAL_WARPZONE] = true;
unlock[Unlock_TIMETRIAL_WARPZONE] = true;
2020-01-01 21:29:24 +01:00
}
if (unlock[UnlockTrophy_GAME_COMPLETE] && stat_trinkets >= 18)
2020-01-01 21:29:24 +01:00
{
unlocknotify[Unlock_TIMETRIAL_FINALLEVEL] = true;
unlock[Unlock_TIMETRIAL_FINALLEVEL] = true;
2020-01-01 21:29:24 +01:00
}
if (temp == 1)
{
createmenu(Menu::unlocktimetrial, true);
savestatsandsettings();
2020-01-01 21:29:24 +01:00
}
else if (temp > 1)
{
createmenu(Menu::unlocktimetrials, true);
savestatsandsettings();
2020-01-01 21:29:24 +01:00
}
}
else
{
//Alright, we haven't unlocked any time trials. How about no death mode?
temp = 0;
if (bestrank[TimeTrial_SPACESTATION1] >= 2) temp++;
if (bestrank[TimeTrial_LABORATORY] >= 2) temp++;
if (bestrank[TimeTrial_TOWER] >= 2) temp++;
if (bestrank[TimeTrial_SPACESTATION2] >= 2) temp++;
if (bestrank[TimeTrial_WARPZONE] >= 2) temp++;
if (bestrank[TimeTrial_FINALLEVEL] >= 2) temp++;
if (temp >= 4 && !unlocknotify[Unlock_NODEATHMODE])
2020-01-01 21:29:24 +01:00
{
//Unlock No Death Mode
unlocknotify[Unlock_NODEATHMODE] = true;
unlock[Unlock_NODEATHMODE] = true;
createmenu(Menu::unlocknodeathmode, true);
savestatsandsettings();
2020-01-01 21:29:24 +01:00
}
//Alright then! Flip mode?
else if (unlock[UnlockTrophy_GAME_COMPLETE]
&& !unlocknotify[Unlock_FLIPMODE])
{
unlock[Unlock_FLIPMODE] = true;
unlocknotify[Unlock_FLIPMODE] = true;
createmenu(Menu::unlockflipmode, true);
savestatsandsettings();
}
//What about the intermission levels?
else if (unlock[Unlock_INTERMISSION2_COMPLETE]
&& !unlocknotify[Unlock_INTERMISSION_REPLAYS])
{
unlock[Unlock_INTERMISSION_REPLAYS] = true;
unlocknotify[Unlock_INTERMISSION_REPLAYS] = true;
createmenu(Menu::unlockintermission, true);
savestatsandsettings();
}
2020-01-01 21:29:24 +01:00
else
{
if (save_exists())
{
option(loc::gettext("continue"));
}
else
{
option(loc::gettext("new game"));
}
//ok, secret lab! no notification, but test:
if (unlock[Unlock_SECRETLAB])
{
option(loc::gettext("secret lab"));
}
option(loc::gettext("play modes"));
if (save_exists())
{
option(loc::gettext("new game"));
}
option(loc::gettext("return"));
if (unlock[Unlock_SECRETLAB])
{
menuyoff = -30;
}
else
{
menuyoff = -40;
}
2020-01-01 21:29:24 +01:00
}
}
break;
}
case Menu::unlocktimetrial:
case Menu::unlocktimetrials:
case Menu::unlocknodeathmode:
case Menu::unlockintermission:
case Menu::unlockflipmode:
option(loc::gettext("proceed"));
2020-01-01 21:29:24 +01:00
menuyoff = 70;
break;
case Menu::newgamewarning:
option(loc::gettext("start new game"));
option(loc::gettext("return"));
2020-01-01 21:29:24 +01:00
menuyoff = 64;
break;
case Menu::playmodes:
option(loc::gettext("time trials"), !nocompetitive_unless_translator());
option(loc::gettext("intermissions"), unlock[Unlock_INTERMISSION_REPLAYS]);
option(loc::gettext("no death mode"), unlock[Unlock_NODEATHMODE] && !nocompetitive());
option(loc::gettext("flip mode"), unlock[Unlock_FLIPMODE]);
option(loc::gettext("return"));
2020-01-01 21:29:24 +01:00
menuyoff = 8;
Make menus automatically centered and narrowed All menus had a hardcoded X position (offset to an arbitrary starting point of 110) and a hardcoded horizontal spacing for the "staircasing" (mostly 30 pixels, but for some specific menus hardcoded to 15, 20 or something else). Not all menus were centered, and seem to have been manually made narrower (with lower horizontal spacing) whenever text ran offscreen during development. This system may already be hard to work with in an English-only menu system, since you may need to adjust horizontal spacing or positioning when adding an option. The main reason I made this change is that it's even less optimal when menu options have to be translated, since maximum string lengths are hard to determine, and it's easy to have menu options running offscreen, especially when not all menus are checked for all languages and when options could be added in the middle of a menu after translations of that menu are already checked. Now, menus are automatically centered based on their options, and they are automatically made narrower if they won't fit with the default horizontal spacing of 30 pixels (with some padding). The game.menuxoff variable for the menu X position is now also offset to 0 instead of 110 The _default_ horizontal spacing can be changed on a per-menu basis, and most menus (not all) which already had a narrower spacing set, retain that as a maximum spacing, simply because they looked odd with 30 pixels of spacing (especially the main menu). They will be made even narrower automatically if needed. In the most extreme case, the spacing can go down to 0 and options will be displayed right below each other. This isn't in the usual style of the game, but at least we did the best we could to prevent options running offscreen. The only exception to automatic menu centering and narrowing is the list of player levels, because it's a special case and existing behavior would be better than automatic centering there.
2020-06-29 02:09:52 +02:00
maxspacing = 20;
break;
case Menu::intermissionmenu:
option(loc::gettext("play intermission 1"));
option(loc::gettext("play intermission 2"));
option(loc::gettext("return"));
2020-01-01 21:29:24 +01:00
menuyoff = -35;
break;
case Menu::playint1:
Add level exploring menu for translators I would, of course, recommend translators to translate the roomnames while playing the full game (optionally in invincibility) so they can immediately get all the context and maybe the most inspiration. And if you want to go back into a specific level, then there's always the time trials and intermission replays which will give you full coverage of all the room names. However, the time trials weren't really made for room name translation. They have some annoying features like the instant restart when you press ENTER at the wrong time, they remove context clues like teleporters and companions, but the worst problem is that the last room in a level is often completely untranslatable inside the time trials because you immediately get sent to the results screen... So, I added a new menu in the translator options, "explore game", which gives you access to all the time trials and the two intermissions, from the same menu. All these time trials (which they're still based off of, under the hood) are stripped of the annoying features that come with time trials. These are the changes I made to time trial behavior in translator exploring mode: - No 3-2-1-Go! countdown - No on-screen time/death/shiny/par - ENTER doesn't restart, and the map menu works. The entire map is also revealed. - Prize for the Reckless is in its normal form - The teleporters in Entanglement Generator, Wheeler's Wormhole and Level Complete are restored as context for room names (actually, we should probably restore them in time trials anyway? Their "press to teleport" prompt is already blocked out in time trials and they do nothing other than being a checkpoint. I guess the reason they were removed was to stop people from opening the teleporter menu when that was not specifically blocked out in time trials yet.) - The companions are there at the end of levels, and behave like in no death mode (become happy and follow you to the teleporter). Also for context. - At the end of each level, you're not suddenly sent to the menu, but you can use the teleporter at your leisure just like in the intermission replays. In the Final Level, you do get sent to the menu automatically, but after a longer delay. I made another mark on VVVVVV: don't be startled, I added gamestates. I wanted all teleporters at the end of levels to behave like the ones at the end of the intermission replays, and all handling for teleporting with specific companions is already done in gamestates, so rather than adding conditional blocks across 5 or so different gamestates, it made more sense to make a single gamestate for "teleporting in translator exploring mode" (3090). I also added an alternative to having to use gamestate 3500 or 82 for the end of the final level: 3091-3092. One other thing I want to add to the "explore game" menu: a per-level count of how many room names are left to translate. That shouldn't be too difficult, and I'm planning that for the next commit.
2022-11-26 03:33:17 +01:00
start_translator_exploring = false;
option(loc::gettext_case("Vitellary", 1));
option(loc::gettext_case("Vermilion", 1));
option(loc::gettext_case("Verdigris", 1));
option(loc::gettext_case("Victoria", 1));
option(loc::gettext("return"));
2020-01-01 21:29:24 +01:00
menuyoff = 10;
break;
case Menu::playint2:
Add level exploring menu for translators I would, of course, recommend translators to translate the roomnames while playing the full game (optionally in invincibility) so they can immediately get all the context and maybe the most inspiration. And if you want to go back into a specific level, then there's always the time trials and intermission replays which will give you full coverage of all the room names. However, the time trials weren't really made for room name translation. They have some annoying features like the instant restart when you press ENTER at the wrong time, they remove context clues like teleporters and companions, but the worst problem is that the last room in a level is often completely untranslatable inside the time trials because you immediately get sent to the results screen... So, I added a new menu in the translator options, "explore game", which gives you access to all the time trials and the two intermissions, from the same menu. All these time trials (which they're still based off of, under the hood) are stripped of the annoying features that come with time trials. These are the changes I made to time trial behavior in translator exploring mode: - No 3-2-1-Go! countdown - No on-screen time/death/shiny/par - ENTER doesn't restart, and the map menu works. The entire map is also revealed. - Prize for the Reckless is in its normal form - The teleporters in Entanglement Generator, Wheeler's Wormhole and Level Complete are restored as context for room names (actually, we should probably restore them in time trials anyway? Their "press to teleport" prompt is already blocked out in time trials and they do nothing other than being a checkpoint. I guess the reason they were removed was to stop people from opening the teleporter menu when that was not specifically blocked out in time trials yet.) - The companions are there at the end of levels, and behave like in no death mode (become happy and follow you to the teleporter). Also for context. - At the end of each level, you're not suddenly sent to the menu, but you can use the teleporter at your leisure just like in the intermission replays. In the Final Level, you do get sent to the menu automatically, but after a longer delay. I made another mark on VVVVVV: don't be startled, I added gamestates. I wanted all teleporters at the end of levels to behave like the ones at the end of the intermission replays, and all handling for teleporting with specific companions is already done in gamestates, so rather than adding conditional blocks across 5 or so different gamestates, it made more sense to make a single gamestate for "teleporting in translator exploring mode" (3090). I also added an alternative to having to use gamestate 3500 or 82 for the end of the final level: 3091-3092. One other thing I want to add to the "explore game" menu: a per-level count of how many room names are left to translate. That shouldn't be too difficult, and I'm planning that for the next commit.
2022-11-26 03:33:17 +01:00
start_translator_exploring = false;
option(loc::gettext_case("Vitellary", 1));
option(loc::gettext_case("Vermilion", 1));
option(loc::gettext_case("Verdigris", 1));
option(loc::gettext_case("Victoria", 1));
option(loc::gettext("return"));
2020-01-01 21:29:24 +01:00
menuyoff = 10;
break;
case Menu::continuemenu:
map.settowercolour(3);
option(loc::gettext("continue from teleporter"));
option(loc::gettext("continue from quicksave"));
option(loc::gettext("return"));
2020-01-01 21:29:24 +01:00
menuyoff = 20;
break;
case Menu::startnodeathmode:
option(loc::gettext("disable cutscenes"));
option(loc::gettext("enable cutscenes"));
option(loc::gettext("return"));
2020-01-01 21:29:24 +01:00
menuyoff = 40;
break;
case Menu::gameover:
2020-01-01 21:29:24 +01:00
menucountdown = 120;
menudest=Menu::gameover2;
break;
case Menu::gameover2:
option(loc::gettext("return to play menu"));
2020-01-01 21:29:24 +01:00
menuyoff = 80;
break;
case Menu::unlockmenutrials:
option(loc::gettext("space station 1"), !unlock[Unlock_TIMETRIAL_SPACESTATION1]);
option(loc::gettext("the laboratory"), !unlock[Unlock_TIMETRIAL_LABORATORY]);
option(loc::gettext("the tower"), !unlock[Unlock_TIMETRIAL_TOWER]);
option(loc::gettext("space station 2"), !unlock[Unlock_TIMETRIAL_SPACESTATION2]);
option(loc::gettext("the warp zone"), !unlock[Unlock_TIMETRIAL_WARPZONE]);
option(loc::gettext("the final level"), !unlock[Unlock_TIMETRIAL_FINALLEVEL]);
option(loc::gettext("return"));
2020-01-01 21:29:24 +01:00
menuyoff = 0;
break;
case Menu::timetrials:
option(loc::gettext(unlock[Unlock_TIMETRIAL_SPACESTATION1] ? "space station 1" : "???"),
unlock[Unlock_TIMETRIAL_SPACESTATION1]);
option(loc::gettext(unlock[Unlock_TIMETRIAL_LABORATORY] ? "the laboratory" : "???"),
unlock[Unlock_TIMETRIAL_LABORATORY]);
option(loc::gettext(unlock[Unlock_TIMETRIAL_TOWER] ? "the tower" : "???"),
unlock[Unlock_TIMETRIAL_TOWER]);
option(loc::gettext(unlock[Unlock_TIMETRIAL_SPACESTATION2] ? "space station 2" : "???"),
unlock[Unlock_TIMETRIAL_SPACESTATION2]);
option(loc::gettext(unlock[Unlock_TIMETRIAL_WARPZONE] ? "the warp zone" : "???"),
unlock[Unlock_TIMETRIAL_WARPZONE]);
option(loc::gettext(unlock[Unlock_TIMETRIAL_FINALLEVEL] ? "the final level" : "???"),
unlock[Unlock_TIMETRIAL_FINALLEVEL]);
option(loc::gettext("return"));
2020-01-01 21:29:24 +01:00
menuyoff = 0;
Make menus automatically centered and narrowed All menus had a hardcoded X position (offset to an arbitrary starting point of 110) and a hardcoded horizontal spacing for the "staircasing" (mostly 30 pixels, but for some specific menus hardcoded to 15, 20 or something else). Not all menus were centered, and seem to have been manually made narrower (with lower horizontal spacing) whenever text ran offscreen during development. This system may already be hard to work with in an English-only menu system, since you may need to adjust horizontal spacing or positioning when adding an option. The main reason I made this change is that it's even less optimal when menu options have to be translated, since maximum string lengths are hard to determine, and it's easy to have menu options running offscreen, especially when not all menus are checked for all languages and when options could be added in the middle of a menu after translations of that menu are already checked. Now, menus are automatically centered based on their options, and they are automatically made narrower if they won't fit with the default horizontal spacing of 30 pixels (with some padding). The game.menuxoff variable for the menu X position is now also offset to 0 instead of 110 The _default_ horizontal spacing can be changed on a per-menu basis, and most menus (not all) which already had a narrower spacing set, retain that as a maximum spacing, simply because they looked odd with 30 pixels of spacing (especially the main menu). They will be made even narrower automatically if needed. In the most extreme case, the spacing can go down to 0 and options will be displayed right below each other. This isn't in the usual style of the game, but at least we did the best we could to prevent options running offscreen. The only exception to automatic menu centering and narrowing is the list of player levels, because it's a special case and existing behavior would be better than automatic centering there.
2020-06-29 02:09:52 +02:00
maxspacing = 15;
break;
case Menu::nodeathmodecomplete:
2020-01-01 21:29:24 +01:00
menucountdown = 90;
menudest = Menu::nodeathmodecomplete2;
break;
case Menu::nodeathmodecomplete2:
option(loc::gettext("return to play menu"));
2020-01-01 21:29:24 +01:00
menuyoff = 70;
break;
case Menu::timetrialcomplete:
2020-01-01 21:29:24 +01:00
menucountdown = 90;
menudest=Menu::timetrialcomplete2;
break;
case Menu::timetrialcomplete2:
2020-01-01 21:29:24 +01:00
menucountdown = 60;
menudest=Menu::timetrialcomplete3;
break;
case Menu::timetrialcomplete3:
option(loc::gettext("return to play menu"));
option(loc::gettext("try again"));
2020-01-01 21:29:24 +01:00
menuyoff = 70;
break;
case Menu::gamecompletecontinue:
option(loc::gettext("return to play menu"));
2020-01-01 21:29:24 +01:00
menuyoff = 70;
break;
case Menu::errorsavingsettings:
option(loc::gettext("ok"));
option(loc::gettext("silence"));
menuyoff = 10;
break;
case Menu::errorloadinglevel:
case Menu::warninglevellist:
option(loc::gettext("ok"));
menuyoff = 50;
break;
2020-01-01 21:29:24 +01:00
}
Make menus automatically centered and narrowed All menus had a hardcoded X position (offset to an arbitrary starting point of 110) and a hardcoded horizontal spacing for the "staircasing" (mostly 30 pixels, but for some specific menus hardcoded to 15, 20 or something else). Not all menus were centered, and seem to have been manually made narrower (with lower horizontal spacing) whenever text ran offscreen during development. This system may already be hard to work with in an English-only menu system, since you may need to adjust horizontal spacing or positioning when adding an option. The main reason I made this change is that it's even less optimal when menu options have to be translated, since maximum string lengths are hard to determine, and it's easy to have menu options running offscreen, especially when not all menus are checked for all languages and when options could be added in the middle of a menu after translations of that menu are already checked. Now, menus are automatically centered based on their options, and they are automatically made narrower if they won't fit with the default horizontal spacing of 30 pixels (with some padding). The game.menuxoff variable for the menu X position is now also offset to 0 instead of 110 The _default_ horizontal spacing can be changed on a per-menu basis, and most menus (not all) which already had a narrower spacing set, retain that as a maximum spacing, simply because they looked odd with 30 pixels of spacing (especially the main menu). They will be made even narrower automatically if needed. In the most extreme case, the spacing can go down to 0 and options will be displayed right below each other. This isn't in the usual style of the game, but at least we did the best we could to prevent options running offscreen. The only exception to automatic menu centering and narrowing is the list of player levels, because it's a special case and existing behavior would be better than automatic centering there.
2020-06-29 02:09:52 +02:00
// Automatically center the menu. We must check the width of the menu with the initial horizontal spacing.
// If it's too wide, reduce the horizontal spacing by 5 and retry.
// Try to limit the menu width to 272 pixels: 320 minus 16*2 for square brackets, minus 8*2 padding.
// The square brackets fall outside the menu width (i.e. selected menu options are printed 16 pixels to the left)
Make menus automatically centered and narrowed All menus had a hardcoded X position (offset to an arbitrary starting point of 110) and a hardcoded horizontal spacing for the "staircasing" (mostly 30 pixels, but for some specific menus hardcoded to 15, 20 or something else). Not all menus were centered, and seem to have been manually made narrower (with lower horizontal spacing) whenever text ran offscreen during development. This system may already be hard to work with in an English-only menu system, since you may need to adjust horizontal spacing or positioning when adding an option. The main reason I made this change is that it's even less optimal when menu options have to be translated, since maximum string lengths are hard to determine, and it's easy to have menu options running offscreen, especially when not all menus are checked for all languages and when options could be added in the middle of a menu after translations of that menu are already checked. Now, menus are automatically centered based on their options, and they are automatically made narrower if they won't fit with the default horizontal spacing of 30 pixels (with some padding). The game.menuxoff variable for the menu X position is now also offset to 0 instead of 110 The _default_ horizontal spacing can be changed on a per-menu basis, and most menus (not all) which already had a narrower spacing set, retain that as a maximum spacing, simply because they looked odd with 30 pixels of spacing (especially the main menu). They will be made even narrower automatically if needed. In the most extreme case, the spacing can go down to 0 and options will be displayed right below each other. This isn't in the usual style of the game, but at least we did the best we could to prevent options running offscreen. The only exception to automatic menu centering and narrowing is the list of player levels, because it's a special case and existing behavior would be better than automatic centering there.
2020-06-29 02:09:52 +02:00
bool done_once = false;
int menuwidth = 0;
for (; !done_once || (menuwidth > 272 && menuspacing > 0); maxspacing -= 5)
Make menus automatically centered and narrowed All menus had a hardcoded X position (offset to an arbitrary starting point of 110) and a hardcoded horizontal spacing for the "staircasing" (mostly 30 pixels, but for some specific menus hardcoded to 15, 20 or something else). Not all menus were centered, and seem to have been manually made narrower (with lower horizontal spacing) whenever text ran offscreen during development. This system may already be hard to work with in an English-only menu system, since you may need to adjust horizontal spacing or positioning when adding an option. The main reason I made this change is that it's even less optimal when menu options have to be translated, since maximum string lengths are hard to determine, and it's easy to have menu options running offscreen, especially when not all menus are checked for all languages and when options could be added in the middle of a menu after translations of that menu are already checked. Now, menus are automatically centered based on their options, and they are automatically made narrower if they won't fit with the default horizontal spacing of 30 pixels (with some padding). The game.menuxoff variable for the menu X position is now also offset to 0 instead of 110 The _default_ horizontal spacing can be changed on a per-menu basis, and most menus (not all) which already had a narrower spacing set, retain that as a maximum spacing, simply because they looked odd with 30 pixels of spacing (especially the main menu). They will be made even narrower automatically if needed. In the most extreme case, the spacing can go down to 0 and options will be displayed right below each other. This isn't in the usual style of the game, but at least we did the best we could to prevent options running offscreen. The only exception to automatic menu centering and narrowing is the list of player levels, because it's a special case and existing behavior would be better than automatic centering there.
2020-06-29 02:09:52 +02:00
{
done_once = true;
menuspacing = maxspacing;
menuwidth = 0;
for (size_t i = 0; i < menuoptions.size(); i++)
{
int width = i*menuspacing + font::len(menuoptions[i].print_flags, menuoptions[i].text);
Make menus automatically centered and narrowed All menus had a hardcoded X position (offset to an arbitrary starting point of 110) and a hardcoded horizontal spacing for the "staircasing" (mostly 30 pixels, but for some specific menus hardcoded to 15, 20 or something else). Not all menus were centered, and seem to have been manually made narrower (with lower horizontal spacing) whenever text ran offscreen during development. This system may already be hard to work with in an English-only menu system, since you may need to adjust horizontal spacing or positioning when adding an option. The main reason I made this change is that it's even less optimal when menu options have to be translated, since maximum string lengths are hard to determine, and it's easy to have menu options running offscreen, especially when not all menus are checked for all languages and when options could be added in the middle of a menu after translations of that menu are already checked. Now, menus are automatically centered based on their options, and they are automatically made narrower if they won't fit with the default horizontal spacing of 30 pixels (with some padding). The game.menuxoff variable for the menu X position is now also offset to 0 instead of 110 The _default_ horizontal spacing can be changed on a per-menu basis, and most menus (not all) which already had a narrower spacing set, retain that as a maximum spacing, simply because they looked odd with 30 pixels of spacing (especially the main menu). They will be made even narrower automatically if needed. In the most extreme case, the spacing can go down to 0 and options will be displayed right below each other. This isn't in the usual style of the game, but at least we did the best we could to prevent options running offscreen. The only exception to automatic menu centering and narrowing is the list of player levels, because it's a special case and existing behavior would be better than automatic centering there.
2020-06-29 02:09:52 +02:00
if (width > menuwidth)
menuwidth = width;
}
}
menuxoff = (320-menuwidth)/2;
2020-01-01 21:29:24 +01:00
}
void Game::deletequick(void)
2020-01-01 21:29:24 +01:00
{
if (inspecial() || map.custommode)
{
return;
}
if (!FILESYSTEM_delete("saves/qsave.vvv"))
{
vlog_error("Error deleting saves/qsave.vvv");
}
else
{
SDL_zero(last_quicksave);
}
2020-01-01 21:29:24 +01:00
}
void Game::deletetele(void)
2020-01-01 21:29:24 +01:00
{
if (inspecial() || map.custommode)
{
return;
}
if (!FILESYSTEM_delete("saves/tsave.vvv"))
{
vlog_error("Error deleting saves/tsave.vvv");
}
else
{
SDL_zero(last_telesave);
}
2020-01-01 21:29:24 +01:00
}
void Game::customdeletequick(const std::string& file)
{
const std::string path = "saves/" + file.substr(7) + ".vvv";
if (!FILESYSTEM_delete(path.c_str()))
{
vlog_error("Error deleting %s", path.c_str());
}
}
void Game::swnpenalty(void)
2020-01-01 21:29:24 +01:00
{
//set the SWN clock back to the closest 5 second interval
if (swntimer <= 150)
{
swntimer += 8;
if (swntimer > 150) swntimer = 150;
}
else if (swntimer <= 300)
{
swntimer += 8;
if (swntimer > 300) swntimer = 300;
}
else if (swntimer <= 450)
{
swntimer += 8;
if (swntimer > 450) swntimer = 450;
}
else if (swntimer <= 600)
{
swntimer += 8;
if (swntimer > 600) swntimer = 600;
}
else if (swntimer <= 750)
{
swntimer += 8;
if (swntimer > 750) swntimer = 750;
}
else if (swntimer <= 900)
{
swntimer += 8;
if (swntimer > 900) swntimer = 900;
}
else if (swntimer <= 1050)
{
swntimer += 8;
if (swntimer > 1050) swntimer = 1050;
}
else if (swntimer <= 1200)
{
swntimer += 8;
if (swntimer > 1200) swntimer = 1200;
}
else if (swntimer <= 1350)
{
swntimer += 8;
if (swntimer > 1350) swntimer = 1350;
}
else if (swntimer <= 1500)
{
swntimer += 8;
if (swntimer > 1500) swntimer = 1500;
}
else if (swntimer <= 1650)
{
swntimer += 8;
if (swntimer > 1650) swntimer = 1650;
}
else if (swntimer <= 1800)
{
swntimer += 8;
if (swntimer > 1800) swntimer = 1800;
}
else if (swntimer <= 2100)
{
swntimer += 8;
if (swntimer > 2100) swntimer = 2100;
}
else if (swntimer <= 2400)
{
swntimer += 8;
if (swntimer > 2400) swntimer = 2400;
}
}
int Game::crewrescued(void)
2020-01-01 21:29:24 +01:00
{
int temp = 0;
for (size_t i = 0; i < SDL_arraysize(crewstats); i++)
{
if (crewstats[i])
{
temp++;
}
}
return temp;
2020-01-01 21:29:24 +01:00
}
void Game::resetgameclock(void)
2020-01-01 21:29:24 +01:00
{
frames = 0;
seconds = 0;
minutes = 0;
hours = 0;
}
int Game::trinkets(void)
{
int temp = 0;
for (size_t i = 0; i < SDL_arraysize(obj.collect); i++)
{
if (obj.collect[i])
{
temp++;
}
}
return temp;
}
int Game::crewmates(void)
{
int temp = 0;
for (size_t i = 0; i < SDL_arraysize(obj.customcollect); i++)
{
if (obj.customcollect[i])
{
temp++;
}
}
return temp;
}
bool Game::anything_unlocked(void)
{
for (size_t i = 0; i < SDL_arraysize(unlock); i++)
{
if (unlock[i] &&
(i == 8 // Secret Lab
|| (i >= 9 && i <= 14) // any Time Trial
|| i == 16 // Intermission replays
|| i == 17 // No Death Mode
|| i == 18)) // Flip Mode
{
return true;
}
}
return false;
}
bool Game::save_exists(void)
{
return last_telesave.exists || last_quicksave.exists;
}
static void hardreset(void)
{
script.hardreset();
}
static void returntoeditor_callback(void)
{
extern Game game;
game.returntoeditor();
ed.show_note(loc::gettext("Level quits to menu"));
}
void Game::quittomenu(void)
{
if (gamestate != EDITORMODE && map.custommode && !map.custommodeforreal)
{
/* We are playtesting! Go back to the editor
* instead of losing unsaved changes. */
/* This needs to be deferred, otherwise some state would persist. */
DEFER_CALLBACK(returntoeditor_callback);
return;
}
gamestate = TITLEMODE;
Enumify all fade modes This removes the magic numbers previously used for controlling the fade mode, which are really not readable at all unless you already know what they mean. 0: FADE_NONE 1: FADE_FULLY_BLACK 2: FADE_START_FADEOUT 3: FADE_FADING_OUT 4: FADE_START_FADEIN 5: FADE_FADING_IN There is also the macro FADEMODE_IS_FADING, which indicates when the intention is to only check if the game is fading right now, which wasn't clearly conveyed previously. I also took the opportunity to clean up the style of any lines I touched. This included rewriting if-else chains into case-switches, turning one-liner if-then statements into proper blocks, fixing up comments, and even commenting the `fademode == FADE_NONE` on the tower spike checks (which, it was previously undocumented why that check was there, but I think I know why it's there). As for type safety, we already get some by transforming the variable types into the enum. Assignment is prohibited without a cast. But, apparently, comparison is perfectly legal and won't even give so much as a warning. To work around this and make absolutely sure I made all existing comparisons now use the enum, I temporarily changed it to be an `enum class`, which is a C++11 feature that makes it so all comparisons are illegal. Unfortunately, it scopes them in a namespace with the same name as a class, so I had to temporarily define macros to make sure my existing code worked. I also had to temporarily up the standard in CMakeLists.txt to get it to compile. But after all that was done, I found the rest of the places where a comparison to an integer was used, and fixed them.
2022-04-25 09:57:47 +02:00
graphics.fademode = FADE_START_FADEIN;
FILESYSTEM_unmountAssets();
loc::unloadtext_custom();
Start rewrite of font system This is still a work in progress, but the existing font system has been removed and replaced by a new one, in Font.cpp. Design goals of the new font system include supporting colored button glyphs, different fonts for different languages, and larger fonts than 8x8 for Chinese, Japanese and Korean, while being able to support their 30000+ characters without hiccups, slowdowns or high memory usage. And to have more flexibility with fonts in general. Plus, Graphics.cpp was long enough as-is, so it's good to have a dedicated file for font storage. The old font system worked with a std::vector<SDL_Surface*> to store 8x8 surfaces for each character, and a std::map<int,int> to store mappings between codepoints and vector indexes. The new system has a per-font collection of pages for every block of 0x1000 (4096) codepoints, that may be allocated as needed. A glyph on a page contains the index of the glyph in the image (giving its coordinates), the advance (how much the cursor should advance, so the width of that glyph) and some flags which would be at least whether the glyph exists and whether it is colored. Most of the *new* features aren't implemented yet; it's currently hardcoded to the regular 8x8 font.png, but it should be functionally equivalent to the previous behavior. The only thing that doesn't really work yet is level-specific font.png, but that'll be supported again soon enough. This commit also adds fontmeta (xml) support. Since the fonts folder is mounted at graphics/, there are two main options for recognizing non-font.png fonts: the font files have to be prefixed with font (or font_) or some special file extension is involved to signal what files are fonts. I always had a font.xml in mind (so font_cn.xml, font_ja.xml, etc) but if there's ever gonna be a need for further xml files inside the graphics folder, we have a problem. So I named them .fontmeta instead. A .fontmeta file looks somewhat like this: <?xml version="1.0" encoding="UTF-8"?> <font_metadata> <width>12</width> <height>12</height> <white_teeth>1</white_teeth> <chars> <range start="0x20" end="0x7E"/> <range start="0x80" end="0x80"/> <range start="0xA0" end="0xDF"/> <range start="0x250" end="0x2A8"/> <range start="0x2AD" end="0x2AD"/> <range start="0x2C7" end="0x2C7"/> <range start="0x2C9" end="0x2CB"/> ... </chars> <special> <range start="0x00" end="0x1F" advance="6"/> <range start="0x61" end="0x66" color="1"/> <range start="0x63" end="0x63" color="0"/> </special> </font_metadata> The <chars> tag can be used to specify characters instead of in a .txt. The original idea was to just always use the existing .txt system for specifying the font charset, and only use the XML for the other stuff that the .txt doesn't cover. However, it's probably better to keep it simple if possible - having to only have a .png and a .fontmeta seems simpler than having the data spread out over three files. And a major advantage: Chinese fonts can have about 30000 characters! It's more efficient to be able to have a tag saying "now there's 20902 characters starting at U+4E00" than to include them all in a text file and having to UTF-8 decode every single one of them. If a font.txt exists, it takes priority over the <chars> tag, and in that case, there's no reason to include the <chars> tag in the XML. But font.txt has to be in the same directory as font.png, otherwise it is rejected. Same for font.fontmeta. If neither font.txt nor <chars> exist, then the font is seen as a 2.2-and-below-style ASCII font. In <special>: advance is the number of pixels the cursor advances after drawing the character (so the width of the character, without affecting the grid in the source image), color is whether the character should have its original colors retained when printed (for button glyphs). As for <white_teeth>: The renderer PR has replaced draw-time whitening of sprites/etc (using BlitSurfaceColoured) by load-time whitening of entire images (using LoadImage with TEX_WHITE as an argument). This means we have a problem: fonts have always had their glyphs whitened at printing time, and since I'm adding support for colored button glyphs, I changed it so glyphs would sometimes not be whitened. But if we can't whiten at print time, then we'd need to whiten at load time, and if we whiten the entire font, any colored glyphs will get destroyed too. If you whiten the image selectively, well, we need more code to target specific squares in the image, and it's kind of a waste when you need to whiten 30000 12x12 Chinese characters when you're only going to need a handful, but you don't know which ones. The solution: Whitening fonts is useless if all the non-colored glyphs are already white, so we don't need to do it anyway! However, any existing fonts that have non-white glyphs (and I know of at least one level like that) will still need to be whitened. So there is now a font property <white_teeth> that can be specified in the fontmeta, which indicates that the font is already pre-whitened. If not specified, traditional whitening behavior will be used, and the font cannot use colored glyphs.
2023-01-02 05:14:53 +01:00
font::unload_custom();
cliplaytest = false;
graphics.titlebg.tdrawback = true;
graphics.flipmode = false;
//Don't be stuck on the summary screen,
//or "who do you want to play the level with?"
//or "do you want cutscenes?"
//or the confirm-load-quicksave menu
if (translator_cutscene_test)
{
returntomenu(Menu::translator_options_cutscenetest);
}
else if (translator_exploring)
Add level exploring menu for translators I would, of course, recommend translators to translate the roomnames while playing the full game (optionally in invincibility) so they can immediately get all the context and maybe the most inspiration. And if you want to go back into a specific level, then there's always the time trials and intermission replays which will give you full coverage of all the room names. However, the time trials weren't really made for room name translation. They have some annoying features like the instant restart when you press ENTER at the wrong time, they remove context clues like teleporters and companions, but the worst problem is that the last room in a level is often completely untranslatable inside the time trials because you immediately get sent to the results screen... So, I added a new menu in the translator options, "explore game", which gives you access to all the time trials and the two intermissions, from the same menu. All these time trials (which they're still based off of, under the hood) are stripped of the annoying features that come with time trials. These are the changes I made to time trial behavior in translator exploring mode: - No 3-2-1-Go! countdown - No on-screen time/death/shiny/par - ENTER doesn't restart, and the map menu works. The entire map is also revealed. - Prize for the Reckless is in its normal form - The teleporters in Entanglement Generator, Wheeler's Wormhole and Level Complete are restored as context for room names (actually, we should probably restore them in time trials anyway? Their "press to teleport" prompt is already blocked out in time trials and they do nothing other than being a checkpoint. I guess the reason they were removed was to stop people from opening the teleporter menu when that was not specifically blocked out in time trials yet.) - The companions are there at the end of levels, and behave like in no death mode (become happy and follow you to the teleporter). Also for context. - At the end of each level, you're not suddenly sent to the menu, but you can use the teleporter at your leisure just like in the intermission replays. In the Final Level, you do get sent to the menu automatically, but after a longer delay. I made another mark on VVVVVV: don't be startled, I added gamestates. I wanted all teleporters at the end of levels to behave like the ones at the end of the intermission replays, and all handling for teleporting with specific companions is already done in gamestates, so rather than adding conditional blocks across 5 or so different gamestates, it made more sense to make a single gamestate for "teleporting in translator exploring mode" (3090). I also added an alternative to having to use gamestate 3500 or 82 for the end of the final level: 3091-3092. One other thing I want to add to the "explore game" menu: a per-level count of how many room names are left to translate. That shouldn't be too difficult, and I'm planning that for the next commit.
2022-11-26 03:33:17 +01:00
{
returntomenu(Menu::translator_options_exploregame);
}
else if (intimetrial)
{
returntomenu(Menu::timetrials);
}
else if (inintermission)
{
returntomenu(Menu::intermissionmenu);
}
else if (nodeathmode)
{
returntomenu(Menu::playmodes);
}
else if (map.custommode)
{
Clean up all exit paths to the menu to use common code There are multiple different exit paths to the main menu. In 2.2, they all had a bunch of copy-pasted code. In 2.3 currently, most of them use game.quittomenu(), but there are some stragglers that still use hand-copied code. This is a bit of a problem, because all exit paths should consistently have FILESYSTEM_unmountassets(), as part of the 2.3 feature of per-level custom assets. Furthermore, most (but not all) of the paths call script.hardreset() too, and some of the stragglers don't. So there could be something persisting through to the title screen (like a really long flash/shake timer) that could only persist if exiting to the title screen through those paths. But, actually, it seems like there's a good reason for some of those to not call script.hardreset() - namely, dying or completing No Death Mode and completing a Time Trial presents some information onscreen that would get reset by script.hardreset(), so I'll fix that in a later commit. So what I've done for this commit is found every exit path that didn't already use game.quittomenu(), and made them use game.quittomenu(). As well, some of them had special handling that existed on top of them already having a corresponding entry in game.quittomenu() (but the path would take the special handling because it never did game.quittomenu()), so I removed that special handling as well (e.g. exiting from a custom level used returntomenu(Menu::levellist) when quittomenu() already had that same returntomenu()). The menu that exiting from the level editor returns to is now handled in game.quittomenu() as well, where the map.custommode branch now also checks for map.custommodeforreal. Unfortunately, it seems like entering the level editor doesn't properly initialize map.custommode, so entering the level editor now initializes map.custommode, too. I've also taken the music.play(6) out of game.quittomenu(), because not all exit paths immediately play Presenting VVVVVV, so all exit paths that DO immediately play Presenting VVVVVV now have music.play(6) special-cased for them, which is fine enough for me. Here is the list of all exit paths to the menu: - Exiting through the pause menu (without glitchrunner mode) - Exiting through the pause menu (with glitchrunner mode) - Completing a custom level - Completing a Time Trial - Dying in No Death Mode - Completing No Death Mode - Completing an Intermission replay - Exiting from the level editor - Completing the main game
2021-01-07 23:20:37 +01:00
if (map.custommodeforreal)
{
returntomenu(Menu::levellist);
}
else
{
//Returning from editor
editor_disabled = !BUTTONGLYPHS_keyboard_is_available();
Clean up all exit paths to the menu to use common code There are multiple different exit paths to the main menu. In 2.2, they all had a bunch of copy-pasted code. In 2.3 currently, most of them use game.quittomenu(), but there are some stragglers that still use hand-copied code. This is a bit of a problem, because all exit paths should consistently have FILESYSTEM_unmountassets(), as part of the 2.3 feature of per-level custom assets. Furthermore, most (but not all) of the paths call script.hardreset() too, and some of the stragglers don't. So there could be something persisting through to the title screen (like a really long flash/shake timer) that could only persist if exiting to the title screen through those paths. But, actually, it seems like there's a good reason for some of those to not call script.hardreset() - namely, dying or completing No Death Mode and completing a Time Trial presents some information onscreen that would get reset by script.hardreset(), so I'll fix that in a later commit. So what I've done for this commit is found every exit path that didn't already use game.quittomenu(), and made them use game.quittomenu(). As well, some of them had special handling that existed on top of them already having a corresponding entry in game.quittomenu() (but the path would take the special handling because it never did game.quittomenu()), so I removed that special handling as well (e.g. exiting from a custom level used returntomenu(Menu::levellist) when quittomenu() already had that same returntomenu()). The menu that exiting from the level editor returns to is now handled in game.quittomenu() as well, where the map.custommode branch now also checks for map.custommodeforreal. Unfortunately, it seems like entering the level editor doesn't properly initialize map.custommode, so entering the level editor now initializes map.custommode, too. I've also taken the music.play(6) out of game.quittomenu(), because not all exit paths immediately play Presenting VVVVVV, so all exit paths that DO immediately play Presenting VVVVVV now have music.play(6) special-cased for them, which is fine enough for me. Here is the list of all exit paths to the menu: - Exiting through the pause menu (without glitchrunner mode) - Exiting through the pause menu (with glitchrunner mode) - Completing a custom level - Completing a Time Trial - Dying in No Death Mode - Completing No Death Mode - Completing an Intermission replay - Exiting from the level editor - Completing the main game
2021-01-07 23:20:37 +01:00
returntomenu(Menu::playerworlds);
}
}
else if (save_exists() || anything_unlocked())
{
returntomenu(Menu::play);
if (!insecretlab)
{
//Select "continue"
currentmenuoption = 0;
}
}
else
{
createmenu(Menu::mainmenu);
}
/* We might not be at the end of the frame yet.
* If we hardreset() now, some state might still persist. */
DEFER_CALLBACK(hardreset);
}
void Game::returntolab(void)
{
gamestate = GAMEMODE;
Enumify all fade modes This removes the magic numbers previously used for controlling the fade mode, which are really not readable at all unless you already know what they mean. 0: FADE_NONE 1: FADE_FULLY_BLACK 2: FADE_START_FADEOUT 3: FADE_FADING_OUT 4: FADE_START_FADEIN 5: FADE_FADING_IN There is also the macro FADEMODE_IS_FADING, which indicates when the intention is to only check if the game is fading right now, which wasn't clearly conveyed previously. I also took the opportunity to clean up the style of any lines I touched. This included rewriting if-else chains into case-switches, turning one-liner if-then statements into proper blocks, fixing up comments, and even commenting the `fademode == FADE_NONE` on the tower spike checks (which, it was previously undocumented why that check was there, but I think I know why it's there). As for type safety, we already get some by transforming the variable types into the enum. Assignment is prohibited without a cast. But, apparently, comparison is perfectly legal and won't even give so much as a warning. To work around this and make absolutely sure I made all existing comparisons now use the enum, I temporarily changed it to be an `enum class`, which is a C++11 feature that makes it so all comparisons are illegal. Unfortunately, it scopes them in a namespace with the same name as a class, so I had to temporarily define macros to make sure my existing code worked. I also had to temporarily up the standard in CMakeLists.txt to get it to compile. But after all that was done, I found the rest of the places where a comparison to an integer was used, and fixed them.
2022-04-25 09:57:47 +02:00
graphics.fademode = FADE_START_FADEIN;
map.gotoroom(119, 107);
int player = obj.getplayer();
if (INBOUNDS_VEC(player, obj.entities))
{
obj.entities[player].xp = 132;
obj.entities[player].yp = 137;
}
gravitycontrol = 0;
savepoint = 0;
saverx = 119;
savery = 107;
savex = 132;
savey = 137;
savegc = 0;
if (INBOUNDS_VEC(player, obj.entities))
{
savedir = obj.entities[player].dir;
}
music.play(Music_PIPEDREAM);
}
static void resetbg(void)
{
graphics.backgrounddrawn = false;
}
void Game::returntoeditor(void)
{
gamestate = EDITORMODE;
graphics.textboxes.clear();
hascontrol = true;
advancetext = false;
completestop = false;
2022-12-07 00:20:48 +01:00
setstate(0);
graphics.showcutscenebars = false;
Enumify all fade modes This removes the magic numbers previously used for controlling the fade mode, which are really not readable at all unless you already know what they mean. 0: FADE_NONE 1: FADE_FULLY_BLACK 2: FADE_START_FADEOUT 3: FADE_FADING_OUT 4: FADE_START_FADEIN 5: FADE_FADING_IN There is also the macro FADEMODE_IS_FADING, which indicates when the intention is to only check if the game is fading right now, which wasn't clearly conveyed previously. I also took the opportunity to clean up the style of any lines I touched. This included rewriting if-else chains into case-switches, turning one-liner if-then statements into proper blocks, fixing up comments, and even commenting the `fademode == FADE_NONE` on the tower spike checks (which, it was previously undocumented why that check was there, but I think I know why it's there). As for type safety, we already get some by transforming the variable types into the enum. Assignment is prohibited without a cast. But, apparently, comparison is perfectly legal and won't even give so much as a warning. To work around this and make absolutely sure I made all existing comparisons now use the enum, I temporarily changed it to be an `enum class`, which is a C++11 feature that makes it so all comparisons are illegal. Unfortunately, it scopes them in a namespace with the same name as a class, so I had to temporarily define macros to make sure my existing code worked. I also had to temporarily up the standard in CMakeLists.txt to get it to compile. But after all that was done, I found the rest of the places where a comparison to an integer was used, and fixed them.
2022-04-25 09:57:47 +02:00
graphics.fademode = FADE_NONE;
ed.keydelay = 6;
ed.settingskey = true;
ed.old_note_timer = 0;
ed.note_timer = 0;
ed.roomnamehide = 0;
// Might've been changed in a script
font::set_level_font(cl.level_font_name.c_str());
DEFER_CALLBACK(resetbg);
music.fadeout();
//If warpdir() is used during playtesting, we need to set it back after!
for (int j = 0; j < cl.maxheight; j++)
{
for (int i = 0; i < cl.maxwidth; i++)
{
cl.roomproperties[i+(j*cl.maxwidth)].warpdir=ed.kludgewarpdir[i+(j*cl.maxwidth)];
}
}
graphics.titlebg.scrolldir = 0;
graphics.backgrounddrawn = false;
graphics.foregrounddrawn = false;
}
static void returntoingametemp(void)
{
extern Game game;
game.returntomenu(game.kludge_ingametemp);
}
static void returntoedsettings(void)
{
extern Game game;
game.returntomenu(Menu::ed_settings);
}
static void nextbgcolor(void)
{
map.nexttowercolour();
}
static void setfademode(void)
{
graphics.fademode = graphics.ingame_fademode;
}
static void setflipmode(void)
{
graphics.flipmode = graphics.setflipmode;
}
void Game::returntoingame(void)
{
ingame_titlemode = false;
mapheld = true;
if (ingame_editormode)
Fix being able to circumvent not-in-Flip-Mode detection So you get a trophy and achievement for completing the game in Flip Mode. Which begs the question, how does the game know that you've played through the game in Flip Mode the entire way, and haven't switched it off at any point? It looks like if you play normally all the way up until the checkpoint in V, and then turn on Flip Mode, the game won't give you the trophy. What gives? Well, actually, what happens is that every time you press Enter on a teleporter, the game will set flag 73 to true if you're NOT in Flip Mode. Then when Game Complete runs, the game will check if flag 73 is off, and then give you the achievement and trophy accordingly. However, what this means is that you could just save your game before pressing Enter on a teleporter, then quit and go into options, turn on Flip Mode, use the teleporter, then save your game (it's automatically saved since you just used a teleporter), quit and go into options, and turn it off. Then you'd get the Flip Mode trophy even though you haven't actually played the entire game in Flip Mode. Furthermore, in 2.3 you can bring up the pause menu to toggle Flip Mode, so you don't even have to quit to circumvent this detection. To fix both of these exploits, I moved the turning on of flag 73 to starting a new game, loading a quicksave, and loading a telesave (cases 0, 1, and 2 respectively in scriptclass::startgamemode()). I also added a Flip Mode check to the routine that runs whenever you exit an options menu back to the pause menu, so you can't circumvent the detection that way, either.
2020-07-11 01:30:28 +02:00
{
ingame_editormode = false;
DEFER_CALLBACK(returntoedsettings);
gamestate = EDITORMODE;
ed.settingskey = true;
}
else
{
DEFER_CALLBACK(returntoingametemp);
gamestate = MAPMODE;
DEFER_CALLBACK(setflipmode);
DEFER_CALLBACK(setfademode);
if (!map.custommode && !graphics.setflipmode)
{
obj.flags[73] = true;
}
Fix being able to circumvent not-in-Flip-Mode detection So you get a trophy and achievement for completing the game in Flip Mode. Which begs the question, how does the game know that you've played through the game in Flip Mode the entire way, and haven't switched it off at any point? It looks like if you play normally all the way up until the checkpoint in V, and then turn on Flip Mode, the game won't give you the trophy. What gives? Well, actually, what happens is that every time you press Enter on a teleporter, the game will set flag 73 to true if you're NOT in Flip Mode. Then when Game Complete runs, the game will check if flag 73 is off, and then give you the achievement and trophy accordingly. However, what this means is that you could just save your game before pressing Enter on a teleporter, then quit and go into options, turn on Flip Mode, use the teleporter, then save your game (it's automatically saved since you just used a teleporter), quit and go into options, and turn it off. Then you'd get the Flip Mode trophy even though you haven't actually played the entire game in Flip Mode. Furthermore, in 2.3 you can bring up the pause menu to toggle Flip Mode, so you don't even have to quit to circumvent this detection. To fix both of these exploits, I moved the turning on of flag 73 to starting a new game, loading a quicksave, and loading a telesave (cases 0, 1, and 2 respectively in scriptclass::startgamemode()). I also added a Flip Mode check to the routine that runs whenever you exit an options menu back to the pause menu, so you can't circumvent the detection that way, either.
2020-07-11 01:30:28 +02:00
}
DEFER_CALLBACK(nextbgcolor);
}
void Game::unlockAchievement(const char* name)
{
#ifdef MAKEANDPLAY
UNUSED(name);
#else
if (map.custommode)
{
return;
}
vlog_debug("Achievement \"%s\" unlocked.", name);
NETWORK_unlockAchievement(name);
#endif
2020-08-01 22:04:37 +02:00
}
2020-12-28 23:23:35 +01:00
void Game::mapmenuchange(const enum GameGamestate newgamestate, const bool user_initiated)
2020-12-28 23:23:35 +01:00
{
if (user_initiated && graphics.resumegamemode)
{
return;
}
prevgamestate = gamestate;
2020-12-28 23:23:35 +01:00
gamestate = newgamestate;
graphics.resumegamemode = false;
mapheld = true;
gameScreen.recacheTextures();
2020-12-28 23:23:35 +01:00
if (prevgamestate == GAMEMODE)
{
graphics.menuoffset = 240;
}
else
2020-12-28 23:23:35 +01:00
{
graphics.menuoffset = 0;
2020-12-28 23:23:35 +01:00
}
graphics.oldmenuoffset = graphics.menuoffset;
}
void Game::copyndmresults(void)
{
ndmresultcrewrescued = crewrescued();
ndmresulttrinkets = trinkets();
ndmresulthardestroom = loc::gettext_roomname(false, hardestroom_x, hardestroom_y, hardestroom.c_str(), hardestroom_specialname);
SDL_memcpy(ndmresultcrewstats, crewstats, sizeof(ndmresultcrewstats));
}
static inline int get_framerate(const int slowdown)
{
switch (slowdown)
{
case 30:
return 34;
case 24:
return 41;
case 18:
return 55;
case 12:
return 83;
}
return 34;
}
int Game::get_timestep(void)
{
switch (gamestate)
{
case GAMEMODE:
return get_framerate(slowdown);
default:
return 34;
}
}
bool Game::physics_frozen(void)
{
return roomname_translator::is_pausing() || level_debugger::is_pausing();
}
bool Game::incompetitive(void)
{
Move Secret Lab nocompetitive check to Super Gravitron It turns out, despite the game attempting to prevent you from using invincibility or slowdown in the Super Gravitron by simply preventing you from entering the Secret Lab from the menu, it's still possible to enter the Super Gravitron with it anyways. Just have invincibility or slowdown (or both!) enabled, enter the game normally, and talk to Victoria when you have 20 trinkets, to start the epilogue cutscene. Yeah, that's a pretty big gaping hole right there... It's also possible to do a trick that speedrunners use called telejumping to the Secret Lab to bypass the invincibility/slowdown check, too. So rather than single-case patch both of these, I'm going to fix it as generally as possible, by moving the invincibility/slowdown check to the gamestate that starts the Super Gravitron, gamestate 9. If you have invincibility/slowdown enabled, you immediately get sent back to the Secret Lab. However, this check is ignored in custom levels, because custom levels may want to use the Super Gravitron and let players have invincibility/slowdown while doing so (and there are in fact custom levels out in the wild that use the Super Gravitron; it was like one of the first things done when people discovered internal scripting). No message pops up when the game sends you back to the Secret Lab, but no message popped up when the Secret Lab menu option was disabled previously in the first place, so I haven't made anything WORSE, per se. A nice effect of this is that you can have invincibility/slowdown enabled and still be able to go to the Secret Lab from the menu. This is useful if you just want to check your trophies and leave, without having to go out of your way to disable invincibility/slowdown just to go inside.
2021-05-04 03:57:13 +02:00
return (
!map.custommode
&& swnmode
&& (swngame == SWN_SUPERGRAVITRON ||
swngame == SWN_START_SUPERGRAVITRON_STEP_1 ||
swngame == SWN_START_SUPERGRAVITRON_STEP_2)
Move Secret Lab nocompetitive check to Super Gravitron It turns out, despite the game attempting to prevent you from using invincibility or slowdown in the Super Gravitron by simply preventing you from entering the Secret Lab from the menu, it's still possible to enter the Super Gravitron with it anyways. Just have invincibility or slowdown (or both!) enabled, enter the game normally, and talk to Victoria when you have 20 trinkets, to start the epilogue cutscene. Yeah, that's a pretty big gaping hole right there... It's also possible to do a trick that speedrunners use called telejumping to the Secret Lab to bypass the invincibility/slowdown check, too. So rather than single-case patch both of these, I'm going to fix it as generally as possible, by moving the invincibility/slowdown check to the gamestate that starts the Super Gravitron, gamestate 9. If you have invincibility/slowdown enabled, you immediately get sent back to the Secret Lab. However, this check is ignored in custom levels, because custom levels may want to use the Super Gravitron and let players have invincibility/slowdown while doing so (and there are in fact custom levels out in the wild that use the Super Gravitron; it was like one of the first things done when people discovered internal scripting). No message pops up when the game sends you back to the Secret Lab, but no message popped up when the Secret Lab menu option was disabled previously in the first place, so I haven't made anything WORSE, per se. A nice effect of this is that you can have invincibility/slowdown enabled and still be able to go to the Secret Lab from the menu. This is useful if you just want to check your trophies and leave, without having to go out of your way to disable invincibility/slowdown just to go inside.
2021-05-04 03:57:13 +02:00
)
|| intimetrial
|| nodeathmode;
}
bool Game::nocompetitive(void)
{
return slowdown < 30 || map.invincibility;
}
2021-08-05 23:31:20 +02:00
bool Game::nocompetitive_unless_translator(void)
{
return slowdown < 30 || (map.invincibility && !roomname_translator::enabled);
}
void Game::sabotage_time_trial(void)
{
timetrialcheater = true;
hours++;
deathcounts += 100;
timetrialparlost = true;
}
bool Game::isingamecompletescreen(void)
2021-08-05 23:31:20 +02:00
{
return (state >= 3501 && state <= 3518) || (state >= 3520 && state <= 3522);
}