tried optimizing the game
This commit is contained in:
@@ -128,6 +128,16 @@ var attack_bomb_scene = preload("res://scenes/attack_bomb.tscn") # Bomb projecti
|
||||
var attack_punch_scene = preload("res://scenes/attack_punch.tscn") # Unarmed punch
|
||||
var attack_axe_swing_scene = preload("res://scenes/attack_axe_swing.tscn") # Axe swing (orbits)
|
||||
var blood_scene = preload("res://scenes/blood_clot.tscn")
|
||||
# Preload for _create_bomb_object so placing a bomb doesn't spike
|
||||
const _INTERACTABLE_OBJECT_SCENE: PackedScene = preload("res://scenes/interactable_object.tscn")
|
||||
|
||||
# Cache appearance texture paths so _apply_appearance_to_sprites() doesn't load() every time (avoids ~29ms spike on equipment change)
|
||||
const _APPEARANCE_TEXTURE_CACHE_MAX: int = 48
|
||||
var _appearance_texture_cache: Dictionary = {}
|
||||
var _appearance_texture_cache_order: Array = [] # FIFO keys for eviction
|
||||
|
||||
# Lazy cache for spell SFX (avoids has_node + $ every frame in _physics_process)
|
||||
var _sfx_spell_incantation: Node = null
|
||||
|
||||
# Simulated Z-axis for height (when thrown)
|
||||
var position_z: float = 0.0
|
||||
@@ -1136,14 +1146,35 @@ func _setup_player_appearance_preserve_race():
|
||||
|
||||
print("Player ", name, " appearance re-initialized (preserved race=", selected_race, ")")
|
||||
|
||||
func _get_appearance_texture(path: String) -> Texture2D:
|
||||
if path.is_empty():
|
||||
return null
|
||||
# Use global preloaded cache first (avoids 13ms+ spike on equip)
|
||||
var global_cache = get_node_or_null("/root/AppearanceTextureCache")
|
||||
if global_cache and global_cache.has_method("get_texture"):
|
||||
var tex = global_cache.get_texture(path)
|
||||
if tex:
|
||||
return tex
|
||||
# Fallback: local cache for paths not in preload (e.g. future content)
|
||||
if _appearance_texture_cache.has(path):
|
||||
return _appearance_texture_cache[path] as Texture2D
|
||||
var t = load(path) as Texture2D
|
||||
if t:
|
||||
if _appearance_texture_cache_order.size() >= _APPEARANCE_TEXTURE_CACHE_MAX:
|
||||
var old_key = _appearance_texture_cache_order.pop_front()
|
||||
_appearance_texture_cache.erase(old_key)
|
||||
_appearance_texture_cache[path] = t
|
||||
_appearance_texture_cache_order.append(path)
|
||||
return t
|
||||
|
||||
func _apply_appearance_to_sprites():
|
||||
# Apply character_stats appearance to sprite layers
|
||||
# Apply character_stats appearance to sprite layers (uses texture cache to avoid load() spikes)
|
||||
if not character_stats:
|
||||
return
|
||||
|
||||
# Body/Skin
|
||||
if sprite_body and character_stats.skin != "":
|
||||
var body_texture = load(character_stats.skin)
|
||||
var body_texture = _get_appearance_texture(character_stats.skin)
|
||||
if body_texture:
|
||||
sprite_body.texture = body_texture
|
||||
sprite_body.hframes = 35
|
||||
@@ -1155,7 +1186,7 @@ func _apply_appearance_to_sprites():
|
||||
var equipped_boots = character_stats.equipment["boots"]
|
||||
# Only render boots if it's actually boots equipment (not a weapon or other type)
|
||||
if equipped_boots and equipped_boots.equipment_type == Item.EquipmentType.BOOTS and equipped_boots.equipmentPath != "":
|
||||
var boots_texture = load(equipped_boots.equipmentPath)
|
||||
var boots_texture = _get_appearance_texture(equipped_boots.equipmentPath)
|
||||
if boots_texture:
|
||||
sprite_boots.texture = boots_texture
|
||||
sprite_boots.hframes = 35
|
||||
@@ -1174,7 +1205,7 @@ func _apply_appearance_to_sprites():
|
||||
var equipped_armour = character_stats.equipment["armour"]
|
||||
# Only render armour if it's actually armour equipment (not a weapon)
|
||||
if equipped_armour and equipped_armour.equipment_type == Item.EquipmentType.ARMOUR and equipped_armour.equipmentPath != "":
|
||||
var armour_texture = load(equipped_armour.equipmentPath)
|
||||
var armour_texture = _get_appearance_texture(equipped_armour.equipmentPath)
|
||||
if armour_texture:
|
||||
sprite_armour.texture = armour_texture
|
||||
sprite_armour.hframes = 35
|
||||
@@ -1191,7 +1222,7 @@ func _apply_appearance_to_sprites():
|
||||
# Facial Hair
|
||||
if sprite_facial_hair:
|
||||
if character_stats.facial_hair != "":
|
||||
var facial_hair_texture = load(character_stats.facial_hair)
|
||||
var facial_hair_texture = _get_appearance_texture(character_stats.facial_hair)
|
||||
if facial_hair_texture:
|
||||
sprite_facial_hair.texture = facial_hair_texture
|
||||
sprite_facial_hair.hframes = 35
|
||||
@@ -1214,7 +1245,7 @@ func _apply_appearance_to_sprites():
|
||||
# Hair
|
||||
if sprite_hair:
|
||||
if character_stats.hairstyle != "":
|
||||
var hair_texture = load(character_stats.hairstyle)
|
||||
var hair_texture = _get_appearance_texture(character_stats.hairstyle)
|
||||
if hair_texture:
|
||||
sprite_hair.texture = hair_texture
|
||||
sprite_hair.hframes = 35
|
||||
@@ -1237,7 +1268,7 @@ func _apply_appearance_to_sprites():
|
||||
# Eyes
|
||||
if sprite_eyes:
|
||||
if character_stats.eyes != "":
|
||||
var eyes_texture = load(character_stats.eyes)
|
||||
var eyes_texture = _get_appearance_texture(character_stats.eyes)
|
||||
if eyes_texture:
|
||||
sprite_eyes.texture = eyes_texture
|
||||
sprite_eyes.hframes = 35
|
||||
@@ -1256,7 +1287,7 @@ func _apply_appearance_to_sprites():
|
||||
# Eyelashes
|
||||
if sprite_eyelashes:
|
||||
if character_stats.eye_lashes != "":
|
||||
var eyelash_texture = load(character_stats.eye_lashes)
|
||||
var eyelash_texture = _get_appearance_texture(character_stats.eye_lashes)
|
||||
if eyelash_texture:
|
||||
sprite_eyelashes.texture = eyelash_texture
|
||||
sprite_eyelashes.hframes = 35
|
||||
@@ -1275,7 +1306,7 @@ func _apply_appearance_to_sprites():
|
||||
# Addons (ears, etc.)
|
||||
if sprite_addons:
|
||||
if character_stats.add_on != "":
|
||||
var addon_texture = load(character_stats.add_on)
|
||||
var addon_texture = _get_appearance_texture(character_stats.add_on)
|
||||
if addon_texture:
|
||||
sprite_addons.texture = addon_texture
|
||||
sprite_addons.hframes = 35
|
||||
@@ -1289,7 +1320,7 @@ func _apply_appearance_to_sprites():
|
||||
if sprite_headgear:
|
||||
var equipped_headgear = character_stats.equipment["headgear"]
|
||||
if equipped_headgear and equipped_headgear.equipmentPath != "":
|
||||
var headgear_texture = load(equipped_headgear.equipmentPath)
|
||||
var headgear_texture = _get_appearance_texture(equipped_headgear.equipmentPath)
|
||||
if headgear_texture:
|
||||
sprite_headgear.texture = headgear_texture
|
||||
sprite_headgear.hframes = 35
|
||||
@@ -1413,8 +1444,6 @@ func _on_character_changed(_char: CharacterStats):
|
||||
_rpc_to_ready_peers("_sync_equipment", [equipment_data])
|
||||
|
||||
# ALWAYS sync race and base stats to all clients (for proper display)
|
||||
# This ensures new clients get appearance data even if they connect after initial setup
|
||||
print("Player ", name, " (authority) SENDING _sync_race_and_stats to all peers: race='", character_stats.race, "'")
|
||||
_rpc_to_ready_peers("_sync_race_and_stats", [character_stats.race, character_stats.baseStats.duplicate()])
|
||||
|
||||
# Sync full appearance data (skin, hair, eyes, colors, etc.) to remote players
|
||||
@@ -1965,6 +1994,8 @@ func _physics_process(delta):
|
||||
|
||||
# Spell charge visuals (incantation, tint pulse) - run for ALL players so others see the pulse
|
||||
if is_charging_spell:
|
||||
if _sfx_spell_incantation == null:
|
||||
_sfx_spell_incantation = get_node_or_null("SfxSpellIncantation")
|
||||
var charge_time = (Time.get_ticks_msec() / 1000.0) - spell_charge_start_time
|
||||
var charge_progress = clamp(charge_time / spell_charge_duration, 0.0, 1.0)
|
||||
spell_charge_particle_timer += delta
|
||||
@@ -1973,14 +2004,14 @@ func _physics_process(delta):
|
||||
if charge_progress >= 1.0:
|
||||
spell_charge_tint_pulse_time += delta * spell_charge_tint_pulse_speed_charged
|
||||
_apply_spell_charge_tint()
|
||||
if not spell_incantation_played and has_node("SfxSpellIncantation"):
|
||||
$SfxSpellIncantation.play()
|
||||
if not spell_incantation_played and _sfx_spell_incantation:
|
||||
_sfx_spell_incantation.play()
|
||||
spell_incantation_played = true
|
||||
else:
|
||||
spell_charge_tint_pulse_time = 0.0
|
||||
_clear_spell_charge_tint()
|
||||
if has_node("SfxSpellIncantation"):
|
||||
$SfxSpellIncantation.stop()
|
||||
if _sfx_spell_incantation:
|
||||
_sfx_spell_incantation.stop()
|
||||
spell_incantation_played = false
|
||||
else:
|
||||
spell_charge_tint_pulse_time = 0.0
|
||||
@@ -2661,11 +2692,6 @@ func _handle_interactions():
|
||||
grab_just_released = Input.is_action_just_released("grab") or (not mouse_right_pressed and was_mouse_right_pressed)
|
||||
was_mouse_right_pressed = mouse_right_pressed
|
||||
|
||||
# DEBUG: Log button states if there's a conflict
|
||||
if grab_just_pressed and grab_just_released:
|
||||
print("DEBUG: WARNING - Both grab_just_pressed and grab_just_released are true!")
|
||||
if grab_just_released and grab_button_down:
|
||||
print("DEBUG: WARNING - grab_just_released=true but grab_button_down=true!")
|
||||
else:
|
||||
# Gamepad input
|
||||
var button_currently_pressed = Input.is_joy_button_pressed(input_device, JOY_BUTTON_A)
|
||||
@@ -2677,10 +2703,11 @@ func _handle_interactions():
|
||||
else:
|
||||
grab_just_released = false
|
||||
|
||||
# Update is_shielding: hold grab with shield in offhand and nothing to grab/lift
|
||||
# One overlap query per frame; reuse for shield check and spell block below
|
||||
var nearby_grabbable_body = _get_nearby_grabbable()
|
||||
var would_shield = (not is_dead and grab_button_down and _has_shield_in_offhand() \
|
||||
and not held_object and not is_lifting and not is_pushing \
|
||||
and not _has_nearby_grabbable() and not is_disarming)
|
||||
and nearby_grabbable_body == null and not is_disarming)
|
||||
if would_shield and shield_block_cooldown_timer > 0.0:
|
||||
is_shielding = false
|
||||
if has_node("SfxDenyActivateShield"):
|
||||
@@ -2716,8 +2743,8 @@ func _handle_interactions():
|
||||
|
||||
print(name, " cancelled bow charge")
|
||||
|
||||
# Check for trap disarm FIRST (Dwarf only) - PRIORITY: disarm takes priority over spell casting
|
||||
if character_stats and character_stats.race == "Dwarf":
|
||||
# Check for trap disarm FIRST (Dwarf only) - only when grab involved to avoid get_nodes_in_group every frame
|
||||
if character_stats and character_stats.race == "Dwarf" and (grab_just_pressed or grab_just_released or grab_button_down):
|
||||
var nearby_trap = _get_nearby_disarmable_trap()
|
||||
if nearby_trap:
|
||||
# Check if we're currently disarming this trap
|
||||
@@ -2738,18 +2765,14 @@ func _handle_interactions():
|
||||
$SfxSpellIncantation.stop()
|
||||
if multiplayer.has_multiplayer_peer():
|
||||
_sync_spell_charge_end.rpc()
|
||||
print(name, " cancelled spell charge to start disarming")
|
||||
|
||||
# Start disarming
|
||||
is_disarming = true
|
||||
nearby_trap.disarming_player = self
|
||||
nearby_trap.disarm_progress = 0.0
|
||||
print(name, " (Dwarf) started disarming trap")
|
||||
elif grab_just_released and currently_disarming:
|
||||
# Cancel disarm if released early
|
||||
is_disarming = false
|
||||
nearby_trap._cancel_disarm()
|
||||
print(name, " (Dwarf) cancelled disarm")
|
||||
elif not currently_disarming:
|
||||
# Not disarming anymore - reset flag
|
||||
is_disarming = false
|
||||
@@ -2780,29 +2803,9 @@ func _handle_interactions():
|
||||
heal_target = _get_heal_target()
|
||||
|
||||
var has_valid_target = ((is_fire or is_frost) and target_pos != Vector2.ZERO) or (is_heal and heal_target != null)
|
||||
# Healing: allow charge even without target (don't disable charge when hovering enemy/wall/etc.)
|
||||
# But prefer to have a target (player or enemy) when possible
|
||||
var can_start_charge = is_heal or has_valid_target
|
||||
|
||||
# Check if there's a grabbable object nearby - prioritize grabbing over spell casting
|
||||
var nearby_grabbable = null
|
||||
if grab_area:
|
||||
var bodies = grab_area.get_overlapping_bodies()
|
||||
for body in bodies:
|
||||
if body == self:
|
||||
continue
|
||||
var is_grabbable = false
|
||||
if body.has_method("can_be_grabbed"):
|
||||
if body.can_be_grabbed():
|
||||
is_grabbable = true
|
||||
elif body.is_in_group("player") or ("peer_id" in body and body is CharacterBody2D):
|
||||
is_grabbable = true
|
||||
|
||||
if is_grabbable:
|
||||
var distance = position.distance_to(body.position)
|
||||
if distance < grab_range:
|
||||
nearby_grabbable = body
|
||||
break
|
||||
# Reuse grabbable from single query above (avoids second get_overlapping_bodies)
|
||||
var nearby_grabbable = nearby_grabbable_body
|
||||
|
||||
if grab_just_pressed and not is_charging_spell and can_start_charge and not nearby_grabbable and not is_lifting and not held_object:
|
||||
# Check if player has enough mana before starting to charge
|
||||
@@ -2816,10 +2819,8 @@ func _handle_interactions():
|
||||
has_enough_mana = character_stats.mp >= 20.0 # Heal spell cost
|
||||
|
||||
if not has_enough_mana:
|
||||
# Not enough mana - show message to local player only
|
||||
if is_local_player:
|
||||
_show_not_enough_mana_text()
|
||||
print(name, " cannot start charging spell - not enough mana")
|
||||
just_grabbed_this_frame = false
|
||||
return
|
||||
|
||||
@@ -2833,7 +2834,6 @@ func _handle_interactions():
|
||||
$SfxSpellCharge.play()
|
||||
if multiplayer.has_multiplayer_peer():
|
||||
_sync_spell_charge_start.rpc()
|
||||
print(name, " started charging spell (", current_spell_element, ")")
|
||||
just_grabbed_this_frame = false
|
||||
return
|
||||
elif grab_just_released and is_charging_spell:
|
||||
@@ -2852,7 +2852,6 @@ func _handle_interactions():
|
||||
$SfxSpellIncantation.stop()
|
||||
if multiplayer.has_multiplayer_peer():
|
||||
_sync_spell_charge_end.rpc()
|
||||
print(name, " cancelled spell (released too quickly)")
|
||||
just_grabbed_this_frame = false
|
||||
return
|
||||
|
||||
@@ -2882,8 +2881,6 @@ func _handle_interactions():
|
||||
else:
|
||||
_cast_heal_spell(heal_target)
|
||||
else:
|
||||
# Not enough mana - cancel spell
|
||||
print(name, " cannot cast spell - not enough mana")
|
||||
is_charging_spell = false
|
||||
current_spell_element = "fire"
|
||||
spell_incantation_played = false
|
||||
@@ -2909,7 +2906,6 @@ func _handle_interactions():
|
||||
if has_node("SfxSpellCharge"):
|
||||
$SfxSpellCharge.stop()
|
||||
else:
|
||||
print(name, " spell not cast (charge: ", charge_time, "s, fully: ", is_fully_charged, ", target ok: ", has_valid_target, ")")
|
||||
is_charging_spell = false
|
||||
current_spell_element = "fire"
|
||||
spell_incantation_played = false
|
||||
@@ -2923,7 +2919,6 @@ func _handle_interactions():
|
||||
$SfxSpellIncantation.stop()
|
||||
if multiplayer.has_multiplayer_peer():
|
||||
_sync_spell_charge_end.rpc()
|
||||
print(name, " released spell (", "healing" if is_heal else ("frost" if is_frost else "fire"), ", fully: ", is_fully_charged, ")")
|
||||
just_grabbed_this_frame = false
|
||||
return
|
||||
elif is_charging_spell and (is_lifting or held_object or ((not is_heal) and not has_valid_target)):
|
||||
@@ -2966,7 +2961,8 @@ func _handle_interactions():
|
||||
if body.can_be_grabbed():
|
||||
is_grabbable = true
|
||||
elif body.is_in_group("player") or ("peer_id" in body and body is CharacterBody2D):
|
||||
is_grabbable = true
|
||||
if body.get("position_z", 0.0) <= 0.0:
|
||||
is_grabbable = true
|
||||
|
||||
if is_grabbable:
|
||||
var distance = position.distance_to(body.position)
|
||||
@@ -3331,9 +3327,10 @@ func _has_shield_in_offhand() -> bool:
|
||||
var off = character_stats.equipment["offhand"]
|
||||
return off != null and "shield" in off.item_name.to_lower()
|
||||
|
||||
func _has_nearby_grabbable() -> bool:
|
||||
func _get_nearby_grabbable() -> Node:
|
||||
# Single overlap query; call once per frame and reuse result (avoids 2x get_overlapping_bodies in _handle_interactions)
|
||||
if not grab_area:
|
||||
return false
|
||||
return null
|
||||
var bodies = grab_area.get_overlapping_bodies()
|
||||
for body in bodies:
|
||||
if body == self:
|
||||
@@ -3343,10 +3340,14 @@ func _has_nearby_grabbable() -> bool:
|
||||
if body.can_be_grabbed():
|
||||
is_grabbable = true
|
||||
elif body.is_in_group("player") or ("peer_id" in body and body is CharacterBody2D):
|
||||
is_grabbable = true
|
||||
if body.get("position_z", 0.0) <= 0.0:
|
||||
is_grabbable = true
|
||||
if is_grabbable and position.distance_to(body.position) < grab_range:
|
||||
return true
|
||||
return false
|
||||
return body
|
||||
return null
|
||||
|
||||
func _has_nearby_grabbable() -> bool:
|
||||
return _get_nearby_grabbable() != null
|
||||
|
||||
func _update_shield_visibility() -> void:
|
||||
if not sprite_shield or not sprite_shield_holding:
|
||||
@@ -3382,9 +3383,10 @@ func _try_grab():
|
||||
if body.has_method("can_be_grabbed"):
|
||||
if body.can_be_grabbed():
|
||||
is_grabbable = true
|
||||
# Also allow grabbing other players
|
||||
# Also allow grabbing other players (not when they're mid-air / thrown)
|
||||
elif body.is_in_group("player") or ("peer_id" in body and body is CharacterBody2D):
|
||||
is_grabbable = true
|
||||
if body.get("position_z", 0.0) <= 0.0:
|
||||
is_grabbable = true
|
||||
|
||||
if is_grabbable:
|
||||
var distance = position.distance_to(body.position)
|
||||
@@ -3823,8 +3825,6 @@ func _throw_object():
|
||||
var obj_name = _get_object_name_for_sync(thrown_obj)
|
||||
_rpc_to_ready_peers("_sync_throw", [obj_name, throw_start_pos, throw_direction * throw_force, name])
|
||||
|
||||
print("Threw ", thrown_obj.name, " from ", throw_start_pos, " with force: ", throw_direction * throw_force)
|
||||
|
||||
func _force_throw_held_object(direction: Vector2):
|
||||
if not held_object or not is_lifting:
|
||||
return
|
||||
@@ -4059,12 +4059,9 @@ func _place_down_object():
|
||||
if placed_obj.has_method("on_released"):
|
||||
placed_obj.on_released(self)
|
||||
|
||||
# Sync place down over network
|
||||
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(placed_obj)
|
||||
_rpc_to_ready_peers("_sync_place_down", [obj_name, place_pos])
|
||||
|
||||
print("Placed down ", placed_obj.name, " at ", place_pos)
|
||||
|
||||
func _perform_attack():
|
||||
if not can_attack or is_attacking or spawn_landing or netted_by_web:
|
||||
@@ -4129,7 +4126,6 @@ func _perform_attack():
|
||||
var is_crit = randf() < crit_chance
|
||||
if is_crit:
|
||||
final_damage *= 2.0 # Critical strikes deal 2x damage
|
||||
print(name, " CRITICAL STRIKE! (LCK: ", character_stats.baseStats.lck + character_stats.get_pass("lck"), ")")
|
||||
|
||||
# Round to 1 decimal place
|
||||
final_damage = round(final_damage * 10.0) / 10.0
|
||||
@@ -4170,22 +4166,17 @@ func _perform_attack():
|
||||
$SfxBowShoot.play()
|
||||
# Consume one arrow
|
||||
arrows.quantity -= 1
|
||||
var remaining = arrows.quantity
|
||||
if arrows.quantity <= 0:
|
||||
# Remove arrows if quantity reaches 0
|
||||
character_stats.equipment["offhand"] = null
|
||||
if character_stats:
|
||||
character_stats.character_changed.emit(character_stats)
|
||||
else:
|
||||
# Update equipment to reflect quantity change
|
||||
if character_stats:
|
||||
character_stats.character_changed.emit(character_stats)
|
||||
print(name, " shot arrow! Arrows remaining: ", remaining)
|
||||
else:
|
||||
# Play bow animation but no projectile - 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
|
||||
@@ -4202,10 +4193,8 @@ func _perform_attack():
|
||||
# Store crit status for visual feedback
|
||||
if is_crit:
|
||||
projectile.set_meta("is_crit", true)
|
||||
# Spawn projectile a bit in front of the player
|
||||
var spawn_offset = attack_direction * 6.0
|
||||
projectile.global_position = global_position + spawn_offset
|
||||
print(name, " attacked with staff projectile! Damage: ", final_damage, " (base: ", base_damage, ", crit: ", is_crit, ")")
|
||||
elif is_axe:
|
||||
# Axe swing - stays on player, plays directional animation
|
||||
if attack_axe_swing_scene and equipped_weapon:
|
||||
@@ -4214,7 +4203,6 @@ func _perform_attack():
|
||||
get_parent().add_child(axe_swing)
|
||||
axe_swing.setup(attack_direction, self, -1.0, equipped_weapon)
|
||||
axe_swing.global_position = global_position
|
||||
print(name, " axe swing! Damage: ", final_damage)
|
||||
elif is_unarmed:
|
||||
# Unarmed punch - low STR-based damage (enemy armour mitigates via take_damage)
|
||||
if attack_punch_scene:
|
||||
@@ -4228,7 +4216,6 @@ func _perform_attack():
|
||||
get_parent().add_child(punch)
|
||||
punch.setup(attack_direction, self, punch_damage)
|
||||
punch.global_position = global_position + attack_direction * 12.0
|
||||
print(name, " punched! Damage: ", punch_damage)
|
||||
else:
|
||||
# Spawn sword projectile for non-bow/staff/axe weapons
|
||||
if sword_projectile_scene:
|
||||
@@ -4239,10 +4226,8 @@ func _perform_attack():
|
||||
# Store crit status for visual feedback
|
||||
if is_crit:
|
||||
projectile.set_meta("is_crit", true)
|
||||
# Spawn projectile a bit in front of the player
|
||||
var spawn_offset = attack_direction * 6.0 # 10 pixels in front
|
||||
var spawn_offset = attack_direction * 6.0
|
||||
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 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():
|
||||
@@ -4334,21 +4319,13 @@ func _create_bomb_object():
|
||||
if character_stats:
|
||||
character_stats.character_changed.emit(character_stats)
|
||||
|
||||
# Load interactable object scene
|
||||
var interactable_object_scene = load("res://scenes/interactable_object.tscn")
|
||||
if not interactable_object_scene:
|
||||
push_error("ERROR: Could not load interactable_object scene!")
|
||||
return
|
||||
|
||||
# Spawn bomb object at player position
|
||||
var entities_node = get_parent()
|
||||
if not entities_node:
|
||||
entities_node = get_tree().get_first_node_in_group("game_world").get_node_or_null("Entities")
|
||||
if not entities_node:
|
||||
push_error("ERROR: Could not find Entities node!")
|
||||
return
|
||||
|
||||
var bomb_obj = interactable_object_scene.instantiate()
|
||||
var bomb_obj = _INTERACTABLE_OBJECT_SCENE.instantiate()
|
||||
bomb_obj.name = "BombObject_" + str(Time.get_ticks_msec())
|
||||
bomb_obj.global_position = global_position + (facing_direction_vector * 8.0) # Spawn slightly in front
|
||||
|
||||
@@ -6597,7 +6574,6 @@ func take_damage(amount: float, attacker_position: Vector2, is_burn_damage: bool
|
||||
func _die():
|
||||
# Already processing death - prevent multiple concurrent death sequences
|
||||
if is_processing_death:
|
||||
print(name, " already processing death, ignoring duplicate call")
|
||||
return
|
||||
|
||||
is_processing_death = true # Set IMMEDIATELY to block duplicates
|
||||
@@ -6641,20 +6617,19 @@ func _die():
|
||||
var obj_name = _get_object_name_for_sync(released_obj)
|
||||
_rpc_to_ready_peers("_sync_release", [obj_name])
|
||||
|
||||
print(name, " released ", released_obj.name, " on death")
|
||||
pass # released on death
|
||||
else:
|
||||
is_lifting = false
|
||||
is_pushing = false
|
||||
|
||||
print(name, " died!")
|
||||
|
||||
# Show concussion status effect above head
|
||||
var status_anim = get_node_or_null("Sprite2DStatus/AnimationPlayerStatus")
|
||||
if status_anim and status_anim.has_animation("concussion"):
|
||||
status_anim.play("concussion")
|
||||
|
||||
# Play death sound effect
|
||||
if sfx_die:
|
||||
# Play death sound effect and spawn blood (preloaded blood_scene; add_child is cheaper than 12x call_deferred)
|
||||
var death_parent = get_parent()
|
||||
if sfx_die and death_parent:
|
||||
for i in 12:
|
||||
var angle = randf_range(0, TAU)
|
||||
var speed = randf_range(50, 100)
|
||||
@@ -6662,12 +6637,12 @@ func _die():
|
||||
var b = blood_scene.instantiate() as CharacterBody2D
|
||||
b.scale = Vector2(randf_range(0.3, 2), randf_range(0.3, 2))
|
||||
b.global_position = global_position
|
||||
|
||||
# Set initial velocities from the synchronized data
|
||||
var direction = Vector2.from_angle(angle)
|
||||
b.velocity = direction * speed
|
||||
b.velocityZ = initial_velocityZ
|
||||
get_parent().call_deferred("add_child", b)
|
||||
death_parent.add_child(b)
|
||||
sfx_die.play()
|
||||
elif sfx_die:
|
||||
sfx_die.play()
|
||||
|
||||
# Play DIE animation
|
||||
@@ -6682,11 +6657,9 @@ func _die():
|
||||
|
||||
# Force holder to drop us NOW (before respawn wait)
|
||||
# Search for any player holding us (don't rely on being_held_by)
|
||||
print(name, " searching for anyone holding us...")
|
||||
var found_holder = false
|
||||
for other_player in get_tree().get_nodes_in_group("player"):
|
||||
if other_player != self and other_player.held_object == self:
|
||||
print(name, " FOUND holder: ", other_player.name, "! Clearing locally and syncing via RPC")
|
||||
|
||||
# Clear LOCALLY first
|
||||
other_player.held_object = null
|
||||
|
||||
Reference in New Issue
Block a user