Compare commits

...

No commits in common. "old/sdl" and "main" have entirely different histories.

107 changed files with 8859 additions and 2518 deletions

30
.eslintrc.js Normal file
View file

@ -0,0 +1,30 @@
module.exports = {
parser: '@typescript-eslint/parser',
plugins: [
'react-hooks',
],
extends: [
'plugin:react/recommended',
'plugin:@typescript-eslint/recommended',
'prettier/@typescript-eslint',
'plugin:prettier/recommended',
],
parserOptions: {
ecmaVersion: 2018,
sourceType: 'module',
ecmaFeatures: {
jsx: true,
},
},
rules: {
'react/prop-types': [0],
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'warn',
'prettier/prettier': 'warn',
},
settings: {
react: {
version: 'detect',
},
},
};

3
.gitignore vendored
View file

@ -1 +1,2 @@
*~
/node_modules
/.webpack

View file

@ -1,10 +0,0 @@
cmake_minimum_required(VERSION 2.8.3)
project(RPGEDIT CXX)
find_package(PkgConfig REQUIRED)
pkg_check_modules(SDL2 REQUIRED sdl2 SDL2_image)
find_package(Lua 5.3 EXACT REQUIRED)
add_subdirectory(src)

15
build/webpack.main.js Normal file
View file

@ -0,0 +1,15 @@
module.exports = {
entry: './src/main/index.ts',
module: {
rules: [
{
test: /\.ts$/,
use: 'ts-loader',
exclude: /node_modules/,
},
],
},
resolve: {
extensions: ['.ts', '.js'],
},
};

30
build/webpack.renderer.js Normal file
View file

@ -0,0 +1,30 @@
/* eslint-disable @typescript-eslint/no-var-requires */
const CopyPlugin = require('copy-webpack-plugin');
module.exports = {
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/,
},
{
test: /\.css$/,
use: [{ loader: 'style-loader' }, { loader: 'css-loader' }],
},
{
test: /\.(woff|woff2|eot|ttf|otf)$/,
use: 'file-loader',
},
{
test: /\.(vs|fs)$/,
use: 'raw-loader',
},
],
},
plugins: [new CopyPlugin([{ from: 'static' }])],
resolve: {
extensions: ['.ts', '.js', '.tsx', '.jsx'],
},
};

67
package.json Normal file
View file

@ -0,0 +1,67 @@
{
"private": true,
"version": "0.0.0",
"main": ".webpack/main",
"scripts": {
"start": "electron-forge start",
"lint": "eslint 'src/**/*.ts' 'src/**/*.tsx'"
},
"config": {
"forge": {
"plugins": [
[
"@electron-forge/plugin-webpack",
{
"mainConfig": "./build/webpack.main.js",
"renderer": {
"config": "./build/webpack.renderer.js",
"entryPoints": [
{
"html": "./src/renderer/index.html",
"js": "./src/renderer/index.tsx",
"name": "main_window"
}
]
}
}
]
]
}
},
"devDependencies": {
"@electron-forge/cli": "^6.0.0-beta.50",
"@electron-forge/plugin-webpack": "^6.0.0-beta.50",
"@types/color": "^3.0.1",
"@types/electron-devtools-installer": "^2.2.0",
"@types/react": "^16.9.25",
"@types/react-dom": "^16.9.5",
"@typescript-eslint/eslint-plugin": "^2.24.0",
"@typescript-eslint/parser": "^2.24.0",
"copy-webpack-plugin": "^5.1.1",
"css-loader": "^3.4.2",
"electron": "^8.1.1",
"electron-devtools-installer": "^2.2.4",
"eslint": "^6.8.0",
"eslint-config-prettier": "^6.10.0",
"eslint-plugin-prettier": "^3.1.2",
"eslint-plugin-react": "^7.19.0",
"eslint-plugin-react-hooks": "^2.5.1",
"file-loader": "^6.0.0",
"prettier": "^2.0.1",
"raw-loader": "^4.0.0",
"style-loader": "^1.1.3",
"ts-loader": "^6.2.1",
"typescript": "^3.8.3",
"webpack": "^4.42.0"
},
"dependencies": {
"@material-ui/core": "^4.9.7",
"color": "^3.1.2",
"fast-glob": "^3.2.2",
"gl-matrix": "^3.2.1",
"immutable": "^4.0.0-rc.12",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"typeface-roboto": "^0.0.75"
}
}

10
prettier.config.js Normal file
View file

@ -0,0 +1,10 @@
module.exports = {
semi: true,
trailingComma: 'all',
arrowParens: 'always',
singleQuote: true,
jsxSingleQuote: true,
printWidth: 120,
useTabs: true,
tabWidth: 8,
};

View file

Before

Width:  |  Height:  |  Size: 1 KiB

After

Width:  |  Height:  |  Size: 1 KiB

View file

@ -0,0 +1,5 @@
{
"name": "Dirt",
"width": 1,
"height": 1
}

View file

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

@ -0,0 +1,5 @@
{
"name": "Grass",
"width": 1,
"height": 1
}

View file

@ -1,104 +0,0 @@
32 32 2
G 0 grass
< 0 road_left
> 0 road_right
11111111111111111111111111111111
11111111111111111111111111111111
11111111111111111111111111111111
11111111111111111111111111111111
11111111111111111111111111111111
11111111111111111111111111111111
11111111111111111111111111111111
11111111111111111111111111111111
11111111111111111111111111111111
11111111111111111111111111111111
11111111111111111111111111111111
11111111111111111111111111111111
11111111111111111111111111111111
11111111111111111111111111111111
11111111111111111111111111111111
11111111111111111111111111111111
11111111111111111111111111111111
11111111111111111111111111111111
11111111111111111111111111111111
11111111111111111111111111111111
11111111111111111111111111111111
11111111111111111111111111111111
11111111111111111111111111111111
11111111111111111111111111111111
11111111111111111111111111111111
11111111111111111111111111111111
11111111111111111111111111111111
11111111111111111111111111111111
11111111111111111111111111111111
11111111111111111111111111111111
11111111111111111111111111111111
11111111111111111111111111111111
GGGGGGGGGGG<>GGGGGGGGGGGGGGGGGGG
GGGGGGGGGGG<>GGGGGGGGGGGGGGGGGGG
GGGGGGGGGGG<>GGGGGGGGGGGGGGGGGGG
GGGGGGGGGGG<>GGGGGGGGGGGGGGGGGGG
GGGGGGGGGGG<>GGGGGGGGGGGGGGGGGGG
GGGGGGGGGGG<>GGGGGGGGGGGGGGGGGGG
GGGGGGGGGGG<>GGGGGGGGGGGGGGGGGGG
GGGGGGGGGGG<>GGGGGGGGGGGGGGGGGGG
GGGGGGGGGGG<>GGGGGGGGGGGGGGGGGGG
GGGGGGGGGGG<>GGGGGGGGGGGGGGGGGGG
GGGGGGGGGGG<>GGGGGGGGGGGGGGGGGGG
GGGGGGGGGGG<>GGGGGGGGGGGGGGGGGGG
GGGGGGGGGGG<>GGGGGGGGGGGGGGGGGGG
GGGGGGGGGGG<>GGGGGGGGGGGGGGGGGGG
GGGGGGGGGGG<>GGGGGGGGGGGGGGGGGGG
GGGGGGGGGGG<>GGGGGGGGGGGGGGGGGGG
GGGGGGGGGGG<>GGGGGGGGGGGGGGGGGGG
GGGGGGGGGGG<>GGGGGGGGGGGGGGGGGGG
GGGGGGGGGGG<>GGGGGGGGGGGGGGGGGGG
GGGGGGGGGGG<>GGGGGGGGGGGGGGGGGGG
GGGGGGGGGGG<>GGGGGGGGGGGGGGGGGGG
GGGGGGGGGGG<>GGGGGGGGGGGGGGGGGGG
GGGGGGGGGGG<>GGGGGGGGGGGGGGGGGGG
GGGGGGGGGGG<>GGGGGGGGGGGGGGGGGGG
GGGGGGGGGGG<>GGGGGGGGGGGGGGGGGGG
GGGGGGGGGGG<>GGGGGGGGGGGGGGGGGGG
GGGGGGGGGGG<>GGGGGGGGGGGGGGGGGGG
GGGGGGGGGGG<>GGGGGGGGGGGGGGGGGGG
GGGGGGGGGGG<>GGGGGGGGGGGGGGGGGGG
GGGGGGGGGGG<>GGGGGGGGGGGGGGGGGGG
GGGGGGGGGGG<>GGGGGGGGGGGGGGGGGGG
GGGGGGGGGGG<>GGGGGGGGGGGGGGGGGGG
................................
................................
................................
................................
................................
................................
................................
................................
................................
................................
................................
................................
................................
................................
................................
................................
................................
................................
................................
................................
................................
................................
................................
................................
................................
................................
................................
................................
................................
................................
................................
................................

View file

@ -1,3 +0,0 @@
function interact(entity, time)
print('Interact! ' .. time)
end

View file

@ -1,20 +0,0 @@
local function print_table(foo, bar)
for k, v in pairs(foo) do
print(bar .. k, v)
--if type(v) == 'table' and bar .. k ~= '_G' then
-- print_table(v, bar .. k .. '.')
--end
end
end
--print(getmetatable(_G))
--setmetatable(_G, {})
--print_table(_G, '')
print(bar)
print(getmetatable(bar))
--print(getmetatable('').bar)
--getmetatable('').bar = 'bar'
--print(getmetatable('').bar)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 808 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 118 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 245 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 242 B

View file

@ -1,16 +0,0 @@
include_directories(${SDL2_INCLUDE_DIRS} ${LUA_INCLUDE_DIR})
link_directories(${SDL2_LIBRARY_DIRS})
add_executable(rpgedit
rpgedit.cpp
control/MapContext.cpp
control/RPGEdit.cpp
control/ScriptContext.cpp
model/Map.cpp
model/Scriptable.cpp
view/MapView.cpp
view/SpriteCache.cpp
)
set_target_properties(rpgedit PROPERTIES COMPILE_FLAGS "-std=c++11 -Wall ${SDL2_CFLAGS_OTHER}")
set_target_properties(rpgedit PROPERTIES LINK_FLAGS "${SDL2_LDFLAGS_OTHER}")
target_link_libraries(rpgedit ${SDL2_LIBRARIES} ${LUA_LIBRARIES})

View file

