ai part 3

pull/28/head
Dr. Sascha Woitschetzki 2025-08-20 14:17:17 +07:00
parent 5a8b173b48
commit 1d204d1c6c
8 changed files with 223 additions and 166 deletions

@ -1,7 +1,7 @@
[gd_scene load_steps=20 format=4 uid="uid://do5dueeowthyn"] [gd_scene load_steps=20 format=4 uid="uid://do5dueeowthyn"]
[ext_resource type="Script" uid="uid://uigg23n8ddht" path="res://scripts/item_desctructable.gd" id="1_txp5f"] [ext_resource type="Script" uid="uid://did0e7iwyya4u" path="res://scripts/item_interactable.gd" id="1_txp5f"]
[ext_resource type="Texture2D" uid="uid://dgv6crdc05p61" path="res://resources/models/dungeon/Textures/dungeon_texture.png" id="2_5cqwm"] [ext_resource type="Texture2D" uid="uid://dy2dk8nax2yxg" path="res://resources/models/dungeon/dungeon_texture.png" id="2_5cqwm"]
[ext_resource type="Script" uid="uid://3rbry6yfuo6u" path="res://scripts/hurtbox.gd" id="3_2kbl0"] [ext_resource type="Script" uid="uid://3rbry6yfuo6u" path="res://scripts/hurtbox.gd" id="3_2kbl0"]
[ext_resource type="AudioStream" uid="uid://bxnu674tk5euv" path="res://resources/audio/wallhit.mp3" id="4_tc4p0"] [ext_resource type="AudioStream" uid="uid://bxnu674tk5euv" path="res://resources/audio/wallhit.mp3" id="4_tc4p0"]

File diff suppressed because one or more lines are too long

@ -96,4 +96,4 @@ func _on_state_changed(_old_state: States, new_state: States) -> void:
States.dead: States.dead:
# Belohnung vergeben, wenn der Spieler existiert # Belohnung vergeben, wenn der Spieler existiert
if player: if player:
player.gold += 100 player.gold += 100

@ -1,13 +1,25 @@
class_name NavigationManager extends NavigationRegion3D class_name NavigationManager
extends NavigationRegion3D
@export var navigation_sources_root: Node
const LOG_TAG := "NavigationManager"
@export var navigation_objects_node: Node
func _ready() -> void: func _ready() -> void:
pass if navigation_sources_root:
#var navigation_objects = navigation_objects_node.get_children() _connect_navigation_sources(navigation_sources_root)
# for navigation_object in navigation_objects:
# navigation_object.recalculate_navigation_map.connect(_recalculate_navigation_map)
func _connect_navigation_sources(root: Node) -> void:
var children := root.get_children()
for child in children:
if child.has_signal("recalculate_navigation_map"):
var callable := Callable(self, "_on_recalculate_navigation_map")
if child.is_connected("recalculate_navigation_map", callable):
child.disconnect("recalculate_navigation_map", callable)
child.connect("recalculate_navigation_map", callable)
func _recalculate_navigation_map(requester: Item) -> void: func _on_recalculate_navigation_map(requester: Node) -> void:
print(requester.item_name + " requests new navigation mesh bake.") print("[%s] %s requests new navigation mesh bake." % [LOG_TAG, requester.name])
bake_navigation_mesh() bake_navigation_mesh()

