commit eaf27df1a08016b020b791b00583d443ff6c3447 Author: lisyarus Date: Sat Aug 17 18:18:11 2024 +0300 Initial commit: basic mechanics diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..57846f9 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "psemek"] + path = psemek + url = git@bitbucket.org:lisyarus/psemek.git diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..11e195f --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,15 @@ +cmake_minimum_required(VERSION 3.16) +project(gmtk-2024) + +set(CMAKE_CXX_STANDARD 20) +set(PSEMEK_EXAMPLES OFF) + +set(PSEMEK_PACKAGE_OUTPUT_PATH package) +set(PSEMEK_GRAPHICS_API OPENGL) + +add_subdirectory(psemek) + +file(GLOB_RECURSE GMTK_SOURCES LIST_DIRECTORIES FALSE "${CMAKE_CURRENT_SOURCE_DIR}/source/*") +file(GLOB_RECURSE GMTK_HEADERS LIST_DIRECTORIES FALSE "${CMAKE_CURRENT_SOURCE_DIR}/include/*") + +psemek_add_application(gmtk-2024 ${GMTK_SOURCES} ${GMTK_HEADERS}) diff --git a/psemek b/psemek new file mode 160000 index 0000000..6368ca5 --- /dev/null +++ b/psemek @@ -0,0 +1 @@ +Subproject commit 6368ca5e680bbcae3d13df4e87ed25ef80728cd3 diff --git a/source/application.cpp b/source/application.cpp new file mode 100644 index 0000000..67d8dca --- /dev/null +++ b/source/application.cpp @@ -0,0 +1,627 @@ +#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, std::uint32_t, + (red) + (green) + (blue) + ) + + gfx::color_rgba to_color(color c) + { + switch (c) + { + case color::red: return {255, 127, 127, 255}; + case color::green: return {127, 255, 127, 255}; + case color::blue: return {127, 127, 255, 255}; + } + throw util::unknown_enum_value_exception{c}; + } + + struct tile; + + struct grid + { + util::array tiles; + util::array item_target; + + util::array>, 2> belts; + + static grid create(); + + static geom::point const indices[9]; + + static geom::vector const neighbours[4]; + }; + + geom::point const grid::indices[9] = + { + {0, 0}, + {1, 0}, + {2, 0}, + {0, 1}, + {1, 1}, + {2, 1}, + {0, 2}, + {1, 2}, + {2, 2}, + }; + + geom::vector const grid::neighbours[4] = + { + {1, 0}, + {0, 1}, + {-1, 0}, + {0, -1}, + }; + + struct empty{}; + + struct source + { + color type; + }; + + struct factory + { + color input; + color output; + }; + + struct zoomer + { + struct grid grid; + }; + + using tile_variant = std::variant; + + struct tile + : tile_variant + { + using tile_variant::variant; + }; + + grid grid::create() + { + grid result; + + result.tiles.resize({3, 3}); + result.belts.resize({3, 3}); + result.item_target.resize({9, 9}, false); + + return result; + } + + struct item + { + color type; + geom::point start; + geom::point target; + float pos; + }; + + geom::point item_to_cell(geom::point const & p) + { + return {geom::idiv(p[0], 3), geom::idiv(p[1], 3)}; + } + + geom::point cell_center_to_item(geom::point const & p) + { + return {p[0] * 3 + 1, p[1] * 3 + 1}; + } + + geom::point task_sink_to_item(geom::point p) + { + p[0] *= 3; + p[1] *= 3; + + if (p[0] == -3) + p += geom::vector{2, 1}; + else if (p[1] == -3) + p += geom::vector{1, 2}; + else if (p[0] == 9) + p += geom::vector{0, 1}; + else if (p[1] == 9) + p += geom::vector{1, 0}; + + return p; + } + + bool within_grid(geom::point const & p) + { + return p[0] >= 0 && p[0] < 3 && p[1] >= 0 && p[1] < 3; + } + + bool item_within_grid(geom::point const & p) + { + return p[0] >= 0 && p[0] < 9 && p[1] >= 0 && p[1] < 9; + } + + struct timestamp + { + int trunc = 0; + float frac = 0.f; + + timestamp & operator += (float dt) + { + frac += dt; + int t = std::floor(frac); + trunc += t; + frac -= t; + return *this; + } + + friend auto operator <=> (timestamp const & x, timestamp const & y) = default; + }; + + struct task + { + color type; + + // In last 15 seconds + std::deque received = {}; + + static constexpr int freq = 3; + }; + + struct map + { + struct grid grid; + + util::hash_map, task> tasks; + + std::vector items; + + timestamp time = {}; + + float spawn_timer = 0.f; + }; + + void generate_next_task(random::generator & rng, map & map) + { + color type; + + while (true) + { + type = random::uniform_from(rng, color_values()); + if (map.tasks.size() >= 3) + break; + + bool good = true; + for (auto const & t : map.tasks) + if (t.second.type == type) + good = false; + + if (good) + break; + } + + while (true) + { + int x = random::uniform(rng, 0, 2); + int y = random::uniform(rng) ? -1 : 3; + if (random::uniform(rng)) + std::swap(x, y); + if (map.tasks.contains({x, y})) + continue; + + map.tasks[{x, y}] = {type}; + break; + } + } + + map starting_map(random::generator & rng) + { + map result; + + result.grid = grid::create(); + + generate_next_task(rng, result); + + return result; + } + + void draw(map const & map, gfx::painter & painter) + { + float const grid_width = 0.1f; + geom::box source_box{{{0.2f, 0.8f}, {0.2f, 0.8f}}}; + + for (int x = 0; x <= 3; ++x) + { + painter.line({x, 0.f}, {x, 3.f}, grid_width, gfx::black, true); + painter.line({0.f, x}, {3.f, x}, grid_width, gfx::black, true); + } + + auto & grid = map.grid; + + for (auto p : grid::indices) + for (auto q : grid.belts(p)) + painter.line(geom::cast(p) + geom::vector{0.5f, 0.5f}, geom::cast(q) + geom::vector{0.5f, 0.5f}, 0.3f, {191, 191, 191, 255}, true); + + for (auto p : grid::indices) + { + for (auto q : grid.belts(p)) + { + for (int i = 0; i < 3; ++i) + { + geom::vector d = geom::cast(q - p); + + auto c = geom::cast(p) + geom::vector{0.5f, 0.5f} + d * (i / 3.f); + + auto n = geom::ort(d); + + c += d / 6.f; + + float s = 1.f / 24.f; + + d *= s; + n *= s; + + painter.triangle(c - d + n, c - d - n, c + d, {255, 127, 0, 255}); + } + } + } + + for (auto p : grid::indices) + { + auto const & cell = grid.tiles(p); + + if (auto source = std::get_if(&cell)) + { + painter.rect(source_box + geom::vector{p[0] * 1.f, p[1] * 1.f}, to_color(source->type)); + painter.text({p[0] + 0.5f, p[1] + 0.5f}, "180/m", {.scale = {0.01f, -0.01f}, .c = {0, 0, 0, 255}}); + } + else if (auto factory = std::get_if(&cell)) + { + auto box = source_box + geom::vector{p[0] * 1.f, p[1] * 1.f}; + painter.triangle(box.corner(0, 0), box.corner(1, 1), box.corner(0, 1), to_color(factory->input)); + painter.triangle(box.corner(0, 0), box.corner(1, 0), box.corner(1, 1), to_color(factory->output)); + } + } + + for (auto const & task : map.tasks) + { + painter.rect(source_box + geom::vector{task.first[0] * 1.f, task.first[1] * 1.f}, to_color(task.second.type)); + painter.text(geom::point{task.first[0] + 0.5f, task.first[1] + 0.5f}, std::format("{}/m", task.second.received.size() * task::freq), {.scale = {0.01f, -0.01f}, .c = {0, 0, 0, 255}}); + } + + for (auto const & item : map.items) + { + auto pos = geom::lerp(geom::cast(item.start), geom::cast(item.target), item.pos) + geom::vector{0.5f, 0.5f}; + pos[0] /= 3.f; + pos[1] /= 3.f; + painter.circle(pos, 0.075f, {0, 0, 0, 255}); + painter.circle(pos, 0.05f, to_color(item.type)); + } + } + + void draw_selection(geom::point const & p, gfx::painter & painter) + { + painter.line({p[0], p[1]}, {p[0] + 1.f, p[1]}, 0.1f, {255, 0, 255, 255}, true); + painter.line({p[0] + 1.f, p[1]}, {p[0] + 1.f, p[1] + 1.f}, 0.1f, {255, 0, 255, 255}, true); + painter.line({p[0] + 1.f, p[1] + 1.f}, {p[0], p[1] + 1.f}, 0.1f, {255, 0, 255, 255}, true); + painter.line({p[0], p[1] + 1.f}, {p[0], p[1]}, 0.1f, {255, 0, 255, 255}, true); + } + + struct application + : app::application + { + application(options const &, context const &) + : rng_{random::device{}} + , map_(starting_map(rng_)) + {} + + void on_event(app::resize_event const & event) override + { + screen_size_ = event.size; + } + + void on_event(app::mouse_move_event const & event) override + { + mouse_ = event.position; + } + + void on_event(app::key_event const & event) override + { + if (event.down && event.key == app::keycode::S) + { + if (selected_ && std::holds_alternative(map_.grid.tiles(*selected_))) + { + util::hash_set types; + if (map_.tasks.size() == 1) + types.insert(map_.tasks.begin()->second.type); + else + for (auto t : color_values()) + types.insert(t); + map_.grid.tiles(*selected_) = source{random::uniform_from(rng_, types)}; + } + } + + if (event.down && event.key == app::keycode::B) + { + if (selected_) + { + if (belt_start_) + { + auto d = *selected_ - *belt_start_; + if (std::abs(d[0]) + std::abs(d[1]) == 1) + { + if (within_grid(*selected_)) + map_.grid.belts(*selected_).erase(*belt_start_); + + if (map_.grid.belts(*belt_start_).contains(*selected_)) + map_.grid.belts(*belt_start_).erase(*selected_); + else + map_.grid.belts(*belt_start_).insert(*selected_); + } + belt_start_ = std::nullopt; + } + else if (within_grid(*selected_)) + belt_start_ = *selected_; + } + } + + if (event.down && event.key == app::keycode::F) + { + if (selected_ && std::holds_alternative(map_.grid.tiles(*selected_))) + { + util::hash_set types; + for (auto const & task : map_.tasks) + types.insert(task.second.type); + + if (types.size() == 1) + { + for (auto c : color_values()) + types.insert(c); + } + + color input = random::uniform_from(rng_, types); + types.erase(input); + color output = random::uniform_from(rng_, types); + + map_.grid.tiles(*selected_) = gmtk::factory{input, output}; + } + } + + if (event.down && event.key == app::keycode::X) + { + if (selected_) + { + map_.grid.tiles(*selected_) = empty{}; + } + } + + if (event.down && event.key == app::keycode::SPACE) + { + generate_next_task(rng_, map_); + } + } + + bool running() const override + { + return running_; + } + + void stop() override + { + running_ = false; + } + + void update() override + { + float const dt = clock_.restart().count(); + + map_.time += dt; + + map_.spawn_timer += 3.f * dt; + if (map_.spawn_timer >= 1.f) + { + map_.spawn_timer -= 1.f; + + for (auto p : grid::indices) + { + if (auto source = std::get_if(&map_.grid.tiles(p))) + { + auto pos = cell_center_to_item(p); + + if (!map_.grid.item_target(pos)) + { + auto & item = map_.items.emplace_back(); + item.type = source->type; + item.start = pos; + item.target = pos; + map_.grid.item_target(pos) = true; + } + } + } + } + + for (auto & task : map_.tasks) + { + auto & received = task.second.received; + auto threshold = map_.time; + threshold.trunc -= 60 / task::freq; + while (!received.empty() && received.front() < threshold) + received.pop_front(); + } + + std::vector alive_items; + for (auto item : map_.items) + { + if (item.start != item.target) + { + item.pos += 3.f * dt; + + if (item.pos < 1.f) + { + alive_items.push_back(item); + continue; + } + + item.pos -= 1.f; + } + + if (item_within_grid(item.target)) + map_.grid.item_target(item.target) = false; + + auto cell = item_to_cell(item.target); + + if (map_.tasks.contains(cell)) + { + if (map_.tasks.at(cell).type == item.type) + map_.tasks.at(cell).received.push_back(map_.time); + continue; + } + + if (item_within_grid(item.target) && item.target == cell_center_to_item(cell)) + { + if (auto factory = std::get_if(&map_.grid.tiles(cell))) + { + if (factory->input == item.type) + item.type = factory->output; + } + } + + std::vector> targets; + + if (geom::imod(item.target[0], 3) == 1 && geom::imod(item.target[1], 3) == 1) + { + for (auto q : map_.grid.belts(cell)) + { + auto t = item.target + (q - cell); + if (!item_within_grid(t) || !map_.grid.item_target(t)) + targets.push_back(t); + } + } + else + { + auto s = item.target; + if (geom::imod(s[0], 3) == 0) + s[0] -= 1; + else if (geom::imod(s[0], 3) == 2) + s[0] += 1; + else if (geom::imod(s[1], 3) == 0) + s[1] -= 1; + else if (geom::imod(s[1], 3) == 2) + s[1] += 1; + + if (!item_within_grid(s) || !map_.grid.item_target(s)) + if (within_grid(item_to_cell(item.target)) && map_.grid.belts(item_to_cell(item.target)).contains(item_to_cell(s))) + targets.push_back(s); + + auto t = item.target - (s - item.target); + + if (!item_within_grid(t) || !map_.grid.item_target(t)) + if (within_grid(item_to_cell(s)) && map_.grid.belts(item_to_cell(s)).contains(item_to_cell(item.target))) + targets.push_back(t); + } + + item.start = item.target; + + if (!targets.empty()) + item.target = random::uniform_from(rng_, targets); + + if (item_within_grid(item.target)) + map_.grid.item_target(item.target) = true; + alive_items.push_back(item); + } + map_.items = std::move(alive_items); + + float aspect_ratio = (screen_size_[0] * 1.f) / screen_size_[1]; + + view_box_[1] = {-1.f, 4.f}; + view_box_[0] = view_box_[1]; + view_box_[0] = geom::expand(view_box_[0], (view_box_[1].length() * aspect_ratio - view_box_[0].length()) / 2.f); + + selected_ = std::nullopt; + + { + auto m = screen_to_grid(geom::cast(mouse_)); + + int x = std::floor(m[0]); + int y = std::floor(m[1]); + + if (x >= 0 && x < 3 && y >= 0 && y < 3) + selected_ = geom::point{x, y}; + else if (map_.tasks.contains({x, y})) + selected_ = geom::point{x, y}; + } + } + + void present() override + { + gl::ClearColor(1.f, 1.f, 1.f, 1.f); + gl::Clear(gl::COLOR_BUFFER_BIT); + + draw(map_, painter_); + + if (selected_) + draw_selection(*selected_, painter_); + + painter_.render(geom::orthographic_camera{view_box_}.transform()); + } + + private: + bool running_ = true; + + random::generator rng_; + map map_; + + util::clock<> clock_; + + geom::vector screen_size_{1, 1}; + geom::point mouse_{0, 0}; + + gfx::painter painter_; + + geom::box view_box_; + + std::optional> selected_; + std::optional> belt_start_; + + geom::point screen_to_grid(geom::point const & p) + { + return view_box_.corner( + p[0] / screen_size_[0], + 1.f - p[1] / screen_size_[1] + ); + } + }; + +} + +namespace psemek::app +{ + + std::unique_ptr make_application_factory() + { + application::options options + { + .name = "GMTK 2024", + }; + + return default_application_factory(options); + } + +}