last init

This commit is contained in:
Marek
2026-03-29 16:05:31 +02:00
commit aa2c182534
53 changed files with 1419 additions and 0 deletions

View File

@@ -0,0 +1,66 @@
extends Resource
class_name Ability
enum Type { SINGLE, AOE, UTILITY, ULT, PASSIVE }
@export var ability_name: String = ""
@export var type: Type = Type.SINGLE
@export var damage: float = 0.0
@export var ability_range: float = 3.0
@export var icon: String = ""
func execute(player: Node, targeting: Node) -> void:
var dmg: float = _get_modified_damage(player, damage)
match type:
Type.SINGLE:
_execute_single(player, targeting, dmg)
Type.AOE:
_execute_aoe(player, dmg)
Type.UTILITY:
_execute_utility(player)
Type.ULT:
_execute_ult(player, targeting, dmg)
func _get_modified_damage(player: Node, base: float) -> float:
var combat: Node = player.get_node("Combat")
return combat.apply_passive(base)
func _execute_single(player: Node, targeting: Node, dmg: float) -> void:
var target: Node3D = targeting.current_target
if not target or not is_instance_valid(target):
return
var dist: float = player.global_position.distance_to(target.global_position)
if dist > ability_range:
return
EventBus.damage_requested.emit(player, target, dmg)
EventBus.attack_executed.emit(player, player.global_position, -player.global_transform.basis.z, dmg)
func _execute_aoe(player: Node, dmg: float) -> void:
var enemies := player.get_tree().get_nodes_in_group("enemies")
for enemy in enemies:
var dist: float = player.global_position.distance_to(enemy.global_position)
if dist <= ability_range:
EventBus.damage_requested.emit(player, enemy, dmg)
EventBus.attack_executed.emit(player, player.global_position, -player.global_transform.basis.z, dmg)
func _execute_utility(player: Node) -> void:
var shield: Node = player.get_node_or_null("Shield")
if shield:
shield.current_shield = shield.max_shield
EventBus.shield_changed.emit(player, shield.current_shield, shield.max_shield)
func _execute_ult(player: Node, targeting: Node, dmg: float) -> void:
var target: Node3D = targeting.current_target
if not target or not is_instance_valid(target):
return
var dist: float = player.global_position.distance_to(target.global_position)
if dist > ability_range:
return
EventBus.damage_requested.emit(player, target, dmg * 4.0)
var enemies := player.get_tree().get_nodes_in_group("enemies")
for enemy in enemies:
if enemy != target:
var enemy_dist: float = target.global_position.distance_to(enemy.global_position)
if enemy_dist <= ability_range:
EventBus.damage_requested.emit(player, enemy, dmg * 2.0)
EventBus.attack_executed.emit(player, player.global_position, -player.global_transform.basis.z, dmg * 4.0)

View File

@@ -0,0 +1 @@
uid://c03xbbf3yhfl3

View File

@@ -0,0 +1,4 @@
extends Resource
class_name AbilitySet
@export var abilities: Array[Ability] = []

View File

@@ -0,0 +1 @@
uid://voedgs25cwrb

View File

@@ -0,0 +1,33 @@
extends Node
@export var max_health := 100.0
const HEALTH_REGEN := 1.0
var current_health: float
func _ready() -> void:
current_health = max_health
EventBus.damage_requested.connect(_on_damage_requested)
func _process(delta: float) -> void:
if current_health > 0 and current_health < max_health:
current_health = min(current_health + HEALTH_REGEN * delta, max_health)
EventBus.health_changed.emit(get_parent(), current_health, max_health)
func _on_damage_requested(attacker: Node, target: Node, amount: float) -> void:
if target != get_parent():
return
var remaining: float = amount
var shield: Node = get_parent().get_node_or_null("Shield")
if shield:
remaining = shield.absorb(remaining)
EventBus.damage_dealt.emit(attacker, get_parent(), amount)
if remaining > 0:
take_damage(remaining, attacker)
func take_damage(amount: float, attacker: Node) -> void:
current_health -= amount
if current_health <= 0:
current_health = 0
EventBus.health_changed.emit(get_parent(), current_health, max_health)
if current_health <= 0:
EventBus.entity_died.emit(get_parent())

