#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include 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 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 inputs; resource_type output; }; static util::hash_map> 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 const neighbours[4] { {1, 0}, {0, 1}, {-1, 0}, {0, -1}, }; struct location { int level; geom::point coords; geom::point center() const { float s = std::pow(3.f, -level); return {(coords[0] + 0.5f) * s, (coords[1] + 0.5f) * s}; } geom::box 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 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 belts_to = {}; boost::container::flat_set belts_from = {}; }; void add_belt(ecs::container & world, ecs::handle from, ecs::handle to) { world.get(from).get().belts_to.insert(to); world.get(to).get().belts_from.insert(from); } bool remove_belt(ecs::container & world, ecs::handle from, ecs::handle to) { bool removed = world.get(from).get().belts_to.contains(to); world.get(from).get().belts_to.erase(to); world.get(to).get().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 position(ecs::container & world, item const & i) { if (i.target) { auto end = world.get(i.target).get().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().location; return 3.f * std::pow(3.f, -geom::lerp(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 { std::unique_ptr world; util::hash_map cards = {}; int stage = 0; float stage_animation = 0.f; bool victory = false; 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 cards; std::vector 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, 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::cyan, shape_type::square}, 30, {}, {}}, {{color_type::magenta, shape_type::square}, 30, {card_type::reshaper, card_type::reshaper, card_type::zoomer, card_type::mixer, card_type::mixer, card_type::mixer, card_type::mixer, card_type::zoomer, card_type::zoomer, card_type::crossing}, {{color_type::black, 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, {}, {}}, }; template struct index_base { static constexpr util::uuid uuid() { return UUID; } index_base(ecs::container & world) : world_(world) { world.apply([this](ecs::handle entity, Component const & v){ index_[v.location] = entity; }); world.constructor([this](ecs::handle entity, Component const & v){ index_[v.location] = entity; }); world.destructor([this](Component const & v){ index_.erase(v.location); }); } std::optional 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 index_; }; using index = index_base; using path_index = index_base; void sink_belt(ecs::container & world, location m) { if (auto entity = world.index().find(m.up()); !entity || !world.get(*entity).contains()) return; auto & index = world.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(); if (cv.belts_to.contains(me)) { auto & mv = world.get(me).get(); 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(); 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().find(m.up()); !entity || !world.get(*entity).contains()) 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().find(l); if (!entity) return; auto acc = map.world->get(*entity); if (acc.contains() || acc.contains()) return; if (acc.contains()) map.put_card(card_type::crossing); else if (acc.contains()) { map.put_card(card_type::zoomer); } else if (auto t = acc.get_if()) { map.put_card(transformer_to_card(t->type)); } auto & index = map.world->index(); 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); if (auto e = index.find(p)) { auto & v = map.world->get(*e).get(); for (auto to : v.belts_to) { auto & tv = map.world->get(to).get(); 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(); if (!within_tile(l, fv.location)) remove_belt(*map.world, *fv.belts_from.begin(), from); remove_belt(*map.world, from, *e); } } } } 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(); for (auto to : v.belts_to) { auto & tv = map.world->get(to).get(); 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(); if (!within_tile(l, fv.location)) remove_belt(*map.world, *fv.belts_from.begin(), from); remove_belt(*map.world, from, *e); } } } } map.world->destroy(*entity); } map start_menu_map() { map result; result.world = std::make_unique(); for (int y = 0; y < 3; ++y) for (int x = 0; x < 3; ++x) result.world->create( vertex{0, {x, y}}, zoomer{} ); result.world->create( vertex{0, {-1, 1}}, source{{color_type::black, shape_type::circle}} ); result.world->create( vertex{0, {3, 1}}, source{{color_type::black, shape_type::circle}} ); result.world->create( vertex{1, {1, 1}}, source{{color_type::red, shape_type::circle}} ); result.world->create( vertex{1, {7, 7}}, source{{color_type::yellow, shape_type::circle}} ); result.world->create( vertex{1, {1, 4}}, transformer{transformer_type::hue_shifter} ); result.world->create( vertex{1, {7, 4}}, transformer{transformer_type::hue_shifter} ); result.world->create( vertex{1, {1, 7}}, lab{} ); result.world->create( vertex{1, {7, 1}}, lab{} ); auto & index = result.world->index(); // Left black to hue shifter add_belt(*result.world, index.get({1, {-2, 4}}), index.get({1, {-1, 4}})); add_belt(*result.world, index.get({1, {-1, 4}}), index.get({2, {0, 13}})); add_belt(*result.world, index.get({2, {0, 13}}), index.get({2, {1, 13}})); add_belt(*result.world, index.get({2, {1, 13}}), index.get({2, {2, 13}})); add_belt(*result.world, index.get({2, {2, 13}}), index.get({2, {3, 13}})); add_belt(*result.world, index.get({2, {3, 13}}), index.get({2, {4, 13}})); // Bottom-left red to hue shifter to left lab add_belt(*result.world, index.get({1, {1, -2}}), index.get({1, {1, -1}})); add_belt(*result.world, index.get({1, {1, -1}}), index.get({2, {4, 0}})); add_belt(*result.world, index.get({2, {4, 0}}), index.get({2, {4, 1}})); for (int y = 1; y < 22; ++y) add_belt(*result.world, index.get({2, {4, y}}), index.get({2, {4, y + 1}})); add_belt(*result.world, index.get({1, {10, 4}}), index.get({1, {9, 4}})); add_belt(*result.world, index.get({1, {9, 4}}), index.get({2, {26, 13}})); add_belt(*result.world, index.get({2, {26, 13}}), index.get({2, {25, 13}})); for (int y = 4; y < 22; ++y) add_belt(*result.world, index.get({2, {22, y + 1}}), index.get({2, {22, y}})); for (int x = 22; x < 25; ++x) add_belt(*result.world, index.get({2, {x + 1, 13}}), index.get({2, {x, 13}})); return result; } map campaign_map(bool challenge) { map result; result.world = std::make_unique(); result.world->index(); result.world->index(); result.world->create( vertex{{0, {1, -1}}}, lab{} ); result.cards[card_type::eraser] = 1000000; if (!challenge) for (auto type : card_type_values()) result.cards[type] = 1000000; return result; } map sandbox_map() { map result; result.world = std::make_unique(); result.world->index(); result.world->index(); result.world->create( vertex{{0, {-1, 0}}}, source{{color_type::red, shape_type::circle}} ); result.world->create( vertex{{0, {-1, 1}}}, source{{color_type::green, shape_type::circle}} ); result.world->create( vertex{{0, {-1, 2}}}, source{{color_type::blue, shape_type::circle}} ); result.world->create( vertex{{0, {3, 0}}}, source{{color_type::cyan, shape_type::circle}} ); result.world->create( vertex{{0, {3, 1}}}, source{{color_type::magenta, shape_type::circle}} ); result.world->create( vertex{{0, {3, 2}}}, source{{color_type::yellow, shape_type::circle}} ); result.world->create( vertex{{0, {0, 3}}}, source{{color_type::black, shape_type::circle}} ); result.world->create( vertex{{0, {1, 3}}}, source{{color_type::gray, shape_type::circle}} ); result.world->create( vertex{{0, {2, 3}}}, source{{color_type::white, shape_type::circle}} ); result.world->create( vertex{{0, {1, -1}}}, lab{} ); for (auto type : card_type_values()) result.cards[type] = 1000000; return result; } void draw_grid(geom::box 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 const & pos, float scale, gfx::painter & painter, bool selected = false, int opacity = 255) { auto color = color_of(type); gfx::color_rgba bcolor = selected ? gfx::white : gfx::black; color[3] = opacity; bcolor[3] = opacity; 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::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 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 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 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 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) for (int k = 1; k < 2; ++k) { geom::point 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 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 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 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 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 & v, zoomer const &) { draw_grid(v.location.bbox(), view_level, painter); }); } void draw(map & map, gfx::painter & painter, float pixel_size, bool sandbox) { map.world->apply([&](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 & 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().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 & vertex) { for (auto b : vertex.belts_to) { auto q = map.world->get(b).get().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 & 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 & 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 & 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}); if (!sandbox) { for (int i = -1; i <= 0; ++i) { auto pen = v.location.bbox(-0.4f).corner(0.5f, 1.f); float vs = 2.f * pixel_size; int opacity = std::round(255 * (1.f - std::abs(i + map.stage_animation))); if (map.stage + i >= 1 && map.stage + i < std::size(stages)) draw_item(stages[map.stage + i].type, pen + geom::vector{(i + map.stage_animation) * 40.f * vs, 0.f}, 1.f, painter, false, opacity); if (i == 0) { 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 & i) { draw_item(i.type, position(*map.world, i), scale(*map.world, i), painter); }); } void draw_selection(geom::box 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 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 & context) : context_(context) , audio_(psemek::audio::make_engine()) { mixer_ = audio::make_mixer(); volume_ = audio::volume(mixer_); audio_->output()->stream(audio::compressor(volume_)); auto sounds_root = util::executable_path().parent_path() / "sounds"; click_low_ = audio::load_mp3(io::read_full(io::file_istream{sounds_root / "click_low.mp3"})); click_high_ = audio::load_mp3(io::read_full(io::file_istream{sounds_root / "click_high.mp3"})); key_click_ = audio::load_mp3(io::read_full(io::file_istream{sounds_root / "key_click.mp3"})); gears_ = audio::load_mp3(io::read_full(io::file_istream{sounds_root / "gears.mp3"})); machine_ = audio::load_mp3(io::read_full(io::file_istream{sounds_root / "machine.mp3"})); pop_ = audio::load_mp3(io::read_full(io::file_istream{sounds_root / "pop.mp3"})); error_ = audio::load_mp3(io::read_full(io::file_istream{sounds_root / "error.mp3"})); context_.windowed(is_windowed_); set_start_menu(); } void on_event(app::resize_event const & event) override { screen_size_ = event.size; } void on_event(app::mouse_wheel_event const & event) override { if (!in_menu()) { if (event.delta > 0) { if (selected_ && !view_transition_) { bool transitioned = false; if (auto entity = map_.world->index().find(*selected_)) if (map_.world->get(*entity).contains()) { 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_button_) { if (*selected_button_ < menu_buttons_.size() && menu_buttons_[*selected_button_].action) { button_click_sound(); menu_buttons_[*selected_button_].action(); } } else if (selected_ && active_card_) { if (auto entity = map_.world->index().find(*selected_)) { if (*active_card_ == card_type::eraser) { destroy_sound(); clear_tile(map_, *selected_); } else if (auto t = map_.world->get(*entity).get_if()) { if (auto n = card_to_transformer(*active_card_); n && n != t->type) { if (map_.take_card(*active_card_)) { built_sound(); map_.put_card(transformer_to_card(t->type)); t->type = *n; active_card_ = std::nullopt; } } } } 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; built_sound(); } if (built && tutorial_state_ <= 3) tutorial_state_ = 4; } } } else if (selected_card_) { active_card_ = *selected_card_; button_click_sound(); } else if (selected_item_) { item_killing_spree_ = true; if (tutorial_state_ <= 4) tutorial_state_ = 5; } else if (selected_ && !belt_start_) { belt_start_ = *selected_; button_mouseover_sound(); } } 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(); ecs::handle e[4]; for (int i = 0; i <= 3; ++i) e[i] = index.get(belt[i]); auto & sv = map_.world->get(e[0]).get(); if (sv.belts_to.contains(e[1])) { for (int i = 0; i < 3; ++i) remove_belt(*map_.world, e[i], e[i + 1]); remove_belt_sound(); } else if (sv.belts_from.contains(e[1])) { for (int i = 0; i < 3; ++i) remove_belt(*map_.world, e[i + 1], e[i]); remove_belt_sound(); } else { for (int i = 0; i < 3; ++i) add_belt(*map_.world, e[i], e[i + 1]); add_belt_sound(); } 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; } if (event.down && event.key == app::keycode::ESCAPE) { if (!in_start_menu_) { in_escape_menu_ ^= true; if (in_escape_menu_) set_escape_menu_buttons(); } } } bool running() const override { return running_; } void stop() override { running_ = false; } void update() override { float const frame_dt = clock_.restart().count(); float const dt = frame_dt * time_speed_ * (in_start_menu_ ? 1.f : (1.f - menu_transition_)); map_.spawn_timer += 3.f * dt; if (map_.spawn_timer >= 1.f) { map_.spawn_timer -= 1.f; map_.world->apply( [&](vertex const & v, source & s) { auto p = v.location.down(); auto t = map_.world->index().get(p); if (!map_.world->get(t).contains()) { if (!map_.world->get(t).get().belts_to.empty()) { s.animate += 1.f; item_produced_sound(); } auto i = map_.world->create( item{s.type, p} ); map_.world->attach(t, occupied{i}); } } ); } if (!in_menu() && !is_sandbox_mode_ && map_.resource_count >= stages[map_.stage].count) { if (map_.stage + 1 < std::size(stages)) { for (auto card : stages[map_.stage].cards) map_.cards[card] += 1; for (auto type : stages[map_.stage].sources) { util::hash_set spots; auto add = [&](location l) { if (!map_.world->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_.stage_animation += 1.f; map_.resource_count = 0; if (map_.stage > 1 && tutorial_state_ <= 2) tutorial_state_ = 3; } else if (!map_.victory) { map_.victory = true; in_victory_menu_ = true; set_victory_menu_buttons(); } } map_.world->apply([&](vertex const & v, transformer & t) { boost::container::flat_multimap has_inputs; auto c = v.location.down(); auto ce = map_.world->index().get(c); if (map_.world->get(ce).contains()) return; for (auto n : neighbours) { auto p = c.moved(n); if (auto ne = map_.world->index().find(p)) if (map_.world->get(*ne).get().belts_to.contains(ce)) if (auto occ = map_.world->get(*ne).get_if()) if (auto i = map_.world->get(occ->entity).get_if()) 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 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().get(l); auto re = map_.world->get(e).get().entity; map_.world->detach(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; switch (t.type) { case transformer_type::mixer: mixer_sound(); break; case transformer_type::hue_shifter: shifter_sound(); break; case transformer_type::reshaper: reshaper_sound(); break; } } if (!crafted && !has_inputs.empty()) { return; } }); map_.world->apply([&](ecs::handle entity, item & i) { if (i.target) { { auto & v = map_.world->get(i.target).get(); if (v.belts_from.empty() && v.belts_to.empty()) { map_.world->detach(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().location; map_.world->detach(i.target); i.target = ecs::handle::null(); } else { auto s = map_.world->index().get(i.start); map_.world->detach(s); auto & v = map_.world->get(s).get(); if (v.belts_to.empty() && v.belts_from.empty()) { map_.world->destroy(entity); return; } } if (auto cell = map_.world->index().find(i.start.up())) { if (auto l = map_.world->get(*cell).get_if()) { if (i.type == stages[map_.stage].type || is_sandbox_mode_) { map_.resource_count += 1; l->animate += 1.f; map_.world->destroy(entity); item_received_sound(); } else { item_error_sound(); l->animate_error += 1.f; map_.world->destroy(entity); } return; } if (map_.world->get(*cell).contains()) { auto c = i.start.up().down(); auto d = c.coords - i.start.coords; auto n = c.moved(d); auto & index = map_.world->index(); auto se = index.get(i.start); auto ce = index.get(c); auto ne = index.get(n); if (map_.world->get(se).get().belts_to.contains(ce)) { if (map_.world->get(ce).get().belts_to.contains(ne) && !map_.world->get(ne).contains()) { i.target = ne; map_.world->attach(ne, occupied{entity}); } else { map_.world->attach(se, occupied{entity}); } return; } } if (map_.world->get(*cell).get_if()) { auto se = map_.world->index().get(i.start); auto ce = map_.world->index().get(i.start.up().down()); if (map_.world->get(se).get().belts_to.contains(ce)) { map_.world->attach(map_.world->index().get(i.start), occupied{entity}); return; } } } std::vector targets; for (auto b : map_.world->get(map_.world->index().get(i.start)).get().belts_to) if (!map_.world->get(b).contains()) 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().get(i.start), occupied{entity}); }); map_.stage_animation = std::max(0.f, map_.stage_animation - 3.f * dt); map_.world->apply([&](source & s) { s.animate = std::max(0.f, s.animate - 3.f * dt); }); map_.world->apply([&](transformer & t) { t.animate = std::max(0.f, t.animate - 3.f * dt); }); map_.world->apply([&](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 += frame_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] ); auto old_selected = selected_; selected_ = std::nullopt; selected_item_ = std::nullopt; if (!in_menu() && !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([&](ecs::handle entity, item const & i) { auto box = geom::expand(geom::box::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(); if (i.target) map_.world->detach(i.target); else map_.world->detach(map_.world->index().get(i.start)); map_.world->destroy(*selected_item_); selected_item_ = std::nullopt; item_removed_sound(); } { 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().find(p.up())) if (map_.world->get(*entity).contains()) break; selected_ = std::nullopt; break; // TODO: selected_ = selected_->up(); } } else if (!within_grid(p)) if (auto entity = map_.world->index().find(p)) selected_ = p; if (belt_start_ && selected_ && old_selected != selected_) { button_mouseover_sound(); } } } for (auto & p : card_animation_) { float const target = p.first == active_card_; p.second += (target - p.second) * (- std::expm1(- 20.f * frame_dt)); } menu_transition_ += ((in_menu() ? 1.f : 0.f) - menu_transition_) * (- std::expm1(- 20.f * frame_dt)); for (int i = 0; i < menu_buttons_.size(); ++i) menu_buttons_[i].selected_state += ((selected_button_ == i ? 1.f : 0.f) - menu_buttons_[i].selected_state) * (- std::expm1(- 20.f * frame_dt)); } void present() override { gl::ClearColor(1.f, 1.f, 1.f, 1.f); gl::Clear(gl::COLOR_BUFFER_BIT); gl::Viewport(0, 0, screen_size_[0], screen_size_[1]); float view_level = view_stack_.back().level; float pixel_size = view_box_[1].length() / screen_size_[1]; std::vector helper_text; if (view_transition_) { float t = geom::smoothstep(view_transition_->timer * 2.f); view_level = geom::lerp(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().find(*selected_)) { auto n = card_to_transformer(*active_card_); auto acc = map_.world->get(*entity); auto t = acc.get_if(); if (n && t && n != t->type) { gfx::color_rgba color = {64, 64, 64, 255}; draw_selection(selected_->bbox(), painter_, color, true); } else if (*active_card_ != card_type::eraser || (!acc.contains() && !acc.contains())) { 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().get(p0); auto e1 = map_.world->index().get(p1); if (map_.world->get(e0).get().belts_to.contains(e1)) c = {244, 160, 160, 255}; else if (map_.world->get(e0).get().belts_from.contains(e1)) c = {244, 160, 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, is_sandbox_mode_); if (!in_menu()) { 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(); draw_item(i.type, position(*map_.world, i), scale(*map_.world, i), painter_, true); } std::optional 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().find(*selected_)) if (auto t = map_.world->get(*entity).get_if()) 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 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; } } auto old_selected_card = selected_card_; 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 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 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; if (old_selected_card != type) button_mouseover_sound(); draw_selection(box, painter_, {128, 128, 128, 255}); } draw_card(box, type, painter_); if (map_.cards.at(type) < 1000) 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().find(*selected_)) { auto acc = map_.world->get(*entity); if (auto t = acc.get_if()) helper_text = card_description(transformer_to_card(t->type)); else if (acc.contains()) helper_text = card_description(card_type::crossing); else if (acc.contains()) helper_text = card_description(card_type::zoomer); else if (acc.contains()) helper_text = {"Source", "Generates circles"}; else if (acc.contains()) helper_text = {"Consumer", "Consumes produced items"}; } if (!in_menu() && 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 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; } } if (!in_start_menu_) { geom::point pen = view_box_.corner(0, 0) + geom::vector{18.f, 24.f} * pixel_size; float s = 2.f * pixel_size; gfx::color_rgba c{127, 127, 127, 255}; gfx::painter::text_options opts { .scale = {s, -s}, .x = gfx::painter::x_align::left, .y = gfx::painter::y_align::bottom, .c = c }; painter_.text(pen + geom::vector{0.f, 32.f * pixel_size}, std::format("Level {}/{}", map_.stage, std::size(stages) - 1), opts); painter_.text(pen, is_sandbox_mode_ ? "Sandbox" : is_challenge_mode_ ? "Challenge" : "Campaign", opts); } auto old_selected_button = selected_button_; selected_button_ = std::nullopt; if (in_menu()) { painter_.rect(view_box_, gfx::to_coloru8(gfx::color_4f{1.f, 1.f, 1.f, menu_transition_ * 0.75f})); float button_width = pixel_size * 500.f; float button_height = pixel_size * 64.f; float button_spacing = pixel_size * 32.f; float total_height = button_height * menu_buttons_.size() + button_spacing * (menu_buttons_.size() - 1.f); auto pen = view_box_.center() + geom::vector{-button_width, total_height} / 2.f; for (int i = 0; i < menu_buttons_.size(); ++i) { geom::box box; box[0] = {pen[0], pen[0] + button_width}; box[1] = {pen[1] - button_height, pen[1]}; bool active = static_cast(menu_buttons_[i].action); gfx::color_rgba bg_color; gfx::color_rgba text_color; if (active) { box = geom::expand(box, menu_buttons_[i].selected_state * 8.f * pixel_size); if (geom::contains(box, mouse_world_)) { selected_button_ = i; if (old_selected_button != i) button_mouseover_sound(); } bg_color = gfx::lerp(gfx::color_rgba{192, 192, 192, 255}, gfx::white.as_color_rgba(), menu_buttons_[i].selected_state); text_color = gfx::lerp(gfx::black.as_color_rgba(), gfx::color_rgba{191, 96, 0, 255}, menu_buttons_[i].selected_state); } else { bg_color = {192, 224, 255, 255}; text_color = {0, 0, 0, 255}; } painter_.rect(geom::expand(box, pixel_size * 5.f), text_color); painter_.rect(box, bg_color); float s = pixel_size * 4.f; painter_.text(box.center(), menu_buttons_[i].text, {.scale = {s, -s}, .c = text_color}); pen[1] -= button_height; pen[1] -= button_spacing; } } painter_.render(geom::orthographic_camera{view_box_}.transform()); } private: context const & context_; bool is_windowed_ = false; std::unique_ptr audio_; audio::mixer_ptr mixer_; std::shared_ptr volume_; audio::track_ptr click_low_; audio::track_ptr click_high_; audio::track_ptr key_click_; audio::track_ptr gears_; audio::track_ptr machine_; audio::track_ptr pop_; audio::track_ptr error_; bool running_ = true; bool in_start_menu_ = true; bool in_escape_menu_ = false; bool in_victory_menu_ = false; float menu_transition_ = 1.f; struct button { std::string text; std::function action; float selected_state = 0.f; }; std::vector