diff --git a/src/main.gd b/src/main.gd index a961c4c..7460970 100644 --- a/src/main.gd +++ b/src/main.gd @@ -113,8 +113,11 @@ func _addPlayer(id: int): if min_distance > best_distance: best_distance = min_distance best_spawn = pos + # CRITICAL: Set position BEFORE adding child so MultiplayerSynchronizer syncs the correct value player.position = best_spawn + print("Setting player ", id, " spawn position to: ", best_spawn) $SpawnRoot.add_child(player) + if id == multiplayer.get_unique_id(): player.initStats(MultiplayerManager.character_data) # iniitate with own stats, cuz this is us... diff --git a/src/main.tscn b/src/main.tscn index 2d42cbe..38a5169 100644 --- a/src/main.tscn +++ b/src/main.tscn @@ -781,30 +781,29 @@ horizontal_alignment = 1 [node name="Node2DPlayerNames" type="Node2D" parent="HUD" unique_id=176021972] [node name="PlayerSpawnPoints" type="Node2D" parent="." unique_id=1413557024] -position = Vector2(0, 1) [node name="SpawnPointA" type="Sprite2D" parent="PlayerSpawnPoints" unique_id=475389955] visible = false z_index = 6 -position = Vector2(-24, 7) +position = Vector2(-24, 8) texture = ExtResource("7_272bh") [node name="SpawnPointA2" type="Sprite2D" parent="PlayerSpawnPoints" unique_id=1602800528] visible = false z_index = 6 -position = Vector2(-24, 151) +position = Vector2(-24, 152) texture = ExtResource("7_272bh") [node name="SpawnPointA3" type="Sprite2D" parent="PlayerSpawnPoints" unique_id=2121277940] visible = false z_index = 6 -position = Vector2(200, 151) +position = Vector2(200, 152) texture = ExtResource("7_272bh") [node name="SpawnPointA4" type="Sprite2D" parent="PlayerSpawnPoints" unique_id=718892012] visible = false z_index = 6 -position = Vector2(216, 7) +position = Vector2(216, 8) texture = ExtResource("7_272bh") [node name="TimerUntilNextRound" type="Timer" parent="." unique_id=807927406] diff --git a/src/scripts/entities/player/player.gd b/src/scripts/entities/player/player.gd index a375570..65b965d 100644 --- a/src/scripts/entities/player/player.gd +++ b/src/scripts/entities/player/player.gd @@ -59,7 +59,7 @@ var accelerationZ = -330.0 # Gravity locked_grab_direction = Vector2.ZERO # Clear locked direction when releasing var is_player = true -@export var direction_vector = Vector2(0, 0) +var direction_vector = Vector2(0, 0) # NOT exported - each player handles their own input locally const ANIMATIONS = { "IDLE": { @@ -577,21 +577,18 @@ func _apply_movement_from_input(_delta: float) -> void: locked_grab_direction = Vector2.ZERO is_grabbing = false - var pot = a.get_parent() + var pot = a.get_parent() + + if multiplayer.is_server(): + # Server can interact directly held_entity = pot held_entity_path = str(pot.get_path()) pot.lift(self) current_animation = "LIFT" - - if multiplayer.is_server(): - # Server can interact directly - held_entity = pot - #held_entity_path = str(pot.get_path()) - #pot.lift(self) - #current_animation = "LIFT" - else: - # Client uses RPC to request from server - MultiplayerManager.request_lift_pot.rpc_id(1, pot.get_path(), multiplayer.get_unique_id()) + else: + # Client uses RPC to request from server + # DON'T set animation or held_entity here - server will sync it + MultiplayerManager.request_lift_pot.rpc_id(1, pot.get_path(), multiplayer.get_unique_id()) break # only allow 1 at a time :) pass pass @@ -602,7 +599,9 @@ func _apply_movement_from_input(_delta: float) -> void: is_grabbing = false if held_entity == null: is_lifting = false - elif held_entity != null and is_lifting == false: + # CRITICAL: Only run auto-throw/put-down logic for the authority (the player controlling this character) + # Otherwise, when held_entity_path syncs, ALL clients will run this and set animations + elif held_entity != null and is_lifting == false and get_multiplayer_authority() == multiplayer.get_unique_id(): if velocity.x != 0 or velocity.y != 0: if multiplayer.is_server(): held_entity.throw(last_direction) @@ -1071,6 +1070,12 @@ func set_grabbed_entity_path_rpc(entity_path: String): # This ensures locked_grab_direction is set correctly via the setter grabbed_entity_path = entity_path +@rpc("authority", "reliable") +func sync_spawn_position(spawn_pos: Vector2): + # Server tells this client where to spawn + position = spawn_pos + print("Client ", multiplayer.get_unique_id(), " received spawn position: ", spawn_pos) + @rpc("call_local") func lift(): diff --git a/src/scripts/entities/player/player.tscn b/src/scripts/entities/player/player.tscn index d1301f1..4eb57f1 100644 --- a/src/scripts/entities/player/player.tscn +++ b/src/scripts/entities/player/player.tscn @@ -73,18 +73,15 @@ properties/9/replication_mode = 2 properties/10/path = NodePath(".:collision_layer") properties/10/spawn = true properties/10/replication_mode = 2 -properties/11/path = NodePath(".:direction_vector") +properties/11/path = NodePath(".:held_entity_path") properties/11/spawn = true properties/11/replication_mode = 2 -properties/12/path = NodePath(".:held_entity_path") +properties/12/path = NodePath(".:grabbed_entity_path") properties/12/spawn = true properties/12/replication_mode = 2 -properties/13/path = NodePath(".:grabbed_entity_path") +properties/13/path = NodePath(".:current_direction") properties/13/spawn = true properties/13/replication_mode = 2 -properties/14/path = NodePath(".:current_direction") -properties/14/spawn = true -properties/14/replication_mode = 2 [sub_resource type="Gradient" id="Gradient_hsjxb"] offsets = PackedFloat32Array(0.847255, 0.861575) diff --git a/src/src/main.gd b/src/src/main.gd new file mode 100644 index 0000000..de73c52 --- /dev/null +++ b/src/src/main.gd @@ -0,0 +1,251 @@ +extends Node2D + +@export var DEBUG_MULTIPLAYER: bool = true + +var player_scene = preload("res://scripts/entities/player/player.tscn") +var pot_scene = preload("res://scripts/entities/world/pot.tscn") + +var has_started = false +var round_started = false +var start_round = false +var round_finished = false + +func _ready() -> void: + MultiplayerManager.addPlayerSignal.connect(_addPlayer) + MultiplayerManager.delPlayerSignal.connect(_delPlayer) + + MultiplayerManager.finished_hosting.connect(_finishedHosting) + + MultiplayerManager.countdownFinished.connect(_finishedCountdown) + + #if id == 1: + #var pot:CharacterBody2D = pot_scene.instantiate() + #pot.position = Vector2(90,80) + #$SpawnRoot.add_child(pot) + + if DEBUG_MULTIPLAYER: + # Add a random delay to ensure instances start at different times + var random_delay = randf_range(0.1, 0.5) + await get_tree().create_timer(random_delay).timeout + call_deferred("_setup_debug_multiplayer") + + pass + +func _finishedCountdown(): + round_started = true + # reset all players hp, kills and deaths + $TimerRound.start($TimerRound.wait_time) + # sync to other players! + if multiplayer.is_server(): + _syncTimerToPlayer.rpc($TimerRound.time_left) + pass + +func time_to_minutes_secs(time: float): + var mins = int(floor(int(time) / 60.0)) + time -= mins * 60 + var secs = int(time) + #var mili = int((time - int(time)) * 100) + var extraSecZero = "0" if secs < 10 else "" + var extraMinZero = "0" if mins < 10 else "" + return extraMinZero + str(mins) + ":" + extraSecZero + str(secs) + +func _process(_delta: float) -> void: + if round_finished == false and round_started == false and start_round == false and multiplayer != null and multiplayer.is_server(): + # make sure atleast 2 players are connected + var _players = 0 + for pl: CharacterBody2D in $SpawnRoot.get_children(): + if "is_player" in pl: + _players += 1 + #if _players == 2: + #start_round_func.rpc() + if round_started: + $HUD/MarginContainerUpperRight/HBoxContainer/VBoxContainer/LabelTimeValue.text = time_to_minutes_secs($TimerRound.time_left) + pass + +@rpc("call_local", "reliable") +func start_round_func(): + if start_round == true: + return + start_round = true + MultiplayerManager.start_round() + + pass + +func _finishedHosting(): + has_started = true + pass + +func _addPlayer(id: int): + print("add player:", id) + #if id == 1: + #var pot:CharacterBody2D = pot_scene.instantiate() + #pot.position = Vector2(90,80) + #$SpawnRoot.add_child(pot, true) + + var player: CharacterBody2D = player_scene.instantiate() + player.name = str(id) + #find empty 16x16 tile to spawn player + ' + if get_parent().get_parent() != null and get_parent().get_parent().has_node("TileMapLayerLower"): + var tile_map = get_parent().get_parent().get_node("TileMapLayerLower") + if tile_map != null: + var player_cell = tile_map.local_to_map(self.global_position) + var cell_tile_data = tile_map.get_cell_tile_data(player_cell) + if cell_tile_data != null: + var terrainData = cell_tile_data.get_custom_data("terrain") + if terrainData != null and terrainData == 8: # 8 = stairs + terrainMultiplier = 0.5 + pass + pass' + # Only server calculates spawn positions - clients receive position via sync + if multiplayer.is_server(): + var best_spawn = Vector2(0, 0) + var best_distance = -1 + + for spawnP: Node2D in $PlayerSpawnPoints.get_children(): + var pos = spawnP.position + var min_distance = INF + # find spawn position which is furthest from all players... + for pl: CharacterBody2D in $SpawnRoot.get_children(): + if "is_player" in pl: + var dist = pl.position.distance_to(pos) + min_distance = min(min_distance, dist) # Keep the smallest distance + pass + # Choose the spawn with the largest minimum distance + if min_distance > best_distance: + best_distance = min_distance + best_spawn = pos + player.position = best_spawn + print("Server: Spawning player ", id, " at position: ", best_spawn) + # else: clients will receive position via MultiplayerSynchronizer after add_child + + $SpawnRoot.add_child(player) + + if id == multiplayer.get_unique_id(): + player.initStats(MultiplayerManager.character_data) # iniitate with own stats, cuz this is us... + + if multiplayer.is_server(): + if !$TimerRound.is_stopped(): + _syncTimerToPlayer.rpc_id(id, $TimerRound.time_left) + pass + +@rpc("reliable") +func _syncTimerToPlayer(iTimeLeft: float): + round_started = true + $TimerRound.start(iTimeLeft) + pass + +func _delPlayer(id: int): + if !$SpawnRoot.has_node(str(id)): + return + $SpawnRoot.get_node(str(id)).queue_free() + pass + + +func _on_timer_timeout() -> void: + if has_started: + var countPots = 0 + for child in $SpawnRoot.get_children(): + if "object_name" in child: + countPots += 1 + pass + if countPots < 8: + var pot = pot_scene.instantiate() + pot.is_spawning = true + pot.positionZ = 90 + pot.position = Vector2(64 + 16 * randi_range(0, 5), 64 + 16 * randi_range(0, 5)) + # Set server as authority for pot synchronization + pot.set_multiplayer_authority(1) + Console.print("Pot spawned with authority: ", pot.get_multiplayer_authority()) + $SpawnRoot.add_child(pot, true) + $TimerSpawnPots.wait_time = randf_range(0.2, 1.4) + $TimerSpawnPots.start() # restart timer... + + pass # Replace with function body. + + +func _on_timer_round_timeout() -> void: + round_started = false + $TimerUntilNextRound.start($TimerUntilNextRound.wait_time) + if multiplayer.is_server(): + MultiplayerManager.round_finished.rpc() + pass # Replace with function body. + + +func _on_timer_until_next_round_timeout() -> void: + if multiplayer.is_server(): + for pl2: CharacterBody2D in $SpawnRoot.get_children(): + if "is_player" in pl2: + var best_spawn = Vector2(0, 0) + var best_distance = -1 + + for spawnP: Node2D in $PlayerSpawnPoints.get_children(): + var pos = spawnP.position + + var min_distance = INF + + # find spawn position which is furthest from all players... + for pl: CharacterBody2D in $SpawnRoot.get_children(): + if "is_player" in pl and pl != pl2: + var dist = pl.position.distance_to(pos) + min_distance = min(min_distance, dist) # Keep the smallest distance + pass + # Choose the spawn with the largest minimum distance + if min_distance > best_distance: + best_distance = min_distance + best_spawn = pos + pl2.position = best_spawn # reset player positions... + start_round = false + MultiplayerManager.new_round_started() + if multiplayer.is_server(): + start_round_func.rpc() + pass # Replace with function body. + +func _setup_debug_multiplayer(): + # Get the character select scene to access the selected character + var character_select = get_tree().get_first_node_in_group("character_select") + if not character_select: + # Try to find it in the scene tree + character_select = get_tree().current_scene.get_node_or_null("CanvasLayer/CharacterSelect") + + if character_select and character_select.has_method("get_current_character_stats"): + # Set the character data from the character select + MultiplayerManager.character_data = character_select.get_current_character_stats() + else: + # Fallback: create a default character + MultiplayerManager.character_data = CharacterStats.new() + MultiplayerManager.character_data.character_name = "DebugPlayer" + $MainMenu._showHostButton() + + # Determine if this instance should host or join + # Check for command line arguments first + var should_host = false + var args = OS.get_cmdline_args() + + # Check if --host or --join argument was passed + if "--host" in args: + should_host = true + print("DEBUG: Host mode specified via command line argument") + elif "--join" in args: + should_host = false + print("DEBUG: Join mode specified via command line argument") + else: + # Fallback: use process ID for deterministic behavior + var process_id = OS.get_process_id() + should_host = process_id % 2 == 1 + print("DEBUG: No command line args, using process ID: ", process_id, " Should host: ", should_host) + + if should_host: + print("DEBUG: Starting as HOST") + $MainMenu._on_button_host_pressed() + else: + print("DEBUG: Starting as CLIENT") + $MainMenu._on_button_join_pressed() + +func _exit_tree(): + # Clean up the debug lock file when exiting + if DEBUG_MULTIPLAYER: + var lock_file_path = "user://debug_host.lock" + if FileAccess.file_exists(lock_file_path): + DirAccess.remove_absolute(lock_file_path) + print("DEBUG: Cleaned up host lock file") diff --git a/src/src/main.gd.uid b/src/src/main.gd.uid new file mode 100644 index 0000000..5124896 --- /dev/null +++ b/src/src/main.gd.uid @@ -0,0 +1 @@ +uid://dwumfuf4tcif1 diff --git a/src/src/scripts/entities/player/player.gd b/src/src/scripts/entities/player/player.gd new file mode 100644 index 0000000..93eaed3 --- /dev/null +++ b/src/src/scripts/entities/player/player.gd @@ -0,0 +1,1081 @@ +extends CharacterBody2D + +@onready var punch_scene = preload("res://scripts/attacks/punch.tscn") +@onready var axe_swing_scene = preload("res://scripts/attacks/axe_swing.tscn") +@onready var sword_slash_scene = preload("res://scripts/attacks/sword_slash.tscn") +@onready var arrow_scene = preload("res://scripts/attacks/arrow.tscn") +@onready var damage_number_scene = preload("res://scripts/components/damage_number.tscn") +@onready var loot_scene = preload("res://scripts/entities/pickups/loot.tscn") + +@onready var body_sprite = $Sprite2DBody +@onready var armour_sprite = $Sprite2DArmour +@onready var facial_sprite = $Sprite2DFacialHair +@onready var hair_sprite = $Sprite2DHair +@onready var eye_sprite = $Sprite2DEyes +@onready var eyelash_sprite = $Sprite2DEyeLashes +@onready var boots_sprite = $Sprite2DBoots +@onready var headgear_sprite = $Sprite2DHeadgear +@onready var addon_sprite = $Sprite2DAddons +@onready var attack_sprite = $Sprite2DWeapon + +const SPEED = 70.0 +const JUMP_VELOCITY = -400.0 + +var held_entity = null +var grabbed_entity = null +var current_height = 0 +var gravity = 800 +var holder = null +var flipFromWall = false + +# Add Z-axis variables similar to loot.gd +@export var positionZ = 0.0 +var velocityZ = 0.0 +var accelerationZ = -330.0 # Gravity + +@export var held_entity_path: String = "": + set(value): + if value != "": + self.held_entity = get_node_or_null(value) + else: + if self.held_entity != null: + self.held_entity.release() + self.held_entity = null + held_entity_path = value + +@export var grabbed_entity_path: String = "": + set(value): + if value != "": + self.grabbed_entity = get_node_or_null(value) + if self.grabbed_entity != null: + self.grabbed_entity.grab(self) + # Lock direction when grabbing - use current last_direction + if locked_grab_direction == Vector2.ZERO: + locked_grab_direction = last_direction + else: + if self.grabbed_entity != null: + self.grabbed_entity.release() + self.grabbed_entity = null + locked_grab_direction = Vector2.ZERO # Clear locked direction when releasing + +var is_player = true +var direction_vector = Vector2(0, 0) # NOT synced - each player handles their own input locally + +const ANIMATIONS = { + "IDLE": { + "frames": [0, 1], + "frameDurations": [500, 500], + "loop": true, + "nextAnimation": null + }, + "RUN": { + "frames": [3, 2, 3, 4], + "frameDurations": [140, 140, 140, 140], + "loop": true, + "nextAnimation": null + }, + "SWORD": { + "frames": [5, 6, 7, 8], + "frameDurations": [40, 60, 90, 80], + "loop": false, + "nextAnimation": "IDLE" + }, + "AXE": { + "frames": [5, 6, 7, 8], + "frameDurations": [50, 70, 100, 90], + "loop": false, + "nextAnimation": "IDLE" + }, + "PUNCH": { + "frames": [16, 17, 18], + "frameDurations": [50, 70, 100], + "loop": false, + "nextAnimation": "IDLE" + }, + "BOW": { + "frames": [9, 10, 11, 12], + "frameDurations": [80, 110, 110, 80], + "loop": false, + "nextAnimation": "IDLE" + }, + "STAFF": { + "frames": [13, 14, 15], + "frameDurations": [200, 200, 400], + "loop": false, + "nextAnimation": "IDLE" + }, + "THROW": { + "frames": [16, 17, 18], + "frameDurations": [80, 80, 300], + "loop": false, + "nextAnimation": "IDLE" + }, + "CONJURE": { + "frames": [19], + "frameDurations": [400], + "loop": false, + "nextAnimation": "IDLE" + }, + "DAMAGE": { + "frames": [20, 21], + "frameDurations": [150, 150], + "loop": false, + "nextAnimation": "IDLE" + }, + "DIE": { + "frames": [21, 22, 23, 24], + "frameDurations": [200, 200, 200, 800], + "loop": false, + "nextAnimation": null + }, + "IDLE_HOLD": { + "frames": [25], + "frameDurations": [500], + "loop": true, + "nextAnimation": null + }, + "RUN_HOLD": { + "frames": [25, 26, 25, 27], + "frameDurations": [150, 150, 150, 150], + "loop": true, + "nextAnimation": null + }, + "JUMP": { + "frames": [25, 26, 27, 28], + "frameDurations": [80, 80, 80, 800], + "loop": false, + "nextAnimation": "IDLE" + }, + "LIFT": { + "frames": [19, 30], + "frameDurations": [70, 70], + "loop": false, + "nextAnimation": "IDLE_HOLD" + }, + "IDLE_PUSH": { + "frames": [30], + "frameDurations": [10], + "loop": true, + "nextAnimation": null + }, + "RUN_PUSH": { + "frames": [30, 29, 30, 31], + "frameDurations": [260, 260, 260, 260], + "loop": true, + "nextAnimation": null + } +} + +enum Direction { + UP = 4, + UP_RIGHT = 3, + RIGHT = 2, + DOWN_RIGHT = 1, + DOWN = 0, + DOWN_LEFT = 7, + LEFT = 6, + UP_LEFT = 5 +} + +@export var direction = Vector2(0, 0) # default down +@export var last_direction = Vector2(0, 1) +var locked_grab_direction: Vector2 = Vector2.ZERO # Direction locked when grabbing a pot +@export var current_direction = Direction.DOWN +var current_frame = 0 +var time_since_last_frame = 0.0 +@export var current_animation: String = "IDLE": + set(iAnimation): + if current_animation != iAnimation: + current_frame = 0 + time_since_last_frame = 0 + current_animation = iAnimation + + +var liftable = true +var is_demo_character = false + +var invul_timer: float = 0.0 +var invul_duration: float = 2.0 +var knockback_strength: float = 160.0 +var knockback_duration: float = 0.4 +var knockback_timer: float = 0.0 +var knockback_direction: Vector2 = Vector2.ZERO + +@export var is_attacking = false +@export var is_using = false +@export var is_grabbing = false +@export var is_lifting = false +@export var is_releasing = false +@export var use_button_down = false +@export var use_button_up = false +@export var attack_button_down = false +@export var attack_button_up = false +@export var is_moving = false +@export var isDemoCharacter = false + +@export var stats: CharacterStats = CharacterStats.new() + +signal player_died + +func _ready() -> void: + body_sprite.material = body_sprite.material.duplicate() + boots_sprite.material = boots_sprite.material.duplicate() + armour_sprite.material = armour_sprite.material.duplicate() + facial_sprite.material = facial_sprite.material.duplicate() + hair_sprite.material = hair_sprite.material.duplicate() + eye_sprite.material = eye_sprite.material.duplicate() + eyelash_sprite.material = eyelash_sprite.material.duplicate() + addon_sprite.material = addon_sprite.material.duplicate() + headgear_sprite.material = headgear_sprite.material.duplicate() + attack_sprite.material = attack_sprite.material.duplicate() + if isDemoCharacter: + $Camera2D.enabled = false + return + if multiplayer.get_unique_id() == int(name): + #set_multiplayer_authority(player_id) + $Camera2D.make_current() + $Camera2D.enabled = true + else: + $Camera2D.enabled = false + #($CanvasLayer/LabelPlayerName as Label).offset_top = global_position.y - 30 + #($CanvasLayer/LabelPlayerName as Label).offset_left = global_position.x - 30 + pass + +func _enter_tree() -> void: + #set_multiplayer_authority(player_id) + if !isDemoCharacter: + set_multiplayer_authority(int(str(name))) + pass + +func _handleInput() -> void: + direction_vector.x = Input.get_axis("ui_left", "ui_right") + direction_vector.y = Input.get_axis("ui_up", "ui_down") + direction_vector = direction_vector.normalized() + + if Input.is_action_just_pressed("Attack"): + attack.rpc() + if Input.is_action_just_pressed("Use"): + use.rpc() + grab.rpc() + $TimerGrab.start() + if Input.is_action_just_released("Use"): + not_use() + if not $TimerGrab.is_stopped(): + lift.rpc() + else: + release.rpc() + pass + +func _physics_process(delta: float) -> void: + if isDemoCharacter: + pass + else: + # Entity synchronization is now handled by setter functions when paths change + if get_multiplayer_authority() == multiplayer.get_unique_id(): + _handleInput() + if stats.hp > 0: # only allow to move if we are alive... + _apply_movement_from_input(delta) + else: + velocity = velocity.move_toward(Vector2.ZERO, 300 * delta) + if knockback_timer > 0: + velocity = velocity.move_toward(Vector2.ZERO, 300 * delta) + knockback_timer -= delta + if knockback_timer <= 0.0: + knockback_timer = 0 + if stats.hp > 0: + #print("we are below", knockback_timer, ", delta was:", delta) + stats.is_invulnerable = false + # Sync invulnerability cleared to clients + if multiplayer.is_server(): + sync_invulnerability.rpc(false) + # Check for collisions to prevent pushing ungrabed objects + # Handle diagonal movement: try moving in each direction separately to allow sliding + var movement = velocity * delta + if movement.length() > 0: + # Store original position to test both directions from the same starting point + var original_position = global_position + var allowed_movement = Vector2.ZERO + + # Test X movement + if abs(movement.x) > 0.01: + var x_movement = Vector2(movement.x, 0) + var collision_x = move_and_collide(x_movement) + var x_actual_movement = global_position - original_position + + if collision_x: + var collider_x = collision_x.get_collider() + if collider_x != null: + var is_pot_x = "is_being_grabbed" in collider_x or "is_being_lifted" in collider_x + var is_player_x = "is_player" in collider_x and collider_x.is_player + # If it's an ungrabed pot/player, move back and don't allow X movement + if ((is_pot_x and not collider_x.is_being_grabbed and not collider_x.is_being_lifted) or (is_player_x and collider_x != self)): + if not (grabbed_entity == collider_x or held_entity == collider_x): + # Move back to original position + global_position = original_position + allowed_movement.x = 0 + else: + # We're grabbing it, allow the movement + allowed_movement.x = x_actual_movement.x + else: + # It's a wall - use the actual movement (move_and_collide handled sliding) + allowed_movement.x = x_actual_movement.x + else: + # No collision, X movement succeeded + allowed_movement.x = x_actual_movement.x + + # Reset to original position for Y test + global_position = original_position + + # Test Y movement + if abs(movement.y) > 0.01: + var y_movement = Vector2(0, movement.y) + var collision_y = move_and_collide(y_movement) + var y_actual_movement = global_position - original_position + + if collision_y: + var collider_y = collision_y.get_collider() + if collider_y != null: + var is_pot_y = "is_being_grabbed" in collider_y or "is_being_lifted" in collider_y + var is_player_y = "is_player" in collider_y and collider_y.is_player + # If it's an ungrabed pot/player, move back and don't allow Y movement + if ((is_pot_y and not collider_y.is_being_grabbed and not collider_y.is_being_lifted) or (is_player_y and collider_y != self)): + if not (grabbed_entity == collider_y or held_entity == collider_y): + # Move back to original position + global_position = original_position + allowed_movement.y = 0 + else: + # We're grabbing it, allow the movement + allowed_movement.y = y_actual_movement.y + else: + # It's a wall - use the actual movement (move_and_collide handled sliding) + allowed_movement.y = y_actual_movement.y + else: + # No collision, Y movement succeeded + allowed_movement.y = y_actual_movement.y + + # Reset to original position + global_position = original_position + + # Apply the combined allowed movement + if allowed_movement.length() > 0.01: + move_and_collide(allowed_movement) + else: + # Client also needs to decrement knockback_timer for visual effects + if knockback_timer > 0: + velocity = velocity.move_toward(Vector2.ZERO, 300 * delta) + knockback_timer -= delta + if knockback_timer <= 0.0: + knockback_timer = 0 + if is_moving: + # this only plays on server.... must play on client also somehow... + if !$SfxWalk.playing and $SfxWalk/TimerWalk.is_stopped(): + $SfxWalk/TimerWalk.start() + $SfxWalk.play() + else: + if $SfxWalk.playing: + $SfxWalk.stop() + _apply_animations(delta) + pass + + +func _apply_movement_from_input(_delta: float) -> void: + direction = direction_vector + if current_animation == "THROW" or current_animation == "DIE" or current_animation == "SWORD" or current_animation == "BOW" or current_animation == "DAMAGE" or current_animation == "LIFT": + pass + else: + var extraString = "_HOLD" if held_entity != null else "" + extraString = "_PUSH" if grabbed_entity != null else extraString + if direction != Vector2.ZERO: + if current_animation != "RUN" + extraString: + time_since_last_frame = 0 + current_frame = 0 + current_animation = "RUN" + extraString + is_moving = true + else: + if current_animation != "IDLE" + extraString: + time_since_last_frame = 0 + current_frame = 0 + current_animation = "IDLE" + extraString + is_moving = false + + var movespeed = SPEED + var terrainMultiplier = 1 + var grabMultiplier = 1 + + # check if player is walking on stairs + if get_parent().get_parent() != null and get_parent().get_parent().has_node("TileMapLayerLower"): + var tile_map = get_parent().get_parent().get_node("TileMapLayerLower") + if tile_map != null: + var player_cell = tile_map.local_to_map(self.global_position) + var cell_tile_data = tile_map.get_cell_tile_data(player_cell) + if cell_tile_data != null: + var terrainData = cell_tile_data.get_custom_data("terrain") + if terrainData != null and terrainData == 8: # 8 = stairs + terrainMultiplier = 0.5 + pass + pass + movespeed *= terrainMultiplier + # Only restrict movement when grabbing/pushing entities, not when lifting/holding + if grabbed_entity != null and held_entity == null: + grabMultiplier = 0.3 + # CRITICAL: Lock direction to locked_grab_direction (set when grabbing) + # If locked_grab_direction is not set, set it now (fallback for edge cases) + if locked_grab_direction == Vector2.ZERO: + locked_grab_direction = last_direction + # set direction to only be locked_grab_direction or inverse locked_grab_direction + if direction != Vector2.ZERO: + var inverseLockedDir = locked_grab_direction * -1 + if abs(direction.angle_to(locked_grab_direction)) > abs(direction.angle_to(inverseLockedDir)): + direction = inverseLockedDir + else: + direction = locked_grab_direction + # DO NOT update last_direction when pushing/pulling - keep it locked! + # This ensures the locked direction stays the same + else: + # Only update last_direction when NOT pushing/pulling + # This ensures direction syncs correctly when holding pots or doing nothing + if direction != Vector2.ZERO: + last_direction = direction + + movespeed *= grabMultiplier + + + if abs(direction.x) >= 0: + velocity.x = move_toward(velocity.x, direction.x * movespeed, 10) + else: + velocity.x = move_toward(velocity.x, 0, SPEED) + if abs(direction.y) >= 0: + velocity.y = move_toward(velocity.y, direction.y * movespeed, 10) + else: + velocity.y = move_toward(velocity.y, 0, SPEED) + + # Note: Pot now follows holder's position directly instead of using velocity + # This ensures push/pull works correctly even when velocity isn't synced + + if is_lifting: + if held_entity == null: + var _a = 2 + var _b = 3 + pass + + if is_attacking: + if grabbed_entity == null and held_entity == null and current_animation != "THROW" and current_animation != "DAMAGE" and current_animation != "DIE": + current_frame = 0 + time_since_last_frame = 0 + current_animation = "SWORD" + if held_entity != null: + # throw it: + if multiplayer.is_server(): + # Server can throw directly + held_entity.throw(last_direction) + current_animation = "THROW" + held_entity = null + held_entity_path = "" + else: + # Client requests throw from server + MultiplayerManager.request_throw_pot.rpc_id(1, held_entity.get_path(), multiplayer.get_unique_id(), last_direction) + is_releasing = false + is_grabbing = false + is_lifting = false + is_attacking = false + pass + pass + + if use_button_up: + if grabbed_entity != null and "release" in grabbed_entity: + grabbed_entity.release() + grabbed_entity = null + grabbed_entity_path = "" + is_releasing = false + is_grabbing = false + use_button_up = false + + if is_releasing: + var is_throwing = false + if held_entity != null: + if velocity.x != 0 or velocity.y != 0: + # THROWING: We have held_entity, so just throw it - DON'T call release() on grabbed_entity + is_throwing = true + if multiplayer.is_server(): + held_entity.throw(last_direction) + held_entity = null + held_entity_path = "" + current_animation = "THROW" + else: + MultiplayerManager.request_throw_pot.rpc_id(1, held_entity.get_path(), multiplayer.get_unique_id(), last_direction) + else: + # PUTTING DOWN: Don't call release(), put_down handles it + if multiplayer.is_server(): + if held_entity.put_down(): + held_entity = null + held_entity_path = "" + else: + MultiplayerManager.request_put_down_pot.rpc_id(1, held_entity.get_path(), multiplayer.get_unique_id()) + + # Only release grabbed_entity if we're NOT throwing AND we have a grabbed_entity (not held_entity) + # When throwing, we have held_entity, so release() should NOT be called + # When releasing (not throwing), we have grabbed_entity, so release() SHOULD be called + if not is_throwing and grabbed_entity != null and "release" in grabbed_entity: + grabbed_entity.release() + + # Clear grabbed_entity_path first so the setter clears the entity properly + grabbed_entity_path = "" + grabbed_entity = null + locked_grab_direction = Vector2.ZERO # Clear locked direction when releasing + is_releasing = false + is_grabbing = false + if held_entity == null: + is_lifting = false + + if held_entity == null and grabbed_entity == null and is_grabbing: + var areas: Array[Area2D] = $Area2DPickup.get_overlapping_areas() + if areas.size() != 0: + for a: Area2D in areas: + # make sure we are looking in direction of the entity + var player_to_pot = (a.get_parent().global_position - self.global_position).normalized() + if player_to_pot.dot(self.last_direction) > 0.78: + var pot = a.get_parent() + if "grab" in pot: + if multiplayer.is_server(): + # Server can interact directly + if pot.grab(self) == true: + if held_entity == null: + is_lifting = false + grabbed_entity = pot + grabbed_entity_path = str(pot.get_path()) + current_animation = "IDLE_PUSH" + # Lock direction to current last_direction when grabbing + locked_grab_direction = last_direction + # Sync to all clients + set_grabbed_entity_path_rpc.rpc(str(pot.get_path())) + sync_animation.rpc("IDLE_PUSH") + else: + # Client uses RPC to request from server + if "request_grab_pot" in pot: + pot.request_grab_pot.rpc(pot.get_path(), multiplayer.get_unique_id()) + break + pass + + #if held_entity == null: + #is_lifting = false + pass + + if held_entity == null and is_lifting: + var areas: Array[Area2D] = $Area2DPickup.get_overlapping_areas() + if areas.size() != 0: + for a: Area2D in areas: + if "lift" in a.get_parent() and a.get_parent() != self and "liftable" in a.get_parent(): + # make sure we are looking in direction of the entity + var player_to_pot = (a.get_parent().global_position - self.global_position).normalized() + if player_to_pot.dot(self.last_direction) > 0.78: + # Release grabbed entity before lifting + if grabbed_entity != null and "release" in grabbed_entity: + grabbed_entity.release() + var _old_grabbed = grabbed_entity + grabbed_entity = null + grabbed_entity_path = "" + locked_grab_direction = Vector2.ZERO + is_grabbing = false + + var pot = a.get_parent() + held_entity = pot + held_entity_path = str(pot.get_path()) + pot.lift(self) + current_animation = "LIFT" + + if multiplayer.is_server(): + # Server can interact directly + held_entity = pot + #held_entity_path = str(pot.get_path()) + #pot.lift(self) + #current_animation = "LIFT" + else: + # Client uses RPC to request from server + MultiplayerManager.request_lift_pot.rpc_id(1, pot.get_path(), multiplayer.get_unique_id()) + break # only allow 1 at a time :) + pass + pass + if grabbed_entity != null and "release" in grabbed_entity: + grabbed_entity.release() + grabbed_entity = null + if grabbed_entity == null: + is_grabbing = false + if held_entity == null: + is_lifting = false + elif held_entity != null and is_lifting == false: + if velocity.x != 0 or velocity.y != 0: + if multiplayer.is_server(): + held_entity.throw(last_direction) + held_entity = null + held_entity_path = "" + current_animation = "THROW" + else: + MultiplayerManager.request_throw_pot.rpc_id(1, held_entity.get_path(), multiplayer.get_unique_id(), last_direction) + else: + if multiplayer.is_server(): + if held_entity.put_down(): + held_entity = null + held_entity_path = "" + else: + is_lifting = true + else: + MultiplayerManager.request_put_down_pot.rpc_id(1, held_entity.get_path(), multiplayer.get_unique_id()) + if grabbed_entity != null and "release" in grabbed_entity: + grabbed_entity.release() + grabbed_entity = null + is_grabbing = false + pass + + + if is_using: + is_using = false + pass + +func _apply_animations(delta: float): + # Always calculate current_direction from last_direction, regardless of grabbed_entity + # This ensures direction is correct when pushing/pulling pots, holding pots, or doing nothing + if last_direction.y < 0 and last_direction.x > 0: + current_direction = Direction.UP_RIGHT + elif last_direction.y < 0 and last_direction.x < 0: + current_direction = Direction.UP_LEFT + elif last_direction.y > 0 and last_direction.x > 0: + current_direction = Direction.DOWN_RIGHT + elif last_direction.y > 0 and last_direction.x < 0: + current_direction = Direction.DOWN_LEFT + elif last_direction.y < 0: + current_direction = Direction.UP + elif last_direction.x > 0: + current_direction = Direction.RIGHT + elif last_direction.y > 0: + current_direction = Direction.DOWN + elif last_direction.x < 0: + current_direction = Direction.LEFT + + # Update label to show animation and direction + if has_node("LabelCurrentAnimation"): + var dir_names = ["DOWN", "DOWN_RIGHT", "RIGHT", "UP_RIGHT", "UP", "UP_LEFT", "LEFT", "DOWN_LEFT"] + var dir_name = dir_names[current_direction] if current_direction < dir_names.size() else "UNKNOWN" + var dir_str = "%.1f,%.1f" % [last_direction.x, last_direction.y] + $LabelCurrentAnimation.text = current_animation + "\nDir:" + dir_name + "\nVec:" + dir_str + + $Sprite2DBody.frame = current_direction * $Sprite2DBody.hframes + ANIMATIONS[current_animation]["frames"][current_frame] + $Sprite2DBoots.frame = $Sprite2DBody.frame + $Sprite2DArmour.frame = $Sprite2DBody.frame + $Sprite2DFacialHair.frame = $Sprite2DBody.frame + $Sprite2DHair.frame = $Sprite2DBody.frame + $Sprite2DEyes.frame = $Sprite2DBody.frame + $Sprite2DEyeLashes.frame = $Sprite2DBody.frame + $Sprite2DAddons.frame = $Sprite2DBody.frame + $Sprite2DHeadgear.frame = $Sprite2DBody.frame + $Sprite2DWeapon.frame = $Sprite2DBody.frame + + time_since_last_frame += delta + if time_since_last_frame >= ANIMATIONS[current_animation]["frameDurations"][current_frame] / 1000.0: + current_frame += 1 + if current_frame >= len(ANIMATIONS[current_animation]["frames"]): + current_frame -= 1 # so it doesnt bug out... + if ANIMATIONS[current_animation]["loop"]: + current_frame = 0 + if ANIMATIONS[current_animation]["nextAnimation"] != null: + current_frame = 0 + current_animation = ANIMATIONS[current_animation]["nextAnimation"] + time_since_last_frame = 0 + + pass + +func _stats_changed(iStats: CharacterStats): + if not is_inside_tree(): + return + + if is_multiplayer_authority(): + # Sync stats to other players + sync_player_stats.rpc( + iStats.save() # Convert stats to dictionary for network transfer + ) + + # check equipment if we have body armour + if iStats.equipment["armour"] == null: + armour_sprite.visible = false + else: + if armour_sprite.texture == null or (armour_sprite.texture as Texture2D).resource_path != iStats.equipment["armour"].equipmentPath: + armour_sprite.texture = load(iStats.equipment["armour"].equipmentPath) + armour_sprite.visible = true + for index in iStats.equipment["armour"].colorReplacements.size(): + var colorReplacement: Dictionary = iStats.equipment["armour"].colorReplacements[index] + armour_sprite.material.set_shader_parameter("original_" + str(index), (colorReplacement["original"] as Color)) + armour_sprite.material.set_shader_parameter("replace_" + str(index), (colorReplacement["replace"] as Color)) + + if iStats.equipment["headgear"] == null: + headgear_sprite.visible = false + else: + if headgear_sprite.texture == null or (headgear_sprite.texture as Texture2D).resource_path != iStats.equipment["headgear"].equipmentPath: + headgear_sprite.texture = load(iStats.equipment["headgear"].equipmentPath) + headgear_sprite.visible = true + for index in iStats.equipment["headgear"].colorReplacements.size(): + var colorReplacement: Dictionary = iStats.equipment["headgear"].colorReplacements[index] + headgear_sprite.material.set_shader_parameter("original_" + str(index), (colorReplacement["original"] as Color)) + headgear_sprite.material.set_shader_parameter("replace_" + str(index), (colorReplacement["replace"] as Color)) + + if iStats.equipment["boots"] == null: + boots_sprite.visible = false + else: + if boots_sprite.texture == null or (boots_sprite.texture as Texture2D).resource_path != iStats.equipment["boots"].equipmentPath: + boots_sprite.texture = load(iStats.equipment["boots"].equipmentPath) + boots_sprite.visible = true + for index in iStats.equipment["boots"].colorReplacements.size(): + var colorReplacement: Dictionary = iStats.equipment["boots"].colorReplacements[index] + boots_sprite.material.set_shader_parameter("original_" + str(index), (colorReplacement["original"] as Color)) + boots_sprite.material.set_shader_parameter("replace_" + str(index), (colorReplacement["replace"] as Color)) + + if body_sprite.texture == null or body_sprite.texture.resource_path != iStats.skin: + #var tex:Texture2D = + #print("The resoucre path is:", body_sprite.texture.resource_path) + body_sprite.texture = load(iStats.skin) + #print("now we change it: ", body_sprite.texture.resource_path) + + if iStats.facial_hair == "": + facial_sprite.visible = false + elif facial_sprite.texture == null or facial_sprite.texture.resource_path != iStats.facial_hair: + facial_sprite.visible = true + + facial_sprite.texture = load(iStats.facial_hair) + #print("facial hair color:", iStats.facial_hair_color) + #facial_sprite.modulate = iStats.facial_hair_color + facial_sprite.material.set_shader_parameter("tint", Vector4(iStats.facial_hair_color.r, iStats.facial_hair_color.g, iStats.facial_hair_color.b, iStats.facial_hair_color.a)) + + + if iStats.hairstyle == "": + hair_sprite.visible = false + elif hair_sprite.texture == null or hair_sprite.texture.resource_path != iStats.hairstyle: + hair_sprite.visible = true + hair_sprite.texture = load(iStats.hairstyle) + hair_sprite.material.set_shader_parameter("tint", Vector4(iStats.hair_color.r, iStats.hair_color.g, iStats.hair_color.b, iStats.hair_color.a)) + + if iStats.eyes == "": + eye_sprite.visible = false + elif eye_sprite.texture == null or eye_sprite.texture.resource_path != iStats.eyes: + eye_sprite.visible = true + eye_sprite.texture = load(iStats.eyes) + + if iStats.eye_lashes == "": + eyelash_sprite.visible = false + elif eyelash_sprite.texture == null or eyelash_sprite.texture.resource_path != iStats.eye_lashes: + eyelash_sprite.visible = true + eyelash_sprite.texture = load(iStats.eye_lashes) + + if iStats.add_on == "": + addon_sprite.visible = false + elif addon_sprite.texture == null or addon_sprite.texture.resource_path != iStats.add_on: + addon_sprite.visible = true + addon_sprite.texture = load(iStats.add_on) + + _updateHp() + pass + +func _updateHp(): + $TextureProgressBarHealth.value = stats.hp / stats.maxhp * 100 + #print("is server?", multiplayer.is_server()) + #print("compare multiplayer id:", multiplayer.get_unique_id(), " with", player_id) + if multiplayer.get_unique_id() == int(name): + (get_parent().get_parent().get_node("HUD/MarginContainer/HBoxContainer/VBoxContainerHearts/TextureProgressBarHearts") as TextureProgressBar).value = $TextureProgressBarHealth.value + (get_parent().get_parent().get_node("HUD/MarginContainer/HBoxContainer/VBoxContainerKills/LabelKillsValue") as Label).text = str(stats.kills) + (get_parent().get_parent().get_node("HUD/MarginContainer/HBoxContainer/VBoxContainerDeaths/LabelDeathsValue") as Label).text = str(stats.deaths) + pass + #$CanvasLayer/LabelPlayerName.text = stats.character_name + $LabelPlayerName.text = stats.character_name + pass + +func initStats(iStats: CharacterStats): + stats = iStats + if stats.is_connected("character_changed", _stats_changed): + stats.disconnect("character_changed", _stats_changed) + stats.connect("character_changed", _stats_changed) + if stats.is_connected("signal_drop_item", _drop_item): + stats.disconnect("signal_drop_item", _drop_item) + stats.connect("signal_drop_item", _drop_item) + + if grabbed_entity != null and "release" in grabbed_entity: + grabbed_entity.release() + grabbed_entity = null + if held_entity != null and "put_down" in held_entity: + held_entity.put_down() + held_entity = null + is_lifting = false + is_grabbing = false + + _stats_changed(stats) + pass + +func _drop_item(iItem: Item): + var loot = loot_scene.instantiate() + #GameManager.get_node("Loot").call_deferred("add_child", loot) + loot.setItem(iItem) + loot.global_position = global_position + + var angle = last_direction.angle() + var speed = randf_range(50, 100) + var velZ = randf_range(100, 200) + + var dir = Vector2.from_angle(angle) + loot.velocity = dir * speed + loot.velocityZ = velZ + pass + +func lose_held_entity(_iBody: CharacterBody2D): + held_entity = null + is_lifting = false + is_grabbing = false + pass + +@rpc("reliable") +func sync_player_stats(stats_dict: Dictionary): + if not is_multiplayer_authority(): # Only non-authority players should receive updates + # Load the received stats into our stats object + stats.load(stats_dict) + # Update visuals + _stats_changed(stats) + + +@rpc("any_peer", "reliable") +func take_dmg_sound(): + $SfxTakeDamage.play() + pass + +@rpc("call_local", "reliable") +func die_sound(): + if $SfxTakeDamage.playing: + $SfxTakeDamage.stop() + $SfxDie.play() + pass + +func take_damage(iBody: Node2D, _iByWhoOrWhat: Node2D) -> void: + # Direct call version - used when called locally + var damager_pos = iBody.global_position if iBody != null else global_position + var damager_path = _iByWhoOrWhat.get_path() if _iByWhoOrWhat != null else "" + _take_damage_internal(damager_pos, damager_path, _iByWhoOrWhat) + +@rpc("any_peer", "reliable") +func take_damage_rpc(damager_position: Vector2, damager_path: String, damager_peer_id: int): + # RPC version - used when server calls on joiner's player node + # Get the actual damager node if we can + var _iByWhoOrWhat = null + if damager_path != "": + _iByWhoOrWhat = get_node_or_null(damager_path) + _take_damage_internal(damager_position, damager_path, _iByWhoOrWhat) + +func _take_damage_internal(damager_position: Vector2, damager_path: String, _iByWhoOrWhat: Node2D): + # Internal function that handles the actual damage logic + # This allows both direct calls and RPC calls to work + if !stats.is_invulnerable: + # Apply damage (works on both server and client since each player has authority over themselves) + stats.take_damage(13.0) + stats.is_invulnerable = true + knockback_timer = knockback_duration + + # Sync invulnerability state to all clients (so other players know this player is invulnerable) + sync_invulnerability.rpc(true) + + if current_animation != "DAMAGE": + time_since_last_frame = 0 + current_frame = 0 + + current_animation = "DAMAGE" + + # Calculate knockback direction from the damaging body position + knockback_direction = (global_position - damager_position).normalized() + velocity = knockback_direction * knockback_strength + _updateHp() + if held_entity != null: + if multiplayer.is_server(): + held_entity.throw(knockback_direction) + else: + MultiplayerManager.request_throw_pot.rpc_id(1, held_entity.get_path(), multiplayer.get_unique_id(), knockback_direction) + held_entity = null + held_entity_path = "" + is_lifting = false + + if grabbed_entity != null and "release" in grabbed_entity: + grabbed_entity.release() + grabbed_entity = null + is_grabbing = false + + # Create damage number + if damage_number_scene: + var damage_number = damage_number_scene.instantiate() + get_tree().current_scene.call_deferred("add_child", damage_number) + damage_number.global_position = global_position + Vector2(0, -16) + damage_number.direction = Vector2(0, -1) + damage_number.label = "1" + + if stats.hp <= 0: + stats.deaths += 1 + sync_player_deaths.rpc(stats.deaths) + if _iByWhoOrWhat != null and _iByWhoOrWhat is CharacterBody2D: + _iByWhoOrWhat.stats.kills += 1 + _iByWhoOrWhat.sync_player_kills.rpc(_iByWhoOrWhat.stats.kills) # run for everyone + ' + if _iByWhoOrWhat.player_id != multiplayer.get_unique_id(): + # Broadcast this character data to all other connected peers + #_iByWhoOrWhat.sync_player_kills.rpc_id(_iByWhoOrWhat.player_id, _iByWhoOrWhat.stats.kills) + pass + else: + _iByWhoOrWhat.stats.forceUpdate() + ' + + # give score to other player... + if current_animation != "DIE": + time_since_last_frame = 0 + current_frame = 0 + current_animation = "DIE" + die_sound.rpc() + # wait a bit so we can hear the die sound before removing the player + #_updateScore.rpc() + call_deferred("_on_died") + return + +@rpc("any_peer", "reliable") +func take_damage_effects(damager_position: Vector2, damager_path: String): + # Client receives damage effects - play visual/audio but don't modify stats + # Note: is_invulnerable is already set to true by sync_invulnerability RPC + # We just need to set knockback_timer for visual effects + take_dmg_sound.rpc() + knockback_timer = knockback_duration + + if current_animation != "DAMAGE": + time_since_last_frame = 0 + current_frame = 0 + + current_animation = "DAMAGE" + + # Calculate knockback direction from the damaging body position + knockback_direction = (global_position - damager_position).normalized() + velocity = knockback_direction * knockback_strength + + if held_entity != null: + if multiplayer.is_server(): + held_entity.throw(knockback_direction) + else: + MultiplayerManager.request_throw_pot.rpc_id(1, held_entity.get_path(), multiplayer.get_unique_id(), knockback_direction) + held_entity = null + held_entity_path = "" + is_lifting = false + + if grabbed_entity != null and "release" in grabbed_entity: + grabbed_entity.release() + grabbed_entity = null + is_grabbing = false + + # Create damage number + if damage_number_scene: + var damage_number = damage_number_scene.instantiate() + get_tree().current_scene.call_deferred("add_child", damage_number) + damage_number.global_position = global_position + Vector2(0, -16) + damage_number.direction = Vector2(0, -1) + damage_number.label = "1" + +@rpc("call_local", "reliable") +func sync_player_kills(iKills: int): + stats.kills = iKills + stats.forceUpdate() + _updateHp() + MultiplayerManager.updateScore() + pass + +@rpc("call_local", "reliable") +func sync_player_deaths(iDeaths: int): + stats.deaths = iDeaths + stats.forceUpdate() + _updateHp() + MultiplayerManager.updateScore() + pass + +@rpc("any_peer", "reliable") +func sync_animation(animation_name: String): + #print("Client ", multiplayer.get_unique_id(), " received sync_animation: ", animation_name) + current_animation = animation_name + pass + +@rpc("any_peer", "reliable") +func sync_invulnerability(is_invul: bool): + # Sync invulnerability state from server to clients + # This ensures both server and client have the same invulnerability state + stats.is_invulnerable = is_invul + # If invulnerability is cleared, also clear knockback_timer on client + if not is_invul: + knockback_timer = 0 + pass + +# RPC functions removed - entity synchronization now handled by setter functions + +@rpc("reliable") +func _updateScore(): + MultiplayerManager.updateScore() + pass + +func _on_died(): + emit_signal("player_died") + # remove collision + self.set_collision_layer_value(10, false) + await get_tree().create_timer(2.1).timeout + #reset hp: + # find spawn point: + var spointPouints = get_parent().get_parent().get_node("PlayerSpawnPoints") + var targetPos = null + for spawnP: Node2D in spointPouints.get_children(): + var pos = spawnP.position + if targetPos == null: + targetPos = pos + else: + # find spawn position which is furthest from all players... + for pl: CharacterBody2D in get_parent().get_children(): + if "is_player" in pl: + # compare + if pl.position.distance_to(pos) > pl.position.distance_to(targetPos): + targetPos = pos + pass + pass + pass + position = targetPos + stats.hp = stats.maxhp + stats.is_invulnerable = false + stats.forceUpdate() + if current_animation != "IDLE": + current_animation = "IDLE" + self.set_collision_layer_value(10, true) + pass + + +@rpc("call_local") +func attack(): + is_attacking = true + +@rpc("call_local") +func use(): + use_button_down = true + +@rpc("call_local") +func not_use(): + use_button_up = true + +@rpc("call_local") +func grab(): + is_grabbing = true + +@rpc("any_peer", "reliable") +func set_held_entity_path_rpc(entity_path: String): + held_entity_path = entity_path + +@rpc("any_peer", "reliable") +func set_grabbed_entity_path_rpc(entity_path: String): + # Set grabbed_entity_path - the setter will handle finding the entity and locking direction + # This ensures locked_grab_direction is set correctly via the setter + grabbed_entity_path = entity_path + + +@rpc("call_local") +func lift(): + is_lifting = !is_lifting + +@rpc("call_local") +func release(): + is_releasing = true diff --git a/src/src/scripts/entities/player/player.gd.uid b/src/src/scripts/entities/player/player.gd.uid new file mode 100644 index 0000000..8945417 --- /dev/null +++ b/src/src/scripts/entities/player/player.gd.uid @@ -0,0 +1 @@ +uid://b5dp4ifyxo38q diff --git a/src/src/scripts/entities/player/player.tscn b/src/src/scripts/entities/player/player.tscn new file mode 100644 index 0000000..4f060ee --- /dev/null +++ b/src/src/scripts/entities/player/player.tscn @@ -0,0 +1,336 @@ +[gd_scene format=3 uid="uid://dgtfy455abe1t"] + +[ext_resource type="Script" uid="uid://cvvy2s6620mcw" path="res://scripts/entities/player/player.gd" id="1_sgemx"] +[ext_resource type="Texture2D" uid="uid://bkninujaqqvb1" path="res://assets/gfx/Puny-Characters/Layer 0 - Skins/Human1_1.png" id="3_0818e"] +[ext_resource type="Shader" uid="uid://cfd38qf1ojmft" path="res://assets/shaders/cloth.gdshader" id="4_6nxnb"] +[ext_resource type="Script" uid="uid://yid4hjp68enj" path="res://scripts/entities/player/camera_2d.gd" id="4_n1hb6"] +[ext_resource type="Texture2D" uid="uid://dx1fovugabbwc" path="res://assets/gfx/Puny-Characters/Layer 1 - Shoes/IronBoots.png" id="5_2bw0v"] +[ext_resource type="Texture2D" uid="uid://bbqk2lcs772q3" path="res://assets/gfx/Puny-Characters/Layer 2 - Clothes/Armour Body/BronzeArmour.png" id="5_7drg4"] +[ext_resource type="Texture2D" uid="uid://bkiexfnpcaxwa" path="res://assets/gfx/Puny-Characters/Layer 4 - Hairstyle/Facial Hairstyles/Mustache1White.png" id="7_2bw0v"] +[ext_resource type="Texture2D" uid="uid://0lmhxwt7k3e4" path="res://assets/gfx/Puny-Characters/Layer 5 - Eyes/Eye Color/EyecolorLightLime.png" id="8_68eso"] +[ext_resource type="Texture2D" uid="uid://ccu5cpyo7jpdr" path="res://assets/gfx/Puny-Characters/Layer 4 - Hairstyle/Hairstyles/MHairstyle8White.png" id="8_pyh4g"] +[ext_resource type="Texture2D" uid="uid://b4vh2v0x58v2f" path="res://assets/gfx/Puny-Characters/Layer 5 - Eyes/Eyelashes/MEyelash1.png" id="9_cvm1n"] +[ext_resource type="Texture2D" uid="uid://jxo0e2x145rs" path="res://assets/gfx/Puny-Characters/Layer 7 - Add-ons/Elf Add-ons/ElfEars3.png" id="10_o8aek"] +[ext_resource type="Texture2D" uid="uid://cu5fkio3ajr5i" path="res://assets/gfx/Puny-Characters/Layer 6 - Headgears/French/MusketeerHatPurple.png" id="11_idlgo"] +[ext_resource type="Texture2D" uid="uid://bloqx3mibftjn" path="res://assets/gfx/Puny-Characters/WeaponOverlayer.png" id="12_0818e"] +[ext_resource type="AudioStream" uid="uid://cbio6f0ssxvd6" path="res://assets/audio/sfx/walk/stone/walk_stone_1.wav.mp3" id="14_0818e"] +[ext_resource type="AudioStream" uid="uid://dq1va2882v23v" path="res://assets/audio/sfx/walk/stone/walk_stone_2.wav.mp3" id="15_2bw0v"] +[ext_resource type="AudioStream" uid="uid://dsuf4oa710gi8" path="res://assets/audio/sfx/walk/stone/walk_stone_3.wav.mp3" id="16_pyh4g"] +[ext_resource type="AudioStream" uid="uid://fvhvmxtcq018" path="res://assets/audio/sfx/walk/stone/walk_stone_4.wav.mp3" id="17_jfw4q"] +[ext_resource type="AudioStream" uid="uid://cw74evef8fm0t" path="res://assets/audio/sfx/walk/stone/walk_stone_5.wav.mp3" id="18_fj670"] +[ext_resource type="AudioStream" uid="uid://c43fyqtos11fd" path="res://assets/audio/sfx/walk/stone/walk_stone_6.wav.mp3" id="19_0j5vc"] +[ext_resource type="FontFile" uid="uid://bajcvmidrnc33" path="res://assets/fonts/standard_font.png" id="21_pyh4g"] +[ext_resource type="AudioStream" uid="uid://b4ng0o2en2hkm" path="res://assets/audio/sfx/player/fall_out/player_fall_infinitely-02.wav.mp3" id="22_jfw4q"] +[ext_resource type="AudioStream" uid="uid://bi546r2d771yg" path="res://assets/audio/sfx/player/take_damage/player_damaged_01.wav.mp3" id="23_7puce"] +[ext_resource type="AudioStream" uid="uid://b8trgc0pbomud" path="res://assets/audio/sfx/player/take_damage/player_damaged_02.wav.mp3" id="24_3n1we"] +[ext_resource type="AudioStream" uid="uid://dsnvagvhs152x" path="res://assets/audio/sfx/player/take_damage/player_damaged_03.wav.mp3" id="25_h8vet"] +[ext_resource type="AudioStream" uid="uid://ce51n4tvvflro" path="res://assets/audio/sfx/player/take_damage/player_damaged_04.wav.mp3" id="26_1rlbx"] +[ext_resource type="AudioStream" uid="uid://caclaiagfnr2o" path="res://assets/audio/sfx/player/take_damage/player_damaged_05.wav.mp3" id="27_1sdav"] +[ext_resource type="AudioStream" uid="uid://dighi525ty7sl" path="res://assets/audio/sfx/player/take_damage/player_damaged_06.wav.mp3" id="28_x7koh"] +[ext_resource type="AudioStream" uid="uid://bdhmel5vyixng" path="res://assets/audio/sfx/player/take_damage/player_damaged_07.wav.mp3" id="29_jl8uc"] + +[sub_resource type="Gradient" id="Gradient_n1hb6"] +offsets = PackedFloat32Array(0.742243, 0.75179) +colors = PackedColorArray(1, 1, 1, 1, 1, 1, 1, 0) + +[sub_resource type="GradientTexture2D" id="GradientTexture2D_n1hb6"] +gradient = SubResource("Gradient_n1hb6") +fill = 1 +fill_from = Vector2(0.508547, 0.487179) +fill_to = Vector2(0.961538, 0.034188) + +[sub_resource type="SceneReplicationConfig" id="SceneReplicationConfig_fgrik"] +properties/0/path = NodePath(".:position") +properties/0/spawn = true +properties/0/replication_mode = 1 +properties/1/path = NodePath(".:is_attacking") +properties/1/spawn = true +properties/1/replication_mode = 2 +properties/2/path = NodePath(".:is_using") +properties/2/spawn = true +properties/2/replication_mode = 2 +properties/3/path = NodePath(".:current_animation") +properties/3/spawn = true +properties/3/replication_mode = 2 +properties/4/path = NodePath(".:last_direction") +properties/4/spawn = true +properties/4/replication_mode = 2 +properties/5/path = NodePath(".:held_entity_path") +properties/5/spawn = true +properties/5/replication_mode = 2 +properties/6/path = NodePath(".:grabbed_entity_path") +properties/6/spawn = true +properties/6/replication_mode = 2 +properties/7/path = NodePath(".:current_direction") +properties/7/spawn = true +properties/7/replication_mode = 2 + +[sub_resource type="Gradient" id="Gradient_hsjxb"] +offsets = PackedFloat32Array(0.847255, 0.861575) +colors = PackedColorArray(0, 0, 0, 0.611765, 0, 0, 0, 0) + +[sub_resource type="GradientTexture2D" id="GradientTexture2D_0818e"] +gradient = SubResource("Gradient_hsjxb") +width = 14 +height = 6 +fill = 1 +fill_from = Vector2(0.504274, 0.478632) +fill_to = Vector2(0.897436, 0.0769231) + +[sub_resource type="ShaderMaterial" id="ShaderMaterial_2bw0v"] +shader = ExtResource("4_6nxnb") +shader_parameter/original_0 = Color(0, 0, 0, 1) +shader_parameter/original_1 = Color(0, 0, 0, 1) +shader_parameter/original_2 = Color(0, 0, 0, 1) +shader_parameter/original_3 = Color(0, 0, 0, 1) +shader_parameter/original_4 = Color(0, 0, 0, 1) +shader_parameter/original_5 = Color(0, 0, 0, 1) +shader_parameter/original_6 = Color(0, 0, 0, 1) +shader_parameter/replace_0 = Color(0, 0, 0, 1) +shader_parameter/replace_1 = Color(0, 0, 0, 1) +shader_parameter/replace_2 = Color(0, 0, 0, 1) +shader_parameter/replace_3 = Color(0, 0, 0, 1) +shader_parameter/replace_4 = Color(0, 0, 0, 1) +shader_parameter/replace_5 = Color(0, 0, 0, 1) +shader_parameter/replace_6 = Color(0, 0, 0, 1) +shader_parameter/tint = Color(1, 1, 1, 1) + +[sub_resource type="ShaderMaterial" id="ShaderMaterial_8ugno"] +shader = ExtResource("4_6nxnb") +shader_parameter/original_0 = Color(0, 0, 0, 1) +shader_parameter/original_1 = Color(0, 0, 0, 1) +shader_parameter/original_2 = Color(0, 0, 0, 1) +shader_parameter/original_3 = Color(0, 0, 0, 1) +shader_parameter/original_4 = Color(0, 0, 0, 1) +shader_parameter/original_5 = Color(0, 0, 0, 1) +shader_parameter/original_6 = Color(0, 0, 0, 1) +shader_parameter/replace_0 = Color(0, 0, 0, 1) +shader_parameter/replace_1 = Color(0, 0, 0, 1) +shader_parameter/replace_2 = Color(0, 0, 0, 1) +shader_parameter/replace_3 = Color(0, 0, 0, 1) +shader_parameter/replace_4 = Color(0, 0, 0, 1) +shader_parameter/replace_5 = Color(0, 0, 0, 1) +shader_parameter/replace_6 = Color(0, 0, 0, 1) +shader_parameter/tint = Color(1, 1, 1, 1) + +[sub_resource type="RectangleShape2D" id="RectangleShape2D_sgemx"] +size = Vector2(8, 6) + +[sub_resource type="AudioStreamRandomizer" id="AudioStreamRandomizer_40ewq"] +streams_count = 6 +stream_0/stream = ExtResource("14_0818e") +stream_1/stream = ExtResource("15_2bw0v") +stream_2/stream = ExtResource("16_pyh4g") +stream_3/stream = ExtResource("17_jfw4q") +stream_4/stream = ExtResource("18_fj670") +stream_5/stream = ExtResource("19_0j5vc") + +[sub_resource type="RectangleShape2D" id="RectangleShape2D_0818e"] +size = Vector2(10, 8) + +[sub_resource type="Gradient" id="Gradient_2bw0v"] +offsets = PackedFloat32Array(0) +colors = PackedColorArray(0, 0, 0, 1) + +[sub_resource type="GradientTexture1D" id="GradientTexture1D_pyh4g"] +gradient = SubResource("Gradient_2bw0v") +width = 16 + +[sub_resource type="Gradient" id="Gradient_jfw4q"] +offsets = PackedFloat32Array(1) +colors = PackedColorArray(1, 0.231947, 0.351847, 1) + +[sub_resource type="GradientTexture1D" id="GradientTexture1D_fj670"] +gradient = SubResource("Gradient_jfw4q") +width = 16 + +[sub_resource type="AudioStreamRandomizer" id="AudioStreamRandomizer_hnhes"] +streams_count = 7 +stream_0/stream = ExtResource("23_7puce") +stream_1/stream = ExtResource("24_3n1we") +stream_2/stream = ExtResource("25_h8vet") +stream_3/stream = ExtResource("26_1rlbx") +stream_4/stream = ExtResource("27_1sdav") +stream_5/stream = ExtResource("28_x7koh") +stream_6/stream = ExtResource("29_jl8uc") + +[node name="Player" type="CharacterBody2D" unique_id=642482055] +collision_layer = 512 +collision_mask = 704 +script = ExtResource("1_sgemx") + +[node name="PlayerLight" type="PointLight2D" parent="." unique_id=98233177] +z_index = 10 +position = Vector2(-1, -6) +blend_mode = 2 +range_layer_max = 2 +texture = SubResource("GradientTexture2D_n1hb6") + +[node name="PlayerSynchronizer" type="MultiplayerSynchronizer" parent="." unique_id=1561958126] +replication_config = SubResource("SceneReplicationConfig_fgrik") + +[node name="Sprite2DShadow" type="Sprite2D" parent="." unique_id=1430953243] +position = Vector2(0, 2) +texture = SubResource("GradientTexture2D_0818e") + +[node name="Sprite2DBody" type="Sprite2D" parent="." unique_id=36949699] +material = SubResource("ShaderMaterial_2bw0v") +position = Vector2(0, -5) +texture = ExtResource("3_0818e") +hframes = 35 +vframes = 8 + +[node name="Sprite2DBoots" type="Sprite2D" parent="." unique_id=1502518208] +material = SubResource("ShaderMaterial_8ugno") +position = Vector2(0, -5) +texture = ExtResource("5_2bw0v") +hframes = 35 +vframes = 8 + +[node name="Sprite2DArmour" type="Sprite2D" parent="." unique_id=1239356181] +material = SubResource("ShaderMaterial_8ugno") +position = Vector2(0, -5) +texture = ExtResource("5_7drg4") +hframes = 35 +vframes = 8 + +[node name="Sprite2DFacialHair" type="Sprite2D" parent="." unique_id=973907314] +material = SubResource("ShaderMaterial_8ugno") +position = Vector2(0, -5) +texture = ExtResource("7_2bw0v") +hframes = 35 +vframes = 8 + +[node name="Sprite2DHair" type="Sprite2D" parent="." unique_id=1924405266] +material = SubResource("ShaderMaterial_8ugno") +position = Vector2(0, -5) +texture = ExtResource("8_pyh4g") +hframes = 35 +vframes = 8 + +[node name="Sprite2DEyes" type="Sprite2D" parent="." unique_id=1443066557] +material = SubResource("ShaderMaterial_8ugno") +position = Vector2(0, -5) +texture = ExtResource("8_68eso") +hframes = 35 +vframes = 8 + +[node name="Sprite2DEyeLashes" type="Sprite2D" parent="." unique_id=691771626] +material = SubResource("ShaderMaterial_8ugno") +position = Vector2(0, -5) +texture = ExtResource("9_cvm1n") +hframes = 35 +vframes = 8 + +[node name="Sprite2DAddons" type="Sprite2D" parent="." unique_id=647154359] +material = SubResource("ShaderMaterial_8ugno") +position = Vector2(0, -5) +texture = ExtResource("10_o8aek") +hframes = 35 +vframes = 8 + +[node name="Sprite2DHeadgear" type="Sprite2D" parent="." unique_id=831310279] +material = SubResource("ShaderMaterial_8ugno") +position = Vector2(0, -5) +texture = ExtResource("11_idlgo") +hframes = 35 +vframes = 8 + +[node name="Sprite2DWeapon" type="Sprite2D" parent="." unique_id=2021209530] +material = SubResource("ShaderMaterial_8ugno") +position = Vector2(0, -5) +texture = ExtResource("12_0818e") +hframes = 35 +vframes = 8 + +[node name="CollisionShape2D" type="CollisionShape2D" parent="." unique_id=131165090] +position = Vector2(0, -1) +shape = SubResource("RectangleShape2D_sgemx") + +[node name="Camera2D" type="Camera2D" parent="." unique_id=643763897] +zoom = Vector2(3, 3) +position_smoothing_enabled = true +script = ExtResource("4_n1hb6") + +[node name="Timer" type="Timer" parent="Camera2D" unique_id=880082893] + +[node name="SfxWalk" type="AudioStreamPlayer2D" parent="." unique_id=568890407] +stream = SubResource("AudioStreamRandomizer_40ewq") +volume_db = -12.0 +attenuation = 8.28211 +bus = &"Sfx" + +[node name="TimerWalk" type="Timer" parent="SfxWalk" unique_id=1285633304] +wait_time = 0.3 +one_shot = true + +[node name="Area2DPickup" type="Area2D" parent="." unique_id=1858677050] +collision_layer = 0 +collision_mask = 1536 + +[node name="CollisionShape2D" type="CollisionShape2D" parent="Area2DPickup" unique_id=1519370124] +position = Vector2(0, -1) +shape = SubResource("RectangleShape2D_0818e") +debug_color = Color(0.7, 0.495943, 0.135446, 0.42) + +[node name="LabelPlayerName" type="Label" parent="." unique_id=900440685] +z_index = 18 +z_as_relative = false +offset_left = -29.82 +offset_top = -26.39 +offset_right = 30.18 +offset_bottom = -20.39 +size_flags_horizontal = 3 +size_flags_vertical = 6 +theme_override_constants/outline_size = 6 +theme_override_fonts/font = ExtResource("21_pyh4g") +theme_override_font_sizes/font_size = 6 +text = "Playername" +horizontal_alignment = 1 + +[node name="LabelCurrentAnimation" type="Label" parent="." unique_id=2024783119] +visible = false +z_index = 18 +z_as_relative = false +offset_left = -29.82 +offset_top = -33.945 +offset_right = 30.18 +offset_bottom = -27.945 +size_flags_horizontal = 3 +size_flags_vertical = 6 +theme_override_constants/outline_size = 6 +theme_override_fonts/font = ExtResource("21_pyh4g") +theme_override_font_sizes/font_size = 6 +text = "CurAnim" +horizontal_alignment = 1 + +[node name="CanvasLayer" type="CanvasLayer" parent="." unique_id=1694102436] +follow_viewport_enabled = true + +[node name="TextureProgressBarHealth" type="TextureProgressBar" parent="." unique_id=1783325028] +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_left = -8.0 +offset_top = -16.0 +offset_right = 8.0 +offset_bottom = -15.0 +grow_horizontal = 2 +grow_vertical = 2 +value = 100.0 +texture_under = SubResource("GradientTexture1D_pyh4g") +texture_progress = SubResource("GradientTexture1D_fj670") + +[node name="SfxDie" type="AudioStreamPlayer2D" parent="." unique_id=1749167232] +stream = ExtResource("22_jfw4q") +bus = &"Sfx" + +[node name="SfxTakeDamage" type="AudioStreamPlayer2D" parent="." unique_id=956824742] +stream = SubResource("AudioStreamRandomizer_hnhes") +bus = &"Sfx" + +[node name="TimerGrab" type="Timer" parent="." unique_id=129649929] +wait_time = 0.2 +one_shot = true + +[connection signal="timeout" from="Camera2D/Timer" to="Camera2D" method="_on_timer_timeout"]