View File

@@ -0,0 +1 @@
uid://b053b4fkkeaod

View File

@@ -0,0 +1,32 @@
extends Node
@export var max_shield := 50.0
const REGEN_DELAY := 3.0
const REGEN_TIME := 5.0
var current_shield: float
var regen_timer := 0.0
func _ready() -> void:
current_shield = max_shield
func _process(delta: float) -> void:
if current_shield < max_shield:
regen_timer += delta
if regen_timer >= REGEN_DELAY:
current_shield += (max_shield / REGEN_TIME) * delta
if current_shield >= max_shield:
current_shield = max_shield
EventBus.shield_regenerated.emit(get_parent())
EventBus.shield_changed.emit(get_parent(), current_shield, max_shield)
func absorb(amount: float) -> float:
if current_shield <= 0:
return amount
regen_timer = 0.0
var absorbed: float = min(amount, current_shield)
current_shield -= absorbed
print("%s Schild: %s/%s (-%s)" % [get_parent().name, current_shield, max_shield, absorbed])
if current_shield <= 0:
EventBus.shield_broken.emit(get_parent())
EventBus.shield_changed.emit(get_parent(), current_shield, max_shield)
return amount - absorbed

View File

@@ -0,0 +1 @@
uid://bpfw71oprcvou

38
scripts/enemy/enemy.gd Normal file
View File

@@ -0,0 +1,38 @@
extends CharacterBody3D
enum State { IDLE, CHASE, ATTACK, RETURN }
var state: int = State.IDLE
var target: Node3D = null
var spawn_position: Vector3
var gravity: float = ProjectSettings.get_setting("physics/3d/default_gravity")
@onready var health: Node = $Health
func _ready() -> void:
spawn_position = global_position
add_to_group("enemies")
EventBus.entity_died.connect(_on_entity_died)
func _on_entity_died(entity: Node) -> void:
if entity == self:
queue_free()
elif entity == target:
target = null
state = State.RETURN
func _physics_process(delta: float) -> void:
if not is_on_floor():
velocity.y -= gravity * delta
move_and_slide()
func _on_detection_area_body_entered(body: Node3D) -> void:
if body is CharacterBody3D and body.name == "Player":
target = body
state = State.CHASE
EventBus.enemy_engaged.emit(self, body)
func _on_detection_area_body_exited(body: Node3D) -> void:
if body == target and state == State.CHASE:
state = State.RETURN
target = null

View File

@@ -0,0 +1 @@
uid://bwi75jx0agktd

View File

@@ -0,0 +1,26 @@
extends Node
const ATTACK_RANGE := 2.0
const ATTACK_COOLDOWN := 1.5
const ATTACK_DAMAGE := 5.0
var attack_timer := 0.0
@onready var enemy: CharacterBody3D = get_parent()
func _physics_process(delta: float) -> void:
attack_timer -= delta
if enemy.state != enemy.State.ATTACK:
return
if not is_instance_valid(enemy.target):
enemy.state = enemy.State.RETURN
return
var dist := enemy.global_position.distance_to(enemy.target.global_position)
if dist > ATTACK_RANGE:
enemy.state = enemy.State.CHASE
return
if attack_timer <= 0:
attack_timer = ATTACK_COOLDOWN
EventBus.damage_requested.emit(enemy, enemy.target, ATTACK_DAMAGE)
enemy.velocity.x = 0
enemy.velocity.z = 0

View File

@@ -0,0 +1 @@
uid://ct4u62xalrjyo

View File

