made some adjustments to brights of rooms
This commit is contained in:
@@ -106,7 +106,7 @@ script = ExtResource("5")
|
|||||||
[node name="CanvasModulate" type="CanvasModulate" parent="." unique_id=948490815]
|
[node name="CanvasModulate" type="CanvasModulate" parent="." unique_id=948490815]
|
||||||
light_mask = 1048575
|
light_mask = 1048575
|
||||||
visibility_layer = 1048575
|
visibility_layer = 1048575
|
||||||
color = Color(0.4140625, 0.4140625, 0.4140625, 1)
|
color = Color(0.69140625, 0.69140625, 0.69140625, 1)
|
||||||
|
|
||||||
[node name="AudioStreamPlayer2D" type="AudioStreamPlayer2D" parent="." unique_id=1141138343]
|
[node name="AudioStreamPlayer2D" type="AudioStreamPlayer2D" parent="." unique_id=1141138343]
|
||||||
stream = ExtResource("6_6c6v5")
|
stream = ExtResource("6_6c6v5")
|
||||||
|
|||||||
@@ -573,12 +573,12 @@ scale = Vector2(1.984375, 2.0937502)
|
|||||||
texture = SubResource("GradientTexture2D_wnwbv")
|
texture = SubResource("GradientTexture2D_wnwbv")
|
||||||
|
|
||||||
[node name="ConeLight" type="PointLight2D" parent="." unique_id=120780131]
|
[node name="ConeLight" type="PointLight2D" parent="." unique_id=120780131]
|
||||||
blend_mode = 2
|
blend_mode = 0
|
||||||
shadow_enabled = true
|
shadow_enabled = true
|
||||||
|
|
||||||
[node name="PointLight2D" type="PointLight2D" parent="." unique_id=1250823818]
|
[node name="PointLight2D" type="PointLight2D" parent="." unique_id=1250823818]
|
||||||
position = Vector2(-1, 0)
|
position = Vector2(-1, 0)
|
||||||
blend_mode = 2
|
blend_mode = 0
|
||||||
shadow_enabled = true
|
shadow_enabled = true
|
||||||
texture = SubResource("GradientTexture2D_f1ej7")
|
texture = SubResource("GradientTexture2D_f1ej7")
|
||||||
|
|
||||||
@@ -745,7 +745,7 @@ panning_strength = 1.11
|
|||||||
visible = false
|
visible = false
|
||||||
rotation = 3.1869712
|
rotation = 3.1869712
|
||||||
energy = 0.13
|
energy = 0.13
|
||||||
blend_mode = 2
|
blend_mode = 0
|
||||||
shadow_enabled = true
|
shadow_enabled = true
|
||||||
max_distance = 100.0
|
max_distance = 100.0
|
||||||
|
|
||||||
|
|||||||
@@ -478,7 +478,8 @@ func _show_damage_number(amount: float, from_position: Vector2, is_critical: boo
|
|||||||
damage_label.color = Color.GRAY
|
damage_label.color = Color.GRAY
|
||||||
else:
|
else:
|
||||||
damage_label.label = str(int(amount))
|
damage_label.label = str(int(amount))
|
||||||
damage_label.color = Color.ORANGE if is_critical else Color.RED
|
damage_label.color = Color(1.0, 0.6, 0.2) if is_critical else Color(1.0, 0.35, 0.35) # Bright orange / bright red
|
||||||
|
damage_label.z_index = 5
|
||||||
|
|
||||||
# Calculate direction from attacker (slight upward variation)
|
# Calculate direction from attacker (slight upward variation)
|
||||||
var direction_from_attacker = (global_position - from_position).normalized()
|
var direction_from_attacker = (global_position - from_position).normalized()
|
||||||
|
|||||||
@@ -63,6 +63,15 @@ var cached_corridor_player_tile: Vector2i = Vector2i(-1, -1)
|
|||||||
var cached_corridor_allowed_room_ids: Dictionary = {}
|
var cached_corridor_allowed_room_ids: Dictionary = {}
|
||||||
var was_in_corridor: bool = false # Track previous corridor state to detect transitions
|
var was_in_corridor: bool = false # Track previous corridor state to detect transitions
|
||||||
var last_corridor_fog_update: float = 0.0 # Time of last fog update in corridor
|
var last_corridor_fog_update: float = 0.0 # Time of last fog update in corridor
|
||||||
|
|
||||||
|
# Torch-based CanvasModulate: only recalc on room/corridor transition, lerp over time
|
||||||
|
var _torch_darken_initialized: bool = false
|
||||||
|
var _torch_darken_in_room_last: bool = false
|
||||||
|
var _torch_darken_last_room_id: String = ""
|
||||||
|
var _torch_darken_target_scale: float = 1.0
|
||||||
|
var _torch_darken_current_scale: float = 1.0
|
||||||
|
const _TORCH_DARKEN_LERP_SPEED: float = 4.0
|
||||||
|
const _TORCH_DARKEN_MIN_SCALE: float = 0.15 # Never go below this; allows player light to punch through
|
||||||
var seen_by_player: Dictionary = {} # player_name -> PackedInt32Array (0 unseen, 1 seen)
|
var seen_by_player: Dictionary = {} # player_name -> PackedInt32Array (0 unseen, 1 seen)
|
||||||
var combined_seen: PackedInt32Array = PackedInt32Array()
|
var combined_seen: PackedInt32Array = PackedInt32Array()
|
||||||
var explored_map: PackedInt32Array = PackedInt32Array() # 0 unseen, 1 explored
|
var explored_map: PackedInt32Array = PackedInt32Array() # 0 unseen, 1 explored
|
||||||
@@ -1127,7 +1136,7 @@ func _show_loot_floating_text(player: Node, text: String, color: Color, item_tex
|
|||||||
floating_text.global_position = Vector2(player.global_position.x, player.global_position.y - 20)
|
floating_text.global_position = Vector2(player.global_position.x, player.global_position.y - 20)
|
||||||
floating_text.setup(text, color, 0.5, 0.5, item_texture, sprite_hframes, sprite_vframes, sprite_frame)
|
floating_text.setup(text, color, 0.5, 0.5, item_texture, sprite_hframes, sprite_vframes, sprite_frame)
|
||||||
|
|
||||||
func _apply_heal_spell_sync(target_name: String, amount_to_apply: float, display_amount: int, is_crit: bool, is_overheal: bool, allow_overheal: bool):
|
func _apply_heal_spell_sync(target_name: String, amount_to_apply: float, display_amount: int, is_crit: bool, is_overheal: bool, allow_overheal: bool, is_revive: bool = false):
|
||||||
var target: Node = null
|
var target: Node = null
|
||||||
for p in get_tree().get_nodes_in_group("player"):
|
for p in get_tree().get_nodes_in_group("player"):
|
||||||
if p.name == target_name and is_instance_valid(p):
|
if p.name == target_name and is_instance_valid(p):
|
||||||
@@ -1137,28 +1146,38 @@ func _apply_heal_spell_sync(target_name: String, amount_to_apply: float, display
|
|||||||
return
|
return
|
||||||
var me = multiplayer.get_unique_id()
|
var me = multiplayer.get_unique_id()
|
||||||
var tid = target.get_multiplayer_authority()
|
var tid = target.get_multiplayer_authority()
|
||||||
if me == tid and target.has_method("heal") and amount_to_apply > 0:
|
if is_revive:
|
||||||
target.heal(amount_to_apply, allow_overheal)
|
if me == tid and target.has_method("_revive_from_heal"):
|
||||||
var entities = get_node_or_null("Entities")
|
target._revive_from_heal(display_amount)
|
||||||
var parent = entities if entities else target.get_parent()
|
elif tid != 0 and target.has_method("_revive_from_heal"):
|
||||||
if not parent:
|
target._revive_from_heal.rpc_id(tid, display_amount)
|
||||||
return
|
else:
|
||||||
var eff_scene = load("res://scenes/healing_effect.tscn") as PackedScene
|
if me == tid and target.has_method("heal") and amount_to_apply > 0:
|
||||||
if eff_scene:
|
target.heal(amount_to_apply, allow_overheal)
|
||||||
var eff = eff_scene.instantiate()
|
# When revive, target's authority already spawned effect+text in _revive_from_heal; skip to avoid double spawn
|
||||||
parent.add_child(eff)
|
var skip_spawn = is_revive and me == tid
|
||||||
eff.global_position = target.global_position
|
if not skip_spawn:
|
||||||
if eff.has_method("setup"):
|
var entities = get_node_or_null("Entities")
|
||||||
eff.setup(target)
|
var parent = entities if entities else target.get_parent()
|
||||||
var prefix = ""
|
if not parent:
|
||||||
if is_crit and is_overheal:
|
pass
|
||||||
prefix = "CRIT OVERHEAL! "
|
else:
|
||||||
elif is_crit:
|
var eff_scene = load("res://scenes/healing_effect.tscn") as PackedScene
|
||||||
prefix = "CRIT! "
|
if eff_scene:
|
||||||
elif is_overheal:
|
var eff = eff_scene.instantiate()
|
||||||
prefix = "OVERHEAL! "
|
parent.add_child(eff)
|
||||||
var heal_text = prefix + "+" + str(display_amount) + " HP"
|
eff.global_position = target.global_position
|
||||||
_show_loot_floating_text(target, heal_text, Color.GREEN, null, 1, 1, 0)
|
if eff.has_method("setup"):
|
||||||
|
eff.setup(target)
|
||||||
|
var prefix = ""
|
||||||
|
if is_crit and is_overheal:
|
||||||
|
prefix = "CRIT OVERHEAL! "
|
||||||
|
elif is_crit:
|
||||||
|
prefix = "CRIT! "
|
||||||
|
elif is_overheal:
|
||||||
|
prefix = "OVERHEAL! "
|
||||||
|
var heal_text = prefix + "+" + str(display_amount) + " HP"
|
||||||
|
_show_loot_floating_text(target, heal_text, Color.GREEN, null, 1, 1, 0)
|
||||||
|
|
||||||
@rpc("authority", "unreliable")
|
@rpc("authority", "unreliable")
|
||||||
func _sync_enemy_position(enemy_name: String, enemy_index: int, pos: Vector2, vel: Vector2, z_pos: float, dir: int, frame: int, anim: String, frame_num: int, state_value: int):
|
func _sync_enemy_position(enemy_name: String, enemy_index: int, pos: Vector2, vel: Vector2, z_pos: float, dir: int, frame: int, anim: String, frame_num: int, state_value: int):
|
||||||
@@ -1645,6 +1664,7 @@ func _process(delta):
|
|||||||
# Update camera to follow local players
|
# Update camera to follow local players
|
||||||
_update_camera()
|
_update_camera()
|
||||||
_update_fog_of_war(delta)
|
_update_fog_of_war(delta)
|
||||||
|
_update_canvas_modulate_by_torches()
|
||||||
|
|
||||||
# Periodic cleanup of disconnected peers (server only)
|
# Periodic cleanup of disconnected peers (server only)
|
||||||
if multiplayer.is_server() and multiplayer.has_multiplayer_peer():
|
if multiplayer.is_server() and multiplayer.has_multiplayer_peer():
|
||||||
@@ -2124,7 +2144,7 @@ func _is_walkable_tile(tile_center: Vector2) -> bool:
|
|||||||
var v = grid[tile_x][tile_y]
|
var v = grid[tile_x][tile_y]
|
||||||
return v == 1 or v == 2 or v == 3
|
return v == 1 or v == 2 or v == 3
|
||||||
|
|
||||||
func _get_adjacent_valid_spell_tile_centers(center_world_pos: Vector2, player_pos: Vector2) -> Array:
|
func _get_adjacent_valid_spell_tile_centers(center_world_pos: Vector2, _player_pos: Vector2) -> Array:
|
||||||
var out: Array = []
|
var out: Array = []
|
||||||
if not dungeon_tilemap_layer or dungeon_data.is_empty() or not dungeon_data.has("grid"):
|
if not dungeon_tilemap_layer or dungeon_data.is_empty() or not dungeon_data.has("grid"):
|
||||||
return out
|
return out
|
||||||
@@ -2172,6 +2192,12 @@ func _init_fog_of_war():
|
|||||||
cached_corridor_allowed_room_ids.clear()
|
cached_corridor_allowed_room_ids.clear()
|
||||||
was_in_corridor = false
|
was_in_corridor = false
|
||||||
last_corridor_fog_update = 0.0
|
last_corridor_fog_update = 0.0
|
||||||
|
# Reset torch-based darkening state for new level
|
||||||
|
_torch_darken_initialized = false
|
||||||
|
_torch_darken_in_room_last = false
|
||||||
|
_torch_darken_last_room_id = ""
|
||||||
|
_torch_darken_target_scale = 1.0
|
||||||
|
_torch_darken_current_scale = 1.0
|
||||||
|
|
||||||
func _update_fog_of_war(delta: float) -> void:
|
func _update_fog_of_war(delta: float) -> void:
|
||||||
if not fog_node or dungeon_data.is_empty() or not dungeon_data.has("map_size"):
|
if not fog_node or dungeon_data.is_empty() or not dungeon_data.has("map_size"):
|
||||||
@@ -2450,6 +2476,104 @@ func _get_room_index_for_tile(tile: Vector2i) -> int:
|
|||||||
return i
|
return i
|
||||||
return -1
|
return -1
|
||||||
|
|
||||||
|
func _count_torches_in_room(room: Dictionary) -> int:
|
||||||
|
if dungeon_data.is_empty() or not dungeon_data.has("torches"):
|
||||||
|
return 0
|
||||||
|
var tile_size := 16
|
||||||
|
var min_x: float = room.x * tile_size
|
||||||
|
var min_y: float = room.y * tile_size
|
||||||
|
var max_x: float = (room.x + room.w) * tile_size
|
||||||
|
var max_y: float = (room.y + room.h) * tile_size
|
||||||
|
var count := 0
|
||||||
|
for t in dungeon_data.torches:
|
||||||
|
var pos: Vector2 = t.get("position", Vector2.ZERO)
|
||||||
|
if pos.x >= min_x and pos.x <= max_x and pos.y >= min_y and pos.y <= max_y:
|
||||||
|
count += 1
|
||||||
|
return count
|
||||||
|
|
||||||
|
func _median_torch_scale_from_rooms(rooms: Array) -> float:
|
||||||
|
if rooms.is_empty():
|
||||||
|
return 0.0
|
||||||
|
var counts: Array = []
|
||||||
|
for r in rooms:
|
||||||
|
counts.append(_count_torches_in_room(r))
|
||||||
|
counts.sort()
|
||||||
|
var n := counts.size()
|
||||||
|
var mid := int(n / 2.0)
|
||||||
|
var median: float
|
||||||
|
if n % 2 == 1:
|
||||||
|
median = float(counts[mid])
|
||||||
|
else:
|
||||||
|
median = (float(counts[mid - 1]) + float(counts[mid])) * 0.5
|
||||||
|
median = clampf(median, 0.0, 4.0)
|
||||||
|
return median / 4.0
|
||||||
|
|
||||||
|
func _update_canvas_modulate_by_torches() -> void:
|
||||||
|
if dungeon_data.is_empty() or not dungeon_data.has("torches"):
|
||||||
|
return
|
||||||
|
var cm = get_node_or_null("CanvasModulate")
|
||||||
|
if not cm or not is_instance_valid(cm):
|
||||||
|
return
|
||||||
|
var local_list = player_manager.get_local_players() if player_manager else []
|
||||||
|
if local_list.is_empty() or not local_list[0]:
|
||||||
|
return
|
||||||
|
var p = local_list[0]
|
||||||
|
var p_tile = Vector2i(int(p.global_position.x / FOG_TILE_SIZE), int(p.global_position.y / FOG_TILE_SIZE))
|
||||||
|
var current_room = _find_room_at_tile(p_tile)
|
||||||
|
var in_room := not current_room.is_empty()
|
||||||
|
var room_id := ""
|
||||||
|
if in_room:
|
||||||
|
room_id = str(current_room.x) + "," + str(current_room.y) + "," + str(current_room.w) + "," + str(current_room.h)
|
||||||
|
var transition := not _torch_darken_initialized
|
||||||
|
if _torch_darken_initialized:
|
||||||
|
if in_room != _torch_darken_in_room_last:
|
||||||
|
transition = true
|
||||||
|
elif in_room and room_id != _torch_darken_last_room_id:
|
||||||
|
transition = true
|
||||||
|
if transition:
|
||||||
|
_torch_darken_initialized = true
|
||||||
|
_torch_darken_in_room_last = in_room
|
||||||
|
_torch_darken_last_room_id = room_id
|
||||||
|
if in_room:
|
||||||
|
var tc = clampi(_count_torches_in_room(current_room), 0, 4)
|
||||||
|
_torch_darken_target_scale = tc / 4.0
|
||||||
|
else:
|
||||||
|
_torch_darken_target_scale = _median_torch_scale_from_rooms(cached_corridor_rooms)
|
||||||
|
var delta := get_process_delta_time()
|
||||||
|
_torch_darken_current_scale = lerpf(_torch_darken_current_scale, _torch_darken_target_scale, clampf(delta * _TORCH_DARKEN_LERP_SPEED, 0.0, 1.0))
|
||||||
|
var s := maxf(_torch_darken_current_scale, _TORCH_DARKEN_MIN_SCALE)
|
||||||
|
cm.color = Color(s, s, s)
|
||||||
|
|
||||||
|
func _reapply_torch_darkening() -> void:
|
||||||
|
if dungeon_data.is_empty() or not dungeon_data.has("torches"):
|
||||||
|
return
|
||||||
|
var cm = get_node_or_null("CanvasModulate")
|
||||||
|
if not cm or not is_instance_valid(cm):
|
||||||
|
return
|
||||||
|
var local_list = player_manager.get_local_players() if player_manager else []
|
||||||
|
if local_list.is_empty() or not local_list[0]:
|
||||||
|
return
|
||||||
|
var p = local_list[0]
|
||||||
|
var p_tile = Vector2i(int(p.global_position.x / FOG_TILE_SIZE), int(p.global_position.y / FOG_TILE_SIZE))
|
||||||
|
var current_room = _find_room_at_tile(p_tile)
|
||||||
|
var in_room := not current_room.is_empty()
|
||||||
|
var target: float
|
||||||
|
if in_room:
|
||||||
|
var tc = clampi(_count_torches_in_room(current_room), 0, 4)
|
||||||
|
target = tc / 4.0
|
||||||
|
else:
|
||||||
|
target = _median_torch_scale_from_rooms(cached_corridor_rooms)
|
||||||
|
var t := maxf(target, _TORCH_DARKEN_MIN_SCALE)
|
||||||
|
_torch_darken_target_scale = target
|
||||||
|
_torch_darken_current_scale = target
|
||||||
|
var room_id := ""
|
||||||
|
if in_room:
|
||||||
|
room_id = str(current_room.x) + "," + str(current_room.y) + "," + str(current_room.w) + "," + str(current_room.h)
|
||||||
|
_torch_darken_initialized = true
|
||||||
|
_torch_darken_in_room_last = in_room
|
||||||
|
_torch_darken_last_room_id = room_id
|
||||||
|
cm.color = Color(t, t, t)
|
||||||
|
|
||||||
func _get_view_angle_weight(view_dir: Vector2, ray_dir: Vector2) -> float:
|
func _get_view_angle_weight(view_dir: Vector2, ray_dir: Vector2) -> float:
|
||||||
if view_dir.length() < 0.1 or ray_dir.length() < 0.1:
|
if view_dir.length() < 0.1 or ray_dir.length() < 0.1:
|
||||||
return 1.0
|
return 1.0
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ var _indicators: Array = [] # { position: Vector2, color: Color, name: String,
|
|||||||
var _font: Font = null
|
var _font: Font = null
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
_update_size()
|
call_deferred("_update_size")
|
||||||
if get_viewport():
|
if get_viewport():
|
||||||
get_viewport().size_changed.connect(_update_size)
|
get_viewport().size_changed.connect(_update_size)
|
||||||
_game_world = get_tree().get_first_node_in_group("game_world")
|
_game_world = get_tree().get_first_node_in_group("game_world")
|
||||||
@@ -38,6 +38,7 @@ func _update_size() -> void:
|
|||||||
if not vp:
|
if not vp:
|
||||||
return
|
return
|
||||||
var rect = vp.get_visible_rect()
|
var rect = vp.get_visible_rect()
|
||||||
|
set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||||
position = rect.position
|
position = rect.position
|
||||||
size = rect.size
|
size = rect.size
|
||||||
custom_minimum_size = rect.size
|
custom_minimum_size = rect.size
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ 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 is_shielding: bool = false # True when holding grab with shield in offhand and nothing to grab/lift
|
||||||
var is_reviving: bool = false # True when holding grab on a corpse and charging revive
|
var is_reviving: bool = false # True when holding grab on a corpse and charging revive
|
||||||
var revive_charge: float = 0.0
|
var revive_charge: float = 0.0
|
||||||
|
var was_reviving_last_frame: bool = false
|
||||||
const REVIVE_DURATION: float = 2.0 # Seconds holding grab to revive
|
const REVIVE_DURATION: float = 2.0 # Seconds holding grab to revive
|
||||||
var last_movement_direction = Vector2.DOWN # Track last direction for placing objects
|
var last_movement_direction = Vector2.DOWN # Track last direction for placing objects
|
||||||
var push_axis = Vector2.ZERO # Locked axis for pushing/pulling
|
var push_axis = Vector2.ZERO # Locked axis for pushing/pulling
|
||||||
@@ -1782,165 +1783,180 @@ func _physics_process(delta):
|
|||||||
else:
|
else:
|
||||||
spell_charge_tint_pulse_time = 0.0
|
spell_charge_tint_pulse_time = 0.0
|
||||||
|
|
||||||
|
# Revive charge visuals - same as healing (healing_charging on AnimationIncantation)
|
||||||
|
if is_reviving:
|
||||||
|
was_reviving_last_frame = true
|
||||||
|
if has_node("AnimationIncantation") and not is_charging_spell:
|
||||||
|
$AnimationIncantation.play("healing_charging")
|
||||||
|
elif was_reviving_last_frame:
|
||||||
|
was_reviving_last_frame = false
|
||||||
|
_stop_spell_charge_incantation()
|
||||||
|
|
||||||
if is_local_player and is_multiplayer_authority():
|
if is_local_player and is_multiplayer_authority():
|
||||||
# Skip all input and logic if dead
|
# When dead: only corpse knockback friction + sync; no input or other logic
|
||||||
if is_dead:
|
if is_dead:
|
||||||
return
|
if is_knocked_back:
|
||||||
|
knockback_time += delta
|
||||||
|
if knockback_time >= knockback_duration:
|
||||||
|
is_knocked_back = false
|
||||||
|
knockback_time = 0.0
|
||||||
|
else:
|
||||||
|
velocity = velocity.lerp(Vector2.ZERO, delta * 8.0)
|
||||||
|
else:
|
||||||
|
# Handle knockback timer (always handle knockback, even when controls are disabled)
|
||||||
|
if is_knocked_back:
|
||||||
|
knockback_time += delta
|
||||||
|
if knockback_time >= knockback_duration:
|
||||||
|
is_knocked_back = false
|
||||||
|
knockback_time = 0.0
|
||||||
|
|
||||||
# Handle knockback timer (always handle knockback, even when controls are disabled)
|
# Update movement lock timer (for bow release)
|
||||||
if is_knocked_back:
|
if movement_lock_timer > 0.0:
|
||||||
knockback_time += delta
|
movement_lock_timer -= delta
|
||||||
if knockback_time >= knockback_duration:
|
if movement_lock_timer <= 0.0:
|
||||||
is_knocked_back = false
|
movement_lock_timer = 0.0
|
||||||
knockback_time = 0.0
|
|
||||||
|
|
||||||
# Update movement lock timer (for bow release)
|
# Update direction lock timer (for attacks)
|
||||||
if movement_lock_timer > 0.0:
|
if direction_lock_timer > 0.0:
|
||||||
movement_lock_timer -= delta
|
direction_lock_timer -= delta
|
||||||
if movement_lock_timer <= 0.0:
|
if direction_lock_timer <= 0.0:
|
||||||
movement_lock_timer = 0.0
|
direction_lock_timer = 0.0
|
||||||
|
|
||||||
# Update direction lock timer (for attacks)
|
# Update damage direction lock timer (block facing change when taking damage)
|
||||||
if direction_lock_timer > 0.0:
|
if damage_direction_lock_timer > 0.0:
|
||||||
direction_lock_timer -= delta
|
damage_direction_lock_timer -= delta
|
||||||
if direction_lock_timer <= 0.0:
|
if damage_direction_lock_timer <= 0.0:
|
||||||
direction_lock_timer = 0.0
|
damage_direction_lock_timer = 0.0
|
||||||
|
|
||||||
# Update damage direction lock timer (block facing change when taking damage)
|
if shield_block_cooldown_timer > 0.0:
|
||||||
if damage_direction_lock_timer > 0.0:
|
shield_block_cooldown_timer -= delta
|
||||||
damage_direction_lock_timer -= delta
|
if shield_block_cooldown_timer <= 0.0:
|
||||||
if damage_direction_lock_timer <= 0.0:
|
shield_block_cooldown_timer = 0.0
|
||||||
damage_direction_lock_timer = 0.0
|
|
||||||
|
|
||||||
if shield_block_cooldown_timer > 0.0:
|
# Update bow charge tint (when fully charged)
|
||||||
shield_block_cooldown_timer -= delta
|
if is_charging_bow:
|
||||||
if shield_block_cooldown_timer <= 0.0:
|
var charge_time = (Time.get_ticks_msec() / 1000.0) - bow_charge_start_time
|
||||||
shield_block_cooldown_timer = 0.0
|
# Smooth curve: charge from 0.2s to 1.0s
|
||||||
|
var charge_progress = clamp((charge_time - 0.2) / 0.8, 0.0, 1.0) # 0.0 at 0.2s, 1.0 at 1.0s
|
||||||
|
|
||||||
# Update bow charge tint (when fully charged)
|
# Update tint pulse timer when fully charged
|
||||||
if is_charging_bow:
|
if charge_progress >= 1.0:
|
||||||
var charge_time = (Time.get_ticks_msec() / 1000.0) - bow_charge_start_time
|
# Use fast pulse speed when fully charged
|
||||||
# Smooth curve: charge from 0.2s to 1.0s
|
bow_charge_tint_pulse_time += delta * bow_charge_tint_pulse_speed_charged
|
||||||
var charge_progress = clamp((charge_time - 0.2) / 0.8, 0.0, 1.0) # 0.0 at 0.2s, 1.0 at 1.0s
|
_apply_bow_charge_tint()
|
||||||
|
else:
|
||||||
# Update tint pulse timer when fully charged
|
bow_charge_tint_pulse_time = 0.0
|
||||||
if charge_progress >= 1.0:
|
_clear_bow_charge_tint()
|
||||||
# Use fast pulse speed when fully charged
|
|
||||||
bow_charge_tint_pulse_time += delta * bow_charge_tint_pulse_speed_charged
|
|
||||||
_apply_bow_charge_tint()
|
|
||||||
else:
|
else:
|
||||||
|
# Reset pulse timer when not charging
|
||||||
bow_charge_tint_pulse_time = 0.0
|
bow_charge_tint_pulse_time = 0.0
|
||||||
_clear_bow_charge_tint()
|
_clear_bow_charge_tint()
|
||||||
else:
|
|
||||||
# Reset pulse timer when not charging
|
|
||||||
bow_charge_tint_pulse_time = 0.0
|
|
||||||
_clear_bow_charge_tint()
|
|
||||||
|
|
||||||
# Update burn debuff (works on both authority and clients)
|
# Update burn debuff (works on both authority and clients)
|
||||||
if burn_debuff_timer > 0.0:
|
if burn_debuff_timer > 0.0:
|
||||||
burn_debuff_timer -= delta
|
burn_debuff_timer -= delta
|
||||||
|
|
||||||
# Only deal damage on authority (where we have authority)
|
# Only deal damage on authority (where we have authority)
|
||||||
if is_multiplayer_authority():
|
if is_multiplayer_authority():
|
||||||
burn_damage_timer += delta
|
burn_damage_timer += delta
|
||||||
|
|
||||||
# Deal burn damage every second (no knockback)
|
# Deal burn damage every second (no knockback)
|
||||||
if burn_damage_timer >= 1.0:
|
if burn_damage_timer >= 1.0:
|
||||||
|
burn_damage_timer = 0.0
|
||||||
|
# Deal burn damage directly (no knockback, no animation)
|
||||||
|
if character_stats:
|
||||||
|
var old_hp = character_stats.hp
|
||||||
|
character_stats.modify_health(-burn_debuff_damage_per_second)
|
||||||
|
if character_stats.hp <= 0:
|
||||||
|
character_stats.no_health.emit()
|
||||||
|
character_stats.character_changed.emit(character_stats)
|
||||||
|
var actual_damage = old_hp - character_stats.hp
|
||||||
|
print(name, " takes ", actual_damage, " burn damage! Health: ", character_stats.hp, "/", character_stats.maxhp)
|
||||||
|
# Show damage number for burn damage
|
||||||
|
_show_damage_number(actual_damage, global_position, false, false, false) # Show burn damage number
|
||||||
|
# Sync burn damage visual to other clients
|
||||||
|
if multiplayer.has_multiplayer_peer() and can_send_rpcs and is_inside_tree():
|
||||||
|
_rpc_to_ready_peers("_sync_damage", [actual_damage, global_position])
|
||||||
|
|
||||||
|
# Animate burn visual if it's a sprite (works on both authority and clients)
|
||||||
|
if burn_debuff_visual and is_instance_valid(burn_debuff_visual):
|
||||||
|
if burn_debuff_visual is Sprite2D:
|
||||||
|
var sprite = burn_debuff_visual as Sprite2D
|
||||||
|
var anim_timer = sprite.get_meta("burn_animation_timer", 0.0)
|
||||||
|
anim_timer += delta
|
||||||
|
if anim_timer >= 0.1: # ~10 FPS
|
||||||
|
anim_timer = 0.0
|
||||||
|
var frame = sprite.get_meta("burn_animation_frame", 0)
|
||||||
|
frame = (frame + 1) % 16
|
||||||
|
sprite.frame = frame
|
||||||
|
sprite.set_meta("burn_animation_frame", frame)
|
||||||
|
sprite.set_meta("burn_animation_timer", anim_timer)
|
||||||
|
|
||||||
|
# Remove burn debuff when timer expires (works on both authority and clients)
|
||||||
|
if burn_debuff_timer <= 0.0:
|
||||||
|
burn_debuff_timer = 0.0
|
||||||
burn_damage_timer = 0.0
|
burn_damage_timer = 0.0
|
||||||
# Deal burn damage directly (no knockback, no animation)
|
_remove_burn_debuff()
|
||||||
if character_stats:
|
|
||||||
var old_hp = character_stats.hp
|
|
||||||
character_stats.modify_health(-burn_debuff_damage_per_second)
|
|
||||||
if character_stats.hp <= 0:
|
|
||||||
character_stats.no_health.emit()
|
|
||||||
character_stats.character_changed.emit(character_stats)
|
|
||||||
var actual_damage = old_hp - character_stats.hp
|
|
||||||
print(name, " takes ", actual_damage, " burn damage! Health: ", character_stats.hp, "/", character_stats.maxhp)
|
|
||||||
# Show damage number for burn damage
|
|
||||||
_show_damage_number(actual_damage, global_position, false, false, false) # Show burn damage number
|
|
||||||
# Sync burn damage visual to other clients
|
|
||||||
if multiplayer.has_multiplayer_peer() and can_send_rpcs and is_inside_tree():
|
|
||||||
_rpc_to_ready_peers("_sync_damage", [actual_damage, global_position])
|
|
||||||
|
|
||||||
# Animate burn visual if it's a sprite (works on both authority and clients)
|
# Skip input if controls are disabled (e.g., when inventory is open)
|
||||||
if burn_debuff_visual and is_instance_valid(burn_debuff_visual):
|
# But still allow knockback to continue (handled above)
|
||||||
if burn_debuff_visual is Sprite2D:
|
var skip_input = controls_disabled
|
||||||
var sprite = burn_debuff_visual as Sprite2D
|
if controls_disabled:
|
||||||
var anim_timer = sprite.get_meta("burn_animation_timer", 0.0)
|
if not is_knocked_back:
|
||||||
anim_timer += delta
|
# Immediately stop movement when controls are disabled (e.g., inventory opened)
|
||||||
if anim_timer >= 0.1: # ~10 FPS
|
velocity = Vector2.ZERO
|
||||||
anim_timer = 0.0
|
# Reset animation to IDLE if not in a special state
|
||||||
var frame = sprite.get_meta("burn_animation_frame", 0)
|
if current_animation != "THROW" and current_animation != "DAMAGE" and current_animation != "SWORD" and current_animation != "BOW" and current_animation != "STAFF":
|
||||||
frame = (frame + 1) % 16
|
if is_lifting:
|
||||||
sprite.frame = frame
|
_set_animation("IDLE_HOLD")
|
||||||
sprite.set_meta("burn_animation_frame", frame)
|
elif is_pushing:
|
||||||
sprite.set_meta("burn_animation_timer", anim_timer)
|
_set_animation("IDLE_PUSH")
|
||||||
|
else:
|
||||||
|
_set_animation("IDLE")
|
||||||
|
|
||||||
# Remove burn debuff when timer expires (works on both authority and clients)
|
# Check if being held by someone
|
||||||
if burn_debuff_timer <= 0.0:
|
var being_held_by_someone = false
|
||||||
burn_debuff_timer = 0.0
|
for other_player in get_tree().get_nodes_in_group("player"):
|
||||||
burn_damage_timer = 0.0
|
if other_player != self and other_player.held_object == self:
|
||||||
_remove_burn_debuff()
|
being_held_by_someone = true
|
||||||
|
being_held_by = other_player
|
||||||
|
break
|
||||||
|
|
||||||
# Skip input if controls are disabled (e.g., when inventory is open)
|
if being_held_by_someone:
|
||||||
# But still allow knockback to continue (handled above)
|
is_shielding = false
|
||||||
var skip_input = controls_disabled
|
was_shielding_last_frame = false
|
||||||
if controls_disabled:
|
_update_shield_visibility()
|
||||||
if not is_knocked_back:
|
# Handle struggle mechanic
|
||||||
# Immediately stop movement when controls are disabled (e.g., inventory opened)
|
_handle_struggle(delta)
|
||||||
velocity = Vector2.ZERO
|
elif is_knocked_back:
|
||||||
# Reset animation to IDLE if not in a special state
|
is_shielding = false
|
||||||
if current_animation != "THROW" and current_animation != "DAMAGE" and current_animation != "SWORD" and current_animation != "BOW" and current_animation != "STAFF":
|
was_shielding_last_frame = false
|
||||||
if is_lifting:
|
_update_shield_visibility()
|
||||||
_set_animation("IDLE_HOLD")
|
# During knockback, no input control - just let velocity carry the player
|
||||||
elif is_pushing:
|
# Apply friction to slow down knockback
|
||||||
_set_animation("IDLE_PUSH")
|
velocity = velocity.lerp(Vector2.ZERO, delta * 8.0)
|
||||||
else:
|
elif not is_airborne and not skip_input:
|
||||||
_set_animation("IDLE")
|
# Normal input handling (only if controls are not disabled)
|
||||||
|
struggle_time = 0.0 # Reset struggle timer
|
||||||
|
struggle_direction = Vector2.ZERO
|
||||||
|
_handle_input()
|
||||||
|
_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
|
||||||
|
|
||||||
# Check if being held by someone
|
# Update held object positions
|
||||||
var being_held_by_someone = false
|
if is_lifting:
|
||||||
for other_player in get_tree().get_nodes_in_group("player"):
|
_update_lifted_object()
|
||||||
if other_player != self and other_player.held_object == self:
|
elif is_pushing:
|
||||||
being_held_by_someone = true
|
_update_pushed_object()
|
||||||
being_held_by = other_player
|
|
||||||
break
|
|
||||||
|
|
||||||
if being_held_by_someone:
|
# Sync position, direction, and animation to other clients (unreliable broadcast)
|
||||||
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)
|
|
||||||
elif not is_airborne and not skip_input:
|
|
||||||
# Normal input handling (only if controls are not disabled)
|
|
||||||
struggle_time = 0.0 # Reset struggle timer
|
|
||||||
struggle_direction = Vector2.ZERO
|
|
||||||
_handle_input()
|
|
||||||
_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
|
|
||||||
|
|
||||||
# Update held object positions
|
|
||||||
if is_lifting:
|
|
||||||
_update_lifted_object()
|
|
||||||
elif is_pushing:
|
|
||||||
_update_pushed_object()
|
|
||||||
|
|
||||||
# Sync position, direction, and animation to other clients (unreliable broadcast)
|
|
||||||
# Only send RPC if we're in the scene tree and ready to send RPCs (prevents errors when player hasn't spawned on all clients yet)
|
# Only send RPC if we're in the scene tree and ready to send RPCs (prevents errors when player hasn't spawned on all clients yet)
|
||||||
# On server, also wait for all clients to be ready
|
# On server, also wait for all clients to be ready
|
||||||
if multiplayer.has_multiplayer_peer() and is_inside_tree() and can_send_rpcs:
|
if multiplayer.has_multiplayer_peer() and is_inside_tree() and can_send_rpcs:
|
||||||
@@ -4035,7 +4051,7 @@ func _cast_heal_spell(target: Node):
|
|||||||
var dungeon_seed: int = 0
|
var dungeon_seed: int = 0
|
||||||
if gw and "dungeon_seed" in gw:
|
if gw and "dungeon_seed" in gw:
|
||||||
dungeon_seed = gw.dungeon_seed
|
dungeon_seed = gw.dungeon_seed
|
||||||
var seed_val = dungeon_seed + hash(target.name) + int(global_position.x) * 31 + int(global_position.y) * 17 + int(Time.get_ticks_msec() / 50)
|
var seed_val = dungeon_seed + hash(target.name) + int(global_position.x) * 31 + int(global_position.y) * 17 + int(Time.get_ticks_msec() / 50.0)
|
||||||
var rng = RandomNumberGenerator.new()
|
var rng = RandomNumberGenerator.new()
|
||||||
rng.seed = seed_val
|
rng.seed = seed_val
|
||||||
|
|
||||||
@@ -4052,30 +4068,41 @@ func _cast_heal_spell(target: Node):
|
|||||||
if is_crit:
|
if is_crit:
|
||||||
amount = floor(amount * 2.0)
|
amount = floor(amount * 2.0)
|
||||||
|
|
||||||
var overheal_chance_pct = 1.0 + lck_val * 0.3
|
var is_revive = "is_dead" in target and target.is_dead
|
||||||
var can_overheal = target.character_stats and target.character_stats.hp <= target.character_stats.maxhp
|
|
||||||
var is_overheal = can_overheal and rng.randf() * 100.0 < overheal_chance_pct
|
|
||||||
|
|
||||||
var display_amount = int(amount)
|
var display_amount = int(amount)
|
||||||
var actual_heal = amount
|
var actual_heal = amount
|
||||||
var allow_overheal = false
|
var allow_overheal = false
|
||||||
if is_overheal:
|
var is_overheal = false
|
||||||
allow_overheal = true
|
|
||||||
|
if is_revive:
|
||||||
|
# Tome revive: no overheal, full amount revives
|
||||||
|
actual_heal = amount
|
||||||
else:
|
else:
|
||||||
var cap = 0.0
|
var overheal_chance_pct = 1.0 + lck_val * 0.3
|
||||||
if target.character_stats:
|
var can_overheal = target.character_stats and target.character_stats.hp <= target.character_stats.maxhp
|
||||||
cap = target.character_stats.maxhp - target.character_stats.hp
|
is_overheal = can_overheal and rng.randf() * 100.0 < overheal_chance_pct
|
||||||
actual_heal = min(amount, max(0.0, cap))
|
if is_overheal:
|
||||||
|
allow_overheal = true
|
||||||
|
else:
|
||||||
|
var cap = 0.0
|
||||||
|
if target.character_stats:
|
||||||
|
cap = target.character_stats.maxhp - target.character_stats.hp
|
||||||
|
actual_heal = min(amount, max(0.0, cap))
|
||||||
|
|
||||||
var me = multiplayer.get_unique_id()
|
var me = multiplayer.get_unique_id()
|
||||||
var tid = target.get_multiplayer_authority()
|
var tid = target.get_multiplayer_authority()
|
||||||
if me == tid and actual_heal > 0:
|
if is_revive:
|
||||||
target.heal(actual_heal, allow_overheal)
|
if me == tid:
|
||||||
|
target._revive_from_heal(display_amount)
|
||||||
|
elif multiplayer.has_multiplayer_peer() and can_send_rpcs and is_inside_tree():
|
||||||
|
target._revive_from_heal.rpc_id(tid, display_amount)
|
||||||
|
else:
|
||||||
|
if me == tid and actual_heal > 0:
|
||||||
|
target.heal(actual_heal, allow_overheal)
|
||||||
_spawn_heal_effect_and_text(target, display_amount, is_crit, is_overheal)
|
_spawn_heal_effect_and_text(target, display_amount, is_crit, is_overheal)
|
||||||
if multiplayer.has_multiplayer_peer() and can_send_rpcs and is_inside_tree():
|
if multiplayer.has_multiplayer_peer() and can_send_rpcs and is_inside_tree() and gw and gw.has_method("_apply_heal_spell_sync"):
|
||||||
if gw and gw.has_method("_apply_heal_spell_sync"):
|
_rpc_to_ready_peers("_sync_heal_spell_via_gw", [target.name, actual_heal, display_amount, is_crit, is_overheal, allow_overheal, is_revive])
|
||||||
_rpc_to_ready_peers("_sync_heal_spell_via_gw", [target.name, actual_heal, display_amount, is_crit, is_overheal, allow_overheal])
|
print(name, " cast heal on ", target.name, " for ", display_amount, " HP", " (actual: ", actual_heal, ", crit: ", is_crit, ", overheal: ", is_overheal, ", revive: ", is_revive, ")")
|
||||||
print(name, " cast heal on ", target.name, " for ", display_amount, " HP", " (actual: ", actual_heal, ", crit: ", is_crit, ", overheal: ", is_overheal, ")")
|
|
||||||
|
|
||||||
func _spawn_heal_effect_and_text(target: Node, display_amount: int, is_crit: bool, is_overheal: bool):
|
func _spawn_heal_effect_and_text(target: Node, display_amount: int, is_crit: bool, is_overheal: bool):
|
||||||
if not target or not is_instance_valid(target):
|
if not target or not is_instance_valid(target):
|
||||||
@@ -4107,12 +4134,12 @@ func _spawn_heal_effect_and_text(target: Node, display_amount: int, is_crit: boo
|
|||||||
ft.setup(heal_text, Color.GREEN, 0.5, 0.5, null, 1, 1, 0)
|
ft.setup(heal_text, Color.GREEN, 0.5, 0.5, null, 1, 1, 0)
|
||||||
|
|
||||||
@rpc("any_peer", "reliable")
|
@rpc("any_peer", "reliable")
|
||||||
func _sync_heal_spell_via_gw(target_name: String, amount_to_apply: float, display_amount: int, is_crit: bool, is_overheal: bool, allow_overheal: bool):
|
func _sync_heal_spell_via_gw(target_name: String, amount_to_apply: float, display_amount: int, is_crit: bool, is_overheal: bool, allow_overheal: bool, is_revive: bool = false):
|
||||||
if is_multiplayer_authority():
|
if is_multiplayer_authority():
|
||||||
return
|
return
|
||||||
var gw = get_tree().get_first_node_in_group("game_world")
|
var gw = get_tree().get_first_node_in_group("game_world")
|
||||||
if gw and gw.has_method("_apply_heal_spell_sync"):
|
if gw and gw.has_method("_apply_heal_spell_sync"):
|
||||||
gw._apply_heal_spell_sync(target_name, amount_to_apply, display_amount, is_crit, is_overheal, allow_overheal)
|
gw._apply_heal_spell_sync(target_name, amount_to_apply, display_amount, is_crit, is_overheal, allow_overheal, is_revive)
|
||||||
|
|
||||||
func _is_healing_spell() -> bool:
|
func _is_healing_spell() -> bool:
|
||||||
if not character_stats or not character_stats.equipment.has("offhand"):
|
if not character_stats or not character_stats.equipment.has("offhand"):
|
||||||
@@ -4138,8 +4165,6 @@ func _get_heal_target() -> Node:
|
|||||||
for p in get_tree().get_nodes_in_group("player"):
|
for p in get_tree().get_nodes_in_group("player"):
|
||||||
if not is_instance_valid(p):
|
if not is_instance_valid(p):
|
||||||
continue
|
continue
|
||||||
if "is_dead" in p and p.is_dead:
|
|
||||||
continue
|
|
||||||
var d = p.global_position.distance_to(mouse_world)
|
var d = p.global_position.distance_to(mouse_world)
|
||||||
if d < best_d:
|
if d < best_d:
|
||||||
best_d = d
|
best_d = d
|
||||||
@@ -5826,19 +5851,7 @@ func _die():
|
|||||||
# Wait for DIE animation to complete (200+200+200+800 = 1400ms = 1.4s)
|
# Wait for DIE animation to complete (200+200+200+800 = 1400ms = 1.4s)
|
||||||
await get_tree().create_timer(1.4).timeout
|
await get_tree().create_timer(1.4).timeout
|
||||||
|
|
||||||
# Fade out over 0.5 seconds (fade all sprite layers)
|
# Force holder to drop us NOW (before respawn wait)
|
||||||
var fade_tween = create_tween()
|
|
||||||
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_shield, sprite_shield_holding, sprite_weapon, shadow]:
|
|
||||||
if sprite_layer:
|
|
||||||
fade_tween.tween_property(sprite_layer, "modulate:a", 0.0, 0.5)
|
|
||||||
|
|
||||||
# Wait for fade to finish
|
|
||||||
await fade_tween.finished
|
|
||||||
|
|
||||||
# Force holder to drop us NOW (after fade, before respawn)
|
|
||||||
# Search for any player holding us (don't rely on being_held_by)
|
# Search for any player holding us (don't rely on being_held_by)
|
||||||
print(name, " searching for anyone holding us...")
|
print(name, " searching for anyone holding us...")
|
||||||
var found_holder = false
|
var found_holder = false
|
||||||
@@ -5999,7 +6012,9 @@ func _respawn():
|
|||||||
global_position = new_respawn_pos
|
global_position = new_respawn_pos
|
||||||
respawn_point = new_respawn_pos # Update respawn point for next time
|
respawn_point = new_respawn_pos # Update respawn point for next time
|
||||||
|
|
||||||
# Play idle animation
|
# Clear concussion and play idle animation (server)
|
||||||
|
if status_anim and status_anim.has_animation("idle"):
|
||||||
|
status_anim.play("idle")
|
||||||
_set_animation("IDLE")
|
_set_animation("IDLE")
|
||||||
|
|
||||||
# Sync respawn over network (only authority sends)
|
# Sync respawn over network (only authority sends)
|
||||||
@@ -6049,7 +6064,10 @@ func _do_revive(corpse: Node):
|
|||||||
if character_stats:
|
if character_stats:
|
||||||
character_stats.hp = max(1, character_stats.hp - half_hp)
|
character_stats.hp = max(1, character_stats.hp - half_hp)
|
||||||
character_stats.character_changed.emit(character_stats)
|
character_stats.character_changed.emit(character_stats)
|
||||||
|
# Show -X HP on reviver (we "took" that much to revive)
|
||||||
|
_show_revive_cost_number(half_hp)
|
||||||
if multiplayer.has_multiplayer_peer() and can_send_rpcs and is_inside_tree():
|
if multiplayer.has_multiplayer_peer() and can_send_rpcs and is_inside_tree():
|
||||||
|
_rpc_to_ready_peers("_sync_revive_cost", [half_hp])
|
||||||
corpse._revive_from_player.rpc_id(corpse.get_multiplayer_authority(), half_hp)
|
corpse._revive_from_player.rpc_id(corpse.get_multiplayer_authority(), half_hp)
|
||||||
else:
|
else:
|
||||||
corpse._revive_from_player(half_hp)
|
corpse._revive_from_player(half_hp)
|
||||||
@@ -6074,6 +6092,42 @@ func _revive_from_player(hp_amount: int):
|
|||||||
if status_anim and status_anim.has_animation("idle"):
|
if status_anim and status_anim.has_animation("idle"):
|
||||||
status_anim.play("idle")
|
status_anim.play("idle")
|
||||||
_set_animation("IDLE")
|
_set_animation("IDLE")
|
||||||
|
# Same healing effect as Tome of Healing (green frames, pulse, +X HP)
|
||||||
|
_spawn_heal_effect_and_text(self, hp_amount, false, false)
|
||||||
|
# Clear concussion on all clients (authority already did above; broadcast for others)
|
||||||
|
if multiplayer.has_multiplayer_peer() and can_send_rpcs and is_inside_tree():
|
||||||
|
_rpc_to_ready_peers("_sync_revived_clear_concussion", [name])
|
||||||
|
|
||||||
|
@rpc("any_peer", "reliable")
|
||||||
|
func _sync_revived_clear_concussion(_player_name: String):
|
||||||
|
# Received on each peer's copy of the revived player; clear our concussion (server + clients)
|
||||||
|
var status_anim = get_node_or_null("Sprite2DStatus/AnimationPlayerStatus")
|
||||||
|
if status_anim and status_anim.has_animation("idle"):
|
||||||
|
status_anim.play("idle")
|
||||||
|
|
||||||
|
@rpc("any_peer", "reliable")
|
||||||
|
func _revive_from_heal(hp_amount: int):
|
||||||
|
if not is_dead:
|
||||||
|
return
|
||||||
|
was_revived = true
|
||||||
|
is_dead = false
|
||||||
|
is_processing_death = false
|
||||||
|
if character_stats:
|
||||||
|
character_stats.hp = float(hp_amount)
|
||||||
|
else:
|
||||||
|
current_health = float(hp_amount)
|
||||||
|
for sprite_layer in [sprite_body, sprite_boots, sprite_armour, sprite_facial_hair,
|
||||||
|
sprite_hair, sprite_eyes, sprite_eyelashes, sprite_addons,
|
||||||
|
sprite_headgear, sprite_shield, sprite_shield_holding, sprite_weapon, shadow]:
|
||||||
|
if sprite_layer:
|
||||||
|
sprite_layer.modulate.a = 1.0
|
||||||
|
var status_anim = get_node_or_null("Sprite2DStatus/AnimationPlayerStatus")
|
||||||
|
if status_anim and status_anim.has_animation("idle"):
|
||||||
|
status_anim.play("idle")
|
||||||
|
_set_animation("IDLE")
|
||||||
|
_spawn_heal_effect_and_text(self, hp_amount, false, false)
|
||||||
|
if multiplayer.has_multiplayer_peer() and can_send_rpcs and is_inside_tree():
|
||||||
|
_rpc_to_ready_peers("_sync_revived_clear_concussion", [name])
|
||||||
|
|
||||||
@rpc("any_peer", "reliable")
|
@rpc("any_peer", "reliable")
|
||||||
func _sync_death():
|
func _sync_death():
|
||||||
@@ -6098,11 +6152,16 @@ func _sync_respawn(spawn_pos: Vector2):
|
|||||||
|
|
||||||
# Restore visibility
|
# Restore visibility
|
||||||
for sprite_layer in [sprite_body, sprite_boots, sprite_armour, sprite_facial_hair,
|
for sprite_layer in [sprite_body, sprite_boots, sprite_armour, sprite_facial_hair,
|
||||||
sprite_hair, sprite_eyes, sprite_eyelashes, sprite_addons,
|
sprite_hair, sprite_eyes, sprite_eyelashes, sprite_addons,
|
||||||
sprite_headgear, sprite_shield, sprite_shield_holding, sprite_weapon, shadow]:
|
sprite_headgear, sprite_shield, sprite_shield_holding, sprite_weapon, shadow]:
|
||||||
if sprite_layer:
|
if sprite_layer:
|
||||||
sprite_layer.modulate.a = 1.0
|
sprite_layer.modulate.a = 1.0
|
||||||
|
|
||||||
|
# Clear concussion on clients (AnimationPlayerStatus -> idle)
|
||||||
|
var status_anim = get_node_or_null("Sprite2DStatus/AnimationPlayerStatus")
|
||||||
|
if status_anim and status_anim.has_animation("idle"):
|
||||||
|
status_anim.play("idle")
|
||||||
|
|
||||||
_set_animation("IDLE")
|
_set_animation("IDLE")
|
||||||
|
|
||||||
func add_coins(amount: int):
|
func add_coins(amount: int):
|
||||||
@@ -6440,7 +6499,8 @@ func _show_damage_number(amount: float, from_position: Vector2, is_crit: bool =
|
|||||||
damage_label.color = Color(0.4, 0.65, 1.0) # Light blue
|
damage_label.color = Color(0.4, 0.65, 1.0) # Light blue
|
||||||
else:
|
else:
|
||||||
damage_label.label = str(int(amount))
|
damage_label.label = str(int(amount))
|
||||||
damage_label.color = Color.ORANGE if is_crit else Color.RED
|
damage_label.color = Color(1.0, 0.6, 0.2) if is_crit else Color(1.0, 0.35, 0.35) # Bright orange / bright red
|
||||||
|
damage_label.z_index = 5
|
||||||
|
|
||||||
# Calculate direction from attacker (slight upward variation)
|
# Calculate direction from attacker (slight upward variation)
|
||||||
var direction_from_attacker = (global_position - from_position).normalized()
|
var direction_from_attacker = (global_position - from_position).normalized()
|
||||||
@@ -6462,6 +6522,32 @@ func _show_damage_number(amount: float, from_position: Vector2, is_crit: bool =
|
|||||||
get_tree().current_scene.add_child(damage_label)
|
get_tree().current_scene.add_child(damage_label)
|
||||||
damage_label.global_position = global_position + Vector2(0, -16)
|
damage_label.global_position = global_position + Vector2(0, -16)
|
||||||
|
|
||||||
|
func _show_revive_cost_number(amount: int):
|
||||||
|
var damage_number_scene = preload("res://scenes/damage_number.tscn")
|
||||||
|
if not damage_number_scene:
|
||||||
|
return
|
||||||
|
var damage_label = damage_number_scene.instantiate()
|
||||||
|
if not damage_label:
|
||||||
|
return
|
||||||
|
damage_label.label = "-" + str(amount) + " HP"
|
||||||
|
damage_label.color = Color(1.0, 0.35, 0.35)
|
||||||
|
damage_label.z_index = 5
|
||||||
|
damage_label.direction = Vector2(0, -1)
|
||||||
|
var game_world = get_tree().get_first_node_in_group("game_world")
|
||||||
|
var parent = game_world.get_node_or_null("Entities") if game_world else get_parent()
|
||||||
|
if parent:
|
||||||
|
parent.add_child(damage_label)
|
||||||
|
damage_label.global_position = global_position + Vector2(0, -16)
|
||||||
|
else:
|
||||||
|
get_tree().current_scene.add_child(damage_label)
|
||||||
|
damage_label.global_position = global_position + Vector2(0, -16)
|
||||||
|
|
||||||
|
@rpc("any_peer", "reliable")
|
||||||
|
func _sync_revive_cost(amount: int):
|
||||||
|
if is_multiplayer_authority():
|
||||||
|
return
|
||||||
|
_show_revive_cost_number(amount)
|
||||||
|
|
||||||
func _on_level_up_stats(stats_increased: Array):
|
func _on_level_up_stats(stats_increased: Array):
|
||||||
# Show floating text for level up - "LEVEL UP!" and stat increases
|
# Show floating text for level up - "LEVEL UP!" and stat increases
|
||||||
# Use damage_number scene with damage_numbers font
|
# Use damage_number scene with damage_numbers font
|
||||||
@@ -6526,6 +6612,20 @@ func _on_level_up_stats(stats_increased: Array):
|
|||||||
stat_text.global_position = global_position + Vector2(0, base_y_offset)
|
stat_text.global_position = global_position + Vector2(0, base_y_offset)
|
||||||
base_y_offset -= y_spacing
|
base_y_offset -= y_spacing
|
||||||
|
|
||||||
|
@rpc("any_peer", "reliable")
|
||||||
|
func rpc_apply_corpse_knockback(dir_x: float, dir_y: float, force: float):
|
||||||
|
if not is_multiplayer_authority():
|
||||||
|
return
|
||||||
|
if not is_dead:
|
||||||
|
return
|
||||||
|
var d = Vector2(dir_x, dir_y)
|
||||||
|
if d.length_squared() < 0.01:
|
||||||
|
return
|
||||||
|
d = d.normalized()
|
||||||
|
velocity = d * force
|
||||||
|
is_knocked_back = true
|
||||||
|
knockback_time = 0.0
|
||||||
|
|
||||||
@rpc("any_peer", "reliable")
|
@rpc("any_peer", "reliable")
|
||||||
func _sync_damage(_amount: float, attacker_position: Vector2, is_crit: bool = false, is_miss: bool = false, is_dodged: bool = false, is_blocked: 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
|
# This RPC only syncs visual effects, not damage application
|
||||||
|
|||||||
@@ -232,4 +232,14 @@ func _on_body_entered(body):
|
|||||||
# Push the hit target away slightly (only for non-enemies)
|
# Push the hit target away slightly (only for non-enemies)
|
||||||
if body is CharacterBody2D and not body.is_in_group("enemy"):
|
if body is CharacterBody2D and not body.is_in_group("enemy"):
|
||||||
var knockback_dir = (body.global_position - global_position).normalized()
|
var knockback_dir = (body.global_position - global_position).normalized()
|
||||||
body.velocity = knockback_dir * 200.0
|
if body.is_in_group("player") and "is_dead" in body and body.is_dead:
|
||||||
|
const CORPSE_KNOCKBACK: float = 50.0
|
||||||
|
var pid = body.get_multiplayer_authority()
|
||||||
|
if pid == multiplayer.get_unique_id():
|
||||||
|
body.rpc_apply_corpse_knockback(knockback_dir.x, knockback_dir.y, CORPSE_KNOCKBACK)
|
||||||
|
elif pid != 0:
|
||||||
|
body.rpc_apply_corpse_knockback.rpc_id(pid, knockback_dir.x, knockback_dir.y, CORPSE_KNOCKBACK)
|
||||||
|
else:
|
||||||
|
body.rpc_apply_corpse_knockback.rpc(knockback_dir.x, knockback_dir.y, CORPSE_KNOCKBACK)
|
||||||
|
else:
|
||||||
|
body.velocity = knockback_dir * 200.0
|
||||||
|
|||||||
@@ -207,4 +207,14 @@ func _on_body_entered(body):
|
|||||||
# Push the hit target away slightly (only for non-enemies)
|
# Push the hit target away slightly (only for non-enemies)
|
||||||
if body is CharacterBody2D and not body.is_in_group("enemy"):
|
if body is CharacterBody2D and not body.is_in_group("enemy"):
|
||||||
var knockback_dir = (body.global_position - global_position).normalized()
|
var knockback_dir = (body.global_position - global_position).normalized()
|
||||||
body.velocity = knockback_dir * 200.0
|
if body.is_in_group("player") and "is_dead" in body and body.is_dead:
|
||||||
|
const CORPSE_KNOCKBACK: float = 50.0
|
||||||
|
var pid = body.get_multiplayer_authority()
|
||||||
|
if pid == multiplayer.get_unique_id():
|
||||||
|
body.rpc_apply_corpse_knockback(knockback_dir.x, knockback_dir.y, CORPSE_KNOCKBACK)
|
||||||
|
elif pid != 0:
|
||||||
|
body.rpc_apply_corpse_knockback.rpc_id(pid, knockback_dir.x, knockback_dir.y, CORPSE_KNOCKBACK)
|
||||||
|
else:
|
||||||
|
body.rpc_apply_corpse_knockback.rpc(knockback_dir.x, knockback_dir.y, CORPSE_KNOCKBACK)
|
||||||
|
else:
|
||||||
|
body.velocity = knockback_dir * 200.0
|
||||||
|
|||||||
@@ -80,4 +80,15 @@ func _on_body_entered(body):
|
|||||||
# Push the hit target away slightly
|
# Push the hit target away slightly
|
||||||
if body is CharacterBody2D:
|
if body is CharacterBody2D:
|
||||||
var knockback_dir = (body.global_position - global_position).normalized()
|
var knockback_dir = (body.global_position - global_position).normalized()
|
||||||
body.velocity = knockback_dir * 200.0
|
if body.is_in_group("player") and "is_dead" in body and body.is_dead:
|
||||||
|
# Corpse: reduced force, sync to victim's client so they see the push
|
||||||
|
const CORPSE_KNOCKBACK: float = 50.0
|
||||||
|
var pid = body.get_multiplayer_authority()
|
||||||
|
if pid == multiplayer.get_unique_id():
|
||||||
|
body.rpc_apply_corpse_knockback(knockback_dir.x, knockback_dir.y, CORPSE_KNOCKBACK)
|
||||||
|
elif pid != 0:
|
||||||
|
body.rpc_apply_corpse_knockback.rpc_id(pid, knockback_dir.x, knockback_dir.y, CORPSE_KNOCKBACK)
|
||||||
|
else:
|
||||||
|
body.rpc_apply_corpse_knockback.rpc(knockback_dir.x, knockback_dir.y, CORPSE_KNOCKBACK)
|
||||||
|
else:
|
||||||
|
body.velocity = knockback_dir * 200.0
|
||||||
|
|||||||
Reference in New Issue
Block a user