@tool
extends Sprite2D

const ANIMATION_FRAMERATE = 15

var _direction := 0
var _progress := 0.0
var _parent_node25d: Node25D
var _parent_math: PlayerMath25D

@onready var _stand: Texture2D = preload("res://assets/player/textures/stand.png")
@onready var _jump: Texture2D = preload("res://assets/player/textures/jump.png")
@onready var _run: Texture2D = preload("res://assets/player/textures/run.png")

func _ready() -> void:
	_parent_node25d = get_parent()
	_parent_math = _parent_node25d.get_child(0)

func _process(delta: float) -> void:
	if Engine.is_editor_hint():
		return  # Don't run this in the editor.

	_sprite_basis()
	var movement := _check_movement()  # Always run to get direction, but don't always use return bool.

	# Test-only move and collide, check if the player is on the ground.
	var k := _parent_math.move_and_collide(Vector3.DOWN * 10 * delta, true, true, true)
	if k != null:
		if movement:
			hframes = 6
			texture = _run
			if Input.is_action_pressed(&"movement_modifier"):
				delta /= 2
			_progress = fmod((_progress + ANIMATION_FRAMERATE * delta), 6)
			frame = _direction * 6 + int(_progress)
		else:
			hframes = 1
			texture = _stand
			_progress = 0
			frame = _direction
	else:
		hframes = 2
		texture = _jump
		_progress = 0
		var jumping := 1 if _parent_math.vertical_speed < 0 else 0
		frame = _direction * 2 + jumping

func set_view_mode(view_mode_index: int) -> void:
	match view_mode_index:
		0:  # 45 Degrees
			transform.x = Vector2(1, 0)
			transform.y = Vector2(0, 0.75)
		1:  # Isometric
			transform.x = Vector2(1, 0)
			transform.y = Vector2(0, 1)
		2:  # Top Down
			transform.x = Vector2(1, 0)
			transform.y = Vector2(0, 0.5)
		3:  # Front Side
			transform.x = Vector2(1, 0)
			transform.y = Vector2(0, 1)
		4:  # Oblique Y
			transform.x = Vector2(1, 0)
			transform.y = Vector2(0.75, 0.75)
		5:  # Oblique Z
			transform.x = Vector2(1, 0.25)
			transform.y = Vector2(0, 1)

# Change the 2D basis of the sprite to try and make it "fit" multiple view modes.
func _sprite_basis() -> void:
	if not Engine.is_editor_hint():
		if Input.is_action_pressed(&"forty_five_mode"):
			set_view_mode(0)
		elif Input.is_action_pressed(&"isometric_mode"):
			set_view_mode(1)
		elif Input.is_action_pressed(&"top_down_mode"):
			set_view_mode(2)
		elif Input.is_action_pressed(&"front_side_mode"):
			set_view_mode(3)
		elif Input.is_action_pressed(&"oblique_y_mode"):
			set_view_mode(4)
		elif Input.is_action_pressed(&"oblique_z_mode"):
			set_view_mode(5)

# This method returns a bool but if true it also outputs to the direction variable.
func _check_movement() -> bool:
	# Gather player input and store movement to these int variables. Note: These indeed have to be integers.
	var x := 0
	var z := 0

	if Input.is_action_pressed(&"move_right"):
		x += 1
	if Input.is_action_pressed(&"move_left"):
		x -= 1
	if Input.is_action_pressed(&"move_forward"):
		z -= 1
	if Input.is_action_pressed(&"move_back"):
		z += 1

	# Check for isometric controls and add more to movement accordingly.
	# For efficiency, only check the X axis since this X axis value isn't used anywhere else.
	if not _parent_math.isometric_controls and is_equal_approx(Node25D.SCALE * 0.86602540378, _parent_node25d.get_basis()[0].x):
		if Input.is_action_pressed(&"move_right"):
			z += 1
		if Input.is_action_pressed(&"move_left"):
			z -= 1
		if Input.is_action_pressed(&"move_forward"):
			x += 1
		if Input.is_action_pressed(&"move_back"):
			x -= 1

	# Set the direction based on which inputs were pressed.
	if x == 0:
		if z == 0:
			return false  # No movement.
		elif z > 0:
			_direction = 0
		else:
			_direction = 4
	elif x > 0:
		if z == 0:
			_direction = 2
			flip_h = true
		elif z > 0:
			_direction = 1
			flip_h = true
		else:
			_direction = 3
			flip_h = true
	else:
		if z == 0:
			_direction = 2
			flip_h = false
		elif z > 0:
			_direction = 1
			flip_h = false
		else:
			_direction = 3
			flip_h = false

	return true  # There is movement.


[gd_scene load_steps=6 format=4 uid="uid://bg27d8sfehmr4"]

[ext_resource type="Script" path="res://addons/node25d/node_25d.gd" id="1"]
[ext_resource type="Script" path="res://assets/player/player_math_25d.gd" id="3"]
[ext_resource type="Texture2D" uid="uid://bfdfertqyhf1u" path="res://assets/player/textures/jump.png" id="4"]
[ext_resource type="Script" path="res://assets/player/player_sprite.gd" id="5"]

