fixat mer med traps och arrows och grejjer

This commit is contained in:
2026-01-22 01:03:01 +01:00
parent c0d229ee86
commit eaf86b39fa
20 changed files with 1589 additions and 194 deletions

View File

@@ -8,7 +8,7 @@ var appearance_rng: RandomNumberGenerator # Deterministic RNG for appearance/sta
@export var move_speed: float = 80.0
@export var grab_range: float = 20.0
@export var throw_force: float = 150.0
@export var base_throw_force: float = 80.0 # Base throw force (reduced from 150), scales with STR
@export var cone_light_angle: float = 180.0 # Cone spread angle in degrees (adjustable, default wider)
# Network identity
@@ -25,6 +25,7 @@ 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 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
var mouse_control_active: bool = false # True when mouse is controlling facing direction
# Interaction
var held_object = null
@@ -63,6 +64,9 @@ var knockback_duration: float = 0.3 # How long knockback lasts
var can_attack: bool = true
var attack_cooldown: float = 0.0 # No cooldown - instant attacks!
var is_attacking: bool = false
var is_charging_bow: bool = false # True when holding attack with bow+arrows
var bow_charge_start_time: float = 0.0
var bow_charge_percentage: float = 1.0 # 0.5, 0.75, or 1.0 based on charge time
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 staff_projectile_scene = preload("res://scenes/attack_staff.tscn") # Staff magic ball projectile
@@ -167,6 +171,12 @@ const ANIMATIONS = {
"loop": false,
"nextAnimation": "IDLE"
},
"BOW_STRING": {
"frames": [9],
"frameDurations": [30],
"loop": true,
"nextAnimation": null,
},
"BOW": {
"frames": [9, 10, 11, 12],
"frameDurations": [80, 110, 110, 80],
@@ -244,6 +254,7 @@ const ANIMATIONS = {
var current_animation = "IDLE"
var current_frame = 0
var current_direction = Direction.DOWN
var facing_direction_vector: Vector2 = Vector2.DOWN # Full 360-degree facing direction for attacks
var time_since_last_frame = 0.0
func _ready():
@@ -253,6 +264,9 @@ func _ready():
# Set respawn point to starting position
respawn_point = global_position
# Initialize facing direction vector based on current direction
facing_direction_vector = Vector2.DOWN
# Set up input device based on local player index
if is_local_player:
if local_player_index == 0:
@@ -268,7 +282,10 @@ func _ready():
_duplicate_sprite_materials()
# Set up player appearance (randomized based on stats)
_setup_player_appearance()
# ONLY run this for the authority (owner of this player)
# Remote players will receive appearance via _sync_equipment and character_changed signal
if is_multiplayer_authority():
_setup_player_appearance()
# Authority is set by player_manager after adding to scene
@@ -552,27 +569,31 @@ func _randomize_stats():
character_stats.baseStats.wis = appearance_rng.randi_range(8, 12)
character_stats.baseStats.cha = appearance_rng.randi_range(8, 12)
character_stats.baseStats.lck = appearance_rng.randi_range(8, 12)
character_stats.baseStats.per = appearance_rng.randi_range(8, 12)
# Apply race-based stat modifiers
match character_stats.race:
"Dwarf":
# Dwarf: Higher STR, Medium DEX, lower INT, lower WIS, lower LCK
# Dwarf: Higher STR, Medium DEX, lower INT, lower WIS, lower LCK, Medium PER (for disarming)
character_stats.baseStats.str += 3
character_stats.baseStats.int -= 2
character_stats.baseStats.wis -= 2
character_stats.baseStats.lck -= 2
character_stats.baseStats.per += 1
"Elf":
# Elf: Medium STR, Higher DEX, lower INT, medium WIS, higher LCK
# Elf: Medium STR, Higher DEX, lower INT, medium WIS, higher LCK, Highest PER (trap detection)
character_stats.baseStats.dex += 3
character_stats.baseStats.int -= 2
character_stats.baseStats.lck += 2
character_stats.baseStats.per += 4 # Highest perception for trap detection
"Human":
# Human: Lower STR, lower DEX, higher INT, higher WIS, lower LCK
# Human: Lower STR, lower DEX, higher INT, higher WIS, lower LCK, Lower PER
character_stats.baseStats.str -= 2
character_stats.baseStats.dex -= 2
character_stats.baseStats.int += 3
character_stats.baseStats.wis += 3
character_stats.baseStats.lck -= 2
character_stats.baseStats.per -= 1
# Stats randomized (verbose logging removed)
@@ -593,6 +614,16 @@ func _setup_player_appearance():
# Randomize stats AFTER race is set (race affects stat modifiers)
_randomize_stats()
# Give Elf race starting bow and arrows
if selected_race == "Elf":
var starting_bow = ItemDatabase.create_item("short_bow")
var starting_arrows = ItemDatabase.create_item("arrow")
if starting_bow and starting_arrows:
starting_arrows.quantity = 3
character_stats.equipment["mainhand"] = starting_bow
character_stats.equipment["offhand"] = starting_arrows
print("Elf player ", name, " spawned with short bow and 3 arrows")
# Randomize skin (human only for players)
# Weighted random: Human1 has highest chance, Human7 has lowest chance
# Weights: Human1=7, Human2=6, Human3=5, Human4=4, Human5=3, Human6=2, Human7=1 (total=28)
@@ -724,6 +755,12 @@ func _setup_player_appearance():
# Apply appearance to sprite layers
_apply_appearance_to_sprites()
# Emit character_changed to trigger equipment/race sync
if character_stats:
character_stats.character_changed.emit(character_stats)
print("Player ", name, " appearance set up: race=", character_stats.race)
func _apply_appearance_to_sprites():
# Apply character_stats appearance to sprite layers
@@ -998,6 +1035,9 @@ func _on_character_changed(_char: CharacterStats):
else:
equipment_data[slot_name] = null
_rpc_to_ready_peers("_sync_equipment", [equipment_data])
# Sync race and base stats to all clients (for proper display)
_rpc_to_ready_peers("_sync_race_and_stats", [character_stats.race, character_stats.baseStats.duplicate()])
# 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
@@ -1221,8 +1261,15 @@ func _update_animation(delta):
sprite_addons.frame = frame_index
if sprite_headgear:
sprite_headgear.frame = frame_index
# Update weapon sprite - use BOW_STRING animation if charging bow
if sprite_weapon:
sprite_weapon.frame = frame_index
if is_charging_bow:
# Show BOW_STRING animation on weapon sprite only
var bow_string_frame = current_direction * 35 + ANIMATIONS["BOW_STRING"]["frames"][0]
sprite_weapon.frame = bow_string_frame
else:
sprite_weapon.frame = frame_index
func _get_direction_from_vector(vec: Vector2) -> int:
if vec.length() < 0.1:
@@ -1263,6 +1310,13 @@ func _update_facing_from_mouse(mouse_direction: Vector2):
if is_pushing:
return
# Mark that mouse control is active (prevents movement keys from overriding attack direction)
mouse_control_active = true
# Store full 360-degree direction for attacks
if mouse_direction.length() > 0.1:
facing_direction_vector = mouse_direction.normalized()
var new_direction = _get_direction_from_vector(mouse_direction) as Direction
# Update direction and cone light rotation if changed
@@ -1702,19 +1756,25 @@ func _handle_input():
if input_vector.length() > 0.1:
last_movement_direction = input_vector.normalized()
# Update facing direction (except when pushing - locked direction)
# Note: Mouse control will override this if mouse is being used
var new_direction = current_direction
if not is_pushing:
new_direction = _get_direction_from_vector(input_vector) as Direction
else:
# Keep locked direction when pushing
new_direction = push_direction_locked as Direction
# Update full 360-degree facing direction for attacks (gamepad/keyboard input)
# Only update if mouse control is not active (i.e., mouse is outside window or using gamepad)
if not is_pushing and (not mouse_control_active or input_device != -1):
facing_direction_vector = input_vector.normalized()
# Update direction and cone light rotation if changed
if new_direction != current_direction:
current_direction = new_direction
_update_cone_light_rotation()
# Update facing direction for animations (except when pushing - locked direction)
# Only update from movement input if mouse control is not active or using gamepad
if not is_pushing and (not mouse_control_active or input_device != -1):
var new_direction = _get_direction_from_vector(input_vector) as Direction
# Update direction and cone light rotation if changed
if new_direction != current_direction:
current_direction = new_direction
_update_cone_light_rotation()
elif is_pushing:
# Keep locked direction when pushing
if push_direction_locked != current_direction:
current_direction = push_direction_locked as Direction
_update_cone_light_rotation()
# Set animation based on state
if is_lifting:
@@ -1817,6 +1877,36 @@ func _handle_interactions():
else:
grab_just_released = false
# Cancel bow charging if grab is pressed
if grab_just_pressed and is_charging_bow:
is_charging_bow = false
# Sync bow charge end to other clients
if multiplayer.has_multiplayer_peer():
_sync_bow_charge_end.rpc()
print(name, " cancelled bow charge")
# Check for trap disarm (Dwarf only)
if character_stats and character_stats.race == "Dwarf":
var nearby_trap = _get_nearby_disarmable_trap()
if nearby_trap:
if grab_just_pressed:
# Start disarming
nearby_trap.disarming_player = self
nearby_trap.disarm_progress = 0.0
print(name, " (Dwarf) started disarming trap")
elif grab_just_released:
# Cancel disarm if released early
if nearby_trap.disarming_player == self:
nearby_trap._cancel_disarm()
print(name, " (Dwarf) cancelled disarm")
# Don't process regular grab actions if near trap
if grab_button_down:
# Skip grab handling below
just_grabbed_this_frame = false
return
# Track how long grab button is held
if grab_button_down:
grab_button_pressed_time += get_process_delta_time()
@@ -1839,14 +1929,8 @@ func _handle_interactions():
grab_start_time = Time.get_ticks_msec() / 1000.0
print("DEBUG: After _try_grab(), held_object=", held_object != null, " is_lifting=", is_lifting, " grab_button_down=", grab_button_down, " just_grabbed_this_frame=", just_grabbed_this_frame)
elif is_lifting:
# Already lifting - check if moving to throw, or just put down
var is_moving = velocity.length() > 10.0
if is_moving:
# Moving + tap E = throw
_throw_object()
else:
# Not moving + tap E = put down
_place_down_object()
# Already lifting - always place down (throwing is now only via attack button)
_place_down_object()
# Handle grab button release
# CRITICAL: Don't process release if:
@@ -1926,27 +2010,108 @@ func _handle_interactions():
_start_pushing()
# Lift will only happen on release if it was a quick tap
# Handle attack input
# Handle attack input with bow charging mechanic
var attack_pressed = false
var attack_just_pressed = false
var attack_just_released = false
if input_device == -1:
# Keyboard or Mouse
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
attack_pressed = Input.is_action_pressed("attack")
attack_just_pressed = Input.is_action_just_pressed("attack")
attack_just_released = Input.is_action_just_released("attack")
else:
# Gamepad (X button)
attack_just_pressed = Input.is_joy_button_pressed(input_device, JOY_BUTTON_X)
attack_pressed = Input.is_joy_button_pressed(input_device, JOY_BUTTON_X)
attack_just_pressed = attack_pressed and not is_attacking and not is_charging_bow
# For gamepad, detect release by checking if was pressing last frame
attack_just_released = not attack_pressed and is_charging_bow
if attack_just_pressed and can_attack:
if is_lifting:
# Attack while lifting -> throw immediately (no movement required)
_force_throw_held_object(last_movement_direction)
elif not is_pushing:
# Check if player has bow + arrows equipped
var has_bow_and_arrows = false
var equipped_weapon = null
var equipped_arrows = null
if character_stats and character_stats.equipment.has("mainhand") and character_stats.equipment.has("offhand"):
equipped_weapon = character_stats.equipment["mainhand"]
equipped_arrows = character_stats.equipment["offhand"]
if equipped_weapon and equipped_arrows:
if equipped_weapon.weapon_type == Item.WeaponType.BOW and equipped_arrows.weapon_type == Item.WeaponType.AMMUNITION and equipped_arrows.quantity > 0:
has_bow_and_arrows = true
# Handle bow charging
if has_bow_and_arrows and not is_lifting and not is_pushing:
if attack_just_pressed and can_attack and not is_charging_bow:
# Start charging bow
is_charging_bow = true
bow_charge_start_time = Time.get_ticks_msec() / 1000.0
# Sync bow charge start to other clients
if multiplayer.has_multiplayer_peer():
_sync_bow_charge_start.rpc()
print(name, " started charging bow")
elif attack_just_released and is_charging_bow:
# Calculate charge time
var charge_time = (Time.get_ticks_msec() / 1000.0) - bow_charge_start_time
# Minimum charge time: 0.2 seconds, otherwise cancel
if charge_time < 0.2:
is_charging_bow = false
print(name, " cancelled arrow (released too quickly, need at least 0.2s)")
return
# Smooth curve: charge from 0.2s to 1.0s
# Speed scales from 50% to 100% (160 to 320 speed)
var charge_progress = clamp((charge_time - 0.2) / 0.8, 0.0, 1.0) # 0.0 at 0.2s, 1.0 at 1.0s
bow_charge_percentage = 0.5 + (charge_progress * 0.5) # 0.5 to 1.0
# Release bow and shoot
is_charging_bow = false
# Sync bow charge end to other clients
if multiplayer.has_multiplayer_peer():
_sync_bow_charge_end.rpc()
_perform_attack()
print(name, " released bow and shot arrow at ", bow_charge_percentage * 100, "% charge (", charge_time, "s)")
else:
# Reset charging if conditions changed (no bow/arrows, started lifting/pushing)
if is_charging_bow:
is_charging_bow = false
# Sync bow charge end to other clients
if multiplayer.has_multiplayer_peer():
_sync_bow_charge_end.rpc()
print(name, " bow charge cancelled (conditions changed)")
# Normal attack (non-bow or no arrows)
if attack_just_pressed and can_attack:
if is_lifting:
# Attack while lifting -> throw immediately in facing direction
_force_throw_held_object(facing_direction_vector)
elif not is_pushing:
_perform_attack()
# Note: just_grabbed_this_frame is reset when we block a release, or stays true if we grabbed this frame
# This ensures it persists to the next frame to block immediate release
func _get_nearby_disarmable_trap() -> Node:
# Check for nearby trap that can be disarmed (Dwarf only)
var traps = get_tree().get_nodes_in_group("trap")
for trap in traps:
if not trap or not is_instance_valid(trap):
continue
# Check if trap is detected, not disarmed, and within disarm range
if trap.is_detected and not trap.is_disarmed:
var distance = global_position.distance_to(trap.global_position)
# Check if within disarm area range (approximate - trap's DisarmArea has radius ~17)
if distance < 20:
return trap
return null
func _try_grab():
if not grab_area:
return
@@ -2102,8 +2267,9 @@ func _start_pushing():
# Snap to one of 8 directions
push_axis = _snap_to_8_directions(initial_direction)
# Lock the facing direction
# Lock the facing direction (for both animation and attacks)
push_direction_locked = _get_direction_from_vector(push_axis)
facing_direction_vector = push_axis.normalized()
# Re-enable collision with walls (layer 7) for pushing, but keep collision with players/objects disabled
if _is_box(held_object):
@@ -2131,6 +2297,51 @@ func _force_drop_held_object():
# Just release
_stop_pushing()
func reset_grab_state():
# Force reset all grab/lift/push states (used when transitioning levels)
if held_object and is_instance_valid(held_object):
# Re-enable collision on held object
if _is_box(held_object):
held_object.set_collision_layer_value(2, true)
held_object.set_collision_mask_value(1, true)
held_object.set_collision_mask_value(2, true)
held_object.set_collision_mask_value(7, true)
if "is_frozen" in held_object:
held_object.is_frozen = false
if "is_being_held" in held_object:
held_object.is_being_held = false
if "held_by_player" in held_object:
held_object.held_by_player = null
elif _is_player(held_object):
held_object.set_collision_layer_value(1, true)
held_object.set_collision_mask_value(1, true)
held_object.set_collision_mask_value(7, true)
if held_object.has_method("set_being_held"):
held_object.set_being_held(false)
# Stop drag sound if playing
if held_object.has_method("stop_drag_sound"):
held_object.stop_drag_sound()
# Clear all state
held_object = null
grab_offset = Vector2.ZERO
grab_distance = 0.0
is_lifting = false
is_pushing = false
push_axis = Vector2.ZERO
initial_grab_position = Vector2.ZERO
initial_player_position = Vector2.ZERO
just_grabbed_this_frame = false
grab_start_time = 0.0
was_dragging_last_frame = false
# Reset to idle animation
if current_animation == "IDLE_HOLD" or current_animation == "RUN_HOLD" or current_animation == "LIFT" or current_animation == "IDLE_PUSH" or current_animation == "RUN_PUSH":
_set_animation("IDLE")
print("Reset grab state for ", name)
func _stop_pushing():
if not held_object:
return
@@ -2158,10 +2369,12 @@ func _stop_pushing():
released_obj.set_collision_layer_value(2, true)
released_obj.set_collision_mask_value(1, true)
released_obj.set_collision_mask_value(2, true)
released_obj.set_collision_mask_value(7, true) # Re-enable wall collision!
elif _is_player(released_obj):
# Players: back on layer 1
released_obj.set_collision_layer_value(1, true)
released_obj.set_collision_mask_value(1, true)
released_obj.set_collision_mask_value(7, true) # Re-enable wall collision!
if released_obj is CharacterBody2D and released_obj.has_method("set_being_held"):
released_obj.set_being_held(false)
@@ -2183,6 +2396,14 @@ func _stop_pushing():
initial_player_position = Vector2.ZERO
print("Stopped pushing")
func _get_throw_force() -> float:
# Calculate throw force based on player's STR stat
# Base: 80, +3 per STR point
var str_stat = 10.0 # Default STR
if character_stats:
str_stat = character_stats.baseStats.str + character_stats.get_pass("str")
return base_throw_force + (str_stat * 3.0)
func _throw_object():
if not held_object or not is_lifting:
return
@@ -2201,6 +2422,9 @@ func _throw_object():
_place_down_object()
return
# Calculate throw force based on STR
var throw_force = _get_throw_force()
# Position object at player's position before throwing
var throw_start_pos = global_position + throw_direction * 10 # Start slightly in front
@@ -2294,6 +2518,9 @@ func _force_throw_held_object(direction: Vector2):
if throw_direction.length() < 0.1:
throw_direction = Vector2.RIGHT
# Calculate throw force based on STR
var throw_force = _get_throw_force()
# Position object at player's position before throwing
var throw_start_pos = global_position + throw_direction * 10 # Start slightly in front
@@ -2333,6 +2560,7 @@ func _force_throw_held_object(direction: Vector2):
thrown_obj.set_collision_layer_value(2, true)
thrown_obj.set_collision_mask_value(1, true)
thrown_obj.set_collision_mask_value(2, true)
thrown_obj.set_collision_mask_value(7, true) # Re-enable wall collision!
elif _is_player(thrown_obj):
# Player: set position and physics first
thrown_obj.global_position = throw_start_pos
@@ -2354,6 +2582,7 @@ func _force_throw_held_object(direction: Vector2):
if thrown_obj and is_instance_valid(thrown_obj):
thrown_obj.set_collision_layer_value(1, true)
thrown_obj.set_collision_mask_value(1, true)
thrown_obj.set_collision_mask_value(7, true) # Re-enable wall collision!
if thrown_obj.has_method("on_thrown"):
thrown_obj.on_thrown(self, throw_direction * throw_force)
@@ -2373,8 +2602,8 @@ func _place_down_object():
if not held_object:
return
# Place object in front of player based on last movement direction
var place_pos = _find_closest_place_pos(last_movement_direction, held_object)
# Place object in front of player based on facing direction (mouse or movement)
var place_pos = _find_closest_place_pos(facing_direction_vector, held_object)
var placed_obj = held_object
print("DEBUG: Attempting to place ", placed_obj.name if placed_obj else "null", " at ", place_pos, " (player at ", global_position, ", distance: ", global_position.distance_to(place_pos), ")")
@@ -2397,6 +2626,7 @@ func _place_down_object():
placed_obj.set_collision_layer_value(2, true)
placed_obj.set_collision_mask_value(1, true)
placed_obj.set_collision_mask_value(2, true)
placed_obj.set_collision_mask_value(7, true) # Re-enable wall collision!
# Stop movement and reset all state
if "throw_velocity" in placed_obj:
@@ -2417,6 +2647,7 @@ func _place_down_object():
# Player: back on layer 1
placed_obj.set_collision_layer_value(1, true)
placed_obj.set_collision_mask_value(1, true)
placed_obj.set_collision_mask_value(7, true) # Re-enable wall collision!
placed_obj.global_position = place_pos
placed_obj.velocity = Vector2.ZERO
if placed_obj.has_method("set_being_held"):
@@ -2460,25 +2691,8 @@ func _perform_attack():
else:
_set_animation("SWORD")
# Calculate attack direction based on player's facing direction
var attack_direction = Vector2.ZERO
match current_direction:
Direction.RIGHT:
attack_direction = Vector2.RIGHT
Direction.DOWN_RIGHT:
attack_direction = Vector2(1, 1).normalized()
Direction.DOWN:
attack_direction = Vector2.DOWN
Direction.DOWN_LEFT:
attack_direction = Vector2(-1, 1).normalized()
Direction.LEFT:
attack_direction = Vector2.LEFT
Direction.UP_LEFT:
attack_direction = Vector2(-1, -1).normalized()
Direction.UP:
attack_direction = Vector2.UP
Direction.UP_RIGHT:
attack_direction = Vector2(1, -1).normalized()
# Use full 360-degree facing direction for attack
var attack_direction = facing_direction_vector.normalized()
# Delay before spawning projectile
await get_tree().create_timer(0.15).timeout
@@ -2519,7 +2733,10 @@ func _perform_attack():
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)
# Spawn arrow 4 pixels in the direction player is looking
var arrow_spawn_pos = global_position + (attack_direction * 4.0)
# Pass charge percentage to arrow (affects speed)
arrow_projectile.shoot(attack_direction, arrow_spawn_pos, self, bow_charge_percentage)
# Play bow shoot sound
if has_node("SfxBowShoot"):
$SfxBowShoot.play()
@@ -2571,7 +2788,7 @@ func _perform_attack():
# Sync attack over network
if multiplayer.has_multiplayer_peer() and is_multiplayer_authority() and can_send_rpcs and is_inside_tree():
_rpc_to_ready_peers("_sync_attack", [current_direction, attack_direction])
_rpc_to_ready_peers("_sync_attack", [current_direction, attack_direction, bow_charge_percentage])
# Reset attack cooldown (instant if cooldown is 0)
if attack_cooldown > 0:
@@ -2823,7 +3040,7 @@ func _sync_position(pos: Vector2, vel: Vector2, z_pos: float = 0.0, airborne: bo
shadow.modulate.a = 0.5 - (position_z / 100.0) * 0.3
@rpc("any_peer", "reliable")
func _sync_attack(direction: int, attack_dir: Vector2):
func _sync_attack(direction: int, attack_dir: Vector2, charge_percentage: float = 1.0):
# Sync attack to other clients
# Check if node still exists and is valid before processing
if not is_inside_tree() or not is_instance_valid(self):
@@ -2881,8 +3098,9 @@ func _sync_attack(direction: int, attack_dir: Vector2):
if attack_arrow_scene:
var arrow_projectile = attack_arrow_scene.instantiate()
get_parent().add_child(arrow_projectile)
arrow_projectile.shoot(attack_dir, global_position, self)
print(name, " performed synced bow attack with arrow!")
# Use charge percentage from sync (matches local player's arrow)
arrow_projectile.shoot(attack_dir, global_position, self, charge_percentage)
print(name, " performed synced bow attack with arrow (charge: ", charge_percentage * 100, "%)")
else:
# No arrows - just play animation, no projectile (matches host behavior)
print(name, " performed synced bow attack without arrows (no projectile)")
@@ -2895,6 +3113,20 @@ func _sync_attack(direction: int, attack_dir: Vector2):
projectile.global_position = global_position + spawn_offset
print(name, " performed synced attack!")
@rpc("any_peer", "reliable")
func _sync_bow_charge_start():
# Sync bow charge start to other clients
if not is_multiplayer_authority():
is_charging_bow = true
print(name, " (synced) started charging bow")
@rpc("any_peer", "reliable")
func _sync_bow_charge_end():
# Sync bow charge end to other clients
if not is_multiplayer_authority():
is_charging_bow = false
print(name, " (synced) ended charging bow")
@rpc("any_peer", "reliable")
func _sync_throw(obj_name: String, throw_pos: Vector2, force: Vector2, thrower_name: String):
# Sync throw to all clients (RPC sender already threw on their side)
@@ -3128,6 +3360,7 @@ func _sync_release(obj_name: String):
obj.set_collision_layer_value(2, true)
obj.set_collision_mask_value(1, true)
obj.set_collision_mask_value(2, true)
obj.set_collision_mask_value(7, true) # Re-enable wall collision!
if "is_frozen" in obj:
obj.is_frozen = false
# CRITICAL: Clear is_being_held so object can be grabbed again and switches can detect it
@@ -3138,6 +3371,7 @@ func _sync_release(obj_name: String):
elif _is_player(obj):
obj.set_collision_layer_value(1, true)
obj.set_collision_mask_value(1, true)
obj.set_collision_mask_value(7, true) # Re-enable wall collision!
if obj.has_method("set_being_held"):
obj.set_being_held(false)
@@ -3177,6 +3411,7 @@ func _sync_place_down(obj_name: String, place_pos: Vector2):
obj.set_collision_layer_value(2, true)
obj.set_collision_mask_value(1, true)
obj.set_collision_mask_value(2, true)
obj.set_collision_mask_value(7, true) # Re-enable wall collision!
# Reset all state
if "throw_velocity" in obj:
@@ -3196,6 +3431,7 @@ func _sync_place_down(obj_name: String, place_pos: Vector2):
elif _is_player(obj):
obj.set_collision_layer_value(1, true)
obj.set_collision_mask_value(1, true)
obj.set_collision_mask_value(7, true) # Re-enable wall collision!
obj.velocity = Vector2.ZERO
if obj.has_method("set_being_held"):
obj.set_being_held(false)
@@ -3344,6 +3580,7 @@ func _force_place_down(direction: Vector2):
placed_obj.set_collision_layer_value(2, true)
placed_obj.set_collision_mask_value(1, true)
placed_obj.set_collision_mask_value(2, true)
placed_obj.set_collision_mask_value(7, true) # Re-enable wall collision!
if "throw_velocity" in placed_obj:
placed_obj.throw_velocity = Vector2.ZERO
@@ -3362,6 +3599,7 @@ func _force_place_down(direction: Vector2):
elif _is_player(placed_obj):
placed_obj.set_collision_layer_value(1, true)
placed_obj.set_collision_mask_value(1, true)
placed_obj.set_collision_mask_value(7, true) # Re-enable wall collision!
placed_obj.velocity = Vector2.ZERO
if placed_obj.has_method("set_being_held"):
placed_obj.set_being_held(false)
@@ -3398,6 +3636,14 @@ func take_damage(amount: float, attacker_position: Vector2):
if is_dead:
return
# Cancel bow charging when taking damage
if is_charging_bow:
is_charging_bow = false
# Sync bow charge end to other clients
if multiplayer.has_multiplayer_peer():
_sync_bow_charge_end.rpc()
# Check for dodge chance (based on DEX)
var _was_dodged = false
if character_stats:
@@ -3458,7 +3704,9 @@ func take_damage(amount: float, attacker_position: Vector2):
velocity = direction_from_attacker * 250.0 # Scaled down for 1x scale
# Face the attacker (opposite of knockback direction)
current_direction = _get_direction_from_vector(-direction_from_attacker) as Direction
var face_direction = -direction_from_attacker
current_direction = _get_direction_from_vector(face_direction) as Direction
facing_direction_vector = face_direction.normalized()
# Enable knockback state (prevents player control for a short time)
is_knocked_back = true
@@ -3514,6 +3762,7 @@ func _die():
released_obj.set_collision_layer_value(2, true)
released_obj.set_collision_mask_value(1, true)
released_obj.set_collision_mask_value(2, true)
released_obj.set_collision_mask_value(7, true) # Re-enable wall collision!
if "is_being_held" in released_obj:
released_obj.is_being_held = false
if "held_by_player" in released_obj:
@@ -3521,6 +3770,7 @@ func _die():
elif _is_player(released_obj):
released_obj.set_collision_layer_value(1, true)
released_obj.set_collision_mask_value(1, true)
released_obj.set_collision_mask_value(7, true) # Re-enable wall collision!
if released_obj.has_method("set_being_held"):
released_obj.set_being_held(false)
@@ -3593,6 +3843,7 @@ func _die():
# Re-enable our collision
set_collision_layer_value(1, true)
set_collision_mask_value(1, true)
set_collision_mask_value(7, true) # Re-enable wall collision!
# THEN sync to other clients
if multiplayer.has_multiplayer_peer() and can_send_rpcs and is_inside_tree():
@@ -3621,6 +3872,7 @@ func _respawn():
# Re-enable collision in case it was disabled while being carried
set_collision_layer_value(1, true)
set_collision_mask_value(1, true)
set_collision_mask_value(7, true) # Re-enable wall collision!
# Reset health and state
if character_stats:
@@ -3748,6 +4000,7 @@ func _force_holder_to_drop_local(holder_name: String):
# Re-enable collision on dropped player
set_collision_layer_value(1, true)
set_collision_mask_value(1, true)
set_collision_mask_value(7, true) # Re-enable wall collision!
else:
print(" ✗ held_object doesn't match self")
else:
@@ -3766,6 +4019,7 @@ func _sync_respawn(spawn_pos: Vector2):
# Re-enable collision in case it was disabled while being carried
set_collision_layer_value(1, true)
set_collision_mask_value(1, true)
set_collision_mask_value(7, true) # Re-enable wall collision!
# Just teleport and reset on clients (AFTER release is processed)
global_position = spawn_pos
@@ -3814,6 +4068,14 @@ func _sync_stats_update(kills_count: int, coins_count: int):
character_stats.coin = coins_count
print(name, " stats synced from server: kills=", kills_count, " coins=", coins_count)
@rpc("any_peer", "reliable")
func _sync_race_and_stats(race: String, base_stats: Dictionary):
# Client receives race and base stats from authority player
if not is_multiplayer_authority():
character_stats.race = race
character_stats.baseStats = base_stats
print(name, " race and stats synced: race=", race, " STR=", base_stats.str, " PER=", base_stats.per)
@rpc("any_peer", "reliable")
func _sync_equipment(equipment_data: Dictionary):
# Client receives equipment update from server or other clients
@@ -3822,6 +4084,12 @@ func _sync_equipment(equipment_data: Dictionary):
if not character_stats:
return
# CRITICAL: Don't accept equipment syncs for our own player
# Each client manages their own equipment locally
if is_multiplayer_authority():
print(name, " ignoring equipment sync (I'm the authority)")
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()
@@ -3845,15 +4113,19 @@ func _sync_equipment(equipment_data: Dictionary):
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
# CRITICAL: Don't accept inventory syncs for our own player
# Each client manages their own inventory locally (same as equipment)
if is_multiplayer_authority():
print(name, " ignoring inventory sync (I'm the authority)")
return
if multiplayer.is_server():
return # Server ignores this (it's the sender)
if not character_stats:
return
# Clear and rebuild inventory from server data
# Clear and rebuild inventory from server data (only for OTHER players we're viewing)
character_stats.inventory.clear()
for item_data in inventory_data:
if item_data != null:
@@ -4039,7 +4311,9 @@ func _sync_damage(_amount: float, attacker_position: Vector2, is_crit: bool = fa
velocity = direction_from_attacker * 250.0
# Face the attacker
current_direction = _get_direction_from_vector(-direction_from_attacker) as Direction
var face_direction = -direction_from_attacker
current_direction = _get_direction_from_vector(face_direction) as Direction
facing_direction_vector = face_direction.normalized()
# Enable knockback state
is_knocked_back = true