@ -1,105 +0,0 @@
/*
Copyright (c) 2014-2015, Matthias Schiffer <mschiffer@universe-factory.net>
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#pragma once
#include "TimeProvider.hpp"
#include <chrono>
#include <condition_variable>
#include <cstdint>
#include <deque>
#include <functional>
#include <limits>
#include <queue>
#include <thread>
namespace RPGEdit {
namespace Control {
class EventBus {
public:
typedef std::function<void ()> Event;
typedef std::pair<uint64_t, Event> EventEntry;
private:
static bool compare_events(const EventEntry &e1, const EventEntry &e2) {
return e1.first > e2.first;
}
std::priority_queue<EventEntry, std::deque<EventEntry>, bool (*)(const EventEntry &, const EventEntry &)> events;
std::mutex mutex;
std::condition_variable cond;
public:
EventBus() : events(compare_events) {
}
void enqueue(const Event &event, uint64_t time) {
std::lock_guard<std::mutex> lock(mutex);
events.push(EventEntry(time, event));
cond.notify_one();
}
EventEntry get(TimeProvider *timeProvider) {
std::unique_lock<std::mutex> lock(mutex);
while (true) {
if (events.empty()) {
cond.wait(lock);
continue;
}
EventEntry top = events.top();
uint64_t time = timeProvider->now();
if (top.first > time) {
cond.wait_for(lock, std::chrono::milliseconds(top.first - time));
continue;
}
events.pop();
return top;
}
}
uint64_t peek() {
std::lock_guard<std::mutex> lock(mutex);
if (events.empty())
return std::numeric_limits<uint64_t>::max();
return events.top().first;
}
};
}
}

View file

@ -1,74 +0,0 @@
/*
Copyright (c) 2014-2015, Matthias Schiffer <mschiffer@universe-factory.net>
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#pragma once
#include <cstdint>
#include <functional>
#include <unordered_set>
#include <vector>
namespace RPGEdit {
namespace Control {
class InputHandler {
public:
typedef std::function<void (uint16_t, bool, uint64_t)> Listener;
private:
std::vector<Listener> listeners;
std::unordered_set<uint16_t> pressedKeys;
public:
void registerListener(const Listener &listener) {
listeners.push_back(listener);
}
void keyPressed(uint16_t key, uint64_t time) {
if (pressedKeys.insert(key).second) {
for (auto &listener : listeners)
listener(key, true, time);
}
}
void keyReleased(uint16_t key, uint64_t time) {
if (pressedKeys.erase(key)) {
for (auto &listener : listeners)
listener(key, false, time);
}
}
bool isKeyPressed(uint16_t key) {
return pressedKeys.count(key);
}
};
}
}

View file

@ -1,119 +0,0 @@
/*
Copyright (c) 2014-2015, Matthias Schiffer <mschiffer@universe-factory.net>
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "MapContext.hpp"
namespace RPGEdit {
namespace Control {
MapContext::MapContext(EventBus *eventBus0, InputHandler *inputHandler0, ScriptContext *scriptContext0, const std::shared_ptr<View::Window> &window, const Model::Map &map0)
: eventBus(eventBus0), inputHandler(inputHandler0), scriptContext(scriptContext0), map(map0) {
view = std::unique_ptr<View::MapView>(new View::MapView(window, map.getTileset()));
Model::Entity *square = map.addEntity("square", Model::Position<int>{10, 10});
square->setScriptInteract("interact", "interact");
playerEntity = map.addEntity("square", Model::Position<int>{8, 8});
view->updateEntities(map.getEntities());
inputHandler->registerListener(
[this] (uint16_t key, bool pressed, uint64_t time) {
if (pressed)
keyPressed(key, time);
else
movePlayerContinue(time);
}
);
}
void MapContext::movePlayer(Model::Direction dir, uint64_t time) {
if (!map.moveEntity(playerEntity, dir, time, time+250))
return;
eventBus->enqueue(
[=] {
map.finishEntityTransition(playerEntity);
movePlayerContinue(time+250);
},
time+250
);
}
void MapContext::movePlayerContinue(uint64_t time) {
if (inputHandler->isKeyPressed(SDL_SCANCODE_UP))
movePlayer(Model::Direction::NORTH, time);
else if (inputHandler->isKeyPressed(SDL_SCANCODE_RIGHT))
movePlayer(Model::Direction::EAST, time);
else if (inputHandler->isKeyPressed(SDL_SCANCODE_DOWN))
movePlayer(Model::Direction::SOUTH, time);
else if (inputHandler->isKeyPressed(SDL_SCANCODE_LEFT))
movePlayer(Model::Direction::WEST, time);
}
void MapContext::interact(uint64_t time) {
Model::Position<int> p = map.getEntityPosition(playerEntity) + playerEntity->getDirection();
Model::Entity *target = map.getEntityAt(p);
if (!target)
return;
const std::pair<std::string, std::string> &interactScript = target->getScriptInteract();
if (interactScript.first.empty())
return;
scriptContext->run(interactScript.first, interactScript.second, nullptr, time);
}
void MapContext::keyPressed(uint16_t key, uint64_t time) {
switch (key) {
case SDL_SCANCODE_UP:
movePlayer(Model::Direction::NORTH, time);
break;
case SDL_SCANCODE_RIGHT:
movePlayer(Model::Direction::EAST, time);
break;
case SDL_SCANCODE_DOWN:
movePlayer(Model::Direction::SOUTH, time);
break;
case SDL_SCANCODE_LEFT:
movePlayer(Model::Direction::WEST, time);
break;
case SDL_SCANCODE_SPACE:
interact(time);
}
}
}
}

View file

@ -1,73 +0,0 @@
/*
Copyright (c) 2014-2015, Matthias Schiffer <mschiffer@universe-factory.net>
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#pragma once
#include "EventBus.hpp"
#include "InputHandler.hpp"
#include "ScriptContext.hpp"
#include "../model/Map.hpp"
#include "../view/MapView.hpp"
#include <map>
#include <memory>
namespace RPGEdit {
namespace Control {
class MapContext {
private:
EventBus *const eventBus;
InputHandler *const inputHandler;
ScriptContext *const scriptContext;
std::unique_ptr<View::MapView> view;
Model::Map map;
Model::Entity *playerEntity;
void movePlayer(Model::Direction dir, uint64_t time);
void movePlayerContinue(uint64_t time);
void keyPressed(uint16_t key, uint64_t time);
void interact(uint64_t time);
Model::Position<float> getViewPosition(uint64_t time) {
return map.getEntityPosition(playerEntity, time);
}
public:
MapContext(EventBus *eventBus0, InputHandler *inputHandler0, ScriptContext *scriptContext0, const std::shared_ptr<View::Window> &window, const Model::Map &map0);
void render(uint64_t time) {
view->render(&map, getViewPosition(time), time);
}
};
}
}

View file

@ -1,120 +0,0 @@
/*
Copyright (c) 2014-2015, Matthias Schiffer <mschiffer@universe-factory.net>
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "RPGEdit.hpp"
#include "MapContext.hpp"
#include "../view/MapView.hpp"
namespace RPGEdit {
namespace Control {
bool RPGEdit::handleSystemEvent(const SDL_Event &event) {
uint64_t time = timeProvider.now();
switch (event.type) {
case SDL_KEYDOWN:
eventBus.enqueue([=] { inputHandler.keyPressed(event.key.keysym.scancode, time); }, time);
break;
case SDL_KEYUP:
eventBus.enqueue([=] { inputHandler.keyReleased(event.key.keysym.scancode, time); }, time);
break;
case SDL_QUIT:
return false;
}
return true;
}
void RPGEdit::systemLoop() {
const int MIN_FRAME_DELAY = 10;
uint32_t lastFrameTicks = SDL_GetTicks();
while (true) {
uint32_t diff = SDL_GetTicks() - lastFrameTicks;
int timeout = std::max(MIN_FRAME_DELAY - int(diff), 0);
SDL_Event event;
if (SDL_WaitEventTimeout(&event, timeout)) {
if (!handleSystemEvent(event))
return;
continue;
}
lastFrameTicks = SDL_GetTicks();
SDL_RenderClear(window->getRenderer());
{
std::unique_lock<std::mutex> lock(modelMutex);
ctx->render(std::min(timeProvider.now(), handledTime));
}
SDL_RenderPresent(window->getRenderer());
}
}
void RPGEdit::eventLoop() {
while (true) {
EventBus::EventEntry event = eventBus.get(&timeProvider);
if (!event.second)
return;
{
std::lock_guard<std::mutex> lock(modelMutex);
event.second();
handledTime = eventBus.peek();
}
}
}
void RPGEdit::run() {
std::unique_ptr<Model::Map> map = Model::Map::load("test");
window = std::make_shared<View::Window>();
ctx = std::make_shared<MapContext>(&eventBus, &inputHandler, &scriptContext, window, *map);
eventThread = std::thread([this] { eventLoop(); });
systemLoop();
eventBus.enqueue(EventBus::Event(), timeProvider.now());
eventThread.join();
}
}
}

View file

@ -1,70 +0,0 @@
/*
Copyright (c) 2014-2015, Matthias Schiffer <mschiffer@universe-factory.net>
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#pragma once
#include "EventBus.hpp"
#include "InputHandler.hpp"
#include "MapContext.hpp"
#include <memory>
#include <mutex>
#include <thread>
namespace RPGEdit {
namespace Control {
class RPGEdit {
private:
TimeProvider timeProvider;
EventBus eventBus;
InputHandler inputHandler;
ScriptContext scriptContext;
std::shared_ptr<View::Window> window;
std::shared_ptr<MapContext> ctx;
std::thread eventThread;
std::mutex modelMutex;
uint64_t handledTime = std::numeric_limits<uint64_t>::max();
bool handleSystemEvent(const SDL_Event &event);
void systemLoop();
void eventLoop();
public:
void run();
};
}
}

View file

@ -1,92 +0,0 @@
/*
Copyright (c) 2014-2015, Matthias Schiffer <mschiffer@universe-factory.net>
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "ScriptContext.hpp"
namespace RPGEdit {
namespace Control {
void ScriptContext::setupEnv() {
const std::pair<const char *, lua_CFunction> libs[] = {
{"_G", luaopen_base},
{"math", luaopen_math},
{"string", luaopen_string},
{"table", luaopen_table},
{"utf8", luaopen_utf8},
};
for (auto &lib : libs) {
lua_pushcfunction(L, lib.second);
lua_pushstring(L, lib.first);
lua_call(L, 1, 1);
lua_setglobal(L, lib.first);
}
for (const char *f : {"dofile", "loadfile", "require"}) {
lua_pushnil(L);
lua_setglobal(L, f);
}
}
void ScriptContext::cleanupEnv() {
lua_pushglobaltable(L);
lua_pushnil(L);
lua_setmetatable(L, -2);
lua_pushnil(L);
while (lua_next(L, -2) != 0) {
lua_pop(L, 1);
lua_pushvalue(L, -1);
lua_pushnil(L);
lua_rawset(L, -4);
}
lua_pop(L, 1);
}
void ScriptContext::load(const std::string &script) {
if (loadedScripts.count(script))
return;
std::string filename = "../resources/script/" + script + ".lua";
lua_rawgetp(L, LUA_REGISTRYINDEX, this);
lua_pushstring(L, script.c_str());
luaL_loadfile(L, filename.c_str());
lua_rawset(L, -3);
lua_pop(L, 1);
loadedScripts.insert(script);
}
}
}

View file

@ -1,134 +0,0 @@
/*
Copyright (c) 2014-2015, Matthias Schiffer <mschiffer@universe-factory.net>
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#pragma once
#include "../model/ScriptValue.hpp"
extern "C" {
#include <lua.h>
#include <lualib.h>
#include <lauxlib.h>
}
#include <memory>
#include <string>
#include <type_traits>
#include <unordered_set>
namespace RPGEdit {
namespace Control {
class ScriptContext {
private:
lua_State *L;
std::unordered_set<std::string> loadedScripts;
void setupEnv();
void cleanupEnv();
void load(const std::string &script);
void pushArg(std::nullptr_t __attribute__((unused)) v) {
lua_pushnil(L);
}
void pushArg(const std::string &v) {
lua_pushstring(L, v.c_str());
}
template<typename T, class = typename std::enable_if<std::is_arithmetic<T>::value>::type>
void pushArg(T v) {
lua_pushnumber(L, v);
}
void pushArg(bool v) {
lua_pushboolean(L, v);
}
void pushArg(Model::ScriptValue &v) {
v.push(L);
}
size_t pushArgs() {
return 0;
}
template<typename T, typename... Args>
size_t pushArgs(T v, Args ...args) {
pushArg(v);
return pushArgs(args...) + 1;
}
public:
ScriptContext(const ScriptContext &other) = delete;
ScriptContext(ScriptContext &&other) = delete;
ScriptContext & operator=(const ScriptContext &other) = delete;
ScriptContext & operator=(ScriptContext &&other) = delete;
ScriptContext() {
L = luaL_newstate();
lua_newtable(L);
lua_rawsetp(L, LUA_REGISTRYINDEX, this);
}
~ScriptContext() {
lua_close(L);
}
void setGlobal(const std::string &key, Model::ScriptValue *value) {
value->push(L);
lua_setglobal(L, key.c_str());
}
template<typename... Args>
void run(const std::string &script, const std::string &name, Args ...args) {
load(script);
setupEnv();
lua_rawgetp(L, LUA_REGISTRYINDEX, this);
lua_getfield(L, -1, script.c_str());
lua_remove(L, -2);
lua_call(L, 0, 0);
lua_getglobal(L, name.c_str());
lua_call(L, pushArgs(args...), 0);
cleanupEnv();
}
};
}
}

View file

@ -1,61 +0,0 @@
/*
Copyright (c) 2014-2015, Matthias Schiffer <mschiffer@universe-factory.net>
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#pragma once
#include <SDL.h>
#include <cstdint>
#include <mutex>
namespace RPGEdit {
namespace Control {
class TimeProvider {
private:
uint64_t time;
std::mutex mutex;
public:
TimeProvider() {
time = SDL_GetTicks();
}
uint64_t now() {
std::lock_guard<std::mutex> lock(mutex);
uint32_t ticks = SDL_GetTicks() - uint32_t(time);
time += ticks;
return time;
}
};
}
}

55
src/main/index.ts Normal file
View file

@ -0,0 +1,55 @@
import { app, BrowserWindow } from 'electron';
declare const MAIN_WINDOW_WEBPACK_ENTRY: string;
const isDevelopment = process.env.NODE_ENV !== 'production';
app.allowRendererProcessReuse = true;
async function installReactDevTools(): Promise<void> {
const { default: installExtension, REACT_DEVELOPER_TOOLS } = await import('electron-devtools-installer');
const name = await installExtension(REACT_DEVELOPER_TOOLS);
console.log(`Added Extension: ${name}`);
}
function createWindow(): void {
const window = new BrowserWindow({ webPreferences: { nodeIntegration: true } });
window.loadURL(MAIN_WINDOW_WEBPACK_ENTRY);
if (isDevelopment) {
window.webContents.openDevTools();
}
window.webContents.on('devtools-opened', () => {
window.webContents.focus();
});
}
// quit application when all windows are closed
app.on('window-all-closed', () => {
// On macOS it is common for applications and their menu bar
// to stay active until the user quits explicitly with Cmd + Q
if (process.platform !== 'darwin') {
app.quit();
}
});
app.on('activate', () => {
// On macOS it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
async function initialize(): Promise<void> {
if (isDevelopment) {
await installReactDevTools();
}
createWindow();
}
// create main BrowserWindow when electron is ready
app.on('ready', initialize);

View file

@ -1,41 +0,0 @@
/*
Copyright (c) 2014-2015, Matthias Schiffer <mschiffer@universe-factory.net>
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#pragma once
namespace RPGEdit {
namespace Model {
enum class CollisionType {
BLOCKED = 0,
EMPTY,
};
}
}

View file

@ -1,62 +0,0 @@
/*
Copyright (c) 2014-2015, Matthias Schiffer <mschiffer@universe-factory.net>
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#pragma once
namespace RPGEdit {
namespace Model {
enum class Direction {
NORTH,
EAST,
SOUTH,
WEST
};
static inline Direction operator-(Direction dir) {
switch (dir) {
case Direction::NORTH:
return Direction::SOUTH;
case Direction::EAST:
return Direction::WEST;
case Direction::SOUTH:
return Direction::NORTH;
case Direction::WEST:
return Direction::EAST;
default:
__builtin_unreachable();
}
}
}
}

View file

@ -1,79 +0,0 @@
/*
Copyright (c) 2014-2015, Matthias Schiffer <mschiffer@universe-factory.net>
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#pragma once
#include "Direction.hpp"
#include "Scriptable.hpp"
#include <string>
#include <utility>
namespace RPGEdit {
namespace Model {
class Entity : public Scriptable {
private:
std::string name;
std::pair<std::string, std::string> scriptInteract;
Direction direction;
public:
Entity(const std::string &name0)
: name(name0), direction(Direction::NORTH) {
}
const std::string & getName() const {
return name;
}
Direction getDirection() const {
return direction;
}
void setDirection(Direction dir) {
direction = dir;
}
const std::pair<std::string, std::string> & getScriptInteract() const {
return scriptInteract;
}
void setScriptInteract(const std::string &script, const std::string &name) {
scriptInteract.first = script;
scriptInteract.second = name;
}
};
}
}

View file

@ -1,166 +0,0 @@
/*
Copyright (c) 2014-2015, Matthias Schiffer <mschiffer@universe-factory.net>
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "Map.hpp"
#include <cstdio>
#include <cstdlib>
#include <fstream>
namespace RPGEdit {
namespace Model {
Position<float> Map::getEntityPosition(const Entity *entity, uint64_t time) const {
const EntityState &state = entityStates.at(entity);
Position<float> p(state.position);
if (state.transitionEnd)
if (time <= state.transitionStart)
return p;
else if (time >= state.transitionEnd)
return p + state.direction;
else
return p.translate(state.direction,
float(time-state.transitionStart)/
float(state.transitionEnd-state.transitionStart));
else
return p;
}
bool Map::moveEntity(Entity *entity, Direction dir, uint64_t start, uint64_t end) {
EntityState &state = entityStates.at(entity);
if (state.transitionEnd)
return false;
entity->setDirection(dir);
if (isBlocked(state.position + dir))
return false;
state.transitionStart = start;
state.transitionEnd = end;
state.direction = dir;
return true;
}
void Map::moveEntityTo(Entity *entity, Position<int> pos) {
EntityState &state = entityStates.at(entity);
removeEntityPosition(state.position, entity);
addEntityPosition(pos, entity);
state.position = pos;
state.transitionStart = state.transitionEnd = 0;
}
void Map::finishEntityTransition(Entity *entity) {
EntityState &state = entityStates.at(entity);
if (state.transitionEnd)
moveEntityTo(entity, state.position + state.direction);
}
std::unique_ptr<Map> Map::load(const std::string &name) {
std::string filename = "../resources/map/" + name + ".map";
std::ifstream file;
file.exceptions(std::ifstream::failbit | std::ifstream::badbit);
file.open(filename.c_str());
std::string line;
std::getline(file, line);
size_t w, h, layers;
if (std::sscanf(line.c_str(), "%zu %zu %zu", &w, &h, &layers) != 3)
throw std::invalid_argument("invalid map file");
std::unique_ptr<Map> map(new Map(w, h, layers));
std::getline(file, line);
if (line.length())
throw std::invalid_argument("invalid map file");
size_t n = 1;
std::unordered_map<char, size_t> tileset;
for (std::getline(file, line); line.length(); std::getline(file, line)) {
char c;
unsigned rot;
char *tile = nullptr;
if (std::sscanf(line.c_str(), "%c %u %ms", &c, &rot, &tile) != 3 || rot >= 4) {
std::free(tile);
throw std::invalid_argument("invalid map file");
}
tileset.emplace(c, n++);
map->tileset.emplace_back(tile, rot);
std::free(tile);
}
for (size_t i = 0; i < h; i++) {
std::getline(file, line);
if (line.length() != w)
throw std::invalid_argument("invalid map file");
for (size_t j = 0; j < w; j++) {
if (line[j] == '1')
map->setCollisionAt(Position<int>{int(j), int(i)}, CollisionType::EMPTY);
else if (line[j] != '0')
throw std::invalid_argument("invalid map file");
}
}
for (size_t layer = 0; layer < layers; layer++) {
std::getline(file, line);
if (line.length())
throw std::invalid_argument("invalid map file");
for (size_t i = 0; i < h; i++) {
std::getline(file, line);
if (line.length() != w)
throw std::invalid_argument("invalid map file");
for (size_t j = 0; j < w; j++) {
auto it = tileset.find(line[j]);
if (it != tileset.end())
map->setTileAt(layer, Position<int>{int(j), int(i)}, it->second);
}
}
}
return map;
}
}
}

View file

@ -1,251 +0,0 @@
/*
Copyright (c) 2014-2015, Matthias Schiffer <mschiffer@universe-factory.net>
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#pragma once
#include <cstdint>
#include <memory>
#include <stdexcept>
#include <unordered_map>
#include <unordered_set>
#include <vector>
#include "CollisionType.hpp"
#include "Entity.hpp"
#include "Position.hpp"
namespace RPGEdit {
namespace Model {
class _Map {
protected:
std::vector<std::pair<std::string, int>> tileset;
size_t width, height;
std::vector<CollisionType> collision;
std::vector<std::vector<uint32_t>> tiles;
_Map(size_t width0, size_t height0, size_t layers)
: width(width0), height(height0), collision(width*height) {
for (size_t i = 0; i < layers; i++)
tiles.emplace_back(width*height);
}
};
class Map : private _Map {
private:
struct EntityState {
Position<int> position;
Direction direction;
uint64_t transitionStart = 0;
uint64_t transitionEnd = 0;
EntityState(const Position<int> &position0) : position(position0) {}
};
std::vector<std::unique_ptr<Entity>> entities;
std::unordered_map<Position<int>, std::unordered_set<Entity *>> positions;
std::unordered_map<const Entity *, EntityState> entityStates;
void addEntityPosition(const Position<int> &position, Entity *entity) {
positions[position].insert(entity);
}
void removeEntityPosition(const Position<int> &position, Entity *entity) {
auto it = positions.find(position);
it->second.erase(entity);
if (it->second.empty())
positions.erase(it);
}
void pushEntity(Entity *entity, EntityState &&state) {
entities.push_back(std::unique_ptr<Entity>(entity));
addEntityPosition(state.position, entity);
entityStates.emplace(entity, std::move(state));
}
void copyEntities(const Map &other) {
for (auto &e : other.entities)
pushEntity(new Entity(*e), EntityState(other.entityStates.at(e.get())));
}
bool hasTransitionTo(const Position<int> &p, Direction dir) const {
auto it = positions.find(p+dir);
if (it == positions.end())
return false;
for (const Entity *entity : it->second) {
const EntityState &state = entityStates.at(entity);
if (state.transitionEnd && (state.direction == -dir))
return true;
}
return false;
}
bool hasTransitionTo(const Position<int> &p) const {
if (hasTransitionTo(p, Direction::NORTH))
return true;
if (hasTransitionTo(p, Direction::EAST))
return true;
if (hasTransitionTo(p, Direction::SOUTH))
return true;
if (hasTransitionTo(p, Direction::WEST))
return true;
return false;
}
Map(size_t width0, size_t height0, size_t layers) : _Map(width0, height0, layers) {
}
public:
Map & operator=(const Map &other) {
static_cast<_Map>(*this) = other;
copyEntities(other);
return *this;
}
Map(const Map &other) : _Map(other) {
copyEntities(other);
}
Map & operator=(Map &&other) {
static_cast<_Map>(*this) = std::move(other);
entities = std::move(other.entities);
positions = std::move(other.positions);
entityStates = std::move(other.entityStates);
return *this;
}
Map(Map &&other) : _Map(std::move(other)) {
entities = std::move(other.entities);
positions = std::move(other.positions);
entityStates = std::move(other.entityStates);
}
std::vector<std::pair<std::string, int>> & getTileset() {
return tileset;
}
const std::vector<std::pair<std::string, int>> & getTileset() const {
return tileset;
}
const std::vector<std::unique_ptr<Entity>> & getEntities() const {
return entities;
}
Entity * addEntity(const std::string &name, const Position<int> &pos) {
Entity *e = new Entity(name);
pushEntity(e, EntityState(pos));
return e;
}
size_t getWidth() const {
return width;
}
size_t getHeight() const {
return height;
}
size_t getLayerCount() const {
return tiles.size();
}
void setCollisionAt(const Position<int> &p, CollisionType value) {
if (p.x < 0 || size_t(p.x) >= width || p.y < 0 || size_t(p.y) >= height)
throw std::range_error("Map::setCollisionAt: bad coordinates");
collision[p.y*width + p.x] = value;
}
bool isBlocked(const Position<int> &p) const {
if (p.x < 0 || size_t(p.x) >= width || p.y < 0 || size_t(p.y) >= height)
return true;
if (positions.find(p) != positions.end())
return true;
if (hasTransitionTo(p))
return true;
return collision[p.y*width + p.x] == CollisionType::BLOCKED;
}
void setTileAt(size_t layer, const Position<int> &p, uint32_t value) {
if (layer >= tiles.size() || p.x < 0 || size_t(p.x) >= width || p.y < 0 || size_t(p.y) >= height)
throw std::range_error("Map::setTileAt: bad coordinates");
tiles[layer][p.y*width + p.x] = value;
}
uint32_t getTileAt(size_t layer, const Position<int> &p) const {
if (layer >= tiles.size() || p.x < 0 || size_t(p.x) >= width || p.y < 0 || size_t(p.y) >= height)
return 0;
return tiles[layer][p.y*width + p.x];
}
Entity * getEntityAt(const Position<int> &p) {
auto it = positions.find(p);
if (it == positions.end())
return nullptr;
return *it->second.begin();
}
Position<int> getEntityPosition(const Entity *entity) const {
return entityStates.at(entity).position;
}
Position<float> getEntityPosition(const Entity *entity, uint64_t time) const;
bool moveEntity(Entity *entity, Direction dir, uint64_t start, uint64_t end);
void moveEntityTo(Entity *entity, Position<int> pos);
void finishEntityTransition(Entity *entity);
static std::unique_ptr<Map> load(const std::string &name);
};
}
}

View file

@ -1,98 +0,0 @@
/*
Copyright (c) 2014-2015, Matthias Schiffer <mschiffer@universe-factory.net>
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#pragma once
#include "Direction.hpp"
#include <functional>
namespace RPGEdit {
namespace Model {
template<typename T>
struct Position {
T x, y;
Position translate(Direction dir, T amount) const {
Position p = *this;
switch (dir) {
case Direction::NORTH:
p.y -= amount;
break;
case Direction::EAST:
p.x += amount;
break;
case Direction::SOUTH:
p.y += amount;
break;
case Direction::WEST:
p.x -= amount;
}
return p;
}
Position<T> operator+(Direction dir) const {
return translate(dir, 1);
}
bool operator==(const Position<T> &p) const {
return (x == p.x) && (y == p.y);
}
template<typename T2>
explicit operator Position<T2>() const {
return Position<T2>{T2(x), T2(y)};
}
};
}
}
namespace std {
template<typename T> struct hash<RPGEdit::Model::Position<T>> {
typedef size_t result_type;
typedef RPGEdit::Model::Position<T> argument_type;
size_t operator()(const RPGEdit::Model::Position<T> &pos) const noexcept {
const int shift = 4*sizeof(size_t); // half the bit count of size_t
std::hash<T> hash;
size_t hash1 = hash(pos.x), hash2 = hash(pos.y);
return hash1 ^ ((hash2 << shift) | (hash2 >> shift));
}
};
}

View file

@ -1,143 +0,0 @@
/*
Copyright (c) 2014-2015, Matthias Schiffer <mschiffer@universe-factory.net>
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#pragma once
extern "C" {
#include <lua.h>
#include <lualib.h>
#include <lauxlib.h>
}
#include <memory>
#include <string>
#include <unordered_map>
namespace RPGEdit {
namespace Model {
class ScriptValue {
public:
virtual void push(lua_State *L) = 0;
virtual ~ScriptValue() {}
};
class ScriptBoolean : public ScriptValue {
private:
bool value;
public:
ScriptBoolean(bool value0) : value(value0) {
}
ScriptBoolean & operator=(bool newValue) {
value = newValue;
return *this;
}
operator bool() const {
return value;
}
virtual void push(lua_State *L) {
lua_pushboolean(L, value);
}
};
class ScriptNumber : public ScriptValue {
private:
lua_Number value;
public:
ScriptNumber(lua_Number value0) : value(value0) {
}
ScriptNumber & operator=(lua_Number newValue) {
value = newValue;
return *this;
}
operator lua_Number() const {
return value;
}
virtual void push(lua_State *L) {
lua_pushnumber(L, value);
}
};
class ScriptString : public ScriptValue {
private:
std::string value;
public:
ScriptString(std::string &value0) : value(value0) {
}
ScriptString & operator=(std::string &newValue) {
value = newValue;
return *this;
}
operator std::string &() {
return value;
}
virtual void push(lua_State *L) {
lua_pushstring(L, value.c_str());
}
};
class ScriptTable : public ScriptValue {
public:
typedef std::unordered_map<std::shared_ptr<ScriptValue>, std::shared_ptr<ScriptValue>> MapType;
private:
MapType value;
public:
std::shared_ptr<ScriptValue> & operator[](const std::shared_ptr<ScriptValue> &key) {
return value[key];
}
virtual void push(lua_State *L) {
lua_createtable(L, 0, value.size());
for (const auto &entry : value) {
entry.first->push(L);
entry.second->push(L);
lua_settable(L, -3);
}
}
};
}
}

View file

@ -1,58 +0,0 @@
/*
Copyright (c) 2014-2015, Matthias Schiffer <mschiffer@universe-factory.net>
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "Scriptable.hpp"
namespace RPGEdit {
namespace Model {
int Scriptable::gc(lua_State *L) {
std::shared_ptr<ScriptValue> *value = static_cast<std::shared_ptr<ScriptValue> *>(lua_touserdata(L, -1));
value->~shared_ptr<ScriptValue>();
return 0;
}
void Scriptable::setupMetatable(lua_State *L) {
if (luaL_newmetatable(L, "RPGEdit::Model::Scriptable")) {
lua_pushstring(L, "__metatable");
lua_pushstring(L, "protected");
lua_rawset(L, -3);
lua_pushstring(L, "__gc");
lua_pushcfunction(L, &gc);
lua_rawset(L, -3);
}
lua_setmetatable(L, -2);
}
}
}

View file

@ -1,57 +0,0 @@
/*
Copyright (c) 2014-2015, Matthias Schiffer <mschiffer@universe-factory.net>
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#pragma once
#include "ScriptValue.hpp"
#include <new>
namespace RPGEdit {
namespace Model {
class Scriptable : public ScriptValue, public std::enable_shared_from_this<Scriptable> {
private:
static int gc(lua_State *L);
void setupMetatable(lua_State *L);
public:
virtual void push(lua_State *L) {
void *ptr = lua_newuserdata(L, sizeof(std::shared_ptr<ScriptValue>));
setupMetatable(L);
new (ptr) std::shared_ptr<ScriptValue>(shared_from_this());
}
};
}
}

16
src/renderer/custom.d.ts vendored Normal file
View file

@ -0,0 +1,16 @@
declare namespace NodeJS {
interface Module {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
hot: any;
}
}
declare module '*.vs' {
const content: string;
export default content;
}
declare module '*.fs' {
const content: string;
export default content;
}

View file

@ -0,0 +1,64 @@
import * as React from 'react';
import 'typeface-roboto';
import { createMuiTheme, makeStyles, Theme, ThemeProvider } from '@material-ui/core/styles';
import CssBaseline from '@material-ui/core/CssBaseline';
import Drawer from '@material-ui/core/Drawer';
import { Library } from './library';
const drawerWidth = 240;
const useStyles = makeStyles((theme: Theme) => ({
root: {
display: 'flex',
},
drawer: {
width: drawerWidth,
flexShrink: 0,
},
drawerPaper: {
width: drawerWidth,
},
content: {
flexGrow: 1,
padding: theme.spacing(3),
},
}));
function EditorLayout(): JSX.Element {
const classes = useStyles();
return (
<div className={classes.root}>
<CssBaseline />
<Drawer
className={classes.drawer}
variant='permanent'
classes={{
paper: classes.drawerPaper,
}}
anchor='left'
></Drawer>
<main className={classes.content}>
<Library />
</main>
</div>
);
}
const theme = createMuiTheme({
palette: {
type: 'dark',
},
});
export function Editor(): JSX.Element {
return (
<ThemeProvider theme={theme}>
<EditorLayout />
</ThemeProvider>
);
}

View file

@ -0,0 +1,118 @@
import * as React from 'react';
const { useCallback, useMemo } = React;
import { makeStyles, Theme } from '@material-ui/core/styles';
import Box from '@material-ui/core/Box';
import Button from '@material-ui/core/Button';
import Container from '@material-ui/core/Container';
import * as Color from 'color';
import * as glob from 'fast-glob';
import { Tiling, TilingMeta } from './types';
import { usePromise, useReadFile, readJSON } from './util';
const useStyles = makeStyles((theme: Theme) => ({
grid: {
display: 'flex',
flexWrap: 'wrap',
margin: theme.spacing(-0.5),
},
tile: {
imageRendering: 'pixelated',
borderStyle: 'solid',
borderWidth: 1,
borderColor: theme.palette.divider,
background: theme.palette.background.paper,
position: 'relative',
width: 130,
height: 130,
margin: theme.spacing(0.5),
// cursor: 'pointer',
// '&:hover': {
// borderColor: theme.palette.text.secondary,
// boxShadow: `0 0 2px 1px ${theme.palette.text.secondary}`,
// },
},
img: {
position: 'absolute',
zIndex: 1,
width: 128,
height: 128,
},
label: {
position: 'absolute',
zIndex: 2,
left: 0,
right: 0,
bottom: 0,
height: '37.5%',
padding: theme.spacing(1),
background: Color(theme.palette.background.default).fade(0.3).string(),
},
}));
function tilingSprite(tiling: Tiling): string {
const x = (tiling.meta.width - 1) / 2;
const y = (tiling.meta.height - 1) / 2;
return `project/tiling/${tiling.id}/${x}_${y}.png`;
}
interface TilingDisplayProps {
tiling: Tiling;
}
function TilingDisplay({ tiling }: TilingDisplayProps): JSX.Element | null {
const classes = useStyles();
const path = tilingSprite(tiling);
const image = useReadFile(path);
const src = useMemo(() => (image ? `data:image/png;base64,${image.toString('base64')}` : undefined), [image]);
return (
<div className={classes.tile}>
<img className={classes.img} src={src} />
<div className={classes.label}>{tiling.meta.name}</div>
</div>
);
}
async function listTilings(): Promise<string[]> {
const matches = await glob('project/tiling/*/meta.json');
return matches.map((m) => m.split('/')[2]);
}
async function loadTilingMeta(id: string): Promise<TilingMeta> {
const path = `project/tiling/${id}/meta.json`;
const meta = await readJSON(path);
return meta as TilingMeta;
}
async function loadTilings(): Promise<Tiling[]> {
const tilings = await listTilings();
return Promise.all(
tilings.map((id) => [id, loadTilingMeta(id)] as const).map(async ([id, p]) => ({ id, meta: await p })),
);
}
export function Library(): JSX.Element {
const classes = useStyles();
const mkLoadTilings = useCallback(() => loadTilings(), []);
const tilings = usePromise(mkLoadTilings) ?? [];
return (
<Container>
<Box mb={2}>
<div className={classes.grid}>
{tilings.map((tiling, i) => (
<TilingDisplay key={i} tiling={tiling} />
))}
</div>
</Box>
<Button variant='contained' color='primary'>
Add
</Button>
</Container>
);
}

