game: add weapon base and sword

This commit is contained in:
2025-08-21 00:32:18 +02:00
parent 156da4898e
commit 8bd7bfa406
21 changed files with 200 additions and 163 deletions

View File

@@ -18,6 +18,7 @@ const GROUP_DAMAGEABLE = "damagable"
const GROUP_PLAYER = "player"
const GROUP_XP_ORB = "xp_orb"
const GROUP_PICKUP = "pickup"
const GROUP_PROJ_MANAGER = "proj_manager"
enum ModRarity { LEGENDARY, EPIC, RARE, NORMAL }

View File

@@ -33,6 +33,7 @@ enemy="Group containing all enemies"
damagable="Can be damaged using take_damage"
xp_orb="Experience orbs"
pickup="Items that can be picked up"
proj_manager="Parent node for all projectiles"
[input]

View File

@@ -1,102 +0,0 @@
extends Node2D
@export var default_attack_time: float = 0.5
@export var player: Player = null
@onready var trigger_area: Area2D = $TriggerArea
@onready var trigger_collision: CollisionShape2D = $TriggerArea/TriggerCollision
@onready var timer: Timer = $Timer
@onready var attack_path: Path2D = $AttackPath
@onready var path_follow_2d: PathFollow2D = $AttackPath/PathFollow2D
@onready var attack_area: Area2D = $AttackPath/PathFollow2D/Sprite2D/AttackArea
var base_damage: float = 5.0
var current_target: Node2D = null
var current_progress: float = 0.0
var damaged_this_attack: Array = []
var is_attacking: bool = false
func _ready() -> void:
attack_path.visible = false
func _process(delta: float) -> void:
if timer.is_stopped() and current_progress == 0.0:
timer.start()
if current_progress > 0.95:
reset_attack()
if (
current_target
and is_instance_valid(current_target)
and not current_target.is_queued_for_deletion()
):
track_target(current_target)
is_attacking = true
attack_path.visible = true
if is_attacking:
# Do attack animation
current_progress += delta / default_attack_time
current_progress = clampf(current_progress, 0.0, 1.0)
path_follow_2d.progress_ratio = current_progress
func reset_attack() -> void:
current_target = null
attack_path.visible = false
current_progress = 0.0
damaged_this_attack = []
is_attacking = false
position = Vector2.ZERO
rotation = 0.0
func set_target(body: Node2D):
current_target = body
is_attacking = true
func track_target(body: Node2D):
var mid_distance = attack_path.curve.get_baked_length() / 2
var mid_point: Vector2 = attack_path.curve.sample_baked(mid_distance)
var offset = body.global_position - to_global(mid_point)
var desired_dir = (body.global_position - to_global(mid_point)).normalized()
var start_point_global = attack_path.to_global(attack_path.curve.sample_baked(0))
var end_point_global = attack_path.to_global(
attack_path.curve.sample_baked(attack_path.curve.get_baked_length())
)
var curve_dir = (start_point_global - end_point_global).normalized()
var angle_diff = curve_dir.angle_to(desired_dir)
if rotation == 0.0:
rotation = curve_dir.angle_to(desired_dir)
position += offset
func _on_timer_timeout() -> void:
if current_target:
if trigger_area.has_overlapping_areas():
if current_target not in trigger_area.get_overlapping_bodies():
current_target = null
return
if trigger_area.has_overlapping_bodies():
for body in trigger_area.get_overlapping_bodies():
if body.is_in_group(GlobalConst.GROUP_ENEMY):
set_target(body)
func _on_attack_area_body_entered(body: Node2D) -> void:
if not attack_path.visible:
return
if body in damaged_this_attack:
return
if body.is_in_group(GlobalConst.GROUP_ENEMY) and body.is_in_group(GlobalConst.GROUP_DAMAGEABLE):
var crit_chance = player.player_stats.get_final("crit_chance", player.modifiers)
var damage_dealt = base_damage
var is_crit = randf() >= 1 - crit_chance
if is_crit:
damage_dealt *= player.player_stats.get_final("crit_multiplier", player.modifiers)
body.take_damage(damage_dealt, is_crit)
damaged_this_attack.append(body)

View File

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

View File

