started working on fog darkness

This commit is contained in:
2026-01-13 00:16:08 +01:00
parent 82a70aa6a2
commit 89a41397d1
30 changed files with 1613 additions and 386 deletions

View File

@@ -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: