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