started working on spellbook

This commit is contained in:
2026-02-08 16:48:21 +01:00
parent 9e2516a5ab
commit 82219474ec
28 changed files with 2009 additions and 151 deletions

View File

@@ -84,7 +84,8 @@ var is_attacking: bool = false
var is_charging_bow: bool = false # True when holding attack with bow+arrows
var bow_charge_start_time: float = 0.0
var bow_charge_percentage: float = 1.0 # 0.5, 0.75, or 1.0 based on charge time
var is_charging_spell: bool = false # True when holding grab with spellbook
var is_charging_spell: bool = false # True when holding grab with spellbook or hotkey 1/2/3
var spell_charge_hotkey_slot: String = "" # "1", "2", or "3" when charging from hotkey (else "")
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
@@ -116,6 +117,9 @@ var shield_block_cooldown_timer: float = 0.0 # After blocking, cannot block agai
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 _key1_was_pressed: bool = false
var _key2_was_pressed: bool = false
var _key3_was_pressed: bool = false
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)
@@ -124,6 +128,8 @@ var staff_projectile_scene = preload("res://scenes/attack_staff.tscn") # Staff m
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 water_bubble_spell_scene = preload("res://scenes/attack_spell_water_bubble.tscn") # Water bubble projectile
var earth_spike_spell_scene = preload("res://scenes/attack_spell_earth_spike.tscn") # Earth spike (like 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 attack_punch_scene = preload("res://scenes/attack_punch.tscn") # Unarmed punch
@@ -881,11 +887,11 @@ func _setup_player_appearance():
character_stats.add_item(debug_sword)
print("Dwarf player ", name, " spawned with 5 bombs and debug axe/dagger/sword in inventory")
# Give Human race (Wizard) starting spellbook (Tome of Flames), Tome of Healing, Tome of Frostspike, and Hat
# Give Human race (Wizard) starting tomes in inventory; use (F) each to learn spell (spell book system)
if selected_race == "Human":
var starting_tome = ItemDatabase.create_item("tome_of_flames")
if starting_tome:
character_stats.equipment["offhand"] = starting_tome
character_stats.add_item(starting_tome)
var tome_healing = ItemDatabase.create_item("tome_of_healing")
if tome_healing:
character_stats.add_item(tome_healing)
@@ -2752,6 +2758,7 @@ func _handle_interactions():
# Start disarming - cancel any spell charging
if is_charging_spell:
is_charging_spell = false
spell_charge_hotkey_slot = ""
current_spell_element = "fire"
spell_incantation_played = false
_stop_spell_charge_particles()
@@ -2784,35 +2791,105 @@ func _handle_interactions():
# No nearby trap - reset disarming flag
is_disarming = false
# 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:
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:
# Spell hotkey key state (for 1/2/3 casting from learnt spells)
var k1 = Input.is_key_pressed(KEY_1)
var k2 = Input.is_key_pressed(KEY_2)
var k3 = Input.is_key_pressed(KEY_3)
var key1_just_pressed = k1 and not _key1_was_pressed
var key2_just_pressed = k2 and not _key2_was_pressed
var key3_just_pressed = k3 and not _key3_was_pressed
var key1_just_released = _key1_was_pressed and not k1
var key2_just_released = _key2_was_pressed and not k2
var key3_just_released = _key3_was_pressed and not k3
_key1_was_pressed = k1
_key2_was_pressed = k2
_key3_was_pressed = k3
# Check for spell casting (Tome of Flames, Frostspike, or Healing) — from offhand OR hotkey 1/2/3
var offhand_item = character_stats.equipment["offhand"] if (character_stats and character_stats.equipment.has("offhand")) else null
var spell_from_offhand = offhand_item and offhand_item.weapon_type == Item.WeaponType.SPELLBOOK
var spell_from_hotkey = character_stats and character_stats.learnt_spells.size() > 0 and spell_charge_hotkey_slot != ""
if character_stats and (spell_from_offhand or spell_from_hotkey or character_stats.learnt_spells.size() > 0):
# Start charge from hotkey 1/2/3 if that key just pressed and slot has a learnt spell
if not is_charging_spell and not spell_from_offhand:
var slot_pressed = ""
var spell_id = ""
if key1_just_pressed and character_stats.spell_hotkeys.get("1", "") in character_stats.learnt_spells:
slot_pressed = "1"
spell_id = character_stats.spell_hotkeys.get("1", "")
elif key2_just_pressed and character_stats.spell_hotkeys.get("2", "") in character_stats.learnt_spells:
slot_pressed = "2"
spell_id = character_stats.spell_hotkeys.get("2", "")
elif key3_just_pressed and character_stats.spell_hotkeys.get("3", "") in character_stats.learnt_spells:
slot_pressed = "3"
spell_id = character_stats.spell_hotkeys.get("3", "")
if slot_pressed != "" and spell_id != "":
var game_world = get_tree().get_first_node_in_group("game_world")
var target_pos = Vector2.ZERO
var heal_target: Node = null
if (is_fire or is_frost) and game_world and game_world.has_method("get_grid_locked_cursor_position"):
var is_heal = (spell_id == "healing")
if (spell_id == "flames" or spell_id == "frostspike" or spell_id == "earth_spike") 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)
var can_start_charge = is_heal or has_valid_target
# 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:
var has_valid_target = ((spell_id == "flames" or spell_id == "frostspike" or spell_id == "earth_spike") and target_pos != Vector2.ZERO) or (is_heal and heal_target != null) or (spell_id == "water_bubble")
var can_start = is_heal or has_valid_target
var mana_ok = (spell_id == "flames" or spell_id == "frostspike" or spell_id == "water_bubble" or spell_id == "earth_spike") and character_stats.mp >= 15.0 or (spell_id == "healing" and character_stats.mp >= 20.0)
if can_start and mana_ok and not nearby_grabbable_body and not is_lifting and not held_object:
spell_charge_hotkey_slot = slot_pressed
is_charging_spell = true
current_spell_element = "healing" if is_heal else ("frost" if spell_id == "frostspike" else ("water" if spell_id == "water_bubble" else ("earth" if spell_id == "earth_spike" else "fire")))
spell_charge_start_time = Time.get_ticks_msec() / 1000.0
spell_incantation_played = false
_start_spell_charge_particles()
_start_spell_charge_incantation()
if has_node("SfxSpellCharge"):
$SfxSpellCharge.play()
if multiplayer.has_multiplayer_peer():
_sync_spell_charge_start.rpc()
elif not mana_ok and is_local_player:
_show_not_enough_mana_text()
if character_stats and (spell_from_offhand or spell_from_hotkey):
var is_fire = false
var is_frost = false
var is_heal = false
var is_water_bubble = false
var is_earth_spike = false
if spell_charge_hotkey_slot != "":
var sid = character_stats.spell_hotkeys.get(spell_charge_hotkey_slot, "")
is_fire = (sid == "flames")
is_frost = (sid == "frostspike")
is_heal = (sid == "healing")
is_water_bubble = (sid == "water_bubble")
is_earth_spike = (sid == "earth_spike")
else:
is_fire = offhand_item.item_name == "Tome of Flames"
is_frost = offhand_item.item_name == "Tome of Frostspike"
is_heal = offhand_item.item_name == "Tome of Healing"
is_water_bubble = offhand_item.item_name == "Tome of Water Bubble"
is_earth_spike = offhand_item.item_name == "Tome of Earth Spike"
if is_fire or is_frost or is_heal or is_water_bubble or is_earth_spike:
var game_world = get_tree().get_first_node_in_group("game_world")
var target_pos = Vector2.ZERO
var heal_target: Node = null
if (is_fire or is_frost or is_earth_spike) 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 or is_earth_spike) and target_pos != Vector2.ZERO) or (is_heal and heal_target != null) or is_water_bubble
var can_start_charge = is_heal or has_valid_target
# Reuse grabbable from single query above (avoids second get_overlapping_bodies)
var nearby_grabbable = nearby_grabbable_body
var hotkey_released = (spell_charge_hotkey_slot == "1" and key1_just_released) or (spell_charge_hotkey_slot == "2" and key2_just_released) or (spell_charge_hotkey_slot == "3" and key3_just_released)
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
var has_enough_mana = false
if character_stats:
if is_fire:
has_enough_mana = character_stats.mp >= 15.0 # Flame spell cost
elif is_frost:
has_enough_mana = character_stats.mp >= 15.0 # Frostspike spell cost
if is_fire or is_frost or is_water_bubble or is_earth_spike:
has_enough_mana = character_stats.mp >= 15.0
else:
has_enough_mana = character_stats.mp >= 20.0 # Heal spell cost
@@ -2823,7 +2900,7 @@ func _handle_interactions():
return
is_charging_spell = true
current_spell_element = "healing" if is_heal else ("frost" if is_frost else "fire")
current_spell_element = "healing" if is_heal else ("frost" if is_frost else ("water" if is_water_bubble else ("earth" if is_earth_spike else "fire")))
spell_charge_start_time = Time.get_ticks_msec() / 1000.0
spell_incantation_played = false
_start_spell_charge_particles()
@@ -2834,94 +2911,11 @@ func _handle_interactions():
_sync_spell_charge_start.rpc()
just_grabbed_this_frame = false
return
elif grab_just_released and is_charging_spell:
var charge_time = (Time.get_ticks_msec() / 1000.0) - spell_charge_start_time
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()
_set_animation("IDLE")
if has_node("SfxSpellCharge"):
$SfxSpellCharge.stop()
if has_node("SfxSpellIncantation"):
$SfxSpellIncantation.stop()
if multiplayer.has_multiplayer_peer():
_sync_spell_charge_end.rpc()
just_grabbed_this_frame = false
return
var is_fully_charged = charge_time >= spell_charge_duration
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)
if has_valid_target and is_fully_charged:
# Check if player has enough mana before casting
var has_enough_mana = false
if character_stats:
if is_fire:
has_enough_mana = character_stats.mp >= 15.0 # Flame spell cost
elif is_frost:
has_enough_mana = character_stats.mp >= 15.0 # Frostspike spell cost
else:
has_enough_mana = character_stats.mp >= 20.0 # Heal spell cost
if has_enough_mana:
if is_fire:
_cast_flame_spell(target_pos)
elif is_frost:
_cast_frostspike_spell(target_pos)
else:
_cast_heal_spell(heal_target)
else:
is_charging_spell = false
current_spell_element = "fire"
spell_incantation_played = false
_stop_spell_charge_particles()
_stop_spell_charge_incantation()
_clear_spell_charge_tint()
_set_animation("IDLE")
if has_node("SfxSpellCharge"):
$SfxSpellCharge.stop()
if has_node("SfxSpellIncantation"):
$SfxSpellIncantation.stop()
if multiplayer.has_multiplayer_peer():
_sync_spell_charge_end.rpc()
return
_set_animation("FINISH_SPELL")
movement_lock_timer = SPELL_CAST_LOCK_DURATION
is_charging_spell = false
current_spell_element = "fire"
spell_incantation_played = false
_stop_spell_charge_particles()
_stop_spell_charge_incantation()
_clear_spell_charge_tint()
if has_node("SfxSpellCharge"):
$SfxSpellCharge.stop()
else:
is_charging_spell = false
current_spell_element = "fire"
spell_incantation_played = false
_stop_spell_charge_particles()
_stop_spell_charge_incantation()
_clear_spell_charge_tint()
_set_animation("IDLE")
if has_node("SfxSpellCharge"):
$SfxSpellCharge.stop()
if has_node("SfxSpellIncantation"):
$SfxSpellIncantation.stop()
if multiplayer.has_multiplayer_peer():
_sync_spell_charge_end.rpc()
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)):
# Don't cancel heal charge for no target (allow hover over enemy/wall); cancel fire/frost
elif (grab_just_released or hotkey_released) and is_charging_spell:
var charge_time = (Time.get_ticks_msec() / 1000.0) - spell_charge_start_time
if charge_time < 0.2:
is_charging_spell = false
spell_charge_hotkey_slot = ""
current_spell_element = "fire"
spell_incantation_played = false
_stop_spell_charge_particles()
@@ -2934,7 +2928,103 @@ func _handle_interactions():
$SfxSpellIncantation.stop()
if multiplayer.has_multiplayer_peer():
_sync_spell_charge_end.rpc()
print(name, " spell charge cancelled (no target / lift / held)")
just_grabbed_this_frame = false
return
var is_fully_charged = charge_time >= spell_charge_duration
if (is_fire or is_frost or is_earth_spike) 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 or is_earth_spike) and target_pos != Vector2.ZERO) or (is_heal and heal_target != null) or is_water_bubble
if has_valid_target and is_fully_charged:
# Check if player has enough mana before casting
var has_enough_mana = false
if character_stats:
if is_fire or is_frost or is_water_bubble or is_earth_spike:
has_enough_mana = character_stats.mp >= 15.0
else:
has_enough_mana = character_stats.mp >= 20.0 # Heal spell cost
if has_enough_mana:
if is_fire:
_cast_flame_spell(target_pos)
elif is_frost:
_cast_frostspike_spell(target_pos)
elif is_water_bubble:
var dir = Vector2.ZERO
if game_world and game_world.has_method("get_grid_locked_cursor_position"):
var cursor_pos = game_world.get_grid_locked_cursor_position()
dir = (cursor_pos - global_position).normalized()
if dir == Vector2.ZERO:
dir = Vector2.RIGHT.rotated(rotation)
_cast_water_bubble_spell(dir)
elif is_earth_spike:
_cast_earth_spike_spell(target_pos)
else:
_cast_heal_spell(heal_target)
else:
is_charging_spell = false
spell_charge_hotkey_slot = ""
current_spell_element = "fire"
spell_incantation_played = false
_stop_spell_charge_particles()
_stop_spell_charge_incantation()
_clear_spell_charge_tint()
_set_animation("IDLE")
if has_node("SfxSpellCharge"):
$SfxSpellCharge.stop()
if has_node("SfxSpellIncantation"):
$SfxSpellIncantation.stop()
if multiplayer.has_multiplayer_peer():
_sync_spell_charge_end.rpc()
return
_set_animation("FINISH_SPELL")
movement_lock_timer = SPELL_CAST_LOCK_DURATION
is_charging_spell = false
spell_charge_hotkey_slot = ""
current_spell_element = "fire"
spell_incantation_played = false
_stop_spell_charge_particles()
_stop_spell_charge_incantation()
_clear_spell_charge_tint()
if has_node("SfxSpellCharge"):
$SfxSpellCharge.stop()
else:
is_charging_spell = false
spell_charge_hotkey_slot = ""
current_spell_element = "fire"
spell_incantation_played = false
_stop_spell_charge_particles()
_stop_spell_charge_incantation()
_clear_spell_charge_tint()
_set_animation("IDLE")
if has_node("SfxSpellCharge"):
$SfxSpellCharge.stop()
if has_node("SfxSpellIncantation"):
$SfxSpellIncantation.stop()
if multiplayer.has_multiplayer_peer():
_sync_spell_charge_end.rpc()
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)):
# Don't cancel heal charge for no target (allow hover over enemy/wall); cancel fire/frost
is_charging_spell = false
spell_charge_hotkey_slot = ""
current_spell_element = "fire"
spell_incantation_played = false
_stop_spell_charge_particles()
_stop_spell_charge_incantation()
_clear_spell_charge_tint()
_set_animation("IDLE")
if has_node("SfxSpellCharge"):
$SfxSpellCharge.stop()
if has_node("SfxSpellIncantation"):
$SfxSpellIncantation.stop()
if multiplayer.has_multiplayer_peer():
_sync_spell_charge_end.rpc()
print(name, " spell charge cancelled (no target / lift / held)")
# Check for bomb usage (if bomb equipped in offhand)
# Also check if we're already holding a bomb - if so, skip normal grab handling
@@ -2945,8 +3035,8 @@ func _handle_interactions():
is_holding_bomb = true
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.BOMB and offhand_item.quantity > 0:
var offhand_equipped = character_stats.equipment["offhand"]
if offhand_equipped and offhand_equipped.weapon_type == Item.WeaponType.BOMB and offhand_equipped.quantity > 0:
# Check if there's a grabbable object nearby - prioritize grabbing over bomb
var nearby_grabbable = null
if grab_area:
@@ -2981,9 +3071,9 @@ func _handle_interactions():
else:
# Human/Elf: Throw bomb or drop next to player
# Consume one bomb
offhand_item.quantity -= 1
var remaining = offhand_item.quantity
if offhand_item.quantity <= 0:
offhand_equipped.quantity -= 1
var remaining = offhand_equipped.quantity
if offhand_equipped.quantity <= 0:
character_stats.equipment["offhand"] = null
if character_stats:
character_stats.character_changed.emit(character_stats)
@@ -4596,6 +4686,74 @@ func _sync_frostspike_spell(target_position: Vector2, spell_damage: float):
get_parent().add_child(frost)
print(name, " (synced) spawned frostspike at ", target_position)
func _cast_water_bubble_spell(direction: Vector2):
if not water_bubble_spell_scene or not is_multiplayer_authority():
return
const MANA_COST = 15.0
if not character_stats or not character_stats.use_mana(MANA_COST):
if is_local_player:
_show_not_enough_mana_text()
return
var spell_damage = 15.0
if character_stats:
spell_damage = character_stats.damage * 0.75
var bubble = water_bubble_spell_scene.instantiate()
bubble.setup(global_position, direction, self, spell_damage)
get_parent().add_child(bubble)
if multiplayer.has_multiplayer_peer() and can_send_rpcs and is_inside_tree():
_rpc_to_ready_peers("_sync_water_bubble_spell", [direction, spell_damage])
print(name, " cast water bubble")
@rpc("any_peer", "reliable")
func _sync_water_bubble_spell(direction: Vector2, spell_damage: float):
if is_multiplayer_authority():
return
if not water_bubble_spell_scene:
return
var bubble = water_bubble_spell_scene.instantiate()
bubble.setup(global_position, direction, self, spell_damage)
get_parent().add_child(bubble)
print(name, " (synced) spawned water bubble")
func _cast_earth_spike_spell(target_position: Vector2):
if not earth_spike_spell_scene or not is_multiplayer_authority():
return
const MANA_COST = 15.0
if not character_stats or not character_stats.use_mana(MANA_COST):
if is_local_player:
_show_not_enough_mana_text()
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:
if character_stats:
character_stats.restore_mana(MANA_COST)
return
var spell_damage = 15.0
if character_stats:
spell_damage = character_stats.damage * 0.75
var earth = earth_spike_spell_scene.instantiate()
earth.setup(valid_target_pos, self, spell_damage, true)
get_parent().add_child(earth)
if multiplayer.has_multiplayer_peer() and can_send_rpcs and is_inside_tree():
_rpc_to_ready_peers("_sync_earth_spike_spell", [valid_target_pos, spell_damage])
print(name, " cast earth spike at ", valid_target_pos)
@rpc("any_peer", "reliable")
func _sync_earth_spike_spell(target_position: Vector2, spell_damage: float):
if is_multiplayer_authority():
return
if not earth_spike_spell_scene:
return
var earth = earth_spike_spell_scene.instantiate()
earth.setup(target_position, self, spell_damage, true)
get_parent().add_child(earth)
print(name, " (synced) spawned earth spike at ", target_position)
func _cast_heal_spell(target: Node):
if not target or not is_instance_valid(target):
return
@@ -4926,9 +5084,9 @@ func _stop_spell_charge_particles():
func _start_spell_charge_incantation():
spell_incantation_fire_ready_shown = false
if has_node("AnimationIncantation"):
if _is_healing_spell():
if _is_healing_spell() or current_spell_element == "healing":
$AnimationIncantation.play("healing_charging")
elif _is_frost_spell():
elif _is_frost_spell() or current_spell_element == "frost" or current_spell_element == "water":
$AnimationIncantation.play("frost_charging")
else:
$AnimationIncantation.play("fire_charging")
@@ -4937,9 +5095,9 @@ func _update_spell_charge_incantation(charge_progress: float):
if not has_node("AnimationIncantation"):
return
if charge_progress >= 1.0 and not spell_incantation_fire_ready_shown:
if _is_healing_spell():
if _is_healing_spell() or current_spell_element == "healing":
$AnimationIncantation.play("healing_ready")
elif _is_frost_spell():
elif _is_frost_spell() or current_spell_element == "frost" or current_spell_element == "water":
$AnimationIncantation.play("frost_ready")
else:
$AnimationIncantation.play("fire_ready")
@@ -4955,10 +5113,14 @@ func _apply_spell_charge_tint():
if not is_charging_spell:
return
var tint = spell_charge_tint
if _is_healing_spell():
if _is_healing_spell() or current_spell_element == "healing":
tint = Color(0.25, 2.0, 0.3, 2.0) # Green pulse for healing
elif _is_frost_spell():
elif _is_frost_spell() or current_spell_element == "frost":
tint = Color(0.25, 0.5, 2.0, 2.0) # Blue pulse for frost
elif current_spell_element == "water":
tint = Color(0.25, 0.6, 2.0, 2.0) # Blue pulse for water bubble
elif current_spell_element == "earth":
tint = Color(0.9, 0.55, 0.2, 2.0) # Brown/orange pulse for earth spike
var sprites = [
{"sprite": sprite_body, "name": "body"},
{"sprite": sprite_boots, "name": "boots"},
@@ -5196,6 +5358,7 @@ func _sync_spell_charge_start():
func _sync_spell_charge_end():
if not is_multiplayer_authority():
is_charging_spell = false
spell_charge_hotkey_slot = ""
current_spell_element = "fire"
spell_incantation_played = false
_stop_spell_charge_particles()
@@ -6421,6 +6584,7 @@ func take_damage(amount: float, attacker_position: Vector2, is_burn_damage: bool
if should_cancel:
is_charging_spell = false
spell_charge_hotkey_slot = ""
spell_incantation_played = false
_stop_spell_charge_particles()
_stop_spell_charge_incantation()
@@ -7277,15 +7441,13 @@ func _sync_race_and_stats(race: String, base_stats: Dictionary):
"Human":
character_stats.setEars(0)
# Give Human (Wizard) starting tomes and hat to remote players ONLY when slots are null (initial sync)
# Never overwrite existing equipment - preserves loadout across level transitions
# Give Human (Wizard) starting tomes and hat to remote players ONLY when headgear empty (initial sync)
if not is_multiplayer_authority():
var offhand_empty = character_stats.equipment["offhand"] == null
var headgear_empty = character_stats.equipment["headgear"] == null
if offhand_empty and headgear_empty:
if headgear_empty:
var starting_tome = ItemDatabase.create_item("tome_of_flames")
if starting_tome:
character_stats.equipment["offhand"] = starting_tome
character_stats.add_item(starting_tome)
var tome_healing = ItemDatabase.create_item("tome_of_healing")
if tome_healing:
character_stats.add_item(tome_healing)