@@ -0,0 +1,22 @@
extends Sprite3D
@onready var viewport: SubViewport = $SubViewport
@onready var health_bar: ProgressBar = $SubViewport/HealthBar
@onready var shield_bar: ProgressBar = $SubViewport/ShieldBar
@onready var border: ColorRect = $SubViewport/Border
@onready var health: Node = get_parent().get_node("Health")
@onready var shield: Node = get_parent().get_node("Shield")
func _ready() -> void:
texture = viewport.get_texture()
health_bar.max_value = health.max_health
shield_bar.max_value = shield.max_shield
border.visible = false
EventBus.target_changed.connect(_on_target_changed)
func _process(_delta: float) -> void:
health_bar.value = health.current_health
shield_bar.value = shield.current_shield
func _on_target_changed(_player: Node, target: Node) -> void:
border.visible = (target == get_parent())

View File

@@ -0,0 +1 @@
uid://dwqx03nfypa7u

View File

@@ -0,0 +1,52 @@
extends Node
const SPEED := 3.0
const LEASH_RANGE := 15.0
const ATTACK_RANGE := 2.0
@onready var enemy: CharacterBody3D = get_parent()
@onready var nav_agent: NavigationAgent3D = get_parent().get_node("NavigationAgent3D")
func _physics_process(_delta: float) -> void:
match enemy.state:
enemy.State.IDLE:
enemy.velocity.x = 0
enemy.velocity.z = 0
enemy.State.CHASE:
_chase()
enemy.State.RETURN:
_return_to_spawn()
func _chase() -> void:
if not is_instance_valid(enemy.target):
enemy.state = enemy.State.RETURN
return
var dist_to_spawn := enemy.global_position.distance_to(enemy.spawn_position)
if dist_to_spawn > LEASH_RANGE:
enemy.state = enemy.State.RETURN
enemy.target = null
return
var dist_to_target := enemy.global_position.distance_to(enemy.target.global_position)
if dist_to_target <= ATTACK_RANGE:
enemy.state = enemy.State.ATTACK
return
nav_agent.target_position = enemy.target.global_position
var next_pos := nav_agent.get_next_path_position()
var direction := (next_pos - enemy.global_position).normalized()
direction.y = 0
enemy.velocity.x = direction.x * SPEED
enemy.velocity.z = direction.z * SPEED
func _return_to_spawn() -> void:
var dist := enemy.global_position.distance_to(enemy.spawn_position)
if dist < 1.0:
enemy.state = enemy.State.IDLE
enemy.velocity.x = 0
enemy.velocity.z = 0
return
nav_agent.target_position = enemy.spawn_position
var next_pos := nav_agent.get_next_path_position()
var direction := (next_pos - enemy.global_position).normalized()
direction.y = 0
enemy.velocity.x = direction.x * SPEED
enemy.velocity.z = direction.z * SPEED

View File

@@ -0,0 +1 @@
uid://tnx6rbnnngn

15
scripts/event_bus.gd Normal file
View File

@@ -0,0 +1,15 @@
extends Node
signal attack_executed(attacker, position, direction, damage)
signal damage_dealt(attacker, target, damage)
signal entity_died(entity)
signal shield_broken(entity)
signal shield_regenerated(entity)
signal target_changed(player, target)
signal player_respawned(player)
signal class_changed(player, class_type)
signal damage_requested(attacker, target, amount)
signal health_changed(entity, current, max_val)
signal shield_changed(entity, current, max_val)
signal respawn_tick(timer)
signal enemy_engaged(enemy, target)

1
scripts/event_bus.gd.uid Normal file
View File

@@ -0,0 +1 @@
uid://g7a7xkg1pgb4

30
scripts/player/camera.gd Normal file
View File

