mirror of
				https://github.com/KartKrewDev/RingRacers.git
				synced 2025-10-30 08:01:28 +00:00 
			
		
		
		
	 cfacbd91be
			
		
	
	
		cfacbd91be
		
	
	
	
	
		
			
			This fixes the issue with certain compilers that have int set to different sizes by either explicitly casting or setting templates manually
		
			
				
	
	
		
			555 lines
		
	
	
	
		
			9.7 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
			
		
		
	
	
			555 lines
		
	
	
	
		
			9.7 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
| // DR. ROBOTNIK'S RING RACERS
 | |
| //-----------------------------------------------------------------------------
 | |
| // Copyright (C) 2024 by Sally "TehRealSalt" Cochenour
 | |
| // Copyright (C) 2024 by Kart Krew
 | |
| // Copyright (C) 2020 by Sonic Team Junior
 | |
| //
 | |
| // 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 "p_tick.h" // P_LevelIsFrozen
 | |
| 
 | |
| #include "v_draw.hpp"
 | |
| 
 | |
| #include "acs/interface.h"
 | |
| 
 | |
| using srb2::Dialogue;
 | |
| 
 | |
| // Dialogue::Typewriter
 | |
| 
 | |
| void Dialogue::Typewriter::ClearText(void)
 | |
| {
 | |
| 	text.clear();
 | |
| 	textDest.clear();
 | |
| }
 | |
| 
 | |
| void Dialogue::Typewriter::NewText(std::string newText)
 | |
| {
 | |
| 	text.clear();
 | |
| 
 | |
| 	textDest = newText;
 | |
| 	std::reverse(textDest.begin(), textDest.end());
 | |
| 
 | |
| 	textTimer = kTextPunctPause;
 | |
| 	textSpeed = kTextSpeedDefault;
 | |
| 	textDone = false;
 | |
| 	textLines = 1;
 | |
| 
 | |
| 	syllable = true;
 | |
| }
 | |
| 
 | |
| void Dialogue::Typewriter::WriteText(void)
 | |
| {
 | |
| 	if (textDone)
 | |
| 		return;
 | |
| 
 | |
| 	bool voicePlayed = false;
 | |
| 	bool empty = textDest.empty();
 | |
| 
 | |
| 	textTimer -= textSpeed;
 | |
| 
 | |
| 	while (textTimer <= 0 && !empty)
 | |
| 	{
 | |
| 		char c = textDest.back(), nextc = '\n';
 | |
| 		text.push_back(c);
 | |
| 
 | |
| 		textDest.pop_back();
 | |
| 		empty = textDest.empty();
 | |
| 
 | |
| 		if (c & 0x80)
 | |
| 		{
 | |
| 			// Color code support
 | |
| 			continue;
 | |
| 		}
 | |
| 
 | |
| 		if (c == '\n')
 | |
| 			textLines++;
 | |
| 
 | |
| 		if (!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 (c == '-' && empty)
 | |
| 		{
 | |
| 			textTimer += textSpeed;
 | |
| 		}
 | |
| 		else if (c != '+' && c != '"' // tutorial hack
 | |
| 			&& std::ispunct(c)
 | |
| 			&& std::isspace(nextc))
 | |
| 		{
 | |
| 			// slow down for punctuation
 | |
| 			textTimer += kTextPunctPause;
 | |
| 		}
 | |
| 		else
 | |
| 		{
 | |
| 			textTimer += FRACUNIT;
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	textDone = (textTimer <= 0 && empty);
 | |
| }
 | |
| 
 | |
| void Dialogue::Typewriter::CompleteText(void)
 | |
| {
 | |
| 	while (!textDest.empty())
 | |
| 	{
 | |
| 		char c = textDest.back();
 | |
| 
 | |
| 		if (c == '\n')
 | |
| 			textLines++;
 | |
| 
 | |
| 		text.push_back( c );
 | |
| 		textDest.pop_back();
 | |
| 	}
 | |
| 
 | |
| 	textTimer = 0;
 | |
| 	textDone = true;
 | |
| }
 | |
| 
 | |
| // Dialogue
 | |
| 
 | |
| void Dialogue::Init(void)
 | |
| {
 | |
| 	if (!active)
 | |
| 	{
 | |
| 		auto cache = [](const char* name)
 | |
| 		{
 | |
| 			return std::pair {std::string_view {name}, srb2::Draw::cache_patch(name)};
 | |
| 		};
 | |
| 		patchCache = {
 | |
| 			cache("TUTDIAGA"),
 | |
| 			cache("TUTDIAGB"),
 | |
| 			cache("TUTDIAGC"),
 | |
| 			cache("TUTDIAGD"),
 | |
| 			cache("TUTDIAGF"),
 | |
| 			cache("TUTDIAGE"),
 | |
| 			cache("TUTDIAG2"),
 | |
| 		};
 | |
| 	}
 | |
| 
 | |
| 	active = true;
 | |
| }
 | |
| 
 | |
| void Dialogue::SetSpeaker(void)
 | |
| {
 | |
| 	// Unset speaker
 | |
| 	speaker.clear();
 | |
| 
 | |
| 	portrait = nullptr;
 | |
| 	portraitColormap = nullptr;
 | |
| 
 | |
| 	typewriter.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];
 | |
| 
 | |
| 			portrait = static_cast<patch_t *>( W_CachePatchNum(sprframe->lumppat[0], PU_CACHE) );
 | |
| 			portraitColormap = R_GetTranslationColormap(skinID, static_cast<skincolornum_t>(skin->prefcolor), GTC_CACHE);
 | |
| 		}
 | |
| 		else
 | |
| 		{
 | |
| 			portrait = nullptr;
 | |
| 			portraitColormap = nullptr;
 | |
| 		}
 | |
| 
 | |
| 		speaker = skin->realname;
 | |
| 
 | |
| 		typewriter.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;
 | |
| 		typewriter.voiceSfx = sfx_ktalk;
 | |
| 		return;
 | |
| 	}
 | |
| 
 | |
| 	portrait = patch;
 | |
| 	portraitColormap = colormap;
 | |
| 
 | |
| 	typewriter.voiceSfx = voice;
 | |
| }
 | |
