added more spell effects, fixed bomb effects, allow to pickup bomb...
This commit is contained in:
Binary file not shown.
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 2.6 KiB |
BIN
src/assets/gfx/fx/shade_spell_effects.png
Normal file
BIN
src/assets/gfx/fx/shade_spell_effects.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 227 KiB |
40
src/assets/gfx/fx/shade_spell_effects.png.import
Normal file
40
src/assets/gfx/fx/shade_spell_effects.png.import
Normal file
@@ -0,0 +1,40 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://bf158atxi7ucy"
|
||||
path="res://.godot/imported/shade_spell_effects.png-1e71e6c58b206f9920df29b69ad9b76f.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://assets/gfx/fx/shade_spell_effects.png"
|
||||
dest_files=["res://.godot/imported/shade_spell_effects.png-1e71e6c58b206f9920df29b69ad9b76f.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
@@ -6,6 +6,9 @@
|
||||
[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"]
|
||||
[ext_resource type="AudioStream" uid="uid://nks0upmnsatn" path="res://assets/audio/sfx/ambience/debris-rocks-01.wav.mp3" id="7_h6264"]
|
||||
[ext_resource type="AudioStream" uid="uid://dpwa2spwtc055" path="res://assets/audio/sfx/ambience/debris-rocks-02.wav.mp3" id="8_fa1rq"]
|
||||
[ext_resource type="AudioStream" uid="uid://cxl1ltxeqd4ye" path="res://assets/audio/sfx/ambience/debris-rocks-03.wav.mp3" id="9_haynv"]
|
||||
|
||||
[sub_resource type="Gradient" id="Gradient_shadow"]
|
||||
colors = PackedColorArray(0, 0, 0, 1, 0, 0, 0, 0)
|
||||
@@ -17,7 +20,7 @@ fill_from = Vector2(0.5, 0.5)
|
||||
fill_to = Vector2(0.8, 0.8)
|
||||
|
||||
[sub_resource type="CircleShape2D" id="CircleShape2D_bomb"]
|
||||
radius = 8.0
|
||||
radius = 4.5
|
||||
|
||||
[sub_resource type="CircleShape2D" id="CircleShape2D_collection"]
|
||||
radius = 16.0
|
||||
@@ -63,6 +66,14 @@ fill = 1
|
||||
fill_from = Vector2(0.5, 0.5)
|
||||
fill_to = Vector2(0.9102564, 0.9188034)
|
||||
|
||||
[sub_resource type="AudioStreamRandomizer" id="AudioStreamRandomizer_tqihf"]
|
||||
playback_mode = 1
|
||||
random_pitch = 1.0376596
|
||||
streams_count = 3
|
||||
stream_0/stream = ExtResource("7_h6264")
|
||||
stream_1/stream = ExtResource("8_fa1rq")
|
||||
stream_2/stream = ExtResource("9_haynv")
|
||||
|
||||
[node name="Bomb" type="CharacterBody2D" unique_id=367943636]
|
||||
collision_layer = 2
|
||||
motion_mode = 1
|
||||
@@ -75,6 +86,7 @@ scale = Vector2(0.1, 0.1)
|
||||
texture = SubResource("GradientTexture2D_shadow")
|
||||
|
||||
[node name="Sprite2D" type="Sprite2D" parent="." unique_id=818862430]
|
||||
position = Vector2(0, -1)
|
||||
texture = ExtResource("2_ng1nl")
|
||||
hframes = 20
|
||||
vframes = 14
|
||||
@@ -95,7 +107,7 @@ 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)
|
||||
debug_color = Color(0.29747584, 0.70196074, 0.6174988, 0.41960785)
|
||||
|
||||
[node name="CollectionArea" type="Area2D" parent="." unique_id=1088408959]
|
||||
collision_layer = 0
|
||||
@@ -103,7 +115,7 @@ 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)
|
||||
debug_color = Color(0.70196074, 0.51966184, 0.5406604, 0.41960785)
|
||||
|
||||
[node name="SfxFuse" type="AudioStreamPlayer2D" parent="." unique_id=1095147141]
|
||||
stream = ExtResource("3_fuse")
|
||||
@@ -125,14 +137,14 @@ bus = &"Sfx"
|
||||
|
||||
[node name="FuseParticles" type="GPUParticles2D" parent="." unique_id=1234567890]
|
||||
z_index = 2
|
||||
position = Vector2(6, -5)
|
||||
position = Vector2(6.44, -6.46)
|
||||
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)
|
||||
position = Vector2(6.485, -6.335)
|
||||
enabled = false
|
||||
color = Color(1, 0.4, 0.1, 1)
|
||||
energy = 0.8
|
||||
@@ -143,3 +155,8 @@ enabled = false
|
||||
color = Color(1, 0.6, 0.2, 1)
|
||||
energy = 2.5
|
||||
texture = SubResource("GradientTexture2D_explosion_light")
|
||||
|
||||
[node name="SfxDebrisFromParticles" type="AudioStreamPlayer2D" parent="." unique_id=1975206979]
|
||||
stream = SubResource("AudioStreamRandomizer_tqihf")
|
||||
attenuation = 1.7411015
|
||||
panning_strength = 1.05
|
||||
|
||||
24
src/scenes/explosion_tile_particle.tscn
Normal file
24
src/scenes/explosion_tile_particle.tscn
Normal file
@@ -0,0 +1,24 @@
|
||||
[gd_scene format=3 uid="uid://explosion_tile_particle_1"]
|
||||
|
||||
[ext_resource type="Script" path="res://scripts/explosion_tile_particle.gd" id="1_script"]
|
||||
[ext_resource type="Texture2D" uid="uid://c4ee36hr5f766" path="res://assets/gfx/RPG DUNGEON VOL 3.png" id="2_dungeon"]
|
||||
|
||||
[sub_resource type="RectangleShape2D" id="RectangleShape2D_1"]
|
||||
size = Vector2(7, 7)
|
||||
|
||||
[node name="ExplosionTileParticle" type="CharacterBody2D"]
|
||||
z_index = 17
|
||||
z_as_relative = false
|
||||
y_sort_enabled = true
|
||||
collision_layer = 0
|
||||
collision_mask = 64
|
||||
script = ExtResource("1_script")
|
||||
|
||||
[node name="CollisionShape2D" type="CollisionShape2D" parent="."]
|
||||
position = Vector2(-3.5, -3.5)
|
||||
shape = SubResource("RectangleShape2D_1")
|
||||
|
||||
[node name="Sprite2D" type="Sprite2D" parent="."]
|
||||
texture = ExtResource("2_dungeon")
|
||||
region_enabled = true
|
||||
region_rect = Rect2(0, 0, 8, 8)
|
||||
@@ -36,6 +36,7 @@
|
||||
[ext_resource type="AudioStream" uid="uid://d1ut5lnlch0k2" path="res://assets/audio/sfx/weapons/bow/bow_release3.mp3" id="32_jc3p3"]
|
||||
[ext_resource type="AudioStream" uid="uid://bvi00vbftbgc5" path="res://assets/audio/sfx/player/ultra_run/shinespark_start.wav" id="35_bj30b"]
|
||||
[ext_resource type="AudioStream" uid="uid://0xm3gyh8051h" path="res://assets/audio/sfx/wizard/incantations/indignation.mp3" id="36_jc3p3"]
|
||||
[ext_resource type="Texture2D" uid="uid://bf158atxi7ucy" path="res://assets/gfx/fx/shade_spell_effects.png" id="37_hax0n"]
|
||||
|
||||
[sub_resource type="Gradient" id="Gradient_wqfne"]
|
||||
colors = PackedColorArray(0, 0, 0, 1, 1, 0.13732082, 0.092538536, 1)
|
||||
@@ -293,6 +294,78 @@ random_pitch = 1.0630184
|
||||
streams_count = 1
|
||||
stream_0/stream = ExtResource("31_487ah")
|
||||
|
||||
[sub_resource type="Animation" id="Animation_t4otl"]
|
||||
length = 0.001
|
||||
tracks/0/type = "value"
|
||||
tracks/0/imported = false
|
||||
tracks/0/enabled = true
|
||||
tracks/0/path = NodePath("IncantationSprite:frame")
|
||||
tracks/0/interp = 1
|
||||
tracks/0/loop_wrap = true
|
||||
tracks/0/keys = {
|
||||
"times": PackedFloat32Array(0),
|
||||
"transitions": PackedFloat32Array(1),
|
||||
"update": 1,
|
||||
"values": [2037]
|
||||
}
|
||||
|
||||
[sub_resource type="Animation" id="Animation_j2b1d"]
|
||||
resource_name = "fire_charging"
|
||||
length = 0.4
|
||||
loop_mode = 1
|
||||
tracks/0/type = "value"
|
||||
tracks/0/imported = false
|
||||
tracks/0/enabled = true
|
||||
tracks/0/path = NodePath("IncantationSprite:frame")
|
||||
tracks/0/interp = 1
|
||||
tracks/0/loop_wrap = true
|
||||
tracks/0/keys = {
|
||||
"times": PackedFloat32Array(0, 0.03333334, 0.06666667, 0.10000002, 0.13333336, 0.16666669, 0.20000002, 0.23333335, 0.26666668, 0.30000004, 0.33333337, 0.3666667, 0.4, 0.43333337, 0.46666673, 0.5, 0.53333336),
|
||||
"transitions": PackedFloat32Array(1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1),
|
||||
"update": 1,
|
||||
"values": [2037, 2038, 2039, 2040, 2041, 2042, 2043, 2044, 2045, 2046, 2047, 2048, 2049, 2050, 2051, 2052, 2053]
|
||||
}
|
||||
|
||||
[sub_resource type="Animation" id="Animation_cs1tg"]
|
||||
resource_name = "fire_ready"
|
||||
length = 0.6
|
||||
tracks/0/type = "value"
|
||||
tracks/0/imported = false
|
||||
tracks/0/enabled = true
|
||||
tracks/0/path = NodePath("IncantationSprite:frame")
|
||||
tracks/0/interp = 1
|
||||
tracks/0/loop_wrap = true
|
||||
tracks/0/keys = {
|
||||
"times": PackedFloat32Array(0, 0.03333334, 0.06666667, 0.10000002, 0.13333336, 0.16666669, 0.20000002, 0.23333335, 0.26666668, 0.30000004, 0.33333337, 0.3666667, 0.4, 0.43333337, 0.46666673, 0.5, 0.53333336),
|
||||
"transitions": PackedFloat32Array(1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1),
|
||||
"update": 1,
|
||||
"values": [2037, 2038, 2039, 2040, 2041, 2042, 2043, 2044, 2045, 2046, 2047, 2048, 2049, 2050, 2051, 2052, 2053]
|
||||
}
|
||||
|
||||
[sub_resource type="Animation" id="Animation_hax0n"]
|
||||
resource_name = "idle"
|
||||
length = 0.1
|
||||
tracks/0/type = "value"
|
||||
tracks/0/imported = false
|
||||
tracks/0/enabled = true
|
||||
tracks/0/path = NodePath("IncantationSprite:frame")
|
||||
tracks/0/interp = 1
|
||||
tracks/0/loop_wrap = true
|
||||
tracks/0/keys = {
|
||||
"times": PackedFloat32Array(0),
|
||||
"transitions": PackedFloat32Array(1),
|
||||
"update": 1,
|
||||
"values": [0]
|
||||
}
|
||||
|
||||
[sub_resource type="AnimationLibrary" id="AnimationLibrary_2dvfe"]
|
||||
_data = {
|
||||
&"RESET": SubResource("Animation_t4otl"),
|
||||
&"fire_charging": SubResource("Animation_j2b1d"),
|
||||
&"fire_ready": SubResource("Animation_cs1tg"),
|
||||
&"idle": SubResource("Animation_hax0n")
|
||||
}
|
||||
|
||||
[node name="Player" type="CharacterBody2D" unique_id=937429705]
|
||||
collision_mask = 67
|
||||
motion_mode = 1
|
||||
@@ -493,3 +566,13 @@ volume_db = 5.729
|
||||
attenuation = 7.727487
|
||||
panning_strength = 1.04
|
||||
bus = &"Sfx"
|
||||
|
||||
[node name="IncantationSprite" type="Sprite2D" parent="." unique_id=1655944614]
|
||||
texture = ExtResource("37_hax0n")
|
||||
hframes = 105
|
||||
vframes = 79
|
||||
frame = 2037
|
||||
|
||||
[node name="AnimationIncantation" type="AnimationPlayer" parent="." unique_id=17658820]
|
||||
libraries/ = SubResource("AnimationLibrary_2dvfe")
|
||||
autoplay = &"idle"
|
||||
|
||||
@@ -1,28 +1,26 @@
|
||||
[gd_scene format=3 uid="uid://bvxp7yw8q1k2l"]
|
||||
|
||||
[ext_resource type="Script" path="res://scripts/sword_slash.gd" id="1_sword"]
|
||||
[ext_resource type="Texture2D" uid="uid://hib38y541eog" path="res://assets/gfx/items_n_shit.png" id="2_texture"]
|
||||
[ext_resource type="Script" uid="uid://bqxbhjq2b4ram" path="res://scripts/sword_slash.gd" id="1_sword"]
|
||||
[ext_resource type="Texture2D" uid="uid://dkpritx47nd4m" path="res://assets/gfx/pickups/items_n_shit.png" id="2_e3omh"]
|
||||
|
||||
[sub_resource type="RectangleShape2D" id="RectangleShape2D_slash"]
|
||||
size = Vector2(120, 60)
|
||||
|
||||
[node name="SwordSlash" type="Node2D"]
|
||||
[node name="SwordSlash" type="Node2D" unique_id=1348241278]
|
||||
script = ExtResource("1_sword")
|
||||
|
||||
[node name="Sprite2D" type="Sprite2D" parent="."]
|
||||
texture = ExtResource("2_texture")
|
||||
[node name="Sprite2D" type="Sprite2D" parent="." unique_id=1244548324]
|
||||
rotation = 3.14159
|
||||
scale = Vector2(3, 3)
|
||||
texture = ExtResource("2_e3omh")
|
||||
hframes = 20
|
||||
vframes = 14
|
||||
frame = 60
|
||||
region_enabled = false
|
||||
|
||||
[node name="Area2D" type="Area2D" parent="."]
|
||||
[node name="Area2D" type="Area2D" parent="." unique_id=1569887807]
|
||||
collision_layer = 4
|
||||
collision_mask = 3
|
||||
|
||||
[node name="CollisionShape2D" type="CollisionShape2D" parent="Area2D"]
|
||||
[node name="CollisionShape2D" type="CollisionShape2D" parent="Area2D" unique_id=1035191880]
|
||||
position = Vector2(60, 0)
|
||||
shape = SubResource("RectangleShape2D_slash")
|
||||
|
||||
|
||||
@@ -288,14 +288,14 @@ func _stick_to_wall():
|
||||
stick_timer = 0.0
|
||||
arrow_area.set_deferred("monitoring", false)
|
||||
|
||||
@rpc("any_peer", "call_local", "reliable")
|
||||
func _sync_arrow_collected():
|
||||
# Sync arrow collection across network - mark as collected and remove
|
||||
if not is_collected:
|
||||
is_collected = true
|
||||
print(name, " arrow collected (synced)")
|
||||
# Queue free on next frame to avoid issues
|
||||
call_deferred("queue_free")
|
||||
func _sync_arrow_collected_via_gameworld(arrow_name: String):
|
||||
# Route sync through game_world (RPC on arrow caused "node not found" on host).
|
||||
# Collector already added to inventory and will queue_free locally.
|
||||
if arrow_name.is_empty():
|
||||
return
|
||||
var gw = get_tree().get_first_node_in_group("game_world")
|
||||
if gw and gw.has_method("_sync_arrow_collected") and multiplayer.has_multiplayer_peer():
|
||||
gw._sync_arrow_collected.rpc(arrow_name)
|
||||
|
||||
@rpc("any_peer", "call_local", "reliable")
|
||||
func _sync_arrow_hit(target_name: String):
|
||||
@@ -409,20 +409,19 @@ func _on_collection_area_body_entered(body: Node2D):
|
||||
print("ERROR: body.character_stats is invalid when trying to collect arrow")
|
||||
return
|
||||
|
||||
# Capture stable arrow name for sync (route through game_world to avoid RPC path issues)
|
||||
var arrow_name = name
|
||||
|
||||
# Check if player has arrows equipped in offhand
|
||||
var offhand_item = body.character_stats.equipment.get("offhand", null)
|
||||
if offhand_item and is_instance_valid(offhand_item) and offhand_item.item_name == "Arrow":
|
||||
is_collected = true
|
||||
|
||||
# Sync arrow collection to all clients
|
||||
if multiplayer.has_multiplayer_peer():
|
||||
_sync_arrow_collected.rpc()
|
||||
|
||||
$SfxPickup.play()
|
||||
# Add directly to equipped arrows
|
||||
offhand_item.quantity += 1
|
||||
body.character_stats.character_changed.emit(body.character_stats)
|
||||
print(body.name, " collected arrow from wall! Total arrows: ", offhand_item.quantity)
|
||||
_sync_arrow_collected_via_gameworld(arrow_name)
|
||||
await $SfxPickup.finished
|
||||
queue_free()
|
||||
return
|
||||
@@ -436,16 +435,12 @@ func _on_collection_area_body_entered(body: Node2D):
|
||||
var new_arrow = ItemDatabase.create_item("arrow")
|
||||
if new_arrow and is_instance_valid(new_arrow):
|
||||
is_collected = true
|
||||
|
||||
# Sync arrow collection to all clients
|
||||
if multiplayer.has_multiplayer_peer():
|
||||
_sync_arrow_collected.rpc()
|
||||
|
||||
$SfxPickup.play()
|
||||
new_arrow.quantity = 1
|
||||
body.character_stats.equipment["offhand"] = new_arrow
|
||||
body.character_stats.character_changed.emit(body.character_stats)
|
||||
print(body.name, " collected arrow and re-equipped to offhand!")
|
||||
_sync_arrow_collected_via_gameworld(arrow_name)
|
||||
await $SfxPickup.finished
|
||||
queue_free()
|
||||
return
|
||||
@@ -457,22 +452,14 @@ func _on_collection_area_body_entered(body: Node2D):
|
||||
return
|
||||
|
||||
inventory_arrow.quantity = 1
|
||||
if not body.character_stats.has_method("add_item_to_inventory"):
|
||||
print("ERROR: character_stats missing add_item_to_inventory method")
|
||||
if not body.character_stats.has_method("add_item"):
|
||||
print("ERROR: character_stats missing add_item method")
|
||||
return
|
||||
|
||||
var success = body.character_stats.add_item_to_inventory(inventory_arrow)
|
||||
if success:
|
||||
is_collected = true
|
||||
|
||||
# Sync arrow collection to all clients
|
||||
if multiplayer.has_multiplayer_peer():
|
||||
_sync_arrow_collected.rpc()
|
||||
|
||||
$SfxPickup.play()
|
||||
print(body.name, " collected arrow from wall into inventory!")
|
||||
await $SfxPickup.finished
|
||||
queue_free()
|
||||
else:
|
||||
print(body.name, " inventory full, couldn't collect arrow")
|
||||
# Don't remove arrow if inventory is full
|
||||
body.character_stats.add_item(inventory_arrow)
|
||||
is_collected = true
|
||||
$SfxPickup.play()
|
||||
print(body.name, " collected arrow from wall into inventory!")
|
||||
_sync_arrow_collected_via_gameworld(arrow_name)
|
||||
await $SfxPickup.finished
|
||||
queue_free()
|
||||
|
||||
@@ -5,7 +5,7 @@ extends CharacterBody2D
|
||||
@export var fuse_duration: float = 3.0 # Time until explosion
|
||||
@export var base_damage: float = 50.0 # Base damage (increased from 30)
|
||||
@export var damage_radius: float = 48.0 # Area of effect radius (48x48)
|
||||
@export var screenshake_strength: float = 5.0 # Base screenshake strength
|
||||
@export var screenshake_strength: float = 18.0 # Base screenshake strength (stronger)
|
||||
|
||||
var player_owner: Node = null
|
||||
var is_fused: bool = false
|
||||
@@ -21,6 +21,7 @@ var velocity_z: float = 0.0
|
||||
var gravity_z: float = 500.0
|
||||
var is_airborne: bool = false
|
||||
var throw_velocity: Vector2 = Vector2.ZERO
|
||||
var rotation_speed: float = 0.0 # Angular velocity when thrown
|
||||
|
||||
# Blinking animation
|
||||
var blink_timer: float = 0.0
|
||||
@@ -43,6 +44,10 @@ var collection_delay: float = 0.2 # Can be collected after 0.2 seconds
|
||||
# Damage area (larger than collision)
|
||||
var damage_area_shape: CircleShape2D = null
|
||||
|
||||
const TILE_SIZE: int = 16
|
||||
const TILE_STRIDE: int = 17 # 16 + separation 1
|
||||
var _explosion_tile_particle_scene: PackedScene = null
|
||||
|
||||
func _ready():
|
||||
# Set collision layer to 2 (interactable objects) so it can be grabbed
|
||||
collision_layer = 2
|
||||
@@ -62,7 +67,17 @@ func _ready():
|
||||
if explosion_sprite:
|
||||
explosion_sprite.visible = false
|
||||
|
||||
# Setup damage area (48x48 radius)
|
||||
# Setup shadow (like interactable - visible, under bomb)
|
||||
if shadow:
|
||||
shadow.visible = true
|
||||
shadow.modulate = Color(0, 0, 0, 0.5)
|
||||
shadow.z_index = -1
|
||||
|
||||
# Defer area/shape setup and fuse start – may run during physics (e.g. trap damage → throw)
|
||||
call_deferred("_deferred_ready")
|
||||
|
||||
func _deferred_ready():
|
||||
# Setup damage area (48x48 radius) – safe to touch Area2D/shape when not flushing queries
|
||||
if bomb_area:
|
||||
var collision_shape = bomb_area.get_node_or_null("CollisionShape2D")
|
||||
if collision_shape:
|
||||
@@ -70,7 +85,7 @@ func _ready():
|
||||
damage_area_shape.radius = damage_radius / 2.0 # 24 pixel radius for 48x48
|
||||
collision_shape.shape = damage_area_shape
|
||||
|
||||
# Start fuse if not thrown (placed bomb starts fusing immediately)
|
||||
# Start fuse if not thrown (placed bomb starts fusing immediately; thrown bombs start fuse on land)
|
||||
if not is_thrown:
|
||||
_start_fuse()
|
||||
|
||||
@@ -88,6 +103,12 @@ func setup(target_position: Vector2, owner_player: Node, throw_force: Vector2 =
|
||||
is_airborne = true
|
||||
position_z = 2.5
|
||||
velocity_z = 100.0
|
||||
# Rotation when thrown (based on throw direction)
|
||||
if throw_force.length_squared() > 1.0:
|
||||
var perp = Vector2(-throw_force.y, throw_force.x)
|
||||
rotation_speed = sign(perp.x + perp.y) * 12.0
|
||||
else:
|
||||
rotation_speed = 8.0
|
||||
# Make sure sprite is visible
|
||||
if sprite:
|
||||
sprite.visible = true
|
||||
@@ -157,17 +178,19 @@ func _physics_process(delta):
|
||||
velocity_z -= gravity_z * delta
|
||||
position_z += velocity_z * delta
|
||||
|
||||
# Update sprite position based on height
|
||||
# Update sprite position and rotation based on height
|
||||
if sprite:
|
||||
sprite.position.y = -position_z * 0.5
|
||||
var height_scale = 1.0 - (position_z / 50.0) * 0.2
|
||||
sprite.scale = Vector2.ONE * max(0.8, height_scale)
|
||||
sprite.rotation += rotation_speed * delta
|
||||
|
||||
# Update shadow
|
||||
# Update shadow (like interactable - scale down when airborne for visibility)
|
||||
if shadow:
|
||||
shadow.visible = true
|
||||
var shadow_scale = 1.0 - (position_z / 75.0) * 0.5
|
||||
shadow.scale = Vector2.ONE * max(0.5, shadow_scale)
|
||||
shadow.modulate.a = 0.5 - (position_z / 100.0) * 0.3
|
||||
shadow.modulate = Color(0, 0, 0, 0.5 - (position_z / 100.0) * 0.3)
|
||||
|
||||
# Apply throw velocity
|
||||
velocity = throw_velocity
|
||||
@@ -176,13 +199,15 @@ func _physics_process(delta):
|
||||
if position_z <= 0.0:
|
||||
_land()
|
||||
else:
|
||||
# On ground - reset sprite/shadow
|
||||
# On ground - reset sprite/shadow (shadow visible like interactable)
|
||||
if sprite:
|
||||
sprite.position.y = 0
|
||||
sprite.scale = Vector2.ONE
|
||||
sprite.rotation = 0.0
|
||||
if shadow:
|
||||
shadow.visible = true
|
||||
shadow.scale = Vector2.ONE
|
||||
shadow.modulate.a = 0.5
|
||||
shadow.modulate = Color(0, 0, 0, 0.5)
|
||||
|
||||
# Apply friction if on ground
|
||||
if not is_airborne:
|
||||
@@ -211,7 +236,7 @@ func _physics_process(delta):
|
||||
if fuse_timer >= collection_delay:
|
||||
can_be_collected = true
|
||||
if collection_area:
|
||||
collection_area.monitoring = true
|
||||
collection_area.set_deferred("monitoring", true)
|
||||
|
||||
func _land():
|
||||
is_airborne = false
|
||||
@@ -228,9 +253,11 @@ func _explode():
|
||||
|
||||
is_exploding = true
|
||||
|
||||
# Hide bomb sprite, show explosion
|
||||
# Hide bomb sprite and shadow, show explosion
|
||||
if sprite:
|
||||
sprite.visible = false
|
||||
if shadow:
|
||||
shadow.visible = false
|
||||
if explosion_sprite:
|
||||
explosion_sprite.visible = true
|
||||
explosion_sprite.frame = 0
|
||||
@@ -263,6 +290,11 @@ func _explode():
|
||||
# Cause screenshake
|
||||
_cause_screenshake()
|
||||
|
||||
# Spawn tile debris particles (4 pieces per affected tile, bounce, fade)
|
||||
_spawn_explosion_tile_particles()
|
||||
if has_node("SfxDebrisFromParticles"):
|
||||
$SfxDebrisFromParticles.play()
|
||||
|
||||
# Disable collision
|
||||
if bomb_area:
|
||||
bomb_area.set_deferred("monitoring", false)
|
||||
@@ -332,6 +364,76 @@ func _deal_explosion_damage():
|
||||
|
||||
print("Bomb hit enemy: ", body.name, " for ", final_damage, " damage!")
|
||||
|
||||
func _spawn_explosion_tile_particles():
|
||||
var game_world = get_tree().get_first_node_in_group("game_world")
|
||||
if not game_world:
|
||||
return
|
||||
|
||||
var layer = game_world.get_node_or_null("Environment/DungeonLayer0")
|
||||
if not layer or not layer is TileMapLayer:
|
||||
return
|
||||
|
||||
if not _explosion_tile_particle_scene:
|
||||
_explosion_tile_particle_scene = load("res://scenes/explosion_tile_particle.tscn") as PackedScene
|
||||
if not _explosion_tile_particle_scene:
|
||||
return
|
||||
|
||||
var tex = load("res://assets/gfx/RPG DUNGEON VOL 3.png") as Texture2D
|
||||
if not tex:
|
||||
return
|
||||
|
||||
var center = global_position
|
||||
var r = damage_radius
|
||||
var layer_pos = center - layer.global_position
|
||||
var center_cell = layer.local_to_map(layer_pos)
|
||||
var half_cells = ceili(r / float(TILE_SIZE)) + 1
|
||||
|
||||
var parent = get_parent()
|
||||
if not parent:
|
||||
parent = game_world.get_node_or_null("Entities")
|
||||
if not parent:
|
||||
return
|
||||
|
||||
for gx in range(center_cell.x - half_cells, center_cell.x + half_cells + 1):
|
||||
for gy in range(center_cell.y - half_cells, center_cell.y + half_cells + 1):
|
||||
var cell = Vector2i(gx, gy)
|
||||
if layer.get_cell_source_id(cell) < 0:
|
||||
continue
|
||||
var atlas = layer.get_cell_atlas_coords(cell)
|
||||
var world = layer.map_to_local(cell) + layer.global_position
|
||||
if world.distance_to(center) > r:
|
||||
continue
|
||||
var bx = atlas.x * TILE_STRIDE
|
||||
var by = atlas.y * TILE_STRIDE
|
||||
var h = 8.0 # TILE_SIZE / 2
|
||||
var regions = [
|
||||
Rect2(bx, by, h, h),
|
||||
Rect2(bx + h, by, h, h),
|
||||
Rect2(bx, by + h, h, h),
|
||||
Rect2(bx + h, by + h, h, h)
|
||||
]
|
||||
# Direction from explosion center to this tile (outward) – particles fly away from bomb
|
||||
var to_tile = world - center
|
||||
var outward = to_tile.normalized() if to_tile.length() > 1.0 else Vector2.RIGHT.rotated(randf() * TAU)
|
||||
# Half the particles: 2 pieces per tile instead of 4 (indices 0 and 2)
|
||||
for i in [0, 2]:
|
||||
var p = _explosion_tile_particle_scene.instantiate() as CharacterBody2D
|
||||
var spr = p.get_node_or_null("Sprite2D") as Sprite2D
|
||||
if not spr:
|
||||
p.queue_free()
|
||||
continue
|
||||
spr.texture = tex
|
||||
spr.region_enabled = true
|
||||
spr.region_rect = regions[i]
|
||||
p.global_position = world
|
||||
var speed = randf_range(280.0, 420.0) # Much faster - fly around more
|
||||
var d = outward + Vector2(randf_range(-0.4, 0.4), randf_range(-0.4, 0.4))
|
||||
p.velocity = d.normalized() * speed
|
||||
p.angular_velocity = randf_range(-14.0, 14.0)
|
||||
p.position_z = 0.0
|
||||
p.velocity_z = randf_range(100.0, 180.0) # Upward burst, then gravity brings them down
|
||||
parent.add_child(p)
|
||||
|
||||
func _cause_screenshake():
|
||||
# Calculate screenshake based on distance from local players
|
||||
var game_world = get_tree().get_first_node_in_group("game_world")
|
||||
@@ -359,9 +461,9 @@ func _cause_screenshake():
|
||||
var shake_strength = screenshake_strength / max(1.0, min_distance / 50.0)
|
||||
shake_strength = min(shake_strength, screenshake_strength * 2.0) # Cap at 2x base
|
||||
|
||||
# Apply screenshake
|
||||
# Apply screenshake (longer duration for bigger boom)
|
||||
if game_world.has_method("add_screenshake"):
|
||||
game_world.add_screenshake(shake_strength, 0.3) # 0.3 second duration
|
||||
game_world.add_screenshake(shake_strength, 0.5) # 0.5 second duration
|
||||
|
||||
func _on_bomb_area_body_entered(_body):
|
||||
# This is for explosion damage (handled in _deal_explosion_damage)
|
||||
@@ -390,6 +492,14 @@ func on_grabbed(by_player):
|
||||
can_collect = true
|
||||
|
||||
if can_collect:
|
||||
# Stop fuse sound, particles, and light when collecting
|
||||
if has_node("SfxFuse"):
|
||||
$SfxFuse.stop()
|
||||
if fuse_particles:
|
||||
fuse_particles.emitting = false
|
||||
if fuse_light:
|
||||
fuse_light.enabled = false
|
||||
|
||||
# Create bomb item
|
||||
var bomb_item = ItemDatabase.create_item("bomb")
|
||||
if bomb_item:
|
||||
@@ -404,6 +514,16 @@ func on_grabbed(by_player):
|
||||
|
||||
by_player.character_stats.character_changed.emit(by_player.character_stats)
|
||||
|
||||
# Show "+1 Bomb" above player
|
||||
var floating_text_scene = load("res://scenes/floating_text.tscn") as PackedScene
|
||||
if floating_text_scene and by_player and is_instance_valid(by_player):
|
||||
var ft = floating_text_scene.instantiate()
|
||||
var parent = by_player.get_parent()
|
||||
if parent:
|
||||
parent.add_child(ft)
|
||||
ft.global_position = Vector2(by_player.global_position.x, by_player.global_position.y - 20)
|
||||
ft.setup("+1 Bomb", Color(0.9, 0.5, 0.2), 0.5, 0.5) # Orange-ish
|
||||
|
||||
# Play pickup sound
|
||||
if has_node("SfxPickup"):
|
||||
$SfxPickup.play()
|
||||
|
||||
@@ -204,6 +204,15 @@ func _deal_periodic_damage():
|
||||
else:
|
||||
print("Flame spell hit enemy: ", body.name, " for ", final_damage, " damage!")
|
||||
|
||||
# Destroy wooden interactable objects (box, barrel, pot, etc.) – they burn and break
|
||||
elif body.is_in_group("interactable_object") and body.has_method("take_fire_damage"):
|
||||
if "is_being_held" in body and body.is_being_held:
|
||||
continue # Don't break objects while held
|
||||
var attacker_pos = player_owner.global_position if player_owner else global_position
|
||||
body.take_fire_damage(final_damage, attacker_pos)
|
||||
if is_first_hit:
|
||||
print("Flame spell burning wooden object: ", body.name, " for ", final_damage, " damage!")
|
||||
|
||||
func _on_body_entered(_body):
|
||||
# Track bodies that enter the area (for periodic damage)
|
||||
# Don't add to hit_targets here - we want to deal damage multiple times
|
||||
|
||||
@@ -31,6 +31,7 @@ var background: ColorRect = null
|
||||
var metropolis_font: FontFile = null
|
||||
|
||||
func _ready():
|
||||
add_to_group("chat_ui")
|
||||
network_manager = get_node_or_null("/root/NetworkManager")
|
||||
|
||||
# Load Metropolis font
|
||||
|
||||
60
src/scripts/explosion_tile_particle.gd
Normal file
60
src/scripts/explosion_tile_particle.gd
Normal file
@@ -0,0 +1,60 @@
|
||||
extends CharacterBody2D
|
||||
# Tile debris from bomb explosion - flies out, bounces off walls, fades
|
||||
|
||||
var angular_velocity: float = 0.0
|
||||
var fade_timer: float = 0.0
|
||||
var fading: bool = false
|
||||
const FADE_DELAY: float = 0.8
|
||||
const BOUNCE_DAMP: float = 0.82 # Keep more speed after bounce - fly around more
|
||||
|
||||
# Z-axis physics (fly up, then fall back down with gravity)
|
||||
var position_z: float = 0.0
|
||||
var velocity_z: float = 0.0
|
||||
var acceleration_z: float = -500.0 # Downward gravity
|
||||
|
||||
func _physics_process(delta: float) -> void:
|
||||
if fading:
|
||||
return
|
||||
|
||||
# Slow down over time (slower decay so they keep flying longer)
|
||||
velocity = velocity.lerp(Vector2.ZERO, delta * 1.8)
|
||||
if abs(angular_velocity) > 0.1:
|
||||
rotation += angular_velocity * delta
|
||||
angular_velocity = lerp(angular_velocity, 0.0, delta * 1.5)
|
||||
|
||||
# Z physics: gravity, then integrate
|
||||
velocity_z += acceleration_z * delta
|
||||
position_z += velocity_z * delta
|
||||
if position_z <= 0.0:
|
||||
position_z = 0.0
|
||||
velocity_z = 0.0
|
||||
|
||||
move_and_slide()
|
||||
|
||||
# Bounce off walls (tilemap uses layer 7 = bit 64)
|
||||
for i in get_slide_collision_count():
|
||||
var col = get_slide_collision(i)
|
||||
if col and col.get_collider() is CollisionObject2D:
|
||||
var wall = col.get_collider() as CollisionObject2D
|
||||
if wall.get_collision_layer_value(7): # Layer 7 = walls
|
||||
velocity = velocity.bounce(col.get_normal()) * BOUNCE_DAMP
|
||||
angular_velocity *= -0.6
|
||||
break
|
||||
|
||||
# Visual: raise sprite when airborne, scale up a little when flying upward
|
||||
var spr = get_node_or_null("Sprite2D")
|
||||
if spr:
|
||||
spr.position.y = -position_z * 0.5
|
||||
var scale_up = 1.0 + (position_z / 60.0) * 0.25 # Slightly bigger when higher
|
||||
spr.scale = Vector2.ONE * clampf(scale_up, 1.0, 1.35)
|
||||
|
||||
# Fade after delay
|
||||
fade_timer += delta
|
||||
if fade_timer >= FADE_DELAY:
|
||||
fading = true
|
||||
if spr:
|
||||
var t = create_tween()
|
||||
t.tween_property(spr, "modulate:a", 0.0, 0.4)
|
||||
t.tween_callback(queue_free)
|
||||
else:
|
||||
queue_free()
|
||||
1
src/scripts/explosion_tile_particle.gd.uid
Normal file
1
src/scripts/explosion_tile_particle.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://c3ae81av1j4qr
|
||||
@@ -2044,8 +2044,8 @@ func _is_valid_spell_target(target_pos: Vector2, player_pos: Vector2) -> bool:
|
||||
if tile_x < 0 or tile_y < 0 or tile_x >= map_size.x or tile_y >= map_size.y:
|
||||
return false
|
||||
|
||||
# Check if it's a floor tile (grid value 1) or corridor (grid value 3)
|
||||
if grid[tile_x][tile_y] != 1 and grid[tile_x][tile_y] != 3:
|
||||
# Check if it's a floor (1), door (2), or corridor (3) tile - same as walkable
|
||||
if grid[tile_x][tile_y] != 1 and grid[tile_x][tile_y] != 2 and grid[tile_x][tile_y] != 3:
|
||||
return false
|
||||
|
||||
# Check if there's a wall between player and target using raycast
|
||||
@@ -6661,6 +6661,20 @@ func _broadcast_object_break(obj_name: String):
|
||||
if is_inside_tree() and multiplayer.has_multiplayer_peer() and multiplayer.is_server():
|
||||
_rpc_to_ready_peers("_sync_object_break", [obj_name])
|
||||
|
||||
@rpc("any_peer", "reliable")
|
||||
func _sync_arrow_collected(arrow_name: String):
|
||||
# Route arrow collection through game_world to avoid node path issues (RPC was on arrow).
|
||||
# Collector already added to inventory, played sound, and queue_freed locally.
|
||||
# Other peers find arrow in Entities by stable name and remove it.
|
||||
if not is_inside_tree() or arrow_name.is_empty():
|
||||
return
|
||||
var entities_node = get_node_or_null("Entities")
|
||||
if not entities_node:
|
||||
return
|
||||
var arrow = entities_node.get_node_or_null(arrow_name)
|
||||
if arrow and is_instance_valid(arrow):
|
||||
arrow.call_deferred("queue_free")
|
||||
|
||||
func _create_level_complete_ui_programmatically() -> Node:
|
||||
# Create level complete UI programmatically
|
||||
var canvas_layer = CanvasLayer.new()
|
||||
|
||||
@@ -408,6 +408,28 @@ func can_be_thrown() -> bool:
|
||||
func can_be_destroyed() -> bool:
|
||||
return is_destroyable
|
||||
|
||||
func _is_wooden_burnable() -> bool:
|
||||
var t = object_type if object_type != "" else _get_configured_object_type()
|
||||
return t in ["Box", "Pot", "LiftableBarrel", "PushableBarrel", "PushableHighBox"]
|
||||
|
||||
func take_fire_damage(amount: float, _attacker_position: Vector2) -> void:
|
||||
if not is_destroyable or is_broken or not _is_wooden_burnable():
|
||||
return
|
||||
health -= amount
|
||||
if health > 0:
|
||||
return
|
||||
var game_world = get_tree().get_first_node_in_group("game_world")
|
||||
if multiplayer.has_multiplayer_peer():
|
||||
if multiplayer.is_server():
|
||||
if game_world and game_world.has_method("_rpc_to_ready_peers"):
|
||||
game_world._rpc_to_ready_peers("_sync_object_break", [name])
|
||||
_break_into_pieces()
|
||||
else:
|
||||
if game_world and game_world.has_method("_sync_object_break"):
|
||||
game_world._sync_object_break.rpc_id(1, name)
|
||||
else:
|
||||
_break_into_pieces()
|
||||
|
||||
func on_grabbed(by_player):
|
||||
# Special handling for chests - open instead of grab
|
||||
if object_type == "Chest" and not is_chest_opened:
|
||||
@@ -526,9 +548,9 @@ func _convert_to_bomb_projectile(by_player, force: Vector2):
|
||||
if bomb.has_node("Sprite2D"):
|
||||
bomb.get_node("Sprite2D").visible = true
|
||||
|
||||
# Sync bomb throw to other clients
|
||||
# Sync bomb throw to other clients (pass our name so they can free the lifted bomb)
|
||||
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])
|
||||
by_player._rpc_to_ready_peers("_sync_throw_bomb", [name, current_pos, force])
|
||||
|
||||
# Remove the interactable object
|
||||
queue_free()
|
||||
|
||||
@@ -514,8 +514,9 @@ func _update_ui():
|
||||
sprite.scale = Vector2(2.0, 2.0) # 2x size as requested
|
||||
button.add_child(sprite)
|
||||
|
||||
# Add quantity label if item can have multiple (like arrows)
|
||||
if equipped_item.can_have_multiple_of and equipped_item.quantity > 1:
|
||||
# Add quantity label if item can have multiple (like arrows, bombs)
|
||||
var show_qty = equipped_item.can_have_multiple_of and (equipped_item.quantity > 1 or equipped_item.weapon_type == Item.WeaponType.BOMB)
|
||||
if show_qty:
|
||||
var quantity_label = Label.new()
|
||||
quantity_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
|
||||
quantity_label.vertical_alignment = VERTICAL_ALIGNMENT_TOP
|
||||
@@ -530,6 +531,7 @@ func _update_ui():
|
||||
font_file.font_data = dmg_font_resource
|
||||
quantity_label.add_theme_font_override("font", font_file)
|
||||
quantity_label.add_theme_font_size_override("font_size", 16)
|
||||
quantity_label.add_theme_color_override("font_color", Color.GREEN)
|
||||
quantity_label.z_index = 100 # High z-index to show above item sprite
|
||||
quantity_label.z_as_relative = false # Absolute z-index
|
||||
button.add_child(quantity_label)
|
||||
@@ -610,6 +612,7 @@ func _update_ui():
|
||||
font_file.font_data = dmg_font_resource
|
||||
quantity_label.add_theme_font_override("font", font_file)
|
||||
quantity_label.add_theme_font_size_override("font_size", 16)
|
||||
quantity_label.add_theme_color_override("font_color", Color.GREEN)
|
||||
quantity_label.z_index = 100 # High z-index to show above item sprite
|
||||
button.add_child(quantity_label)
|
||||
|
||||
|
||||
@@ -40,24 +40,24 @@ var use_function = null
|
||||
|
||||
var item_name: String = "Red Apple"
|
||||
var description: String = "Restores 5 HP"
|
||||
var spritePath: String = "res://assets/gfx/items_n_shit.png"
|
||||
var spritePath: String = "res://assets/gfx/pickups/items_n_shit.png"
|
||||
var equipmentPath: String = "res://assets/gfx/Puny-Characters/Layer 2 - Clothes/Basic Body/BasicRed.png"
|
||||
var colorReplacements: Array = []
|
||||
var spriteFrames:Vector2i = Vector2i(20,14)
|
||||
var spriteFrame:int = 0
|
||||
var modifiers: Dictionary = { "hp": +20 }
|
||||
var spriteFrames: Vector2i = Vector2i(20, 14)
|
||||
var spriteFrame: int = 0
|
||||
var modifiers: Dictionary = {"hp": + 20}
|
||||
var duration: float = 0
|
||||
|
||||
var buy_cost: int = 10
|
||||
var sell_worth: int = 3
|
||||
var sellable:bool = true
|
||||
var sellable: bool = true
|
||||
var item_type: ItemType = ItemType.Restoration
|
||||
var equipment_type: EquipmentType = EquipmentType.NONE
|
||||
var weapon_type: WeaponType = WeaponType.NONE
|
||||
var two_handed:bool = false
|
||||
var two_handed: bool = false
|
||||
var quantity = 1
|
||||
var can_have_multiple_of:bool = false
|
||||
var weight: float = 2.0 # Item weight for encumbrance system (default 2.0 for equipment)
|
||||
var can_have_multiple_of: bool = false
|
||||
var weight: float = 2.0 # Item weight for encumbrance system (default 2.0 for equipment)
|
||||
|
||||
func save():
|
||||
var json = {
|
||||
|
||||
@@ -73,8 +73,10 @@ var bow_charge_percentage: float = 1.0 # 0.5, 0.75, or 1.0 based on charge time
|
||||
var is_charging_spell: bool = false # True when holding grab with spellbook
|
||||
var spell_charge_start_time: float = 0.0
|
||||
var spell_charge_duration: float = 1.0 # Time to fully charge spell (1 second)
|
||||
var use_spell_charge_particles: bool = false # If true, use red_star particles; if false, use AnimationIncantation
|
||||
var spell_charge_particles: Node2D = null # Particle system for charging
|
||||
var spell_charge_particle_timer: float = 0.0 # Timer for spawning particles
|
||||
var spell_incantation_fire_ready_shown: bool = false # Track when we've switched to fire_ready
|
||||
var spell_charge_tint: Color = Color(2.0, 0.2, 0.2, 2.0) # Tint color when fully charged
|
||||
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
|
||||
@@ -92,6 +94,10 @@ var burn_damage_timer: float = 0.0 # Timer for burn damage ticks
|
||||
var movement_lock_timer: float = 0.0 # Lock movement when bow is released
|
||||
var direction_lock_timer: float = 0.0 # Lock facing direction when attacking
|
||||
var locked_facing_direction: Vector2 = Vector2.ZERO # Locked facing direction during attack
|
||||
var damage_direction_lock_timer: float = 0.0 # Lock facing when taking damage (enemies/players)
|
||||
var damage_direction_lock_duration: float = 0.3 # Duration to block direction change after damage
|
||||
var empty_bow_shot_attempts: int = 0
|
||||
var _arrow_spawn_seq: int = 0 # Per-player sequence for stable arrow names (multiplayer sync)
|
||||
var sword_slash_scene = preload("res://scenes/sword_slash.tscn") # Old rotating version (kept for reference)
|
||||
var sword_projectile_scene = preload("res://scenes/sword_projectile.tscn") # New projectile version
|
||||
var staff_projectile_scene = preload("res://scenes/attack_staff.tscn") # Staff magic ball projectile
|
||||
@@ -1493,6 +1499,10 @@ func _get_direction_from_vector(vec: Vector2) -> int:
|
||||
|
||||
# Update facing direction from mouse position (called by GameWorld)
|
||||
func _update_facing_from_mouse(mouse_direction: Vector2):
|
||||
# Don't update facing when dead
|
||||
if is_dead:
|
||||
return
|
||||
|
||||
# Only update if using keyboard input (not gamepad)
|
||||
if input_device != -1:
|
||||
return
|
||||
@@ -1505,6 +1515,10 @@ func _update_facing_from_mouse(mouse_direction: Vector2):
|
||||
if direction_lock_timer > 0.0:
|
||||
return
|
||||
|
||||
# Don't update if direction is locked (taking damage from enemies/players)
|
||||
if damage_direction_lock_timer > 0.0:
|
||||
return
|
||||
|
||||
# Mark that mouse control is active (prevents movement keys from overriding attack direction)
|
||||
mouse_control_active = true
|
||||
|
||||
@@ -1739,6 +1753,12 @@ func _physics_process(delta):
|
||||
if direction_lock_timer <= 0.0:
|
||||
direction_lock_timer = 0.0
|
||||
|
||||
# Update damage direction lock timer (block facing change when taking damage)
|
||||
if damage_direction_lock_timer > 0.0:
|
||||
damage_direction_lock_timer -= delta
|
||||
if damage_direction_lock_timer <= 0.0:
|
||||
damage_direction_lock_timer = 0.0
|
||||
|
||||
# Update spell charging
|
||||
if is_charging_spell:
|
||||
var charge_time = (Time.get_ticks_msec() / 1000.0) - spell_charge_start_time
|
||||
@@ -1747,6 +1767,7 @@ func _physics_process(delta):
|
||||
# Update particles (spawn and animate)
|
||||
spell_charge_particle_timer += delta
|
||||
_update_spell_charge_particles(charge_progress)
|
||||
_update_spell_charge_incantation(charge_progress)
|
||||
|
||||
# Update tint pulse timer when fully charged
|
||||
if charge_progress >= 1.0:
|
||||
@@ -2292,6 +2313,7 @@ func _handle_interactions():
|
||||
spell_charge_start_time = Time.get_ticks_msec() / 1000.0
|
||||
spell_incantation_played = false # Reset flag when starting new charge
|
||||
_start_spell_charge_particles()
|
||||
_start_spell_charge_incantation()
|
||||
|
||||
# Play spell charging sound (incantation plays when fully charged)
|
||||
if has_node("SfxSpellCharge"):
|
||||
@@ -2314,6 +2336,7 @@ func _handle_interactions():
|
||||
is_charging_spell = false
|
||||
spell_incantation_played = false
|
||||
_stop_spell_charge_particles()
|
||||
_stop_spell_charge_incantation()
|
||||
_clear_spell_charge_tint()
|
||||
|
||||
# Return to IDLE animation
|
||||
@@ -2351,6 +2374,7 @@ func _handle_interactions():
|
||||
is_charging_spell = false
|
||||
spell_incantation_played = false
|
||||
_stop_spell_charge_particles()
|
||||
_stop_spell_charge_incantation()
|
||||
_clear_spell_charge_tint() # This will restore original tints
|
||||
|
||||
# Stop spell charging sound, but let incantation play to completion
|
||||
@@ -2369,6 +2393,7 @@ func _handle_interactions():
|
||||
is_charging_spell = false
|
||||
spell_incantation_played = false
|
||||
_stop_spell_charge_particles()
|
||||
_stop_spell_charge_incantation()
|
||||
_clear_spell_charge_tint() # This will restore original tints
|
||||
|
||||
# Return to IDLE animation
|
||||
@@ -2393,6 +2418,7 @@ func _handle_interactions():
|
||||
is_charging_spell = false
|
||||
spell_incantation_played = false
|
||||
_stop_spell_charge_particles()
|
||||
_stop_spell_charge_incantation()
|
||||
_clear_spell_charge_tint()
|
||||
|
||||
# Return to IDLE animation
|
||||
@@ -2788,6 +2814,12 @@ func _try_grab():
|
||||
closest_body = body
|
||||
|
||||
if closest_body:
|
||||
# Placed bomb (attack_bomb with fuse): collect to inventory, don't lift
|
||||
if "is_fused" in closest_body and "can_be_collected" in closest_body and "player_owner" in closest_body:
|
||||
if closest_body.has_method("can_be_grabbed") and closest_body.can_be_grabbed() and closest_body.has_method("on_grabbed"):
|
||||
closest_body.on_grabbed(self)
|
||||
return
|
||||
|
||||
held_object = closest_body
|
||||
# Store the initial positions - don't change the grabbed object's position yet!
|
||||
initial_grab_position = closest_body.global_position
|
||||
@@ -3363,9 +3395,31 @@ func _place_down_object():
|
||||
if not held_object:
|
||||
return
|
||||
|
||||
# Place object in front of player based on facing direction (mouse or movement)
|
||||
var place_pos = _find_closest_place_pos(facing_direction_vector, held_object)
|
||||
var placed_obj = held_object
|
||||
var place_pos = _find_closest_place_pos(facing_direction_vector, placed_obj)
|
||||
|
||||
# Dwarf dropping bomb: place attack_bomb with fuse lit (explodes if not picked up in time)
|
||||
if "object_type" in placed_obj and placed_obj.object_type == "Bomb":
|
||||
if not _can_place_down_at(place_pos, placed_obj):
|
||||
return
|
||||
var bomb_name = placed_obj.name
|
||||
held_object = null
|
||||
grab_offset = Vector2.ZERO
|
||||
is_lifting = false
|
||||
is_pushing = false
|
||||
placed_obj.queue_free()
|
||||
if not attack_bomb_scene:
|
||||
return
|
||||
var bomb = attack_bomb_scene.instantiate()
|
||||
get_parent().add_child(bomb)
|
||||
bomb.global_position = place_pos
|
||||
bomb.setup(place_pos, self, Vector2.ZERO, false) # not thrown, fuse starts in _deferred_ready
|
||||
if multiplayer.has_multiplayer_peer():
|
||||
bomb.set_multiplayer_authority(get_multiplayer_authority())
|
||||
if multiplayer.has_multiplayer_peer() and is_multiplayer_authority() and can_send_rpcs and is_inside_tree():
|
||||
_rpc_to_ready_peers("_sync_bomb_dropped", [bomb_name, place_pos])
|
||||
print(name, " dropped bomb at ", place_pos, " (fuse lit)")
|
||||
return
|
||||
|
||||
print("DEBUG: Attempting to place ", placed_obj.name if placed_obj else "null", " at ", place_pos, " (player at ", global_position, ", distance: ", global_position.distance_to(place_pos), ")")
|
||||
|
||||
@@ -3484,6 +3538,10 @@ func _perform_attack():
|
||||
# Round to 1 decimal place
|
||||
final_damage = round(final_damage * 10.0) / 10.0
|
||||
|
||||
# Track what we spawned so we only sync when we actually shot a projectile
|
||||
var spawned_projectile_type: String = ""
|
||||
var sync_arrow_name: String = "" # Stable name for arrow sync (multiplayer)
|
||||
|
||||
# Handle bow attacks - require arrows in off-hand
|
||||
if is_bow:
|
||||
# Check for arrows in off-hand
|
||||
@@ -3493,12 +3551,19 @@ func _perform_attack():
|
||||
if offhand_item and offhand_item.weapon_type == Item.WeaponType.AMMUNITION:
|
||||
arrows = offhand_item
|
||||
|
||||
# Reset empty-bow counter when we have arrows
|
||||
if arrows and arrows.quantity > 0:
|
||||
empty_bow_shot_attempts = 0
|
||||
|
||||
# Only spawn arrow if we have arrows
|
||||
if arrows and arrows.quantity > 0:
|
||||
if attack_arrow_scene:
|
||||
|
||||
spawned_projectile_type = "arrow"
|
||||
$SfxBowShoot.play()
|
||||
var arrow_projectile = attack_arrow_scene.instantiate()
|
||||
sync_arrow_name = "arrow_%d_%d" % [multiplayer.get_unique_id(), _arrow_spawn_seq]
|
||||
_arrow_spawn_seq += 1
|
||||
arrow_projectile.name = sync_arrow_name
|
||||
get_parent().add_child(arrow_projectile)
|
||||
# Spawn arrow 4 pixels in the direction player is looking
|
||||
var arrow_spawn_pos = global_position + (attack_direction * 4.0)
|
||||
@@ -3521,14 +3586,20 @@ func _perform_attack():
|
||||
character_stats.character_changed.emit(character_stats)
|
||||
print(name, " shot arrow! Arrows remaining: ", remaining)
|
||||
else:
|
||||
# Play bow animation but no projectile
|
||||
# Play sound for trying to shoot without arrows
|
||||
# Play bow animation but no projectile - DO NOT sync attack (no arrow spawned)
|
||||
if has_node("SfxBowWithoutArrow"):
|
||||
$SfxBowWithoutArrow.play()
|
||||
print(name, " tried to shoot but has no arrows!")
|
||||
|
||||
# Track empty bow attempts; after 3, unequip bow and equip another weapon
|
||||
empty_bow_shot_attempts += 1
|
||||
if empty_bow_shot_attempts >= 3:
|
||||
empty_bow_shot_attempts = 0
|
||||
_unequip_bow_and_equip_other_weapon()
|
||||
elif is_staff:
|
||||
# Spawn staff projectile for staff weapons
|
||||
if staff_projectile_scene and equipped_weapon:
|
||||
spawned_projectile_type = "staff"
|
||||
var projectile = staff_projectile_scene.instantiate()
|
||||
get_parent().add_child(projectile)
|
||||
projectile.setup(attack_direction, self, final_damage, equipped_weapon)
|
||||
@@ -3542,6 +3613,7 @@ func _perform_attack():
|
||||
else:
|
||||
# Spawn sword projectile for non-bow/staff weapons
|
||||
if sword_projectile_scene:
|
||||
spawned_projectile_type = "sword"
|
||||
var projectile = sword_projectile_scene.instantiate()
|
||||
get_parent().add_child(projectile)
|
||||
projectile.setup(attack_direction, self, final_damage)
|
||||
@@ -3553,9 +3625,10 @@ func _perform_attack():
|
||||
projectile.global_position = global_position + spawn_offset
|
||||
print(name, " attacked with sword projectile! Damage: ", final_damage, " (base: ", base_damage, ", crit: ", is_crit, ")")
|
||||
|
||||
# Sync attack over network
|
||||
if multiplayer.has_multiplayer_peer() and is_multiplayer_authority() and can_send_rpcs and is_inside_tree():
|
||||
_rpc_to_ready_peers("_sync_attack", [current_direction, attack_direction, bow_charge_percentage])
|
||||
# Sync attack over network only when we actually spawned a projectile
|
||||
if spawned_projectile_type != "" and multiplayer.has_multiplayer_peer() and is_multiplayer_authority() and can_send_rpcs and is_inside_tree():
|
||||
var arrow_name_arg = sync_arrow_name if spawned_projectile_type == "arrow" else ""
|
||||
_rpc_to_ready_peers("_sync_attack", [current_direction, attack_direction, bow_charge_percentage, spawned_projectile_type, arrow_name_arg])
|
||||
|
||||
# Reset attack cooldown (instant if cooldown is 0)
|
||||
if attack_cooldown > 0:
|
||||
@@ -3564,6 +3637,64 @@ func _perform_attack():
|
||||
can_attack = true
|
||||
is_attacking = false
|
||||
|
||||
func _unequip_bow_and_equip_other_weapon():
|
||||
# After 3 empty bow shots: unequip bow, equip another mainhand weapon if any, show messages
|
||||
if not is_multiplayer_authority() or not character_stats:
|
||||
return
|
||||
|
||||
var mainhand = character_stats.equipment.get("mainhand", null)
|
||||
if not mainhand or mainhand.weapon_type != Item.WeaponType.BOW:
|
||||
return
|
||||
|
||||
# Unequip bow (moves it to inventory)
|
||||
character_stats.unequip_item(mainhand, true)
|
||||
|
||||
# Show "Bow unequipped" message
|
||||
_show_equipment_message("Bow unequipped.")
|
||||
|
||||
# Find first mainhand weapon in inventory that is not a bow
|
||||
var other_weapon = null
|
||||
for i in range(character_stats.inventory.size()):
|
||||
var it = character_stats.inventory[i]
|
||||
if not it:
|
||||
continue
|
||||
if it.equipment_type != Item.EquipmentType.MAINHAND:
|
||||
continue
|
||||
if it.weapon_type == Item.WeaponType.BOW:
|
||||
continue
|
||||
other_weapon = it
|
||||
break
|
||||
|
||||
if other_weapon:
|
||||
character_stats.equip_item(other_weapon, -1)
|
||||
_show_equipment_message("%s equipped." % other_weapon.item_name)
|
||||
|
||||
# Sync equipment/inventory to other clients (same as _on_character_changed)
|
||||
if multiplayer.has_multiplayer_peer() and is_multiplayer_authority() and can_send_rpcs and is_inside_tree():
|
||||
var equipment_data = {}
|
||||
for slot_name in character_stats.equipment.keys():
|
||||
var item = character_stats.equipment[slot_name]
|
||||
if item:
|
||||
equipment_data[slot_name] = item.save()
|
||||
else:
|
||||
equipment_data[slot_name] = null
|
||||
_rpc_to_ready_peers("_sync_equipment", [equipment_data])
|
||||
var inventory_data = []
|
||||
for item in character_stats.inventory:
|
||||
if item:
|
||||
inventory_data.append(item.save())
|
||||
_rpc_to_ready_peers("_sync_inventory", [inventory_data])
|
||||
|
||||
_apply_appearance_to_sprites()
|
||||
var other_name = other_weapon.item_name if other_weapon else "none"
|
||||
print(name, " unequipped bow (no arrows x3); other weapon: ", other_name)
|
||||
|
||||
func _show_equipment_message(text: String):
|
||||
# Local-only so the player who unequipped always sees it (host or client)
|
||||
var chat_ui = get_tree().get_first_node_in_group("chat_ui")
|
||||
if chat_ui and chat_ui.has_method("add_local_message"):
|
||||
chat_ui.add_local_message("System", text)
|
||||
|
||||
func _create_bomb_object():
|
||||
# Dwarf: Create interactable bomb object that can be lifted/thrown
|
||||
if not is_multiplayer_authority():
|
||||
@@ -3629,9 +3760,13 @@ func _create_bomb_object():
|
||||
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
|
||||
push_axis = _snap_to_8_directions(facing_direction_vector) if facing_direction_vector.length() > 0.1 else Vector2.DOWN
|
||||
if "is_being_held" in bomb_obj:
|
||||
bomb_obj.is_being_held = true
|
||||
if "held_by_player" in bomb_obj:
|
||||
bomb_obj.held_by_player = self
|
||||
|
||||
# Freeze the bomb
|
||||
if "is_frozen" in bomb_obj:
|
||||
@@ -3644,20 +3779,15 @@ func _create_bomb_object():
|
||||
# Play lift animation
|
||||
_set_animation("LIFT")
|
||||
|
||||
# Sync to network
|
||||
# Sync bomb spawn to other clients so they see it when lifted, then sync grab state
|
||||
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)
|
||||
var obj_name = bomb_obj.name
|
||||
if obj_name != "" and is_instance_valid(bomb_obj) and bomb_obj.is_inside_tree():
|
||||
_rpc_to_ready_peers("_sync_create_bomb_object", [obj_name, bomb_obj.global_position])
|
||||
_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
|
||||
@@ -3787,10 +3917,9 @@ func _can_cast_spell_at(target_position: Vector2) -> bool:
|
||||
if tile_x < 0 or tile_y < 0 or tile_x >= map_size.x or tile_y >= map_size.y:
|
||||
return false
|
||||
|
||||
# Check if it's a floor tile (grid value 1) or corridor (grid value 3)
|
||||
# Allow casting on both floor and corridor tiles
|
||||
# Check if it's floor (1), door (2), or corridor (3) - same as walkable
|
||||
var grid_value = grid[tile_x][tile_y]
|
||||
if grid_value != 1 and grid_value != 3:
|
||||
if grid_value != 1 and grid_value != 2 and grid_value != 3:
|
||||
return false
|
||||
|
||||
# Check if there's a wall between player and target using raycast
|
||||
@@ -3811,7 +3940,9 @@ func _can_cast_spell_at(target_position: Vector2) -> bool:
|
||||
return true
|
||||
|
||||
func _start_spell_charge_particles():
|
||||
# Create particle system for spell charging
|
||||
# Create particle system for spell charging (only if enabled)
|
||||
if not use_spell_charge_particles:
|
||||
return
|
||||
if spell_charge_particles:
|
||||
_stop_spell_charge_particles()
|
||||
|
||||
@@ -3821,8 +3952,8 @@ func _start_spell_charge_particles():
|
||||
spell_charge_particle_timer = 0.0
|
||||
|
||||
func _update_spell_charge_particles(charge_progress: float):
|
||||
# Update particle system based on charge progress
|
||||
if not spell_charge_particles or not is_instance_valid(spell_charge_particles):
|
||||
# Update particle system based on charge progress (skip if disabled)
|
||||
if not use_spell_charge_particles or not spell_charge_particles or not is_instance_valid(spell_charge_particles):
|
||||
return
|
||||
|
||||
var star_texture = load("res://assets/gfx/fx/magic/red_star.png")
|
||||
@@ -3892,6 +4023,26 @@ func _stop_spell_charge_particles():
|
||||
spell_charge_particles.queue_free()
|
||||
spell_charge_particles = null
|
||||
|
||||
func _start_spell_charge_incantation():
|
||||
# Play fire_charging on AnimationIncantation when starting spell charge
|
||||
spell_incantation_fire_ready_shown = false
|
||||
if has_node("AnimationIncantation"):
|
||||
$AnimationIncantation.play("fire_charging")
|
||||
|
||||
func _update_spell_charge_incantation(charge_progress: float):
|
||||
# Switch to fire_ready when fully charged (fire_charging already playing from start)
|
||||
if not has_node("AnimationIncantation"):
|
||||
return
|
||||
if charge_progress >= 1.0 and not spell_incantation_fire_ready_shown:
|
||||
$AnimationIncantation.play("fire_ready")
|
||||
spell_incantation_fire_ready_shown = true
|
||||
|
||||
func _stop_spell_charge_incantation():
|
||||
# Reset incantation when spell charge ends
|
||||
spell_incantation_fire_ready_shown = false
|
||||
if has_node("AnimationIncantation"):
|
||||
$AnimationIncantation.play("idle")
|
||||
|
||||
func _apply_spell_charge_tint():
|
||||
# Apply pulsing tint to all sprite layers when fully charged using shader parameters
|
||||
# Pulse between original tint and spell charge tint
|
||||
@@ -4129,6 +4280,7 @@ func _sync_spell_charge_start():
|
||||
is_charging_spell = true
|
||||
spell_charge_start_time = Time.get_ticks_msec() / 1000.0
|
||||
_start_spell_charge_particles()
|
||||
_start_spell_charge_incantation()
|
||||
print(name, " (synced) started charging spell")
|
||||
|
||||
@rpc("any_peer", "reliable")
|
||||
@@ -4138,6 +4290,7 @@ func _sync_spell_charge_end():
|
||||
is_charging_spell = false
|
||||
spell_incantation_played = false
|
||||
_stop_spell_charge_particles()
|
||||
_stop_spell_charge_incantation()
|
||||
_clear_spell_charge_tint()
|
||||
|
||||
# Return to IDLE animation
|
||||
@@ -4486,76 +4639,51 @@ func _sync_position(pos: Vector2, vel: Vector2, z_pos: float = 0.0, airborne: bo
|
||||
shadow.modulate.a = 0.5 - (position_z / 100.0) * 0.3
|
||||
|
||||
@rpc("any_peer", "reliable")
|
||||
func _sync_attack(direction: int, attack_dir: Vector2, charge_percentage: float = 1.0):
|
||||
# Sync attack to other clients
|
||||
# Check if node still exists and is valid before processing
|
||||
func _sync_attack(direction: int, attack_dir: Vector2, charge_percentage: float = 1.0, projectile_type: String = "sword", arrow_name: String = ""):
|
||||
# Sync attack to other clients. Use projectile_type from authority (what was actually spawned),
|
||||
# not equipment - fixes no-arrows and post-unequip bow desync.
|
||||
if not is_inside_tree() or not is_instance_valid(self):
|
||||
return
|
||||
|
||||
if not is_multiplayer_authority():
|
||||
current_direction = direction as Direction
|
||||
|
||||
# Determine weapon type for animation and projectile
|
||||
var equipped_weapon = null
|
||||
var is_staff = false
|
||||
var is_bow = false
|
||||
if character_stats and character_stats.equipment.has("mainhand"):
|
||||
equipped_weapon = character_stats.equipment["mainhand"]
|
||||
if equipped_weapon:
|
||||
if equipped_weapon.weapon_type == Item.WeaponType.STAFF:
|
||||
is_staff = true
|
||||
elif equipped_weapon.weapon_type == Item.WeaponType.BOW:
|
||||
is_bow = true
|
||||
|
||||
# Set appropriate animation
|
||||
if is_staff:
|
||||
_set_animation("STAFF")
|
||||
elif is_bow:
|
||||
_set_animation("BOW")
|
||||
else:
|
||||
_set_animation("SWORD")
|
||||
# Set animation from projectile_type (authority knows what they shot)
|
||||
match projectile_type:
|
||||
"staff":
|
||||
_set_animation("STAFF")
|
||||
"arrow":
|
||||
_set_animation("BOW")
|
||||
_:
|
||||
_set_animation("SWORD")
|
||||
|
||||
# Delay before spawning projectile
|
||||
await get_tree().create_timer(0.15).timeout
|
||||
|
||||
# Check again after delay - node might have been destroyed
|
||||
if not is_inside_tree() or not is_instance_valid(self):
|
||||
return
|
||||
|
||||
# Spawn appropriate projectile on client
|
||||
if is_staff and staff_projectile_scene and equipped_weapon:
|
||||
# Spawn only what authority actually spawned (ignore equipment)
|
||||
if projectile_type == "staff" and staff_projectile_scene:
|
||||
var equipped_weapon = character_stats.equipment.get("mainhand", null) if character_stats else null
|
||||
var projectile = staff_projectile_scene.instantiate()
|
||||
get_parent().add_child(projectile)
|
||||
projectile.setup(attack_dir, self, 20.0, equipped_weapon)
|
||||
# Spawn projectile a bit in front of the player
|
||||
var spawn_offset = attack_dir * 10.0 # 10 pixels in front
|
||||
projectile.setup(attack_dir, self, 20.0, equipped_weapon if equipped_weapon else null)
|
||||
var spawn_offset = attack_dir * 10.0
|
||||
projectile.global_position = global_position + spawn_offset
|
||||
print(name, " performed synced staff attack!")
|
||||
elif is_bow:
|
||||
# For bow attacks, check if we have arrows (same logic as host)
|
||||
var arrows = null
|
||||
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.AMMUNITION:
|
||||
arrows = offhand_item
|
||||
|
||||
# Only spawn arrow if we have arrows (matches host behavior)
|
||||
if arrows and arrows.quantity > 0:
|
||||
if attack_arrow_scene:
|
||||
var arrow_projectile = attack_arrow_scene.instantiate()
|
||||
get_parent().add_child(arrow_projectile)
|
||||
# Use charge percentage from sync (matches local player's arrow)
|
||||
arrow_projectile.shoot(attack_dir, global_position, self, charge_percentage)
|
||||
print(name, " performed synced bow attack with arrow (charge: ", charge_percentage * 100, "%)")
|
||||
else:
|
||||
# No arrows - just play animation, no projectile (matches host behavior)
|
||||
print(name, " performed synced bow attack without arrows (no projectile)")
|
||||
elif sword_projectile_scene:
|
||||
elif projectile_type == "arrow" and attack_arrow_scene:
|
||||
var arrow_projectile = attack_arrow_scene.instantiate()
|
||||
if arrow_name != "":
|
||||
arrow_projectile.name = arrow_name
|
||||
get_parent().add_child(arrow_projectile)
|
||||
arrow_projectile.shoot(attack_dir, global_position, self, charge_percentage)
|
||||
print(name, " performed synced bow attack with arrow (charge: ", charge_percentage * 100, "%)")
|
||||
elif (projectile_type == "sword" or projectile_type == "") and sword_projectile_scene:
|
||||
var projectile = sword_projectile_scene.instantiate()
|
||||
get_parent().add_child(projectile)
|
||||
projectile.setup(attack_dir, self)
|
||||
# Spawn projectile a bit in front of the player
|
||||
var spawn_offset = attack_dir * 10.0 # 10 pixels in front
|
||||
var spawn_offset = attack_dir * 10.0
|
||||
projectile.global_position = global_position + spawn_offset
|
||||
print(name, " performed synced attack!")
|
||||
|
||||
@@ -4575,9 +4703,54 @@ func _sync_bow_charge_end():
|
||||
_clear_bow_charge_tint()
|
||||
print(name, " (synced) ended charging bow")
|
||||
|
||||
@rpc("any_peer", "reliable")
|
||||
func _sync_create_bomb_object(bomb_name: String, spawn_pos: Vector2):
|
||||
# Sync Dwarf's lifted bomb spawn to other clients so they see it when held
|
||||
if is_multiplayer_authority():
|
||||
return
|
||||
var game_world = get_tree().get_first_node_in_group("game_world")
|
||||
if not game_world:
|
||||
return
|
||||
var entities_node = game_world.get_node_or_null("Entities")
|
||||
if not entities_node:
|
||||
return
|
||||
if entities_node.get_node_or_null(bomb_name):
|
||||
return # Already exists (e.g. duplicate RPC)
|
||||
var interactable_scene = load("res://scenes/interactable_object.tscn") as PackedScene
|
||||
if not interactable_scene:
|
||||
return
|
||||
var bomb_obj = interactable_scene.instantiate()
|
||||
bomb_obj.name = bomb_name
|
||||
bomb_obj.global_position = spawn_pos
|
||||
if multiplayer.has_multiplayer_peer():
|
||||
bomb_obj.set_multiplayer_authority(get_multiplayer_authority())
|
||||
entities_node.add_child(bomb_obj)
|
||||
bomb_obj.setup_bomb()
|
||||
print(name, " (synced) created bomb object ", bomb_name, " at ", spawn_pos)
|
||||
|
||||
@rpc("any_peer", "reliable")
|
||||
func _sync_bomb_dropped(bomb_name: String, place_pos: Vector2):
|
||||
# Sync Dwarf drop: free lifted bomb on clients, spawn attack_bomb with fuse lit
|
||||
if not is_multiplayer_authority():
|
||||
var game_world = get_tree().get_first_node_in_group("game_world")
|
||||
var entities_node = game_world.get_node_or_null("Entities") if game_world else null
|
||||
if entities_node and bomb_name.begins_with("BombObject_"):
|
||||
var lifted = entities_node.get_node_or_null(bomb_name)
|
||||
if lifted and is_instance_valid(lifted):
|
||||
lifted.queue_free()
|
||||
if not attack_bomb_scene:
|
||||
return
|
||||
var bomb = attack_bomb_scene.instantiate()
|
||||
get_parent().add_child(bomb)
|
||||
bomb.global_position = place_pos
|
||||
bomb.setup(place_pos, self, Vector2.ZERO, false) # not thrown, fuse lit
|
||||
if multiplayer.has_multiplayer_peer():
|
||||
bomb.set_multiplayer_authority(get_multiplayer_authority())
|
||||
print(name, " (synced) dropped bomb at ", place_pos)
|
||||
|
||||
@rpc("any_peer", "reliable")
|
||||
func _sync_place_bomb(target_pos: Vector2):
|
||||
# Sync bomb placement to other clients
|
||||
# Sync bomb placement to other clients (Human/Elf)
|
||||
if not is_multiplayer_authority():
|
||||
if not attack_bomb_scene:
|
||||
return
|
||||
@@ -4593,24 +4766,23 @@ func _sync_place_bomb(target_pos: Vector2):
|
||||
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
|
||||
func _sync_throw_bomb(bomb_name: String, bomb_pos: Vector2, throw_force: Vector2):
|
||||
# Sync bomb throw to other clients; free lifted bomb (BombObject_*) if it exists
|
||||
if not is_multiplayer_authority():
|
||||
var game_world = get_tree().get_first_node_in_group("game_world")
|
||||
var entities_node = game_world.get_node_or_null("Entities") if game_world else null
|
||||
if entities_node and bomb_name.begins_with("BombObject_"):
|
||||
var lifted = entities_node.get_node_or_null(bomb_name)
|
||||
if lifted and is_instance_valid(lifted):
|
||||
lifted.queue_free()
|
||||
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")
|
||||
@@ -5153,6 +5325,7 @@ func take_damage(amount: float, attacker_position: Vector2, is_burn_damage: bool
|
||||
is_charging_spell = false
|
||||
spell_incantation_played = false
|
||||
_stop_spell_charge_particles()
|
||||
_stop_spell_charge_incantation()
|
||||
_clear_spell_charge_tint()
|
||||
|
||||
# Return to IDLE animation
|
||||
@@ -5223,6 +5396,9 @@ func take_damage(amount: float, attacker_position: Vector2, is_burn_damage: bool
|
||||
# Play damage animation
|
||||
_set_animation("DAMAGE")
|
||||
|
||||
# Lock facing direction briefly so player can't change it while taking damage
|
||||
damage_direction_lock_timer = damage_direction_lock_duration
|
||||
|
||||
# Only apply knockback if not burn damage
|
||||
if not is_burn_damage:
|
||||
# Calculate direction FROM attacker TO victim
|
||||
@@ -5280,6 +5456,7 @@ func _die():
|
||||
is_dead = true # Ensure flag is set
|
||||
velocity = Vector2.ZERO
|
||||
is_knocked_back = false
|
||||
damage_direction_lock_timer = 0.0
|
||||
|
||||
# CRITICAL: Release any held object/player BEFORE dying to restore their collision layers
|
||||
if held_object:
|
||||
@@ -5417,6 +5594,7 @@ func _respawn():
|
||||
velocity = Vector2.ZERO
|
||||
is_knocked_back = false
|
||||
is_airborne = false
|
||||
damage_direction_lock_timer = 0.0
|
||||
position_z = 0.0
|
||||
velocity_z = 0.0
|
||||
|
||||
|
||||
Reference in New Issue
Block a user