@@ -0,0 +1,30 @@
extends Node3D
const SENSITIVITY := 0.003
const PITCH_MIN := -80.0
const PITCH_MAX := 80.0
var pitch := -0.3
var camera_yaw := 0.0
var player_yaw := -2.356
func _ready() -> void:
get_parent().rotation.y = player_yaw
rotation = Vector3(pitch, camera_yaw, 0)
func _unhandled_input(event: InputEvent) -> void:
if event is InputEventMouseMotion:
var lmb := Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT)
var rmb := Input.is_mouse_button_pressed(MOUSE_BUTTON_RIGHT)
if lmb or rmb:
pitch -= event.relative.y * SENSITIVITY
pitch = clamp(pitch, deg_to_rad(PITCH_MIN), deg_to_rad(PITCH_MAX))
if rmb:
player_yaw -= event.relative.x * SENSITIVITY
get_parent().rotation.y = player_yaw
else:
camera_yaw -= event.relative.x * SENSITIVITY
rotation = Vector3(pitch, camera_yaw, 0)

View File

@@ -0,0 +1 @@
uid://cohjyjge1kqxb

35
scripts/player/combat.gd Normal file
View File

@@ -0,0 +1,35 @@
extends Node
@onready var player: CharacterBody3D = get_parent()
@onready var targeting: Node = get_parent().get_node("Targeting")
@onready var player_class: Node = get_parent().get_node("PlayerClass")
var abilities: Array = []
func _ready() -> void:
_load_abilities()
EventBus.class_changed.connect(_on_class_changed)
func _load_abilities() -> void:
var ability_set: AbilitySet = player_class.get_ability_set()
if ability_set:
abilities = ability_set.abilities
else:
abilities = []
func _unhandled_input(event: InputEvent) -> void:
for i in range(min(abilities.size(), 5)):
if event.is_action_pressed("ability_%s" % (i + 1)) and abilities[i]:
if abilities[i].type == Ability.Type.PASSIVE:
return
abilities[i].execute(player, targeting)
return
func apply_passive(base_damage: float) -> float:
for ability in abilities:
if ability and ability.type == Ability.Type.PASSIVE:
return base_damage * (1.0 + ability.damage / 100.0)
return base_damage
func _on_class_changed(_player: Node, _class_type: int) -> void:
_load_abilities()

View File

@@ -0,0 +1 @@
uid://d15til6fsxw5b

43
scripts/player/hud.gd Normal file
View File

@@ -0,0 +1,43 @@
extends CanvasLayer
@onready var health_bar: ProgressBar = $HealthBar
@onready var shield_bar: ProgressBar = $ShieldBar
@onready var respawn_label: Label = $RespawnTimer
@onready var class_icon: Label = $AbilityBar/ClassIcon/Label
var player_node: Node = null
func _ready() -> void:
respawn_label.visible = false
EventBus.health_changed.connect(_on_health_changed)
EventBus.shield_changed.connect(_on_shield_changed)
EventBus.entity_died.connect(_on_entity_died)
EventBus.player_respawned.connect(_on_player_respawned)
EventBus.class_changed.connect(_on_class_changed)
EventBus.respawn_tick.connect(_on_respawn_tick)
func _on_health_changed(entity: Node, current: float, max_val: float) -> void:
if entity.name == "Player":
health_bar.max_value = max_val
health_bar.value = current
func _on_shield_changed(entity: Node, current: float, max_val: float) -> void:
if entity.name == "Player":
shield_bar.max_value = max_val
shield_bar.value = current
func _on_entity_died(entity: Node) -> void:
if entity.name == "Player":
respawn_label.visible = true
func _on_player_respawned(_player: Node) -> void:
respawn_label.visible = false
func _on_respawn_tick(timer: float) -> void:
respawn_label.text = str(ceil(timer))
func _on_class_changed(_player: Node, class_type: int) -> void:
match class_type:
0: class_icon.text = "T"
1: class_icon.text = "D"
2: class_icon.text = "H"

View File

@@ -0,0 +1 @@
uid://c4jhr8k4uwoy7

View File

