diff --git a/addons/debug_menu/LICENSE.md b/addons/debug_menu/LICENSE.md
new file mode 100644
index 0000000..54fc020
--- /dev/null
+++ b/addons/debug_menu/LICENSE.md
@@ -0,0 +1,21 @@
+# MIT License
+
+Copyright © 2023-present Hugo Locurcio and contributors
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/addons/debug_menu/debug_menu.gd b/addons/debug_menu/debug_menu.gd
new file mode 100644
index 0000000..a1ab064
--- /dev/null
+++ b/addons/debug_menu/debug_menu.gd
@@ -0,0 +1,479 @@
+extends CanvasLayer
+
+@export var fps: Label
+@export var frame_time: Label
+@export var frame_number: Label
+@export var frame_history_total_avg: Label
+@export var frame_history_total_min: Label
+@export var frame_history_total_max: Label
+@export var frame_history_total_last: Label
+@export var frame_history_cpu_avg: Label
+@export var frame_history_cpu_min: Label
+@export var frame_history_cpu_max: Label
+@export var frame_history_cpu_last: Label
+@export var frame_history_gpu_avg: Label
+@export var frame_history_gpu_min: Label
+@export var frame_history_gpu_max: Label
+@export var frame_history_gpu_last: Label
+@export var fps_graph: Panel
+@export var total_graph: Panel
+@export var cpu_graph: Panel
+@export var gpu_graph: Panel
+@export var information: Label
+@export var settings: Label
+
+## The number of frames to keep in history for graph drawing and best/worst calculations.
+## Currently, this also affects how FPS is measured.
+const HISTORY_NUM_FRAMES = 150
+
+const GRAPH_SIZE = Vector2(150, 25)
+const GRAPH_MIN_FPS = 10
+const GRAPH_MAX_FPS = 160
+const GRAPH_MIN_FRAMETIME = 1.0 / GRAPH_MIN_FPS
+const GRAPH_MAX_FRAMETIME = 1.0 / GRAPH_MAX_FPS
+
+## Debug menu display style.
+enum Style {
+ HIDDEN, ## Debug menu is hidden.
+ VISIBLE_COMPACT, ## Debug menu is visible, with only the FPS, FPS cap (if any) and time taken to render the last frame.
+ VISIBLE_DETAILED, ## Debug menu is visible with full information, including graphs.
+ MAX, ## Represents the size of the Style enum.
+}
+
+## The style to use when drawing the debug menu.
+var style := Style.HIDDEN:
+ set(value):
+ style = value
+ match style:
+ Style.HIDDEN:
+ visible = false
+ Style.VISIBLE_COMPACT, Style.VISIBLE_DETAILED:
+ visible = true
+ frame_number.visible = style == Style.VISIBLE_DETAILED
+ $DebugMenu/VBoxContainer/FrameTimeHistory.visible = style == Style.VISIBLE_DETAILED
+ $DebugMenu/VBoxContainer/FPSGraph.visible = style == Style.VISIBLE_DETAILED
+ $DebugMenu/VBoxContainer/TotalGraph.visible = style == Style.VISIBLE_DETAILED
+ $DebugMenu/VBoxContainer/CPUGraph.visible = style == Style.VISIBLE_DETAILED
+ $DebugMenu/VBoxContainer/GPUGraph.visible = style == Style.VISIBLE_DETAILED
+ information.visible = style == Style.VISIBLE_DETAILED
+ settings.visible = style == Style.VISIBLE_DETAILED
+
+# Value of `Time.get_ticks_usec()` on the previous frame.
+var last_tick := 0
+
+var thread := Thread.new()
+
+## Returns the sum of all values of an array (use as a parameter to `Array.reduce()`).
+var sum_func := func avg(accum: float, number: float) -> float: return accum + number
+
+# History of the last `HISTORY_NUM_FRAMES` rendered frames.
+var frame_history_total: Array[float] = []
+var frame_history_cpu: Array[float] = []
+var frame_history_gpu: Array[float] = []
+var fps_history: Array[float] = [] # Only used for graphs.
+
+var frametime_avg := GRAPH_MIN_FRAMETIME
+var frametime_cpu_avg := GRAPH_MAX_FRAMETIME
+var frametime_gpu_avg := GRAPH_MIN_FRAMETIME
+var frames_per_second := float(GRAPH_MIN_FPS)
+var frame_time_gradient := Gradient.new()
+
+func _init() -> void:
+ # This must be done here instead of `_ready()` to avoid having `visibility_changed` be emitted immediately.
+ visible = false
+
+ if not InputMap.has_action("cycle_debug_menu"):
+ # Create default input action if no user-defined override exists.
+ # We can't do it in the editor plugin's activation code as it doesn't seem to work there.
+ InputMap.add_action("cycle_debug_menu")
+ var event := InputEventKey.new()
+ event.keycode = KEY_F3
+ InputMap.action_add_event("cycle_debug_menu", event)
+
+
+func _ready() -> void:
+ fps_graph.draw.connect(_fps_graph_draw)
+ total_graph.draw.connect(_total_graph_draw)
+ cpu_graph.draw.connect(_cpu_graph_draw)
+ gpu_graph.draw.connect(_gpu_graph_draw)
+
+ fps_history.resize(HISTORY_NUM_FRAMES)
+ frame_history_total.resize(HISTORY_NUM_FRAMES)
+ frame_history_cpu.resize(HISTORY_NUM_FRAMES)
+ frame_history_gpu.resize(HISTORY_NUM_FRAMES)
+
+ # NOTE: Both FPS and frametimes are colored following FPS logic
+ # (red = 10 FPS, yellow = 60 FPS, green = 110 FPS, cyan = 160 FPS).
+ # This makes the color gradient non-linear.
+ # Colors are taken from .
+ frame_time_gradient.set_color(0, Color8(239, 68, 68)) # red-500
+ frame_time_gradient.set_color(1, Color8(56, 189, 248)) # light-blue-400
+ frame_time_gradient.add_point(0.3333, Color8(250, 204, 21)) # yellow-400
+ frame_time_gradient.add_point(0.6667, Color8(128, 226, 95)) # 50-50 mix of lime-400 and green-400
+
+ get_viewport().size_changed.connect(update_settings_label)
+
+ # Display loading text while information is being queried,
+ # in case the user toggles the full debug menu just after starting the project.
+ information.text = "Loading hardware information...\n\n "
+ settings.text = "Loading project information..."
+ thread.start(
+ func():
+ # Disable thread safety checks as they interfere with this add-on.
+ # This only affects this particular thread, not other thread instances in the project.
+ # See for details.
+ # Use a Callable so that this can be ignored on Godot 4.0 without causing a script error
+ # (thread safety checks were added in Godot 4.1).
+ if Engine.get_version_info()["hex"] >= 0x040100:
+ Callable(Thread, "set_thread_safety_checks_enabled").call(false)
+
+ # Enable required time measurements to display CPU/GPU frame time information.
+ # These lines are time-consuming operations, so run them in a separate thread.
+ RenderingServer.viewport_set_measure_render_time(get_viewport().get_viewport_rid(), true)
+ update_information_label()
+ update_settings_label()
+ )
+
+
+func _input(event: InputEvent) -> void:
+ if event.is_action_pressed("cycle_debug_menu"):
+ style = wrapi(style + 1, 0, Style.MAX) as Style
+
+
+func _exit_tree() -> void:
+ thread.wait_to_finish()
+
+
+## Update hardware information label (this can change at runtime based on window
+## size and graphics settings). This is only called when the window is resized.
+## To update when graphics settings are changed, the function must be called manually
+## using `DebugMenu.update_settings_label()`.
+func update_settings_label() -> void:
+ settings.text = ""
+ if ProjectSettings.has_setting("application/config/version"):
+ settings.text += "Project Version: %s\n" % ProjectSettings.get_setting("application/config/version")
+
+ var rendering_method := str(ProjectSettings.get_setting_with_override("rendering/renderer/rendering_method"))
+ var rendering_method_string := rendering_method
+ match rendering_method:
+ "forward_plus":
+ rendering_method_string = "Forward+"
+ "mobile":
+ rendering_method_string = "Forward Mobile"
+ "gl_compatibility":
+ rendering_method_string = "Compatibility"
+ settings.text += "Rendering Method: %s\n" % rendering_method_string
+
+ var viewport := get_viewport()
+
+ # The size of the viewport rendering, which determines which resolution 3D is rendered at.
+ var viewport_render_size := Vector2i()
+
+ if viewport.content_scale_mode == Window.CONTENT_SCALE_MODE_VIEWPORT:
+ viewport_render_size = viewport.get_visible_rect().size
+ settings.text += "Viewport: %d×%d, Window: %d×%d\n" % [viewport.get_visible_rect().size.x, viewport.get_visible_rect().size.y, viewport.size.x, viewport.size.y]
+ else:
+ # Window size matches viewport size.
+ viewport_render_size = viewport.size
+ settings.text += "Viewport: %d×%d\n" % [viewport.size.x, viewport.size.y]
+
+ # Display 3D settings only if relevant.
+ if viewport.get_camera_3d():
+ var scaling_3d_mode_string := "(unknown)"
+ match viewport.scaling_3d_mode:
+ Viewport.SCALING_3D_MODE_BILINEAR:
+ scaling_3d_mode_string = "Bilinear"
+ Viewport.SCALING_3D_MODE_FSR:
+ scaling_3d_mode_string = "FSR 1.0"
+ Viewport.SCALING_3D_MODE_FSR2:
+ scaling_3d_mode_string = "FSR 2.2"
+
+ var antialiasing_3d_string := ""
+ if viewport.scaling_3d_mode == Viewport.SCALING_3D_MODE_FSR2:
+ # The FSR2 scaling mode includes its own temporal antialiasing implementation.
+ antialiasing_3d_string += (" + " if not antialiasing_3d_string.is_empty() else "") + "FSR 2.2"
+ if viewport.scaling_3d_mode != Viewport.SCALING_3D_MODE_FSR2 and viewport.use_taa:
+ # Godot's own TAA is ignored when using FSR2 scaling mode, as FSR2 provides its own TAA implementation.
+ antialiasing_3d_string += (" + " if not antialiasing_3d_string.is_empty() else "") + "TAA"
+ if viewport.msaa_3d >= Viewport.MSAA_2X:
+ antialiasing_3d_string += (" + " if not antialiasing_3d_string.is_empty() else "") + "%d× MSAA" % pow(2, viewport.msaa_3d)
+ if viewport.screen_space_aa == Viewport.SCREEN_SPACE_AA_FXAA:
+ antialiasing_3d_string += (" + " if not antialiasing_3d_string.is_empty() else "") + "FXAA"
+
+ settings.text += "3D scale (%s): %d%% = %d×%d" % [
+ scaling_3d_mode_string,
+ viewport.scaling_3d_scale * 100,
+ viewport_render_size.x * viewport.scaling_3d_scale,
+ viewport_render_size.y * viewport.scaling_3d_scale,
+ ]
+
+ if not antialiasing_3d_string.is_empty():
+ settings.text += "\n3D Antialiasing: %s" % antialiasing_3d_string
+
+ var environment := viewport.get_camera_3d().get_world_3d().environment
+ if environment:
+ if environment.ssr_enabled:
+ settings.text += "\nSSR: %d Steps" % environment.ssr_max_steps
+
+ if environment.ssao_enabled:
+ settings.text += "\nSSAO: On"
+ if environment.ssil_enabled:
+ settings.text += "\nSSIL: On"
+
+ if environment.sdfgi_enabled:
+ settings.text += "\nSDFGI: %d Cascades" % environment.sdfgi_cascades
+
+ if environment.glow_enabled:
+ settings.text += "\nGlow: On"
+
+ if environment.volumetric_fog_enabled:
+ settings.text += "\nVolumetric Fog: On"
+ var antialiasing_2d_string := ""
+ if viewport.msaa_2d >= Viewport.MSAA_2X:
+ antialiasing_2d_string = "%d× MSAA" % pow(2, viewport.msaa_2d)
+
+ if not antialiasing_2d_string.is_empty():
+ settings.text += "\n2D Antialiasing: %s" % antialiasing_2d_string
+
+
+## Update hardware/software information label (this never changes at runtime).
+func update_information_label() -> void:
+ var adapter_string := ""
+ # Make "NVIDIA Corporation" and "NVIDIA" be considered identical (required when using OpenGL to avoid redundancy).
+ if RenderingServer.get_video_adapter_vendor().trim_suffix(" Corporation") in RenderingServer.get_video_adapter_name():
+ # Avoid repeating vendor name before adapter name.
+ # Trim redundant suffix sometimes reported by NVIDIA graphics cards when using OpenGL.
+ adapter_string = RenderingServer.get_video_adapter_name().trim_suffix("/PCIe/SSE2")
+ else:
+ adapter_string = RenderingServer.get_video_adapter_vendor() + " - " + RenderingServer.get_video_adapter_name().trim_suffix("/PCIe/SSE2")
+
+ # Graphics driver version information isn't always availble.
+ var driver_info := OS.get_video_adapter_driver_info()
+ var driver_info_string := ""
+ if driver_info.size() >= 2:
+ driver_info_string = driver_info[1]
+ else:
+ driver_info_string = "(unknown)"
+
+ var release_string := ""
+ if OS.has_feature("editor"):
+ # Editor build (implies `debug`).
+ release_string = "editor"
+ elif OS.has_feature("debug"):
+ # Debug export template build.
+ release_string = "debug"
+ else:
+ # Release export template build.
+ release_string = "release"
+
+ var rendering_method := str(ProjectSettings.get_setting_with_override("rendering/renderer/rendering_method"))
+ var rendering_driver := str(ProjectSettings.get_setting_with_override("rendering/rendering_device/driver"))
+ var graphics_api_string := rendering_driver
+ if rendering_method != "gl_compatibility":
+ if rendering_driver == "d3d12":
+ graphics_api_string = "Direct3D 12"
+ elif rendering_driver == "metal":
+ graphics_api_string = "Metal"
+ elif rendering_driver == "vulkan":
+ if OS.has_feature("macos") or OS.has_feature("ios"):
+ graphics_api_string = "Vulkan via MoltenVK"
+ else:
+ graphics_api_string = "Vulkan"
+ else:
+ if rendering_driver == "opengl3_angle":
+ graphics_api_string = "OpenGL via ANGLE"
+ elif OS.has_feature("mobile") or rendering_driver == "opengl3_es":
+ graphics_api_string = "OpenGL ES"
+ elif OS.has_feature("web"):
+ graphics_api_string = "WebGL"
+ elif rendering_driver == "opengl3":
+ graphics_api_string = "OpenGL"
+
+ information.text = (
+ "%s, %d threads\n" % [OS.get_processor_name().replace("(R)", "").replace("(TM)", ""), OS.get_processor_count()]
+ + "%s %s (%s %s), %s %s\n" % [OS.get_name(), "64-bit" if OS.has_feature("64") else "32-bit", release_string, "double" if OS.has_feature("double") else "single", graphics_api_string, RenderingServer.get_video_adapter_api_version()]
+ + "%s, %s" % [adapter_string, driver_info_string]
+ )
+
+
+func _fps_graph_draw() -> void:
+ var fps_polyline := PackedVector2Array()
+ fps_polyline.resize(HISTORY_NUM_FRAMES)
+ for fps_index in fps_history.size():
+ fps_polyline[fps_index] = Vector2(
+ remap(fps_index, 0, fps_history.size(), 0, GRAPH_SIZE.x),
+ remap(clampf(fps_history[fps_index], GRAPH_MIN_FPS, GRAPH_MAX_FPS), GRAPH_MIN_FPS, GRAPH_MAX_FPS, GRAPH_SIZE.y, 0.0)
+ )
+ # Don't use antialiasing to speed up line drawing, but use a width that scales with
+ # viewport scale to keep the line easily readable on hiDPI displays.
+ fps_graph.draw_polyline(fps_polyline, frame_time_gradient.sample(remap(frames_per_second, GRAPH_MIN_FPS, GRAPH_MAX_FPS, 0.0, 1.0)), 1.0)
+
+
+func _total_graph_draw() -> void:
+ var total_polyline := PackedVector2Array()
+ total_polyline.resize(HISTORY_NUM_FRAMES)
+ for total_index in frame_history_total.size():
+ total_polyline[total_index] = Vector2(
+ remap(total_index, 0, frame_history_total.size(), 0, GRAPH_SIZE.x),
+ remap(clampf(frame_history_total[total_index], GRAPH_MIN_FPS, GRAPH_MAX_FPS), GRAPH_MIN_FPS, GRAPH_MAX_FPS, GRAPH_SIZE.y, 0.0)
+ )
+ # Don't use antialiasing to speed up line drawing, but use a width that scales with
+ # viewport scale to keep the line easily readable on hiDPI displays.
+ total_graph.draw_polyline(total_polyline, frame_time_gradient.sample(remap(1000.0 / frametime_avg, GRAPH_MIN_FPS, GRAPH_MAX_FPS, 0.0, 1.0)), 1.0)
+
+
+func _cpu_graph_draw() -> void:
+ var cpu_polyline := PackedVector2Array()
+ cpu_polyline.resize(HISTORY_NUM_FRAMES)
+ for cpu_index in frame_history_cpu.size():
+ cpu_polyline[cpu_index] = Vector2(
+ remap(cpu_index, 0, frame_history_cpu.size(), 0, GRAPH_SIZE.x),
+ remap(clampf(frame_history_cpu[cpu_index], GRAPH_MIN_FPS, GRAPH_MAX_FPS), GRAPH_MIN_FPS, GRAPH_MAX_FPS, GRAPH_SIZE.y, 0.0)
+ )
+ # Don't use antialiasing to speed up line drawing, but use a width that scales with
+ # viewport scale to keep the line easily readable on hiDPI displays.
+ cpu_graph.draw_polyline(cpu_polyline, frame_time_gradient.sample(remap(1000.0 / frametime_cpu_avg, GRAPH_MIN_FPS, GRAPH_MAX_FPS, 0.0, 1.0)), 1.0)
+
+
+func _gpu_graph_draw() -> void:
+ var gpu_polyline := PackedVector2Array()
+ gpu_polyline.resize(HISTORY_NUM_FRAMES)
+ for gpu_index in frame_history_gpu.size():
+ gpu_polyline[gpu_index] = Vector2(
+ remap(gpu_index, 0, frame_history_gpu.size(), 0, GRAPH_SIZE.x),
+ remap(clampf(frame_history_gpu[gpu_index], GRAPH_MIN_FPS, GRAPH_MAX_FPS), GRAPH_MIN_FPS, GRAPH_MAX_FPS, GRAPH_SIZE.y, 0.0)
+ )
+ # Don't use antialiasing to speed up line drawing, but use a width that scales with
+ # viewport scale to keep the line easily readable on hiDPI displays.
+ gpu_graph.draw_polyline(gpu_polyline, frame_time_gradient.sample(remap(1000.0 / frametime_gpu_avg, GRAPH_MIN_FPS, GRAPH_MAX_FPS, 0.0, 1.0)), 1.0)
+
+
+func _process(_delta: float) -> void:
+ if visible:
+ fps_graph.queue_redraw()
+ total_graph.queue_redraw()
+ cpu_graph.queue_redraw()
+ gpu_graph.queue_redraw()
+
+ # Difference between the last two rendered frames in milliseconds.
+ var frametime := (Time.get_ticks_usec() - last_tick) * 0.001
+
+ frame_history_total.push_back(frametime)
+ if frame_history_total.size() > HISTORY_NUM_FRAMES:
+ frame_history_total.pop_front()
+
+ # Frametimes are colored following FPS logic (red = 10 FPS, yellow = 60 FPS, green = 110 FPS, cyan = 160 FPS).
+ # This makes the color gradient non-linear.
+ frametime_avg = frame_history_total.reduce(sum_func) / frame_history_total.size()
+ frame_history_total_avg.text = str(frametime_avg).pad_decimals(2)
+ frame_history_total_avg.modulate = frame_time_gradient.sample(remap(1000.0 / frametime_avg, GRAPH_MIN_FPS, GRAPH_MAX_FPS, 0.0, 1.0))
+
+ var frametime_min: float = frame_history_total.min()
+ frame_history_total_min.text = str(frametime_min).pad_decimals(2)
+ frame_history_total_min.modulate = frame_time_gradient.sample(remap(1000.0 / frametime_min, GRAPH_MIN_FPS, GRAPH_MAX_FPS, 0.0, 1.0))
+
+ var frametime_max: float = frame_history_total.max()
+ frame_history_total_max.text = str(frametime_max).pad_decimals(2)
+ frame_history_total_max.modulate = frame_time_gradient.sample(remap(1000.0 / frametime_max, GRAPH_MIN_FPS, GRAPH_MAX_FPS, 0.0, 1.0))
+
+ frame_history_total_last.text = str(frametime).pad_decimals(2)
+ frame_history_total_last.modulate = frame_time_gradient.sample(remap(1000.0 / frametime, GRAPH_MIN_FPS, GRAPH_MAX_FPS, 0.0, 1.0))
+
+ var viewport_rid := get_viewport().get_viewport_rid()
+ var frametime_cpu := RenderingServer.viewport_get_measured_render_time_cpu(viewport_rid) + RenderingServer.get_frame_setup_time_cpu()
+ frame_history_cpu.push_back(frametime_cpu)
+ if frame_history_cpu.size() > HISTORY_NUM_FRAMES:
+ frame_history_cpu.pop_front()
+
+ frametime_cpu_avg = frame_history_cpu.reduce(sum_func) / frame_history_cpu.size()
+ frame_history_cpu_avg.text = str(frametime_cpu_avg).pad_decimals(2)
+ frame_history_cpu_avg.modulate = frame_time_gradient.sample(remap(1000.0 / frametime_cpu_avg, GRAPH_MIN_FPS, GRAPH_MAX_FPS, 0.0, 1.0))
+
+ var frametime_cpu_min: float = frame_history_cpu.min()
+ frame_history_cpu_min.text = str(frametime_cpu_min).pad_decimals(2)
+ frame_history_cpu_min.modulate = frame_time_gradient.sample(remap(1000.0 / frametime_cpu_min, GRAPH_MIN_FPS, GRAPH_MAX_FPS, 0.0, 1.0))
+
+ var frametime_cpu_max: float = frame_history_cpu.max()
+ frame_history_cpu_max.text = str(frametime_cpu_max).pad_decimals(2)
+ frame_history_cpu_max.modulate = frame_time_gradient.sample(remap(1000.0 / frametime_cpu_max, GRAPH_MIN_FPS, GRAPH_MAX_FPS, 0.0, 1.0))
+
+ frame_history_cpu_last.text = str(frametime_cpu).pad_decimals(2)
+ frame_history_cpu_last.modulate = frame_time_gradient.sample(remap(1000.0 / frametime_cpu, GRAPH_MIN_FPS, GRAPH_MAX_FPS, 0.0, 1.0))
+
+ var frametime_gpu := RenderingServer.viewport_get_measured_render_time_gpu(viewport_rid)
+ frame_history_gpu.push_back(frametime_gpu)
+ if frame_history_gpu.size() > HISTORY_NUM_FRAMES:
+ frame_history_gpu.pop_front()
+
+ frametime_gpu_avg = frame_history_gpu.reduce(sum_func) / frame_history_gpu.size()
+ frame_history_gpu_avg.text = str(frametime_gpu_avg).pad_decimals(2)
+ frame_history_gpu_avg.modulate = frame_time_gradient.sample(remap(1000.0 / frametime_gpu_avg, GRAPH_MIN_FPS, GRAPH_MAX_FPS, 0.0, 1.0))
+
+ var frametime_gpu_min: float = frame_history_gpu.min()
+ frame_history_gpu_min.text = str(frametime_gpu_min).pad_decimals(2)
+ frame_history_gpu_min.modulate = frame_time_gradient.sample(remap(1000.0 / frametime_gpu_min, GRAPH_MIN_FPS, GRAPH_MAX_FPS, 0.0, 1.0))
+
+ var frametime_gpu_max: float = frame_history_gpu.max()
+ frame_history_gpu_max.text = str(frametime_gpu_max).pad_decimals(2)
+ frame_history_gpu_max.modulate = frame_time_gradient.sample(remap(1000.0 / frametime_gpu_max, GRAPH_MIN_FPS, GRAPH_MAX_FPS, 0.0, 1.0))
+
+ frame_history_gpu_last.text = str(frametime_gpu).pad_decimals(2)
+ frame_history_gpu_last.modulate = frame_time_gradient.sample(remap(1000.0 / frametime_gpu, GRAPH_MIN_FPS, GRAPH_MAX_FPS, 0.0, 1.0))
+
+ frames_per_second = 1000.0 / frametime_avg
+ fps_history.push_back(frames_per_second)
+ if fps_history.size() > HISTORY_NUM_FRAMES:
+ fps_history.pop_front()
+
+ fps.text = str(floor(frames_per_second)) + " FPS"
+ var frame_time_color := frame_time_gradient.sample(remap(frames_per_second, GRAPH_MIN_FPS, GRAPH_MAX_FPS, 0.0, 1.0))
+ fps.modulate = frame_time_color
+
+ frame_time.text = str(frametime).pad_decimals(2) + " mspf"
+ frame_time.modulate = frame_time_color
+
+ var vsync_string := ""
+ match DisplayServer.window_get_vsync_mode():
+ DisplayServer.VSYNC_ENABLED:
+ vsync_string = "V-Sync"
+ DisplayServer.VSYNC_ADAPTIVE:
+ vsync_string = "Adaptive V-Sync"
+ DisplayServer.VSYNC_MAILBOX:
+ vsync_string = "Mailbox V-Sync"
+
+ if Engine.max_fps > 0 or OS.low_processor_usage_mode:
+ # Display FPS cap determined by `Engine.max_fps` or low-processor usage mode sleep duration
+ # (the lowest FPS cap is used).
+ var low_processor_max_fps := roundi(1000000.0 / OS.low_processor_usage_mode_sleep_usec)
+ var fps_cap := low_processor_max_fps
+ if Engine.max_fps > 0:
+ fps_cap = mini(Engine.max_fps, low_processor_max_fps)
+ frame_time.text += " (cap: " + str(fps_cap) + " FPS"
+
+ if not vsync_string.is_empty():
+ frame_time.text += " + " + vsync_string
+
+ frame_time.text += ")"
+ else:
+ if not vsync_string.is_empty():
+ frame_time.text += " (" + vsync_string + ")"
+
+ frame_number.text = "Frame: " + str(Engine.get_frames_drawn())
+
+ last_tick = Time.get_ticks_usec()
+
+
+func _on_visibility_changed() -> void:
+ if visible:
+ # Reset graphs to prevent them from looking strange before `HISTORY_NUM_FRAMES` frames
+ # have been drawn.
+ var frametime_last := (Time.get_ticks_usec() - last_tick) * 0.001
+ fps_history.resize(HISTORY_NUM_FRAMES)
+ fps_history.fill(1000.0 / frametime_last)
+ frame_history_total.resize(HISTORY_NUM_FRAMES)
+ frame_history_total.fill(frametime_last)
+ frame_history_cpu.resize(HISTORY_NUM_FRAMES)
+ var viewport_rid := get_viewport().get_viewport_rid()
+ frame_history_cpu.fill(RenderingServer.viewport_get_measured_render_time_cpu(viewport_rid) + RenderingServer.get_frame_setup_time_cpu())
+ frame_history_gpu.resize(HISTORY_NUM_FRAMES)
+ frame_history_gpu.fill(RenderingServer.viewport_get_measured_render_time_gpu(viewport_rid))
diff --git a/addons/debug_menu/debug_menu.gd.uid b/addons/debug_menu/debug_menu.gd.uid
new file mode 100644
index 0000000..170cac6
--- /dev/null
+++ b/addons/debug_menu/debug_menu.gd.uid
@@ -0,0 +1 @@
+uid://bpsslluwe827a
diff --git a/addons/debug_menu/debug_menu.tscn b/addons/debug_menu/debug_menu.tscn
new file mode 100644
index 0000000..9bfc9d6
--- /dev/null
+++ b/addons/debug_menu/debug_menu.tscn
@@ -0,0 +1,401 @@
+[gd_scene load_steps=3 format=3 uid="uid://cggqb75a8w8r"]
+
+[ext_resource type="Script" path="res://addons/debug_menu/debug_menu.gd" id="1_p440y"]
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ki0n8"]
+bg_color = Color(0, 0, 0, 0.25098)
+
+[node name="CanvasLayer" type="CanvasLayer" node_paths=PackedStringArray("fps", "frame_time", "frame_number", "frame_history_total_avg", "frame_history_total_min", "frame_history_total_max", "frame_history_total_last", "frame_history_cpu_avg", "frame_history_cpu_min", "frame_history_cpu_max", "frame_history_cpu_last", "frame_history_gpu_avg", "frame_history_gpu_min", "frame_history_gpu_max", "frame_history_gpu_last", "fps_graph", "total_graph", "cpu_graph", "gpu_graph", "information", "settings")]
+layer = 128
+script = ExtResource("1_p440y")
+fps = NodePath("DebugMenu/VBoxContainer/FPS")
+frame_time = NodePath("DebugMenu/VBoxContainer/FrameTime")
+frame_number = NodePath("DebugMenu/VBoxContainer/FrameNumber")
+frame_history_total_avg = NodePath("DebugMenu/VBoxContainer/FrameTimeHistory/TotalAvg")
+frame_history_total_min = NodePath("DebugMenu/VBoxContainer/FrameTimeHistory/TotalMin")
+frame_history_total_max = NodePath("DebugMenu/VBoxContainer/FrameTimeHistory/TotalMax")
+frame_history_total_last = NodePath("DebugMenu/VBoxContainer/FrameTimeHistory/TotalLast")
+frame_history_cpu_avg = NodePath("DebugMenu/VBoxContainer/FrameTimeHistory/CPUAvg")
+frame_history_cpu_min = NodePath("DebugMenu/VBoxContainer/FrameTimeHistory/CPUMin")
+frame_history_cpu_max = NodePath("DebugMenu/VBoxContainer/FrameTimeHistory/CPUMax")
+frame_history_cpu_last = NodePath("DebugMenu/VBoxContainer/FrameTimeHistory/CPULast")
+frame_history_gpu_avg = NodePath("DebugMenu/VBoxContainer/FrameTimeHistory/GPUAvg")
+frame_history_gpu_min = NodePath("DebugMenu/VBoxContainer/FrameTimeHistory/GPUMin")
+frame_history_gpu_max = NodePath("DebugMenu/VBoxContainer/FrameTimeHistory/GPUMax")
+frame_history_gpu_last = NodePath("DebugMenu/VBoxContainer/FrameTimeHistory/GPULast")
+fps_graph = NodePath("DebugMenu/VBoxContainer/FPSGraph/Graph")
+total_graph = NodePath("DebugMenu/VBoxContainer/TotalGraph/Graph")
+cpu_graph = NodePath("DebugMenu/VBoxContainer/CPUGraph/Graph")
+gpu_graph = NodePath("DebugMenu/VBoxContainer/GPUGraph/Graph")
+information = NodePath("DebugMenu/VBoxContainer/Information")
+settings = NodePath("DebugMenu/VBoxContainer/Settings")
+
+[node name="DebugMenu" type="Control" parent="."]
+custom_minimum_size = Vector2(400, 400)
+layout_mode = 3
+anchors_preset = 1
+anchor_left = 1.0
+anchor_right = 1.0
+offset_left = -416.0
+offset_top = 8.0
+offset_right = -16.0
+offset_bottom = 408.0
+grow_horizontal = 0
+size_flags_horizontal = 8
+size_flags_vertical = 4
+mouse_filter = 2
+
+[node name="VBoxContainer" type="VBoxContainer" parent="DebugMenu"]
+layout_mode = 1
+anchors_preset = 1
+anchor_left = 1.0
+anchor_right = 1.0
+offset_left = -300.0
+offset_bottom = 374.0
+grow_horizontal = 0
+mouse_filter = 2
+theme_override_constants/separation = 0
+
+[node name="FPS" type="Label" parent="DebugMenu/VBoxContainer"]
+modulate = Color(0, 1, 0, 1)
+layout_mode = 2
+theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
+theme_override_constants/outline_size = 5
+theme_override_constants/line_spacing = 0
+theme_override_font_sizes/font_size = 18
+text = "60 FPS"
+horizontal_alignment = 2
+
+[node name="FrameTime" type="Label" parent="DebugMenu/VBoxContainer"]
+modulate = Color(0, 1, 0, 1)
+layout_mode = 2
+theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
+theme_override_constants/outline_size = 3
+theme_override_font_sizes/font_size = 12
+text = "16.67 mspf (cap: 123 FPS + Adaptive V-Sync)"
+horizontal_alignment = 2
+
+[node name="FrameNumber" type="Label" parent="DebugMenu/VBoxContainer"]
+layout_mode = 2
+theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
+theme_override_constants/outline_size = 3
+theme_override_font_sizes/font_size = 12
+text = "Frame: 1234"
+horizontal_alignment = 2
+
+[node name="FrameTimeHistory" type="GridContainer" parent="DebugMenu/VBoxContainer"]
+layout_mode = 2
+size_flags_horizontal = 8
+mouse_filter = 2
+theme_override_constants/h_separation = 0
+theme_override_constants/v_separation = 0
+columns = 5
+
+[node name="Spacer" type="Control" parent="DebugMenu/VBoxContainer/FrameTimeHistory"]
+custom_minimum_size = Vector2(60, 0)
+layout_mode = 2
+mouse_filter = 2
+
+[node name="AvgHeader" type="Label" parent="DebugMenu/VBoxContainer/FrameTimeHistory"]
+custom_minimum_size = Vector2(50, 0)
+layout_mode = 2
+theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
+theme_override_constants/outline_size = 3
+theme_override_font_sizes/font_size = 12
+text = "Average"
+horizontal_alignment = 2
+
+[node name="MinHeader" type="Label" parent="DebugMenu/VBoxContainer/FrameTimeHistory"]
+custom_minimum_size = Vector2(50, 0)
+layout_mode = 2
+theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
+theme_override_constants/outline_size = 3
+theme_override_font_sizes/font_size = 12
+text = "Best"
+horizontal_alignment = 2
+
+[node name="MaxHeader" type="Label" parent="DebugMenu/VBoxContainer/FrameTimeHistory"]
+custom_minimum_size = Vector2(50, 0)
+layout_mode = 2
+theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
+theme_override_constants/outline_size = 3
+theme_override_font_sizes/font_size = 12
+text = "Worst"
+horizontal_alignment = 2
+
+[node name="LastHeader" type="Label" parent="DebugMenu/VBoxContainer/FrameTimeHistory"]
+custom_minimum_size = Vector2(50, 0)
+layout_mode = 2
+theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
+theme_override_constants/outline_size = 3
+theme_override_font_sizes/font_size = 12
+text = "Last"
+horizontal_alignment = 2
+
+[node name="TotalHeader" type="Label" parent="DebugMenu/VBoxContainer/FrameTimeHistory"]
+custom_minimum_size = Vector2(50, 0)
+layout_mode = 2
+theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
+theme_override_constants/outline_size = 3
+theme_override_font_sizes/font_size = 12
+text = "Total:"
+horizontal_alignment = 2
+
+[node name="TotalAvg" type="Label" parent="DebugMenu/VBoxContainer/FrameTimeHistory"]
+modulate = Color(0, 1, 0, 1)
+custom_minimum_size = Vector2(50, 0)
+layout_mode = 2
+theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
+theme_override_constants/outline_size = 3
+theme_override_font_sizes/font_size = 12
+text = "123.45"
+horizontal_alignment = 2
+
+[node name="TotalMin" type="Label" parent="DebugMenu/VBoxContainer/FrameTimeHistory"]
+modulate = Color(0, 1, 0, 1)
+custom_minimum_size = Vector2(50, 0)
+layout_mode = 2
+theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
+theme_override_constants/outline_size = 3
+theme_override_font_sizes/font_size = 12
+text = "123.45"
+horizontal_alignment = 2
+
+[node name="TotalMax" type="Label" parent="DebugMenu/VBoxContainer/FrameTimeHistory"]
+modulate = Color(0, 1, 0, 1)
+custom_minimum_size = Vector2(50, 0)
+layout_mode = 2
+theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
+theme_override_constants/outline_size = 3
+theme_override_font_sizes/font_size = 12
+text = "123.45"
+horizontal_alignment = 2
+
+[node name="TotalLast" type="Label" parent="DebugMenu/VBoxContainer/FrameTimeHistory"]
+modulate = Color(0, 1, 0, 1)
+custom_minimum_size = Vector2(50, 0)
+layout_mode = 2
+theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
+theme_override_constants/outline_size = 3
+theme_override_font_sizes/font_size = 12
+text = "123.45"
+horizontal_alignment = 2
+
+[node name="CPUHeader" type="Label" parent="DebugMenu/VBoxContainer/FrameTimeHistory"]
+custom_minimum_size = Vector2(50, 0)
+layout_mode = 2
+theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
+theme_override_constants/outline_size = 3
+theme_override_font_sizes/font_size = 12
+text = "CPU:"
+horizontal_alignment = 2
+
+[node name="CPUAvg" type="Label" parent="DebugMenu/VBoxContainer/FrameTimeHistory"]
+modulate = Color(0, 1, 0, 1)
+custom_minimum_size = Vector2(50, 0)
+layout_mode = 2
+theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
+theme_override_constants/outline_size = 3
+theme_override_font_sizes/font_size = 12
+text = "123.45"
+horizontal_alignment = 2
+
+[node name="CPUMin" type="Label" parent="DebugMenu/VBoxContainer/FrameTimeHistory"]
+modulate = Color(0, 1, 0, 1)
+custom_minimum_size = Vector2(50, 0)
+layout_mode = 2
+theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
+theme_override_constants/outline_size = 3
+theme_override_font_sizes/font_size = 12
+text = "12.34"
+horizontal_alignment = 2
+
+[node name="CPUMax" type="Label" parent="DebugMenu/VBoxContainer/FrameTimeHistory"]
+modulate = Color(0, 1, 0, 1)
+custom_minimum_size = Vector2(50, 0)
+layout_mode = 2
+theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
+theme_override_constants/outline_size = 3
+theme_override_font_sizes/font_size = 12
+text = "123.45"
+horizontal_alignment = 2
+
+[node name="CPULast" type="Label" parent="DebugMenu/VBoxContainer/FrameTimeHistory"]
+modulate = Color(0, 1, 0, 1)
+custom_minimum_size = Vector2(50, 0)
+layout_mode = 2
+theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
+theme_override_constants/outline_size = 3
+theme_override_font_sizes/font_size = 12
+text = "123.45"
+horizontal_alignment = 2
+
+[node name="GPUHeader" type="Label" parent="DebugMenu/VBoxContainer/FrameTimeHistory"]
+custom_minimum_size = Vector2(50, 0)
+layout_mode = 2
+theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
+theme_override_constants/outline_size = 3
+theme_override_font_sizes/font_size = 12
+text = "GPU:"
+horizontal_alignment = 2
+
+[node name="GPUAvg" type="Label" parent="DebugMenu/VBoxContainer/FrameTimeHistory"]
+modulate = Color(0, 1, 0, 1)
+custom_minimum_size = Vector2(50, 0)
+layout_mode = 2
+theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
+theme_override_constants/outline_size = 3
+theme_override_font_sizes/font_size = 12
+text = "123.45"
+horizontal_alignment = 2
+
+[node name="GPUMin" type="Label" parent="DebugMenu/VBoxContainer/FrameTimeHistory"]
+modulate = Color(0, 1, 0, 1)
+custom_minimum_size = Vector2(50, 0)
+layout_mode = 2
+theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
+theme_override_constants/outline_size = 3
+theme_override_font_sizes/font_size = 12
+text = "1.23"
+horizontal_alignment = 2
+
+[node name="GPUMax" type="Label" parent="DebugMenu/VBoxContainer/FrameTimeHistory"]
+modulate = Color(0, 1, 0, 1)
+custom_minimum_size = Vector2(50, 0)
+layout_mode = 2
+theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
+theme_override_constants/outline_size = 3
+theme_override_font_sizes/font_size = 12
+text = "123.45"
+horizontal_alignment = 2
+
+[node name="GPULast" type="Label" parent="DebugMenu/VBoxContainer/FrameTimeHistory"]
+modulate = Color(0, 1, 0, 1)
+custom_minimum_size = Vector2(50, 0)
+layout_mode = 2
+theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
+theme_override_constants/outline_size = 3
+theme_override_font_sizes/font_size = 12
+text = "123.45"
+horizontal_alignment = 2
+
+[node name="FPSGraph" type="HBoxContainer" parent="DebugMenu/VBoxContainer"]
+layout_mode = 2
+mouse_filter = 2
+alignment = 2
+
+[node name="Title" type="Label" parent="DebugMenu/VBoxContainer/FPSGraph"]
+custom_minimum_size = Vector2(0, 27)
+layout_mode = 2
+size_flags_horizontal = 8
+theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
+theme_override_constants/outline_size = 3
+theme_override_font_sizes/font_size = 12
+text = "FPS: ↑"
+vertical_alignment = 1
+
+[node name="Graph" type="Panel" parent="DebugMenu/VBoxContainer/FPSGraph"]
+custom_minimum_size = Vector2(150, 25)
+layout_mode = 2
+size_flags_vertical = 0
+mouse_filter = 2
+theme_override_styles/panel = SubResource("StyleBoxFlat_ki0n8")
+
+[node name="TotalGraph" type="HBoxContainer" parent="DebugMenu/VBoxContainer"]
+layout_mode = 2
+mouse_filter = 2
+alignment = 2
+
+[node name="Title" type="Label" parent="DebugMenu/VBoxContainer/TotalGraph"]
+custom_minimum_size = Vector2(0, 27)
+layout_mode = 2
+size_flags_horizontal = 8
+theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
+theme_override_constants/outline_size = 3
+theme_override_font_sizes/font_size = 12
+text = "Total: ↓"
+vertical_alignment = 1
+
+[node name="Graph" type="Panel" parent="DebugMenu/VBoxContainer/TotalGraph"]
+custom_minimum_size = Vector2(150, 25)
+layout_mode = 2
+size_flags_vertical = 0
+mouse_filter = 2
+theme_override_styles/panel = SubResource("StyleBoxFlat_ki0n8")
+
+[node name="CPUGraph" type="HBoxContainer" parent="DebugMenu/VBoxContainer"]
+layout_mode = 2
+mouse_filter = 2
+alignment = 2
+
+[node name="Title" type="Label" parent="DebugMenu/VBoxContainer/CPUGraph"]
+custom_minimum_size = Vector2(0, 27)
+layout_mode = 2
+size_flags_horizontal = 8
+theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
+theme_override_constants/outline_size = 3
+theme_override_font_sizes/font_size = 12
+text = "CPU: ↓"
+vertical_alignment = 1
+
+[node name="Graph" type="Panel" parent="DebugMenu/VBoxContainer/CPUGraph"]
+custom_minimum_size = Vector2(150, 25)
+layout_mode = 2
+size_flags_vertical = 0
+mouse_filter = 2
+theme_override_styles/panel = SubResource("StyleBoxFlat_ki0n8")
+
+[node name="GPUGraph" type="HBoxContainer" parent="DebugMenu/VBoxContainer"]
+layout_mode = 2
+mouse_filter = 2
+alignment = 2
+
+[node name="Title" type="Label" parent="DebugMenu/VBoxContainer/GPUGraph"]
+custom_minimum_size = Vector2(0, 27)
+layout_mode = 2
+size_flags_horizontal = 8
+theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
+theme_override_constants/outline_size = 3
+theme_override_font_sizes/font_size = 12
+text = "GPU: ↓"
+vertical_alignment = 1
+
+[node name="Graph" type="Panel" parent="DebugMenu/VBoxContainer/GPUGraph"]
+custom_minimum_size = Vector2(150, 25)
+layout_mode = 2
+size_flags_vertical = 0
+mouse_filter = 2
+theme_override_styles/panel = SubResource("StyleBoxFlat_ki0n8")
+
+[node name="Information" type="Label" parent="DebugMenu/VBoxContainer"]
+modulate = Color(1, 1, 1, 0.752941)
+layout_mode = 2
+theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
+theme_override_constants/outline_size = 3
+theme_override_font_sizes/font_size = 12
+text = "12th Gen Intel(R) Core(TM) i0-1234K
+Windows 12 64-bit (double precision), Vulkan 1.2.34
+NVIDIA GeForce RTX 1234, 123.45.67"
+horizontal_alignment = 2
+
+[node name="Settings" type="Label" parent="DebugMenu/VBoxContainer"]
+modulate = Color(0.8, 0.84, 1, 0.752941)
+layout_mode = 2
+theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
+theme_override_constants/outline_size = 3
+theme_override_font_sizes/font_size = 12
+text = "Project Version: 1.2.3
+Rendering Method: Forward+
+Window: 1234×567, Viewport: 1234×567
+3D Scale (FSR 1.0): 100% = 1234×567
+3D Antialiasing: TAA + 2× MSAA + FXAA
+SSR: 123 Steps
+SSAO: On
+SSIL: On
+SDFGI: 1 Cascades
+Glow: On
+Volumetric Fog: On
+2D Antialiasing: 2× MSAA"
+horizontal_alignment = 2
+
+[connection signal="visibility_changed" from="." to="." method="_on_visibility_changed"]
diff --git a/addons/debug_menu/plugin.cfg b/addons/debug_menu/plugin.cfg
new file mode 100644
index 0000000..54100f7
--- /dev/null
+++ b/addons/debug_menu/plugin.cfg
@@ -0,0 +1,7 @@
+[plugin]
+
+name="Debug Menu"
+description="In-game debug menu displaying performance metrics and hardware information"
+author="Calinou"
+version="1.2.0"
+script="plugin.gd"
diff --git a/addons/debug_menu/plugin.gd b/addons/debug_menu/plugin.gd
new file mode 100644
index 0000000..5ec132e
--- /dev/null
+++ b/addons/debug_menu/plugin.gd
@@ -0,0 +1,29 @@
+@tool
+extends EditorPlugin
+
+func _enter_tree() -> void:
+ add_autoload_singleton("DebugMenu", "res://addons/debug_menu/debug_menu.tscn")
+
+ # FIXME: This appears to do nothing.
+# if not ProjectSettings.has_setting("application/config/version"):
+# ProjectSettings.set_setting("application/config/version", "1.0.0")
+#
+# ProjectSettings.set_initial_value("application/config/version", "1.0.0")
+# ProjectSettings.add_property_info({
+# name = "application/config/version",
+# type = TYPE_STRING,
+# })
+#
+# if not InputMap.has_action("cycle_debug_menu"):
+# InputMap.add_action("cycle_debug_menu")
+# var event := InputEventKey.new()
+# event.keycode = KEY_F3
+# InputMap.action_add_event("cycle_debug_menu", event)
+#
+# ProjectSettings.save()
+
+
+func _exit_tree() -> void:
+ remove_autoload_singleton("DebugMenu")
+ # Don't remove the project setting's value and input map action,
+ # as the plugin may be re-enabled in the future.
diff --git a/addons/debug_menu/plugin.gd.uid b/addons/debug_menu/plugin.gd.uid
new file mode 100644
index 0000000..24ac21b
--- /dev/null
+++ b/addons/debug_menu/plugin.gd.uid
@@ -0,0 +1 @@
+uid://bloj6h52fp002
diff --git a/assets/projectiles/basic_projectile.tscn b/assets/projectiles/basic_projectile.tscn
new file mode 100644
index 0000000..f37b077
--- /dev/null
+++ b/assets/projectiles/basic_projectile.tscn
@@ -0,0 +1,20 @@
+[gd_scene load_steps=4 format=3 uid="uid://d2q0gh76v2wjm"]
+
+[ext_resource type="Script" uid="uid://d5tiwy16ivu6" path="res://player/weapons/projectile.gd" id="1_4urkw"]
+[ext_resource type="Texture2D" uid="uid://b82eovf1htp4i" path="res://assets/sprites/characters/pink/Rock1.png" id="2_5842l"]
+
+[sub_resource type="CircleShape2D" id="CircleShape2D_o4f18"]
+radius = 1.0
+
+[node name="BasicProjectile" type="Area2D"]
+collision_layer = 16
+collision_mask = 8
+script = ExtResource("1_4urkw")
+
+[node name="CollisionShape2D" type="CollisionShape2D" parent="."]
+shape = SubResource("CircleShape2D_o4f18")
+
+[node name="Sprite2D" type="Sprite2D" parent="."]
+position = Vector2(5.96046e-08, 5.96046e-08)
+scale = Vector2(0.3125, 0.3125)
+texture = ExtResource("2_5842l")
diff --git a/assets/sprites/enemies/slimes_blue.png b/assets/sprites/enemies/slimes_blue.png
new file mode 100644
index 0000000..4dd1aa1
Binary files /dev/null and b/assets/sprites/enemies/slimes_blue.png differ
diff --git a/assets/sprites/enemies/slimes_blue.png.import b/assets/sprites/enemies/slimes_blue.png.import
new file mode 100644
index 0000000..95ac8f9
--- /dev/null
+++ b/assets/sprites/enemies/slimes_blue.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://gan2vkax8nwy"
+path="res://.godot/imported/slimes_blue.png-714717b05c2104cf50fc9fd6f4afe1c1.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://assets/sprites/enemies/slimes_blue.png"
+dest_files=["res://.godot/imported/slimes_blue.png-714717b05c2104cf50fc9fd6f4afe1c1.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
diff --git a/assets/sprites/enemies/slimes_dark.png b/assets/sprites/enemies/slimes_dark.png
new file mode 100644
index 0000000..e7ae015
Binary files /dev/null and b/assets/sprites/enemies/slimes_dark.png differ
diff --git a/assets/sprites/enemies/slimes_dark.png.import b/assets/sprites/enemies/slimes_dark.png.import
new file mode 100644
index 0000000..7be9f93
--- /dev/null
+++ b/assets/sprites/enemies/slimes_dark.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://ievi6pon3ptw"
+path="res://.godot/imported/slimes_dark.png-b89c4cb22ec4d8d8ca0b6255a8a99a81.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://assets/sprites/enemies/slimes_dark.png"
+dest_files=["res://.godot/imported/slimes_dark.png-b89c4cb22ec4d8d8ca0b6255a8a99a81.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
diff --git a/assets/sprites/enemies/slimes_green.png b/assets/sprites/enemies/slimes_green.png
new file mode 100644
index 0000000..43ae589
Binary files /dev/null and b/assets/sprites/enemies/slimes_green.png differ
diff --git a/assets/sprites/enemies/slimes_green.png.import b/assets/sprites/enemies/slimes_green.png.import
new file mode 100644
index 0000000..11e43ff
--- /dev/null
+++ b/assets/sprites/enemies/slimes_green.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://cqwmi3v81uhmf"
+path="res://.godot/imported/slimes_green.png-08e345c322ad9d9ec7922af9737a7e3b.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://assets/sprites/enemies/slimes_green.png"
+dest_files=["res://.godot/imported/slimes_green.png-08e345c322ad9d9ec7922af9737a7e3b.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
diff --git a/assets/sprites/enemies/slimes_pink.png b/assets/sprites/enemies/slimes_pink.png
new file mode 100644
index 0000000..6c9e6b9
Binary files /dev/null and b/assets/sprites/enemies/slimes_pink.png differ
diff --git a/assets/sprites/enemies/slimes_pink.png.import b/assets/sprites/enemies/slimes_pink.png.import
new file mode 100644
index 0000000..8fa3c6a
--- /dev/null
+++ b/assets/sprites/enemies/slimes_pink.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://d1eqf5k6fywlu"
+path="res://.godot/imported/slimes_pink.png-306435e1f9ca80f46b8658abf39a07a5.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://assets/sprites/enemies/slimes_pink.png"
+dest_files=["res://.godot/imported/slimes_pink.png-306435e1f9ca80f46b8658abf39a07a5.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
diff --git a/assets/sprites/enemies/slimes_white.png b/assets/sprites/enemies/slimes_white.png
new file mode 100644
index 0000000..c1b4949
Binary files /dev/null and b/assets/sprites/enemies/slimes_white.png differ
diff --git a/assets/sprites/enemies/slimes_white.png.import b/assets/sprites/enemies/slimes_white.png.import
new file mode 100644
index 0000000..7ba66fa
--- /dev/null
+++ b/assets/sprites/enemies/slimes_white.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://badj3651o6jc8"
+path="res://.godot/imported/slimes_white.png-fb1b6c0ee38065fb578a69501028a0b7.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://assets/sprites/enemies/slimes_white.png"
+dest_files=["res://.godot/imported/slimes_white.png-fb1b6c0ee38065fb578a69501028a0b7.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
diff --git a/assets/sprites/enemies/slimes_yellow.png b/assets/sprites/enemies/slimes_yellow.png
new file mode 100644
index 0000000..d7814af
Binary files /dev/null and b/assets/sprites/enemies/slimes_yellow.png differ
diff --git a/assets/sprites/enemies/slimes_yellow.png.import b/assets/sprites/enemies/slimes_yellow.png.import
new file mode 100644
index 0000000..b9ccc2e
--- /dev/null
+++ b/assets/sprites/enemies/slimes_yellow.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://cv7txwf7xhngf"
+path="res://.godot/imported/slimes_yellow.png-7184a698ae163f66b11534f79ba701c9.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://assets/sprites/enemies/slimes_yellow.png"
+dest_files=["res://.godot/imported/slimes_yellow.png-7184a698ae163f66b11534f79ba701c9.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
diff --git a/assets/sprites/tilesets/forest.tres b/assets/sprites/tilesets/forest.tres
index 09ff695..e161dc1 100644
--- a/assets/sprites/tilesets/forest.tres
+++ b/assets/sprites/tilesets/forest.tres
@@ -1131,7 +1131,8 @@ texture_region_size = Vector2i(32, 32)
[resource]
tile_size = Vector2i(32, 32)
-physics_layer_0/collision_layer = 4
+physics_layer_0/collision_layer = 2
+physics_layer_0/collision_mask = 9
terrain_set_0/mode = 0
terrain_set_0/terrain_0/name = "Floor"
terrain_set_0/terrain_0/color = Color(0.5, 0.4375, 0.25, 1)
diff --git a/assets/weapons/ranged_weapon.tscn b/assets/weapons/ranged_weapon.tscn
new file mode 100644
index 0000000..7309407
--- /dev/null
+++ b/assets/weapons/ranged_weapon.tscn
@@ -0,0 +1,6 @@
+[gd_scene load_steps=2 format=3 uid="uid://cgxn1f4p4vik6"]
+
+[ext_resource type="Script" uid="uid://dcenqdci4hjes" path="res://player/weapons/ranged_weapon.gd" id="1_x1kyd"]
+
+[node name="RangedWeapon" type="Node2D"]
+script = ExtResource("1_x1kyd")
diff --git a/enemies/scripts/enemy.gd b/enemies/scripts/enemy.gd
new file mode 100644
index 0000000..61510e1
--- /dev/null
+++ b/enemies/scripts/enemy.gd
@@ -0,0 +1 @@
+extends Node
diff --git a/enemies/scripts/enemy.gd.uid b/enemies/scripts/enemy.gd.uid
new file mode 100644
index 0000000..3252d34
--- /dev/null
+++ b/enemies/scripts/enemy.gd.uid
@@ -0,0 +1 @@
+uid://btwllkb7meyrw
diff --git a/enemies/test_enemy.tscn b/enemies/test_enemy.tscn
new file mode 100644
index 0000000..553831d
--- /dev/null
+++ b/enemies/test_enemy.tscn
@@ -0,0 +1,70 @@
+[gd_scene load_steps=10 format=3 uid="uid://cait7d0k1kmsq"]
+
+[ext_resource type="Texture2D" uid="uid://gan2vkax8nwy" path="res://assets/sprites/enemies/slimes_blue.png" id="1_lcl3f"]
+
+[sub_resource type="CircleShape2D" id="CircleShape2D_2bghh"]
+radius = 14.0357
+
+[sub_resource type="AtlasTexture" id="AtlasTexture_8ifd5"]
+atlas = ExtResource("1_lcl3f")
+region = Rect2(0, 0, 46, 33)
+
+[sub_resource type="AtlasTexture" id="AtlasTexture_bkvxi"]
+atlas = ExtResource("1_lcl3f")
+region = Rect2(46, 0, 46, 33)
+
+[sub_resource type="AtlasTexture" id="AtlasTexture_if8y3"]
+atlas = ExtResource("1_lcl3f")
+region = Rect2(92, 0, 46, 33)
+
+[sub_resource type="AtlasTexture" id="AtlasTexture_evp1i"]
+atlas = ExtResource("1_lcl3f")
+region = Rect2(138, 0, 46, 33)
+
+[sub_resource type="AtlasTexture" id="AtlasTexture_38q4i"]
+atlas = ExtResource("1_lcl3f")
+region = Rect2(184, 0, 46, 33)
+
+[sub_resource type="AtlasTexture" id="AtlasTexture_8o875"]
+atlas = ExtResource("1_lcl3f")
+region = Rect2(230, 0, 46, 33)
+
+[sub_resource type="SpriteFrames" id="SpriteFrames_ciurg"]
+animations = [{
+"frames": [{
+"duration": 1.0,
+"texture": SubResource("AtlasTexture_8ifd5")
+}, {
+"duration": 1.0,
+"texture": SubResource("AtlasTexture_bkvxi")
+}, {
+"duration": 1.0,
+"texture": SubResource("AtlasTexture_if8y3")
+}, {
+"duration": 1.0,
+"texture": SubResource("AtlasTexture_evp1i")
+}, {
+"duration": 1.0,
+"texture": SubResource("AtlasTexture_38q4i")
+}, {
+"duration": 1.0,
+"texture": SubResource("AtlasTexture_8o875")
+}],
+"loop": true,
+"name": &"idle",
+"speed": 5.0
+}]
+
+[node name="TestEnemy" type="CharacterBody2D" groups=["enemies"]]
+collision_layer = 8
+collision_mask = 7
+
+[node name="CollisionShape2D" type="CollisionShape2D" parent="."]
+position = Vector2(0, 1)
+shape = SubResource("CircleShape2D_2bghh")
+
+[node name="AnimatedSprite2D" type="AnimatedSprite2D" parent="."]
+sprite_frames = SubResource("SpriteFrames_ciurg")
+animation = &"idle"
+autoplay = "idle"
+frame_progress = 0.531099
diff --git a/map/Map.tscn b/map/Map.tscn
index b4071fe..e98271a 100644
--- a/map/Map.tscn
+++ b/map/Map.tscn
@@ -1,7 +1,8 @@
-[gd_scene load_steps=4 format=3 uid="uid://cfkusqucsap26"]
+[gd_scene load_steps=5 format=3 uid="uid://cfkusqucsap26"]
[ext_resource type="Script" uid="uid://begwu0icmrxyw" path="res://map/map.gd" id="1_l804v"]
[ext_resource type="TileSet" uid="uid://c66l102pgntht" path="res://assets/sprites/tilesets/forest.tres" id="2_3nv2f"]
+[ext_resource type="PackedScene" uid="uid://cait7d0k1kmsq" path="res://enemies/test_enemy.tscn" id="4_raxr4"]
[ext_resource type="PackedScene" uid="uid://bo5aw2cad3akl" path="res://player/player.tscn" id="5_3nv2f"]
[node name="Map" type="Node2D"]
@@ -17,3 +18,7 @@ tile_set = ExtResource("2_3nv2f")
tile_set = ExtResource("2_3nv2f")
[node name="Player" parent="." instance=ExtResource("5_3nv2f")]
+position = Vector2(20, 20)
+
+[node name="TestEnemy" parent="." instance=ExtResource("4_raxr4")]
+position = Vector2(87, 72)
diff --git a/player/modifiers/fire_rate_additive.gd b/player/modifiers/fire_rate_additive.gd
index 9ffdbd8..aa62830 100644
--- a/player/modifiers/fire_rate_additive.gd
+++ b/player/modifiers/fire_rate_additive.gd
@@ -8,6 +8,6 @@ func _init():
description = "Increases fire rate by %0.1f shots per second" % fire_rate_bonus
modifier_type = ModifierType.ADDITIVE
-func apply_stats_modification(final_stats: Dictionary, base_stats: Dictionary) -> void:
+func apply_stats_modification(final_stats: Dictionary, _base_stats: Dictionary) -> void:
if final_stats.has("fire_rate"):
final_stats.fire_rate += fire_rate_bonus
\ No newline at end of file
diff --git a/player/modifiers/fire_rate_multiplicative.gd b/player/modifiers/fire_rate_multiplicative.gd
index 9a7867f..6b9fcf8 100644
--- a/player/modifiers/fire_rate_multiplicative.gd
+++ b/player/modifiers/fire_rate_multiplicative.gd
@@ -8,6 +8,6 @@ func _init():
description = "Increases fire rate by %d%%" % ((fire_rate_multiplier - 1.0) * 100)
modifier_type = ModifierType.MULTIPLICATIVE
-func apply_stats_modification(final_stats: Dictionary, base_stats: Dictionary) -> void:
+func apply_stats_modification(final_stats: Dictionary, _base_stats: Dictionary) -> void:
if final_stats.has("fire_rate"):
final_stats.fire_rate *= fire_rate_multiplier
\ No newline at end of file
diff --git a/player/player.tscn b/player/player.tscn
index 65945b8..4d7e767 100644
--- a/player/player.tscn
+++ b/player/player.tscn
@@ -1,4 +1,4 @@
-[gd_scene load_steps=82 format=3 uid="uid://bo5aw2cad3akl"]
+[gd_scene load_steps=83 format=3 uid="uid://bo5aw2cad3akl"]
[ext_resource type="Script" uid="uid://bq038uo4cm6nv" path="res://player/scripts/player.gd" id="1_oul6g"]
[ext_resource type="Texture2D" uid="uid://dqgq2c1h6yk3k" path="res://assets/sprites/characters/pink/Pink_Monster_Attack1_4.png" id="2_yllr7"]
@@ -13,6 +13,7 @@
[ext_resource type="Texture2D" uid="uid://538sc3bsdell" path="res://assets/sprites/characters/pink/Pink_Monster_Throw_4.png" id="11_bjvpn"]
[ext_resource type="Texture2D" uid="uid://efnfh4mf0ia2" path="res://assets/sprites/characters/pink/Pink_Monster_Walk_6.png" id="12_s7qer"]
[ext_resource type="Texture2D" uid="uid://cyfq0x0h2qeof" path="res://assets/sprites/characters/pink/Pink_Monster_Walk+Attack_6.png" id="13_g4c7l"]
+[ext_resource type="PackedScene" uid="uid://cgxn1f4p4vik6" path="res://assets/weapons/ranged_weapon.tscn" id="14_kb6p2"]
[sub_resource type="CircleShape2D" id="CircleShape2D_rkbax"]
@@ -541,7 +542,7 @@ animations = [{
"speed": 5.0
}]
-[node name="Player" type="CharacterBody2D"]
+[node name="Player" type="CharacterBody2D" groups=["friendly"]]
collision_mask = 14
script = ExtResource("1_oul6g")
@@ -551,8 +552,11 @@ shape = SubResource("CircleShape2D_rkbax")
[node name="PlayerSprite" type="AnimatedSprite2D" parent="."]
sprite_frames = SubResource("SpriteFrames_qjt2w")
-animation = &"attack_2"
-frame_progress = 0.752485
+animation = &"idle"
+autoplay = "idle"
+frame_progress = 0.749332
[node name="Camera2D" type="Camera2D" parent="."]
zoom = Vector2(2, 2)
+
+[node name="RangedWeapon" parent="." instance=ExtResource("14_kb6p2")]
diff --git a/player/scripts/player.gd b/player/scripts/player.gd
index ab1dff0..e893280 100644
--- a/player/scripts/player.gd
+++ b/player/scripts/player.gd
@@ -1,9 +1,12 @@
extends CharacterBody2D
@export var speed = 200
-@export var weapon: RangedWeapon
@export var special_ability: Ability
-@export var movement: PlayerMovement
+
+var weapon: RangedWeapon
+
+var movement: PlayerMovement
+var combat: PlayerCombat
# Last direction for idle state
var last_direction = Vector2.DOWN
@@ -11,19 +14,16 @@ var last_direction = Vector2.DOWN
@onready var animated_sprite = $PlayerSprite
func _ready():
- weapon = RangedWeapon.new()
- Log.pr("Weapon", weapon)
+ weapon = $RangedWeapon
+ combat = PlayerCombat.new()
- # Initialize the movement resource with references
- if movement:
- movement.player = self
- movement.animated_sprite = animated_sprite
- movement.last_direction = Vector2.DOWN # Default direction
- else:
- # Create a new resource instance if none was assigned in the editor
- movement = PlayerMovement.new()
- movement.player = self
- movement.animated_sprite = animated_sprite
+ movement = PlayerMovement.new()
+ movement.player = self
+ movement.animated_sprite = animated_sprite
+ movement.speed = speed
+
+ combat.player = self
+ combat.animated_sprite = animated_sprite
Log.pr("Adding projectile size additive modifier")
weapon.add_modifier(ProjectileSizeAdditive.new())
@@ -37,11 +37,15 @@ func _ready():
# Size is now 1.5 * 1.5 = 2.25
# Add another additive size modifier (+0.7 or 70% increase)
- Log.pr("Adding another projectile size additive modifier", 0.7)
+ Log.pr("Adding another projectile size additive modifier", 2)
var another_size_mod = ProjectileSizeAdditive.new()
another_size_mod.size_increase = 0.7
weapon.add_modifier(another_size_mod)
Log.pr(weapon.stats.get_stat("projectile_size"))
+ weapon.add_modifier(FireRateAdditive.new())
+
+
func _physics_process(delta):
movement.process(delta)
+ combat.process(delta)
diff --git a/player/scripts/player_combat.gd b/player/scripts/player_combat.gd
new file mode 100644
index 0000000..21a25ae
--- /dev/null
+++ b/player/scripts/player_combat.gd
@@ -0,0 +1,30 @@
+extends Resource
+class_name PlayerCombat
+
+var player: CharacterBody2D
+var animated_sprite: AnimatedSprite2D
+
+func process(_delta):
+ # Get mouse position in global coordinates
+ var mouse_position = player.get_global_mouse_position()
+
+ # Get player position (assuming this script is attached to the player)
+ var player_position = player.global_position
+
+ # Calculate direction vector from player to mouse
+ var direction = mouse_position - player_position
+
+ # You can normalize this vector if you want a unit vector (length of 1)
+ # This is useful if you only care about direction, not distance
+ var normalized_direction = direction.normalized()
+
+ if Input.is_action_pressed("fire"):
+ player.weapon.fire(normalized_direction)
+ # Update animation
+ #update_animation()
+
+func update_animation():
+ Log.pr(animated_sprite.animation)
+ if animated_sprite.animation != "throw":
+ Log.pr('Throwing animation!')
+ animated_sprite.play("throw")
diff --git a/player/scripts/player_combat.gd.uid b/player/scripts/player_combat.gd.uid
new file mode 100644
index 0000000..1d95b1c
--- /dev/null
+++ b/player/scripts/player_combat.gd.uid
@@ -0,0 +1 @@
+uid://cx4ugb3hbh8t1
diff --git a/player/scripts/player_movement.gd b/player/scripts/player_movement.gd
index dea2c9e..b1d4f24 100644
--- a/player/scripts/player_movement.gd
+++ b/player/scripts/player_movement.gd
@@ -4,7 +4,7 @@ class_name PlayerMovement
var player: CharacterBody2D
var animated_sprite: AnimatedSprite2D
-var speed: float = 300.0
+var speed: float
var last_direction: Vector2 = Vector2.ZERO
func process(_delta):
@@ -36,6 +36,9 @@ func process(_delta):
func update_animation(direction):
var anim_name = "idle" # Default animation
+
+ if animated_sprite.animation == "throw":
+ return # Don't change animation if throwing
if direction == Vector2.ZERO:
# Character is idle
diff --git a/player/weapons/projectile.gd b/player/weapons/projectile.gd
new file mode 100644
index 0000000..62e9956
--- /dev/null
+++ b/player/weapons/projectile.gd
@@ -0,0 +1,110 @@
+class_name Projectile extends Area2D
+
+signal on_hit(projectile, target)
+signal on_spawned(projectile)
+signal on_destroyed(projectile)
+
+@export var speed: float = 500.0
+@export var damage: float = 10.0
+@export var lifetime: float = 5.0
+@export var direction: Vector2 = Vector2.RIGHT
+@export var is_friendly: bool = true
+
+# Modifier-related properties
+var pierce_count: int = 0
+var has_explosive_impact: bool = true
+var explosion_projectile_count: int = 2
+var explosion_projectile_damage_mult: float = 0.5
+var explosion_projectile_speed: float = 300.0
+var explosion_spread_angle: float = 360.0 # Full circle by default
+# References
+var source_weapon: RangedWeapon # Reference to the weapon that fired this
+var lifetime_timer: Timer
+# Add a variable to track the entity that triggered the explosion
+var ignore_target = null
+
+func _ready():
+ lifetime_timer = Timer.new()
+ add_child(lifetime_timer)
+ lifetime_timer.one_shot = true
+ lifetime_timer.wait_time = lifetime
+ lifetime_timer.connect("timeout", _on_lifetime_timeout)
+ lifetime_timer.start()
+
+ emit_signal("on_spawned", self)
+ connect("body_entered", _on_body_entered)
+
+func _physics_process(delta):
+ position += direction * speed * delta
+
+func _on_body_entered(body):
+ # Check if this is a body we should ignore
+ if body == ignore_target:
+ return
+
+ if body.is_in_group("enemies") and is_friendly:
+ Log.pr("Hit enemy: ", body.name)
+ # Deal damage to enemy
+ if body.has_method("take_damage"):
+ body.take_damage(damage)
+
+ # Emit signal for modifiers to react to
+ emit_signal("on_hit", self, body)
+
+ # Handle piercing
+ if pierce_count > 0:
+ pierce_count -= 1
+ else:
+ # Handle explosive impact
+ if has_explosive_impact:
+ # Store the target that triggered the explosion
+ ignore_target = body
+ _trigger_explosion()
+
+ # Destroy the projectile
+ destroy()
+
+func _trigger_explosion():
+ # Create the explosion VFX
+ # var explosion = preload("res://scenes/explosion_effect.tscn").instantiate()
+ # explosion.global_position = global_position
+ # get_tree().root.add_child(explosion)
+ # Spawn the additional projectiles
+ if explosion_projectile_count > 0:
+ _spawn_explosion_projectiles()
+
+func _spawn_explosion_projectiles():
+ # Calculate even angle distribution
+ var angle_step = explosion_spread_angle / explosion_projectile_count
+ var start_angle = - explosion_spread_angle / 2
+
+ for i in range(explosion_projectile_count):
+ # Create a new projectile
+ var new_proj = duplicate()
+ new_proj.global_position = global_position
+
+ # Calculate new direction based on spread
+ var random_angle = randf_range(0, 2 * PI)
+ var new_dir = Vector2.RIGHT.rotated(random_angle)
+
+ # Set properties for the new projectile
+ new_proj.direction = new_dir
+ new_proj.damage = damage * explosion_projectile_damage_mult
+ new_proj.speed = explosion_projectile_speed
+
+ # Clear explosive properties so we don't get infinite loops
+ new_proj.has_explosive_impact = true
+ new_proj.explosion_projectile_count = 1
+
+ # Pass the ignore_target to the new projectiles
+ new_proj.ignore_target = ignore_target
+
+ # Add to scene tree
+ get_tree().root.add_child(new_proj)
+
+func destroy():
+ emit_signal("on_destroyed", self)
+ queue_free()
+
+func _on_lifetime_timeout():
+ destroy()
diff --git a/player/weapons/projectile.gd.uid b/player/weapons/projectile.gd.uid
new file mode 100644
index 0000000..a9e306d
--- /dev/null
+++ b/player/weapons/projectile.gd.uid
@@ -0,0 +1 @@
+uid://d5tiwy16ivu6
diff --git a/player/weapons/ranged_weapon.gd b/player/weapons/ranged_weapon.gd
index 167bf68..99ab71a 100644
--- a/player/weapons/ranged_weapon.gd
+++ b/player/weapons/ranged_weapon.gd
@@ -7,10 +7,12 @@ signal projectile_spawned(projectile)
# Base stats - will be modified by modifiers
var base_stats = {
"damage": 10.0,
- "fire_rate": 2.0, # Shots per second
+ "fire_rate": 2.0,
"projectile_speed": 500.0,
"projectile_size": 1.0,
"projectile_lifetime": 5.0,
+ "projectile_quantity": 1,
+ "projectile_spread": 33,
"max_pierce": 0
}
@@ -24,14 +26,15 @@ func _init() -> void:
Log.pr(stats)
add_child(stats)
-func _ready():
- # Connect to stats updated signal
- stats.connect("stats_updated", _on_stats_updated)
-
# Setup fire timer
fire_timer = Timer.new()
add_child(fire_timer)
fire_timer.one_shot = true
+
+ projectile_scene = preload("res://assets/projectiles/basic_projectile.tscn")
+
+func _ready():
+ stats.connect("stats_updated", _on_stats_updated)
fire_timer.connect("timeout", _on_fire_timer_timeout)
# Initial update
@@ -47,30 +50,55 @@ func fire(direction: Vector2):
fire_timer.start(1.0 / stats.get_stat("fire_rate"))
func _spawn_projectile(spawn_position: Vector2, spawn_direction: Vector2):
- var projectile = projectile_scene.instantiate()
- projectile.global_position = spawn_position
- projectile.direction = spawn_direction
+ # Get projectile quantity and spread from stats
+ var quantity = stats.get_stat("projectile_quantity")
+ var spread_angle = stats.get_stat("projectile_spread")
- # Apply stats to projectile
- projectile.speed = stats.get_stat("projectile_speed")
- projectile.damage = stats.get_stat("damage")
- projectile.lifetime = stats.get_stat("projectile_lifetime")
- projectile.pierce_count = stats.get_stat("max_pierce")
- projectile.source_weapon = self
+ # Calculate the angle between each projectile
+ var angle_step = 0.0
+ if quantity > 1 and spread_angle > 0:
+ angle_step = spread_angle / (quantity - 1)
- # Apply size (scale)
- var size = stats.get_stat("projectile_size")
- projectile.scale = Vector2(size, size)
+ # Calculate starting angle (to center the spread)
+ var start_angle = - spread_angle / 2
- # Allow modifiers to directly modify the projectile
- for modifier in stats.modifiers:
- modifier.modify_projectile(projectile)
-
- get_tree().root.add_child(projectile)
- projectile.emit_signal("on_spawned", projectile)
- emit_signal("projectile_spawned", projectile)
+ # Spawn each projectile
+ for i in range(quantity):
+ var projectile = projectile_scene.instantiate()
+ projectile.global_position = spawn_position
+
+ # Calculate the direction with spread
+ var direction = spawn_direction
+ if quantity > 1:
+ var current_angle = start_angle + (i * angle_step)
+ direction = spawn_direction.rotated(deg_to_rad(current_angle))
+
+ projectile.direction = direction
+
+ # Apply stats to projectile
+ projectile.speed = stats.get_stat("projectile_speed")
+ projectile.damage = stats.get_stat("damage")
+ projectile.lifetime = stats.get_stat("projectile_lifetime")
+ projectile.source_weapon = self
+
+ # Set base size
+ var size = stats.get_stat("projectile_size")
+ projectile.scale = Vector2(size, size)
+
+ # Allow modifiers to directly modify the projectile
+ for modifier in stats.modifiers:
+ modifier.modify_projectile(projectile)
+
+ # Add to scene tree
+ if get_tree() and get_tree().get_root():
+ get_tree().get_root().add_child(projectile)
+
+ # Emit the spawn signal
+ if projectile.has_signal("on_spawned"):
+ projectile.emit_signal("on_spawned", projectile)
func add_modifier(modifier: Modifier):
+ Log.pr("Adding modifier: ", modifier)
stats.add_modifier(modifier)
func remove_modifier(modifier_id: String):
diff --git a/project.godot b/project.godot
index 7a567a2..fe612eb 100644
--- a/project.godot
+++ b/project.godot
@@ -21,6 +21,7 @@ RNG="*res://utility/RngUtility.gd"
Global="*res://utility/Globals.gd"
SceneSelector="*res://utility/SceneSelector.gd"
MapBuilder="*res://utility/MapBuilder.gd"
+DebugMenu="*res://addons/debug_menu/debug_menu.tscn"
[display]
@@ -30,7 +31,7 @@ window/stretch/scale_mode="integer"
[editor_plugins]
-enabled=PackedStringArray("res://addons/log/plugin.cfg")
+enabled=PackedStringArray("res://addons/debug_menu/plugin.cfg", "res://addons/log/plugin.cfg")
[input]
@@ -54,6 +55,11 @@ move_right={
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":68,"key_label":0,"unicode":100,"location":0,"echo":false,"script":null)
]
}
+fire={
+"deadzone": 0.2,
+"events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":1,"position":Vector2(115, 24),"global_position":Vector2(129, 97),"factor":1.0,"button_index":1,"canceled":false,"pressed":true,"double_click":false,"script":null)
+]
+}
[layer_names]
@@ -61,6 +67,7 @@ move_right={
2d_physics/layer_2="Water"
2d_physics/layer_3="Objects"
2d_physics/layer_4="Enemies"
+2d_physics/layer_5="Projectiles"
[rendering]