View file

@ -0,0 +1,11 @@
export interface TilingMeta {
name: string;
desc?: string;
width: number;
height: number;
}
export interface Tiling {
id: string;
meta: TilingMeta;
}

View file

@ -0,0 +1,36 @@
import { useCallback, useEffect, useState } from 'react';
import * as fs from 'fs';
export function usePromise<T>(f: () => Promise<T>): T | null {
const [value, setValue] = useState<T | null>(null);
useEffect(() => {
setValue(null);
let cancelled = false;
(async (): Promise<void> => {
const v = await f();
if (!cancelled) {
setValue(v);
}
})();
return (): void => {
cancelled = true;
};
}, [f]);
return value;
}
export function useReadFile(path: string): Buffer | null {
const readFile = useCallback(() => fs.promises.readFile(path), [path]);
return usePromise(readFile);
}
export async function readJSON(path: string): Promise<unknown> {
const content = await fs.promises.readFile(path, 'utf8');
return JSON.parse(content);
}

18
src/renderer/index.html Normal file
View file

@ -0,0 +1,18 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline';" />
<meta name="viewport" content="minimum-scale=1, initial-scale=1, width=device-width" />
<title>RPGedit</title>
<style>
html, body, #app {
height: 100%;
overflow: hidden;
}
</style>
</head>
<body>
<div id="app"></div>
</body>
</html>

8
src/renderer/index.tsx Normal file
View file

@ -0,0 +1,8 @@
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { Editor } from './editor/editor';
ReactDOM.render(<Editor />, document.querySelector('#app'));
module.hot?.accept();

View file

@ -0,0 +1,60 @@
import { Collision } from '../model/data/collision';
import { Circle } from '../math/circle';
import { Collidable } from '../math/collision';
import { LineSegment, Movement } from '../math/line';
import { Point } from '../math/point';
import { vec2 } from 'gl-matrix';
export function mkCollision(collision: Collision[]): Collidable[] {
const ret: Collidable[] = [];
for (const c of collision) {
switch (c.type) {
case 'polygon':
if (!c.vertices.length) continue;
let prev = c.vertices[c.vertices.length - 1];
for (const v of c.vertices) {
ret.push(LineSegment.fromPoints(vec2.clone(prev), vec2.clone(v)));
prev = v;
}
for (const v of c.vertices) {
ret.push(new Point(vec2.clone(v)));
prev = v;
}
break;
case 'circle':
ret.push(new Circle(vec2.clone(c.center), c.radius));
break;
}
}
return ret;
}
export interface CollidableGroup {
getTranslation(): vec2 | null;
getCollidables(): Collidable[];
}
export function collide(collision: CollidableGroup, out: vec2, move: Movement, radius: number): boolean {
const t = collision.getTranslation();
if (t) move = move.translate(vec2.negate(vec2.create(), t));
for (const c of collision.getCollidables()) {
if (!c.collide(out, move, radius)) continue;
if (vec2.squaredDistance(move.src, out) >= vec2.squaredDistance(move.src, move.dest)) continue;
if (t) vec2.add(out, out, t);
return true;
}
return false;
}

