extends Area2D # Web shot from boss: frame 568 in shade_spell_effects (105h x 79v). 8x8 collision. # Flies to target; on hit player or arrival: animate 568->573. # If arrived at point: stay 30s, fade out at end (net doesn't stick during fade). # If hit player: player stuck 5s (frame 679 netted), fade last 0.3s; other player can cut net. # Netted player cannot attack with main weapon. var target_position: Vector2 = Vector2.ZERO var speed: float = 140.0 var state: String = "flying" # flying | impact_anim | landed_net | hit_player var impact_frame: int = 568 var impact_frame_end: int = 573 var landed_lifetime: float = 30.0 var landed_timer: float = 0.0 var fade_start: float = 4.0 # Start fade this many seconds before end var fired_by_boss: Node = null var hit_player: Node = null # When state == hit_player, the netted player var netted_duration: float = 5.0 var netted_timer: float = 0.0 var netted_fade_start: float = 0.3 const TILE_SIZE: int = 16 const HFRAMES: int = 105 const VFRAMES: int = 79 @onready var sprite: Sprite2D = $Sprite2D @onready var sfx_webbed: AudioStreamPlayer2D = $SfxWebbed @onready var anim_timer: float = 0.0 var anim_frame: int = 568 func _ready() -> void: # Collision: detect players (layer 1) collision_layer = 0 collision_mask = 1 body_entered.connect(_on_body_entered) anim_frame = impact_frame if sprite: sprite.hframes = HFRAMES sprite.vframes = VFRAMES sprite.frame = impact_frame sprite.centered = true func set_target(pos: Vector2) -> void: target_position = pos func set_fired_by_boss(boss: Node) -> void: fired_by_boss = boss func _process(delta: float) -> void: if state == "flying": var dir = (target_position - global_position).normalized() var dist = global_position.distance_to(target_position) if dist <= speed * delta: global_position = target_position _impact_at_point() else: global_position += dir * speed * delta return if state == "impact_anim": anim_timer += delta var frame_count = impact_frame_end - impact_frame + 1 var duration = 0.25 var t = clamp(anim_timer / duration, 0.0, 1.0) var idx = impact_frame + int(t * (frame_count - 1)) if sprite: sprite.frame = idx if anim_timer >= duration: state = "landed_net" landed_timer = 0.0 collision_layer = 1 # Can be hit by other players to cut? collision_mask = 1 # Detect players (for cutting / fade stick logic) return if state == "landed_net": landed_timer += delta # Fade during last fade_start seconds if landed_timer >= landed_lifetime - fade_start: var fade_t = (landed_timer - (landed_lifetime - fade_start)) / fade_start if sprite: sprite.modulate.a = 1.0 - fade_t # During fade, net doesn't stick - no collision with player for stick if landed_timer >= landed_lifetime: queue_free() return if state == "hit_player": netted_timer += delta # Keep net at player position if hit_player and is_instance_valid(hit_player): global_position = hit_player.global_position # Fade last 0.3s if netted_timer >= netted_duration - netted_fade_start: var fade_t = (netted_timer - (netted_duration - netted_fade_start)) / netted_fade_start if sprite: sprite.modulate.a = 1.0 - fade_t if netted_timer >= netted_duration: _release_netted_player() queue_free() else: queue_free() func _on_body_entered(body: Node2D) -> void: if state == "flying": if body.is_in_group("player"): _impact_on_player(body) return # Reached target (or hit wall) - treat as impact at point if global_position.distance_to(target_position) < 24.0: _impact_at_point() return if state == "landed_net" and body.is_in_group("player"): _impact_on_player(body) func _impact_at_point() -> void: state = "impact_anim" anim_timer = 0.0 # Stop moving; play 568->573 collision_layer = 0 collision_mask = 0 # After anim, stay 30s then fade (handled in _process) func _impact_on_player(player: Node2D) -> void: if not player.is_in_group("player"): return # Already netted? if "netted_by_web" in player and player.netted_by_web != null: return state = "hit_player" hit_player = player netted_timer = 0.0 if sfx_webbed: sfx_webbed.play() # Netted: cannot move? User said "player gets stuck" - so disable movement and main weapon if player.has_method("_web_net_apply"): player._web_net_apply(self) else: # Fallback: set a flag player script can check player.set_meta("netted_by_web", self) # Show net on player (frame 679) - player script will show overlay if player.has_method("_web_net_show_netted_frame"): player._web_net_show_netted_frame(true) # Impact anim 568->573 briefly anim_timer = 0.0 if sprite: sprite.frame = impact_frame # Layer 8 so player attack areas (punch/sword/axe mask 8) can hit and cut the net collision_layer = 8 collision_mask = 0 func _release_netted_player() -> void: if hit_player and is_instance_valid(hit_player): if hit_player.has_method("_web_net_release"): hit_player._web_net_release(self) else: hit_player.remove_meta("netted_by_web") if hit_player.has_method("_web_net_show_netted_frame"): hit_player._web_net_show_netted_frame(false) hit_player = null # Called when another player attacks this web (to cut the net) func cut_by_attack(_from_player: Node2D) -> void: if state == "hit_player": _release_netted_player() queue_free()