@ -1,18 +1,17 @@
class_name Player extends Unit class_name Player
extends Unit
const PITCH_MIN_DEG := -90.0
const PITCH_MAX_DEG := 30.0
const JOY_DEADZONE := 0.10
const CAMERA_PITCH_MIN_DEG := -90.0
const CAMERA_PITCH_MAX_DEG := 30.0
const JOYPAD_DEADZONE := 0.10
@export var mouse_sensitivity: float = 0.006 @export var mouse_sensitivity: float = 0.006
@export var rotation_speed: float = 24.0 @export var rotation_speed: float = 24.0
@export var joypad_sensitivity: float = 2.0 @export var joypad_sensitivity: float = 2.0
@export var spring_arm: SpringArm3D @export var spring_arm: SpringArm3D
signal gold_changed(current_gold: int) signal gold_changed(current_gold: int)
signal player_loaded() signal player_loaded()
var _gold: int = 0 var _gold: int = 0
var gold: int: var gold: int:
get: get:
return _gold return _gold
@ -22,56 +21,95 @@ var gold: int:
_gold = value _gold = value
gold_changed.emit(_gold) gold_changed.emit(_gold)
func _ready() -> void: func _ready() -> void:
health = maximum_health health = maximum_health
stamina = maximum_stamina stamina = maximum_stamina
player_loaded.emit() player_loaded.emit()
func _physics_process(delta: float) -> void:
# Gravity
velocity.y += -gravity * delta
# Movementw func _physics_process(delta: float) -> void:
get_move_input(delta) # Physik: Schwerkraft, Bewegung, Ausrichtung, Kamera per Joypad
apply_gravity(delta)
update_movement(delta)
move_and_slide() move_and_slide()
update_facing(delta)
handle_joypad_camera(delta)
# Smooth face direction to camera
if velocity.length() > 1.0:
model.rotation.y = lerp_angle(model.rotation.y, spring_arm.rotation.y, rotation_speed * delta)
# Joypad camera
var axis_x := Input.get_joy_axis(0, JOY_AXIS_RIGHT_X)
var axis_y := Input.get_joy_axis(0, JOY_AXIS_RIGHT_Y)
if absf(axis_x) > JOY_DEADZONE or absf(axis_y) > JOY_DEADZONE:
move_camera_joypad(axis_x, axis_y, delta)
func rotate_camera(pitch_delta: float, yaw_delta: float) -> void: func rotate_camera(pitch_delta: float, yaw_delta: float) -> void:
# Apply deltas and clamp pitch; yaw is unbounded # Deltas anwenden; Pitch clampen, Yaw ungebunden
spring_arm.rotation.x += pitch_delta spring_arm.rotation.x += pitch_delta
spring_arm.rotation_degrees.x = clampf(spring_arm.rotation_degrees.x, PITCH_MIN_DEG, PITCH_MAX_DEG) spring_arm.rotation_degrees.x = clampf(spring_arm.rotation_degrees.x, CAMERA_PITCH_MIN_DEG, CAMERA_PITCH_MAX_DEG)
spring_arm.rotation.y += yaw_delta spring_arm.rotation.y += yaw_delta
func move_camera_joypad(x: float, y: float, delta: float) -> void: func move_camera_joypad(x: float, y: float, delta: float) -> void:
# Scale by sensitivity and frame time; invert to match mouse feel # Skalierung nach Sensitivität und Framezeit; invertiert für Maus-ähnliches Gefühl
var pitch_delta := -y * joypad_sensitivity * delta var pitch_delta := -y * joypad_sensitivity * delta
var yaw_delta := -x * joypad_sensitivity * delta var yaw_delta := -x * joypad_sensitivity * delta
rotate_camera(pitch_delta, yaw_delta) rotate_camera(pitch_delta, yaw_delta)
func player_game_over(): func player_game_over():
GameManager.game_over() GameManager.game_over()
queue_free() queue_free()
func _input(event: InputEvent) -> void: func _input(event: InputEvent) -> void:
# Move camera via mouse # Kamera per Maus
handle_mouse_camera(event)
# Boden-/Luft-Logik
var on_floor := is_on_floor()
handle_jump(on_floor)
update_ground_state(on_floor)
# Menü
if event.is_action_pressed("Menue"):
get_tree().quit()
# --- Extracted/neu strukturierte Hilfsfunktionen ---
func apply_gravity(delta: float) -> void:
velocity.y += -gravity * delta
func update_movement(delta: float) -> void:
# Bewegungsinput lesen und anwenden
var vy := velocity.y
velocity.y = 0.0
var input_vector := Input.get_vector("left", "right", "forward", "back")
var direction := Vector3(input_vector.x, 0.0, input_vector.y).rotated(Vector3.UP, spring_arm.rotation.y)
if state == States.blocking:
direction = Vector3.ZERO # TODO: Gehen während Blocken erlauben
velocity = velocity.lerp(direction * speed, acceleration * delta)
var v_local := velocity * model.transform.basis
anim_tree.set("parameters/IWR/blend_position", Vector2(v_local.x, -v_local.z) / speed)
velocity.y = vy
func update_facing(delta: float) -> void:
# Sanft in Blickrichtung der Kamera drehen, wenn wir uns merklich bewegen
if velocity.length() > 1.0:
model.rotation.y = lerp_angle(model.rotation.y, spring_arm.rotation.y, rotation_speed * delta)
func handle_joypad_camera(delta: float) -> void:
var right_stick_x := Input.get_joy_axis(0, JOY_AXIS_RIGHT_X)
var right_stick_y := Input.get_joy_axis(0, JOY_AXIS_RIGHT_Y)
if absf(right_stick_x) > JOYPAD_DEADZONE or absf(right_stick_y) > JOYPAD_DEADZONE:
move_camera_joypad(right_stick_x, right_stick_y, delta)
func handle_mouse_camera(event: InputEvent) -> void:
if event is InputEventMouseMotion: if event is InputEventMouseMotion:
var motion := event as InputEventMouseMotion var motion := event as InputEventMouseMotion
var pitch_delta := -motion.relative.y * mouse_sensitivity var pitch_delta := -motion.relative.y * mouse_sensitivity
var yaw_delta := -motion.relative.x * mouse_sensitivity var yaw_delta := -motion.relative.x * mouse_sensitivity
rotate_camera(pitch_delta, yaw_delta) rotate_camera(pitch_delta, yaw_delta)
var on_floor := is_on_floor()
# Jump func handle_jump(on_floor: bool) -> void:
if on_floor and Input.is_action_just_pressed("jump"): if on_floor and Input.is_action_just_pressed("jump"):
if enough_stamina_available(jump_cost): if enough_stamina_available(jump_cost):
use_stamina(jump_cost) use_stamina(jump_cost)
@ -79,36 +117,16 @@ func _input(event: InputEvent) -> void:
state = States.jumping state = States.jumping
anim_tree.set("parameters/conditions/grounded", false) anim_tree.set("parameters/conditions/grounded", false)
# We just hit the floor after being in the air
func update_ground_state(on_floor: bool) -> void:
# Gerade gelandet
if on_floor and not last_floor: if on_floor and not last_floor:
if state != States.idle: if state != States.idle:
state = States.idle state = States.idle
anim_tree.set("parameters/conditions/grounded", true) anim_tree.set("parameters/conditions/grounded", true)
# In der Luft ohne aktiven Sprung
# We're in the air, but we didn't jump
if not on_floor and state != States.jumping: if not on_floor and state != States.jumping:
anim_state.travel("Jump_Idle") anim_state.travel("Jump_Idle")
anim_tree.set("parameters/conditions/grounded", false) anim_tree.set("parameters/conditions/grounded", false)
anim_tree.set("parameters/conditions/jumping", state == States.jumping) anim_tree.set("parameters/conditions/jumping", state == States.jumping)
last_floor = on_floor last_floor = on_floor
# Menu
if event.is_action_pressed("Menue"):
get_tree().quit()
func get_move_input(delta: float) -> void:
var vy := velocity.y
velocity.y = 0.0
var input_vector := Input.get_vector("left", "right", "forward", "back")
var direction := Vector3(input_vector.x, 0.0, input_vector.y).rotated(Vector3.UP, spring_arm.rotation.y)
if state == States.blocking:
direction = Vector3.ZERO # TODO: Walk while blocking
velocity = velocity.lerp(direction * speed, acceleration * delta)
var v_local := velocity * model.transform.basis
anim_tree.set("parameters/IWR/blend_position", Vector2(v_local.x, -v_local.z) / speed)
velocity.y = vy

