diff --git a/config/seed_runtime.json b/config/seed_runtime.json index 42c7e3d..e170497 100644 --- a/config/seed_runtime.json +++ b/config/seed_runtime.json @@ -18,6 +18,8 @@ "move_back": "S", "move_left": "A", "move_right": "D", + "fly_up": "Q", + "fly_down": "Z", "music_toggle": "M", "music_toggle_gamepad": "start", "gamepad_move_x_axis": "leftx", diff --git a/scripts/cube_logic.lua b/scripts/cube_logic.lua index d9e6963..45c5b53 100644 --- a/scripts/cube_logic.lua +++ b/scripts/cube_logic.lua @@ -70,21 +70,25 @@ end start_music() local math3d = require("math3d") +local Gui = require("gui") local string_format = string.format local InputState = {} InputState.__index = InputState function InputState:new() + local sharedKeys = {} local instance = { mouseX = 0.0, mouseY = 0.0, mouseDeltaX = 0.0, mouseDeltaY = 0.0, mouseDown = false, + mouseDownPrevious = false, wheel = 0.0, textInput = "", - keyStates = {}, + keyStates = sharedKeys, + keys = sharedKeys, lastMouseX = nil, lastMouseY = nil, gamepad = { @@ -107,6 +111,7 @@ function InputState:resetTransient() end function InputState:setMouse(x, y, isDown, deltaX, deltaY) + self.mouseDownPrevious = self.mouseDown if type(deltaX) == "number" and type(deltaY) == "number" then self.mouseDeltaX = deltaX self.mouseDeltaY = deltaY @@ -129,6 +134,18 @@ function InputState:setKey(keyName, isDown) self.keyStates[keyName] = isDown end +function InputState:mouseJustPressed() + return self.mouseDown and not self.mouseDownPrevious +end + +function InputState:mouseJustReleased() + return not self.mouseDown and self.mouseDownPrevious +end + +function InputState:isKeyDown(keyName) + return self.keyStates[keyName] +end + function InputState:addTextInput(text) if text then self.textInput = self.textInput .. text @@ -147,6 +164,29 @@ end gui_input = InputState:new() +local gui_context = Gui.newContext() +local ui_layout = { + width = 1024, + height = 768, + margin = 16, +} +local compass_layout = { + size = 120, + label_height = 18, + padding = 10, +} +local flight_layout = { + width = 120, + height = 28, + spacing = 8, +} +local ui_state = { + flyUpActive = false, + flyDownActive = false, + flyUpPulse = false, + flyDownPulse = false, +} + local function log_debug(fmt, ...) if not lua_debug or not fmt then return @@ -177,6 +217,7 @@ local camera = { local controls = { move_speed = 4.0, + fly_speed = 3.0, mouse_sensitivity = 0.0025, gamepad_look_speed = 2.5, stick_deadzone = 0.2, @@ -259,6 +300,7 @@ local function update_camera(dt) local move_x = 0.0 local move_z = 0.0 + local move_y = 0.0 if gui_input.keyStates["move_forward"] then move_z = move_z + 1.0 @@ -273,6 +315,15 @@ local function update_camera(dt) move_x = move_x - 1.0 end + if gui_input.keyStates["fly_up"] or ui_state.flyUpActive or ui_state.flyUpPulse then + move_y = move_y + 1.0 + end + if gui_input.keyStates["fly_down"] or ui_state.flyDownActive or ui_state.flyDownPulse then + move_y = move_y - 1.0 + end + ui_state.flyUpPulse = false + ui_state.flyDownPulse = false + if pad and pad.connected then move_x = move_x + apply_deadzone(pad.leftX, controls.stick_deadzone) move_z = move_z - apply_deadzone(pad.leftY, controls.stick_deadzone) @@ -290,6 +341,10 @@ local function update_camera(dt) camera.position[2] = camera.position[2] + (right[2] * move_x + forward_flat[2] * move_z) * speed camera.position[3] = camera.position[3] + (right[3] * move_x + forward_flat[3] * move_z) * speed end + + if move_y ~= 0.0 then + camera.position[2] = camera.position[2] + move_y * controls.fly_speed * dt + end end local function update_audio_controls() @@ -325,6 +380,122 @@ local function create_spinning_cube() } end +local function heading_from_yaw(yaw) + local forward = forward_from_angles(yaw, 0.0) + local heading = math.deg(math.atan2(forward[1], -forward[3])) % 360 + return heading +end + +local function heading_to_cardinal(degrees) + local directions = {"N", "NE", "E", "SE", "S", "SW", "W", "NW"} + local index = math.floor((degrees + 22.5) / 45.0) % 8 + 1 + return directions[index] +end + +local function draw_compass_widget() + local size = compass_layout.size + local x = ui_layout.width - size - ui_layout.margin + local y = ui_layout.margin + local rect = {x = x, y = y, width = size, height = size} + gui_context:pushRect(rect, { + color = {0.06, 0.07, 0.09, 0.88}, + borderColor = {0.35, 0.38, 0.42, 1.0}, + }) + + Gui.text(gui_context, { + x = x, + y = y + 2, + width = size, + height = compass_layout.label_height, + }, "Compass", { + fontSize = 12, + alignX = "center", + color = {0.82, 0.88, 0.95, 1.0}, + }) + + local ring_rect = { + x = x + compass_layout.padding, + y = y + compass_layout.label_height, + width = size - compass_layout.padding * 2, + height = size - compass_layout.label_height - compass_layout.padding, + } + local center_x = ring_rect.x + ring_rect.width / 2 + local center_y = ring_rect.y + ring_rect.height / 2 + local radius = math.min(ring_rect.width, ring_rect.height) / 2 - 6 + + Gui.text(gui_context, {x = center_x - 8, y = ring_rect.y - 2, width = 16, height = 14}, "N", { + fontSize = 12, + alignX = "center", + color = {0.78, 0.82, 0.88, 1.0}, + }) + Gui.text(gui_context, {x = ring_rect.x + ring_rect.width - 12, y = center_y - 7, width = 14, height = 14}, "E", { + fontSize = 12, + alignX = "center", + color = {0.78, 0.82, 0.88, 1.0}, + }) + Gui.text(gui_context, {x = center_x - 8, y = ring_rect.y + ring_rect.height - 12, width = 16, height = 14}, "S", { + fontSize = 12, + alignX = "center", + color = {0.78, 0.82, 0.88, 1.0}, + }) + Gui.text(gui_context, {x = ring_rect.x - 2, y = center_y - 7, width = 14, height = 14}, "W", { + fontSize = 12, + alignX = "center", + color = {0.78, 0.82, 0.88, 1.0}, + }) + + local heading = heading_from_yaw(camera.yaw) + local heading_int = math.floor(heading + 0.5) % 360 + local direction = heading_to_cardinal(heading) + local angle = math.rad(heading) - math.pi / 2.0 + local needle_x = center_x + math.cos(angle) * radius + local needle_y = center_y + math.sin(angle) * radius + + gui_context:pushRect({ + x = needle_x - 3, + y = needle_y - 3, + width = 6, + height = 6, + }, { + color = {0.98, 0.78, 0.35, 1.0}, + radius = 2, + }) + + Gui.text(gui_context, { + x = x, + y = y + size - 18, + width = size, + height = 16, + }, string_format("%03d deg %s", heading_int, direction), { + fontSize = 11, + alignX = "center", + color = {0.9, 0.92, 0.95, 1.0}, + }) +end + +local function draw_flight_buttons() + local x = ui_layout.width - flight_layout.width - ui_layout.margin + local y = ui_layout.margin * 2 + compass_layout.size + + local up_clicked = Gui.button(gui_context, "fly_up", { + x = x, + y = y, + width = flight_layout.width, + height = flight_layout.height, + }, "Fly Up") + local down_clicked = Gui.button(gui_context, "fly_down", { + x = x, + y = y + flight_layout.height + flight_layout.spacing, + width = flight_layout.width, + height = flight_layout.height, + }, "Fly Down") + + ui_state.flyUpActive = gui_context.activeWidget == "fly_up" + ui_state.flyDownActive = gui_context.activeWidget == "fly_down" + ui_state.flyUpPulse = up_clicked + ui_state.flyDownPulse = down_clicked +end + function get_scene_objects() return { create_spinning_cube(), @@ -362,3 +533,11 @@ function get_view_projection(aspect) local projection = math3d.perspective(camera.fov, aspect, camera.near, camera.far) return math3d.multiply(projection, view) end + +function get_gui_commands() + gui_context:beginFrame(gui_input) + draw_compass_widget() + draw_flight_buttons() + gui_context:endFrame() + return gui_context:getCommands() +end diff --git a/src/services/impl/json_config_service.cpp b/src/services/impl/json_config_service.cpp index ab81300..d63b3ea 100644 --- a/src/services/impl/json_config_service.cpp +++ b/src/services/impl/json_config_service.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -216,31 +217,43 @@ RuntimeConfig JsonConfigService::LoadFromJson(std::shared_ptr logger, c if (!bindingsValue.IsObject()) { throw std::runtime_error("JSON member 'input_bindings' must be an object"); } - auto readBinding = [&](const char* name, std::string& target) { - if (!bindingsValue.HasMember(name)) { + struct BindingSpec { + const char* name; + std::string InputBindings::* member; + }; + const std::array bindingSpecs = {{ + {"move_forward", &InputBindings::moveForwardKey}, + {"move_back", &InputBindings::moveBackKey}, + {"move_left", &InputBindings::moveLeftKey}, + {"move_right", &InputBindings::moveRightKey}, + {"fly_up", &InputBindings::flyUpKey}, + {"fly_down", &InputBindings::flyDownKey}, + {"music_toggle", &InputBindings::musicToggleKey}, + {"music_toggle_gamepad", &InputBindings::musicToggleGamepadButton}, + {"gamepad_move_x_axis", &InputBindings::gamepadMoveXAxis}, + {"gamepad_move_y_axis", &InputBindings::gamepadMoveYAxis}, + {"gamepad_look_x_axis", &InputBindings::gamepadLookXAxis}, + {"gamepad_look_y_axis", &InputBindings::gamepadLookYAxis}, + {"gamepad_dpad_up", &InputBindings::gamepadDpadUpButton}, + {"gamepad_dpad_down", &InputBindings::gamepadDpadDownButton}, + {"gamepad_dpad_left", &InputBindings::gamepadDpadLeftButton}, + {"gamepad_dpad_right", &InputBindings::gamepadDpadRightButton}, + }}; + + auto readBinding = [&](const BindingSpec& spec) { + if (!bindingsValue.HasMember(spec.name)) { return; } - const auto& value = bindingsValue[name]; + const auto& value = bindingsValue[spec.name]; if (!value.IsString()) { - throw std::runtime_error("JSON member 'input_bindings." + std::string(name) + "' must be a string"); + throw std::runtime_error("JSON member 'input_bindings." + std::string(spec.name) + "' must be a string"); } - target = value.GetString(); + config.inputBindings.*(spec.member) = value.GetString(); }; - readBinding("move_forward", config.inputBindings.moveForwardKey); - readBinding("move_back", config.inputBindings.moveBackKey); - readBinding("move_left", config.inputBindings.moveLeftKey); - readBinding("move_right", config.inputBindings.moveRightKey); - readBinding("music_toggle", config.inputBindings.musicToggleKey); - readBinding("music_toggle_gamepad", config.inputBindings.musicToggleGamepadButton); - readBinding("gamepad_move_x_axis", config.inputBindings.gamepadMoveXAxis); - readBinding("gamepad_move_y_axis", config.inputBindings.gamepadMoveYAxis); - readBinding("gamepad_look_x_axis", config.inputBindings.gamepadLookXAxis); - readBinding("gamepad_look_y_axis", config.inputBindings.gamepadLookYAxis); - readBinding("gamepad_dpad_up", config.inputBindings.gamepadDpadUpButton); - readBinding("gamepad_dpad_down", config.inputBindings.gamepadDpadDownButton); - readBinding("gamepad_dpad_left", config.inputBindings.gamepadDpadLeftButton); - readBinding("gamepad_dpad_right", config.inputBindings.gamepadDpadRightButton); + for (const auto& spec : bindingSpecs) { + readBinding(spec); + } auto readMapping = [&](const char* name, std::unordered_map& target) { diff --git a/src/services/impl/json_config_writer_service.cpp b/src/services/impl/json_config_writer_service.cpp index 322df59..1627dd6 100644 --- a/src/services/impl/json_config_writer_service.cpp +++ b/src/services/impl/json_config_writer_service.cpp @@ -5,6 +5,7 @@ #include #include +#include #include #include #include @@ -70,20 +71,31 @@ void JsonConfigWriterService::WriteConfig(const RuntimeConfig& config, const std rapidjson::Value stringValue(value.c_str(), allocator); bindingsObject.AddMember(nameValue, stringValue, allocator); }; - addBindingMember("move_forward", config.inputBindings.moveForwardKey); - addBindingMember("move_back", config.inputBindings.moveBackKey); - addBindingMember("move_left", config.inputBindings.moveLeftKey); - addBindingMember("move_right", config.inputBindings.moveRightKey); - addBindingMember("music_toggle", config.inputBindings.musicToggleKey); - addBindingMember("music_toggle_gamepad", config.inputBindings.musicToggleGamepadButton); - addBindingMember("gamepad_move_x_axis", config.inputBindings.gamepadMoveXAxis); - addBindingMember("gamepad_move_y_axis", config.inputBindings.gamepadMoveYAxis); - addBindingMember("gamepad_look_x_axis", config.inputBindings.gamepadLookXAxis); - addBindingMember("gamepad_look_y_axis", config.inputBindings.gamepadLookYAxis); - addBindingMember("gamepad_dpad_up", config.inputBindings.gamepadDpadUpButton); - addBindingMember("gamepad_dpad_down", config.inputBindings.gamepadDpadDownButton); - addBindingMember("gamepad_dpad_left", config.inputBindings.gamepadDpadLeftButton); - addBindingMember("gamepad_dpad_right", config.inputBindings.gamepadDpadRightButton); + struct BindingSpec { + const char* name; + std::string InputBindings::* member; + }; + const std::array bindingSpecs = {{ + {"move_forward", &InputBindings::moveForwardKey}, + {"move_back", &InputBindings::moveBackKey}, + {"move_left", &InputBindings::moveLeftKey}, + {"move_right", &InputBindings::moveRightKey}, + {"fly_up", &InputBindings::flyUpKey}, + {"fly_down", &InputBindings::flyDownKey}, + {"music_toggle", &InputBindings::musicToggleKey}, + {"music_toggle_gamepad", &InputBindings::musicToggleGamepadButton}, + {"gamepad_move_x_axis", &InputBindings::gamepadMoveXAxis}, + {"gamepad_move_y_axis", &InputBindings::gamepadMoveYAxis}, + {"gamepad_look_x_axis", &InputBindings::gamepadLookXAxis}, + {"gamepad_look_y_axis", &InputBindings::gamepadLookYAxis}, + {"gamepad_dpad_up", &InputBindings::gamepadDpadUpButton}, + {"gamepad_dpad_down", &InputBindings::gamepadDpadDownButton}, + {"gamepad_dpad_left", &InputBindings::gamepadDpadLeftButton}, + {"gamepad_dpad_right", &InputBindings::gamepadDpadRightButton}, + }}; + for (const auto& spec : bindingSpecs) { + addBindingMember(spec.name, config.inputBindings.*(spec.member)); + } auto addMappingObject = [&](const char* name, const std::unordered_map& mappings, diff --git a/src/services/impl/sdl_input_service.cpp b/src/services/impl/sdl_input_service.cpp index ac2b595..3dc76df 100644 --- a/src/services/impl/sdl_input_service.cpp +++ b/src/services/impl/sdl_input_service.cpp @@ -312,6 +312,8 @@ void SdlInputService::BuildActionKeyMapping() { addKey("move_back", bindings.moveBackKey); addKey("move_left", bindings.moveLeftKey); addKey("move_right", bindings.moveRightKey); + addKey("fly_up", bindings.flyUpKey); + addKey("fly_down", bindings.flyDownKey); addKey("music_toggle", bindings.musicToggleKey); auto toLower = [](std::string value) { diff --git a/src/services/interfaces/config_types.hpp b/src/services/interfaces/config_types.hpp index 0129dba..fe92735 100644 --- a/src/services/interfaces/config_types.hpp +++ b/src/services/interfaces/config_types.hpp @@ -15,6 +15,8 @@ struct InputBindings { std::string moveBackKey = "S"; std::string moveLeftKey = "A"; std::string moveRightKey = "D"; + std::string flyUpKey = "Q"; + std::string flyDownKey = "Z"; std::string musicToggleKey = "M"; std::string musicToggleGamepadButton = "start"; std::string gamepadMoveXAxis = "leftx";