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