Some discussion on the Discord server resulted in this change. It's a
quality-of-life improvement where, if the game is in slowdown mode, it
will return to 100% speed for the duration of the death animation.
The reasoning is obvious. There is nothing to do during the death
animation, so making it take longer during slowdown is just an annoyance
to the player, almost a penalty for them using an accessibility option.
This is the same reason why slowdown no longer applies in menus, etc.
This makes it so that it is possible to obtain the Master of the
Universe trophy/achievement, usually unlocked by beating No Death Mode,
outside of NDM.
There are several conditions that need to be met:
1. The game needs to be started from a new game and cannot be from
loading a save.
2. Accessibility modes (invincibility and slowdown) must never be
enabled.
If either condition is violated, then the boolean that keeps track of
NDM eligibility will be set to false.
If there was a scaling mode value (serialized in the XML as <stretch>
for legacy reasons) that was not 0 or 1 or 2, then the rectangle with
the stretch information would not be initialized by get_stretch_info,
which would lead to a crash, either from dividing by zero (most likely)
or from reading an uninitialized value.
To fix this, when reading <stretch>, normalize it to a sane default if
the value is otherwise bogus. And for good measure, an assertion is
added in get_stretch_info() if the value is still somehow bogus.
Fixes#1155.
These weren't getting updated when cycling language with CTRL+F8. This
is because they would be already baked. Luckily, at least the bool
keeping track of whether or not to translate them in the first place
already exists, so we can just rely on that.
Not gonna lie, I am a bit disappointed at having to do this, because it
actually worked pretty well despite a few bugs depending on which
language you entered with. But that's only because I'm working with
the official translation files, which are in sync with each other.
With translation files that are completely arbitrary, it would be
apparent that switching languages during the cutscene test doesn't
really make sense. Like, at all. That's because the list of cutscenes is
populated entirely from language-specific XML and the cutscenes in them
are also from language-specific XML. So keeping the same position in the
menu doesn't really make sense, and keeping the same position in a
cutscene definitely doesn't make sense.
In order to be able to retranslate the game time text box in particular,
I had to create new variables to bake the saved time, since the existing
savetime variable is just an std::string. From there, the saved time can
be retranslated on-the-fly.
This adds an attribute to textboxclass to allow a text box to keep an
index that references another text box inside the graphics.textboxes
std::vector.
This is needed because the second text box of a "You have found a
shiny trinket!" or "You have found a lost crewmate!" pair of text boxes
explicitly relies on the height of the first text box. With this, I have
moved those text boxes over to the new text box translation system.
Since the update order now matters, I added a comment to
recomputetextboxes() that clarifies that the text boxes must be updated
in linear order, starting from 0.
These seemed annoying to do without copy-pasting, because I didn't want
to make a separate function for every single dialogue, and I didn't know
how to pass through the English text, until I realized that I can just
use the existing original.lines vector in the text box to store the
English text. After that, getting it translated on-the-fly isn't too
bad.
Several text boxes in the gamestate system are unused and are
untranslated. To prevent them from becoming empty when retranslating
text boxes, we need to save their original context by calling
graphics.textboxoriginalcontextauto() (which is just
graphics.textboxoriginalcontext() but automatically saving whatever is
already in the text box at the time).
This splits the text wrapping functionality of Graphics::textboxwrap to
a new function textboxclass::wrap, and with this new function, some more
text boxes can be moved to the new TEXTTRANSLATE_FUNCTION system.
Namely, Game Saved (specifically the game failing to save text box),
instructional text boxes in Space Station 1, and the Intermission 1
instructional text boxes.
This adds a way to save the text box state of the crew remaining, ACTION
prompt, etc. text boxes by just letting there be a function that is
called to retranslate the text box when needed.
It also adds a way to ignore translating a text box and to leave it
alone, in case there's actually no text in the text box, which is the
case with Level Complete and Game Complete.
Both ways are now in an enum, TextboxTranslate. The former is
TEXTTRANSLATE_FUNCTION and the latter is TEXTTRANSLATE_NONE. The
existing way of translating text boxes became TEXTTRANSLATE_CUTSCENE,
since it's only used for cutscene scripts.
Here's a quick guide to the three ways of creating a text box now.
- TEXTTRANSLATE_NONE: You must call
graphics.textboxoriginalcontextauto() to save the existing text to the
original context of the text box, as that will be copied back to the
text box after the text of the text box is updated due to not having a
translation.
- TEXTTRANSLATE_CUTSCENE: Translates the text from cutscenes.xml, and
overrides the spacing (padding and text centering). Shouldn't need to
be used outside of scriptclass.
- TEXTTRANSLATE_FUNCTION: You must pass in a function that takes in a
single parameter, a pointer to the textboxclass object to be modified.
General advice when retranslating text is to clear the `lines` vector
and then push_back the retranslated text. The function is also solely
responsible for spacing.
In most cases, you will also need to call
graphics.textboxapplyposition() or graphics.textboxadjust() afterwards.
(Some text boxes shouldn't use graphics.textboxadjust() as they are
within the 10-pixel inner border around the screen that
textboxclass::adjust tries to push the text box out of.)
This commit doesn't fix every text box just yet, though. But it fixes
the Level Complete, Game Complete, crew remaining, and ACTION prompt
text boxes, for a start.
This allows switching languages while a text box is on screen by saving
the necessary state for a text box to be retranslated when the language
is switched.
This saves the state of the position and direction of the crewmate that
the text box position is based off of (if applicable), and the text
case of the text box, the script name of the script, and the original
(English) lines of the text box. I did not explicitly label the original
lines as English lines except in a main game context, because
technically, custom levels could have original lines in a different
language.
Unfortunately, this doesn't work for every text box in the game.
Notably, the Level Complete, Game Complete, number of crewmates
remaining, trinket collection, Intermission 1 guides, etc. text boxes
are special and require further fixes, but that will be coming in later
commits.
This fixes a regression where attempting to warp to ship with a trinket
text box open in glitchrunner 2.0, and then incrementing the gamestate
afterwards, would result in a softlock. This is a speedrunning strat
that speedrunners use.
The state lock wasn't ever intended to remove any strats or anything,
just fix warping to ship under normal circumstances. So it's okay to
re-enable interrupting the state by having glitchrunner enabled.
This bug was reported by mohoc in the VVVVVV Speedrunning Discord
server.
No Death Mode is intended to be unlocked by getting at least S-rank in
at least 4 time trials. Before 2.3, completing a time trial put you at
the main menu, so you would always be notified of having unlocked No
Death Mode once you went to the play menu again. But since 2.3,
completing a time trial puts you back at the Time Trial selection
screen, which isn't the play menu, so you would need to back all the way
out first in order to get the notification. And since you don't actually
unlock No Death Mode until you see the notification, this would be
required to be able to play No Death Mode.
To fix this, I decided to do something a bit kludge-y and just re-use
the code to check and unlock No Death Mode when the player presses
ACTION on the Time Trial complete screen (and there's also another path
by pressing Escape). At least I put it in a function, so it's not a pure
copy-paste, although it might as well be. I don't have time to think of
a proper solution, but it would probably involve disentangling unlock
notifications from Menu::play, for starters. But that's for later.
This fixes a bug where pressing Escape in the following menus would not
play Presenting VVVVVV (the title screen music) and would instead leave
you with silence: Game Complete, Time Trial complete, Game Over, and No
Death Mode complete.
Originally, I was thinking of adding a notification text that you took a
screenshot, but this is better because it is language-agnostic and it
doesn't contribute to potential UI clutter/clashing.
It flashes yellow if the screenshot successfully saved, and red if it
didn't.
If you load in to gameplay with invincibility mode, glitchrunner mode,
Flip Mode, or slowdown enabled, then there will be text displayed on
screen for a few seconds that says so.
This is to serve as a useful reminder. A common pitfall with using
invincibility is forgetting to turn it off when you don't want it
anymore. What usually happens is that players forget that they have it
on until they encounter a hazard. Now, they can realize it as soon as
they load in.
See #1091.
I forgot to add the PR_RTL_XFLIP flag to these menu options, so they
were always left-aligned, no matter what.
What actually took me a bit to figure out was how to make the level
completion stars work regardless of the contents of the title - the
stars should always be to the left of the title in an LTR language, and
always to the right of the title in an RTL language. Level titles can
contain bidi characters regardless of the level's rtl flag being set,
so I just let bidi handle all the level menu options, with some control
characters to make sure everything always appears in the correct order.
Okay, the "Font:" thing needed some local code after all, because both
the interface font as well as the level font are used there. But it's
good enough - all the other places can just use the flag.
Notably, I also used this for the menus, since the existing ones are
kinda LTR-oriented, and it's something that we don't *really* have to
do, but I think it shows we care!
Again, the RTL property controls whether textboxes will be
right-aligned, and that kind of stuff. It can't be font-bound, since
Space Station supports Hebrew characters and we want to be able to
support, say, a Hebrew translation or Hebrew levels in the future
without having to make a dedicated (or duplicated) font for it.
Therefore it's a property of both the language pack as well as custom
levels - like custom levels already had a <font> tag, they now also
have an <rtl> tag that sets this property.
Right now, we'll have to hardcode it so the menu option for the Arabic
font sets the <rtl> property to 1, and all the other options set it to
0. But it's future-proof in that we can later decide to split the
option for Space Station into an LTR option and an RTL option (so both
"english/..." and "עברית" would select Space Station, but one sets the
RTL property to 0 and the other sets it to 1).
This lets you hold down F to fast-forward the game if you have the level
debugger interface open (with Y) and the game isn't paused.
This is most useful for quickly skipping through cutscenes to test
something.
Or well, lock yourself out if you don't have (easy) access to a
keyboard, like on Steam Deck.
In 2.3, this problem used to be much worse, since you could bind any
button to "menu" - which is actually also "return" in menus - and that
button could then no longer be bound to any other action, because
exiting the bindings menu had priority over assigning a different
binding. The result would be that people could have all their buttons
bound to "escape" with no way of undoing it or using their controllers
at all other than manually going into their config file to change it.
In 2.4, the most important bugs in the bindings menu are fixed, but
it's still possible to remove all your bindings from the "flip"
(confirm) action, meaning you can't navigate the menus anymore with a
controller to fix your bindings or even do anything.
There is one interesting part to all this: if an action has no buttons
bound to it at all when the game is started, then that action is
populated with the default button for that action. This is done for
each action separately, without accounting for the case where the
default button was already bound to another action which was not empty.
(This is something that the binding menu does try to prevent).
Therefore, having no buttons bound to "flip" while having A and B bound
to "menu", would result in A being bound to "flip" and A and B bound to
"menu".
That would still make you unable to enter the gamepad menu, since both
"confirm" and "return" are pressed in a row.
This commit fixes the specific situation where flip/confirm buttons are
also bound to menu/return, by removing all buttons that are in the flip
button list from the menu list. This means that, on Steam Deck, you can
still go to your bindings menu.
Textboxes created with graphics.createtextboxflipme() use PR_FONT_LEVEL
by default, but can be overridden with graphics.textboxprintflags() to,
for example, set PR_FONT_INTERFACE. This happens for the textboxes on
the Game Complete screen, which use interface text. The textboxes are
centered by setting the X position to -1 though, which means they're
solely centered based on the width of the first line, in the level
font (because the font hasn't been changed to the interface font yet).
Normally, this isn't a problem, because in the main game (where the
Game Complete screen usually appears), the level font is always equal
to the interface font. However, in custom levels you can still get it
(by calling gamestate 3500) and in that case some of the text may be
misaligned. This change fixes that by adding graphics.textboxcenterx()
to these textboxes.
As far as I can tell, these are the only textboxes that are centered
by just x=-1 despite changing the font afterwards.
At first my CJK changes also misaligned this sprite, and my solution
that time was to position the textbox higher depending on the height
of the textbox, so it would be centered around the crewmate sprite
(which stayed at a hardcoded place onscreen). Recently, #987 changed
these sprites to be relative to the position of the textbox instead of
relative to the screen, which is much more logical, but it stopped
centering these sprites again. But it's an easy fix: simply account for
the extra-added height when adding the sprite in.
The level debugger is toggleable in playtesting mode by pressing Y.
You can toggle whether or not the game is paused inside of the debugger
by pressing TAB. The debugger screen allows you to see entity and block
properties, and allows you to move them around.
The hardest room used to be stored as a room name in whatever language
it was in when you last died enough times to break the record (before
localization, that was always English). Even after localization became
a thing we could get away with this since we only had a single font,
but now we might have actual question marks appearing when the new font
doesn't support characters from the old language.
Therefore, this commit adds more info about the hardest room to save
files - everything that is needed to know in order to do the
translation at display time. These are hardestroom_x and hardestroom_y
for the room coordinates, as well as hardestroom_specialname to mark
special names, in addition to changing the stored room name back to
English. I've also added hardestroom_finalstretch in case we later
decide to drop the English name as a key and rely on just the
coordinates (even though I think that change itself would be more
complicated than any simplification it would accomplish, and I don't
think it's necessary, but better to have it if we do need it later)
As described in #1016, there used to be a bug that inflated
levelstats.vvv in 2.3, which was fixed in 2.4, but there was no way
for inflated files to get smaller yet.
This commit changes the storage of levelstats from a std::vector of
structs to a std::map, so that uniqueness is guaranteed and thus the
stats can be optimized automatically. And it also simplifies *and*
optimizes the code that handles the levelstats - no more big loops that
iterated over every element to find the matching level.
(Farewell to the "life optimisation and all that" comment, too)
I tested this with both my own levelstats.vvv, as well as some inflated
ones (including Balneor's 93 MB one) and saw this code correctly reduce
the filesize and speed up the levels list.
Fixes#1016.
The declarations of `std::vector<std::string> customlevelnames` and
`std::vector<int> customlevelscores` are made quite early in the
function, commented with "Old system", but the place where the old
system is processed is after a big chunk of code that processes the new
system (and indeed never uses these vectors). So for readability,
they're now closer to where they're used.
`levelcomplete` and `gamecomplete` were hardcoded using textbox colors
which were offset by 1. This PR fixes that, no longer requiring
slightly-off colors, and instead adding a new property to textboxes
which tell the game to display either level complete or game complete.
For both `tele` and `quick`, I removed these attributes of class Game:
- std::string *_gametime
- int *_trinkets
- std::string *_currentarea
- bool *_crewstats[numcrew]
All this info can now be gotten from members of Game::last_telesave and
Game::last_telesave. I've also cleaned up the continue menu to not have
all the display code appear twice (once for telesave and once for
quicksave).
RIP "Error! Error!" though lol
This is what got saved to the area part of the <summary> tags, and it
was specifically set upon pressing ACTION to save in the map menu.
Which meant tsave.vvv may not get an accurate area name (notably
"nowhere" if you hadn't quicksaved before in that session) even though
it's not displayed anywhere so it didn't really matter. But this
variable can be removed - there's only one place where <summary> is
written for both quicksaves and telesaves, so that now gets the area
at saving time.
Fun fact: custom level quicksaves also have a <summary> tag, and it's
even less functional than the one in tsave.vvv, because it stores
whatever main-game area name applies to your current coordinates.
So I simply filled in the level's name instead (just like what the
actual save box says).
Game::telesummary and Game::quicksummary stored the summary string for
the save files - which is the <summary> tag that says something like
"Space Station, 10:30:59". The game only ever displays the quicksave
variant of these two, for "Last Save:" on the map menu's SAVE tab.
So the telesave has a <summary> too, but it's never displayed anywhere.
(In fact, the area is often set to "nowhere"...)
However, the summary strings have another function: detect that both
the telesave and quicksave exist. If a summary string for a save is
empty, then that save is considered not to exist.
I'm refactoring the summary string system, by making the new variables
Game::last_telesave and Game::last_quicksave of type struct
Game::Summary. This struct should have all data necessary to display
the summary string at runtime, and thus translate it at runtime (so
we don't store a summary in a certain language and then display it in
the wrong font later - the summary can always be in the current
language). It also has an `exists` member, to replace the need to
check for empty strings.
The <summary> tag is now completely unused, but is still written to
for older versions of the game to read.
(This commit does not add the new string to the language files, since
Terry now added it separately in his own branch)
It used to take a single int: the area number returned by
mapclass::area(roomx, roomy). All uses of currentarea() were called
with an extra area() call as its argument. Additionally, there's a
good reason why currentarea() should have the room coordinates: in one
of the cases that it's called, there's a special case for the ship's
coordinates. This results in the SAVE screen in the map menu being able
to show "The Ship", while the continue screen shows "Dimension VVVVVV"
instead. Therefore, why not put that exception inside currentarea()
instead, and remove a few callsite map.area() wrappers by making
currentarea() take the room x and y coordinates?
It'll start working in the next commit... See the description there.
(This commit does not add the new strings to the language files, since
Terry now added them separately in his own branch)