@@ -0,0 +1,35 @@
extends Node
const SPEED := 5.0
const JUMP_VELOCITY := 4.5
var gravity: float = ProjectSettings.get_setting("physics/3d/default_gravity")
@onready var player: CharacterBody3D = get_parent()
func _physics_process(delta: float) -> void:
if not player.is_on_floor():
player.velocity.y -= gravity * delta
if Input.is_action_just_pressed("ui_accept") and player.is_on_floor():
player.velocity.y = JUMP_VELOCITY
var input_dir := Input.get_vector("move_left", "move_right", "move_forward", "move_back")
var camera_pivot := player.get_node("CameraPivot") as Node3D
var forward := -camera_pivot.global_transform.basis.z
forward.y = 0
forward = forward.normalized()
var right := camera_pivot.global_transform.basis.x
right.y = 0
right = right.normalized()
var direction := (forward * -input_dir.y + right * input_dir.x).normalized()
if direction:
player.velocity.x = direction.x * SPEED
player.velocity.z = direction.z * SPEED
else:
player.velocity.x = move_toward(player.velocity.x, 0, SPEED)
player.velocity.z = move_toward(player.velocity.z, 0, SPEED)
player.move_and_slide()

View File

@@ -0,0 +1 @@
uid://fg87dh8fbc8

1
scripts/player/player.gd Normal file
View File

@@ -0,0 +1 @@
extends CharacterBody3D

View File

@@ -0,0 +1 @@
uid://bfpt2p7uucfyb

View File

@@ -0,0 +1,37 @@
extends Node
enum PlayerClass { TANK, DAMAGE, HEALER }
var current_class: int = PlayerClass.DAMAGE
@export var tank_set: AbilitySet
@export var damage_set: AbilitySet
@export var healer_set: AbilitySet
@onready var player: CharacterBody3D = get_parent()
func _unhandled_input(event: InputEvent) -> void:
if event.is_action_pressed("class_tank"):
set_class(PlayerClass.TANK)
elif event.is_action_pressed("class_damage"):
set_class(PlayerClass.DAMAGE)
elif event.is_action_pressed("class_healer"):
set_class(PlayerClass.HEALER)
func set_class(new_class: int) -> void:
current_class = new_class
EventBus.class_changed.emit(player, current_class)
func get_class_icon() -> String:
match current_class:
PlayerClass.TANK: return "T"
PlayerClass.DAMAGE: return "D"
PlayerClass.HEALER: return "H"
return ""
func get_ability_set() -> AbilitySet:
match current_class:
PlayerClass.TANK: return tank_set
PlayerClass.DAMAGE: return damage_set
PlayerClass.HEALER: return healer_set
return damage_set

View File

@@ -0,0 +1 @@
uid://rus4umqvvqq4

46
scripts/player/respawn.gd Normal file
View File

@@ -0,0 +1,46 @@
extends Node
const RESPAWN_TIME := 3.0
var respawn_timer := 0.0
var is_dead := false
var spawn_position: Vector3
@onready var player: CharacterBody3D = get_parent()
func _ready() -> void:
spawn_position = player.global_position
EventBus.entity_died.connect(_on_entity_died)
func _process(delta: float) -> void:
if is_dead:
respawn_timer -= delta
EventBus.respawn_tick.emit(respawn_timer)
if respawn_timer <= 0:
_respawn()
func _on_entity_died(entity: Node) -> void:
if entity == player and not is_dead:
is_dead = true
respawn_timer = RESPAWN_TIME
player.velocity = Vector3.ZERO
player.get_node("Mesh").visible = false
player.get_node("CollisionShape3D").disabled = true
player.get_node("Movement").set_physics_process(false)
player.get_node("Combat").set_process_unhandled_input(false)
player.get_node("Targeting").set_process_unhandled_input(false)
func _respawn() -> void:
is_dead = false
player.global_position = spawn_position
player.get_node("Mesh").visible = true
player.get_node("CollisionShape3D").disabled = false
player.get_node("Movement").set_physics_process(true)
player.get_node("Combat").set_process_unhandled_input(true)
player.get_node("Targeting").set_process_unhandled_input(true)
var health_node: Node = player.get_node("Health")
var shield_node: Node = player.get_node("Shield")
health_node.current_health = health_node.max_health
shield_node.current_shield = shield_node.max_shield
EventBus.health_changed.emit(player, health_node.current_health, health_node.max_health)
EventBus.shield_changed.emit(player, shield_node.current_shield, shield_node.max_shield)
EventBus.player_respawned.emit(player)

