RingRacers/src/k_dialogue.cpp
toaster 8326a2d588 Dialogue::WriteText improvements
- Only have a long delay between symbols if the current character is std::ispunct() and the NEXT character is std::isspace
- Add native color code support
    - Requires string concatenation to actually use it, though, which I don't know how to do in ACS
2023-09-13 17:53:07 +01:00

360 lines
5.7 KiB
C++

// DR. ROBOTNIK'S RING RACERS
//-----------------------------------------------------------------------------
// Copyright (C) by Sonic Team Junior
// Copyright (C) by Kart Krew
// Copyright (C) by Sally "TehRealSalt" Cochenour
//
// This program is free software distributed under the
// terms of the GNU General Public License, version 2.
// See the 'LICENSE' file for more details.
//-----------------------------------------------------------------------------
/// \file k_dialogue.cpp
/// \brief Basic text prompts
#include "k_dialogue.hpp"
#include "k_dialogue.h"
#include <string>
#include <algorithm>
#include "info.h"
#include "sounds.h"
#include "g_game.h"
#include "v_video.h"
#include "r_draw.h"
#include "m_easing.h"
#include "r_skins.h"
#include "s_sound.h"
#include "z_zone.h"
#include "k_hud.h"
#include "v_draw.hpp"
#include "acs/interface.h"
using srb2::Dialogue;
void Dialogue::Init(void)
{
active = true;
syllable = true;
}
void Dialogue::SetSpeaker(void)
{
// Unset speaker
speaker.clear();
portrait = nullptr;
portraitColormap = nullptr;
voiceSfx = sfx_ktalk;
}
void Dialogue::SetSpeaker(std::string skinName, int portraitID)
{
Init();
// Set speaker based on a skin
int skinID = -1;
if (!skinName.empty())
{
skinID = R_SkinAvailable(skinName.c_str());
}
if (skinID >= 0 && skinID < numskins)
{
const skin_t *skin = &skins[skinID];
const spritedef_t *sprdef = &skin->sprites[SPR2_TALK];
if (sprdef->numframes > 0)
{
portraitID %= sprdef->numframes;
}
const spriteframe_t *sprframe = &sprdef->spriteframes[portraitID];
speaker = skin->realname;
portrait = static_cast<patch_t *>( W_CachePatchNum(sprframe->lumppat[0], PU_CACHE) );
portraitColormap = R_GetTranslationColormap(skinID, static_cast<skincolornum_t>(skin->prefcolor), GTC_CACHE);
voiceSfx = skin->soundsid[ S_sfx[sfx_ktalk].skinsound ];
}
else
{
SetSpeaker();
}
}
void Dialogue::SetSpeaker(std::string name, patch_t *patch, UINT8 *colormap, sfxenum_t voice)
{
Init();
// Set custom speaker
speaker = name;
if (speaker.empty())
{
portrait = nullptr;
portraitColormap = nullptr;
voiceSfx = sfx_ktalk;
return;
}
portrait = patch;
portraitColormap = colormap;
voiceSfx = voice;
}
void Dialogue::NewText(std::string newText)
{
Init();
newText = V_ScaledWordWrap(
290 << FRACBITS,
FRACUNIT, FRACUNIT, FRACUNIT,
0, HU_FONT,
newText.c_str()
);
text.clear();
textDest = newText;
std::reverse(textDest.begin(), textDest.end());
textTimer = kTextPunctPause;
textSpeed = kTextSpeedDefault;
textDone = false;
}
bool Dialogue::Active(void)
{
return active;
}
bool Dialogue::TextDone(void)
{
return textDone;
}
bool Dialogue::Dismissable(void)
{
return dismissable;
}
void Dialogue::SetDismissable(bool value)
{
dismissable = value;
}
void Dialogue::WriteText(void)
{
bool voicePlayed = false;
textTimer -= textSpeed;
while (textTimer <= 0 && !textDest.empty())
{
char c = textDest.back(), nextc = '\n';
text.push_back(c);
textDest.pop_back();
if (c & 0x80)
{
// Color code support
continue;
}
if (!textDest.empty())
nextc = textDest.back();
if (voicePlayed == false
&& std::isprint(c)
&& c != ' ')
{
if (syllable)
{
S_StopSoundByNum(voiceSfx);
S_StartSound(nullptr, voiceSfx);
}
syllable = !syllable;
voicePlayed = true;
}
if (std::ispunct(c)
&& std::isspace(nextc))
{
// slow down for punctuation
textTimer += kTextPunctPause;
}
else
{
textTimer += FRACUNIT;
}
}
textDone = (textTimer <= 0 && textDest.empty());
}
bool Dialogue::Held(void)
{
return ((players[serverplayer].cmd.buttons & BT_VOTE) == BT_VOTE);
}
bool Dialogue::Pressed(void)
{
return (
((players[serverplayer].cmd.buttons & BT_VOTE) == BT_VOTE) &&
((players[serverplayer].oldcmd.buttons & BT_VOTE) == 0)
);
}
void Dialogue::CompleteText(void)
{
while (!textDest.empty())
{
text.push_back( textDest.back() );
textDest.pop_back();
}
textTimer = 0;
textDone = true;
}
void Dialogue::Tick(void)
{
if (Active())
{
if (slide < FRACUNIT)
{
slide += kSlideSpeed;
}
}
else
{
if (slide > 0)
{
slide -= kSlideSpeed;
if (slide <= 0)
{
Unset();
}
}
}
slide = std::clamp(slide, 0, FRACUNIT);
if (slide != FRACUNIT)
{
return;
}
WriteText();
if (Dismissable() == true)
{
if (Pressed() == true)
{
if (TextDone())
{
Dismiss();
}
else
{
CompleteText();
}
}
}
}
void Dialogue::Draw(void)
{
if (slide == 0)
{
return;
}
srb2::Draw drawer =
srb2::Draw(
0, FixedToFloat(Easing_OutCubic(slide, -78 * FRACUNIT, 0))
).flags(V_SNAPTOTOP);
drawer.patch("TUTDIAG1");
if (portrait != nullptr)
{
drawer
.xy(10, 41)
.colormap(portraitColormap)
.patch(portrait);
}
drawer
.xy(45, 39)
.font(srb2::Draw::Font::kConsole)
.text( speaker.c_str() );
drawer
.xy(10, 3)
.font(srb2::Draw::Font::kConsole)
.text( text.c_str() );
if (Dismissable())
{
if (TextDone())
{
drawer
.xy(304, 7)
.patch("TUTDIAG2");
}
K_drawButton(
FloatToFixed(drawer.x() + 303),
FloatToFixed(drawer.y() + 39),
V_SNAPTOTOP,
kp_button_z[0], Held()
);
}
}
void Dialogue::Dismiss(void)
{
active = false;
text.clear();
textDest.clear();
}
void Dialogue::Unset(void)
{
Dismiss();
SetSpeaker();
}
/*
Ideally, the Dialogue class would be on player_t instead of in global space
for full multiplayer compatibility, but right now it's only being used for
the tutorial, and I don't feel like writing network code. If you feel like
doing that, then you can remove g_dialogue entirely.
*/
Dialogue g_dialogue;
void K_UnsetDialogue(void)
{
g_dialogue.Unset();
}
void K_DrawDialogue(void)
{
g_dialogue.Draw();
}
void K_TickDialogue(void)
{
g_dialogue.Tick();
}