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]
|
||||
stream = ExtResource("10_2hde6")
|
||||
volume_db = -11.018
|
||||
pitch_scale = 0.93
|
||||
attenuation = 1.5157177
|
||||
panning_strength = 1.04
|
||||
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 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 = []
|
||||
const BASE_CAMERA_ZOOM: float = 4.0
|
||||
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():
|
||||
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
|
||||
var center = Vector2.ZERO
|
||||
for player in local_players:
|
||||
center += player.position
|
||||
center /= local_players.size()
|
||||
|
||||
# Smooth camera movement
|
||||
camera.position = camera.position.lerp(center, 0.1)
|
||||
# Smooth camera movement (with screenshake)
|
||||
camera.position = camera.position.lerp(center + screenshake_offset, 0.1)
|
||||
|
||||
# Base zoom with aspect ratio adjustment (show more on wider screens)
|
||||
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
|
||||
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():
|
||||
# Create cursor layer with high Z index
|
||||
cursor_layer = CanvasLayer.new()
|
||||
|
||||
@@ -470,6 +470,11 @@ func on_released(by_player):
|
||||
print(name, " released by ", by_player.name)
|
||||
|
||||
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
|
||||
if not is_liftable:
|
||||
return
|
||||
@@ -490,6 +495,46 @@ func on_thrown(by_player, force: Vector2):
|
||||
|
||||
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")
|
||||
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)
|
||||
@@ -666,6 +711,21 @@ func setup_pushable_high_box():
|
||||
if sprite_above:
|
||||
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):
|
||||
if is_chest_opened:
|
||||
return
|
||||
|
||||
@@ -32,7 +32,8 @@ enum WeaponType {
|
||||
STAFF,
|
||||
SPEAR,
|
||||
MACE,
|
||||
SPELLBOOK
|
||||
SPELLBOOK,
|
||||
BOMB
|
||||
}
|
||||
|
||||
var use_function = null
|
||||
|
||||
@@ -1255,6 +1255,24 @@ static func _load_all_items():
|
||||
]
|
||||
})
|
||||
|
||||
# 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
|
||||
static func _register_item(item_id: String, item_data: Dictionary):
|
||||
item_data["item_id"] = item_id
|
||||
|
||||
@@ -38,6 +38,7 @@ var is_pulling = false # True when pulling (moving backwards while pushing)
|
||||
var is_disarming = false # True when disarming a trap
|
||||
var grab_button_pressed_time = 0.0
|
||||
var just_grabbed_this_frame = false # Prevents immediate release bug - persists across frames
|
||||
var grab_released_while_lifting = false # Track if grab was released while lifting (for drop-on-second-press logic)
|
||||
var grab_start_time = 0.0 # Time when we first grabbed (to detect quick tap)
|
||||
var grab_tap_threshold = 0.2 # Seconds to distinguish tap from hold
|
||||
var last_movement_direction = Vector2.DOWN # Track last direction for placing objects
|
||||
@@ -78,6 +79,9 @@ var spell_charge_tint: Color = Color(2.0, 0.2, 0.2, 2.0) # Tint color when fully
|
||||
var spell_charge_tint_pulse_time: float = 0.0 # Timer for pulsing tint animation
|
||||
var spell_charge_tint_pulse_speed: float = 8.0 # Speed of tint pulse animation while charging
|
||||
var spell_charge_tint_pulse_speed_charged: float = 20.0 # Speed of tint pulse animation when fully charged
|
||||
var bow_charge_tint: Color = Color(1.2, 1.2, 1.2, 1.0) # White tint when fully charged
|
||||
var bow_charge_tint_pulse_time: float = 0.0 # Timer for pulsing tint animation
|
||||
var bow_charge_tint_pulse_speed_charged: float = 20.0 # Speed of tint pulse animation when fully charged
|
||||
var original_sprite_tints: Dictionary = {} # Store original tint values for restoration
|
||||
var spell_incantation_played: bool = false # Track if incantation sound has been played
|
||||
var burn_debuff_timer: float = 0.0 # Timer for burn debuff
|
||||
@@ -93,6 +97,7 @@ var sword_projectile_scene = preload("res://scenes/sword_projectile.tscn") # New
|
||||
var staff_projectile_scene = preload("res://scenes/attack_staff.tscn") # Staff magic ball projectile
|
||||
var attack_arrow_scene = preload("res://scenes/attack_arrow.tscn")
|
||||
var flame_spell_scene = preload("res://scenes/attack_spell_flame.tscn") # Flame spell for Tome of Flames
|
||||
var attack_bomb_scene = preload("res://scenes/attack_bomb.tscn") # Bomb projectile
|
||||
var blood_scene = preload("res://scenes/blood_clot.tscn")
|
||||
|
||||
# Simulated Z-axis for height (when thrown)
|
||||
@@ -674,6 +679,14 @@ func _setup_player_appearance():
|
||||
character_stats.equipment["offhand"] = starting_arrows
|
||||
print("Elf player ", name, " spawned with short bow and 3 arrows")
|
||||
|
||||
# Give Dwarf race starting bomb
|
||||
if selected_race == "Dwarf":
|
||||
var starting_bomb = ItemDatabase.create_item("bomb")
|
||||
if starting_bomb:
|
||||
starting_bomb.quantity = 5 # Give Dwarf 5 bombs to start
|
||||
character_stats.equipment["offhand"] = starting_bomb
|
||||
print("Dwarf player ", name, " spawned with 5 bombs")
|
||||
|
||||
# Give Human race starting spellbook (Tome of Flames)
|
||||
if selected_race == "Human":
|
||||
var starting_tome = ItemDatabase.create_item("tome_of_flames")
|
||||
@@ -1757,6 +1770,25 @@ func _physics_process(delta):
|
||||
# Reset pulse timer when not charging
|
||||
spell_charge_tint_pulse_time = 0.0
|
||||
|
||||
# Update bow charge tint (when fully charged)
|
||||
if is_charging_bow:
|
||||
var charge_time = (Time.get_ticks_msec() / 1000.0) - bow_charge_start_time
|
||||
# Smooth curve: charge from 0.2s to 1.0s
|
||||
var charge_progress = clamp((charge_time - 0.2) / 0.8, 0.0, 1.0) # 0.0 at 0.2s, 1.0 at 1.0s
|
||||
|
||||
# Update tint pulse timer when fully charged
|
||||
if charge_progress >= 1.0:
|
||||
# Use fast pulse speed when fully charged
|
||||
bow_charge_tint_pulse_time += delta * bow_charge_tint_pulse_speed_charged
|
||||
_apply_bow_charge_tint()
|
||||
else:
|
||||
bow_charge_tint_pulse_time = 0.0
|
||||
_clear_bow_charge_tint()
|
||||
else:
|
||||
# Reset pulse timer when not charging
|
||||
bow_charge_tint_pulse_time = 0.0
|
||||
_clear_bow_charge_tint()
|
||||
|
||||
# Update burn debuff (works on both authority and clients)
|
||||
if burn_debuff_timer > 0.0:
|
||||
burn_debuff_timer -= delta
|
||||
@@ -2213,6 +2245,9 @@ func _handle_interactions():
|
||||
if grab_just_pressed and is_charging_bow:
|
||||
is_charging_bow = false
|
||||
|
||||
# Clear bow charge tint
|
||||
_clear_bow_charge_tint()
|
||||
|
||||
# Sync bow charge end to other clients
|
||||
if multiplayer.has_multiplayer_peer():
|
||||
_sync_bow_charge_end.rpc()
|
||||
@@ -2281,6 +2316,9 @@ func _handle_interactions():
|
||||
_stop_spell_charge_particles()
|
||||
_clear_spell_charge_tint()
|
||||
|
||||
# Return to IDLE animation
|
||||
_set_animation("IDLE")
|
||||
|
||||
# Stop spell charging sounds
|
||||
if has_node("SfxSpellCharge"):
|
||||
$SfxSpellCharge.stop()
|
||||
@@ -2308,6 +2346,21 @@ func _handle_interactions():
|
||||
_cast_flame_spell(target_pos)
|
||||
# Play FINISH_SPELL animation after casting
|
||||
_set_animation("FINISH_SPELL")
|
||||
|
||||
# Stop charging and clear tint (but let incantation sound finish)
|
||||
is_charging_spell = false
|
||||
spell_incantation_played = false
|
||||
_stop_spell_charge_particles()
|
||||
_clear_spell_charge_tint() # This will restore original tints
|
||||
|
||||
# Stop spell charging sound, but let incantation play to completion
|
||||
if has_node("SfxSpellCharge"):
|
||||
$SfxSpellCharge.stop()
|
||||
# Don't stop SfxSpellIncantation - let it finish playing
|
||||
|
||||
# Sync spell charge end to other clients
|
||||
if multiplayer.has_multiplayer_peer():
|
||||
_sync_spell_charge_end.rpc()
|
||||
else:
|
||||
# Not fully charged or no target - just cancel without casting
|
||||
print(name, " spell not cast (charge: ", charge_time, "s, fully charged: ", is_fully_charged, ", target: ", target_pos, ")")
|
||||
@@ -2318,6 +2371,9 @@ func _handle_interactions():
|
||||
_stop_spell_charge_particles()
|
||||
_clear_spell_charge_tint() # This will restore original tints
|
||||
|
||||
# Return to IDLE animation
|
||||
_set_animation("IDLE")
|
||||
|
||||
# Stop spell charging sounds
|
||||
if has_node("SfxSpellCharge"):
|
||||
$SfxSpellCharge.stop()
|
||||
@@ -2339,6 +2395,9 @@ func _handle_interactions():
|
||||
_stop_spell_charge_particles()
|
||||
_clear_spell_charge_tint()
|
||||
|
||||
# Return to IDLE animation
|
||||
_set_animation("IDLE")
|
||||
|
||||
# Stop spell charging sounds
|
||||
if has_node("SfxSpellCharge"):
|
||||
$SfxSpellCharge.stop()
|
||||
@@ -2382,6 +2441,79 @@ func _handle_interactions():
|
||||
# No nearby trap - reset disarming flag
|
||||
is_disarming = false
|
||||
|
||||
# Check for bomb usage (if bomb equipped in offhand)
|
||||
# Also check if we're already holding a bomb - if so, skip normal grab handling
|
||||
var is_holding_bomb = false
|
||||
if held_object and "object_type" in held_object:
|
||||
# Check if it's a bomb by checking object_type
|
||||
if held_object.object_type == "Bomb":
|
||||
is_holding_bomb = true
|
||||
|
||||
if character_stats and character_stats.equipment.has("offhand"):
|
||||
var offhand_item = character_stats.equipment["offhand"]
|
||||
if offhand_item and offhand_item.weapon_type == Item.WeaponType.BOMB and offhand_item.quantity > 0:
|
||||
# Check if there's a grabbable object nearby - prioritize grabbing over bomb
|
||||
var nearby_grabbable = null
|
||||
if grab_area:
|
||||
var bodies = grab_area.get_overlapping_bodies()
|
||||
for body in bodies:
|
||||
if body == self:
|
||||
continue
|
||||
var is_grabbable = false
|
||||
if body.has_method("can_be_grabbed"):
|
||||
if body.can_be_grabbed():
|
||||
is_grabbable = true
|
||||
elif body.is_in_group("player") or ("peer_id" in body and body is CharacterBody2D):
|
||||
is_grabbable = true
|
||||
|
||||
if is_grabbable:
|
||||
var distance = position.distance_to(body.position)
|
||||
if distance < grab_range:
|
||||
nearby_grabbable = body
|
||||
break
|
||||
|
||||
# Only use bomb if no grabbable object nearby and not lifting/grabbing
|
||||
if grab_just_pressed and not nearby_grabbable and not is_lifting and not held_object:
|
||||
# Use bomb based on race
|
||||
if character_stats.race == "Dwarf":
|
||||
# Dwarf: Create interactable bomb object that can be lifted/thrown
|
||||
_create_bomb_object()
|
||||
# Skip the normal grab handling below - bomb is already lifted
|
||||
just_grabbed_this_frame = true # Set to true to prevent immediate release
|
||||
grab_start_time = Time.get_ticks_msec() / 1000.0 # Record grab time
|
||||
return
|
||||
else:
|
||||
# Human/Elf: Place bomb directly
|
||||
var game_world = get_tree().get_first_node_in_group("game_world")
|
||||
var target_pos = Vector2.ZERO
|
||||
if game_world and game_world.has_method("get_grid_locked_cursor_position"):
|
||||
target_pos = game_world.get_grid_locked_cursor_position()
|
||||
|
||||
if target_pos != Vector2.ZERO:
|
||||
# Consume one bomb
|
||||
offhand_item.quantity -= 1
|
||||
var remaining = offhand_item.quantity
|
||||
if offhand_item.quantity <= 0:
|
||||
character_stats.equipment["offhand"] = null
|
||||
if character_stats:
|
||||
character_stats.character_changed.emit(character_stats)
|
||||
|
||||
# Place bomb
|
||||
_place_bomb(target_pos)
|
||||
|
||||
print(name, " used bomb! Remaining: ", remaining)
|
||||
just_grabbed_this_frame = false
|
||||
return
|
||||
|
||||
# If holding a bomb, skip normal grab press handling to prevent dropping it
|
||||
# But still allow grab release handling for the drop-on-second-press logic
|
||||
if is_holding_bomb:
|
||||
# Update bomb position if holding it
|
||||
if held_object and grab_button_down and is_lifting:
|
||||
_update_lifted_object()
|
||||
# Skip grab press handling, but continue to release handling below
|
||||
# This allows the drop-on-second-press logic to work
|
||||
|
||||
# Track how long grab button is held
|
||||
if grab_button_down:
|
||||
grab_button_pressed_time += get_process_delta_time()
|
||||
@@ -2394,6 +2526,15 @@ func _handle_interactions():
|
||||
# Handle grab button press FIRST (before checking release)
|
||||
# Note: just_grabbed_this_frame is reset at the END of this function
|
||||
if grab_just_pressed and can_grab:
|
||||
# Skip grab press handling if holding a bomb (to prevent instant drop)
|
||||
# But still allow the drop-on-second-press logic
|
||||
if is_holding_bomb:
|
||||
if is_lifting and grab_released_while_lifting:
|
||||
# Already lifting AND grab was released then pressed again - drop the bomb
|
||||
_place_down_object()
|
||||
grab_released_while_lifting = false
|
||||
else:
|
||||
# Normal grab handling for non-bomb objects
|
||||
print("DEBUG: grab_just_pressed, can_grab=", can_grab, " held_object=", held_object != null, " is_lifting=", is_lifting, " grab_button_down=", grab_button_down)
|
||||
if not held_object:
|
||||
# Try to grab something (but don't lift yet - wait for release to determine if it's a tap)
|
||||
@@ -2403,9 +2544,10 @@ func _handle_interactions():
|
||||
# Record when we grabbed to detect quick tap on release
|
||||
grab_start_time = Time.get_ticks_msec() / 1000.0
|
||||
print("DEBUG: After _try_grab(), held_object=", held_object != null, " is_lifting=", is_lifting, " grab_button_down=", grab_button_down, " just_grabbed_this_frame=", just_grabbed_this_frame)
|
||||
elif is_lifting:
|
||||
# Already lifting - always place down (throwing is now only via attack button)
|
||||
elif is_lifting and grab_released_while_lifting:
|
||||
# Already lifting AND grab was released then pressed again - drop the object
|
||||
_place_down_object()
|
||||
grab_released_while_lifting = false
|
||||
|
||||
# Handle grab button release
|
||||
# CRITICAL: Don't process release if:
|
||||
@@ -2413,12 +2555,16 @@ func _handle_interactions():
|
||||
# 2. Button is still down (shouldn't happen, but safety check)
|
||||
# 3. grab_just_pressed is also true (same frame tap)
|
||||
if grab_just_released and held_object:
|
||||
# For bombs that are already lifted, skip the "just grabbed" logic
|
||||
# and go straight to the normal release handling (drop-on-second-press)
|
||||
var is_bomb_already_lifted = is_holding_bomb and is_lifting
|
||||
|
||||
# Check if we just grabbed (either this frame or recently)
|
||||
# Use grab_start_time to determine if this was a quick tap
|
||||
var time_since_grab = (Time.get_ticks_msec() / 1000.0) - grab_start_time
|
||||
var was_recent_grab = time_since_grab <= grab_tap_threshold * 2.0 # Give some buffer
|
||||
|
||||
if just_grabbed_this_frame or (grab_start_time > 0.0 and was_recent_grab):
|
||||
if not is_bomb_already_lifted and (just_grabbed_this_frame or (grab_start_time > 0.0 and was_recent_grab)):
|
||||
# Just grabbed - check if it was a quick tap (within threshold)
|
||||
var was_quick_tap = time_since_grab <= grab_tap_threshold
|
||||
print("DEBUG: Release after grab - was_quick_tap=", was_quick_tap, " time_since_grab=", time_since_grab, " threshold=", grab_tap_threshold, " is_pushing=", is_pushing)
|
||||
@@ -2455,11 +2601,11 @@ func _handle_interactions():
|
||||
print("DEBUG: Release check - grab_just_released=", grab_just_released, " held_object=", held_object != null, " just_grabbed_this_frame=", just_grabbed_this_frame, " grab_button_down=", grab_button_down, " grab_just_pressed=", grab_just_pressed, " can_release=", can_release)
|
||||
if can_release:
|
||||
print("DEBUG: Processing release - is_lifting=", is_lifting, " is_pushing=", is_pushing)
|
||||
# Button was just released - release the object
|
||||
# Button was just released
|
||||
if is_lifting:
|
||||
# If lifting, place down
|
||||
print("DEBUG: Releasing lifted object")
|
||||
_place_down_object()
|
||||
# If lifting, mark that grab was released (but don't drop - wait for next press)
|
||||
print("DEBUG: Grab released while lifting - will drop on next press")
|
||||
grab_released_while_lifting = true
|
||||
elif is_pushing:
|
||||
# If pushing, stop pushing
|
||||
_stop_pushing()
|
||||
@@ -2470,6 +2616,9 @@ func _handle_interactions():
|
||||
if held_object and grab_button_down:
|
||||
if is_lifting:
|
||||
_update_lifted_object()
|
||||
# Clear the "released while lifting" flag if button is held again
|
||||
if grab_released_while_lifting:
|
||||
grab_released_while_lifting = false
|
||||
elif is_pushing:
|
||||
_update_pushed_object()
|
||||
else:
|
||||
@@ -2551,6 +2700,9 @@ func _handle_interactions():
|
||||
# Release bow and shoot
|
||||
is_charging_bow = false
|
||||
|
||||
# Clear bow charge tint
|
||||
_clear_bow_charge_tint()
|
||||
|
||||
# Lock movement for 0.15 seconds when bow is released
|
||||
movement_lock_timer = 0.15
|
||||
|
||||
@@ -2560,12 +2712,18 @@ func _handle_interactions():
|
||||
|
||||
_perform_attack()
|
||||
print(name, " released bow and shot arrow at ", bow_charge_percentage * 100, "% charge (", charge_time, "s)")
|
||||
else:
|
||||
# Reset charging if conditions changed (no bow/arrows, started lifting/pushing)
|
||||
|
||||
# Handle normal attack (non-bow or no arrows) or cancel bow if conditions changed
|
||||
if not (has_bow_and_arrows and not is_lifting and not is_pushing):
|
||||
# Conditions for charging are no longer met (no bow/arrows, started lifting/pushing)
|
||||
# Only cancel if we were actually charging
|
||||
if is_charging_bow:
|
||||
$SfxBuckleBow.stop()
|
||||
is_charging_bow = false
|
||||
|
||||
# Clear bow charge tint
|
||||
_clear_bow_charge_tint()
|
||||
|
||||
# Sync bow charge end to other clients
|
||||
if multiplayer.has_multiplayer_peer():
|
||||
_sync_bow_charge_end.rpc()
|
||||
@@ -2573,11 +2731,12 @@ func _handle_interactions():
|
||||
print(name, " bow charge cancelled (conditions changed)")
|
||||
|
||||
# Normal attack (non-bow or no arrows)
|
||||
# Also allow throwing when lifting (even if bow is equipped)
|
||||
if attack_just_pressed and can_attack:
|
||||
if is_lifting:
|
||||
# Attack while lifting -> throw immediately in facing direction
|
||||
_force_throw_held_object(facing_direction_vector)
|
||||
elif not is_pushing:
|
||||
elif not has_bow_and_arrows and not is_pushing:
|
||||
_perform_attack()
|
||||
|
||||
# Note: just_grabbed_this_frame is reset when we block a release, or stays true if we grabbed this frame
|
||||
@@ -2652,6 +2811,12 @@ func _try_grab():
|
||||
closest_body.set_collision_mask_value(2, false) # Disable collision with other objects
|
||||
# IMPORTANT: Keep collision with walls (layer 7) enabled so we can detect wall collisions!
|
||||
closest_body.set_collision_mask_value(7, true) # Enable collision with walls
|
||||
elif closest_body.has_method("can_be_grabbed") and closest_body.can_be_grabbed():
|
||||
# Bomb or other grabbable CharacterBody2D - disable collision
|
||||
closest_body.set_collision_layer_value(2, false)
|
||||
closest_body.set_collision_mask_value(1, false)
|
||||
closest_body.set_collision_mask_value(2, false)
|
||||
closest_body.set_collision_mask_value(7, true) # Keep wall collision
|
||||
elif _is_player(closest_body):
|
||||
# Players are on layer 1
|
||||
closest_body.set_collision_layer_value(1, false)
|
||||
@@ -2827,6 +2992,7 @@ func reset_grab_state():
|
||||
initial_player_position = Vector2.ZERO
|
||||
just_grabbed_this_frame = false
|
||||
grab_start_time = 0.0
|
||||
grab_released_while_lifting = false
|
||||
was_dragging_last_frame = false
|
||||
|
||||
# Reset to idle animation
|
||||
@@ -2930,28 +3096,43 @@ func _throw_object():
|
||||
is_lifting = false
|
||||
is_pushing = false
|
||||
|
||||
# Track if on_thrown was already called (bombs convert to projectile and free themselves)
|
||||
var on_thrown_called = false
|
||||
|
||||
# Check if it's a bomb (bombs convert to projectile and free themselves)
|
||||
var is_bomb = false
|
||||
if thrown_obj and is_instance_valid(thrown_obj) and "object_type" in thrown_obj:
|
||||
is_bomb = (thrown_obj.object_type == "Bomb")
|
||||
|
||||
# Re-enable collision completely
|
||||
if _is_box(thrown_obj):
|
||||
# Box: set position and physics first
|
||||
if _is_box(thrown_obj) or is_bomb:
|
||||
# Box or Bomb: set position and physics first
|
||||
thrown_obj.global_position = throw_start_pos
|
||||
|
||||
# Set throw velocity for box (same force as player throw)
|
||||
# Set throw velocity (same force as player throw)
|
||||
if "throw_velocity" in thrown_obj:
|
||||
thrown_obj.throw_velocity = throw_direction * throw_force / thrown_obj.weight
|
||||
var obj_weight = thrown_obj.weight if "weight" in thrown_obj else 1.0
|
||||
thrown_obj.throw_velocity = throw_direction * throw_force / obj_weight
|
||||
if "is_frozen" in thrown_obj:
|
||||
thrown_obj.is_frozen = false
|
||||
|
||||
# Make box airborne with same arc as players
|
||||
# Make box/bomb airborne with same arc as players
|
||||
if "is_airborne" in thrown_obj:
|
||||
thrown_obj.is_airborne = true
|
||||
thrown_obj.position_z = 2.5
|
||||
thrown_obj.velocity_z = 100.0 # Scaled down for 1x scale
|
||||
|
||||
# Call on_thrown if available
|
||||
if thrown_obj.has_method("on_thrown"):
|
||||
# Call on_thrown if available (check validity first)
|
||||
# Note: For bombs, this will convert to projectile and free the object
|
||||
if thrown_obj and is_instance_valid(thrown_obj) and thrown_obj.has_method("on_thrown"):
|
||||
thrown_obj.on_thrown(self, throw_direction * throw_force)
|
||||
on_thrown_called = true
|
||||
# Check if object was freed (bomb conversion)
|
||||
if not is_instance_valid(thrown_obj):
|
||||
thrown_obj = null
|
||||
|
||||
# ⚡ Delay collision re-enable to prevent self-collision
|
||||
# ⚡ Delay collision re-enable to prevent self-collision (only if object still exists)
|
||||
if thrown_obj and is_instance_valid(thrown_obj):
|
||||
await get_tree().create_timer(0.1).timeout
|
||||
if thrown_obj and is_instance_valid(thrown_obj):
|
||||
thrown_obj.set_collision_layer_value(2, true)
|
||||
@@ -2980,8 +3161,41 @@ func _throw_object():
|
||||
thrown_obj.set_collision_layer_value(1, true)
|
||||
thrown_obj.set_collision_mask_value(1, true)
|
||||
thrown_obj.set_collision_mask_value(7, true)
|
||||
elif thrown_obj and thrown_obj.has_method("can_be_grabbed") and thrown_obj.can_be_grabbed():
|
||||
# Bomb or other grabbable object - handle like box
|
||||
thrown_obj.global_position = throw_start_pos
|
||||
|
||||
if thrown_obj.has_method("on_thrown"):
|
||||
# Set throw velocity
|
||||
if "throw_velocity" in thrown_obj:
|
||||
thrown_obj.throw_velocity = throw_direction * throw_force / (thrown_obj.weight if "weight" in thrown_obj else 1.0)
|
||||
if "is_frozen" in thrown_obj:
|
||||
thrown_obj.is_frozen = false
|
||||
|
||||
# Make airborne
|
||||
if "is_airborne" in thrown_obj:
|
||||
thrown_obj.is_airborne = true
|
||||
thrown_obj.position_z = 2.5
|
||||
thrown_obj.velocity_z = 100.0
|
||||
|
||||
# Call on_thrown if available (bombs will convert to projectile here)
|
||||
if thrown_obj and is_instance_valid(thrown_obj) and thrown_obj.has_method("on_thrown"):
|
||||
thrown_obj.on_thrown(self, throw_direction * throw_force)
|
||||
on_thrown_called = true
|
||||
# Check if object was freed (bomb conversion)
|
||||
if not is_instance_valid(thrown_obj):
|
||||
thrown_obj = null
|
||||
|
||||
# Delay collision re-enable (only if object still exists)
|
||||
if thrown_obj and is_instance_valid(thrown_obj):
|
||||
await get_tree().create_timer(0.1).timeout
|
||||
if thrown_obj and is_instance_valid(thrown_obj):
|
||||
thrown_obj.set_collision_layer_value(2, true)
|
||||
thrown_obj.set_collision_mask_value(1, true)
|
||||
thrown_obj.set_collision_mask_value(2, true)
|
||||
thrown_obj.set_collision_mask_value(7, true)
|
||||
|
||||
# Only call on_thrown if it wasn't already called (prevents double-call for bombs)
|
||||
if not on_thrown_called and thrown_obj and is_instance_valid(thrown_obj) and thrown_obj.has_method("on_thrown"):
|
||||
thrown_obj.on_thrown(self, throw_direction * throw_force)
|
||||
|
||||
# Play throw animation
|
||||
@@ -3026,28 +3240,43 @@ func _force_throw_held_object(direction: Vector2):
|
||||
is_lifting = false
|
||||
is_pushing = false
|
||||
|
||||
# Track if on_thrown was already called (bombs convert to projectile and free themselves)
|
||||
var on_thrown_called = false
|
||||
|
||||
# Check if it's a bomb (bombs convert to projectile and free themselves)
|
||||
var is_bomb = false
|
||||
if thrown_obj and is_instance_valid(thrown_obj) and "object_type" in thrown_obj:
|
||||
is_bomb = (thrown_obj.object_type == "Bomb")
|
||||
|
||||
# Re-enable collision completely
|
||||
if _is_box(thrown_obj):
|
||||
# Box: set position and physics first
|
||||
if _is_box(thrown_obj) or is_bomb:
|
||||
# Box or Bomb: set position and physics first
|
||||
thrown_obj.global_position = throw_start_pos
|
||||
|
||||
# Set throw velocity for box (same force as player throw)
|
||||
# Set throw velocity (same force as player throw)
|
||||
if "throw_velocity" in thrown_obj:
|
||||
thrown_obj.throw_velocity = throw_direction * throw_force / thrown_obj.weight
|
||||
var obj_weight = thrown_obj.weight if "weight" in thrown_obj else 1.0
|
||||
thrown_obj.throw_velocity = throw_direction * throw_force / obj_weight
|
||||
if "is_frozen" in thrown_obj:
|
||||
thrown_obj.is_frozen = false
|
||||
|
||||
# Make box airborne with same arc as players
|
||||
# Make box/bomb airborne with same arc as players
|
||||
if "is_airborne" in thrown_obj:
|
||||
thrown_obj.is_airborne = true
|
||||
thrown_obj.position_z = 2.5
|
||||
thrown_obj.velocity_z = 100.0 # Scaled down for 1x scale
|
||||
|
||||
# Call on_thrown if available
|
||||
if thrown_obj.has_method("on_thrown"):
|
||||
# Call on_thrown if available (check validity first)
|
||||
# Note: For bombs, this will convert to projectile and free the object
|
||||
if thrown_obj and is_instance_valid(thrown_obj) and thrown_obj.has_method("on_thrown"):
|
||||
thrown_obj.on_thrown(self, throw_direction * throw_force)
|
||||
on_thrown_called = true
|
||||
# Check if object was freed (bomb conversion)
|
||||
if not is_instance_valid(thrown_obj):
|
||||
thrown_obj = null
|
||||
|
||||
# ⚡ Delay collision re-enable to prevent self-collision
|
||||
# ⚡ Delay collision re-enable to prevent self-collision (only if object still exists)
|
||||
if thrown_obj and is_instance_valid(thrown_obj):
|
||||
await get_tree().create_timer(0.1).timeout
|
||||
if thrown_obj and is_instance_valid(thrown_obj):
|
||||
thrown_obj.set_collision_layer_value(2, true)
|
||||
@@ -3076,20 +3305,59 @@ func _force_throw_held_object(direction: Vector2):
|
||||
thrown_obj.set_collision_layer_value(1, true)
|
||||
thrown_obj.set_collision_mask_value(1, true)
|
||||
thrown_obj.set_collision_mask_value(7, true) # Re-enable wall collision!
|
||||
elif thrown_obj and thrown_obj.has_method("can_be_grabbed") and thrown_obj.can_be_grabbed():
|
||||
# Other grabbable object - handle like box
|
||||
thrown_obj.global_position = throw_start_pos
|
||||
|
||||
if thrown_obj.has_method("on_thrown"):
|
||||
# Set throw velocity
|
||||
if "throw_velocity" in thrown_obj:
|
||||
var obj_weight = thrown_obj.weight if "weight" in thrown_obj else 1.0
|
||||
thrown_obj.throw_velocity = throw_direction * throw_force / obj_weight
|
||||
if "is_frozen" in thrown_obj:
|
||||
thrown_obj.is_frozen = false
|
||||
|
||||
# Make airborne
|
||||
if "is_airborne" in thrown_obj:
|
||||
thrown_obj.is_airborne = true
|
||||
thrown_obj.position_z = 2.5
|
||||
thrown_obj.velocity_z = 100.0
|
||||
|
||||
# Call on_thrown if available
|
||||
if thrown_obj and is_instance_valid(thrown_obj) and thrown_obj.has_method("on_thrown"):
|
||||
thrown_obj.on_thrown(self, throw_direction * throw_force)
|
||||
on_thrown_called = true
|
||||
# Check if object was freed (bomb conversion)
|
||||
if not is_instance_valid(thrown_obj):
|
||||
thrown_obj = null
|
||||
|
||||
# Delay collision re-enable (only if object still exists)
|
||||
if thrown_obj and is_instance_valid(thrown_obj):
|
||||
await get_tree().create_timer(0.1).timeout
|
||||
if thrown_obj and is_instance_valid(thrown_obj):
|
||||
thrown_obj.set_collision_layer_value(2, true)
|
||||
thrown_obj.set_collision_mask_value(1, true)
|
||||
thrown_obj.set_collision_mask_value(2, true)
|
||||
thrown_obj.set_collision_mask_value(7, true)
|
||||
|
||||
# Only call on_thrown if it wasn't already called (prevents double-call for bombs)
|
||||
if not on_thrown_called and thrown_obj and is_instance_valid(thrown_obj) and thrown_obj.has_method("on_thrown"):
|
||||
thrown_obj.on_thrown(self, throw_direction * throw_force)
|
||||
|
||||
# Play throw animation
|
||||
_set_animation("THROW")
|
||||
$SfxThrow.play()
|
||||
|
||||
# Sync throw over network
|
||||
# Sync throw over network (only if object still exists - bombs convert to projectile)
|
||||
if thrown_obj and is_instance_valid(thrown_obj):
|
||||
if multiplayer.has_multiplayer_peer() and is_multiplayer_authority() and can_send_rpcs and is_inside_tree():
|
||||
var obj_name = _get_object_name_for_sync(thrown_obj)
|
||||
if obj_name != "":
|
||||
_rpc_to_ready_peers("_sync_throw", [obj_name, throw_start_pos, throw_direction * throw_force, name])
|
||||
|
||||
print("Threw ", thrown_obj.name, " from ", throw_start_pos, " with force: ", throw_direction * throw_force)
|
||||
else:
|
||||
# Bomb was converted to projectile (object was freed)
|
||||
print("Threw bomb (converted to projectile) from ", throw_start_pos, " with force: ", throw_direction * throw_force)
|
||||
|
||||
func _place_down_object():
|
||||
if not held_object:
|
||||
@@ -3296,6 +3564,143 @@ func _perform_attack():
|
||||
can_attack = true
|
||||
is_attacking = false
|
||||
|
||||
func _create_bomb_object():
|
||||
# Dwarf: Create interactable bomb object that can be lifted/thrown
|
||||
if not is_multiplayer_authority():
|
||||
return
|
||||
|
||||
# Consume one bomb
|
||||
if not character_stats or not character_stats.equipment.has("offhand"):
|
||||
return
|
||||
|
||||
var offhand_item = character_stats.equipment["offhand"]
|
||||
if not offhand_item or offhand_item.weapon_type != Item.WeaponType.BOMB or offhand_item.quantity <= 0:
|
||||
return
|
||||
|
||||
offhand_item.quantity -= 1
|
||||
var remaining = offhand_item.quantity
|
||||
if offhand_item.quantity <= 0:
|
||||
character_stats.equipment["offhand"] = null
|
||||
if character_stats:
|
||||
character_stats.character_changed.emit(character_stats)
|
||||
|
||||
# Load interactable object scene
|
||||
var interactable_object_scene = load("res://scenes/interactable_object.tscn")
|
||||
if not interactable_object_scene:
|
||||
push_error("ERROR: Could not load interactable_object scene!")
|
||||
return
|
||||
|
||||
# Spawn bomb object at player position
|
||||
var entities_node = get_parent()
|
||||
if not entities_node:
|
||||
entities_node = get_tree().get_first_node_in_group("game_world").get_node_or_null("Entities")
|
||||
if not entities_node:
|
||||
push_error("ERROR: Could not find Entities node!")
|
||||
return
|
||||
|
||||
var bomb_obj = interactable_object_scene.instantiate()
|
||||
bomb_obj.name = "BombObject_" + str(Time.get_ticks_msec())
|
||||
bomb_obj.global_position = global_position + (facing_direction_vector * 8.0) # Spawn slightly in front
|
||||
|
||||
# Set multiplayer authority
|
||||
if multiplayer.has_multiplayer_peer():
|
||||
bomb_obj.set_multiplayer_authority(get_multiplayer_authority())
|
||||
|
||||
entities_node.add_child(bomb_obj)
|
||||
|
||||
# Setup as bomb object
|
||||
bomb_obj.setup_bomb()
|
||||
|
||||
# Immediately grab it
|
||||
held_object = bomb_obj
|
||||
grab_offset = bomb_obj.position - position
|
||||
grab_distance = global_position.distance_to(bomb_obj.global_position)
|
||||
initial_grab_position = bomb_obj.global_position
|
||||
initial_player_position = global_position
|
||||
|
||||
# Disable collision
|
||||
bomb_obj.set_collision_layer_value(2, false)
|
||||
bomb_obj.set_collision_mask_value(1, false)
|
||||
bomb_obj.set_collision_mask_value(2, false)
|
||||
bomb_obj.set_collision_mask_value(7, true) # Keep wall collision
|
||||
|
||||
# Notify object it's being grabbed
|
||||
if bomb_obj.has_method("on_grabbed"):
|
||||
bomb_obj.on_grabbed(self)
|
||||
|
||||
# Immediately lift the bomb (Dwarf lifts it directly)
|
||||
# Set is_lifting BEFORE calling _lift_object to prevent it from being reset
|
||||
is_lifting = true
|
||||
is_pushing = false
|
||||
|
||||
# Freeze the bomb
|
||||
if "is_frozen" in bomb_obj:
|
||||
bomb_obj.is_frozen = true
|
||||
|
||||
# Call on_lifted if available
|
||||
if bomb_obj.has_method("on_lifted"):
|
||||
bomb_obj.on_lifted(self)
|
||||
|
||||
# Play lift animation
|
||||
_set_animation("LIFT")
|
||||
|
||||
# Sync to network
|
||||
if multiplayer.has_multiplayer_peer() and is_multiplayer_authority() and can_send_rpcs and is_inside_tree():
|
||||
var obj_name = _get_object_name_for_sync(bomb_obj)
|
||||
if obj_name != "" and is_instance_valid(bomb_obj) and bomb_obj.is_inside_tree():
|
||||
_rpc_to_ready_peers("_sync_grab", [obj_name, true, push_axis]) # true = lifting
|
||||
|
||||
print(name, " created bomb object! Remaining bombs: ", remaining)
|
||||
|
||||
# Sync grab to network
|
||||
if multiplayer.has_multiplayer_peer() and can_send_rpcs and is_inside_tree():
|
||||
var obj_name = _get_object_name_for_sync(bomb_obj)
|
||||
if obj_name != "":
|
||||
_rpc_to_ready_peers("_sync_grab", [obj_name, true, push_axis]) # true = lifting
|
||||
|
||||
func _throw_bomb(_target_position: Vector2):
|
||||
# Legacy function - kept for Human/Elf if needed, but Dwarf uses _create_bomb_object now
|
||||
# This is now unused for Dwarf but kept for compatibility
|
||||
pass
|
||||
|
||||
func _place_bomb(target_position: Vector2):
|
||||
# Human/Elf places bomb (no throw, just spawns at target)
|
||||
if not attack_bomb_scene:
|
||||
return
|
||||
|
||||
# Only authority can spawn bombs
|
||||
if not is_multiplayer_authority():
|
||||
return
|
||||
|
||||
# Find valid target position
|
||||
var game_world = get_tree().get_first_node_in_group("game_world")
|
||||
var valid_target_pos = target_position
|
||||
if game_world and game_world.has_method("_get_valid_spell_target_position"):
|
||||
var found_pos = game_world._get_valid_spell_target_position(target_position)
|
||||
if found_pos != Vector2.ZERO:
|
||||
valid_target_pos = found_pos
|
||||
else:
|
||||
print(name, " cannot place bomb - no valid target position")
|
||||
return
|
||||
|
||||
# Spawn bomb at target position
|
||||
var bomb = attack_bomb_scene.instantiate()
|
||||
get_parent().add_child(bomb)
|
||||
bomb.global_position = valid_target_pos
|
||||
|
||||
# Set multiplayer authority
|
||||
if multiplayer.has_multiplayer_peer():
|
||||
bomb.set_multiplayer_authority(get_multiplayer_authority())
|
||||
|
||||
# Setup bomb without throw (placed directly)
|
||||
bomb.setup(valid_target_pos, self, Vector2.ZERO, false) # false = not thrown
|
||||
|
||||
# Sync bomb spawn to other clients
|
||||
if multiplayer.has_multiplayer_peer() and is_multiplayer_authority() and can_send_rpcs and is_inside_tree():
|
||||
_rpc_to_ready_peers("_sync_place_bomb", [valid_target_pos])
|
||||
|
||||
print(name, " placed bomb!")
|
||||
|
||||
func _cast_flame_spell(target_position: Vector2):
|
||||
# Cast flame spell at target position (grid-locked cursor)
|
||||
# If target is blocked, find closest valid position
|
||||
@@ -3602,6 +4007,121 @@ func _clear_spell_charge_tint():
|
||||
for key in keys_to_remove:
|
||||
original_sprite_tints.erase(key)
|
||||
|
||||
func _apply_bow_charge_tint():
|
||||
# Apply pulsing white tint to all sprite layers when fully charged using shader parameters
|
||||
# Pulse between original tint and bow charge tint (white)
|
||||
# IMPORTANT: Only apply to THIS player's sprites (not other players)
|
||||
if not is_charging_bow:
|
||||
return
|
||||
|
||||
var sprites = [
|
||||
{"sprite": sprite_body, "name": "body"},
|
||||
{"sprite": sprite_boots, "name": "boots"},
|
||||
{"sprite": sprite_armour, "name": "armour"},
|
||||
{"sprite": sprite_facial_hair, "name": "facial_hair"},
|
||||
{"sprite": sprite_hair, "name": "hair"},
|
||||
{"sprite": sprite_eyes, "name": "eyes"},
|
||||
{"sprite": sprite_eyelashes, "name": "eyelashes"},
|
||||
{"sprite": sprite_addons, "name": "addons"},
|
||||
{"sprite": sprite_headgear, "name": "headgear"}
|
||||
]
|
||||
|
||||
# Calculate pulse value (0.0 to 1.0) using sine wave
|
||||
var pulse_value = (sin(bow_charge_tint_pulse_time) + 1.0) / 2.0 # 0.0 to 1.0
|
||||
|
||||
for sprite_data in sprites:
|
||||
var sprite = sprite_data.sprite
|
||||
var sprite_name = sprite_data.name
|
||||
|
||||
# Double-check sprite belongs to this player instance
|
||||
if not sprite or not is_instance_valid(sprite):
|
||||
continue
|
||||
|
||||
# Verify sprite is a child of this player node
|
||||
if sprite.get_parent() != self and not is_ancestor_of(sprite):
|
||||
continue
|
||||
|
||||
if sprite.material and sprite.material is ShaderMaterial:
|
||||
var shader_material = sprite.material as ShaderMaterial
|
||||
|
||||
# Store original tint if not already stored (use unique key per player)
|
||||
var tint_key = str(get_instance_id()) + "_bow_" + sprite_name
|
||||
if not tint_key in original_sprite_tints:
|
||||
# Try to get the current tint parameter value
|
||||
var original_tint_param = shader_material.get_shader_parameter("tint")
|
||||
if original_tint_param is Vector4:
|
||||
# Convert Vector4 to Color
|
||||
original_sprite_tints[tint_key] = Color(original_tint_param.x, original_tint_param.y, original_tint_param.z, original_tint_param.w)
|
||||
elif original_tint_param is Color:
|
||||
# Already a Color
|
||||
original_sprite_tints[tint_key] = original_tint_param
|
||||
else:
|
||||
# Default to white if no tint parameter or invalid
|
||||
original_sprite_tints[tint_key] = Color.WHITE
|
||||
|
||||
# Get original tint
|
||||
var original_tint = original_sprite_tints.get(tint_key, Color.WHITE)
|
||||
|
||||
# Calculate fully charged tint (original * bow_charge_tint - white tint)
|
||||
var full_charged_tint = Color(
|
||||
original_tint.r * bow_charge_tint.r,
|
||||
original_tint.g * bow_charge_tint.g,
|
||||
original_tint.b * bow_charge_tint.b,
|
||||
original_tint.a * bow_charge_tint.a
|
||||
)
|
||||
|
||||
# Interpolate between original and fully charged tint based on pulse
|
||||
var current_tint = original_tint.lerp(full_charged_tint, pulse_value)
|
||||
|
||||
# Apply the pulsing tint
|
||||
shader_material.set_shader_parameter("tint", Vector4(current_tint.r, current_tint.g, current_tint.b, current_tint.a))
|
||||
|
||||
func _clear_bow_charge_tint():
|
||||
# Restore original tint values for all sprite layers
|
||||
# IMPORTANT: Only restore THIS player's sprites (not other players)
|
||||
var sprites = [
|
||||
{"sprite": sprite_body, "name": "body"},
|
||||
{"sprite": sprite_boots, "name": "boots"},
|
||||
{"sprite": sprite_armour, "name": "armour"},
|
||||
{"sprite": sprite_facial_hair, "name": "facial_hair"},
|
||||
{"sprite": sprite_hair, "name": "hair"},
|
||||
{"sprite": sprite_eyes, "name": "eyes"},
|
||||
{"sprite": sprite_eyelashes, "name": "eyelashes"},
|
||||
{"sprite": sprite_addons, "name": "addons"},
|
||||
{"sprite": sprite_headgear, "name": "headgear"}
|
||||
]
|
||||
|
||||
var instance_id_str = str(get_instance_id())
|
||||
var keys_to_remove = []
|
||||
|
||||
for sprite_data in sprites:
|
||||
var sprite = sprite_data.sprite
|
||||
var sprite_name = sprite_data.name
|
||||
|
||||
# Double-check sprite belongs to this player instance
|
||||
if not sprite or not is_instance_valid(sprite):
|
||||
continue
|
||||
|
||||
# Verify sprite is a child of this player node
|
||||
if sprite.get_parent() != self and not is_ancestor_of(sprite):
|
||||
continue
|
||||
|
||||
if sprite.material and sprite.material is ShaderMaterial:
|
||||
var shader_material = sprite.material as ShaderMaterial
|
||||
|
||||
# Use unique key per player (with "_bow_" prefix to separate from spell tints)
|
||||
var tint_key = instance_id_str + "_bow_" + sprite_name
|
||||
|
||||
# Restore original tint if we stored it
|
||||
if tint_key in original_sprite_tints:
|
||||
var original_tint = original_sprite_tints[tint_key]
|
||||
shader_material.set_shader_parameter("tint", Vector4(original_tint.r, original_tint.g, original_tint.b, original_tint.a))
|
||||
keys_to_remove.append(tint_key)
|
||||
|
||||
# Clear stored tints for this player only
|
||||
for key in keys_to_remove:
|
||||
original_sprite_tints.erase(key)
|
||||
|
||||
@rpc("any_peer", "reliable")
|
||||
func _sync_spell_charge_start():
|
||||
# Sync spell charge start to other clients
|
||||
@@ -3616,8 +4136,19 @@ func _sync_spell_charge_end():
|
||||
# Sync spell charge end to other clients
|
||||
if not is_multiplayer_authority():
|
||||
is_charging_spell = false
|
||||
spell_incantation_played = false
|
||||
_stop_spell_charge_particles()
|
||||
_clear_spell_charge_tint()
|
||||
|
||||
# Return to IDLE animation
|
||||
_set_animation("IDLE")
|
||||
|
||||
# Stop spell charging sounds
|
||||
if has_node("SfxSpellCharge"):
|
||||
$SfxSpellCharge.stop()
|
||||
if has_node("SfxSpellIncantation"):
|
||||
$SfxSpellIncantation.stop()
|
||||
|
||||
print(name, " (synced) ended charging spell")
|
||||
|
||||
func _apply_burn_debuff():
|
||||
@@ -4040,8 +4571,48 @@ func _sync_bow_charge_end():
|
||||
# Sync bow charge end to other clients
|
||||
if not is_multiplayer_authority():
|
||||
is_charging_bow = false
|
||||
bow_charge_tint_pulse_time = 0.0
|
||||
_clear_bow_charge_tint()
|
||||
print(name, " (synced) ended charging bow")
|
||||
|
||||
@rpc("any_peer", "reliable")
|
||||
func _sync_place_bomb(target_pos: Vector2):
|
||||
# Sync bomb placement to other clients
|
||||
if not is_multiplayer_authority():
|
||||
if not attack_bomb_scene:
|
||||
return
|
||||
|
||||
# Spawn bomb at target position
|
||||
var bomb = attack_bomb_scene.instantiate()
|
||||
get_parent().add_child(bomb)
|
||||
bomb.global_position = target_pos
|
||||
|
||||
# Setup bomb without throw (placed directly)
|
||||
bomb.setup(target_pos, self, Vector2.ZERO, false) # false = not thrown
|
||||
|
||||
print(name, " (synced) placed bomb at ", target_pos)
|
||||
|
||||
@rpc("any_peer", "reliable")
|
||||
func _sync_throw_bomb(bomb_pos: Vector2, throw_force: Vector2):
|
||||
# Sync bomb throw to other clients
|
||||
if not is_multiplayer_authority():
|
||||
if not attack_bomb_scene:
|
||||
return
|
||||
|
||||
# Spawn bomb projectile at position
|
||||
var bomb = attack_bomb_scene.instantiate()
|
||||
get_parent().add_child(bomb)
|
||||
bomb.global_position = bomb_pos
|
||||
|
||||
# Setup bomb with throw physics
|
||||
bomb.setup(bomb_pos, self, throw_force, true) # true = is_thrown
|
||||
|
||||
# Make sure bomb sprite is visible
|
||||
if bomb.has_node("Sprite2D"):
|
||||
bomb.get_node("Sprite2D").visible = true
|
||||
|
||||
print(name, " (synced) threw bomb from ", bomb_pos)
|
||||
|
||||
@rpc("any_peer", "reliable")
|
||||
func _sync_throw(obj_name: String, throw_pos: Vector2, force: Vector2, thrower_name: String):
|
||||
# Sync throw to all clients (RPC sender already threw on their side)
|
||||
@@ -4555,10 +5126,50 @@ func take_damage(amount: float, attacker_position: Vector2, is_burn_damage: bool
|
||||
if is_charging_bow:
|
||||
is_charging_bow = false
|
||||
|
||||
# Clear bow charge tint
|
||||
_clear_bow_charge_tint()
|
||||
|
||||
# Sync bow charge end to other clients
|
||||
if multiplayer.has_multiplayer_peer():
|
||||
_sync_bow_charge_end.rpc()
|
||||
|
||||
# Check if spell charging should be cancelled (50% chance, using gameworld seed)
|
||||
if is_charging_spell:
|
||||
var should_cancel = false
|
||||
var world_node = get_tree().get_first_node_in_group("game_world")
|
||||
if world_node and "dungeon_seed" in world_node:
|
||||
# Use deterministic RNG based on gameworld seed and player position/time
|
||||
var rng_seed = world_node.dungeon_seed
|
||||
rng_seed += int(global_position.x) * 1000 + int(global_position.y)
|
||||
rng_seed += int(Time.get_ticks_msec() / 100.0) # Add time component for uniqueness
|
||||
var rng = RandomNumberGenerator.new()
|
||||
rng.seed = rng_seed
|
||||
should_cancel = rng.randf() < 0.5 # 50% chance
|
||||
else:
|
||||
# Fallback to regular random if no gameworld seed
|
||||
should_cancel = randf() < 0.5
|
||||
|
||||
if should_cancel:
|
||||
is_charging_spell = false
|
||||
spell_incantation_played = false
|
||||
_stop_spell_charge_particles()
|
||||
_clear_spell_charge_tint()
|
||||
|
||||
# Return to IDLE animation
|
||||
_set_animation("IDLE")
|
||||
|
||||
# Stop spell charging sounds
|
||||
if has_node("SfxSpellCharge"):
|
||||
$SfxSpellCharge.stop()
|
||||
if has_node("SfxSpellIncantation"):
|
||||
$SfxSpellIncantation.stop()
|
||||
|
||||
# Sync spell charge end to other clients
|
||||
if multiplayer.has_multiplayer_peer():
|
||||
_sync_spell_charge_end.rpc()
|
||||
|
||||
print(name, " spell charging cancelled due to damage!")
|
||||
|
||||
# Check for dodge chance (based on DEX)
|
||||
var _was_dodged = false
|
||||
if character_stats:
|
||||
@@ -5053,6 +5664,29 @@ func _sync_race_and_stats(race: String, base_stats: Dictionary):
|
||||
_apply_appearance_to_sprites()
|
||||
print("Elf player ", name, " (remote) received short bow and 3 arrows via race sync")
|
||||
|
||||
"Dwarf":
|
||||
character_stats.setEars(0)
|
||||
# Give Dwarf starting bomb to remote players
|
||||
# (Authority players get this in _setup_player_appearance)
|
||||
# Check if equipment is missing - give it regardless of whether race changed
|
||||
if not is_multiplayer_authority():
|
||||
var needs_equipment = false
|
||||
if character_stats.equipment["offhand"] == null:
|
||||
needs_equipment = true
|
||||
else:
|
||||
# Check if offhand is not a bomb
|
||||
var offhand = character_stats.equipment["offhand"]
|
||||
if not offhand or offhand.item_name != "Bomb" or offhand.quantity < 1:
|
||||
needs_equipment = true
|
||||
|
||||
if needs_equipment:
|
||||
var starting_bomb = ItemDatabase.create_item("bomb")
|
||||
if starting_bomb:
|
||||
starting_bomb.quantity = 5 # Give Dwarf 5 bombs to start
|
||||
character_stats.equipment["offhand"] = starting_bomb
|
||||
_apply_appearance_to_sprites()
|
||||
print("Dwarf player ", name, " (remote) received 5 bombs via race sync")
|
||||
|
||||
"Human":
|
||||
character_stats.setEars(0)
|
||||
# Give Human starting spellbook (Tome of Flames) to remote players
|
||||
|
||||
Reference in New Issue
Block a user