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 }