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 }