@@ -1,53 +0,0 @@
[gd_scene load_steps=6 format=3 uid="uid://cdojqe2m4kxx1"]
[ext_resource type="Texture2D" uid="uid://dycw7c3484dir" path="res://assets/sprites/sword.png" id="1_3fwwl"]
[ext_resource type="Script" uid="uid://db326gu8abue5" path="res://scenes/attacks/attack_sword.gd" id="1_frsqi"]
[sub_resource type="Curve2D" id="Curve2D_frsqi"]
bake_interval = 2.0
_data = {
"points": PackedVector2Array(0, 0, 0, 0, 0, 0, 0, 0, 25, 10, 200, 0)
}
point_count = 2
[sub_resource type="RectangleShape2D" id="RectangleShape2D_frsqi"]
size = Vector2(13.9997, 46.999)
[sub_resource type="CircleShape2D" id="CircleShape2D_3fwwl"]
radius = 267.002
[node name="AttackSword" type="Node2D"]
script = ExtResource("1_frsqi")
[node name="AttackPath" type="Path2D" parent="."]
curve = SubResource("Curve2D_frsqi")
[node name="PathFollow2D" type="PathFollow2D" parent="AttackPath"]
loop = false
[node name="Sprite2D" type="Sprite2D" parent="AttackPath/PathFollow2D"]
position = Vector2(0.322462, -0.946582)
rotation = 0.328329
texture = ExtResource("1_3fwwl")
[node name="AttackArea" type="Area2D" parent="AttackPath/PathFollow2D/Sprite2D"]
collision_layer = 0
collision_mask = 2
[node name="AttackCollision" type="CollisionShape2D" parent="AttackPath/PathFollow2D/Sprite2D/AttackArea"]
position = Vector2(-0.0328934, -4.50646)
shape = SubResource("RectangleShape2D_frsqi")
[node name="TriggerArea" type="Area2D" parent="."]
visible = false
collision_layer = 0
collision_mask = 2
[node name="TriggerCollision" type="CollisionShape2D" parent="TriggerArea"]
shape = SubResource("CircleShape2D_3fwwl")
[node name="Timer" type="Timer" parent="."]
one_shot = true
[connection signal="body_entered" from="AttackPath/PathFollow2D/Sprite2D/AttackArea" to="." method="_on_attack_area_body_entered"]
[connection signal="timeout" from="Timer" to="." method="_on_timer_timeout"]

View File

@@ -55,6 +55,7 @@ func do_movement(delta: float) -> void:
else:
_do_nav_agent_movement()
func _has_direct_path():
target_cast.target_position = to_local(target.global_position)
target_cast.enabled = true

View File

@@ -14,6 +14,7 @@ func _process(delta: float) -> void:
elapsed_time += delta
main_ui.player_ui.set_elapsed_time(elapsed_time)
func _unhandled_input(event: InputEvent) -> void:
if event.is_action_pressed("ui_cancel"):
main_ui.pause_ui.toggle_pause_ui()

View File

@@ -1,4 +1,4 @@
[gd_scene load_steps=8 format=3 uid="uid://bjg50n7aab3ng"]
[gd_scene load_steps=9 format=3 uid="uid://bjg50n7aab3ng"]
[ext_resource type="Script" uid="uid://brb4ssksmtq8k" path="res://scenes/main.gd" id="1_jyhfs"]
[ext_resource type="PackedScene" uid="uid://4xha2nhf8fya" path="res://scenes/test_level.tscn" id="1_o5qli"]
@@ -7,6 +7,7 @@
[ext_resource type="PackedScene" uid="uid://dy73qrxcgrwg3" path="res://scenes/managers/enemy_manager.tscn" id="5_tbgi4"]
[ext_resource type="PackedScene" uid="uid://bbev8m5g0p3a3" path="res://scenes/pickups/pickup_magnet.tscn" id="6_tefeu"]
[ext_resource type="PackedScene" uid="uid://cr8gj1dlloamp" path="res://scenes/pickups/pickup_hp.tscn" id="7_o6xl0"]
[ext_resource type="PackedScene" uid="uid://c2o1lpm4pimpr" path="res://scenes/managers/projectile_manager.tscn" id="8_tipki"]
[node name="Main" type="Node2D"]
script = ExtResource("1_jyhfs")
@@ -32,3 +33,5 @@ position = Vector2(1697, 414)
[node name="PickupHP" parent="." instance=ExtResource("7_o6xl0")]
position = Vector2(1678, 939)
[node name="ProjectileManager" parent="." instance=ExtResource("8_tipki")]

View File

@@ -26,11 +26,13 @@ func _on_timer_timeout() -> void:
new_enemy.target = target
add_child(new_enemy)
func _on_stop_spawning(val: bool):
if val:
timer.stop()
elif timer.is_stopped():
timer.start()
func _on_set_spawn_rate(val: float):
timer.wait_time = 1 / val