@ -6,47 +6,49 @@ signal hit(collider: Object)
@export var initial_speed: float = 50.0 @export var initial_speed: float = 50.0
@export var damage: float = 3.0 @export var damage: float = 3.0
@export var lifetime_after_hit: float = 3.0 @export var lifetime_after_hit: float = 3.0
@export var uses_x_forward: bool = true # Mesh zeigt mit +X nach vorne @export var mesh_uses_x_forward: bool = true # Mesh zeigt mit +X nach vorne
var speed: float = 0.0 var speed: float = 0.0
var direction: Vector3 = Vector3.ZERO var velocity_dir: Vector3 = Vector3.ZERO
@onready var ray_cast: RayCast3D = $RayCast3D @onready var ray_cast: RayCast3D = $RayCast3D
func _physics_process(delta: float) -> void: func _physics_process(delta: float) -> void:
if speed > 0.0: if speed <= 0.0:
var move_dir := direction if direction != Vector3.ZERO else -global_transform.basis.z return
global_translate(move_dir * speed * delta) var move_dir := velocity_dir if velocity_dir != Vector3.ZERO else _forward_basis()
global_position += move_dir * speed * delta
if ray_cast and ray_cast.is_colliding(): if ray_cast and ray_cast.is_colliding():
var collider: Object = ray_cast.get_collider() var collider: Object = ray_cast.get_collider()
_on_hit(collider) _on_hit(collider)
func shoot(new_direction: Vector3 = Vector3.ZERO) -> void: func shoot(new_direction: Vector3 = Vector3.ZERO) -> void:
direction = new_direction.normalized() if new_direction != Vector3.ZERO else -global_transform.basis.z velocity_dir = new_direction.normalized() if new_direction != Vector3.ZERO else _forward_basis()
speed = initial_speed speed = initial_speed
# Beim Abfeuern aus der Waffen-Hierarchie lösen, damit Maus-/Kamerabewegung den Pfeil nicht beeinflusst # Beim Abfeuern aus der Waffen-Hierarchie lösen, damit Maus-/Kamerabewegung den Pfeil nicht beeinflusst
_detach_to_world() _detach_to_world()
# Visuelle Ausrichtung konsistent zur Flugrichtung setzen # Visuelle Ausrichtung konsistent zur Flugrichtung setzen
_align_visual_to_direction() _align_visual_to_direction()
if ray_cast: if ray_cast:
ray_cast.enabled = true ray_cast.enabled = true
func _forward_basis() -> Vector3:
return -global_transform.basis.z
func _align_visual_to_direction() -> void: func _align_visual_to_direction() -> void:
look_at(global_transform.origin + direction, Vector3.UP) look_at(global_transform.origin + velocity_dir, Vector3.UP)
# Godot richtet -Z nach vorne aus; wenn das Mesh +X als "vorne" hat, um -90° um Y korrigieren # Godot richtet -Z nach vorne aus; wenn das Mesh +X als "vorne" hat, um -90° um Y korrigieren
if uses_x_forward: if mesh_uses_x_forward:
rotate_z(deg_to_rad(90.0)) rotate_y(deg_to_rad(-90.0))
func _detach_to_world() -> void: func _detach_to_world() -> void:
var target_parent: Node = get_tree().current_scene if get_tree().current_scene != null else get_tree().root var tree := get_tree()
var target_parent: Node = tree.current_scene if tree.current_scene != null else tree.root
if target_parent == null: if target_parent == null:
return return
var gt := global_transform var gt := global_transform
@ -63,10 +65,8 @@ func _on_hit(collider: Object) -> void:
speed = 0.0 speed = 0.0
if ray_cast: if ray_cast:
ray_cast.enabled = false ray_cast.enabled = false
emit_signal("hit", collider) emit_signal("hit", collider)
_apply_damage_if_supported(collider) _apply_damage_if_supported(collider)
if lifetime_after_hit > 0.0: if lifetime_after_hit > 0.0:
var timer := get_tree().create_timer(lifetime_after_hit) var timer := get_tree().create_timer(lifetime_after_hit)
timer.timeout.connect(queue_free) timer.timeout.connect(queue_free)

