added some amazing changes

This commit is contained in:
2026-01-25 21:31:33 +01:00
parent e7204b92d2
commit def35dd64b
69 changed files with 2586 additions and 113 deletions

View File

@@ -59,6 +59,7 @@ var controls_disabled: bool = false # True when player has reached exit and cont
# Being held state
var being_held_by: Node = null
var grabbed_by_enemy_hand: Node = null # Set when enemy hand grabs (snatch); locks movement until release
var struggle_time: float = 0.0
var struggle_threshold: float = 0.8 # Seconds to break free
var struggle_direction: Vector2 = Vector2.ZERO
@@ -97,7 +98,8 @@ var burn_debuff_duration: float = 5.0 # Burn lasts 5 seconds
var burn_debuff_damage_per_second: float = 1.0 # 1 HP per second
var burn_debuff_visual: Node2D = null # Visual indicator for burn debuff
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 movement_lock_timer: float = 0.0 # Lock movement when bow is released or after casting spell
const SPELL_CAST_LOCK_DURATION: float = 0.28 # Lock in place briefly after casting a spell
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)
@@ -124,9 +126,22 @@ var velocity_z: float = 0.0
var gravity_z: float = 500.0 # Gravity pulling down (scaled for 1x scale)
var is_airborne: bool = false
# Spawn fall-down: hidden at start, fall from high Z, land with DIE+concussion, then stand up
const SPAWN_FALL_INITIAL_Z: float = 700.0 # High enough to start off-screen (y_offset = -350)
const SPAWN_FALL_GRAVITY: float = 180.0 # Slower fall so descent is visible (~2.8s from 700)
const SPAWN_LANDING_BOUNCE_GRAVITY: float = 420.0 # Heavier gravity during bounce so it's snappier
const SPAWN_LANDING_BOUNCE_UP: float = 95.0 # Upward velocity on first impact for a noticeable bounce
const SPAWN_LANDING_LAND_DURATION: float = 0.85 # Time in LAND (and concussion) before switching to STAND
const SPAWN_LANDING_STAND_DURATION: float = 0.16 # STAND anim duration (40+40+40+40 ms) before allow control
var spawn_landing: bool = false
var spawn_landing_landed: bool = false
var spawn_landing_bounced: bool = false
# Components
# @onready var sprite = $Sprite2D # REMOVED: Now using layered sprites
@onready var shadow = $Shadow
@onready var cone_light = $ConeLight
@onready var point_light = $PointLight2D
@onready var collision_shape = $CollisionShape2D
@onready var grab_area = $GrabArea
@onready var interaction_indicator = $InteractionIndicator
@@ -150,7 +165,6 @@ var is_airborne: bool = false
@onready var sprite_shield = $Sprite2DShield
@onready var sprite_shield_holding = $Sprite2DShieldHolding
@onready var sprite_weapon = $Sprite2DWeapon
@onready var cone_light = $ConeLight
# Player stats (legacy - now using character_stats)
var max_health: float:
@@ -308,6 +322,24 @@ const ANIMATIONS = {
"frameDurations": [260, 260, 260, 260],
"loop": true,
"nextAnimation": null
},
"FALL": {
"frames": [19, 21],
"frameDurations": [10, 10],
"loop": true,
"nextAnimation": null
},
"LAND": {
"frames": [23],
"frameDurations": [10],
"loop": true,
"nextAnimation": null
},
"STAND": {
"frames": [23,24,22,1],
"frameDurations": [40,40,40,40],
"loop": false,
"nextAnimation": "IDLE"
}
}
@@ -321,6 +353,20 @@ func _ready():
# Add to player group for easy identification
add_to_group("player")
# Spawn fall-down: hidden at start for everyone, then show and fall from high Z
visible = false
spawn_landing = true
spawn_landing_landed = false
spawn_landing_bounced = false
position_z = SPAWN_FALL_INITIAL_Z
velocity_z = 0.0
is_airborne = true
if cone_light:
cone_light.visible = false
if point_light:
point_light.visible = false
call_deferred("_spawn_landing_show")
# Set respawn point to starting position
respawn_point = global_position
@@ -469,6 +515,11 @@ func _ready():
# Emit character_changed to trigger appearance sync for any newly connected clients
character_stats.character_changed.emit(character_stats)
func _spawn_landing_show():
if not is_instance_valid(self):
return
visible = true
func _duplicate_sprite_materials():
# Duplicate shader materials for ALL sprites that use tint parameters
# This prevents shared material state between players
@@ -1530,6 +1581,9 @@ func _update_facing_from_mouse(mouse_direction: Vector2):
# Don't update facing when dead
if is_dead:
return
# Don't update facing during spawn fall/land/stand (locked to DOWN until stand up)
if spawn_landing:
return
# Only update if using keyboard input (not gamepad)
if input_device != -1:
@@ -1702,8 +1756,11 @@ func _snap_to_8_directions(direction: Vector2) -> Vector2:
return best_dir
func _update_z_physics(delta):
# Apply gravity
velocity_z -= gravity_z * delta
# Apply gravity (slower spawn-fall; snappier bounce)
var g = gravity_z
if spawn_landing and not spawn_landing_landed:
g = SPAWN_LANDING_BOUNCE_GRAVITY if spawn_landing_bounced else SPAWN_FALL_GRAVITY
velocity_z -= g * delta
# Update Z position
position_z += velocity_z * delta
@@ -1711,15 +1768,28 @@ func _update_z_physics(delta):
# Check if landed
if position_z <= 0.0:
position_z = 0.0
velocity_z = 0.0
is_airborne = false
# Stop horizontal movement on landing (with some friction)
velocity = velocity * 0.3
# Visual effect when landing (removed - using layered sprites now)
print(name, " landed!")
# Spawn landing: first impact -> bounce + LAND; second impact -> settle, then STAND
if spawn_landing and not spawn_landing_landed and is_multiplayer_authority() and not spawn_landing_bounced:
spawn_landing_bounced = true
velocity_z = SPAWN_LANDING_BOUNCE_UP
velocity = velocity * 0.3
current_direction = Direction.RIGHT
facing_direction_vector = Vector2.RIGHT
_set_animation("LAND")
if has_node("SfxFallDownLand"):
$SfxFallDownLand.play()
if multiplayer.has_multiplayer_peer() and can_send_rpcs and is_inside_tree():
_rpc_to_ready_peers("_sync_spawn_bounced", [name])
# keep is_airborne true, continue falling after bounce
else:
velocity_z = 0.0
is_airborne = false
velocity = velocity * 0.3
if not spawn_landing:
print(name, " landed!")
elif spawn_landing and not spawn_landing_landed and is_multiplayer_authority():
spawn_landing_landed = true
_spawn_landing_on_land()
# Update visual offset based on height for all sprite layers
var y_offset = - position_z * 0.5 # Visual height is half of z for perspective
@@ -1754,6 +1824,19 @@ func _physics_process(delta):
# Reset teleport flag at start of frame
teleported_this_frame = false
# Spawn fall: DOWN during fall; RIGHT on landing (bounce + rest). FALL until first impact, then LAND.
if spawn_landing:
if not spawn_landing_bounced:
current_direction = Direction.DOWN
facing_direction_vector = Vector2.DOWN
else:
current_direction = Direction.RIGHT
facing_direction_vector = Vector2.RIGHT
if is_airborne and not spawn_landing_landed and not spawn_landing_bounced:
_set_animation("FALL")
elif is_airborne and spawn_landing_bounced:
_set_animation("LAND")
# Update animations
_update_animation(delta)
@@ -1898,15 +1981,15 @@ func _physics_process(delta):
burn_damage_timer = 0.0
_remove_burn_debuff()
# Skip input if controls are disabled (e.g., when inventory is open)
# Skip input if controls are disabled (e.g., when inventory is open) or spawn landing (fall → DIE → stand up)
# But still allow knockback to continue (handled above)
var skip_input = controls_disabled
if controls_disabled:
var skip_input = controls_disabled or spawn_landing
if controls_disabled or spawn_landing:
if not is_knocked_back:
# Immediately stop movement when controls are disabled (e.g., inventory opened)
velocity = Vector2.ZERO
# Reset animation to IDLE if not in a special state
if current_animation != "THROW" and current_animation != "DAMAGE" and current_animation != "SWORD" and current_animation != "BOW" and current_animation != "STAFF":
# Reset animation to IDLE if not in a special state (skip when spawn_landing: we use DIE until stand up)
if not spawn_landing and current_animation != "THROW" and current_animation != "DAMAGE" and current_animation != "SWORD" and current_animation != "BOW" and current_animation != "STAFF":
if is_lifting:
_set_animation("IDLE_HOLD")
elif is_pushing:
@@ -1914,7 +1997,7 @@ func _physics_process(delta):
else:
_set_animation("IDLE")
# Check if being held by someone
# Check if being held by someone (another player) or grabbed by enemy hand
var being_held_by_someone = false
for other_player in get_tree().get_nodes_in_group("player"):
if other_player != self and other_player.held_object == self:
@@ -1928,6 +2011,11 @@ func _physics_process(delta):
_update_shield_visibility()
# Handle struggle mechanic
_handle_struggle(delta)
elif grabbed_by_enemy_hand:
velocity = Vector2.ZERO
is_shielding = false
was_shielding_last_frame = false
_update_shield_visibility()
elif is_knocked_back:
is_shielding = false
was_shielding_last_frame = false
@@ -2378,6 +2466,8 @@ func _handle_interactions():
heal_target = _get_heal_target()
var has_valid_target = ((is_fire or is_frost) and target_pos != Vector2.ZERO) or (is_heal and heal_target != null)
# Healing: allow charge even without target (don't disable charge when hovering enemy/wall/etc.)
var can_start_charge = is_heal or has_valid_target
# Check if there's a grabbable object nearby - prioritize grabbing over spell casting
var nearby_grabbable = null
@@ -2399,7 +2489,7 @@ func _handle_interactions():
nearby_grabbable = body
break
if grab_just_pressed and not is_charging_spell and has_valid_target and not nearby_grabbable and not is_lifting and not held_object:
if grab_just_pressed and not is_charging_spell and can_start_charge and not nearby_grabbable and not is_lifting and not held_object:
is_charging_spell = true
current_spell_element = "healing" if is_heal else ("frost" if is_frost else "fire")
spell_charge_start_time = Time.get_ticks_msec() / 1000.0
@@ -2448,6 +2538,7 @@ func _handle_interactions():
else:
_cast_heal_spell(heal_target)
_set_animation("FINISH_SPELL")
movement_lock_timer = SPELL_CAST_LOCK_DURATION
is_charging_spell = false
current_spell_element = "fire"
spell_incantation_played = false
@@ -2474,7 +2565,8 @@ func _handle_interactions():
print(name, " released spell (", "healing" if is_heal else ("frost" if is_frost else "fire"), ", fully: ", is_fully_charged, ")")
just_grabbed_this_frame = false
return
elif is_charging_spell and (not has_valid_target or is_lifting or held_object):
elif is_charging_spell and (is_lifting or held_object or ((not is_heal) and not has_valid_target)):
# Don't cancel heal charge for no target (allow hover over enemy/wall); cancel fire/frost
is_charging_spell = false
current_spell_element = "fire"
spell_incantation_played = false
@@ -2488,7 +2580,7 @@ func _handle_interactions():
$SfxSpellIncantation.stop()
if multiplayer.has_multiplayer_peer():
_sync_spell_charge_end.rpc()
print(name, " spell charge cancelled (no target)")
print(name, " spell charge cancelled (no target / lift / held)")
# Check for trap disarm (Dwarf only)
if character_stats and character_stats.race == "Dwarf":
@@ -4068,14 +4160,31 @@ func _cast_heal_spell(target: Node):
if is_crit:
amount = floor(amount * 2.0)
var is_revive = "is_dead" in target and target.is_dead
var display_amount = int(amount)
# Undead enemies take damage from healing spell
if target.is_in_group("enemy") and "is_undead" in target and target.is_undead:
var damage_amount = float(display_amount)
var eid = target.get_multiplayer_authority()
var my_id = multiplayer.get_unique_id()
if eid == my_id:
target.take_damage(damage_amount, global_position, is_crit)
elif multiplayer.has_multiplayer_peer() and can_send_rpcs and is_inside_tree():
target.rpc_take_damage.rpc_id(eid, damage_amount, global_position, is_crit, false, false)
else:
target.rpc_take_damage.rpc(damage_amount, global_position, is_crit, false, false)
_spawn_heal_effect_and_text(target, display_amount, is_crit, false, true)
if gw and gw.has_method("_apply_heal_spell_sync"):
_rpc_to_ready_peers("_sync_heal_spell_via_gw", [target.name, damage_amount, display_amount, is_crit, false, false, false, true])
print(name, " cast heal on undead ", target.name, " for ", display_amount, " damage (crit: ", is_crit, ")")
return
var is_revive = "is_dead" in target and target.is_dead
var actual_heal = amount
var allow_overheal = false
var is_overheal = false
if is_revive:
# Tome revive: no overheal, full amount revives
actual_heal = amount
else:
var overheal_chance_pct = 1.0 + lck_val * 0.3
@@ -4099,12 +4208,12 @@ func _cast_heal_spell(target: Node):
else:
if me == tid and actual_heal > 0:
target.heal(actual_heal, allow_overheal)
_spawn_heal_effect_and_text(target, display_amount, is_crit, is_overheal)
_spawn_heal_effect_and_text(target, display_amount, is_crit, is_overheal, false)
if multiplayer.has_multiplayer_peer() and can_send_rpcs and is_inside_tree() and gw and gw.has_method("_apply_heal_spell_sync"):
_rpc_to_ready_peers("_sync_heal_spell_via_gw", [target.name, actual_heal, display_amount, is_crit, is_overheal, allow_overheal, is_revive])
_rpc_to_ready_peers("_sync_heal_spell_via_gw", [target.name, actual_heal, display_amount, is_crit, is_overheal, allow_overheal, is_revive, false])
print(name, " cast heal on ", target.name, " for ", display_amount, " HP", " (actual: ", actual_heal, ", crit: ", is_crit, ", overheal: ", is_overheal, ", revive: ", is_revive, ")")
func _spawn_heal_effect_and_text(target: Node, display_amount: int, is_crit: bool, is_overheal: bool):
func _spawn_heal_effect_and_text(target: Node, display_amount: int, is_crit: bool, is_overheal: bool, is_damage_to_enemy: bool = false):
if not target or not is_instance_valid(target):
return
var game_world = get_tree().get_first_node_in_group("game_world")
@@ -4118,6 +4227,9 @@ func _spawn_heal_effect_and_text(target: Node, display_amount: int, is_crit: boo
eff.global_position = target.global_position
if eff.has_method("setup"):
eff.setup(target)
if is_damage_to_enemy:
# Undead: enemy's take_damage already shows damage number; we only spawn effect
return
var prefix = ""
if is_crit and is_overheal:
prefix = "CRIT OVERHEAL! "
@@ -4134,12 +4246,12 @@ func _spawn_heal_effect_and_text(target: Node, display_amount: int, is_crit: boo
ft.setup(heal_text, Color.GREEN, 0.5, 0.5, null, 1, 1, 0)
@rpc("any_peer", "reliable")
func _sync_heal_spell_via_gw(target_name: String, amount_to_apply: float, display_amount: int, is_crit: bool, is_overheal: bool, allow_overheal: bool, is_revive: bool = false):
func _sync_heal_spell_via_gw(target_name: String, amount_to_apply: float, display_amount: int, is_crit: bool, is_overheal: bool, allow_overheal: bool, is_revive: bool = false, is_damage_to_enemy: bool = false):
if is_multiplayer_authority():
return
var gw = get_tree().get_first_node_in_group("game_world")
if gw and gw.has_method("_apply_heal_spell_sync"):
gw._apply_heal_spell_sync(target_name, amount_to_apply, display_amount, is_crit, is_overheal, allow_overheal, is_revive)
gw._apply_heal_spell_sync(target_name, amount_to_apply, display_amount, is_crit, is_overheal, allow_overheal, is_revive, is_damage_to_enemy)
func _is_healing_spell() -> bool:
if not character_stats or not character_stats.equipment.has("offhand"):
@@ -4169,6 +4281,15 @@ func _get_heal_target() -> Node:
if d < best_d:
best_d = d
best = p
for e in get_tree().get_nodes_in_group("enemy"):
if not is_instance_valid(e) or ("is_dead" in e and e.is_dead):
continue
if not ("is_undead" in e and e.is_undead):
continue
var d = e.global_position.distance_to(mouse_world)
if d < best_d:
best_d = d
best = e
return best
func _can_cast_spell_at(target_position: Vector2) -> bool:
@@ -5580,6 +5701,34 @@ func set_being_held(held: bool):
struggle_direction = Vector2.ZERO
being_held_by = null
@rpc("any_peer", "reliable")
func rpc_grabbed_by_enemy_hand(enemy_name: String) -> void:
var en: Node = null
var gw = get_tree().get_first_node_in_group("game_world")
if gw:
var entities = gw.get_node_or_null("Entities")
if entities:
en = entities.get_node_or_null(enemy_name)
if not en:
en = _find_node_by_name(gw, enemy_name)
grabbed_by_enemy_hand = en if en and is_instance_valid(en) else null
velocity = Vector2.ZERO
@rpc("any_peer", "reliable")
func rpc_released_from_enemy_hand() -> void:
grabbed_by_enemy_hand = null
func _find_node_by_name(node: Node, n: String) -> Node:
if not node:
return null
if node.name == n:
return node
for c in node.get_children():
var found = _find_node_by_name(c, n)
if found:
return found
return null
# RPC function called by attacker to deal damage to this player
@rpc("any_peer", "reliable")
func rpc_take_damage(amount: float, attacker_position: Vector2, is_burn_damage: bool = false, apply_burn_debuff: bool = false):
@@ -5903,6 +6052,64 @@ func _are_all_players_dead() -> bool:
return false
return true
func _spawn_landing_on_land():
if has_node("SfxFallDownLand"):
$SfxFallDownLand.play()
_set_animation("LAND")
var status_anim = get_node_or_null("Sprite2DStatus/AnimationPlayerStatus")
if status_anim and status_anim.has_animation("concussion"):
status_anim.play("concussion")
var gw = get_tree().get_first_node_in_group("game_world")
if gw and gw.has_method("add_screenshake"):
gw.add_screenshake(7.0, 0.28)
if multiplayer.has_multiplayer_peer() and can_send_rpcs and is_inside_tree():
_rpc_to_ready_peers("_sync_spawn_landed", [name])
get_tree().create_timer(SPAWN_LANDING_LAND_DURATION).timeout.connect(_spawn_landing_to_stand)
@rpc("any_peer", "reliable")
func _sync_spawn_bounced(_player_name: String):
spawn_landing_bounced = true
current_direction = Direction.RIGHT
facing_direction_vector = Vector2.RIGHT
_set_animation("LAND")
if has_node("SfxFallDownLand"):
$SfxFallDownLand.play()
@rpc("any_peer", "reliable")
func _sync_spawn_landed(_player_name: String):
# Received on remote copies when authority lands from spawn fall
spawn_landing_landed = true
position_z = 0.0
velocity_z = 0.0
is_airborne = false
if has_node("SfxFallDownLand"):
$SfxFallDownLand.play()
_set_animation("LAND")
var status_anim = get_node_or_null("Sprite2DStatus/AnimationPlayerStatus")
if status_anim and status_anim.has_animation("concussion"):
status_anim.play("concussion")
get_tree().create_timer(SPAWN_LANDING_LAND_DURATION).timeout.connect(_spawn_landing_to_stand)
func _spawn_landing_to_stand():
if not is_instance_valid(self):
return
_set_animation("STAND")
get_tree().create_timer(SPAWN_LANDING_STAND_DURATION).timeout.connect(_spawn_landing_stand_up)
func _spawn_landing_stand_up():
if not is_instance_valid(self):
return
# Clear concussion status (was showing during LAND)
var status_anim = get_node_or_null("Sprite2DStatus/AnimationPlayerStatus")
if status_anim and status_anim.has_animation("idle"):
status_anim.play("idle")
# STAND's nextAnimation -> IDLE, so we're already IDLE or about to be
spawn_landing = false
if cone_light:
cone_light.visible = true
if point_light:
point_light.visible = true
func _respawn():
print(name, " respawning!")
was_revived = false
@@ -6542,6 +6749,24 @@ func _show_revive_cost_number(amount: int):
get_tree().current_scene.add_child(damage_label)
damage_label.global_position = global_position + Vector2(0, -16)
func show_floating_status(text: String, col: Color = Color.WHITE) -> void:
"""Show a damage-number-style floating text above player (e.g. 'Encumbered!')."""
var damage_number_scene = preload("res://scenes/damage_number.tscn")
if not damage_number_scene:
return
var lbl = damage_number_scene.instantiate()
if not lbl:
return
lbl.label = text
lbl.color = col
lbl.z_index = 5
lbl.direction = Vector2(0, -1)
var game_world = get_tree().get_first_node_in_group("game_world")
var parent = game_world.get_node_or_null("Entities") if game_world else get_tree().current_scene
if parent:
parent.add_child(lbl)
lbl.global_position = global_position + Vector2(0, -20)
@rpc("any_peer", "reliable")
func _sync_revive_cost(amount: int):
if is_multiplayer_authority():
@@ -6589,12 +6814,16 @@ func _on_level_up_stats(stats_increased: Array):
var base_y_offset = -32.0 # Start above player head
var y_spacing = 12.0 # Space between each text
# Show "LEVEL UP!" first (in white)
# Show "LEVEL UP +1!" prominently (gold, larger, longer on screen)
var level_up_text = damage_number_scene.instantiate()
if level_up_text:
level_up_text.label = "LEVEL UP!"
level_up_text.color = Color.WHITE
level_up_text.direction = Vector2(0, -1) # Straight up
level_up_text.label = "LEVEL UP +1!"
level_up_text.color = Color(1.0, 0.88, 0.2) # Gold
level_up_text.direction = Vector2(0, -1)
level_up_text.rise_distance = 48.0
level_up_text.fade_delay = 1.4
level_up_text.fade_duration = 0.6
level_up_text.add_theme_font_size_override("font_size", 20)
entities_node.add_child(level_up_text)
level_up_text.global_position = global_position + Vector2(0, base_y_offset)
base_y_offset -= y_spacing