summaryrefslogtreecommitdiff
path: root/2026/games/just__jane/src
diff options
context:
space:
mode:
authorLLLL Colonq <llll@colonq>2026-08-18 00:08:28 -0400
committerLLLL Colonq <llll@colonq>2026-08-18 00:08:28 -0400
commit32a62cfcb925225359a5c90761b795f2f737ebd2 (patch)
tree95c2261964803b19f2d39cb35d8d514ed6e5a6f4 /2026/games/just__jane/src
parentde99c5f7636a952025ea13de02bafd25bcb6bdec (diff)
Re-populate games
Diffstat (limited to '2026/games/just__jane/src')
-rw-r--r--2026/games/just__jane/src/clock.cpp19
-rw-r--r--2026/games/just__jane/src/cut.cpp37
-rw-r--r--2026/games/just__jane/src/game_loop.cpp425
-rw-r--r--2026/games/just__jane/src/main.cpp17
-rw-r--r--2026/games/just__jane/src/score.cpp79
5 files changed, 577 insertions, 0 deletions
diff --git a/2026/games/just__jane/src/clock.cpp b/2026/games/just__jane/src/clock.cpp
new file mode 100644
index 0000000..fbe1dbb
--- /dev/null
+++ b/2026/games/just__jane/src/clock.cpp
@@ -0,0 +1,19 @@
+#include "raylib.h"
+#include <algorithm>
+#include <clock.hpp>
+
+void Clock::Update() { current_frame_ = std::max(0.0f, (float)current_frame_ - 1); }
+void Clock::Draw() {
+
+ DrawCircle(center_position_.x, center_position_.y, radius_, {0x3c, 0x3a, 0x3a, 0xFF});
+ DrawCircleSector(
+ center_position_,
+ radius_ - 2,
+ -90,
+ -90 + (-360 * (current_frame_ / (float)frame_total_)),
+ 36,
+ {0xDE, 0xAD, 0xFF, 0xFF});
+}
+
+bool Clock::TimeIsUp() { return current_frame_ <= 0; }
+void Clock::Reset() { current_frame_ = frame_total_; }
diff --git a/2026/games/just__jane/src/cut.cpp b/2026/games/just__jane/src/cut.cpp
new file mode 100644
index 0000000..67e4c25
--- /dev/null
+++ b/2026/games/just__jane/src/cut.cpp
@@ -0,0 +1,37 @@
+#include "raylib.h"
+#include "raymath.h"
+#include <cut.hpp>
+
+Cut::Cut(Vector2 start, Vector2 end) {
+ InitAudioDevice();
+ this->start_ = start;
+ this->end_ = end;
+ this->current_step_ = 0.0;
+ this->slash_ = LoadSound("assets/slash.wav");
+}
+
+void Cut::Draw() {
+ if (this->its_joever) {
+ return;
+ }
+
+ Vector2 head = Vector2Lerp(start_, end_, current_step_);
+ DrawLineV(start_, head, {0x0F, 0x0F, 0x0F, 0x40});
+}
+
+void Cut::Update() {
+ if (this->its_joever) {
+ return;
+ }
+
+ if (this->current_step_ == 0.0) {
+ PlaySound(slash_);
+ }
+
+ if (this->current_step_ >= 1.0) {
+ this->its_joever = true;
+ return;
+ }
+
+ this->current_step_ += (1.0 / 60.0);
+}
diff --git a/2026/games/just__jane/src/game_loop.cpp b/2026/games/just__jane/src/game_loop.cpp
new file mode 100644
index 0000000..e95fc26
--- /dev/null
+++ b/2026/games/just__jane/src/game_loop.cpp
@@ -0,0 +1,425 @@
+#include "raylib.h"
+#include <format>
+#include <random>
+#include <raymath.h>
+
+#include <emscripten/em_types.h>
+#include <emscripten/emscripten.h>
+#include <emscripten/html5.h>
+#include <optional>
+
+#include <clock.hpp>
+#include <cut.hpp>
+#include <game_loop.hpp>
+#include <petal.hpp>
+#include <score.hpp>
+#include <sstream>
+
+static std::uniform_real_distribution<float> rng_ = std::uniform_real_distribution<float>();
+std::mt19937 generator_;
+
+static bool initialized;
+static bool game_over = false;
+static int score;
+static int score_to_win;
+
+static std::optional<Cut> the_cut;
+static std::optional<Vector2> initial_point;
+static std::optional<Vector2> terminal_point;
+
+static bool mouse_is_down;
+static bool mouse_is_up;
+
+static Texture2D bg_texture_;
+static Texture2D score_petal_texture;
+static Texture2D ronin_textures;
+
+static RenderTexture2D draw_target;
+static std::vector<Petal> petals;
+static std::vector<Petal> tutorial_petals;
+
+static bool tutorial_is_running = true;
+static long frame_counter;
+static int ronin_anim_frame = 0;
+static Vector2 ronin_position = {-40, GAME_HEIGHT - 30};
+constexpr Rectangle RONIN_FRAME = {0, 0, 44, 34};
+
+static std::optional<bool> game_started;
+static int TUTORIAL_END_FRAME = 90;
+static int TUTORIAL_MOVE_START = 10;
+static int TUTORIAL_CUT_FRAME = 50;
+constexpr int TOTAL_GAME_TIME = 400;
+static Clock game_clock(10, Vector2{GAME_WIDTH - 11, GAME_HEIGHT - 11}, TOTAL_GAME_TIME);
+
+static double difficulty = 0.0;
+
+static std::vector<ScorePetal> score_petals;
+
+void RenderFreezeFrames() {
+ BeginTextureMode(draw_target);
+ if (the_cut) {
+ the_cut->Draw();
+ }
+ EndTextureMode();
+}
+
+void RenderDrawTarget() {
+ BeginTextureMode(draw_target);
+ Rectangle source = Rectangle{
+ .x = 0,
+ .y = 0,
+ .width = (float)bg_texture_.width,
+ .height = (float)bg_texture_.height,
+ };
+
+ Rectangle dest = Rectangle{
+ .x = 0,
+ .y = 0,
+ .width = GAME_WIDTH,
+ .height = GAME_HEIGHT,
+ };
+
+ DrawTexturePro(bg_texture_, source, dest, {0, 0}, 0, WHITE);
+
+ for (Petal petal : petals) {
+ petal.Draw();
+ }
+
+ if (the_cut) {
+ the_cut->Draw();
+ }
+
+ game_clock.Draw();
+ EndTextureMode();
+}
+
+EM_BOOL OnGlobalMouse(int event_type, const EmscriptenMouseEvent *mouse_event, void *user_data) {
+ if (event_type == EMSCRIPTEN_EVENT_MOUSEDOWN) {
+ mouse_is_down = true;
+ } else if (event_type == EMSCRIPTEN_EVENT_MOUSEUP) {
+ mouse_is_down = false;
+ mouse_is_up = true;
+ }
+
+ return EM_FALSE;
+}
+
+void StartNewGame(bool with_tutorial = true) {
+ game_clock.Reset();
+ petals.clear();
+ tutorial_petals.clear();
+ score_petals.clear();
+
+ Rectangle window = Rectangle{.x = 0, .y = 0, .width = GAME_WIDTH, .height = GAME_HEIGHT};
+ tutorial_petals.push_back(Petal(window, Vector2{10, -10}, 0));
+ tutorial_petals.push_back(Petal(window, Vector2{50, -1}, 0));
+ tutorial_petals.push_back(Petal(window, Vector2{70, -5}, 2));
+ tutorial_petals.push_back(Petal(window, Vector2{98, 10}, 0, 0.9));
+ tutorial_petals.push_back(Petal(window, Vector2{14, 7}, 1));
+ tutorial_petals.push_back(Petal(window, Vector2{37, 5}, 1, 0.2));
+ tutorial_petals.push_back(Petal(window, Vector2{19, 13}, 0));
+ tutorial_petals.push_back(Petal(window, Vector2{39, 19}, 0));
+ tutorial_petals.push_back(Petal(window, Vector2{65, 0}, 2, 0.8));
+ tutorial_petals.push_back(Petal(window, Vector2{104, 5}, 1));
+ tutorial_petals.push_back(Petal(window, Vector2{12, 4}, 0, 0.4));
+
+ game_over = false;
+ the_cut = std::nullopt;
+ initial_point = std::nullopt;
+ terminal_point = std::nullopt;
+ score = 0;
+ tutorial_is_running = with_tutorial;
+ frame_counter = 0;
+ ronin_anim_frame = 0;
+ ronin_position = {-40, GAME_HEIGHT - 44};
+ game_started = true;
+
+ std::random_device rd;
+ generator_ = std::mt19937(rd());
+}
+
+void InitializeAssets() {
+ score_petal_texture = LoadTexture("assets/petals/cut_petal.png");
+ bg_texture_ = LoadTexture("assets/background.png");
+ ronin_textures = LoadTexture("assets/ronin.png");
+
+ draw_target = LoadRenderTexture(GAME_WIDTH, GAME_HEIGHT);
+
+ emscripten_set_mousedown_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, nullptr, EM_TRUE, OnGlobalMouse);
+ emscripten_set_mouseup_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, nullptr, EM_TRUE, OnGlobalMouse);
+
+ initialized = true;
+}
+
+void Draw() {
+ ClearBackground(RAYWHITE);
+
+ if (the_cut.has_value() && !the_cut.value().its_joever) {
+ RenderFreezeFrames();
+ } else {
+ RenderDrawTarget();
+ }
+
+ Rectangle target_src{
+ .x = 0,
+ .y = 0,
+ .width = GAME_WIDTH,
+ .height = -GAME_HEIGHT,
+ };
+
+ Rectangle target_dest{
+ .x = 0,
+ .y = 0,
+ .width = GAME_WIDTH * SCALE,
+ .height = GAME_HEIGHT * SCALE,
+ };
+
+ DrawTexturePro(draw_target.texture, target_src, target_dest, {0, 0}, 0, WHITE);
+}
+
+void Update() {
+ if (!game_started) {
+ game_started = true;
+ }
+
+ if (!game_over) {
+ game_clock.Update();
+ if (game_clock.TimeIsUp() && !the_cut.has_value()) {
+ the_cut = Cut(Vector2{0, 0}, Vector2{0, 0});
+ game_over = true;
+ }
+
+ if (!game_over && the_cut.has_value() && !the_cut.value().its_joever) {
+ the_cut->Update();
+ return;
+ }
+
+ if (!game_over && the_cut.has_value() && the_cut->its_joever) {
+ for (Petal &petal : petals) {
+ petal.Cut(*initial_point, *terminal_point);
+ if (petal.IsCut) {
+ score += 1;
+ score_petals.push_back(
+ ScorePetal{
+ .Position{.x = 20 + (float)score * (score_petal_texture.width + 3), .y = GAME_HEIGHT - 5},
+ .Texture = score_petal_texture});
+ }
+ }
+ post_score(score);
+ game_over = true;
+ }
+
+ if (!initial_point.has_value() && mouse_is_down) {
+ initial_point = Vector2Scale(GetMousePosition(), 1.0f / SCALE);
+ }
+
+ if (!terminal_point.has_value() && initial_point.has_value() && mouse_is_up) {
+ terminal_point = Vector2Scale(GetMousePosition(), 1.0f / SCALE);
+ }
+
+ if (!the_cut.has_value() && initial_point.has_value() && terminal_point.has_value()) {
+ Vector2 direction = Vector2Normalize(Vector2Subtract(terminal_point.value(), initial_point.value()));
+ terminal_point = Vector2Add(initial_point.value(), Vector2Scale(direction, 70.0f));
+ the_cut = Cut(*initial_point, *terminal_point);
+ }
+ }
+
+ for (Petal &petal : petals) {
+ petal.Update();
+ }
+
+ if (petals.size() < 20 && rng_(generator_) > .90) {
+ std::random_device rd;
+ Rectangle window = Rectangle{.x = 0, .y = 0, .width = GAME_WIDTH, .height = GAME_HEIGHT};
+ petals.push_back(Petal(window, rd()));
+ }
+
+ petals.erase(
+ std::remove_if(petals.begin(), petals.end(), [](Petal petal) { return petal.GetPosition().y > GAME_HEIGHT; }),
+ petals.end());
+}
+
+void UpdateTutorial() {
+ for (auto &blossom : tutorial_petals) {
+ blossom.Update();
+ }
+
+ frame_counter += 1;
+ if (frame_counter % 2 != 0) {
+ return;
+ }
+
+ if (frame_counter > TUTORIAL_END_FRAME) {
+ tutorial_is_running = false;
+ }
+
+ if (frame_counter > TUTORIAL_MOVE_START) {
+ if (ronin_position.x < 10) {
+ ronin_position.x += 2;
+ return;
+ }
+
+ if (ronin_anim_frame < 5) {
+ ronin_anim_frame += 1;
+ return;
+ }
+ }
+
+ if (tutorial_petals.size() > 0 && !tutorial_petals[0].IsCut && frame_counter > TUTORIAL_CUT_FRAME) {
+ for (auto &blossom : tutorial_petals) {
+ blossom.Cut();
+ }
+ }
+}
+
+void RenderTutorialDrawTarget() {
+ BeginTextureMode(draw_target);
+ Rectangle source = Rectangle{
+ .x = 0,
+ .y = 0,
+ .width = (float)bg_texture_.width,
+ .height = (float)bg_texture_.height,
+ };
+
+ Rectangle dest = Rectangle{
+ .x = 0,
+ .y = 0,
+ .width = GAME_WIDTH,
+ .height = GAME_HEIGHT,
+ };
+
+ DrawTexturePro(bg_texture_, source, dest, {0, 0}, 0, WHITE);
+
+ Rectangle src = Rectangle{
+ .x = RONIN_FRAME.width * ronin_anim_frame,
+ .y = 0,
+ .width = RONIN_FRAME.width,
+ .height = RONIN_FRAME.height};
+
+ Rectangle ronin_dest =
+ Rectangle{.x = ronin_position.x, .y = ronin_position.y, .width = RONIN_FRAME.width, .height = RONIN_FRAME.height};
+
+ DrawTexturePro(ronin_textures, src, ronin_dest, {0, 0}, 0, WHITE);
+
+ for (auto blossom : tutorial_petals) {
+ blossom.Draw();
+ }
+
+ EndTextureMode();
+}
+
+void DrawTutorial() {
+ ClearBackground(RAYWHITE);
+
+ RenderTutorialDrawTarget();
+
+ Rectangle target_src{
+ .x = 0,
+ .y = 0,
+ .width = GAME_WIDTH,
+ .height = -GAME_HEIGHT,
+ };
+
+ Rectangle target_dest{
+ .x = 0,
+ .y = 0,
+ .width = GAME_WIDTH * SCALE,
+ .height = GAME_HEIGHT * SCALE,
+ };
+
+ Color c = WHITE;
+ DrawTexturePro(draw_target.texture, target_src, target_dest, {0, 0}, 0, c);
+}
+
+static bool user_started_game = false;
+void UpdateStart() {
+ if (IsMouseButtonReleased(MOUSE_BUTTON_LEFT)) {
+ InitAudioDevice();
+ SetAudioStreamBufferSizeDefault(4096);
+ user_started_game = true;
+ }
+}
+
+void DrawStart() {
+ ClearBackground(WHITE);
+ DrawText("click to start", GAME_WIDTH / 2 - 20, GAME_HEIGHT / 2 - 5, 20, BLACK);
+}
+
+void UpdateGameOver() {
+ static int counter = 0;
+ Update();
+ if (counter++ > 120) {
+ counter = 0;
+ if (score > difficulty) {
+ emscripten_run_script("window.parent.postMessage({op: \"done\", win: true});");
+ } else {
+ emscripten_run_script("window.parent.postMessage({op: \"done\", win: false});");
+ }
+ initialized = false;
+ StartNewGame(false);
+ }
+}
+
+void DrawGameOver() {
+ ClearBackground(WHITE);
+ Draw();
+
+ // std::string text = std::format("SCORE: {}", score);
+ // DrawText(text.c_str(), GAME_WIDTH / 2 - 20, GAME_HEIGHT / 2 - 5, 20, BLACK);
+ for (auto petal : score_petals) {
+ Vector2 position = {petal.Position.x * SCALE, petal.Position.y * SCALE};
+ DrawTextureEx(petal.Texture, position, 0, SCALE, WHITE);
+ }
+}
+
+void UpdateDrawFrame() {
+ if (!initialized) {
+ difficulty = EM_ASM_DOUBLE({ return window.lcolonqJamStart || -1.0; });
+ if (difficulty < 0.0) {
+ return;
+ }
+
+ if (difficulty < 3) {
+ score_to_win = 2;
+ } else if (difficulty < 10) {
+ score_to_win = 3;
+ } else if (difficulty < 20) {
+ score_to_win = 5;
+ } else {
+ score_to_win = 8;
+ }
+
+ std::stringstream ss;
+ ss << "window.parent.postMessage({op: \"started\", verb: \"cut" << score_to_win;
+ ss << "\" });";
+ emscripten_run_script(ss.str().c_str());
+ InitializeAssets();
+ InitAudioDevice();
+ SetAudioStreamBufferSizeDefault(4096);
+ StartNewGame();
+
+ initialized = true;
+ return;
+ }
+
+ if (tutorial_is_running) {
+ UpdateTutorial();
+ BeginDrawing();
+ DrawTutorial();
+ EndDrawing();
+ return;
+ }
+
+ if (game_over) {
+ UpdateGameOver();
+ BeginDrawing();
+ DrawGameOver();
+ EndDrawing();
+ return;
+ }
+
+ Update();
+ BeginDrawing();
+ Draw();
+ EndDrawing();
+}
diff --git a/2026/games/just__jane/src/main.cpp b/2026/games/just__jane/src/main.cpp
new file mode 100644
index 0000000..2c07641
--- /dev/null
+++ b/2026/games/just__jane/src/main.cpp
@@ -0,0 +1,17 @@
+#include "raylib.h"
+
+#include <emscripten/em_types.h>
+#include <emscripten/emscripten.h>
+#include <emscripten/html5.h>
+
+#include <cut.hpp>
+#include <game_loop.hpp>
+
+int main() {
+ InitWindow(GAME_WIDTH * SCALE, GAME_HEIGHT * SCALE, "sakura samurai");
+ emscripten_run_script("window.parent.postMessage({op: \"ready\"});");
+ emscripten_set_main_loop(UpdateDrawFrame, 30, 1);
+ // EM_ASM(window.lcolonqJamStart = 1.0;);
+
+ return 0;
+}
diff --git a/2026/games/just__jane/src/score.cpp b/2026/games/just__jane/src/score.cpp
new file mode 100644
index 0000000..ea0aa78
--- /dev/null
+++ b/2026/games/just__jane/src/score.cpp
@@ -0,0 +1,79 @@
+#include <cstring>
+#include <format>
+#include <string>
+
+#include <emscripten/em_types.h>
+#include <emscripten/emscripten.h>
+#include <emscripten/fetch.h>
+#include <emscripten/html5.h>
+
+struct PostData {
+ char *body;
+};
+
+EM_JS(char *, get_cookie, (const char *name), {
+ const cookieName = UTF8ToString(name) + "=";
+ const cookies = document.cookie.split(";");
+
+ for (let c of cookies) {
+ c = c.trim();
+
+ if (c.startsWith(cookieName)) {
+ const value = c.substring(cookieName.length);
+ const len = lengthBytesUTF8(value) + 1;
+ const buffer = _malloc(len);
+ stringToUTF8(value, buffer, len);
+ return buffer;
+ }
+ }
+
+ return 0;
+});
+
+void cleanup(emscripten_fetch_t *fetch) {
+ auto *data = static_cast<PostData *>(fetch->userData);
+ free(data->body);
+ delete data;
+ emscripten_fetch_close(fetch);
+}
+
+void on_success(emscripten_fetch_t *fetch) {
+ printf("success: %d\n", fetch->status);
+ cleanup(fetch);
+}
+
+void on_error(emscripten_fetch_t *fetch) {
+ printf("error: %d\n", fetch->status);
+ cleanup(fetch);
+}
+
+void post_score(int score) {
+ auto username = get_cookie("bahms_user_login");
+ if (!username) {
+ return;
+ }
+
+ std::string text = std::format("## {}\nscore: {}", username, score);
+
+ auto *data = new PostData;
+ data->body = static_cast<char *>(malloc(text.size()));
+ memcpy(data->body, text.data(), text.size());
+
+ emscripten_fetch_attr_t attr;
+ emscripten_fetch_attr_init(&attr);
+
+ strcpy(attr.requestMethod, "POST");
+ attr.withCredentials = true;
+
+ const char *headers[] = {"Content-Type", "text/plain; charset=utf-8", nullptr};
+
+ attr.requestHeaders = headers;
+ attr.requestData = data->body;
+ attr.requestDataSize = text.size();
+ attr.userData = data;
+
+ attr.onsuccess = on_success;
+ attr.onerror = on_error;
+
+ emscripten_fetch(&attr, "https://api.bahms.org/v1/game/sakura-samurai");
+}