| 
 | |
| void Dialogue::NewText(std::string_view rawText)
 | |
| {
 | |
| 	Init();
 | |
| 
 | |
| 	char* newText = V_ScaledWordWrap(
 | |
| 		290 << FRACBITS,
 | |
| 		FRACUNIT, FRACUNIT, FRACUNIT,
 | |
| 		0, HU_FONT,
 | |
| 		srb2::Draw::TextElement().parse(rawText).string().c_str() // parse special characters
 | |
| 	);
 | |
| 
 | |
| 	typewriter.NewText(newText);
 | |
| 
 | |
| 	Z_Free(newText);
 | |
| }
 | |
| 
 | |
| bool Dialogue::Active(void)
 | |
| {
 | |
| 	return active;
 | |
| }
 | |
| 
 | |
| bool Dialogue::TextDone(void)
 | |
| {
 | |
| 	return typewriter.textDone;
 | |
| }
 | |
| 
 | |
| bool Dialogue::Dismissable(void)
 | |
| {
 | |
| 	return dismissable;
 | |
| }
 | |
| 
 | |
| void Dialogue::SetDismissable(bool value)
 | |
| {
 | |
| 	dismissable = value;
 | |
| }
 | |
| 
 | |
| 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::Tick(void)
 | |
| {
 | |
| 	if (Active())
 | |
| 	{
 | |
| 		if (slide < FRACUNIT)
 | |
| 		{
 | |
| 			slide += kSlideSpeed;
 | |
| 		}
 | |
| 	}
 | |
| 	else
 | |
| 	{
 | |
| 		if (slide > 0)
 | |
| 		{
 | |
| 			slide -= kSlideSpeed;
 | |
| 
 | |
| 			if (slide <= 0)
 | |
| 			{
 | |
| 				Unset();
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	slide = std::clamp<size_t>(slide, 0, FRACUNIT);
 | |
| 
 | |
| 	if (slide != FRACUNIT)
 | |
| 	{
 | |
| 		return;
 | |
| 	}
 | |
| 
 | |
| 	typewriter.WriteText();
 | |
| 
 | |
| 	if (Dismissable() == true)
 | |
| 	{
 | |
| 		if (Pressed() == true)
 | |
| 		{
 | |
| 			if (TextDone())
 | |
| 			{
 | |
| 				Dismiss();
 | |
| 			}
 | |
| 			else
 | |
| 			{
 | |
| 				typewriter.CompleteText();
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| INT32 Dialogue::SlideAmount(fixed_t multiplier)
 | |
| {
 | |
| 	if (slide == 0)
 | |
| 		return 0;
 | |
| 	if (slide == FRACUNIT)
 | |
| 		return multiplier;
 | |
| 	return Easing_OutCubic(slide, 0, multiplier);
 | |
| }
 | |
| 
 | |
| void Dialogue::Draw(void)
 | |
| {
 | |
| 	if (slide == 0)
 | |
| 	{
 | |
| 		return;
 | |
| 	}
 | |
| 
 | |
| 	const UINT8 bgcol = 235, speakerhilicol = 240;
 | |
| 
 | |
| 	const fixed_t height = 78 * FRACUNIT;
 | |
| 
 | |
| 	INT32 speakernameedge = -6;
 | |
| 
 | |
| 	srb2::Draw drawer =
 | |
| 		srb2::Draw(
 | |
| 			BASEVIDWIDTH, BASEVIDHEIGHT - FixedToFloat(SlideAmount(height) - height)
 | |
| 		).flags(V_SNAPTOBOTTOM);
 | |
| 
 | |
| 	// TODO -- hack, change when dialogue is made per-player/netsynced
 | |
| 	UINT32 speakerbgflags = (players[consoleplayer].nocontrol == 0 && P_LevelIsFrozen() == false)
 | |
| 		? V_30TRANS
 | |
| 		: 0;
 | |
| 
 | |
| 	drawer
 | |
| 		.flags(speakerbgflags|V_VFLIP|V_FLIP)
 | |
| 		.patch(patchCache["TUTDIAGA"]);
 | |
| 
 | |
| 	drawer
 | |
| 		.flags(V_VFLIP|V_FLIP)
 | |
| 		.patch(patchCache["TUTDIAGB"]);
 | |
| 
 | |
| 	if (portrait != nullptr)
 | |
| 	{
 | |
| 		drawer
 | |
| 			.flags(V_VFLIP|V_FLIP)
 | |
| 			.patch(patchCache["TUTDIAGC"]);
 | |
| 
 | |
| 		drawer
 | |
| 			.xy(-10-32, -41-32)
 | |
| 			.colormap(portraitColormap)
 | |
| 			.patch(portrait);
 | |
| 
 | |
| 		speakernameedge -= 39; // -45
 | |
| 	}
 | |
| 
 | |
| 	const char *speakername = speaker.c_str();
 | |
| 
 | |
| 	const INT32 arrowstep = 8; // width of TUTDIAGD
 | |
| 
 | |
| 	if (speakername && speaker[0])
 | |
| 	{
 | |
| 		INT32 speakernamewidth = V_MenuStringWidth(speakername, 0);
 | |
| 		INT32 existingborder = (portrait == nullptr ? -4 : 3);
 | |
| 
 | |
| 		INT32 speakernamewidthoffset = (speakernamewidth + (arrowstep - existingborder) - 2) % arrowstep;
 | |
| 		if (speakernamewidthoffset)
 | |
| 		{
 | |
| 			speakernamewidthoffset = (arrowstep - speakernamewidthoffset);
 | |
| 			speakernamewidth += speakernamewidthoffset;
 | |
| 		}
 | |
| 
 | |
| 		if (portrait == nullptr)
 | |
| 		{
 | |
| 			speakernameedge -= 3;
 | |
| 			speakernamewidth += 3;
 | |
| 			existingborder += 2;
 | |
| 			drawer
 | |
| 				.xy(speakernameedge, -36)
 | |
| 				.width(2)
 | |
| 				.height(3+11)
 | |
| 				.fill(bgcol);
 | |
| 		}
 | |
| 
 | |
| 		if (speakernamewidth > existingborder)
 | |
| 		{
 | |
| 			drawer
 | |
| 				.x(speakernameedge - speakernamewidth)
 | |
| 				.width(speakernamewidth - existingborder)
 | |
| 				.y(-36-3)
 | |
| 				.height(3)
 | |
| 				.fill(bgcol);
 | |
| 
 | |
| 			drawer
 | |
| 				.x(speakernameedge - speakernamewidth)
 | |
| 				.width(speakernamewidth - existingborder)
 | |
| 				.y(-38-11)
 | |
| 				.height(11)
 | |
| 				.fill(speakerhilicol);
 | |
| 		}
 | |
| 
 | |
| 		speakernameedge -= speakernamewidth;
 | |
| 
 | |
| 		drawer
 | |
| 			.xy(speakernamewidthoffset + speakernameedge, -39-9)
 | |
| 			.font(srb2::Draw::Font::kMenu)
 | |
| 			.text(speakername);
 | |
| 
 | |
| 		speakernameedge -= 5;
 | |
| 
 | |
| 		drawer
 | |
| 			.xy(speakernameedge, -36)
 | |
| 			.flags(V_VFLIP|V_FLIP)
 | |
| 			.patch(patchCache["TUTDIAGD"]);
 | |
| 
 | |
| 		drawer
 | |
| 			.xy(speakernameedge, -36-3-11)
 | |
| 			.width(5)
 | |
| 			.height(3+11)
 | |
| 			.fill(bgcol);
 | |
| 
 | |
| 		drawer
 | |
| 			.xy(speakernameedge + 5, -36)
 | |
| 			.flags(V_VFLIP|V_FLIP)
 | |
| 			.patch(patchCache["TUTDIAGF"]);
 | |
| 	}
 | |
| 
 | |
| 	while (speakernameedge > -142) // the left-most edge
 | |
| 	{
 | |
| 		speakernameedge -= arrowstep;
 | |
| 
 | |
| 		drawer
 | |
| 			.xy(speakernameedge, -36)
 | |
| 			.flags(V_VFLIP|V_FLIP)
 | |
| 			.patch(patchCache["TUTDIAGD"]);
 | |
| 	}
 | |
| 
 | |
| 	drawer
 | |
| 		.xy(speakernameedge - arrowstep, -36)
 | |
| 		.flags(V_VFLIP|V_FLIP)
 | |
| 		.patch(patchCache["TUTDIAGE"]);
 | |
| 
 | |
| 	drawer
 | |
| 		.xy(10 - BASEVIDWIDTH, -3-32)
 | |
| 		.font(srb2::Draw::Font::kConsole)
 | |
| 		.text( typewriter.text.c_str() );
 | |
| 
 | |
| 	if (Dismissable())
 | |
| 	{
 | |
| 		if (TextDone())
 | |
| 		{
 | |
| 			drawer
 | |
| 				.xy(-14, -7-5)
 | |
| 				.patch(patchCache["TUTDIAG2"]);
 | |
| 		}
 | |
| 
 | |
| 		auto bt_translate_press = [this]() -> std::optional<bool>
 | |
| 		{
 | |
| 			if (Held())
 | |
| 				return true;
 | |
| 			if (TextDone())
 | |
| 				return {};
 | |
| 			return false;
 | |
| 		};
 | |
| 
 | |
| 		drawer
 | |
| 			.xy(17-14 - BASEVIDWIDTH, -39-16)
 | |
| 			.button(srb2::Draw::Button::z, bt_translate_press());
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void Dialogue::Dismiss(void)
 | |
| {
 | |
| 	active = false;
 | |
| 	typewriter.ClearText();
 | |
| }
 | |
| 
 | |
| UINT32 Dialogue::GetNewEra(void)
 | |
| {
 | |
| 	return (++current_era);
 | |
| }
 | |
| 
 | |
| bool Dialogue::EraIsValid(INT32 comparison)
 | |
| {
 | |
| 	return (current_era == comparison);
 | |
| }
 | |
| 
 | |
| void Dialogue::Unset(void)
 | |
| {
 | |
| 	Dismiss();
 | |
| 	SetSpeaker();
 | |
| 	slide = 0;
 | |
| 	current_era = 0;
 | |
| }
 | |
| 
 | |
| /*
 | |
| 	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();
 | |
| }
 | |
| 
 | |
| INT32 K_GetDialogueSlide(fixed_t multiplier)
 | |
| {
 | |
| 	return g_dialogue.SlideAmount(multiplier);
 | |
| }
 |