made some adjustments to brights of rooms

This commit is contained in:
2026-01-25 18:28:30 +01:00
parent 7abadb92a9
commit e7204b92d2
9 changed files with 476 additions and 219 deletions

View File

@@ -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")

View File

@@ -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

View File

@@ -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()

View File

@@ -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,12 +1146,22 @@ 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 is_revive:
if me == tid and target.has_method("_revive_from_heal"):
target._revive_from_heal(display_amount)
elif tid != 0 and target.has_method("_revive_from_heal"):
target._revive_from_heal.rpc_id(tid, display_amount)
else:
if me == tid and target.has_method("heal") and amount_to_apply > 0: if me == tid and target.has_method("heal") and amount_to_apply > 0:
target.heal(amount_to_apply, allow_overheal) target.heal(amount_to_apply, allow_overheal)
# When revive, target's authority already spawned effect+text in _revive_from_heal; skip to avoid double spawn
var skip_spawn = is_revive and me == tid
if not skip_spawn:
var entities = get_node_or_null("Entities") var entities = get_node_or_null("Entities")
var parent = entities if entities else target.get_parent() var parent = entities if entities else target.get_parent()
if not parent: if not parent:
return pass
else:
var eff_scene = load("res://scenes/healing_effect.tscn") as PackedScene var eff_scene = load("res://scenes/healing_effect.tscn") as PackedScene
if eff_scene: if eff_scene:
var eff = eff_scene.instantiate() var eff = eff_scene.instantiate()
@@ -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

View File

@@ -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

View File

@@ -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,11 +1783,26 @@ func _physics_process(delta):
else: else:
spell_charge_tint_pulse_time = 0.0 spell_charge_tint_pulse_time = 0.0
if is_local_player and is_multiplayer_authority(): # Revive charge visuals - same as healing (healing_charging on AnimationIncantation)
# Skip all input and logic if dead if is_reviving:
if is_dead: was_reviving_last_frame = true
return 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():
# When dead: only corpse knockback friction + sync; no input or other logic
if is_dead:
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) # Handle knockback timer (always handle knockback, even when controls are disabled)
if is_knocked_back: if is_knocked_back:
knockback_time += delta knockback_time += delta
@@ -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,13 +4068,19 @@ 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
var is_overheal = false
if is_revive:
# Tome revive: no overheal, full amount revives
actual_heal = amount
else:
var overheal_chance_pct = 1.0 + lck_val * 0.3
var can_overheal = target.character_stats and target.character_stats.hp <= target.character_stats.maxhp
is_overheal = can_overheal and rng.randf() * 100.0 < overheal_chance_pct
if is_overheal: if is_overheal:
allow_overheal = true allow_overheal = true
else: else:
@@ -4069,13 +4091,18 @@ func _cast_heal_spell(target: Node):
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 is_revive:
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: if me == tid and actual_heal > 0:
target.heal(actual_heal, allow_overheal) 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():
@@ -6103,6 +6157,11 @@ func _sync_respawn(spawn_pos: Vector2):
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

View File

@@ -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()
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 body.velocity = knockback_dir * 200.0

View File

@@ -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()
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 body.velocity = knockback_dir * 200.0

View File

@@ -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()
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 body.velocity = knockback_dir * 200.0