added bomb
This commit is contained in:
419
src/scripts/attack_bomb.gd
Normal file
419
src/scripts/attack_bomb.gd
Normal file
@@ -0,0 +1,419 @@
|
||||
extends CharacterBody2D
|
||||
|
||||
# Bomb - Explosive projectile that can be thrown or placed
|
||||
|
||||
@export var fuse_duration: float = 3.0 # Time until explosion
|
||||
@export var base_damage: float = 50.0 # Base damage (increased from 30)
|
||||
@export var damage_radius: float = 48.0 # Area of effect radius (48x48)
|
||||
@export var screenshake_strength: float = 5.0 # Base screenshake strength
|
||||
|
||||
var player_owner: Node = null
|
||||
var is_fused: bool = false
|
||||
var fuse_timer: float = 0.0
|
||||
var is_thrown: bool = false # True if thrown by Dwarf, false if placed
|
||||
var is_exploding: bool = false
|
||||
var explosion_frame: int = 0
|
||||
var explosion_timer: float = 0.0
|
||||
|
||||
# Z-axis physics (like interactable_object)
|
||||
var position_z: float = 0.0
|
||||
var velocity_z: float = 0.0
|
||||
var gravity_z: float = 500.0
|
||||
var is_airborne: bool = false
|
||||
var throw_velocity: Vector2 = Vector2.ZERO
|
||||
|
||||
# Blinking animation
|
||||
var blink_timer: float = 0.0
|
||||
var bomb_visible: bool = true # Renamed to avoid shadowing CanvasItem.is_visible
|
||||
var blink_start_time: float = 1.0 # Start blinking 1 second before explosion
|
||||
|
||||
# Collection
|
||||
var can_be_collected: bool = false
|
||||
var collection_delay: float = 0.2 # Can be collected after 0.2 seconds
|
||||
|
||||
@onready var sprite = $Sprite2D
|
||||
@onready var explosion_sprite = $ExplosionSprite
|
||||
@onready var shadow = $Shadow
|
||||
@onready var bomb_area = $BombArea
|
||||
@onready var collection_area = $CollectionArea
|
||||
@onready var fuse_particles = $FuseParticles
|
||||
@onready var fuse_light = $FuseLight
|
||||
@onready var explosion_light = $ExplosionLight
|
||||
|
||||
# Damage area (larger than collision)
|
||||
var damage_area_shape: CircleShape2D = null
|
||||
|
||||
func _ready():
|
||||
# Set collision layer to 2 (interactable objects) so it can be grabbed
|
||||
collision_layer = 2
|
||||
collision_mask = 1 | 2 | 64 # Collide with players, objects, and walls
|
||||
|
||||
# Connect area signals
|
||||
if bomb_area and not bomb_area.body_entered.is_connected(_on_bomb_area_body_entered):
|
||||
bomb_area.body_entered.connect(_on_bomb_area_body_entered)
|
||||
if collection_area and not collection_area.body_entered.is_connected(_on_collection_area_body_entered):
|
||||
collection_area.body_entered.connect(_on_collection_area_body_entered)
|
||||
|
||||
# Ensure bomb sprite is visible
|
||||
if sprite:
|
||||
sprite.visible = true
|
||||
|
||||
# Hide explosion sprite initially
|
||||
if explosion_sprite:
|
||||
explosion_sprite.visible = false
|
||||
|
||||
# Setup damage area (48x48 radius)
|
||||
if bomb_area:
|
||||
var collision_shape = bomb_area.get_node_or_null("CollisionShape2D")
|
||||
if collision_shape:
|
||||
damage_area_shape = CircleShape2D.new()
|
||||
damage_area_shape.radius = damage_radius / 2.0 # 24 pixel radius for 48x48
|
||||
collision_shape.shape = damage_area_shape
|
||||
|
||||
# Start fuse if not thrown (placed bomb starts fusing immediately)
|
||||
if not is_thrown:
|
||||
_start_fuse()
|
||||
|
||||
func setup(target_position: Vector2, owner_player: Node, throw_force: Vector2 = Vector2.ZERO, thrown: bool = false):
|
||||
# Don't overwrite position if already set (for thrown bombs)
|
||||
if not thrown:
|
||||
global_position = target_position
|
||||
|
||||
player_owner = owner_player
|
||||
is_thrown = thrown
|
||||
throw_velocity = throw_force
|
||||
|
||||
if is_thrown:
|
||||
# Thrown bomb - start airborne
|
||||
is_airborne = true
|
||||
position_z = 2.5
|
||||
velocity_z = 100.0
|
||||
# Make sure sprite is visible
|
||||
if sprite:
|
||||
sprite.visible = true
|
||||
# Start fuse after landing
|
||||
else:
|
||||
# Placed bomb - start fusing immediately
|
||||
_start_fuse()
|
||||
|
||||
func _start_fuse():
|
||||
if is_fused:
|
||||
return
|
||||
|
||||
is_fused = true
|
||||
fuse_timer = 0.0
|
||||
|
||||
# Play fuse sound
|
||||
if has_node("SfxFuse"):
|
||||
$SfxFuse.play()
|
||||
|
||||
# Start fuse particles
|
||||
if fuse_particles:
|
||||
fuse_particles.emitting = true
|
||||
|
||||
# Enable fuse light
|
||||
if fuse_light:
|
||||
fuse_light.enabled = true
|
||||
|
||||
print("Bomb fuse started!")
|
||||
|
||||
func _physics_process(delta):
|
||||
if is_exploding:
|
||||
# Handle explosion animation
|
||||
explosion_timer += delta
|
||||
if explosion_sprite:
|
||||
# Play 9 frames of explosion animation at ~15 FPS
|
||||
if explosion_timer >= 0.06666667: # ~15 FPS
|
||||
explosion_timer = 0.0
|
||||
explosion_frame += 1
|
||||
if explosion_frame < 9:
|
||||
explosion_sprite.frame = explosion_frame
|
||||
else:
|
||||
# Explosion animation complete - remove
|
||||
queue_free()
|
||||
return
|
||||
|
||||
# Update fuse timer
|
||||
if is_fused:
|
||||
fuse_timer += delta
|
||||
|
||||
# Start blinking when close to explosion
|
||||
if fuse_timer >= (fuse_duration - blink_start_time):
|
||||
blink_timer += delta
|
||||
if blink_timer >= 0.1: # Blink every 0.1 seconds
|
||||
blink_timer = 0.0
|
||||
bomb_visible = not bomb_visible
|
||||
if sprite:
|
||||
sprite.modulate.a = 1.0 if bomb_visible else 0.3
|
||||
|
||||
# Explode when fuse runs out
|
||||
if fuse_timer >= fuse_duration:
|
||||
_explode()
|
||||
return
|
||||
|
||||
# Z-axis physics (if thrown)
|
||||
if is_thrown and is_airborne:
|
||||
# Apply gravity
|
||||
velocity_z -= gravity_z * delta
|
||||
position_z += velocity_z * delta
|
||||
|
||||
# Update sprite position based on height
|
||||
if sprite:
|
||||
sprite.position.y = -position_z * 0.5
|
||||
var height_scale = 1.0 - (position_z / 50.0) * 0.2
|
||||
sprite.scale = Vector2.ONE * max(0.8, height_scale)
|
||||
|
||||
# Update shadow
|
||||
if shadow:
|
||||
var shadow_scale = 1.0 - (position_z / 75.0) * 0.5
|
||||
shadow.scale = Vector2.ONE * max(0.5, shadow_scale)
|
||||
shadow.modulate.a = 0.5 - (position_z / 100.0) * 0.3
|
||||
|
||||
# Apply throw velocity
|
||||
velocity = throw_velocity
|
||||
|
||||
# Check if landed
|
||||
if position_z <= 0.0:
|
||||
_land()
|
||||
else:
|
||||
# On ground - reset sprite/shadow
|
||||
if sprite:
|
||||
sprite.position.y = 0
|
||||
sprite.scale = Vector2.ONE
|
||||
if shadow:
|
||||
shadow.scale = Vector2.ONE
|
||||
shadow.modulate.a = 0.5
|
||||
|
||||
# Apply friction if on ground
|
||||
if not is_airborne:
|
||||
throw_velocity = throw_velocity.lerp(Vector2.ZERO, delta * 5.0)
|
||||
if throw_velocity.length() < 5.0:
|
||||
throw_velocity = Vector2.ZERO
|
||||
velocity = throw_velocity
|
||||
|
||||
# Move
|
||||
move_and_slide()
|
||||
|
||||
# Check for collisions while airborne (instant explode on enemy/player hit)
|
||||
if is_airborne and throw_velocity.length() > 50.0:
|
||||
for i in get_slide_collision_count():
|
||||
var collision = get_slide_collision(i)
|
||||
var collider = collision.get_collider()
|
||||
|
||||
if collider and (collider.is_in_group("player") or collider.is_in_group("enemy")):
|
||||
if collider != player_owner:
|
||||
# Instant explode on hit
|
||||
_explode()
|
||||
return
|
||||
|
||||
# Enable collection after delay
|
||||
if is_fused and not can_be_collected:
|
||||
if fuse_timer >= collection_delay:
|
||||
can_be_collected = true
|
||||
if collection_area:
|
||||
collection_area.monitoring = true
|
||||
|
||||
func _land():
|
||||
is_airborne = false
|
||||
position_z = 0.0
|
||||
velocity_z = 0.0
|
||||
|
||||
# Start fuse when landing
|
||||
if not is_fused:
|
||||
_start_fuse()
|
||||
|
||||
func _explode():
|
||||
if is_exploding:
|
||||
return
|
||||
|
||||
is_exploding = true
|
||||
|
||||
# Hide bomb sprite, show explosion
|
||||
if sprite:
|
||||
sprite.visible = false
|
||||
if explosion_sprite:
|
||||
explosion_sprite.visible = true
|
||||
explosion_sprite.frame = 0
|
||||
explosion_frame = 0
|
||||
explosion_timer = 0.0
|
||||
|
||||
# Stop fuse sound and particles
|
||||
if has_node("SfxFuse"):
|
||||
$SfxFuse.stop()
|
||||
if fuse_particles:
|
||||
fuse_particles.emitting = false
|
||||
|
||||
# Disable fuse light, enable explosion light
|
||||
if fuse_light:
|
||||
fuse_light.enabled = false
|
||||
if explosion_light:
|
||||
explosion_light.enabled = true
|
||||
# Fade out explosion light over time
|
||||
var tween = create_tween()
|
||||
tween.tween_property(explosion_light, "energy", 0.0, 0.3)
|
||||
tween.tween_callback(func(): if explosion_light: explosion_light.enabled = false)
|
||||
|
||||
# Play explosion sound
|
||||
if has_node("SfxExplosion"):
|
||||
$SfxExplosion.play()
|
||||
|
||||
# Deal area damage
|
||||
_deal_explosion_damage()
|
||||
|
||||
# Cause screenshake
|
||||
_cause_screenshake()
|
||||
|
||||
# Disable collision
|
||||
if bomb_area:
|
||||
bomb_area.set_deferred("monitoring", false)
|
||||
if collection_area:
|
||||
collection_area.set_deferred("monitoring", false)
|
||||
|
||||
print("Bomb exploded!")
|
||||
|
||||
func _deal_explosion_damage():
|
||||
if not bomb_area:
|
||||
return
|
||||
|
||||
# Get all bodies in explosion radius
|
||||
var bodies = bomb_area.get_overlapping_bodies()
|
||||
|
||||
# Calculate total damage including strength bonus
|
||||
var total_damage = base_damage
|
||||
|
||||
# Add strength-based bonus damage
|
||||
if player_owner and player_owner.character_stats:
|
||||
var strength = player_owner.character_stats.baseStats.str + player_owner.character_stats.get_pass("str")
|
||||
# Add 1.5 damage per strength point
|
||||
var strength_bonus = strength * 1.5
|
||||
total_damage += strength_bonus
|
||||
print("Bomb damage: base=", base_damage, " + str_bonus=", strength_bonus, " (STR=", strength, ") = ", total_damage)
|
||||
|
||||
for body in bodies:
|
||||
# CRITICAL: Only the bomb owner (authority) should deal damage
|
||||
if player_owner and not player_owner.is_multiplayer_authority():
|
||||
continue
|
||||
|
||||
# Note: Removed the check that skips player_owner - bombs now damage the player who used them!
|
||||
|
||||
# Calculate distance for damage falloff
|
||||
var distance = global_position.distance_to(body.global_position)
|
||||
var damage_multiplier = 1.0 - (distance / damage_radius) # Linear falloff
|
||||
damage_multiplier = max(0.1, damage_multiplier) # Minimum 10% damage
|
||||
var final_damage = total_damage * damage_multiplier
|
||||
|
||||
# Deal damage to players
|
||||
if body.is_in_group("player") and body.has_method("rpc_take_damage"):
|
||||
var attacker_pos = player_owner.global_position if player_owner else global_position
|
||||
var player_peer_id = body.get_multiplayer_authority()
|
||||
|
||||
if player_peer_id != 0:
|
||||
if multiplayer.is_server() and player_peer_id == multiplayer.get_unique_id():
|
||||
body.rpc_take_damage(final_damage, attacker_pos)
|
||||
else:
|
||||
body.rpc_take_damage.rpc_id(player_peer_id, final_damage, attacker_pos)
|
||||
else:
|
||||
body.rpc_take_damage.rpc(final_damage, attacker_pos)
|
||||
|
||||
print("Bomb hit player: ", body.name, " for ", final_damage, " damage!")
|
||||
|
||||
# Deal damage to enemies
|
||||
elif body.is_in_group("enemy") and body.has_method("rpc_take_damage"):
|
||||
var attacker_pos = player_owner.global_position if player_owner else global_position
|
||||
var enemy_peer_id = body.get_multiplayer_authority()
|
||||
|
||||
if enemy_peer_id != 0:
|
||||
if multiplayer.is_server() and enemy_peer_id == multiplayer.get_unique_id():
|
||||
body.rpc_take_damage(final_damage, attacker_pos, false, false, false)
|
||||
else:
|
||||
body.rpc_take_damage.rpc_id(enemy_peer_id, final_damage, attacker_pos, false, false, false)
|
||||
else:
|
||||
body.rpc_take_damage.rpc(final_damage, attacker_pos, false, false, false)
|
||||
|
||||
print("Bomb hit enemy: ", body.name, " for ", final_damage, " damage!")
|
||||
|
||||
func _cause_screenshake():
|
||||
# Calculate screenshake based on distance from local players
|
||||
var game_world = get_tree().get_first_node_in_group("game_world")
|
||||
if not game_world:
|
||||
return
|
||||
|
||||
# Get local players from player_manager
|
||||
var player_manager = game_world.get_node_or_null("PlayerManager")
|
||||
if not player_manager or not player_manager.has_method("get_local_players"):
|
||||
return
|
||||
|
||||
var local_players = player_manager.get_local_players()
|
||||
if local_players.is_empty():
|
||||
return
|
||||
|
||||
# Find closest local player
|
||||
var min_distance = INF
|
||||
for player in local_players:
|
||||
if not is_instance_valid(player):
|
||||
continue
|
||||
var distance = global_position.distance_to(player.global_position)
|
||||
min_distance = min(min_distance, distance)
|
||||
|
||||
# Calculate screenshake strength (inverse distance, capped)
|
||||
var shake_strength = screenshake_strength / max(1.0, min_distance / 50.0)
|
||||
shake_strength = min(shake_strength, screenshake_strength * 2.0) # Cap at 2x base
|
||||
|
||||
# Apply screenshake
|
||||
if game_world.has_method("add_screenshake"):
|
||||
game_world.add_screenshake(shake_strength, 0.3) # 0.3 second duration
|
||||
|
||||
func _on_bomb_area_body_entered(_body):
|
||||
# This is for explosion damage (handled in _deal_explosion_damage)
|
||||
pass
|
||||
|
||||
func can_be_grabbed() -> bool:
|
||||
# Bomb can be grabbed if it's fused and can be collected
|
||||
return is_fused and can_be_collected and not is_exploding
|
||||
|
||||
func on_grabbed(by_player):
|
||||
# When bomb is grabbed, collect it immediately (don't wait for release)
|
||||
if not can_be_collected or is_exploding:
|
||||
return
|
||||
|
||||
if not by_player.character_stats:
|
||||
return
|
||||
|
||||
var offhand_item = by_player.character_stats.equipment.get("offhand", null)
|
||||
var can_collect = false
|
||||
|
||||
if not offhand_item:
|
||||
# Empty offhand - can collect
|
||||
can_collect = true
|
||||
elif offhand_item.item_name == "Bomb":
|
||||
# Already has bombs - can stack
|
||||
can_collect = true
|
||||
|
||||
if can_collect:
|
||||
# Create bomb item
|
||||
var bomb_item = ItemDatabase.create_item("bomb")
|
||||
if bomb_item:
|
||||
bomb_item.quantity = 1
|
||||
|
||||
if not offhand_item:
|
||||
# Equip to offhand
|
||||
by_player.character_stats.equipment["offhand"] = bomb_item
|
||||
else:
|
||||
# Add to existing stack
|
||||
offhand_item.quantity += 1
|
||||
|
||||
by_player.character_stats.character_changed.emit(by_player.character_stats)
|
||||
|
||||
# Play pickup sound
|
||||
if has_node("SfxPickup"):
|
||||
$SfxPickup.play()
|
||||
|
||||
print(by_player.name, " collected bomb!")
|
||||
|
||||
# Remove bomb immediately
|
||||
queue_free()
|
||||
|
||||
func _on_collection_area_body_entered(_body):
|
||||
# This is a backup - main collection happens via can_be_grabbed/on_grabbed
|
||||
# But we can also handle it here if needed
|
||||
pass
|
||||
Reference in New Issue
Block a user