View File

@@ -0,0 +1,3 @@
[gd_scene format=3 uid="uid://c2o1lpm4pimpr"]
[node name="ProjectileManager" type="Node2D"]

View File

@@ -5,7 +5,6 @@ extends CharacterBody2D
@export var main_ui: MainUI
@onready var sprite_2d: Sprite2D = $Sprite2D
@onready var attack_sword: Node2D = $AttackSword
var player_stats: PlayerStats = PlayerStats.new()
var modifiers: Array[PlayerStatsModifier] = []
@@ -61,7 +60,6 @@ func die():
dead = true
remove_from_group("damagable")
get_tree().call_group("enemy", "cheer")
attack_sword.queue_free()
GlobalConst.sig_stop_spawning.emit(true)
sprite_2d.z_index += 10
get_taunted()

View File

@@ -2,7 +2,7 @@
[ext_resource type="Texture2D" uid="uid://5x5wimok8uw2" path="res://assets/sprites/roguelikeChar_transparent.png" id="1_3vyb7"]
[ext_resource type="Script" uid="uid://cvqaxckx4num3" path="res://scenes/player.gd" id="1_g2els"]
[ext_resource type="PackedScene" uid="uid://cdojqe2m4kxx1" path="res://scenes/attacks/attack_sword.tscn" id="3_qhqgy"]
[ext_resource type="PackedScene" uid="uid://dfikvj27k01tu" path="res://scenes/weapons/weapon_sword.tscn" id="3_qhqgy"]
[sub_resource type="CircleShape2D" id="CircleShape2D_3vyb7"]
radius = 8.0
@@ -22,9 +22,6 @@ region_rect = Rect2(0, 104, 16, 14)
[node name="CollisionShape2D" type="CollisionShape2D" parent="."]
shape = SubResource("CircleShape2D_3vyb7")
[node name="AttackSword" parent="." node_paths=PackedStringArray("player") instance=ExtResource("3_qhqgy")]
player = NodePath("..")
[node name="PickupArea" type="Area2D" parent="."]
collision_layer = 0
collision_mask = 24
@@ -33,4 +30,6 @@ monitorable = false
[node name="CollisionShape2D" type="CollisionShape2D" parent="PickupArea"]
shape = SubResource("CircleShape2D_qhqgy")
[node name="WeaponSword" parent="." instance=ExtResource("3_qhqgy")]
[connection signal="area_entered" from="PickupArea" to="." method="_on_pickup_area_area_entered"]

View File

@@ -0,0 +1,42 @@
class_name WeaponBase
extends Node2D
@export var attack_cd: float
@export var attack_damage: float
@export var attack_aoe: float
@export var attack_duration: float
@export var attack_range: float
func _on_attack_cd_timer_timeout() -> void:
do_attack()
func do_attack() -> void:
push_error("%s does not implement do_attack" % self)
func find_target_in_radius() -> EnemyBase:
var space_state: PhysicsDirectSpaceState2D = get_world_2d().direct_space_state
var shape := CircleShape2D.new()
shape.radius = attack_range
var query := PhysicsShapeQueryParameters2D.new()
query.shape = shape
query.transform = Transform2D(0, global_position)
query.collision_mask = 2
query.collide_with_bodies = true
var results := space_state.intersect_shape(query)
if len(results) < 1:
return null
var closest: PhysicsBody2D = results[0]["collider"]
for r in results:
var c: PhysicsBody2D = r["collider"]
if not c.is_in_group(GlobalConst.GROUP_ENEMY):
continue
if (
c.global_position.distance_to(global_position)
< closest.global_position.distance_to(global_position)
):
closest = c
return closest

View File

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

View File

@@ -0,0 +1,11 @@
[gd_scene load_steps=2 format=3 uid="uid://i8mtky2req41"]
[ext_resource type="Script" uid="uid://d6nwfhyethdw" path="res://scenes/weapons/weapon_base.gd" id="1_v4xn6"]
[node name="WeaponBase" type="Node2D"]
script = ExtResource("1_v4xn6")
[node name="AttackCDTimer" type="Timer" parent="."]
autostart = true
[connection signal="timeout" from="AttackCDTimer" to="." method="_on_attack_cd_timer_timeout"]

View File

