added bomb
This commit is contained in:
145
src/scenes/attack_bomb.tscn
Normal file
145
src/scenes/attack_bomb.tscn
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
[gd_scene format=3 uid="uid://bv5jfd7ck3d5u"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" uid="uid://cejgtbmf5y0wk" path="res://scripts/attack_bomb.gd" id="1_script"]
|
||||||
|
[ext_resource type="Texture2D" uid="uid://ceitcsfb2fq6m" path="res://assets/gfx/fx/big-explosion.png" id="2_explosion"]
|
||||||
|
[ext_resource type="Texture2D" uid="uid://dkpritx47nd4m" path="res://assets/gfx/pickups/items_n_shit.png" id="2_ng1nl"]
|
||||||
|
[ext_resource type="AudioStream" uid="uid://d4dweg04wrw6a" path="res://assets/audio/sfx/sub_weapons/bomb_fuse.mp3" id="3_fuse"]
|
||||||
|
[ext_resource type="AudioStream" uid="uid://qcb5u7dqw1ck" path="res://assets/audio/sfx/explode_01.wav.mp3" id="4_explode"]
|
||||||
|
[ext_resource type="AudioStream" uid="uid://d4kjyb1olr74s" path="res://assets/audio/sfx/weapons/bow/bow_draw-02.mp3" id="5_pickup"]
|
||||||
|
|
||||||
|
[sub_resource type="Gradient" id="Gradient_shadow"]
|
||||||
|
colors = PackedColorArray(0, 0, 0, 1, 0, 0, 0, 0)
|
||||||
|
|
||||||
|
[sub_resource type="GradientTexture2D" id="GradientTexture2D_shadow"]
|
||||||
|
gradient = SubResource("Gradient_shadow")
|
||||||
|
fill = 1
|
||||||
|
fill_from = Vector2(0.5, 0.5)
|
||||||
|
fill_to = Vector2(0.8, 0.8)
|
||||||
|
|
||||||
|
[sub_resource type="CircleShape2D" id="CircleShape2D_bomb"]
|
||||||
|
radius = 8.0
|
||||||
|
|
||||||
|
[sub_resource type="CircleShape2D" id="CircleShape2D_collection"]
|
||||||
|
radius = 16.0
|
||||||
|
|
||||||
|
[sub_resource type="Curve" id="Curve_ng1nl"]
|
||||||
|
_data = [Vector2(0, 1), 0.0, 0.0, 0, 0, Vector2(0.4198473, 0.8850688), 0.0, 0.0, 0, 0, Vector2(1, 0), 0.0, 0.0, 0, 0]
|
||||||
|
point_count = 3
|
||||||
|
|
||||||
|
[sub_resource type="CurveTexture" id="CurveTexture_bugki"]
|
||||||
|
curve = SubResource("Curve_ng1nl")
|
||||||
|
|
||||||
|
[sub_resource type="ParticleProcessMaterial" id="ParticleProcessMaterial_fuse"]
|
||||||
|
particle_flag_disable_z = true
|
||||||
|
direction = Vector3(0, -1, 0)
|
||||||
|
spread = 360.0
|
||||||
|
initial_velocity_min = 20.0
|
||||||
|
initial_velocity_max = 50.0
|
||||||
|
gravity = Vector3(0, -30, 0)
|
||||||
|
scale_min = 0.3
|
||||||
|
scale_max = 0.8
|
||||||
|
color = Color(0.9137255, 0.44651705, 0.2455241, 1)
|
||||||
|
alpha_curve = SubResource("CurveTexture_bugki")
|
||||||
|
hue_variation_min = -0.100000024
|
||||||
|
hue_variation_max = 0.12999998
|
||||||
|
|
||||||
|
[sub_resource type="Gradient" id="Gradient_fuse_light"]
|
||||||
|
offsets = PackedFloat32Array(0, 0.7250608, 1)
|
||||||
|
colors = PackedColorArray(1, 0.4, 0.1, 1, 1, 0.4, 0.1, 0.5, 1, 0.4, 0.1, 0)
|
||||||
|
|
||||||
|
[sub_resource type="GradientTexture2D" id="GradientTexture2D_fuse_light"]
|
||||||
|
gradient = SubResource("Gradient_fuse_light")
|
||||||
|
fill = 1
|
||||||
|
fill_from = Vector2(0.5, 0.5)
|
||||||
|
fill_to = Vector2(0.85897434, 0.8247863)
|
||||||
|
|
||||||
|
[sub_resource type="Gradient" id="Gradient_explosion_light"]
|
||||||
|
offsets = PackedFloat32Array(0, 0.69343066, 1)
|
||||||
|
colors = PackedColorArray(1, 0.6, 0.2, 1, 1, 0.6, 0.2, 0.5, 1, 0.6, 0.2, 0)
|
||||||
|
|
||||||
|
[sub_resource type="GradientTexture2D" id="GradientTexture2D_explosion_light"]
|
||||||
|
gradient = SubResource("Gradient_explosion_light")
|
||||||
|
fill = 1
|
||||||
|
fill_from = Vector2(0.5, 0.5)
|
||||||
|
fill_to = Vector2(0.9102564, 0.9188034)
|
||||||
|
|
||||||
|
[node name="Bomb" type="CharacterBody2D" unique_id=367943636]
|
||||||
|
collision_layer = 2
|
||||||
|
motion_mode = 1
|
||||||
|
script = ExtResource("1_script")
|
||||||
|
|
||||||
|
[node name="Shadow" type="Sprite2D" parent="." unique_id=1404868451]
|
||||||
|
z_index = 1
|
||||||
|
z_as_relative = false
|
||||||
|
scale = Vector2(0.1, 0.1)
|
||||||
|
texture = SubResource("GradientTexture2D_shadow")
|
||||||
|
|
||||||
|
[node name="Sprite2D" type="Sprite2D" parent="." unique_id=818862430]
|
||||||
|
texture = ExtResource("2_ng1nl")
|
||||||
|
hframes = 20
|
||||||
|
vframes = 14
|
||||||
|
frame = 199
|
||||||
|
|
||||||
|
[node name="ExplosionSprite" type="Sprite2D" parent="." unique_id=2038174438]
|
||||||
|
position = Vector2(0, -23)
|
||||||
|
texture = ExtResource("2_explosion")
|
||||||
|
hframes = 9
|
||||||
|
|
||||||
|
[node name="CollisionShape2D" type="CollisionShape2D" parent="." unique_id=1640139333]
|
||||||
|
shape = SubResource("CircleShape2D_bomb")
|
||||||
|
debug_color = Color(0.3825145, 0.70196074, 0.30829018, 0.41960785)
|
||||||
|
|
||||||
|
[node name="BombArea" type="Area2D" parent="." unique_id=97949411]
|
||||||
|
collision_layer = 4
|
||||||
|
collision_mask = 3
|
||||||
|
|
||||||
|
[node name="CollisionShape2D" type="CollisionShape2D" parent="BombArea" unique_id=963327610]
|
||||||
|
shape = SubResource("CircleShape2D_bomb")
|
||||||
|
debug_color = Color(0, 0.06808392, 0.70196074, 0.41960785)
|
||||||
|
|
||||||
|
[node name="CollectionArea" type="Area2D" parent="." unique_id=1088408959]
|
||||||
|
collision_layer = 0
|
||||||
|
monitoring = false
|
||||||
|
|
||||||
|
[node name="CollisionShape2D" type="CollisionShape2D" parent="CollectionArea" unique_id=1383974781]
|
||||||
|
shape = SubResource("CircleShape2D_collection")
|
||||||
|
debug_color = Color(0.38218734, 0.5838239, 0.70196074, 0.41960785)
|
||||||
|
|
||||||
|
[node name="SfxFuse" type="AudioStreamPlayer2D" parent="." unique_id=1095147141]
|
||||||
|
stream = ExtResource("3_fuse")
|
||||||
|
volume_db = -2.0
|
||||||
|
attenuation = 2.0
|
||||||
|
bus = &"Sfx"
|
||||||
|
|
||||||
|
[node name="SfxExplosion" type="AudioStreamPlayer2D" parent="." unique_id=1652373167]
|
||||||
|
stream = ExtResource("4_explode")
|
||||||
|
volume_db = 2.0
|
||||||
|
attenuation = 3.0
|
||||||
|
bus = &"Sfx"
|
||||||
|
|
||||||
|
[node name="SfxPickup" type="AudioStreamPlayer2D" parent="." unique_id=898603969]
|
||||||
|
stream = ExtResource("5_pickup")
|
||||||
|
volume_db = -2.0
|
||||||
|
attenuation = 2.0
|
||||||
|
bus = &"Sfx"
|
||||||
|
|
||||||
|
[node name="FuseParticles" type="GPUParticles2D" parent="." unique_id=1234567890]
|
||||||
|
z_index = 2
|
||||||
|
position = Vector2(6, -5)
|
||||||
|
amount = 32
|
||||||
|
lifetime = 0.3
|
||||||
|
randomness = 1.0
|
||||||
|
process_material = SubResource("ParticleProcessMaterial_fuse")
|
||||||
|
|
||||||
|
[node name="FuseLight" type="PointLight2D" parent="." unique_id=1286608618]
|
||||||
|
position = Vector2(6, -5)
|
||||||
|
enabled = false
|
||||||
|
color = Color(1, 0.4, 0.1, 1)
|
||||||
|
energy = 0.8
|
||||||
|
texture = SubResource("GradientTexture2D_fuse_light")
|
||||||
|
|
||||||
|
[node name="ExplosionLight" type="PointLight2D" parent="." unique_id=1111111111]
|
||||||
|
enabled = false
|
||||||
|
color = Color(1, 0.6, 0.2, 1)
|
||||||
|
energy = 2.5
|
||||||
|
texture = SubResource("GradientTexture2D_explosion_light")
|
||||||
@@ -81,6 +81,8 @@ panning_strength = 1.09
|
|||||||
|
|
||||||
[node name="SfxInit" type="AudioStreamPlayer2D" parent="." unique_id=467371620]
|
[node name="SfxInit" type="AudioStreamPlayer2D" parent="." unique_id=467371620]
|
||||||
stream = ExtResource("10_2hde6")
|
stream = ExtResource("10_2hde6")
|
||||||
|
volume_db = -11.018
|
||||||
|
pitch_scale = 0.93
|
||||||
attenuation = 1.5157177
|
attenuation = 1.5157177
|
||||||
panning_strength = 1.04
|
panning_strength = 1.04
|
||||||
bus = &"Sfx"
|
bus = &"Sfx"
|
||||||
|
|||||||
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
|
||||||
1
src/scripts/attack_bomb.gd.uid
Normal file
1
src/scripts/attack_bomb.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://cejgtbmf5y0wk
|
||||||
@@ -6,6 +6,12 @@ extends Node2D
|
|||||||
@onready var camera = $Camera2D
|
@onready var camera = $Camera2D
|
||||||
@onready var network_manager = $"/root/NetworkManager"
|
@onready var network_manager = $"/root/NetworkManager"
|
||||||
|
|
||||||
|
# Screenshake system
|
||||||
|
var screenshake_offset: Vector2 = Vector2.ZERO
|
||||||
|
var screenshake_timer: float = 0.0
|
||||||
|
var screenshake_duration: float = 0.0
|
||||||
|
var screenshake_strength: float = 0.0
|
||||||
|
|
||||||
var local_players = []
|
var local_players = []
|
||||||
const BASE_CAMERA_ZOOM: float = 4.0
|
const BASE_CAMERA_ZOOM: float = 4.0
|
||||||
const BASE_CAMERA_ZOOM_MOBILE: float = 5.5 # More zoomed in for mobile devices
|
const BASE_CAMERA_ZOOM_MOBILE: float = 5.5 # More zoomed in for mobile devices
|
||||||
@@ -1713,14 +1719,25 @@ func _update_camera():
|
|||||||
if local_players.is_empty():
|
if local_players.is_empty():
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Update screenshake
|
||||||
|
if screenshake_timer > 0.0:
|
||||||
|
screenshake_timer -= get_process_delta_time()
|
||||||
|
var shake_amount = screenshake_strength * (screenshake_timer / screenshake_duration)
|
||||||
|
screenshake_offset = Vector2(
|
||||||
|
randf_range(-shake_amount, shake_amount),
|
||||||
|
randf_range(-shake_amount, shake_amount)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
screenshake_offset = Vector2.ZERO
|
||||||
|
|
||||||
# Calculate center of all local players
|
# Calculate center of all local players
|
||||||
var center = Vector2.ZERO
|
var center = Vector2.ZERO
|
||||||
for player in local_players:
|
for player in local_players:
|
||||||
center += player.position
|
center += player.position
|
||||||
center /= local_players.size()
|
center /= local_players.size()
|
||||||
|
|
||||||
# Smooth camera movement
|
# Smooth camera movement (with screenshake)
|
||||||
camera.position = camera.position.lerp(center, 0.1)
|
camera.position = camera.position.lerp(center + screenshake_offset, 0.1)
|
||||||
|
|
||||||
# Base zoom with aspect ratio adjustment (show more on wider screens)
|
# Base zoom with aspect ratio adjustment (show more on wider screens)
|
||||||
var viewport_size = get_viewport().get_visible_rect().size
|
var viewport_size = get_viewport().get_visible_rect().size
|
||||||
@@ -1760,6 +1777,12 @@ func _update_camera():
|
|||||||
var lerped_center = center + cursor_offset * CURSOR_CAMERA_LERP_AMOUNT
|
var lerped_center = center + cursor_offset * CURSOR_CAMERA_LERP_AMOUNT
|
||||||
camera.position = camera.position.lerp(lerped_center, 0.1)
|
camera.position = camera.position.lerp(lerped_center, 0.1)
|
||||||
|
|
||||||
|
func add_screenshake(strength: float, duration: float):
|
||||||
|
# Add screenshake effect
|
||||||
|
screenshake_strength = max(screenshake_strength, strength) # Use max if already shaking
|
||||||
|
screenshake_duration = max(screenshake_duration, duration) # Use max duration
|
||||||
|
screenshake_timer = max(screenshake_timer, duration) # Reset timer if new shake is stronger
|
||||||
|
|
||||||
func _init_mouse_cursor():
|
func _init_mouse_cursor():
|
||||||
# Create cursor layer with high Z index
|
# Create cursor layer with high Z index
|
||||||
cursor_layer = CanvasLayer.new()
|
cursor_layer = CanvasLayer.new()
|
||||||
|
|||||||
@@ -470,6 +470,11 @@ func on_released(by_player):
|
|||||||
print(name, " released by ", by_player.name)
|
print(name, " released by ", by_player.name)
|
||||||
|
|
||||||
func on_thrown(by_player, force: Vector2):
|
func on_thrown(by_player, force: Vector2):
|
||||||
|
# Special handling for bombs - convert to projectile when thrown
|
||||||
|
if object_type == "Bomb":
|
||||||
|
_convert_to_bomb_projectile(by_player, force)
|
||||||
|
return
|
||||||
|
|
||||||
# Only allow throwing if object is liftable
|
# Only allow throwing if object is liftable
|
||||||
if not is_liftable:
|
if not is_liftable:
|
||||||
return
|
return
|
||||||
@@ -490,6 +495,46 @@ func on_thrown(by_player, force: Vector2):
|
|||||||
|
|
||||||
print(name, " thrown with velocity ", throw_velocity)
|
print(name, " thrown with velocity ", throw_velocity)
|
||||||
|
|
||||||
|
func _convert_to_bomb_projectile(by_player, force: Vector2):
|
||||||
|
# Convert bomb object to projectile bomb when thrown
|
||||||
|
var attack_bomb_scene = load("res://scenes/attack_bomb.tscn")
|
||||||
|
if not attack_bomb_scene:
|
||||||
|
push_error("ERROR: Could not load attack_bomb scene!")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Only authority can spawn bombs
|
||||||
|
if not is_multiplayer_authority():
|
||||||
|
return
|
||||||
|
|
||||||
|
# Store current position before freeing
|
||||||
|
var current_pos = global_position
|
||||||
|
|
||||||
|
# Spawn bomb projectile at current position
|
||||||
|
var bomb = attack_bomb_scene.instantiate()
|
||||||
|
get_parent().add_child(bomb)
|
||||||
|
bomb.global_position = current_pos # Use current position, not target
|
||||||
|
|
||||||
|
# Set multiplayer authority
|
||||||
|
if multiplayer.has_multiplayer_peer():
|
||||||
|
bomb.set_multiplayer_authority(by_player.get_multiplayer_authority())
|
||||||
|
|
||||||
|
# Setup bomb with throw physics (pass force as throw_velocity)
|
||||||
|
# The bomb will use throw_velocity for movement
|
||||||
|
bomb.setup(current_pos, by_player, force, true) # true = is_thrown, force is throw_velocity
|
||||||
|
|
||||||
|
# Make sure bomb sprite is visible
|
||||||
|
if bomb.has_node("Sprite2D"):
|
||||||
|
bomb.get_node("Sprite2D").visible = true
|
||||||
|
|
||||||
|
# Sync bomb throw to other clients
|
||||||
|
if multiplayer.has_multiplayer_peer() and by_player.is_multiplayer_authority() and by_player.has_method("_rpc_to_ready_peers") and by_player.is_inside_tree():
|
||||||
|
by_player._rpc_to_ready_peers("_sync_throw_bomb", [current_pos, force])
|
||||||
|
|
||||||
|
# Remove the interactable object
|
||||||
|
queue_free()
|
||||||
|
|
||||||
|
print("Bomb object converted to projectile and thrown!")
|
||||||
|
|
||||||
@rpc("authority", "unreliable")
|
@rpc("authority", "unreliable")
|
||||||
func _sync_box_state(pos: Vector2, vel: Vector2, z_pos: float, z_vel: float, airborne: bool):
|
func _sync_box_state(pos: Vector2, vel: Vector2, z_pos: float, z_vel: float, airborne: bool):
|
||||||
# Only update on clients (server already has correct state)
|
# Only update on clients (server already has correct state)
|
||||||
@@ -663,8 +708,23 @@ func setup_pushable_high_box():
|
|||||||
|
|
||||||
if sprite:
|
if sprite:
|
||||||
sprite.frame = bottom_frames[index]
|
sprite.frame = bottom_frames[index]
|
||||||
if sprite_above:
|
if sprite_above:
|
||||||
sprite_above.frame = top_frames[index]
|
sprite_above.frame = top_frames[index]
|
||||||
|
|
||||||
|
func setup_bomb():
|
||||||
|
object_type = "Bomb"
|
||||||
|
is_grabbable = true
|
||||||
|
can_be_pushed = false
|
||||||
|
is_destroyable = false # Bombs don't break, they explode
|
||||||
|
is_liftable = true
|
||||||
|
weight = 0.5 # Light weight for easy throwing
|
||||||
|
|
||||||
|
# Set bomb sprite (frame 199 from items_n_shit.png)
|
||||||
|
if sprite:
|
||||||
|
sprite.texture = load("res://assets/gfx/pickups/items_n_shit.png")
|
||||||
|
sprite.hframes = 20
|
||||||
|
sprite.vframes = 14
|
||||||
|
sprite.frame = 199
|
||||||
|
|
||||||
func _open_chest(by_player: Node = null):
|
func _open_chest(by_player: Node = null):
|
||||||
if is_chest_opened:
|
if is_chest_opened:
|
||||||
|
|||||||
@@ -32,7 +32,8 @@ enum WeaponType {
|
|||||||
STAFF,
|
STAFF,
|
||||||
SPEAR,
|
SPEAR,
|
||||||
MACE,
|
MACE,
|
||||||
SPELLBOOK
|
SPELLBOOK,
|
||||||
|
BOMB
|
||||||
}
|
}
|
||||||
|
|
||||||
var use_function = null
|
var use_function = null
|
||||||
|
|||||||
@@ -1254,6 +1254,24 @@ static func _load_all_items():
|
|||||||
{"old_color": Color(1.0, 1.0, 1.0), "new_color": Color(1.0, 0.8, 0.6)} # Warm orange tint for fire
|
{"old_color": Color(1.0, 1.0, 1.0), "new_color": Color(1.0, 0.8, 0.6)} # Warm orange tint for fire
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
# BOMB item (sprite index 199 = row 9, col 19)
|
||||||
|
_register_item("bomb", {
|
||||||
|
"item_name": "Bomb",
|
||||||
|
"description": "A dangerous explosive device",
|
||||||
|
"item_type": Item.ItemType.Equippable,
|
||||||
|
"equipment_type": Item.EquipmentType.OFFHAND,
|
||||||
|
"weapon_type": Item.WeaponType.BOMB,
|
||||||
|
"spriteFrame": 199, # 9 * 20 + 19
|
||||||
|
"quantity": 1,
|
||||||
|
"can_have_multiple_of": true,
|
||||||
|
"modifiers": {},
|
||||||
|
"buy_cost": 50,
|
||||||
|
"sell_worth": 15,
|
||||||
|
"weight": 0.5,
|
||||||
|
"rarity": ItemRarity.COMMON,
|
||||||
|
"drop_chance": 5.0
|
||||||
|
})
|
||||||
|
|
||||||
# Register an item in the database
|
# Register an item in the database
|
||||||
static func _register_item(item_id: String, item_data: Dictionary):
|
static func _register_item(item_id: String, item_data: Dictionary):
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user