aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJon Santmyer <jon@jonsantmyer.com>2021-05-27 19:07:57 -0400
committerJon Santmyer <jon@jonsantmyer.com>2021-05-27 19:07:57 -0400
commitd3602d5cccf7f6c4a7f975694db2b16db28cdd60 (patch)
tree8640f324adea00102d039378c3a46bb9331cf266
downloadpostmodern-d3602d5cccf7f6c4a7f975694db2b16db28cdd60.tar.gz
postmodern-d3602d5cccf7f6c4a7f975694db2b16db28cdd60.tar.bz2
postmodern-d3602d5cccf7f6c4a7f975694db2b16db28cdd60.zip
working screens / windows system. title screen and escape menu
-rw-r--r--.gitignore2
-rw-r--r--Makefile59
-rw-r--r--bin/assets/title.txt6
-rw-r--r--include/canvaswindow.h20
-rw-r--r--include/game.h47
-rw-r--r--include/input.h48
-rw-r--r--include/pausescreen.h28
-rw-r--r--include/screen.h29
-rw-r--r--include/testscreen.h4
-rw-r--r--include/textwindow.h25
-rw-r--r--include/titlescreen.h32
-rw-r--r--include/window.h51
-rw-r--r--src/game.cpp167
-rw-r--r--src/input.cpp85
-rw-r--r--src/main.cpp21
-rw-r--r--src/pausescreen.cpp101
-rw-r--r--src/screen.cpp24
-rw-r--r--src/textwindow.cpp56
-rw-r--r--src/titlescreen.cpp124
-rw-r--r--src/window.cpp94
20 files changed, 1023 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..5bdc739
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+*.o
+postmodern
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..f4825bd
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,59 @@
+TRUECOLOR := 1
+EXTCOLOR := 0
+VGACOLOR := 0
+
+PWD := $(shell pwd)
+
+SRCDIR := $(PWD)/src
+INCDIR := $(PWD)/include
+SRCFILES := $(wildcard $(SRCDIR)/*.cpp)
+
+OBJFILES := $(patsubst %.cpp,%.o,$(SRCFILES))
+
+TARGET_NAME := postmodern
+TARGET_PREFIX := $(PWD)/bin
+TARGET := $(TARGET_PREFIX)/$(TARGET_NAME)
+
+WARNINGS := -Wall \
+ -Wextra \
+ -Wformat-signedness \
+ -Wlogical-op \
+ -Wmissing-declarations \
+ -Wmissing-noreturn \
+ -Wnon-virtual-dtor \
+ -Wold-style-cast \
+ -Woverloaded-virtual \
+ -Wpedantic \
+ -Wsuggest-override \
+ -Wunused-macros \
+ -Wzero-as-null-pointer-constant
+
+CPP := g++
+CPPFLAGS := -std=gnu++20 $(WARNINGS) -I$(INCDIR) -g
+LIBS :=
+
+ifeq ($(TRUECOLOR), 1)
+ CPPFLAGS += -DTRUECOLOR
+else ifeq ($(EXTCOLOR), 1)
+ CPPFLAGS += -DEXTCOLOR
+else ifeq ($(VGACOLOR), 1)
+ CPPFLAGS += -DVGACOLOR
+else
+ CPPFLAGS += -DNOCOLOR
+endif
+
+.PHONY: all
+all: $(OBJFILES)
+ $(CPP) $(CPPFLAGS) $(LIBS) $(OBJFILES) -o $(TARGET)
+
+run:
+ cd $(TARGET_PREFIX); ./$(TARGET_NAME)
+
+clean:
+ rm $(OBJFILES)
+ rm $(TARGET)
+
+%.o:%.cpp
+ $(CPP) $(CPPFLAGS) -c $< -o $@
+
+# vim: set ts=4 sw=0 tw=0 noet :
diff --git a/bin/assets/title.txt b/bin/assets/title.txt
new file mode 100644
index 0000000..f027939
--- /dev/null
+++ b/bin/assets/title.txt
@@ -0,0 +1,6 @@
+ ____ ___ ____ _____ __ __ ___ ____ _____ ____ _ _
+| _ \ / _ \/ ___|_ _| \/ |/ _ \| _ \| ____| _ \| \ | |
+| |_) | | | \___ \ | | | |\/| | | | | | | | _| | |_) | \| |
+| __/| |_| |___) || | | | | | |_| | |_| | |___| _ <| |\ |
+|_| \___/|____/ |_| |_| |_|\___/|____/|_____|_| \_\_| \_|
+
diff --git a/include/canvaswindow.h b/include/canvaswindow.h
new file mode 100644
index 0000000..a44056f
--- /dev/null
+++ b/include/canvaswindow.h
@@ -0,0 +1,20 @@
+#ifndef POSTMODERN_CANVAS_WINDOW_H
+#define POSTMODERN_CANVAS_WINDOW_H 1
+
+#include "window.h"
+
+class CanvasWindow : public Window
+{
+protected:
+
+private:
+
+public:
+ CanvasWindow(int x, int y, int width, int height, bool bordered);
+ ~CanvasWindow();
+
+ void plot(int x, int y, char c);
+
+};
+
+#endif
diff --git a/include/game.h b/include/game.h
new file mode 100644
index 0000000..66a4eb4
--- /dev/null
+++ b/include/game.h
@@ -0,0 +1,47 @@
+#ifndef POSTMODERN_GAME_H
+#define POSTMODERN_GAME_H 1
+
+#include <string>
+#include <memory>
+#include <unordered_map>
+#include <deque>
+#include <termios.h>
+
+#include "screen.h"
+
+class Game {
+private:
+ static int m_width;
+ static int m_height;
+ static bool m_running;
+
+ static std::unordered_map<std::string, std::shared_ptr<Screen>> m_screens;
+ static std::deque<std::weak_ptr<Screen>> m_order;
+ static bool m_dirty;
+
+ static bool m_rawmode;
+ static struct termios m_tosOriginal;
+
+ static void update();
+ static void render();
+protected:
+public:
+ static void init();
+ static void loop();
+
+ static void addScreen(const std::string &key, std::shared_ptr<Screen> value);
+ static bool pushScreen(const std::string &key);
+ static bool popScreen();
+ static bool popUpto(const std::string &key);
+ static bool popUptoIncluding(const std::string &key);
+
+ static void enableRawmode();
+ static void disableRawmode();
+
+ static void stop() { m_running = false; }
+
+ static int getWidth() { return m_width; }
+ static int getHeight() { return m_height; }
+};
+
+#endif
diff --git a/include/input.h b/include/input.h
new file mode 100644
index 0000000..f3943d5
--- /dev/null
+++ b/include/input.h
@@ -0,0 +1,48 @@
+#ifndef POSTMODERN_INPUT_H
+#define POSTMODERN_INPUT_H 1
+
+enum class Keycode {
+ TERMINATE = 128,
+ F1,
+ F2,
+ F3,
+ F4,
+ F5,
+ F6,
+ F7,
+ F8,
+ F9,
+ F10,
+ F11,
+ F12,
+ NUMPAD_HOME,
+ NUMPAD_UP,
+ NUMPAD_PGUP,
+ NUMPAD_LEFT,
+ NUMPAD_5,
+ NUMPAD_RIGHT,
+ NUMPAD_END,
+ NUMPAD_DOWN,
+ NUMPAD_PGDN,
+ HOME,
+ UP,
+ PGUP,
+ LEFT,
+ RIGHT,
+ END ,
+ DOWN,
+ PGDOWN,
+ INSERT,
+ DELETE,
+ CTRL_MOD,
+ ALT_MOD,
+};
+
+class Input {
+private:
+protected:
+public:
+ static int getch();
+};
+
+#endif
diff --git a/include/pausescreen.h b/include/pausescreen.h
new file mode 100644
index 0000000..02d3c4d
--- /dev/null
+++ b/include/pausescreen.h
@@ -0,0 +1,28 @@
+#ifndef POSTMODERN_PAUSESCREEN_H
+#define POSTMODERN_PAUSESCREEN_H 1
+
+#include "textwindow.h"
+#include "screen.h"
+
+class PauseScreen : public Screen {
+private:
+ enum class Selection {
+ RESUME = 0,
+ SETTINGS,
+ ABORT,
+ SELECTION_COUNT
+ };
+
+ std::shared_ptr<TextWindow> m_menuWindow;
+
+ int m_selection;
+ void parseSelection();
+protected:
+public:
+ PauseScreen();
+ ~PauseScreen();
+
+ void update() override;
+};
+
+#endif
diff --git a/include/screen.h b/include/screen.h
new file mode 100644
index 0000000..d7de4bb
--- /dev/null
+++ b/include/screen.h
@@ -0,0 +1,29 @@
+#ifndef POSTMODERN_SCREEN_H
+#define POSTMODERN_SCREEN_H 1
+
+#include "window.h"
+
+#include <unordered_map>
+#include <deque>
+#include <string>
+#include <memory>
+
+class Screen {
+private:
+ bool m_redraw;
+protected:
+ std::unordered_map<std::string, std::shared_ptr<Window>> m_windows;
+ std::deque<std::weak_ptr<Window>> m_order;
+public:
+ Screen() {}
+ virtual ~Screen() {}
+
+ void addWindow(const std::string &key, std::shared_ptr<Window> value);
+
+ virtual void update() {}
+ void render();
+
+ void setRedraw() { m_redraw = true; }
+};
+
+#endif
diff --git a/include/testscreen.h b/include/testscreen.h
new file mode 100644
index 0000000..6d17760
--- /dev/null
+++ b/include/testscreen.h
@@ -0,0 +1,4 @@
+#ifndef POSTMODERN_TESTSCREEN_H
+#define POSTMODERN_TESTSCREEN_H 1
+
+#endif
diff --git a/include/textwindow.h b/include/textwindow.h
new file mode 100644
index 0000000..00ac3ed
--- /dev/null
+++ b/include/textwindow.h
@@ -0,0 +1,25 @@
+#ifndef POSTMODERN_TEXT_WINDOW_H
+#define POSTMODERN_TEXT_WINDOW_H 1
+
+#include <cstdarg>
+
+#include "window.h"
+
+class TextWindow : public Window
+{
+private:
+protected:
+ int m_colorfg;
+ int m_colorbg;
+public:
+ TextWindow(int x, int y, int width, int height, bool bordered);
+ ~TextWindow();
+
+ void setfg(int fg) { m_colorfg = fg; }
+ void setbg(int bg) { m_colorbg = bg; }
+
+ int vprint(const char *fmt, va_list ap);
+ int print(const char *fmt, ...);
+};
+
+#endif
diff --git a/include/titlescreen.h b/include/titlescreen.h
new file mode 100644
index 0000000..7d8088a
--- /dev/null
+++ b/include/titlescreen.h
@@ -0,0 +1,32 @@
+#ifndef POSTMODERN_SCREEN_TITLE_H
+#define POSTMODERN_SCREEN_TITLE_H 1
+
+#include "textwindow.h"
+#include "screen.h"
+
+class TitleScreen : public Screen{
+private:
+ enum class Selection {
+ PLAY = 0,
+ WORLDS,
+ SETTINGS,
+ TESTGEN,
+ QUIT,
+ SELECTION_COUNT
+ };
+
+ std::shared_ptr<Window> m_graphicWindow;
+ std::shared_ptr<TextWindow> m_selectionWindow;
+
+ int m_selection;
+
+ void parseSelection();
+protected:
+public:
+ TitleScreen();
+ ~TitleScreen();
+
+ void update() override;
+};
+
+#endif
diff --git a/include/window.h b/include/window.h
new file mode 100644
index 0000000..425da0f
--- /dev/null
+++ b/include/window.h
@@ -0,0 +1,51 @@
+#ifndef POSTMODERN_WINDOW_H
+#define POSTMODERN_WINDOW_H 1
+
+struct Cell {
+ Cell() : chr(0), fg(0), bg(0) {}
+
+ char chr;
+ unsigned int fg;
+ unsigned int bg;
+
+ bool operator==(const Cell &a)
+ {
+ return a.chr == chr && a.fg == fg && a.bg == bg;
+ }
+};
+
+class Window {
+private:
+ Cell *m_foreplane;
+ Cell *m_backplane;
+protected:
+ int m_x;
+ int m_y;
+ int m_width;
+ int m_height;
+
+ int m_cursorx;
+ int m_cursory;
+
+ bool m_bordered;
+ bool m_redraw;
+public:
+ Window(int x, int y, int width, int height, bool bordered);
+ ~Window();
+
+ void refresh();
+
+ void clear(int fg, int bg);
+ void plot(int x, int y, char c, int fg, int bg);
+
+ void setRedraw() { m_redraw = true; }
+
+ void moveCursor(int x, int y);
+ void move(int x, int y);
+
+ int getWidth() { return m_width; }
+ int getHeight() { return m_height; }
+ bool getCenter(int &x, int &y); //Returns if has true center
+};
+
+#endif
diff --git a/src/game.cpp b/src/game.cpp
new file mode 100644
index 0000000..b47f0c7
--- /dev/null
+++ b/src/game.cpp
@@ -0,0 +1,167 @@
+#include "game.h"
+
+#include <cstdio>
+#include <cstring>
+#include <sys/ioctl.h>
+
+int Game::m_width;
+int Game::m_height;
+bool Game::m_running;
+
+bool Game::m_rawmode;
+struct termios Game::m_tosOriginal;
+
+std::unordered_map<std::string, std::shared_ptr<Screen>> Game::m_screens;
+std::deque<std::weak_ptr<Screen>> Game::m_order;
+bool Game::m_dirty;
+
+void
+Game::init()
+{
+ //Get terminal width/length
+ struct winsize w;
+ ioctl(0, TIOCGWINSZ, &w);
+
+ m_width = w.ws_col;
+ m_height = w.ws_row;
+
+ //Disable stdout buffer, Clear terminal
+ setbuf(stdout, NULL);
+ puts("\033[40;97m\033[2J");
+
+ m_running = true;
+ m_rawmode = false;
+ m_dirty = false;
+}
+
+void
+Game::enableRawmode()
+{
+ if(m_rawmode) return;
+ tcgetattr(STDIN_FILENO, &m_tosOriginal);
+
+ struct termios tos;
+ memcpy(&tos, &m_tosOriginal, sizeof(struct termios));
+
+ tos.c_lflag &= ~(ECHO | ICANON | IEXTEN | ISIG);
+ tos.c_iflag &= ~(ICRNL | IXON | INPCK | ISTRIP);
+ tos.c_oflag &= ~(OPOST);
+ tos.c_cflag |= (CS8);
+
+ tos.c_cc[VMIN] = 0;
+ tos.c_cc[VTIME] = 1;
+
+ puts("\033[?25l");
+ tcsetattr(STDIN_FILENO, TCSAFLUSH, &tos);
+ m_rawmode = true;
+}
+
+void
+Game::disableRawmode()
+{
+ if(!m_rawmode) return;
+ tcsetattr(STDIN_FILENO, TCSAFLUSH, &m_tosOriginal);
+ puts("\033[?25h");
+ m_rawmode = false;
+}
+
+void
+Game::loop()
+{
+ while(m_running)
+ {
+ render();
+ update();
+ }
+ disableRawmode();
+ puts("\033[0m\033[2J");
+}
+
+void
+Game::update()
+{
+ std::shared_ptr<Screen> locked = nullptr;
+ while(!m_order.empty()){
+ if((locked = m_order.back().lock())) break;
+ m_order.pop_back();
+ }
+
+ if(locked == nullptr) {
+ stop();
+ return;
+ }
+
+ locked->update();
+}
+
+void
+Game::render()
+{
+ for(auto it = m_order.cbegin(); it != m_order.cend(); it++){
+ auto lock = it->lock();
+ if(!lock) continue;
+
+ if(m_dirty){
+ lock->setRedraw();
+ }
+ lock->render();
+ }
+ m_dirty = false;
+}
+
+void
+Game::addScreen(const std::string &key, std::shared_ptr<Screen> value)
+{
+ m_screens.emplace(key, value);
+}
+bool
+Game::pushScreen(const std::string &key)
+{
+ auto it = m_screens.find(key);
+ if(it != m_screens.end()){
+ m_order.push_back(it->second);
+ m_dirty = true;
+ return true;
+ }
+ return false;
+}
+
+bool
+Game::popScreen()
+{
+ m_order.pop_back();
+ m_dirty = true;
+ return m_order.empty();
+}
+
+bool
+Game::popUpto(const std::string &key)
+{
+ auto find = m_screens.find(key);
+ if(find == m_screens.end()){
+ return false;
+ }
+
+ while(!m_order.empty()){
+ auto lock = m_order.back().lock();
+ if(lock == nullptr){
+ m_order.pop_back();
+ continue;
+ }
+
+ if(lock == find->second) break;
+ m_order.pop_back();
+ }
+ m_dirty = true;
+ return true;
+}
+
+bool
+Game::popUptoIncluding(const std::string &key)
+{
+ bool r = popUpto(key);
+ if(!r) return r;
+
+ m_order.pop_back();
+ return true;
+}
diff --git a/src/input.cpp b/src/input.cpp
new file mode 100644
index 0000000..46ba0d7
--- /dev/null
+++ b/src/input.cpp
@@ -0,0 +1,85 @@
+#include "input.h"
+#include "game.h"
+#include <cstdio>
+
+static int
+waitforch()
+{
+ int c = 0;
+ for(; c == 0; read(STDIN_FILENO, &c, 1));
+ return c;
+}
+
+int
+Input::getch()
+{
+ int c = waitforch();
+ if(c == '\033'){
+ char eseq[4];
+ if(read(STDIN_FILENO, eseq, 1) != 1) return c;
+ if(read(STDIN_FILENO, eseq + 1, 1) != 1) return c;
+
+ if(eseq[0] == '\033') return c;
+ if(eseq[0] == '['){
+ if(eseq[1] >= '0' && eseq[1] <= '9'){
+ if(read(STDIN_FILENO, eseq + 2, 1) != 1) return c;
+ if(eseq[2] == '~'){
+ switch(eseq[1]){
+ case '1': return static_cast<int>(Keycode::HOME);
+ case '3': return static_cast<int>(Keycode::DELETE);
+ case '4': return static_cast<int>(Keycode::END);
+ case '5': return static_cast<int>(Keycode::PGUP);
+ case '6': return static_cast<int>(Keycode::PGDOWN);
+ case '7': return static_cast<int>(Keycode::NUMPAD_HOME);
+ case '8': return static_cast<int>(Keycode::NUMPAD_END);
+ }
+ }else if(eseq[2] == 'h'){
+ switch(eseq[1]){
+ case '4': return static_cast<int>(Keycode::INSERT);
+ }
+ }else if(eseq[2] >= '0' && eseq[2] <= '9'){
+ if(read(STDIN_FILENO, eseq + 3, 1) != 1) return c;
+ switch(eseq[1]){
+ case '1': {
+ switch(eseq[2]) {
+ case '5': return static_cast<int>(Keycode::F5);
+ case '7': return static_cast<int>(Keycode::F6);
+ case '8': return static_cast<int>(Keycode::F7);
+ case '9': return static_cast<int>(Keycode::F8);
+ }
+ }
+ break;
+ case '2': {
+ switch(eseq[2]) {
+ case '0': return static_cast<int>(Keycode::F9);
+ case '1': return static_cast<int>(Keycode::F10);
+ case '3': return static_cast<int>(Keycode::F11);
+ case '4': return static_cast<int>(Keycode::F12);
+ }
+ }
+ break;
+ }
+ }
+ }
+ switch(eseq[1]){
+ case 'A': return static_cast<int>(Keycode::UP);
+ case 'B': return static_cast<int>(Keycode::DOWN);
+ case 'C': return static_cast<int>(Keycode::RIGHT);
+ case 'D': return static_cast<int>(Keycode::LEFT);
+ case 'E': return static_cast<int>(Keycode::NUMPAD_5);
+ case 'H': return static_cast<int>(Keycode::HOME);
+ case 'F': return static_cast<int>(Keycode::END);
+ case 'P': return static_cast<int>(Keycode::DELETE);
+ }
+ }
+ if(eseq[0] == 'O'){
+ switch(eseq[1]){
+ case 'P': return static_cast<int>(Keycode::F1);
+ case 'Q': return static_cast<int>(Keycode::F2);
+ case 'R': return static_cast<int>(Keycode::F3);
+ case 'S': return static_cast<int>(Keycode::F4);
+ }
+ }
+ }
+ return c;
+}
diff --git a/src/main.cpp b/src/main.cpp
new file mode 100644
index 0000000..b38a9e0
--- /dev/null
+++ b/src/main.cpp
@@ -0,0 +1,21 @@
+#include "game.h"
+#include "titlescreen.h"
+#include "pausescreen.h"
+
+int
+main(int argc, char **argv)
+{
+ (void)argc;
+ (void)argv;
+
+ Game::init();
+
+ Game::addScreen("title", std::make_shared<TitleScreen>());
+ Game::addScreen("pause", std::make_shared<PauseScreen>());
+
+ Game::pushScreen("title");
+
+ Game::loop();
+
+ return 0;
+}
diff --git a/src/pausescreen.cpp b/src/pausescreen.cpp
new file mode 100644
index 0000000..bbed192
--- /dev/null
+++ b/src/pausescreen.cpp
@@ -0,0 +1,101 @@
+#include "pausescreen.h"
+#include "game.h"
+#include "input.h"
+
+static const int selectionCount = 3;
+static const std::string selectionText[selectionCount] =
+{
+ " Resume ",
+ " Settings ",
+ " Abort "
+};
+
+static void
+printSelections(std::shared_ptr<TextWindow> menuWindow, int selection)
+{
+ int optx = 1;
+ int opty = 1;
+
+ menuWindow->clear(0xFFFFFF, 0x888888);
+ for(int y = 1; y < menuWindow->getHeight() - 1; y++)
+ for(int x = 1; x < menuWindow->getWidth() - 1; x++){
+ menuWindow->plot(x, y, ' ', 0xFFFFFF, 0x0);
+ }
+
+ menuWindow->setfg(0x0);
+ menuWindow->setbg(0x888888);
+ menuWindow->moveCursor(1, 0);
+ menuWindow->print("The game is Paused");
+
+ for(int i = 0; i < selectionCount; i++){
+ menuWindow->setfg(0xFFFFFF);
+ menuWindow->setbg(0x0);
+ if(selection == i){
+ menuWindow->setfg(0x0);
+ menuWindow->setbg(0xFFFFFF);
+ }
+ menuWindow->moveCursor(optx, opty++);
+ menuWindow->print("%s", selectionText[i].c_str());
+ }
+}
+
+PauseScreen::PauseScreen() :
+ m_menuWindow(nullptr),
+ m_selection(0)
+{
+ int sw = Game::getWidth();
+ int sh = Game::getHeight();
+
+ int menuWidth = 20;
+ int menuHeight = 10;
+ int menuX = (sw / 2) - (menuWidth / 2);
+ int menuY = (sh / 2) - (menuHeight / 2);
+
+ m_menuWindow = std::make_shared<TextWindow>(menuX, menuY / 2, menuWidth, menuHeight, false);
+ addWindow("main", m_menuWindow);
+
+ printSelections(m_menuWindow, m_selection);
+}
+
+PauseScreen::~PauseScreen()
+{
+
+}
+
+void
+PauseScreen::update()
+{
+ int c = Input::getch();
+ if(c == '\033'){
+ Game::popScreen();
+ return;
+ }
+
+ if(c == static_cast<int>(Keycode::UP)){
+ m_selection--;
+ if(m_selection < 0) m_selection = 0;
+ }
+
+ if(c == static_cast<int>(Keycode::DOWN)){
+ m_selection++;
+ if(m_selection >= selectionCount) m_selection = selectionCount - 1;
+ }
+
+ if(c == 13){
+ parseSelection();
+ }
+
+ printSelections(m_menuWindow, m_selection);
+}
+
+void
+PauseScreen::parseSelection()
+{
+ if(m_selection == 0) {
+ Game::popScreen();
+ return;
+ }
+ if(m_selection == 2){
+ Game::stop();
+ }
+}
diff --git a/src/screen.cpp b/src/screen.cpp
new file mode 100644
index 0000000..4925c31
--- /dev/null
+++ b/src/screen.cpp
@@ -0,0 +1,24 @@
+#include "screen.h"
+
+#include <ncurses.h>
+
+void
+Screen::addWindow(const std::string &key, std::shared_ptr<Window> value)
+{
+ m_windows.emplace(key, value);
+ m_order.push_back(std::weak_ptr<Window>(value));
+}
+
+void
+Screen::render()
+{
+ for(auto it = m_order.begin(); it != m_order.end(); it++){
+ if(auto obs = it->lock()){
+ if(m_redraw) obs->setRedraw();
+ obs->refresh();
+ }else{
+ m_order.erase(it--);
+ }
+ }
+ m_redraw = false;
+}
diff --git a/src/textwindow.cpp b/src/textwindow.cpp
new file mode 100644
index 0000000..37ec2f0
--- /dev/null
+++ b/src/textwindow.cpp
@@ -0,0 +1,56 @@
+#include "textwindow.h"
+#include <cstdio>
+
+TextWindow::TextWindow(int x, int y, int width, int height, bool bordered) :
+ m_colorfg(0xFFFFFF), m_colorbg(0x0),
+ Window(x, y, width, height, bordered)
+{
+
+}
+TextWindow::~TextWindow()
+{
+
+}
+
+int
+TextWindow::vprint(const char *fmt, va_list ap)
+{
+ //TODO
+ char buffer[2048];
+ vsnprintf(buffer, 2048, fmt, ap);
+
+ for(char *c = buffer; *c; c++){
+ switch(*c){
+ case '\n':
+ m_cursory++;
+ m_cursorx = 0;
+ break;
+ default:
+ plot(m_cursorx++, m_cursory, *c, m_colorfg, m_colorbg);
+ break;
+ }
+ if(m_cursorx >= m_width){
+ m_cursorx = 0;
+ m_cursory++;
+ }
+ if(m_cursory >= m_height){
+ //TODO: Scroll the screen
+ m_cursory = m_height - 1;
+ }
+ }
+
+ return 0;
+}
+
+int
+TextWindow::print(const char *fmt, ...)
+{
+ va_list ap;
+ va_start(ap, fmt);
+
+ int r = vprint(fmt, ap);
+ va_end(ap);
+
+ //TODO
+ return r;
+}
diff --git a/src/titlescreen.cpp b/src/titlescreen.cpp
new file mode 100644
index 0000000..6fda23e
--- /dev/null
+++ b/src/titlescreen.cpp
@@ -0,0 +1,124 @@
+#include "titlescreen.h"
+#include "game.h"
+#include "input.h"
+
+#include <fstream>
+
+static void
+printTitle(std::shared_ptr<Window> graphicWindow)
+{
+ std::fstream titlefile;
+ std::string tmp;
+ int cx = 0;
+ int cy = 0;
+ int startb = 0;
+
+ (void)graphicWindow->getCenter(cx, cy);
+ titlefile.open("assets/title.txt", std::ios::in);
+ if(!titlefile.is_open()) {
+ return;
+ }
+
+ cy /= 3;
+ while(getline(titlefile, tmp)){
+ int r = 255;
+ int b = startb;
+ int x = cx - (tmp.length() / 2);
+ for(const char *c = tmp.c_str(); *c; c++){
+ graphicWindow->plot(x++, cy, *c, (r << 16) | (b), 0);
+ r -= 3;
+ b += 3;
+ }
+ startb += 18;
+ cy++;
+ }
+}
+
+static const int selectionsTotal = 5;
+static const std::string selectionTexts[selectionsTotal] =
+{
+ " Play ",
+ " Worlds ",
+ " Settings ",
+ " Test Gen ",
+ " Quit "
+};
+
+static void
+printSelections(std::shared_ptr<TextWindow> selectionWindow, int selection)
+{
+ int selwidth = selectionWindow->getWidth() / selectionsTotal;
+ int selcenter = selwidth / 2;
+ int sely = (selectionWindow->getHeight() / 3) * 2;
+
+ selectionWindow->clear(0xFFFFFF, 0x0);
+
+ for(int i = 0; i < selectionsTotal; i++){
+ if(selection == i){
+ selectionWindow->setfg(0x0);
+ selectionWindow->setbg(0xFFFFFF);
+ }
+ selectionWindow->moveCursor((selwidth * i) + (selcenter - (selectionTexts[i].length() / 2)), sely);
+ selectionWindow->print(selectionTexts[i].c_str());
+ selectionWindow->setbg(0x0);
+ selectionWindow->setfg(0xFFFFFF);
+ selectionWindow->print(" ");
+ }
+}
+
+TitleScreen::TitleScreen() :
+ m_graphicWindow(nullptr),
+ m_selectionWindow(nullptr),
+ m_selection(0)
+{
+ int sw = Game::getWidth();
+ int sh = Game::getHeight();
+
+ int graphicHeight = (sh / 4) * 3;
+ int selectionHeight = (sh / 4);
+
+ m_graphicWindow = std::make_shared<Window>(0, 0, sw, graphicHeight + 1, false);
+ m_selectionWindow = std::make_shared<TextWindow>(0, graphicHeight + 1, sw, selectionHeight + 1, false);
+
+ addWindow("graphic", m_graphicWindow);
+ addWindow("selection", m_selectionWindow);
+
+ printTitle(m_graphicWindow);
+ printSelections(m_selectionWindow, m_selection);
+
+ Game::enableRawmode();
+}
+
+TitleScreen::~TitleScreen()
+{
+
+}
+
+void
+TitleScreen::parseSelection()
+{
+ if(m_selection == static_cast<int>(Selection::QUIT)) Game::stop();
+}
+
+void
+TitleScreen::update()
+{
+ int c = Input::getch();
+ if(c == '\033') {
+ Game::pushScreen("pause");
+ }
+
+ if(c == static_cast<int>(Keycode::LEFT)){
+ m_selection--;
+ if(m_selection < 0) m_selection = selectionsTotal - 1;
+ }
+ if(c == static_cast<int>(Keycode::RIGHT)){
+ m_selection++;
+ if(m_selection >= selectionsTotal) m_selection = 0;
+ }
+ if(c == 13){
+ parseSelection();
+ }
+
+ printSelections(m_selectionWindow, m_selection);
+}
diff --git a/src/window.cpp b/src/window.cpp
new file mode 100644
index 0000000..0c06c30
--- /dev/null
+++ b/src/window.cpp
@@ -0,0 +1,94 @@
+#include "window.h"
+#include <cstdio>
+
+Window::Window(int x, int y, int width, int height, bool bordered) :
+ m_x(x), m_y(y), m_width(width), m_height(height),
+ m_cursorx(0), m_cursory(0),
+ m_bordered(bordered), m_redraw(true)
+{
+ m_foreplane = new Cell[width * height];
+ m_backplane = new Cell[width * height];
+
+ for(int i = 0; i < width * height; i++){
+ m_foreplane[i].chr = ' ';
+ m_foreplane[i].fg = 0xFFFFFF;
+ }
+}
+
+Window::~Window()
+{
+ delete[] m_foreplane;
+ delete[] m_backplane;
+}
+
+static void
+replot(int x, int y, char c, int fg, int bg)
+{
+#if defined(TRUECOLOR)
+ printf("\033[%i;%iH", y, x);
+ printf("\033[38;2;%i;%i;%im", (fg & 0xFF0000) >> 16, (fg & 0x00FF00) >> 8, (fg & 0x0000FF));
+ printf("\033[48;2;%i;%i;%im", (bg & 0xFF0000) >> 16, (bg & 0x00FF00) >> 8, (bg & 0x0000FF));
+ printf("%c", c);
+#endif
+}
+
+void
+Window::refresh()
+{
+ for(int y = 0; y < m_height; y++)
+ for(int x = 0; x < m_width; x++)
+ {
+ int index = x + (y * m_width);
+ if(m_foreplane[index] == m_backplane[index] && !m_redraw) continue;
+ replot(x + m_x + 1, y + m_y + 1, m_foreplane[index].chr, m_foreplane[index].fg, m_foreplane[index].bg);
+ m_backplane[index] = m_foreplane[index];
+ }
+ if(m_redraw) puts("\033[1;1H");
+ m_redraw = false;
+}
+
+void
+Window::clear(int fg, int bg)
+{
+ for(int i = 0; i < m_width * m_height; i++){
+ m_foreplane[i].chr = ' ';
+ m_foreplane[i].fg = fg;
+ m_foreplane[i].bg = bg;
+ }
+}
+
+void
+Window::plot(int x, int y, char c, int fg, int bg)
+{
+ int index = x + (y * m_width);
+ m_foreplane[index].chr = c;
+ m_foreplane[index].fg = fg;
+ m_foreplane[index].bg = bg;
+}
+
+void
+Window::moveCursor(int x, int y)
+{
+ m_cursorx = x;
+ m_cursory = y;
+
+ if(m_cursorx >= m_width) m_cursorx = m_width - 1;
+ if(m_cursory >= m_height) m_cursory = m_height - 1;
+}
+
+void
+Window::move(int x, int y)
+{
+ m_x = x;
+ m_y = y;
+ m_redraw = true;
+}
+
+bool
+Window::getCenter(int &x, int &y)
+{
+ x = (m_width / 2) + 1;
+ y = (m_height / 2) + 1;
+
+ return ((x * 2) - 1 == m_width) && ((y * 2) - 1 == m_height);
+}