diff --git a/godot/autoloads/GameState.gd b/godot/autoloads/GameState.gd index 9068a82..1219fef 100644 --- a/godot/autoloads/GameState.gd +++ b/godot/autoloads/GameState.gd @@ -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 @@ -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) ──────────────────────── diff --git a/godot/scenes/objects/DeathArea.gd b/godot/scenes/objects/DeathArea.gd index 2dd53be..54e3879 100644 --- a/godot/scenes/objects/DeathArea.gd +++ b/godot/scenes/objects/DeathArea.gd @@ -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() diff --git a/godot/scenes/objects/Enemy.gd b/godot/scenes/objects/Enemy.gd index 7263f65..00f6e17 100644 --- a/godot/scenes/objects/Enemy.gd +++ b/godot/scenes/objects/Enemy.gd @@ -3,7 +3,9 @@ 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 @@ -11,17 +13,37 @@ extends Area2D @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 @@ -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 diff --git a/godot/scenes/objects/Enemy.tscn b/godot/scenes/objects/Enemy.tscn index ba9ee68..2de4280 100644 --- a/godot/scenes/objects/Enemy.tscn +++ b/godot/scenes/objects/Enemy.tscn @@ -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 diff --git a/godot/scenes/player/Player.gd b/godot/scenes/player/Player.gd index 66cf5ff..1414659 100644 --- a/godot/scenes/player/Player.gd +++ b/godot/scenes/player/Player.gd @@ -2,21 +2,28 @@ extends CharacterBody2D ## Sluggers – Player ## -## Physics-based platformer character with three movement states: +## Physics-based platformer character with movement states: ## GROUND – walking, with friction-based deceleration -## AIR – in-flight with reduced horizontal control and variable jump height -## WALL – sliding on a wall, with reduced vertical friction and wall-jump +## AIR – in-flight (includes coyote-time window and jump buffering) +## WALL – sliding on a wall, reduced vertical friction and wall-jump +## DEAD – death animation; auto-respawns after DEATH_RESPAWN_DELAY +## +## Platforming improvements over original GML port: +## - floor_snap_length fixes collision stepping on small ledges +## - Coyote time: brief window after leaving ground where jump still works +## - Jump buffering: pre-pressed jump fires on the next landing +## - Wall-jump lock prevents re-sticking to the wall immediately +## - Air acceleration raised to 40 % of ground (was 25 %) ## ## Block placement: ## Z (create_solid) – places a solid StaticBody2D platform ## X (create_bouncy) – places a bouncy Area2D that launches the player up # ── Physics constants (all velocities in px/s, accelerations in px/s²) ──── -# Original GML values were in px/frame at 60 fps. Multiply by 60 for px/s. -const GRAVITY: float = 2880.0 # 0.8 px/frame² × 60 × 60 +const GRAVITY: float = 2880.0 # 0.8 px/frame² × 60² const MAX_FALL_SPEED: float = 1200.0 # 20 px/frame × 60 const MAX_SPEED: float = 600.0 # 10 px/frame × 60 -const ACCELERATION: float = 1800.0 # 0.5 px/frame² × 60 × 60 (used with delta) +const ACCELERATION: float = 1800.0 # 0.5 px/frame² × 60² (used with delta) const FRICTION_COEFF: float = 0.2 # proportional, applied per physics frame const WALL_FRICTION_COEFF: float = 0.08 # proportional, applied per physics frame const JUMP_SPEED: float = -1200.0 # -20 px/frame × 60 @@ -24,11 +31,26 @@ const MIN_JUMP_SPEED: float = -480.0 # -8 px/frame × 60 const WALL_JUMP_H: float = 480.0 # 8 px/frame × 60 const WALL_JUMP_V: float = -960.0 # -16 px/frame × 60 +# ── Platforming-feel constants ───────────────────────────────────────────── +const COYOTE_TIME: float = 0.12 # s after leaving ground to still jump +const JUMP_BUFFER_TIME: float = 0.12 # s before landing where jump is queued +const WALL_JUMP_LOCK_TIME: float = 0.25 # s after wall-jump before wall-slide re-engages + +# ── Combat constants ─────────────────────────────────────────────────────── +const KNOCKBACK_H: float = 360.0 # px/s horizontal knockback on damage +const KNOCKBACK_V: float = -360.0 # px/s upward knockback on damage +const IFRAMES_DURATION: float = 1.2 # s of invincibility after being hit +## Minimum downward speed (px/s) the player must have to count as a stomp. +const STOMP_MIN_VY: float = 50.0 +const STOMP_BOUNCE: float = -600.0 # upward velocity given after stomping + +const DEATH_RESPAWN_DELAY: float = 2.0 # s before respawn after death + ## Horizontal offset from player centre when placing a block. const BLOCK_PLACE_OFFSET: float = 32.0 # ── State machine ────────────────────────────────────────────────────────── -enum State { GROUND, AIR, WALL } +enum State { GROUND, AIR, WALL, DEAD } var _state: State = State.AIR ## Direction of the wall the player is currently touching (-1 = left, +1 = right). @@ -37,8 +59,18 @@ var _wall_dir: int = 0 ## Last horizontal facing direction (+1 = right, -1 = left). var _facing: int = 1 +# ── Platforming timers ───────────────────────────────────────────────────── +var _coyote_timer: float = 0.0 +var _jump_buffer_timer: float = 0.0 +var _wall_jump_lock: float = 0.0 + +# ── Health / combat ──────────────────────────────────────────────────────── +var _health: int = 0 +var _iframes_timer: float = 0.0 +var _respawn_timer: float = 0.0 +var _spawn_position: Vector2 + # ── Scene references ─────────────────────────────────────────────────────── -## Packed scenes assigned from the editor (or via Room scene). @export var solid_creation_scene: PackedScene @export var bouncy_creation_scene: PackedScene @@ -47,17 +79,60 @@ var _facing: int = 1 func _ready() -> void: add_to_group("player") + _spawn_position = position + # Preserve current health across room warps; use full HP on a fresh game start. + _health = GameState.player_stamina if GameState.player_stamina > 0 else GameState.max_player_stamina + GameState.player_stamina = _health + # Step-up collision: snap to floor over small ledges + floor_snap_length = 6.0 if _sprite: _sprite.play("idle") func _physics_process(delta: float) -> void: + if _state == State.DEAD: + _tick_respawn(delta) + return + + _tick_timers(delta) _handle_meta_input() _apply_gravity(delta) _handle_movement_input(delta) _execute_movement() _update_state() _update_animation() + _tick_iframes(delta) + + +# ── Per-frame ticks ──────────────────────────────────────────────────────── + +func _tick_timers(delta: float) -> void: + if _coyote_timer > 0.0: + _coyote_timer -= delta + if _jump_buffer_timer > 0.0: + _jump_buffer_timer -= delta + if _wall_jump_lock > 0.0: + _wall_jump_lock -= delta + + +func _tick_iframes(delta: float) -> void: + if _iframes_timer <= 0.0: + if _sprite: + _sprite.modulate.a = 1.0 + return + _iframes_timer -= delta + # Blink effect: alternate opacity every ~75 ms + if _sprite: + _sprite.modulate.a = 0.3 if fmod(_iframes_timer, 0.15) < 0.075 else 1.0 + + +func _tick_respawn(delta: float) -> void: + _respawn_timer -= delta + # Pulse red while dead + if _sprite: + _sprite.modulate = Color(1.0, 0.2, 0.2, 0.5 + 0.5 * absf(sin(_respawn_timer * 6.0))) + if _respawn_timer <= 0.0: + _respawn() # ── Input ────────────────────────────────────────────────────────────────── @@ -73,25 +148,35 @@ func _handle_meta_input() -> void: func _handle_movement_input(delta: float) -> void: var left := Input.is_action_pressed("move_left") var right := Input.is_action_pressed("move_right") - var jump := Input.is_action_just_pressed("jump") + + # Buffer the jump press so it can fire on the next landing. + if Input.is_action_just_pressed("jump"): + _jump_buffer_timer = JUMP_BUFFER_TIME match _state: State.GROUND: _ground_move(left, right, delta) - if jump: + if _jump_buffer_timer > 0.0: _start_jump() + _jump_buffer_timer = 0.0 State.AIR: _air_move(left, right, delta) - # Variable jump height: cut velocity if the jump button is released early. + # Coyote-time jump: recently left the ground and jump was buffered. + if _jump_buffer_timer > 0.0 and _coyote_timer > 0.0: + _start_jump() + _coyote_timer = 0.0 + _jump_buffer_timer = 0.0 + # Variable jump height: cut velocity if jump is released early. if not Input.is_action_pressed("jump") and velocity.y < MIN_JUMP_SPEED: velocity.y = MIN_JUMP_SPEED _handle_creation_input() State.WALL: _apply_wall_friction() - if jump: + if _jump_buffer_timer > 0.0: _wall_jump() + _jump_buffer_timer = 0.0 _handle_creation_input() if _state == State.GROUND: @@ -122,8 +207,8 @@ func _ground_move(left: bool, right: bool, delta: float) -> void: func _air_move(left: bool, right: bool, delta: float) -> void: - # Air control is 1/4 of ground acceleration (matches original GML). - var air_accel := ACCELERATION * 0.25 * delta + # Air control is 40 % of ground acceleration (improved from original 25 %). + var air_accel := ACCELERATION * 0.4 * delta if right and not left: velocity.x = _accelerate_toward(velocity.x, MAX_SPEED, air_accel) _facing = 1 @@ -141,14 +226,20 @@ func _apply_wall_friction() -> void: func _start_jump() -> void: velocity.y = JUMP_SPEED _state = State.AIR + _coyote_timer = 0.0 func _wall_jump() -> void: velocity.x = -_wall_dir * WALL_JUMP_H velocity.y = WALL_JUMP_V _facing = -_wall_dir - _state = State.AIR + # Lock out wall-slide re-entry so the player can move away cleanly. + _wall_jump_lock = WALL_JUMP_LOCK_TIME + _state = State.AIR _update_facing_visual() + # Play jump animation immediately rather than waiting for _update_animation. + if _sprite: + _sprite.play("jump") # ── Physics ──────────────────────────────────────────────────────────────── @@ -165,12 +256,23 @@ func _execute_movement() -> void: # ── State machine ────────────────────────────────────────────────────────── func _update_state() -> void: + var was_grounded := (_state == State.GROUND) + if is_on_floor(): - _state = State.GROUND - elif is_on_wall() and velocity.y >= 0.0: + _coyote_timer = 0.0 + # Consume a buffered jump on the frame we land. + if not was_grounded and _jump_buffer_timer > 0.0: + _jump_buffer_timer = 0.0 + _start_jump() + else: + _state = State.GROUND + elif _wall_jump_lock <= 0.0 and is_on_wall() and velocity.y >= 0.0: _state = State.WALL _detect_wall_direction() else: + if was_grounded: + # Just walked off an edge – open coyote window. + _coyote_timer = COYOTE_TIME _state = State.AIR @@ -181,6 +283,58 @@ func _detect_wall_direction() -> void: break +# ── Health / combat ──────────────────────────────────────────────────────── + +func take_damage(amount: int, knockback_dir: int) -> void: + """Apply damage and knockback; ignored while invincible or dead.""" + if _iframes_timer > 0.0 or _state == State.DEAD: + return + _health -= amount + GameState.player_stamina = _health + if _health <= 0: + _die() + return + velocity.x = knockback_dir * KNOCKBACK_H + velocity.y = KNOCKBACK_V + _state = State.AIR + _iframes_timer = IFRAMES_DURATION + + +func instant_kill() -> void: + """Kill the player immediately, bypassing invincibility frames.""" + if _state == State.DEAD: + return + _health = 0 + GameState.player_stamina = 0 + _die() + + +func stomp_bounce() -> void: + """Called when the player successfully stomps an enemy – gives an upward bounce.""" + velocity.y = STOMP_BOUNCE + _state = State.AIR + + +func _die() -> void: + _state = State.DEAD + velocity = Vector2.ZERO + _respawn_timer = DEATH_RESPAWN_DELAY + GameState.player_died.emit() + + +func _respawn() -> void: + _health = GameState.max_player_stamina + GameState.player_stamina = _health + position = _spawn_position + velocity = Vector2.ZERO + _state = State.AIR + _iframes_timer = IFRAMES_DURATION + if _sprite: + _sprite.modulate = Color.WHITE + _sprite.play("idle") + GameState.player_respawned.emit() + + # ── Block placement ──────────────────────────────────────────────────────── func _place_block(scene: PackedScene) -> void: diff --git a/godot/scenes/rooms/Room1.tscn b/godot/scenes/rooms/Room1.tscn index 71edea8..26fa136 100644 --- a/godot/scenes/rooms/Room1.tscn +++ b/godot/scenes/rooms/Room1.tscn @@ -251,6 +251,11 @@ x_max = 740.0 position = Vector2(20, 192) target_scene = "res://scenes/rooms/Room0.tscn" +; ── Room warp forward to Room 2 ──────────────────────────────────────────── +[node name="RoomWarpForward" parent="." instance=ExtResource("5_warp")] +position = Vector2(752, 192) +target_scene = "res://scenes/rooms/Room2.tscn" + ; ── Player ───────────────────────────────────────────────────────────────── [node name="Player" parent="." instance=ExtResource("1_player")] position = Vector2(48, 192) diff --git a/godot/scenes/rooms/Room2.tscn b/godot/scenes/rooms/Room2.tscn new file mode 100644 index 0000000..fe8aa64 --- /dev/null +++ b/godot/scenes/rooms/Room2.tscn @@ -0,0 +1,276 @@ +[gd_scene load_steps=27 format=3 uid="uid://broom2001"] + +[ext_resource type="PackedScene" path="res://scenes/player/Player.tscn" id="1_player"] +[ext_resource type="PackedScene" path="res://scenes/objects/Diamond.tscn" id="2_diamond"] +[ext_resource type="PackedScene" path="res://scenes/objects/MovingBlock.tscn" id="3_movingb"] +[ext_resource type="PackedScene" path="res://scenes/objects/DeathArea.tscn" id="4_deatha"] +[ext_resource type="PackedScene" path="res://scenes/objects/RoomWarp.tscn" id="5_warp"] +[ext_resource type="PackedScene" path="res://scenes/objects/Enemy.tscn" id="6_enemy"] +[ext_resource type="PackedScene" path="res://scenes/ui/HUD.tscn" id="7_hud"] +[ext_resource type="PackedScene" path="res://scenes/ui/PauseMenu.tscn" id="8_pausemenu"] +[ext_resource type="PackedScene" path="res://scenes/camera/GameCamera.tscn" id="9_camera"] +[ext_resource type="PackedScene" path="res://scenes/objects/SolidCreation.tscn" id="10_solidcr"] +[ext_resource type="PackedScene" path="res://scenes/objects/BouncyCreation.tscn" id="11_bouncycr"] +[ext_resource type="Texture2D" path="res://assets/sprites/s_bg_sky.png" id="12_bgsky"] +[ext_resource type="Texture2D" path="res://assets/sprites/s_mg_city.png" id="13_mgcity"] +[ext_resource type="Texture2D" path="res://assets/sprites/s_mg_silhoette.png" id="14_mgsilhouette"] + +[sub_resource type="RectangleShape2D" id="RectangleShape2D_floor"] +size = Vector2(768, 16) + +[sub_resource type="RectangleShape2D" id="RectangleShape2D_wallL"] +size = Vector2(16, 216) + +[sub_resource type="RectangleShape2D" id="RectangleShape2D_wallR"] +size = Vector2(16, 216) + +[sub_resource type="RectangleShape2D" id="RectangleShape2D_plat1"] +size = Vector2(64, 16) + +[sub_resource type="RectangleShape2D" id="RectangleShape2D_plat2"] +size = Vector2(48, 16) + +[sub_resource type="RectangleShape2D" id="RectangleShape2D_plat3"] +size = Vector2(80, 16) + +[sub_resource type="RectangleShape2D" id="RectangleShape2D_plat4"] +size = Vector2(48, 16) + +[sub_resource type="RectangleShape2D" id="RectangleShape2D_plat5"] +size = Vector2(64, 16) + +[sub_resource type="RectangleShape2D" id="RectangleShape2D_plat6"] +size = Vector2(80, 16) + +; ── Root ────────────────────────────────────────────────────────────────── +[node name="Room2" type="Node2D"] + +; ── Background ───────────────────────────────────────────────────────────── +[node name="Background" type="Sprite2D" parent="."] +position = Vector2(384, 108) +texture = ExtResource("12_bgsky") +region_enabled = true +region_rect = Rect2(0, 0, 768, 216) +z_index = -10 + +[node name="MidgroundCity" type="Sprite2D" parent="."] +position = Vector2(384, 108) +texture = ExtResource("13_mgcity") +region_enabled = true +region_rect = Rect2(0, 0, 768, 216) +z_index = -9 + +[node name="MidgroundSilhouette" type="Sprite2D" parent="."] +position = Vector2(384, 108) +texture = ExtResource("14_mgsilhouette") +region_enabled = true +region_rect = Rect2(0, 0, 768, 216) +z_index = -8 + +; ── Terrain ──────────────────────────────────────────────────────────────── +[node name="Terrain" type="Node2D" parent="."] + +[node name="Floor" type="StaticBody2D" parent="Terrain"] +position = Vector2(384, 208) + +[node name="CollisionShape2D" type="CollisionShape2D" parent="Terrain/Floor"] +shape = SubResource("RectangleShape2D_floor") + +[node name="Visual" type="ColorRect" parent="Terrain/Floor"] +offset_left = -384.0 +offset_top = -8.0 +offset_right = 384.0 +offset_bottom = 8.0 +color = Color(0.25, 0.15, 0.05, 1) + +[node name="WallLeft" type="StaticBody2D" parent="Terrain"] +position = Vector2(8, 108) + +[node name="CollisionShape2D" type="CollisionShape2D" parent="Terrain/WallLeft"] +shape = SubResource("RectangleShape2D_wallL") + +[node name="Visual" type="ColorRect" parent="Terrain/WallLeft"] +offset_left = -8.0 +offset_top = -108.0 +offset_right = 8.0 +offset_bottom = 108.0 +color = Color(0.25, 0.15, 0.05, 1) + +[node name="WallRight" type="StaticBody2D" parent="Terrain"] +position = Vector2(760, 108) + +[node name="CollisionShape2D" type="CollisionShape2D" parent="Terrain/WallRight"] +shape = SubResource("RectangleShape2D_wallR") + +[node name="Visual" type="ColorRect" parent="Terrain/WallRight"] +offset_left = -8.0 +offset_top = -108.0 +offset_right = 8.0 +offset_bottom = 108.0 +color = Color(0.25, 0.15, 0.05, 1) + +; Staggered high-low layout – harder than Room 1 +[node name="Platform1" type="StaticBody2D" parent="Terrain"] +position = Vector2(96, 172) + +[node name="CollisionShape2D" type="CollisionShape2D" parent="Terrain/Platform1"] +shape = SubResource("RectangleShape2D_plat1") + +[node name="Visual" type="ColorRect" parent="Terrain/Platform1"] +offset_left = -32.0 +offset_top = -8.0 +offset_right = 32.0 +offset_bottom = 8.0 +color = Color(0.25, 0.15, 0.05, 1) + +[node name="Platform2" type="StaticBody2D" parent="Terrain"] +position = Vector2(208, 136) + +[node name="CollisionShape2D" type="CollisionShape2D" parent="Terrain/Platform2"] +shape = SubResource("RectangleShape2D_plat2") + +[node name="Visual" type="ColorRect" parent="Terrain/Platform2"] +offset_left = -24.0 +offset_top = -8.0 +offset_right = 24.0 +offset_bottom = 8.0 +color = Color(0.25, 0.15, 0.05, 1) + +[node name="Platform3" type="StaticBody2D" parent="Terrain"] +position = Vector2(336, 100) + +[node name="CollisionShape2D" type="CollisionShape2D" parent="Terrain/Platform3"] +shape = SubResource("RectangleShape2D_plat3") + +[node name="Visual" type="ColorRect" parent="Terrain/Platform3"] +offset_left = -40.0 +offset_top = -8.0 +offset_right = 40.0 +offset_bottom = 8.0 +color = Color(0.25, 0.15, 0.05, 1) + +[node name="Platform4" type="StaticBody2D" parent="Terrain"] +position = Vector2(432, 152) + +[node name="CollisionShape2D" type="CollisionShape2D" parent="Terrain/Platform4"] +shape = SubResource("RectangleShape2D_plat4") + +[node name="Visual" type="ColorRect" parent="Terrain/Platform4"] +offset_left = -24.0 +offset_top = -8.0 +offset_right = 24.0 +offset_bottom = 8.0 +color = Color(0.25, 0.15, 0.05, 1) + +[node name="Platform5" type="StaticBody2D" parent="Terrain"] +position = Vector2(560, 116) + +[node name="CollisionShape2D" type="CollisionShape2D" parent="Terrain/Platform5"] +shape = SubResource("RectangleShape2D_plat5") + +[node name="Visual" type="ColorRect" parent="Terrain/Platform5"] +offset_left = -32.0 +offset_top = -8.0 +offset_right = 32.0 +offset_bottom = 8.0 +color = Color(0.25, 0.15, 0.05, 1) + +[node name="Platform6" type="StaticBody2D" parent="Terrain"] +position = Vector2(672, 76) + +[node name="CollisionShape2D" type="CollisionShape2D" parent="Terrain/Platform6"] +shape = SubResource("RectangleShape2D_plat6") + +[node name="Visual" type="ColorRect" parent="Terrain/Platform6"] +offset_left = -40.0 +offset_top = -8.0 +offset_right = 40.0 +offset_bottom = 8.0 +color = Color(0.25, 0.15, 0.05, 1) + +; ── Death pit ────────────────────────────────────────────────────────────── +[node name="DeathPit" parent="." instance=ExtResource("4_deatha")] +position = Vector2(384, 224) + +; ── Collectibles ─────────────────────────────────────────────────────────── +[node name="Diamond1" parent="." instance=ExtResource("2_diamond")] +position = Vector2(96, 156) + +[node name="Diamond2" parent="." instance=ExtResource("2_diamond")] +position = Vector2(208, 120) + +[node name="Diamond3" parent="." instance=ExtResource("2_diamond")] +position = Vector2(336, 84) + +[node name="Diamond4" parent="." instance=ExtResource("2_diamond")] +position = Vector2(432, 136) + +[node name="Diamond5" parent="." instance=ExtResource("2_diamond")] +position = Vector2(560, 100) + +[node name="Diamond6" parent="." instance=ExtResource("2_diamond")] +position = Vector2(672, 60) + +; ── Moving platforms ─────────────────────────────────────────────────────── +[node name="MovingBlock1" parent="." instance=ExtResource("3_movingb")] +position = Vector2(160, 136) +x_min = 100.0 +x_max = 220.0 +speed = 220.0 + +[node name="MovingBlock2" parent="." instance=ExtResource("3_movingb")] +position = Vector2(500, 116) +x_min = 440.0 +x_max = 580.0 +speed = 240.0 + +; ── Enemies ──────────────────────────────────────────────────────────────── +[node name="Enemy1" parent="." instance=ExtResource("6_enemy")] +position = Vector2(200, 200) +x_min = 120.0 +x_max = 280.0 +chase_speed = 130.0 +detect_range = 180.0 + +[node name="Enemy2" parent="." instance=ExtResource("6_enemy")] +position = Vector2(400, 200) +x_min = 300.0 +x_max = 500.0 +chase_speed = 130.0 +detect_range = 180.0 + +[node name="Enemy3" parent="." instance=ExtResource("6_enemy")] +position = Vector2(600, 200) +x_min = 500.0 +x_max = 700.0 +chase_speed = 140.0 +detect_range = 160.0 +max_health = 2 + +; ── Warps ────────────────────────────────────────────────────────────────── +[node name="RoomWarpBack" parent="." instance=ExtResource("5_warp")] +position = Vector2(20, 192) +target_scene = "res://scenes/rooms/Room1.tscn" + +[node name="RoomWarpForward" parent="." instance=ExtResource("5_warp")] +position = Vector2(752, 192) +target_scene = "res://scenes/rooms/Room3.tscn" + +; ── Player ───────────────────────────────────────────────────────────────── +[node name="Player" parent="." instance=ExtResource("1_player")] +position = Vector2(48, 192) +solid_creation_scene = ExtResource("10_solidcr") +bouncy_creation_scene = ExtResource("11_bouncycr") + +; ── Camera ───────────────────────────────────────────────────────────────── +[node name="GameCamera" parent="." instance=ExtResource("9_camera")] +target_path = NodePath("../Player") +limit_left = 0 +limit_right = 768 +limit_top = 0 +limit_bottom = 216 + +; ── UI ───────────────────────────────────────────────────────────────────── +[node name="HUD" parent="." instance=ExtResource("7_hud")] + +[node name="PauseMenu" parent="." instance=ExtResource("8_pausemenu")] diff --git a/godot/scenes/rooms/Room3.tscn b/godot/scenes/rooms/Room3.tscn new file mode 100644 index 0000000..4d6d97b --- /dev/null +++ b/godot/scenes/rooms/Room3.tscn @@ -0,0 +1,284 @@ +[gd_scene load_steps=27 format=3 uid="uid://broom3001"] + +[ext_resource type="PackedScene" path="res://scenes/player/Player.tscn" id="1_player"] +[ext_resource type="PackedScene" path="res://scenes/objects/Diamond.tscn" id="2_diamond"] +[ext_resource type="PackedScene" path="res://scenes/objects/MovingBlock.tscn" id="3_movingb"] +[ext_resource type="PackedScene" path="res://scenes/objects/DeathArea.tscn" id="4_deatha"] +[ext_resource type="PackedScene" path="res://scenes/objects/RoomWarp.tscn" id="5_warp"] +[ext_resource type="PackedScene" path="res://scenes/objects/Enemy.tscn" id="6_enemy"] +[ext_resource type="PackedScene" path="res://scenes/ui/HUD.tscn" id="7_hud"] +[ext_resource type="PackedScene" path="res://scenes/ui/PauseMenu.tscn" id="8_pausemenu"] +[ext_resource type="PackedScene" path="res://scenes/camera/GameCamera.tscn" id="9_camera"] +[ext_resource type="PackedScene" path="res://scenes/objects/SolidCreation.tscn" id="10_solidcr"] +[ext_resource type="PackedScene" path="res://scenes/objects/BouncyCreation.tscn" id="11_bouncycr"] +[ext_resource type="Texture2D" path="res://assets/sprites/s_bg_sky.png" id="12_bgsky"] +[ext_resource type="Texture2D" path="res://assets/sprites/s_bg_clouds_behind.png" id="13_clouds_behind"] +[ext_resource type="Texture2D" path="res://assets/sprites/s_bg_clouds_front.png" id="14_clouds_front"] + +[sub_resource type="RectangleShape2D" id="RectangleShape2D_floor"] +size = Vector2(768, 16) + +[sub_resource type="RectangleShape2D" id="RectangleShape2D_wallL"] +size = Vector2(16, 216) + +[sub_resource type="RectangleShape2D" id="RectangleShape2D_wallR"] +size = Vector2(16, 216) + +[sub_resource type="RectangleShape2D" id="RectangleShape2D_plat1"] +size = Vector2(48, 16) + +[sub_resource type="RectangleShape2D" id="RectangleShape2D_plat2"] +size = Vector2(48, 16) + +[sub_resource type="RectangleShape2D" id="RectangleShape2D_plat3"] +size = Vector2(48, 16) + +[sub_resource type="RectangleShape2D" id="RectangleShape2D_plat4"] +size = Vector2(48, 16) + +[sub_resource type="RectangleShape2D" id="RectangleShape2D_plat5"] +size = Vector2(48, 16) + +[sub_resource type="RectangleShape2D" id="RectangleShape2D_plat6"] +size = Vector2(48, 16) + +; ── Root ────────────────────────────────────────────────────────────────── +[node name="Room3" type="Node2D"] + +; ── Background ───────────────────────────────────────────────────────────── +[node name="Background" type="Sprite2D" parent="."] +position = Vector2(384, 108) +texture = ExtResource("12_bgsky") +region_enabled = true +region_rect = Rect2(0, 0, 768, 216) +z_index = -10 + +[node name="CloudsBehind" type="Sprite2D" parent="."] +position = Vector2(384, 108) +texture = ExtResource("13_clouds_behind") +region_enabled = true +region_rect = Rect2(0, 0, 768, 216) +z_index = -9 + +[node name="CloudsFront" type="Sprite2D" parent="."] +position = Vector2(384, 108) +texture = ExtResource("14_clouds_front") +region_enabled = true +region_rect = Rect2(0, 0, 768, 216) +z_index = -8 + +; ── Terrain ──────────────────────────────────────────────────────────────── +[node name="Terrain" type="Node2D" parent="."] + +[node name="Floor" type="StaticBody2D" parent="Terrain"] +position = Vector2(384, 208) + +[node name="CollisionShape2D" type="CollisionShape2D" parent="Terrain/Floor"] +shape = SubResource("RectangleShape2D_floor") + +[node name="Visual" type="ColorRect" parent="Terrain/Floor"] +offset_left = -384.0 +offset_top = -8.0 +offset_right = 384.0 +offset_bottom = 8.0 +color = Color(0.2, 0.3, 0.2, 1) + +[node name="WallLeft" type="StaticBody2D" parent="Terrain"] +position = Vector2(8, 108) + +[node name="CollisionShape2D" type="CollisionShape2D" parent="Terrain/WallLeft"] +shape = SubResource("RectangleShape2D_wallL") + +[node name="Visual" type="ColorRect" parent="Terrain/WallLeft"] +offset_left = -8.0 +offset_top = -108.0 +offset_right = 8.0 +offset_bottom = 108.0 +color = Color(0.2, 0.3, 0.2, 1) + +[node name="WallRight" type="StaticBody2D" parent="Terrain"] +position = Vector2(760, 108) + +[node name="CollisionShape2D" type="CollisionShape2D" parent="Terrain/WallRight"] +shape = SubResource("RectangleShape2D_wallR") + +[node name="Visual" type="ColorRect" parent="Terrain/WallRight"] +offset_left = -8.0 +offset_top = -108.0 +offset_right = 8.0 +offset_bottom = 108.0 +color = Color(0.2, 0.3, 0.2, 1) + +; Alternating-height layout designed to require wall jumps +[node name="Platform1" type="StaticBody2D" parent="Terrain"] +position = Vector2(80, 168) + +[node name="CollisionShape2D" type="CollisionShape2D" parent="Terrain/Platform1"] +shape = SubResource("RectangleShape2D_plat1") + +[node name="Visual" type="ColorRect" parent="Terrain/Platform1"] +offset_left = -24.0 +offset_top = -8.0 +offset_right = 24.0 +offset_bottom = 8.0 +color = Color(0.2, 0.3, 0.2, 1) + +[node name="Platform2" type="StaticBody2D" parent="Terrain"] +position = Vector2(192, 128) + +[node name="CollisionShape2D" type="CollisionShape2D" parent="Terrain/Platform2"] +shape = SubResource("RectangleShape2D_plat2") + +[node name="Visual" type="ColorRect" parent="Terrain/Platform2"] +offset_left = -24.0 +offset_top = -8.0 +offset_right = 24.0 +offset_bottom = 8.0 +color = Color(0.2, 0.3, 0.2, 1) + +[node name="Platform3" type="StaticBody2D" parent="Terrain"] +position = Vector2(304, 88) + +[node name="CollisionShape2D" type="CollisionShape2D" parent="Terrain/Platform3"] +shape = SubResource("RectangleShape2D_plat3") + +[node name="Visual" type="ColorRect" parent="Terrain/Platform3"] +offset_left = -24.0 +offset_top = -8.0 +offset_right = 24.0 +offset_bottom = 8.0 +color = Color(0.2, 0.3, 0.2, 1) + +[node name="Platform4" type="StaticBody2D" parent="Terrain"] +position = Vector2(464, 128) + +[node name="CollisionShape2D" type="CollisionShape2D" parent="Terrain/Platform4"] +shape = SubResource("RectangleShape2D_plat4") + +[node name="Visual" type="ColorRect" parent="Terrain/Platform4"] +offset_left = -24.0 +offset_top = -8.0 +offset_right = 24.0 +offset_bottom = 8.0 +color = Color(0.2, 0.3, 0.2, 1) + +[node name="Platform5" type="StaticBody2D" parent="Terrain"] +position = Vector2(576, 88) + +[node name="CollisionShape2D" type="CollisionShape2D" parent="Terrain/Platform5"] +shape = SubResource("RectangleShape2D_plat5") + +[node name="Visual" type="ColorRect" parent="Terrain/Platform5"] +offset_left = -24.0 +offset_top = -8.0 +offset_right = 24.0 +offset_bottom = 8.0 +color = Color(0.2, 0.3, 0.2, 1) + +[node name="Platform6" type="StaticBody2D" parent="Terrain"] +position = Vector2(688, 52) + +[node name="CollisionShape2D" type="CollisionShape2D" parent="Terrain/Platform6"] +shape = SubResource("RectangleShape2D_plat6") + +[node name="Visual" type="ColorRect" parent="Terrain/Platform6"] +offset_left = -24.0 +offset_top = -8.0 +offset_right = 24.0 +offset_bottom = 8.0 +color = Color(0.2, 0.3, 0.2, 1) + +; ── Death pit ────────────────────────────────────────────────────────────── +[node name="DeathPit" parent="." instance=ExtResource("4_deatha")] +position = Vector2(384, 224) + +; ── Collectibles ─────────────────────────────────────────────────────────── +[node name="Diamond1" parent="." instance=ExtResource("2_diamond")] +position = Vector2(80, 152) + +[node name="Diamond2" parent="." instance=ExtResource("2_diamond")] +position = Vector2(192, 112) + +[node name="Diamond3" parent="." instance=ExtResource("2_diamond")] +position = Vector2(304, 72) + +[node name="Diamond4" parent="." instance=ExtResource("2_diamond")] +position = Vector2(464, 112) + +[node name="Diamond5" parent="." instance=ExtResource("2_diamond")] +position = Vector2(576, 72) + +[node name="Diamond6" parent="." instance=ExtResource("2_diamond")] +position = Vector2(688, 36) + +; ── Moving platforms ─────────────────────────────────────────────────────── +[node name="MovingBlock1" parent="." instance=ExtResource("3_movingb")] +position = Vector2(128, 128) +x_min = 60.0 +x_max = 200.0 +speed = 250.0 + +[node name="MovingBlock2" parent="." instance=ExtResource("3_movingb")] +position = Vector2(384, 88) +x_min = 280.0 +x_max = 460.0 +speed = 260.0 + +; ── Enemies ──────────────────────────────────────────────────────────────── +[node name="Enemy1" parent="." instance=ExtResource("6_enemy")] +position = Vector2(240, 200) +x_min = 160.0 +x_max = 340.0 +chase_speed = 140.0 +detect_range = 200.0 + +[node name="Enemy2" parent="." instance=ExtResource("6_enemy")] +position = Vector2(480, 200) +x_min = 380.0 +x_max = 580.0 +chase_speed = 140.0 +detect_range = 200.0 +max_health = 2 + +[node name="Enemy3" parent="." instance=ExtResource("6_enemy")] +position = Vector2(680, 200) +x_min = 600.0 +x_max = 740.0 +chase_speed = 150.0 +detect_range = 180.0 +max_health = 2 + +[node name="Enemy4" parent="." instance=ExtResource("6_enemy")] +position = Vector2(120, 200) +x_min = 40.0 +x_max = 200.0 +chase_speed = 130.0 +detect_range = 160.0 + +; ── Warps ────────────────────────────────────────────────────────────────── +[node name="RoomWarpBack" parent="." instance=ExtResource("5_warp")] +position = Vector2(20, 192) +target_scene = "res://scenes/rooms/Room2.tscn" + +[node name="RoomWarpForward" parent="." instance=ExtResource("5_warp")] +position = Vector2(752, 192) +target_scene = "res://scenes/rooms/Room4.tscn" + +; ── Player ───────────────────────────────────────────────────────────────── +[node name="Player" parent="." instance=ExtResource("1_player")] +position = Vector2(48, 192) +solid_creation_scene = ExtResource("10_solidcr") +bouncy_creation_scene = ExtResource("11_bouncycr") + +; ── Camera ───────────────────────────────────────────────────────────────── +[node name="GameCamera" parent="." instance=ExtResource("9_camera")] +target_path = NodePath("../Player") +limit_left = 0 +limit_right = 768 +limit_top = 0 +limit_bottom = 216 + +; ── UI ───────────────────────────────────────────────────────────────────── +[node name="HUD" parent="." instance=ExtResource("7_hud")] + +[node name="PauseMenu" parent="." instance=ExtResource("8_pausemenu")] diff --git a/godot/scenes/rooms/Room4.tscn b/godot/scenes/rooms/Room4.tscn new file mode 100644 index 0000000..5a5b7c9 --- /dev/null +++ b/godot/scenes/rooms/Room4.tscn @@ -0,0 +1,278 @@ +[gd_scene load_steps=26 format=3 uid="uid://broom4001"] + +[ext_resource type="PackedScene" path="res://scenes/player/Player.tscn" id="1_player"] +[ext_resource type="PackedScene" path="res://scenes/objects/Diamond.tscn" id="2_diamond"] +[ext_resource type="PackedScene" path="res://scenes/objects/MovingBlock.tscn" id="3_movingb"] +[ext_resource type="PackedScene" path="res://scenes/objects/DeathArea.tscn" id="4_deatha"] +[ext_resource type="PackedScene" path="res://scenes/objects/RoomWarp.tscn" id="5_warp"] +[ext_resource type="PackedScene" path="res://scenes/objects/Enemy.tscn" id="6_enemy"] +[ext_resource type="PackedScene" path="res://scenes/ui/HUD.tscn" id="7_hud"] +[ext_resource type="PackedScene" path="res://scenes/ui/PauseMenu.tscn" id="8_pausemenu"] +[ext_resource type="PackedScene" path="res://scenes/camera/GameCamera.tscn" id="9_camera"] +[ext_resource type="PackedScene" path="res://scenes/objects/SolidCreation.tscn" id="10_solidcr"] +[ext_resource type="PackedScene" path="res://scenes/objects/BouncyCreation.tscn" id="11_bouncycr"] +[ext_resource type="Texture2D" path="res://assets/sprites/s_bg_sky.png" id="12_bgsky"] +[ext_resource type="Texture2D" path="res://assets/sprites/s_bg_clouds_behind.png" id="13_clouds_behind"] + +[sub_resource type="RectangleShape2D" id="RectangleShape2D_floor"] +size = Vector2(768, 16) + +[sub_resource type="RectangleShape2D" id="RectangleShape2D_wallL"] +size = Vector2(16, 216) + +[sub_resource type="RectangleShape2D" id="RectangleShape2D_wallR"] +size = Vector2(16, 216) + +[sub_resource type="RectangleShape2D" id="RectangleShape2D_plat1"] +size = Vector2(48, 16) + +[sub_resource type="RectangleShape2D" id="RectangleShape2D_plat2"] +size = Vector2(64, 16) + +[sub_resource type="RectangleShape2D" id="RectangleShape2D_plat3"] +size = Vector2(48, 16) + +[sub_resource type="RectangleShape2D" id="RectangleShape2D_plat4"] +size = Vector2(48, 16) + +[sub_resource type="RectangleShape2D" id="RectangleShape2D_plat5"] +size = Vector2(64, 16) + +[sub_resource type="RectangleShape2D" id="RectangleShape2D_plat6"] +size = Vector2(48, 16) + +; ── Root ────────────────────────────────────────────────────────────────── +[node name="Room4" type="Node2D"] + +; ── Background ───────────────────────────────────────────────────────────── +[node name="Background" type="Sprite2D" parent="."] +position = Vector2(384, 108) +texture = ExtResource("12_bgsky") +region_enabled = true +region_rect = Rect2(0, 0, 768, 216) +z_index = -10 + +[node name="CloudsBehind" type="Sprite2D" parent="."] +position = Vector2(384, 108) +texture = ExtResource("13_clouds_behind") +region_enabled = true +region_rect = Rect2(0, 0, 768, 216) +z_index = -9 + +; ── Terrain ──────────────────────────────────────────────────────────────── +[node name="Terrain" type="Node2D" parent="."] + +[node name="Floor" type="StaticBody2D" parent="Terrain"] +position = Vector2(384, 208) + +[node name="CollisionShape2D" type="CollisionShape2D" parent="Terrain/Floor"] +shape = SubResource("RectangleShape2D_floor") + +[node name="Visual" type="ColorRect" parent="Terrain/Floor"] +offset_left = -384.0 +offset_top = -8.0 +offset_right = 384.0 +offset_bottom = 8.0 +color = Color(0.15, 0.1, 0.2, 1) + +[node name="WallLeft" type="StaticBody2D" parent="Terrain"] +position = Vector2(8, 108) + +[node name="CollisionShape2D" type="CollisionShape2D" parent="Terrain/WallLeft"] +shape = SubResource("RectangleShape2D_wallL") + +[node name="Visual" type="ColorRect" parent="Terrain/WallLeft"] +offset_left = -8.0 +offset_top = -108.0 +offset_right = 8.0 +offset_bottom = 108.0 +color = Color(0.15, 0.1, 0.2, 1) + +[node name="WallRight" type="StaticBody2D" parent="Terrain"] +position = Vector2(760, 108) + +[node name="CollisionShape2D" type="CollisionShape2D" parent="Terrain/WallRight"] +shape = SubResource("RectangleShape2D_wallR") + +[node name="Visual" type="ColorRect" parent="Terrain/WallRight"] +offset_left = -8.0 +offset_top = -108.0 +offset_right = 8.0 +offset_bottom = 108.0 +color = Color(0.15, 0.1, 0.2, 1) + +; Gauntlet layout – small platforms, wide gaps, enemies everywhere +[node name="Platform1" type="StaticBody2D" parent="Terrain"] +position = Vector2(80, 164) + +[node name="CollisionShape2D" type="CollisionShape2D" parent="Terrain/Platform1"] +shape = SubResource("RectangleShape2D_plat1") + +[node name="Visual" type="ColorRect" parent="Terrain/Platform1"] +offset_left = -24.0 +offset_top = -8.0 +offset_right = 24.0 +offset_bottom = 8.0 +color = Color(0.15, 0.1, 0.2, 1) + +[node name="Platform2" type="StaticBody2D" parent="Terrain"] +position = Vector2(208, 124) + +[node name="CollisionShape2D" type="CollisionShape2D" parent="Terrain/Platform2"] +shape = SubResource("RectangleShape2D_plat2") + +[node name="Visual" type="ColorRect" parent="Terrain/Platform2"] +offset_left = -32.0 +offset_top = -8.0 +offset_right = 32.0 +offset_bottom = 8.0 +color = Color(0.15, 0.1, 0.2, 1) + +[node name="Platform3" type="StaticBody2D" parent="Terrain"] +position = Vector2(320, 80) + +[node name="CollisionShape2D" type="CollisionShape2D" parent="Terrain/Platform3"] +shape = SubResource("RectangleShape2D_plat3") + +[node name="Visual" type="ColorRect" parent="Terrain/Platform3"] +offset_left = -24.0 +offset_top = -8.0 +offset_right = 24.0 +offset_bottom = 8.0 +color = Color(0.15, 0.1, 0.2, 1) + +[node name="Platform4" type="StaticBody2D" parent="Terrain"] +position = Vector2(448, 124) + +[node name="CollisionShape2D" type="CollisionShape2D" parent="Terrain/Platform4"] +shape = SubResource("RectangleShape2D_plat4") + +[node name="Visual" type="ColorRect" parent="Terrain/Platform4"] +offset_left = -24.0 +offset_top = -8.0 +offset_right = 24.0 +offset_bottom = 8.0 +color = Color(0.15, 0.1, 0.2, 1) + +[node name="Platform5" type="StaticBody2D" parent="Terrain"] +position = Vector2(560, 80) + +[node name="CollisionShape2D" type="CollisionShape2D" parent="Terrain/Platform5"] +shape = SubResource("RectangleShape2D_plat5") + +[node name="Visual" type="ColorRect" parent="Terrain/Platform5"] +offset_left = -32.0 +offset_top = -8.0 +offset_right = 32.0 +offset_bottom = 8.0 +color = Color(0.15, 0.1, 0.2, 1) + +[node name="Platform6" type="StaticBody2D" parent="Terrain"] +position = Vector2(672, 44) + +[node name="CollisionShape2D" type="CollisionShape2D" parent="Terrain/Platform6"] +shape = SubResource("RectangleShape2D_plat6") + +[node name="Visual" type="ColorRect" parent="Terrain/Platform6"] +offset_left = -24.0 +offset_top = -8.0 +offset_right = 24.0 +offset_bottom = 8.0 +color = Color(0.15, 0.1, 0.2, 1) + +; ── Death pit ────────────────────────────────────────────────────────────── +[node name="DeathPit" parent="." instance=ExtResource("4_deatha")] +position = Vector2(384, 224) + +; ── Collectibles ─────────────────────────────────────────────────────────── +[node name="Diamond1" parent="." instance=ExtResource("2_diamond")] +position = Vector2(80, 148) + +[node name="Diamond2" parent="." instance=ExtResource("2_diamond")] +position = Vector2(208, 108) + +[node name="Diamond3" parent="." instance=ExtResource("2_diamond")] +position = Vector2(320, 64) + +[node name="Diamond4" parent="." instance=ExtResource("2_diamond")] +position = Vector2(448, 108) + +[node name="Diamond5" parent="." instance=ExtResource("2_diamond")] +position = Vector2(560, 64) + +[node name="Diamond6" parent="." instance=ExtResource("2_diamond")] +position = Vector2(672, 28) + +; ── Moving platforms ─────────────────────────────────────────────────────── +[node name="MovingBlock1" parent="." instance=ExtResource("3_movingb")] +position = Vector2(144, 124) +x_min = 60.0 +x_max = 224.0 +speed = 260.0 + +[node name="MovingBlock2" parent="." instance=ExtResource("3_movingb")] +position = Vector2(400, 80) +x_min = 296.0 +x_max = 496.0 +speed = 280.0 + +; ── Enemies ──────────────────────────────────────────────────────────────── +[node name="Enemy1" parent="." instance=ExtResource("6_enemy")] +position = Vector2(140, 200) +x_min = 60.0 +x_max = 220.0 +chase_speed = 150.0 +detect_range = 220.0 +max_health = 2 + +[node name="Enemy2" parent="." instance=ExtResource("6_enemy")] +position = Vector2(320, 200) +x_min = 220.0 +x_max = 420.0 +chase_speed = 155.0 +detect_range = 220.0 +max_health = 2 + +[node name="Enemy3" parent="." instance=ExtResource("6_enemy")] +position = Vector2(520, 200) +x_min = 400.0 +x_max = 640.0 +chase_speed = 160.0 +detect_range = 220.0 +max_health = 2 + +[node name="Enemy4" parent="." instance=ExtResource("6_enemy")] +position = Vector2(680, 200) +x_min = 600.0 +x_max = 740.0 +chase_speed = 160.0 +detect_range = 200.0 +max_health = 3 + +; ── Warps ────────────────────────────────────────────────────────────────── +[node name="RoomWarpBack" parent="." instance=ExtResource("5_warp")] +position = Vector2(20, 192) +target_scene = "res://scenes/rooms/Room3.tscn" + +[node name="RoomWarpForward" parent="." instance=ExtResource("5_warp")] +position = Vector2(752, 192) +target_scene = "res://scenes/rooms/Room0.tscn" + +; ── Player ───────────────────────────────────────────────────────────────── +[node name="Player" parent="." instance=ExtResource("1_player")] +position = Vector2(48, 192) +solid_creation_scene = ExtResource("10_solidcr") +bouncy_creation_scene = ExtResource("11_bouncycr") + +; ── Camera ───────────────────────────────────────────────────────────────── +[node name="GameCamera" parent="." instance=ExtResource("9_camera")] +target_path = NodePath("../Player") +limit_left = 0 +limit_right = 768 +limit_top = 0 +limit_bottom = 216 + +; ── UI ───────────────────────────────────────────────────────────────────── +[node name="HUD" parent="." instance=ExtResource("7_hud")] + +[node name="PauseMenu" parent="." instance=ExtResource("8_pausemenu")] diff --git a/godot/scenes/ui/HUD.gd b/godot/scenes/ui/HUD.gd index b39f67c..f8a3ed6 100644 --- a/godot/scenes/ui/HUD.gd +++ b/godot/scenes/ui/HUD.gd @@ -2,18 +2,27 @@ extends CanvasLayer ## Sluggers – HUD ## -## Displays the game timer, collected diamonds, and remaining block-creation -## budget. Connects to GameState signals so values update reactively. +## Displays the level name, player health, game timer, collected diamonds, +## and remaining block-creation budget. Also shows a full-screen respawn +## overlay when the player dies, hiding it automatically on respawn. +## Connects to GameState signals so values update reactively. -@onready var _timer_label: Label = $Panel/VBox/TimerLabel -@onready var _diamond_label: Label = $Panel/VBox/DiamondLabel -@onready var _creation_label: Label = $Panel/VBox/CreationLabel +@onready var _level_label: Label = $StatsPanel/VBox/LevelLabel +@onready var _health_label: Label = $StatsPanel/VBox/HealthLabel +@onready var _timer_label: Label = $StatsPanel/VBox/TimerLabel +@onready var _diamond_label: Label = $StatsPanel/VBox/DiamondLabel +@onready var _creation_label: Label = $StatsPanel/VBox/CreationLabel +@onready var _respawn_overlay: ColorRect = $RespawnOverlay func _ready() -> void: GameState.timer_updated.connect(_on_timer_updated) GameState.diamonds_changed.connect(_on_diamonds_changed) GameState.creations_changed.connect(_on_creations_changed) + GameState.health_changed.connect(_on_health_changed) + GameState.player_died.connect(_on_player_died) + GameState.player_respawned.connect(_on_player_respawned) + _level_label.text = get_parent().name _refresh_all() @@ -21,6 +30,7 @@ func _refresh_all() -> void: _on_timer_updated(GameState.minutes, int(GameState.seconds)) _on_diamonds_changed(GameState.diamonds) _on_creations_changed(GameState.creations_remaining) + _on_health_changed(GameState.player_stamina, GameState.max_player_stamina) func _on_timer_updated(mins: int, secs: int) -> void: @@ -33,3 +43,17 @@ func _on_diamonds_changed(count: int) -> void: func _on_creations_changed(remaining: int) -> void: _creation_label.text = "Creations: %d" % remaining + + +func _on_health_changed(current: int, maximum: int) -> void: + _health_label.text = "♥".repeat(current) + "♡".repeat(maximum - current) + if current > 0 and _respawn_overlay.visible: + _respawn_overlay.visible = false + + +func _on_player_died() -> void: + _respawn_overlay.visible = true + + +func _on_player_respawned() -> void: + _respawn_overlay.visible = false diff --git a/godot/scenes/ui/HUD.tscn b/godot/scenes/ui/HUD.tscn index 23c3473..4be15cd 100644 --- a/godot/scenes/ui/HUD.tscn +++ b/godot/scenes/ui/HUD.tscn @@ -6,24 +6,51 @@ layer = 10 script = ExtResource("1_hud") -[node name="Panel" type="PanelContainer" parent="."] -offset_right = 120.0 -offset_bottom = 56.0 +[node name="StatsPanel" type="PanelContainer" parent="."] +offset_right = 190.0 +offset_bottom = 82.0 -[node name="VBox" type="VBoxContainer" parent="Panel"] +[node name="VBox" type="VBoxContainer" parent="StatsPanel"] layout_mode = 2 -[node name="TimerLabel" type="Label" parent="Panel/VBox"] +[node name="LevelLabel" type="Label" parent="StatsPanel/VBox"] +layout_mode = 2 +text = "" +theme_override_font_sizes/font_size = 10 + +[node name="HealthLabel" type="Label" parent="StatsPanel/VBox"] +layout_mode = 2 +text = "♥♥♥♥" +theme_override_font_sizes/font_size = 10 + +[node name="TimerLabel" type="Label" parent="StatsPanel/VBox"] layout_mode = 2 text = "00:00" theme_override_font_sizes/font_size = 10 -[node name="DiamondLabel" type="Label" parent="Panel/VBox"] +[node name="DiamondLabel" type="Label" parent="StatsPanel/VBox"] layout_mode = 2 text = "Diamonds: 0" theme_override_font_sizes/font_size = 10 -[node name="CreationLabel" type="Label" parent="Panel/VBox"] +[node name="CreationLabel" type="Label" parent="StatsPanel/VBox"] layout_mode = 2 text = "Creations: 5" theme_override_font_sizes/font_size = 10 + +[node name="RespawnOverlay" type="ColorRect" parent="."] +anchor_right = 1.0 +anchor_bottom = 1.0 +color = Color(0, 0, 0, 0.7) +visible = false + +[node name="RespawnLabel" type="Label" parent="RespawnOverlay"] +anchor_left = 0.0 +anchor_top = 0.5 +anchor_right = 1.0 +anchor_bottom = 0.5 +offset_top = -10.0 +offset_bottom = 10.0 +horizontal_alignment = 1 +text = "Respawning..." +theme_override_font_sizes/font_size = 16