extends CharacterBody2D # ---- Movement ---- var speed: float = 300.0 var jump_velocity: float = -450.0 var gravity: float = 1200.0 # ---- Health ---- var max_health: int = 100 var health: int = 100 # ---- Attack ---- var attack_range: float = 95.0 var attack_forward_only: bool = true var attack_damage: int = 10 var attack_cooldown: float = 0.0 var attack_windup: float = 0.15 var attack_recover: float = 0.35 # ---- Block / stun ---- var is_blocking: bool = false var block_damage_multiplier: float = 0.35 var hit_stun: float = 0.0 # ---- Facing / bounds ---- var facing_dir: int = 1 var min_x: float = 40.0 var max_x: float = 1240.0 # ---- Links ---- var enemy_ref: CharacterBody2D = null func set_enemy(e: CharacterBody2D) -> void: enemy_ref = e # ---- Visuals ---- var anim: AnimatedSprite2D = null # assets folder const SPRITE_FOLDER := "res://assets/player" func _ready() -> void: await get_tree().process_frame health = max_health velocity = Vector2.ZERO # 1) atrod vai izveido AnimatedSprite2D anim = _get_or_create_anim() # 2) ja nav sprite_frames, uzbūvē no mapes if anim.sprite_frames == null or anim.sprite_frames.get_animation_names().is_empty(): anim.sprite_frames = _build_sprite_frames_from_folder(SPRITE_FOLDER) anim.visible = true anim.modulate.a = 1.0 if anim.scale == Vector2.ZERO: anim.scale = Vector2.ONE _safe_play("idle") func _physics_process(delta: float) -> void: # timers if attack_cooldown > 0.0: attack_cooldown = max(attack_cooldown - delta, 0.0) if hit_stun > 0.0: hit_stun = max(hit_stun - delta, 0.0) # blocking input is_blocking = Input.is_action_pressed("block") and hit_stun <= 0.0 and attack_cooldown <= 0.0 # gravity + jump if not is_on_floor(): velocity.y += gravity * delta else: if velocity.y > 0.0: velocity.y = 0.0 if Input.is_action_just_pressed("jump") and is_on_floor() and hit_stun <= 0.0 and not is_blocking: velocity.y = jump_velocity # movement (disabled on stun/block) var dir: int = 0 if hit_stun <= 0.0 and not is_blocking: if Input.is_action_pressed("move_left"): dir -= 1 if Input.is_action_pressed("move_right"): dir += 1 if dir != 0: facing_dir = dir velocity.x = float(dir) * speed move_and_slide() global_position.x = clamp(global_position.x, min_x, max_x) # flip if anim != null: anim.flip_h = (facing_dir == -1) # attack input if Input.is_action_just_pressed("attack") and attack_cooldown <= 0.0 and hit_stun <= 0.0 and not is_blocking: _start_attack() # animations if is_blocking: _safe_play("block") elif hit_stun > 0.0: _safe_play("hit") else: if abs(velocity.x) > 5.0: _safe_play("walk") else: _safe_play("idle") func _start_attack() -> void: attack_cooldown = attack_windup + attack_recover _safe_play("attack") var t := get_tree().create_timer(attack_windup) t.timeout.connect(func(): _try_apply_damage()) func _try_apply_damage() -> void: if enemy_ref == null: return if hit_stun > 0.0 or is_blocking: return var dx: float = enemy_ref.global_position.x - global_position.x var dist: float = abs(dx) if dist > attack_range: return if attack_forward_only: if facing_dir == 1 and dx < 0.0: return if facing_dir == -1 and dx > 0.0: return if enemy_ref.has_method("take_damage"): enemy_ref.call("take_damage", attack_damage, global_position.x) func take_damage(amount: int, _attacker_x: float = 0.0) -> void: if health <= 0: return var final_amount := amount if is_blocking: final_amount = int(round(float(amount) * block_damage_multiplier)) health = max(health - final_amount, 0) if not is_blocking: hit_stun = 0.20 _safe_play("hit") if health <= 0: var parent_node := get_parent() if parent_node != null and parent_node.has_method("end_fight"): parent_node.call("end_fight", false) # ---------------- Sprite helpers ---------------- func _get_or_create_anim() -> AnimatedSprite2D: var direct := get_node_or_null("AnimatedSprite2D") if direct != null and direct is AnimatedSprite2D: return direct as AnimatedSprite2D var found := _find_first_animated_sprite(self) if found != null: return found var new_anim := AnimatedSprite2D.new() new_anim.name = "AnimatedSprite2D" add_child(new_anim) new_anim.position = Vector2(0, -40) new_anim.scale = Vector2(0.25, 0.25) return new_anim func _find_first_animated_sprite(root: Node) -> AnimatedSprite2D: if root is AnimatedSprite2D: return root for c in root.get_children(): var res := _find_first_animated_sprite(c) if res != null: return res return null func _safe_play(anim_name: String) -> void: if anim == null or anim.sprite_frames == null: return if not anim.sprite_frames.has_animation(anim_name): if anim_name != "idle" and anim.sprite_frames.has_animation("idle"): if anim.animation != "idle": anim.play("idle") return if anim.animation != anim_name: anim.play(anim_name) func _build_sprite_frames_from_folder(folder: String) -> SpriteFrames: var frames := SpriteFrames.new() _add_anim_from_prefix(frames, folder, "idle", 6.0, true) _add_anim_from_prefix(frames, folder, "walk", 10.0, true) _add_anim_from_prefix(frames, folder, "attack", 12.0, false) _add_anim_from_prefix(frames, folder, "hit", 10.0, false) _add_anim_from_prefix(frames, folder, "block", 8.0, true) if frames.get_animation_names().is_empty(): push_warning("Player: No sprites found in " + folder + " (expected idle_1.png, walk_1.png etc.)") return frames func _add_anim_from_prefix(frames: SpriteFrames, folder: String, anim_name: String, fps: float, loop: bool) -> void: var textures: Array[Texture2D] = _load_textures_sorted(folder, anim_name + "_") if textures.is_empty(): return if not frames.has_animation(anim_name): frames.add_animation(anim_name) frames.set_animation_speed(anim_name, fps) frames.set_animation_loop(anim_name, loop) for t in textures: frames.add_frame(anim_name, t) func _load_textures_sorted(folder: String, prefix: String) -> Array[Texture2D]: var out: Array[Texture2D] = [] var dir := DirAccess.open(folder) if dir == null: push_warning("Player: Folder not found: " + folder) return out dir.list_dir_begin() var file := dir.get_next() while file != "": if not dir.current_is_dir(): var lower := file.to_lower() if lower.ends_with(".png") and file.begins_with(prefix): var tex := load(folder + "/" + file) as Texture2D if tex != null: out.append(tex) file = dir.get_next() dir.list_dir_end() out.sort_custom(func(a: Texture2D, b: Texture2D) -> bool: return _extract_suffix_number(a.resource_path, prefix) < _extract_suffix_number(b.resource_path, prefix) ) return out func _extract_suffix_number(path: String, prefix: String) -> int: var file := path.get_file() if not file.begins_with(prefix): return 0 var s := file.replace(prefix, "").replace(".png", "") return int(s)