View file

@ -0,0 +1,43 @@
import { EntityView } from '../view/entity';
import { Renderer } from '../view/renderer/renderer';
import { Collidable } from '../math/collision';
import { CollidableGroup, mkCollision } from './collision';
import { vec2 } from 'gl-matrix';
export class EntityContext implements CollidableGroup {
public static async load(renderer: Renderer, name: string): Promise<EntityContext> {
return new EntityContext(renderer, name, await EntityView.load(renderer, name));
}
public readonly pos: vec2 = vec2.create();
private readonly collision: Collidable[];
private constructor(
private readonly renderer: Renderer,
private readonly name: string,
private readonly view: EntityView,
) {
this.collision = mkCollision(view.data.collision);
}
public render(time: number): void {
this.renderer.setTranslation(this.pos);
this.view.renderByTime(time);
}
public getTranslation(): vec2 {
return this.pos;
}
public getCollidables(): Collidable[] {
return this.collision;
}
public interact(): void {
alert(`You've interacted with ${this.name}!`);
}
}

View file

@ -0,0 +1,163 @@
import { CollidableGroup, collide, mkCollision } from './collision';
import { EntityContext } from './entitycontext';
import { MapData, MapDataInput } from '../model/data/map';
import { ButtonCode, GameInputHandler } from '../view/input/gameinput';
import { MapView } from '../view/map';
import { Renderer } from '../view/renderer/renderer';
import { Collidable } from '../math/collision';
import { Movement } from '../math/line';
import { getJSON, nextAnimationFrame } from '../util';
import { vec2 } from 'gl-matrix';
export class GameContext implements CollidableGroup {
public static async load(renderer: Renderer): Promise<GameContext> {
const map = this.loadMap(renderer, 'test');
const loadPlayer = EntityContext.load(renderer, 'green_circle');
const loadEntity = EntityContext.load(renderer, 'red_circle');
const [mapView, mapCollision] = await map;
const player = await loadPlayer;
const entity = await loadEntity;
vec2.set(player.pos, 7, 6);
vec2.set(entity.pos, 4, 3);
return new GameContext(renderer, mapView, player, [entity], mapCollision);
}
private static async loadMap(renderer: Renderer, name: string): Promise<[MapView, Collidable[]]> {
const map = new MapData((await getJSON(`resources/map/${name}.json`)) as MapDataInput);
return [await MapView.load(renderer, map), mkCollision(map.collision)];
}
private readonly initTime: number = performance.now();
private time = 0;
private readonly tick = 10; // ms per tick
private readonly maxSpeed = 0.04; // movement per tick
private readonly maxSkip = 20; // maximum ticks to process in a single render step
private readonly input: GameInputHandler;
private readonly playerDir: vec2 = vec2.fromValues(0, 1);
private speed = 0;
private readonly collisionRadius = 15 / 32;
private readonly interactLength = 1 / 32;
private constructor(
private readonly renderer: Renderer,
private readonly mapView: MapView,
private readonly player: EntityContext,
private readonly entities: EntityContext[],
private readonly collision: Collidable[],
) {
this.input = new GameInputHandler();
this.input.addListener((input) => {
switch (input.type) {
case 'button':
if (input.button === ButtonCode.Action) this.interact();
break;
case 'direction':
if (vec2.sqrLen(input.direction) > 0) {
vec2.copy(this.playerDir, input.direction);
this.speed = this.maxSpeed;
} else {
this.speed = 0;
}
break;
}
});
this.renderLoop();
}
public getTranslation(): null {
return null;
}
public getCollidables(): Collidable[] {
return this.collision;
}
private updateTime(time: number): number {
const diff = Math.round(time / this.tick) - Math.round(this.time / this.tick);
this.time = time;
return diff;
}
private canInteract(c: CollidableGroup): boolean {
const dest = vec2.scaleAndAdd(vec2.create(), this.player.pos, this.playerDir, this.interactLength);
const move = new Movement(this.player.pos, dest);
return collide(c, vec2.create(), move, this.collisionRadius);
}
private interact(): void {
for (const e of this.entities) {
if (!this.canInteract(e)) continue;
e.interact();
break;
}
}
private updateStepCollide(out: vec2, dest: vec2): boolean {
const move = new Movement(this.player.pos, dest);
for (const c of [this, ...this.entities]) {
if (collide(c, out, move, this.collisionRadius)) {
if (vec2.squaredDistance(move.src, out) >= vec2.squaredDistance(move.src, move.dest))
continue;
return true;
}
}
return false;
}
private updateStep(): void {
const dest = vec2.scaleAndAdd(vec2.create(), this.player.pos, this.playerDir, this.speed);
const newDest = vec2.create();
while (this.updateStepCollide(newDest, dest)) {
if (vec2.equals(newDest, this.player.pos)) return;
vec2.copy(dest, newDest);
}
vec2.copy(this.player.pos, dest);
}
private update(time: number): void {
const diff = Math.min(this.maxSkip, this.updateTime(time));
if (!this.speed) {
this.renderer.snapToGrid(this.player.pos, this.player.pos);
return;
}
for (let i = 0; i < diff; i++) this.updateStep();
}
private render(): void {
this.renderer.setCenter(this.player.pos);
this.renderer.clear();
for (const r of [this.mapView, ...this.entities, this.player]) r.render(this.time);
}
private async renderLoop(): Promise<void> {
while (true) {
this.update((await nextAnimationFrame()) - this.initTime);
this.render();
}
}
}

View file

@ -0,0 +1,12 @@
html, body, div {
width: 100%;
height: 100%;
}
* {
margin: 0px;
padding: 0px;
border: 0px;
background: #223;
overflow: hidden;
}

View file

@ -0,0 +1,15 @@
import { Collidable } from './collision';
import { Movement } from './line';
import { Point } from './point';
import { vec2 } from 'gl-matrix';
export class Circle implements Collidable, Point {
private pointCollide = Point.prototype.collide;
constructor(public readonly p: vec2, public readonly r: number) {}
public collide(out: vec2, move: Movement, r: number): boolean {
return this.pointCollide(out, move, r + this.r);
}
}

View file

@ -0,0 +1,7 @@
import { Movement } from './line';
import { vec2 } from 'gl-matrix';
export interface Collidable {
collide(out: vec2, move: Movement, r: number): boolean;
}

View file

@ -0,0 +1,124 @@
import { mat2, vec2 } from 'gl-matrix';
import { Collidable } from './collision';
const rot90 = mat2.fromValues(0, 1, -1, 0);
export function normal(out: vec2, a: vec2): vec2 {
return vec2.transformMat2(out, a, rot90);
}
export function crossz(a: vec2, b: vec2): number {
return a[0] * b[1] - a[1] * b[0];
}
export class Line {
constructor(public readonly p: vec2, public readonly v: vec2) {}
public getNormal(out: vec2): vec2 {
return normal(out, this.v);
}
public projectPointDistance(p2: vec2): number {
const v2 = vec2.sub(vec2.create(), p2, this.p);
return vec2.dot(this.v, v2);
}
public projectPoint(out: vec2, p2: vec2): vec2 {
const d = this.projectPointDistance(p2);
return vec2.scaleAndAdd(out, this.p, this.v, d);
}
public distancePoint(p2: vec2): number {
const v2 = vec2.sub(vec2.create(), p2, this.p);
return crossz(this.v, v2);
}
public intersectLine(out: vec2, l2: Line): vec2 {
const vp = vec2.sub(vec2.create(), l2.p, this.p);
const d = crossz(vp, this.v);
const d2 = d / crossz(this.v, l2.v);
return vec2.scaleAndAdd(out, l2.p, l2.v, d2);
}
}
export class Movement {
public readonly v: vec2;
constructor(public readonly src: vec2, public readonly dest: vec2) {
this.v = vec2.sub(vec2.create(), dest, src);
}
public intersectLine(out: vec2, l: Line): vec2 {
const vp = vec2.sub(vec2.create(), l.p, this.src);
const d = crossz(vp, this.v);
const d2 = d / crossz(this.v, l.v);
return vec2.scaleAndAdd(out, l.p, l.v, d2);
}
public passes(p: vec2): boolean {
const vp = vec2.sub(vec2.create(), p, this.src);
const d = vec2.dot(this.v, vp);
return d >= 0 && d <= vec2.sqrLen(this.v);
}
public toLineSegment(): LineSegment {
// eslint-disable-next-line @typescript-eslint/no-use-before-define
return LineSegment.fromPoints(this.src, this.dest);
}
public translate(t: vec2): Movement {
const src = vec2.add(vec2.create(), this.src, t);
const dest = vec2.add(vec2.create(), this.dest, t);
return new Movement(src, dest);
}
}
export class LineSegment extends Line implements Collidable {
public static fromPoints(p1: vec2, p2: vec2): LineSegment {
const d = vec2.dist(p1, p2);
const v = vec2.sub(vec2.create(), p2, p1);
vec2.scale(v, v, 1 / d);
return new LineSegment(p1, v, d);
}
constructor(p: vec2, v: vec2, public readonly l: number) {
super(p, v);
}
public getP2(out: vec2): vec2 {
return vec2.scaleAndAdd(out, this.p, this.v, this.l);
}
public containsPoint(p2: vec2): boolean {
const d = this.projectPointDistance(p2);
return d >= 0 && d <= this.l;
}
public collide(out: vec2, move: Movement, r: number): boolean {
if (this.distancePoint(move.src) < 0) return false;
if (crossz(move.v, this.v) < 0) return false;
const t = this.getNormal(vec2.create());
vec2.scale(t, t, -r);
const refMove = move.translate(t);
if (!this.collideRef(out, refMove)) return false;
vec2.sub(out, out, t);
return true;
}
private collideRef(out: vec2, move: Movement): boolean {
if (this.distancePoint(move.dest) >= 0) return false;
const x = move.intersectLine(vec2.create(), this);
if (!this.containsPoint(x)) return false;
this.projectPoint(out, move.dest);
return true;
}
}

