fix spider bat boss alittle

This commit is contained in:
2026-02-06 02:49:58 +01:00
parent 9b8d84357f
commit fa7e969363
86 changed files with 4319 additions and 763 deletions

View File

@@ -72,6 +72,10 @@ var is_knocked_back: bool = false
var knockback_time: float = 0.0
var knockback_duration: float = 0.3 # How long knockback lasts
# Web net (boss): when netted_by_web != null, player is stuck and cannot attack with main weapon
var netted_by_web: Node = null
var netted_overlay_sprite: Sprite2D = null # Frame 679 for netted visual
# Attack/Combat
var can_attack: bool = true
var attack_cooldown: float = 0.0 # No cooldown - instant attacks!
@@ -182,6 +186,7 @@ const HELD_POSITION_Z: float = 12.0 # Z height when held/lifted (above ground; i
@onready var sfx_die = $SfxDie
@onready var sfx_look_out = $SfxLookOut
@onready var sfx_ahaa = $SfxAhaa
@onready var sfx_secret_found = $SfxSecretFound
# Alert indicator (exclamation mark)
var alert_indicator: Sprite2D = null
@@ -1493,32 +1498,23 @@ func _can_place_down_at(place_pos: Vector2, placed_obj: Node) -> bool:
placed_shape_transform = Transform2D(0.0, place_pos)
# Check if the placed object's collision shape would collide with anything
# This includes: walls, other objects, and players
# This includes: walls, other objects, and players (not Area2D triggers - those don't block placement)
var params = PhysicsShapeQueryParameters2D.new()
params.shape = placed_shape
params.transform = placed_shape_transform
params.collision_mask = 1 | 2 | 64 # Players (layer 1), objects (layer 2), walls (layer 7 = bit 6 = 64)
params.collide_with_areas = false # Only solid bodies block; ignore trigger areas (e.g. door key zones)
params.collide_with_bodies = true
# CRITICAL: Exclude self, the object being placed, and make sure to exclude it properly
# The object might still be in the scene tree with collision disabled, so we need to exclude it
var exclude_list = [self]
if placed_obj and is_instance_valid(placed_obj):
exclude_list.append(placed_obj)
# CRITICAL: Exclude using RIDs so the physics engine actually excludes them (Node refs may not work)
var exclude_list: Array[RID] = [get_rid()]
if placed_obj and is_instance_valid(placed_obj) and placed_obj is CollisionObject2D:
exclude_list.append(placed_obj.get_rid())
params.exclude = exclude_list
# Test the actual collision shape at the placement position
var hits = space_state.intersect_shape(params, 32) # Check up to 32 collisions
# Debug: Log what we found
if hits.size() > 0:
print("DEBUG: Placement blocked - found ", hits.size(), " collisions at ", place_pos)
for i in min(hits.size(), 3): # Log first 3 collisions
var hit = hits[i]
if hit.has("collider"):
print(" - Collision with: ", hit.collider, " (", hit.collider.name if hit.collider else "null", ")")
if hit.has("rid"):
print(" - RID: ", hit.rid)
# If any collisions found, placement is invalid
return hits.size() == 0
@@ -1964,6 +1960,8 @@ func _physics_process(delta):
const MANA_REGEN_RATE = 2.0 # mana per second
if character_stats.mp < character_stats.maxmp:
character_stats.restore_mana(MANA_REGEN_RATE * delta)
# Tick down temporary buffs (e.g. dodge potion)
character_stats.tick_buffs(delta)
# Spell charge visuals (incantation, tint pulse) - run for ALL players so others see the pulse
if is_charging_spell:
@@ -2496,8 +2494,8 @@ func _handle_input():
if new_direction != current_direction:
current_direction = new_direction
_update_cone_light_rotation()
elif is_pushing:
# Keep locked direction when pushing
elif is_pushing or (held_object and not is_lifting):
# Keep direction from when grab started (don't turn to face the object)
if push_direction_locked != current_direction:
current_direction = push_direction_locked as Direction
_update_cone_light_rotation()
@@ -2539,9 +2537,10 @@ func _handle_input():
elif is_lifting:
if current_animation != "LIFT" and current_animation != "IDLE_HOLD":
_set_animation("IDLE_HOLD")
elif is_pushing:
_set_animation("IDLE_PUSH")
# Keep locked direction when pushing
elif is_pushing or (held_object and not is_lifting):
if is_pushing:
_set_animation("IDLE_PUSH")
# Keep direction from when grab started
if push_direction_locked != current_direction:
current_direction = push_direction_locked as Direction
_update_cone_light_rotation()
@@ -2598,8 +2597,8 @@ func _handle_input():
if character_stats and character_stats.is_over_encumbered():
current_speed = base_speed * 0.25
# Lock movement if movement_lock_timer is active or reviving a corpse
if movement_lock_timer > 0.0 or is_reviving:
# Lock movement if movement_lock_timer is active, reviving a corpse, or netted by web
if movement_lock_timer > 0.0 or is_reviving or netted_by_web:
velocity = Vector2.ZERO
else:
velocity = input_vector * current_speed
@@ -2623,6 +2622,32 @@ func _handle_walking_sfx():
if sfx_walk and sfx_walk.playing:
sfx_walk.stop()
func _web_net_apply(web_node: Node) -> void:
netted_by_web = web_node
func _web_net_release(_web_node: Node) -> void:
netted_by_web = null
_web_net_show_netted_frame(false)
func _web_net_show_netted_frame(show_net: bool) -> void:
if show_net:
if netted_overlay_sprite == null:
netted_overlay_sprite = Sprite2D.new()
var tex = load("res://assets/gfx/fx/shade_spell_effects.png") as Texture2D
if tex:
netted_overlay_sprite.texture = tex
netted_overlay_sprite.hframes = 105
netted_overlay_sprite.vframes = 79
netted_overlay_sprite.frame = 679
netted_overlay_sprite.centered = true
netted_overlay_sprite.z_index = 5
add_child(netted_overlay_sprite)
if netted_overlay_sprite:
netted_overlay_sprite.visible = true
else:
if netted_overlay_sprite:
netted_overlay_sprite.visible = false
func _handle_interactions():
var grab_button_down = false
var grab_just_pressed = false
@@ -3192,7 +3217,7 @@ func _handle_interactions():
# Handle bow charging
if has_bow_and_arrows and not is_lifting and not is_pushing:
if attack_just_pressed and can_attack and not is_charging_bow and not spawn_landing:
if attack_just_pressed and can_attack and not is_charging_bow and not spawn_landing and not netted_by_web:
if !$SfxBuckleBow.playing:
$SfxBuckleBow.play()
# Start charging bow
@@ -3260,7 +3285,7 @@ func _handle_interactions():
# Normal attack (non-bow or no arrows)
# Also allow throwing when lifting (even if bow is equipped). Block during spawn fall.
if attack_just_pressed and can_attack and not spawn_landing:
if attack_just_pressed and can_attack and not spawn_landing and not netted_by_web:
if is_lifting:
# Attack while lifting -> throw immediately in facing direction
_force_throw_held_object(facing_direction_vector)
@@ -3382,11 +3407,12 @@ func _try_grab():
# Store the distance from player to object when grabbed (for placement)
grab_distance = global_position.distance_to(closest_body.global_position)
# Calculate push axis from grab direction (but don't move the object yet)
var grab_direction = grab_offset.normalized()
# Use player's current facing when grab started (do not turn to face the object)
var grab_direction = facing_direction_vector.normalized() if facing_direction_vector.length() > 0.1 else last_movement_direction.normalized()
if grab_direction.length() < 0.1:
grab_direction = last_movement_direction
grab_direction = Vector2.DOWN
push_axis = _snap_to_8_directions(grab_direction)
push_direction_locked = _get_direction_from_vector(push_axis) as Direction
# Disable collision with players and other objects when grabbing
# But keep collision with walls (layer 7) enabled for pushing
@@ -3503,15 +3529,11 @@ func _start_pushing():
is_pushing = true
is_lifting = false
# Lock to the direction we're facing when we start pushing
var initial_direction = grab_offset.normalized()
# Keep the direction we had when we started the grab (do not face the object)
var initial_direction = facing_direction_vector.normalized() if facing_direction_vector.length() > 0.1 else last_movement_direction.normalized()
if initial_direction.length() < 0.1:
initial_direction = last_movement_direction.normalized()
# Snap to one of 8 directions
initial_direction = Vector2.DOWN
push_axis = _snap_to_8_directions(initial_direction)
# Lock the facing direction (for both animation and attacks)
push_direction_locked = _get_direction_from_vector(push_axis)
facing_direction_vector = push_axis.normalized()
@@ -4045,7 +4067,7 @@ func _place_down_object():
print("Placed down ", placed_obj.name, " at ", place_pos)
func _perform_attack():
if not can_attack or is_attacking or spawn_landing:
if not can_attack or is_attacking or spawn_landing or netted_by_web:
return
can_attack = false
@@ -6387,6 +6409,13 @@ func take_damage(amount: float, attacker_position: Vector2, is_burn_damage: bool
# Invulnerable during fallout sink (can't take damage from anything while falling)
if fallout_state:
return
# Taking damage while webbed immediately frees you from the web
if netted_by_web:
var web = netted_by_web
netted_by_web = null
_web_net_show_netted_frame(false)
if web and is_instance_valid(web) and web.has_method("cut_by_attack"):
web.cut_by_attack(null)
# Cancel bow charging when taking damage
if is_charging_bow:
@@ -6552,10 +6581,9 @@ func take_damage(amount: float, attacker_position: Vector2, is_burn_damage: bool
if multiplayer.has_multiplayer_peer() and can_send_rpcs and is_inside_tree():
_rpc_to_ready_peers("_sync_damage", [actual_damage, attacker_position])
# Check if dead - but wait for damage animation to play first
# Use small epsilon to handle floating point precision issues (HP might be 0.0000001 instead of exactly 0.0)
# Check if dead - below 1 HP must always trigger death (trap, etc.)
var health = character_stats.hp if character_stats else current_health
if health <= 0.001: # Use epsilon to catch values very close to 0
if health < 1.0:
if character_stats:
character_stats.hp = 0.0 # Clamp to exactly 0
else:
@@ -6563,7 +6591,8 @@ func take_damage(amount: float, attacker_position: Vector2, is_burn_damage: bool
is_dead = true # Set flag immediately to prevent more damage
# Wait a bit for damage animation and knockback to show
await get_tree().create_timer(0.3).timeout
_die()
if is_instance_valid(self) and is_dead:
_die()
func _die():
# Already processing death - prevent multiple concurrent death sequences
@@ -7575,6 +7604,10 @@ func _on_level_up_stats(stats_increased: Array):
if not character_stats:
return
# Play level-up fanfare locally only when this player (you) gained the level
if is_multiplayer_authority() and has_node("SfxLevelUp"):
$SfxLevelUp.play()
# Stat name to display name mapping
var stat_display_names = {
"str": "STR",
@@ -7651,6 +7684,17 @@ func rpc_apply_corpse_knockback(dir_x: float, dir_y: float, force: float):
is_knocked_back = true
knockback_time = 0.0
@rpc("any_peer", "reliable")
func _on_attack_blocked_by_enemy(blocker_position: Vector2):
# Called when this player's attack was blocked by an enemy (e.g. humanoid with shield). Show BLOCKED and small knockback.
var dir_away = (global_position - blocker_position).normalized()
if dir_away.length() < 0.01:
dir_away = Vector2.RIGHT
velocity = dir_away * 75.0
is_knocked_back = true
knockback_time = 0.0
_show_damage_number(0.0, blocker_position, false, false, false, true)
@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):
# This RPC only syncs visual effects, not damage application
@@ -7796,6 +7840,26 @@ func _sync_trap_detected_alert():
if sfx_look_out:
sfx_look_out.play()
@rpc("any_peer", "reliable")
func _on_cracked_floor_detected():
# Called when this player detects a cracked floor (perception roll success). Only the detecting player plays SfxLookOut and sees alert.
if not is_multiplayer_authority():
return
_show_alert_indicator()
if sfx_look_out:
sfx_look_out.play()
@rpc("any_peer", "reliable")
func _on_secret_chest_detected():
# Called when this player detects a hidden chest (perception roll success). Only the detecting player plays SfxAhaa + SfxSecretFound and sees alert.
if not is_multiplayer_authority():
return
_show_alert_indicator()
if sfx_ahaa:
sfx_ahaa.play()
if sfx_secret_found:
sfx_secret_found.play()
@rpc("any_peer", "reliable")
func _sync_exit_found_alert():
# Sync exit found alert to all clients