View File

@@ -0,0 +1 @@
uid://dw3dtax5bx0of

113
scripts/player/targeting.gd Normal file
View File

@@ -0,0 +1,113 @@
extends Node
const TARGET_RANGE := 20.0
const COMBAT_TIMEOUT := 3.0
var current_target: Node3D = null
var mouse_press_pos: Vector2 = Vector2.ZERO
var in_combat := false
var combat_timer := 0.0
@onready var player: CharacterBody3D = get_parent()
@onready var camera: Camera3D = get_parent().get_node("CameraPivot/Camera3D")
func _ready() -> void:
EventBus.damage_dealt.connect(_on_damage_dealt)
EventBus.entity_died.connect(_on_entity_died)
EventBus.enemy_engaged.connect(_on_enemy_engaged)
func _process(delta: float) -> void:
if in_combat:
combat_timer -= delta
if combat_timer <= 0:
in_combat = false
func _unhandled_input(event: InputEvent) -> void:
if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT:
if event.pressed:
mouse_press_pos = event.position
else:
var drag: float = event.position.distance_to(mouse_press_pos)
if drag < 5.0:
_try_target_under_mouse(event.position)
if event.is_action_pressed("target_next"):
_cycle_target()
func _try_target_under_mouse(mouse_pos: Vector2) -> void:
var from := camera.project_ray_origin(mouse_pos)
var to := from + camera.project_ray_normal(mouse_pos) * TARGET_RANGE
var space := player.get_world_3d().direct_space_state
var query := PhysicsRayQueryParameters3D.create(from, to)
query.collision_mask = 4
query.collide_with_areas = true
query.collide_with_bodies = false
var result := space.intersect_ray(query)
if result:
var hit_target := result.collider.get_parent() as Node3D
set_target(hit_target)
else:
set_target(null)
func _cycle_target() -> void:
var enemies := get_tree().get_nodes_in_group("enemies")
if enemies.is_empty():
set_target(null)
return
if current_target == null or current_target not in enemies:
set_target(enemies[0])
return
var idx := enemies.find(current_target)
var next_idx := (idx + 1) % enemies.size()
set_target(enemies[next_idx])
func set_target(target: Node3D) -> void:
current_target = target
EventBus.target_changed.emit(player, target)
func _on_enemy_engaged(_enemy: Node, target: Node) -> void:
if target == player:
combat_timer = COMBAT_TIMEOUT
if not in_combat:
in_combat = true
if current_target == null:
_target_nearest()
func _on_damage_dealt(_attacker: Node, target: Node, _amount: float) -> void:
if target == player:
combat_timer = COMBAT_TIMEOUT
if not in_combat:
in_combat = true
if current_target == null:
_target_nearest()
func _on_entity_died(entity: Node) -> void:
if entity == current_target:
set_target(null)
if in_combat:
_target_nearest_except(entity)
func _target_nearest_except(exclude: Node = null) -> void:
var enemies := get_tree().get_nodes_in_group("enemies")
var nearest: Node3D = null
var nearest_dist: float = INF
for enemy in enemies:
if is_instance_valid(enemy) and enemy != exclude:
var dist: float = player.global_position.distance_to(enemy.global_position)
if dist < nearest_dist:
nearest_dist = dist
nearest = enemy
if nearest:
set_target(nearest)
func _target_nearest() -> void:
var enemies := get_tree().get_nodes_in_group("enemies")
var nearest: Node3D = null
var nearest_dist: float = INF
for enemy in enemies:
if is_instance_valid(enemy):
var dist: float = player.global_position.distance_to(enemy.global_position)
if dist < nearest_dist:
nearest_dist = dist
nearest = enemy
if nearest:
set_target(nearest)

View File

@@ -0,0 +1 @@
uid://b05nkuryipwny