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 * The core module for Novelate exposes misc. core functionality of the engine.
12 */
13 module novelate.core;
14 
15 import std.file : readText, write, exists;
16 import std.array : replace, split, array;
17 import std.string : strip, stripLeft, stripRight, format;
18 import std.algorithm : filter;
19 import std.conv : to;
20 
21 import novelate.layer;
22 import novelate.config;
23 import novelate.mainmenu;
24 import novelate.play;
25 import novelate.state;
26 import novelate.fonts;
27 import novelate.parser;
28 import novelate.events;
29 import novelate.colormanager;
30 import novelate.external;
31 
32 /// Enumeration of layer types. There are 10 available layers ranging from 0 - 9 as indexes. 7 is the largest named frame.
33 enum LayerType : size_t
34 {
35   /// The background layer.
36   background = 0,
37   /// The object background.
38   objectBackground = 1,
39   /// The object foreground.
40   objectForeground = 2,
41   /// The character layer.
42   character = 3,
43   /// The front character layer.
44   characterFront = 4,
45   /// The dialogue box layer.
46   dialogueBox = 5,
47   /// The dialogue box interaction layer. Generally used for text, options etc.
48   dialogueBoxInteraction = 6,
49   /// The front layer.
50   front = 7
51 }
52 
53 /// Clears the temp layers.
54 void clearTempLayers()
55 {
56   _isTempScreen = false;
57   selectedLayers = _layers;
58 
59   foreach (tempLayer; _tempLayers)
60   {
61     tempLayer.clear();
62   }
63 
64   fireEvent!(EventType.onTempScreenClear);
65 }
66 
67 /// Sets the temp layers.
68 void setTempLayers()
69 {
70   _isTempScreen = true;
71   selectedLayers = _tempLayers;
72 
73   fireEvent!(EventType.onTempScreenShow);
74 }
75 
76 /**
77 * Gets a layer by its index. It retrieves it from the current selected layers whether it's the main layers or the temp layers.
78 * Params:
79 *   index = The index of the layer to get.
80 * Returns:
81 *   The layer within the current selected layers.
82 */
83 Layer getLayer(size_t index)
84 {
85   if (!selectedLayers || index >= selectedLayers.length)
86   {
87     return null;
88   }
89 
90   return selectedLayers[index];
91 }
92 
93 /**
94 * Changes the resolution of the game. This should preferebly only be of the following resolutions: 800x600, 1024x768 or 1280x720. However any resolutions are acceptable but may have side-effects attached such as invalid rendering etc. All set resolutions are saved to a res.ini file in the data folder allowing for resolutions to be kept across instances of the game.
95 * Params:
96 *   width = The width of the resolution.
97 *   height = The height of the resolution.
98 *   fullScreen = A boolean determining whether the game is full-screen or not.
99 */
100 void changeResolution(size_t width, size_t height, bool fullScreen)
101 {
102   _width = width;
103   _height = height;
104 
105   if (_window && _window.isOpen)
106   {
107     _window.close();
108 
109     _window = null;
110   }
111 
112   write(config.dataFolder ~ "/res.ini", format("Width=%s\r\nHeight=%s\r\nFullScreen=%s", width, height, fullScreen));
113 
114   _window = ExternalWindow.create(_title, width, height, fullScreen);
115 
116   _window.fps = _fps;
117 
118   if (_layers)
119   {
120     foreach (layer; _layers)
121     {
122       layer.refresh(_width, _height);
123     }
124   }
125 
126   fireEvent!(EventType.onResolutionChange);
127 }
128 
129 /// Loads the credits video. Currently does nothing.
130 void loadCreditsVideo()
131 {
132   fireEvent!(EventType.onLoadingCreditsVideo);
133 
134   // ...
135 }
136 
137 /// Clears all layers for their components except for the background layer as that should usually be cleared by fading-in and fading-out when adding a new background. This adds smoothness to the game.
138 void clearAllLayersButBackground()
139 {
140   if (!selectedLayers || !selectedLayers.length)
141   {
142     return;
143   }
144 
145   foreach (i; 1 .. selectedLayers.length)
146   {
147     selectedLayers[i].clear();
148   }
149 
150   fireEvent!(EventType.onClearingAllLayersButBackground);
151 }
152 
153 /// Enumeration of screens.
154 enum Screen : string
155 {
156   /// No screen.
157   none = "none",
158   /// The main menu.
159   mainMenu = "mainMenu",
160   /// The load screen.
161   load = "load",
162   /// The save screen.
163   save = "save",
164   /// The about screen.
165   about = "about",
166   /// The characters screen.
167   characters = "characters",
168   /// The game play screen (scene)
169   scene = "scene"
170 }
171 
172 /**
173 * Changes the screen.
174 * Params:
175 *   screen = The screen to change to. You should use the "Screen" enum for accuracy of the screen name.
176 *   data = The data passed onto the screen.
177 */
178 void changeScreen(string screen, string[] data = null)
179 {
180   clearAllLayersButBackground();
181 
182   switch (screen)
183   {
184     case Screen.mainMenu: showMainMenu(); break;
185 
186     case Screen.scene: if (data && data.length) changeScene(data[0]); break;
187 
188     default: break; // TODO: Custom screen handling through events.
189   }
190 
191   fireEvent!(EventType.onScreenChange);
192 }
193 
194 /// Initializes the game.
195 void initialize()
196 {
197   parseFile("main.txt");
198 
199   loadFonts(config.dataFolder ~ "/fonts");
200 
201   _title = config.gameTitle;
202 
203   if (config.gameSlogan && config.gameSlogan.length)
204   {
205     _title ~= " - " ~ config.gameSlogan;
206   }
207 
208   fullScreen = false;
209 
210   if (exists(config.dataFolder ~ "/res.ini"))
211   {
212     auto lines = readText(config.dataFolder ~ "/res.ini").replace("\r", "").split("\n");
213 
214     foreach (line; lines)
215     {
216       if (!line || !line.strip.length)
217       {
218         continue;
219       }
220 
221       auto data = line.split("=");
222 
223       if (data.length != 2)
224       {
225         continue;
226       }
227 
228       switch (data[0])
229       {
230         case "Width": _width = to!size_t(data[1]); break;
231         case "Height": _height = to!size_t(data[1]); break;
232         case "FullScreen": fullScreen = to!bool(data[1]); break;
233 
234         default: break;
235       }
236     }
237   }
238 
239   foreach (_; 0 .. 10)
240   {
241     _layers ~= new Layer(_width, _height);
242     _tempLayers ~= new Layer(_width, _height);
243   }
244 
245   selectedLayers = _layers;
246 
247   playScene = config.startScene;
248 }
249 
250 /// Runs the game/event/UI loop.
251 void run()
252 {
253     auto backgroundColor = colorFromRGBA(0,0,0,0xff);
254 
255     changeResolution(_width, _height, fullScreen);
256 
257     changeScreen(Screen.mainMenu);
258 
259     auto manager = new ExternalEventManager;
260     manager.addHandler(ExternalEventType.closed, {
261       _window.close();
262     });
263 
264     manager.addHandler(ExternalEventType.mouseMoved, {
265       if (selectedLayers && selectedLayers.length)
266       {
267         foreach_reverse (layer; selectedLayers)
268         {
269           bool stopEvent = false;
270 
271           layer.mouseMove(ExternalEventState.mouseMoveEvent.x, ExternalEventState.mouseMoveEvent.y, stopEvent);
272 
273           if (stopEvent)
274           {
275             break;
276           }
277         }
278       }
279     });
280     manager.addHandler(ExternalEventType.mouseButtonPressed, {
281       if (selectedLayers && selectedLayers.length)
282       {
283         foreach_reverse (layer; selectedLayers)
284         {
285           bool stopEvent = false;
286 
287           layer.mousePress(ExternalEventState.mouseButtonEvent.button, stopEvent);
288 
289           if (stopEvent)
290           {
291             break;
292           }
293         }
294       }
295     });
296     manager.addHandler(ExternalEventType.mouseButtonReleased, {
297       if (selectedLayers && selectedLayers.length)
298       {
299         foreach_reverse (layer; selectedLayers)
300         {
301           bool stopEvent = false;
302 
303           layer.mouseRelease(ExternalEventState.mouseButtonEvent.button, stopEvent);
304 
305           if (stopEvent)
306           {
307             break;
308           }
309         }
310       }
311     });
312 
313     manager.addHandler(ExternalEventType.keyPressed, {
314       if (selectedLayers && selectedLayers.length)
315       {
316         foreach_reverse (layer; selectedLayers)
317         {
318           bool stopEvent = false;
319 
320           layer.keyPress(ExternalEventState.keyEvent.code, stopEvent);
321 
322           if (stopEvent)
323           {
324             break;
325           }
326         }
327       }
328     });
329     manager.addHandler(ExternalEventType.keyReleased, {
330       if (selectedLayers && selectedLayers.length)
331       {
332         foreach_reverse (layer; selectedLayers)
333         {
334           bool stopEvent = false;
335 
336           layer.keyRelease(ExternalEventState.keyEvent.code, stopEvent);
337 
338           if (stopEvent)
339           {
340             break;
341           }
342         }
343       }
344     });
345 
346     while (running && _window && _window.isOpen)
347     {
348       if (exitGame)
349       {
350         exitGame = false;
351         running = false;
352         _window.close();
353         goto exit;
354       }
355 
356       if (endGame)
357       {
358         if (config.creditsVideo)
359         {
360           // Should be a component ...
361           loadCreditsVideo();
362         }
363         else
364         {
365           changeScreen(Screen.mainMenu);
366         }
367 
368         endGame = false;
369         playScene = config.startScene;
370       }
371 
372       if (changeTempScreen != Screen.none)
373       {
374         changeScreen(changeTempScreen);
375 
376         changeTempScreen = Screen.none;
377       }
378 
379       if (nextScene)
380       {
381         changeScene(nextScene);
382 
383         nextScene = null;
384       }
385 
386       if (!_window.processEvents(manager))
387       {
388         goto exit;
389       }
390 
391       _window.clear(backgroundColor);
392 
393       if (_layers && _layers.length)
394       {
395         foreach (layer; _layers)
396         {
397           layer.render(_window);
398         }
399       }
400 
401       if (_tempLayers && _tempLayers.length)
402       {
403         foreach (layer; _tempLayers)
404         {
405           layer.render(_window);
406         }
407       }
408 
409       fireEvent!(EventType.onRender);
410 
411       _window.render();
412     }
413     exit:
414 }