diff --git a/app.py b/app.py index 7944d5c..329210c 100644 --- a/app.py +++ b/app.py @@ -251,4 +251,9 @@ def logout(): flask_login.logout_user() return redirect(url_for("login")) +@app.route("/pumpkin_memory") +@login_required +def pumpkin_memory(): + return render_template("pumpkin_memory.jinja2") + app.run(host=os.getenv("HOST", "0.0.0.0"), port=int(os.getenv("PORT", 8080)), debug=os.getenv("DEBUG_MODE", False).lower() == "true") \ No newline at end of file diff --git a/static/game.js b/static/game.js new file mode 100644 index 0000000..d8bf807 --- /dev/null +++ b/static/game.js @@ -0,0 +1,113 @@ +const WIDTH = 1280; +const HEIGHT = 720; + +const SETTINGS = { + "Graphics": { + "Anti-Aliasing": {"type": "bool", "default": true}, + "Texture Filtering": {"type": "option", "options": ["Nearest", "Linear"], "default": "Linear"}, + "VSync": {"type": "bool", "default": true}, + "FPS Limit": {"type": "slider", "min": 0, "max": 480, "default": 60}, + }, + "Sound": { + "Music": {"type": "bool", "default": true}, + "SFX": {"type": "bool", "default": true}, + "Music Volume": {"type": "slider", "min": 0, "max": 100, "default": 50}, + "SFX Volume": {"type": "slider", "min": 0, "max": 100, "default": 50}, + } +} + +kaplay( + { + width: WIDTH, + height: HEIGHT, + canvas: document.getElementById("canvas"), + root: document.getElementById("game-container"), + font: "New Rocker", + background: "#e18888", + buttons: { + up_: { + keyboard: "up", + gamepad: "south", + }, + } + } +); + +function change_setting(category, setting, value) { + localStorage.setItem(setting, value); + go("settings", category); +} + +function show_settings(category) { + const x = 400; + const label_x = 50; + const space_between = 100; + let y = 130; + + for (let key in SETTINGS[category]) { + const settings_dict = SETTINGS[category][key]; + const currentKey = key; + + create_label(label_x, y + 10, key, 32); + + let value = localStorage.getItem(key); + + if (value == undefined) { + localStorage.setItem(key, settings_dict.default); + value = settings_dict.default; + } + + if (settings_dict.type == "bool") { + horizontal_buttons(x, y, [ + [ + "ON", + value === "true" ? color(255, 255, 255) : color(127, 127, 127), + color(0, 0, 0, 0), + () => { change_setting(category, currentKey, true); } + ], + [ + "OFF", + value === "false" ? color(255, 255, 255) : color(127, 127, 127), + color(0, 0, 0, 0), + () => { change_setting(category, currentKey, false); } + ] + ], 100, 50, 20); + + } + else if (settings_dict.type == "option") { + create_dropdown(x, y, 300, 75, settings_dict.options, 0, (option) => { + localStorage.setItem(currentKey, option); + }); + } + else if (settings_dict.type == "slider") { + create_slider(x, y, 400, Number(settings_dict.min), Number(settings_dict.max), Number(value), () => { + localStorage.setItem(currentKey, value); + }); + } + + y = y + space_between; + } +} + +function start_game(title) { + scene("settings", (setting_category) => { + let generated_button_lists = Object.entries(SETTINGS).map(([key, value]) => [key, color(127, 127, 127), color(0, 0, 0, 0), scene_lambda("settings", key)]); + generated_button_lists = [["Back", color(127, 127, 127), color(0, 0, 0, 0), scene_lambda("main_menu")]].concat(generated_button_lists); + + horizontal_buttons(10, 10, generated_button_lists, 200, 75, 10); + + if (setting_category != null) { + show_settings(setting_category); + } + else { + show_settings(Object.keys(SETTINGS)[0]); + } + }) + + scene("main_menu", () => { + create_label(WIDTH / 2 - 16 * title.length, HEIGHT / 4, title, 56); + vertical_buttons(WIDTH / 4, HEIGHT / 2.25, [["Play", color(127, 127, 127), color(0, 0, 0, 0), scene_lambda("play")], ["Settings", color(127, 127, 127), color(0, 0, 0, 0), scene_lambda("settings")]], WIDTH / 2, HEIGHT / 8, HEIGHT / 50) + }); + + go("main_menu"); +} \ No newline at end of file diff --git a/static/gameui.js b/static/gameui.js new file mode 100644 index 0000000..3c73193 --- /dev/null +++ b/static/gameui.js @@ -0,0 +1,308 @@ +function create_button(x, y, w, h, label_text, bg, text_color, on_click) { + let button = add([ + rect(w, h), + pos(x, y), + bg, + area(), + ]); + + button.add([ + text(label_text, { + width: w / 1.5, + size: 28, + }), + text_color, + pos(w / 2 - (label_text.length * 8), h / 2 - 18) + ]); + + button.onClick(on_click); + button.onHover(() => { + button.scale = vec2(1.025, 1.025); + setCursor("pointer"); + }); + + button.onHoverEnd(() => { + button.scale = vec2(1, 1); + setCursor("default"); + }); + + + return button; +} + +function create_slider(x, y, w, min_val, max_val, initial_val, on_change) { + const slider_height = 15; + const handle_size = 30; + + let slider_container = add([ + pos(x, y), + ]); + + let track = slider_container.add([ + rect(w, slider_height), + pos(0, handle_size / 2 - slider_height / 2), + color(100, 100, 100), + area() + ]); + + let value = initial_val; + let handle_x = ((value - min_val) / (max_val - min_val)) * w; + + let handle = slider_container.add([ + rect(handle_size, handle_size), + pos(handle_x - handle_size / 2, 0), + color(255, 255, 255), + area(), + "slider_handle" + ]); + + let value_label = slider_container.add([ + text(value.toFixed(0), { size: 16 }), + pos(w + 10, handle_size / 2 - 8), + color(255, 255, 255), + ]); + + let is_dragging = false; + + handle.onHover(() => { + setCursor("pointer"); + }); + + handle.onHoverEnd(() => { + if (!is_dragging) { + setCursor("default"); + } + }); + + handle.onMousePress(() => { + is_dragging = true; + setCursor("grabbing"); + }); + + onMouseRelease(() => { + if (is_dragging) { + is_dragging = false; + setCursor("default"); + } + }); + + onMouseMove(() => { + if (is_dragging) { + let mouse_x = mousePos().x - slider_container.pos.x; + mouse_x = Math.max(0, Math.min(w, mouse_x)); + + handle.pos.x = mouse_x - handle_size / 2; + + value = min_val + (mouse_x / w) * (max_val - min_val); + value_label.text = value.toFixed(0); + + if (on_change) { + on_change(value); + } + } + }); + + track.onHover(() => { + setCursor("pointer"); + }); + + track.onHoverEnd(() => { + setCursor("default"); + }); + + track.onClick(() => { + if (!is_dragging) { + let mouse_x = mousePos().x - slider_container.pos.x; + mouse_x = Math.max(0, Math.min(w, mouse_x)); + + handle.pos.x = mouse_x - handle_size / 2; + value = min_val + (mouse_x / w) * (max_val - min_val); + value_label.text = value.toFixed(0); + + if (on_change) { + on_change(value); + } + } + }); + + track.use(area()); + + return { + obj: slider_container, + getValue: () => value, + setValue: (new_val) => { + value = Math.max(min_val, Math.min(max_val, new_val)); + handle_x = ((value - min_val) / (max_val - min_val)) * w; + handle.pos.x = handle_x - handle_size / 2; + value_label.text = value.toFixed(0); + } + }; +} + +function create_dropdown(x, y, w, h, options, initial_index, on_select) { + let selected_index = initial_index || 0; + let is_open = false; + + let dropdown = add([ + pos(x, y), + z(10), + ]); + + let selected_box = dropdown.add([ + rect(w, h), + pos(0, 0), + color(60, 60, 60), + area(), + outline(2, rgb(100, 100, 100)), + ]); + + let selected_text = dropdown.add([ + text(options[selected_index], { size: 20 }), + pos(10, h / 2 - 10), + color(255, 255, 255), + ]); + + let arrow = dropdown.add([ + text("▼", { size: 16 }), + pos(w - 25, h / 2 - 8), + color(200, 200, 200), + ]); + + let options_container = null; + let option_items = []; + + function create_options_menu() { + if (options_container) { + destroy(options_container); + } + + options_container = dropdown.add([ + pos(0, h + 2), + z(20), + ]); + + option_items = []; + + options.forEach((option, index) => { + let option_box = options_container.add([ + rect(w, h), + pos(0, index * h), + color(50, 50, 50), + area(), + outline(1, rgb(80, 80, 80)), + ]); + + let option_text = options_container.add([ + text(option, { size: 20 }), + pos(10, index * h + h / 2 - 10), + color(255, 255, 255), + ]); + + option_box.onHover(() => { + option_box.color = rgb(80, 80, 80); + setCursor("pointer"); + }); + + option_box.onHoverEnd(() => { + option_box.color = rgb(50, 50, 50); + setCursor("default"); + }); + + option_box.onClick(() => { + selected_index = index; + selected_text.text = options[index]; + close_dropdown(); + if (on_select) { + on_select(options[index], index); + } + }); + + option_items.push({ box: option_box, text: option_text }); + }); + } + + function open_dropdown() { + is_open = true; + arrow.text = "▲"; + create_options_menu(); + } + + function close_dropdown() { + is_open = false; + arrow.text = "▼"; + if (options_container) { + destroy(options_container); + options_container = null; + } + } + + selected_box.onHover(() => { + selected_box.color = rgb(70, 70, 70); + setCursor("pointer"); + }); + + selected_box.onHoverEnd(() => { + selected_box.color = rgb(60, 60, 60); + setCursor("default"); + }); + + selected_box.onClick(() => { + if (is_open) { + close_dropdown(); + } else { + open_dropdown(); + } + }); + + onClick(() => { + if (is_open) { + let mouse = mousePos(); + let in_bounds = mouse.x >= x && mouse.x <= x + w && + mouse.y >= y && mouse.y <= y + h + (options.length * h); + if (!in_bounds) { + close_dropdown(); + } + } + }); + + return { + obj: dropdown, + getValue: () => options[selected_index], + getIndex: () => selected_index, + setIndex: (index) => { + if (index >= 0 && index < options.length) { + selected_index = index; + selected_text.text = options[index]; + } + }, + close: close_dropdown + }; +} + +function create_label(x, y, label_text, font_size) { + return add([ + text(label_text, { + size: font_size, + }), + color(0, 0, 0), + pos(x, y) + ]) +} + +function horizontal_buttons(start_x, start_y, buttons, width, height, space_between) { + for (let i = 0; i < buttons.length; i++) { + create_button(start_x + i * (width + space_between), start_y, width, height, buttons[i][0], buttons[i][1], buttons[i][2], buttons[i][3]) + } +} + +function vertical_buttons(start_x, start_y, buttons, width, height, space_between) { + for (let i = 0; i < buttons.length; i++) { + create_button(start_x, start_y + i * (height + space_between), width, height, buttons[i][0], buttons[i][1], buttons[i][2], buttons[i][3]) + } +} + +function scene_lambda(scene, args) { + return () => { + go(scene, args); + } +} \ No newline at end of file diff --git a/static/pumpkin_memory.js b/static/pumpkin_memory.js new file mode 100644 index 0000000..5da6232 --- /dev/null +++ b/static/pumpkin_memory.js @@ -0,0 +1,12 @@ +scene("game", (pumpkin_pairs) => { +}) + +scene("play", () => { + create_label(WIDTH / 2 - 16 * "Difficulty Selector".length, HEIGHT / 8, "Difficulty Selector", 56); + vertical_buttons(WIDTH / 4, HEIGHT / 4, [ + ["Easy", color(127, 127, 127), color(0, 0, 0, 0), scene_lambda("game", 6)], + ["Medium", color(127, 127, 127), color(0, 0, 0, 0), scene_lambda("game", 9)], + ["Hard", color(127, 127, 127), color(0, 0, 0, 0), scene_lambda("game", 12)], + ["Extra Hard", color(127, 127, 127), color(0, 0, 0, 0), scene_lambda("game", 15)] + ], WIDTH / 2, HEIGHT / 8, HEIGHT / 50) +}) \ No newline at end of file diff --git a/templates/base.jinja2 b/templates/base.jinja2 index a7f0b1c..e75f92d 100644 --- a/templates/base.jinja2 +++ b/templates/base.jinja2 @@ -5,7 +5,9 @@ + +