Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e93efc533b | ||
|
|
1b797a3e90 |
No files matched your search
@@ -1,3 +1,6 @@
|
|||||||
# Godot 4+ specific ignores
|
# Godot 4+ specific ignores
|
||||||
.godot/
|
.godot/
|
||||||
/android/
|
/android/
|
||||||
|
|
||||||
|
# Local export artifacts
|
||||||
|
build/
|
||||||
@@ -1876,13 +1876,28 @@ Actualizar esta sección conforme avance el proyecto.
|
|||||||
## NOW
|
## NOW
|
||||||
|
|
||||||
```text
|
```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.
|
||||||
|
- Controles táctiles provisionales para Android usando las mismas acciones del InputMap.
|
||||||
|
- Preset Android preparado para generar un APK arm64 de pruebas.
|
||||||
|
- Script scripts/export_android_debug.sh para exportar el APK desde un servidor sin GUI.
|
||||||
```
|
```
|
||||||
|
|
||||||
## NEXT
|
## NEXT
|
||||||
|
|
||||||
```text
|
```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.
|
||||||
|
- Probar tamaños y colocación de los controles táctiles en varios teléfonos.
|
||||||
|
- Añadir UI provisional de vida si hace falta para seguir afinando el combate.
|
||||||
```
|
```
|
||||||
|
|
||||||
## LATER
|
## LATER
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -0,0 +1,518 @@
|
|||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
enum AttackState {
|
||||||
|
IDLE,
|
||||||
|
WINDUP,
|
||||||
|
ACTIVE,
|
||||||
|
RECOVERY,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# MOVEMENT
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
@export_category("Movement")
|
||||||
|
|
||||||
|
@export var move_speed: float = 85.0
|
||||||
|
@export var max_fall_speed: float = 1200.0
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# COMBAT
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
@export_category("Combat")
|
||||||
|
|
||||||
|
@export_range(1, 100, 1) var max_health: int = 3
|
||||||
|
@export_range(0, 100, 1) var attack_damage: int = 1
|
||||||
|
|
||||||
|
## Minimum time between the beginning of two attacks.
|
||||||
|
@export var attack_interval: float = 2.0
|
||||||
|
|
||||||
|
## Horizontal distance at which the Walker stops chasing and prepares a hit.
|
||||||
|
## This is intentionally wider than the final hitbox so entering range does not
|
||||||
|
## mean taking unavoidable damage.
|
||||||
|
@export var attack_trigger_distance: float = 125.0
|
||||||
|
|
||||||
|
## Keeps the Walker from starting a ground attack against a player far above or
|
||||||
|
## below it.
|
||||||
|
@export var attack_vertical_tolerance: float = 70.0
|
||||||
|
|
||||||
|
## Readable pause before the dangerous part of the attack starts. During this
|
||||||
|
## time the direction is locked, so moving behind the enemy is a valid dodge.
|
||||||
|
@export var attack_windup_duration: float = 0.35
|
||||||
|
|
||||||
|
## Duration of the actual hit. The hitbox sweeps forward during this window.
|
||||||
|
@export var attack_duration: float = 0.22
|
||||||
|
|
||||||
|
## Small recovery after the hit before the enemy can move again.
|
||||||
|
@export var attack_recovery_duration: float = 0.30
|
||||||
|
|
||||||
|
## Where the hitbox starts, relative to the enemy while facing right.
|
||||||
|
@export var attack_start_offset: Vector2 = Vector2(34.0, 0.0)
|
||||||
|
|
||||||
|
## Furthest horizontal position reached by the moving hitbox.
|
||||||
|
@export var attack_reach: float = 105.0
|
||||||
|
|
||||||
|
## A successful player hit briefly interrupts the Walker. This makes attacking
|
||||||
|
## during its windup useful instead of forcing an unavoidable damage trade.
|
||||||
|
@export var hit_stun_duration: float = 0.18
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# RESPAWN
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
@export_category("Respawn")
|
||||||
|
|
||||||
|
## Enough downtime to make defeating an enemy meaningful before it comes back.
|
||||||
|
@export var respawn_delay: float = 5.0
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# DEBUG
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
@export_category("Debug")
|
||||||
|
|
||||||
|
## Enabled while the enemy has no final sprite/animation. The grey rectangle is
|
||||||
|
## the temporary body. Red overlays are the hurtbox and active attack hitbox.
|
||||||
|
## The orange area during windup previews the complete attack sweep.
|
||||||
|
@export var show_debug_hitboxes: bool = true
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# NODES
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
@onready var body_collision: CollisionShape2D = $CollisionShape2D
|
||||||
|
@onready var hurtbox_collision: CollisionShape2D = $Hurtbox/CollisionShape2D
|
||||||
|
@onready var attack_hitbox: Area2D = $AttackHitbox
|
||||||
|
@onready var attack_collision: CollisionShape2D = $AttackHitbox/CollisionShape2D
|
||||||
|
@onready var floor_probe: RayCast2D = $FloorProbe
|
||||||
|
@onready var visibility_notifier: VisibleOnScreenNotifier2D = $VisibleOnScreenNotifier2D
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# RUNTIME STATE
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
var current_health: int
|
||||||
|
var facing_direction: float = -1.0
|
||||||
|
|
||||||
|
var attack_state: int = AttackState.IDLE
|
||||||
|
var attack_phase_timer: float = 0.0
|
||||||
|
var attack_cooldown_timer: float = 0.0
|
||||||
|
var attack_direction: float = -1.0
|
||||||
|
var hit_stun_timer: float = 0.0
|
||||||
|
|
||||||
|
var is_awake: bool = false
|
||||||
|
var is_dead: bool = false
|
||||||
|
|
||||||
|
var spawn_position: Vector2
|
||||||
|
var target: Node2D
|
||||||
|
|
||||||
|
## Prevents one sweep from damaging the same target once per physics frame.
|
||||||
|
var attack_hit_targets: Dictionary = {}
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
spawn_position = global_position
|
||||||
|
current_health = max_health
|
||||||
|
target = get_tree().get_first_node_in_group(&"player") as Node2D
|
||||||
|
|
||||||
|
attack_hitbox.collision_layer = 0
|
||||||
|
attack_hitbox.collision_mask = PLAYER_HURTBOX_MASK
|
||||||
|
attack_hitbox.monitoring = true
|
||||||
|
|
||||||
|
visibility_notifier.screen_entered.connect(_wake_up)
|
||||||
|
|
||||||
|
_update_facing()
|
||||||
|
_reset_attack_hitbox_position()
|
||||||
|
queue_redraw()
|
||||||
|
|
||||||
|
|
||||||
|
func _physics_process(delta: float) -> void:
|
||||||
|
if is_dead:
|
||||||
|
return
|
||||||
|
|
||||||
|
_update_timers(delta)
|
||||||
|
_apply_gravity(delta)
|
||||||
|
|
||||||
|
# The enemy deliberately does nothing until the camera has shown it once.
|
||||||
|
# After waking up it stays active, even if the player later runs away from
|
||||||
|
# the screen area where the enemy was first seen.
|
||||||
|
if not is_awake:
|
||||||
|
velocity.x = 0.0
|
||||||
|
move_and_slide()
|
||||||
|
queue_redraw()
|
||||||
|
return
|
||||||
|
|
||||||
|
_acquire_target_if_needed()
|
||||||
|
if target == null:
|
||||||
|
velocity.x = 0.0
|
||||||
|
move_and_slide()
|
||||||
|
queue_redraw()
|
||||||
|
return
|
||||||
|
|
||||||
|
# Being hit pauses both movement and attack for only a few frames. The
|
||||||
|
# cooldown keeps running, so interrupting an attack does not make the enemy
|
||||||
|
# immediately start another one.
|
||||||
|
if hit_stun_timer > 0.0:
|
||||||
|
velocity.x = 0.0
|
||||||
|
move_and_slide()
|
||||||
|
queue_redraw()
|
||||||
|
return
|
||||||
|
|
||||||
|
if attack_state == AttackState.IDLE:
|
||||||
|
_update_idle_behaviour()
|
||||||
|
else:
|
||||||
|
_update_attack(delta)
|
||||||
|
|
||||||
|
move_and_slide()
|
||||||
|
queue_redraw()
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# ACTIVATION AND TARGETING
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
func _wake_up() -> void:
|
||||||
|
is_awake = true
|
||||||
|
|
||||||
|
|
||||||
|
func _acquire_target_if_needed() -> void:
|
||||||
|
if target != null and is_instance_valid(target):
|
||||||
|
return
|
||||||
|
|
||||||
|
target = get_tree().get_first_node_in_group(&"player") as Node2D
|
||||||
|
|
||||||
|
|
||||||
|
func _face_target() -> void:
|
||||||
|
var direction_to_target: float = signf(target.global_position.x - global_position.x)
|
||||||
|
if direction_to_target == 0.0 or direction_to_target == facing_direction:
|
||||||
|
return
|
||||||
|
|
||||||
|
facing_direction = direction_to_target
|
||||||
|
_update_facing()
|
||||||
|
|
||||||
|
|
||||||
|
func _update_facing() -> void:
|
||||||
|
# The ledge probe follows the current movement direction. Attack direction is
|
||||||
|
# stored separately once a windup starts so the enemy cannot rotate mid-hit.
|
||||||
|
floor_probe.position.x = abs(floor_probe.position.x) * facing_direction
|
||||||
|
|
||||||
|
if attack_state == AttackState.IDLE:
|
||||||
|
_reset_attack_hitbox_position()
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# MOVEMENT
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
func _apply_gravity(delta: float) -> void:
|
||||||
|
if is_on_floor():
|
||||||
|
return
|
||||||
|
|
||||||
|
velocity += get_gravity() * delta
|
||||||
|
velocity.y = min(velocity.y, max_fall_speed)
|
||||||
|
|
||||||
|
|
||||||
|
func _update_idle_behaviour() -> void:
|
||||||
|
_face_target()
|
||||||
|
|
||||||
|
if attack_cooldown_timer <= 0.0 and _is_target_in_attack_trigger():
|
||||||
|
_start_attack()
|
||||||
|
return
|
||||||
|
|
||||||
|
_chase_target()
|
||||||
|
|
||||||
|
|
||||||
|
func _chase_target() -> void:
|
||||||
|
# Do not blindly walk off a platform while chasing. Proper pathfinding is
|
||||||
|
# intentionally postponed until levels actually require it.
|
||||||
|
if is_on_floor() and not floor_probe.is_colliding():
|
||||||
|
velocity.x = 0.0
|
||||||
|
return
|
||||||
|
|
||||||
|
if is_on_wall():
|
||||||
|
velocity.x = 0.0
|
||||||
|
return
|
||||||
|
|
||||||
|
velocity.x = facing_direction * move_speed
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# ATTACK
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
func _is_target_in_attack_trigger() -> bool:
|
||||||
|
var offset_to_target: Vector2 = target.global_position - global_position
|
||||||
|
return (
|
||||||
|
abs(offset_to_target.x) <= attack_trigger_distance
|
||||||
|
and abs(offset_to_target.y) <= attack_vertical_tolerance
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _start_attack() -> void:
|
||||||
|
# Lock the direction here. During windup/active frames the player can dodge
|
||||||
|
# behind the enemy without the hitbox snapping around to follow them.
|
||||||
|
_face_target()
|
||||||
|
attack_direction = facing_direction
|
||||||
|
attack_state = AttackState.WINDUP
|
||||||
|
attack_phase_timer = attack_windup_duration
|
||||||
|
attack_cooldown_timer = attack_interval
|
||||||
|
attack_hit_targets.clear()
|
||||||
|
velocity.x = 0.0
|
||||||
|
_reset_attack_hitbox_position()
|
||||||
|
|
||||||
|
|
||||||
|
func _update_attack(delta: float) -> void:
|
||||||
|
velocity.x = 0.0
|
||||||
|
|
||||||
|
match attack_state:
|
||||||
|
AttackState.WINDUP:
|
||||||
|
_update_attack_windup(delta)
|
||||||
|
AttackState.ACTIVE:
|
||||||
|
_update_active_attack(delta)
|
||||||
|
AttackState.RECOVERY:
|
||||||
|
_update_attack_recovery(delta)
|
||||||
|
|
||||||
|
|
||||||
|
func _update_attack_windup(delta: float) -> void:
|
||||||
|
attack_phase_timer = max(attack_phase_timer - delta, 0.0)
|
||||||
|
if attack_phase_timer > 0.0:
|
||||||
|
return
|
||||||
|
|
||||||
|
attack_state = AttackState.ACTIVE
|
||||||
|
attack_phase_timer = attack_duration
|
||||||
|
_set_attack_hitbox_progress(0.0)
|
||||||
|
|
||||||
|
|
||||||
|
func _update_active_attack(delta: float) -> void:
|
||||||
|
attack_phase_timer = max(attack_phase_timer - delta, 0.0)
|
||||||
|
|
||||||
|
# Sweep the actual physics hitbox from close to the enemy towards its maximum
|
||||||
|
# reach. This gives the player time to jump, back away or move behind it.
|
||||||
|
var progress: float = 1.0
|
||||||
|
if attack_duration > 0.0:
|
||||||
|
progress = 1.0 - (attack_phase_timer / attack_duration)
|
||||||
|
_set_attack_hitbox_progress(clampf(progress, 0.0, 1.0))
|
||||||
|
_update_attack_damage()
|
||||||
|
|
||||||
|
if attack_phase_timer > 0.0:
|
||||||
|
return
|
||||||
|
|
||||||
|
attack_state = AttackState.RECOVERY
|
||||||
|
attack_phase_timer = attack_recovery_duration
|
||||||
|
|
||||||
|
|
||||||
|
func _update_attack_recovery(delta: float) -> void:
|
||||||
|
attack_phase_timer = max(attack_phase_timer - delta, 0.0)
|
||||||
|
if attack_phase_timer > 0.0:
|
||||||
|
return
|
||||||
|
|
||||||
|
attack_state = AttackState.IDLE
|
||||||
|
attack_hit_targets.clear()
|
||||||
|
_reset_attack_hitbox_position()
|
||||||
|
|
||||||
|
|
||||||
|
func _set_attack_hitbox_progress(progress: float) -> void:
|
||||||
|
var start_x: float = abs(attack_start_offset.x)
|
||||||
|
var current_x: float = lerpf(start_x, attack_reach, progress)
|
||||||
|
attack_hitbox.position = Vector2(
|
||||||
|
current_x * attack_direction,
|
||||||
|
attack_start_offset.y
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _reset_attack_hitbox_position() -> void:
|
||||||
|
var direction: float = attack_direction
|
||||||
|
if attack_state == AttackState.IDLE:
|
||||||
|
direction = facing_direction
|
||||||
|
|
||||||
|
attack_hitbox.position = Vector2(
|
||||||
|
abs(attack_start_offset.x) * direction,
|
||||||
|
attack_start_offset.y
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _update_attack_damage() -> void:
|
||||||
|
for area in attack_hitbox.get_overlapping_areas():
|
||||||
|
var damage_target: Node = area.get_parent()
|
||||||
|
if damage_target == null or not damage_target.has_method("take_damage"):
|
||||||
|
continue
|
||||||
|
|
||||||
|
var target_id: int = damage_target.get_instance_id()
|
||||||
|
if attack_hit_targets.has(target_id):
|
||||||
|
continue
|
||||||
|
|
||||||
|
attack_hit_targets[target_id] = true
|
||||||
|
damage_target.take_damage(attack_damage, attack_hitbox.global_position)
|
||||||
|
|
||||||
|
|
||||||
|
func _interrupt_attack() -> void:
|
||||||
|
if attack_state == AttackState.IDLE:
|
||||||
|
return
|
||||||
|
|
||||||
|
attack_state = AttackState.IDLE
|
||||||
|
attack_phase_timer = 0.0
|
||||||
|
attack_hit_targets.clear()
|
||||||
|
_reset_attack_hitbox_position()
|
||||||
|
|
||||||
|
|
||||||
|
func _update_timers(delta: float) -> void:
|
||||||
|
attack_cooldown_timer = max(attack_cooldown_timer - delta, 0.0)
|
||||||
|
hit_stun_timer = max(hit_stun_timer - delta, 0.0)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# DAMAGE AND RESPAWN
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
# Light enemies are interruptible. This makes a well-timed player attack a
|
||||||
|
# defensive option and avoids mandatory damage trades at close range.
|
||||||
|
_interrupt_attack()
|
||||||
|
hit_stun_timer = hit_stun_duration
|
||||||
|
velocity.x = 0.0
|
||||||
|
|
||||||
|
var reaction_direction: float = signf(source_position.x - global_position.x)
|
||||||
|
if reaction_direction != 0.0:
|
||||||
|
facing_direction = reaction_direction
|
||||||
|
_update_facing()
|
||||||
|
|
||||||
|
|
||||||
|
func _die() -> void:
|
||||||
|
is_dead = true
|
||||||
|
died.emit()
|
||||||
|
|
||||||
|
velocity = Vector2.ZERO
|
||||||
|
visible = false
|
||||||
|
|
||||||
|
# take_damage() can run while Godot is flushing physics queries, therefore
|
||||||
|
# collision shapes are disabled deferred instead of changing them directly.
|
||||||
|
body_collision.set_deferred("disabled", true)
|
||||||
|
hurtbox_collision.set_deferred("disabled", true)
|
||||||
|
attack_collision.set_deferred("disabled", true)
|
||||||
|
|
||||||
|
await get_tree().create_timer(respawn_delay).timeout
|
||||||
|
_respawn()
|
||||||
|
|
||||||
|
|
||||||
|
func _respawn() -> void:
|
||||||
|
global_position = spawn_position
|
||||||
|
velocity = Vector2.ZERO
|
||||||
|
current_health = max_health
|
||||||
|
|
||||||
|
attack_state = AttackState.IDLE
|
||||||
|
attack_phase_timer = 0.0
|
||||||
|
attack_cooldown_timer = 0.0
|
||||||
|
hit_stun_timer = 0.0
|
||||||
|
attack_hit_targets.clear()
|
||||||
|
attack_direction = facing_direction
|
||||||
|
_reset_attack_hitbox_position()
|
||||||
|
|
||||||
|
body_collision.set_deferred("disabled", false)
|
||||||
|
hurtbox_collision.set_deferred("disabled", false)
|
||||||
|
attack_collision.set_deferred("disabled", false)
|
||||||
|
|
||||||
|
visible = true
|
||||||
|
is_dead = false
|
||||||
|
|
||||||
|
# If the spawn point is currently inside the camera, wake immediately.
|
||||||
|
# Otherwise the notifier will wake the enemy when the camera reaches it.
|
||||||
|
is_awake = visibility_notifier.is_on_screen()
|
||||||
|
queue_redraw()
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# TEMPORARY VISUAL + DEBUG HITBOXES
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
func _draw() -> void:
|
||||||
|
# The enemy has no sprite yet. This neutral block makes the actual entity
|
||||||
|
# visible independently from the 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)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Windup: preview the full sweep in orange. It is explicitly NOT dangerous
|
||||||
|
# yet, so the different colour makes the reaction window easy to understand.
|
||||||
|
if attack_state == AttackState.WINDUP:
|
||||||
|
_draw_attack_telegraph()
|
||||||
|
|
||||||
|
# Active attack: draw the real moving physics hitbox in strong red.
|
||||||
|
if attack_state == AttackState.ACTIVE:
|
||||||
|
_draw_rectangle_collision(
|
||||||
|
attack_collision,
|
||||||
|
Color(1.0, 0.05, 0.05, 0.28),
|
||||||
|
Color(1.0, 0.05, 0.05, 0.95),
|
||||||
|
attack_hitbox.position
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _draw_attack_telegraph() -> void:
|
||||||
|
var rectangle: RectangleShape2D = attack_collision.shape as RectangleShape2D
|
||||||
|
if rectangle == null:
|
||||||
|
return
|
||||||
|
|
||||||
|
var start_x: float = abs(attack_start_offset.x) * attack_direction
|
||||||
|
var end_x: float = attack_reach * attack_direction
|
||||||
|
var center_x: float = (start_x + end_x) * 0.5
|
||||||
|
var sweep_width: float = abs(end_x - start_x) + rectangle.size.x
|
||||||
|
var center: Vector2 = Vector2(center_x, attack_start_offset.y)
|
||||||
|
var sweep_size: Vector2 = Vector2(sweep_width, rectangle.size.y)
|
||||||
|
var rect: Rect2 = Rect2(center - sweep_size / 2.0, sweep_size)
|
||||||
|
|
||||||
|
draw_rect(rect, Color(1.0, 0.55, 0.05, 0.12), true)
|
||||||
|
draw_rect(rect, Color(1.0, 0.55, 0.05, 0.85), false, 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)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://dbt03i3eiutv5
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
[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="AttackShape"]
|
||||||
|
size = Vector2(50, 60)
|
||||||
|
|
||||||
|
[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="AttackHitbox" type="Area2D" parent="."]
|
||||||
|
collision_layer = 0
|
||||||
|
collision_mask = 8
|
||||||
|
monitoring = true
|
||||||
|
monitorable = false
|
||||||
|
|
||||||
|
[node name="CollisionShape2D" type="CollisionShape2D" parent="AttackHitbox"]
|
||||||
|
shape = SubResource("AttackShape")
|
||||||
|
|
||||||
|
[node name="FloorProbe" type="RayCast2D" parent="."]
|
||||||
|
position = Vector2(24, 34)
|
||||||
|
target_position = Vector2(0, 34)
|
||||||
|
collision_mask = 1
|
||||||
|
enabled = true
|
||||||
|
|
||||||
|
[node name="VisibleOnScreenNotifier2D" type="VisibleOnScreenNotifier2D" parent="."]
|
||||||
|
rect = Rect2(-32, -48, 64, 96)
|
||||||
+4
-8
@@ -1,7 +1,3 @@
|
|||||||
[runnable_presets]
|
|
||||||
|
|
||||||
Android="Android"
|
|
||||||
|
|
||||||
[preset.0]
|
[preset.0]
|
||||||
|
|
||||||
name="Android"
|
name="Android"
|
||||||
@@ -11,7 +7,7 @@ custom_features=""
|
|||||||
export_filter="all_resources"
|
export_filter="all_resources"
|
||||||
include_filter=""
|
include_filter=""
|
||||||
exclude_filter=""
|
exclude_filter=""
|
||||||
export_path=""
|
export_path="build/android/mortal-maze-debug.apk"
|
||||||
patches=PackedStringArray()
|
patches=PackedStringArray()
|
||||||
patch_delta_encoding=false
|
patch_delta_encoding=false
|
||||||
patch_delta_compression_level_zstd=19
|
patch_delta_compression_level_zstd=19
|
||||||
@@ -42,9 +38,9 @@ architectures/arm64-v8a=true
|
|||||||
architectures/x86=false
|
architectures/x86=false
|
||||||
architectures/x86_64=false
|
architectures/x86_64=false
|
||||||
version/code=1
|
version/code=1
|
||||||
version/name=""
|
version/name="0.1.0"
|
||||||
package/unique_name="com.example.$genname"
|
package/unique_name="com.mortalmaze.thegame"
|
||||||
package/name=""
|
package/name="Mortal Maze: The Game"
|
||||||
package/signed=true
|
package/signed=true
|
||||||
package/app_category=2
|
package/app_category=2
|
||||||
package/retain_data_on_uninstall=false
|
package/retain_data_on_uninstall=false
|
||||||
|
|||||||
@@ -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")]
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
[gd_scene load_steps=7 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"]
|
||||||
|
[ext_resource type="PackedScene" path="res://ui/mobile_controls/mobile_controls.tscn" id="4_mobile_controls"]
|
||||||
|
|
||||||
|
[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\nTambién puedes usar los controles táctiles rojos"
|
||||||
|
|
||||||
|
[node name="MobileControls" parent="." instance=ExtResource("4_mobile_controls")]
|
||||||
@@ -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)
|
|
||||||
@@ -1,518 +0,0 @@
|
|||||||
extends CharacterBody2D
|
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# MOVIMIENTO
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
@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
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
@export_category("Jump")
|
|
||||||
|
|
||||||
@export var jump_velocity: float = -600.0
|
|
||||||
|
|
||||||
# Permite saltar ligeramente después de abandonar el suelo.
|
|
||||||
@export var coyote_time: float = 0.12
|
|
||||||
|
|
||||||
# Permite pulsar salto ligeramente antes de tocar el suelo.
|
|
||||||
@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.
|
|
||||||
@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
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
@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)
|
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# GROUND POUND
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
@export_category("Ground Pound")
|
|
||||||
|
|
||||||
@export var ground_pound_damage: int = 3
|
|
||||||
|
|
||||||
# Velocidad mínima que adquiere al comenzar el golpe hacia abajo.
|
|
||||||
@export var ground_pound_start_speed: float = 450.0
|
|
||||||
|
|
||||||
# Gravedad adicional mientras cae haciendo ground pound.
|
|
||||||
@export var ground_pound_gravity_multiplier: float = 4.5
|
|
||||||
|
|
||||||
# Control horizontal mientras cae.
|
|
||||||
@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)
|
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# DEBUG
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
@export_category("Debug")
|
|
||||||
|
|
||||||
@export var show_attack_hitboxes: bool = true
|
|
||||||
@export var debug_impact_duration: float = 0.18
|
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# NODOS
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
@onready var animated_sprite: AnimatedSprite2D = $AnimatedSprite2D
|
|
||||||
|
|
||||||
@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
|
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# ESTADO INTERNO
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
var facing_direction: float = 1.0
|
|
||||||
|
|
||||||
var coyote_timer: float = 0.0
|
|
||||||
var jump_buffer_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
|
|
||||||
|
|
||||||
# Evita que un enemigo reciba daño 60 veces durante el mismo puñetazo.
|
|
||||||
var punch_hit_targets: Dictionary = {}
|
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
|
||||||
_setup_hitboxes()
|
|
||||||
|
|
||||||
|
|
||||||
func _physics_process(delta: float) -> void:
|
|
||||||
var was_on_floor := 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.
|
|
||||||
if ground_pound_active and not was_on_floor and is_on_floor():
|
|
||||||
_ground_pound_impact()
|
|
||||||
|
|
||||||
queue_redraw()
|
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# MOVIMIENTO
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
func _handle_horizontal_movement(delta: float) -> void:
|
|
||||||
var direction := Input.get_axis("move_left", "move_right")
|
|
||||||
|
|
||||||
if direction != 0.0:
|
|
||||||
facing_direction = sign(direction)
|
|
||||||
_update_facing()
|
|
||||||
|
|
||||||
if ground_pound_active:
|
|
||||||
var target_speed := (
|
|
||||||
direction
|
|
||||||
* 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
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
velocity.x = move_toward(
|
|
||||||
velocity.x,
|
|
||||||
direction * speed,
|
|
||||||
air_acceleration * delta
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
func _update_facing() -> void:
|
|
||||||
animated_sprite.flip_h = facing_direction < 0.0
|
|
||||||
|
|
||||||
punch_hitbox.position = Vector2(
|
|
||||||
abs(punch_offset.x) * facing_direction,
|
|
||||||
punch_offset.y
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# GRAVEDAD
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
func _apply_gravity(delta: float) -> void:
|
|
||||||
if is_on_floor():
|
|
||||||
return
|
|
||||||
|
|
||||||
var gravity := get_gravity()
|
|
||||||
|
|
||||||
if ground_pound_active:
|
|
||||||
velocity += (
|
|
||||||
gravity
|
|
||||||
* ground_pound_gravity_multiplier
|
|
||||||
* delta
|
|
||||||
)
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
velocity.y = min(velocity.y, max_fall_speed)
|
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# SALTO
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
func _update_jump_windows(delta: float) -> void:
|
|
||||||
if is_on_floor():
|
|
||||||
coyote_timer = coyote_time
|
|
||||||
else:
|
|
||||||
coyote_timer = max(coyote_timer - delta, 0.0)
|
|
||||||
|
|
||||||
if Input.is_action_just_pressed("jump"):
|
|
||||||
jump_buffer_timer = jump_buffer_time
|
|
||||||
else:
|
|
||||||
jump_buffer_timer = max(
|
|
||||||
jump_buffer_timer - delta,
|
|
||||||
0.0
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
func _handle_jump() -> void:
|
|
||||||
if (
|
|
||||||
jump_buffer_timer > 0.0
|
|
||||||
and coyote_timer > 0.0
|
|
||||||
and not ground_pound_active
|
|
||||||
):
|
|
||||||
velocity.y = 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
|
|
||||||
):
|
|
||||||
velocity.y *= jump_cut_multiplier
|
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# ATAQUE
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
func _handle_attack_input() -> void:
|
|
||||||
if not Input.is_action_just_pressed("attack"):
|
|
||||||
return
|
|
||||||
|
|
||||||
if attack_cooldown_timer > 0.0:
|
|
||||||
return
|
|
||||||
|
|
||||||
if is_on_floor():
|
|
||||||
_start_punch()
|
|
||||||
else:
|
|
||||||
_start_ground_pound()
|
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# PUÑETAZO
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
func _start_punch() -> void:
|
|
||||||
punch_timer = punch_duration
|
|
||||||
attack_cooldown_timer = punch_cooldown
|
|
||||||
|
|
||||||
punch_hit_targets.clear()
|
|
||||||
|
|
||||||
|
|
||||||
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
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# GROUND POUND
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
func _start_ground_pound() -> void:
|
|
||||||
if ground_pound_active:
|
|
||||||
return
|
|
||||||
|
|
||||||
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
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
func _ground_pound_impact() -> void:
|
|
||||||
ground_pound_active = false
|
|
||||||
ground_pound_debug_timer = debug_impact_duration
|
|
||||||
|
|
||||||
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
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# DAÑO
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
func _damage_target(
|
|
||||||
collider: Node,
|
|
||||||
damage: int,
|
|
||||||
hit_targets: Dictionary
|
|
||||||
) -> void:
|
|
||||||
if collider == self:
|
|
||||||
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()
|
|
||||||
|
|
||||||
if hit_targets.has(target_id):
|
|
||||||
return
|
|
||||||
|
|
||||||
hit_targets[target_id] = true
|
|
||||||
|
|
||||||
target.take_damage(
|
|
||||||
damage,
|
|
||||||
global_position
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# TIMERS
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
func _update_timers(delta: float) -> void:
|
|
||||||
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
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# ANIMACIONES
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
func _update_animation() -> void:
|
|
||||||
if ground_pound_active:
|
|
||||||
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
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
func _setup_hitboxes() -> void:
|
|
||||||
# Puñetazo
|
|
||||||
var punch_shape := 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.monitoring = true
|
|
||||||
|
|
||||||
# Ground pound
|
|
||||||
var pound_shape := 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.monitoring = true
|
|
||||||
|
|
||||||
_update_facing()
|
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# DEBUG HITBOXES
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
func _draw() -> void:
|
|
||||||
if not show_attack_hitboxes:
|
|
||||||
return
|
|
||||||
|
|
||||||
# Puñetazo
|
|
||||||
if punch_timer > 0.0:
|
|
||||||
var center := punch_hitbox.position
|
|
||||||
|
|
||||||
var rect := Rect2(
|
|
||||||
center - punch_hitbox_size / 2.0,
|
|
||||||
punch_hitbox_size
|
|
||||||
)
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
draw_circle(
|
|
||||||
center,
|
|
||||||
ground_pound_radius,
|
|
||||||
Color(1.0, 0.5, 0.0, 0.20)
|
|
||||||
)
|
|
||||||
|
|
||||||
draw_arc(
|
|
||||||
center,
|
|
||||||
ground_pound_radius,
|
|
||||||
0.0,
|
|
||||||
TAU,
|
|
||||||
48,
|
|
||||||
Color(1.0, 0.5, 0.0, 0.9),
|
|
||||||
3.0
|
|
||||||
)
|
|
||||||
@@ -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.
@@ -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 = 400.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
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://bi04q2fe5goqn
|
||||||
@@ -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 = 400.0
|
||||||
|
jump_velocity = -600.0
|
||||||
|
basic_attack_damage = 1
|
||||||
|
air_attack_damage = 3
|
||||||
@@ -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 = 400.0
|
||||||
|
jump_velocity = -600.0
|
||||||
|
basic_attack_damage = 1
|
||||||
|
air_attack_damage = 3
|
||||||
@@ -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 = 400.0
|
||||||
|
jump_velocity = -600.0
|
||||||
|
basic_attack_damage = 1
|
||||||
|
air_attack_damage = 3
|
||||||
+37
-61
@@ -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="1_idle"]
|
||||||
[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="2_walk"]
|
||||||
[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)
|
|
||||||
|
|
||||||
[sub_resource type="AtlasTexture" id="AtlasTexture_lquwl"]
|
[sub_resource type="AtlasTexture" id="AtlasTexture_lquwl"]
|
||||||
atlas = ExtResource("2_yi62x")
|
atlas = ExtResource("1_idle")
|
||||||
region = Rect2(0, 0, 256, 256)
|
region = Rect2(0, 0, 256, 256)
|
||||||
|
|
||||||
[sub_resource type="AtlasTexture" id="AtlasTexture_7mycd"]
|
[sub_resource type="AtlasTexture" id="AtlasTexture_7mycd"]
|
||||||
atlas = ExtResource("2_yi62x")
|
atlas = ExtResource("1_idle")
|
||||||
region = Rect2(256, 0, 256, 256)
|
region = Rect2(256, 0, 256, 256)
|
||||||
|
|
||||||
[sub_resource type="AtlasTexture" id="AtlasTexture_272bh"]
|
[sub_resource type="AtlasTexture" id="AtlasTexture_272bh"]
|
||||||
atlas = ExtResource("2_yi62x")
|
atlas = ExtResource("1_idle")
|
||||||
region = Rect2(512, 0, 256, 256)
|
region = Rect2(512, 0, 256, 256)
|
||||||
|
|
||||||
[sub_resource type="AtlasTexture" id="AtlasTexture_5vw27"]
|
[sub_resource type="AtlasTexture" id="AtlasTexture_5vw27"]
|
||||||
atlas = ExtResource("2_yi62x")
|
atlas = ExtResource("1_idle")
|
||||||
region = Rect2(768, 0, 256, 256)
|
region = Rect2(768, 0, 256, 256)
|
||||||
|
|
||||||
[sub_resource type="AtlasTexture" id="AtlasTexture_kek77"]
|
[sub_resource type="AtlasTexture" id="AtlasTexture_kek77"]
|
||||||
atlas = ExtResource("2_yi62x")
|
atlas = ExtResource("1_idle")
|
||||||
region = Rect2(1024, 0, 256, 256)
|
region = Rect2(1024, 0, 256, 256)
|
||||||
|
|
||||||
[sub_resource type="AtlasTexture" id="AtlasTexture_4c57u"]
|
[sub_resource type="AtlasTexture" id="AtlasTexture_4c57u"]
|
||||||
atlas = ExtResource("2_yi62x")
|
atlas = ExtResource("1_idle")
|
||||||
region = Rect2(0, 256, 256, 256)
|
region = Rect2(0, 256, 256, 256)
|
||||||
|
|
||||||
[sub_resource type="AtlasTexture" id="AtlasTexture_efxa6"]
|
[sub_resource type="AtlasTexture" id="AtlasTexture_efxa6"]
|
||||||
atlas = ExtResource("2_yi62x")
|
atlas = ExtResource("1_idle")
|
||||||
region = Rect2(256, 256, 256, 256)
|
region = Rect2(256, 256, 256, 256)
|
||||||
|
|
||||||
[sub_resource type="AtlasTexture" id="AtlasTexture_dg77c"]
|
[sub_resource type="AtlasTexture" id="AtlasTexture_dg77c"]
|
||||||
atlas = ExtResource("2_yi62x")
|
atlas = ExtResource("1_idle")
|
||||||
region = Rect2(512, 256, 256, 256)
|
region = Rect2(512, 256, 256, 256)
|
||||||
|
|
||||||
[sub_resource type="AtlasTexture" id="AtlasTexture_ycdy4"]
|
[sub_resource type="AtlasTexture" id="AtlasTexture_ycdy4"]
|
||||||
atlas = ExtResource("2_yi62x")
|
atlas = ExtResource("1_idle")
|
||||||
region = Rect2(768, 256, 256, 256)
|
region = Rect2(768, 256, 256, 256)
|
||||||
|
|
||||||
[sub_resource type="AtlasTexture" id="AtlasTexture_w48qg"]
|
[sub_resource type="AtlasTexture" id="AtlasTexture_w48qg"]
|
||||||
atlas = ExtResource("2_yi62x")
|
atlas = ExtResource("1_idle")
|
||||||
region = Rect2(1024, 256, 256, 256)
|
region = Rect2(1024, 256, 256, 256)
|
||||||
|
|
||||||
[sub_resource type="AtlasTexture" id="AtlasTexture_vivmo"]
|
[sub_resource type="AtlasTexture" id="AtlasTexture_vivmo"]
|
||||||
atlas = ExtResource("2_yi62x")
|
atlas = ExtResource("1_idle")
|
||||||
region = Rect2(0, 512, 256, 256)
|
region = Rect2(0, 512, 256, 256)
|
||||||
|
|
||||||
[sub_resource type="AtlasTexture" id="AtlasTexture_2cqfq"]
|
[sub_resource type="AtlasTexture" id="AtlasTexture_2cqfq"]
|
||||||
atlas = ExtResource("2_yi62x")
|
atlas = ExtResource("1_idle")
|
||||||
region = Rect2(256, 512, 256, 256)
|
region = Rect2(256, 512, 256, 256)
|
||||||
|
|
||||||
[sub_resource type="AtlasTexture" id="AtlasTexture_yaehf"]
|
[sub_resource type="AtlasTexture" id="AtlasTexture_yaehf"]
|
||||||
atlas = ExtResource("3_sn41o")
|
atlas = ExtResource("2_walk")
|
||||||
region = Rect2(0, 0, 256, 256)
|
region = Rect2(0, 0, 256, 256)
|
||||||
|
|
||||||
[sub_resource type="AtlasTexture" id="AtlasTexture_074og"]
|
[sub_resource type="AtlasTexture" id="AtlasTexture_074og"]
|
||||||
atlas = ExtResource("3_sn41o")
|
atlas = ExtResource("2_walk")
|
||||||
region = Rect2(256, 0, 256, 256)
|
region = Rect2(256, 0, 256, 256)
|
||||||
|
|
||||||
[sub_resource type="AtlasTexture" id="AtlasTexture_cegan"]
|
[sub_resource type="AtlasTexture" id="AtlasTexture_cegan"]
|
||||||
atlas = ExtResource("3_sn41o")
|
atlas = ExtResource("2_walk")
|
||||||
region = Rect2(512, 0, 256, 256)
|
region = Rect2(512, 0, 256, 256)
|
||||||
|
|
||||||
[sub_resource type="AtlasTexture" id="AtlasTexture_82xsv"]
|
[sub_resource type="AtlasTexture" id="AtlasTexture_82xsv"]
|
||||||
atlas = ExtResource("3_sn41o")
|
atlas = ExtResource("2_walk")
|
||||||
region = Rect2(768, 0, 256, 256)
|
region = Rect2(768, 0, 256, 256)
|
||||||
|
|
||||||
[sub_resource type="AtlasTexture" id="AtlasTexture_getpj"]
|
[sub_resource type="AtlasTexture" id="AtlasTexture_getpj"]
|
||||||
atlas = ExtResource("3_sn41o")
|
atlas = ExtResource("2_walk")
|
||||||
region = Rect2(1024, 0, 256, 256)
|
region = Rect2(1024, 0, 256, 256)
|
||||||
|
|
||||||
[sub_resource type="AtlasTexture" id="AtlasTexture_ryguw"]
|
[sub_resource type="AtlasTexture" id="AtlasTexture_ryguw"]
|
||||||
atlas = ExtResource("3_sn41o")
|
atlas = ExtResource("2_walk")
|
||||||
region = Rect2(0, 256, 256, 256)
|
region = Rect2(0, 256, 256, 256)
|
||||||
|
|
||||||
[sub_resource type="AtlasTexture" id="AtlasTexture_d13ii"]
|
[sub_resource type="AtlasTexture" id="AtlasTexture_d13ii"]
|
||||||
atlas = ExtResource("3_sn41o")
|
atlas = ExtResource("2_walk")
|
||||||
region = Rect2(256, 256, 256, 256)
|
region = Rect2(256, 256, 256, 256)
|
||||||
|
|
||||||
[sub_resource type="AtlasTexture" id="AtlasTexture_1u8w0"]
|
[sub_resource type="AtlasTexture" id="AtlasTexture_1u8w0"]
|
||||||
atlas = ExtResource("3_sn41o")
|
atlas = ExtResource("2_walk")
|
||||||
region = Rect2(512, 256, 256, 256)
|
region = Rect2(512, 256, 256, 256)
|
||||||
|
|
||||||
[sub_resource type="AtlasTexture" id="AtlasTexture_0odxb"]
|
[sub_resource type="AtlasTexture" id="AtlasTexture_0odxb"]
|
||||||
atlas = ExtResource("3_sn41o")
|
atlas = ExtResource("2_walk")
|
||||||
region = Rect2(768, 256, 256, 256)
|
region = Rect2(768, 256, 256, 256)
|
||||||
|
|
||||||
[sub_resource type="AtlasTexture" id="AtlasTexture_lswn8"]
|
[sub_resource type="AtlasTexture" id="AtlasTexture_lswn8"]
|
||||||
atlas = ExtResource("3_sn41o")
|
atlas = ExtResource("2_walk")
|
||||||
region = Rect2(1024, 256, 256, 256)
|
region = Rect2(1024, 256, 256, 256)
|
||||||
|
|
||||||
[sub_resource type="AtlasTexture" id="AtlasTexture_a6jrf"]
|
[sub_resource type="AtlasTexture" id="AtlasTexture_a6jrf"]
|
||||||
atlas = ExtResource("3_sn41o")
|
atlas = ExtResource("2_walk")
|
||||||
region = Rect2(0, 512, 256, 256)
|
region = Rect2(0, 512, 256, 256)
|
||||||
|
|
||||||
[sub_resource type="AtlasTexture" id="AtlasTexture_xuqvo"]
|
[sub_resource type="AtlasTexture" id="AtlasTexture_xuqvo"]
|
||||||
atlas = ExtResource("3_sn41o")
|
atlas = ExtResource("2_walk")
|
||||||
region = Rect2(256, 512, 256, 256)
|
region = Rect2(256, 512, 256, 256)
|
||||||
|
|
||||||
[sub_resource type="AtlasTexture" id="AtlasTexture_qsp4k"]
|
[sub_resource type="AtlasTexture" id="AtlasTexture_qsp4k"]
|
||||||
atlas = ExtResource("3_sn41o")
|
atlas = ExtResource("2_walk")
|
||||||
region = Rect2(512, 512, 256, 256)
|
region = Rect2(512, 512, 256, 256)
|
||||||
|
|
||||||
[sub_resource type="AtlasTexture" id="AtlasTexture_kq58d"]
|
[sub_resource type="AtlasTexture" id="AtlasTexture_kq58d"]
|
||||||
atlas = ExtResource("3_sn41o")
|
atlas = ExtResource("2_walk")
|
||||||
region = Rect2(768, 512, 256, 256)
|
region = Rect2(768, 512, 256, 256)
|
||||||
|
|
||||||
[sub_resource type="AtlasTexture" id="AtlasTexture_seu75"]
|
[sub_resource type="AtlasTexture" id="AtlasTexture_seu75"]
|
||||||
atlas = ExtResource("3_sn41o")
|
atlas = ExtResource("2_walk")
|
||||||
region = Rect2(1024, 512, 256, 256)
|
region = Rect2(1024, 512, 256, 256)
|
||||||
|
|
||||||
[sub_resource type="AtlasTexture" id="AtlasTexture_htxhm"]
|
[sub_resource type="AtlasTexture" id="AtlasTexture_htxhm"]
|
||||||
atlas = ExtResource("3_sn41o")
|
atlas = ExtResource("2_walk")
|
||||||
region = Rect2(0, 768, 256, 256)
|
region = Rect2(0, 768, 256, 256)
|
||||||
|
|
||||||
[sub_resource type="AtlasTexture" id="AtlasTexture_jq2sk"]
|
[sub_resource type="AtlasTexture" id="AtlasTexture_jq2sk"]
|
||||||
atlas = ExtResource("3_sn41o")
|
atlas = ExtResource("2_walk")
|
||||||
region = Rect2(256, 768, 256, 256)
|
region = Rect2(256, 768, 256, 256)
|
||||||
|
|
||||||
[sub_resource type="AtlasTexture" id="AtlasTexture_4k2k6"]
|
[sub_resource type="AtlasTexture" id="AtlasTexture_4k2k6"]
|
||||||
atlas = ExtResource("3_sn41o")
|
atlas = ExtResource("2_walk")
|
||||||
region = Rect2(512, 768, 256, 256)
|
region = Rect2(512, 768, 256, 256)
|
||||||
|
|
||||||
[sub_resource type="AtlasTexture" id="AtlasTexture_0rl1p"]
|
[sub_resource type="AtlasTexture" id="AtlasTexture_0rl1p"]
|
||||||
atlas = ExtResource("3_sn41o")
|
atlas = ExtResource("2_walk")
|
||||||
region = Rect2(768, 768, 256, 256)
|
region = Rect2(768, 768, 256, 256)
|
||||||
|
|
||||||
[sub_resource type="AtlasTexture" id="AtlasTexture_ok6jj"]
|
[sub_resource type="AtlasTexture" id="AtlasTexture_ok6jj"]
|
||||||
atlas = ExtResource("3_sn41o")
|
atlas = ExtResource("2_walk")
|
||||||
region = Rect2(1024, 768, 256, 256)
|
region = Rect2(1024, 768, 256, 256)
|
||||||
|
|
||||||
[sub_resource type="AtlasTexture" id="AtlasTexture_facbu"]
|
[sub_resource type="AtlasTexture" id="AtlasTexture_facbu"]
|
||||||
atlas = ExtResource("3_sn41o")
|
atlas = ExtResource("2_walk")
|
||||||
region = Rect2(0, 1024, 256, 256)
|
region = Rect2(0, 1024, 256, 256)
|
||||||
|
|
||||||
[sub_resource type="SpriteFrames" id="SpriteFrames_741h5"]
|
[resource]
|
||||||
animations = [{
|
animations = [{
|
||||||
"frames": [{
|
"frames": [{
|
||||||
"duration": 1.0,
|
"duration": 1.0,
|
||||||
@@ -251,22 +246,3 @@ animations = [{
|
|||||||
"name": &"walk",
|
"name": &"walk",
|
||||||
"speed": 5.0
|
"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")
|
|
||||||
@@ -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 = 400.0
|
||||||
|
jump_velocity = -600.0
|
||||||
|
basic_attack_damage = 1
|
||||||
|
air_attack_damage = 3
|
||||||
@@ -0,0 +1,519 @@
|
|||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# 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")
|
||||||
|
|
||||||
|
## Reaches full running speed quickly, but not instantly.
|
||||||
|
@export var ground_acceleration: float = 3600.0
|
||||||
|
|
||||||
|
## Used when there is no horizontal input. Slightly stronger than acceleration
|
||||||
|
## so releasing the controls feels precise instead of slippery.
|
||||||
|
@export var ground_friction: float = 4800.0
|
||||||
|
|
||||||
|
## Braking used only when the player asks for the opposite direction. The
|
||||||
|
## character loses the old momentum first and then accelerates the other way,
|
||||||
|
## producing a small but readable turn delay.
|
||||||
|
@export var turn_braking: float = 5600.0
|
||||||
|
|
||||||
|
## Air control stays responsive but weaker than movement on the floor.
|
||||||
|
@export var air_acceleration: float = 1800.0
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# JUMP
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
@export_category("Jump")
|
||||||
|
|
||||||
|
## Allows a jump a few milliseconds after walking off a ledge.
|
||||||
|
@export var coyote_time: float = 0.12
|
||||||
|
|
||||||
|
## Remembers an early jump press just before landing.
|
||||||
|
@export var jump_buffer_time: float = 0.12
|
||||||
|
|
||||||
|
## Releasing jump while rising cuts the jump short.
|
||||||
|
@export_range(0.1, 1.0) var jump_cut_multiplier: float = 0.45
|
||||||
|
|
||||||
|
@export var gravity_multiplier: float = 1.0
|
||||||
|
@export var fall_gravity_multiplier: float = 1.7
|
||||||
|
@export var max_fall_speed: float = 1200.0
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# 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_duration: float = 0.13
|
||||||
|
@export var punch_cooldown: float = 0.28
|
||||||
|
@export var punch_hitbox_size: Vector2 = Vector2(55.0, 38.0)
|
||||||
|
@export var punch_offset: Vector2 = Vector2(48.0, 0.0)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# GROUND POUND
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
@export_category("Ground Pound")
|
||||||
|
|
||||||
|
## Minimum downward speed when the air attack starts.
|
||||||
|
@export var ground_pound_start_speed: float = 450.0
|
||||||
|
|
||||||
|
## Extra gravity while performing the air attack.
|
||||||
|
@export var ground_pound_gravity_multiplier: float = 4.5
|
||||||
|
|
||||||
|
## 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 = Vector2(0.0, 28.0)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# DEBUG
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
@export_category("Debug")
|
||||||
|
|
||||||
|
## 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
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# 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
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# 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
|
||||||
|
|
||||||
|
## 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:
|
||||||
|
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:
|
||||||
|
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()
|
||||||
|
|
||||||
|
move_and_slide()
|
||||||
|
|
||||||
|
# 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()
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# MOVEMENT
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
func _handle_horizontal_movement(delta: float) -> void:
|
||||||
|
var direction: float = Input.get_axis("move_left", "move_right")
|
||||||
|
|
||||||
|
if direction != 0.0:
|
||||||
|
facing_direction = signf(direction)
|
||||||
|
_update_facing()
|
||||||
|
|
||||||
|
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, 0.0, ground_friction * delta)
|
||||||
|
elif velocity.x != 0.0 and signf(velocity.x) != signf(direction):
|
||||||
|
# Do not snap directly from full speed right to full speed left. Brake
|
||||||
|
# the previous momentum first; on the next frames normal acceleration
|
||||||
|
# takes over in the requested direction.
|
||||||
|
velocity.x = move_toward(velocity.x, 0.0, turn_braking * delta)
|
||||||
|
else:
|
||||||
|
velocity.x = move_toward(
|
||||||
|
velocity.x,
|
||||||
|
target_speed,
|
||||||
|
ground_acceleration * delta
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
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
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# GRAVITY AND JUMP
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
func _apply_gravity(delta: float) -> void:
|
||||||
|
if is_on_floor():
|
||||||
|
return
|
||||||
|
|
||||||
|
var gravity: Vector2 = get_gravity()
|
||||||
|
var multiplier: float = gravity_multiplier
|
||||||
|
|
||||||
|
if ground_pound_active:
|
||||||
|
multiplier = ground_pound_gravity_multiplier
|
||||||
|
elif velocity.y > 0.0:
|
||||||
|
multiplier = fall_gravity_multiplier
|
||||||
|
|
||||||
|
velocity += gravity * multiplier * delta
|
||||||
|
velocity.y = min(velocity.y, max_fall_speed)
|
||||||
|
|
||||||
|
|
||||||
|
func _update_jump_windows(delta: float) -> void:
|
||||||
|
if is_on_floor():
|
||||||
|
coyote_timer = coyote_time
|
||||||
|
else:
|
||||||
|
coyote_timer = max(coyote_timer - delta, 0.0)
|
||||||
|
|
||||||
|
if Input.is_action_just_pressed("jump"):
|
||||||
|
jump_buffer_timer = jump_buffer_time
|
||||||
|
else:
|
||||||
|
jump_buffer_timer = max(jump_buffer_timer - delta, 0.0)
|
||||||
|
|
||||||
|
|
||||||
|
func _handle_jump() -> void:
|
||||||
|
if (
|
||||||
|
jump_buffer_timer > 0.0
|
||||||
|
and coyote_timer > 0.0
|
||||||
|
and not ground_pound_active
|
||||||
|
):
|
||||||
|
velocity.y = character_data.jump_velocity
|
||||||
|
jump_buffer_timer = 0.0
|
||||||
|
coyote_timer = 0.0
|
||||||
|
|
||||||
|
if Input.is_action_just_released("jump") and velocity.y < 0.0:
|
||||||
|
velocity.y *= jump_cut_multiplier
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# ATTACK INPUT
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
func _handle_attack_input() -> void:
|
||||||
|
if not Input.is_action_just_pressed("attack"):
|
||||||
|
return
|
||||||
|
|
||||||
|
if attack_cooldown_timer > 0.0:
|
||||||
|
return
|
||||||
|
|
||||||
|
if is_on_floor():
|
||||||
|
_start_punch()
|
||||||
|
else:
|
||||||
|
_start_ground_pound()
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# PUNCH
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
func _start_punch() -> void:
|
||||||
|
punch_timer = punch_duration
|
||||||
|
attack_cooldown_timer = punch_cooldown
|
||||||
|
punch_hit_targets.clear()
|
||||||
|
|
||||||
|
|
||||||
|
func _update_punch_damage() -> void:
|
||||||
|
if punch_timer <= 0.0:
|
||||||
|
return
|
||||||
|
|
||||||
|
for area in punch_hitbox.get_overlapping_areas():
|
||||||
|
_damage_target(area, character_data.basic_attack_damage, punch_hit_targets)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# GROUND POUND
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
func _start_ground_pound() -> void:
|
||||||
|
if ground_pound_active:
|
||||||
|
return
|
||||||
|
|
||||||
|
ground_pound_active = true
|
||||||
|
attack_cooldown_timer = punch_cooldown
|
||||||
|
|
||||||
|
# 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:
|
||||||
|
ground_pound_active = false
|
||||||
|
ground_pound_debug_timer = debug_impact_duration
|
||||||
|
|
||||||
|
var hit_targets: Dictionary = {}
|
||||||
|
|
||||||
|
for area in ground_pound_hitbox.get_overlapping_areas():
|
||||||
|
_damage_target(area, character_data.air_attack_damage, hit_targets)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# DEALING AND RECEIVING DAMAGE
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
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_id: int = target.get_instance_id()
|
||||||
|
if hit_targets.has(target_id):
|
||||||
|
return
|
||||||
|
|
||||||
|
hit_targets[target_id] = true
|
||||||
|
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 AND ANIMATION
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
func _update_timers(delta: float) -> void:
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
func _update_animation() -> void:
|
||||||
|
# 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")
|
||||||
|
else:
|
||||||
|
animated_sprite.play("idle")
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# HITBOX SETUP
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
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 = ENEMY_HURTBOX_MASK
|
||||||
|
punch_hitbox.monitoring = true
|
||||||
|
|
||||||
|
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 = ENEMY_HURTBOX_MASK
|
||||||
|
ground_pound_hitbox.monitoring = true
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# DEBUG HITBOX DRAWING
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
func _draw() -> void:
|
||||||
|
if not show_debug_hitboxes:
|
||||||
|
return
|
||||||
|
|
||||||
|
# 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:
|
||||||
|
_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
|
||||||
|
)
|
||||||
|
|
||||||
|
# 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(
|
||||||
|
ground_pound_hitbox.position,
|
||||||
|
ground_pound_radius,
|
||||||
|
Color(1.0, 0.18, 0.05, fill_alpha)
|
||||||
|
)
|
||||||
|
draw_arc(
|
||||||
|
ground_pound_hitbox.position,
|
||||||
|
ground_pound_radius,
|
||||||
|
0.0,
|
||||||
|
TAU,
|
||||||
|
48,
|
||||||
|
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.
@@ -0,0 +1,61 @@
|
|||||||
|
[gd_scene load_steps=7 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"]
|
||||||
|
[ext_resource type="PackedScene" path="res://ui/hud/player_health_hud.tscn" id="5_health_hud"]
|
||||||
|
|
||||||
|
[sub_resource type="RectangleShape2D" id="PlayerShape"]
|
||||||
|
size = Vector2(65, 115)
|
||||||
|
|
||||||
|
[node name="Player" type="CharacterBody2D" groups=["player"]]
|
||||||
|
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")
|
||||||
|
|
||||||
|
[node name="PlayerHealthHUD" parent="." instance=ExtResource("5_health_hud")]
|
||||||
+13
-1
@@ -11,12 +11,15 @@ config_version=5
|
|||||||
[application]
|
[application]
|
||||||
|
|
||||||
config/name="Mortal Maze: The Game"
|
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/features=PackedStringArray("4.7", "GL Compatibility")
|
||||||
config/icon="res://icon.svg"
|
config/icon="res://icon.svg"
|
||||||
|
|
||||||
[display]
|
[display]
|
||||||
|
|
||||||
|
window/size/viewport_width=1152
|
||||||
|
window/size/viewport_height=648
|
||||||
|
window/handheld/orientation=0
|
||||||
window/stretch/mode="canvas_items"
|
window/stretch/mode="canvas_items"
|
||||||
window/stretch/aspect="expand"
|
window/stretch/aspect="expand"
|
||||||
|
|
||||||
@@ -48,12 +51,21 @@ 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]
|
[physics]
|
||||||
|
|
||||||
3d/physics_engine="Jolt Physics"
|
3d/physics_engine="Jolt Physics"
|
||||||
|
|
||||||
[rendering]
|
[rendering]
|
||||||
|
|
||||||
|
textures/vram_compression/import_etc2_astc=true
|
||||||
rendering_device/driver.windows="d3d12"
|
rendering_device/driver.windows="d3d12"
|
||||||
renderer/rendering_method="gl_compatibility"
|
renderer/rendering_method="gl_compatibility"
|
||||||
renderer/rendering_method.mobile="gl_compatibility"
|
renderer/rendering_method.mobile="gl_compatibility"
|
||||||
Executable
+41
@@ -0,0 +1,41 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Exporta el APK de pruebas sin necesitar interfaz gráfica.
|
||||||
|
#
|
||||||
|
# Requisitos del entorno:
|
||||||
|
# - GODOT_BIN: ejecutable de Godot 4.7.x (por defecto: godot)
|
||||||
|
# - JAVA_HOME: ruta a OpenJDK 17
|
||||||
|
# - ANDROID_HOME: ruta al Android SDK
|
||||||
|
#
|
||||||
|
# El primer argumento es opcional y permite elegir otra ruta de salida.
|
||||||
|
|
||||||
|
PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
GODOT_BIN="${GODOT_BIN:-godot}"
|
||||||
|
OUTPUT_APK="${1:-${PROJECT_DIR}/build/android/mortal-maze-debug.apk}"
|
||||||
|
|
||||||
|
if [[ -z "${JAVA_HOME:-}" ]]; then
|
||||||
|
echo "ERROR: JAVA_HOME no está definido." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "${ANDROID_HOME:-}" ]]; then
|
||||||
|
echo "ERROR: ANDROID_HOME no está definido." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! command -v "${GODOT_BIN}" >/dev/null 2>&1; then
|
||||||
|
echo "ERROR: No encuentro Godot: ${GODOT_BIN}" >&2
|
||||||
|
echo "Define GODOT_BIN con la ruta al ejecutable si no está en PATH." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "$(dirname "${OUTPUT_APK}")"
|
||||||
|
|
||||||
|
"${GODOT_BIN}" \
|
||||||
|
--headless \
|
||||||
|
--path "${PROJECT_DIR}" \
|
||||||
|
--export-debug "Android" \
|
||||||
|
"${OUTPUT_APK}"
|
||||||
|
|
||||||
|
echo "APK generado en: ${OUTPUT_APK}"
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
extends CanvasLayer
|
||||||
|
|
||||||
|
|
||||||
|
## Minimal player HUD.
|
||||||
|
##
|
||||||
|
## This is a CanvasLayer on purpose: its coordinates belong to the camera
|
||||||
|
## viewport, not to the level. Therefore (18, 18) always means "18 pixels from
|
||||||
|
## the top-left corner currently visible by the camera", even while the player
|
||||||
|
## and Camera2D move through the world.
|
||||||
|
|
||||||
|
@onready var health_bar: ProgressBar = $HUD/HealthBar
|
||||||
|
@onready var health_label: Label = $HUD/HealthBar/HealthLabel
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
var player: Node = get_parent()
|
||||||
|
if player == null or not player.has_signal(&"health_changed"):
|
||||||
|
push_error("PlayerHealthHUD must be a child of a Player with health_changed signal.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Child _ready() runs before Player._ready(). We connect here first and the
|
||||||
|
# Player will emit its initial health value a moment later during its _ready().
|
||||||
|
player.connect(&"health_changed", _on_health_changed)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_health_changed(current_health: int, max_health: int) -> void:
|
||||||
|
health_bar.max_value = float(max_health)
|
||||||
|
health_bar.value = float(current_health)
|
||||||
|
health_label.text = "HP %d / %d" % [current_health, max_health]
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://djbmxa3ubj4yk
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
[gd_scene load_steps=4 format=3]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://ui/hud/player_health_hud.gd" id="1_hud"]
|
||||||
|
|
||||||
|
[sub_resource type="StyleBoxFlat" id="HealthBackground"]
|
||||||
|
bg_color = Color(0.08, 0.08, 0.09, 0.82)
|
||||||
|
border_width_left = 2
|
||||||
|
border_width_top = 2
|
||||||
|
border_width_right = 2
|
||||||
|
border_width_bottom = 2
|
||||||
|
border_color = Color(0.95, 0.95, 0.95, 0.75)
|
||||||
|
corner_radius_top_left = 7
|
||||||
|
corner_radius_top_right = 7
|
||||||
|
corner_radius_bottom_right = 7
|
||||||
|
corner_radius_bottom_left = 7
|
||||||
|
|
||||||
|
[sub_resource type="StyleBoxFlat" id="HealthFill"]
|
||||||
|
bg_color = Color(0.72, 0.06, 0.06, 0.92)
|
||||||
|
corner_radius_top_left = 5
|
||||||
|
corner_radius_top_right = 5
|
||||||
|
corner_radius_bottom_right = 5
|
||||||
|
corner_radius_bottom_left = 5
|
||||||
|
|
||||||
|
[node name="PlayerHealthHUD" type="CanvasLayer"]
|
||||||
|
layer = 90
|
||||||
|
script = ExtResource("1_hud")
|
||||||
|
|
||||||
|
[node name="HUD" type="Control" parent="."]
|
||||||
|
layout_mode = 3
|
||||||
|
anchors_preset = 15
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
mouse_filter = 2
|
||||||
|
|
||||||
|
[node name="HealthBar" type="ProgressBar" parent="HUD"]
|
||||||
|
layout_mode = 0
|
||||||
|
offset_left = 18.0
|
||||||
|
offset_top = 18.0
|
||||||
|
offset_right = 278.0
|
||||||
|
offset_bottom = 58.0
|
||||||
|
mouse_filter = 2
|
||||||
|
theme_override_styles/background = SubResource("HealthBackground")
|
||||||
|
theme_override_styles/fill = SubResource("HealthFill")
|
||||||
|
max_value = 5.0
|
||||||
|
value = 5.0
|
||||||
|
show_percentage = false
|
||||||
|
|
||||||
|
[node name="HealthLabel" type="Label" parent="HUD/HealthBar"]
|
||||||
|
layout_mode = 1
|
||||||
|
anchors_preset = 15
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
mouse_filter = 2
|
||||||
|
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 = 1
|
||||||
|
theme_override_constants/shadow_offset_y = 1
|
||||||
|
theme_override_font_sizes/font_size = 18
|
||||||
|
text = "HP 5 / 5"
|
||||||
|
horizontal_alignment = 1
|
||||||
|
vertical_alignment = 1
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
extends CanvasLayer
|
||||||
|
|
||||||
|
|
||||||
|
## Controles táctiles provisionales para jugar en móvil.
|
||||||
|
##
|
||||||
|
## Los botones no llaman directamente al Player. En su lugar pulsan y liberan
|
||||||
|
## las mismas acciones del InputMap que ya usa el teclado. De esta forma la
|
||||||
|
## lógica de movimiento/combate sigue teniendo una única fuente de entrada:
|
||||||
|
##
|
||||||
|
## teclado -> InputMap <- controles táctiles
|
||||||
|
## |
|
||||||
|
## Player
|
||||||
|
##
|
||||||
|
## Cuando llegue el arte definitivo, esta escena puede cambiar por completo sin
|
||||||
|
## tocar player.gd mientras conserve estas cuatro acciones.
|
||||||
|
|
||||||
|
@onready var move_left_button: Button = $Controls/MoveLeft
|
||||||
|
@onready var move_right_button: Button = $Controls/MoveRight
|
||||||
|
@onready var jump_button: Button = $Controls/Jump
|
||||||
|
@onready var attack_button: Button = $Controls/Attack
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
_bind_button(move_left_button, &"move_left")
|
||||||
|
_bind_button(move_right_button, &"move_right")
|
||||||
|
_bind_button(jump_button, &"jump")
|
||||||
|
_bind_button(attack_button, &"attack")
|
||||||
|
|
||||||
|
|
||||||
|
func _exit_tree() -> void:
|
||||||
|
# Si la escena cambia mientras un dedo sigue apoyado, Godot puede no llegar
|
||||||
|
# a emitir button_up. Liberamos las acciones para no dejar una tecla virtual
|
||||||
|
# atascada al cargar la siguiente escena.
|
||||||
|
Input.action_release(&"move_left")
|
||||||
|
Input.action_release(&"move_right")
|
||||||
|
Input.action_release(&"jump")
|
||||||
|
Input.action_release(&"attack")
|
||||||
|
|
||||||
|
|
||||||
|
func _bind_button(button: Button, action: StringName) -> void:
|
||||||
|
button.button_down.connect(_press_action.bind(action))
|
||||||
|
button.button_up.connect(_release_action.bind(action))
|
||||||
|
|
||||||
|
|
||||||
|
func _press_action(action: StringName) -> void:
|
||||||
|
Input.action_press(action)
|
||||||
|
|
||||||
|
|
||||||
|
func _release_action(action: StringName) -> void:
|
||||||
|
Input.action_release(action)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://2kbajcohyem7
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
[gd_scene load_steps=4 format=3]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://ui/mobile_controls/mobile_controls.gd" id="1_controls"]
|
||||||
|
|
||||||
|
[sub_resource type="StyleBoxFlat" id="ButtonNormal"]
|
||||||
|
bg_color = Color(0.65, 0.04, 0.04, 0.28)
|
||||||
|
border_width_left = 3
|
||||||
|
border_width_top = 3
|
||||||
|
border_width_right = 3
|
||||||
|
border_width_bottom = 3
|
||||||
|
border_color = Color(1, 0.15, 0.15, 0.72)
|
||||||
|
corner_radius_top_left = 14
|
||||||
|
corner_radius_top_right = 14
|
||||||
|
corner_radius_bottom_right = 14
|
||||||
|
corner_radius_bottom_left = 14
|
||||||
|
|
||||||
|
[sub_resource type="StyleBoxFlat" id="ButtonPressed"]
|
||||||
|
bg_color = Color(0.95, 0.05, 0.05, 0.52)
|
||||||
|
border_width_left = 4
|
||||||
|
border_width_top = 4
|
||||||
|
border_width_right = 4
|
||||||
|
border_width_bottom = 4
|
||||||
|
border_color = Color(1, 0.45, 0.45, 0.95)
|
||||||
|
corner_radius_top_left = 14
|
||||||
|
corner_radius_top_right = 14
|
||||||
|
corner_radius_bottom_right = 14
|
||||||
|
corner_radius_bottom_left = 14
|
||||||
|
|
||||||
|
[node name="MobileControls" type="CanvasLayer"]
|
||||||
|
layer = 100
|
||||||
|
script = ExtResource("1_controls")
|
||||||
|
|
||||||
|
[node name="Controls" type="Control" parent="."]
|
||||||
|
layout_mode = 3
|
||||||
|
anchors_preset = 15
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
mouse_filter = 2
|
||||||
|
|
||||||
|
[node name="MoveLeft" type="Button" parent="Controls"]
|
||||||
|
layout_mode = 0
|
||||||
|
anchor_top = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
offset_left = 34.0
|
||||||
|
offset_top = -132.0
|
||||||
|
offset_right = 134.0
|
||||||
|
offset_bottom = -32.0
|
||||||
|
grow_vertical = 0
|
||||||
|
focus_mode = 0
|
||||||
|
keep_pressed_outside = true
|
||||||
|
theme_override_colors/font_color = Color(1, 1, 1, 0.95)
|
||||||
|
theme_override_colors/font_pressed_color = Color(1, 1, 1, 1)
|
||||||
|
theme_override_font_sizes/font_size = 42
|
||||||
|
theme_override_styles/normal = SubResource("ButtonNormal")
|
||||||
|
theme_override_styles/hover = SubResource("ButtonNormal")
|
||||||
|
theme_override_styles/pressed = SubResource("ButtonPressed")
|
||||||
|
text = "<"
|
||||||
|
|
||||||
|
[node name="MoveRight" type="Button" parent="Controls"]
|
||||||
|
layout_mode = 0
|
||||||
|
anchor_top = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
offset_left = 150.0
|
||||||
|
offset_top = -132.0
|
||||||
|
offset_right = 250.0
|
||||||
|
offset_bottom = -32.0
|
||||||
|
grow_vertical = 0
|
||||||
|
focus_mode = 0
|
||||||
|
keep_pressed_outside = true
|
||||||
|
theme_override_colors/font_color = Color(1, 1, 1, 0.95)
|
||||||
|
theme_override_colors/font_pressed_color = Color(1, 1, 1, 1)
|
||||||
|
theme_override_font_sizes/font_size = 42
|
||||||
|
theme_override_styles/normal = SubResource("ButtonNormal")
|
||||||
|
theme_override_styles/hover = SubResource("ButtonNormal")
|
||||||
|
theme_override_styles/pressed = SubResource("ButtonPressed")
|
||||||
|
text = ">"
|
||||||
|
|
||||||
|
[node name="Jump" type="Button" parent="Controls"]
|
||||||
|
layout_mode = 0
|
||||||
|
anchor_left = 1.0
|
||||||
|
anchor_top = 1.0
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
offset_left = -264.0
|
||||||
|
offset_top = -132.0
|
||||||
|
offset_right = -154.0
|
||||||
|
offset_bottom = -32.0
|
||||||
|
grow_horizontal = 0
|
||||||
|
grow_vertical = 0
|
||||||
|
focus_mode = 0
|
||||||
|
keep_pressed_outside = true
|
||||||
|
theme_override_colors/font_color = Color(1, 1, 1, 0.95)
|
||||||
|
theme_override_colors/font_pressed_color = Color(1, 1, 1, 1)
|
||||||
|
theme_override_font_sizes/font_size = 22
|
||||||
|
theme_override_styles/normal = SubResource("ButtonNormal")
|
||||||
|
theme_override_styles/hover = SubResource("ButtonNormal")
|
||||||
|
theme_override_styles/pressed = SubResource("ButtonPressed")
|
||||||
|
text = "JUMP"
|
||||||
|
|
||||||
|
[node name="Attack" type="Button" parent="Controls"]
|
||||||
|
layout_mode = 0
|
||||||
|
anchor_left = 1.0
|
||||||
|
anchor_top = 1.0
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
offset_left = -138.0
|
||||||
|
offset_top = -132.0
|
||||||
|
offset_right = -28.0
|
||||||
|
offset_bottom = -32.0
|
||||||
|
grow_horizontal = 0
|
||||||
|
grow_vertical = 0
|
||||||
|
focus_mode = 0
|
||||||
|
keep_pressed_outside = true
|
||||||
|
theme_override_colors/font_color = Color(1, 1, 1, 0.95)
|
||||||
|
theme_override_colors/font_pressed_color = Color(1, 1, 1, 1)
|
||||||
|
theme_override_font_sizes/font_size = 22
|
||||||
|
theme_override_styles/normal = SubResource("ButtonNormal")
|
||||||
|
theme_override_styles/hover = SubResource("ButtonNormal")
|
||||||
|
theme_override_styles/pressed = SubResource("ButtonPressed")
|
||||||
|
text = "ATK"
|
||||||
Reference in new issue
Block a user