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 }