➕ADD: Personajes y enemigo de testeo

This commit is contained in:
ankdev committed 2026-10-01 14:29:59 +02:00
1 parent 3d356166ca
commit 1b797a3e90
21 files changed
+696 -340

No files matched your search

+13 -2
View File
@@ -1876,13 +1876,24 @@ Actualizar esta sección conforme avance el proyecto.
## NOW
```text
Pendiente de establecer según el estado real del proyecto.
- Estructura mínima separada en game/, player/, enemies/ y levels/test/.
- Player único con movimiento, salto, coyote time y jump buffer.
- Ataque terrestre y ground pound con hitboxes reales.
- Vida, daño, knockback, invulnerabilidad breve y muerte/reinicio del jugador.
- CharacterData con cuatro recursos: singer, guitarist, bassist y drummer.
- Los cuatro personajes usan provisionalmente las mismas animaciones idle/walk de Diego.
- Primer enemigo Walker: patrulla, gira en paredes/bordes, hace daño por contacto,
recibe daño y muere.
- Hurtboxes y zonas de ataque visibles en modo debug con rojo semitransparente.
- Sala levels/test/combat_test.tscn para probar el combate.
```
## NEXT
```text
Pendiente.
- Probar y ajustar tamaños/offsets de hurtboxes e hitboxes jugando la sala de combate.
- Sustituir las animaciones provisionales cuando exista arte para cada integrante.
- Añadir UI provisional de vida si hace falta para seguir afinando el combate.
```
## LATER
-14
View File
@@ -1,14 +0,0 @@
extends Camera2D
# Called when the node enters the scene tree for the first time.
func _ready() -> void:
limit_left = 0
limit_top = 0
limit_right = 3000
limit_bottom = 2000
# Called every frame. 'delta' is the elapsed time since the previous frame.
func _process(delta: float) -> void:
pass
+176
View File
@@ -0,0 +1,176 @@
extends CharacterBody2D
signal died
# First real enemy on purpose: no generic Enemy base class yet.
# Once a second/third enemy exists, we can extract only the behaviour they
# actually share instead of guessing an abstraction in advance.
const PLAYER_HURTBOX_MASK: int = 1 << 3
@export_category("Movement")
@export var move_speed: float = 85.0
@export var max_fall_speed: float = 1200.0
@export_category("Combat")
@export_range(1, 100, 1) var max_health: int = 3
@export_range(0, 100, 1) var contact_damage: int = 1
@export var contact_damage_interval: float = 0.45
@export_category("Debug")
## Enabled while the enemy has no final sprite/animation. The grey rectangle is
## the temporary body; red overlays are the gameplay hurtbox/contact hitbox.
@export var show_debug_hitboxes: bool = true
@onready var body_collision: CollisionShape2D = $CollisionShape2D
@onready var hurtbox_collision: CollisionShape2D = $Hurtbox/CollisionShape2D
@onready var contact_hitbox: Area2D = $ContactHitbox
@onready var contact_collision: CollisionShape2D = $ContactHitbox/CollisionShape2D
@onready var floor_probe: RayCast2D = $FloorProbe
var current_health: int
var move_direction: float = -1.0
var contact_damage_timer: float = 0.0
var is_dead: bool = false
func _ready() -> void:
current_health = max_health
contact_hitbox.collision_layer = 0
contact_hitbox.collision_mask = PLAYER_HURTBOX_MASK
contact_hitbox.monitoring = true
_update_floor_probe()
queue_redraw()
func _physics_process(delta: float) -> void:
if is_dead:
return
contact_damage_timer = max(contact_damage_timer - delta, 0.0)
_apply_gravity(delta)
_turn_before_ledge()
velocity.x = move_direction * move_speed
move_and_slide()
if is_on_wall():
_turn_around()
_damage_player_on_contact()
queue_redraw()
func _apply_gravity(delta: float) -> void:
if is_on_floor():
return
velocity += get_gravity() * delta
velocity.y = min(velocity.y, max_fall_speed)
func _turn_before_ledge() -> void:
# The ray sits just in front of the feet. If it stops finding floor while the
# enemy is grounded, turning here prevents the basic walker from suiciding
# off every platform.
if is_on_floor() and not floor_probe.is_colliding():
_turn_around()
func _turn_around() -> void:
move_direction *= -1.0
_update_floor_probe()
func _update_floor_probe() -> void:
floor_probe.position.x = abs(floor_probe.position.x) * move_direction
func _damage_player_on_contact() -> void:
if contact_damage_timer > 0.0:
return
for area in contact_hitbox.get_overlapping_areas():
var target: Node = area.get_parent()
if target != null and target.has_method("take_damage"):
target.take_damage(contact_damage, global_position)
contact_damage_timer = contact_damage_interval
return
func take_damage(amount: int, source_position: Vector2) -> void:
if amount <= 0 or is_dead:
return
current_health = max(current_health - amount, 0)
if current_health == 0:
_die()
return
# Turn away from the attacker. This gives a tiny readable reaction without
# inventing a hit-stun/knockback system before it is actually needed.
var reaction_direction: float = signf(global_position.x - source_position.x)
if reaction_direction != 0.0:
move_direction = reaction_direction
_update_floor_probe()
func _die() -> void:
is_dead = true
died.emit()
queue_free()
# ============================================================================
# TEMPORARY VISUAL + DEBUG HITBOXES
# ============================================================================
func _draw() -> void:
# The enemy has no sprite yet. This neutral block makes the actual entity
# visible independently from the red debug overlays.
_draw_rectangle_collision(
body_collision,
Color(0.18, 0.18, 0.20, 0.90),
Color(0.85, 0.85, 0.90, 1.0)
)
if not show_debug_hitboxes:
return
# Hurtbox: where punches / ground pound are allowed to damage the enemy.
_draw_rectangle_collision(
hurtbox_collision,
Color(1.0, 0.05, 0.05, 0.12),
Color(1.0, 0.05, 0.05, 0.55)
)
# Contact hitbox: slightly larger, so it is obvious exactly when touching
# the enemy can damage the player.
_draw_rectangle_collision(
contact_collision,
Color(1.0, 0.05, 0.05, 0.18),
Color(1.0, 0.05, 0.05, 0.85),
contact_hitbox.position
)
func _draw_rectangle_collision(
collision: CollisionShape2D,
fill_color: Color,
outline_color: Color,
extra_offset: Vector2 = Vector2.ZERO
) -> void:
var rectangle: RectangleShape2D = collision.shape as RectangleShape2D
if rectangle == null:
return
var center: Vector2 = collision.position + extra_offset
var rect: Rect2 = Rect2(center - rectangle.size / 2.0, rectangle.size)
draw_rect(rect, fill_color, true)
draw_rect(rect, outline_color, false, 2.0)
+1
View File
@@ -0,0 +1 @@
uid://dbt03i3eiutv5
+42
View File
@@ -0,0 +1,42 @@
[gd_scene load_steps=4 format=3]
[ext_resource type="Script" path="res://enemies/walker/walker.gd" id="1_walker"]
[sub_resource type="RectangleShape2D" id="BodyShape"]
size = Vector2(42, 78)
[sub_resource type="RectangleShape2D" id="ContactShape"]
size = Vector2(58, 88)
[node name="Walker" type="CharacterBody2D"]
collision_layer = 4
collision_mask = 1
script = ExtResource("1_walker")
[node name="CollisionShape2D" type="CollisionShape2D" parent="."]
position = Vector2(0, 0)
shape = SubResource("BodyShape")
[node name="Hurtbox" type="Area2D" parent="."]
collision_layer = 16
collision_mask = 0
monitoring = false
monitorable = true
[node name="CollisionShape2D" type="CollisionShape2D" parent="Hurtbox"]
shape = SubResource("BodyShape")
[node name="ContactHitbox" type="Area2D" parent="."]
collision_layer = 0
collision_mask = 8
monitoring = true
monitorable = false
[node name="CollisionShape2D" type="CollisionShape2D" parent="ContactHitbox"]
shape = SubResource("ContactShape")
[node name="FloorProbe" type="RayCast2D" parent="."]
position = Vector2(24, 34)
target_position = Vector2(0, 34)
collision_mask = 1
enabled = true
+7
View File
@@ -0,0 +1,7 @@
[gd_scene load_steps=2 format=3]
[ext_resource type="PackedScene" path="res://levels/test/combat_test.tscn" id="1_test"]
[node name="Main" type="Node"]
[node name="CombatTest" parent="." instance=ExtResource("1_test")]
+47
View File
@@ -0,0 +1,47 @@
[gd_scene load_steps=6 format=3]
[ext_resource type="Texture2D" uid="uid://jmadeu351yh6" path="res://assets/photo_5837080277660930029_w.jpg" id="1_background"]
[ext_resource type="PackedScene" path="res://player/player.tscn" id="2_player"]
[ext_resource type="PackedScene" path="res://enemies/walker/walker.tscn" id="3_walker"]
[sub_resource type="RectangleShape2D" id="FloorShape"]
size = Vector2(1171, 20)
[sub_resource type="RectangleShape2D" id="WallShape"]
size = Vector2(136, 156)
[node name="CombatTest" type="Node2D"]
[node name="Background" type="Sprite2D" parent="."]
position = Vector2(576, 324)
scale = Vector2(0.8181818, 0.84374994)
texture = ExtResource("1_background")
[node name="World" type="StaticBody2D" parent="."]
collision_layer = 1
collision_mask = 0
[node name="Floor" type="CollisionShape2D" parent="World"]
position = Vector2(585, 465)
shape = SubResource("FloorShape")
[node name="LeftWall" type="CollisionShape2D" parent="World"]
position = Vector2(77, 361)
shape = SubResource("WallShape")
[node name="Player" parent="." instance=ExtResource("2_player")]
position = Vector2(95, 215)
[node name="Walker" parent="." instance=ExtResource("3_walker")]
position = Vector2(650, 390)
[node name="DebugInfo" type="Label" parent="."]
offset_left = 16.0
offset_top = 14.0
offset_right = 560.0
offset_bottom = 64.0
theme_override_colors/font_color = Color(1, 1, 1, 1)
theme_override_colors/font_shadow_color = Color(0, 0, 0, 0.9)
theme_override_constants/shadow_offset_x = 2
theme_override_constants/shadow_offset_y = 2
text = "A / D: mover W: saltar J: atacar\nRojo = hurtboxes y zonas de ataque reales"
-30
View File
@@ -1,30 +0,0 @@
[gd_scene format=3 uid="uid://dux51kbeg032e"]
[ext_resource type="Texture2D" uid="uid://jmadeu351yh6" path="res://assets/photo_5837080277660930029_w.jpg" id="1_1bvp3"]
[ext_resource type="PackedScene" uid="uid://nwbjxttgghpq" path="res://assets/characters/ankarde.tscn" id="2_0xm2m"]
[sub_resource type="RectangleShape2D" id="RectangleShape2D_rcrqh"]
size = Vector2(1171, 20)
[sub_resource type="RectangleShape2D" id="RectangleShape2D_741h5"]
size = Vector2(136, 156)
[node name="Main" type="Node2D" unique_id=1908609963]
[node name="Photo5837080277660930029W" type="Sprite2D" parent="." unique_id=2065196895]
position = Vector2(576, 324)
scale = Vector2(0.8181818, 0.84374994)
texture = ExtResource("1_1bvp3")
[node name="Limits" type="StaticBody2D" parent="." unique_id=648220000]
[node name="CollisionShape2D" type="CollisionShape2D" parent="Limits" unique_id=423774102]
position = Vector2(585, 465)
shape = SubResource("RectangleShape2D_rcrqh")
[node name="CollisionShape2D2" type="CollisionShape2D" parent="Limits" unique_id=296247895]
position = Vector2(77, 361)
shape = SubResource("RectangleShape2D_741h5")
[node name="Player" parent="." unique_id=309719231 instance=ExtResource("2_0xm2m")]
position = Vector2(95, 215)
+10
View File
@@ -0,0 +1,10 @@
extends Camera2D
## Límites provisionales de cámara para la sala de pruebas actual.
## Cuando existan niveles reales, estos límites podrán configurarse por nivel.
func _ready() -> void:
limit_left = 0
limit_top = 0
limit_right = 3000
limit_bottom = 2000
File renamed without changes.
+24
View File
@@ -0,0 +1,24 @@
extends Resource
class_name CharacterData
## Datos que pueden variar entre los cuatro integrantes de la banda.
##
## La lógica vive en Player. Este recurso solo contiene configuración para que
## cambiar de personaje no obligue a duplicar ni modificar player.gd.
@export_category("Identity")
@export var id: StringName = &"character"
@export var display_name: String = "Character"
@export_category("Visuals")
## Los cuatro recursos apuntan por ahora a las mismas animaciones de Diego.
## Más adelante cada integrante podrá tener su propio SpriteFrames sin tocar Player.
@export var sprite_frames: SpriteFrames
@export_category("Stats")
@export_range(1, 100, 1) var max_health: int = 5
@export var move_speed: float = 300.0
@export var jump_velocity: float = -600.0
@export_range(0, 100, 1) var basic_attack_damage: int = 1
@export_range(0, 100, 1) var air_attack_damage: int = 3
+1
View File
@@ -0,0 +1 @@
uid://bi04q2fe5goqn
+15
View File
@@ -0,0 +1,15 @@
[gd_resource type="Resource" script_class="CharacterData" load_steps=3 format=3]
[ext_resource type="Script" path="res://player/character_data.gd" id="1_data"]
[ext_resource type="SpriteFrames" path="res://player/characters/shared/prototype_sprite_frames.tres" id="2_frames"]
[resource]
script = ExtResource("1_data")
id = &"bassist"
display_name = "Bassist"
sprite_frames = ExtResource("2_frames")
max_health = 5
move_speed = 300.0
jump_velocity = -600.0
basic_attack_damage = 1
air_attack_damage = 3
+15
View File
@@ -0,0 +1,15 @@
[gd_resource type="Resource" script_class="CharacterData" load_steps=3 format=3]
[ext_resource type="Script" path="res://player/character_data.gd" id="1_data"]
[ext_resource type="SpriteFrames" path="res://player/characters/shared/prototype_sprite_frames.tres" id="2_frames"]
[resource]
script = ExtResource("1_data")
id = &"drummer"
display_name = "Drummer"
sprite_frames = ExtResource("2_frames")
max_health = 5
move_speed = 300.0
jump_velocity = -600.0
basic_attack_damage = 1
air_attack_damage = 3
+15
View File
@@ -0,0 +1,15 @@
[gd_resource type="Resource" script_class="CharacterData" load_steps=3 format=3]
[ext_resource type="Script" path="res://player/character_data.gd" id="1_data"]
[ext_resource type="SpriteFrames" path="res://player/characters/shared/prototype_sprite_frames.tres" id="2_frames"]
[resource]
script = ExtResource("1_data")
id = &"guitarist"
display_name = "Guitarist"
sprite_frames = ExtResource("2_frames")
max_health = 5
move_speed = 300.0
jump_velocity = -600.0
basic_attack_damage = 1
air_attack_damage = 3
@@ -1,146 +1,141 @@
[gd_scene format=3 uid="uid://nwbjxttgghpq"]
[gd_resource type="SpriteFrames" load_steps=36 format=3]
[ext_resource type="Script" uid="uid://dccfebno1gvh7" path="res://player.gd" id="1_seisg"]
[ext_resource type="Texture2D" uid="uid://cjxrw1p7eh82i" path="res://assets/characters/ankarde/Diego-idle-trimmed.png" id="2_yi62x"]
[ext_resource type="Texture2D" uid="uid://dg1x17ci14p4f" path="res://assets/characters/ankarde/Diego-walk-trimmed.png" id="3_sn41o"]
[ext_resource type="Script" uid="uid://kg3i7b0dj2p8" path="res://assets/characters/camera_2d.gd" id="4_yi62x"]
[sub_resource type="RectangleShape2D" id="RectangleShape2D_rcrqh"]
size = Vector2(65, 115)
[ext_resource type="Texture2D" uid="uid://cjxrw1p7eh82i" path="res://assets/characters/ankarde/Diego-idle-trimmed.png" id="1_idle"]
[ext_resource type="Texture2D" uid="uid://dg1x17ci14p4f" path="res://assets/characters/ankarde/Diego-walk-trimmed.png" id="2_walk"]
[sub_resource type="AtlasTexture" id="AtlasTexture_lquwl"]
atlas = ExtResource("2_yi62x")
atlas = ExtResource("1_idle")
region = Rect2(0, 0, 256, 256)
[sub_resource type="AtlasTexture" id="AtlasTexture_7mycd"]
atlas = ExtResource("2_yi62x")
atlas = ExtResource("1_idle")
region = Rect2(256, 0, 256, 256)
[sub_resource type="AtlasTexture" id="AtlasTexture_272bh"]
atlas = ExtResource("2_yi62x")
atlas = ExtResource("1_idle")
region = Rect2(512, 0, 256, 256)
[sub_resource type="AtlasTexture" id="AtlasTexture_5vw27"]
atlas = ExtResource("2_yi62x")
atlas = ExtResource("1_idle")
region = Rect2(768, 0, 256, 256)
[sub_resource type="AtlasTexture" id="AtlasTexture_kek77"]
atlas = ExtResource("2_yi62x")
atlas = ExtResource("1_idle")
region = Rect2(1024, 0, 256, 256)
[sub_resource type="AtlasTexture" id="AtlasTexture_4c57u"]
atlas = ExtResource("2_yi62x")
atlas = ExtResource("1_idle")
region = Rect2(0, 256, 256, 256)
[sub_resource type="AtlasTexture" id="AtlasTexture_efxa6"]
atlas = ExtResource("2_yi62x")
atlas = ExtResource("1_idle")
region = Rect2(256, 256, 256, 256)
[sub_resource type="AtlasTexture" id="AtlasTexture_dg77c"]
atlas = ExtResource("2_yi62x")
atlas = ExtResource("1_idle")
region = Rect2(512, 256, 256, 256)
[sub_resource type="AtlasTexture" id="AtlasTexture_ycdy4"]
atlas = ExtResource("2_yi62x")
atlas = ExtResource("1_idle")
region = Rect2(768, 256, 256, 256)
[sub_resource type="AtlasTexture" id="AtlasTexture_w48qg"]
atlas = ExtResource("2_yi62x")
atlas = ExtResource("1_idle")
region = Rect2(1024, 256, 256, 256)
[sub_resource type="AtlasTexture" id="AtlasTexture_vivmo"]
atlas = ExtResource("2_yi62x")
atlas = ExtResource("1_idle")
region = Rect2(0, 512, 256, 256)
[sub_resource type="AtlasTexture" id="AtlasTexture_2cqfq"]
atlas = ExtResource("2_yi62x")
atlas = ExtResource("1_idle")
region = Rect2(256, 512, 256, 256)
[sub_resource type="AtlasTexture" id="AtlasTexture_yaehf"]
atlas = ExtResource("3_sn41o")
atlas = ExtResource("2_walk")
region = Rect2(0, 0, 256, 256)
[sub_resource type="AtlasTexture" id="AtlasTexture_074og"]
atlas = ExtResource("3_sn41o")
atlas = ExtResource("2_walk")
region = Rect2(256, 0, 256, 256)
[sub_resource type="AtlasTexture" id="AtlasTexture_cegan"]
atlas = ExtResource("3_sn41o")
atlas = ExtResource("2_walk")
region = Rect2(512, 0, 256, 256)
[sub_resource type="AtlasTexture" id="AtlasTexture_82xsv"]
atlas = ExtResource("3_sn41o")
atlas = ExtResource("2_walk")
region = Rect2(768, 0, 256, 256)
[sub_resource type="AtlasTexture" id="AtlasTexture_getpj"]
atlas = ExtResource("3_sn41o")
atlas = ExtResource("2_walk")
region = Rect2(1024, 0, 256, 256)
[sub_resource type="AtlasTexture" id="AtlasTexture_ryguw"]
atlas = ExtResource("3_sn41o")
atlas = ExtResource("2_walk")
region = Rect2(0, 256, 256, 256)
[sub_resource type="AtlasTexture" id="AtlasTexture_d13ii"]
atlas = ExtResource("3_sn41o")
atlas = ExtResource("2_walk")
region = Rect2(256, 256, 256, 256)
[sub_resource type="AtlasTexture" id="AtlasTexture_1u8w0"]
atlas = ExtResource("3_sn41o")
atlas = ExtResource("2_walk")
region = Rect2(512, 256, 256, 256)
[sub_resource type="AtlasTexture" id="AtlasTexture_0odxb"]
atlas = ExtResource("3_sn41o")
atlas = ExtResource("2_walk")
region = Rect2(768, 256, 256, 256)
[sub_resource type="AtlasTexture" id="AtlasTexture_lswn8"]
atlas = ExtResource("3_sn41o")
atlas = ExtResource("2_walk")
region = Rect2(1024, 256, 256, 256)
[sub_resource type="AtlasTexture" id="AtlasTexture_a6jrf"]
atlas = ExtResource("3_sn41o")
atlas = ExtResource("2_walk")
region = Rect2(0, 512, 256, 256)
[sub_resource type="AtlasTexture" id="AtlasTexture_xuqvo"]
atlas = ExtResource("3_sn41o")
atlas = ExtResource("2_walk")
region = Rect2(256, 512, 256, 256)
[sub_resource type="AtlasTexture" id="AtlasTexture_qsp4k"]
atlas = ExtResource("3_sn41o")
atlas = ExtResource("2_walk")
region = Rect2(512, 512, 256, 256)
[sub_resource type="AtlasTexture" id="AtlasTexture_kq58d"]
atlas = ExtResource("3_sn41o")
atlas = ExtResource("2_walk")
region = Rect2(768, 512, 256, 256)
[sub_resource type="AtlasTexture" id="AtlasTexture_seu75"]
atlas = ExtResource("3_sn41o")
atlas = ExtResource("2_walk")
region = Rect2(1024, 512, 256, 256)
[sub_resource type="AtlasTexture" id="AtlasTexture_htxhm"]
atlas = ExtResource("3_sn41o")
atlas = ExtResource("2_walk")
region = Rect2(0, 768, 256, 256)
[sub_resource type="AtlasTexture" id="AtlasTexture_jq2sk"]
atlas = ExtResource("3_sn41o")
atlas = ExtResource("2_walk")
region = Rect2(256, 768, 256, 256)
[sub_resource type="AtlasTexture" id="AtlasTexture_4k2k6"]
atlas = ExtResource("3_sn41o")
atlas = ExtResource("2_walk")
region = Rect2(512, 768, 256, 256)
[sub_resource type="AtlasTexture" id="AtlasTexture_0rl1p"]
atlas = ExtResource("3_sn41o")
atlas = ExtResource("2_walk")
region = Rect2(768, 768, 256, 256)
[sub_resource type="AtlasTexture" id="AtlasTexture_ok6jj"]
atlas = ExtResource("3_sn41o")
atlas = ExtResource("2_walk")
region = Rect2(1024, 768, 256, 256)
[sub_resource type="AtlasTexture" id="AtlasTexture_facbu"]
atlas = ExtResource("3_sn41o")
atlas = ExtResource("2_walk")
region = Rect2(0, 1024, 256, 256)
[sub_resource type="SpriteFrames" id="SpriteFrames_741h5"]
[resource]
animations = [{
"frames": [{
"duration": 1.0,
@@ -251,22 +246,3 @@ animations = [{
"name": &"walk",
"speed": 5.0
}]
[node name="Player" type="CharacterBody2D" unique_id=309719231]
script = ExtResource("1_seisg")
[node name="CollisionShape2D" type="CollisionShape2D" parent="." unique_id=1243984711]
position = Vector2(0.5, 5.5)
shape = SubResource("RectangleShape2D_rcrqh")
[node name="AnimatedSprite2D" type="AnimatedSprite2D" parent="." unique_id=21110687]
scale = Vector2(0.80251265, 0.8025128)
sprite_frames = SubResource("SpriteFrames_741h5")
animation = &"walk"
frame_progress = 0.7021592
speed_scale = 2.0
[node name="Camera2D" type="Camera2D" parent="." unique_id=1765495955]
scale = Vector2(0.5, 0.5)
zoom = Vector2(2, 2)
script = ExtResource("4_yi62x")
+15
View File
@@ -0,0 +1,15 @@
[gd_resource type="Resource" script_class="CharacterData" load_steps=3 format=3]
[ext_resource type="Script" path="res://player/character_data.gd" id="1_data"]
[ext_resource type="SpriteFrames" path="res://player/characters/shared/prototype_sprite_frames.tres" id="2_frames"]
[resource]
script = ExtResource("1_data")
id = &"singer"
display_name = "Singer"
sprite_frames = ExtResource("2_frames")
max_health = 5
move_speed = 300.0
jump_velocity = -600.0
basic_attack_damage = 1
air_attack_damage = 3
+211 -232
View File
@@ -1,57 +1,85 @@
extends CharacterBody2D
signal health_changed(current_health: int, max_health: int)
signal died
# Collision layers used by this first playable slice.
# Keeping the numbers here makes the scene setup easier to understand:
# 1 -> world
# 2 -> player body
# 3 -> enemy body
# 4 -> player hurtbox
# 5 -> enemy hurtbox
const ENEMY_HURTBOX_MASK: int = 1 << 4
# ============================================================================
# MOVIMIENTO
# CHARACTER
# ============================================================================
@export_category("Character")
## Resource that identifies the selected band member and contains the stats
## that are allowed to differ between characters.
@export var character_data: CharacterData
# ============================================================================
# MOVEMENT
# ============================================================================
@export_category("Movement")
@export var speed: float = 300.0
@export var ground_acceleration: float = 2200.0
@export var ground_friction: float = 2600.0
@export var air_acceleration: float = 1000.0
# ============================================================================
# SALTO
# JUMP
# ============================================================================
@export_category("Jump")
@export var jump_velocity: float = -600.0
# Permite saltar ligeramente después de abandonar el suelo.
## Allows a jump a few milliseconds after walking off a ledge.
@export var coyote_time: float = 0.12
# Permite pulsar salto ligeramente antes de tocar el suelo.
## Remembers an early jump press just before landing.
@export var jump_buffer_time: float = 0.12
# Al soltar el botón durante la subida reducimos la velocidad vertical.
# Cuanto más pequeño, más brusco será el salto corto.
## Releasing jump while rising cuts the jump short.
@export_range(0.1, 1.0) var jump_cut_multiplier: float = 0.45
# La gravedad normal durante la subida.
@export var gravity_multiplier: float = 1.0
# La caída es más rápida que la subida.
@export var fall_gravity_multiplier: float = 1.7
@export var max_fall_speed: float = 1200.0
# ============================================================================
# PUÑETAZO
# DAMAGE RECEIVED
# ============================================================================
@export_category("Damage received")
## Short grace period so a contact hitbox cannot drain all health in one frame.
@export var invulnerability_duration: float = 0.65
## Horizontal and vertical impulse applied when the player is hit.
@export var damage_knockback: Vector2 = Vector2(300.0, 260.0)
# ============================================================================
# PUNCH
# ============================================================================
@export_category("Punch")
@export var punch_damage: int = 1
@export var punch_duration: float = 0.13
@export var punch_cooldown: float = 0.28
@export var punch_hitbox_size := Vector2(55.0, 38.0)
@export var punch_offset := Vector2(38.0, 0.0)
@export var punch_hitbox_size: Vector2 = Vector2(55.0, 38.0)
@export var punch_offset: Vector2 = Vector2(48.0, 0.0)
# ============================================================================
@@ -60,19 +88,17 @@ extends CharacterBody2D
@export_category("Ground Pound")
@export var ground_pound_damage: int = 3
# Velocidad mínima que adquiere al comenzar el golpe hacia abajo.
## Minimum downward speed when the air attack starts.
@export var ground_pound_start_speed: float = 450.0
# Gravedad adicional mientras cae haciendo ground pound.
## Extra gravity while performing the air attack.
@export var ground_pound_gravity_multiplier: float = 4.5
# Control horizontal mientras cae.
## Small amount of steering kept during the fall.
@export_range(0.0, 1.0) var ground_pound_horizontal_control: float = 0.25
@export var ground_pound_radius: float = 85.0
@export var ground_pound_offset := Vector2(0.0, 20.0)
@export var ground_pound_offset: Vector2 = Vector2(0.0, 28.0)
# ============================================================================
@@ -81,121 +107,122 @@ extends CharacterBody2D
@export_category("Debug")
@export var show_attack_hitboxes: bool = true
## Draws the actual hurtbox and attack areas used by gameplay.
## It is deliberately enabled while the final animations do not exist.
@export var show_debug_hitboxes: bool = true
## Keeps the ground-pound area visible briefly after impact.
@export var debug_impact_duration: float = 0.18
# ============================================================================
# NODOS
# NODES
# ============================================================================
@onready var animated_sprite: AnimatedSprite2D = $AnimatedSprite2D
@onready var hurtbox_collision: CollisionShape2D = $Hurtbox/CollisionShape2D
@onready var punch_hitbox: Area2D = $PunchHitbox
@onready var punch_collision: CollisionShape2D = $PunchHitbox/CollisionShape2D
@onready var ground_pound_hitbox: Area2D = $GroundPoundHitbox
@onready var ground_pound_collision: CollisionShape2D = \
$GroundPoundHitbox/CollisionShape2D
@onready var ground_pound_collision: CollisionShape2D = $GroundPoundHitbox/CollisionShape2D
# ============================================================================
# ESTADO INTERNO
# RUNTIME STATE
# ============================================================================
var current_health: int = 1
var facing_direction: float = 1.0
var coyote_timer: float = 0.0
var jump_buffer_timer: float = 0.0
var invulnerability_timer: float = 0.0
var punch_timer: float = 0.0
var attack_cooldown_timer: float = 0.0
var ground_pound_active: bool = false
var ground_pound_debug_timer: float = 0.0
var is_dead: bool = false
# Evita que un enemigo reciba daño 60 veces durante el mismo puñetazo.
## Prevents one target from receiving damage every physics frame during the
## same punch. A new punch starts with an empty set again.
var punch_hit_targets: Dictionary = {}
func _ready() -> void:
_setup_hitboxes()
if character_data == null:
push_error("Player needs a CharacterData resource.")
return
current_health = character_data.max_health
if character_data.sprite_frames != null:
animated_sprite.sprite_frames = character_data.sprite_frames
_setup_attack_hitboxes()
_update_facing()
_update_animation()
health_changed.emit(current_health, character_data.max_health)
func _physics_process(delta: float) -> void:
var was_on_floor := is_on_floor()
if character_data == null or is_dead:
return
var was_on_floor: bool = is_on_floor()
_update_timers(delta)
_update_jump_windows(delta)
_handle_attack_input()
_handle_jump()
_apply_gravity(delta)
_handle_horizontal_movement(delta)
_update_punch_damage()
_update_animation()
move_and_slide()
# El impacto debe comprobarse DESPUÉS de move_and_slide(),
# porque es entonces cuando sabemos si hemos aterrizado.
# Landing is only reliable after move_and_slide() has resolved collisions.
if ground_pound_active and not was_on_floor and is_on_floor():
_ground_pound_impact()
_update_animation()
queue_redraw()
# ============================================================================
# MOVIMIENTO
# MOVEMENT
# ============================================================================
func _handle_horizontal_movement(delta: float) -> void:
var direction := Input.get_axis("move_left", "move_right")
var direction: float = Input.get_axis("move_left", "move_right")
if direction != 0.0:
facing_direction = sign(direction)
facing_direction = signf(direction)
_update_facing()
if ground_pound_active:
var target_speed := (
direction
* speed
* ground_pound_horizontal_control
)
var target_speed: float = direction * character_data.move_speed
if ground_pound_active:
target_speed *= ground_pound_horizontal_control
velocity.x = move_toward(
velocity.x,
target_speed,
air_acceleration * delta
)
return
if is_on_floor():
if direction != 0.0:
velocity.x = move_toward(
velocity.x,
direction * speed,
ground_acceleration * delta
)
else:
velocity.x = move_toward(
velocity.x,
0.0,
ground_friction * delta
)
var acceleration: float = ground_acceleration if direction != 0.0 else ground_friction
velocity.x = move_toward(velocity.x, target_speed, acceleration * delta)
else:
velocity.x = move_toward(
velocity.x,
direction * speed,
air_acceleration * delta
)
velocity.x = move_toward(velocity.x, target_speed, air_acceleration * delta)
func _update_facing() -> void:
animated_sprite.flip_h = facing_direction < 0.0
# Only the horizontal offset changes when turning around. The hitbox size
# stays identical for both directions.
punch_hitbox.position = Vector2(
abs(punch_offset.x) * facing_direction,
punch_offset.y
@@ -203,37 +230,25 @@ func _update_facing() -> void:
# ============================================================================
# GRAVEDAD
# GRAVITY AND JUMP
# ============================================================================
func _apply_gravity(delta: float) -> void:
if is_on_floor():
return
var gravity := get_gravity()
var gravity: Vector2 = get_gravity()
var multiplier: float = gravity_multiplier
if ground_pound_active:
velocity += (
gravity
* ground_pound_gravity_multiplier
* delta
)
multiplier = ground_pound_gravity_multiplier
elif velocity.y > 0.0:
# Más gravedad al caer.
velocity += gravity * fall_gravity_multiplier * delta
else:
# Gravedad normal durante la subida.
velocity += gravity * gravity_multiplier * delta
multiplier = fall_gravity_multiplier
velocity += gravity * multiplier * delta
velocity.y = min(velocity.y, max_fall_speed)
# ============================================================================
# SALTO
# ============================================================================
func _update_jump_windows(delta: float) -> void:
if is_on_floor():
coyote_timer = coyote_time
@@ -243,10 +258,7 @@ func _update_jump_windows(delta: float) -> void:
if Input.is_action_just_pressed("jump"):
jump_buffer_timer = jump_buffer_time
else:
jump_buffer_timer = max(
jump_buffer_timer - delta,
0.0
)
jump_buffer_timer = max(jump_buffer_timer - delta, 0.0)
func _handle_jump() -> void:
@@ -255,27 +267,16 @@ func _handle_jump() -> void:
and coyote_timer > 0.0
and not ground_pound_active
):
velocity.y = jump_velocity
velocity.y = character_data.jump_velocity
jump_buffer_timer = 0.0
coyote_timer = 0.0
# Salto variable.
#
# Mantienes pulsado:
# salto alto
#
# Sueltas pronto:
# salto corto
if (
Input.is_action_just_released("jump")
and velocity.y < 0.0
):
if Input.is_action_just_released("jump") and velocity.y < 0.0:
velocity.y *= jump_cut_multiplier
# ============================================================================
# ATAQUE
# ATTACK INPUT
# ============================================================================
func _handle_attack_input() -> void:
@@ -292,13 +293,12 @@ func _handle_attack_input() -> void:
# ============================================================================
# PUÑETAZO
# PUNCH
# ============================================================================
func _start_punch() -> void:
punch_timer = punch_duration
attack_cooldown_timer = punch_cooldown
punch_hit_targets.clear()
@@ -306,19 +306,8 @@ func _update_punch_damage() -> void:
if punch_timer <= 0.0:
return
for body in punch_hitbox.get_overlapping_bodies():
_damage_target(
body,
punch_damage,
punch_hit_targets
)
for area in punch_hitbox.get_overlapping_areas():
_damage_target(
area,
punch_damage,
punch_hit_targets
)
_damage_target(area, character_data.basic_attack_damage, punch_hit_targets)
# ============================================================================
@@ -332,11 +321,9 @@ func _start_ground_pound() -> void:
ground_pound_active = true
attack_cooldown_timer = punch_cooldown
# Si todavía estamos subiendo, cortamos inmediatamente la subida.
velocity.y = max(
velocity.y,
ground_pound_start_speed
)
# If the player is still rising, the attack immediately turns the movement
# into a fall instead of waiting for the jump apex.
velocity.y = max(velocity.y, ground_pound_start_speed)
func _ground_pound_impact() -> void:
@@ -345,174 +332,166 @@ func _ground_pound_impact() -> void:
var hit_targets: Dictionary = {}
for body in ground_pound_hitbox.get_overlapping_bodies():
_damage_target(
body,
ground_pound_damage,
hit_targets
)
for area in ground_pound_hitbox.get_overlapping_areas():
_damage_target(
area,
ground_pound_damage,
hit_targets
)
_damage_target(area, character_data.air_attack_damage, hit_targets)
# ============================================================================
# DAÑO
# DEALING AND RECEIVING DAMAGE
# ============================================================================
func _damage_target(
collider: Node,
damage: int,
hit_targets: Dictionary
) -> void:
if collider == self:
func _damage_target(collider: Node, damage: int, hit_targets: Dictionary) -> void:
var target: Node = collider
# Hurtboxes are Area2D children. Damage belongs to the entity that owns the
# hurtbox, so we ask the parent for take_damage().
if not target.has_method("take_damage"):
target = target.get_parent()
if target == null or not target.has_method("take_damage"):
return
var target := collider
# Esto permite que el enemigo tenga una Hurtbox Area2D.
if not target.has_method("take_damage"):
var parent := target.get_parent()
if parent != null and parent.has_method("take_damage"):
target = parent
if not target.has_method("take_damage"):
return
var target_id := target.get_instance_id()
var target_id: int = target.get_instance_id()
if hit_targets.has(target_id):
return
hit_targets[target_id] = true
target.take_damage(damage, global_position)
target.take_damage(
damage,
global_position
)
func take_damage(amount: int, source_position: Vector2) -> void:
if amount <= 0 or is_dead or invulnerability_timer > 0.0:
return
current_health = max(current_health - amount, 0)
invulnerability_timer = invulnerability_duration
# Knock the player away from the source. If both centers are aligned, use
# the opposite of the current facing direction as a deterministic fallback.
var knockback_direction: float = signf(global_position.x - source_position.x)
if knockback_direction == 0.0:
knockback_direction = -facing_direction
velocity.x = knockback_direction * damage_knockback.x
velocity.y = -damage_knockback.y
health_changed.emit(current_health, character_data.max_health)
if current_health == 0:
_die()
func _die() -> void:
is_dead = true
died.emit()
# Reloading from a deferred call avoids changing the scene tree while a
# physics overlap is still being processed.
call_deferred("_reload_level")
func _reload_level() -> void:
get_tree().reload_current_scene()
# ============================================================================
# TIMERS
# TIMERS AND ANIMATION
# ============================================================================
func _update_timers(delta: float) -> void:
punch_timer = max(
punch_timer - delta,
0.0
)
punch_timer = max(punch_timer - delta, 0.0)
attack_cooldown_timer = max(attack_cooldown_timer - delta, 0.0)
ground_pound_debug_timer = max(ground_pound_debug_timer - delta, 0.0)
invulnerability_timer = max(invulnerability_timer - delta, 0.0)
attack_cooldown_timer = max(
attack_cooldown_timer - delta,
0.0
)
ground_pound_debug_timer = max(
ground_pound_debug_timer - delta,
0.0
)
# ============================================================================
# ANIMACIONES
# ============================================================================
func _update_animation() -> void:
if ground_pound_active:
# There are only idle and walk animations at the moment. Until proper jump
# and attack animations arrive, walk is used only for horizontal walking and
# idle is the safe fallback for every other state.
if is_on_floor() and abs(velocity.x) > 10.0:
animated_sprite.play("walk")
elif not is_on_floor():
animated_sprite.play("walk")
elif abs(velocity.x) > 10.0:
animated_sprite.play("walk")
else:
animated_sprite.play("idle")
# ============================================================================
# CONFIGURACIÓN DE HITBOX
# HITBOX SETUP
# ============================================================================
func _setup_hitboxes() -> void:
# Puñetazo
var punch_shape := RectangleShape2D.new()
func _setup_attack_hitboxes() -> void:
var punch_shape: RectangleShape2D = RectangleShape2D.new()
punch_shape.size = punch_hitbox_size
punch_collision.shape = punch_shape
punch_hitbox.collision_layer = 0
punch_hitbox.collision_mask = 2
punch_hitbox.collision_mask = ENEMY_HURTBOX_MASK
punch_hitbox.monitoring = true
# Ground pound
var pound_shape := CircleShape2D.new()
var pound_shape: CircleShape2D = CircleShape2D.new()
pound_shape.radius = ground_pound_radius
ground_pound_collision.shape = pound_shape
ground_pound_hitbox.position = ground_pound_offset
ground_pound_hitbox.collision_layer = 0
ground_pound_hitbox.collision_mask = 2
ground_pound_hitbox.collision_mask = ENEMY_HURTBOX_MASK
ground_pound_hitbox.monitoring = true
_update_facing()
# ============================================================================
# DEBUG HITBOXES
# DEBUG HITBOX DRAWING
# ============================================================================
func _draw() -> void:
if not show_attack_hitboxes:
if not show_debug_hitboxes:
return
# Puñetazo
# Player hurtbox: always visible and deliberately subtle.
_draw_rectangle_collision(
hurtbox_collision,
Color(1.0, 0.08, 0.08, 0.10),
Color(1.0, 0.08, 0.08, 0.45)
)
# Punch: only visible while it can actually deal damage.
if punch_timer > 0.0:
var center := punch_hitbox.position
var rect := Rect2(
center - punch_hitbox_size / 2.0,
punch_hitbox_size
_draw_rectangle_collision(
punch_collision,
Color(1.0, 0.05, 0.05, 0.25),
Color(1.0, 0.05, 0.05, 0.95),
punch_hitbox.position
)
draw_rect(
rect,
Color(1.0, 0.1, 0.1, 0.25),
true
)
draw_rect(
rect,
Color(1.0, 0.1, 0.1, 0.9),
false,
2.0
)
# Ground pound:
# solamente mostramos el área cuando realmente produce daño.
if ground_pound_debug_timer > 0.0:
var center := ground_pound_hitbox.position
# Ground pound: show the real damage radius throughout the fall, then make
# it stronger for a few frames at the exact impact moment.
if ground_pound_active or ground_pound_debug_timer > 0.0:
var fill_alpha: float = 0.14 if ground_pound_active else 0.28
var outline_alpha: float = 0.60 if ground_pound_active else 0.95
draw_circle(
center,
ground_pound_hitbox.position,
ground_pound_radius,
Color(1.0, 0.5, 0.0, 0.20)
Color(1.0, 0.18, 0.05, fill_alpha)
)
draw_arc(
center,
ground_pound_hitbox.position,
ground_pound_radius,
0.0,
TAU,
48,
Color(1.0, 0.5, 0.0, 0.9),
3.0
Color(1.0, 0.18, 0.05, outline_alpha),
2.0
)
func _draw_rectangle_collision(
collision: CollisionShape2D,
fill_color: Color,
outline_color: Color,
extra_offset: Vector2 = Vector2.ZERO
) -> void:
var rectangle: RectangleShape2D = collision.shape as RectangleShape2D
if rectangle == null:
return
var center: Vector2 = collision.position + extra_offset
var rect: Rect2 = Rect2(center - rectangle.size / 2.0, rectangle.size)
draw_rect(rect, fill_color, true)
draw_rect(rect, outline_color, false, 2.0)
File renamed without changes.
+58
View File
@@ -0,0 +1,58 @@
[gd_scene load_steps=6 format=3 uid="uid://nwbjxttgghpq"]
[ext_resource type="Script" uid="uid://dccfebno1gvh7" path="res://player/player.gd" id="1_player"]
[ext_resource type="SpriteFrames" path="res://player/characters/shared/prototype_sprite_frames.tres" id="2_frames"]
[ext_resource type="Script" uid="uid://kg3i7b0dj2p8" path="res://player/camera_2d.gd" id="3_camera"]
[ext_resource type="Resource" path="res://player/characters/singer.tres" id="4_character"]
[sub_resource type="RectangleShape2D" id="PlayerShape"]
size = Vector2(65, 115)
[node name="Player" type="CharacterBody2D"]
z_index = 1
collision_layer = 2
collision_mask = 1
script = ExtResource("1_player")
character_data = ExtResource("4_character")
[node name="CollisionShape2D" type="CollisionShape2D" parent="."]
position = Vector2(0.5, 5.5)
shape = SubResource("PlayerShape")
[node name="Hurtbox" type="Area2D" parent="."]
collision_layer = 8
collision_mask = 0
monitoring = false
monitorable = true
[node name="CollisionShape2D" type="CollisionShape2D" parent="Hurtbox"]
position = Vector2(0.5, 5.5)
shape = SubResource("PlayerShape")
[node name="PunchHitbox" type="Area2D" parent="."]
collision_layer = 0
collision_mask = 16
monitoring = true
monitorable = false
[node name="CollisionShape2D" type="CollisionShape2D" parent="PunchHitbox"]
[node name="GroundPoundHitbox" type="Area2D" parent="."]
collision_layer = 0
collision_mask = 16
monitoring = true
monitorable = false
[node name="CollisionShape2D" type="CollisionShape2D" parent="GroundPoundHitbox"]
[node name="AnimatedSprite2D" type="AnimatedSprite2D" parent="."]
z_index = -1
scale = Vector2(0.80251265, 0.8025128)
sprite_frames = ExtResource("2_frames")
animation = &"idle"
speed_scale = 2.0
[node name="Camera2D" type="Camera2D" parent="."]
scale = Vector2(0.5, 0.5)
zoom = Vector2(2, 2)
script = ExtResource("3_camera")
+9 -1
View File
@@ -11,7 +11,7 @@ config_version=5
[application]
config/name="Mortal Maze: The Game"
run/main_scene="uid://dux51kbeg032e"
run/main_scene="res://game/main.tscn"
config/features=PackedStringArray("4.7", "GL Compatibility")
config/icon="res://icon.svg"
@@ -48,6 +48,14 @@ attack={
]
}
[layer_names]
2d_physics/layer_1="World"
2d_physics/layer_2="PlayerBody"
2d_physics/layer_3="EnemyBody"
2d_physics/layer_4="PlayerHurtbox"
2d_physics/layer_5="EnemyHurtbox"
[physics]
3d/physics_engine="Jolt Physics"