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 }