From ea5332e4a1a035e888709bee278fcafb182f7922 Mon Sep 17 00:00:00 2001 From: LLLL Colonq Date: Fri, 31 Jul 2026 05:00:49 -0400 Subject: Update --- 2026/games/just__jane/src/clock.cpp | 19 ++ 2026/games/just__jane/src/cut.cpp | 37 +++ 2026/games/just__jane/src/game_loop.cpp | 425 ++++++++++++++++++++++++++++++++ 2026/games/just__jane/src/main.cpp | 17 ++ 2026/games/just__jane/src/score.cpp | 79 ++++++ 5 files changed, 577 insertions(+) create mode 100644 2026/games/just__jane/src/clock.cpp create mode 100644 2026/games/just__jane/src/cut.cpp create mode 100644 2026/games/just__jane/src/game_loop.cpp create mode 100644 2026/games/just__jane/src/main.cpp create mode 100644 2026/games/just__jane/src/score.cpp (limited to '2026/games/just__jane/src') 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 +#include + +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::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 +#include +#include + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +static std::uniform_real_distribution rng_ = std::uniform_real_distribution(); +std::mt19937 generator_; + +static bool initialized; +static bool game_over = false; +static int score; +static int score_to_win; + +static std::optional the_cut; +static std::optional initial_point; +static std::optional 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 petals; +static std::vector 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 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 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 +#include +#include + +#include +#include + +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 +#include +#include + +#include +#include +#include +#include + +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(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(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"); +} -- cgit v1.3.1