View file

@ -0,0 +1,37 @@
import { Collidable } from './collision';
import { Line, Movement, normal } from './line';
import { vec2 } from 'gl-matrix';
export class Point implements Collidable {
constructor(public readonly p: vec2) {}
public collide(out: vec2, move: Movement, r: number): boolean {
const moveLine = move.toLineSegment();
if (moveLine.projectPointDistance(this.p) < 0) return false;
const d = moveLine.distancePoint(this.p) / r;
if (Math.abs(d) >= 1) return false;
const e = Math.sqrt(1 - d * d);
const t = moveLine.getNormal(vec2.create());
vec2.scale(t, t, d);
vec2.scaleAndAdd(t, t, moveLine.v, e);
const tr = vec2.scale(vec2.create(), t, r);
const refMove = move.translate(tr);
if (vec2.sqrDist(this.p, move.src) > r * r && !refMove.passes(this.p)) return false;
normal(t, t);
const tang = new Line(this.p, t);
tang.projectPoint(out, refMove.dest);
vec2.sub(out, out, tr);
return true;
}
}

View file

@ -0,0 +1,12 @@
export interface PolygonCollision {
readonly type: 'polygon';
readonly vertices: Array<[number, number]>;
}
export interface CircleCollision {
readonly type: 'circle';
readonly center: [number, number];
readonly radius: number;
}
export type Collision = PolygonCollision | CircleCollision;

View file

@ -0,0 +1,29 @@
import { Collision } from './collision';
export interface EntityAnimation {
readonly sequence: ReadonlyArray<[number, number]>;
}
export interface EntityDataInput {
readonly sprite: string;
readonly anchor?: [number, number];
readonly collision?: Collision[];
readonly frames?: number;
readonly animation?: EntityAnimation;
}
export class EntityData {
public readonly sprite: string;
public readonly anchor: [number, number];
public readonly collision: Collision[];
public readonly frames: number;
public readonly animation?: EntityAnimation;
constructor(input: EntityDataInput) {
this.sprite = input.sprite;
this.anchor = input.anchor || [0.5, 0.5];
this.collision = input.collision || [];
this.frames = input.frames || 1;
this.animation = input.animation;
}
}

View file

@ -0,0 +1,29 @@
import { Collision } from './collision';
export interface MapLayer {
readonly tiles: number[][];
}
export interface MapDataInput {
readonly tiles: string[];
readonly layers: MapLayer[];
readonly collision: Collision[];
}
export class MapData {
public readonly tiles: string[];
public readonly layers: MapLayer[];
public readonly collision: Collision[];
public readonly width: number;
public readonly height: number;
constructor(data: MapDataInput) {
this.tiles = data.tiles;
this.layers = data.layers;
this.collision = data.collision;
this.height = this.layers[0].tiles.length;
this.width = this.layers[0].tiles[0].length;
}
}

View file