[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_b17hs"]

[node name="Player25D" type="Node2D"]
z_index = 100
position = Vector2(0, -11.3137)
script = ExtResource("1")
spatial_position = Vector3(0, 0.5, 0)

[node name="PlayerMath25D" type="CharacterBody3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.5, 0)
script = ExtResource("3")

[node name="CollisionShape3D" type="CollisionShape3D" parent="PlayerMath25D"]
shape = SubResource("CapsuleShape3D_b17hs")

[node name="PlayerSprite" type="Sprite2D" parent="."]
z_index = 1
scale = Vector2(1, 0.75)
texture = ExtResource("4")
offset = Vector2(0, 4)
hframes = 2
vframes = 5
script = ExtResource("5")

[node name="PlayerCamera" type="Camera2D" parent="PlayerSprite"]


func _ready() -> void:
	# Grab focus so that the list can be scrolled (for keyboard/controller-friendly navigation).
	rtl.grab_focus()

	add_header("Audio")
	add_line("Mix rate", "%d Hz" % AudioServer.get_mix_rate())
	add_line("Output latency", "%f ms" % (AudioServer.get_output_latency() * 1000))
	add_line("Output device list", ", ".join(AudioServer.get_output_device_list()))
	add_line("Capture device list", ", ".join(AudioServer.get_input_device_list()))
	add_line("Connected MIDI inputs", scan_midi_inputs())

	add_header("Date and time")
	add_line("Date and time (local)", Time.get_datetime_string_from_system(false, true))
	add_line("Date and time (UTC)", Time.get_datetime_string_from_system(true, true))
	add_line("Date (local)", Time.get_date_string_from_system(false))
	add_line("Date (UTC)", Time.get_date_string_from_system(true))
	add_line("Time (local)", Time.get_time_string_from_system(false))
	add_line("Time (UTC)", Time.get_time_string_from_system(true))
	add_line("Timezone", Time.get_time_zone_from_system())
	add_line("UNIX time", Time.get_unix_time_from_system())

	add_header("Display")
	add_line("Screen count", DisplayServer.get_screen_count())
	add_line("DPI", DisplayServer.screen_get_dpi())
	add_line("Scale factor", DisplayServer.screen_get_scale())
	add_line("Maximum scale factor", DisplayServer.screen_get_max_scale())
	add_line("Startup screen position", DisplayServer.screen_get_position())
	add_line("Startup screen size", DisplayServer.screen_get_size())
	add_line("Startup screen refresh rate", ("%f Hz" % DisplayServer.screen_get_refresh_rate()) if DisplayServer.screen_get_refresh_rate() > 0.0 else "")
	add_line("Usable (safe) area rectangle", DisplayServer.get_display_safe_area())
	add_line("Screen orientation", [
		"Landscape",
		"Portrait",
		"Landscape (reverse)",
		"Portrait (reverse)",
		"Landscape (defined by sensor)",
		"Portrait (defined by sensor)",
		"Defined by sensor",
	][DisplayServer.screen_get_orientation()])

	add_header("Engine")
	add_line("Version", Engine.get_version_info()["string"])
	add_line("Compiled for architecture", Engine.get_architecture_name())
	add_line("Command-line arguments", str(OS.get_cmdline_args()))
	add_line("Is debug build", OS.is_debug_build())
	add_line("Executable path", OS.get_executable_path())
	add_line("User data directory", OS.get_user_data_dir())
	add_line("Filesystem is persistent", OS.is_userfs_persistent())
	add_line("Process ID (PID)", OS.get_process_id())
	add_line("Main thread ID", OS.get_main_thread_id())
	add_line("Thread caller ID", OS.get_thread_caller_id())
	add_line("Memory information", OS.get_memory_info())
	add_line("Static memory usage", OS.get_static_memory_usage())
	add_line("Static memory peak usage", OS.get_static_memory_peak_usage())

	add_header("Environment")
	add_line("Value of `PATH`", OS.get_environment("PATH"))
	# Check for case-sensitivity behavior across platforms.
	add_line("Value of `path`", OS.get_environment("path"))

	add_header("Hardware")
	add_line("Model name", OS.get_model_name())
	add_line("Processor name", OS.get_processor_name())
	add_line("Processor count", OS.get_processor_count())
	add_line("Device unique ID", OS.get_unique_id())

	add_header("Input")
	add_line("Device has touch screen", DisplayServer.is_touchscreen_available())
	var has_virtual_keyboard := DisplayServer.has_feature(DisplayServer.FEATURE_VIRTUAL_KEYBOARD)
	add_line("Device has virtual keyboard", has_virtual_keyboard)
	if has_virtual_keyboard:
		add_line("Virtual keyboard height", DisplayServer.virtual_keyboard_get_height())

	add_header("Localization")
	add_line("Locale", OS.get_locale())
	add_line("Language", OS.get_locale_language())

	add_header("Mobile")
	add_line("Granted permissions", OS.get_granted_permissions())

	add_header(".NET (C#)")
	var csharp_enabled := ResourceLoader.exists("res://CSharpTest.cs")
	add_line("Mono module enabled", "Yes" if csharp_enabled else "No")
	if csharp_enabled:
		csharp_test.set_script(load("res://CSharpTest.cs"))
		add_line("Operating system", csharp_test.OperatingSystem())
		add_line("Platform type", csharp_test.PlatformType())

	add_header("Software")
	add_line("OS name", OS.get_name())
	add_line("OS version", OS.get_version())
	add_line("Distribution name", OS.get_distribution_name())
	add_line("System dark mode supported", DisplayServer.is_dark_mode_supported())
	add_line("System dark mode enabled", DisplayServer.is_dark_mode())
	add_line("System accent color", "#%s" % DisplayServer.get_accent_color().to_html())
	add_line("System fonts", "%d fonts available" % OS.get_system_fonts().size())
	add_line("System font path (\"sans-serif\")", OS.get_system_font_path("sans-serif"))
	add_line("System font path (\"sans-serif\") for English text", ", ".join(OS.get_system_font_path_for_text("sans-serif", "Hello")))
	add_line("System font path (\"sans-serif\") for Chinese text", ", ".join(OS.get_system_font_path_for_text("sans-serif", "你好")))
	add_line("System font path (\"sans-serif\") for Japanese text", ", ".join(OS.get_system_font_path_for_text("sans-serif", "こんにちは")))

	add_header("Security")
	add_line("Is sandboxed", OS.is_sandboxed())
	add_line("Entropy (8 random bytes)", OS.get_entropy(8))
	add_line("System CA certificates", ("Available (%d bytes)" % OS.get_system_ca_certificates().length()) if not OS.get_system_ca_certificates().is_empty() else "Not available")

	add_header("Engine directories")
	add_line("User data", OS.get_data_dir())
	add_line("Configuration", OS.get_config_dir())
	add_line("Cache", OS.get_cache_dir())

	add_header("System directories")
	add_line("Desktop", OS.get_system_dir(OS.SYSTEM_DIR_DESKTOP))
	add_line("DCIM", OS.get_system_dir(OS.SYSTEM_DIR_DCIM))
	add_line("Documents", OS.get_system_dir(OS.SYSTEM_DIR_DOCUMENTS))
	add_line("Downloads", OS.get_system_dir(OS.SYSTEM_DIR_DOWNLOADS))
	add_line("Movies", OS.get_system_dir(OS.SYSTEM_DIR_MOVIES))
	add_line("Music", OS.get_system_dir(OS.SYSTEM_DIR_MUSIC))
	add_line("Pictures", OS.get_system_dir(OS.SYSTEM_DIR_PICTURES))
	add_line("Ringtones", OS.get_system_dir(OS.SYSTEM_DIR_RINGTONES))

	add_header("Video")
	add_line("Adapter name", RenderingServer.get_video_adapter_name())
	add_line("Adapter vendor", RenderingServer.get_video_adapter_vendor())
	if ProjectSettings.get_setting_with_override("rendering/renderer/rendering_method") != "gl_compatibility":
		# Querying the adapter type isn't supported in Compatibility.
		add_line("Adapter type", [
			"Other (Unknown)",
			"Integrated",
			"Discrete",
			"Virtual",
			"CPU",
		][RenderingServer.get_video_adapter_type()])
	add_line("Adapter graphics API version", RenderingServer.get_video_adapter_api_version())

	var video_adapter_driver_info := OS.get_video_adapter_driver_info()
	if video_adapter_driver_info.size() > 0:
		add_line("Adapter driver name", video_adapter_driver_info[0])
	if video_adapter_driver_info.size() > 1:
		add_line("Adapter driver version", video_adapter_driver_info[1])


extends Node

func _on_open_shell_web_pressed() -> void:
	OS.shell_open("https://example.com")

func _on_open_shell_folder_pressed() -> void:
	var path := OS.get_environment("HOME")
	if path == "":
		# Windows-specific.
		path = OS.get_environment("USERPROFILE")

	if OS.get_name() == "macOS":
		# MacOS-specific.
		path = "file://" + path

	OS.shell_show_in_file_manager(path)

func _on_change_window_title_pressed() -> void:
	DisplayServer.window_set_title("Modified window title. Unicode characters for testing: é € × Ù ¨")

func _on_change_window_icon_pressed() -> void:
	if not DisplayServer.has_feature(DisplayServer.FEATURE_ICON):
		OS.alert("Changing the window icon is not supported by the current display server (%s)." % DisplayServer.get_name())
		return

	var image := Image.create(128, 128, false, Image.FORMAT_RGB8)
	image.fill(Color(1, 0.6, 0.3))
	DisplayServer.set_icon(image)

func _on_move_window_to_foreground_pressed() -> void:
	DisplayServer.window_set_title("Will move window to foreground in 5 seconds, try unfocusing the window...")
	await get_tree().create_timer(5).timeout
	DisplayServer.window_move_to_foreground()
	# Restore the previous window title.
	DisplayServer.window_set_title(ProjectSettings.get_setting("application/config/name"))

func _on_request_attention_pressed() -> void:
	DisplayServer.window_set_title("Will request attention in 5 seconds, try unfocusing the window...")
	await get_tree().create_timer(5).timeout
	DisplayServer.window_request_attention()
	# Restore the previous window title.
	DisplayServer.window_set_title(ProjectSettings.get_setting("application/config/name"))

func _on_vibrate_device_short_pressed() -> void:
	Input.vibrate_handheld(200)

func _on_vibrate_device_long_pressed() -> void:
	Input.vibrate_handheld(1000)

func _on_add_global_menu_items_pressed() -> void:
	if not DisplayServer.has_feature(DisplayServer.FEATURE_GLOBAL_MENU):
		OS.alert("Global menus are not supported by the current display server (%s)." % DisplayServer.get_name())
		return

	# Add a menu to the main menu bar.
	DisplayServer.global_menu_add_submenu_item("_main", "Hello", "_main/Hello")
	DisplayServer.global_menu_add_item(
			"_main/Hello",
			"World",
			func(tag: String) -> void: print("Clicked main 1 " + str(tag)),
			func(tag: String) -> void: print("Key main 1 " + str(tag)),
			null,
			(KEY_MASK_META | KEY_1) as Key
	)
	DisplayServer.global_menu_add_separator("_main/Hello")
	DisplayServer.global_menu_add_item("_main/Hello", "World2", func(tag: String) -> void: print("Clicked main 2 " + str(tag)))

	# Add a menu to the Dock context menu.
	DisplayServer.global_menu_add_submenu_item("_dock", "Hello", "_dock/Hello")
	DisplayServer.global_menu_add_item("_dock/Hello", "World", func(tag: String) -> void: print("Clicked dock 1 " + str(tag)))
	DisplayServer.global_menu_add_separator("_dock/Hello")
	DisplayServer.global_menu_add_item("_dock/Hello", "World2", func(tag: String) -> void: print("Clicked dock 2 " + str(tag)))

func _on_remove_global_menu_item_pressed() -> void:
	if not DisplayServer.has_feature(DisplayServer.FEATURE_GLOBAL_MENU):
		OS.alert("Global menus are not supported by the current display server (%s)." % DisplayServer.get_name())
		return

	DisplayServer.global_menu_remove_item("_main/Hello", 2)
	DisplayServer.global_menu_remove_item("_main/Hello", 1)
	DisplayServer.global_menu_remove_item("_main/Hello", 0)
	DisplayServer.global_menu_remove_item("_main", 0)

	DisplayServer.global_menu_remove_item("_dock/Hello", 2)
	DisplayServer.global_menu_remove_item("_dock/Hello", 1)
	DisplayServer.global_menu_remove_item("_dock/Hello", 0)
	DisplayServer.global_menu_remove_item("_dock", 0)

func _on_get_clipboard_pressed() -> void:
	if not DisplayServer.has_feature(DisplayServer.FEATURE_CLIPBOARD):
		OS.alert("Clipboard I/O is not supported by the current display server (%s)." % DisplayServer.get_name())
		return

	OS.alert("Clipboard contents:\n\n%s" % DisplayServer.clipboard_get())

func _on_set_clipboard_pressed() -> void:
	if not DisplayServer.has_feature(DisplayServer.FEATURE_CLIPBOARD):
		OS.alert("Clipboard I/O is not supported by the current display server (%s)." % DisplayServer.get_name())
		return

	DisplayServer.clipboard_set("Modified clipboard contents. Unicode characters for testing: é € × Ù ¨")

func _on_display_alert_pressed() -> void:
	OS.alert("Hello from Godot! Close this dialog to resume the main window.")

func _on_kill_current_process_pressed() -> void:
	OS.kill(OS.get_process_id())


extends Area3D

var taken := false

func _on_coin_body_enter(body: Node) -> void:
	if not taken and body is Player:
		$Animation.play(&"take")
		taken = true
		# We've already checked whether the colliding body is a Player, which has a `coins` property.
		# As a result, we can safely increment its `coins` property.
		body.coins += 1


class_name Player
extends CharacterBody3D

enum _Anim {
	FLOOR,
	AIR,
}

const SHOOT_TIME = 1.5
const SHOOT_SCALE = 2.0
const CHAR_SCALE = Vector3(0.3, 0.3, 0.3)
const MAX_SPEED = 6.0
const TURN_SPEED = 40.0
const JUMP_VELOCITY = 12.5
const BULLET_SPEED = 20.0
const AIR_IDLE_DEACCEL = false
const ACCEL = 14.0
const DEACCEL = 14.0
const AIR_ACCEL_FACTOR = 0.5
const SHARP_TURN_THRESHOLD = deg_to_rad(140.0)

var movement_dir := Vector3()
var jumping := false
var prev_shoot := false
var shoot_blend := 0.0

# Number of coins collected.
var coins := 0

@onready var initial_position := position
@onready var gravity: Vector3 = ProjectSettings.get_setting("physics/3d/default_gravity") * \
		ProjectSettings.get_setting("physics/3d/default_gravity_vector")

@onready var _camera := $Target/Camera3D as Camera3D
@onready var _animation_tree := $AnimationTree as AnimationTree

func _physics_process(delta: float) -> void:
	if Input.is_action_pressed("reset_position") or global_position.y < -12:
		# Player hit the reset button or fell off the map.
		position = initial_position
		velocity = Vector3.ZERO

	# Update coin count and its "parallax" copies.
	# This gives text a pseudo-3D appearance while still using Label3D instead of the more limited TextMesh.
	%CoinCount.text = str(coins)
	%CoinCount.get_node("Parallax").text = str(coins)
	%CoinCount.get_node("Parallax2").text = str(coins)
	%CoinCount.get_node("Parallax3").text = str(coins)
	%CoinCount.get_node("Parallax4").text = str(coins)

	velocity += gravity * delta

	var anim := _Anim.FLOOR

	var vertical_velocity := velocity.y
	var horizontal_velocity := Vector3(velocity.x, 0, velocity.z)

	var horizontal_direction := horizontal_velocity.normalized()
	var horizontal_speed := horizontal_velocity.length()

	# Player input.
	var cam_basis := _camera.get_global_transform().basis
	var movement_vec2 := Input.get_vector(&"move_left", &"move_right", &"move_forward", &"move_back")
	var movement_direction := cam_basis * Vector3(movement_vec2.x, 0, movement_vec2.y)
	movement_direction.y = 0
	movement_direction = movement_direction.normalized()

	var jump_attempt := Input.is_action_pressed(&"jump")
	var shoot_attempt := Input.is_action_pressed(&"shoot")

	if is_on_floor():
		var sharp_turn := horizontal_speed > 0.1 and \
				acos(movement_direction.dot(horizontal_direction)) > SHARP_TURN_THRESHOLD

		if movement_direction.length() > 0.1 and not sharp_turn:
			if horizontal_speed > 0.001:
				horizontal_direction = adjust_facing(
					horizontal_direction,
					movement_direction,
					delta,
					1.0 / horizontal_speed * TURN_SPEED,
					Vector3.UP
				)
			else:
				horizontal_direction = movement_direction

			if horizontal_speed < MAX_SPEED:
				horizontal_speed += ACCEL * delta
		else:
			horizontal_speed -= DEACCEL * delta
			if horizontal_speed < 0:
				horizontal_speed = 0

		horizontal_velocity = horizontal_direction * horizontal_speed

		var mesh_xform := ($Player/Skeleton as Node3D).get_transform()
		var facing_mesh := -mesh_xform.basis[0].normalized()
		facing_mesh = (facing_mesh - Vector3.UP * facing_mesh.dot(Vector3.UP)).normalized()

		if horizontal_speed > 0:
			facing_mesh = adjust_facing(
				facing_mesh,
				movement_direction,
				delta,
				1.0 / horizontal_speed * TURN_SPEED,
				Vector3.UP
			)
		var m3 := Basis(
			-facing_mesh,
			Vector3.UP,
			-facing_mesh.cross(Vector3.UP).normalized()
		).scaled(CHAR_SCALE)

		$Player/Skeleton.set_transform(Transform3D(m3, mesh_xform.origin))

		if not jumping and jump_attempt:
			vertical_velocity = JUMP_VELOCITY
			jumping = true
			$SoundJump.play()

	else:
		anim = _Anim.AIR

		if movement_direction.length() > 0.1:
			horizontal_velocity += movement_direction * (ACCEL * AIR_ACCEL_FACTOR * delta)
			if horizontal_velocity.length() > MAX_SPEED:
				horizontal_velocity = horizontal_velocity.normalized() * MAX_SPEED
		elif AIR_IDLE_DEACCEL:
			horizontal_speed = horizontal_speed - (DEACCEL * AIR_ACCEL_FACTOR * delta)
			if horizontal_speed < 0:
				horizontal_speed = 0
			horizontal_velocity = horizontal_direction * horizontal_speed

		if Input.is_action_just_released("jump") and velocity.y > 0.0:
			# Reduce jump height if releasing the jump key before reaching the apex.
			vertical_velocity *= 0.7

	if jumping and vertical_velocity < 0:
		jumping = false

	velocity = horizontal_velocity + Vector3.UP * vertical_velocity

	if is_on_floor():
		movement_dir = velocity

	move_and_slide()

	if shoot_blend > 0:
		shoot_blend *= 0.97
		if (shoot_blend < 0):
			shoot_blend = 0

	if shoot_attempt and not prev_shoot:
		shoot_blend = SHOOT_TIME
		var bullet := preload("res://player/bullet/bullet.tscn").instantiate() as Bullet
		bullet.set_transform($Player/Skeleton/Bullet.get_global_transform().orthonormalized())
		get_parent().add_child(bullet)
		bullet.set_linear_velocity(
			$Player/Skeleton/Bullet.get_global_transform().basis[2].normalized() * BULLET_SPEED
		)
		bullet.add_collision_exception_with(self)
		$SoundShoot.play()

	prev_shoot = shoot_attempt

	if is_on_floor():
		# How much the player should be blending between the "idle" and "walk/run" animations.
		_animation_tree[&"parameters/run/blend_amount"] = horizontal_speed / MAX_SPEED

		# How much the player should be running (as opposed to walking). 0.0 = fully walking, 1.0 = fully running.
		_animation_tree[&"parameters/speed/blend_amount"] = minf(1.0, horizontal_speed / (MAX_SPEED * 0.5))

	_animation_tree[&"parameters/state/blend_amount"] = anim
	_animation_tree[&"parameters/air_dir/blend_amount"] = clampf(-velocity.y / 4 + 0.5, 0, 1)
	_animation_tree[&"parameters/gun/blend_amount"] = minf(shoot_blend, 1.0)

func adjust_facing(facing: Vector3, target: Vector3, step: float, adjust_rate: float, \
		current_gn: Vector3) -> Vector3:
	var normal := target
	var t := normal.cross(current_gn).normalized()

	var x := normal.dot(facing)
	var y := t.dot(facing)

	var ang := atan2(y,x)

	if absf(ang) < 0.001:
		return facing

	var s := signf(ang)
	ang = ang * s
	var turn := ang * adjust_rate * step
	var a: float
	if ang < turn:
		a = ang
	else:
		a = turn
	ang = (ang - a) * s

	return (normal * cos(ang) + t * sin(ang)) * facing.length()


extends Camera3D

const MAX_HEIGHT = 2.0
const MIN_HEIGHT = 0.0

var collision_exception: Array[RID] = []

@export var min_distance := 0.5
@export var max_distance := 3.5
@export var angle_v_adjust := 0.0
@export var autoturn_ray_aperture := 25.0
@export var autoturn_speed := 50.0

func _ready() -> void:
	# Find collision exceptions for ray.
	var node: Node = self
	while is_instance_valid(node):
		if node is RigidBody3D:
			collision_exception.append(node.get_rid())
			break
		else:
			node = node.get_parent()
	set_physics_process(true)
	# This detaches the camera transform from the parent spatial node.
	set_as_top_level(true)

func _physics_process(delta: float) -> void:
	var target := (get_parent() as Node3D).get_global_transform().origin
	var pos := get_global_transform().origin

	var difference := pos - target

	# Regular delta follow.

	# Check ranges.
	if difference.length() < min_distance:
		difference = difference.normalized() * min_distance
	elif  difference.length() > max_distance:
		difference = difference.normalized() * max_distance

	# Check upper and lower height.
	difference.y = clamp(difference.y, MIN_HEIGHT, MAX_HEIGHT)

	# Check autoturn.
	var ds := PhysicsServer3D.space_get_direct_state(get_world_3d().get_space())

	var col_left := ds.intersect_ray(PhysicsRayQueryParameters3D.create(
			target,
			target + Basis(Vector3.UP, deg_to_rad(autoturn_ray_aperture)) * (difference),
			0xffffffff,
			collision_exception
	))
	var col := ds.intersect_ray(PhysicsRayQueryParameters3D.create(
			target,
			target + difference,
			0xffffffff,
			collision_exception
	))
	var col_right := ds.intersect_ray(PhysicsRayQueryParameters3D.create(
			target,
			target + Basis(Vector3.UP, deg_to_rad(-autoturn_ray_aperture)) * (difference),
			0xffffffff,
			collision_exception
	))

	if not col.is_empty():
		# If main ray was occluded, get camera closer, this is the worst case scenario.
		difference = col.position - target
	elif not col_left.is_empty() and col_right.is_empty():
		# If only left ray is occluded, turn the camera around to the right.
		difference = Basis(Vector3.UP, deg_to_rad(-delta * (autoturn_speed))) * difference
	elif col_left.is_empty() and not col_right.is_empty():
		# If only right ray is occluded, turn the camera around to the left.
		difference = Basis(Vector3.UP, deg_to_rad(delta * autoturn_speed)) * difference
	# Do nothing otherwise, left and right are occluded but center is not, so do not autoturn.

	# Apply lookat.
	if difference.is_zero_approx():
		difference = (pos - target).normalized() * 0.0001

	pos = target + difference

	look_at_from_position(pos, target, Vector3.UP)

	# Turn a little up or down.
	transform.basis = Basis(transform.basis[0], deg_to_rad(angle_v_adjust)) * transform.basis


extends VBoxContainer

func _ready() -> void:
	get_viewport().size_changed.connect(_on_viewport_size_changed)
	_on_viewport_size_changed()

func _process(_delta: float) -> void:
	$FPS.text = "%d FPS (%.2f mspf)" % [Engine.get_frames_per_second(), 1000.0 / Engine.get_frames_per_second()]

func _on_viewport_size_changed() -> void:
	$Resolution.text = "%s × %s" % [get_viewport().size.x, get_viewport().size.y]


extends Node

const ROT_SPEED = 0.003
const ZOOM_SPEED = 0.125
const MAIN_BUTTONS = MOUSE_BUTTON_MASK_LEFT | MOUSE_BUTTON_MASK_RIGHT | MOUSE_BUTTON_MASK_MIDDLE

var tester_index := 0
var rot_x := -TAU / 16  # This must be kept in sync with RotationX.
var rot_y := TAU / 8  # This must be kept in sync with CameraHolder.
var camera_distance := 4.0
var base_height := int(ProjectSettings.get_setting("display/window/size/viewport_height"))

@onready var testers: Node3D = $Testers
@onready var camera_holder: Node3D = $CameraHolder  # Has a position and rotates on Y.
@onready var rotation_x: Node3D = $CameraHolder/RotationX
@onready var camera: Camera3D = $CameraHolder/RotationX/Camera3D

func _ready() -> void:
	camera_holder.transform.basis = Basis.from_euler(Vector3(0, rot_y, 0))
	rotation_x.transform.basis = Basis.from_euler(Vector3(rot_x, 0, 0))
	update_gui()

func _unhandled_input(event: InputEvent) -> void:
	if event.is_action_pressed(&"ui_left"):
		_on_previous_pressed()
	if event.is_action_pressed(&"ui_right"):
		_on_next_pressed()

	if event is InputEventMouseButton:
		if event.button_index == MOUSE_BUTTON_WHEEL_UP:
			camera_distance -= ZOOM_SPEED
		if event.button_index == MOUSE_BUTTON_WHEEL_DOWN:
			camera_distance += ZOOM_SPEED
		camera_distance = clamp(camera_distance, 1.5, 6)

	if event is InputEventMouseMotion and event.button_mask & MAIN_BUTTONS:
		# Compensate motion speed to be resolution-independent (based on the window height).
		var relative_motion: Vector2 = event.relative * DisplayServer.window_get_size().y / base_height
		rot_y -= relative_motion.x * ROT_SPEED
		rot_x -= relative_motion.y * ROT_SPEED
		rot_x = clamp(rot_x, -1.57, 0)
		camera_holder.transform.basis = Basis.from_euler(Vector3(0, rot_y, 0))
		rotation_x.transform.basis = Basis.from_euler(Vector3(rot_x, 0, 0))

func _process(delta: float) -> void:
	var current_tester: Node3D = testers.get_child(tester_index)
	# This code assumes CameraHolder's X and Y coordinates are already correct.
	var current_position := camera_holder.global_transform.origin.z
	var target_position := current_tester.global_transform.origin.z
	camera_holder.global_transform.origin.z = lerpf(current_position, target_position, 3 * delta)
	camera.position.z = lerpf(camera.position.z, camera_distance, 10 * delta)

func _on_previous_pressed() -> void:
	tester_index = max(0, tester_index - 1)
	update_gui()

func _on_next_pressed() -> void:
	tester_index = min(tester_index + 1, testers.get_child_count() - 1)
	update_gui()

func update_gui() -> void:
	$TestName.text = str(testers.get_child(tester_index).name).capitalize()
	$Previous.disabled = tester_index == 0
	$Next.disabled = tester_index == testers.get_child_count() - 1


# 3D Labels and Texts

This project showcases the two main 3D text techniques supported by Godot:
Label3D and TextMesh.

Both Label3D and TextMesh exist in 3D space and can optionally be occluded by
other objects, but they serve different use cases.

**Label3D:** The Label3D node is a 3D node like any other. It draws text using
one quad per character, which can optionally be set to work as a billboard.

**TextMesh:** Unlike Label3D, TextMesh can optionally have actual depth since it
generates geometry to represent the text. TextMesh is not a node, but a
PrimitiveMesh resource you use within a MeshsInstance3D node. Therefore, you
won't see TextMesh in the Create New Node dialog.

Icons can also be displayed in Label3D and TextMesh using icon fonts, which can
be generated from SVG files using serivces like
[Fontello](https://fontello.com/). Note that while Label3D supports colored
rasterized fonts (such as emoji), only monochrome fonts can be generated from
Fontello. TextMesh and Label3D with MSDF fonts are limited to monochrome fonts
too.

Both standard rasterized and MSDF fonts can be used in Label3D, while TextMesh
will only support fonts that can be drawn as MSDF well. This excludes fonts that
have self-intersecting outlines, which Godot currently doesn't handle well when
converting them to MSDF.

In most scenarios, Label3D is easier to use and can look better (thanks to
outlines and MSDF rendering). TextMesh is more powerful and is intended for more
specialized use cases.

Language: GDScript

Renderer: Forward+

Check out this demo on the asset library: https://godotengine.org/asset-library/asset/2740

## Screenshots

![Screenshot](screenshots/3d_labels_and_texts.png)


extends Node

const ROT_SPEED = 0.003
const ZOOM_SPEED = 0.125
const MAIN_BUTTONS = MOUSE_BUTTON_MASK_LEFT | MOUSE_BUTTON_MASK_RIGHT | MOUSE_BUTTON_MASK_MIDDLE

var tester_index := 0
var rot_x := -TAU / 16  # This must be kept in sync with RotationX.
var rot_y := TAU / 8  # This must be kept in sync with CameraHolder.
var camera_distance := 2.0
var base_height: = int(ProjectSettings.get_setting("display/window/size/viewport_height"))

@onready var testers: Node3D = $Testers
@onready var camera_holder: Node3D = $CameraHolder  # Has a position and rotates on Y.
@onready var rotation_x: Node3D = $CameraHolder/RotationX
@onready var camera: Camera3D = $CameraHolder/RotationX/Camera3D

func _ready() -> void:
	camera_holder.transform.basis = Basis.from_euler(Vector3(0, rot_y, 0))
	rotation_x.transform.basis = Basis.from_euler(Vector3(rot_x, 0, 0))
	update_gui()

func _unhandled_input(event: InputEvent) -> void:
	if event.is_action_pressed(&"ui_left"):
		_on_previous_pressed()
	if event.is_action_pressed(&"ui_right"):
		_on_next_pressed()

	if event is InputEventMouseButton:
		if event.button_index == MOUSE_BUTTON_WHEEL_UP:
			camera_distance -= ZOOM_SPEED
		if event.button_index == MOUSE_BUTTON_WHEEL_DOWN:
			camera_distance += ZOOM_SPEED
		camera_distance = clamp(camera_distance, 1.5, 6)

	if event is InputEventMouseMotion and event.button_mask & MAIN_BUTTONS:
		# Compensate motion speed to be resolution-independent (based on the window height).
		var relative_motion: Vector2 = event.relative * DisplayServer.window_get_size().y / base_height
		rot_y -= relative_motion.x * ROT_SPEED
		rot_x -= relative_motion.y * ROT_SPEED
		rot_x = clamp(rot_x, -1.57, 0)
		camera_holder.transform.basis = Basis.from_euler(Vector3(0, rot_y, 0))
		rotation_x.transform.basis = Basis.from_euler(Vector3(rot_x, 0, 0))

func _process(delta: float) -> void:
	var current_tester: Node3D = testers.get_child(tester_index)
	# This code assumes CameraHolder's X and Y coordinates are already correct.
	var current_position := camera_holder.global_transform.origin.z
	var target_position := current_tester.global_transform.origin.z
	camera_holder.global_transform.origin.z = lerpf(current_position, target_position, 3 * delta)
	camera.position.z = lerpf(camera.position.z, camera_distance, 10 * delta)

func _on_previous_pressed() -> void:
	tester_index = max(0, tester_index - 1)
	update_gui()

func _on_next_pressed() -> void:
	tester_index = min(tester_index + 1, testers.get_child_count() - 1)
	update_gui()

func update_gui() -> void:
	$TestName.text = str(testers.get_child(tester_index).name).capitalize()
	$Previous.disabled = tester_index == 0
	$Next.disabled = tester_index == testers.get_child_count() - 1

	# Only display player name field if relevant.
	$Testers/Label3DHealthBar/Name2.visible = str(testers.get_child(tester_index).name) == "Label3DHealthBar"
	$Testers/Label3DHealthBar/LineEdit.visible = str(testers.get_child(tester_index).name) == "Label3DHealthBar"

func _on_line_edit_text_submitted(_new_text: String) -> void:
	$Testers/Label3DHealthBar/LineEdit.release_focus()


extends Control

# Window project settings:
#  - Stretch mode is set to `canvas_items` (`2d` in Godot 3.x)
#  - Stretch aspect is set to `expand`
@onready var world_environment := $WorldEnvironment
@onready var directional_light := $Node3D/DirectionalLight3D
@onready var camera := $Node3D/Camera3D
@onready var fps_label := $FPSLabel
@onready var resolution_label := $ResolutionLabel

var counter := 0.0

# When the screen changes size, we need to update the 3D
# viewport quality setting. If we don't do this, the viewport will take
# the size from the main viewport.
var viewport_start_size := Vector2(
	ProjectSettings.get_setting(&"display/window/size/viewport_width"),
	ProjectSettings.get_setting(&"display/window/size/viewport_height")
)

func _ready() -> void:
	get_viewport().size_changed.connect(update_resolution_label)
	update_resolution_label()

	# Disable V-Sync to uncap framerate on supported platforms. This makes performance comparison
	# easier on high-end machines that easily reach the monitor's refresh rate.
	DisplayServer.window_set_vsync_mode(DisplayServer.VSYNC_DISABLED)

func _process(delta: float) -> void:
	counter += delta
	# Hide FPS label until it's initially updated by the engine (this can take up to 1 second).
	fps_label.visible = counter >= 1.0
	fps_label.text = "%d FPS (%.2f mspf)" % [Engine.get_frames_per_second(), 1000.0 / Engine.get_frames_per_second()]
	# Color FPS counter depending on framerate.
	# The Gradient resource is stored as metadata within the FPSLabel node (accessible in the inspector).
	fps_label.modulate = fps_label.get_meta("gradient").sample(remap(Engine.get_frames_per_second(), 0, 180, 0.0, 1.0))

func update_resolution_label() -> void:
	var viewport_render_size = get_viewport().size * get_viewport().scaling_3d_scale
	resolution_label.text = "3D viewport resolution: %d × %d (%d%%)" \
			% [viewport_render_size.x, viewport_render_size.y, round(get_viewport().scaling_3d_scale * 100)]

func _on_HideShowButton_toggled(show_settings: bool) -> void:
	# Option to hide the settings so you can see the changes to the 3d world better.
	var button := $HideShowButton
	var settings_menu := $SettingsMenu
	if show_settings:
		button.text = "Hide settings"
	else:
		button.text = "Show settings"
	settings_menu.visible = show_settings

# Video settings.

func _on_ui_scale_option_button_item_selected(index: int) -> void:
	# For changing the UI, we take the viewport size, which we set in the project settings.
	var new_size := viewport_start_size
	if index == 0: # Smaller (66%)
		new_size *= 1.5
	elif index == 1: # Small (80%)
		new_size *= 1.25
	elif index == 2: # Medium (100%) (default)
		new_size *= 1.0
	elif index == 3: # Large (133%)
		new_size *= 0.75
	elif index == 4: # Larger (200%)
		new_size *= 0.5
	get_tree().root.set_content_scale_size(new_size)

func _on_quality_slider_value_changed(value: float) -> void:
	get_viewport().scaling_3d_scale = value
	update_resolution_label()

func _on_filter_option_button_item_selected(index: int) -> void:
	# Viewport scale mode setting.
	if index == 0: # Bilinear (Fastest)
		get_viewport().scaling_3d_mode = Viewport.SCALING_3D_MODE_BILINEAR
		# FSR Sharpness is only effective when the scaling mode is FSR 1.0 or 2.2.
		%FSRSharpnessLabel.visible = false
		%FSRSharpnessSlider.visible = false
	elif index == 1: # FSR 1.0 (Fast)
		get_viewport().scaling_3d_mode = Viewport.SCALING_3D_MODE_FSR
		# FSR Sharpness is only effective when the scaling mode is FSR 1.0 or 2.2.
		%FSRSharpnessLabel.visible = true
		%FSRSharpnessSlider.visible = true
	elif index == 2: # FSR 2.2 (Fast)
		get_viewport().scaling_3d_mode = Viewport.SCALING_3D_MODE_FSR2
		# FSR Sharpness is only effective when the scaling mode is FSR 1.0 or 2.2.
		%FSRSharpnessLabel.visible = true
		%FSRSharpnessSlider.visible = true

func _on_fsr_sharpness_slider_value_changed(value: float) -> void:
	# Lower FSR sharpness values result in a sharper image.
	# Invert the slider so that higher values result in a sharper image,
	# which is generally expected from users.
	get_viewport().fsr_sharpness = 2.0 - value

func _on_vsync_option_button_item_selected(index: int) -> void:
	# Vsync is enabled by default.
	# Vertical synchronization locks framerate and makes screen tearing not visible at the cost of
	# higher input latency and stuttering when the framerate target is not met.
	# Adaptive V-Sync automatically disables V-Sync when the framerate target is not met, and enables
	# V-Sync otherwise. This prevents suttering and reduces input latency when the framerate target
	# is not met, at the cost of visible tearing.
	if index == 0: # Disabled (default)
		DisplayServer.window_set_vsync_mode(DisplayServer.VSYNC_DISABLED)
	elif index == 1: # Adaptive
		DisplayServer.window_set_vsync_mode(DisplayServer.VSYNC_ADAPTIVE)
	elif index == 2: # Enabled
		DisplayServer.window_set_vsync_mode(DisplayServer.VSYNC_ENABLED)

func _on_limit_fps_slider_value_changed(value: float):
	# The maximum number of frames per second that can be rendered.
	# A value of 0 means "no limit".
	Engine.max_fps = value

func _on_msaa_option_button_item_selected(index: int) -> void:
	# Multi-sample anti-aliasing. High quality, but slow. It also does not smooth out the edges of
	# transparent (alpha scissor) textures.
	if index == 0: # Disabled (default)
		get_viewport().msaa_3d = Viewport.MSAA_DISABLED
	elif index == 1: # 2×
		get_viewport().msaa_3d = Viewport.MSAA_2X
	elif index == 2: # 4×
		get_viewport().msaa_3d = Viewport.MSAA_4X
	elif index == 3: # 8×
		get_viewport().msaa_3d = Viewport.MSAA_8X

func _on_taa_option_button_item_selected(index: int) -> void:
	# Temporal antialiasing. Smooths out everything including specular aliasing, but can introduce
	# ghosting artifacts and blurring in motion. Moderate performance cost.
	get_viewport().use_taa = index == 1

func _on_fxaa_option_button_item_selected(index: int) -> void:
	# Fast approximate anti-aliasing. Much faster than MSAA (and works on alpha scissor edges),
	# but blurs the whole scene rendering slightly.
	get_viewport().screen_space_aa = int(index == 1) as Viewport.ScreenSpaceAA

func _on_fullscreen_option_button_item_selected(index: int) -> void:
	# To change between winow, fullscreen and other window modes,
	# set the root mode to one of the options of Window.MODE_*.
	# Other modes are maximized and minimized.
	if index == 0: # Disabled (default)
		get_tree().root.set_mode(Window.MODE_WINDOWED)
	elif index == 1: # Fullscreen
		get_tree().root.set_mode(Window.MODE_FULLSCREEN)
	elif index == 2: # Exclusive Fullscreen
		get_tree().root.set_mode(Window.MODE_EXCLUSIVE_FULLSCREEN)

func _on_fov_slider_value_changed(value: float) -> void:
	camera.fov = value

# Quality settings.

fu


extends Camera3D

const MOUSE_SENSITIVITY = 0.002
const MOVE_SPEED = 1.5

var rot := Vector3()
var velocity := Vector3()

func _ready() -> void:
	Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)

func _input(event: InputEvent) -> void:
	# Mouse look (only if the mouse is captured).
	if event is InputEventMouseMotion and Input.get_mouse_mode() == Input.MOUSE_MODE_CAPTURED:
		# Horizontal mouse look.
		rot.y -= event.relative.x * MOUSE_SENSITIVITY
		# Vertical mouse look.
		rot.x = clamp(rot.x - event.relative.y * MOUSE_SENSITIVITY, -1.57, 1.57)
		transform.basis = Basis.from_euler(rot)

	if event.is_action_pressed("toggle_mouse_capture"):
		if Input.get_mouse_mode() == Input.MOUSE_MODE_CAPTURED:
			Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)
		else:
			Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)

