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.scripting.parser;
22 import novelate.config;
23 import novelate.screens;
24 import novelate.state;
25 import novelate.fonts;
26 import novelate.events;
27 import novelate.colormanager;
28 import novelate.external;
29 
30 /// Enumeration of layer types. There are 10 available layers ranging from 0 - 9 as indexes. 7 is the largest named frame.
31 enum LayerType : size_t
32 {
33   /// The background layer.
34   background = 0,
35   /// The object background.
36   objectBackground = 1,
37   /// The object foreground.
38   objectForeground = 2,
39   /// The character layer.
40   character = 3,
41   /// The front character layer.
42   characterFront = 4,
43   /// The dialogue box layer.
44   dialogueBox = 5,
45   /// The dialogue box interaction layer. Generally used for text, options etc.
46   dialogueBoxInteraction = 6,
47   /// The front layer.
48   front = 7
49 }
50 
51 /**
52 * 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.
53 * Params:
54 *   width = The width of the resolution.
55 *   height = The height of the resolution.
56 *   fullScreen = A boolean determining whether the game is full-screen or not.
57 */
58 void changeResolution(size_t width, size_t height, bool fullScreen)
59 {
60   _width = width;
61   _height = height;
62 
63   if (_window && _window.isOpen)
64   {
65     _window.close();
66 
67     _window = null;
68   }
69 
70   write(config.dataFolder ~ "/res.ini", format("Width=%s\r\nHeight=%s\r\nFullScreen=%s", width, height, fullScreen));
71 
72   _window = ExternalWindow.create(_title, width, height, fullScreen);
73 
74   _window.fps = _fps;
75 
76   if (_activeScreens)
77   {
78     foreach (k,v; _activeScreens)
79     {
80       v.refresh(_width, _height);
81     }
82   }
83 
84   fireEvent!(EventType.onResolutionChange);
85 }
86 
87 /// Loads the credits video. Currently does nothing.
88 void loadCreditsVideo()
89 {
90   fireEvent!(EventType.onLoadingCreditsVideo);
91 
92   // ...
93 }
94 
95 /// Enumeration of standard screens.
96 enum StandardScreen : string
97 {
98   /// No screen.
99   none = "none",
100   /// The main menu.
101   mainMenu = "mainMenu",
102   /// The load screen.
103   load = "load",
104   /// The save screen.
105   save = "save",
106   /// The about screen.
107   about = "about",
108   /// The characters screen.
109   characters = "characters",
110   /// The game play screen (scene)
111   scene = "scene"
112 }
113 
114 /**
115 * Adds a screen to the game.
116 * Params:
117 *   screen = The screen to add.
118 */
119 void addScreen(Screen screen)
120 {
121   if (!screen)
122   {
123     return;
124   }
125 
126   screen.setWidthAndHeight(_width, _height);
127 
128   _activeScreens[screen.name] = screen;
129 }
130 
131 /**
132 * Removes a screen from the game.
133 * Params:
134 *   screenName = The name of the screen.
135 */
136 void removeScreen(string screenName)
137 {
138   if (!_activeScreens)
139   {
140     return;
141   }
142 
143   _activeScreens.remove(screenName);
144 }
145 
146 /**
147 * Changes the active screen.
148 * Params:
149 *   screenName = The screen to change to. You should use the "Screen" enum for accuracy of the screen name.
150 *   data = The data passed onto the screen.
151 */
152 void changeActiveScreen(string screenName, string[] data = null)
153 {
154   if (!_activeScreens)
155   {
156     return;
157   }
158 
159   auto screen = _activeScreens.get(screenName, null);
160 
161   if (!screen)
162   {
163     return;
164   }
165 
166   if (screen.shouldClearLayers(data))
167   {
168     screen.clearAllLayersButBackground();
169   }
170 
171   screen.update(data);
172 
173   _activeScreen = screen;
174   _activeScreenName = screen.name;
175 
176   fireEvent!(EventType.onScreenChange);
177 }
178 
179 /// Initializes the game.
180 void initialize()
181 {
182   initExternalBinding();
183 
184   parseFile("main.txt");
185 
186   loadFonts(config.dataFolder ~ "/fonts");
187 
188   _title = config.gameTitle;
189 
190   if (config.gameSlogan && config.gameSlogan.length)
191   {
192     _title ~= " - " ~ config.gameSlogan;
193   }
194 
195   fullScreen = false;
196 
197   if (exists(config.dataFolder ~ "/res.ini"))
198   {
199     auto lines = readText(config.dataFolder ~ "/res.ini").replace("\r", "").split("\n");
200 
201     foreach (line; lines)
202     {
203       if (!line || !line.strip.length)
204       {
205         continue;
206       }
207 
208       auto data = line.split("=");
209 
210       if (data.length != 2)
211       {
212         continue;
213       }
214 
215       switch (data[0])
216       {
217         case "Width": _width = to!size_t(data[1]); break;
218         case "Height": _height = to!size_t(data[1]); break;
219         case "FullScreen": fullScreen = to!bool(data[1]); break;
220 
221         default: break;
222       }
223     }
224   }
225 
226   addScreen(new MainMenuScreen);
227   addScreen(new PlayScreen);
228 
229   playScene = config.startScene;
230 }
231 
232 /// Runs the game/event/UI loop.
233 void run()
234 {
235     auto backgroundColor = colorFromRGBA(0,0,0,0xff);
236 
237     changeResolution(_width, _height, fullScreen);
238 
239     changeActiveScreen(StandardScreen.mainMenu);
240 
241     auto manager = new ExternalEventManager;
242     manager.addHandler(ExternalEventType.closed, {
243       _window.close();
244     });
245 
246     manager.addHandler(ExternalEventType.mouseMoved, {
247       if (_activeScreen)
248       {
249         _activeScreen.mouseMove(ExternalEventState.mouseMoveEvent.x, ExternalEventState.mouseMoveEvent.y);
250       }
251     });
252     manager.addHandler(ExternalEventType.mouseButtonPressed, {
253       if (_activeScreen)
254       {
255         _activeScreen.mousePress(ExternalEventState.mouseButtonEvent.button);
256       }
257     });
258     manager.addHandler(ExternalEventType.mouseButtonReleased, {
259       if (_activeScreen)
260       {
261         _activeScreen.mouseRelease(ExternalEventState.mouseButtonEvent.button);
262       }
263     });
264 
265     manager.addHandler(ExternalEventType.keyPressed, {
266       if (_activeScreen)
267       {
268         _activeScreen.keyPress(ExternalEventState.keyEvent.code);
269       }
270     });
271     manager.addHandler(ExternalEventType.keyReleased, {
272       if (_activeScreen)
273       {
274         _activeScreen.keyRelease(ExternalEventState.keyEvent.code);
275       }
276     });
277 
278     while (running && _window && _window.isOpen)
279     {
280       if (exitGame)
281       {
282         exitGame = false;
283         running = false;
284         _window.close();
285         goto exit;
286       }
287 
288       if (endGame)
289       {
290         if (config.creditsVideo)
291         {
292           // Should be a component ...
293           loadCreditsVideo();
294         }
295         else
296         {
297           changeActiveScreen(StandardScreen.mainMenu);
298         }
299 
300         endGame = false;
301         playScene = config.startScene;
302       }
303 
304       if (nextScene)
305       {
306         auto sceneScreen = _activeScreens[StandardScreen.scene];
307 
308         if (sceneScreen)
309         {
310           sceneScreen.update([nextScene]);
311         }
312 
313         nextScene = null;
314       }
315 
316       if (!_window.processEvents(manager))
317       {
318         goto exit;
319       }
320 
321       if (_window.canUpdate)
322       {
323         _window.clear(backgroundColor);
324 
325         if (_activeScreen)
326         {
327           _activeScreen.render(_window);
328         }
329 
330         fireEvent!(EventType.onRender);
331 
332         _window.render();
333       }
334     }
335     exit:
336     quit();
337 }