Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions godot/autoloads/GameState.gd
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ signal resumed
signal diamonds_changed(count: int)
signal creations_changed(remaining: int)
signal timer_updated(minutes: int, seconds: int)
signal health_changed(current: int, maximum: int)
signal player_died
signal player_respawned

# ── Pause ──────────────────────────────────────────────────────────────────
var is_paused: bool = false
Expand All @@ -33,8 +36,11 @@ var creations_remaining: int = 5:
creations_remaining = value
creations_changed.emit(creations_remaining)

# ── Stamina ────────────────────────────────────────────────────────────────
var player_stamina: int = 4
# ── Stamina / health ───────────────────────────────────────────────────────
var player_stamina: int = 4:
set(value):
player_stamina = clampi(value, 0, max_player_stamina)
health_changed.emit(player_stamina, max_player_stamina)
var max_player_stamina: int = 4

# ── Difficulty (0 = Easy, 1 = Medium, 2 = Extreme) ────────────────────────
Expand Down
7 changes: 5 additions & 2 deletions godot/scenes/objects/DeathArea.gd
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,8 @@ func _ready() -> void:

func _on_body_entered(body: Node2D) -> void:
if body.is_in_group("player"):
GameState.reset_level()
body.get_tree().reload_current_scene()
if body.has_method("instant_kill"):
body.instant_kill()
else:
GameState.reset_level()
body.get_tree().reload_current_scene()
72 changes: 68 additions & 4 deletions godot/scenes/objects/Enemy.gd
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,47 @@ extends Area2D
## Sluggers – Enemy
##
## A hazard that patrols horizontally between x_min and x_max.
## Reloads the current scene (player death) when the player touches it.
## When the player enters detect_range the enemy switches to chase mode.
## The player can stomp the enemy from above to deal damage; otherwise the
## enemy deals damage with knockback when the player touches it.

## Left boundary of the patrol path (world x-coordinate).
@export var x_min: float = 0.0
## Right boundary of the patrol path (world x-coordinate).
@export var x_max: float = 100.0
## Patrol speed in pixels per second.
@export var speed: float = 60.0
## Chase speed in pixels per second (used when player is within detect_range).
@export var chase_speed: float = 120.0
## Radius within which the enemy switches from patrol to chase mode.
@export var detect_range: float = 160.0
## Starting health points.
@export var max_health: int = 1

var _direction: int = 1
var _health: int = 1

@onready var _sprite: AnimatedSprite2D = $Sprite
@onready var _health_label: Label = $HealthLabel


func _ready() -> void:
body_entered.connect(_on_body_entered)
_health = max_health
_update_health_display()


func _physics_process(delta: float) -> void:
var player := _find_player()
if player and position.distance_to(player.position) < detect_range:
_chase(player, delta)
else:
_patrol(delta)


# ── Movement ───────────────────────────────────────────────────────────────

func _patrol(delta: float) -> void:
position.x += speed * _direction * delta
if position.x >= x_max:
position.x = x_max
Expand All @@ -33,7 +55,49 @@ func _physics_process(delta: float) -> void:
_sprite.flip_h = false


func _chase(player: Node2D, delta: float) -> void:
var dir := signf(player.position.x - position.x)
position.x += chase_speed * dir * delta
_sprite.flip_h = (dir < 0.0)


# ── Contact ────────────────────────────────────────────────────────────────

func _on_body_entered(body: Node2D) -> void:
if body.is_in_group("player"):
GameState.reset_level()
body.get_tree().reload_current_scene()
if not body.is_in_group("player"):
return
# Stomp check: player is above the enemy's centre and falling fast enough.
# The 50 px/s threshold matches Player.STOMP_MIN_VY; the 8 px vertical gap
# ensures the player's feet are clearly above the enemy's collision centre.
const STOMP_VY_THRESHOLD: float = 50.0
const STOMP_VERTICAL_OFFSET: float = 8.0
var player_vel := (body as CharacterBody2D).velocity if body is CharacterBody2D else Vector2.ZERO
if player_vel.y > STOMP_VY_THRESHOLD and body.position.y < position.y - STOMP_VERTICAL_OFFSET:
take_damage(1)
if body.has_method("stomp_bounce"):
body.stomp_bounce()
else:
var knockback_dir := int(signf(body.position.x - position.x))
if body.has_method("take_damage"):
body.take_damage(1, knockback_dir)


# ── Health ─────────────────────────────────────────────────────────────────

func take_damage(amount: int) -> void:
_health -= amount
_update_health_display()
if _health <= 0:
queue_free()


func _update_health_display() -> void:
if _health_label:
_health_label.text = "♥".repeat(maxi(_health, 0))


# ── Helpers ────────────────────────────────────────────────────────────────

func _find_player() -> Node2D:
var group := get_tree().get_nodes_in_group("player")
return group[0] if not group.is_empty() else null
9 changes: 9 additions & 0 deletions godot/scenes/objects/Enemy.tscn
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,12 @@ position = Vector2(0, -31)
sprite_frames = SubResource("SpriteFrames_enemy")
animation = &"default"
autoplay = "default"

[node name="HealthLabel" type="Label" parent="."]
offset_left = -16.0
offset_top = -64.0
offset_right = 16.0
offset_bottom = -54.0
horizontal_alignment = 1
text = "♥"
theme_override_font_sizes/font_size = 8
Loading