func _process(delta: float) -> void:
	var motion := Vector3(
			Input.get_axis(&"move_left", &"move_right"),
			0,
			Input.get_axis(&"move_forward", &"move_back")
	)

	# Normalize motion to prevent diagonal movement from being
	# `sqrt(2)` times faster than straight movement.
	motion = motion.normalized()

	velocity += MOVE_SPEED * delta * (transform.basis * motion)
	velocity *= 0.85
	position += velocity


# This acts as a staging scene shown until the main scene is fully loaded.
extends Control

func _ready() -> void:
	for i in 2:
		# Wait 2 frames before starting to change to the main scene,
		# so that the loading text can be shown instead of the splash screen.
		await get_tree().process_frame

	# Do not use `preload()` to avoid incurring the loading time before the
	# loading text can be shown.
	get_tree().change_scene_to_packed(load("res://test.tscn"))


# This script creates the ImageTexture and assigns it to an existing material at runtime.
# By not having `@tool`, this avoids saving the raw image data in the scene file,
# which would make it much larger.
extends MeshInstance3D

const TEXTURE_SIZE = Vector2i(512, 512)
const GRID_SIZE = 32
const GRID_THICKNESS = 4

func _ready() -> void:
	var image := Image.create(TEXTURE_SIZE.x, TEXTURE_SIZE.y, false, Image.FORMAT_RGB8)
	# Use 1-dimensional loop as it's faster than a nested loop.
	for i in TEXTURE_SIZE.x * TEXTURE_SIZE.y:
		var x := i % TEXTURE_SIZE.y
		var y := i / TEXTURE_SIZE.y
		var color := Color()

		# Draw a grid with more contrasted points where X and Y lines meet.
		# Center the grid's lines so that lines are visible on all the texture's edges.
		if (x + GRID_THICKNESS / 2) % GRID_SIZE < GRID_THICKNESS and (y + GRID_THICKNESS / 2) % GRID_SIZE < GRID_THICKNESS:
			color.g = 0.8
		elif (x + GRID_THICKNESS / 2) % GRID_SIZE < GRID_THICKNESS or (y + GRID_THICKNESS / 2) % GRID_SIZE < GRID_THICKNESS:
			color.g = 0.25

		# Add some random noise for detail.
		color += Color(randf(), randf(), randf()) * 0.1

		image.set_pixel(x, y, color)

	image.generate_mipmaps()
	var image_texture := ImageTexture.create_from_image(image)
	get_surface_override_material(0).albedo_texture = image_texture

	image.bump_map_to_normal_map(5.0)
	image.generate_mipmaps()
	var image_texture_normal := ImageTexture.create_from_image(image)
	get_surface_override_material(0).normal_texture = image_texture_normal


extends Camera3D

const MOUSE_SENSITIVITY = 0.002
const MOVE_SPEED = 0.65

var rot := Vector3()
var velocity := Vector3()

func _ready() -> void:
	Input.mouse_mode = Input.MOUSE_MODE_CAPTURED

func _input(event: InputEvent) -> void:
	# Mouse look (only if the mouse is captured).
	if event is InputEventMouseMotion and Input.get_mouse_mode() == Input.MOUSE_MODE_CAPTURED:
		# Horizontal mouse look.
		rot.y -= event.relative.x * MOUSE_SENSITIVITY
		# Vertical mouse look.
		rot.x = clamp(rot.x - event.relative.y * MOUSE_SENSITIVITY, -1.57, 1.57)
		transform.basis = Basis.from_euler(rot)

	if event.is_action_pressed(&"toggle_mouse_capture"):
		if Input.get_mouse_mode() == Input.MOUSE_MODE_CAPTURED:
			Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)
		else:
			Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)

func _process(delta: float) -> void:
	var motion := Vector3(
			Input.get_axis(&"move_left", &"move_right"),
			0,
			Input.get_axis(&"move_forward", &"move_back")
	)

	# Normalize motion to prevent diagonal movement from being
	# `sqrt(2)` times faster than straight movement.
	motion = motion.normalized()

	velocity += MOVE_SPEED * delta * (transform.basis * motion)
	velocity *= 0.85
	position += velocity


[gd_scene load_steps=3 format=3 uid="uid://3gkujifjokqw"]

[sub_resource type="PlaneMesh" id="1"]

[sub_resource type="ConcavePolygonShape3D" id="2"]
data = PackedVector3Array(-1, 0, 1, 1, 0, -1, 1, 0, 1, -1, 0, 1, -1, 0, -1, 1, 0, -1)

[node name="StaticBodyPlane" type="StaticBody3D"]

[node name="MeshInstance3D" type="MeshInstance3D" parent="."]
transform = Transform3D(50, 0, 0, 0, 1, 0, 0, 0, 50, 0, 0, 0)
mesh = SubResource( "1" )

[node name="CollisionShape" type="CollisionShape3D" parent="."]
transform = Transform3D(50, 0, 0, 0, 1, 0, 0, 0, 50, 0, 0, 0)
shape = SubResource( "2" )


[gd_scene load_steps=4 format=3 uid="uid://cl2vpuxqgnylc"]

[ext_resource type="Shape3D" path="res://assets/robot_head/godot3_robot_head_collision.tres" id="1"]
[ext_resource type="ArrayMesh" path="res://assets/robot_head/godot3_robot_head.mesh" id="2"]
[ext_resource type="PackedScene" uid="uid://3gkujifjokqw" path="res://tests/static_scene_plane.tscn" id="3"]

[node name="StaticScene" type="Node3D"]

[node name="StaticBodyPlane" parent="." instance=ExtResource( "3" )]

[node name="StaticBodyHead" type="StaticBody3D" parent="."]
transform = Transform3D(10, 0, 0, 0, 8.66025, 5, 0, -5, 8.66025, 0, -11.1389, 2.29332)

[node name="RobotHead" type="MeshInstance3D" parent="StaticBodyHead"]
mesh = ExtResource( "2" )

[node name="CollisionShape" type="CollisionShape3D" parent="StaticBodyHead"]
shape = ExtResource( "1" )


extends Test

const OPTION_TEST_CASE_HIT_FROM_INSIDE = "Test case/Hit from inside"

var _hit_from_inside := false
var _do_raycasts := false

@onready var _raycast_visuals := ImmediateMesh.new()
@onready var _material := StandardMaterial3D.new()

func _ready() -> void:
	var options: OptionMenu = $Options

	options.add_menu_item(OPTION_TEST_CASE_HIT_FROM_INSIDE, true, false)

	options.option_changed.connect(_on_option_changed)

	_material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
	_material.vertex_color_use_as_albedo = true

	var raycast_mesh_instance := MeshInstance3D.new()
	raycast_mesh_instance.mesh = _raycast_visuals
	add_child(raycast_mesh_instance)
	move_child(raycast_mesh_instance, get_child_count())

	await start_timer(0.5).timeout
	if is_timer_canceled():
		return

	_do_raycasts = true

func _physics_process(delta: float) -> void:
	super._physics_process(delta)

	if not _do_raycasts:
		return

	_do_raycasts = false

	Log.print_log("* Start Raycasting...")

	_raycast_visuals.clear_surfaces()
	_raycast_visuals.surface_begin(Mesh.PRIMITIVE_LINES)

	for shape in $Shapes.get_children():
		var body: PhysicsBody3D = shape
		var space_state := body.get_world_3d().direct_space_state

		Log.print_log("* Testing: %s" % body.name)

		var center := body.global_transform.origin

		# Raycast entering from the top.
		var res := _add_raycast(space_state, center + Vector3(0.0, 2.0, 0.0), center)
		Log.print_log("Raycast in: %s" % ("HIT" if res else "NO HIT"))

		# Raycast exiting from inside.
		center.x -= 0.2
		res = _add_raycast(space_state, center, center - Vector3(0.0, 3.0, 0.0))
		Log.print_log("Raycast out: %s" % ("HIT" if res else "NO HIT"))

		# Raycast all inside.
		center.x += 0.4
		res = _add_raycast(space_state, center, center - Vector3(0.0, 0.8, 0.0))
		Log.print_log("Raycast inside: %s" % ("HIT" if res else "NO HIT"))

	_raycast_visuals.surface_end()

	_raycast_visuals.surface_set_material(0, _material)

func _on_option_changed(option: String, checked: bool) -> void:
	match option:
		OPTION_TEST_CASE_HIT_FROM_INSIDE:
			_hit_from_inside = checked
			_do_raycasts = true

func _add_raycast(space_state: PhysicsDirectSpaceState3D, pos_start: Vector3, pos_end: Vector3) -> Dictionary:
	var params := PhysicsRayQueryParameters3D.new()
	params.from = pos_start
	params.to = pos_end
	params.hit_from_inside = _hit_from_inside

	var result := space_state.intersect_ray(params)
	if result:
		_raycast_visuals.surface_set_color(Color.GREEN)
	else:
		_raycast_visuals.surface_set_color(Color.RED.darkened(0.5))

	# Draw raycast line.
	_raycast_visuals.surface_add_vertex(pos_start)
	_raycast_visuals.surface_add_vertex(pos_end)

	# Draw raycast arrow.
	_raycast_visuals.surface_add_vertex(pos_end)
	_raycast_visuals.surface_add_vertex(pos_end + Vector3(-0.05, 0.1, 0.0))
	_raycast_visuals.surface_add_vertex(pos_end)
	_raycast_visuals.surface_add_vertex(pos_end + Vector3(0.05, 0.1, 0.0))

	return result


[gd_scene load_steps=28 format=3 uid="uid://se7gyhmygqul"]

[ext_resource type="Script" path="res://utils/rigidbody_ground_check.gd" id="1"]
[ext_resource type="PackedScene" uid="uid://b1ihqm3x8jru" path="res://tests/test_options.tscn" id="2"]
[ext_resource type="Script" path="res://tests/functional/test_rigidbody_ground_check.gd" id="3"]
[ext_resource type="Script" path="res://utils/camera_orbit.gd" id="4"]

[sub_resource type="PhysicsMaterial" id="1"]
friction = 0.0

[sub_resource type="BoxShape3D" id="2"]

[sub_resource type="BoxMesh" id="3"]

[sub_resource type="PhysicsMaterial" id="5"]
friction = 0.0

[sub_resource type="CapsuleShape3D" id="6"]

[sub_resource type="CapsuleMesh" id="7"]

[sub_resource type="PhysicsMaterial" id="9"]
friction = 0.0

[sub_resource type="CylinderShape3D" id="10"]

[sub_resource type="CylinderMesh" id="11"]

[sub_resource type="PhysicsMaterial" id="13"]
friction = 0.0

[sub_resource type="ConvexPolygonShape3D" id="14"]
points = PackedVector3Array(-0.7, 0, -0.7, -0.3, 0, 0.8, 0.8, 0, -0.3, 0, -1, 0)

[sub_resource type="ArrayMesh" id="15"]
_surfaces = [{
"aabb": AABB(-0.7, -1, -0.7, 1.5, 1.00001, 1.5),
"format": 34359742467,
"index_count": 12,
"index_data": PackedByteArray(0, 0, 1, 0, 3, 0, 1, 0, 2, 0, 3, 0, 2, 0, 0, 0, 3, 0, 2, 0, 1, 0, 0, 0),
"primitive": 3,
"uv_scale": Vector4(0, 0, 0, 0),
"vertex_count": 4,
"vertex_data": PackedByteArray(51, 51, 51, 191, 0, 0, 0, 0, 51, 51, 51, 191, 154, 153, 153, 190, 0, 0, 0, 0, 205, 204, 76, 63, 205, 204, 76, 63, 0, 0, 0, 0, 154, 153, 153, 190, 0, 0, 0, 0, 0, 0, 128, 191, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 96, 59, 182, 3, 0, 0, 0, 0, 0, 0)
}]

[sub_resource type="PhysicsMaterial" id="17"]
friction = 0.0

[sub_resource type="SphereShape3D" id="18"]

[sub_resource type="SphereMesh" id="19"]

[sub_resource type="PlaneMesh" id="23"]
size = Vector2(50, 20)

[sub_resource type="ConvexPolygonShape3D" id="24"]
points = PackedVector3Array(25, 0, 10, -25, 0, 10, 25, 0, -10, -25, 0, -10)

[sub_resource type="ConvexPolygonShape3D" id="25"]
points = PackedVector3Array(25, 0, 10, -25, 0, 10, 25, 0, -10, -25, 0, -10)

[sub_resource type="ConvexPolygonShape3D" id="26"]
points = PackedVector3Array(50, 0, 50, -50, 0, 50, 50, 0, -50, -50, 0, -50)

[sub_resource type="ConcavePolygonShape3D" id="27"]
data = PackedVector3Array(-1, 0, 1, 1, 0, -1, 1, 0, 1, -1, 0, 1, -1, 0, -1, 1, 0, -1)

[sub_resource type="ConcavePolygonShape3D" id="28"]
data = PackedVector3Array(50, 0, 50, -50, 0, 50, 50, 0, -50, -50, 0, 50, -50, 0, -50, 50, 0, -50)

[sub_resource type="BoxShape3D" id="29"]
size = Vector3(100, 2, 40)

[sub_resource type="BoxShape3D" id="30"]
size = Vector3(200, 2, 200)

[node name="Test" type="Node3D"]
script = ExtResource("3")

[node name="LabelBodyType" type="Label" parent="."]
offset_left = 14.0
offset_top = 62.0
offset_right = 171.0
offset_bottom = 85.0
text = "Floor Type: "

[node name="Options" parent="." instance=ExtResource("2")]
offset_top = 120.0
offset_bottom = 140.0
focus_mode = 2

[node name="DynamicShapes" type="Node3D" parent="."]

[node name="Bodies" type="Node3D" parent="DynamicShapes"]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 5.2912, 0)

[node name="RigidBodyBox" type="RigidBody3D" parent="DynamicShapes/Bodies"]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -6, 0, 0)
axis_lock_angular_x = true
axis_lock_angular_y = true
axis_lock_angular_z = true
physics_material_override = SubResource("1")
script = ExtResource("1")

[node name="CollisionShape" type="CollisionShape3D" parent="DynamicShapes/Bodies/RigidBodyBox"]
transform = Transform3D(0.6, 0, 0, 0, 1, 0, 0, 0, 0.6, 0, 0, 0)
shape = SubResource("2")

[node name="MeshInstance3D" type="MeshInstance3D" parent="DynamicShapes/Bodies/RigidBodyBox/CollisionShape"]
mesh = SubResource("3")

[node name="RigidBodyCapsule" type="RigidBody3D" parent="DynamicShapes/Bodies"]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -3, 0, 0)
axis_lock_angular_x = true
axis_lock_angular_y = true
axis_lock_angular_z = true
physics_material_override = SubResource("5")
script = ExtResource("1")

[node name="CollisionShape" type="CollisionShape3D" parent="DynamicShapes/Bodies/RigidBodyCapsule"]
transform = Transform3D(0.8, 0, 0, 0, 0.8, 0, 0, 0, 0.8, 0, 0, 0)
shape = SubResource("6")

[node name="MeshInstance3D" type="MeshInstance3D" parent="DynamicShapes/Bodies/RigidBodyCapsule/CollisionShape"]
mesh = SubResource("7")

[node name="RigidBodyCylinder" type="RigidBody3D" parent="DynamicShapes/Bodies"]
axis_lock_angular_x = true
axis_lock_angular_y = true
axis_lock_angular_z = true
physics_material_override = SubResource("9")
script = ExtResource("1")

[node name="CollisionShape" type="CollisionShape3D" parent="DynamicShapes/Bodies/RigidBodyCylinder"]
transform = Transform3D(0.8, 0, 0, 0, 1, 0, 0, 0, 0.8, 0, 0, 0)
shape = SubResource("10")

[node name="MeshInstance3D" type="MeshInstance3D" parent="DynamicShapes/Bodies/RigidBodyCylinder/CollisionShape"]
mesh = SubResource("11")


[node name="RigidBodyConvex" type="RigidBody3D" parent="DynamicShapes/Bodies"]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 3, 0.974548, 0)
axis_lock_angular_x = true
axis_lock_angular_y = true
axis_lock_angular_z = true
physics_material_override = SubResource("13")
script = ExtResource("1")

[node name="CollisionShape" type="CollisionShape3D" parent="DynamicShapes/Bodies/RigidBodyConvex"]
transform = Transform3D(1.5, 0, 0, 0, 2, 0, 0, 0, 1.5, 0, 0, 0)
shape = SubResource("14")

[node name="MeshInstance3D" type="MeshInstance3D" parent="DynamicShapes/Bodies/RigidBodyConvex/CollisionShape"]
mesh = SubResource("15")

[node name="RigidBodySphere" type="RigidBody3D" parent="DynamicShapes/Bodies"]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 6, 0, 0)
axis_lock_angular_x = true
axis_lock_angular_y = true
axis_lock_angular_z = true
physics_material_override = SubResource("17")
script = ExtResource("1")

[node name="CollisionShape" type="CollisionShape3D" parent="DynamicShapes/Bodies/RigidBodySphere"]
transform = Transform3D(0.8, 0, 0, 0, 0.8, 0, 0, 0, 0.8, 0, 0, 0)
shape = SubResource("18")

[node name="MeshInstance3D" type="MeshInstance3D" parent="DynamicShapes/Bodies/RigidBodySphere/CollisionShape"]
mesh = SubResource("19")

[node name="Floors" type="Node3D" parent="."]

[node name="ConvexSmall" type="Node3D" parent="Floors"]

[node name="ConvexFloor" type="StaticBody3D" parent="Floors/ConvexSmall"]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, -10)

[node name="MeshInstance3D" type="MeshInstance3D" parent="Floors/ConvexSmall/ConvexFloor"]
visible = false
mesh = SubResource("23")

[node name="CollisionShape" type="CollisionShape3D" parent="Floors/ConvexSmall/ConvexFloor"]
shape = SubResource("24")

[node name="ConvexFloor2" type="StaticBody3D" parent="Floors/ConvexSmall"]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 10)

[node name="MeshInstance3D" type="MeshInstance3D" parent="Floors/ConvexSmall/ConvexFloor2"]
visible = false
mesh = SubResource("23")

[node name="CollisionShape" type="CollisionShape3D" parent="Floors/ConvexSmall/ConvexFloor2"]
shape = SubResource("25")

[node name="ConvexBig" type="Node3D" parent="Floors"]

[node name="ConvexFloor" type="StaticBody3D" parent="Floors/ConvexBig"]

[node name="MeshInstance3D" type="MeshInstance3D" parent="Floors/ConvexBig/ConvexFloor"]
visible = false
mesh = SubResource("23")

[node name="CollisionShape" type="CollisionShape3D" parent="Floors/ConvexBig/ConvexFloor"]
shape = SubResource("26")

[node name="ConcaveSmall" type="Node3D" parent="Floors"]

[node name="ConcaveFloor" type="StaticBody3D" parent="Floors/ConcaveSmall"]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, -10)

[node name="MeshInstance3D" type="MeshInstance3D" parent="Floors/ConcaveSmall/ConcaveFloor"]
visible = false
mesh = SubResource("23")

[node name="CollisionShape" type="CollisionShape3D" parent="Floors/ConcaveSmall/ConcaveFloor"]
transform = Transform3D(25, 0, 0, 0, 1, 0, 0, 0, 10, 0, 0, 0)
shape = SubResource("27")

[node name="ConcaveFloor2" type="StaticBody3D" parent="Floors/ConcaveSmall"]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 10)

[node name="MeshInstance3D" type="MeshInstance3D" parent="Floors/ConcaveSmall/ConcaveFloor2"]
visible = false
mesh = SubResource("23")

[node name="CollisionShape" type="CollisionShape3D" parent="Floors/ConcaveSmall/ConcaveFloor2"]
transform = Transform3D(25, 0, 0, 0, 1, 0, 0, 0, 10, 0, 0, 0)
shape = SubResource("27")

[node name="ConcaveBig" type="Node3D" parent="Floors"]

[node name="ConcaveFloor" type="StaticBody3D" parent="Floors/ConcaveBig"]

[node name="MeshInstance3D" type="MeshInstance3D" parent="Floors/ConcaveBig/ConcaveFloor"]
visible = false
mesh = SubResource("23")

[node name="CollisionShape" type="CollisionShape3D" parent="Floors/ConcaveBig/ConcaveFloor"]
shape = SubResource("28")

[node name="BoxSmall" type="Node3D" parent="Floors"]

[node name="BoxFloor" type="StaticBody3D" parent="Floors/BoxSmall"]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, -10)

[node name="MeshInstance3D" type="MeshInstance3D" parent="Floors/BoxSmall/BoxFloor"]
visible = false
mesh = SubResource("23")

[node name="CollisionShape" type="CollisionShape3D" parent="Floors/BoxSmall/BoxFloor"]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -1, 0)
shape = SubResource("29")

[node name="BoxFloor2" type="StaticBody3D" parent="Floors/BoxSmall"]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 10)

[node name="MeshInstance3D" type="MeshInstance3D" parent="Floors/BoxSmall/BoxFloor2"]
visible = false
mesh = SubResource("23")

[node name="CollisionShape" type="CollisionShape3D" parent="Floors/BoxSmall/BoxFloor2"]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -1, 0)
shape = SubResource("29")

[node name="BoxBig" type="Node3D" parent="Floors"]

[node name="BoxFloor" type="StaticBody3D" parent="Floors/BoxBig"]

[node name="MeshInstance3D" type="MeshInstance3D" parent="Floors/BoxBig/BoxFloor"]
visible = false
mesh = SubResource("23")

[node name="CollisionShape" type="CollisionShape3D" parent="Floors/BoxBig/BoxFloor"]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -1, 0)
shape = SubResource("30")

[node name="Camera3D" type="Camera3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 3.604, 22.124)
far = 1000.0
script = ExtResource("4")


[gd_scene load_steps=13 format=3 uid="uid://dgh5drb4q81kh"]

[ext_resource type="Script" path="res://utils/camera_orbit.gd" id="1"]
[ext_resource type="Script" path="res://tests/functional/test_moving_platform.gd" id="2"]
[ext_resource type="PackedScene" uid="uid://b1ihqm3x8jru" path="res://tests/test_options.tscn" id="3"]
[ext_resource type="Script" path="res://utils/characterbody_physics.gd" id="4"]

[sub_resource type="CapsuleShape3D" id="1"]
radius = 0.3

[sub_resource type="BoxShape3D" id="2"]
size = Vector3(0.6, 1.6, 0.6)

[sub_resource type="CylinderShape3D" id="3"]
radius = 0.3
height = 1.60005

[sub_resource type="SphereShape3D" id="4"]
radius = 0.79945

[sub_resource type="ConvexPolygonShape3D" id="5"]
points = PackedVector3Array(-0.7, 0, -0.7, -0.3, 0, 0.8, 0.8, 0, -0.3, 0, -0.8, 0)

[sub_resource type="PhysicsMaterial" id="7"]

[sub_resource type="BoxShape3D" id="8"]
size = Vector3(4, 0.4, 2)

[sub_resource type="Animation" id="9"]
length = 9.0

[node name="Test2" type="Node3D"]
script = ExtResource( "2" )

[node name="LabelBodyType" type="Label" parent="."]
offset_left = 14.0
offset_top = 78.0
offset_right = 171.0
offset_bottom = 92.0
text = "Body Type: "
__meta__ = {
"_edit_use_anchors_": false
}

[node name="Options" parent="." instance=ExtResource( "3" )]
offset_top = 153.0
offset_right = 134.0
offset_bottom = 182.0

[node name="Bodies" type="Node3D" parent="."]

[node name="CharacterBody3D" type="CharacterBody3D" parent="Bodies"]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -7, -4.18538, 0)
collision_layer = 2
script = ExtResource( "4" )
_stop_on_slopes = true
use_snap = true