@ -5,13 +5,7 @@ extends Player
const ATTACK_ANIMS: Array[String] = ["2H_Ranged_Shoot"] const ATTACK_ANIMS: Array[String] = ["2H_Ranged_Shoot"]
const RELOAD_ANIMS: Array[String] = ["2H_Ranged_Reload"] const RELOAD_ANIMS: Array[String] = ["2H_Ranged_Reload"]
const AIM_ANIMS: Array[String] = ["2H_Ranged_Aiming"] const AIM_ANIMS: Array[String] = ["2H_Ranged_Aiming"]
const FOOTSTEP_SOUNDS: Array[String] = [ const FOOTSTEP_SOUNDS: Array[String] = ["res://resources/audio/footstep_grass_000.ogg", "res://resources/audio/footstep_grass_001.ogg", "res://resources/audio/footstep_grass_002.ogg", "res://resources/audio/footstep_grass_003.ogg", "res://resources/audio/footstep_grass_004.ogg"]
"res://resources/audio/footstep_grass_000.ogg",
"res://resources/audio/footstep_grass_001.ogg",
"res://resources/audio/footstep_grass_002.ogg",
"res://resources/audio/footstep_grass_003.ogg",
"res://resources/audio/footstep_grass_004.ogg"
]
# Tuning-Konstanten # Tuning-Konstanten
const FOV_AIM: float = 40.0 const FOV_AIM: float = 40.0
const FOV_DEFAULT: float = 70.0 const FOV_DEFAULT: float = 70.0

