The open source repository for the action RPG game in development by Sig Productions titled 'Adventures in Lestoria'!
https://forums.lestoria.net
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
303 lines
12 KiB
303 lines
12 KiB
#pragma region License
|
|
/*
|
|
License (OLC-3)
|
|
~~~~~~~~~~~~~~~
|
|
|
|
Copyright 2018 - 2023 OneLoneCoder.com
|
|
|
|
Redistribution and use in source and binary forms, with or without modification,
|
|
are permitted provided that the following conditions are met:
|
|
|
|
1. Redistributions or derivations of source code must retain the above copyright
|
|
notice, this list of conditions and the following disclaimer.
|
|
|
|
2. Redistributions or derivative works in binary form must reproduce the above
|
|
copyright notice. This list of conditions and the following disclaimer must be
|
|
reproduced in the documentation and/or other materials provided with the distribution.
|
|
|
|
3. Neither the name of the copyright holder nor the names of its contributors may
|
|
be used to endorse or promote products derived from this software without specific
|
|
prior written permission.
|
|
|
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
|
|
EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
|
|
OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
|
|
SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
|
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
|
|
TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
|
|
BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
|
|
ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
|
|
SUCH DAMAGE.
|
|
*/
|
|
#pragma endregion
|
|
#include "VisualNovel.h"
|
|
#include "GameState.h"
|
|
#include "Crawler.h"
|
|
#include <fstream>
|
|
#include "DEFINES.h"
|
|
#include "Unlock.h"
|
|
#include "Menu.h"
|
|
#include "util.h"
|
|
|
|
INCLUDE_game
|
|
INCLUDE_GFX
|
|
|
|
VisualNovel VisualNovel::novel;
|
|
safemap<std::string,std::vector<std::unique_ptr<Command>>>VisualNovel::storyLevelData;
|
|
std::set<std::string>VisualNovel::graphicsToLoad;
|
|
|
|
void VisualNovel::Initialize(){
|
|
for(int chapter=1;chapter<=6;chapter++){
|
|
std::string chapterFilename="assets/"+"story_directory"_S+"Chapter "+std::to_string(chapter)+".txt";
|
|
std::ifstream file(chapterFilename);
|
|
if(!file)ERR("Failed to open file "<<chapterFilename)
|
|
std::string line;
|
|
std::string currentStory;
|
|
while(file.good()){
|
|
|
|
auto trim=[](std::string line){
|
|
for(int counter=0;counter<line.length();counter++){
|
|
if(line[counter]!=' '&&line[counter]!='\t')return line.substr(counter);
|
|
}
|
|
return line;
|
|
};
|
|
auto ReadCSVArgs=[](std::string text){
|
|
std::vector<std::string>args;
|
|
size_t counter=0;
|
|
while(text.find(',',counter)!=std::string::npos){
|
|
std::string arg=text.substr(counter,text.find(',',counter)-counter);
|
|
counter=text.find(',',counter)+1;
|
|
if(arg.find(',')!=std::string::npos)ERR("Found an erraneous comma inside parsed arg "<<arg<<". Inside of main text: "<<text);
|
|
args.push_back(arg);
|
|
}
|
|
std::string arg=text.substr(counter);
|
|
args.push_back(arg);
|
|
return args;
|
|
};
|
|
std::getline(file,line);
|
|
line=trim(line);
|
|
if(line.length()==0)continue; //It's a blank line, so we skip it.
|
|
|
|
switch(line[0]){
|
|
case '=':{ //Initializes a new story section.
|
|
currentStory=line.substr(3,line.find('=',3)-3);
|
|
if(currentStory.find('=')!=std::string::npos)ERR("Story name "<<currentStory<<" is an invalid name! Failed to load story.");
|
|
storyLevelData[currentStory];
|
|
}break;
|
|
case '{':{ //Start of a command.
|
|
auto&data=storyLevelData.at(currentStory);
|
|
|
|
auto AddImagesForLoading=[](std::vector<std::string>&arguments){
|
|
for(std::string&arg:arguments){
|
|
if(arg=="story_player_name"_S){
|
|
graphicsToLoad.insert("character_image_location"_S+"Player_F.png");
|
|
graphicsToLoad.insert("character_image_location"_S+"Player_M.png");
|
|
}else{
|
|
graphicsToLoad.insert("character_image_location"_S+arg+".png");
|
|
}
|
|
}
|
|
};
|
|
|
|
size_t spacePos=line.find(' ');
|
|
std::vector<std::string>arguments;
|
|
|
|
if(spacePos!=std::string::npos){//There are arguments to parse...
|
|
size_t endingBracePos=line.find('}',spacePos+1);
|
|
if(endingBracePos==std::string::npos)ERR("Cannot parse arguments from "<<line<<": No closing ending brace found")
|
|
std::string args=line.substr(spacePos+1,endingBracePos-(spacePos+1));
|
|
if(line.size()==0||line[0]==' '||args.find('}')!=std::string::npos)ERR("Cannot parse args, found invalid tokens in "<<args)
|
|
arguments=ReadCSVArgs(args);
|
|
}
|
|
|
|
if(line.find("{LOCATION")!=std::string::npos){//Location command
|
|
if(arguments.size()!=1)ERR("Arguments size is "<<arguments.size()<<". Expecting only 1 argument.")
|
|
data.push_back(std::make_unique<LocationCommand>(arguments[0]));
|
|
}else
|
|
if(line.find("{BACKGROUND")!=std::string::npos){//Background command
|
|
if(arguments.size()!=1)ERR("Arguments size is "<<arguments.size()<<". Expecting only 1 argument.")
|
|
graphicsToLoad.insert("story_background_image_location"_S+arguments[0]);
|
|
data.push_back(std::make_unique<BackgroundCommand>(arguments[0]));
|
|
}else
|
|
if(line.find("{LEFT")!=std::string::npos){//Left command
|
|
AddImagesForLoading(arguments);
|
|
data.push_back(std::make_unique<LeftCommand>(arguments));
|
|
}else
|
|
if(line.find("{RIGHT")!=std::string::npos){//Right command
|
|
AddImagesForLoading(arguments);
|
|
data.push_back(std::make_unique<RightCommand>(arguments));
|
|
}else
|
|
if(line.find("{PAUSE")!=std::string::npos){//Pause command
|
|
if(arguments.size()!=0)ERR("Arguments size is "<<arguments.size()<<". Expecting no arguments.")
|
|
data.push_back(std::make_unique<PauseCommand>());
|
|
}else{
|
|
ERR("Unknown command "<<line<<". Could not parse!");
|
|
}
|
|
}break;
|
|
case '[':{
|
|
auto&data=storyLevelData.at(currentStory);
|
|
|
|
size_t endingBracePos=line.find(']');
|
|
if(endingBracePos==std::string::npos)ERR("Cannot parse arguments from "<<line<<": No closing ending bracket found")
|
|
std::string args=line.substr(1,endingBracePos-1);
|
|
if(args.find(']')!=std::string::npos)ERR("Cannot parse args, found invalid tokens in "<<args)
|
|
std::vector<std::string>arguments=ReadCSVArgs(args);
|
|
if(arguments.size()>2)ERR("Expecting a maximum of two arguments for parsed args in "<<args<<", got "<<arguments.size()<<" arguments instead.");
|
|
if(arguments.size()!=2){
|
|
data.push_back(std::make_unique<SpeakerCommand>(arguments[0]));
|
|
}else{
|
|
data.push_back(std::make_unique<SpeakerCommand>(arguments[0],arguments[1]));
|
|
}
|
|
}break;
|
|
default:{
|
|
auto&data=storyLevelData.at(currentStory);
|
|
|
|
data.push_back(std::make_unique<DialogCommand>(line));
|
|
}break;
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
storyLevelData.SetInitialized();
|
|
};
|
|
void VisualNovel::LoadVisualNovel(std::string storyLevelName){
|
|
novel.storyLevel=storyLevelName;
|
|
novel.activeText="";
|
|
novel.leftCharacters.clear();
|
|
novel.rightCharacters.clear();
|
|
novel.backgroundFilename="";
|
|
novel.commands.clear();
|
|
for(std::unique_ptr<Command>&command:storyLevelData.at(storyLevelName)){
|
|
novel.commands.push_back(command.get());
|
|
}
|
|
GameState::ChangeState(States::STORY,0.5f);
|
|
novel.ExecuteNextCommand();
|
|
novel.prevTheme=Menu::GetCurrentTheme().GetThemeName();
|
|
Menu::themeSelection="Purple";
|
|
}
|
|
void VisualNovel::Update(){
|
|
if(transitionTime==0&&game->KEY_CONFIRM.Pressed()){
|
|
activeText="";
|
|
novel.ExecuteNextCommand();
|
|
}
|
|
locationDisplayTime=std::max(0.f,locationDisplayTime-game->GetElapsedTime());
|
|
transitionTime=std::max(0.f,transitionTime-game->GetElapsedTime());
|
|
}
|
|
void VisualNovel::ExecuteNextCommand(){
|
|
if(commandIndex<commands.size()){
|
|
commandIndex++;
|
|
commands[commandIndex-1]->Execute(novel);
|
|
}else{
|
|
Unlock::UnlockCurrentMap();
|
|
Menu::themeSelection=novel.prevTheme;
|
|
GameState::ChangeState(States::OVERWORLD_MAP,0.5f);
|
|
}
|
|
}
|
|
void VisualNovel::Draw(){
|
|
if(backgroundFilename!=""){
|
|
float alpha=1;
|
|
if(transitionTime>0){
|
|
alpha=util::lerp(0,1,1-(transitionTime/maxTransitionTime));
|
|
}
|
|
if(prevBackgroundFilename!=""){
|
|
game->DrawDecal({0,0},GFX["backgrounds/"+prevBackgroundFilename].Decal());
|
|
}
|
|
game->DrawDecal({0,0},GFX["backgrounds/"+backgroundFilename].Decal(),{1,1},{255,255,255,uint8_t(255*alpha)});
|
|
}else{
|
|
game->FillRectDecal({0,0},game->GetScreenSize());
|
|
}
|
|
if(locationDisplayTime>0){
|
|
vi2d textSize=game->GetTextSizeProp(locationDisplayText)*2;
|
|
game->FillRectDecal(game->GetScreenSize()/2-textSize/2-vi2d{4,4},textSize+vi2d{8,8},BLACK);
|
|
game->DrawRectDecal(game->GetScreenSize()/2-textSize/2-vi2d{4,4},textSize+vi2d{8,8},WHITE);
|
|
game->DrawShadowStringPropDecal(game->GetScreenSize()/2-textSize/2,locationDisplayText,WHITE,VERY_DARK_BLUE,{2.f,2.f});
|
|
}
|
|
if(activeText.length()>0){
|
|
vf2d nameDisplayPos={24.f,game->GetScreenSize().y-60.f};
|
|
vf2d nameDisplayWindowSize={48.f,-12.f};
|
|
if(speakerDisplayName.length()>0){
|
|
Menu::DrawThemedWindow(nameDisplayPos,nameDisplayWindowSize);
|
|
}
|
|
vf2d dialogDisplayPos={24.f,game->GetScreenSize().y-48.f};
|
|
Menu::DrawThemedWindow(dialogDisplayPos,{game->GetScreenSize().x-48.f,20.f});
|
|
vf2d speakerTextSize=game->GetTextSizeProp(speakerDisplayName);
|
|
game->DrawShadowStringPropDecal(nameDisplayPos-vf2d{10,8}+(nameDisplayWindowSize+vf2d{24,0})/2-speakerTextSize/2,speakerDisplayName);
|
|
game->DrawShadowStringPropDecal(dialogDisplayPos-vf2d{10,10},activeText);
|
|
}
|
|
}
|
|
|
|
VisualNovel::VisualNovel(){}
|
|
|
|
Command::Command(){}
|
|
|
|
void LocationCommand::Execute(VisualNovel&vn){
|
|
vn.locationDisplayTime=5.f;
|
|
vn.locationDisplayText=location;
|
|
vn.ExecuteNextCommand();
|
|
}
|
|
LocationCommand::LocationCommand(std::string location)
|
|
:location(location){}
|
|
CommandType::CommandType LocationCommand::GetType(){return CommandType::LOCATION;}
|
|
|
|
void BackgroundCommand::Execute(VisualNovel&vn){
|
|
vn.prevBackgroundFilename=vn.backgroundFilename;
|
|
vn.backgroundFilename=backgroundFilename;
|
|
vn.transitionTime=2.0f;
|
|
vn.ExecuteNextCommand();
|
|
}
|
|
BackgroundCommand::BackgroundCommand(std::string backgroundFilename)
|
|
:backgroundFilename(backgroundFilename){}
|
|
CommandType::CommandType BackgroundCommand::GetType(){return CommandType::BACKGROUND;}
|
|
|
|
void LeftCommand::Execute(VisualNovel&vn){
|
|
vn.leftCharacters=characters;
|
|
vn.ExecuteNextCommand();
|
|
}
|
|
LeftCommand::LeftCommand(std::vector<std::string>characters)
|
|
:characters(characters){}
|
|
CommandType::CommandType LeftCommand::GetType(){return CommandType::LEFT;}
|
|
|
|
void RightCommand::Execute(VisualNovel&vn){
|
|
vn.rightCharacters=characters;
|
|
vn.ExecuteNextCommand();
|
|
}
|
|
RightCommand::RightCommand(std::vector<std::string>characters)
|
|
:characters(characters){}
|
|
CommandType::CommandType RightCommand::GetType(){return CommandType::RIGHT;}
|
|
|
|
void SpeakerCommand::Execute(VisualNovel&vn){
|
|
vn.speakerDisplayName=displayedName;
|
|
vn.actualSpeakerName=actualSpeakerName;
|
|
vn.ExecuteNextCommand();
|
|
}
|
|
SpeakerCommand::SpeakerCommand(std::string speaker)
|
|
:displayedName(speaker),actualSpeakerName(speaker){}
|
|
SpeakerCommand::SpeakerCommand(std::string displayedName,std::string speaker)
|
|
:displayedName(displayedName),actualSpeakerName(speaker){}
|
|
CommandType::CommandType SpeakerCommand::GetType(){return CommandType::SPEAKER;}
|
|
|
|
void DialogCommand::Execute(VisualNovel&vn){
|
|
bool mustDisplay=vn.activeText.length()==0;
|
|
std::string newText=util::WrapText(game,vn.activeText+(vn.activeText.length()>0?" ":"")+dialog,game->GetScreenSize().x-48,true,{1,1});
|
|
if(game->GetTextSizeProp(newText).y>40){//Hit the maximum of 3 lines.
|
|
if(!mustDisplay){
|
|
vn.commandIndex--;
|
|
}
|
|
}else{
|
|
vn.activeText=newText;
|
|
if(vn.commandIndex<vn.commands.size()){
|
|
CommandType::CommandType nextCommandType=vn.commands[vn.commandIndex]->GetType();
|
|
if(nextCommandType==CommandType::DIALOG){ //Only add to dialog when the next command type is a dialog as well.
|
|
vn.ExecuteNextCommand();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
DialogCommand::DialogCommand(std::string dialog)
|
|
:dialog(dialog){}
|
|
CommandType::CommandType DialogCommand::GetType(){return CommandType::DIALOG;}
|
|
|
|
void PauseCommand::Execute(VisualNovel&vn){}
|
|
PauseCommand::PauseCommand(){}
|
|
CommandType::CommandType PauseCommand::GetType(){return CommandType::PAUSE;} |