@ -0,0 +1,55 @@
export function recordToMap<T>(r: Record<string, T>): Map<string, T> {
const ret = new Map();
for (const k of Object.keys(r)) ret.set(k, r[k]);
return ret;
}
export function mapValues<K, V1, V2>(f: (v: V1) => V2, map: Map<K, V1>): Map<K, V2> {
const ret: Map<K, V2> = new Map();
for (const [k, v] of map) ret.set(k, f(v));
return ret;
}
export async function mapValuesAsync<K, V1, V2>(f: (v: V1) => Promise<V2>, map: Map<K, V1>): Promise<Map<K, V2>> {
const ret: Map<K, V2> = new Map();
for (const [k, v] of mapValues(f, map)) ret.set(k, await v);
return ret;
}
export function nextPowerOf2(n: number): number {
let i = 1;
while (i < n) i *= 2;
return i;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export class Listenable<T extends any[]> {
private readonly listeners: Array<(...args: T) => void> = [];
public addListener(listener: (...args: T) => void): void {
this.listeners.push(listener);
}
protected runListeners(...args: T): void {
this.listeners.forEach((l) => l(...args));
}
}
export async function getJSON(url: string): Promise<unknown> {
const res = await window.fetch(url);
if (res.status < 200 || res.status >= 300) throw new Error(res.statusText);
return await res.json();
}
export function nextAnimationFrame(): Promise<DOMHighResTimeStamp> {
return new Promise((resolve) => window.requestAnimationFrame(resolve));
}

View file

@ -0,0 +1,62 @@
import { EntityData, EntityDataInput } from '../model/data/entity';
import { Renderer } from './renderer/renderer';
import { SpriteCoords, SpriteView, SpriteViewBuilder } from './sprite';
import { loadImage, mkTexture } from './util/image';
import { getJSON } from '../util';
import { vec2 } from 'gl-matrix';
export class EntityView {
public static async load(r: Renderer, name: string): Promise<EntityView> {
const data = new EntityData((await getJSON(`resources/entity/${name}.json`)) as EntityDataInput);
const tile = await loadImage(`resources/sprite/entity/${data.sprite}.png`);
const [texture, size] = mkTexture(r, tile);
const frameSize: [number, number] = [size[0], size[1] / data.frames];
const offset = vec2.mul(vec2.create(), frameSize, data.anchor);
r.snapToGrid(offset, offset);
const coords: SpriteCoords = [
-offset[0],
-offset[1],
-offset[0] + frameSize[0],
-offset[1] + frameSize[1],
];
const sprites: SpriteView[] = [];
for (let frame = 0; frame < data.frames; frame++) {
const builder = new SpriteViewBuilder(r, texture);
builder.addSprite(coords, [0, frame / data.frames, 1, (frame + 1) / data.frames]);
sprites.push(builder.build());
}
return new EntityView(data, sprites);
}
private readonly totalTime: number;
private constructor(public readonly data: EntityData, public readonly sprites: SpriteView[]) {
if (data.animation) this.totalTime = data.animation.sequence.reduce((a, s) => a + s[0], 0);
else this.totalTime = 0;
}
public getSpriteByTime(time: number): SpriteView {
time %= this.totalTime;
if (this.data.animation) {
for (const [len, sprite] of this.data.animation.sequence) {
time -= len;
if (time < 0) return this.sprites[sprite];
}
}
return this.sprites[0];
}
public renderByTime(time: number): void {
this.getSpriteByTime(time).render();
}
}

View file

@ -0,0 +1,68 @@
import { InputHandler } from './inputhandler';
import { Listenable } from '../../util';
import { vec2 } from 'gl-matrix';
export enum ButtonCode {
Action,
Back,
Menu,
}
const buttonMapping: Record<string, ButtonCode> = {
KeyZ: ButtonCode.Action,
KeyX: ButtonCode.Back,
KeyC: ButtonCode.Menu,
};
export interface DirectionInput {
type: 'direction';
direction: vec2;
}
export interface ButtonInput {
type: 'button';
button: ButtonCode;
}
export type GameInput = DirectionInput | ButtonInput;
export class GameInputHandler extends Listenable<[GameInput]> {
private readonly input: InputHandler;
constructor() {
super();
this.input = new InputHandler(
new Set(['ArrowLeft', 'ArrowUp', 'ArrowRight', 'ArrowDown', ...Object.keys(buttonMapping)]),
);
this.input.addListener((key: string, pressed: boolean) => {
const button = buttonMapping[key];
if (button !== undefined) {
if (pressed)
this.runListeners({
type: 'button',
button,
});
return;
}
const dir = vec2.create();
if (this.input.has('ArrowLeft')) vec2.add(dir, dir, [-1, 0]);
if (this.input.has('ArrowUp')) vec2.add(dir, dir, [0, -1]);
if (this.input.has('ArrowRight')) vec2.add(dir, dir, [1, 0]);
if (this.input.has('ArrowDown')) vec2.add(dir, dir, [0, 1]);
if (vec2.sqrLen(dir) > 0) vec2.normalize(dir, dir);
this.runListeners({
type: 'direction',
direction: dir,
});
});
}
}

View file

@ -0,0 +1,40 @@
import { Listenable } from '../../util';
export class InputHandler extends Listenable<[string, boolean]> {
private readonly keys: Set<string> = new Set();
constructor(relevantKeys: Set<string>) {
super();
window.addEventListener('keydown', (ev) => {
if (!relevantKeys.has(ev.code)) return;
ev.preventDefault();
if (ev.repeat) return;
this.keys.add(ev.code);
this.runListeners(ev.code, true);
});
window.addEventListener('keyup', (ev) => {
if (!relevantKeys.has(ev.code)) return;
ev.preventDefault();
if (!this.keys.has(ev.code)) return;
this.keys.delete(ev.code);
this.runListeners(ev.code, false);
});
window.addEventListener('blur', () => {
this.keys.clear();
this.runListeners('', false);
});
}
public has(key: string): boolean {
return this.keys.has(key);
}
}

View file

@ -0,0 +1,172 @@
import { EntityView } from './entity';
import { Renderer } from './renderer/renderer';
import { SpriteCoords, SpriteView, SpriteViewBuilder } from './sprite';
import { loadImage, mkTexture } from './util/image';
import { MapData } from '../model/data/map';
import { nextPowerOf2 } from '../util';
interface StaticMapTile {
type: 'static';
image: HTMLImageElement;
}
interface EntityTile {
type: 'entity';
entity: EntityView;
}
type MapTile = StaticMapTile | EntityTile;
interface StaticTilesetTile {
type: 'static';
coords: SpriteCoords;
}
type TilesetTile = StaticTilesetTile | EntityTile;
interface Tileset {
texture: WebGLTexture;
tiles: TilesetTile[];
}
async function loadTile(r: Renderer, tile: string): Promise<MapTile> {
const name = tile.substr(1);
switch (tile[0]) {
case '-':
return {
type: 'static',
image: await loadImage(`resources/sprite/tile/${name}.png`),
};
case '@':
return {
type: 'entity',
entity: await EntityView.load(r, name),
};
default:
throw new Error('invalid tile specifier');
}
}
function loadTiles(r: Renderer, tiles: string[]): Promise<MapTile[]> {
return Promise.all(tiles.map((tile) => loadTile(r, tile)));
}
function mkTileset(r: Renderer, mapTiles: MapTile[]): Tileset {
const tileSize = 32;
const canvasDim = nextPowerOf2(Math.sqrt(mapTiles.length));
const canvasSize = canvasDim * tileSize;
const canvas = document.createElement('canvas');
canvas.width = canvas.height = canvasSize;
let x = 0;
let y = 0;
const tiles: TilesetTile[] = [];
const ctx = canvas.getContext('2d') as CanvasRenderingContext2D;
for (const tile of mapTiles) {
switch (tile.type) {
case 'static':
ctx.drawImage(tile.image, x * tileSize, y * tileSize);
tiles.push({
type: 'static',
coords: [
x / canvasDim,
y / canvasDim,
(x + 1) / canvasDim,
(y + 1) / canvasDim,
],
});
x++;
if (x === canvasDim) {
x = 0;
y++;
}
break;
case 'entity':
tiles.push(tile);
break;
}
}
const [texture] = mkTexture(r, canvas);
return {
texture,
tiles,
};
}
function addSprite(
builder: SpriteViewBuilder,
entityTiles: Array<[[number, number], EntityView]>,
tileset: Tileset,
x: number,
y: number,
tile: number,
): void {
if (!tile) return;
const tilesetTile = tileset.tiles[tile - 1];
switch (tilesetTile.type) {
case 'static':
builder.addSprite([x, y, x + 1, y + 1], tilesetTile.coords);
break;
case 'entity':
entityTiles.push([[x + 0.5, y + 0.5], tilesetTile.entity]);
break;
}
}
class MapLayerView {
public constructor(
private r: Renderer,
private staticTiles: SpriteView,
private entityTiles: Array<[[number, number], EntityView]>,
) {}
public render(time: number): void {
this.r.setTranslation([0, 0]);
this.staticTiles.render();
for (const [coords, entity] of this.entityTiles) {
this.r.setTranslation(coords);
entity.renderByTime(time);
}
}
}
function buildMapLayer(r: Renderer, tileset: Tileset, layer: number[][]): MapLayerView {
const builder = new SpriteViewBuilder(r, tileset.texture);
const entityTiles: Array<[[number, number], EntityView]> = [];
for (let x = 0; x < layer[0].length; x++)
for (let y = 0; y < layer.length; y++) addSprite(builder, entityTiles, tileset, x, y, layer[y][x]);
return new MapLayerView(r, builder.build(), entityTiles);
}
export class MapView {
public static async load(r: Renderer, map: MapData): Promise<MapView> {
const tiles = await loadTiles(r, map.tiles);
const tileset = mkTileset(r, tiles);
const layers = map.layers.map((layer) => buildMapLayer(r, tileset, layer.tiles));
return new MapView(layers);
}
private constructor(private layers: MapLayerView[]) {}
public render(time: number): void {
for (const layer of this.layers) layer.render(time);
}
}

View file

@ -0,0 +1,96 @@
import { nextPowerOf2 } from '../../util';
import { Shaders } from './shaders';
import { mat4, vec2 } from 'gl-matrix';
export class Renderer {
public readonly coordScale = 32;
private readonly viewScale = 2;
private readonly gl: WebGLRenderingContext;
private readonly shaders: Shaders;
private readonly center: vec2 = vec2.create();
private readonly translation: vec2 = vec2.create();
private readonly viewport: mat4 = mat4.create();
constructor(private readonly canvas: HTMLCanvasElement) {
this.gl = this.mkContext();
this.shaders = new Shaders(this.gl);
this.gl.clearColor(0.0, 0.0, 0.0, 1.0);
this.gl.enable(this.gl.BLEND);
this.gl.blendFunc(this.gl.ONE, this.gl.ONE_MINUS_SRC_ALPHA);
this.gl.pixelStorei(this.gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, 1);
}
public createBuffer(): WebGLBuffer {
const ret = this.gl.createBuffer();
if (!ret) throw new Error('unable to create buffer');
return ret;
}
public getContext(): WebGLRenderingContext {
return this.gl;
}
public getVertexPosLoc(): number {
return this.shaders.vertexPosLoc;
}
public getTextureCoordLoc(): number {
return this.shaders.textureCoordLoc;
}
public getSamplerLoc(): WebGLUniformLocation {
return this.shaders.samplerLoc;
}
public setCenter(v: vec2): void {
this.snapToGrid(this.center, v);
}
public setTranslation(v: vec2): void {
vec2.sub(this.translation, v, this.center);
this.snapToGrid(this.translation, this.translation);
this.gl.uniform2fv(this.shaders.translateLoc, this.translation);
}
public clear(): void {
this.gl.clear(this.gl.COLOR_BUFFER_BIT);
this.setTranslation([0, 0]);
}
public snapToGrid(out: vec2, a: vec2): void {
vec2.scale(out, a, this.coordScale);
vec2.round(out, out);
vec2.scale(out, out, 1 / this.coordScale);
}
public resize(): void {
const w = this.canvas.width;
const h = this.canvas.height;
const ws = nextPowerOf2(w);
const hs = nextPowerOf2(h);
this.gl.viewport((w - ws) / 2, (h - hs) / 2, ws, hs);
this.clear();
const scale = this.viewScale * this.coordScale;
mat4.identity(this.viewport);
mat4.scale(this.viewport, this.viewport, [(2 * scale) / ws, (-2 * scale) / hs, 1.0]);
this.gl.uniformMatrix4fv(this.shaders.viewportLoc, false, this.viewport);
}
private mkContext(): WebGLRenderingContext {
const gl = this.canvas.getContext('webgl');
if (!gl) throw new Error('unable to initialize WebGL context');
return gl;
}
}

View file

@ -0,0 +1,75 @@
import fragmentShaderSrc from './shaders/default.fs';
import vertexShaderSrc from './shaders/default.vs';
export class Shaders {
public readonly viewportLoc: WebGLUniformLocation;
public readonly translateLoc: WebGLUniformLocation;
public readonly vertexPosLoc: number;
public readonly textureCoordLoc: number;
public readonly samplerLoc: WebGLUniformLocation;
constructor(private readonly gl: WebGLRenderingContext) {
const shaderProgram = this.gl.createProgram();
if (!shaderProgram) throw new Error('Unable to create shader program');
const vertexShader = this.compileShader(this.gl.VERTEX_SHADER, vertexShaderSrc);
const fragmentShader = this.compileShader(this.gl.FRAGMENT_SHADER, fragmentShaderSrc);
this.gl.attachShader(shaderProgram, vertexShader);
this.gl.attachShader(shaderProgram, fragmentShader);
this.gl.linkProgram(shaderProgram);
if (!this.gl.getProgramParameter(shaderProgram, this.gl.LINK_STATUS)) {
const err = this.gl.getProgramInfoLog(shaderProgram);
this.gl.deleteShader(vertexShader);
this.gl.deleteShader(fragmentShader);
this.gl.deleteProgram(shaderProgram);
throw new Error('Unable to link shader: ' + err);
}
this.gl.useProgram(shaderProgram);
this.vertexPosLoc = this.getAttribLocation(shaderProgram, 'aVertexPos');
this.gl.enableVertexAttribArray(this.vertexPosLoc);
this.textureCoordLoc = this.getAttribLocation(shaderProgram, 'aTextureCoord');
this.gl.enableVertexAttribArray(this.textureCoordLoc);
this.viewportLoc = this.getUniformLocation(shaderProgram, 'uViewport');
this.translateLoc = this.getUniformLocation(shaderProgram, 'uTranslate');
this.samplerLoc = this.getUniformLocation(shaderProgram, 'uSampler');
}
private compileShader(type: number, src: string): WebGLShader {
const shader = this.gl.createShader(type);
if (!shader) throw new Error('Unable to create shader');
this.gl.shaderSource(shader, src);
this.gl.compileShader(shader);
if (!this.gl.getShaderParameter(shader, this.gl.COMPILE_STATUS)) {
const err = this.gl.getShaderInfoLog(shader);
this.gl.deleteShader(shader);
throw new Error('Unable to compile shader: ' + err);
}
return shader;
}
private getAttribLocation(program: WebGLProgram, name: string): number {
const ret = this.gl.getAttribLocation(program, name);
if (ret < 0) throw new Error(`unable to get location of attribute '${name}'`);
return ret;
}
private getUniformLocation(program: WebGLProgram, name: string): WebGLUniformLocation {
const ret = this.gl.getUniformLocation(program, name);
if (!ret) throw new Error(`unable to get location of uniform '${name}'`);
return ret;
}
}

View file

@ -0,0 +1,10 @@
precision highp float;
varying vec2 vTextureCoord;
uniform sampler2D uSampler;
void main(void) {
gl_FragColor = texture2D(uSampler, vTextureCoord);
}

View file

@ -0,0 +1,13 @@
attribute vec2 aVertexPos;
attribute vec2 aTextureCoord;
uniform mat4 uViewport;
uniform vec2 uTranslate;
varying highp vec2 vTextureCoord;
void main(void) {
gl_Position = uViewport * vec4(aVertexPos + uTranslate, 0.0, 1.0);
vTextureCoord = aTextureCoord;
}

View file

@ -0,0 +1,78 @@
import { Renderer } from './renderer/renderer';
export type SpriteCoords = [number, number, number, number];
export class SpriteView {
private readonly vertexCount: number;
private readonly vertexBuffer: WebGLBuffer;
private readonly textureBuffer: WebGLBuffer;
constructor(
private readonly r: Renderer,
private readonly texture: WebGLTexture,
vertexData: number[],
textureData: number[],
) {
const gl = r.getContext();
this.vertexBuffer = r.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertexData), gl.STATIC_DRAW);
this.textureBuffer = r.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, this.textureBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(textureData), gl.STATIC_DRAW);
this.vertexCount = vertexData.length / 2;
}
public render(): void {
const gl = this.r.getContext();
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, this.texture);
gl.uniform1i(this.r.getSamplerLoc(), 0);
gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer);
gl.vertexAttribPointer(this.r.getVertexPosLoc(), 2, gl.FLOAT, false, 0, 0);
gl.bindBuffer(gl.ARRAY_BUFFER, this.textureBuffer);
gl.vertexAttribPointer(this.r.getTextureCoordLoc(), 2, gl.FLOAT, false, 0, 0);
gl.drawArrays(gl.TRIANGLES, 0, this.vertexCount);
}
}
export class SpriteViewBuilder {
private static pushSprite(buf: number[], coords: SpriteCoords): void {
const [x1, y1, x2, y2] = coords;
buf.push(x1);
buf.push(y1);
buf.push(x2);
buf.push(y1);
buf.push(x1);
buf.push(y2);
buf.push(x1);
buf.push(y2);
buf.push(x2);
buf.push(y1);
buf.push(x2);
buf.push(y2);
}
private readonly vertexData: number[] = [];
private readonly textureData: number[] = [];
constructor(private readonly r: Renderer, private readonly texture: WebGLTexture) {}
public addSprite(vertexCoords: SpriteCoords, texCoords: SpriteCoords): void {
SpriteViewBuilder.pushSprite(this.vertexData, vertexCoords);
SpriteViewBuilder.pushSprite(this.textureData, texCoords);
}
public build(): SpriteView {
return new SpriteView(this.r, this.texture, this.vertexData, this.textureData);
}
}

View file

@ -0,0 +1,31 @@
import { Renderer } from '../renderer/renderer';
export function loadImage(url: string): Promise<HTMLImageElement> {
return new Promise((resolve, reject) => {
const img = new Image();
img.addEventListener('load', () => {
resolve(img);
});
img.addEventListener('error', () => {
reject(new Error('failed to load ' + url));
});
img.src = url;
});
}
export function mkTexture(r: Renderer, src: HTMLCanvasElement | HTMLImageElement): [WebGLTexture, [number, number]] {
const gl = r.getContext();
const texture = gl.createTexture();
if (!texture) throw new Error('unable to create texture');
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, src);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
const size: [number, number] = [src.width / r.coordScale, src.height / r.coordScale];
return [texture, size];
}

View file

@ -1,54 +0,0 @@
/*
Copyright (c) 2014-2015, Matthias Schiffer <mschiffer@universe-factory.net>
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "control/RPGEdit.hpp"
#include <SDL.h>
#include <SDL_image.h>
#include <SDL_main.h>
#include <iostream>
extern "C"
int main(__attribute__((unused)) int argc, __attribute__((unused)) char *argv[]) {
if (SDL_Init(SDL_INIT_VIDEO) < 0) {
std::cerr << "Unable to initialize SDL" << std::endl;
return 1;
}
if (!(IMG_Init(IMG_INIT_PNG) & IMG_INIT_PNG)) {
std::cerr << "Unable to initialize PNG loader" << std::endl;
return 1;
}
RPGEdit::Control::RPGEdit().run();
IMG_Quit();
SDL_Quit();
return 0;
}

View file

@ -1,161 +0,0 @@
/*
Copyright (c) 2014-2015, Matthias Schiffer <mschiffer@universe-factory.net>
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "MapView.hpp"
namespace RPGEdit {
namespace View {
MapView::MapView(const std::shared_ptr<Window> &window0, const std::vector<std::pair<std::string, int>> &tileset)
: window(window0) {
uint32_t rmask, gmask, bmask, amask;
#if SDL_BYTEORDER == SDL_BIG_ENDIAN
rmask = 0xff000000;
gmask = 0x00ff0000;
bmask = 0x0000ff00;
amask = 0x000000ff;
#else
rmask = 0x000000ff;
gmask = 0x0000ff00;
bmask = 0x00ff0000;
amask = 0xff000000;
#endif
SpriteCache *spriteCache = window->getSpriteCache();
SDL_Surface *surface = SDL_CreateRGBSurface(0, getTileSize()*tileset.size(), getTileSize(), 32, rmask, gmask, bmask, amask);
for (size_t i = 0; i < tileset.size(); i++) {
SDL_Rect rect = {
.x = int(getTileSize()*i),
.y = 0,
.w = 0,
.h = 0,
};
SDL_Surface *sprite = spriteCache->get("tile", tileset[i].first, tileset[i].second);
SDL_SetSurfaceBlendMode(sprite, SDL_BLENDMODE_NONE);
SDL_BlitSurface(sprite, nullptr, surface, &rect);
SDL_SetSurfaceBlendMode(sprite, SDL_BLENDMODE_BLEND);
}
tiles = SDL_CreateTextureFromSurface(window->getRenderer(), surface);
SDL_FreeSurface(surface);
}
MapView::~MapView() {
SDL_DestroyTexture(tiles);
clearEntities();
}
void MapView::updateEntities(const std::vector<std::unique_ptr<Model::Entity>> &entities) {
SpriteCache *spriteCache = window->getSpriteCache();
for (auto &entity : entities) {
const std::string &name = entity->getName();
if (!entitySprites[name])
entitySprites[name] = SDL_CreateTextureFromSurface(window->getRenderer(), spriteCache->get("entity", name));
}
}
void MapView::clearEntities() {
for (auto &entity : entitySprites)
SDL_DestroyTexture(entity.second);
entitySprites.clear();
}
void MapView::render(const Model::Map *map, Model::Position<float> center, uint64_t time) {
std::pair<int, int> viewport = window->getViewport();
float pixels = std::max(viewport.first/20.0f, viewport.second/15.0f);
int tilePixels = getTileSize() * std::ceil(pixels / getTileSize());
float tilesW = viewport.first / tilePixels;
float tilesH = viewport.second / tilePixels;
int minX = std::floor(center.x - tilesW/2 - 0.5f), maxX = std::ceil(center.x + tilesW/2 + 0.5f);
int minY = std::floor(center.y - tilesH/2 - 0.5f), maxY = std::ceil(center.y + tilesH/2 + 0.5f);
int baseX = viewport.first/2 - int(center.x * tilePixels) - tilePixels/2, baseY = viewport.second/2 - int(center.y * tilePixels) - tilePixels/2;
for (size_t layer = 0; layer < map->getLayerCount(); layer++) {
for (int x = minX; x <= maxX; x++) {
for (int y = minY; y <= maxY; y++) {
uint32_t tile = map->getTileAt(layer, Model::Position<int>{x, y});
if (!tile)
continue;
SDL_Rect src = {
.x = int(getTileSize()*(tile-1)),
.y = 0,
.w = getTileSize(),
.h = getTileSize(),
};
SDL_Rect dst = {
.x = baseX + x*tilePixels,
.y = baseY + y*tilePixels,
.w = tilePixels,
.h = tilePixels,
};
SDL_RenderCopy(window->getRenderer(), tiles, &src, &dst);
}
}
}
for (auto &entity : map->getEntities()) {
Model::Position<float> pos = map->getEntityPosition(entity.get(), time);
Model::Direction dir = entity->getDirection();
SDL_Rect src = {
.x = getTileSize()*int(dir),
.y = 0,
.w = getTileSize(),
.h = getTileSize(),
};
SDL_Rect dst = {
.x = baseX + int(pos.x * tilePixels),
.y = baseY + int(pos.y * tilePixels),
.w = tilePixels,
.h = tilePixels,
};
SDL_RenderCopy(window->getRenderer(), entitySprites[entity->getName()], &src, &dst);
}
}
}
}

View file

@ -1,66 +0,0 @@
/*
Copyright (c) 2014-2015, Matthias Schiffer <mschiffer@universe-factory.net>
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#pragma once
#include "Window.hpp"
#include "../model/Map.hpp"
#include <cmath>
#include <map>
#include <unordered_set>
namespace RPGEdit {
namespace View {
class MapView {
private:
std::shared_ptr<Window> window;
SDL_Texture *tiles;
std::map<std::string, SDL_Texture *> entitySprites;
int getTileSize() {
return 32;
}
public:
MapView(const std::shared_ptr<Window> &window0, const std::vector<std::pair<std::string, int>> &tileset);
~MapView();
void updateEntities(const std::vector<std::unique_ptr<Model::Entity>> &entities);
void clearEntities();
void clear();
void render(const Model::Map *map, Model::Position<float> center, uint64_t time);
};
}
}

View file

@ -1,128 +0,0 @@
/*
Copyright (c) 2014-2015, Matthias Schiffer <mschiffer@universe-factory.net>
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "SpriteCache.hpp"
#include <cstring>
#include <SDL_image.h>
namespace RPGEdit {
namespace View {
SpriteCache::sprite_value SpriteCache::load(const std::string &id) {
std::string filename = "../resources/sprite/" + id + ".png";
SDL_Surface *surface = IMG_Load(filename.c_str());
return sprite_value(surface, SDL_Surface_deleter());
}
SpriteCache::sprite_value SpriteCache::load(const std::string &id, unsigned rotation) {
if (!rotation)
return load(id);
sprite_value &base = get(id, 0);
SDL_Surface *surface;
SDL_LockSurface(base.get());
int w = base->w, h = base->h, d = base->format->BytesPerPixel, w2, h2;
if (rotation == 2) {
w2 = w;
h2 = h;
}
else {
w2 = h;
h2 = w;
}
surface = SDL_CreateRGBSurface(0, w2, h2, base->format->BitsPerPixel,
base->format->Rmask, base->format->Gmask, base->format->Bmask, base->format->Amask);
SDL_LockSurface(surface);
uint8_t *src = reinterpret_cast<uint8_t *>(base->pixels);
uint8_t *dst = reinterpret_cast<uint8_t *>(surface->pixels);
int pitch = base->pitch;
int pitch2 = surface->pitch;
for (int x = 0; x < w; x++) {
for (int y = 0; y < h; y++) {
int x2, y2;
switch (rotation) {
case 1:
x2 = y;
y2 = w - x - 1;
break;
case 2:
x2 = w - x - 1;
y2 = h - y - 1;
break;
case 3:
x2 = h - y - 1;
y2 = x;
}
std::memcpy(dst + y2*pitch2 + d*x2, src + y*pitch + d*x, d);
}
}
SDL_UnlockSurface(surface);
SDL_UnlockSurface(base.get());
return sprite_value(surface, SDL_Surface_deleter());
}
SpriteCache::sprite_value & SpriteCache::get(const std::string &id, unsigned rotation) {
sprite_key key(id, rotation);
sprite_value &surface = sprites[key];
if (!surface)
surface = load(id, rotation);
if (!surface)
sprites.erase(sprites.find(key));
return surface;
}
SDL_Surface * SpriteCache::get(const std::string &type, const std::string &name, unsigned rotation) {
if (rotation >= 4)
return nullptr;
return get(type + "/" + name, rotation).get();
}
}
}

View file

@ -1,76 +0,0 @@
/*
Copyright (c) 2014-2015, Matthias Schiffer <mschiffer@universe-factory.net>
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#pragma once
#include <memory>
#include <string>
#include <unordered_map>
#include <SDL.h>
namespace RPGEdit {
namespace View {
class SpriteCache {
private:
struct SDL_Surface_deleter {
void operator()(SDL_Surface *surface) const {
SDL_FreeSurface(surface);
}
};
typedef std::pair<std::string, int> sprite_key;
typedef std::unique_ptr<SDL_Surface, SDL_Surface_deleter> sprite_value;
struct sprite_key_hash {
size_t operator()(const sprite_key &k) const {
std::hash<std::string> string_hash;
std::hash<int> int_hash;
return string_hash(k.first) ^ int_hash(k.second);
}
};
std::unordered_map<sprite_key, sprite_value, sprite_key_hash> sprites;
sprite_value load(const std::string &id);
sprite_value load(const std::string &id, unsigned rotation);
sprite_value & get(const std::string &id, unsigned rotation);
public:
SDL_Surface * get(const std::string &type, const std::string &name, unsigned rotation = 0);
void clear() {
sprites.clear();
}
};
}
}

View file

@ -1,76 +0,0 @@
/*
Copyright (c) 2014-2015, Matthias Schiffer <mschiffer@universe-factory.net>
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#pragma once
#include "SpriteCache.hpp"
#include <utility>
#include <SDL.h>
namespace RPGEdit {
namespace View {
class Window {
private:
SpriteCache spriteCache;
SDL_Window *window;
SDL_Renderer *renderer;
public:
Window() {
window = SDL_CreateWindow("RPGedit", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, 640, 480, SDL_WINDOW_OPENGL|SDL_WINDOW_RESIZABLE);
renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED|SDL_RENDERER_PRESENTVSYNC);
}
~Window() {
SDL_DestroyRenderer(renderer);
SDL_DestroyWindow(window);
}
SpriteCache * getSpriteCache() {
return &spriteCache;
}
SDL_Renderer * getRenderer() {
return renderer;
}
std::pair<int, int> getViewport() {
int w, h;
SDL_GetWindowSize(window, &w, &h);
return std::pair<int, int>(w, h);
}
};
}
}

View file

@ -0,0 +1,3 @@
{
"sprite": "green_circle"
}

View file

@ -0,0 +1,10 @@
{
"sprite": "red_circle",
"collision": [
{
"type": "circle",
"center": [0, 0],
"radius": 0.46875
}
]
}

View file

@ -0,0 +1,3 @@
{
"sprite": "red_ellipse"
}

View file

@ -0,0 +1,23 @@
{
"sprite": "square",
"collision": [
{
"type": "polygon",
"vertices": [
[-0.46875, -0.46875],
[-0.46875, 0.46875],
[0.46875, 0.46875],
[0.46875, -0.46875]
]
}
],
"frames": 4,
"animation": {
"sequence": [
[500, 0],
[500, 1],
[500, 2],
[500, 3]
]
}
}

View file

@ -0,0 +1,12 @@
{
"sprite": "water",
"frames": 4,
"animation": {
"sequence": [
[500, 0],
[500, 1],
[500, 2],
[500, 3]
]
}
}

View file

@ -0,0 +1,84 @@
{
"tiles": [
"-stone/floor",
"-stone/plate",
"-stone/wall/top",
"-stone/wall/right",
"-stone/wall/bottom",
"-stone/wall/left",
"-stone/wall/top_left",
"-stone/wall/top_right",
"-stone/wall/bottom_right",
"-stone/wall/bottom_left",
"-stone/wall/top_left_inner",
"-stone/wall/top_right_inner",
"-stone/wall/bottom_right_inner",
"-stone/wall/bottom_left_inner",
"@water",
"-stone/border/bottom_right",
"-stone/border/bottom_left"
],
"layers": [
{
"tiles": [
[0, 7, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 8, 0],
[0, 6, 1, 1, 1, 1, 15, 15, 1, 1, 1, 1, 4, 0],
[0, 6, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 4, 0],
[0, 6, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 4, 0],
[3, 11, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 12, 3],
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
[5, 14, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 13, 5],
[0, 6, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 4, 0],
[0, 6, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 4, 0],
[0, 6, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 4, 0],
[0, 10, 5, 5, 5, 14, 1, 1, 13, 5, 5, 5, 9, 0],
[0, 0, 0, 0, 0, 6, 1, 1, 4, 0, 0, 0, 0, 0]
]
},
{
"tiles": [
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 17, 16, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2],
[2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2],
[0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, 0, 0]
]
}
],
"collision": [
{
"type": "polygon",
"vertices": [
[ 2, 1],
[ 6, 1],
[ 6, 2],
[ 8, 2],
[ 8, 1],
[12, 1],
[12, 5],
[14, 5],
[14, 7],
[12, 7],
[12, 11],
[ 8, 11],
[ 8, 13],
[ 6, 13],
[ 6, 11],
[ 2, 11],
[ 2, 7],
[ 0, 7],
[ 0, 5],
[ 2, 5]
]
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 528 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 930 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 927 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 765 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Some files were not shown because too many files have changed in this diff Show more