[node name="Capsule" type="CollisionShape3D" parent="Bodies/CharacterBody3D"]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.8, 0)
shape = SubResource( "1" )

[node name="Box" type="CollisionShape3D" parent="Bodies/CharacterBody3D"]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.8, 0)
shape = SubResource( "2" )

[node name="Cylinder" type="CollisionShape3D" parent="Bodies/CharacterBody3D"]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.8, 0)
shape = SubResource( "3" )

[node name="Sphere" type="CollisionShape3D" parent="Bodies/CharacterBody3D"]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.8, 0)
shape = SubResource( "4" )

[node name="Convex" type="CollisionShape3D" parent="Bodies/CharacterBody3D"]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.8, 0)
shape = SubResource( "5" )

[node name="RigidBody" type="RigidDynamicBody3D" parent="Bodies"]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -7, -4.18538, 0)
collision_layer = 4
axis_lock_angular_x = true
axis_lock_angular_y = true
axis_lock_angular_z = true
physics_material_override = SubResource( "7" )

[node name="Capsule" type="CollisionShape3D" parent="Bodies/RigidBody"]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.8, 0)
shape = SubResource( "1" )

[node name="Box" type="CollisionShape3D" parent="Bodies/RigidBody"]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.8, 0)
shape = SubResource( "2" )

[node name="Cylinder" type="CollisionShape3D" parent="Bodies/RigidBody"]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.8, 0)
shape = SubResource( "3" )

[node name="Sphere" type="CollisionShape3D" parent="Bodies/RigidBody"]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.8, 0)
shape = SubResource( "4" )

[node name="Convex" type="CollisionShape3D" parent="Bodies/RigidBody"]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.8, 0)
shape = SubResource( "5" )

[node name="Platforms" type="Node3D" parent="."]

[node name="MovingPlatform" type="AnimatableBody3D" parent="Platforms"]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -7, -4.235, 0)

[node name="CollisionShape" type="CollisionShape3D" parent="Platforms/MovingPlatform"]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.2, 0)
shape = SubResource( "8" )

[node name="AnimationPlayer" type="AnimationPlayer" parent="Platforms/MovingPlatform"]
anims/Move = SubResource( "9" )

[node name="Camera3D" type="Camera3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 10)
current = true
script = ExtResource( "1" )

[node name="OmniLight" type="OmniLight3D" parent="Camera3D"]
omni_range = 50.0


extends RigidBody3D

var _dir := 1.0
var _distance := 10.0
var _walk_spd := 100.0
var _acceleration := 22.0
var _is_on_floor := false

@onready var _forward := -transform.basis.z
@onready var _collision_shape := $CollisionShape
@onready var _material: StandardMaterial3D = $CollisionShape/MeshInstance3D.get_active_material(0)

func _ready() -> void:
	if not _material:
		_material = StandardMaterial3D.new()
		$CollisionShape/MeshInstance3D.set_surface_override_material(0, _material)

func _process(_delta: float) -> void:
	if _is_on_floor:
		_material.albedo_color = Color.WHITE
	else:
		_material.albedo_color = Color.RED

func _integrate_forces(state: PhysicsDirectBodyState3D) -> void:
	var delta := state.step
	var velocity := (_forward * _dir * _walk_spd * delta) + (state.linear_velocity  * Vector3.UP)
	state.linear_velocity = state.linear_velocity.move_toward(velocity, _acceleration * delta)

	if state.transform.origin.z < -_distance:
		_dir = -1
	if state.transform.origin.z > _distance:
		_dir = 1

	ground_check()

func ground_check() -> void:
	var space_state := get_world_3d().direct_space_state
	var shape := PhysicsShapeQueryParameters3D.new()
	shape.transform = _collision_shape.global_transform
	shape.shape_rid = _collision_shape.shape.get_rid()
	shape.collision_mask = 2
	var result := space_state.get_rest_info(shape)
	if result:
		_is_on_floor = true
	else:
		_is_on_floor = false


extends RigidBody3D

const MOUSE_DELTA_COEFFICIENT = 0.01
const CAMERA_DISTANCE_COEFFICIENT = 0.2

var _picked := false
var _last_mouse_pos := Vector2.ZERO
var _mouse_pos := Vector2.ZERO

func _ready() -> void:
	input_ray_pickable = true

func _input(event: InputEvent) -> void:
	if event is InputEventMouseButton:
		if not event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
			_picked = false

	if event is InputEventMouseMotion:
		_mouse_pos = event.position

func _input_event(_camera: Camera3D, event: InputEvent, _position: Vector3, _normal: Vector3, _shape_idx: int) -> void:
	if event is InputEventMouseButton:
		if event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
			_picked = true
			_mouse_pos = event.position
			_last_mouse_pos = _mouse_pos

func _physics_process(delta: float) -> void:
	if _picked:
		var mouse_delta := _mouse_pos - _last_mouse_pos

		var world_delta := Vector3.ZERO
		world_delta.x = mouse_delta.x * MOUSE_DELTA_COEFFICIENT
		world_delta.y = -mouse_delta.y * MOUSE_DELTA_COEFFICIENT

		var camera := get_viewport().get_camera_3d()
		if camera:
			var camera_basis := camera.global_transform.basis
			world_delta = camera_basis * world_delta

			var camera_dist := camera.global_transform.origin.distance_to(global_transform.origin)
			const DEFAULT_CAMERA_FOV = 75.0
			var fov_coefficient := camera.fov / DEFAULT_CAMERA_FOV
			world_delta *= CAMERA_DISTANCE_COEFFICIENT * camera_dist * fov_coefficient

		if freeze:
			global_transform.origin += world_delta
		else:
			linear_velocity = world_delta / delta
		_last_mouse_pos = _mouse_pos


extends Node

enum PhysicsEngine {
	GODOT_PHYSICS,
	OTHER,
}

var _engine := PhysicsEngine.OTHER

func _enter_tree() -> void:
	process_mode = Node.PROCESS_MODE_ALWAYS

	# Always enable visible collision shapes on startup
	# (same as the Debug > Visible Collision Shapes option).
	get_tree().debug_collisions_hint = true

	var engine_string: String = ProjectSettings.get_setting("physics/3d/physics_engine")
	match engine_string:
		"DEFAULT":
			_engine = PhysicsEngine.GODOT_PHYSICS
		"GodotPhysics3D":
			_engine = PhysicsEngine.GODOT_PHYSICS
		_:
			_engine = PhysicsEngine.OTHER

func _process(_delta: float) -> void:
	if Input.is_action_just_pressed(&"toggle_full_screen"):
		if DisplayServer.window_get_mode() == DisplayServer.WINDOW_MODE_FULLSCREEN:
			DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_WINDOWED)
		else:
			DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_FULLSCREEN)

	if Input.is_action_just_pressed(&"toggle_debug_collision"):
		var debug_collision_enabled := not _is_debug_collision_enabled()
		_set_debug_collision_enabled(debug_collision_enabled)
		if debug_collision_enabled:
			Log.print_log("Debug Collision ON")
		else:
			Log.print_log("Debug Collision OFF")

	if Input.is_action_just_pressed(&"toggle_pause"):
		get_tree().paused = not get_tree().paused

	if Input.is_action_just_pressed(&"exit"):
		get_tree().quit()

func get_physics_engine() -> PhysicsEngine:
	return _engine

func _set_debug_collision_enabled(enabled: bool) -> void:
	get_tree().debug_collisions_hint = enabled

func _is_debug_collision_enabled() -> bool:
	return get_tree().debug_collisions_hint


extends CharacterBody3D

@export var _stop_on_slopes := false
@export var use_snap := false

var _gravity := 20.0

func _physics_process(delta: float) -> void:
	if is_on_floor():
		floor_snap_length = 0.2
	else:
		velocity += Vector3.DOWN * _gravity * delta
		floor_snap_length = 0.0

	floor_stop_on_slope = _stop_on_slopes
	move_and_slide()


extends Node3D

const Character = preload("res://character.gd")

var _cam_rotation := 0.0

@onready var _camera := $CameraBase/Camera3D as Camera3D
@onready var _robot := $RobotBase as Character

func _unhandled_input(event: InputEvent) -> void:
	if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT and event.pressed:
		# Get closest point on navmesh for the current mouse cursor position.
		var mouse_cursor_position: Vector2 = event.position
		var camera_ray_length := 1000.0
		var camera_ray_start := _camera.project_ray_origin(mouse_cursor_position)
		var camera_ray_end := camera_ray_start + _camera.project_ray_normal(mouse_cursor_position) * camera_ray_length

		var closest_point_on_navmesh := NavigationServer3D.map_get_closest_point_to_segment(
				get_world_3d().navigation_map,
				camera_ray_start,
				camera_ray_end
		)
		_robot.set_target_position(closest_point_on_navmesh)

	elif event is InputEventMouseMotion:
		if event.button_mask & (MOUSE_BUTTON_MASK_MIDDLE + MOUSE_BUTTON_MASK_RIGHT):
			_cam_rotation += event.relative.x * 0.005
			$CameraBase.set_rotation(Vector3.UP * _cam_rotation)


extends Camera3D

const MOUSE_SENSITIVITY = 0.002
const MOVE_SPEED = 0.6

var volumetric_fog_volume_size := int(ProjectSettings.get_setting("rendering/environment/volumetric_fog/volume_size"))
var volumetric_fog_volume_depth := int(ProjectSettings.get_setting("rendering/environment/volumetric_fog/volume_depth"))

var rot := Vector3()
var velocity := Vector3()

@onready var label: Label = $Label

func _ready() -> void:
	Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
	update_label()

func _process(delta: float) -> void:
	var motion := Vector3(
			Input.get_action_strength("move_right") - Input.get_action_strength("move_left"),
			0,
			Input.get_action_strength("move_back") - Input.get_action_strength("move_forward")
	)

	# Normalize motion to prevent diagonal movement from being
	# `sqrt(2)` times faster than straight movement.
	motion = motion.normalized()

	velocity += MOVE_SPEED * delta * (transform.basis * motion)
	velocity *= 0.85
	position += velocity

func _input(event: InputEvent) -> void:
	# Mouse look (only if the mouse is captured).
	if event is InputEventMouseMotion and Input.get_mouse_mode() == Input.MOUSE_MODE_CAPTURED:
		# Horizontal mouse look.
		rot.y -= event.relative.x * MOUSE_SENSITIVITY
		# Vertical mouse look.
		rot.x = clamp(rot.x - event.relative.y * MOUSE_SENSITIVITY, -1.57, 1.57)
		transform.basis = Basis.from_euler(rot)

	if event.is_action_pressed("toggle_mouse_capture"):
		if Input.get_mouse_mode() == Input.MOUSE_MODE_CAPTURED:
			Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)
		else:
			Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)

	if event.is_action_pressed("toggle_temporal_reprojection"):
		get_world_3d().environment.volumetric_fog_temporal_reprojection_enabled = not get_world_3d().environment.volumetric_fog_temporal_reprojection_enabled
		update_label()
	elif event.is_action_pressed("increase_temporal_reprojection"):
		get_world_3d().environment.volumetric_fog_temporal_reprojection_amount = clamp(get_world_3d().environment.volumetric_fog_temporal_reprojection_amount + 0.01, 0.5, 0.99)
		update_label()
	elif event.is_action_pressed("decrease_temporal_reprojection"):
		get_world_3d().environment.volumetric_fog_temporal_reprojection_amount = clamp(get_world_3d().environment.volumetric_fog_temporal_reprojection_amount - 0.01, 0.5, 0.99)
		update_label()
	elif event.is_action_pressed("increase_fog_density"):
		get_world_3d().environment.volumetric_fog_density = clamp(get_world_3d().environment.volumetric_fog_density + 0.01, 0.0, 1.0)
		update_label()
	elif event.is_action_pressed("decrease_fog_density"):
		get_world_3d().environment.volumetric_fog_density = clamp(get_world_3d().environment.volumetric_fog_density - 0.01, 0.0, 1.0)
		update_label()
	elif event.is_action_pressed("increase_volumetric_fog_quality"):
		volumetric_fog_volume_size = clamp(volumetric_fog_volume_size + 16, 16, 384)
		volumetric_fog_volume_depth = clamp(volumetric_fog_volume_depth + 16, 16, 384)
		RenderingServer.environment_set_volumetric_fog_volume_size(volumetric_fog_volume_size, volumetric_fog_volume_depth)
		update_label()
	elif event.is_action_pressed("decrease_volumetric_fog_quality"):
		volumetric_fog_volume_size = clamp(volumetric_fog_volume_size - 16, 16, 384)
		volumetric_fog_volume_depth = clamp(volumetric_fog_volume_depth - 16, 16, 384)
		RenderingServer.environment_set_volumetric_fog_volume_size(volumetric_fog_volume_size, volumetric_fog_volume_depth)
		update_label()

