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 }