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