fixat mer med traps och arrows och grejjer
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user