@ -1,71 +1,108 @@
extends Node extends Node
@export var max_enemy := 3 @export var max_enemies: int = 3
@export var enemies_container: Node
@export var enemies_folder: Node @export var enemy_scenes: Array[PackedScene]
@export var enemies: Array[PackedScene]
@export var enemies_ui: PackedScene
@export var spawn_points: Array[SpawnPoint]
@export var enemies_ui_manager: Node @export var enemies_ui_manager: Node
@export var spawn_points_container: Node @export var spawn_points_container: Node
var player: Player var player: Player
var current_enemies: Array[Enemy] = [] var current_enemies: Array[Enemy] = []
var enemy_number := 0 var _enemy_counter: int = 0
var enemy_ui_ready := false: var _enemy_ui_ready: bool = false
var spawn_points: Array[SpawnPoint]
var enemy_ui_ready: bool:
get: get:
return enemy_ui_ready return _enemy_ui_ready
set(value): set(value):
enemy_ui_ready = value _enemy_ui_ready = value
if value: if value:
enemy_count_changed() ensure_enemy_count()
func _ready() -> void: func _ready() -> void:
player = get_tree().get_first_node_in_group("player") player = get_tree().get_first_node_in_group("player")
if player == null: if player == null:
print("Error: No player found!!") print("Error: No player found!")
# SpawnPoints aus dem Container laden (falls gesetzt)
func enemy_count_changed() -> void: populate_spawn_points_from_container()
print("Enough enemies? " + str(current_enemies.size()) + "/" + str(max_enemy))
if current_enemies.size() < max_enemy: func populate_spawn_points_from_container() -> void:
var empty_spawn_points = find_empty_spawn_points() if spawn_points_container == null:
if empty_spawn_points.size() == 0: push_warning("spawn_points_container is not assigned; cannot auto-collect SpawnPoints.")
print("No free SpawnPoints found!") return
else: var collected: Array[SpawnPoint] = []
spawn_new_enemy(empty_spawn_points.pick_random()) var children: Array = spawn_points_container.get_children()
enemy_count_changed() for child in children:
else: if child is SpawnPoint:
collected.append(child)
spawn_points = collected
func ensure_enemy_count() -> void:
var current := current_enemies.size()
print("Enough enemies? %d/%d" % [current, max_enemies])
if current >= max_enemies:
print("Enough enemies!") print("Enough enemies!")
return
var empty_points := find_empty_spawn_points()
if empty_points.is_empty():
print("No free SpawnPoints found!")
return
var need: int = min(max_enemies - current, empty_points.size())
for i in need:
spawn_enemy(empty_points[i])
func spawn_enemy(spawn_point: SpawnPoint) -> void:
var scene: PackedScene = enemy_scenes.pick_random()
if scene == null:
push_warning("No enemy scene available to spawn.")
return
var new_enemy := scene.instantiate() as Enemy
if new_enemy == null:
push_warning("PackedScene did not instantiate as Enemy.")
return
func spawn_new_enemy(spawn_point: SpawnPoint) -> void:
var new_enemy = enemies.pick_random().instantiate() as Enemy
new_enemy.player = player new_enemy.player = player
enemy_number += 1 _enemy_counter += 1
new_enemy.name = new_enemy.name + str(enemy_number) new_enemy.name = "%s%d" % [new_enemy.name, _enemy_counter]
new_enemy.unit_name = new_enemy.name new_enemy.unit_name = new_enemy.name
print("Spawn " + new_enemy.unit_name + " on " + spawn_point.name) print("Spawn %s on %s" % [new_enemy.unit_name, spawn_point.name])
new_enemy.position = spawn_point.position new_enemy.position = spawn_point.position
new_enemy.unit_died.connect(_on_unit_died) new_enemy.unit_died.connect(_on_unit_died)
enemies_folder.add_child(new_enemy)
enemies_container.add_child(new_enemy)
spawn_point.isFull = true spawn_point.isFull = true
spawn_point.enemy = new_enemy spawn_point.enemy = new_enemy
current_enemies.append(new_enemy) current_enemies.append(new_enemy)
enemies_ui_manager.update_enemies_ui(current_enemies) enemies_ui_manager.update_enemies_ui(current_enemies)
func find_empty_spawn_points() -> Array[SpawnPoint]: func find_empty_spawn_points() -> Array[SpawnPoint]:
var empty_spawn_points: Array[SpawnPoint] = [] var empty_spawn_points: Array[SpawnPoint] = []
for spawn_point in spawn_points: print("Count of spawn points: " + str(spawn_points.size()))
if not spawn_point.isFull: for sp in spawn_points:
empty_spawn_points.append(spawn_point) if not sp.isFull:
empty_spawn_points.append(sp)
else:
print("Spawn point " + sp.name + " is full!")
return empty_spawn_points return empty_spawn_points
func _on_unit_died(unit: Unit) -> void: func _on_unit_died(unit: Unit) -> void:
if unit is Enemy: if unit is Enemy:
print(unit.unit_name + " died -> Remove from current_enemies.") print("%s died -> Remove from current_enemies." % unit.unit_name)
var index = current_enemies.find(unit) var index := current_enemies.find(unit)
if index != -1: if index != -1:
current_enemies.remove_at(index) current_enemies.remove_at(index)
enemy_count_changed() ensure_enemy_count()
else: else:
print("Enemy is not in the list!") print("Enemy is not in the list!")
else: else: