color-fractory/source/application.cpp
2024-08-20 12:04:17 +03:00

2077 lines
54 KiB
C++

#include <psemek/app/default_application_factory.hpp>
#include <psemek/gfx/gl.hpp>
#include <psemek/gfx/painter.hpp>
#include <psemek/util/array.hpp>
#include <psemek/random/generator.hpp>
#include <psemek/random/device.hpp>
#include <psemek/random/uniform.hpp>
#include <psemek/util/hash_table.hpp>
#include <psemek/util/enum.hpp>
#include <psemek/util/clock.hpp>
#include <psemek/geom/box.hpp>
#include <psemek/geom/camera.hpp>
#include <psemek/geom/contains.hpp>
#include <psemek/geom/gradient.hpp>
#include <psemek/ecs/container.hpp>
#include <psemek/ecs/declare_uuid.hpp>
#include <psemek/log/log.hpp>
#include <boost/container/flat_set.hpp>
#include <boost/container/flat_map.hpp>
#include <map>
namespace gmtk
{
using namespace psemek;
psemek_declare_enum(color_type, std::uint32_t,
(white)
(black)
(gray)
(red)
(green)
(blue)
(cyan)
(magenta)
(yellow)
)
psemek_declare_enum(shape_type, std::uint32_t,
(circle)
(square)
(hexagon)
)
struct resource_type
{
color_type color;
shape_type shape;
friend bool operator == (resource_type const &, resource_type const &) = default;
friend auto operator <=> (resource_type const &, resource_type const &) = default;
};
struct resource_hash
{
std::size_t operator()(resource_type const & r) const noexcept
{
return util::hash_all(r.color, r.shape);
}
};
psemek_declare_enum(transformer_type, std::uint32_t,
(mixer)
(reshaper)
(hue_shifter)
)
psemek_declare_enum(card_type, std::uint32_t,
(mixer)
(reshaper)
(hue_shifter)
(crossing)
(zoomer)
(eraser)
)
gfx::color_rgba color_of(color_type c)
{
switch (c)
{
case color_type::white: return {255, 255, 255, 255};
case color_type::black: return {64, 64, 64, 255};
case color_type::gray: return {160, 160, 160, 255};
case color_type::red: return {255, 96, 96, 255};
case color_type::green: return {96, 255, 96, 255};
case color_type::blue: return {96, 96, 255, 255};
case color_type::cyan: return {96, 255, 255, 255};
case color_type::magenta: return {255, 96, 255, 255};
case color_type::yellow: return {255, 255, 96, 255};
}
throw util::unknown_enum_value_exception{c};
}
gfx::color_rgba color_of(resource_type const & r)
{
return color_of(r.color);
}
card_type transformer_to_card(transformer_type type)
{
switch (type)
{
case transformer_type::mixer: return card_type::mixer;
case transformer_type::reshaper: return card_type::reshaper;
case transformer_type::hue_shifter: return card_type::hue_shifter;
}
throw util::unknown_enum_value_exception{type};
}
std::optional<transformer_type> card_to_transformer(card_type type)
{
switch (type)
{
case card_type::mixer: return transformer_type::mixer;
case card_type::reshaper: return transformer_type::reshaper;
case card_type::hue_shifter: return transformer_type::hue_shifter;
default: return std::nullopt;
}
return std::nullopt;
}
struct recipe
{
boost::container::flat_multiset<resource_type> inputs;
resource_type output;
};
static util::hash_map<transformer_type, std::vector<recipe>> const recipies
{
{
transformer_type::mixer,
{
{{{color_type::white, shape_type::circle}, {color_type::black, shape_type::circle}}, {color_type::gray, shape_type::circle}},
{{{color_type::white, shape_type::circle}, {color_type::red, shape_type::circle}, {color_type::green, shape_type::circle}}, {color_type::yellow, shape_type::circle}},
{{{color_type::white, shape_type::circle}, {color_type::red, shape_type::circle}, {color_type::blue, shape_type::circle}}, {color_type::magenta, shape_type::circle}},
{{{color_type::white, shape_type::circle}, {color_type::green, shape_type::circle}, {color_type::blue, shape_type::circle}}, {color_type::cyan, shape_type::circle}},
{{{color_type::red, shape_type::square}, {color_type::green, shape_type::square}, {color_type::blue, shape_type::square}}, {color_type::white, shape_type::square}},
{{{color_type::cyan, shape_type::square}, {color_type::magenta, shape_type::square}, {color_type::yellow, shape_type::square}}, {color_type::black, shape_type::square}},
{{{color_type::white, shape_type::square}, {color_type::black, shape_type::square}}, {color_type::gray, shape_type::square}},
},
},
{
transformer_type::reshaper,
{
{{{color_type::gray, shape_type::circle}, {color_type::red, shape_type::circle}}, {color_type::red, shape_type::square}},
{{{color_type::gray, shape_type::circle}, {color_type::green, shape_type::circle}}, {color_type::green, shape_type::square}},
{{{color_type::gray, shape_type::circle}, {color_type::blue, shape_type::circle}}, {color_type::blue, shape_type::square}},
{{{color_type::gray, shape_type::circle}, {color_type::cyan, shape_type::circle}}, {color_type::cyan, shape_type::square}},
{{{color_type::gray, shape_type::circle}, {color_type::magenta, shape_type::circle}}, {color_type::magenta, shape_type::square}},
{{{color_type::gray, shape_type::circle}, {color_type::yellow, shape_type::circle}}, {color_type::yellow, shape_type::square}},
// {{{color_type::white, shape_type::square}, {color_type::red, shape_type::square}}, {color_type::red, shape_type::hexagon}},
// {{{color_type::white, shape_type::square}, {color_type::green, shape_type::square}}, {color_type::green, shape_type::hexagon}},
// {{{color_type::white, shape_type::square}, {color_type::blue, shape_type::square}}, {color_type::blue, shape_type::hexagon}},
// {{{color_type::white, shape_type::square}, {color_type::cyan, shape_type::square}}, {color_type::cyan, shape_type::hexagon}},
// {{{color_type::white, shape_type::square}, {color_type::magenta, shape_type::square}}, {color_type::magenta, shape_type::hexagon}},
// {{{color_type::white, shape_type::square}, {color_type::yellow, shape_type::square}}, {color_type::yellow, shape_type::hexagon}},
},
},
{
transformer_type::hue_shifter,
{
{{{color_type::black, shape_type::circle}, {color_type::red, shape_type::circle}}, {color_type::green, shape_type::circle}},
{{{color_type::black, shape_type::circle}, {color_type::green, shape_type::circle}}, {color_type::blue, shape_type::circle}},
{{{color_type::black, shape_type::circle}, {color_type::blue, shape_type::circle}}, {color_type::red, shape_type::circle}},
{{{color_type::black, shape_type::circle}, {color_type::cyan, shape_type::circle}}, {color_type::magenta, shape_type::circle}},
{{{color_type::black, shape_type::circle}, {color_type::magenta, shape_type::circle}}, {color_type::yellow, shape_type::circle}},
{{{color_type::black, shape_type::circle}, {color_type::yellow, shape_type::circle}}, {color_type::cyan, shape_type::circle}},
},
},
};
geom::vector<int, 2> const neighbours[4]
{
{1, 0},
{0, 1},
{-1, 0},
{0, -1},
};
struct location
{
int level;
geom::point<int, 2> coords;
geom::point<float, 2> center() const
{
float s = std::pow(3.f, -level);
return {(coords[0] + 0.5f) * s, (coords[1] + 0.5f) * s};
}
geom::box<float, 2> bbox(float extra = 0.f) const
{
float s = std::pow(3.f, -level);
return {{{(coords[0] - extra) * s, (coords[0] + 1.f + extra) * s}, {(coords[1] - extra) * s, (coords[1] + 1.f + extra) * s}}};
}
location left() const
{
return {level, {coords[0] - 1, coords[1]}};
}
location right() const
{
return {level, {coords[0] + 1, coords[1]}};
}
location bottom() const
{
return {level, {coords[0], coords[1] - 1}};
}
location top() const
{
return {level, {coords[0], coords[1] + 1}};
}
location moved(geom::vector<int, 2> const & delta) const
{
return {level, coords + delta};
}
location down() const
{
return {level + 1, {coords[0] * 3 + 1, coords[1] * 3 + 1}};
}
location up() const
{
return {level - 1, {geom::idiv(coords[0], 3), geom::idiv(coords[1], 3)}};
}
friend bool operator == (location const & x, location const & y) = default;
friend auto operator <=> (location const & x, location const & y) = default;
};
struct location_hash
{
std::size_t operator()(location const & x) const noexcept
{
return util::hash_all(x.level, x.coords[0], x.coords[1]);
}
};
struct vertex
{
psemek_ecs_declare_uuid("vertex")
struct location location;
};
struct source
{
psemek_ecs_declare_uuid("source")
resource_type type;
float animate = 0.f;
};
struct transformer
{
psemek_ecs_declare_uuid("transformer")
transformer_type type;
float animate = 0.f;
};
struct crossing
{
psemek_ecs_declare_uuid("crossing")
};
struct zoomer
{
psemek_ecs_declare_uuid("zoomer")
};
struct path_vertex
{
psemek_ecs_declare_uuid("path_vertex")
struct location location;
boost::container::flat_set<ecs::handle> belts_to = {};
boost::container::flat_set<ecs::handle> belts_from = {};
};
void add_belt(ecs::container & world, ecs::handle from, ecs::handle to)
{
world.get(from).get<path_vertex>().belts_to.insert(to);
world.get(to).get<path_vertex>().belts_from.insert(from);
}
bool remove_belt(ecs::container & world, ecs::handle from, ecs::handle to)
{
bool removed = world.get(from).get<path_vertex>().belts_to.contains(to);
world.get(from).get<path_vertex>().belts_to.erase(to);
world.get(to).get<path_vertex>().belts_from.erase(from);
return removed;
}
struct occupied
{
psemek_ecs_declare_uuid("occupied")
ecs::handle entity;
};
struct item
{
psemek_ecs_declare_uuid("item")
resource_type type;
location start;
ecs::handle target = ecs::handle::null();
float state = 0.f;
};
geom::point<float, 2> position(ecs::container & world, item const & i)
{
if (i.target)
{
auto end = world.get(i.target).get<path_vertex const>().location;
return geom::lerp(i.start.center(), end.center(), i.state);
}
else
return i.start.center();
}
float scale(ecs::container & world, item const & i)
{
if (i.target)
{
auto end = world.get(i.target).get<path_vertex const>().location;
return 3.f * std::pow(3.f, -geom::lerp<float>(i.start.level, end.level, i.state));
}
else
return 3.f * std::pow(3.f, -i.start.level);
}
bool within_grid(location const & l)
{
int max = std::pow(3, l.level + 1);
return l.coords[0] >= 0 && l.coords[0] < max && l.coords[1] >= 0 && l.coords[1] < max;
}
bool within_tile(location const & tile, location const & l)
{
if (l.level < tile.level)
return false;
int s = std::pow(3, l.level - tile.level);
geom::interval xrange{tile.coords[0] * s, (tile.coords[0] + 1) * s - 1};
geom::interval yrange{tile.coords[1] * s, (tile.coords[1] + 1) * s - 1};
return geom::contains(xrange, l.coords[0]) && geom::contains(yrange, l.coords[1]);
}
struct lab
{
psemek_ecs_declare_uuid("lab")
float animate = 0.f;
float animate_error = 0.f;
};
struct map
{
ecs::container world;
util::hash_map<card_type, int> cards = {};
int stage = 0;
int resource_count = 0;
float spawn_timer = 0.f;
bool take_card(card_type type)
{
if (type == card_type::eraser)
return true;
if (!cards.contains(type))
return false;
if (cards.at(type) == 0)
return false;
cards.at(type) -= 1;
return true;
}
void put_card(card_type type)
{
cards[type] += 1;
}
};
struct stage_info
{
resource_type type;
int count;
std::vector<card_type> cards;
std::vector<resource_type> sources;
};
static stage_info stages[]
{
{{color_type::white, shape_type::circle}, 0, {}, {{color_type::white, shape_type::circle}}},
{{color_type::white, shape_type::circle}, 15, {card_type::mixer}, {{color_type::black, shape_type::circle}}},
{{color_type::gray, shape_type::circle}, 15, {card_type::hue_shifter, card_type::crossing}, {{color_type::red, shape_type::circle}}},
{{color_type::green, shape_type::circle}, 15, {card_type::reshaper, card_type::crossing}, {}},
{{color_type::green, shape_type::square}, 30, {card_type::hue_shifter, card_type::crossing, card_type::zoomer}, {}},
{{color_type::blue, shape_type::square}, 30, {card_type::mixer, card_type::crossing, card_type::zoomer}, {}},
{{color_type::yellow, shape_type::square}, 30, {card_type::zoomer}, {}},
{{color_type::magenta, shape_type::square}, 30, {}, {}},
{{color_type::cyan, shape_type::square}, 30, {card_type::reshaper, card_type::reshaper, card_type::zoomer}, {{color_type::black, shape_type::circle}}},
{{color_type::white, shape_type::square}, 45, {card_type::mixer, card_type::mixer, card_type::mixer, card_type::mixer, card_type::zoomer, card_type::zoomer, card_type::crossing}, {{color_type::white, shape_type::circle}}},
{{color_type::black, shape_type::square}, 45, {card_type::mixer, card_type::mixer, card_type::reshaper, card_type::reshaper, card_type::reshaper, card_type::zoomer, card_type::zoomer, card_type::crossing, card_type::crossing, card_type::crossing}, {{color_type::red, shape_type::circle}}},
{{color_type::gray, shape_type::square}, 60, {card_type::zoomer}, {}},
};
template <typename Component, util::uuid UUID>
struct index_base
{
static constexpr util::uuid uuid()
{
return UUID;
}
index_base(ecs::container & world)
: world_(world)
{
world.apply<Component>([this](ecs::handle entity, Component const & v){
index_[v.location] = entity;
});
world.constructor<Component>([this](ecs::handle entity, Component const & v){
index_[v.location] = entity;
});
world.destructor<Component>([this](Component const & v){
index_.erase(v.location);
});
}
std::optional<ecs::handle> find(location const & l) const
{
if (auto it = index_.find(l); it != index_.end())
return it->second;
return std::nullopt;
}
ecs::handle get(location const & l) const
{
if (auto entity = find(l))
return *entity;
return world_.create(Component{l});
}
private:
ecs::container & world_;
util::hash_map<location, ecs::handle, location_hash> index_;
};
using index = index_base<vertex, util::make_uuid("vertex_index")>;
using path_index = index_base<path_vertex, util::make_uuid("path_vertex_index")>;
void sink_belt(ecs::container & world, location m)
{
if (auto entity = world.index<index>().find(m.up()); !entity || !world.get(*entity).contains<zoomer>())
return;
auto & index = world.index<path_index>();
auto c = m.up().down();
auto d = m.coords - c.coords;
auto ce = index.get(c);
auto me = index.get(m);
auto & cv = world.get(ce).get<path_vertex>();
if (cv.belts_to.contains(me))
{
auto & mv = world.get(me).get<path_vertex>();
auto ne = *mv.belts_to.begin();
remove_belt(world, ce, me);
remove_belt(world, me, ne);
auto cd = m.down();
auto md = cd.moved(d);
auto cde = index.get(cd);
auto mde = index.get(md);
add_belt(world, cde, mde);
add_belt(world, mde, ne);
sink_belt(world, md);
}
else if (cv.belts_from.contains(me))
{
auto & mv = world.get(me).get<path_vertex>();
auto ne = *mv.belts_from.begin();
remove_belt(world, ne, me);
remove_belt(world, me, ce);
auto cd = m.down();
auto md = cd.moved(d);
auto cde = index.get(cd);
auto mde = index.get(md);
add_belt(world, ne, mde);
add_belt(world, mde, cde);
sink_belt(world, md);
}
}
void sink(ecs::container & world, location & c, location & m)
{
if (auto entity = world.index<index>().find(m.up()); !entity || !world.get(*entity).contains<zoomer>())
return;
auto d = m.coords - c.coords;
c = m.down();
m = c.moved(d);
sink(world, c, m);
}
void clear_tile(map & map, location const & l)
{
auto entity = map.world.index<index>().find(l);
if (!entity)
return;
auto acc = map.world.get(*entity);
if (acc.contains<source>() || acc.contains<lab>())
return;
if (acc.contains<crossing>())
map.put_card(card_type::crossing);
else if (acc.contains<zoomer>())
{
map.put_card(card_type::zoomer);
// TODO: CLEAR ZOOMER
for (int y = 0; y < 3; ++y)
{
for (int x = 0; x < 3; ++x)
{
location p{l.level + 1, {3 * l.coords[0] + x, 3 * l.coords[1] + y}};
clear_tile(map, p);
}
}
auto & index = map.world.index<path_index>();
for (int y = 0; y < 9; ++y)
{
for (int x = 0; x < 9; ++x)
{
location p{l.level + 2, {9 * l.coords[0] + x, 9 * l.coords[1] + y}};
if (auto e = index.find(p))
{
auto & v = map.world.get(*e).get<path_vertex>();
for (auto to : v.belts_to)
{
auto & tv = map.world.get(to).get<path_vertex>();
if (!within_tile(l, tv.location))
remove_belt(map.world, to, *tv.belts_to.begin());
remove_belt(map.world, *e, to);
}
for (auto from : v.belts_from)
{
auto & fv = map.world.get(from).get<path_vertex>();
if (!within_tile(l, fv.location))
remove_belt(map.world, *fv.belts_from.begin(), from);
remove_belt(map.world, from, *e);
}
}
}
}
}
else if (auto t = acc.get_if<transformer>())
{
map.put_card(transformer_to_card(t->type));
}
map.world.destroy(*entity);
}
map starting_map()
{
map result;
result.world.index<index>();
result.world.index<path_index>();
result.world.create(
vertex{{0, {1, -1}}},
lab{}
);
result.cards[card_type::eraser] = 1;
return result;
}
void draw_grid(geom::box<float, 2> const & box, float view_level, gfx::painter & painter, bool solid = false)
{
float const grid_width = 0.025f * std::min(box[0].length() / 3.f, std::pow(3.f, -1.f - view_level));
gfx::color_rgba color = gfx::black;
if (!solid)
color = gfx::light(color, 0.75f);
for (int x = 0; x <= 3; ++x)
{
if (solid)
{
painter.line(box.corner(x / 3.f, 0.f), box.corner(x / 3.f, 1.f), grid_width, color, true);
painter.line(box.corner(0.f, x / 3.f), box.corner(1.f, x / 3.f), grid_width, color, true);
}
else
{
for (int i = 0; i < 7; ++i)
{
float s = (i - 1.f / 3.f) / 6.f;
float t = (i + 1.f / 3.f) / 6.f;
s = std::max(s, 0.f);
t = std::min(t, 1.f);
painter.line(box.corner(x / 3.f, s), box.corner(x / 3.f, t), grid_width, color, true);
painter.line(box.corner(s, x / 3.f), box.corner(t, x / 3.f), grid_width, color, true);
}
}
}
}
void draw_item(resource_type const & type, geom::point<float, 2> const & pos, float scale, gfx::painter & painter, bool selected = false)
{
auto color = color_of(type);
gfx::color_rgba bcolor = selected ? gfx::white : gfx::black;
switch (type.shape)
{
case shape_type::circle:
painter.circle(pos, 0.075f * scale, bcolor);
painter.circle(pos, 0.05f * scale, color);
break;
case shape_type::square:
{
auto box = geom::expand(geom::box<float, 2>::singleton(pos), 0.075f * scale);
painter.rect(box, bcolor);
box = geom::shrink(box, 0.025f * scale);
painter.rect(box, color);
}
break;
case shape_type::hexagon:
{
for (int i = 0; i < 2; ++i)
{
float r = (i == 0 ? 0.0875f : 0.0875f - 0.025f / std::sqrt(0.75f)) * scale;
auto c = (i == 0) ? bcolor : color;
for (int j = 0; j < 6; ++j)
{
float a = geom::rad(j * 60.f);
float b = geom::rad((j + 1) * 60.f);
painter.triangle(pos, pos + geom::direction(a) * r, pos + geom::direction(b) * r, c);
}
}
}
break;
}
}
float animation_factor(float animate)
{
static geom::gradient<float> const g
{
std::pair{0.5f, 0.f},
geom::easing_type::cubic,
std::pair{0.875f, 1.f},
geom::easing_type::cubic,
std::pair{1.f, 0.f},
};
return g(animate);
}
void draw_structure(geom::box<float, 2> const & bbox, transformer const & t, gfx::painter & painter)
{
switch (t.type)
{
case transformer_type::mixer:
{
float an = animation_factor(t.animate);
auto box = geom::shrink(bbox, bbox[0].length() * 0.2f);
float r1 = std::sqrt(2.f) * (2.f + 2.f * an) / 16.f * box[0].length();
float r2 = std::sqrt(2.f) * (4.f + 2.f * an) / 16.f * box[0].length();
float s = bbox[0].length() * 0.025f;
float t = 0.6f * s;
box = geom::expand(box, an * box[0].length() * 0.125f);
geom::point<float, 2> points[]
{
box.corner(0.f, 0.75f),
box.corner(0.f, 0.25f),
box.corner(0.25f, 0.f),
box.corner(0.75f, 0.f),
box.corner(1.f, 0.25f),
box.corner(1.f, 0.75f),
box.corner(0.75f, 1.f),
box.corner(0.25f, 1.f),
};
static geom::triangle<std::uint32_t> const triangles[]
{
{0, 1, 2},
{0, 2, 3},
{0, 3, 4},
{0, 4, 5},
{0, 5, 6},
{0, 6, 7},
};
for (int i = 0; i < 2; ++i)
{
auto color = (i == 0) ? gfx::black : gfx::white;
if (i == 1)
{
points[0] += geom::vector{ s, -t};
points[1] += geom::vector{ s, t};
points[2] += geom::vector{ t, s};
points[3] += geom::vector{-t, s};
points[4] += geom::vector{-s, t};
points[5] += geom::vector{-s, -t};
points[6] += geom::vector{-t, -s};
points[7] += geom::vector{ t, -s};
}
for (auto const & t : triangles)
{
auto p0 = points[t[0]];
auto p1 = points[t[1]];
auto p2 = points[t[2]];
painter.triangle(p0, p1, p2, color);
}
}
auto c = bbox.center();
for (int i = 0; i < 4; ++i)
{
auto d = geom::direction(geom::rad(45.f + 90.f * i));
painter.line(c + d * r1, c + d * r2, s, gfx::black, true);
}
}
break;
case transformer_type::reshaper:
{
auto box = geom::shrink(bbox, bbox[0].length() * 0.2f);
float s = bbox[0].length() * 0.025f;
for (int k = 0; k < 2; ++k)
{
geom::point<float, 2> points[]
{
box.corner(0.f, 1.f),
box.corner(0.f, 0.75f),
box.corner(0.25f, 0.5f),
box.corner(0.75f, 0.5f),
box.corner(1.f, 0.75f),
box.corner(1.f, 1.f),
};
static geom::triangle<std::uint32_t> const triangles[]
{
{0, 1, 2},
{0, 2, 3},
{0, 3, 4},
{0, 4, 5},
};
auto c = bbox.center();
for (int i = 0; i < 2; ++i)
{
auto color = (i == 0) ? gfx::black : gfx::white;
if (i == 1)
{
points[0] += geom::vector{s, -s};
points[1] += geom::vector{s, 0.6f * s};
points[2] += geom::vector{0.6f * s, s};
points[3] += geom::vector{- 0.6f * s, s};
points[4] += geom::vector{-s, 0.6f * s};
points[5] += geom::vector{-s, -s};
}
for (int j = 0; j < 2; ++j)
{
float d = 1.f - animation_factor(t.animate);
d *= box[0].length() * 0.125f;
if (j == 1)
d = -d;
for (auto const & t : triangles)
{
auto p0 = points[t[0]];
auto p1 = points[t[1]];
auto p2 = points[t[2]];
if (j == 1)
{
float m = box[1].min + box[1].max;
p0[1] = m - p0[1];
p1[1] = m - p1[1];
p2[1] = m - p2[1];
}
p0[1] += d;
p1[1] += d;
p2[1] += d;
if (k == 0)
{
p0 = c + geom::ort(p0 - c);
p1 = c + geom::ort(p1 - c);
p2 = c + geom::ort(p2 - c);
}
painter.triangle(p0, p1, p2, color);
}
}
}
}
break;
}
case transformer_type::hue_shifter:
{
float size = bbox[0].length();
float s = size * 0.025f;
float r1 = size * 0.33f - s / 2.f;
float r2 = r1 * 0.5f - s / 2.f;
auto c = bbox.center();
static geom::gradient<float> const g
{
std::pair{0.5f, 1.f},
geom::easing_type::cubic,
std::pair{1.f, 0.f},
};
float offset = - g(t.animate) * geom::pi / 3.f;
int n = 6;
for (int i = 0; i < n; ++i)
{
float a = (i * 2.f * geom::pi) / n + offset;
float b = ((i + 1) * 2.f * geom::pi) / n + offset;
auto da = geom::direction(a);
auto db = geom::direction(b);
painter.triangle(c + da * r2, c + db * r2, c + da * r1, gfx::white);
painter.triangle(c + db * r2, c + da * r1, c + db * r1, gfx::white);
}
for (int i = 0; i < n; ++i)
{
float a = (i * 2.f * geom::pi) / n + offset;
float b = ((i + 1) * 2.f * geom::pi) / n + offset;
auto da = geom::direction(a);
auto db = geom::direction(b);
painter.line(c + da * r1, c + db * r1, s, gfx::black, false);
painter.line(c + da * r2, c + db * r2, s, gfx::black, false);
if (i % (n / 6) == 0)
painter.line(c + da * r2, c + da * r1, s, gfx::black, true);
}
}
break;
}
}
void draw_structure(geom::box<float, 2> const & bbox, crossing const &, gfx::painter & painter)
{
auto wbox = geom::shrink(bbox, bbox[0].length() * 0.1f);
auto sbox = geom::shrink(bbox, bbox[0].length() * 0.3f);
gfx::color_rgba color{64, 64, 64, 255};
painter.rect({wbox[0], sbox[1]}, color);
painter.rect({sbox[0], wbox[1]}, color);
}
void draw_card(geom::box<float, 2> const & bbox, card_type type, gfx::painter & painter)
{
switch (type)
{
case card_type::crossing:
draw_structure(bbox, crossing{}, painter);
break;
case card_type::mixer:
case card_type::reshaper:
case card_type::hue_shifter:
{
float animate = 0.f;
// if (type == card_type::reshaper)
// animate = 0.875f;
draw_structure(bbox, transformer{.type = card_to_transformer(type).value(), .animate = animate}, painter);
}
break;
case card_type::zoomer:
draw_grid(bbox, -1.f, painter, true);
break;
case card_type::eraser:
{
float const w = 0.075f * bbox[0].length();
float s = 0.2f;
float t = 0.8f;
gfx::color_rgba color = {255, 128, 128, 255};
painter.line(bbox.corner(s, s), bbox.corner(t, t), w, color);
painter.line(bbox.corner(t, s), bbox.corner(s, t), w, color);
}
break;
}
}
void draw_grids(map & map, float view_level, gfx::painter & painter)
{
draw_grid({{{0.f, 3.f}, {0.f, 3.f}}}, view_level, painter);
map.world.apply<vertex const, zoomer const>([&](vertex const & v, zoomer const &)
{
draw_grid(v.location.bbox(), view_level, painter);
});
}
void draw(map & map, gfx::painter & painter, float pixel_size)
{
map.world.apply<vertex const, crossing const>([&](vertex const & v, crossing const & c)
{
draw_structure(v.location.bbox(), c, painter);
});
for (int i = 0; i < 2; ++i)
map.world.apply<path_vertex const>([&](path_vertex const & vertex)
{
float w = (i == 0 ? 0.3f : 0.25f);
float w0 = w * std::pow(3.f, 1.f - vertex.location.level);
gfx::color_rgba c{192, 192, 192, 255};
if (i == 0)
c = {0, 0, 0, 255};
if (vertex.location.up().down() == vertex.location && (!vertex.belts_to.empty() || !vertex.belts_from.empty()))
painter.circle(vertex.location.center(), w0 / 2.f, c);
for (auto b : vertex.belts_to)
{
auto q = map.world.get(b).get<path_vertex const>().location;
float w1 = w * std::pow(3.f, 1.f - q.level);
painter.line(vertex.location.center(), q.center(), w0, w1, c, c, false);
}
});
map.world.apply<path_vertex const>([&](path_vertex const & vertex)
{
for (auto b : vertex.belts_to)
{
auto q = map.world.get(b).get<path_vertex const>().location;
geom::vector d = q.center() - vertex.location.center();
auto c = vertex.location.center() + d / 2.f;
auto n = geom::ort(d);
float s = 1.f / 6.f;
d *= s * 0.5f;
n *= s;
// gfx::color_rgba color = {255, 128, 0, 255};
gfx::color_rgba color = {64, 64, 64, 255};
painter.triangle(c - d + n, c - d - n, c + d, color);
}
});
map.world.apply<vertex const, source const>([&](vertex const & v, source const & s)
{
geom::vector shift{0.f, 0.f};
if (v.location.coords[0] < 0)
shift = {1.f, 0.f};
else if (v.location.coords[0] >= 3)
shift = {-1.f, 0.f};
else
shift = {0.f, -1.f};
shift *= v.location.bbox()[0].length() * animation_factor(s.animate) / 12.f;
painter.rect(shift + v.location.bbox(-0.2f), gfx::black);
painter.rect(shift + v.location.bbox(-0.225f), color_of(s.type));
});
map.world.apply<vertex const, transformer const>([&](vertex const & v, transformer const & t)
{
auto box = v.location.bbox();
// box = geom::expand(box, 0.125f * animation_factor(t.animate) * box[0].length());
draw_structure(box, t, painter);
});
map.world.apply<vertex const, lab const>([&](vertex const & v, lab const & l)
{
geom::vector shift{1.f, -1.f};
shift[0] *= v.location.bbox()[0].length() * geom::sqr(std::sin(l.animate_error * geom::pi)) * std::sin(l.animate_error * geom::pi * 5.f) / 12.f;
shift[1] *= v.location.bbox()[0].length() * animation_factor(l.animate) / 12.f;
painter.rect(shift + v.location.bbox(-0.2f), {0, 0, 0, 255});
painter.rect(shift + v.location.bbox(-0.225f), {192, 192, 192, 255});
auto pen = v.location.bbox(-0.4f).corner(0.5f, 1.f);
float vs = 2.f * pixel_size;
draw_item(stages[map.stage].type, pen, 1.f, painter);
pen[1] -= 24.f * vs;
painter.text(pen, std::format("{}/{}", map.resource_count, stages[map.stage].count), {.scale = {vs, -vs}, .c = {0, 0, 0, 255}});
});
map.world.apply<item const>([&](item const & i)
{
draw_item(i.type, position(map.world, i), scale(map.world, i), painter);
});
}
void draw_selection(geom::box<float, 2> const & b, gfx::painter & painter, gfx::color_rgba const & color, bool solid = false)
{
float w = b[0].length() * 0.05f;
if (solid)
{
painter.line(b.corner(0, 0), b.corner(1, 0), w, color, true);
painter.line(b.corner(1, 0), b.corner(1, 1), w, color, true);
painter.line(b.corner(1, 1), b.corner(0, 1), w, color, true);
painter.line(b.corner(0, 1), b.corner(0, 0), w, color, true);
}
else
{
for (int i = 0; i <= 2; ++i)
{
float s = (i - 1.f / 3.f) / 2.f;
float t = (i + 1.f / 3.f) / 2.f;
s = std::max(s, 0.f);
t = std::min(t, 1.f);
painter.line(b.corner(s, 0), b.corner(t, 0), w, color, true);
painter.line(b.corner(s, 1), b.corner(t, 1), w, color, true);
painter.line(b.corner(0, s), b.corner(0, t), w, color, true);
painter.line(b.corner(1, s), b.corner(1, t), w, color, true);
}
}
}
std::vector<std::string> card_description(card_type type)
{
switch (type)
{
case card_type::mixer:
return {"Mixer", "Mixes input colors"};
case card_type::hue_shifter:
return {"Hue shifter", "Shifts the hue of", " input colors"};
case card_type::reshaper:
return {"Reshaper", "Turns circles into squares"};
case card_type::crossing:
return {"Bridge", "Allows belts to cross", " without merging"};
case card_type::zoomer:
return {"Grid", "Creates an embedded 3x3 grid", "Zoom using mouse wheel"};
case card_type::eraser:
return {"Eraser", "Erases placed structures"};
}
throw util::unknown_enum_value_exception{type};
}
std::uint64_t make_seed()
{
random::device d;
return (std::uint64_t(d()) << 32) | d();
}
struct application
: app::application
{
application(options const &, context const &)
: seed_(make_seed())
, map_rng_{seed_, 0xf24130ddef6fb31full}
, item_rng_{seed_, 0xb9fc3979f9860bbdull}
, map_(starting_map())
{
log::info() << "Starting seed: " << seed_;
view_stack_.push_back({-1, {0, 0}});
}
void on_event(app::resize_event const & event) override
{
screen_size_ = event.size;
}
void on_event(app::mouse_wheel_event const & event) override
{
if (event.delta > 0)
{
if (selected_ && !view_transition_)
{
bool transitioned = false;
if (auto entity = map_.world.index<index>().find(*selected_))
if (map_.world.get(*entity).contains<zoomer>())
{
view_transition_ = {view_stack_.back()};
view_stack_.push_back(*selected_);
selected_ = std::nullopt;
transitioned = true;
}
if (!transitioned && within_grid(*selected_) && selected_->level == view_stack_.back().level + 1 && selected_->up() != view_stack_.back())
{
view_transition_ = {view_stack_.back()};
view_stack_.back() = selected_->up();
selected_ = std::nullopt;
transitioned = true;
}
}
}
if (event.delta < 0)
{
if (!view_transition_ && view_stack_.size() > 1)
{
view_transition_ = {view_stack_.back()};
view_stack_.pop_back();
selected_ = std::nullopt;
}
}
}
void on_event(app::mouse_move_event const & event) override
{
mouse_ = event.position;
}
void on_event(app::mouse_button_event const & event) override
{
if (event.down && event.button == app::mouse_button::left)
{
if (selected_ && active_card_)
{
if (auto entity = map_.world.index<index>().find(*selected_))
{
if (*active_card_ == card_type::eraser)
{
clear_tile(map_, *selected_);
}
}
else
{
if (map_.take_card(*active_card_))
{
bool built = false;
switch (*active_card_)
{
case card_type::mixer:
case card_type::hue_shifter:
case card_type::reshaper:
map_.world.create(
vertex{*selected_},
transformer{card_to_transformer(*active_card_).value()}
);
built = true;
break;
case card_type::crossing:
map_.world.create(
vertex{*selected_},
crossing{}
);
built = true;
break;
case card_type::zoomer:
{
map_.world.create(
vertex{*selected_},
zoomer{}
);
auto c = selected_->down();
sink_belt(map_.world, c.left());
sink_belt(map_.world, c.right());
sink_belt(map_.world, c.bottom());
sink_belt(map_.world, c.top());
built = true;
}
break;
case card_type::eraser:
break;
}
if (built)
active_card_ = std::nullopt;
if (built && tutorial_state_ <= 3)
tutorial_state_ = 4;
}
}
}
else if (selected_card_)
active_card_ = *selected_card_;
else if (selected_item_)
{
item_killing_spree_ = true;
if (tutorial_state_ <= 4)
tutorial_state_ = 5;
}
else if (selected_ && !belt_start_)
belt_start_ = *selected_;
}
if (!event.down && event.button == app::mouse_button::left)
{
item_killing_spree_ = false;
if (selected_ && belt_start_ && selected_->level == belt_start_->level)
{
auto d = selected_->coords - belt_start_->coords;
if (std::abs(d[0]) + std::abs(d[1]) == 1)
{
auto s = belt_start_->down();
location belt[4];
for (int i = 0; i <= 3; ++i)
belt[i] = s.moved(d * i);
sink(map_.world, belt[0], belt[1]);
sink(map_.world, belt[3], belt[2]);
auto & index = map_.world.index<path_index>();
for (int i = 0; i < 3; ++i)
{
auto p = belt[i];
auto q = belt[i + 1];
auto s = index.get(p);
auto t = index.get(q);
auto & sv = map_.world.get(s).get<path_vertex>();
if (sv.belts_to.contains(t))
{
remove_belt(map_.world, s, t);
if (tutorial_state_ <= 1)
tutorial_state_ = 2;
}
else
{
if (remove_belt(map_.world, t, s))
if (tutorial_state_ <= 1)
tutorial_state_ = 2;
add_belt(map_.world, s, t);
}
}
if (tutorial_state_ <= 0)
tutorial_state_ = 1;
}
}
belt_start_ = std::nullopt;
}
if (event.down && event.button == app::mouse_button::right)
active_card_ = std::nullopt;
}
void on_event(app::key_event const & event) override
{
if (event.down && event.key == app::keycode::F1)
{
for (auto type : card_type_values())
map_.cards[type] += 10;
}
if (event.down && event.key == app::keycode::NUM_1)
{
time_speed_ = 1.f;
}
if (event.down && event.key == app::keycode::NUM_2)
{
time_speed_ = 2.f;
}
if (event.down && event.key == app::keycode::NUM_3)
{
time_speed_ = 3.f;
}
}
bool running() const override
{
return running_;
}
void stop() override
{
running_ = false;
}
void update() override
{
float const dt = clock_.restart().count() * time_speed_;
map_.spawn_timer += 3.f * dt;
if (map_.spawn_timer >= 1.f)
{
map_.spawn_timer -= 1.f;
map_.world.apply<vertex const, source>(
[&](vertex const & v, source & s)
{
auto p = v.location.down();
auto t = map_.world.index<path_index>().get(p);
if (!map_.world.get(t).contains<occupied>())
{
if (!map_.world.get(t).get<path_vertex>().belts_to.empty())
s.animate += 1.f;
auto i = map_.world.create(
item{s.type, p}
);
map_.world.attach(t, occupied{i});
}
}
);
}
if (map_.stage + 1 < std::size(stages) && map_.resource_count >= stages[map_.stage].count)
{
for (auto card : stages[map_.stage].cards)
map_.cards[card] += 1;
for (auto type : stages[map_.stage].sources)
{
util::hash_set<location, location_hash> spots;
auto add = [&](location l)
{
if (!map_.world.index<index>().find(l))
spots.insert(l);
};
for (int i = 0; i < 3; ++i)
{
add({0, {-1, i}});
add({0, {3, i}});
add({0, {i, 3}});
}
if (!map_.cards.contains(card_type::zoomer))
{
spots.erase(location{0, {0, 3}});
spots.erase(location{0, {2, 3}});
}
if (!spots.empty())
{
map_.world.create(
vertex{random::uniform_from(map_rng_, spots)},
source{type}
);
}
}
map_.stage += 1;
map_.resource_count = 0;
if (map_.stage > 1 && tutorial_state_ <= 2)
tutorial_state_ = 3;
}
map_.world.apply<vertex const, transformer>([&](vertex const & v, transformer & t)
{
boost::container::flat_multimap<resource_type, location> has_inputs;
auto c = v.location.down();
auto ce = map_.world.index<path_index>().get(c);
if (map_.world.get(ce).contains<occupied>())
return;
for (auto n : neighbours)
{
auto p = c.moved(n);
if (auto ne = map_.world.index<path_index>().find(p))
if (map_.world.get(*ne).get<path_vertex>().belts_to.contains(ce))
if (auto occ = map_.world.get(*ne).get_if<occupied const>())
if (auto i = map_.world.get(occ->entity).get_if<item const>())
if (i->start == p)
has_inputs.insert({i->type, p});
}
bool crafted = false;
for (auto const & recipe : recipies.at(t.type))
{
boost::container::flat_set<resource_type> input_types;
for (auto type : recipe.inputs)
input_types.insert(type);
bool has_all = true;
for (auto type : input_types)
{
has_all &= (has_inputs.count(type) >= recipe.inputs.count(type));
}
if (!has_all)
continue;
for (auto type : input_types)
{
auto it = has_inputs.lower_bound(type);
for (int i = 0; i < recipe.inputs.count(type); ++i)
{
auto l = it->second;
auto e = map_.world.index<path_index>().get(l);
auto re = map_.world.get(e).get<occupied>().entity;
map_.world.detach<occupied>(e);
map_.world.destroy(re);
++it;
}
}
auto r = map_.world.create(
item{recipe.output, c}
);
map_.world.attach(ce, occupied{r});
crafted = true;
break;
}
if (crafted)
t.animate += 1.f;
if (!crafted && !has_inputs.empty())
{
return;
}
});
map_.world.apply<item>([&](ecs::handle entity, item & i)
{
if (i.target)
{
{
auto & v = map_.world.get(i.target).get<path_vertex const>();
if (v.belts_from.empty() && v.belts_to.empty())
{
map_.world.detach<occupied>(i.target);
map_.world.destroy(entity);
return;
}
}
i.state += 3.f * dt;
if (i.state < 1.f)
return;
i.state -= 1.f;
i.start = map_.world.get(i.target).get<path_vertex const>().location;
map_.world.detach<occupied>(i.target);
i.target = ecs::handle::null();
}
else
{
auto s = map_.world.index<path_index>().get(i.start);
map_.world.detach<occupied>(s);
auto & v = map_.world.get(s).get<path_vertex const>();
if (v.belts_to.empty() && v.belts_from.empty())
{
map_.world.destroy(entity);
return;
}
}
if (auto cell = map_.world.index<index>().find(i.start.up()))
{
if (auto l = map_.world.get(*cell).get_if<lab>())
{
if (i.type == stages[map_.stage].type)
{
map_.resource_count += 1;
l->animate += 1.f;
map_.world.destroy(entity);
}
else
{
l->animate_error += 1.f;
map_.world.destroy(entity);
}
return;
}
if (map_.world.get(*cell).contains<crossing>())
{
auto c = i.start.up().down();
auto d = c.coords - i.start.coords;
auto n = c.moved(d);
auto & index = map_.world.index<path_index>();
auto se = index.get(i.start);
auto ce = index.get(c);
auto ne = index.get(n);
if (map_.world.get(se).get<path_vertex>().belts_to.contains(ce))
{
if (map_.world.get(ce).get<path_vertex>().belts_to.contains(ne) && !map_.world.get(ne).contains<occupied>())
{
i.target = ne;
map_.world.attach(ne, occupied{entity});
}
else
{
map_.world.attach(se, occupied{entity});
}
return;
}
}
if (map_.world.get(*cell).get_if<transformer>())
{
auto se = map_.world.index<path_index>().get(i.start);
auto ce = map_.world.index<path_index>().get(i.start.up().down());
if (map_.world.get(se).get<path_vertex>().belts_to.contains(ce))
{
map_.world.attach(map_.world.index<path_index>().get(i.start), occupied{entity});
return;
}
}
}
std::vector<ecs::handle> targets;
for (auto b : map_.world.get(map_.world.index<path_index>().get(i.start)).get<path_vertex>().belts_to)
if (!map_.world.get(b).contains<occupied>())
targets.push_back(b);
if (!targets.empty())
i.target = random::uniform_from(item_rng_, targets);
if (i.target)
map_.world.attach(i.target, occupied{entity});
else
map_.world.attach(map_.world.index<path_index>().get(i.start), occupied{entity});
});
map_.world.apply<source>([&](source & s)
{
s.animate = std::max(0.f, s.animate - 3.f * dt);
});
map_.world.apply<transformer>([&](transformer & t)
{
t.animate = std::max(0.f, t.animate - 3.f * dt);
});
map_.world.apply<lab>([&](lab & l)
{
l.animate = std::max(0.f, l.animate - 3.f * dt);
l.animate_error = std::max(0.f, l.animate_error - 3.f * dt);
});
float aspect_ratio = (screen_size_[0] * 1.f) / screen_size_[1];
if (view_transition_)
{
auto from = view_transition_->old.bbox(1.f / 3.f);
auto to = view_stack_.back().bbox(1.f / 3.f);
float t = geom::smoothstep(view_transition_->timer * 2.f);
view_box_[0].min = geom::lerp(from[0].min, to[0].min, t);
view_box_[0].max = geom::lerp(from[0].max, to[0].max, t);
view_box_[1].min = geom::lerp(from[1].min, to[1].min, t);
view_box_[1].max = geom::lerp(from[1].max, to[1].max, t);
view_transition_->timer += dt;
if (view_transition_->timer >= 0.5f)
view_transition_ = std::nullopt;
}
else
{
view_box_ = view_stack_.back().bbox(1.f / 3.f);
}
view_box_[0] = geom::expand(view_box_[0], (view_box_[1].length() * aspect_ratio - view_box_[0].length()) / 2.f);
mouse_world_ = view_box_.corner(
mouse_[0] * 1.f / screen_size_[0],
1.f - mouse_[1] * 1.f / screen_size_[1]
);
selected_ = std::nullopt;
selected_item_ = std::nullopt;
if (!view_transition_)
{
if (geom::contains(geom::shrink(view_box_[0], (view_box_[0].length() - view_box_[1].length()) / 2.f), mouse_world_[0]))
map_.world.apply<item const>([&](ecs::handle entity, item const & i)
{
auto box = geom::expand(geom::box<float, 2>::singleton(position(map_.world, i)), scale(map_.world, i) / 9.f);
if (geom::contains(box, mouse_world_))
selected_item_ = entity;
});
if (selected_item_ && item_killing_spree_)
{
auto const & i = map_.world.get(*selected_item_).get<item const>();
if (i.target)
map_.world.detach<occupied>(i.target);
else
map_.world.detach<occupied>(map_.world.index<path_index>().get(i.start));
map_.world.destroy(*selected_item_);
selected_item_ = std::nullopt;
}
{
float s = std::pow(3.f, 1 + view_stack_.back().level);
auto m = mouse_world_;
m[0] *= s;
m[1] *= s;
auto l = view_stack_.back();
location p{1 + l.level, {std::floor(m[0]), std::floor(m[1])}};
geom::interval xrange{3 * l.coords[0], 3 * l.coords[0] + 2};
geom::interval yrange{3 * l.coords[1], 3 * l.coords[1] + 2};
auto xwrange = geom::expand(xrange, 1);
auto ywrange = geom::expand(yrange, 1);
if (geom::contains(xrange, p.coords[0]) && geom::contains(yrange, p.coords[1]))
selected_ = p;
else if (view_stack_.size() > 1 && within_grid(p) && ((geom::contains(xwrange, p.coords[0]) && geom::contains(yrange, p.coords[1])) || (geom::contains(xrange, p.coords[0]) && geom::contains(ywrange, p.coords[1]))))
{
selected_ = p;
while (selected_->level > 0)
{
if (auto entity = map_.world.index<index>().find(p.up()))
if (map_.world.get(*entity).contains<zoomer>())
break;
selected_ = std::nullopt;
break;
// TODO:
selected_ = selected_->up();
}
}
else if (!within_grid(p))
if (auto entity = map_.world.index<index>().find(p))
selected_ = p;
}
}
for (auto & p : card_animation_)
{
float const target = p.first == active_card_;
p.second += (target - p.second) * (- std::expm1(- 20.f * dt));
}
}
void present() override
{
gl::ClearColor(1.f, 1.f, 1.f, 1.f);
gl::Clear(gl::COLOR_BUFFER_BIT);
float view_level = view_stack_.back().level;
float pixel_size = view_box_[1].length() / screen_size_[1];
std::vector<std::string> helper_text;
if (view_transition_)
{
float t = geom::smoothstep(view_transition_->timer * 2.f);
view_level = geom::lerp<float>(view_transition_->old.level, view_stack_.back().level, t);
}
draw_grids(map_, view_level, painter_);
if (active_card_)
{
if (selected_)
{
if (auto entity = map_.world.index<index>().find(*selected_))
{
auto acc = map_.world.get(*entity);
if (*active_card_ != card_type::eraser || (!acc.contains<source>() && !acc.contains<lab>()))
{
gfx::color_rgba color = {255, 128, 128, 255};
draw_selection(selected_->bbox(), painter_, color, true);
}
}
else if (*active_card_ != card_type::eraser)
{
gfx::color_rgba color = {64, 64, 64, 255};
draw_selection(selected_->bbox(), painter_, color, true);
}
}
}
else
{
if (selected_)
{
gfx::color_rgba color = {128, 128, 128, 255};
if (belt_start_)
{
auto d = selected_->coords - belt_start_->coords;
if (selected_->level != belt_start_->level || std::abs(d[0]) + std::abs(d[1]) != 1)
color = {255, 128, 128, 255};
}
draw_selection(selected_->bbox(), painter_, color);
if (belt_start_)
{
auto d = selected_->coords - belt_start_->coords;
if (selected_->level == belt_start_->level && std::abs(d[0]) + std::abs(d[1]) == 1)
{
gfx::color_rgba c{224, 224, 224, 255};
location p0 = belt_start_->down();
location p1 = p0.moved(d);
sink(map_.world, p0, p1);
auto e0 = map_.world.index<path_index>().get(p0);
auto e1 = map_.world.index<path_index>().get(p1);
if (map_.world.get(e0).get<path_vertex>().belts_to.contains(e1))
c = {244, 160, 160, 255};
else if (map_.world.get(e0).get<path_vertex>().belts_from.contains(e1))
c = {244, 244, 160, 255};
float w = 0.25f * std::pow(3.f, 1.f - selected_->level) / 2.f;
painter_.line(selected_->center(), belt_start_->center(), w, c, true);
}
}
}
if (belt_start_)
draw_selection(belt_start_->bbox(), painter_, {64, 64, 64, 255});
}
draw(map_, painter_, pixel_size);
{
float w = (view_box_[0].length() - view_box_[1].length()) / 2.f;
geom::vector t{view_box_[1].length() / 5.f, 0.f};
geom::vector d{w, 0.f};
geom::vector n{w + t[0], 0.f};
auto p00 = view_box_.corner(0, 0);
auto p01 = view_box_.corner(0, 1);
auto p10 = view_box_.corner(1, 0);
auto p11 = view_box_.corner(1, 1);
float s = 0.f;
if (view_stack_.size() > 1)
{
if (view_transition_ && view_transition_->old.level == -1)
s = 2.f * view_transition_->timer;
else
s = 1.f;
}
else
{
if (view_transition_)
s = 1.f - 2.f * view_transition_->timer;
else
s = 0.f;
}
gfx::color_rgba c1{255, 255, 255, std::round(224.f * s)};
gfx::color_rgba c0{255, 255, 255, 0};
painter_.triangle(p00, p00 + d, p01 + d, c1, c1, c1);
painter_.triangle(p00, p01 + d, p01, c1, c1, c1);
painter_.triangle(p00 + d, p00 + n, p01 + n, c1, c0, c0);
painter_.triangle(p00 + d, p01 + n, p01 + d, c1, c0, c1);
painter_.triangle(p10, p10 - d, p11 - d, c1, c1, c1);
painter_.triangle(p10, p11 - d, p11, c1, c1, c1);
painter_.triangle(p10 - d, p10 - n, p11 - n, c1, c0, c0);
painter_.triangle(p10 - d, p11 - n, p11 - d, c1, c0, c1);
}
if (selected_item_)
{
auto const & i = map_.world.get(*selected_item_).get<item const>();
draw_item(i.type, position(map_.world, i), scale(map_.world, i), painter_, true);
}
std::optional<transformer_type> shown_recipies;
if (selected_card_ || active_card_)
{
if (auto type = card_to_transformer(selected_card_.value_or(*active_card_)))
shown_recipies = *type;
}
else if (selected_)
{
if (auto entity = map_.world.index<index>().find(*selected_))
if (auto t = map_.world.get(*entity).get_if<transformer>())
shown_recipies = t->type;
}
if (shown_recipies)
{
float const scale = std::pow(3.f, -1.f - view_level);
float const step = 1.5f * scale / 9.f;
geom::point<float, 2> pen = view_box_.corner(0, 1) + geom::vector{18.f, -120.f} * pixel_size + geom::vector{step, - 3.f * step} / 2.f;
for (auto const & recipe : recipies.at(*shown_recipies))
{
int i = 0;
for (auto type : recipe.inputs)
{
draw_item(type, pen + geom::vector{i * step, 0.f}, scale, painter_);
++i;
}
i = 3;
float s = 0.25f;
float t = 0.25f * std::sqrt(0.75f);
painter_.triangle(
pen + geom::vector{(i - t) * step, -s * step},
pen + geom::vector{(i - t) * step, s * step},
pen + geom::vector{(i + t) * step, 0.f},
{192, 192, 192, 255}
);
i = 4;
draw_item(recipe.output, pen + geom::vector{i * step, 0.f}, scale, painter_);
pen[1] -= step;
}
}
selected_card_ = std::nullopt;
{
float const step = 0.5f * std::pow(3.f, - 1.f - view_level);
float vs = 2.f * pixel_size;
geom::point<float, 2> pen = view_box_.corner(1, 1) - geom::vector{step, step} / 2.f;
for (auto type : card_type_values())
{
if (!map_.cards.contains(type))
continue;
geom::box<float, 2> box{{{pen[0] - step, pen[0]}, {pen[1] - step, pen[1]}}};
box[0] -= step * card_animation_[type] / 2.f;
if (active_card_ == type)
{
draw_selection(box, painter_, {64, 64, 64, 255}, true);
}
else if (geom::contains(box, mouse_world_))
{
selected_card_ = type;
draw_selection(box, painter_, {128, 128, 128, 255});
}
draw_card(box, type, painter_);
if (type != card_type::eraser)
{
painter_.text(box.center() - geom::vector{step, 0.f}, std::to_string(map_.cards.at(type)), {.scale = {vs, -vs}, .c = gfx::black});
}
pen[1] -= step;
}
}
if (selected_card_ || active_card_)
helper_text = card_description(selected_card_.value_or(*active_card_));
else if (selected_)
if (auto entity = map_.world.index<index>().find(*selected_))
{
auto acc = map_.world.get(*entity);
if (auto t = acc.get_if<transformer>())
helper_text = card_description(transformer_to_card(t->type));
else if (acc.contains<crossing>())
helper_text = card_description(card_type::crossing);
else if (acc.contains<zoomer>())
helper_text = card_description(card_type::zoomer);
else if (acc.contains<source>())
helper_text = {"Source", "Generates circles"};
else if (acc.contains<lab>())
helper_text = {"Consumer", "Consumes produced items"};
}
if (helper_text.empty())
{
if (tutorial_state_ == 0)
{
helper_text.push_back("Click and drag to add");
helper_text.push_back(" conveyor belts");
}
else if (tutorial_state_ == 1)
{
helper_text.push_back("Drag again to remove belts");
helper_text.push_back(" or switch direction");
}
else if (tutorial_state_ == 2)
{
helper_text.push_back("Deliver items from");
helper_text.push_back("source to consumer");
}
else if (tutorial_state_ == 3)
{
helper_text.push_back("Place a mixer to");
helper_text.push_back(" combine items");
}
else if (tutorial_state_ == 4)
{
helper_text.push_back("Click on items to");
helper_text.push_back(" remove clogging");
}
else if (tutorial_state_ == 5)
{
helper_text.push_back("Let's see how far you can get.");
helper_text.push_back(" Good luck!");
}
}
{
geom::point<float, 2> pen = view_box_.corner(0, 1) + geom::vector{18.f, -24.f} * pixel_size;
float s = 2.f * pixel_size;
for (int i = 0; i < helper_text.size(); ++i)
{
painter_.text(pen, helper_text[i], {.scale = {s, -s}, .x = gfx::painter::x_align::left, .y = gfx::painter::y_align::top, .c = gfx::black});
pen[1] -= 32.f * pixel_size;
}
}
painter_.render(geom::orthographic_camera{view_box_}.transform());
}
private:
bool running_ = true;
std::uint64_t seed_;
random::generator map_rng_;
random::generator item_rng_;
map map_;
util::clock<> clock_;
float time_speed_ = 1.f;
geom::vector<int, 2> screen_size_{1, 1};
geom::point<int, 2> mouse_{0, 0};
geom::point<float, 2> mouse_world_{0, 0};
bool item_killing_spree_ = false;
gfx::painter painter_;
struct view_transition
{
location old;
float timer = 0.f;
};
std::optional<view_transition> view_transition_;
std::vector<location> view_stack_;
geom::box<float, 2> view_box_;
std::optional<location> selected_;
std::optional<location> belt_start_;
std::optional<ecs::handle> selected_item_;
std::optional<card_type> selected_card_;
std::optional<card_type> active_card_;
util::hash_map<card_type, float> card_animation_;
int tutorial_state_ = 0;
};
}
namespace psemek::app
{
std::unique_ptr<application::factory> make_application_factory()
{
application::options options
{
.name = "GMTK 2024",
};
return default_application_factory<gmtk::application>(options);
}
}