func update_label() -> void:
	if get_world_3d().environment.volumetric_fog_temporal_reprojection_enabled:
		label.text = "Fog density: %.2f\nTemporal reprojection: Enabled\nTemporal reprojection strength: %.2f\nVolumetric fog quality: %d×%d×%d" % [
			get_world_3d().environment.volumetric_fog_density,
			get_world_3d().environment.volumetric_fog_temporal_reprojection_amount,
			volumetric_fog_volume_size,
			volumetric_fog_volume_size,
			volumetric_fog_volume_depth,
		]
	else:
		label.text = "Fog density: %.2f\nTemporal reprojection: Disabled\nVolumetric fog quality: %d×%d×%d" % [
			get_world_3d().environment.volumetric_fog_density,
			volumetric_fog_volume_size,
			volumetric_fog_volume_size,
			volumetric_fog_volume_depth,
		]


extends Camera3D

var collision_exception := []
var max_height := 2.0
var min_height := 0

@export var min_distance := 0.5
@export var max_distance := 3.0
@export var angle_v_adjust := 0.0
@onready var target_node: Node3D = get_parent()

func _ready() -> void:
	collision_exception.append(target_node.get_parent().get_rid())
	# Detach the camera transform from the parent spatial node.
	top_level = true

func _physics_process(_delta: float) -> void:
	var target_pos := target_node.global_transform.origin
	var camera_pos := global_transform.origin

	var delta_pos := camera_pos - target_pos

	# Regular delta follow.

	# Check ranges.
	if delta_pos.length() < min_distance:
		delta_pos = delta_pos.normalized() * min_distance
	elif delta_pos.length() > max_distance:
		delta_pos = delta_pos.normalized() * max_distance

	# Check upper and lower height.
	delta_pos.y = clamp(delta_pos.y, min_height, max_height)
	camera_pos = target_pos + delta_pos

	look_at_from_position(camera_pos, target_pos, Vector3.UP)

	# Turn a little up or down.
	var t := transform
	t.basis = Basis(t.basis[0], deg_to_rad(angle_v_adjust)) * t.basis
	transform = t


[gd_scene load_steps=11 format=3 uid="uid://csdy1t6j24awl"]

[ext_resource type="Texture2D" uid="uid://c2eorasvxyf5u" path="res://menu/main/title.png" id="1"]
[ext_resource type="Script" path="res://menu/main/splash_text.gd" id="2"]
[ext_resource type="Script" path="res://menu/main/main_menu.gd" id="3"]
[ext_resource type="Texture2D" uid="uid://c514noc8ngc4x" path="res://menu/main/dark_dirt.png" id="4"]
[ext_resource type="PackedScene" uid="uid://e22n700qhxqr" path="res://menu/options/options.tscn" id="5"]
[ext_resource type="Theme" uid="uid://ckax6htygmelo" path="res://menu/theme/theme.tres" id="6"]
[ext_resource type="Texture2D" uid="uid://ej6lqf2fptv8" path="res://menu/button.png" id="7"]
[ext_resource type="Texture2D" uid="uid://blcjvv0p0a6v8" path="res://menu/button_pressed.png" id="7_hsmm5"]
[ext_resource type="Texture2D" uid="uid://cfh73c8wnvgol" path="res://menu/button_hover.png" id="8_tutt7"]
[ext_resource type="Texture2D" uid="uid://h208mfx7dyy2" path="res://menu/button_focus.png" id="9_d3hm1"]

[node name="MainMenu" type="Control"]
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
theme = ExtResource("6")
script = ExtResource("3")

[node name="Background" type="TextureRect" parent="."]
layout_mode = 0
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
texture = ExtResource("4")
stretch_mode = 1

[node name="TitleScreen" type="VBoxContainer" parent="."]
layout_mode = 0
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2

[node name="Logo" type="CenterContainer" parent="TitleScreen"]
texture_repeat = 2
custom_minimum_size = Vector2(0, 350)
layout_mode = 2

[node name="Logo" type="TextureRect" parent="TitleScreen/Logo"]
layout_mode = 2
texture = ExtResource("1")

[node name="SplashHolder" type="Control" parent="TitleScreen/Logo/Logo"]
layout_mode = 1
anchors_preset = 3
anchor_left = 1.0
anchor_top = 1.0
anchor_right = 1.0
anchor_bottom = 1.0
offset_left = -36.0
offset_right = -36.0
grow_horizontal = 0
grow_vertical = 0
script = ExtResource("2")

[node name="SplashText" type="Label" parent="TitleScreen/Logo/Logo/SplashHolder"]
modulate = Color(1, 1, 0, 1)
texture_filter = 1
layout_mode = 0
anchor_left = 1.0
anchor_top = 1.0
anchor_right = 1.0
anchor_bottom = 1.0
offset_left = -120.0
offset_right = 124.0
offset_bottom = 52.0
grow_horizontal = 0
grow_vertical = 0
rotation = -0.261799
theme_override_font_sizes/font_size = 61
text = "Made in Godot!"

[node name="ButtonHolder" type="HBoxContainer" parent="TitleScreen"]
layout_mode = 2
size_flags_vertical = 2
alignment = 1

[node name="MainButtons" type="VBoxContainer" parent="TitleScreen/ButtonHolder"]
layout_mode = 2
theme_override_constants/separation = 20
alignment = 1

[node name="Start" type="TextureButton" parent="TitleScreen/ButtonHolder/MainButtons"]
layout_mode = 2
texture_normal = ExtResource("7")
texture_pressed = ExtResource("7_hsmm5")
texture_hover = ExtResource("8_tutt7")
texture_focused = ExtResource("9_d3hm1")

[node name="Label" type="Label" parent="TitleScreen/ButtonHolder/MainButtons/Start"]
texture_filter = 1
layout_mode = 0
anchor_right = 1.0
anchor_bottom = 1.0
offset_top = -1.0
offset_bottom = -18.0
grow_horizontal = 2
grow_vertical = 2
theme_override_font_sizes/font_size = 60
text = "Start Game"
horizontal_alignment = 1
vertical_alignment = 1

[node name="Options" type="TextureButton" parent="TitleScreen/ButtonHolder/MainButtons"]
layout_mode = 2
texture_normal = ExtResource("7")
texture_pressed = ExtResource("7_hsmm5")
texture_hover = ExtResource("8_tutt7")
texture_focused = ExtResource("9_d3hm1")

[node name="Label" type="Label" parent="TitleScreen/ButtonHolder/MainButtons/Options"]
texture_filter = 1
layout_mode = 0
anchor_right = 1.0
anchor_bottom = 1.0
offset_top = -1.0
offset_bottom = -18.0
grow_horizontal = 2
grow_vertical = 2
theme_override_font_sizes/font_size = 60
text = "Options"
horizontal_alignment = 1
vertical_alignment = 1

[node name="Exit" type="TextureButton" parent="TitleScreen/ButtonHolder/MainButtons"]
layout_mode = 2
texture_normal = ExtResource("7")
texture_pressed = ExtResource("7_hsmm5")
texture_hover = ExtResource("8_tutt7")
texture_focused = ExtResource("9_d3hm1")

[node name="Label" type="Label" parent="TitleScreen/ButtonHolder/MainButtons/Exit"]
texture_filter = 1
layout_mode = 0
anchor_right = 1.0
anchor_bottom = 1.0
offset_top = -1.0
offset_bottom = -18.0
grow_horizontal = 2
grow_vertical = 2
theme_override_font_sizes/font_size = 60
text = "Exit Game"
horizontal_alignment = 1
vertical_alignment = 1

[node name="Spacer" type="Control" parent="TitleScreen"]
layout_mode = 2

[node name="StartGame" type="HBoxContainer" parent="."]
visible = false
layout_mode = 0
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
alignment = 1

[node name="StartButtons" type="VBoxContainer" parent="StartGame"]
layout_mode = 2
theme_override_constants/separation = 20
alignment = 1

[node name="Spacer" type="Control" parent="StartGame/StartButtons"]
layout_mode = 2

[node name="RandomBlocks" type="TextureButton" parent="StartGame/StartButtons"]
layout_mode = 2
texture_normal = ExtResource("7")
texture_pressed = ExtResource("7_hsmm5")
texture_hover = ExtResource("8_tutt7")
texture_focused = ExtResource("9_d3hm1")

[node name="Label" type="Label" parent="StartGame/StartButtons/RandomBlocks"]
layout_mode = 0
anchor_right = 1.0
anchor_bottom = 1.0
offset_top = -1.0
offset_bottom = -18.0
grow_horizontal = 2
grow_vertical = 2
theme_override_font_sizes/font_size = 60
text = "Random Blocks"
horizontal_alignment = 1
vertical_alignment = 1

[node name="FlatGrass" type="TextureButton" parent="StartGame/StartButtons"]
layout_mode = 2
texture_normal = ExtResource("7")
texture_pressed = ExtResource("7_hsmm5")
texture_hover = ExtResource("8_tutt7")
texture_focused = ExtResource("9_d3hm1")

[node name="Label" type="Label" parent="StartGame/StartButtons/FlatGrass"]
layout_mode = 0
anchor_right = 1.0
anchor_bottom = 1.0
offset_top = -1.0
offset_bottom = -18.0
grow_horizontal = 2
grow_vertical = 2
theme_override_font_sizes/font_size = 60
text = "Flat Grass"
horizontal_alignment = 1
vertical_alignment = 1

[node name="BackToTitle" type="TextureButton" parent="StartGame/StartButtons"]
layout_mode = 2
texture_normal = ExtResource("7")
texture_pressed = ExtResource("7_hsmm5")
texture_hover = ExtResource("8_tutt7")
texture_focused = ExtResource("9_d3hm1")


extends CharacterBody3D

const EYE_HEIGHT_STAND = 1.6
const EYE_HEIGHT_CROUCH = 1.4

const MOVEMENT_SPEED_GROUND = 0.6
const MOVEMENT_SPEED_AIR = 0.11
const MOVEMENT_SPEED_CROUCH_MODIFIER = 0.5
const MOVEMENT_FRICTION_GROUND = 0.9
const MOVEMENT_FRICTION_AIR = 0.98

var _mouse_motion := Vector2()
var _selected_block := 6

@onready var gravity := float(ProjectSettings.get_setting("physics/3d/default_gravity"))

@onready var head: Node3D = $Head
@onready var raycast: RayCast3D = $Head/RayCast3D
@onready var camera_attributes: CameraAttributes = $Head/Camera3D.attributes
@onready var selected_block_texture: TextureRect = $SelectedBlock
@onready var voxel_world: Node = $"../VoxelWorld"
@onready var crosshair: CenterContainer = $"../PauseMenu/Crosshair"

func _ready() -> void:
	Input.mouse_mode = Input.MOUSE_MODE_CAPTURED

func _process(_delta: float) -> void:
	# Mouse movement.
	_mouse_motion.y = clampf(_mouse_motion.y, -1560, 1560)
	transform.basis = Basis.from_euler(Vector3(0, _mouse_motion.x * -0.001, 0))
	head.transform.basis = Basis.from_euler(Vector3(_mouse_motion.y * -0.001, 0, 0))

	# Block selection.
	var ray_position := raycast.get_collision_point()
	var ray_normal := raycast.get_collision_normal()
	if Input.is_action_just_pressed(&"pick_block"):
		# Block picking.
		var block_global_position := Vector3i((ray_position - ray_normal / 2).floor())
		_selected_block = voxel_world.get_block_global_position(block_global_position)
	else:
		# Block prev/next keys.
		if Input.is_action_just_pressed(&"prev_block"):
			_selected_block -= 1
		if Input.is_action_just_pressed(&"next_block"):
			_selected_block += 1
		_selected_block = wrapi(_selected_block, 1, 30)
	# Set the appropriate texture.
	var uv := Chunk.calculate_block_uvs(_selected_block)
	selected_block_texture.texture.region = Rect2(uv[0] * 512, Vector2.ONE * 64)

	# Block breaking/placing.
	if crosshair.visible and raycast.is_colliding():
		var breaking := Input.is_action_just_pressed(&"break")
		var placing := Input.is_action_just_pressed(&"place")
		# Either both buttons were pressed or neither are, so stop.
		if breaking == placing:
			return

		if breaking:
			var block_global_position := Vector3i((ray_position - ray_normal / 2).floor())
			voxel_world.set_block_global_position(block_global_position, 0)
		elif placing:
			var block_global_position := Vector3i((ray_position + ray_normal / 2).floor())
			voxel_world.set_block_global_position(block_global_position, _selected_block)

func _physics_process(delta: float) -> void:
	camera_attributes.dof_blur_far_enabled = Settings.fog_enabled
	camera_attributes.dof_blur_far_distance = Settings.fog_distance * 1.5
	camera_attributes.dof_blur_far_transition = Settings.fog_distance * 0.125
	# Crouching.
	var crouching := Input.is_action_pressed(&"crouch")
	head.transform.origin.y = lerpf(head.transform.origin.y, EYE_HEIGHT_CROUCH if crouching else EYE_HEIGHT_STAND, 16 * delta)

	# Keyboard movement.
	var movement_vec2 := Input.get_vector("move_left", "move_right", "move_forward", "move_back")
	var movement := transform.basis * (Vector3(movement_vec2.x, 0, movement_vec2.y))

	if is_on_floor():
		movement *= MOVEMENT_SPEED_GROUND
	else:
		movement *= MOVEMENT_SPEED_AIR

	if crouching:
		movement *= MOVEMENT_SPEED_CROUCH_MODIFIER

	# Gravity.
	velocity.y -= gravity * delta

	velocity += Vector3(movement.x, 0, movement.z)
	# Apply horizontal friction.
	velocity.x *= MOVEMENT_FRICTION_GROUND if is_on_floor() else MOVEMENT_FRICTION_AIR
	velocity.z *= MOVEMENT_FRICTION_GROUND if is_on_floor() else MOVEMENT_FRICTION_AIR
	move_and_slide()

	# Jumping, applied next frame.
	if is_on_floor() and Input.is_action_pressed(&"jump"):
		velocity.y = 7.5

