added more tomes
This commit is contained in:
@@ -41,6 +41,7 @@ var just_grabbed_this_frame = false # Prevents immediate release bug - persists
|
||||
var grab_released_while_lifting = false # Track if grab was released while lifting (for drop-on-second-press logic)
|
||||
var grab_start_time = 0.0 # Time when we first grabbed (to detect quick tap)
|
||||
var grab_tap_threshold = 0.2 # Seconds to distinguish tap from hold
|
||||
var is_shielding: bool = false # True when holding grab with shield in offhand and nothing to grab/lift
|
||||
var last_movement_direction = Vector2.DOWN # Track last direction for placing objects
|
||||
var push_axis = Vector2.ZERO # Locked axis for pushing/pulling
|
||||
var push_direction_locked: int = Direction.DOWN # Locked facing direction when pushing
|
||||
@@ -86,6 +87,7 @@ var bow_charge_tint_pulse_time: float = 0.0 # Timer for pulsing tint animation
|
||||
var bow_charge_tint_pulse_speed_charged: float = 20.0 # Speed of tint pulse animation when fully charged
|
||||
var original_sprite_tints: Dictionary = {} # Store original tint values for restoration
|
||||
var spell_incantation_played: bool = false # Track if incantation sound has been played
|
||||
var current_spell_element: String = "fire" # "fire" or "healing" for cursor/tint
|
||||
var burn_debuff_timer: float = 0.0 # Timer for burn debuff
|
||||
var burn_debuff_duration: float = 5.0 # Burn lasts 5 seconds
|
||||
var burn_debuff_damage_per_second: float = 1.0 # 1 HP per second
|
||||
@@ -96,6 +98,10 @@ 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 shield_block_cooldown_timer: float = 0.0 # After blocking, cannot block again until this reaches 0
|
||||
var shield_block_cooldown_duration: float = 1.2 # Seconds before can block again
|
||||
var shield_block_direction: Vector2 = Vector2.DOWN # Facing direction when we started blocking (locked)
|
||||
var was_shielding_last_frame: bool = false # For detecting shield activate transition
|
||||
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)
|
||||
@@ -103,6 +109,8 @@ var sword_projectile_scene = preload("res://scenes/sword_projectile.tscn") # New
|
||||
var staff_projectile_scene = preload("res://scenes/attack_staff.tscn") # Staff magic ball projectile
|
||||
var attack_arrow_scene = preload("res://scenes/attack_arrow.tscn")
|
||||
var flame_spell_scene = preload("res://scenes/attack_spell_flame.tscn") # Flame spell for Tome of Flames
|
||||
var frostspike_spell_scene = preload("res://scenes/attack_spell_frostspike.tscn") # Frostspike for Tome of Frostspike
|
||||
var healing_effect_scene = preload("res://scenes/healing_effect.tscn") # Visual effect for Tome of Healing
|
||||
var attack_bomb_scene = preload("res://scenes/attack_bomb.tscn") # Bomb projectile
|
||||
var blood_scene = preload("res://scenes/blood_clot.tscn")
|
||||
|
||||
@@ -135,6 +143,8 @@ var is_airborne: bool = false
|
||||
@onready var sprite_eyelashes = $Sprite2DEyeLashes
|
||||
@onready var sprite_addons = $Sprite2DAddons
|
||||
@onready var sprite_headgear = $Sprite2DHeadgear
|
||||
@onready var sprite_shield = $Sprite2DShield
|
||||
@onready var sprite_shield_holding = $Sprite2DShieldHolding
|
||||
@onready var sprite_weapon = $Sprite2DWeapon
|
||||
@onready var cone_light = $ConeLight
|
||||
|
||||
@@ -478,6 +488,10 @@ func _duplicate_sprite_materials():
|
||||
sprite_headgear.material = sprite_headgear.material.duplicate()
|
||||
if sprite_weapon and sprite_weapon.material:
|
||||
sprite_weapon.material = sprite_weapon.material.duplicate()
|
||||
if sprite_shield and sprite_shield.material:
|
||||
sprite_shield.material = sprite_shield.material.duplicate()
|
||||
if sprite_shield_holding and sprite_shield_holding.material:
|
||||
sprite_shield_holding.material = sprite_shield_holding.material.duplicate()
|
||||
|
||||
func _initialize_character_stats():
|
||||
# Create character_stats if it doesn't exist
|
||||
@@ -693,12 +707,15 @@ func _setup_player_appearance():
|
||||
character_stats.equipment["offhand"] = starting_bomb
|
||||
print("Dwarf player ", name, " spawned with 5 bombs")
|
||||
|
||||
# Give Human race starting spellbook (Tome of Flames)
|
||||
# Give Human race (Wizard) starting spellbook (Tome of Flames) and Hat
|
||||
if selected_race == "Human":
|
||||
var starting_tome = ItemDatabase.create_item("tome_of_flames")
|
||||
if starting_tome:
|
||||
character_stats.equipment["offhand"] = starting_tome
|
||||
print("Human player ", name, " spawned with Tome of Flames")
|
||||
var starting_hat = ItemDatabase.create_item("hat")
|
||||
if starting_hat:
|
||||
character_stats.equipment["headgear"] = starting_hat
|
||||
print("Human player ", name, " spawned with Tome of Flames and Hat")
|
||||
|
||||
# Randomize skin (human only for players)
|
||||
# Weighted random: Human1 has highest chance, Human7 has lowest chance
|
||||
@@ -1129,6 +1146,8 @@ func _apply_appearance_to_sprites():
|
||||
_apply_weapon_color_replacements(sprite_weapon, equipped_weapon)
|
||||
else:
|
||||
_clear_weapon_color_replacements(sprite_weapon)
|
||||
|
||||
_update_shield_visibility()
|
||||
|
||||
# Appearance applied (verbose logging removed)
|
||||
|
||||
@@ -1458,6 +1477,10 @@ func _update_animation(delta):
|
||||
sprite_addons.frame = frame_index
|
||||
if sprite_headgear:
|
||||
sprite_headgear.frame = frame_index
|
||||
if sprite_shield:
|
||||
sprite_shield.frame = frame_index
|
||||
if sprite_shield_holding:
|
||||
sprite_shield_holding.frame = frame_index
|
||||
|
||||
# Update weapon sprite - use BOW_STRING animation if charging bow
|
||||
if sprite_weapon:
|
||||
@@ -1511,6 +1534,10 @@ func _update_facing_from_mouse(mouse_direction: Vector2):
|
||||
if is_pushing:
|
||||
return
|
||||
|
||||
# Don't update if shielding (locked block direction)
|
||||
if is_shielding:
|
||||
return
|
||||
|
||||
# Don't update if direction is locked (during attack)
|
||||
if direction_lock_timer > 0.0:
|
||||
return
|
||||
@@ -1699,7 +1726,7 @@ func _update_z_physics(delta):
|
||||
# Apply to all sprite layers
|
||||
for sprite_layer in [sprite_body, sprite_boots, sprite_armour, sprite_facial_hair,
|
||||
sprite_hair, sprite_eyes, sprite_eyelashes, sprite_addons,
|
||||
sprite_headgear, sprite_weapon]:
|
||||
sprite_headgear, sprite_shield, sprite_shield_holding, sprite_weapon]:
|
||||
if sprite_layer:
|
||||
sprite_layer.position.y = y_offset
|
||||
if position_z > 0:
|
||||
@@ -1729,6 +1756,28 @@ func _physics_process(delta):
|
||||
if is_airborne:
|
||||
_update_z_physics(delta)
|
||||
|
||||
# Spell charge visuals (incantation, tint pulse) - run for ALL players so others see the pulse
|
||||
if is_charging_spell:
|
||||
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
|
||||
_update_spell_charge_particles(charge_progress)
|
||||
_update_spell_charge_incantation(charge_progress)
|
||||
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()
|
||||
spell_incantation_played = true
|
||||
else:
|
||||
spell_charge_tint_pulse_time = 0.0
|
||||
_clear_spell_charge_tint()
|
||||
if has_node("SfxSpellIncantation"):
|
||||
$SfxSpellIncantation.stop()
|
||||
spell_incantation_played = false
|
||||
else:
|
||||
spell_charge_tint_pulse_time = 0.0
|
||||
|
||||
if is_local_player and is_multiplayer_authority():
|
||||
# Skip all input and logic if dead
|
||||
if is_dead:
|
||||
@@ -1759,37 +1808,10 @@ func _physics_process(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
|
||||
var charge_progress = clamp(charge_time / spell_charge_duration, 0.0, 1.0)
|
||||
|
||||
# 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:
|
||||
# Use much faster pulse speed when fully charged
|
||||
spell_charge_tint_pulse_time += delta * spell_charge_tint_pulse_speed_charged
|
||||
_apply_spell_charge_tint()
|
||||
|
||||
# Play incantation sound when fully charged (only once)
|
||||
if not spell_incantation_played:
|
||||
if has_node("SfxSpellIncantation"):
|
||||
$SfxSpellIncantation.play()
|
||||
spell_incantation_played = true
|
||||
else:
|
||||
spell_charge_tint_pulse_time = 0.0
|
||||
_clear_spell_charge_tint()
|
||||
# Stop incantation if not fully charged
|
||||
if has_node("SfxSpellIncantation"):
|
||||
$SfxSpellIncantation.stop()
|
||||
spell_incantation_played = false
|
||||
else:
|
||||
# Reset pulse timer when not charging
|
||||
spell_charge_tint_pulse_time = 0.0
|
||||
if shield_block_cooldown_timer > 0.0:
|
||||
shield_block_cooldown_timer -= delta
|
||||
if shield_block_cooldown_timer <= 0.0:
|
||||
shield_block_cooldown_timer = 0.0
|
||||
|
||||
# Update bow charge tint (when fully charged)
|
||||
if is_charging_bow:
|
||||
@@ -1881,9 +1903,15 @@ func _physics_process(delta):
|
||||
break
|
||||
|
||||
if being_held_by_someone:
|
||||
is_shielding = false
|
||||
was_shielding_last_frame = false
|
||||
_update_shield_visibility()
|
||||
# Handle struggle mechanic
|
||||
_handle_struggle(delta)
|
||||
elif is_knocked_back:
|
||||
is_shielding = false
|
||||
was_shielding_last_frame = false
|
||||
_update_shield_visibility()
|
||||
# During knockback, no input control - just let velocity carry the player
|
||||
# Apply friction to slow down knockback
|
||||
velocity = velocity.lerp(Vector2.ZERO, delta * 8.0)
|
||||
@@ -1895,6 +1923,9 @@ func _physics_process(delta):
|
||||
_handle_movement(delta)
|
||||
_handle_interactions()
|
||||
else:
|
||||
is_shielding = false
|
||||
was_shielding_last_frame = false
|
||||
_update_shield_visibility()
|
||||
# Reset struggle when airborne
|
||||
struggle_time = 0.0
|
||||
struggle_direction = Vector2.ZERO
|
||||
@@ -2100,8 +2131,10 @@ func _handle_input():
|
||||
|
||||
# Update full 360-degree facing direction for attacks (gamepad/keyboard input)
|
||||
# Only update if mouse control is not active (i.e., mouse is outside window or using gamepad)
|
||||
# Don't update if direction is locked (during attack)
|
||||
if not is_pushing and (not mouse_control_active or input_device != -1) and direction_lock_timer <= 0.0:
|
||||
# Don't update if direction is locked (during attack) or shielding
|
||||
if is_shielding:
|
||||
facing_direction_vector = shield_block_direction
|
||||
elif not is_pushing and (not mouse_control_active or input_device != -1) and direction_lock_timer <= 0.0:
|
||||
facing_direction_vector = input_vector.normalized()
|
||||
elif direction_lock_timer > 0.0 and locked_facing_direction.length() > 0.0:
|
||||
# Use locked direction during attack
|
||||
@@ -2109,8 +2142,13 @@ func _handle_input():
|
||||
|
||||
# Update facing direction for animations (except when pushing - locked direction)
|
||||
# Only update from movement input if mouse control is not active or using gamepad
|
||||
# Don't update if direction is locked (during attack)
|
||||
if not is_pushing and (not mouse_control_active or input_device != -1) and direction_lock_timer <= 0.0:
|
||||
# Don't update if direction is locked (during attack) or shielding
|
||||
if is_shielding:
|
||||
var new_direction = _get_direction_from_vector(shield_block_direction) as Direction
|
||||
if new_direction != current_direction:
|
||||
current_direction = new_direction
|
||||
_update_cone_light_rotation()
|
||||
elif not is_pushing and (not mouse_control_active or input_device != -1) and direction_lock_timer <= 0.0:
|
||||
var new_direction = _get_direction_from_vector(input_vector) as Direction
|
||||
|
||||
# Update direction and cone light rotation if changed
|
||||
@@ -2164,6 +2202,12 @@ func _handle_input():
|
||||
if push_direction_locked != current_direction:
|
||||
current_direction = push_direction_locked as Direction
|
||||
_update_cone_light_rotation()
|
||||
elif is_shielding:
|
||||
# Keep locked block direction when shielding and idle
|
||||
var new_direction = _get_direction_from_vector(shield_block_direction) as Direction
|
||||
if new_direction != current_direction:
|
||||
current_direction = new_direction
|
||||
_update_cone_light_rotation()
|
||||
else:
|
||||
if current_animation != "THROW" and current_animation != "DAMAGE" and current_animation != "SWORD" and current_animation != "BOW" and current_animation != "STAFF" and current_animation != "CONJURE" and current_animation != "LIFT" and current_animation != "FINISH_SPELL":
|
||||
_set_animation("IDLE")
|
||||
@@ -2192,6 +2236,7 @@ func _handle_input():
|
||||
# Reduce speed by half when pushing/pulling
|
||||
# Reduce speed by 50% when charging bow
|
||||
# Reduce speed by 80% when charging spell (20% speed)
|
||||
# Reduce speed to 60% when shielding
|
||||
# Calculate speed with encumbrance penalty
|
||||
var speed_multiplier = 1.0
|
||||
if is_pushing:
|
||||
@@ -2200,6 +2245,8 @@ func _handle_input():
|
||||
speed_multiplier = 0.5
|
||||
elif is_charging_spell:
|
||||
speed_multiplier = 0.2 # 20% speed (80% reduction)
|
||||
elif is_shielding:
|
||||
speed_multiplier = 0.6 # 60% speed when blocking with shield
|
||||
|
||||
var base_speed = move_speed * speed_multiplier
|
||||
var current_speed = base_speed
|
||||
@@ -2262,6 +2309,25 @@ func _handle_interactions():
|
||||
else:
|
||||
grab_just_released = false
|
||||
|
||||
# Update is_shielding: hold grab with shield in offhand and nothing to grab/lift
|
||||
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)
|
||||
if would_shield and shield_block_cooldown_timer > 0.0:
|
||||
is_shielding = false
|
||||
if has_node("SfxDenyActivateShield"):
|
||||
$SfxDenyActivateShield.play()
|
||||
elif would_shield:
|
||||
if not was_shielding_last_frame:
|
||||
shield_block_direction = facing_direction_vector.normalized() if facing_direction_vector.length() > 0.1 else last_movement_direction.normalized()
|
||||
if has_node("SfxActivateShield"):
|
||||
$SfxActivateShield.play()
|
||||
is_shielding = true
|
||||
else:
|
||||
is_shielding = false
|
||||
was_shielding_last_frame = is_shielding
|
||||
_update_shield_visibility()
|
||||
|
||||
# Cancel bow charging if grab is pressed
|
||||
if grab_just_pressed and is_charging_bow:
|
||||
is_charging_bow = false
|
||||
@@ -2275,17 +2341,23 @@ func _handle_interactions():
|
||||
|
||||
print(name, " cancelled bow charge")
|
||||
|
||||
# Check for spell casting (with Tome of Flames)
|
||||
# Handle spell charging (Tome of Flames)
|
||||
# Check for spell casting (Tome of Flames, Frostspike, or Healing)
|
||||
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.SPELLBOOK:
|
||||
if offhand_item.item_name == "Tome of Flames":
|
||||
# Check for valid target position
|
||||
var is_fire = offhand_item.item_name == "Tome of Flames"
|
||||
var is_frost = offhand_item.item_name == "Tome of Frostspike"
|
||||
var is_heal = offhand_item.item_name == "Tome of Healing"
|
||||
if is_fire or is_frost or is_heal:
|
||||
var game_world = get_tree().get_first_node_in_group("game_world")
|
||||
var target_pos = Vector2.ZERO
|
||||
if game_world and game_world.has_method("get_grid_locked_cursor_position"):
|
||||
var heal_target: Node = null
|
||||
if (is_fire or is_frost) and game_world and game_world.has_method("get_grid_locked_cursor_position"):
|
||||
target_pos = game_world.get_grid_locked_cursor_position()
|
||||
elif is_heal:
|
||||
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)
|
||||
|
||||
# Check if there's a grabbable object nearby - prioritize grabbing over spell casting
|
||||
var nearby_grabbable = null
|
||||
@@ -2307,133 +2379,95 @@ func _handle_interactions():
|
||||
nearby_grabbable = body
|
||||
break
|
||||
|
||||
# Only start charging spell if no grabbable object is nearby and not lifting/grabbing
|
||||
if grab_just_pressed and not is_charging_spell and target_pos != Vector2.ZERO and not nearby_grabbable and not is_lifting and not held_object:
|
||||
if grab_just_pressed and not is_charging_spell and has_valid_target and not nearby_grabbable and not is_lifting and not held_object:
|
||||
is_charging_spell = true
|
||||
current_spell_element = "healing" if is_heal else ("frost" if is_frost else "fire")
|
||||
spell_charge_start_time = Time.get_ticks_msec() / 1000.0
|
||||
spell_incantation_played = false # Reset flag when starting new charge
|
||||
spell_incantation_played = false
|
||||
_start_spell_charge_particles()
|
||||
_start_spell_charge_incantation()
|
||||
|
||||
# Play spell charging sound (incantation plays when fully charged)
|
||||
if has_node("SfxSpellCharge"):
|
||||
$SfxSpellCharge.play()
|
||||
|
||||
# Sync spell charge start to other clients
|
||||
if multiplayer.has_multiplayer_peer():
|
||||
_sync_spell_charge_start.rpc()
|
||||
|
||||
print(name, " started charging spell")
|
||||
# Skip regular grab handling
|
||||
print(name, " started charging spell (", current_spell_element, ")")
|
||||
just_grabbed_this_frame = false
|
||||
return
|
||||
# Release spell
|
||||
elif grab_just_released and is_charging_spell:
|
||||
var charge_time = (Time.get_ticks_msec() / 1000.0) - spell_charge_start_time
|
||||
|
||||
# Minimum charge time: 0.2 seconds, otherwise cancel
|
||||
if charge_time < 0.2:
|
||||
is_charging_spell = false
|
||||
current_spell_element = "fire"
|
||||
spell_incantation_played = false
|
||||
_stop_spell_charge_particles()
|
||||
_stop_spell_charge_incantation()
|
||||
_clear_spell_charge_tint()
|
||||
|
||||
# Return to IDLE animation
|
||||
_set_animation("IDLE")
|
||||
|
||||
# Stop spell charging sounds
|
||||
if has_node("SfxSpellCharge"):
|
||||
$SfxSpellCharge.stop()
|
||||
if has_node("SfxSpellIncantation"):
|
||||
$SfxSpellIncantation.stop()
|
||||
|
||||
# Sync spell charge end to other clients
|
||||
if multiplayer.has_multiplayer_peer():
|
||||
_sync_spell_charge_end.rpc()
|
||||
|
||||
print(name, " cancelled spell (released too quickly, need at least 0.2s)")
|
||||
print(name, " cancelled spell (released too quickly)")
|
||||
just_grabbed_this_frame = false
|
||||
return
|
||||
|
||||
# Check if fully charged (1.0 seconds)
|
||||
var is_fully_charged = charge_time >= spell_charge_duration
|
||||
|
||||
# Get target position again (in case it changed)
|
||||
if game_world and game_world.has_method("get_grid_locked_cursor_position"):
|
||||
if (is_fire or is_frost) and game_world and game_world.has_method("get_grid_locked_cursor_position"):
|
||||
target_pos = game_world.get_grid_locked_cursor_position()
|
||||
if is_heal:
|
||||
heal_target = _get_heal_target()
|
||||
has_valid_target = ((is_fire or is_frost) and target_pos != Vector2.ZERO) or (is_heal and heal_target != null)
|
||||
|
||||
# Cast spell if fully charged (will find valid position if target is blocked)
|
||||
if target_pos != Vector2.ZERO and is_fully_charged:
|
||||
# Cast spell (will find closest valid position if target is blocked)
|
||||
_cast_flame_spell(target_pos)
|
||||
# Play FINISH_SPELL animation after casting
|
||||
if has_valid_target and is_fully_charged:
|
||||
if is_fire:
|
||||
_cast_flame_spell(target_pos)
|
||||
elif is_frost:
|
||||
_cast_frostspike_spell(target_pos)
|
||||
else:
|
||||
_cast_heal_spell(heal_target)
|
||||
_set_animation("FINISH_SPELL")
|
||||
|
||||
# Stop charging and clear tint (but let incantation sound finish)
|
||||
is_charging_spell = false
|
||||
current_spell_element = "fire"
|
||||
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
|
||||
_clear_spell_charge_tint()
|
||||
if has_node("SfxSpellCharge"):
|
||||
$SfxSpellCharge.stop()
|
||||
# Don't stop SfxSpellIncantation - let it finish playing
|
||||
|
||||
# Sync spell charge end to other clients
|
||||
if multiplayer.has_multiplayer_peer():
|
||||
_sync_spell_charge_end.rpc()
|
||||
else:
|
||||
# Not fully charged or no target - just cancel without casting
|
||||
print(name, " spell not cast (charge: ", charge_time, "s, fully charged: ", is_fully_charged, ", target: ", target_pos, ")")
|
||||
|
||||
# Stop charging and clear tint
|
||||
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
|
||||
_stop_spell_charge_particles()
|
||||
_stop_spell_charge_incantation()
|
||||
_clear_spell_charge_tint() # This will restore original tints
|
||||
|
||||
# Return to IDLE animation
|
||||
_clear_spell_charge_tint()
|
||||
_set_animation("IDLE")
|
||||
|
||||
# Stop spell charging sounds
|
||||
if has_node("SfxSpellCharge"):
|
||||
$SfxSpellCharge.stop()
|
||||
if has_node("SfxSpellIncantation"):
|
||||
$SfxSpellIncantation.stop()
|
||||
|
||||
# Sync spell charge end to other clients
|
||||
if multiplayer.has_multiplayer_peer():
|
||||
_sync_spell_charge_end.rpc()
|
||||
|
||||
print(name, " released spell (charge: ", charge_time, "s, fully charged: ", is_fully_charged, ")")
|
||||
# Skip regular grab handling
|
||||
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
|
||||
# Cancel if no target position available or if player starts lifting/grabbing
|
||||
elif is_charging_spell and (target_pos == Vector2.ZERO or is_lifting or held_object):
|
||||
elif is_charging_spell and (not has_valid_target or is_lifting or held_object):
|
||||
is_charging_spell = false
|
||||
current_spell_element = "fire"
|
||||
spell_incantation_played = false
|
||||
_stop_spell_charge_particles()
|
||||
_stop_spell_charge_incantation()
|
||||
_clear_spell_charge_tint()
|
||||
|
||||
# Return to IDLE animation
|
||||
_set_animation("IDLE")
|
||||
|
||||
# Stop spell charging sounds
|
||||
if has_node("SfxSpellCharge"):
|
||||
$SfxSpellCharge.stop()
|
||||
if has_node("SfxSpellIncantation"):
|
||||
$SfxSpellIncantation.stop()
|
||||
|
||||
# Sync spell charge end to other clients
|
||||
if multiplayer.has_multiplayer_peer():
|
||||
_sync_spell_charge_end.rpc()
|
||||
|
||||
print(name, " spell charge cancelled (no target)")
|
||||
|
||||
# Check for trap disarm (Dwarf only)
|
||||
@@ -2784,6 +2818,44 @@ func _get_nearby_disarmable_trap() -> Node:
|
||||
|
||||
return null
|
||||
|
||||
func _has_shield_in_offhand() -> bool:
|
||||
if not character_stats or not character_stats.equipment.has("offhand"):
|
||||
return false
|
||||
var off = character_stats.equipment["offhand"]
|
||||
return off != null and "shield" in off.item_name.to_lower()
|
||||
|
||||
func _has_nearby_grabbable() -> bool:
|
||||
if not grab_area:
|
||||
return false
|
||||
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 and position.distance_to(body.position) < grab_range:
|
||||
return true
|
||||
return false
|
||||
|
||||
func _update_shield_visibility() -> void:
|
||||
if not sprite_shield or not sprite_shield_holding:
|
||||
return
|
||||
var has_shield = _has_shield_in_offhand()
|
||||
if not has_shield:
|
||||
sprite_shield.visible = false
|
||||
sprite_shield_holding.visible = false
|
||||
return
|
||||
if is_shielding:
|
||||
sprite_shield.visible = false
|
||||
sprite_shield_holding.visible = true
|
||||
else:
|
||||
sprite_shield.visible = true
|
||||
sprite_shield_holding.visible = false
|
||||
|
||||
func _try_grab():
|
||||
if not grab_area:
|
||||
return
|
||||
@@ -3026,6 +3098,9 @@ func reset_grab_state():
|
||||
grab_start_time = 0.0
|
||||
grab_released_while_lifting = false
|
||||
was_dragging_last_frame = false
|
||||
is_shielding = false
|
||||
was_shielding_last_frame = false
|
||||
_update_shield_visibility()
|
||||
|
||||
# Reset to idle animation
|
||||
if current_animation == "IDLE_HOLD" or current_animation == "RUN_HOLD" or current_animation == "LIFT" or current_animation == "IDLE_PUSH" or current_animation == "RUN_PUSH":
|
||||
@@ -3411,6 +3486,7 @@ func _place_down_object():
|
||||
if not attack_bomb_scene:
|
||||
return
|
||||
var bomb = attack_bomb_scene.instantiate()
|
||||
bomb.name = "PlacedBomb_" + bomb_name
|
||||
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
|
||||
@@ -3813,8 +3889,10 @@ func _place_bomb(target_position: Vector2):
|
||||
print(name, " cannot place bomb - no valid target position")
|
||||
return
|
||||
|
||||
# Spawn bomb at target position
|
||||
# Unique id for sync (collect/remove on other clients)
|
||||
var bomb_id = "DirectBomb_%d_%d" % [Time.get_ticks_msec(), get_multiplayer_authority()]
|
||||
var bomb = attack_bomb_scene.instantiate()
|
||||
bomb.name = bomb_id
|
||||
get_parent().add_child(bomb)
|
||||
bomb.global_position = valid_target_pos
|
||||
|
||||
@@ -3827,7 +3905,7 @@ func _place_bomb(target_position: Vector2):
|
||||
|
||||
# Sync bomb spawn to other clients
|
||||
if multiplayer.has_multiplayer_peer() and is_multiplayer_authority() and can_send_rpcs and is_inside_tree():
|
||||
_rpc_to_ready_peers("_sync_place_bomb", [valid_target_pos])
|
||||
_rpc_to_ready_peers("_sync_place_bomb", [bomb_id, valid_target_pos])
|
||||
|
||||
print(name, " placed bomb!")
|
||||
|
||||
@@ -3889,6 +3967,128 @@ func _sync_flame_spell(target_position: Vector2, spell_damage: float):
|
||||
|
||||
print(name, " (synced) spawned flame spell at ", target_position)
|
||||
|
||||
func _cast_frostspike_spell(target_position: Vector2):
|
||||
if not frostspike_spell_scene:
|
||||
return
|
||||
if not is_multiplayer_authority():
|
||||
return
|
||||
var game_world = get_tree().get_first_node_in_group("game_world")
|
||||
var valid_target_pos = target_position
|
||||
if game_world and game_world.has_method("_get_valid_spell_target_position"):
|
||||
var found_pos = game_world._get_valid_spell_target_position(target_position)
|
||||
if found_pos != Vector2.ZERO:
|
||||
valid_target_pos = found_pos
|
||||
else:
|
||||
print(name, " cannot cast frostspike - no valid target position")
|
||||
return
|
||||
var spell_damage = 15.0
|
||||
if character_stats:
|
||||
spell_damage = character_stats.damage * 0.75
|
||||
var frost = frostspike_spell_scene.instantiate()
|
||||
frost.setup(valid_target_pos, self, spell_damage, true)
|
||||
get_parent().add_child(frost)
|
||||
if multiplayer.has_multiplayer_peer() and can_send_rpcs and is_inside_tree():
|
||||
_rpc_to_ready_peers("_sync_frostspike_spell", [valid_target_pos, spell_damage])
|
||||
print(name, " cast frostspike at ", valid_target_pos)
|
||||
|
||||
@rpc("any_peer", "reliable")
|
||||
func _sync_frostspike_spell(target_position: Vector2, spell_damage: float):
|
||||
if is_multiplayer_authority():
|
||||
return
|
||||
if not frostspike_spell_scene:
|
||||
return
|
||||
var frost = frostspike_spell_scene.instantiate()
|
||||
frost.setup(target_position, self, spell_damage, true)
|
||||
get_parent().add_child(frost)
|
||||
print(name, " (synced) spawned frostspike at ", target_position)
|
||||
|
||||
func _cast_heal_spell(target: Node):
|
||||
if not target or not is_instance_valid(target):
|
||||
return
|
||||
if not character_stats:
|
||||
return
|
||||
var int_val = character_stats.baseStats.int + character_stats.get_pass("int")
|
||||
var base_heal = 10.0
|
||||
var amount = base_heal + int_val * 0.5
|
||||
amount = max(1.0, floor(amount))
|
||||
var cap = 0.0
|
||||
if target.character_stats:
|
||||
cap = target.character_stats.maxhp - target.character_stats.hp
|
||||
amount = min(amount, max(0.0, cap))
|
||||
if amount <= 0:
|
||||
return
|
||||
var me = multiplayer.get_unique_id()
|
||||
var tid = target.get_multiplayer_authority()
|
||||
if me == tid:
|
||||
target.heal(amount)
|
||||
_spawn_heal_effect_and_text(target, amount)
|
||||
if multiplayer.has_multiplayer_peer() and can_send_rpcs and is_inside_tree():
|
||||
var gw = get_tree().get_first_node_in_group("game_world")
|
||||
if gw and gw.has_method("_sync_heal_spell"):
|
||||
_rpc_to_ready_peers("_sync_heal_spell_via_gw", [target.name, amount])
|
||||
print(name, " cast heal on ", target.name, " for ", int(amount), " HP")
|
||||
|
||||
func _spawn_heal_effect_and_text(target: Node, amount: float):
|
||||
if not target or not is_instance_valid(target):
|
||||
return
|
||||
var game_world = get_tree().get_first_node_in_group("game_world")
|
||||
var entities = game_world.get_node_or_null("Entities") if game_world else null
|
||||
var parent = entities if entities else target.get_parent()
|
||||
if not parent:
|
||||
return
|
||||
if healing_effect_scene:
|
||||
var eff = healing_effect_scene.instantiate()
|
||||
parent.add_child(eff)
|
||||
eff.global_position = target.global_position
|
||||
if eff.has_method("setup"):
|
||||
eff.setup(target)
|
||||
var floating_text_scene = preload("res://scenes/floating_text.tscn")
|
||||
if floating_text_scene:
|
||||
var ft = floating_text_scene.instantiate()
|
||||
parent.add_child(ft)
|
||||
ft.global_position = Vector2(target.global_position.x, target.global_position.y - 20)
|
||||
ft.setup("+" + str(int(amount)) + " HP", Color.GREEN, 0.5, 0.5, null, 1, 1, 0)
|
||||
|
||||
@rpc("any_peer", "reliable")
|
||||
func _sync_heal_spell_via_gw(target_name: String, amount: float):
|
||||
if is_multiplayer_authority():
|
||||
return
|
||||
var gw = get_tree().get_first_node_in_group("game_world")
|
||||
if gw and gw.has_method("_apply_heal_spell_sync"):
|
||||
gw._apply_heal_spell_sync(target_name, amount)
|
||||
|
||||
func _is_healing_spell() -> bool:
|
||||
if not character_stats or not character_stats.equipment.has("offhand"):
|
||||
return false
|
||||
var off = character_stats.equipment["offhand"]
|
||||
return off != null and off.item_name == "Tome of Healing"
|
||||
|
||||
func _is_frost_spell() -> bool:
|
||||
if not character_stats or not character_stats.equipment.has("offhand"):
|
||||
return false
|
||||
var off = character_stats.equipment["offhand"]
|
||||
return off != null and off.item_name == "Tome of Frostspike"
|
||||
|
||||
func _get_heal_target() -> Node:
|
||||
var game_world = get_tree().get_first_node_in_group("game_world")
|
||||
if not game_world or not game_world.has_node("Camera2D"):
|
||||
return null
|
||||
var cam = game_world.get_node("Camera2D")
|
||||
var mouse_world = cam.get_global_mouse_position()
|
||||
const HEAL_RANGE: float = 56.0
|
||||
var best: Node = null
|
||||
var best_d: float = HEAL_RANGE
|
||||
for p in get_tree().get_nodes_in_group("player"):
|
||||
if not is_instance_valid(p):
|
||||
continue
|
||||
if "is_dead" in p and p.is_dead:
|
||||
continue
|
||||
var d = p.global_position.distance_to(mouse_world)
|
||||
if d < best_d:
|
||||
best_d = d
|
||||
best = p
|
||||
return best
|
||||
|
||||
func _can_cast_spell_at(target_position: Vector2) -> bool:
|
||||
# Check if spell can be cast at target position
|
||||
# Must be on floor tile and not blocked by walls
|
||||
@@ -4024,17 +4224,25 @@ func _stop_spell_charge_particles():
|
||||
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")
|
||||
if _is_healing_spell():
|
||||
$AnimationIncantation.play("healing_charging")
|
||||
elif _is_frost_spell():
|
||||
$AnimationIncantation.play("frost_charging")
|
||||
else:
|
||||
$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")
|
||||
if _is_healing_spell():
|
||||
$AnimationIncantation.play("healing_ready")
|
||||
elif _is_frost_spell():
|
||||
$AnimationIncantation.play("frost_ready")
|
||||
else:
|
||||
$AnimationIncantation.play("fire_ready")
|
||||
spell_incantation_fire_ready_shown = true
|
||||
|
||||
func _stop_spell_charge_incantation():
|
||||
@@ -4044,12 +4252,13 @@ func _stop_spell_charge_incantation():
|
||||
$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
|
||||
# IMPORTANT: Only apply to THIS player's sprites (not other players)
|
||||
if not is_charging_spell:
|
||||
return
|
||||
|
||||
var tint = spell_charge_tint
|
||||
if _is_healing_spell():
|
||||
tint = Color(0.25, 2.0, 0.3, 2.0) # Green pulse for healing
|
||||
elif _is_frost_spell():
|
||||
tint = Color(0.25, 0.5, 2.0, 2.0) # Blue pulse for frost
|
||||
var sprites = [
|
||||
{"sprite": sprite_body, "name": "body"},
|
||||
{"sprite": sprite_boots, "name": "boots"},
|
||||
@@ -4098,12 +4307,12 @@ func _apply_spell_charge_tint():
|
||||
# Get original tint
|
||||
var original_tint = original_sprite_tints.get(tint_key, Color.WHITE)
|
||||
|
||||
# Calculate fully charged tint (original * spell_charge_tint)
|
||||
# Calculate fully charged tint (original * tint)
|
||||
var full_charged_tint = Color(
|
||||
original_tint.r * spell_charge_tint.r,
|
||||
original_tint.g * spell_charge_tint.g,
|
||||
original_tint.b * spell_charge_tint.b,
|
||||
original_tint.a * spell_charge_tint.a
|
||||
original_tint.r * tint.r,
|
||||
original_tint.g * tint.g,
|
||||
original_tint.b * tint.b,
|
||||
original_tint.a * tint.a
|
||||
)
|
||||
|
||||
# Interpolate between original and fully charged tint based on pulse
|
||||
@@ -4285,15 +4494,13 @@ func _sync_spell_charge_start():
|
||||
|
||||
@rpc("any_peer", "reliable")
|
||||
func _sync_spell_charge_end():
|
||||
# Sync spell charge end to other clients
|
||||
if not is_multiplayer_authority():
|
||||
is_charging_spell = false
|
||||
current_spell_element = "fire"
|
||||
spell_incantation_played = false
|
||||
_stop_spell_charge_particles()
|
||||
_stop_spell_charge_incantation()
|
||||
_clear_spell_charge_tint()
|
||||
|
||||
# Return to IDLE animation
|
||||
_set_animation("IDLE")
|
||||
|
||||
# Stop spell charging sounds
|
||||
@@ -4741,6 +4948,7 @@ func _sync_bomb_dropped(bomb_name: String, place_pos: Vector2):
|
||||
if not attack_bomb_scene:
|
||||
return
|
||||
var bomb = attack_bomb_scene.instantiate()
|
||||
bomb.name = "PlacedBomb_" + bomb_name
|
||||
get_parent().add_child(bomb)
|
||||
bomb.global_position = place_pos
|
||||
bomb.setup(place_pos, self, Vector2.ZERO, false) # not thrown, fuse lit
|
||||
@@ -4749,20 +4957,17 @@ func _sync_bomb_dropped(bomb_name: String, place_pos: Vector2):
|
||||
print(name, " (synced) dropped bomb at ", place_pos)
|
||||
|
||||
@rpc("any_peer", "reliable")
|
||||
func _sync_place_bomb(target_pos: Vector2):
|
||||
func _sync_place_bomb(bomb_id: String, target_pos: Vector2):
|
||||
# Sync bomb placement to other clients (Human/Elf)
|
||||
if not is_multiplayer_authority():
|
||||
if not attack_bomb_scene:
|
||||
return
|
||||
|
||||
# Spawn bomb at target position
|
||||
var bomb = attack_bomb_scene.instantiate()
|
||||
bomb.name = bomb_id
|
||||
get_parent().add_child(bomb)
|
||||
bomb.global_position = target_pos
|
||||
|
||||
# Setup bomb without throw (placed directly)
|
||||
bomb.setup(target_pos, self, Vector2.ZERO, false) # false = not thrown
|
||||
|
||||
print(name, " (synced) placed bomb at ", target_pos)
|
||||
|
||||
@rpc("any_peer", "reliable")
|
||||
@@ -4778,6 +4983,7 @@ func _sync_throw_bomb(bomb_name: String, bomb_pos: Vector2, throw_force: Vector2
|
||||
if not attack_bomb_scene:
|
||||
return
|
||||
var bomb = attack_bomb_scene.instantiate()
|
||||
bomb.name = "ThrownBomb_" + bomb_name
|
||||
get_parent().add_child(bomb)
|
||||
bomb.global_position = bomb_pos
|
||||
bomb.setup(bomb_pos, self, throw_force, true) # true = is_thrown
|
||||
@@ -4785,6 +4991,16 @@ func _sync_throw_bomb(bomb_name: String, bomb_pos: Vector2, throw_force: Vector2
|
||||
bomb.get_node("Sprite2D").visible = true
|
||||
print(name, " (synced) threw bomb from ", bomb_pos)
|
||||
|
||||
@rpc("any_peer", "reliable")
|
||||
func _sync_bomb_collected(bomb_name: String):
|
||||
# Another peer collected this bomb – remove our copy so it doesn't keep exploding
|
||||
# Collector already removed and added to inventory locally; we just free our instance
|
||||
var bombs = get_tree().get_nodes_in_group("attack_bomb")
|
||||
for b in bombs:
|
||||
if b.name == bomb_name and is_instance_valid(b):
|
||||
b.queue_free()
|
||||
return
|
||||
|
||||
@rpc("any_peer", "reliable")
|
||||
func _sync_throw(obj_name: String, throw_pos: Vector2, force: Vector2, thrower_name: String):
|
||||
# Sync throw to all clients (RPC sender already threw on their side)
|
||||
@@ -5358,6 +5574,28 @@ func take_damage(amount: float, attacker_position: Vector2, is_burn_damage: bool
|
||||
_rpc_to_ready_peers("_sync_damage", [0.0, attacker_position, false, false, true]) # is_dodged = true
|
||||
return # No damage taken, exit early
|
||||
|
||||
# Check for shield block (would have hit; enemy attack from blocked direction; no burn)
|
||||
if not is_burn_damage and is_shielding and shield_block_cooldown_timer <= 0.0:
|
||||
var dir_to_attacker = (attacker_position - global_position).normalized()
|
||||
if dir_to_attacker.length() < 0.01:
|
||||
dir_to_attacker = Vector2.RIGHT
|
||||
var block_dir = shield_block_direction.normalized() if shield_block_direction.length() > 0.01 else Vector2.DOWN
|
||||
var dot = block_dir.dot(dir_to_attacker)
|
||||
if dot > 0.5: # Lenient: attacker in front (~60° cone)
|
||||
# Blocked: no damage, small knockback, BLOCKED notification, cooldown
|
||||
shield_block_cooldown_timer = shield_block_cooldown_duration
|
||||
var direction_from_attacker = (global_position - attacker_position).normalized()
|
||||
velocity = direction_from_attacker * 90.0 # Small knockback
|
||||
is_knocked_back = true
|
||||
knockback_time = 0.0
|
||||
if has_node("SfxBlockWithShield"):
|
||||
$SfxBlockWithShield.play()
|
||||
_show_damage_number(0.0, attacker_position, false, false, false, true) # is_blocked = true
|
||||
if multiplayer.has_multiplayer_peer() and can_send_rpcs and is_inside_tree():
|
||||
_rpc_to_ready_peers("_sync_damage", [0.0, attacker_position, false, false, false, true])
|
||||
print(name, " BLOCKED attack from direction ", dir_to_attacker)
|
||||
return
|
||||
|
||||
# If taking damage while holding something, drop/throw immediately
|
||||
if held_object:
|
||||
if is_lifting:
|
||||
@@ -5457,6 +5695,9 @@ func _die():
|
||||
velocity = Vector2.ZERO
|
||||
is_knocked_back = false
|
||||
damage_direction_lock_timer = 0.0
|
||||
is_shielding = false
|
||||
was_shielding_last_frame = false
|
||||
_update_shield_visibility()
|
||||
|
||||
# CRITICAL: Release any held object/player BEFORE dying to restore their collision layers
|
||||
if held_object:
|
||||
@@ -5528,7 +5769,7 @@ func _die():
|
||||
fade_tween.set_parallel(true)
|
||||
for sprite_layer in [sprite_body, sprite_boots, sprite_armour, sprite_facial_hair,
|
||||
sprite_hair, sprite_eyes, sprite_eyelashes, sprite_addons,
|
||||
sprite_headgear, sprite_weapon, shadow]:
|
||||
sprite_headgear, sprite_shield, sprite_shield_holding, sprite_weapon, shadow]:
|
||||
if sprite_layer:
|
||||
fade_tween.tween_property(sprite_layer, "modulate:a", 0.0, 0.5)
|
||||
|
||||
@@ -5601,7 +5842,7 @@ func _respawn():
|
||||
# Restore visibility (fade all sprite layers back in)
|
||||
for sprite_layer in [sprite_body, sprite_boots, sprite_armour, sprite_facial_hair,
|
||||
sprite_hair, sprite_eyes, sprite_eyelashes, sprite_addons,
|
||||
sprite_headgear, sprite_weapon, shadow]:
|
||||
sprite_headgear, sprite_shield, sprite_shield_holding, sprite_weapon, shadow]:
|
||||
if sprite_layer:
|
||||
sprite_layer.modulate.a = 1.0
|
||||
|
||||
@@ -5741,7 +5982,7 @@ func _sync_respawn(spawn_pos: Vector2):
|
||||
# Restore visibility
|
||||
for sprite_layer in [sprite_body, sprite_boots, sprite_armour, sprite_facial_hair,
|
||||
sprite_hair, sprite_eyes, sprite_eyelashes, sprite_addons,
|
||||
sprite_headgear, sprite_weapon, shadow]:
|
||||
sprite_headgear, sprite_shield, sprite_shield_holding, sprite_weapon, shadow]:
|
||||
if sprite_layer:
|
||||
sprite_layer.modulate.a = 1.0
|
||||
|
||||
@@ -5867,7 +6108,7 @@ func _sync_race_and_stats(race: String, base_stats: Dictionary):
|
||||
|
||||
"Human":
|
||||
character_stats.setEars(0)
|
||||
# Give Human starting spellbook (Tome of Flames) to remote players
|
||||
# Give Human (Wizard) starting spellbook (Tome of Flames) and Hat to remote players
|
||||
# (Authority players get this in _setup_player_appearance)
|
||||
# Check if equipment is missing - give it regardless of whether race changed
|
||||
if not is_multiplayer_authority():
|
||||
@@ -5875,17 +6116,25 @@ func _sync_race_and_stats(race: String, base_stats: Dictionary):
|
||||
if character_stats.equipment["offhand"] == null:
|
||||
needs_equipment = true
|
||||
else:
|
||||
# Check if offhand is not Tome of Flames
|
||||
var offhand = character_stats.equipment["offhand"]
|
||||
if not offhand or offhand.item_name != "Tome of Flames":
|
||||
needs_equipment = true
|
||||
if character_stats.equipment["headgear"] == null:
|
||||
needs_equipment = true
|
||||
else:
|
||||
var headgear = character_stats.equipment["headgear"]
|
||||
if not headgear or headgear.item_name != "Hat":
|
||||
needs_equipment = true
|
||||
|
||||
if needs_equipment:
|
||||
var starting_tome = ItemDatabase.create_item("tome_of_flames")
|
||||
if starting_tome:
|
||||
character_stats.equipment["offhand"] = starting_tome
|
||||
_apply_appearance_to_sprites()
|
||||
print("Human player ", name, " (remote) received Tome of Flames via race sync")
|
||||
var starting_hat = ItemDatabase.create_item("hat")
|
||||
if starting_hat:
|
||||
character_stats.equipment["headgear"] = starting_hat
|
||||
_apply_appearance_to_sprites()
|
||||
print("Human player ", name, " (remote) received Tome of Flames and Hat via race sync")
|
||||
|
||||
_:
|
||||
character_stats.setEars(0)
|
||||
@@ -5990,6 +6239,25 @@ func _sync_inventory(inventory_data: Array):
|
||||
character_stats.character_changed.emit(character_stats)
|
||||
print(name, " inventory synced: ", character_stats.inventory.size(), " items")
|
||||
|
||||
@rpc("any_peer", "reliable")
|
||||
func _apply_inventory_and_equipment_from_server(inventory_data: Array, equipment_data: Dictionary):
|
||||
# Joiner receives inventory+equipment push from server after loot pickup (or other server-driven change).
|
||||
# Always apply – no authority rejection. Used only when server adds items to a remote player.
|
||||
if multiplayer.is_server():
|
||||
return
|
||||
if not character_stats:
|
||||
return
|
||||
character_stats.inventory.clear()
|
||||
for item_data in inventory_data:
|
||||
if item_data != null:
|
||||
character_stats.inventory.append(Item.new(item_data))
|
||||
for slot_name in character_stats.equipment.keys():
|
||||
var item_data = equipment_data.get(slot_name, null)
|
||||
character_stats.equipment[slot_name] = Item.new(item_data) if item_data != null else null
|
||||
_apply_appearance_to_sprites()
|
||||
character_stats.character_changed.emit(character_stats)
|
||||
print(name, " inventory+equipment applied from server: ", character_stats.inventory.size(), " items")
|
||||
|
||||
func heal(amount: float):
|
||||
if is_dead:
|
||||
return
|
||||
@@ -6030,9 +6298,9 @@ func _sync_keys(new_key_count: int):
|
||||
keys = new_key_count
|
||||
|
||||
@rpc("authority", "reliable")
|
||||
func _show_damage_number(amount: float, from_position: Vector2, is_crit: bool = false, is_miss: bool = false, is_dodged: bool = false):
|
||||
func _show_damage_number(amount: float, from_position: Vector2, is_crit: bool = false, is_miss: bool = false, is_dodged: bool = false, is_blocked: bool = false):
|
||||
# Show damage number (red, using dmg_numbers.png font) above player
|
||||
# Show even if amount is 0 for MISS/DODGED
|
||||
# Show even if amount is 0 for MISS/DODGED/BLOCKED
|
||||
|
||||
var damage_number_scene = preload("res://scenes/damage_number.tscn")
|
||||
if not damage_number_scene:
|
||||
@@ -6049,6 +6317,9 @@ func _show_damage_number(amount: float, from_position: Vector2, is_crit: bool =
|
||||
elif is_miss:
|
||||
damage_label.label = "MISS"
|
||||
damage_label.color = Color.GRAY
|
||||
elif is_blocked:
|
||||
damage_label.label = "BLOCKED"
|
||||
damage_label.color = Color(0.4, 0.65, 1.0) # Light blue
|
||||
else:
|
||||
damage_label.label = str(int(amount))
|
||||
damage_label.color = Color.ORANGE if is_crit else Color.RED
|
||||
@@ -6138,13 +6409,23 @@ func _on_level_up_stats(stats_increased: Array):
|
||||
base_y_offset -= y_spacing
|
||||
|
||||
@rpc("any_peer", "reliable")
|
||||
func _sync_damage(_amount: float, attacker_position: Vector2, is_crit: bool = false, is_miss: bool = false, is_dodged: bool = false):
|
||||
func _sync_damage(_amount: float, attacker_position: Vector2, is_crit: bool = false, is_miss: bool = false, is_dodged: bool = false, is_blocked: bool = false):
|
||||
# This RPC only syncs visual effects, not damage application
|
||||
# (damage is already applied via rpc_take_damage)
|
||||
if not is_multiplayer_authority():
|
||||
# If dodged, only show dodge text, no other effects
|
||||
if is_dodged:
|
||||
_show_damage_number(0.0, attacker_position, false, false, true)
|
||||
_show_damage_number(0.0, attacker_position, false, false, true, false)
|
||||
return
|
||||
# If blocked, show BLOCKED, small knockback, block sound; no damage effects
|
||||
if is_blocked:
|
||||
var block_knock_dir = (global_position - attacker_position).normalized()
|
||||
velocity = block_knock_dir * 90.0
|
||||
is_knocked_back = true
|
||||
knockback_time = 0.0
|
||||
if has_node("SfxBlockWithShield"):
|
||||
$SfxBlockWithShield.play()
|
||||
_show_damage_number(0.0, attacker_position, false, false, false, true)
|
||||
return
|
||||
|
||||
# Play damage sound and effects (rate limited to prevent spam when tab becomes active)
|
||||
@@ -6181,7 +6462,7 @@ func _sync_damage(_amount: float, attacker_position: Vector2, is_crit: bool = fa
|
||||
tween.tween_property(sprite_body, "modulate", Color.WHITE, 0.1)
|
||||
|
||||
# Show damage number
|
||||
_show_damage_number(_amount, attacker_position, is_crit, is_miss, false)
|
||||
_show_damage_number(_amount, attacker_position, is_crit, is_miss, false, false)
|
||||
|
||||
func on_grabbed(by_player):
|
||||
print(name, " grabbed by ", by_player.name)
|
||||
|
||||
Reference in New Issue
Block a user