1 /** 2 * Copyright © Novelate 2020 3 * License: MIT (https://github.com/Novelate/NovelateEngine/blob/master/LICENSE) 4 * Author: Jacob Jensen (bausshf) 5 * Website: https://novelate.com/ 6 * ------ 7 * Novelate is a free and open-source visual novel engine and framework written in the D programming language. 8 * It can be used freely for both personal and commercial projects. 9 * ------ 10 * Module Description: 11 * This module handles the core game play of the visual novel. 12 */ 13 module novelate.play; 14 15 import std.array : split; 16 import std.conv : to; 17 18 import dsfml.system : Vector2f; 19 import dsfml.audio : Music; 20 import dsfml.window : Event, Keyboard, Mouse; 21 22 public alias MouseButton = Mouse.Button; 23 public alias Key = Keyboard.Key; 24 25 import novelate.config; 26 import novelate.scene; 27 import novelate.media; 28 import novelate.music; 29 import novelate.imagecomponent; 30 import novelate.dialoguebox; 31 import novelate.colormanager; 32 import novelate.timedtext; 33 import novelate.character; 34 import novelate.queue; 35 import novelate.label; 36 import novelate.animatedimage; 37 import novelate.events; 38 39 import novelate.core : getLayer, LayerType, clearTempLayers, setTempLayers, Screen; 40 import novelate.state : nextScene, endGame, playScene, changeTempScreen; 41 42 private 43 { 44 /// The current scene. 45 NovelateScene _currentScene; 46 /// The current music name. 47 string _music; 48 /// The current background. 49 string _background; 50 /// The current music stream. 51 Music _currentMusic; 52 } 53 54 package(novelate): 55 /** 56 * Handles the play screen. 57 * Params: 58 * sceneName = The name of the scene to play. 59 */ 60 void changeScene(string sceneName) 61 { 62 if (_currentScene && _currentScene.name == sceneName) 63 { 64 clearTempLayers(); 65 return; 66 } 67 68 _currentScene = getScene(sceneName); 69 70 if (!_currentScene) 71 { 72 return; 73 } 74 75 playScene = sceneName; 76 77 fireEvent!(EventType.onSceneChange); 78 79 if (_currentScene.music && _currentScene.music.length) 80 { 81 auto musicFile = getMusicFile(_currentScene.music); 82 83 if (musicFile && musicFile.path.length && _music != musicFile.path) 84 { 85 _music = musicFile.path; 86 87 if (_currentMusic) 88 { 89 _currentMusic.stop(); 90 } 91 92 _currentMusic = new Music(); 93 94 if (_currentMusic.openFromFile(to!string(_music))) 95 { 96 _currentMusic.isLooping = true; 97 98 _currentMusic.play(); 99 } 100 } 101 } 102 103 if (_currentScene.background && _currentScene.background.length) 104 { 105 _background = _currentScene.background; 106 } 107 108 auto backgroundLayer = getLayer(LayerType.background); 109 110 { 111 auto oldBackground = cast(ImageComponent)backgroundLayer.getComponent("background"); 112 113 if (oldBackground) 114 { 115 backgroundLayer.addComponent(oldBackground, "background_old"); 116 backgroundLayer.removeComponent("background"); 117 oldBackground.fadeOut(20); 118 } 119 } 120 121 { 122 auto oldBackground = cast(AnimatedImage)backgroundLayer.getComponent("background"); 123 124 if (oldBackground) 125 { 126 backgroundLayer.addComponent(oldBackground, "background_old"); 127 backgroundLayer.removeComponent("background"); 128 oldBackground.fadeOut(20); 129 } 130 } 131 132 if (_background && _background.length) 133 { 134 auto backgroundImage = getMediaFile(_background); 135 136 if (backgroundImage) 137 { 138 auto image = new ImageComponent(backgroundImage.relativePath(backgroundLayer.width)); 139 image.fadeIn(20); 140 image.fadedIn = () 141 { 142 backgroundLayer.removeComponent("background_old"); 143 }; 144 image.fullScreen = true; 145 image.refresh(backgroundLayer.width, backgroundLayer.height); 146 147 backgroundLayer.addComponent(image, "background"); 148 } 149 } 150 151 auto dialogueBoxLayer = getLayer(LayerType.dialogueBox); 152 dialogueBoxLayer.removeComponent("dialogueBox"); 153 154 auto dialogueBox = new DialogueBox; 155 dialogueBox.globalKeyRelease = (k, ref b) 156 { 157 if (k == Key.BackSpace || k == Key.Escape) 158 { 159 setTempLayers(); 160 161 changeTempScreen = Screen.mainMenu; 162 } 163 }; 164 165 dialogueBox.refresh(backgroundLayer.width, backgroundLayer.height); 166 167 dialogueBox.color = colorFromString(config.defaultDialogueBackground); 168 169 dialogueBoxLayer.addComponent(dialogueBox, "dialogueBox"); 170 171 auto dialogueBoxInteractionLayer = getLayer(LayerType.dialogueBoxInteraction); 172 dialogueBoxInteractionLayer.clear(); 173 174 auto dialogueText = new TimedText; 175 dialogueText.color = colorFromString(config.defaultDialogueColor); 176 dialogueText.fontSize = config.defaultDialogueTextFontSize; 177 if (config.defaultDialogueTextFont && config.defaultDialogueTextFont.length) 178 { 179 dialogueText.fontName = config.defaultDialogueTextFont; 180 } 181 dialogueText.text = ""; 182 dialogueText.refresh(backgroundLayer.width, backgroundLayer.height); 183 dialogueBoxInteractionLayer.addComponent(dialogueText, "dialogueText"); 184 185 auto nameLabel = new Label; 186 nameLabel.color = colorFromString(config.defaultDialogueColor); 187 nameLabel.fontSize = config.defaultDialogueNameFontSize; 188 if (config.defaultDialogueNameFont && config.defaultDialogueNameFont.length) 189 { 190 nameLabel.fontName = config.defaultDialogueNameFont; 191 } 192 nameLabel.text = ""; 193 nameLabel.refresh(backgroundLayer.width, backgroundLayer.height); 194 dialogueBoxInteractionLayer.addComponent(nameLabel, "nameLabel"); 195 196 NovelateCharacter currentCharacter = null; 197 string nextSpritePosition = "Left"; 198 bool keepSprite = false; 199 200 auto characterLayer = getLayer(LayerType.character); 201 characterLayer.clear(); 202 203 Queue!NovelateSceneAction actionQueue = new Queue!NovelateSceneAction; 204 205 foreach (action; _currentScene.actions) 206 { 207 actionQueue.enqueue(action); 208 } 209 210 bool firstText = false; 211 212 void handleAction() 213 { 214 if (!actionQueue.has) 215 { 216 return; 217 } 218 219 auto action = actionQueue.dequeue(); 220 221 if (!action) 222 { 223 return; 224 } 225 226 switch (action.type) 227 { 228 case NovelateSceneActionType.characterChange: 229 { 230 currentCharacter = getCharacter(action.name); 231 232 nameLabel.text = ""; 233 nameLabel.color = colorFromString(currentCharacter.nameColor); 234 nameLabel.text = to!dstring(currentCharacter.name); 235 break; 236 } 237 238 case NovelateSceneActionType.action: 239 { 240 switch (action.name) 241 { 242 case "KeepSprite": 243 { 244 keepSprite = true; 245 break; 246 } 247 248 case "End": 249 { 250 if (_currentMusic) 251 { 252 _currentMusic.stop(); 253 } 254 255 _currentScene = null; 256 _music = null; 257 _background = null; 258 _currentMusic = null; 259 endGame = true; 260 break; 261 } 262 263 default: break; 264 } 265 break; 266 } 267 268 case NovelateSceneActionType.option: 269 { 270 switch (action.name) 271 { 272 case "SpritePosition": 273 nextSpritePosition = action.value; 274 break; 275 276 case "Sprite": 277 { 278 if (!keepSprite) 279 { 280 characterLayer.clear(); 281 } 282 283 keepSprite = false; 284 285 auto image = new ImageComponent(currentCharacter.getImage(action.value, backgroundLayer.width)); 286 image.fadeIn(12); 287 auto imgSize = image.size; 288 289 switch (nextSpritePosition) 290 { 291 case "Left": 292 image.position = Vector2f(12, backgroundLayer.height - imgSize.y); 293 break; 294 295 case "Right": 296 image.position = Vector2f(backgroundLayer.width - (12 + imgSize.x), backgroundLayer.height - imgSize.y); 297 break; 298 299 case "Center": 300 image.position = Vector2f((backgroundLayer.width / 2) - (imgSize.x / 2), backgroundLayer.height - imgSize.y); 301 break; 302 303 default: break; 304 } 305 306 characterLayer.addComponent(image, "character_" ~ currentCharacter.name); 307 break; 308 } 309 310 case "Text": 311 { 312 dialogueText.text = to!dstring(action.value); 313 break; 314 } 315 316 case "Option": 317 { 318 dialogueBoxInteractionLayer.removeComponent("dialogueText"); 319 dialogueBoxInteractionLayer.removeComponent("nameLabel"); 320 321 auto optionData = action.value.split("|"); 322 auto optionSceneName = optionData[0]; 323 auto optionText = optionData[1]; 324 325 auto optionLabel = new Label; 326 optionLabel.color = colorFromString(config.defaultDialogueColor); 327 optionLabel.fontSize = config.defaultDialogueTextFontSize; 328 if (config.defaultDialogueTextFont && config.defaultDialogueTextFont.length) 329 { 330 optionLabel.fontName = config.defaultDialogueTextFont; 331 } 332 optionLabel.text = to!dstring(optionText); 333 auto optionY = cast(double)nameLabel.y; 334 optionY += config.defaultDialogueTextFontSize * dialogueBoxInteractionLayer.length; 335 optionY += 8 * dialogueBoxInteractionLayer.length; 336 337 optionLabel.position = Vector2f(nameLabel.x, cast(float)optionY); 338 optionLabel.mouseRelease = (b, ref s) 339 { 340 nextScene = optionSceneName; 341 }; 342 dialogueBoxInteractionLayer.addComponent(optionLabel, optionSceneName); 343 break; 344 } 345 346 default: break; 347 } 348 break; 349 } 350 351 default: break; 352 } 353 354 if (actionQueue.has) 355 { 356 auto peek = actionQueue.peek; 357 358 if (peek.type == NovelateSceneActionType.actionChange) 359 { 360 actionQueue.dequeue(); 361 362 dialogueText.globalMouseReleaseTextFinished.enqueue((b, ref s) 363 { 364 handleAction(); 365 }); 366 } 367 else 368 { 369 handleAction(); 370 } 371 } 372 } 373 374 dialogueText.globalMouseReleaseTextFinished.enqueue((b, ref s) 375 { 376 handleAction(); 377 }); 378 379 handleAction(); 380 }