fixat mer med traps och arrows och grejjer

This commit is contained in:
2026-01-22 01:03:01 +01:00
parent c0d229ee86
commit eaf86b39fa
20 changed files with 1589 additions and 194 deletions

321
src/scripts/trap.gd Normal file
View File

@@ -0,0 +1,321 @@
extends Node2D
@onready var sprite = $Sprite2D
@onready var animation_player = $AnimationPlayer
@onready var activation_area = $ActivationArea
@onready var disarm_area = $DisarmArea
@onready var detection_area = $DetectionArea
# Trap state
var is_detected: bool = false # Becomes true when any player detects it
var is_disarmed: bool = false # True if trap has been disarmed
var is_active: bool = false # True when trap is currently triggering
var has_cooldown: bool = false # Some traps can reset
var cooldown_time: float = 5.0 # Time until trap can re-activate
var cooldown_timer: float = 0.0
# Trap properties
var trap_damage: float = 15.0
var trap_type: String = "Floor_Lance"
# Per-player detection tracking (Dictionary: peer_id -> has_attempted_detection)
var player_detection_attempts: Dictionary = {}
# Disarm tracking
var disarming_player: Node = null
var disarm_progress: float = 0.0
var disarm_duration: float = 1.0
var disarm_label: Label = null
func _ready() -> void:
# Add to trap group for detection by players
add_to_group("trap")
# Randomize trap visual based on dungeon seed
var highbox_seed = 0
var world = get_tree().get_first_node_in_group("game_world")
if world and "dungeon_seed" in world:
highbox_seed = world.dungeon_seed
highbox_seed += int(global_position.x) * 1000 + int(global_position.y)
var rng = RandomNumberGenerator.new()
rng.seed = highbox_seed
var index = rng.randi() % 2
if index == 0:
sprite.texture = load("res://assets/gfx/traps/Floor_Lance.png")
trap_type = "Floor_Lance"
has_cooldown = true # Lance traps can reset
# Start hidden (invisible until detected)
sprite.modulate.a = 0.0
# Setup detection area to check for players
if detection_area:
detection_area.body_entered.connect(_on_detection_area_body_entered)
detection_area.body_exited.connect(_on_detection_area_body_exited)
# Setup disarm area to show "DISARM" text for dwarves
if disarm_area:
disarm_area.body_entered.connect(_on_disarm_area_body_entered)
disarm_area.body_exited.connect(_on_disarm_area_body_exited)
func _process(delta: float) -> void:
# Handle cooldown timer for resetting traps
if has_cooldown and not is_active and cooldown_timer > 0:
cooldown_timer -= delta
if cooldown_timer <= 0:
# Trap has cooled down - ready to trigger again
pass
# Handle disarm progress
if disarming_player and is_instance_valid(disarming_player):
# Check if player is still holding grab button
if disarming_player.is_multiplayer_authority() and Input.is_action_pressed("grab"):
# Play disarming sound (only if not already playing)
if $SfxDisarming.playing == false:
$SfxDisarming.play()
disarm_progress += delta
_update_disarm_ui()
if disarm_progress >= disarm_duration:
# Disarm complete!
_complete_disarm()
else:
# Player released grab - cancel disarm
_cancel_disarm()
func _on_detection_area_body_entered(body: Node) -> void:
# When a player enters detection range, roll perception check (once per player per game)
if not body.is_in_group("player"):
return
if is_detected or is_disarmed:
return # Already detected or disarmed
# Get player peer ID
var peer_id = body.get_multiplayer_authority()
# Check if this player has already attempted detection
if player_detection_attempts.has(peer_id):
return # Already tried once this game
# Mark that this player has attempted detection
player_detection_attempts[peer_id] = true
# Roll perception check (only on server/authority)
if multiplayer.is_server() or not multiplayer.has_multiplayer_peer():
_roll_perception_check(body)
func _on_detection_area_body_exited(_body: Node) -> void:
pass # Detection is permanent once attempted
func _roll_perception_check(player: Node) -> void:
# Roll perception check for player
if not player or not player.character_stats:
return
var per_stat = player.character_stats.baseStats.per + player.character_stats.get_pass("per")
# Perception roll: d20 + PER modifier
# Target DC: 15 (medium difficulty)
var roll = randi() % 20 + 1 # 1d20
var total = roll + int(per_stat / 2) - 5 # PER modifier: (PER - 10) / 2
var dc = 15
print(player.name, " rolls Perception: ", roll, " + ", int(per_stat / 2) - 5, " = ", total, " vs DC ", dc)
if total >= dc:
# Success! Player detects the trap
_detect_trap(player)
else:
# Failure - trap remains hidden to this player
print(player.name, " failed to detect trap")
func _detect_trap(detecting_player: Node) -> void:
# Trap is detected - make it visible to ALL players
is_detected = true
# Make trap visible
sprite.modulate.a = 1.0
# Sync detection to all clients (including server with call_local)
if multiplayer.has_multiplayer_peer() and is_inside_tree():
if multiplayer.is_server():
_sync_trap_detected.rpc()
print(detecting_player.name, " detected trap at ", global_position)
@rpc("authority", "call_local", "reliable")
func _sync_trap_detected() -> void:
# Client receives trap detection notification
is_detected = true
sprite.modulate.a = 1.0
func _on_disarm_area_body_entered(body: Node) -> void:
# Show "DISARM" text if player is Dwarf and trap is detected
if not body.is_in_group("player"):
return
if not is_detected or is_disarmed:
return
# Check if player is Dwarf
if body.character_stats and body.character_stats.race == "Dwarf":
_show_disarm_text(body)
func _on_disarm_area_body_exited(body: Node) -> void:
# Hide disarm text when player leaves area
if body == disarming_player:
_cancel_disarm()
_hide_disarm_text(body)
func _show_disarm_text(_player: Node) -> void:
# Create "DISARM" label above trap
if disarm_label:
return # Already showing
disarm_label = Label.new()
disarm_label.text = "DISARM"
disarm_label.add_theme_font_size_override("font_size", 16)
disarm_label.add_theme_color_override("font_color", Color.YELLOW)
disarm_label.add_theme_color_override("font_outline_color", Color.BLACK)
disarm_label.add_theme_constant_override("outline_size", 2)
disarm_label.position = Vector2(-25, -30)
disarm_label.z_index = 100
add_child(disarm_label)
func _hide_disarm_text(_player: Node) -> void:
if disarm_label:
disarm_label.queue_free()
disarm_label = null
func _update_disarm_ui() -> void:
# Update disarm progress (could show radial timer here)
if disarm_label:
var progress_percent = int((disarm_progress / disarm_duration) * 100)
disarm_label.text = "DISARM (" + str(progress_percent) + "%)"
func _cancel_disarm() -> void:
disarming_player = null
disarm_progress = 0.0
# Stop disarming sound
if $SfxDisarming.playing:
$SfxDisarming.stop()
if disarm_label:
disarm_label.text = "DISARM"
func _complete_disarm() -> void:
# Trap successfully disarmed!
is_disarmed = true
disarming_player = null
disarm_progress = 0.0
# Stop disarming sound
if $SfxDisarming.playing:
$SfxDisarming.stop()
# Hide disarm text
_hide_disarm_text(null)
# Disable activation area
if activation_area:
activation_area.monitoring = false
# Show "TRAP DISARMED" in chat
var chat_ui = get_tree().get_first_node_in_group("chat_ui")
if chat_ui and chat_ui.has_method("send_system_message"):
chat_ui.send_system_message("Trap disarmed by Dwarf!")
# Show floating text "TRAP DISARMED"
_show_floating_text("TRAP DISARMED", Color.YELLOW)
# Change trap visual to show it's disarmed (optional - could fade out or change color)
sprite.modulate = Color(0.5, 0.5, 0.5, 0.5)
# Sync disarm to all clients
if multiplayer.has_multiplayer_peer() and is_inside_tree():
if multiplayer.is_server():
_sync_trap_disarmed.rpc()
print("Trap disarmed!")
@rpc("authority", "call_local", "reliable")
func _sync_trap_disarmed() -> void:
# Client receives trap disarm notification
is_disarmed = true
sprite.modulate = Color(0.5, 0.5, 0.5, 0.5)
if activation_area:
activation_area.monitoring = false
func _show_floating_text(text: String, color: Color) -> void:
var floating_text_scene = preload("res://scenes/floating_text.tscn")
if floating_text_scene:
var floating_text = floating_text_scene.instantiate()
var parent = get_parent()
if parent:
parent.add_child(floating_text)
floating_text.global_position = Vector2(global_position.x, global_position.y - 20)
floating_text.setup(text, color, 0.5, 0.5, null, 1, 1, 0)
func _on_activation_area_body_shape_entered(_body_rid: RID, body: Node2D, _body_shape_index: int, _local_shape_index: int) -> void:
# Trap triggered!
if not body.is_in_group("player"):
return
if is_disarmed or is_active:
return # Can't trigger if disarmed or already active
if has_cooldown and cooldown_timer > 0:
return # Still on cooldown
# Trigger trap
is_active = true
$SfxActivate.play()
animation_player.play("activate")
# Trap is now visible to all players (once triggered)
if not is_detected:
is_detected = true
sprite.modulate.a = 1.0
if multiplayer.has_multiplayer_peer() and is_inside_tree():
if multiplayer.is_server():
_sync_trap_detected.rpc()
# Deal damage to player (with luck-based avoidance)
_deal_trap_damage(body)
# Start cooldown if applicable
if has_cooldown:
cooldown_timer = cooldown_time
await animation_player.animation_finished
animation_player.play("reset")
await animation_player.animation_finished
is_active = false
else:
# One-time trap - stays triggered
pass
func _deal_trap_damage(player: Node) -> void:
if not player or not player.character_stats:
return
# Luck-based avoidance check
var luck_stat = player.character_stats.baseStats.lck + player.character_stats.get_pass("lck")
var avoid_chance = luck_stat * 0.02 # 2% per luck point (e.g., 10 luck = 20% avoid)
var avoid_roll = randf()
if avoid_roll < avoid_chance:
$SfxAvoid.play()
# Player avoided trap damage!
print(player.name, " avoided trap damage! (", avoid_chance * 100, "% chance)")
_show_floating_text("AVOIDED", Color.GREEN)
return
# Apply trap damage (affected by player's defense)
var final_damage = player.character_stats.calculate_damage(trap_damage, false, false)
if player.has_method("rpc_take_damage"):
player.rpc_take_damage(trap_damage, global_position)
print(player.name, " took ", final_damage, " trap damage")