added more spell effects, fixed bomb effects, allow to pickup bomb...
This commit is contained in:
@@ -73,8 +73,10 @@ var bow_charge_percentage: float = 1.0 # 0.5, 0.75, or 1.0 based on charge time
|
||||
var is_charging_spell: bool = false # True when holding grab with spellbook
|
||||
var spell_charge_start_time: float = 0.0
|
||||
var spell_charge_duration: float = 1.0 # Time to fully charge spell (1 second)
|
||||
var use_spell_charge_particles: bool = false # If true, use red_star particles; if false, use AnimationIncantation
|
||||
var spell_charge_particles: Node2D = null # Particle system for charging
|
||||
var spell_charge_particle_timer: float = 0.0 # Timer for spawning particles
|
||||
var spell_incantation_fire_ready_shown: bool = false # Track when we've switched to fire_ready
|
||||
var spell_charge_tint: Color = Color(2.0, 0.2, 0.2, 2.0) # Tint color when fully charged
|
||||
var spell_charge_tint_pulse_time: float = 0.0 # Timer for pulsing tint animation
|
||||
var spell_charge_tint_pulse_speed: float = 8.0 # Speed of tint pulse animation while charging
|
||||
@@ -92,6 +94,10 @@ var burn_damage_timer: float = 0.0 # Timer for burn damage ticks
|
||||
var movement_lock_timer: float = 0.0 # Lock movement when bow is released
|
||||
var direction_lock_timer: float = 0.0 # Lock facing direction when attacking
|
||||
var locked_facing_direction: Vector2 = Vector2.ZERO # Locked facing direction during attack
|
||||
var damage_direction_lock_timer: float = 0.0 # Lock facing when taking damage (enemies/players)
|
||||
var damage_direction_lock_duration: float = 0.3 # Duration to block direction change after damage
|
||||
var empty_bow_shot_attempts: int = 0
|
||||
var _arrow_spawn_seq: int = 0 # Per-player sequence for stable arrow names (multiplayer sync)
|
||||
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
|
||||
@@ -1493,6 +1499,10 @@ func _get_direction_from_vector(vec: Vector2) -> int:
|
||||
|
||||
# Update facing direction from mouse position (called by GameWorld)
|
||||
func _update_facing_from_mouse(mouse_direction: Vector2):
|
||||
# Don't update facing when dead
|
||||
if is_dead:
|
||||
return
|
||||
|
||||
# Only update if using keyboard input (not gamepad)
|
||||
if input_device != -1:
|
||||
return
|
||||
@@ -1505,6 +1515,10 @@ func _update_facing_from_mouse(mouse_direction: Vector2):
|
||||
if direction_lock_timer > 0.0:
|
||||
return
|
||||
|
||||
# Don't update if direction is locked (taking damage from enemies/players)
|
||||
if damage_direction_lock_timer > 0.0:
|
||||
return
|
||||
|
||||
# Mark that mouse control is active (prevents movement keys from overriding attack direction)
|
||||
mouse_control_active = true
|
||||
|
||||
@@ -1739,6 +1753,12 @@ func _physics_process(delta):
|
||||
if direction_lock_timer <= 0.0:
|
||||
direction_lock_timer = 0.0
|
||||
|
||||
# Update damage direction lock timer (block facing change when taking damage)
|
||||
if damage_direction_lock_timer > 0.0:
|
||||
damage_direction_lock_timer -= delta
|
||||
if damage_direction_lock_timer <= 0.0:
|
||||
damage_direction_lock_timer = 0.0
|
||||
|
||||
# Update spell charging
|
||||
if is_charging_spell:
|
||||
var charge_time = (Time.get_ticks_msec() / 1000.0) - spell_charge_start_time
|
||||
@@ -1747,6 +1767,7 @@ func _physics_process(delta):
|
||||
# Update particles (spawn and animate)
|
||||
spell_charge_particle_timer += delta
|
||||
_update_spell_charge_particles(charge_progress)
|
||||
_update_spell_charge_incantation(charge_progress)
|
||||
|
||||
# Update tint pulse timer when fully charged
|
||||
if charge_progress >= 1.0:
|
||||
@@ -2292,6 +2313,7 @@ func _handle_interactions():
|
||||
spell_charge_start_time = Time.get_ticks_msec() / 1000.0
|
||||
spell_incantation_played = false # Reset flag when starting new charge
|
||||
_start_spell_charge_particles()
|
||||
_start_spell_charge_incantation()
|
||||
|
||||
# Play spell charging sound (incantation plays when fully charged)
|
||||
if has_node("SfxSpellCharge"):
|
||||
@@ -2314,6 +2336,7 @@ func _handle_interactions():
|
||||
is_charging_spell = false
|
||||
spell_incantation_played = false
|
||||
_stop_spell_charge_particles()
|
||||
_stop_spell_charge_incantation()
|
||||
_clear_spell_charge_tint()
|
||||
|
||||
# Return to IDLE animation
|
||||
@@ -2351,6 +2374,7 @@ func _handle_interactions():
|
||||
is_charging_spell = false
|
||||
spell_incantation_played = false
|
||||
_stop_spell_charge_particles()
|
||||
_stop_spell_charge_incantation()
|
||||
_clear_spell_charge_tint() # This will restore original tints
|
||||
|
||||
# Stop spell charging sound, but let incantation play to completion
|
||||
@@ -2369,6 +2393,7 @@ func _handle_interactions():
|
||||
is_charging_spell = false
|
||||
spell_incantation_played = false
|
||||
_stop_spell_charge_particles()
|
||||
_stop_spell_charge_incantation()
|
||||
_clear_spell_charge_tint() # This will restore original tints
|
||||
|
||||
# Return to IDLE animation
|
||||
@@ -2393,6 +2418,7 @@ func _handle_interactions():
|
||||
is_charging_spell = false
|
||||
spell_incantation_played = false
|
||||
_stop_spell_charge_particles()
|
||||
_stop_spell_charge_incantation()
|
||||
_clear_spell_charge_tint()
|
||||
|
||||
# Return to IDLE animation
|
||||
@@ -2788,6 +2814,12 @@ func _try_grab():
|
||||
closest_body = body
|
||||
|
||||
if closest_body:
|
||||
# Placed bomb (attack_bomb with fuse): collect to inventory, don't lift
|
||||
if "is_fused" in closest_body and "can_be_collected" in closest_body and "player_owner" in closest_body:
|
||||
if closest_body.has_method("can_be_grabbed") and closest_body.can_be_grabbed() and closest_body.has_method("on_grabbed"):
|
||||
closest_body.on_grabbed(self)
|
||||
return
|
||||
|
||||
held_object = closest_body
|
||||
# Store the initial positions - don't change the grabbed object's position yet!
|
||||
initial_grab_position = closest_body.global_position
|
||||
@@ -3363,9 +3395,31 @@ func _place_down_object():
|
||||
if not held_object:
|
||||
return
|
||||
|
||||
# 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
|
||||
var place_pos = _find_closest_place_pos(facing_direction_vector, placed_obj)
|
||||
|
||||
# Dwarf dropping bomb: place attack_bomb with fuse lit (explodes if not picked up in time)
|
||||
if "object_type" in placed_obj and placed_obj.object_type == "Bomb":
|
||||
if not _can_place_down_at(place_pos, placed_obj):
|
||||
return
|
||||
var bomb_name = placed_obj.name
|
||||
held_object = null
|
||||
grab_offset = Vector2.ZERO
|
||||
is_lifting = false
|
||||
is_pushing = false
|
||||
placed_obj.queue_free()
|
||||
if not attack_bomb_scene:
|
||||
return
|
||||
var bomb = attack_bomb_scene.instantiate()
|
||||
get_parent().add_child(bomb)
|
||||
bomb.global_position = place_pos
|
||||
bomb.setup(place_pos, self, Vector2.ZERO, false) # not thrown, fuse starts in _deferred_ready
|
||||
if multiplayer.has_multiplayer_peer():
|
||||
bomb.set_multiplayer_authority(get_multiplayer_authority())
|
||||
if multiplayer.has_multiplayer_peer() and is_multiplayer_authority() and can_send_rpcs and is_inside_tree():
|
||||
_rpc_to_ready_peers("_sync_bomb_dropped", [bomb_name, place_pos])
|
||||
print(name, " dropped bomb at ", place_pos, " (fuse lit)")
|
||||
return
|
||||
|
||||
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), ")")
|
||||
|
||||
@@ -3484,6 +3538,10 @@ func _perform_attack():
|
||||
# Round to 1 decimal place
|
||||
final_damage = round(final_damage * 10.0) / 10.0
|
||||
|
||||
# Track what we spawned so we only sync when we actually shot a projectile
|
||||
var spawned_projectile_type: String = ""
|
||||
var sync_arrow_name: String = "" # Stable name for arrow sync (multiplayer)
|
||||
|
||||
# Handle bow attacks - require arrows in off-hand
|
||||
if is_bow:
|
||||
# Check for arrows in off-hand
|
||||
@@ -3493,12 +3551,19 @@ func _perform_attack():
|
||||
if offhand_item and offhand_item.weapon_type == Item.WeaponType.AMMUNITION:
|
||||
arrows = offhand_item
|
||||
|
||||
# Reset empty-bow counter when we have arrows
|
||||
if arrows and arrows.quantity > 0:
|
||||
empty_bow_shot_attempts = 0
|
||||
|
||||
# Only spawn arrow if we have arrows
|
||||
if arrows and arrows.quantity > 0:
|
||||
if attack_arrow_scene:
|
||||
|
||||
spawned_projectile_type = "arrow"
|
||||
$SfxBowShoot.play()
|
||||
var arrow_projectile = attack_arrow_scene.instantiate()
|
||||
sync_arrow_name = "arrow_%d_%d" % [multiplayer.get_unique_id(), _arrow_spawn_seq]
|
||||
_arrow_spawn_seq += 1
|
||||
arrow_projectile.name = sync_arrow_name
|
||||
get_parent().add_child(arrow_projectile)
|
||||
# Spawn arrow 4 pixels in the direction player is looking
|
||||
var arrow_spawn_pos = global_position + (attack_direction * 4.0)
|
||||
@@ -3521,14 +3586,20 @@ func _perform_attack():
|
||||
character_stats.character_changed.emit(character_stats)
|
||||
print(name, " shot arrow! Arrows remaining: ", remaining)
|
||||
else:
|
||||
# Play bow animation but no projectile
|
||||
# Play sound for trying to shoot without arrows
|
||||
# Play bow animation but no projectile - DO NOT sync attack (no arrow spawned)
|
||||
if has_node("SfxBowWithoutArrow"):
|
||||
$SfxBowWithoutArrow.play()
|
||||
print(name, " tried to shoot but has no arrows!")
|
||||
|
||||
# Track empty bow attempts; after 3, unequip bow and equip another weapon
|
||||
empty_bow_shot_attempts += 1
|
||||
if empty_bow_shot_attempts >= 3:
|
||||
empty_bow_shot_attempts = 0
|
||||
_unequip_bow_and_equip_other_weapon()
|
||||
elif is_staff:
|
||||
# Spawn staff projectile for staff weapons
|
||||
if staff_projectile_scene and equipped_weapon:
|
||||
spawned_projectile_type = "staff"
|
||||
var projectile = staff_projectile_scene.instantiate()
|
||||
get_parent().add_child(projectile)
|
||||
projectile.setup(attack_direction, self, final_damage, equipped_weapon)
|
||||
@@ -3542,6 +3613,7 @@ func _perform_attack():
|
||||
else:
|
||||
# Spawn sword projectile for non-bow/staff weapons
|
||||
if sword_projectile_scene:
|
||||
spawned_projectile_type = "sword"
|
||||
var projectile = sword_projectile_scene.instantiate()
|
||||
get_parent().add_child(projectile)
|
||||
projectile.setup(attack_direction, self, final_damage)
|
||||
@@ -3553,9 +3625,10 @@ func _perform_attack():
|
||||
projectile.global_position = global_position + spawn_offset
|
||||
print(name, " attacked with sword projectile! Damage: ", final_damage, " (base: ", base_damage, ", crit: ", is_crit, ")")
|
||||
|
||||
# Sync attack over network
|
||||
if multiplayer.has_multiplayer_peer() and is_multiplayer_authority() and can_send_rpcs and is_inside_tree():
|
||||
_rpc_to_ready_peers("_sync_attack", [current_direction, attack_direction, bow_charge_percentage])
|
||||
# Sync attack over network only when we actually spawned a projectile
|
||||
if spawned_projectile_type != "" and multiplayer.has_multiplayer_peer() and is_multiplayer_authority() and can_send_rpcs and is_inside_tree():
|
||||
var arrow_name_arg = sync_arrow_name if spawned_projectile_type == "arrow" else ""
|
||||
_rpc_to_ready_peers("_sync_attack", [current_direction, attack_direction, bow_charge_percentage, spawned_projectile_type, arrow_name_arg])
|
||||
|
||||
# Reset attack cooldown (instant if cooldown is 0)
|
||||
if attack_cooldown > 0:
|
||||
@@ -3564,6 +3637,64 @@ func _perform_attack():
|
||||
can_attack = true
|
||||
is_attacking = false
|
||||
|
||||
func _unequip_bow_and_equip_other_weapon():
|
||||
# After 3 empty bow shots: unequip bow, equip another mainhand weapon if any, show messages
|
||||
if not is_multiplayer_authority() or not character_stats:
|
||||
return
|
||||
|
||||
var mainhand = character_stats.equipment.get("mainhand", null)
|
||||
if not mainhand or mainhand.weapon_type != Item.WeaponType.BOW:
|
||||
return
|
||||
|
||||
# Unequip bow (moves it to inventory)
|
||||
character_stats.unequip_item(mainhand, true)
|
||||
|
||||
# Show "Bow unequipped" message
|
||||
_show_equipment_message("Bow unequipped.")
|
||||
|
||||
# Find first mainhand weapon in inventory that is not a bow
|
||||
var other_weapon = null
|
||||
for i in range(character_stats.inventory.size()):
|
||||
var it = character_stats.inventory[i]
|
||||
if not it:
|
||||
continue
|
||||
if it.equipment_type != Item.EquipmentType.MAINHAND:
|
||||
continue
|
||||
if it.weapon_type == Item.WeaponType.BOW:
|
||||
continue
|
||||
other_weapon = it
|
||||
break
|
||||
|
||||
if other_weapon:
|
||||
character_stats.equip_item(other_weapon, -1)
|
||||
_show_equipment_message("%s equipped." % other_weapon.item_name)
|
||||
|
||||
# Sync equipment/inventory to other clients (same as _on_character_changed)
|
||||
if multiplayer.has_multiplayer_peer() and is_multiplayer_authority() and can_send_rpcs and is_inside_tree():
|
||||
var equipment_data = {}
|
||||
for slot_name in character_stats.equipment.keys():
|
||||
var item = character_stats.equipment[slot_name]
|
||||
if item:
|
||||
equipment_data[slot_name] = item.save()
|
||||
else:
|
||||
equipment_data[slot_name] = null
|
||||
_rpc_to_ready_peers("_sync_equipment", [equipment_data])
|
||||
var inventory_data = []
|
||||
for item in character_stats.inventory:
|
||||
if item:
|
||||
inventory_data.append(item.save())
|
||||
_rpc_to_ready_peers("_sync_inventory", [inventory_data])
|
||||
|
||||
_apply_appearance_to_sprites()
|
||||
var other_name = other_weapon.item_name if other_weapon else "none"
|
||||
print(name, " unequipped bow (no arrows x3); other weapon: ", other_name)
|
||||
|
||||
func _show_equipment_message(text: String):
|
||||
# Local-only so the player who unequipped always sees it (host or client)
|
||||
var chat_ui = get_tree().get_first_node_in_group("chat_ui")
|
||||
if chat_ui and chat_ui.has_method("add_local_message"):
|
||||
chat_ui.add_local_message("System", text)
|
||||
|
||||
func _create_bomb_object():
|
||||
# Dwarf: Create interactable bomb object that can be lifted/thrown
|
||||
if not is_multiplayer_authority():
|
||||
@@ -3629,9 +3760,13 @@ func _create_bomb_object():
|
||||
bomb_obj.on_grabbed(self)
|
||||
|
||||
# Immediately lift the bomb (Dwarf lifts it directly)
|
||||
# Set is_lifting BEFORE calling _lift_object to prevent it from being reset
|
||||
is_lifting = true
|
||||
is_pushing = false
|
||||
push_axis = _snap_to_8_directions(facing_direction_vector) if facing_direction_vector.length() > 0.1 else Vector2.DOWN
|
||||
if "is_being_held" in bomb_obj:
|
||||
bomb_obj.is_being_held = true
|
||||
if "held_by_player" in bomb_obj:
|
||||
bomb_obj.held_by_player = self
|
||||
|
||||
# Freeze the bomb
|
||||
if "is_frozen" in bomb_obj:
|
||||
@@ -3644,19 +3779,14 @@ func _create_bomb_object():
|
||||
# Play lift animation
|
||||
_set_animation("LIFT")
|
||||
|
||||
# Sync to network
|
||||
# Sync bomb spawn to other clients so they see it when lifted, then sync grab state
|
||||
if multiplayer.has_multiplayer_peer() and is_multiplayer_authority() and can_send_rpcs and is_inside_tree():
|
||||
var obj_name = _get_object_name_for_sync(bomb_obj)
|
||||
var obj_name = bomb_obj.name
|
||||
if obj_name != "" and is_instance_valid(bomb_obj) and bomb_obj.is_inside_tree():
|
||||
_rpc_to_ready_peers("_sync_create_bomb_object", [obj_name, bomb_obj.global_position])
|
||||
_rpc_to_ready_peers("_sync_grab", [obj_name, true, push_axis]) # true = lifting
|
||||
|
||||
print(name, " created bomb object! Remaining bombs: ", remaining)
|
||||
|
||||
# Sync grab to network
|
||||
if multiplayer.has_multiplayer_peer() and can_send_rpcs and is_inside_tree():
|
||||
var obj_name = _get_object_name_for_sync(bomb_obj)
|
||||
if obj_name != "":
|
||||
_rpc_to_ready_peers("_sync_grab", [obj_name, true, push_axis]) # true = lifting
|
||||
|
||||
func _throw_bomb(_target_position: Vector2):
|
||||
# Legacy function - kept for Human/Elf if needed, but Dwarf uses _create_bomb_object now
|
||||
@@ -3787,10 +3917,9 @@ func _can_cast_spell_at(target_position: Vector2) -> bool:
|
||||
if tile_x < 0 or tile_y < 0 or tile_x >= map_size.x or tile_y >= map_size.y:
|
||||
return false
|
||||
|
||||
# Check if it's a floor tile (grid value 1) or corridor (grid value 3)
|
||||
# Allow casting on both floor and corridor tiles
|
||||
# Check if it's floor (1), door (2), or corridor (3) - same as walkable
|
||||
var grid_value = grid[tile_x][tile_y]
|
||||
if grid_value != 1 and grid_value != 3:
|
||||
if grid_value != 1 and grid_value != 2 and grid_value != 3:
|
||||
return false
|
||||
|
||||
# Check if there's a wall between player and target using raycast
|
||||
@@ -3811,7 +3940,9 @@ func _can_cast_spell_at(target_position: Vector2) -> bool:
|
||||
return true
|
||||
|
||||
func _start_spell_charge_particles():
|
||||
# Create particle system for spell charging
|
||||
# Create particle system for spell charging (only if enabled)
|
||||
if not use_spell_charge_particles:
|
||||
return
|
||||
if spell_charge_particles:
|
||||
_stop_spell_charge_particles()
|
||||
|
||||
@@ -3821,8 +3952,8 @@ func _start_spell_charge_particles():
|
||||
spell_charge_particle_timer = 0.0
|
||||
|
||||
func _update_spell_charge_particles(charge_progress: float):
|
||||
# Update particle system based on charge progress
|
||||
if not spell_charge_particles or not is_instance_valid(spell_charge_particles):
|
||||
# Update particle system based on charge progress (skip if disabled)
|
||||
if not use_spell_charge_particles or not spell_charge_particles or not is_instance_valid(spell_charge_particles):
|
||||
return
|
||||
|
||||
var star_texture = load("res://assets/gfx/fx/magic/red_star.png")
|
||||
@@ -3892,6 +4023,26 @@ func _stop_spell_charge_particles():
|
||||
spell_charge_particles.queue_free()
|
||||
spell_charge_particles = null
|
||||
|
||||
func _start_spell_charge_incantation():
|
||||
# Play fire_charging on AnimationIncantation when starting spell charge
|
||||
spell_incantation_fire_ready_shown = false
|
||||
if has_node("AnimationIncantation"):
|
||||
$AnimationIncantation.play("fire_charging")
|
||||
|
||||
func _update_spell_charge_incantation(charge_progress: float):
|
||||
# Switch to fire_ready when fully charged (fire_charging already playing from start)
|
||||
if not has_node("AnimationIncantation"):
|
||||
return
|
||||
if charge_progress >= 1.0 and not spell_incantation_fire_ready_shown:
|
||||
$AnimationIncantation.play("fire_ready")
|
||||
spell_incantation_fire_ready_shown = true
|
||||
|
||||
func _stop_spell_charge_incantation():
|
||||
# Reset incantation when spell charge ends
|
||||
spell_incantation_fire_ready_shown = false
|
||||
if has_node("AnimationIncantation"):
|
||||
$AnimationIncantation.play("idle")
|
||||
|
||||
func _apply_spell_charge_tint():
|
||||
# Apply pulsing tint to all sprite layers when fully charged using shader parameters
|
||||
# Pulse between original tint and spell charge tint
|
||||
@@ -4129,6 +4280,7 @@ func _sync_spell_charge_start():
|
||||
is_charging_spell = true
|
||||
spell_charge_start_time = Time.get_ticks_msec() / 1000.0
|
||||
_start_spell_charge_particles()
|
||||
_start_spell_charge_incantation()
|
||||
print(name, " (synced) started charging spell")
|
||||
|
||||
@rpc("any_peer", "reliable")
|
||||
@@ -4138,6 +4290,7 @@ func _sync_spell_charge_end():
|
||||
is_charging_spell = false
|
||||
spell_incantation_played = false
|
||||
_stop_spell_charge_particles()
|
||||
_stop_spell_charge_incantation()
|
||||
_clear_spell_charge_tint()
|
||||
|
||||
# Return to IDLE animation
|
||||
@@ -4486,76 +4639,51 @@ 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, charge_percentage: float = 1.0):
|
||||
# Sync attack to other clients
|
||||
# Check if node still exists and is valid before processing
|
||||
func _sync_attack(direction: int, attack_dir: Vector2, charge_percentage: float = 1.0, projectile_type: String = "sword", arrow_name: String = ""):
|
||||
# Sync attack to other clients. Use projectile_type from authority (what was actually spawned),
|
||||
# not equipment - fixes no-arrows and post-unequip bow desync.
|
||||
if not is_inside_tree() or not is_instance_valid(self):
|
||||
return
|
||||
|
||||
if not is_multiplayer_authority():
|
||||
current_direction = direction as Direction
|
||||
|
||||
# Determine weapon type for animation and projectile
|
||||
var equipped_weapon = null
|
||||
var is_staff = false
|
||||
var is_bow = false
|
||||
if character_stats and character_stats.equipment.has("mainhand"):
|
||||
equipped_weapon = character_stats.equipment["mainhand"]
|
||||
if equipped_weapon:
|
||||
if equipped_weapon.weapon_type == Item.WeaponType.STAFF:
|
||||
is_staff = true
|
||||
elif equipped_weapon.weapon_type == Item.WeaponType.BOW:
|
||||
is_bow = true
|
||||
|
||||
# Set appropriate animation
|
||||
if is_staff:
|
||||
_set_animation("STAFF")
|
||||
elif is_bow:
|
||||
_set_animation("BOW")
|
||||
else:
|
||||
_set_animation("SWORD")
|
||||
# Set animation from projectile_type (authority knows what they shot)
|
||||
match projectile_type:
|
||||
"staff":
|
||||
_set_animation("STAFF")
|
||||
"arrow":
|
||||
_set_animation("BOW")
|
||||
_:
|
||||
_set_animation("SWORD")
|
||||
|
||||
# Delay before spawning projectile
|
||||
await get_tree().create_timer(0.15).timeout
|
||||
|
||||
# Check again after delay - node might have been destroyed
|
||||
if not is_inside_tree() or not is_instance_valid(self):
|
||||
return
|
||||
|
||||
# Spawn appropriate projectile on client
|
||||
if is_staff and staff_projectile_scene and equipped_weapon:
|
||||
# Spawn only what authority actually spawned (ignore equipment)
|
||||
if projectile_type == "staff" and staff_projectile_scene:
|
||||
var equipped_weapon = character_stats.equipment.get("mainhand", null) if character_stats else null
|
||||
var projectile = staff_projectile_scene.instantiate()
|
||||
get_parent().add_child(projectile)
|
||||
projectile.setup(attack_dir, self, 20.0, equipped_weapon)
|
||||
# Spawn projectile a bit in front of the player
|
||||
var spawn_offset = attack_dir * 10.0 # 10 pixels in front
|
||||
projectile.setup(attack_dir, self, 20.0, equipped_weapon if equipped_weapon else null)
|
||||
var spawn_offset = attack_dir * 10.0
|
||||
projectile.global_position = global_position + spawn_offset
|
||||
print(name, " performed synced staff attack!")
|
||||
elif is_bow:
|
||||
# For bow attacks, check if we have arrows (same logic as host)
|
||||
var arrows = null
|
||||
if character_stats and character_stats.equipment.has("offhand"):
|
||||
var offhand_item = character_stats.equipment["offhand"]
|
||||
if offhand_item and offhand_item.weapon_type == Item.WeaponType.AMMUNITION:
|
||||
arrows = offhand_item
|
||||
|
||||
# Only spawn arrow if we have arrows (matches host behavior)
|
||||
if arrows and arrows.quantity > 0:
|
||||
if attack_arrow_scene:
|
||||
var arrow_projectile = attack_arrow_scene.instantiate()
|
||||
get_parent().add_child(arrow_projectile)
|
||||
# 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)")
|
||||
elif sword_projectile_scene:
|
||||
elif projectile_type == "arrow" and attack_arrow_scene:
|
||||
var arrow_projectile = attack_arrow_scene.instantiate()
|
||||
if arrow_name != "":
|
||||
arrow_projectile.name = arrow_name
|
||||
get_parent().add_child(arrow_projectile)
|
||||
arrow_projectile.shoot(attack_dir, global_position, self, charge_percentage)
|
||||
print(name, " performed synced bow attack with arrow (charge: ", charge_percentage * 100, "%)")
|
||||
elif (projectile_type == "sword" or projectile_type == "") and sword_projectile_scene:
|
||||
var projectile = sword_projectile_scene.instantiate()
|
||||
get_parent().add_child(projectile)
|
||||
projectile.setup(attack_dir, self)
|
||||
# Spawn projectile a bit in front of the player
|
||||
var spawn_offset = attack_dir * 10.0 # 10 pixels in front
|
||||
var spawn_offset = attack_dir * 10.0
|
||||
projectile.global_position = global_position + spawn_offset
|
||||
print(name, " performed synced attack!")
|
||||
|
||||
@@ -4575,9 +4703,54 @@ func _sync_bow_charge_end():
|
||||
_clear_bow_charge_tint()
|
||||
print(name, " (synced) ended charging bow")
|
||||
|
||||
@rpc("any_peer", "reliable")
|
||||
func _sync_create_bomb_object(bomb_name: String, spawn_pos: Vector2):
|
||||
# Sync Dwarf's lifted bomb spawn to other clients so they see it when held
|
||||
if is_multiplayer_authority():
|
||||
return
|
||||
var game_world = get_tree().get_first_node_in_group("game_world")
|
||||
if not game_world:
|
||||
return
|
||||
var entities_node = game_world.get_node_or_null("Entities")
|
||||
if not entities_node:
|
||||
return
|
||||
if entities_node.get_node_or_null(bomb_name):
|
||||
return # Already exists (e.g. duplicate RPC)
|
||||
var interactable_scene = load("res://scenes/interactable_object.tscn") as PackedScene
|
||||
if not interactable_scene:
|
||||
return
|
||||
var bomb_obj = interactable_scene.instantiate()
|
||||
bomb_obj.name = bomb_name
|
||||
bomb_obj.global_position = spawn_pos
|
||||
if multiplayer.has_multiplayer_peer():
|
||||
bomb_obj.set_multiplayer_authority(get_multiplayer_authority())
|
||||
entities_node.add_child(bomb_obj)
|
||||
bomb_obj.setup_bomb()
|
||||
print(name, " (synced) created bomb object ", bomb_name, " at ", spawn_pos)
|
||||
|
||||
@rpc("any_peer", "reliable")
|
||||
func _sync_bomb_dropped(bomb_name: String, place_pos: Vector2):
|
||||
# Sync Dwarf drop: free lifted bomb on clients, spawn attack_bomb with fuse lit
|
||||
if not is_multiplayer_authority():
|
||||
var game_world = get_tree().get_first_node_in_group("game_world")
|
||||
var entities_node = game_world.get_node_or_null("Entities") if game_world else null
|
||||
if entities_node and bomb_name.begins_with("BombObject_"):
|
||||
var lifted = entities_node.get_node_or_null(bomb_name)
|
||||
if lifted and is_instance_valid(lifted):
|
||||
lifted.queue_free()
|
||||
if not attack_bomb_scene:
|
||||
return
|
||||
var bomb = attack_bomb_scene.instantiate()
|
||||
get_parent().add_child(bomb)
|
||||
bomb.global_position = place_pos
|
||||
bomb.setup(place_pos, self, Vector2.ZERO, false) # not thrown, fuse lit
|
||||
if multiplayer.has_multiplayer_peer():
|
||||
bomb.set_multiplayer_authority(get_multiplayer_authority())
|
||||
print(name, " (synced) dropped bomb at ", place_pos)
|
||||
|
||||
@rpc("any_peer", "reliable")
|
||||
func _sync_place_bomb(target_pos: Vector2):
|
||||
# Sync bomb placement to other clients
|
||||
# Sync bomb placement to other clients (Human/Elf)
|
||||
if not is_multiplayer_authority():
|
||||
if not attack_bomb_scene:
|
||||
return
|
||||
@@ -4593,24 +4766,23 @@ func _sync_place_bomb(target_pos: Vector2):
|
||||
print(name, " (synced) placed bomb at ", target_pos)
|
||||
|
||||
@rpc("any_peer", "reliable")
|
||||
func _sync_throw_bomb(bomb_pos: Vector2, throw_force: Vector2):
|
||||
# Sync bomb throw to other clients
|
||||
func _sync_throw_bomb(bomb_name: String, bomb_pos: Vector2, throw_force: Vector2):
|
||||
# Sync bomb throw to other clients; free lifted bomb (BombObject_*) if it exists
|
||||
if not is_multiplayer_authority():
|
||||
var game_world = get_tree().get_first_node_in_group("game_world")
|
||||
var entities_node = game_world.get_node_or_null("Entities") if game_world else null
|
||||
if entities_node and bomb_name.begins_with("BombObject_"):
|
||||
var lifted = entities_node.get_node_or_null(bomb_name)
|
||||
if lifted and is_instance_valid(lifted):
|
||||
lifted.queue_free()
|
||||
if not attack_bomb_scene:
|
||||
return
|
||||
|
||||
# Spawn bomb projectile at position
|
||||
var bomb = attack_bomb_scene.instantiate()
|
||||
get_parent().add_child(bomb)
|
||||
bomb.global_position = bomb_pos
|
||||
|
||||
# Setup bomb with throw physics
|
||||
bomb.setup(bomb_pos, self, throw_force, true) # true = is_thrown
|
||||
|
||||
# Make sure bomb sprite is visible
|
||||
if bomb.has_node("Sprite2D"):
|
||||
bomb.get_node("Sprite2D").visible = true
|
||||
|
||||
print(name, " (synced) threw bomb from ", bomb_pos)
|
||||
|
||||
@rpc("any_peer", "reliable")
|
||||
@@ -5153,6 +5325,7 @@ func take_damage(amount: float, attacker_position: Vector2, is_burn_damage: bool
|
||||
is_charging_spell = false
|
||||
spell_incantation_played = false
|
||||
_stop_spell_charge_particles()
|
||||
_stop_spell_charge_incantation()
|
||||
_clear_spell_charge_tint()
|
||||
|
||||
# Return to IDLE animation
|
||||
@@ -5223,6 +5396,9 @@ func take_damage(amount: float, attacker_position: Vector2, is_burn_damage: bool
|
||||
# Play damage animation
|
||||
_set_animation("DAMAGE")
|
||||
|
||||
# Lock facing direction briefly so player can't change it while taking damage
|
||||
damage_direction_lock_timer = damage_direction_lock_duration
|
||||
|
||||
# Only apply knockback if not burn damage
|
||||
if not is_burn_damage:
|
||||
# Calculate direction FROM attacker TO victim
|
||||
@@ -5280,6 +5456,7 @@ func _die():
|
||||
is_dead = true # Ensure flag is set
|
||||
velocity = Vector2.ZERO
|
||||
is_knocked_back = false
|
||||
damage_direction_lock_timer = 0.0
|
||||
|
||||
# CRITICAL: Release any held object/player BEFORE dying to restore their collision layers
|
||||
if held_object:
|
||||
@@ -5417,6 +5594,7 @@ func _respawn():
|
||||
velocity = Vector2.ZERO
|
||||
is_knocked_back = false
|
||||
is_airborne = false
|
||||
damage_direction_lock_timer = 0.0
|
||||
position_z = 0.0
|
||||
velocity_z = 0.0
|
||||
|
||||
|
||||
Reference in New Issue
Block a user