Skip to content

Instantly share code, notes, and snippets.

@nikki93
Created August 31, 2020 18:36
Show Gist options
  • Select an option

  • Save nikki93/5893543a5e3d76a7858d54b74a4c1a16 to your computer and use it in GitHub Desktop.

Select an option

Save nikki93/5893543a5e3d76a7858d54b74a4c1a16 to your computer and use it in GitHub Desktop.
#include "precomp.h"
#include "edit.h"
#include "ui.h"
// Frame
auto Edit::frame() -> void {
input();
// Box update rules
ker.run<UpdateBoxes>();
}
auto Edit::input() -> void {
// Input rules
ker.run<Input>();
auto &ev = ker.ctx<Events>();
auto &touches = ev.getTouches();
if (mode == "select") {
// Touch-to-select
if (touches.size() == 1 && touches[0].pressed) {
// Collect hits in ascending area order
std::vector<std::pair<double, Entity>> hits;
ker.view<Box>().each([&](const Entity ent, const Box &box) {
if (abs(touches[0].x - box.x) < 0.5 * box.width
&& abs(touches[0].y - box.y) < 0.5 * box.height) {
hits.emplace_back(box.width * box.height, ent);
}
});
std::sort(hits.begin(), hits.end());
// Pick after current selection or first if none
auto pick = Kernel::null;
auto pickNext = true;
for (const auto &[order, ent] : hits) {
if (ker.has<Select>(ent)) {
pickNext = true;
} else if (pickNext) {
pick = ent;
pickNext = false;
}
}
ker.clear<Select>();
if (pick != Kernel::null) {
ker.add<Select>(pick);
}
}
}
if (mode == "pan") {
// Pan view by dragging
if (touches.size() == 1) {
auto [zx, zy] = gfx.viewToWorld(0, 0);
auto [dx, dy] = gfx.viewToWorld(touches[0].screenDX, touches[0].screenDY);
viewX -= dx - zx;
viewY -= dy - zy;
}
}
}
// Draw
auto Edit::applyView() -> void {
gfx.setView(viewX, viewY, viewWidth, viewHeight);
}
auto Edit::draw() -> void {
if (mode == "select") {
// Red boxes for unselected
gfx.scope([&]() {
gfx.setColor(0xff, 0, 0);
ker.view<Box>(Kernel::exclude<Select>).each([&](const Box &box) {
gfx.drawRectangle(box.x, box.y, box.width, box.height);
});
});
// Doubled green boxes for selected
gfx.scope([&]() {
double vx = 0, vy = 0, vw = 0, vh = 0;
std::tie(vx, vy, vw, vh) = gfx.getView();
gfx.setColor(0, 0x80, 0x40);
ker.view<Select, Box>().each([&](const Box &box) {
auto x = box.x, y = box.y, w = box.width, h = box.height;
if (x - 0.5 * w <= vx - 0.5 * vw && x + 0.5 * w >= vx + 0.5 * vw
&& y - 0.5 * h <= vy - 0.5 * vh && y + 0.5 * h >= vy + 0.5 * vh) {
// Covers entire view, just draw at view boundary
x = vx;
y = vy;
w = vw - 4;
h = vh - 4;
}
gfx.drawRectangle(x, y, w, h);
gfx.drawRectangle(x, y, w + 4, h + 4);
});
});
}
// Draw rules
ker.run<Draw>();
}
// UI
inline static auto titleify(const char *s) -> std::string {
std::string r = s;
for (size_t i = 0; i < r.length(); ++i) {
if (std::isupper(r[i])) {
if (i > 0) {
r.insert(i++, " ");
}
r[i] = std::tolower(r[i]);
}
}
return r;
}
auto Edit::uiToolbar() -> void {
ui.button()(enabled ? "play" : "stop")("click", [&](emscripten::val) {
if (enabled) {
play();
} else {
stop();
}
});
ui.div()("spacer");
if (enabled) {
ui.button()("undo")("disabled", undos.size() <= 1)("click", [&](emscripten::val) {
undo();
});
ui.button()("redo")("disabled", redos.size() == 0)("click", [&](emscripten::val) {
redo();
});
ui.div()("small-gap");
ui.button()("pan")("selected", mode == "pan")("click", [&](emscripten::val) {
mode = mode != "pan" ? "pan" : "select";
});
ui.button()("zoom-in")("click", [&](emscripten::val) {
viewWidth *= 0.5;
viewHeight *= 0.5;
});
ui.button()("zoom-out")("click", [&](emscripten::val) {
viewWidth *= 2;
viewHeight *= 2;
});
}
}
auto Edit::uiStatus() -> void {
if (enabled) {
ui.div()([&]() {
ui.text("{}x", viewWidth / 800);
});
ui.div()("small-gap");
ui.div()([&]() {
ui.text(mode);
});
}
}
auto Edit::uiInspect() -> void {
// Inspector for selected
ker.view<Select>().each([&](const Entity ent) {
ui.div()("inspector")([&]() {
// Callbacks to run after. Prevents the UI from displaying inconsistent states.
std::vector<std::function<void(void)>> after;
// Section for each type the entity has
ker.types(ent, [&](const Kernel::MetaType &type, const char *typeName) {
// Header with lowercase type name and remove button
auto typeTitle = titleify(typeName);
ui.key(typeTitle).elem("details")(typeTitle)("open", true)([&]() {
ui.elem("summary")([&]() {
ui.text(typeTitle);
ui.button()("remove")("click", [&](emscripten::val) {
after.emplace_back([=]() {
ker.remove(type, ent);
action(fmt::format("remove {}", typeTitle));
});
});
});
// Details for type instance
if (auto inst = ker.get(type, ent)) {
// Custom inspect
if (auto inspectFn = type.func("uiInspect"_hs)) {
inspectFn.invoke({}, inst, std::ref(ker), ent);
}
// Regular fields
Kernel::fields(type, [&](const Kernel::MetaField &field, const char *fieldName) {
auto value = field.get(inst);
ui.div()("info")([&]() {
if (auto p = value.try_cast<double>()) {
ui.text("{}: {:.2f}", fieldName, *p);
}
});
});
}
});
});
// Add button for each addable type the entity doesn't have
ui.div()("add-bar")([&]() {
for (auto &type : ker.types()) {
if (auto typeName = ker.typeName(type)) {
if (!ker.has(type, ent) && ker.addable(type, ent)) {
// Button with lowercase type name
auto typeTitle = titleify(typeName);
ui.button()("add")("label", typeTitle)("click", [&](emscripten::val) {
ker.add(type, ent);
action(fmt::format("add {}", typeTitle));
});
}
}
}
});
// Run the after callbacks
for (auto &func : after) {
func();
}
});
});
}
// Undo / redo
auto Edit::action(std::string desc) -> void {
Archive ar;
ar.save(ker, [&](const Entity ent, Archive::Writer &w) {
if (ker.has<Select>(ent)) {
w.boolean("select", true);
}
});
undos.push_back(Action { std::move(desc), std::move(ar) });
while (undos.size() > 50) {
undos.pop_front();
}
redos.clear();
}
auto Edit::clearActions() -> void {
undos.clear();
redos.clear();
}
auto Edit::swapAction(std::deque<Action> &from, std::deque<Action> &to) -> void {
to.push_back(std::move(from.back()));
from.pop_back();
restore();
}
auto Edit::undo() -> void {
if (undos.size() > 1) {
swapAction(undos, redos);
fmt::print("undid: {}\n", redos.back().desc);
}
}
auto Edit::redo() -> void {
if (redos.size() > 0) {
swapAction(redos, undos);
fmt::print("redid: {}\n", undos.back().desc);
}
}
auto Edit::restore() -> void {
if (undos.size() > 0) {
ker.clear();
undos.back().ar.load(ker, [&](const Entity ent, Archive::Reader &r) {
if (r.has("select")) {
ker.add<Select>(ent);
}
});
}
}
/* Dependencies */
@import url('https://necolas.github.io/normalize.css/8.0.1/normalize.css');
@import url('https://rsms.me/inter/inter.css');
@import url('https://kit-free.fontawesome.com/releases/latest/css/free-v4-font-face.min.css');
@import url('https://kit-free.fontawesome.com/releases/latest/css/free-v4-shims.min.css');
@import url('https://kit-free.fontawesome.com/releases/latest/css/free.min.css');
/* Font */
html {
font-family: 'Inter', sans-serif;
}
@supports (font-variation-settings: normal) {
html {
font-family: 'Inter var', sans-serif;
}
}
/* Basics */
html {
width: 100%;
height: 100%;
}
body {
background-color: #121212;
color: rgba(255, 255, 255, 0.8);
font-size: 14px;
width: 100%;
height: 100%;
}
/* --- Layout --------------------------------------------------------------------- */
/* Root */
div.root-container {
width: 100%;
height: 100%;
display: flex;
flex-direction: row;
}
/* Main */
div.main-container {
flex: 1;
display: flex;
flex-direction: column;
}
/* Scene */
div.scene-container {
background-color: black;
flex: 1;
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
}
div.scene-aspect-ratio {
width: 100%;
padding-top: 56.25%;
position: relative;
}
canvas.scene {
background-color: black;
outline: none;
-webkit-tap-highlight-color: transparent;
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
}
/* Top */
div.top-panel {
height: 100%;
display: flex;
flex-direction: row;
}
/* Bottom */
div.bottom-panel {
height: 100%;
display: flex;
flex-direction: row;
}
/* Side */
div.side-container {
flex: 0.5;
max-width: 400px;
position: relative;
}
div.side-panel {
position: absolute; /* Force layout boundary */
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
}
/* Status */
div.status {
flex: 1;
padding: 4px 14px 4px 14px;
display: flex;
flex-direction: row;
align-items: center;
}
div.status > *:not(:last-child) {
margin-right: 8px;
}
div.status > div.spacer {
flex: 1;
}
div.toolbar > div.small-gap {
width: 12px;
}
/* Toolbar */
div.toolbar {
flex: 1;
padding: 6px 16px 6px 16px;
display: flex;
flex-direction: row;
align-items: center;
}
div.toolbar > *:not(:last-child) {
margin-right: 8px;
}
div.toolbar > div.spacer {
flex: 1;
}
div.toolbar > div.small-gap {
width: 12px;
}
/* Inspector */
div.inspector {
flex: 1;
padding: 6px 16px 6px 16px;
display: flex;
flex-direction: column;
overflow-y: scroll;
-ms-overflow-style: none;
scrollbar-width: none;
}
div.inspector::-webkit-scrollbar {
display: none;
}
/* Inspector sections */
div.inspector > details {
padding: 8px;
}
div.inspector > details > summary {
font-size: 16px;
font-weight: 500;
cursor: pointer;
user-select: none;
-moz-user-select: none;
-khtml-user-select: none;
-webkit-user-select: none;
padding: 6px;
margin-left: -4px;
margin-right: -4px;
border-radius: 8px;
transition: 0.1s;
}
div.inspector > details > summary:focus {
outline: 0;
}
@media (hover: hover) {
div.inspector > details > summary:hover {
background-color: rgba(255, 255, 255, 0.1);
}
}
div.inspector > details[open] > summary {
margin-bottom: 4px;
}
div.inspector > details[open] {
margin-bottom: 4px;
}
/* Inspector header extra buttons */
div.inspector > details > summary > button {
margin-top: -6px;
margin-right: -6px;
float: right;
font-weight: normal;
}
/* Inspector info text */
div.inspector > details > div.info {
display: flex;
flex-direction: row;
padding: 6px;
align-items: center;
}
/* Inspector add bar */
div.inspector > div.add-bar {
padding: 24px;
display: flex;
flex-direction: row;
flex-wrap: wrap;
}
div.inspector > div.add-bar > button {
margin: 4px;
}
/* Sprite inspector */
div.inspector > details.sprite > img.preview {
margin: 6px;
max-height: 120px;
max-width: 25%;
background-position: 0px 0px, 10px 10px;
background-size: 20px 20px;
background-image: linear-gradient(
45deg,
#eee 25%,
transparent 25%,
transparent 75%,
#eee 75%,
#eee 100%
),
linear-gradient(45deg, #eee 25%, white 25%, white 75%, #eee 75%, #eee 100%);
}
/* Feet inspector */
div.inspector > details.feet > div.info > button.shape {
margin-left: 6px;
}
div.inspector > details.feet > div.info > button.shape::before {
content: '\f040';
}
/* Scene switcher */
div.scene-switcher {
display: flex;
flex-direction: row;
align-items: center;
margin: -2px;
}
div.scene-switcher > button {
margin: 0;
}
div.scene-switcher > button.prev::before {
content: '\f060';
}
div.scene-switcher > button.prev {
content: '\f060';
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
div.scene-switcher > button.next::before {
content: '\f061';
}
div.scene-switcher > button.next {
content: '\f061';
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
div.scene-switcher > div.name {
padding-left: 4px;
padding-right: 4px;
}
/* --- Components ----------------------------------------------------------------- */
/* Button base */
button {
border: none;
cursor: pointer;
font: inherit;
color: inherit;
background-color: transparent;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
padding: 6px;
margin: -2px;
border-radius: 8px;
transition: 0.1s;
}
button:focus {
outline: 0;
}
button::before {
font-size: 16px;
font-family: FontAwesome;
}
button::after {
padding-left: 5px;
}
@media (hover: hover) {
button:hover:not([disabled]) {
background-color: rgba(255, 255, 255, 0.1);
}
}
button:active {
transform: translate(0px, 1px);
}
button[selected] {
color: coral;
}
button[disabled] {
color: #303030;
cursor: default;
}
button[label]::after {
content: attr(label);
}
/* Reload button */
button.reload::before {
content: '\f021';
}
/* Add button */
button.add::before {
content: '\f067';
}
/* Remove button */
button.remove::before {
content: '\f00d';
}
/* Save button */
button.save::before {
content: '\f0c7';
}
/* Profiler button */
button.profiler::before {
content: '\f017';
}
/* Zoom buttons */
button.zoom-in::before {
content: '\f067';
}
button.zoom-out::before {
content: '\f068';
}
/* Pan button */
button.pan::before {
content: '\f256';
}
/* Undo / redo buttons */
button.undo::before {
content: '\f0e2';
}
button.redo::before {
content: '\f01e';
}
/* Play / stop button */
button.play::before {
content: '\f04b';
}
button.stop::before {
content: '\f04d';
}
#include "precomp.h"
#include "archive.h"
#include "edit.h"
#include "events.h"
#include "graphics.h"
#include "kernel.h"
#include "physics.h"
#include "platform.h"
#include "timing.h"
#include "ui.h"
//
// Types
//
// Basics
struct Position {
ngType(Position);
double x = 0;
ngField(x);
double y = 0;
ngField(y);
};
struct Sprite {
ngType(Sprite);
Graphics::Image image;
double scale = 0.25;
ngField(scale);
double depth = 0;
ngField(depth);
static auto add(Kernel &ker, const Entity ent, Archive::Reader &bp = Archive::empty) -> void {
auto &gfx = ker.ctx<Graphics>();
ker.add<Sprite>(
ent, gfx.createImage(Platform::getAssetPath(bp.str("imageName", "player.png"))));
}
ngMethod(add);
auto save(Archive::Writer &bp) -> void {
bp.str("imageName", std::filesystem::path(image.getPath()).filename().u8string());
}
ngMethod(save);
};
// Physics
struct Feet {
ngType(Feet);
Physics::Body body;
Physics::Shape shape;
double offsetX = 0, offsetY = 0;
static auto add(Kernel &ker, const Entity ent, Archive::Reader &bp = Archive::empty) -> void {
auto &phy = ker.ctx<Physics>();
// Body
auto body = !std::strcmp(bp.str("type", "static"), "dynamic")
? phy.createDynamic(bp.num("mass", 1), bp.num("moment", INFINITY))
: phy.createStatic();
body.setEntity(ent);
const auto &pos = ker.get<Position>(ent);
auto offsetX = bp.num("offsetX", 0), offsetY = bp.num("offsetY", 0);
body.setPosition({ pos.x + offsetX, pos.y + offsetY });
// Shape
std::vector<Vec2> verts;
bp.arr("verts", [&]() {
auto size = bp.size();
for (auto i = 0; i + 1 < size; i += 2) {
verts.push_back({ bp.num(i), bp.num(i + 1) });
}
});
auto shape = verts.size() > 0 ? phy.createPoly(body, verts) : phy.createBox(body, 40, 40);
shape.setRadius(bp.num("radius", 0));
ker.add<Feet>(ent, std::move(body), std::move(shape), offsetX, offsetY);
}
ngMethod(add);
auto save(Archive::Writer &bp) -> void {
// Body
if (offsetX != 0) {
bp.num("offsetX", offsetX);
}
if (offsetY != 0) {
bp.num("offsetY", offsetY);
}
if (body.getType() == Physics::Body::Type::Dynamic) {
bp.str("type", "dynamic");
bp.num("mass", body.getMass());
bp.num("moment", body.getMoment());
}
// Shape
bp.arr("verts", [&]() {
for (auto nVerts = shape.getNumVertices(), i = 0; i < nVerts; ++i) {
auto [x, y] = shape.getVertex(i);
bp.num(x);
bp.num(y);
}
});
if (auto radius = shape.getRadius(); radius != 0) {
bp.num("radius", radius);
}
}
ngMethod(save);
};
struct Walk {
Physics::Body target;
Physics::Constraint constraint;
double touchTime = 0;
static auto add(Kernel &ker, const Entity ent) -> void {
auto &phy = ker.ctx<Physics>();
if (ker.has<Feet>(ent)) {
auto &feet = ker.get<Feet>(ent);
auto target = phy.createStatic().setPosition(feet.body.getPosition());
auto constraint = phy.createPivot(target, feet.body, { 0, 0 }, { 0, 0 })
.setMaxForce(2000)
.setMaxBias(180);
ker.add<Walk>(ent, std::move(target), std::move(constraint), ker.tim.t());
}
}
};
struct Friction {
ngType(Friction);
Physics::Constraint constraint;
static auto add(Kernel &ker, const Entity ent) -> void {
auto &phy = ker.ctx<Physics>();
if (ker.has<Feet>(ent)) {
auto &feet = ker.get<Feet>(ent);
auto constraint = phy.createPivot(phy.getBackground(), feet.body, { 0, 0 }, { 0, 0 })
.setMaxForce(800)
.setMaxBias(0);
ker.add<Friction>(ent, std::move(constraint));
}
}
};
struct WorldBounds {
ngType(WorldBounds);
double minX, maxX, minY, maxY;
ngFields(minX, maxX, minY, maxY);
std::array<Physics::Body, 4> bodies;
std::array<Physics::Shape, 4> shapes;
static auto add(Kernel &ker, const Entity ent, Archive::Reader &bp = Archive::empty) -> void {
double minX = bp.num("minX", 0), maxX = bp.num("maxX", 0);
double minY = bp.num("minY", 0), maxY = bp.num("maxY", 0);
if (ker.has<Position>(ent) && ker.has<Sprite>(ent)) { // Auto-read from sprite
auto &pos = ker.get<Position>(ent);
auto &spr = ker.get<Sprite>(ent);
auto [imgW, imgH] = spr.image.getSize();
auto w = spr.scale * imgW, h = spr.scale * imgH;
minX = pos.x - 0.5 * w;
maxX = pos.x + 0.5 * w;
minY = pos.y - 0.5 * h;
maxY = pos.y + 0.5 * h;
}
if (maxX > minX && maxY > minY) {
auto &phy = ker.ctx<Physics>();
constexpr auto border = 100;
std::array bodies {
phy.createStatic().setPosition({ minX - 0.5 * border, 0.5 * (minY + maxY) }), // Left
phy.createStatic().setPosition({ maxX + 0.5 * border, 0.5 * (minY + maxY) }), // Right
phy.createStatic().setPosition({ 0.5 * (minX + maxX), minY - 0.5 * border }), // Top
phy.createStatic().setPosition({ 0.5 * (minX + maxX), maxY + 0.5 * border }), // Bottom
};
std::array shapes {
phy.createBox(bodies[0], border, maxY - minY + 2 * border), // Left
phy.createBox(bodies[1], border, maxY - minY + 2 * border), // Right
phy.createBox(bodies[2], maxX - minX + 2 * border, border), // Top
phy.createBox(bodies[3], maxX - minX + 2 * border, border), // Bottom
};
ker.add<WorldBounds>(ent, minX, maxX, minY, maxY, std::move(bodies), std::move(shapes));
}
}
ngMethod(add);
};
// View
struct ViewFollow {
ngType(ViewFollow);
static constexpr double defaultWidth = 800, defaultHeight = 450, defaultPadding = 180;
double x = 0, y = 0;
ngFields(x, y);
double offsetX = 0, offsetY = 0;
ngFields(offsetX, offsetY);
double width = defaultWidth, height = defaultHeight;
ngFields(width, height);
double padding = defaultPadding;
ngField(padding);
double rate = 200;
ngField(rate);
static auto add(Kernel &ker, const Entity ent) -> void {
if (ker.has<Position>(ent)) {
auto &pos = ker.get<Position>(ent);
ker.add<ViewFollow>(ent, pos.x, pos.y);
}
}
};
// Player
struct Player {
ngType(Player);
Graphics::Image footprints;
static auto add(Kernel &ker, const Entity ent) -> void {
auto &gfx = ker.ctx<Graphics>();
ker.add<Player>(ent, gfx.createImage(Platform::getAssetPath("footprints.png")));
}
};
//
// Triggers
//
// Physics
struct PhysicsPre : Kernel::Trigger<> {};
struct PhysicsPost : Kernel::Trigger<> {};
// Draw
struct ApplyView : Kernel::Trigger<> {};
struct Draw : Kernel::Trigger<> {};
struct DrawOverlay : Kernel::Trigger<> {};
//
// Rules
//
// Sprite
ngRule(Draw, DrawSprites)(Kernel &ker) {
// Order sprites by depth and draw at positions
ker.isort<Sprite>([](const Sprite &a, const Sprite &b) {
return a.depth < b.depth;
});
auto &gfx = ker.ctx<Graphics>();
ker.view<Sprite, Position>().each([&](const Sprite &spr, const Position &pos) {
gfx.drawImage(spr.image, pos.x, pos.y, spr.scale);
});
};
// Player
ngRule(PhysicsPre, WalkPlayerToTouch)(Kernel &ker) {
auto &ev = ker.ctx<Events>();
auto &touches = ev.getTouches();
if (touches.size() > 0) {
// Add walk if needed, set target to first touch
ker.view<Player>().each([&](const Entity ent, const Player &) {
auto &walk = ker.has<Walk>(ent) ? ker.get<Walk>(ent) : ker.add<Walk>(ent);
walk.target.setPosition({ touches[0].x, touches[0].y });
walk.touchTime = ker.tim.t();
});
}
};
ngRule(PhysicsPost, CheckPlayerWalk)(Kernel &ker) {
// Remove walk if reached target or obstructed
ker.view<Player, Feet, Walk>().each(
[&](const Entity ent, const Player &, const Feet &feet, const Walk &walk) {
if (ker.tim.t() - walk.touchTime < 1) { // Recently touched
return;
}
auto [vx, vy] = feet.body.getVelocity();
if (vx * vx + vy * vy >= 20 * 20) { // Moving fast enough
return;
}
auto [fx, fy] = feet.body.getPosition();
auto [wx, wy] = walk.target.getPosition();
auto dx = wx - fx, dy = wy - fy;
auto dLen = std::sqrt(dx * dx + dy * dy);
if (dLen < 10) { // We're close, we'll probably get there
return;
} else if (dLen < 1) { // We're there!
ker.remove<Walk>(ent);
}
// Remove if velocity along direction toward target is too low
dx /= dLen;
dy /= dLen;
auto dot = vx * dx + vy * dy;
if (dot <= 7) {
ker.remove<Walk>(ent);
}
});
};
ngRule(PhysicsPost, ReadPlayerPhysics)(Kernel &ker) {
// Read physics to position
ker.view<Player, Feet, Position>().each([&](const Player &, const Feet &feet, Position &pos) {
auto [x, y] = feet.body.getPosition();
pos.x = x - feet.offsetX;
pos.y = y - feet.offsetY;
});
};
ngRule(PhysicsPost, UpdatePlayerDepth)(Kernel &ker) {
// Set player depth behind objects that obscure it
auto &phy = ker.ctx<Physics>();
ker.view<Player, Feet, Sprite>().each([&](const Entity ent, const Player &, const Feet &feet,
Sprite &spr) {
spr.depth = 1000;
const auto query = [&](double x, double y) {
phy.segmentQuery({ x, y }, { x, y + 1e4 }, 1, [&](Physics::Shape &shape, Vec2, Vec2, double) {
auto other = shape.getBody().getEntity();
if (other != Kernel::null && other != ent && ker.has<Sprite>(other)) {
spr.depth = std::min(spr.depth, ker.get<Sprite>(other).depth - 0.2);
}
});
};
auto [x, y] = feet.body.getPosition();
query(x + 22, y);
query(x - 22, y);
query(x, y);
});
};
ngRule(DrawOverlay, DrawPlayerFootprints)(Kernel &ker) {
// Draw footprints at target when walking
auto &gfx = ker.ctx<Graphics>();
ker.view<Player, Feet, Walk>().each([&](const Player &pl, const Feet &feet, const Walk &walk) {
auto [fx, fy] = feet.body.getPosition();
auto [wx, wy] = walk.target.getPosition();
auto dx = fx - wx, dy = fy - wy;
if (dx * dx + dy * dy > 30 * 30) { // Don't draw if target is too close
gfx.drawImage(pl.footprints, wx, wy, 0.65);
}
});
};
// View
ngRule(PhysicsPost, UpdateViewFollow)(Kernel &ker) {
ker.view<ViewFollow, Position>().each([&](ViewFollow &vf, const Position &pos) {
// See https://www.gamasutra.com/blogs/ScottLembcke/20180404/316046/Improved_Lerp_Smoothing.php
auto dx = pos.x + vf.offsetX - vf.x, dy = pos.y + vf.offsetY - vf.y;
if (abs(dx) > 0.5 * vf.width - vf.padding) {
vf.x += pow(2, -vf.rate * ker.tim.dt()) * abs(dx) / dx
* (abs(dx) - (0.5 * vf.width - vf.padding));
}
if (abs(dy) > 0.5 * vf.height - vf.padding) {
vf.y += pow(2, -vf.rate * ker.tim.dt()) * abs(dy) / dy
* (abs(dy) - (0.5 * vf.height - vf.padding));
}
ker.view<WorldBounds>().each([&](WorldBounds &wb) {
if (vf.x - 0.5 * vf.width < wb.minX) {
vf.x = wb.minX + 0.5 * vf.width;
}
if (vf.x + 0.5 * vf.width > wb.maxX) {
vf.x = wb.maxX - 0.5 * vf.width;
}
if (vf.y - 0.5 * vf.height < wb.minY) {
vf.y = wb.minY + 0.5 * vf.height;
}
if (vf.y + 0.5 * vf.height > wb.maxY) {
vf.y = wb.maxY - 0.5 * vf.height;
}
});
});
};
ngRule(ApplyView, ApplyViewFollow)(Kernel &ker) {
auto &gfx = ker.ctx<Graphics>();
ker.view<ViewFollow>().each([&](const ViewFollow &vf) {
gfx.setView(vf.x, vf.y, vf.width, vf.height);
});
};
//
// Edit
//
ngRule(Edit::UpdateBoxes, EditBoxes)(Kernel &ker) {
auto &edit = ker.ctx<Edit>();
ker.view<Position, Sprite>().each([&](const Entity ent, const Position &pos, const Sprite &spr) {
auto [imgW, imgH] = spr.image.getSize();
edit.setBox(ent, pos.x, pos.y, spr.scale * imgW, spr.scale * imgH);
});
ker.view<WorldBounds>(Kernel::exclude<Sprite>).each([&](const Entity ent, const WorldBounds &wb) {
edit.setBox(ent, 0.5 * (wb.minX + wb.maxX), 0.5 * (wb.minY + wb.maxY), wb.maxX - wb.minX,
wb.maxY - wb.minY);
});
};
ngRule(Edit::Input, EditShape)(Kernel &ker) {
auto &edit = ker.ctx<Edit>();
auto &mode = edit.getMode();
auto &ev = ker.ctx<Events>();
auto &touches = ev.getTouches();
if (mode == "shape") {
auto view = ker.view<Edit::Select, Feet>();
if (view.size() == 0) {
edit.setMode("select");
} else if (touches.size() == 1 && (touches[0].pressed || touches[0].released)) {
auto &phy = ker.ctx<Physics>();
view.each([&](Feet &feet) {
std::vector<Vec2> verts;
for (auto nVerts = feet.shape.getNumVertices(), i = 0; i < nVerts; ++i) {
auto v = feet.shape.getVertex(i);
auto [wx, wy] = feet.body.toWorld(v);
if (!(abs(touches[0].x - wx) < 2 && abs(touches[0].y - wy) < 2)) {
verts.push_back(v);
}
}
if (touches[0].released || verts.size() == 0) {
verts.push_back(feet.body.toLocal({ touches[0].x, touches[0].y }));
}
feet.shape = phy.createPoly(feet.body, verts);
});
if (touches[0].released) {
edit.action("edit feet shape");
}
}
}
};
ngRule(Edit::Draw, EditOverlay)(Kernel &ker) {
auto &edit = ker.ctx<Edit>();
auto &mode = edit.getMode();
auto &gfx = ker.ctx<Graphics>();
if (mode == "shape") {
// Feet shape and vertices for selected only
gfx.setColor(0, 0, 0xff);
ker.view<Edit::Select, Feet>().each([&](const Feet &feet) {
for (auto nVerts = feet.shape.getNumVertices(), i = 0; i < nVerts; ++i) {
auto [wx, wy] = feet.body.toWorld(feet.shape.getVertex(i));
gfx.drawRectangleFill(wx, wy, 4, 4);
}
feet.body.draw(gfx);
});
}
if (mode == "select") {
// All feet shapes
gfx.scope([&]() {
gfx.setColor(0, 0, 0xff);
ker.view<Feet>().each([&](const Feet &feet) {
feet.body.draw(gfx);
});
});
}
};
auto Sprite_uiInspect(Sprite &spr, Kernel &ker, const Entity) -> void {
auto &ui = ker.ctx<UI>();
ui.elem("img")("preview")("src", spr.image.getBlobUrl());
ui.div()("info")([&]() {
ui.text("path: {}", spr.image.getPath());
});
ui.div()("info")([&]() {
auto [imgW, imgH] = spr.image.getSize();
ui.text("width: {}, height: {}", imgW, imgH);
});
}
ngMethod(Sprite, uiInspect);
auto Feet_uiInspect(Feet &feet, Kernel &ker, const Entity ent) -> void {
auto &ui = ker.ctx<UI>();
auto &edit = ker.ctx<Edit>();
ui.div()("info")([&]() {
ui.text("shape: {} vertices", feet.shape.getNumVertices());
if (edit.getEnabled() && !ker.has<Player>(ent)) {
ui.button()("shape")("selected", edit.getMode() == "shape")("click", [&](emscripten::val) {
edit.setMode(edit.getMode() == "shape" ? "select" : "shape");
});
}
});
}
ngMethod(Feet, uiInspect);
//
// Stage
//
struct Stage {
explicit Stage(Kernel &ker_)
: ker(ker_) {
}
auto getSceneName() -> const std::string & {
return sceneName;
}
auto createPlayer(double x, double y) -> void {
auto &gfx = ker.ctx<Graphics>();
auto &phy = ker.ctx<Physics>();
auto ent = ker.create();
ker.add<Player>(ent);
ker.add<Sprite>(ent, gfx.createImage(Platform::getAssetPath("player.png")), 0.25, 1000.0);
auto &pos = ker.add<Position>(ent, x, y);
constexpr auto offsetY = 65.0;
auto body
= phy.createDynamic(1, INFINITY).setEntity(ent).setPosition({ pos.x, pos.y + offsetY });
constexpr auto radius = 8;
auto shape = phy.createBox(body, 45 - 2 * radius, 20 - 2 * radius, radius);
ker.add<Feet>(ent, std::move(body), std::move(shape), 0.0, offsetY);
ker.add<Friction>(ent);
ker.add<ViewFollow>(ent, pos.x, pos.y, 0.0, 30.0);
}
auto load(const std::string &sceneName_) -> void {
sceneName = sceneName_;
ker.clear();
Archive::fromFile(Platform::getAssetPath(sceneName + ".scn")).load(ker);
createPlayer(230, 115);
auto &edit = ker.ctx<Edit>();
edit.clearActions();
edit.action(fmt::format("load scene '{}'", sceneName));
}
auto load(int d) -> void {
auto i = std::distance(
sceneNames.begin(), std::find(sceneNames.begin(), sceneNames.end(), sceneName));
load(sceneNames[(i + d) % sceneNames.size()]);
}
auto save() -> void {
Archive ar;
ar.save(ker, [&](const Entity ent) {
return !ker.has<Player>(ent);
});
#ifdef __EMSCRIPTEN__
emscripten::val::global("console").call<void>("log", ar.toString());
emscripten::val::global("window").call<void>(
"alert", std::string("Please check the JavaScript console..."));
#endif
}
auto convert(const std::string &path, double scale = 0.5) -> void {
auto &gfx = ker.ctx<Graphics>();
auto depth = 0.0;
auto ar = Archive::fromFile(path);
ar.read([&](Archive::Reader &r) {
r.each("objects", [&]() {
auto type = r.str("type", "prop");
auto x = r.num("x", 0);
auto y = r.num("y", 0);
auto imagePath = Platform::getAssetPath(r.str("imageName", "player.png"));
if (!std::strcmp(type, "prop")) {
auto ent = ker.create();
auto &spr = ker.add<Sprite>(ent, gfx.createImage(imagePath), scale, depth++);
auto [imgW, imgH] = spr.image.getSize();
ker.add<Position>(ent, scale * (x + 0.5 * imgW), scale * (y + 0.5 * imgH));
}
});
});
}
private:
Kernel &ker;
std::string sceneName;
inline static auto sceneNames = []() {
std::vector<std::string> result;
for (auto &file : std::filesystem::directory_iterator(Platform::getAssetPath(""))) {
if (file.path().extension() == ".scn") {
result.push_back(file.path().stem().u8string());
}
}
return result;
}();
};
//
// main
//
auto main(int, char **) -> int {
// Modules
Timing tim;
Graphics gfx("dream hotel");
UI ui(tim);
Events ev(gfx);
Physics phy(tim);
Kernel ker(tim);
Edit edit(ker, gfx, ui);
Stage stage(ker);
ker.ctx(gfx, ui, ev, phy, edit, stage);
// Start!
stage.load("pool");
// Loop
ev.loop([&]() {
// Timing
tim.frame();
if (tim.dt() >= 3 * 1 / 60.0) { // Frame drop
return;
}
// Unfocused?
if (!ev.isWindowFocused()) {
return;
}
// Logic
if (edit.getEnabled()) {
// Edit
edit.frame();
} else {
// Physics
ker.run<PhysicsPre>();
phy.frame();
ker.run<PhysicsPost>();
}
// Graphics
gfx.frame([&]() {
// Background color
gfx.clear(0xcc, 0xe4, 0xf5);
// Draw
if (edit.getEnabled()) {
edit.applyView();
} else {
ker.run<ApplyView>();
}
ker.run<Draw>();
ker.run<DrawOverlay>();
if (edit.getEnabled()) {
edit.draw();
}
});
// UI
ui.frame([&]() {
// Top
ui.panel("top", [&]() {
ui.div()("toolbar")([&]() {
edit.uiToolbar();
if (edit.getEnabled()) {
ui.div()("small-gap");
ui.div()("scene-switcher")([&]() {
ui.button()("prev")("click", [&](emscripten::val) {
stage.load(-1);
});
ui.div()("name")([&]() {
ui.text(stage.getSceneName());
});
ui.button()("next")("click", [&](emscripten::val) {
stage.load(1);
});
});
ui.button()("save")("click", [&](emscripten::val) {
stage.save();
});
}
});
});
// Bottom
ui.panel("bottom", [&]() {
ui.div()("status")([&]() {
ui.button()("reload")("click", [&](emscripten::val) {
emscripten::val::global("location").call<void>("reload");
});
ui.button()("profiler")("selected", tim.runningProf())("click", [&](emscripten::val) {
tim.startProf(5);
});
ui.div()([&]() {
ui.text("fps: {}", int(round(tim.getFPS())));
});
ui.div()("spacer");
edit.uiStatus();
});
});
// Side
ui.panel("side", [&]() {
edit.uiInspect();
});
});
});
return 0;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment