allow lookin g in mouse direction

This commit is contained in:
2026-01-22 01:02:46 +01:00
parent c153c69e37
commit c0d229ee86
6 changed files with 248 additions and 11 deletions

BIN
src/assets/gfx/cursor.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 B

View File

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dxyhsfq22vxfn"
path="res://.godot/imported/cursor.png-308a6879f38fa7a626935612b5aab578.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/gfx/cursor.png"
dest_files=["res://.godot/imported/cursor.png-308a6879f38fa7a626935612b5aab578.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://bc0eg8xeht1m0"
path="res://.godot/imported/ELR_Corsshairs.png-1be59a5a0530c35e5b746d569963749c.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/gfx/ui/ELR_Corsshairs.png"
dest_files=["res://.godot/imported/ELR_Corsshairs.png-1be59a5a0530c35e5b746d569963749c.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

View File

@@ -11,6 +11,17 @@ const BASE_CAMERA_ZOOM: float = 4.0
const BASE_CAMERA_ZOOM_MOBILE: float = 5.5 # More zoomed in for mobile devices const BASE_CAMERA_ZOOM_MOBILE: float = 5.5 # More zoomed in for mobile devices
const REFERENCE_ASPECT: float = 16.0 / 9.0 const REFERENCE_ASPECT: float = 16.0 / 9.0
# Mouse cursor system
var cursor_sprite: Sprite2D = null # Free movement cursor (frame 0)
var grid_cursor_sprite: Sprite2D = null # Grid-locked cursor (frame 1)
var cursor_layer: CanvasLayer = null
const CURSOR_LAYER_Z: int = 2000 # Very high Z index for cursor
var use_mouse_control: bool = true # Enable/disable mouse control
var camera_lerp_to_cursor: bool = false # Optional: lerp camera slightly toward cursor
const CURSOR_CAMERA_LERP_AMOUNT: float = 0.15 # How much camera lerps toward cursor (0.0 = none, 1.0 = full)
var cursor_pulse_time: float = 0.0 # Time accumulator for pulsing animation
const CURSOR_PULSE_SPEED: float = 3.0 # Speed of color pulse animation
# Fog of war # Fog of war
const FOG_TILE_SIZE: int = 16 const FOG_TILE_SIZE: int = 16
const FOG_VIEW_RANGE_TILES: float = 10.0 const FOG_VIEW_RANGE_TILES: float = 10.0
@@ -130,6 +141,9 @@ func _ready():
# Create inventory UI # Create inventory UI
_create_inventory_ui() _create_inventory_ui()
# Initialize mouse cursor system
_init_mouse_cursor()
# Generate dungeon on host only # Generate dungeon on host only
# Only generate if we're the server (not just "no multiplayer peer") # Only generate if we're the server (not just "no multiplayer peer")
# This prevents clients from generating their own dungeon before connecting # This prevents clients from generating their own dungeon before connecting
@@ -1512,7 +1526,11 @@ func _check_tab_visibility():
return window_visible return window_visible
return true # Always visible on non-web platforms return true # Always visible on non-web platforms
func _process(_delta): func _process(delta):
# Update mouse cursor
if use_mouse_control:
_update_mouse_cursor(delta)
# Check tab visibility for buffer overflow protection (clients only) # Check tab visibility for buffer overflow protection (clients only)
if not multiplayer.is_server(): if not multiplayer.is_server():
var is_tab_visible = _check_tab_visibility() var is_tab_visible = _check_tab_visibility()
@@ -1560,11 +1578,11 @@ func _process(_delta):
# Update camera to follow local players # Update camera to follow local players
_update_camera() _update_camera()
_update_fog_of_war(_delta) _update_fog_of_war(delta)
# Periodic cleanup of disconnected peers (server only) # Periodic cleanup of disconnected peers (server only)
if multiplayer.is_server() and multiplayer.has_multiplayer_peer(): if multiplayer.is_server() and multiplayer.has_multiplayer_peer():
peer_cleanup_timer += _delta peer_cleanup_timer += delta
if peer_cleanup_timer >= PEER_CLEANUP_INTERVAL: if peer_cleanup_timer >= PEER_CLEANUP_INTERVAL:
peer_cleanup_timer = 0.0 peer_cleanup_timer = 0.0
_cleanup_disconnected_peers() _cleanup_disconnected_peers()
@@ -1696,7 +1714,7 @@ func _update_camera():
# Adjust zoom based on player spread (for split-screen effect) # Adjust zoom based on player spread (for split-screen effect)
if local_players.size() > 1: if local_players.size() > 1:
var max_distance = 0.0a var max_distance = 0.0
for player in local_players: for player in local_players:
var distance = center.distance_to(player.position) var distance = center.distance_to(player.position)
max_distance = max(max_distance, distance) max_distance = max(max_distance, distance)
@@ -1708,6 +1726,121 @@ func _update_camera():
# Always update zoom (for both single and multi-player) # Always update zoom (for both single and multi-player)
camera.zoom = camera.zoom.lerp(Vector2.ONE * target_zoom, 0.05) camera.zoom = camera.zoom.lerp(Vector2.ONE * target_zoom, 0.05)
# Optional: Lerp camera slightly toward cursor direction
if camera_lerp_to_cursor and use_mouse_control:
var cursor_world_pos = camera.get_global_mouse_position()
var cursor_offset = cursor_world_pos - center
var lerped_center = center + cursor_offset * CURSOR_CAMERA_LERP_AMOUNT
camera.position = camera.position.lerp(lerped_center, 0.1)
func _init_mouse_cursor():
# Create cursor layer with high Z index
cursor_layer = CanvasLayer.new()
cursor_layer.name = "MouseCursorLayer"
cursor_layer.layer = CURSOR_LAYER_Z
add_child(cursor_layer)
# Load cursor texture
var cursor_texture = load("res://assets/gfx/cursor.png")
if not cursor_texture:
push_error("GameWorld: Could not load cursor.png!")
return
# Create free movement cursor sprite (frame 0)
cursor_sprite = Sprite2D.new()
cursor_sprite.name = "MouseCursorFree"
cursor_sprite.texture = cursor_texture
# Set up sprite sheet (32x16 with 2 frames of 16x16 each)
cursor_sprite.hframes = 2
cursor_sprite.vframes = 1
cursor_sprite.frame = 0 # Frame 0 = free movement
cursor_layer.add_child(cursor_sprite)
# Create grid-locked cursor sprite (frame 1)
grid_cursor_sprite = Sprite2D.new()
grid_cursor_sprite.name = "MouseCursorGrid"
grid_cursor_sprite.texture = cursor_texture
grid_cursor_sprite.hframes = 2
grid_cursor_sprite.vframes = 1
grid_cursor_sprite.frame = 1 # Frame 1 = grid-locked
grid_cursor_sprite.modulate.a = 0.5 # 50% opacity
cursor_layer.add_child(grid_cursor_sprite)
# Hide system cursor
Input.mouse_mode = Input.MOUSE_MODE_HIDDEN
func _update_mouse_cursor(delta: float):
if not use_mouse_control:
return
if not cursor_sprite or not is_instance_valid(cursor_sprite):
return
if not grid_cursor_sprite or not is_instance_valid(grid_cursor_sprite):
return
# Update pulse time for grid cursor color animation
cursor_pulse_time += delta * CURSOR_PULSE_SPEED
# Get mouse position in viewport (screen space)
var mouse_pos = get_viewport().get_mouse_position()
# Convert to world position for game logic
var world_pos = camera.get_global_mouse_position()
# Scale cursors to match camera zoom level (so grid-locked cursor aligns with tiles)
# Each cursor frame is 16x16 pixels, and tiles are 16x16 pixels
# Scale by camera zoom to maintain 1:1 pixel ratio
var cursor_scale = camera.zoom.x # Use x zoom (should be same as y)
cursor_sprite.scale = Vector2.ONE * cursor_scale
grid_cursor_sprite.scale = Vector2.ONE * cursor_scale
# Check if we should show grid-locked cursor (when mouse is over game world tiles)
var show_grid_cursor = false
var grid_locked_world_pos = world_pos
if dungeon_tilemap_layer:
var tile_pos = dungeon_tilemap_layer.local_to_map(world_pos - dungeon_tilemap_layer.global_position)
var tile_data = dungeon_tilemap_layer.get_cell_source_id(tile_pos)
if tile_data >= 0: # Valid tile
show_grid_cursor = true
# Snap to tile center for world position calculation
grid_locked_world_pos = dungeon_tilemap_layer.map_to_local(tile_pos) + dungeon_tilemap_layer.global_position
# Convert grid-locked world position to screen position for cursor display
# Formula: (world_pos - camera.position) * camera.zoom + viewport_center
var viewport_size = get_viewport().get_visible_rect().size
var viewport_center = viewport_size / 2.0
var grid_locked_screen_pos = (grid_locked_world_pos - camera.position) * camera.zoom.x + viewport_center
grid_cursor_sprite.position = grid_locked_screen_pos
else:
grid_cursor_sprite.position = Vector2(-1000, -1000) # Hide off-screen
# Update free cursor position (always follows mouse)
cursor_sprite.position = mouse_pos
# Update grid cursor visibility and pulsing color
grid_cursor_sprite.visible = show_grid_cursor
if show_grid_cursor:
# Pulse color: oscillate between normal and brighter color
var pulse_value = (sin(cursor_pulse_time) + 1.0) / 2.0 # 0.0 to 1.0
# Interpolate between normal (1,1,1) and brighter (1.5, 1.2, 1.0) for a warm pulse
var base_color = Color(1.0, 1.0, 1.0)
var pulse_color = Color(1.5, 1.2, 1.0)
grid_cursor_sprite.modulate = base_color.lerp(pulse_color, pulse_value * 0.5) # 50% of the way to pulse color
grid_cursor_sprite.modulate.a = 0.5 # Keep opacity at 50%
# Update player facing direction based on mouse position (use world position)
if local_players.size() > 0:
var player = local_players[0] # Use first local player
if player and is_instance_valid(player) and player.is_local_player:
var player_pos = player.global_position
# Use grid-locked position if available, otherwise use free mouse position
var target_world_pos = grid_locked_world_pos if show_grid_cursor else world_pos
var mouse_direction = (target_world_pos - player_pos).normalized()
# Only update facing if mouse is far enough from player
if mouse_direction.length() > 0.1:
player._update_facing_from_mouse(mouse_direction)
func _init_fog_of_war(): func _init_fog_of_war():
if dungeon_data.is_empty() or not dungeon_data.has("map_size"): if dungeon_data.is_empty() or not dungeon_data.has("map_size"):
return return
@@ -1832,7 +1965,7 @@ func _update_fog_of_war(delta: float) -> void:
cached_corridor_rooms = _get_rooms_connected_to_corridor(cached_corridor_mask, player_tile) cached_corridor_rooms = _get_rooms_connected_to_corridor(cached_corridor_mask, player_tile)
cached_corridor_player_tile = player_tile cached_corridor_player_tile = player_tile
# Build a set of allowed room IDs for fast lookup # Build a set of allowed room IDs for fast lookup
cached_corridor_allowed_room_ids = {} cached_corridor_allowed_room_ids = {}
for room in cached_corridor_rooms: for room in cached_corridor_rooms:
var room_id = str(room.x) + "," + str(room.y) + "," + str(room.w) + "," + str(room.h) var room_id = str(room.x) + "," + str(room.y) + "," + str(room.w) + "," + str(room.h)

View File

@@ -23,6 +23,8 @@ var teleported_this_frame: bool = false # Flag to prevent position sync from ove
# Input device (for local multiplayer) # Input device (for local multiplayer)
var input_device: int = -1 # -1 for keyboard, 0+ for gamepad index var input_device: int = -1 # -1 for keyboard, 0+ for gamepad index
var virtual_joystick_input: Vector2 = Vector2.ZERO # Virtual joystick input from mobile controls var virtual_joystick_input: Vector2 = Vector2.ZERO # Virtual joystick input from mobile controls
var was_mouse_right_pressed: bool = false # Track previous mouse right button state
var was_mouse_left_pressed: bool = false # Track previous mouse left button state
# Interaction # Interaction
var held_object = null var held_object = null
@@ -1251,6 +1253,23 @@ func _get_direction_from_vector(vec: Vector2) -> int:
else: # 292.5 to 337.5 else: # 292.5 to 337.5
return Direction.UP_RIGHT return Direction.UP_RIGHT
# Update facing direction from mouse position (called by GameWorld)
func _update_facing_from_mouse(mouse_direction: Vector2):
# Only update if using keyboard input (not gamepad)
if input_device != -1:
return
# Don't update if pushing (locked direction)
if is_pushing:
return
var new_direction = _get_direction_from_vector(mouse_direction) as Direction
# Update direction and cone light rotation if changed
if new_direction != current_direction:
current_direction = new_direction
_update_cone_light_rotation()
func _set_animation(anim_name: String): func _set_animation(anim_name: String):
if current_animation != anim_name: if current_animation != anim_name:
current_animation = anim_name current_animation = anim_name
@@ -1684,6 +1703,7 @@ func _handle_input():
last_movement_direction = input_vector.normalized() last_movement_direction = input_vector.normalized()
# Update facing direction (except when pushing - locked direction) # Update facing direction (except when pushing - locked direction)
# Note: Mouse control will override this if mouse is being used
var new_direction = current_direction var new_direction = current_direction
if not is_pushing: if not is_pushing:
new_direction = _get_direction_from_vector(input_vector) as Direction new_direction = _get_direction_from_vector(input_vector) as Direction
@@ -1774,10 +1794,12 @@ func _handle_interactions():
var grab_just_released = false var grab_just_released = false
if input_device == -1: if input_device == -1:
# Keyboard input # Keyboard or Mouse input
grab_button_down = Input.is_action_pressed("grab") var mouse_right_pressed = Input.is_mouse_button_pressed(MOUSE_BUTTON_RIGHT)
grab_just_pressed = Input.is_action_just_pressed("grab") grab_button_down = Input.is_action_pressed("grab") or mouse_right_pressed
grab_just_released = Input.is_action_just_released("grab") grab_just_pressed = Input.is_action_just_pressed("grab") or (mouse_right_pressed and not was_mouse_right_pressed)
grab_just_released = Input.is_action_just_released("grab") or (not mouse_right_pressed and was_mouse_right_pressed)
was_mouse_right_pressed = mouse_right_pressed
# DEBUG: Log button states if there's a conflict # DEBUG: Log button states if there's a conflict
if grab_just_pressed and grab_just_released: if grab_just_pressed and grab_just_released:
@@ -1907,8 +1929,10 @@ func _handle_interactions():
# Handle attack input # Handle attack input
var attack_just_pressed = false var attack_just_pressed = false
if input_device == -1: if input_device == -1:
# Keyboard # Keyboard or Mouse
attack_just_pressed = Input.is_action_just_pressed("attack") var mouse_left_pressed = Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT)
attack_just_pressed = Input.is_action_just_pressed("attack") or (mouse_left_pressed and not was_mouse_left_pressed)
was_mouse_left_pressed = mouse_left_pressed
else: else:
# Gamepad (X button) # Gamepad (X button)
attack_just_pressed = Input.is_joy_button_pressed(input_device, JOY_BUTTON_X) attack_just_pressed = Input.is_joy_button_pressed(input_device, JOY_BUTTON_X)