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 }