started working on fog darkness
This commit is contained in:
@@ -9,6 +9,7 @@ var appearance_rng: RandomNumberGenerator # Deterministic RNG for appearance/sta
|
||||
@export var move_speed: float = 100.0
|
||||
@export var grab_range: float = 20.0
|
||||
@export var throw_force: float = 150.0
|
||||
@export var cone_light_angle: float = 180.0 # Cone spread angle in degrees (adjustable, default wider)
|
||||
|
||||
# Network identity
|
||||
var peer_id: int = 1
|
||||
@@ -60,6 +61,7 @@ var attack_cooldown: float = 0.0 # No cooldown - instant attacks!
|
||||
var is_attacking: bool = false
|
||||
var sword_slash_scene = preload("res://scenes/sword_slash.tscn") # Old rotating version (kept for reference)
|
||||
var sword_projectile_scene = preload("res://scenes/sword_projectile.tscn") # New projectile version
|
||||
var attack_arrow_scene = preload("res://scenes/attack_arrow.tscn")
|
||||
var blood_scene = preload("res://scenes/blood_clot.tscn")
|
||||
|
||||
# Simulated Z-axis for height (when thrown)
|
||||
@@ -92,6 +94,7 @@ var is_airborne: bool = false
|
||||
@onready var sprite_addons = $Sprite2DAddons
|
||||
@onready var sprite_headgear = $Sprite2DHeadgear
|
||||
@onready var sprite_weapon = $Sprite2DWeapon
|
||||
@onready var cone_light = $ConeLight
|
||||
|
||||
# Player stats (legacy - now using character_stats)
|
||||
var max_health: float:
|
||||
@@ -266,6 +269,12 @@ func _ready():
|
||||
if interaction_indicator:
|
||||
interaction_indicator.visible = false
|
||||
|
||||
# Set up cone light blend mode, texture, initial rotation, and spread
|
||||
if cone_light:
|
||||
_create_cone_light_texture()
|
||||
_update_cone_light_rotation()
|
||||
_update_cone_light_spread()
|
||||
|
||||
# Wait before allowing RPCs to ensure player is fully spawned on all clients
|
||||
# This prevents "Node not found" errors when RPCs try to resolve node paths
|
||||
if multiplayer.is_server():
|
||||
@@ -600,7 +609,7 @@ func _on_character_changed(_char: CharacterStats):
|
||||
# Update appearance when character stats change (e.g., equipment)
|
||||
_apply_appearance_to_sprites()
|
||||
|
||||
# Sync equipment changes to other clients
|
||||
# Sync equipment changes to other clients (when authority player changes equipment)
|
||||
if multiplayer.has_multiplayer_peer() and is_multiplayer_authority() and can_send_rpcs and is_inside_tree():
|
||||
# Sync equipment to all clients
|
||||
var equipment_data = {}
|
||||
@@ -611,6 +620,32 @@ func _on_character_changed(_char: CharacterStats):
|
||||
else:
|
||||
equipment_data[slot_name] = null
|
||||
_sync_equipment.rpc(equipment_data)
|
||||
|
||||
# Sync equipment and inventory to client (when server adds/removes items for a client player)
|
||||
# This ensures joiners see items they pick up and equipment changes
|
||||
# This must be checked separately from the authority-based sync because on the server,
|
||||
# a joiner's player has authority set to their peer_id, not the server's unique_id
|
||||
if multiplayer.has_multiplayer_peer() and multiplayer.is_server() and can_send_rpcs and is_inside_tree():
|
||||
var the_peer_id = get_multiplayer_authority()
|
||||
# Only sync if this is a client player (not server's own player)
|
||||
if the_peer_id != 0 and the_peer_id != multiplayer.get_unique_id():
|
||||
# Sync equipment
|
||||
var equipment_data = {}
|
||||
for slot_name in character_stats.equipment.keys():
|
||||
var item = character_stats.equipment[slot_name]
|
||||
if item:
|
||||
equipment_data[slot_name] = item.save()
|
||||
else:
|
||||
equipment_data[slot_name] = null
|
||||
_sync_equipment.rpc_id(the_peer_id, equipment_data)
|
||||
|
||||
# Sync inventory
|
||||
var inventory_data = []
|
||||
for item in character_stats.inventory:
|
||||
if item:
|
||||
inventory_data.append(item.save())
|
||||
_sync_inventory.rpc_id(the_peer_id, inventory_data)
|
||||
print(name, " syncing equipment and inventory to client peer_id=", the_peer_id, " inventory_size=", inventory_data.size())
|
||||
|
||||
func _get_player_color() -> Color:
|
||||
# Legacy function - now returns white (no color tint)
|
||||
@@ -700,6 +735,107 @@ func _set_animation(anim_name: String):
|
||||
current_frame = 0
|
||||
time_since_last_frame = 0.0
|
||||
|
||||
# Convert Direction enum to angle in radians for light rotation
|
||||
func _direction_to_angle(direction: int) -> float:
|
||||
match direction:
|
||||
Direction.DOWN:
|
||||
return PI / 2.0 # 90 degrees
|
||||
Direction.DOWN_RIGHT:
|
||||
return PI / 4.0 # 45 degrees
|
||||
Direction.RIGHT:
|
||||
return 0.0 # 0 degrees
|
||||
Direction.UP_RIGHT:
|
||||
return -PI / 4.0 # -45 degrees
|
||||
Direction.UP:
|
||||
return -PI / 2.0 # -90 degrees
|
||||
Direction.UP_LEFT:
|
||||
return -3.0 * PI / 4.0 # -135 degrees
|
||||
Direction.LEFT:
|
||||
return PI # 180 degrees
|
||||
Direction.DOWN_LEFT:
|
||||
return 3.0 * PI / 4.0 # 135 degrees
|
||||
_:
|
||||
return PI / 2.0 # Default to DOWN
|
||||
|
||||
# Update cone light rotation based on player's facing direction
|
||||
func _update_cone_light_rotation():
|
||||
if cone_light:
|
||||
cone_light.rotation = _direction_to_angle(current_direction)+(PI/2)
|
||||
|
||||
# Create a cone-shaped light texture programmatically
|
||||
# Creates a directional cone texture that extends forward and fades to the sides
|
||||
func _create_cone_light_texture():
|
||||
if not cone_light:
|
||||
return
|
||||
|
||||
# Create a square texture (recommended size for lights)
|
||||
var texture_size = 256
|
||||
var image = Image.create(texture_size, texture_size, false, Image.FORMAT_RGBA8)
|
||||
|
||||
var center = Vector2(texture_size / 2.0, texture_size / 2.0)
|
||||
var max_distance = texture_size / 2.0
|
||||
|
||||
# Cone parameters (these control the shape)
|
||||
var cone_angle_rad = deg_to_rad(cone_light_angle) # Convert to radians
|
||||
var half_cone = cone_angle_rad / 2.0
|
||||
var forward_dir = Vector2(0, -1) # Pointing up in texture space (will be rotated by light rotation)
|
||||
|
||||
for x in range(texture_size):
|
||||
for y in range(texture_size):
|
||||
var pos = Vector2(x, y)
|
||||
var offset = pos - center
|
||||
var distance = offset.length()
|
||||
|
||||
if distance > 0.0:
|
||||
# Normalize offset to get direction
|
||||
var dir = offset / distance
|
||||
|
||||
# Calculate angle from forward direction
|
||||
# forward_dir is (0, -1) which has angle -PI/2
|
||||
# We want to find the angle difference
|
||||
var pixel_angle = dir.angle() # Angle of pixel direction
|
||||
var forward_angle = forward_dir.angle() # Angle of forward direction (-PI/2)
|
||||
|
||||
# Calculate angle difference (wrapped to -PI to PI)
|
||||
var angle_diff = pixel_angle - forward_angle
|
||||
# Normalize to -PI to PI range
|
||||
angle_diff = fmod(angle_diff + PI, 2.0 * PI) - PI
|
||||
var abs_angle_diff = abs(angle_diff)
|
||||
|
||||
# Check if within cone angle (hard edge - no smooth falloff)
|
||||
if abs_angle_diff <= half_cone:
|
||||
# Within cone - calculate brightness
|
||||
var normalized_distance = distance / max_distance
|
||||
|
||||
# Fade based on distance (from center) - keep distance falloff
|
||||
# Hard edge for angle (pixely) - no smoothstep on angle
|
||||
var distance_factor = 1.0 - smoothstep(0.0, 1.0, normalized_distance)
|
||||
var alpha = distance_factor # Hard edge on angle, smooth fade on distance
|
||||
var color = Color(1.0, 1.0, 1.0, alpha)
|
||||
image.set_pixel(x, y, color)
|
||||
else:
|
||||
# Outside cone - transparent (hard edge)
|
||||
image.set_pixel(x, y, Color.TRANSPARENT)
|
||||
else:
|
||||
# Center point - full brightness
|
||||
image.set_pixel(x, y, Color.WHITE)
|
||||
|
||||
# Create ImageTexture from the image
|
||||
var texture = ImageTexture.create_from_image(image)
|
||||
cone_light.texture = texture
|
||||
|
||||
# Update cone light spread/angle
|
||||
# Recreates the texture with the new angle to properly show the cone shape
|
||||
func _update_cone_light_spread():
|
||||
if cone_light:
|
||||
# Recreate the texture with the new angle
|
||||
_create_cone_light_texture()
|
||||
|
||||
# Set the cone light angle (in degrees) and update the light
|
||||
func set_cone_light_angle(angle_degrees: float):
|
||||
cone_light_angle = angle_degrees
|
||||
_update_cone_light_spread()
|
||||
|
||||
# Helper function to snap direction to 8-way directions
|
||||
func _snap_to_8_directions(direction: Vector2) -> Vector2:
|
||||
if direction.length() < 0.1:
|
||||
@@ -803,10 +939,19 @@ func _physics_process(delta):
|
||||
|
||||
# Skip input if controls are disabled (e.g., when inventory is open)
|
||||
# But still allow knockback to continue (handled above)
|
||||
var skip_input = controls_disabled
|
||||
if controls_disabled:
|
||||
if not is_knocked_back:
|
||||
velocity = velocity.lerp(Vector2.ZERO, delta * 8.0) # Slow down movement
|
||||
return
|
||||
# Immediately stop movement when controls are disabled (e.g., inventory opened)
|
||||
velocity = Vector2.ZERO
|
||||
# Reset animation to IDLE if not in a special state
|
||||
if current_animation != "THROW" and current_animation != "DAMAGE" and current_animation != "SWORD":
|
||||
if is_lifting:
|
||||
_set_animation("IDLE_HOLD")
|
||||
elif is_pushing:
|
||||
_set_animation("IDLE_PUSH")
|
||||
else:
|
||||
_set_animation("IDLE")
|
||||
|
||||
# Check if being held by someone
|
||||
var being_held_by_someone = false
|
||||
@@ -823,8 +968,8 @@ func _physics_process(delta):
|
||||
# During knockback, no input control - just let velocity carry the player
|
||||
# Apply friction to slow down knockback
|
||||
velocity = velocity.lerp(Vector2.ZERO, delta * 8.0)
|
||||
elif not is_airborne:
|
||||
# Normal input handling
|
||||
elif not is_airborne and not skip_input:
|
||||
# Normal input handling (only if controls are not disabled)
|
||||
struggle_time = 0.0 # Reset struggle timer
|
||||
struggle_direction = Vector2.ZERO
|
||||
_handle_input()
|
||||
@@ -1006,11 +1151,17 @@ func _handle_input():
|
||||
last_movement_direction = input_vector.normalized()
|
||||
|
||||
# Update facing direction (except when pushing - locked direction)
|
||||
var new_direction = current_direction
|
||||
if not is_pushing:
|
||||
current_direction = _get_direction_from_vector(input_vector) as Direction
|
||||
new_direction = _get_direction_from_vector(input_vector) as Direction
|
||||
else:
|
||||
# Keep locked direction when pushing
|
||||
current_direction = push_direction_locked as Direction
|
||||
new_direction = push_direction_locked as Direction
|
||||
|
||||
# Update direction and cone light rotation if changed
|
||||
if new_direction != current_direction:
|
||||
current_direction = new_direction
|
||||
_update_cone_light_rotation()
|
||||
|
||||
# Set animation based on state
|
||||
if is_lifting:
|
||||
@@ -1027,7 +1178,9 @@ func _handle_input():
|
||||
elif is_pushing:
|
||||
_set_animation("IDLE_PUSH")
|
||||
# Keep locked direction when pushing
|
||||
current_direction = push_direction_locked as Direction
|
||||
if push_direction_locked != current_direction:
|
||||
current_direction = push_direction_locked as Direction
|
||||
_update_cone_light_rotation()
|
||||
else:
|
||||
if current_animation != "THROW" and current_animation != "DAMAGE" and current_animation != "SWORD":
|
||||
_set_animation("IDLE")
|
||||
@@ -1054,7 +1207,13 @@ func _handle_input():
|
||||
was_dragging_last_frame = is_dragging_now
|
||||
|
||||
# Reduce speed by half when pushing/pulling
|
||||
var current_speed = move_speed * (0.5 if is_pushing else 1.0)
|
||||
# Calculate speed with encumbrance penalty
|
||||
var base_speed = move_speed * (0.5 if is_pushing else 1.0)
|
||||
var current_speed = base_speed
|
||||
|
||||
# Apply encumbrance penalty (1/4 speed if over-encumbered)
|
||||
if character_stats and character_stats.is_over_encumbered():
|
||||
current_speed = base_speed * 0.25
|
||||
velocity = input_vector * current_speed
|
||||
|
||||
func _handle_movement(_delta):
|
||||
@@ -1611,8 +1770,20 @@ func _perform_attack():
|
||||
can_attack = false
|
||||
is_attacking = true
|
||||
|
||||
# Play attack animation
|
||||
_set_animation("SWORD")
|
||||
# Check what weapon is equipped
|
||||
var equipped_weapon = null
|
||||
if character_stats and character_stats.equipment.has("mainhand"):
|
||||
equipped_weapon = character_stats.equipment["mainhand"]
|
||||
|
||||
var is_bow = false
|
||||
if equipped_weapon and equipped_weapon.weapon_type == Item.WeaponType.BOW:
|
||||
is_bow = true
|
||||
|
||||
# Play attack animation based on weapon
|
||||
if is_bow:
|
||||
_set_animation("BOW")
|
||||
else:
|
||||
_set_animation("SWORD")
|
||||
|
||||
# Calculate attack direction based on player's facing direction
|
||||
var attack_direction = Vector2.ZERO
|
||||
@@ -1634,7 +1805,7 @@ func _perform_attack():
|
||||
Direction.UP_RIGHT:
|
||||
attack_direction = Vector2(1, -1).normalized()
|
||||
|
||||
# Delay before spawning sword slash
|
||||
# Delay before spawning projectile
|
||||
await get_tree().create_timer(0.15).timeout
|
||||
|
||||
# Calculate damage from character_stats with randomization
|
||||
@@ -1659,18 +1830,50 @@ func _perform_attack():
|
||||
# Round to 1 decimal place
|
||||
final_damage = round(final_damage * 10.0) / 10.0
|
||||
|
||||
# Spawn sword projectile
|
||||
if sword_projectile_scene:
|
||||
var projectile = sword_projectile_scene.instantiate()
|
||||
get_parent().add_child(projectile)
|
||||
projectile.setup(attack_direction, self, final_damage)
|
||||
# Store crit status for visual feedback
|
||||
if is_crit:
|
||||
projectile.set_meta("is_crit", true)
|
||||
# Spawn projectile a bit in front of the player
|
||||
var spawn_offset = attack_direction * 10.0 # 10 pixels in front
|
||||
projectile.global_position = global_position + spawn_offset
|
||||
print(name, " attacked with sword projectile! Damage: ", final_damage, " (base: ", base_damage, ", crit: ", is_crit, ")")
|
||||
# Handle bow attacks - require arrows in off-hand
|
||||
if is_bow:
|
||||
# Check for arrows in off-hand
|
||||
var arrows = null
|
||||
if character_stats and character_stats.equipment.has("offhand"):
|
||||
var offhand_item = character_stats.equipment["offhand"]
|
||||
if offhand_item and offhand_item.weapon_type == Item.WeaponType.AMMUNITION:
|
||||
arrows = offhand_item
|
||||
|
||||
# Only spawn arrow if we have arrows
|
||||
if arrows and arrows.quantity > 0:
|
||||
if attack_arrow_scene:
|
||||
var arrow_projectile = attack_arrow_scene.instantiate()
|
||||
get_parent().add_child(arrow_projectile)
|
||||
arrow_projectile.shoot(attack_direction, global_position, self)
|
||||
# Consume one arrow
|
||||
arrows.quantity -= 1
|
||||
var remaining = arrows.quantity
|
||||
if arrows.quantity <= 0:
|
||||
# Remove arrows if quantity reaches 0
|
||||
character_stats.equipment["offhand"] = null
|
||||
if character_stats:
|
||||
character_stats.character_changed.emit(character_stats)
|
||||
else:
|
||||
# Update equipment to reflect quantity change
|
||||
if character_stats:
|
||||
character_stats.character_changed.emit(character_stats)
|
||||
print(name, " shot arrow! Arrows remaining: ", remaining)
|
||||
else:
|
||||
# Play bow animation but no projectile
|
||||
print(name, " tried to shoot but has no arrows!")
|
||||
else:
|
||||
# Spawn sword projectile for non-bow weapons
|
||||
if sword_projectile_scene:
|
||||
var projectile = sword_projectile_scene.instantiate()
|
||||
get_parent().add_child(projectile)
|
||||
projectile.setup(attack_direction, self, final_damage)
|
||||
# Store crit status for visual feedback
|
||||
if is_crit:
|
||||
projectile.set_meta("is_crit", true)
|
||||
# Spawn projectile a bit in front of the player
|
||||
var spawn_offset = attack_direction * 10.0 # 10 pixels in front
|
||||
projectile.global_position = global_position + spawn_offset
|
||||
print(name, " attacked with sword projectile! Damage: ", final_damage, " (base: ", base_damage, ", crit: ", is_crit, ")")
|
||||
|
||||
# Sync attack over network
|
||||
if multiplayer.has_multiplayer_peer() and is_multiplayer_authority() and can_send_rpcs and is_inside_tree():
|
||||
@@ -2572,15 +2775,19 @@ func _sync_stats_update(kills_count: int, coins_count: int):
|
||||
|
||||
@rpc("any_peer", "reliable")
|
||||
func _sync_equipment(equipment_data: Dictionary):
|
||||
# Client receives equipment update from server
|
||||
# Update equipment to match other players
|
||||
# Only process if we're not the authority (remote player)
|
||||
if is_multiplayer_authority():
|
||||
return # Authority ignores this (it's the sender)
|
||||
|
||||
# Client receives equipment update from server or other clients
|
||||
# Update equipment to match server/other players
|
||||
# Server also needs to accept equipment sync from clients (joiners) to update server's copy of joiner's player
|
||||
if not character_stats:
|
||||
return
|
||||
|
||||
# On server, only accept if this is a client player (not server's own player)
|
||||
if multiplayer.is_server():
|
||||
var the_peer_id = get_multiplayer_authority()
|
||||
# If this is the server's own player, ignore (server's own changes are handled differently)
|
||||
if the_peer_id == 0 or the_peer_id == multiplayer.get_unique_id():
|
||||
return
|
||||
|
||||
# Update equipment from data
|
||||
for slot_name in equipment_data.keys():
|
||||
var item_data = equipment_data[slot_name]
|
||||
@@ -2591,7 +2798,29 @@ func _sync_equipment(equipment_data: Dictionary):
|
||||
|
||||
# Update appearance
|
||||
_apply_appearance_to_sprites()
|
||||
print(name, " equipment synced from server")
|
||||
print(name, " equipment synced: ", equipment_data.size(), " slots")
|
||||
|
||||
@rpc("any_peer", "reliable")
|
||||
func _sync_inventory(inventory_data: Array):
|
||||
# Client receives inventory update from server
|
||||
# Update inventory to match server's inventory
|
||||
# Unlike _sync_equipment, we WANT to receive our own inventory from the server
|
||||
# So we check if we're the server (sender) and ignore, not if we're the authority
|
||||
if multiplayer.is_server():
|
||||
return # Server ignores this (it's the sender)
|
||||
|
||||
if not character_stats:
|
||||
return
|
||||
|
||||
# Clear and rebuild inventory from server data
|
||||
character_stats.inventory.clear()
|
||||
for item_data in inventory_data:
|
||||
if item_data != null:
|
||||
character_stats.inventory.append(Item.new(item_data))
|
||||
|
||||
# Emit character_changed to update UI
|
||||
character_stats.character_changed.emit(character_stats)
|
||||
print(name, " inventory synced from server: ", character_stats.inventory.size(), " items")
|
||||
|
||||
func heal(amount: float):
|
||||
if is_dead:
|
||||
|
||||
Reference in New Issue
Block a user