@@ -0,0 +1,42 @@
class_name WeaponSword
extends WeaponBase
const WEAPON_SWORD_PROJECTILE = preload("res://scenes/weapons/weapon_sword_projectile.tscn")
@onready var targeting_range: Area2D = $TargetingRange
@onready var targeting_range_shape: CollisionShape2D = $TargetingRange/CollisionShape2D
signal projectile_hit(projectile: WeaponSwordProjectile, enemy: EnemyBase)
var _player: Player
func _ready() -> void:
targeting_range_shape.shape.radius = attack_range
projectile_hit.connect(_on_projectile_hit)
_player = get_tree().get_first_node_in_group(GlobalConst.GROUP_PLAYER)
func do_attack() -> void:
var target: EnemyBase = find_target_in_radius()
if not target:
return
var projectile = WEAPON_SWORD_PROJECTILE.instantiate()
projectile.damage = attack_damage
projectile.target = target
projectile.on_hit_sig = projectile_hit
add_child(projectile)
func deal_damage(enemy: EnemyBase):
var crit_chance = _player.player_stats.get_final("crit_chance", _player.modifiers)
var damage_dealt = attack_damage
var is_crit = randf() >= 1 - crit_chance
if is_crit:
damage_dealt *= _player.player_stats.get_final("crit_multiplier", _player.modifiers)
enemy.take_damage(damage_dealt, is_crit)
func _on_projectile_hit(projectile: WeaponSwordProjectile, enemy: EnemyBase):
deal_damage(enemy)

View File

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

View File

@@ -0,0 +1,22 @@
[gd_scene load_steps=4 format=3 uid="uid://dfikvj27k01tu"]
[ext_resource type="PackedScene" uid="uid://i8mtky2req41" path="res://scenes/weapons/weapon_base.tscn" id="1_2dti5"]
[ext_resource type="Script" uid="uid://b072d866r4usq" path="res://scenes/weapons/weapon_sword.gd" id="2_ruf80"]
[sub_resource type="CircleShape2D" id="CircleShape2D_ruf80"]
[node name="WeaponSword" instance=ExtResource("1_2dti5")]
script = ExtResource("2_ruf80")
attack_cd = 1.0
attack_damage = 5.0
attack_aoe = 1.0
attack_duration = 1.0
attack_range = 150.0
[node name="TargetingRange" type="Area2D" parent="." index="1"]
collision_layer = 0
collision_mask = 2
monitorable = false
[node name="CollisionShape2D" type="CollisionShape2D" parent="TargetingRange" index="0"]
shape = SubResource("CircleShape2D_ruf80")

View File

@@ -0,0 +1,40 @@
class_name WeaponSwordProjectile
extends Node2D
@export var speed: float = 500.0
@export var range: float = 200.0
@export var target: Node2D
@export var damage: float
@export var on_hit_sig: Signal
var _direction: Vector2
var _traveled: float = 0.0
var _already_hit: Array[PhysicsBody2D] = []
func _ready() -> void:
_direction = global_position.direction_to(target.global_position).normalized()
_traveled = 0.0
rotation = _direction.angle()
if _direction.x < 0:
rotation += PI
var proj_manager = get_tree().get_first_node_in_group(GlobalConst.GROUP_PROJ_MANAGER)
if not proj_manager:
return
reparent(proj_manager, true)
func _physics_process(delta: float) -> void:
var step = _direction * speed * delta
global_position += step
_traveled += step.length()
if _traveled >= range:
queue_free()
func _on_area_2d_body_entered(body: Node2D) -> void:
if body in _already_hit:
return
on_hit_sig.emit(self, body)
_already_hit.append(body)

View File

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

View File

@@ -0,0 +1,24 @@
[gd_scene load_steps=4 format=3 uid="uid://bv5f47x3ishmf"]
[ext_resource type="Script" uid="uid://c40iaqdubwl0p" path="res://scenes/weapons/weapon_sword_projectile.gd" id="1_asuu4"]
[ext_resource type="Texture2D" uid="uid://dycw7c3484dir" path="res://assets/sprites/sword.png" id="2_pxap4"]
[sub_resource type="RectangleShape2D" id="RectangleShape2D_ygc1t"]
size = Vector2(16, 46)
[node name="WeaponSwordProjectile" type="Node2D"]
script = ExtResource("1_asuu4")
[node name="Sprite2D" type="Sprite2D" parent="."]
texture = ExtResource("2_pxap4")
[node name="Area2D" type="Area2D" parent="."]
collision_layer = 0
collision_mask = 2
monitorable = false
[node name="CollisionShape2D" type="CollisionShape2D" parent="Area2D"]
position = Vector2(0, -5)
shape = SubResource("RectangleShape2D_ygc1t")
[connection signal="body_entered" from="Area2D" to="." method="_on_area_2d_body_entered"]