func _input(event: InputEvent) -> void:
	if event is InputEventMouseMotion:
		if Input.get_mouse_mode() == Input.MOUSE_MODE_CAPTURED:
			_mouse_motion += event.relative

func chunk_pos() -> Vector3i:
	return Vector3i((transform.origin / Chunk.CHUNK_SIZE).floor())


[node name="TestName" type="Label" parent="."]
anchors_preset = 7
anchor_left = 0.5
anchor_top = 1.0
anchor_right = 0.5
anchor_bottom = 1.0
offset_left = -192.0
offset_top = -58.0
offset_right = 192.0
offset_bottom = -24.0
grow_horizontal = 2
grow_vertical = 0
theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
theme_override_constants/outline_size = 5
theme_override_font_sizes/font_size = 24
horizontal_alignment = 1

[node name="Previous" type="Button" parent="."]
anchors_preset = 2
anchor_top = 1.0
anchor_bottom = 1.0
offset_left = 24.0
offset_top = -55.0
offset_right = 135.0
offset_bottom = -24.0
grow_vertical = 0
text = "«  Previous"

[node name="Next" type="Button" parent="."]
anchors_preset = 3
anchor_left = 1.0
anchor_top = 1.0
anchor_right = 1.0
anchor_bottom = 1.0
offset_left = -107.0
offset_top = -55.0
offset_right = -24.0
offset_bottom = -24.0
grow_horizontal = 0
grow_vertical = 0
text = "Next  »"

[connection signal="pressed" from="Previous" to="." method="_on_previous_pressed"]
[connection signal="pressed" from="Next" to="." method="_on_next_pressed"]


extends WorldEnvironment

const ROT_SPEED = 0.003
const ZOOM_SPEED = 0.125
const MAIN_BUTTONS = MOUSE_BUTTON_MASK_LEFT | MOUSE_BUTTON_MASK_RIGHT | MOUSE_BUTTON_MASK_MIDDLE

var tester_index := 0
var rot_x := deg_to_rad(-22.5)  # This must be kept in sync with RotationX.
var rot_y := deg_to_rad(90)  # This must be kept in sync with CameraHolder.
var zoom := 2.5
var base_height := int(ProjectSettings.get_setting("display/window/size/viewport_height"))

@onready var testers: Node3D = $Testers
@onready var camera_holder: Node3D = $CameraHolder # Has a position and rotates on Y.
@onready var rotation_x: Node3D = $CameraHolder/RotationX
@onready var camera: Camera3D = $CameraHolder/RotationX/Camera3D

func _ready() -> void:
	camera_holder.transform.basis = Basis.from_euler(Vector3(0, rot_y, 0))
	rotation_x.transform.basis = Basis.from_euler(Vector3(rot_x, 0, 0))
	update_gui()

func _unhandled_input(event: InputEvent) -> void:
	if event.is_action_pressed(&"ui_left"):
		_on_previous_pressed()
	if event.is_action_pressed(&"ui_right"):
		_on_next_pressed()

	if event is InputEventMouseButton:
		if event.button_index == MOUSE_BUTTON_WHEEL_UP:
			zoom -= ZOOM_SPEED
		if event.button_index == MOUSE_BUTTON_WHEEL_DOWN:
			zoom += ZOOM_SPEED
		zoom = clamp(zoom, 1.5, 4)

	if event is InputEventMouseMotion and event.button_mask & MAIN_BUTTONS:
		# Compensate motion speed to be resolution-independent (based on the window height).
		var relative_motion: Vector2 = event.relative * DisplayServer.window_get_size().y / base_height
		rot_y -= relative_motion.x * ROT_SPEED
		rot_x -= relative_motion.y * ROT_SPEED
		rot_x = clamp(rot_x, deg_to_rad(-90), 0)
		camera_holder.transform.basis = Basis.from_euler(Vector3(0, rot_y, 0))
		rotation_x.transform.basis = Basis.from_euler(Vector3(rot_x, 0, 0))

func _process(delta: float) -> void:
	var current_tester: Node3D = testers.get_child(tester_index)
	# This code assumes CameraHolder's X and Y coordinates are already correct.
	var current_position := camera_holder.global_transform.origin.z
	var target_position := current_tester.global_transform.origin.z
	camera_holder.global_transform.origin.z = lerpf(current_position, target_position, 3 * delta)
	camera.position.z = lerpf(camera.position.z, zoom, 10 * delta)

func _on_previous_pressed() -> void:
	tester_index = max(0, tester_index - 1)
	update_gui()

func _on_next_pressed() -> void:
	tester_index = min(tester_index + 1, testers.get_child_count() - 1)
	update_gui()

func update_gui() -> void:
	$TestName.text = str(testers.get_child(tester_index).name).capitalize()
	$Previous.disabled = tester_index == 0
	$Next.disabled = tester_index == testers.get_child_count() - 1


extends Camera3D

# Higher values cause the field of view to increase more at high speeds.
const FOV_SPEED_FACTOR = 60

# Higher values cause the field of view to adapt to speed changes faster.
const FOV_SMOOTH_FACTOR = 0.2

# Don't change FOV if moving below this speed. This prevents shadows from flickering when driving slowly.
const FOV_CHANGE_MIN_SPEED = 0.05

@export var min_distance := 2.0
@export var max_distance := 4.0
@export var angle_v_adjust := 0.0
@export var height := 1.5

var camera_type := CameraType.EXTERIOR

var initial_transform := transform

var base_fov := fov

# The field of view to smoothly interpolate to.
var desired_fov := fov

# Position on the last physics frame (used to measure speed).
@onready var previous_position := global_position

enum CameraType {
	EXTERIOR,
	INTERIOR,
	TOP_DOWN,
	MAX,  # Represents the size of the CameraType enum.
}

func _ready() -> void:
	update_camera()

func _input(event: InputEvent) -> void:
	if event.is_action_pressed(&"cycle_camera"):
		camera_type = wrapi(camera_type + 1, 0, CameraType.MAX) as CameraType
		update_camera()

func _physics_process(_delta: float) -> void:
	if camera_type == CameraType.EXTERIOR:
		var target: Vector3 = get_parent().global_transform.origin
		var pos := global_transform.origin

		var from_target := pos - target

		# Check ranges.
		if from_target.length() < min_distance:
			from_target = from_target.normalized() * min_distance
		elif from_target.length() > max_distance:
			from_target = from_target.normalized() * max_distance

		from_target.y = height

		pos = target + from_target

		look_at_from_position(pos, target, Vector3.UP)
	elif camera_type == CameraType.TOP_DOWN:
		position.x = get_parent().global_transform.origin.x
		position.z = get_parent().global_transform.origin.z
		# Force rotation to prevent camera from being slanted after switching cameras while on a slope.
		rotation_degrees = Vector3(270, 180, 0)

	# Dynamic field of view based on car speed, with smoothing to prevent sudden changes on impact.
	desired_fov = clamp(base_fov + (abs(global_position.length() - previous_position.length()) - FOV_CHANGE_MIN_SPEED) * FOV_SPEED_FACTOR, base_fov, 100)
	fov = lerpf(fov, desired_fov, FOV_SMOOTH_FACTOR)

	# Turn a little up or down
	transform.basis = Basis(transform.basis[0], deg_to_rad(angle_v_adjust)) * transform.basis

	previous_position = global_position

func update_camera() -> void:
	match camera_type:
		CameraType.EXTERIOR:
			transform = initial_transform
		CameraType.INTERIOR:
			global_transform = get_node(^"../../InteriorCameraPosition").global_transform
		CameraType.TOP_DOWN:
			global_transform = get_node(^"../../TopDownCameraPosition").global_transform

	# This detaches the camera transform from the parent spatial node, but only
	# for exterior and top-down cameras.
	set_as_top_level(camera_type != CameraType.INTERIOR)


[gd_scene load_steps=15 format=3 uid="uid://2y3ar4s86yra"]

[ext_resource type="Script" path="res://Main.gd" id="1"]
[ext_resource type="PackedScene" uid="uid://dp478jyugrn7o" path="res://Player.tscn" id="2"]
[ext_resource type="PackedScene" uid="uid://ha0ar5s2c3m4" path="res://Mob.tscn" id="3"]
[ext_resource type="Theme" uid="uid://cqquurjk1i7yw" path="res://ui_theme.tres" id="4_gnyca"]
[ext_resource type="Script" path="res://ScoreLabel.gd" id="6"]

[sub_resource type="BoxShape3D" id="1"]
size = Vector3(60, 2, 60)

[sub_resource type="BoxMesh" id="2"]
size = Vector3(60, 2, 60)

[sub_resource type="WorldBoundaryShape3D" id="WorldBoundaryShape3D_i5n5q"]

[sub_resource type="ProceduralSkyMaterial" id="ProceduralSkyMaterial_m3njk"]
sky_horizon_color = Color(0.64625, 0.65575, 0.67075, 1)
ground_horizon_color = Color(0.64625, 0.65575, 0.67075, 1)

[sub_resource type="Sky" id="Sky_yutuu"]
sky_material = SubResource("ProceduralSkyMaterial_m3njk")

[sub_resource type="Environment" id="Environment_0tcge"]
background_mode = 2
sky = SubResource("Sky_yutuu")
tonemap_mode = 2

[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_cr5ed"]
albedo_color = Color(0.635294, 0.211765, 0.0235294, 1)

[sub_resource type="CylinderMesh" id="3"]
material = SubResource("StandardMaterial3D_cr5ed")
radial_segments = 24
rings = 1

[sub_resource type="Curve3D" id="5"]
_data = {
"points": PackedVector3Array(0, 0, 0, 0, 0, 0, 14, 0, -15, 0, 0, 0, 0, 0, 0, -13, 0, -15, 0, 0, 0, 0, 0, 0, -13, 0, 16, 0, 0, 0, 0, 0, 0, 14, 0, 16, 0, 0, 0, 0, 0, 0, 14, 0, -15),
"tilts": PackedFloat32Array(0, 0, 0, 0, 0)
}
point_count = 5

[node name="Main" type="Node"]
script = ExtResource("1")
mob_scene = ExtResource("3")

[node name="Ground" type="StaticBody3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -1, 0)
collision_layer = 4
collision_mask = 0

[node name="CollisionShape" type="CollisionShape3D" parent="Ground"]
shape = SubResource("1")

[node name="MeshInstance" type="MeshInstance3D" parent="Ground"]
mesh = SubResource("2")

[node name="Walls" type="StaticBody3D" parent="."]
editor_description = "These invisible walls are used to prevent the player from escaping the playable area.

These walls use WorldBoundaryShape3D collision shapes, which are infinitely large planes
(even though they look finite in the editor)."
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 5)
collision_layer = 4
collision_mask = 0

[node name="CollisionShape3D" type="CollisionShape3D" parent="Walls"]
transform = Transform3D(1, 0, 0, 0, 1.31134e-07, 1, 0, -1, 1.31134e-07, 0, 10, 11)
shape = SubResource("WorldBoundaryShape3D_i5n5q")

[node name="CollisionShape3D2" type="CollisionShape3D" parent="Walls"]
transform = Transform3D(-4.37114e-08, 1, -1.31134e-07, 0, 1.31134e-07, 1, 1, 4.37114e-08, -5.73206e-15, -13, 10, -4.76837e-07)
shape = SubResource("WorldBoundaryShape3D_i5n5q")

[node name="CollisionShape3D3" type="CollisionShape3D" parent="Walls"]
transform = Transform3D(4.37114e-08, -1, 2.18557e-07, 3.82137e-15, -2.18557e-07, -1, 1, 4.37114e-08, -5.73206e-15, 14, 10, -4.76837e-07)
shape = SubResource("WorldBoundaryShape3D_i5n5q")

[node name="CollisionShape3D4" type="CollisionShape3D" parent="Walls"]
transform = Transform3D(1, 5.96046e-08, -1.42109e-14, 3.82137e-15, -2.18557e-07, -1, -5.96046e-08, 1, -2.18557e-07, 9.53674e-07, 10, -20)
shape = SubResource("WorldBoundaryShape3D_i5n5q")

[node name="DirectionalLight" type="DirectionalLight3D" parent="."]
transform = Transform3D(0.5, -0.777049, 0.382355, 0, 0.441506, 0.897258, -0.866025, -0.448629, 0.220753, 0, 12.5592, 14.7757)
shadow_enabled = true
shadow_bias = 0.04
shadow_blur = 1.5
directional_shadow_mode = 0
directional_shadow_fade_start = 1.0
directional_shadow_max_distance = 40.0

[node name="WorldEnvironment" type="WorldEnvironment" parent="."]
environment = SubResource("Environment_0tcge")

[node name="Player" parent="." instance=ExtResource("2")]

[node name="CameraPivot" type="Marker3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 0.707107, 0.707107, 0, -0.707107, 0.707107, 0.183514, 0, 0)

[node name="Camera" type="Camera3D" parent="CameraPivot"]
transform = Transform3D(1, 0, 0, 0, 1, -2.98023e-08, 0, 2.98023e-08, 1, 0, 9.53674e-07, 19)
projection = 1
fov = 48.6
size = 24.0
far = 40.0

[node name="Cylinders" type="Node3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 8, 0, 0)

[node name="MeshInstance" type="MeshInstance3D" parent="Cylinders"]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 6, 1, -15)
mesh = SubResource("3")

[node name="MeshInstance3" type="MeshInstance3D" parent="Cylinders"]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -21, 1, -15)
mesh = SubResource("3")

[node name="MeshInstance2" type="MeshInstance3D" parent="Cylinders"]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -21, 1, 16)
mesh = SubResource("3")