added more spell effects, fixed bomb effects, allow to pickup bomb...

This commit is contained in:
2026-01-24 05:20:24 +01:00
parent b9e836d394
commit 9ab4a13244
18 changed files with 715 additions and 158 deletions

View File

@@ -5,7 +5,7 @@ extends CharacterBody2D
@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
@export var screenshake_strength: float = 18.0 # Base screenshake strength (stronger)
var player_owner: Node = null
var is_fused: bool = false
@@ -21,6 +21,7 @@ var velocity_z: float = 0.0
var gravity_z: float = 500.0
var is_airborne: bool = false
var throw_velocity: Vector2 = Vector2.ZERO
var rotation_speed: float = 0.0 # Angular velocity when thrown
# Blinking animation
var blink_timer: float = 0.0
@@ -43,6 +44,10 @@ var collection_delay: float = 0.2 # Can be collected after 0.2 seconds
# Damage area (larger than collision)
var damage_area_shape: CircleShape2D = null
const TILE_SIZE: int = 16
const TILE_STRIDE: int = 17 # 16 + separation 1
var _explosion_tile_particle_scene: PackedScene = null
func _ready():
# Set collision layer to 2 (interactable objects) so it can be grabbed
collision_layer = 2
@@ -62,7 +67,17 @@ func _ready():
if explosion_sprite:
explosion_sprite.visible = false
# Setup damage area (48x48 radius)
# Setup shadow (like interactable - visible, under bomb)
if shadow:
shadow.visible = true
shadow.modulate = Color(0, 0, 0, 0.5)
shadow.z_index = -1
# Defer area/shape setup and fuse start may run during physics (e.g. trap damage → throw)
call_deferred("_deferred_ready")
func _deferred_ready():
# Setup damage area (48x48 radius) safe to touch Area2D/shape when not flushing queries
if bomb_area:
var collision_shape = bomb_area.get_node_or_null("CollisionShape2D")
if collision_shape:
@@ -70,7 +85,7 @@ func _ready():
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)
# Start fuse if not thrown (placed bomb starts fusing immediately; thrown bombs start fuse on land)
if not is_thrown:
_start_fuse()
@@ -88,6 +103,12 @@ func setup(target_position: Vector2, owner_player: Node, throw_force: Vector2 =
is_airborne = true
position_z = 2.5
velocity_z = 100.0
# Rotation when thrown (based on throw direction)
if throw_force.length_squared() > 1.0:
var perp = Vector2(-throw_force.y, throw_force.x)
rotation_speed = sign(perp.x + perp.y) * 12.0
else:
rotation_speed = 8.0
# Make sure sprite is visible
if sprite:
sprite.visible = true
@@ -157,17 +178,19 @@ func _physics_process(delta):
velocity_z -= gravity_z * delta
position_z += velocity_z * delta
# Update sprite position based on height
# Update sprite position and rotation 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)
sprite.rotation += rotation_speed * delta
# Update shadow
# Update shadow (like interactable - scale down when airborne for visibility)
if shadow:
shadow.visible = true
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
shadow.modulate = Color(0, 0, 0, 0.5 - (position_z / 100.0) * 0.3)
# Apply throw velocity
velocity = throw_velocity
@@ -176,13 +199,15 @@ func _physics_process(delta):
if position_z <= 0.0:
_land()
else:
# On ground - reset sprite/shadow
# On ground - reset sprite/shadow (shadow visible like interactable)
if sprite:
sprite.position.y = 0
sprite.scale = Vector2.ONE
sprite.rotation = 0.0
if shadow:
shadow.visible = true
shadow.scale = Vector2.ONE
shadow.modulate.a = 0.5
shadow.modulate = Color(0, 0, 0, 0.5)
# Apply friction if on ground
if not is_airborne:
@@ -211,7 +236,7 @@ func _physics_process(delta):
if fuse_timer >= collection_delay:
can_be_collected = true
if collection_area:
collection_area.monitoring = true
collection_area.set_deferred("monitoring", true)
func _land():
is_airborne = false
@@ -228,9 +253,11 @@ func _explode():
is_exploding = true
# Hide bomb sprite, show explosion
# Hide bomb sprite and shadow, show explosion
if sprite:
sprite.visible = false
if shadow:
shadow.visible = false
if explosion_sprite:
explosion_sprite.visible = true
explosion_sprite.frame = 0
@@ -263,6 +290,11 @@ func _explode():
# Cause screenshake
_cause_screenshake()
# Spawn tile debris particles (4 pieces per affected tile, bounce, fade)
_spawn_explosion_tile_particles()
if has_node("SfxDebrisFromParticles"):
$SfxDebrisFromParticles.play()
# Disable collision
if bomb_area:
bomb_area.set_deferred("monitoring", false)
@@ -332,6 +364,76 @@ func _deal_explosion_damage():
print("Bomb hit enemy: ", body.name, " for ", final_damage, " damage!")
func _spawn_explosion_tile_particles():
var game_world = get_tree().get_first_node_in_group("game_world")
if not game_world:
return
var layer = game_world.get_node_or_null("Environment/DungeonLayer0")
if not layer or not layer is TileMapLayer:
return
if not _explosion_tile_particle_scene:
_explosion_tile_particle_scene = load("res://scenes/explosion_tile_particle.tscn") as PackedScene
if not _explosion_tile_particle_scene:
return
var tex = load("res://assets/gfx/RPG DUNGEON VOL 3.png") as Texture2D
if not tex:
return
var center = global_position
var r = damage_radius
var layer_pos = center - layer.global_position
var center_cell = layer.local_to_map(layer_pos)
var half_cells = ceili(r / float(TILE_SIZE)) + 1
var parent = get_parent()
if not parent:
parent = game_world.get_node_or_null("Entities")
if not parent:
return
for gx in range(center_cell.x - half_cells, center_cell.x + half_cells + 1):
for gy in range(center_cell.y - half_cells, center_cell.y + half_cells + 1):
var cell = Vector2i(gx, gy)
if layer.get_cell_source_id(cell) < 0:
continue
var atlas = layer.get_cell_atlas_coords(cell)
var world = layer.map_to_local(cell) + layer.global_position
if world.distance_to(center) > r:
continue
var bx = atlas.x * TILE_STRIDE
var by = atlas.y * TILE_STRIDE
var h = 8.0 # TILE_SIZE / 2
var regions = [
Rect2(bx, by, h, h),
Rect2(bx + h, by, h, h),
Rect2(bx, by + h, h, h),
Rect2(bx + h, by + h, h, h)
]
# Direction from explosion center to this tile (outward) particles fly away from bomb
var to_tile = world - center
var outward = to_tile.normalized() if to_tile.length() > 1.0 else Vector2.RIGHT.rotated(randf() * TAU)
# Half the particles: 2 pieces per tile instead of 4 (indices 0 and 2)
for i in [0, 2]:
var p = _explosion_tile_particle_scene.instantiate() as CharacterBody2D
var spr = p.get_node_or_null("Sprite2D") as Sprite2D
if not spr:
p.queue_free()
continue
spr.texture = tex
spr.region_enabled = true
spr.region_rect = regions[i]
p.global_position = world
var speed = randf_range(280.0, 420.0) # Much faster - fly around more
var d = outward + Vector2(randf_range(-0.4, 0.4), randf_range(-0.4, 0.4))
p.velocity = d.normalized() * speed
p.angular_velocity = randf_range(-14.0, 14.0)
p.position_z = 0.0
p.velocity_z = randf_range(100.0, 180.0) # Upward burst, then gravity brings them down
parent.add_child(p)
func _cause_screenshake():
# Calculate screenshake based on distance from local players
var game_world = get_tree().get_first_node_in_group("game_world")
@@ -359,9 +461,9 @@ func _cause_screenshake():
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
# Apply screenshake (longer duration for bigger boom)
if game_world.has_method("add_screenshake"):
game_world.add_screenshake(shake_strength, 0.3) # 0.3 second duration
game_world.add_screenshake(shake_strength, 0.5) # 0.5 second duration
func _on_bomb_area_body_entered(_body):
# This is for explosion damage (handled in _deal_explosion_damage)
@@ -390,6 +492,14 @@ func on_grabbed(by_player):
can_collect = true
if can_collect:
# Stop fuse sound, particles, and light when collecting
if has_node("SfxFuse"):
$SfxFuse.stop()
if fuse_particles:
fuse_particles.emitting = false
if fuse_light:
fuse_light.enabled = false
# Create bomb item
var bomb_item = ItemDatabase.create_item("bomb")
if bomb_item:
@@ -404,6 +514,16 @@ func on_grabbed(by_player):
by_player.character_stats.character_changed.emit(by_player.character_stats)
# Show "+1 Bomb" above player
var floating_text_scene = load("res://scenes/floating_text.tscn") as PackedScene
if floating_text_scene and by_player and is_instance_valid(by_player):
var ft = floating_text_scene.instantiate()
var parent = by_player.get_parent()
if parent:
parent.add_child(ft)
ft.global_position = Vector2(by_player.global_position.x, by_player.global_position.y - 20)
ft.setup("+1 Bomb", Color(0.9, 0.5, 0.2), 0.5, 0.5) # Orange-ish
# Play pickup sound
if has_node("SfxPickup"):
$SfxPickup.play()